From 0313b2c0928950afc34cfb467d77fd4bf6a43dfd Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 9 Apr 2013 12:41:49 +0200 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 From 2b0afb54eb7633b31237704cfbc9a93f86724dde Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 10 Apr 2013 18:52:52 +0530 Subject: [PATCH 05/11] Make virtual library button bold when using a restriction --- src/calibre/gui2/ui.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 2ade363338..685604e2ce 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -15,7 +15,7 @@ from threading import Thread from collections import OrderedDict from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction, - QMenu, QIcon, pyqtSignal, QUrl, + QMenu, QIcon, pyqtSignal, QUrl, QFont, QDialog, QSystemTrayIcon, QApplication) from calibre import prints, force_unicode @@ -601,8 +601,11 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ restrictions = [x for x in (db.data.get_base_restriction_name(), db.data.get_search_restriction_name()) if x] restrictions = ' :: '.join(restrictions) + font = QFont() if restrictions: restrictions = ' :: ' + restrictions + font.setBold(True) + self.virtual_library.setFont(font) title = u'{0} - || {1}{2} ||'.format( __appname__, self.iactions['Choose Library'].library_name(), restrictions) self.setWindowTitle(title) From d7d840aa52852ec4bab9d1a2e3bc374d5ee45436 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 11 Apr 2013 11:38:34 +0530 Subject: [PATCH 06/11] Add some hrlp text to the create vl dialog and remove the tooltips from the vl menu as they were creating rendering artefacts on my linux machine --- src/calibre/gui2/search_restriction_mixin.py | 121 ++++++++++--------- 1 file changed, 66 insertions(+), 55 deletions(-) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 54274a104d..cad8ba9f00 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -6,15 +6,18 @@ Created on 10 Jun 2010 from functools import partial -from PyQt4.Qt import (Qt, QMenu, QPoint, QIcon, QDialog, QGridLayout, QLabel, - QLineEdit, QDialogButtonBox, QEvent, QToolTip) +from PyQt4.Qt import ( + Qt, QMenu, QPoint, QIcon, QDialog, QGridLayout, QLabel, QLineEdit, + QDialogButtonBox, QSize) + 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): +class CreateVirtualLibrary(QDialog): # {{{ + def __init__(self, gui, existing_names): QDialog.__init__(self, None, Qt.WindowSystemMenuHint | Qt.WindowTitleHint) @@ -22,22 +25,49 @@ class CreateVirtualLibrary(QDialog): self.existing_names = existing_names self.setWindowTitle(_('Create virtual library')) + self.setWindowIcon(QIcon(I('lt.png'))) + gl = QGridLayout() self.setLayout(gl) - gl.addWidget(QLabel(_('Virtual library name')), 0, 0) + self.la1 = la1 = QLabel(_('Virtual library &name:')) + gl.addWidget(la1, 0, 0) self.vl_name = QLineEdit() - self.vl_name.setMinimumWidth(400) + la1.setBuddy(self.vl_name) gl.addWidget(self.vl_name, 0, 1) - gl.addWidget(QLabel(_('Search expression')), 1, 0) + + self.la2 = la2 = QLabel(_('&Search expression:')) + gl.addWidget(la2, 1, 0) self.vl_text = QLineEdit() + la2.setBuddy(self.vl_text) 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 = [ + self.hl = hl = QLabel(_(''' +

Virtual Libraries

+ +

Using virtual libraries you can restrict calibre to only show + you books that match a search. When a virtual library is in effect, calibre + behaves as though the library contains only the matched books. The Tag Browser + display only the tags/authors/series/etc. that belong to the matched books and any searches + you do will only search within the books in the virtual library. This + is a good way to partition your large library into smaller and easier to work with subsets.

+ +

For example you can use a Virtual Library to only show you books with the Tag "Unread" + or only books by "My Favorite Author" or only books in a particular series.

