Initial implementation of virtual libraries

This commit is contained in:
Kovid Goyal 2013-04-10 15:30:41 +05:30
commit 48ca11a9ec
17 changed files with 513 additions and 143 deletions

View File

@ -422,6 +422,8 @@ class DB(object):
('uuid', False), ('comments', True), ('id', False), ('pubdate', False), ('uuid', False), ('comments', True), ('id', False), ('pubdate', False),
('last_modified', False), ('size', False), ('languages', False), ('last_modified', False), ('size', False), ('languages', False),
] ]
defs['virtual_libraries'] = {}
defs['virtual_lib_on_startup'] = defs['cs_virtual_lib_on_startup'] = ''
# Migrate the bool tristate tweak # Migrate the bool tristate tweak
defs['bools_are_tristate'] = \ defs['bools_are_tristate'] = \
@ -470,6 +472,24 @@ class DB(object):
except: except:
pass pass
# migrate the gui_restriction preference to a virtual library
gr_pref = self.prefs.get('gui_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['gui_restriction'] = ''
self.prefs['virtual_lib_on_startup'] = gr_pref
# migrate the cs_restriction preference to a virtual library
gr_pref = self.prefs.get('cs_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['cs_restriction'] = ''
self.prefs['cs_virtual_lib_on_startup'] = gr_pref
# Rename any user categories with names that differ only in case # Rename any user categories with names that differ only in case
user_cats = self.prefs.get('user_categories', []) user_cats = self.prefs.get('user_categories', [])
catmap = {} catmap = {}

View File

@ -49,7 +49,8 @@ class View(object):
self.cache = cache self.cache = cache
self.marked_ids = {} self.marked_ids = {}
self.search_restriction_book_count = 0 self.search_restriction_book_count = 0
self.search_restriction = '' self.search_restriction = self.base_restriction = ''
self.search_restriction_name = self.base_restriction_name = ''
self._field_getters = {} self._field_getters = {}
for col, idx in cache.backend.FIELD_MAP.iteritems(): for col, idx in cache.backend.FIELD_MAP.iteritems():
if isinstance(col, int): if isinstance(col, int):
@ -168,8 +169,19 @@ class View(object):
return ans return ans
self._map_filtered = tuple(ans) self._map_filtered = tuple(ans)
def _build_restriction_string(self, restriction):
if self.base_restriction:
if restriction:
return u'(%s) and (%s)' % (self.base_restriction, restriction)
else:
return self.base_restriction
else:
return restriction
def search_getting_ids(self, query, search_restriction, def search_getting_ids(self, query, search_restriction,
set_restriction_count=False): set_restriction_count=False, use_virtual_library=True):
if use_virtual_library:
search_restriction = self._build_restriction_string(search_restriction)
q = '' q = ''
if not query or not query.strip(): if not query or not query.strip():
q = search_restriction q = search_restriction
@ -188,11 +200,32 @@ class View(object):
self.search_restriction_book_count = len(rv) self.search_restriction_book_count = len(rv)
return rv return rv
def get_search_restriction(self):
return self.search_restriction
def set_search_restriction(self, s): def set_search_restriction(self, s):
self.search_restriction = s self.search_restriction = s
def get_base_restriction(self):
return self.base_restriction
def set_base_restriction(self, s):
self.base_restriction = s
def get_base_restriction_name(self):
return self.base_restriction_name
def set_base_restriction_name(self, s):
self.base_restriction_name = s
def get_search_restriction_name(self):
return self.search_restriction_name
def set_search_restriction_name(self, s):
self.search_restriction_name = s
def search_restriction_applied(self): def search_restriction_applied(self):
return bool(self.search_restriction) return bool(self.search_restriction) or bool(self.base_restriction)
def get_search_restriction_book_count(self): def get_search_restriction_book_count(self):
return self.search_restriction_book_count return self.search_restriction_book_count

View File

@ -16,7 +16,6 @@ from calibre.constants import __appname__
from calibre.gui2.search_box import SearchBox2, SavedSearchBox from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2.bars import BarsManager from calibre.gui2.bars import BarsManager
from calibre.gui2.widgets import ComboBoxWithHelp
from calibre.utils.config_base import tweaks from calibre.utils.config_base import tweaks
from calibre import human_readable from calibre import human_readable
@ -173,11 +172,13 @@ class SearchBar(QWidget): # {{{
self.setLayout(self._layout) self.setLayout(self._layout)
self._layout.setContentsMargins(0,5,0,0) self._layout.setContentsMargins(0,5,0,0)
x = ComboBoxWithHelp(self) x = QToolButton(self)
x.setMaximumSize(QSize(150, 16777215)) x.setText(_('Virtual Library'))
x.setObjectName("search_restriction") x.setIcon(QIcon(I('lt.png')))
x.setObjectName("virtual_library")
x.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
l.addWidget(x) l.addWidget(x)
parent.search_restriction = x parent.virtual_library = x
x = QLabel(self) x = QLabel(self)
x.setObjectName("search_count") x.setObjectName("search_count")

View File

@ -14,7 +14,6 @@ from calibre.gui2.preferences.behavior_ui import Ui_Form
from calibre.gui2 import config, info_dialog, dynamic, gprefs from calibre.gui2 import config, info_dialog, dynamic, gprefs
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.customize.ui import available_output_formats, all_input_formats from calibre.customize.ui import available_output_formats, all_input_formats
from calibre.utils.search_query_parser import saved_searches
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.oeb.iterator import is_supported from calibre.ebooks.oeb.iterator import is_supported
from calibre.constants import iswindows from calibre.constants import iswindows
@ -48,9 +47,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
choices = [(x.upper(), x) for x in output_formats] choices = [(x.upper(), x) for x in output_formats]
r('output_format', prefs, choices=choices, setting=OutputFormatSetting) r('output_format', prefs, choices=choices, setting=OutputFormatSetting)
restrictions = sorted(saved_searches().names(), key=sort_key) restrictions = sorted(db.prefs['virtual_libraries'].iterkeys(), key=sort_key)
choices = [('', '')] + [(x, x) for x in restrictions] choices = [('', '')] + [(x, x) for x in restrictions]
r('gui_restriction', db.prefs, choices=choices) # check that the virtual library still exists
vls = db.prefs['virtual_lib_on_startup']
if vls and vls not in restrictions:
db.prefs['virtual_lib_on_startup'] = ''
r('virtual_lib_on_startup', db.prefs, choices=choices)
self.reset_confirmation_button.clicked.connect(self.reset_confirmation_dialogs) self.reset_confirmation_button.clicked.connect(self.reset_confirmation_dialogs)
self.input_up_button.clicked.connect(self.up_input) self.input_up_button.clicked.connect(self.up_input)

View File

@ -147,15 +147,15 @@ If not checked, the values can be Yes or No.</string>
<item> <item>
<widget class="QLabel" name="label_170"> <widget class="QLabel" name="label_170">
<property name="text"> <property name="text">
<string>Restriction to apply when the current library is opened:</string> <string>Virtual library to apply when the current library is opened:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>opt_gui_restriction</cstring> <cstring>opt_virtual_lib_on_startup</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QComboBox" name="opt_gui_restriction"> <widget class="QComboBox" name="opt_virtual_lib_on_startup">
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>250</width> <width>250</width>
@ -163,7 +163,7 @@ If not checked, the values can be Yes or No.</string>
</size> </size>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>Apply this restriction on calibre startup if the current library is being used. Also applied when switching to this library. Note that this setting is per library. </string> <string>Use this virtual library on calibre startup if the current library is being used. Also applied when switching to this library. Note that this setting is per library. </string>
</property> </property>
<property name="sizeAdjustPolicy"> <property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum> <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>

View File

@ -12,7 +12,6 @@ from PyQt4.Qt import Qt, QUrl, QDialog, QSize, QVBoxLayout, QLabel, \
from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.server_ui import Ui_Form from calibre.gui2.preferences.server_ui import Ui_Form
from calibre.utils.search_query_parser import saved_searches
from calibre.library.server import server_config from calibre.library.server import server_config
from calibre.utils.config import ConfigProxy from calibre.utils.config import ConfigProxy
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \ from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
@ -44,13 +43,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
else self.opt_password.Password)) else self.opt_password.Password))
self.opt_password.setEchoMode(self.opt_password.Password) self.opt_password.setEchoMode(self.opt_password.Password)
restrictions = sorted(saved_searches().names(), key=sort_key) restrictions = sorted(db.prefs['virtual_libraries'].iterkeys(), key=sort_key)
# verify that the current restriction still exists. If not, clear it.
csr = db.prefs.get('cs_restriction', None)
if csr and csr not in restrictions:
db.prefs.set('cs_restriction', '')
choices = [('', '')] + [(x, x) for x in restrictions] choices = [('', '')] + [(x, x) for x in restrictions]
r('cs_restriction', db.prefs, choices=choices) # check that the virtual library still exists
vls = db.prefs['cs_virtual_lib_on_startup']
if vls and vls not in restrictions:
db.prefs['cs_virtual_lib_on_startup'] = ''
r('cs_virtual_lib_on_startup', db.prefs, choices=choices)
self.start_button.setEnabled(not getattr(self.server, 'is_running', False)) self.start_button.setEnabled(not getattr(self.server, 'is_running', False))
self.test_button.setEnabled(not self.start_button.isEnabled()) self.test_button.setEnabled(not self.start_button.isEnabled())

