From eabbe6bc4625d0d337a27f52c91040a99a541a57 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 15 Feb 2011 08:55:11 +0000 Subject: [PATCH 1/2] Make selection and dragging behave more like other grid apps: drag only if already selected --- src/calibre/gui2/library/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 61161cd5e6..c62936a46f 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -551,8 +551,10 @@ class BooksView(QTableView): # {{{ return mods & Qt.ControlModifier or mods & Qt.ShiftModifier def mousePressEvent(self, event): - if event.button() == Qt.LeftButton and not self.event_has_mods(): - self.drag_start_pos = event.pos() + ep = event.pos() + if self.indexAt(ep) in self.selectionModel().selectedIndexes() and \ + event.button() == Qt.LeftButton and not self.event_has_mods(): + self.drag_start_pos = ep return QTableView.mousePressEvent(self, event) def mouseMoveEvent(self, event): From 1c72ea27b3f9de2bdcb6a781864a00bdb782c796 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 15 Feb 2011 14:27:47 +0000 Subject: [PATCH 2/2] Add user categories from grouped search terms. Move the group_search_term tweak to the search preferences dialog. --- resources/default_tweaks.py | 13 --- src/calibre/gui2/preferences/search.py | 149 ++++++++++++++++++++++++- src/calibre/gui2/preferences/search.ui | 117 +++++++++++++++++-- src/calibre/gui2/tag_view.py | 42 ++++--- src/calibre/library/caches.py | 11 ++ src/calibre/library/database2.py | 35 ++++-- src/calibre/library/field_metadata.py | 16 ++- 7 files changed, 332 insertions(+), 51 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index c22d24a6a7..47036a7b6d 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -245,19 +245,6 @@ sony_collection_name_template='{value}{category:| (|)}' sony_collection_sorting_rules = [] -#: Create search terms to apply a query across several built-in search terms. -# Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...} -# Example: create the term 'myseries' that when used as myseries:foo would -# search all of the search categories 'series', '#myseries', and '#myseries2': -# grouped_search_terms={'myseries':['series','#myseries', '#myseries2']} -# Example: two search terms 'a' and 'b' both that search 'tags' and '#mytags': -# grouped_search_terms={'a':['tags','#mytags'], 'b':['tags','#mytags']} -# Note: You cannot create a search term that is a duplicate of an existing term. -# Such duplicates will be silently ignored. Also note that search terms ignore -# case. 'MySearch' and 'mysearch' are the same term. -grouped_search_terms = {} - - #: Control how tags are applied when copying books to another library # Set this to True to ensure that tags in 'Tags to add when adding # a book' are added when copying books to another library diff --git a/src/calibre/gui2/preferences/search.py b/src/calibre/gui2/preferences/search.py index 749a7c8de0..4f8d78d62b 100644 --- a/src/calibre/gui2/preferences/search.py +++ b/src/calibre/gui2/preferences/search.py @@ -5,18 +5,20 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QApplication +from PyQt4.Qt import QApplication, QIcon from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \ CommaSeparatedList from calibre.gui2.preferences.search_ui import Ui_Form -from calibre.gui2 import config +from calibre.gui2 import config, error_dialog from calibre.utils.config import prefs class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): self.gui = gui + db = gui.library_view.model().db + self.db = db r = self.register @@ -24,11 +26,152 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('highlight_search_matches', config) r('limit_search_columns', prefs) r('limit_search_columns_to', prefs, setting=CommaSeparatedList) - fl = gui.library_view.model().db.field_metadata.get_search_terms() + fl = db.field_metadata.get_search_terms() self.opt_limit_search_columns_to.update_items_cache(fl) self.clear_history_button.clicked.connect(self.clear_histories) + self.gst_explanation.setText(_( + "Grouped search terms are search names that permit a query to automatically " + "search across more than one column. For example, if you create a grouped " + "search term 'myseries' with the value 'series, #myseries, #myseries2', " + "the query 'myseries:adhoc' will find the string 'adhoc' in any of the " + "columns 'series', '#myseries', and '#myseries2'. Enter the name of the " + "grouped search term in the drop-down box, enter the list of columns " + "to search in the value box, then push the Save button. " + "Notes: You cannot create a search term that is a duplicate of an existing " + "term or user category. Search terms are forced to lower case; 'MySearch' " + "and 'mysearch' are the same term.")) + + self.gst = db.prefs.get('grouped_search_terms', {}) + self.orig_gst_keys = self.gst.keys() + + del_icon = QIcon(I('trash.png')) + self.gst_delete_button.setIcon(del_icon) + fl = [] + for f in db.all_field_keys(): + fm = db.metadata_for_field(f) + if not fm['search_terms']: + continue + if not fm['is_category']: + continue + fl.append(f) + self.gst_value.update_items_cache(fl) + self.fill_gst_box(select=None) + + self.gst_delete_button.setEnabled(False) + self.gst_save_button.setEnabled(False) + self.gst_names.currentIndexChanged[int].connect(self.gst_index_changed) + self.gst_names.editTextChanged.connect(self.gst_text_changed) + self.gst_value.textChanged.connect(self.gst_text_changed) + self.gst_save_button.clicked.connect(self.gst_save_clicked) + self.gst_delete_button.clicked.connect(self.gst_delete_clicked) + self.gst_changed = False + + self.muc_explanation.setText(_( + "Add a grouped search term name to this box to automatically generate " + "a user category with the name of the search term. The user category will be " + "populated with all the items in the categories included in the grouped " + "search term. This permits you to see easily all the category items that " + "are in the fields contained in the grouped search term. Using the above " + "'myseries' example, the automatically-generated user category would contain " + "all the series mentioned in 'series', '#myseries1', and '#myseries2'. This " + "can be useful to check for duplications or to find which column contains " + "a particular item.")) + + if not db.prefs.get('grouped_search_make_user_categories', None): + db.prefs.set('grouped_search_make_user_categories', []) + r('grouped_search_make_user_categories', db.prefs, setting=CommaSeparatedList) + self.muc_changed = False + self.opt_grouped_search_make_user_categories.editingFinished.connect( + self.muc_box_changed) + + def muc_box_changed(self): + self.muc_changed = True + + def gst_save_clicked(self): + idx = self.gst_names.currentIndex() + name = icu_lower(unicode(self.gst_names.currentText())) + if not name: + return error_dialog(self.gui, _('Grouped Search Terms'), + _('The search term cannot be blank'), + show=True) + if idx != 0: + orig_name = unicode(self.gst_names.itemData(idx).toString()) + else: + orig_name = '' + if name != orig_name: + if name in self.db.field_metadata.get_search_terms() and \ + name not in self.orig_gst_keys: + return error_dialog(self.gui, _('Grouped Search Terms'), + _('That name is already used for a column or grouped search term'), + show=True) + if name in [icu_lower(p) for p in self.db.prefs.get('user_categories', {})]: + return error_dialog(self.gui, _('Grouped Search Terms'), + _('That name is already used for user category'), + show=True) + + val = [v.strip() for v in unicode(self.gst_value.text()).split(',') if v.strip()] + if not val: + return error_dialog(self.gui, _('Grouped Search Terms'), + _('The value box cannot be empty'), show=True) + + if orig_name and name != orig_name: + del self.gst[orig_name] + self.gst_changed = True + self.gst[name] = val + self.fill_gst_box(select=name) + self.changed_signal.emit() + + def gst_delete_clicked(self): + if self.gst_names.currentIndex() == 0: + return error_dialog(self.gui, _('Grouped Search Terms'), + _('The empty grouped search term cannot be deleted'), show=True) + name = unicode(self.gst_names.currentText()) + if name in self.gst: + del self.gst[name] + self.fill_gst_box(select='') + self.changed_signal.emit() + self.gst_changed = True + + def fill_gst_box(self, select=None): + terms = sorted(self.gst.keys()) + self.opt_grouped_search_make_user_categories.update_items_cache(terms) + self.gst_names.blockSignals(True) + self.gst_names.clear() + self.gst_names.addItem('', '') + for t in terms: + self.gst_names.addItem(t, t) + self.gst_names.blockSignals(False) + if select is not None: + if select == '': + self.gst_index_changed(0) + elif select in terms: + self.gst_names.setCurrentIndex(self.gst_names.findText(select)) + + def gst_text_changed(self): + self.gst_delete_button.setEnabled(False) + self.gst_save_button.setEnabled(True) + + def gst_index_changed(self, idx): + self.gst_delete_button.setEnabled(idx != 0) + self.gst_save_button.setEnabled(False) + self.gst_value.blockSignals(True) + if idx == 0: + self.gst_value.setText('') + else: + name = unicode(self.gst_names.itemData(idx).toString()) + self.gst_value.setText(','.join(self.gst[name])) + self.gst_value.blockSignals(False) + + def commit(self): + if self.gst_changed: + self.db.prefs.set('grouped_search_terms', self.gst) + self.db.field_metadata.add_grouped_search_terms(self.gst) + return ConfigWidgetBase.commit(self) + def refresh_gui(self, gui): + if self.muc_changed: + gui.tags_view.set_new_model() gui.search.search_as_you_type(config['search_as_you_type']) gui.library_view.model().set_highlight_only(config['highlight_search_matches']) gui.search.do_search() diff --git a/src/calibre/gui2/preferences/search.ui b/src/calibre/gui2/preferences/search.ui index 7d40f723ea..8a16a517c5 100644 --- a/src/calibre/gui2/preferences/search.ui +++ b/src/calibre/gui2/preferences/search.ui @@ -7,7 +7,7 @@ 0 0 670 - 392 + 391 @@ -77,7 +77,112 @@ + + + + Clear search histories from all over calibre. Including the book list, e-book viewer, fetch news dialog, etc. + + + Clear search &histories + + + + + + Grouped Search Terms + + + + + + TextLabel + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Grouped search terms: + + + + + + + true + + + 10 + + + + + + + Delete the current search term + + + ... + + + + + + + + + + Save the current search term. You can rename a search term by +changing the name then pressing Save. You can change the value +of a search term by changing the value box then pressing Save. + + + Save + + + + + + + + + TextLabel + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Make user categories: + + + + + + + + + + + + Qt::Vertical @@ -90,16 +195,6 @@ - - - - Clear search histories from all over calibre. Including the book list, e-book viewer, fetch news dialog, etc. - - - Clear search &histories - - - diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 3bc5d724ba..6b5de37bbe 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -466,10 +466,7 @@ class TagTreeItem(object): # {{{ icon_map[0] = data.icon self.tag, self.icon_state_map = data, list(map(QVariant, icon_map)) if tooltip: - if tooltip.endswith(':'): - self.tooltip = tooltip + ' ' - else: - self.tooltip = tooltip + ': ' + self.tooltip = tooltip + ' ' else: self.tooltip = '' @@ -589,11 +586,17 @@ class TagsModel(QAbstractItemModel): # {{{ # get_node_tree cannot return None here, because row_map is empty data = self.get_node_tree(config['sort_tags_by']) + gst = db.prefs.get('grouped_search_terms', {}) self.root_item = TagTreeItem() for i, r in enumerate(self.row_map): if self.hidden_categories and self.categories[i] in self.hidden_categories: continue - tt = _(u'The lookup/search name is "{0}"').format(r) + if r.startswith('@') and r[1:] in gst: + tt = _(u'The grouped search term name is "{0}"').format(r[1:]) + elif r == 'news': + tt = '' + else: + tt = _(u'The lookup/search name is "{0}"').format(r) TagTreeItem(parent=self.root_item, data=self.categories[i], category_icon=self.category_icon_map[r], @@ -735,6 +738,14 @@ class TagsModel(QAbstractItemModel): # {{{ self.row_map = [] self.categories = [] + # Get the categories + if self.search_restriction: + data = self.db.get_categories(sort=sort, + icon_map=self.category_icon_map, + ids=self.db.search('', return_matches=True)) + else: + data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map) + # Reconstruct the user categories, putting them into metadata self.db.field_metadata.remove_dynamic_categories() tb_cats = self.db.field_metadata @@ -746,17 +757,16 @@ class TagsModel(QAbstractItemModel): # {{{ except ValueError: import traceback traceback.print_exc() + + for cat in sorted(self.db.prefs.get('grouped_search_terms', {}), + key=sort_key): + if (u'@' + cat) in data: + tb_cats.add_user_category(label=u'@' + cat, name=cat) + self.db.data.change_search_locations(self.db.field_metadata.get_search_terms()) + if len(saved_searches().names()): tb_cats.add_search_category(label='search', name=_('Searches')) - # Now get the categories - if self.search_restriction: - data = self.db.get_categories(sort=sort, - icon_map=self.category_icon_map, - ids=self.db.search('', return_matches=True)) - else: - data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map) - if self.filter_categories_by: for category in data.keys(): data[category] = [t for t in data[category] @@ -767,6 +777,7 @@ class TagsModel(QAbstractItemModel): # {{{ if category in data: # The search category can come and go self.row_map.append(category) self.categories.append(tb_categories[category]['name']) + if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map): # A category has been added or removed. We must force a rebuild of # the model @@ -822,6 +833,7 @@ class TagsModel(QAbstractItemModel): # {{{ not self.db.field_metadata[r]['is_custom'] and \ not self.db.field_metadata[r]['kind'] == 'user' \ else False + tt = r if self.db.field_metadata[r]['kind'] == 'user' else None for idx,tag in enumerate(data[r]): if clear_rating: tag.avg_rating = None @@ -861,10 +873,10 @@ class TagsModel(QAbstractItemModel): # {{{ category_icon = category_node.icon, tooltip = None, category_key=category_node.category_key) - t = TagTreeItem(parent=sub_cat, data=tag, tooltip=r, + t = TagTreeItem(parent=sub_cat, data=tag, tooltip=tt, icon_map=self.icon_state_map) else: - t = TagTreeItem(parent=category, data=tag, tooltip=r, + t = TagTreeItem(parent=category, data=tag, tooltip=tt, icon_map=self.icon_state_map) self.endInsertRows() return True diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 70e1fec131..847a0493eb 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -433,6 +433,10 @@ class ResultCache(SearchQueryParser): # {{{ if len(candidates) == 0: return matches + if len(location) > 2 and location.startswith('@') and \ + location[1:] in self.db_prefs['grouped_search_terms']: + location = location[1:] + if query and query.strip(): # get metadata key associated with the search term. Eliminates # dealing with plurals and other aliases @@ -440,9 +444,16 @@ class ResultCache(SearchQueryParser): # {{{ # grouped search terms if isinstance(location, list): if allow_recursion: + if query.lower() == 'false': + invert = True + query = 'true' + else: + invert = False for loc in location: matches |= self.get_matches(loc, query, candidates=candidates, allow_recursion=False) + if invert: + matches = self.universal_set() - matches return matches raise ParseException(query, len(query), 'Recursive query group detected', self) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index b0497eb53e..09c5a09951 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -188,6 +188,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): migrate_preference('saved_searches', {}) set_saved_searches(self, 'saved_searches') + # migrate grouped_search_terms + if self.prefs.get('grouped_search_terms', None) is None: + try: + ogst = tweaks['grouped_search_terms'] + print 'migrating' + ngst = {} + for t in ogst: + ngst[icu_lower(t)] = ogst[t] + self.prefs.set('grouped_search_terms', ngst) + except: + pass + # Rename any user categories with names that differ only in case user_cats = self.prefs.get('user_categories', []) catmap = {} @@ -349,12 +361,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if len(saved_searches().names()): tb_cats.add_search_category(label='search', name=_('Searches')) - gst = tweaks['grouped_search_terms'] - for t in gst: - try: - self.field_metadata._add_search_terms_to_map(gst[t], [t]) - except ValueError: - traceback.print_exc() + self.field_metadata.add_grouped_search_terms( + self.prefs.get('grouped_search_terms', {})) self.book_on_device_func = None self.data = ResultCache(self.FIELD_MAP, self.field_metadata, db_prefs=self.prefs) @@ -1293,7 +1301,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # icon_map is not None if get_categories is to store an icon and # possibly a tooltip in the tag structure. icon = None - tooltip = '' + tooltip = '(' + category + ')' label = tb_cats.key_to_label(category) if icon_map: if not tb_cats.is_custom_field(category): @@ -1379,7 +1387,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): categories['formats'].sort(key = lambda x:x.name) #### Now do the user-defined categories. #### - user_categories = self.prefs['user_categories'] + user_categories = dict.copy(self.prefs['user_categories']) # We want to use same node in the user category as in the source # category. To do that, we need to find the original Tag node. There is @@ -1390,6 +1398,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for c in categories.keys(): taglist[c] = dict(map(lambda t:(t.name, t), categories[c])) + muc = self.prefs.get('grouped_search_make_user_categories', []) + gst = self.prefs.get('grouped_search_terms', {}) + for c in gst: + if c not in muc: + continue + user_categories[c] = [] + for sc in gst[c]: + if sc in categories.keys(): + for t in categories[sc]: + user_categories[c].append([t.name, sc, 0]) + for user_cat in sorted(user_categories.keys(), key=sort_key): items = [] for (name,label,ign) in user_categories[user_cat]: diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index d64ea54424..9b481a89d0 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -3,7 +3,7 @@ Created on 25 May 2010 @author: charles ''' -import copy +import copy, traceback from calibre.utils.ordered_dict import OrderedDict from calibre.utils.config import tweaks @@ -488,6 +488,20 @@ class FieldMetadata(dict): del self._search_term_map[k] del self._tb_cats[key] + def _remove_grouped_search_terms(self): + to_remove = [v for v in self._search_term_map + if isinstance(self._search_term_map[v], list)] + for v in to_remove: + del self._search_term_map[v] + + def add_grouped_search_terms(self, gst): + self._remove_grouped_search_terms() + for t in gst: + try: + self._add_search_terms_to_map(gst[t], [t]) + except ValueError: + traceback.print_exc() + def cc_series_index_column_for(self, key): return self._tb_cats[key]['rec_index'] + 1