+ ''')) + hl.setWordWrap(True) + hl.setFrameStyle(hl.StyledPanel) + gl.addWidget(hl, 0, 3, 3, 1) + + bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + gl.addWidget(bb, 3, 0, 1, 0) + + self.resize(self.sizeHint()+QSize(150, 25)) + + def build_full_search_string(self): + search_templates = ( '', '{cl}', '{cr}', @@ -46,11 +76,10 @@ class CreateVirtualLibrary(QDialog): '(({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 + db = self.gui.current_db cr = db.data.get_search_restriction() cl = db.data.get_base_restriction() dex = 0 @@ -60,10 +89,10 @@ class CreateVirtualLibrary(QDialog): dex += 2 if cl: dex += 1 - template = self.search_templates[dex] + template = search_templates[dex] return template.format(cl=cl, cr=cr, sb=sb) - def accepted(self): + def accept(self): n = unicode(self.vl_name.text()) if not n: error_dialog(self.gui, _('No name'), @@ -94,42 +123,17 @@ class CreateVirtualLibrary(QDialog): 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: + if not recs and not 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): 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) + QDialog.accept(self) +# }}} class SearchRestrictionMixin(object): @@ -139,7 +143,7 @@ class SearchRestrictionMixin(object): self.checked = QIcon(I('ok.png')) self.empty = QIcon() - self.virtual_library_menu = VirtLibMenu() + self.virtual_library_menu = QMenu() self.virtual_library.clicked.connect(self.virtual_library_clicked) @@ -161,8 +165,7 @@ class SearchRestrictionMixin(object): 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: + if cd.exec_() == cd.Accepted: self.add_virtual_library(db, cd.library_name, cd.library_search) self.apply_virtual_library(cd.library_name) @@ -180,9 +183,8 @@ class SearchRestrictionMixin(object): 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() + self.rm_menu = a = QMenu() a.setTitle(_('Remove Virtual Library')) a.aboutToShow.connect(self.build_virtual_library_list) m.addMenu(a) @@ -212,7 +214,6 @@ class SearchRestrictionMixin(object): 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)) @@ -238,7 +239,6 @@ class SearchRestrictionMixin(object): 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): @@ -368,3 +368,14 @@ class SearchRestrictionMixin(object): self.search_count.setStyleSheet( 'QLabel { background-color: transparent; }') self.search_count.setText(t) + +if __name__ == '__main__': + from calibre.gui2 import Application + from calibre.gui2.preferences import init_gui + app = Application([]) + app + gui = init_gui() + d = CreateVirtualLibrary(gui, []) + d.exec_() + + From f13ccc6d9723ad5a34526b75240abec336dc165a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 11 Apr 2013 11:42:53 +0530 Subject: [PATCH 07/11] ... --- src/calibre/gui2/search_restriction_mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index cad8ba9f00..119cf34d14 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -19,7 +19,7 @@ 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) + QDialog.__init__(self, gui) self.gui = gui self.existing_names = existing_names From edb2d969343a2d11a995a43112164de089aa51e3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 11 Apr 2013 13:33:29 +0530 Subject: [PATCH 08/11] Add wizard for easily creatin VL based on authors/tags/series/publishers --- src/calibre/gui2/search_restriction_mixin.py | 56 ++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 119cf34d14..a09927be5d 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -8,7 +8,7 @@ from functools import partial from PyQt4.Qt import ( Qt, QMenu, QPoint, QIcon, QDialog, QGridLayout, QLabel, QLineEdit, - QDialogButtonBox, QSize) + QDialogButtonBox, QSize, QVBoxLayout, QListWidget, QStringList) from calibre.gui2 import error_dialog, question_dialog from calibre.gui2.widgets import ComboBoxWithHelp @@ -16,6 +16,34 @@ from calibre.utils.icu import sort_key from calibre.utils.pyparsing import ParseException from calibre.utils.search_query_parser import saved_searches +class SelectNames(QDialog): # {{{ + + def __init__(self, names, txt, parent=None): + QDialog.__init__(self, parent) + self.l = l = QVBoxLayout(self) + self.setLayout(l) + + self.la = la = QLabel(_('Create a Virtual Library based on %s') % txt) + l.addWidget(la) + + self._names = QListWidget(self) + self._names.addItems(QStringList(sorted(names, key=sort_key))) + self._names.setSelectionMode(self._names.ExtendedSelection) + l.addWidget(self._names) + + self.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.bb.accepted.connect(self.accept) + self.bb.rejected.connect(self.reject) + l.addWidget(self.bb) + + self.resize(self.sizeHint()) + + @property + def names(self): + for item in self._names.selectedItems(): + yield unicode(item.data(Qt.DisplayRole).toString()) +# }}} + class CreateVirtualLibrary(QDialog): # {{{ def __init__(self, gui, existing_names): @@ -42,6 +70,16 @@ class CreateVirtualLibrary(QDialog): # {{{ gl.addWidget(self.vl_text, 1, 1) self.vl_text.setText(self.build_full_search_string()) + self.sl = sl = QLabel('

'+_('Create a virtual library based on: ')+ + ('{0}, ' + '{1}, ' + '{2}, ' + '{3}.').format(_('Authors'), _('Tags'), _('Publishers'), _('Series'))) + sl.setWordWrap(True) + sl.setTextInteractionFlags(Qt.LinksAccessibleByMouse) + sl.linkActivated.connect(self.link_activated) + gl.addWidget(sl, 2, 0, 1, 2) + self.hl = hl = QLabel(_('''

