From 0313b2c0928950afc34cfb467d77fd4bf6a43dfd Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 9 Apr 2013 12:41:49 +0200 Subject: [PATCH 1/4] Change search restrictions to virtual libraries --- src/calibre/gui2/layout.py | 8 +- src/calibre/gui2/preferences/behavior.py | 5 +- src/calibre/gui2/preferences/behavior.ui | 8 +- src/calibre/gui2/preferences/server.py | 6 +- src/calibre/gui2/preferences/server.ui | 6 +- src/calibre/gui2/search_box.py | 12 +- src/calibre/gui2/search_restriction_mixin.py | 317 +++++++++++++++++-- src/calibre/gui2/tag_browser/model.py | 7 +- src/calibre/gui2/tag_browser/view.py | 4 - src/calibre/gui2/ui.py | 17 +- src/calibre/library/caches.py | 41 ++- src/calibre/library/database2.py | 20 ++ src/calibre/library/server/base.py | 36 ++- src/calibre/library/server/browse.py | 14 +- src/calibre/library/server/main.py | 7 +- 15 files changed, 410 insertions(+), 98 deletions(-) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 6563059821..a37b3545fd 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -173,11 +173,11 @@ class SearchBar(QWidget): # {{{ self.setLayout(self._layout) self._layout.setContentsMargins(0,5,0,0) - x = ComboBoxWithHelp(self) - x.setMaximumSize(QSize(150, 16777215)) - x.setObjectName("search_restriction") + x = QToolButton(self) + x.setText(_('Virtual Libraries')) + x.setObjectName("virtual_library") l.addWidget(x) - parent.search_restriction = x + parent.virtual_library = x x = QLabel(self) x.setObjectName("search_count") diff --git a/src/calibre/gui2/preferences/behavior.py b/src/calibre/gui2/preferences/behavior.py index b5070cbdea..8afda3ca00 100644 --- a/src/calibre/gui2/preferences/behavior.py +++ b/src/calibre/gui2/preferences/behavior.py @@ -14,7 +14,6 @@ from calibre.gui2.preferences.behavior_ui import Ui_Form from calibre.gui2 import config, info_dialog, dynamic, gprefs from calibre.utils.config import prefs 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.oeb.iterator import is_supported from calibre.constants import iswindows @@ -48,9 +47,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): choices = [(x.upper(), x) for x in output_formats] r('output_format', prefs, choices=choices, setting=OutputFormatSetting) - restrictions = sorted(saved_searches().names(), key=sort_key) + restrictions = sorted(db.prefs.get('virtual_libraries').keys(), key=sort_key) choices = [('', '')] + [(x, x) for x in restrictions] - r('gui_restriction', db.prefs, choices=choices) + r('virtual_lib_on_startup', db.prefs, choices=choices) self.reset_confirmation_button.clicked.connect(self.reset_confirmation_dialogs) self.input_up_button.clicked.connect(self.up_input) diff --git a/src/calibre/gui2/preferences/behavior.ui b/src/calibre/gui2/preferences/behavior.ui index ffd59d72bb..61620e4e21 100644 --- a/src/calibre/gui2/preferences/behavior.ui +++ b/src/calibre/gui2/preferences/behavior.ui @@ -147,15 +147,15 @@ If not checked, the values can be Yes or No. - Restriction to apply when the current library is opened: + Virtual library to apply when the current library is opened: - opt_gui_restriction + opt_virtual_lib_on_startup - + 250 @@ -163,7 +163,7 @@ If not checked, the values can be Yes or No. - 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. + 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. QComboBox::AdjustToMinimumContentsLengthWithIcon diff --git a/src/calibre/gui2/preferences/server.py b/src/calibre/gui2/preferences/server.py index 2a62fe555f..a3055fe2f7 100644 --- a/src/calibre/gui2/preferences/server.py +++ b/src/calibre/gui2/preferences/server.py @@ -44,13 +44,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): else self.opt_password.Password)) self.opt_password.setEchoMode(self.opt_password.Password) - restrictions = sorted(saved_searches().names(), key=sort_key) + restrictions = sorted(db.prefs.get('virtual_libraries').keys(), key=sort_key) # verify that the current restriction still exists. If not, clear it. - csr = db.prefs.get('cs_restriction', None) + csr = db.prefs.get('cs_virtual_lib_on_startup', None) if csr and csr not in restrictions: db.prefs.set('cs_restriction', '') choices = [('', '')] + [(x, x) for x in restrictions] - r('cs_restriction', db.prefs, choices=choices) + r('cs_virtual_lib_on_startup', db.prefs, choices=choices) self.start_button.setEnabled(not getattr(self.server, 'is_running', False)) self.test_button.setEnabled(not self.start_button.isEnabled()) diff --git a/src/calibre/gui2/preferences/server.ui b/src/calibre/gui2/preferences/server.ui index 163221594f..674e4bdbc2 100644 --- a/src/calibre/gui2/preferences/server.ui +++ b/src/calibre/gui2/preferences/server.ui @@ -139,14 +139,14 @@ - Restriction (saved search) to apply: + Virtual library to apply: - + - 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). + 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). QComboBox::AdjustToMinimumContentsLengthWithIcon diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 54a80571e6..ddcd02cce5 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -452,7 +452,7 @@ class SavedSearchBoxMixin(object): # {{{ self.saved_search.save_search_button_clicked) self.copy_search_button.clicked.connect( self.saved_search.copy_search_button_clicked) - self.saved_searches_changed() +# self.saved_searches_changed() self.saved_search.initialize(self.search, colorize=True, help_text=_('Saved Searches')) self.saved_search.setToolTip( @@ -479,17 +479,9 @@ class SavedSearchBoxMixin(object): # {{{ partial(self.do_saved_search_edit, None)) def saved_searches_changed(self, set_restriction=None, recount=True): - p = sorted(saved_searches().names(), key=sort_key) - 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')) + self.build_search_restriction_list() if 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 self.apply_named_search_restriction(set_restriction) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 1319f8d17d..1cd1edeb21 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -4,23 +4,296 @@ Created on 10 Jun 2010 @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): + no_restriction = _('') + def __init__(self): - self.search_restriction.initialize(help_text=_('Restrict to')) - self.search_restriction.activated[int].connect(self.apply_search_restriction) - self.library_view.model().count_changed_signal.connect(self.set_number_of_books_shown) - self.search_restriction.setSizeAdjustPolicy( - self.search_restriction.AdjustToMinimumContentsLengthWithIcon) - self.search_restriction.setMinimumContentsLength(10) - self.search_restriction.setStatusTip(self.search_restriction.toolTip()) + self.checked = QIcon(I('ok.png')) + self.empty = QIcon() + + self.virtual_library_menu = VirtLibMenu() + + self.virtual_library.clicked.connect(self.virtual_library_clicked) + + 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_restriction_tooltip = \ - _('Books display will be restricted to those matching a ' - 'selected saved search') - self.search_restriction.setToolTip(self.search_restriction_tooltip) + + def add_virtual_library(self, db, name, search): + 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 + + self.ar_menu = a = QMenu(_('Additional restriction')) + 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): if not name: @@ -29,15 +302,14 @@ class SearchRestrictionMixin(object): r = self.search_restriction.findText(name) if r < 0: r = 0 - if r != self.search_restriction.currentIndex(): - self.search_restriction.setCurrentIndex(r) - self.apply_search_restriction(r) + self.search_restriction.setCurrentIndex(r) + self.apply_search_restriction(r) def apply_text_search_restriction(self, search): search = unicode(search) if not search: self.search_restriction.setCurrentIndex(0) - self._apply_search_restriction('') + self._apply_search_restriction('', '') else: s = '*' + search if self.search_restriction.count() > 1: @@ -49,10 +321,7 @@ class SearchRestrictionMixin(object): else: self.search_restriction.insertItem(2, s) self.search_restriction.setCurrentIndex(2) - self.search_restriction.setToolTip('

