')
+
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