View File

@ -139,14 +139,14 @@
<item row="7" column="0"> <item row="7" column="0">
<widget class="QLabel" name="label_164"> <widget class="QLabel" name="label_164">
<property name="text"> <property name="text">
<string>Restriction (saved search) to apply:</string> <string>Virtual library to apply:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="1" colspan="2"> <item row="7" column="1" colspan="2">
<widget class="QComboBox" name="opt_cs_restriction"> <widget class="QComboBox" name="opt_cs_virtual_lib_on_startup">
<property name="toolTip"> <property name="toolTip">
<string>This restriction (based on a saved search) will restrict the books the content server makes available to those matching the search. This setting is per library (i.e. you can have a different restriction per library).</string> <string>Setting a virtual library will restrict the books the content server makes available to those in the library. This setting is per library (i.e. you can have a different value per library).</string>
</property> </property>
<property name="sizeAdjustPolicy"> <property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum> <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>

View File

@ -19,7 +19,6 @@ from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
from calibre.gui2.dialogs.search import SearchDialog from calibre.gui2.dialogs.search import SearchDialog
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.utils.icu import sort_key
class SearchLineEdit(QLineEdit): # {{{ class SearchLineEdit(QLineEdit): # {{{
key_pressed = pyqtSignal(object) key_pressed = pyqtSignal(object)
@ -332,6 +331,10 @@ class SavedSearchBox(QComboBox): # {{{
name = unicode(self.currentText()) name = unicode(self.currentText())
if not name.strip(): if not name.strip():
name = unicode(self.search_box.text()).replace('"', '') name = unicode(self.search_box.text()).replace('"', '')
if not (name and self.search_box.text()):
error_dialog(self, _('Create saved search'),
_('There is no search to save'), show=True)
return
saved_searches().delete(name) saved_searches().delete(name)
saved_searches().add(name, unicode(self.search_box.text())) saved_searches().add(name, unicode(self.search_box.text()))
# now go through an initialization cycle to ensure that the combobox has # now go through an initialization cycle to ensure that the combobox has
@ -362,7 +365,7 @@ class SavedSearchBox(QComboBox): # {{{
# SIGNALed from the main UI # SIGNALed from the main UI
def copy_search_button_clicked(self): def copy_search_button_clicked(self):
idx = self.currentIndex(); idx = self.currentIndex()
if idx < 0: if idx < 0:
return return
self.search_box.set_search_string(saved_searches().lookup(unicode(self.currentText()))) self.search_box.set_search_string(saved_searches().lookup(unicode(self.currentText())))
@ -452,7 +455,7 @@ class SavedSearchBoxMixin(object): # {{{
self.saved_search.save_search_button_clicked) self.saved_search.save_search_button_clicked)
self.copy_search_button.clicked.connect( self.copy_search_button.clicked.connect(
self.saved_search.copy_search_button_clicked) self.saved_search.copy_search_button_clicked)
self.saved_searches_changed() # self.saved_searches_changed()
self.saved_search.initialize(self.search, colorize=True, self.saved_search.initialize(self.search, colorize=True,
help_text=_('Saved Searches')) help_text=_('Saved Searches'))
self.saved_search.setToolTip( self.saved_search.setToolTip(
@ -479,17 +482,9 @@ class SavedSearchBoxMixin(object): # {{{
partial(self.do_saved_search_edit, None)) partial(self.do_saved_search_edit, None))
def saved_searches_changed(self, set_restriction=None, recount=True): def saved_searches_changed(self, set_restriction=None, recount=True):
p = sorted(saved_searches().names(), key=sort_key) self.build_search_restriction_list()
if set_restriction is None:
set_restriction = unicode(self.search_restriction.currentText())
# rebuild the restrictions combobox using current saved searches
self.search_restriction.clear()
self.search_restriction.addItem('')
self.search_restriction.addItem(_('*Current search'))
if recount: if recount:
self.tags_view.recount() self.tags_view.recount()
for s in p:
self.search_restriction.addItem(s)
if set_restriction: # redo the search restriction if there was one if set_restriction: # redo the search restriction if there was one
self.apply_named_search_restriction(set_restriction) self.apply_named_search_restriction(set_restriction)