' + - self.search_restriction_tooltip + - _(' or the search ') + "'" + search + "'

") - self._apply_search_restriction(search) + self._apply_search_restriction(search, s) def apply_search_restriction(self, i): if i == 1: @@ -66,18 +335,20 @@ class SearchRestrictionMixin(object): restriction = 'search:"%s"'%(r) else: 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() # 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 # the book count. 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.tags_view.set_search_restriction(restriction) + self.tags_view.recount() self.set_number_of_books_shown() self.current_view().setFocus(Qt.OtherFocusReason) + self.set_window_title() def set_number_of_books_shown(self): db = self.library_view.model().db diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 742f2b2776..d6d40ca4f7 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -264,13 +264,8 @@ class TagsModel(QAbstractItemModel): # {{{ if rebuild: 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): self.beginResetModel() - self.search_restriction = None hidden_cats = db.prefs.get('tag_browser_hidden_categories', None) # migrate from config to db prefs if hidden_cats is None: @@ -848,7 +843,7 @@ class TagsModel(QAbstractItemModel): # {{{ self.categories = {} # Get the categories - if self.search_restriction: + if self.db.data.get_base_restriction or self.db.data.get_search_restriction: try: data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map, diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index 7070eaaa04..cefa0f8975 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -232,10 +232,6 @@ class TagsView(QTreeView): # {{{ except: pass - def set_search_restriction(self, s): - s = s if s else None - self._model.set_search_restriction(s) - def mouseMoveEvent(self, event): dex = self.indexAt(event.pos()) if self.in_drag_drop or not dex.isValid(): diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 65993ff31c..54384df0cd 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -279,6 +279,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ UpdateMixin.__init__(self, opts) ####################### Search boxes ######################## + SearchRestrictionMixin.__init__(self) SavedSearchBoxMixin.__init__(self) SearchBoxMixin.__init__(self) @@ -313,9 +314,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ TagBrowserMixin.__init__(self, db) ######################### Search Restriction ########################## - SearchRestrictionMixin.__init__(self) - if db.prefs['gui_restriction']: - self.apply_named_search_restriction(db.prefs['gui_restriction']) + if db.prefs['virtual_lib_on_startup']: + self.apply_virtual_library(db.prefs['virtual_lib_on_startup']) ########################### Cover Flow ################################ @@ -598,7 +598,12 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ def set_window_title(self): - self.setWindowTitle(__appname__ + u' - || %s ||'%self.iactions['Choose Library'].library_name()) + title = u'{0} - || {1} :: {2} :: {3} ||'.format( + __appname__, + self.iactions['Choose Library'].library_name(), + self.library_view.model().db.data.get_base_restriction_name(), + self.library_view.model().db.data.get_search_restriction_name()) + self.setWindowTitle(title) def location_selected(self, location): ''' @@ -613,10 +618,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ for action in self.iactions.values(): action.location_selected(location) if location == 'library': - self.search_restriction.setEnabled(True) + self.virtual_library_menu.setEnabled(True) self.highlight_only_button.setEnabled(True) else: - self.search_restriction.setEnabled(False) + self.virtual_library_menu.setEnabled(False) self.highlight_only_button.setEnabled(False) # Reset the view in case something changed while it was invisible self.current_view().reset() diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index b453c654df..048288ef71 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -209,7 +209,8 @@ class ResultCache(SearchQueryParser): # {{{ self._data = [] self._map = self._map_filtered = [] 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.marked_ids_dict = {} self.field_metadata = field_metadata @@ -825,8 +826,19 @@ class ResultCache(SearchQueryParser): # {{{ return 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, - 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 = '' if not query or not query.strip(): q = search_restriction @@ -847,11 +859,32 @@ class ResultCache(SearchQueryParser): # {{{ self.search_restriction_book_count = len(rv) return rv + def get_search_restriction(self): + return self.search_restriction + def set_search_restriction(self, 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): - return bool(self.search_restriction) + return bool(self.search_restriction) or bool((self.base_restriction)) def get_search_restriction_book_count(self): return self.search_restriction_book_count @@ -1002,7 +1035,7 @@ class ResultCache(SearchQueryParser): # {{{ if field is not None: self.sort(field, ascending) self._map_filtered = list(self._map) - if self.search_restriction: + if self.search_restriction or self.base_restriction: self.search('', return_matches=False) # Sorting functions {{{ diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 376eb52c3c..14c71d5918 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -229,6 +229,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ('uuid', False), ('comments', True), ('id', False), ('pubdate', 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 defs['bools_are_tristate'] = \ @@ -279,6 +281,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): except: 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 user_cats = self.prefs.get('user_categories', []) catmap = {} diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py index 9c14f128dd..bbd5239b42 100644 --- a/src/calibre/library/server/base.py +++ b/src/calibre/library/server/base.py @@ -205,26 +205,32 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache, def set_database(self, db): self.db = db + virt_libs = db.prefs.get('virtual_libraries', {}) sr = getattr(self.opts, 'restriction', None) - sr = db.prefs.get('cs_restriction', '') if sr is None else sr - self.set_search_restriction(sr) + if 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): 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): access_file = log_access_file error_file = log_error_file diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index c520e42f34..d25c34d52b 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -145,10 +145,7 @@ def render_rating(rating, url_prefix, container='span', prefix=None): # {{{ # }}} -def get_category_items(category, items, restriction, datatype, prefix): # {{{ - - if category == 'search': - items = [x for x in items if x.name != restriction] +def get_category_items(category, items, datatype, prefix): # {{{ def item(i): templ = (u'
' @@ -489,8 +486,7 @@ class BrowseServer(object): if not cats and len(items) == 1: # Only one item in category, go directly to book list html = get_category_items(category, items, - self.search_restriction_name, datatype, - self.opts.url_prefix) + datatype, self.opts.url_prefix) href = re.search(r' Date: Tue, 9 Apr 2013 14:51:34 +0200 Subject: [PATCH 2/4] Fix exception if search is saved before virtual lib button is clicked. Add quotes around VL created when migrating restriction preferences to VL preferences --- src/calibre/gui2/search_restriction_mixin.py | 4 +++- src/calibre/library/database2.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 1cd1edeb21..52b366b50a 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -150,6 +150,7 @@ class SearchRestrictionMixin(object): self.search_restriction = ComboBoxWithHelp(self) self.search_restriction.setVisible(False) self.search_count.setText(_("(all books)")) + self.ar_menu = QMenu(_('Additional restriction')) def add_virtual_library(self, db, name, search): virt_libs = db.prefs.get('virtual_libraries', {}) @@ -190,7 +191,8 @@ class SearchRestrictionMixin(object): db = self.library_view.model().db - self.ar_menu = a = QMenu(_('Additional restriction')) + 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) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index ccb614fbce..0a781e5948 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -285,7 +285,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 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 + 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 @@ -294,7 +294,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 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 + 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 From 4f6ec55b2e79b25f5dd5fd5669d587ed25acb829 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 10 Apr 2013 08:35:25 +0200 Subject: [PATCH 3/4] Fix stupid typo --- src/calibre/gui2/tag_browser/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index d6d40ca4f7..33d1235f8b 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -843,7 +843,7 @@ class TagsModel(QAbstractItemModel): # {{{ self.categories = {} # Get the categories - if self.db.data.get_base_restriction or self.db.data.get_search_restriction: + if self.db.data.get_base_restriction() or self.db.data.get_search_restriction(): try: data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map, From 57ee4a0fb6dcbb0fe7792251d0a8ca142af06405 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 10 Apr 2013 08:45:12 +0200 Subject: [PATCH 4/4] Prevent creation of empty saved searches --- src/calibre/gui2/search_box.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index ddcd02cce5..1ae6b23fdb 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -332,6 +332,10 @@ class SavedSearchBox(QComboBox): # {{{ name = unicode(self.currentText()) if not name.strip(): 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().add(name, unicode(self.search_box.text())) # now go through an initialization cycle to ensure that the combobox has