Virtual Libraries

@@ -57,15 +95,27 @@ class CreateVirtualLibrary(QDialog): # {{{ ''')) hl.setWordWrap(True) hl.setFrameStyle(hl.StyledPanel) - gl.addWidget(hl, 0, 3, 3, 1) + gl.addWidget(hl, 0, 3, 4, 1) bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) - gl.addWidget(bb, 3, 0, 1, 0) + gl.addWidget(bb, 4, 0, 1, 0) self.resize(self.sizeHint()+QSize(150, 25)) + def link_activated(self, url): + db = self.gui.current_db + f, txt = unicode(url).partition('.')[0::2] + names = getattr(db, 'all_%s_names'%f)() + d = SelectNames(names, txt, parent=self) + if d.exec_() == d.Accepted: + prefix = f+'s' if f in {'tag', 'author'} else f + search = ['%s:"=%s"'%(prefix, x.replace('"', '\\"')) for x in d.names] + if search: + self.vl_name.setText(d.names.next()) + self.vl_text.setText(' or '.join(search)) + def build_full_search_string(self): search_templates = ( '', From 554c937a76a8117dda7f8abd7c2502dd671403de Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 12 Apr 2013 11:34:58 +0200 Subject: [PATCH 09/11] 1) Add "Edit virtual library". This also solves the problem of no longer having tooltips because the user can "edit" to see the VL search 2) Remove remaining tooltips 3) Add "current search" as a VL 4) Limit VL and restriction names to 40 characters --- src/calibre/gui2/search_restriction_mixin.py | 161 +++++++++++++------ 1 file changed, 109 insertions(+), 52 deletions(-) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index a09927be5d..702f67f69d 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -44,15 +44,46 @@ class SelectNames(QDialog): # {{{ yield unicode(item.data(Qt.DisplayRole).toString()) # }}} +MAX_VIRTUAL_LIBRARY_NAME_LENGTH = 40 + +def _build_full_search_string(gui): + search_templates = ( + '', + '{cl}', + '{cr}', + '(({cl}) and ({cr}))', + '{sb}', + '(({cl}) and ({sb}))', + '(({cr}) and ({sb}))', + '(({cl}) and ({cr}) and ({sb}))' + ) + + sb = gui.search.current_text + db = gui.current_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 = search_templates[dex] + return template.format(cl=cl, cr=cr, sb=sb).strip() + class CreateVirtualLibrary(QDialog): # {{{ - def __init__(self, gui, existing_names): + def __init__(self, gui, existing_names, editing=None): QDialog.__init__(self, gui) self.gui = gui self.existing_names = existing_names - self.setWindowTitle(_('Create virtual library')) + if editing: + self.setWindowTitle(_('Edit virtual library')) + else: + self.setWindowTitle(_('Create virtual library')) self.setWindowIcon(QIcon(I('lt.png'))) gl = QGridLayout() @@ -60,15 +91,19 @@ class CreateVirtualLibrary(QDialog): # {{{ self.la1 = la1 = QLabel(_('Virtual library &name:')) gl.addWidget(la1, 0, 0) self.vl_name = QLineEdit() + self.vl_name.setMaxLength(MAX_VIRTUAL_LIBRARY_NAME_LENGTH) la1.setBuddy(self.vl_name) gl.addWidget(self.vl_name, 0, 1) + self.editing = editing + if editing: + self.vl_name.setText(editing) self.la2 = la2 = QLabel(_('&Search expression:')) gl.addWidget(la2, 1, 0) self.vl_text = QLineEdit() la2.setBuddy(self.vl_text) gl.addWidget(self.vl_text, 1, 1) - self.vl_text.setText(self.build_full_search_string()) + self.vl_text.setText(_build_full_search_string(self.gui)) self.sl = sl = QLabel('