View File

@ -4,23 +4,297 @@ Created on 10 Jun 2010
@author: charles @author: charles
''' '''
from PyQt4.Qt import Qt from functools import partial
from PyQt4.Qt import (Qt, QMenu, QPoint, QIcon, QDialog, QGridLayout, QLabel,
QLineEdit, QDialogButtonBox, QEvent, QToolTip)
from calibre.gui2 import error_dialog, question_dialog
from calibre.gui2.widgets import ComboBoxWithHelp
from calibre.utils.icu import sort_key
from calibre.utils.pyparsing import ParseException
from calibre.utils.search_query_parser import saved_searches
class CreateVirtualLibrary(QDialog):
def __init__(self, gui, existing_names):
QDialog.__init__(self, None, Qt.WindowSystemMenuHint | Qt.WindowTitleHint)
self.gui = gui
self.existing_names = existing_names
self.setWindowTitle(_('Create virtual library'))
gl = QGridLayout()
self.setLayout(gl)
gl.addWidget(QLabel(_('Virtual library name')), 0, 0)
self.vl_name = QLineEdit()
self.vl_name.setMinimumWidth(400)
gl.addWidget(self.vl_name, 0, 1)
gl.addWidget(QLabel(_('Search expression')), 1, 0)
self.vl_text = QLineEdit()
gl.addWidget(self.vl_text, 1, 1)
self.vl_text.setText(self.build_full_search_string())
bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
bb.accepted.connect(self.accepted)
bb.rejected.connect(self.rejected)
gl.addWidget(bb, 2, 0, 1, 0)
search_templates = [
'',
'{cl}',
'{cr}',
'(({cl}) and ({cr}))',
'{sb}',
'(({cl}) and ({sb}))',
'(({cr}) and ({sb}))',
'(({cl}) and ({cr}) and ({sb}))'
]
def build_full_search_string(self):
sb = self.gui.search.current_text
db = self.gui.library_view.model().db
cr = db.data.get_search_restriction()
cl = db.data.get_base_restriction()
dex = 0
if sb:
dex += 4
if cr:
dex += 2
if cl:
dex += 1
template = self.search_templates[dex]
return template.format(cl=cl, cr=cr, sb=sb)
def accepted(self):
n = unicode(self.vl_name.text())
if not n:
error_dialog(self.gui, _('No name'),
_('You must provide a name for the new virtual library'),
show=True)
return
if n in self.existing_names:
if question_dialog(self.gui, _('Name already in use'),
_('That name is already in use. Do you want to replace it '
'with the new search?'),
default_yes=False) == self.Rejected:
return
v = unicode(self.vl_text.text())
if not v:
error_dialog(self.gui, _('No search string'),
_('You must provide a search to define the new virtual library'),
show=True)
return
try:
db = self.gui.library_view.model().db
recs = db.data.search_getting_ids('', v, use_virtual_library=False)
except ParseException as e:
error_dialog(self.gui, _('Invalid search string'),
_('The search string is not a valid search expression'),
det_msg=e.msg, show=True)
return
if not recs:
if question_dialog(self.gui, _('Search found no books'),
_('The search found no books, so the virtual library '
'will be empty. Do you really want to use that search?'),
default_yes=False) == self.Rejected:
return
self.library_name = n
self.library_search = v
self.accept()
def rejected(self):
self.reject()
class VirtLibMenu(QMenu):
def __init__(self):
QMenu.__init__(self)
self.show_tt_for = []
def event(self, e):
QMenu.event(self, e)
if e.type() == QEvent.ToolTip:
a = self.activeAction()
if a and a in self.show_tt_for:
tt = a.toolTip()
if tt:
QToolTip.showText(e.globalPos(), tt)
return True
def clear(self):
self.show_tt_for = []
QMenu.clear(self)
def show_tooltip_for_action(self, a):
self.show_tt_for.append(a)
class SearchRestrictionMixin(object): class SearchRestrictionMixin(object):
no_restriction = _('<None>')
def __init__(self): def __init__(self):
self.search_restriction.initialize(help_text=_('Restrict to')) self.checked = QIcon(I('ok.png'))
self.search_restriction.activated[int].connect(self.apply_search_restriction) self.empty = QIcon()
self.library_view.model().count_changed_signal.connect(self.set_number_of_books_shown)
self.search_restriction.setSizeAdjustPolicy( self.virtual_library_menu = VirtLibMenu()
self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10) self.virtual_library.clicked.connect(self.virtual_library_clicked)
self.search_restriction.setStatusTip(self.search_restriction.toolTip())
self.virtual_library_tooltip = \
_('Books display will show only those books matching the search')
self.virtual_library.setToolTip(self.virtual_library_tooltip)
self.search_restriction = ComboBoxWithHelp(self)
self.search_restriction.setVisible(False)
self.search_count.setText(_("(all books)")) self.search_count.setText(_("(all books)"))
self.search_restriction_tooltip = \ self.ar_menu = QMenu(_('Additional restriction'))
_('Books display will be restricted to those matching a '
'selected saved search') def add_virtual_library(self, db, name, search):
self.search_restriction.setToolTip(self.search_restriction_tooltip) virt_libs = db.prefs.get('virtual_libraries', {})
virt_libs[name] = search
db.prefs.set('virtual_libraries', virt_libs)
def do_create(self):
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
cd = CreateVirtualLibrary(self, virt_libs.keys())
ret = cd.exec_()
if ret == cd.Accepted:
self.add_virtual_library(db, cd.library_name, cd.library_search)
self.apply_virtual_library(cd.library_name)
def do_remove(self):
db = self.library_view.model().db
db.data.set_base_restriction("")
db.data.set_base_restriction_name("")
self._apply_search_restriction(db.data.get_search_restriction(),
db.data.get_search_restriction_name())
def virtual_library_clicked(self):
m = self.virtual_library_menu
m.clear()
a = m.addAction(_('Create Virtual Library'))
a.triggered.connect(self.do_create)
a.setToolTip(_('Create a new virtual library from the results of a search'))
m.show_tooltip_for_action(a)
self.rm_menu = a = VirtLibMenu()
a.setTitle(_('Remove Virtual Library'))
a.aboutToShow.connect(self.build_virtual_library_list)
m.addMenu(a)
m.addSeparator()
db = self.library_view.model().db
a = self.ar_menu
a.clear()
a.setIcon(self.checked if db.data.get_search_restriction_name() else self.empty)
a.aboutToShow.connect(self.build_search_restriction_list)
m.addMenu(a)
m.addSeparator()
current_lib = db.data.get_base_restriction_name()
if current_lib == '':
a = m.addAction(self.checked, self.no_restriction)
else:
a = m.addAction(self.empty, self.no_restriction)
a.triggered.connect(partial(self.apply_virtual_library, library=''))
virt_libs = db.prefs.get('virtual_libraries', {})
for vl in sorted(virt_libs.keys(), key=sort_key):
a = m.addAction(self.checked if vl == current_lib else self.empty, vl)
a.setToolTip(virt_libs[vl])
a.triggered.connect(partial(self.apply_virtual_library, library=vl))
m.show_tooltip_for_action(a)
p = QPoint(0, self.virtual_library.height())
self.virtual_library_menu.popup(self.virtual_library.mapToGlobal(p))
def apply_virtual_library(self, library=None):
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
if not library:
db.data.set_base_restriction('')
db.data.set_base_restriction_name('')
elif library in virt_libs:
db.data.set_base_restriction(virt_libs[library])
db.data.set_base_restriction_name(library)
self._apply_search_restriction(db.data.get_search_restriction(),
db.data.get_search_restriction_name())
def build_virtual_library_list(self):
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
m = self.rm_menu
m.clear()
def add_action(name, search):
a = m.addAction(name)
a.setToolTip(search)
m.show_tooltip_for_action(a)
a.triggered.connect(partial(self.remove_vl_triggered, name=name))
for n in sorted(virt_libs.keys(), key=sort_key):
add_action(n, virt_libs[n])
def remove_vl_triggered(self, name=None):
if not question_dialog(self, _('Are you sure?'),
_('Are you sure you want to remove '
'the virtual library {0}').format(name),
default_yes=False):
return
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
virt_libs.pop(name, None)
db.prefs.set('virtual_libraries', virt_libs)
if db.data.get_base_restriction_name() == name:
self.apply_virtual_library('')
def build_search_restriction_list(self):
m = self.ar_menu
m.clear()
current_restriction_text = None
if self.search_restriction.count() > 1:
txt = unicode(self.search_restriction.itemText(2))
if txt.startswith('*'):
current_restriction_text = txt
self.search_restriction.clear()
current_restriction = self.library_view.model().db.data.get_search_restriction_name()
m.setIcon(self.checked if current_restriction else self.empty)
def add_action(txt, index):
self.search_restriction.addItem(txt)
if txt == current_restriction:
a = m.addAction(self.checked, txt if txt else self.no_restriction)
else:
a = m.addAction(self.empty, txt if txt else self.no_restriction)
a.triggered.connect(partial(self.search_restriction_triggered,
action=a, index=index))
add_action('', 0)
add_action('*current search', 1)
dex = 2
if current_restriction_text:
add_action(current_restriction_text, 2)
dex += 1
for n in sorted(saved_searches().names(), key=sort_key):
add_action(n, dex)
dex += 1
def search_restriction_triggered(self, action=None, index=None):
self.search_restriction.setCurrentIndex(index)
self.apply_search_restriction(index)
def apply_named_search_restriction(self, name): def apply_named_search_restriction(self, name):
if not name: if not name:
@ -29,7 +303,6 @@ class SearchRestrictionMixin(object):
r = self.search_restriction.findText(name) r = self.search_restriction.findText(name)
if r < 0: if r < 0:
r = 0 r = 0
if r != self.search_restriction.currentIndex():
self.search_restriction.setCurrentIndex(r) self.search_restriction.setCurrentIndex(r)
self.apply_search_restriction(r) self.apply_search_restriction(r)
@ -37,7 +310,7 @@ class SearchRestrictionMixin(object):
search = unicode(search) search = unicode(search)
if not search: if not search:
self.search_restriction.setCurrentIndex(0) self.search_restriction.setCurrentIndex(0)
self._apply_search_restriction('') self._apply_search_restriction('', '')
else: else:
s = '*' + search s = '*' + search
if self.search_restriction.count() > 1: if self.search_restriction.count() > 1:
@ -49,10 +322,7 @@ class SearchRestrictionMixin(object):
else: else:
self.search_restriction.insertItem(2, s) self.search_restriction.insertItem(2, s)
self.search_restriction.setCurrentIndex(2) self.search_restriction.setCurrentIndex(2)
self.search_restriction.setToolTip('<p>' + self._apply_search_restriction(search, s)
self.search_restriction_tooltip +
_(' or the search ') + "'" + search + "'</p>")
self._apply_search_restriction(search)
def apply_search_restriction(self, i): def apply_search_restriction(self, i):
if i == 1: if i == 1:
@ -66,18 +336,20 @@ class SearchRestrictionMixin(object):
restriction = 'search:"%s"'%(r) restriction = 'search:"%s"'%(r)
else: else:
restriction = '' restriction = ''
self._apply_search_restriction(restriction) self._apply_search_restriction(restriction, r)
def _apply_search_restriction(self, restriction): def _apply_search_restriction(self, restriction, name):
self.saved_search.clear() self.saved_search.clear()
# The order below is important. Set the restriction, force a '' search # The order below is important. Set the restriction, force a '' search
# to apply it, reset the tag browser to take it into account, then set # to apply it, reset the tag browser to take it into account, then set
# the book count. # the book count.
self.library_view.model().db.data.set_search_restriction(restriction) self.library_view.model().db.data.set_search_restriction(restriction)
self.library_view.model().db.data.set_search_restriction_name(name)
self.search.clear(emit_search=True) self.search.clear(emit_search=True)
self.tags_view.set_search_restriction(restriction) self.tags_view.recount()
self.set_number_of_books_shown() self.set_number_of_books_shown()
self.current_view().setFocus(Qt.OtherFocusReason) self.current_view().setFocus(Qt.OtherFocusReason)
self.set_window_title()
def set_number_of_books_shown(self): def set_number_of_books_shown(self):
db = self.library_view.model().db db = self.library_view.model().db
@ -86,8 +358,8 @@ class SearchRestrictionMixin(object):
rows = self.current_view().row_count() rows = self.current_view().row_count()
rbc = max(rows, db.data.get_search_restriction_book_count()) rbc = max(rows, db.data.get_search_restriction_book_count())
t = _("({0} of {1})").format(rows, rbc) t = _("({0} of {1})").format(rows, rbc)
self.search_count.setStyleSheet \ self.search_count.setStyleSheet(
('QLabel { border-radius: 8px; background-color: yellow; }') 'QLabel { border-radius: 8px; background-color: yellow; }')
else: # No restriction or not library view else: # No restriction or not library view
if not self.search.in_a_search(): if not self.search.in_a_search():
t = _("(all books)") t = _("(all books)")

View File

@ -264,13 +264,8 @@ class TagsModel(QAbstractItemModel): # {{{
if rebuild: if rebuild:
self.rebuild_node_tree(state_map) self.rebuild_node_tree(state_map)
def set_search_restriction(self, s):
self.search_restriction = s
self.rebuild_node_tree()
def set_database(self, db): def set_database(self, db):
self.beginResetModel() self.beginResetModel()
self.search_restriction = None
hidden_cats = db.prefs.get('tag_browser_hidden_categories', None) hidden_cats = db.prefs.get('tag_browser_hidden_categories', None)
# migrate from config to db prefs # migrate from config to db prefs
if hidden_cats is None: if hidden_cats is None:
@ -848,7 +843,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.categories = {} self.categories = {}
# Get the categories # Get the categories
if self.search_restriction: if self.db.data.get_base_restriction() or self.db.data.get_search_restriction():
try: try:
data = self.db.get_categories(sort=sort, data = self.db.get_categories(sort=sort,
icon_map=self.category_icon_map, icon_map=self.category_icon_map,

View File

@ -232,10 +232,6 @@ class TagsView(QTreeView): # {{{
except: except:
pass pass
def set_search_restriction(self, s):
s = s if s else None
self._model.set_search_restriction(s)
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
dex = self.indexAt(event.pos()) dex = self.indexAt(event.pos())
if self.in_drag_drop or not dex.isValid(): if self.in_drag_drop or not dex.isValid():

View File

@ -187,7 +187,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
else: else:
stmap[st.name] = st stmap[st.name] = st
def initialize(self, library_path, db, listener, actions, show_gui=True): def initialize(self, library_path, db, listener, actions, show_gui=True):
opts = self.opts opts = self.opts
self.preferences_action, self.quit_action = actions self.preferences_action, self.quit_action = actions
@ -279,6 +278,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
UpdateMixin.__init__(self, opts) UpdateMixin.__init__(self, opts)
####################### Search boxes ######################## ####################### Search boxes ########################
SearchRestrictionMixin.__init__(self)
SavedSearchBoxMixin.__init__(self) SavedSearchBoxMixin.__init__(self)
SearchBoxMixin.__init__(self) SearchBoxMixin.__init__(self)
@ -313,9 +313,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
TagBrowserMixin.__init__(self, db) TagBrowserMixin.__init__(self, db)
######################### Search Restriction ########################## ######################### Search Restriction ##########################
SearchRestrictionMixin.__init__(self) if db.prefs['virtual_lib_on_startup']:
if db.prefs['gui_restriction']: self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
self.apply_named_search_restriction(db.prefs['gui_restriction'])
########################### Cover Flow ################################ ########################### Cover Flow ################################
@ -339,7 +338,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if config['autolaunch_server']: if config['autolaunch_server']:
self.start_content_server() self.start_content_server()
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
self.read_settings() self.read_settings()
@ -494,7 +492,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
path = os.path.abspath(argv[1]) path = os.path.abspath(argv[1])
if os.access(path, os.R_OK): if os.access(path, os.R_OK):
self.iactions['Add Books'].add_filesystem_book(path) self.iactions['Add Books'].add_filesystem_book(path)
self.setWindowState(self.windowState() & \ self.setWindowState(self.windowState() &
~Qt.WindowMinimized|Qt.WindowActive) ~Qt.WindowMinimized|Qt.WindowActive)
self.show_windows() self.show_windows()
self.raise_() self.raise_()
@ -526,7 +524,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def library_moved(self, newloc, copy_structure=False, call_close=True, def library_moved(self, newloc, copy_structure=False, call_close=True,
allow_rebuild=False): allow_rebuild=False):
if newloc is None: return if newloc is None:
return
default_prefs = None default_prefs = None
try: try:
olddb = self.library_view.model().db olddb = self.library_view.model().db
@ -537,7 +536,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
try: try:
db = LibraryDatabase2(newloc, default_prefs=default_prefs) db = LibraryDatabase2(newloc, default_prefs=default_prefs)
except (DatabaseException, sqlite.Error): except (DatabaseException, sqlite.Error):
if not allow_rebuild: raise if not allow_rebuild:
raise
import traceback import traceback
repair = question_dialog(self, _('Corrupted database'), repair = question_dialog(self, _('Corrupted database'),
_('The library database at %s appears to be corrupted. Do ' _('The library database at %s appears to be corrupted. Do '
@ -596,9 +596,16 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
# interface later # interface later
gc.collect() gc.collect()
def set_window_title(self): def set_window_title(self):
self.setWindowTitle(__appname__ + u' - || %s ||'%self.iactions['Choose Library'].library_name()) db = self.current_db
restrictions = [x for x in (db.data.get_base_restriction_name(),
db.data.get_search_restriction_name()) if x]
restrictions = ' :: '.join(restrictions)
if restrictions:
restrictions = ' :: ' + restrictions
title = u'{0} - || {1}{2} ||'.format(
__appname__, self.iactions['Choose Library'].library_name(), restrictions)
self.setWindowTitle(title)
def location_selected(self, location): def location_selected(self, location):
''' '''
@ -613,17 +620,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
for action in self.iactions.values(): for action in self.iactions.values():
action.location_selected(location) action.location_selected(location)
if location == 'library': if location == 'library':
self.search_restriction.setEnabled(True) self.virtual_library_menu.setEnabled(True)
self.highlight_only_button.setEnabled(True) self.highlight_only_button.setEnabled(True)
else: else:
self.search_restriction.setEnabled(False) self.virtual_library_menu.setEnabled(False)
self.highlight_only_button.setEnabled(False) self.highlight_only_button.setEnabled(False)
# Reset the view in case something changed while it was invisible # Reset the view in case something changed while it was invisible
self.current_view().reset() self.current_view().reset()
self.set_number_of_books_shown() self.set_number_of_books_shown()
def job_exception(self, job, dialog_title=_('Conversion Error')): def job_exception(self, job, dialog_title=_('Conversion Error')):
if not hasattr(self, '_modeless_dialogs'): if not hasattr(self, '_modeless_dialogs'):
self._modeless_dialogs = [] self._modeless_dialogs = []
@ -748,7 +753,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
return False return False
return True return True
def shutdown(self, write_settings=True): def shutdown(self, write_settings=True):
try: try:
db = self.library_view.model().db db = self.library_view.model().db
@ -808,13 +812,11 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
pass pass
QApplication.instance().quit() QApplication.instance().quit()
def closeEvent(self, e): def closeEvent(self, e):
self.write_settings() self.write_settings()
if self.system_tray_icon.isVisible(): if self.system_tray_icon.isVisible():
if not dynamic['systray_msg'] and not isosx: if not dynamic['systray_msg'] and not isosx:
info_dialog(self, 'calibre', 'calibre '+ \ info_dialog(self, 'calibre', 'calibre '+
_('will keep running in the system tray. To close it, ' _('will keep running in the system tray. To close it, '
'choose <b>Quit</b> in the context menu of the ' 'choose <b>Quit</b> in the context menu of the '
'system tray.'), show_copy_button=False).exec_() 'system tray.'), show_copy_button=False).exec_()

View File

@ -209,7 +209,8 @@ class ResultCache(SearchQueryParser): # {{{
self._data = [] self._data = []
self._map = self._map_filtered = [] self._map = self._map_filtered = []
self.first_sort = True self.first_sort = True
self.search_restriction = '' self.search_restriction = self.base_restriction = ''
self.base_restriction_name = self.search_restriction_name = ''
self.search_restriction_book_count = 0 self.search_restriction_book_count = 0
self.marked_ids_dict = {} self.marked_ids_dict = {}
self.field_metadata = field_metadata self.field_metadata = field_metadata
@ -825,8 +826,19 @@ class ResultCache(SearchQueryParser): # {{{
return ans return ans
self._map_filtered = ans self._map_filtered = ans
def _build_restriction_string(self, restriction):
if self.base_restriction:
if restriction:
return u'(%s) and (%s)' % (self.base_restriction, restriction)
else:
return self.base_restriction
else:
return restriction
def search_getting_ids(self, query, search_restriction, def search_getting_ids(self, query, search_restriction,
set_restriction_count=False): set_restriction_count=False, use_virtual_library=True):
if use_virtual_library:
search_restriction = self._build_restriction_string(search_restriction)
q = '' q = ''
if not query or not query.strip(): if not query or not query.strip():
q = search_restriction q = search_restriction
@ -847,11 +859,32 @@ class ResultCache(SearchQueryParser): # {{{
self.search_restriction_book_count = len(rv) self.search_restriction_book_count = len(rv)
return rv return rv
def get_search_restriction(self):
return self.search_restriction
def set_search_restriction(self, s): def set_search_restriction(self, s):
self.search_restriction = s self.search_restriction = s
def get_base_restriction(self):
return self.base_restriction
def set_base_restriction(self, s):
self.base_restriction = s
def get_base_restriction_name(self):
return self.base_restriction_name
def set_base_restriction_name(self, s):
self.base_restriction_name = s
def get_search_restriction_name(self):
return self.search_restriction_name
def set_search_restriction_name(self, s):
self.search_restriction_name = s
def search_restriction_applied(self): def search_restriction_applied(self):
return bool(self.search_restriction) return bool(self.search_restriction) or bool((self.base_restriction))
def get_search_restriction_book_count(self): def get_search_restriction_book_count(self):
return self.search_restriction_book_count return self.search_restriction_book_count
@ -1002,7 +1035,7 @@ class ResultCache(SearchQueryParser): # {{{
if field is not None: if field is not None:
self.sort(field, ascending) self.sort(field, ascending)
self._map_filtered = list(self._map) self._map_filtered = list(self._map)
if self.search_restriction: if self.search_restriction or self.base_restriction:
self.search('', return_matches=False) self.search('', return_matches=False)
# Sorting functions {{{ # Sorting functions {{{

View File

@ -229,6 +229,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
('uuid', False), ('comments', True), ('id', False), ('pubdate', False), ('uuid', False), ('comments', True), ('id', False), ('pubdate', False),
('last_modified', False), ('size', False), ('languages', False), ('last_modified', False), ('size', False), ('languages', False),
] ]
defs['virtual_libraries'] = {}
defs['virtual_lib_on_startup'] = defs['cs_virtual_lib_on_startup'] = ''
# Migrate the bool tristate tweak # Migrate the bool tristate tweak
defs['bools_are_tristate'] = \ defs['bools_are_tristate'] = \
@ -279,6 +281,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
except: except:
pass pass
# migrate the gui_restriction preference to a virtual library
gr_pref = self.prefs.get('gui_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['gui_restriction'] = ''
self.prefs['virtual_lib_on_startup'] = gr_pref
# migrate the cs_restriction preference to a virtual library
gr_pref = self.prefs.get('cs_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['cs_restriction'] = ''
self.prefs['cs_virtual_lib_on_startup'] = gr_pref
# Rename any user categories with names that differ only in case # Rename any user categories with names that differ only in case
user_cats = self.prefs.get('user_categories', []) user_cats = self.prefs.get('user_categories', [])
catmap = {} catmap = {}

View File

@ -205,26 +205,32 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
def set_database(self, db): def set_database(self, db):
self.db = db self.db = db
virt_libs = db.prefs.get('virtual_libraries', {})
sr = getattr(self.opts, 'restriction', None) sr = getattr(self.opts, 'restriction', None)
sr = db.prefs.get('cs_restriction', '') if sr is None else sr if sr:
self.set_search_restriction(sr) if sr in virt_libs:
sr = virt_libs[sr]
elif sr not in saved_searches().names():
prints('WARNING: Content server: search restriction ',
sr, ' does not exist')
sr = ''
else:
sr = 'search:"%s"'%sr
else:
sr = db.prefs.get('cs_virtual_lib_on_startup', '')
if sr:
if sr not in virt_libs:
prints('WARNING: Content server: virtual library ',
sr, ' does not exist')
sr = ''
else:
sr = virt_libs[sr]
self.search_restriction = sr
self.reset_caches()
def graceful(self): def graceful(self):
cherrypy.engine.graceful() cherrypy.engine.graceful()
def set_search_restriction(self, restriction):
self.search_restriction_name = restriction
if restriction:
if restriction not in saved_searches().names():
prints('WARNING: Content server: search restriction ',
restriction, ' does not exist')
self.search_restriction = ''
else:
self.search_restriction = 'search:"%s"'%restriction
else:
self.search_restriction = ''
self.reset_caches()
def setup_loggers(self): def setup_loggers(self):
access_file = log_access_file access_file = log_access_file
error_file = log_error_file error_file = log_error_file

View File

@ -145,10 +145,7 @@ def render_rating(rating, url_prefix, container='span', prefix=None): # {{{
# }}} # }}}
def get_category_items(category, items, restriction, datatype, prefix): # {{{ def get_category_items(category, items, datatype, prefix): # {{{
if category == 'search':
items = [x for x in items if x.name != restriction]
def item(i): def item(i):
templ = (u'<div title="{4}" class="category-item">' templ = (u'<div title="{4}" class="category-item">'
@ -489,8 +486,7 @@ class BrowseServer(object):
if not cats and len(items) == 1: if not cats and len(items) == 1:
# Only one item in category, go directly to book list # Only one item in category, go directly to book list
html = get_category_items(category, items, html = get_category_items(category, items,
self.search_restriction_name, datatype, datatype, self.opts.url_prefix)
self.opts.url_prefix)
href = re.search(r'<a href="([^"]+)"', html) href = re.search(r'<a href="([^"]+)"', html)
if href is not None: if href is not None:
raise cherrypy.HTTPRedirect(href.group(1)) raise cherrypy.HTTPRedirect(href.group(1))
@ -498,8 +494,7 @@ class BrowseServer(object):
if len(items) <= self.opts.max_opds_ungrouped_items: if len(items) <= self.opts.max_opds_ungrouped_items:
script = 'false' script = 'false'
items = get_category_items(category, items, items = get_category_items(category, items,
self.search_restriction_name, datatype, datatype, self.opts.url_prefix)
self.opts.url_prefix)
else: else:
getter = lambda x: unicode(getattr(x, 'sort', x.name)) getter = lambda x: unicode(getattr(x, 'sort', x.name))
starts = set([]) starts = set([])
@ -588,8 +583,7 @@ class BrowseServer(object):
sort = self.browse_sort_categories(entries, sort) sort = self.browse_sort_categories(entries, sort)
entries = get_category_items(category, entries, entries = get_category_items(category, entries,
self.search_restriction_name, datatype, datatype, self.opts.url_prefix)
self.opts.url_prefix)
return json.dumps(entries, ensure_ascii=True) return json.dumps(entries, ensure_ascii=True)

View File

@ -54,10 +54,11 @@ The OPDS interface is advertised via BonJour automatically.
help=_('Write process PID to the specified file')) help=_('Write process PID to the specified file'))
parser.add_option('--daemonize', default=False, action='store_true', parser.add_option('--daemonize', default=False, action='store_true',
help='Run process in background as a daemon. No effect on windows.') help='Run process in background as a daemon. No effect on windows.')
parser.add_option('--restriction', default=None, parser.add_option('--restriction', '--virtual-library', default=None,
help=_('Specifies a restriction to be used for this invocation. ' help=_('Specifies a virtual library to be used for this invocation. '
'This option overrides any per-library settings specified' 'This option overrides any per-library settings specified'
' in the GUI')) ' in the GUI. For compatibility, if the value is not a '
'virtual library but is a saved search, that saved search is used.'))
parser.add_option('--auto-reload', default=False, action='store_true', parser.add_option('--auto-reload', default=False, action='store_true',
help=_('Auto reload server when source code changes. May not' help=_('Auto reload server when source code changes. May not'
' work in all environments.')) ' work in all environments.'))