'+_('Create a virtual library based on: ')+ ('{0}, ' @@ -102,6 +137,11 @@ class CreateVirtualLibrary(QDialog): # {{{ bb.rejected.connect(self.reject) gl.addWidget(bb, 4, 0, 1, 0) + if editing: + db = self.gui.current_db + virt_libs = db.prefs.get('virtual_libraries', {}) + self.vl_text.setText(virt_libs.get(editing, '')) + self.resize(self.sizeHint()+QSize(150, 25)) def link_activated(self, url): @@ -116,48 +156,28 @@ class CreateVirtualLibrary(QDialog): # {{{ self.vl_name.setText(d.names.next()) self.vl_text.setText(' or '.join(search)) - def build_full_search_string(self): - search_templates = ( - '', - '{cl}', - '{cr}', - '(({cl}) and ({cr}))', - '{sb}', - '(({cl}) and ({sb}))', - '(({cr}) and ({sb}))', - '(({cl}) and ({cr}) and ({sb}))' - ) - - sb = self.gui.search.current_text - db = self.gui.current_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 = search_templates[dex] - return template.format(cl=cl, cr=cr, sb=sb) - def accept(self): - n = unicode(self.vl_name.text()) + n = unicode(self.vl_name.text()).strip() 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 n.startswith('*'): + error_dialog(self.gui, _('Invalid name'), + _('A virtual library name cannot begin with "*"'), + show=True) + return + + if n in self.existing_names and n != self.editing: 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()) + v = unicode(self.vl_text.text()).strip() if not v: error_dialog(self.gui, _('No search string'), _('You must provide a search to define the new virtual library'), @@ -192,6 +212,8 @@ class SearchRestrictionMixin(object): def __init__(self): self.checked = QIcon(I('ok.png')) self.empty = QIcon() + self.search_based_vl_name = None + self.search_based_vl = None self.virtual_library_menu = QMenu() @@ -211,32 +233,31 @@ class SearchRestrictionMixin(object): virt_libs[name] = search db.prefs.set('virtual_libraries', virt_libs) - def do_create(self): + def do_create_edit(self, editing=None): db = self.library_view.model().db virt_libs = db.prefs.get('virtual_libraries', {}) - cd = CreateVirtualLibrary(self, virt_libs.keys()) + cd = CreateVirtualLibrary(self, virt_libs.keys(), editing=editing) if cd.exec_() == cd.Accepted: + if editing: + self._remove_vl(editing, reapply=False) 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')) + a.triggered.connect(partial(self.do_create_edit, editing=None)) + + self.edit_menu = a = QMenu() + a.setTitle(_('Edit Virtual Library')) + a.aboutToShow.connect(partial(self.build_virtual_library_list, remove=False)) + m.addMenu(a) self.rm_menu = a = QMenu() a.setTitle(_('Remove Virtual Library')) - a.aboutToShow.connect(self.build_virtual_library_list) + a.aboutToShow.connect(partial(self.build_virtual_library_list, remove=True)) m.addMenu(a) m.addSeparator() @@ -259,10 +280,20 @@ class SearchRestrictionMixin(object): a = m.addAction(self.empty, self.no_restriction) a.triggered.connect(partial(self.apply_virtual_library, library='')) + a = m.addAction(self.empty, _('*current search')) + a.triggered.connect(partial(self.apply_virtual_library, library='*')) + + if self.search_based_vl_name: + a = m.addAction( + self.checked if db.data.get_base_restriction_name().startswith('*') + else self.empty, + self.search_based_vl_name) + a.triggered.connect(partial(self.apply_virtual_library, + library=self.search_based_vl_name)) + 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)) p = QPoint(0, self.virtual_library.height()) @@ -274,22 +305,41 @@ class SearchRestrictionMixin(object): if not library: db.data.set_base_restriction('') db.data.set_base_restriction_name('') + elif library == '*': + if not _build_full_search_string(self): + error_dialog(self, _('No search'), + _('There is no current search to use'), show=True) + return + + self.search_based_vl = _build_full_search_string(self) + db.data.set_base_restriction(self.search_based_vl) + self.search_based_vl_name = self._trim_restriction_name( + '*' + self.search_based_vl) + db.data.set_base_restriction_name(self.search_based_vl_name) + elif library == self.search_based_vl_name: + db.data.set_base_restriction(self.search_based_vl) + db.data.set_base_restriction_name(self.search_based_vl_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): + def build_virtual_library_list(self, remove=False): db = self.library_view.model().db virt_libs = db.prefs.get('virtual_libraries', {}) - m = self.rm_menu + if remove: + m = self.rm_menu + else: + m = self.edit_menu m.clear() def add_action(name, search): a = m.addAction(name) - a.setToolTip(search) - a.triggered.connect(partial(self.remove_vl_triggered, name=name)) + if remove: + a.triggered.connect(partial(self.remove_vl_triggered, name=name)) + else: + a.triggered.connect(partial(self.do_create_edit, editing=name)) for n in sorted(virt_libs.keys(), key=sort_key): add_action(n, virt_libs[n]) @@ -300,13 +350,19 @@ class SearchRestrictionMixin(object): 'the virtual library {0}').format(name), default_yes=False): return + self._remove_vl(name, reapply=True) + + def _remove_vl(self, name, reapply=True): 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: + if reapply and db.data.get_base_restriction_name() == name: self.apply_virtual_library('') + def _trim_restriction_name(self, name): + return name[0:MAX_VIRTUAL_LIBRARY_NAME_LENGTH].strip() + def build_search_restriction_list(self): m = self.ar_menu m.clear() @@ -324,6 +380,7 @@ class SearchRestrictionMixin(object): def add_action(txt, index): self.search_restriction.addItem(txt) + txt = self._trim_restriction_name(txt) if txt == current_restriction: a = m.addAction(self.checked, txt if txt else self.no_restriction) else: @@ -332,7 +389,7 @@ class SearchRestrictionMixin(object): action=a, index=index)) add_action('', 0) - add_action('*current search', 1) + add_action(_('*current search'), 1) dex = 2 if current_restriction_text: add_action(current_restriction_text, 2) @@ -372,7 +429,7 @@ class SearchRestrictionMixin(object): else: self.search_restriction.insertItem(2, s) self.search_restriction.setCurrentIndex(2) - self._apply_search_restriction(search, s) + self._apply_search_restriction(search, self._trim_restriction_name(s)) def apply_search_restriction(self, i): if i == 1: From 311d2cc8944c9925d1eeb03c4f7582f33e4b35b3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 12 Apr 2013 16:05:37 +0530 Subject: [PATCH 10/11] Add a checkbox to create and instead of or based VLs --- src/calibre/gui2/search_restriction_mixin.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 702f67f69d..8338baf6c4 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -8,7 +8,7 @@ from functools import partial from PyQt4.Qt import ( Qt, QMenu, QPoint, QIcon, QDialog, QGridLayout, QLabel, QLineEdit, - QDialogButtonBox, QSize, QVBoxLayout, QListWidget, QStringList) + QDialogButtonBox, QSize, QVBoxLayout, QListWidget, QStringList, QCheckBox) from calibre.gui2 import error_dialog, question_dialog from calibre.gui2.widgets import ComboBoxWithHelp @@ -31,6 +31,9 @@ class SelectNames(QDialog): # {{{ self._names.setSelectionMode(self._names.ExtendedSelection) l.addWidget(self._names) + self._and = QCheckBox(_('Match all selected %s names')%txt) + l.addWidget(self._and) + self.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.bb.accepted.connect(self.accept) self.bb.rejected.connect(self.reject) @@ -42,6 +45,11 @@ class SelectNames(QDialog): # {{{ def names(self): for item in self._names.selectedItems(): yield unicode(item.data(Qt.DisplayRole).toString()) + + @property + def match_type(self): + return ' and ' if self._and.isChecked() else ' or ' + # }}} MAX_VIRTUAL_LIBRARY_NAME_LENGTH = 40 @@ -154,7 +162,9 @@ class CreateVirtualLibrary(QDialog): # {{{ search = ['%s:"=%s"'%(prefix, x.replace('"', '\\"')) for x in d.names] if search: self.vl_name.setText(d.names.next()) - self.vl_text.setText(' or '.join(search)) + self.vl_text.setText(d.match_type.join(search)) + self.vl_text.setCursorPosition(0) + self.vl_name.setCursorPosition(0) def accept(self): n = unicode(self.vl_name.text()).strip() From b04b8d72a8c5d2bc8621086dd32d89ac8f86a984 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 12 Apr 2013 13:03:52 +0200 Subject: [PATCH 11/11] Only create a search-based VL if there is something in the search box. --- src/calibre/gui2/search_restriction_mixin.py | 22 +++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 702f67f69d..95b81b7841 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -188,8 +188,8 @@ class CreateVirtualLibrary(QDialog): # {{{ 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'), + error_dialog(self.gui, _('Invalid search'), + _('The search in the search box is not valid'), det_msg=e.msg, show=True) return @@ -306,15 +306,23 @@ class SearchRestrictionMixin(object): db.data.set_base_restriction('') db.data.set_base_restriction_name('') elif library == '*': - if not _build_full_search_string(self): + if not self.search.current_text: error_dialog(self, _('No search'), _('There is no current search to use'), show=True) return - self.search_based_vl = _build_full_search_string(self) - db.data.set_base_restriction(self.search_based_vl) - self.search_based_vl_name = self._trim_restriction_name( - '*' + self.search_based_vl) + txt = _build_full_search_string(self) + try: + db.data.search_getting_ids('', txt, use_virtual_library=False) + except ParseException as e: + error_dialog(self, _('Invalid search'), + _('The search in the search box is not valid'), + det_msg=e.msg, show=True) + return + + self.search_based_vl = txt + db.data.set_base_restriction(txt) + self.search_based_vl_name = self._trim_restriction_name('*' + txt) db.data.set_base_restriction_name(self.search_based_vl_name) elif library == self.search_based_vl_name: db.data.set_base_restriction(self.search_based_vl)