From 62d5c99297b518dec3b65ba680db655ea7ed528f Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 4 Jul 2012 20:10:53 +0200 Subject: [PATCH 1/9] Make contains searches be more forgiving about accents. --- src/calibre/library/caches.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 191ac53f94..998c21c50c 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -20,6 +20,7 @@ from calibre.utils.localization import (canonicalize_lang, lang_map, get_udc, from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre import prints +from calibre.utils.icu import primary_find class MetadataBackup(Thread): # {{{ ''' @@ -147,9 +148,10 @@ def _match(query, value, matchkind): return True elif query == t: return True - elif ((matchkind == REGEXP_MATCH and re.search(query, t, re.I|re.UNICODE)) or ### search unanchored - (matchkind == CONTAINS_MATCH and query in t)): - return True + elif matchkind == REGEXP_MATCH: + return re.search(query, t, re.I|re.UNICODE) + elif matchkind == CONTAINS_MATCH: + return primary_find(query, t)[0] != -1 except re.error: pass return False From d52590bc8c99f83b93635dc1b5390c403d68d033 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 4 Jul 2012 20:17:52 +0200 Subject: [PATCH 2/9] Take out the ascii_author stuff. --- src/calibre/library/caches.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 998c21c50c..72e39e5651 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -229,9 +229,6 @@ class ResultCache(SearchQueryParser): # {{{ def __init__(self, FIELD_MAP, field_metadata, db_prefs=None): self.FIELD_MAP = FIELD_MAP l = get_lang() - asciize_author_names = l and l.lower() in ('en', 'eng') - if not asciize_author_names: - self.ascii_name = lambda x: False self.db_prefs = db_prefs self.composites = {} self.udc = get_udc() @@ -281,15 +278,6 @@ class ResultCache(SearchQueryParser): # {{{ # Search functions {{{ - def ascii_name(self, name): - try: - ans = self.udc.decode(name) - if ans == name: - ans = False - except: - ans = False - return ans - def universal_set(self): return set([i[0] for i in self._data if i is not None]) @@ -807,9 +795,6 @@ class ResultCache(SearchQueryParser): # {{{ if loc not in exclude_fields: # time for text matching if is_multiple_cols[loc] is not None: vals = [v.strip() for v in item[loc].split(is_multiple_cols[loc])] - if loc == au_loc: - vals += filter(None, map(self.ascii_name, - vals)) else: vals = [item[loc]] ### make into list to make _match happy if _match(q, vals, matchkind): From 3e42eeae66969ab9af85f6c9da4cc7aed20915be Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 5 Jul 2012 12:37:48 +0200 Subject: [PATCH 3/9] Add span contractions to tag browser to fix first letter display for languages with 'letters' made of more than one character --- src/calibre/gui2/tag_browser/model.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 8c6cb7d0eb..508d4474bc 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -17,11 +17,12 @@ from PyQt4.Qt import (QAbstractItemModel, QIcon, QVariant, QFont, Qt, from calibre.gui2 import NONE, gprefs, config, error_dialog from calibre.library.database2 import Tag from calibre.utils.config import tweaks -from calibre.utils.icu import sort_key, lower, strcmp +from calibre.utils.icu import sort_key, lower, strcmp, span_contractions from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.gui2.dialogs.confirm_delete import confirm from calibre.utils.formatter import EvalFormatter from calibre.utils.search_query_parser import saved_searches +from calibre.utils.localization import get_lang TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_plusplus': 2, 'mark_minus': 3, 'mark_minusminus': 4} @@ -353,6 +354,8 @@ class TagsModel(QAbstractItemModel): # {{{ self.category_nodes.append(node) self._create_node_tree(data, state_map) + langs_no_span_contractions = frozenset(["en", "it", "ru", "nl", "de", "fr"]) + def _create_node_tree(self, data, state_map): sort_by = config['sort_tags_by'] @@ -376,6 +379,10 @@ class TagsModel(QAbstractItemModel): # {{{ else: collapse_model = 'partition' collapse_template = tweaks['categories_collapsed_popularity_template'] + if get_lang() in self.langs_no_span_contractions: + use_span_contractions = False + else: + use_span_contractions = True def get_name_components(name): components = [t.strip() for t in name.split('.') if t.strip()] @@ -416,7 +423,15 @@ class TagsModel(QAbstractItemModel): # {{{ if not tag.sort: c = ' ' else: - c = icu_upper(tag.sort[0]) + if not use_span_contractions: + c = icu_upper(tag.sort)[0] + else: + v = icu_upper(tag.sort) + le = span_contractions(v) + if le > 0: + c = v[:le] + else: + c = v[0] if c not in chardict: chardict[c] = [idx, idx] else: From 093390884af1e105b9604d2474021819017bdbf7 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 5 Jul 2012 12:48:19 +0200 Subject: [PATCH 4/9] Add Spanish to the list of no-contraction languages in the tag browser. --- 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 508d4474bc..badaf3eff6 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -354,7 +354,7 @@ class TagsModel(QAbstractItemModel): # {{{ self.category_nodes.append(node) self._create_node_tree(data, state_map) - langs_no_span_contractions = frozenset(["en", "it", "ru", "nl", "de", "fr"]) + langs_no_span_contractions = frozenset(['en', 'it', 'ru', 'nl', 'de', 'fr', 'es']) def _create_node_tree(self, data, state_map): sort_by = config['sort_tags_by'] From c9e37072a912ca3c04b0a481a273637668d329ca Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 5 Jul 2012 13:44:18 +0200 Subject: [PATCH 5/9] Turn off spanning with a bool for the moment. --- 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 badaf3eff6..5b3996ac76 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -379,7 +379,7 @@ class TagsModel(QAbstractItemModel): # {{{ else: collapse_model = 'partition' collapse_template = tweaks['categories_collapsed_popularity_template'] - if get_lang() in self.langs_no_span_contractions: + if True or get_lang() in self.langs_no_span_contractions: use_span_contractions = False else: use_span_contractions = True From d5e7f83da4297f69094438d6fd515166aaeffe94 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 5 Jul 2012 17:44:36 +0200 Subject: [PATCH 6/9] Change tag browser to handle multi-character "characters" that some languages use when doing first letter categorization. --- src/calibre/gui2/tag_browser/model.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 5b3996ac76..0bc602622c 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -17,7 +17,7 @@ from PyQt4.Qt import (QAbstractItemModel, QIcon, QVariant, QFont, Qt, from calibre.gui2 import NONE, gprefs, config, error_dialog from calibre.library.database2 import Tag from calibre.utils.config import tweaks -from calibre.utils.icu import sort_key, lower, strcmp, span_contractions +from calibre.utils.icu import sort_key, lower, strcmp, contractions from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.gui2.dialogs.confirm_delete import confirm from calibre.utils.formatter import EvalFormatter @@ -259,6 +259,16 @@ class TagsModel(QAbstractItemModel): # {{{ self.hidden_categories.add(cat) db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories)) + conts = contractions() + if len(conts) == 0: + self.do_contraction = False + else: + self.do_contraction = True + nconts = set() + for s in conts: + nconts.add(icu_upper(s)) + self.contraction_set = frozenset(nconts) + self.db = db self._run_rebuild() self.endResetModel() @@ -379,10 +389,6 @@ class TagsModel(QAbstractItemModel): # {{{ else: collapse_model = 'partition' collapse_template = tweaks['categories_collapsed_popularity_template'] - if True or get_lang() in self.langs_no_span_contractions: - use_span_contractions = False - else: - use_span_contractions = True def get_name_components(name): components = [t.strip() for t in name.split('.') if t.strip()] @@ -423,15 +429,14 @@ class TagsModel(QAbstractItemModel): # {{{ if not tag.sort: c = ' ' else: - if not use_span_contractions: + if not self.do_contraction: c = icu_upper(tag.sort)[0] else: v = icu_upper(tag.sort) - le = span_contractions(v) - if le > 0: - c = v[:le] - else: - c = v[0] + c = v[0] + for s in self.contraction_set: + if len(s) > len(c) and v.startswith(s): + c = s if c not in chardict: chardict[c] = [idx, idx] else: From 3c664b75916ab07b76ae928a1539341e21f6ecb0 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 6 Jul 2012 09:43:58 +0200 Subject: [PATCH 7/9] 1) Add a tweak to turn on/off multi-character first-letter partioning in the tag browser 2) Change preferences.tweaks to add a "tweak number" to the tweak name string. This helps tell people which tweak to look at. To keep these valid, we should always add tweaks to the end. --- resources/default_tweaks.py | 10 ++++++++++ src/calibre/gui2/preferences/tweaks.py | 11 ++++++++--- src/calibre/gui2/tag_browser/model.py | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 10ce9b5c2c..cc798a09ba 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -515,3 +515,13 @@ compile_gpm_templates = True # default_tweak_format = 'remember' default_tweak_format = None +#: Enable multi-character first-letters in the tag browser +# Some languages have letters that can be represented by multiple characters. +# For example, Czech has a 'character' "ch" that sorts between "h" and "i". +# If this tweak is True, then the tag browser will take these characters into +# consideration when partitioning by first letter. +# Examples: +# enable_multicharacters_in_tag_browser = True +# enable_multicharacters_in_tag_browser = True +enable_multicharacters_in_tag_browser = True + diff --git a/src/calibre/gui2/preferences/tweaks.py b/src/calibre/gui2/preferences/tweaks.py index ebea8574a7..8169d799e0 100644 --- a/src/calibre/gui2/preferences/tweaks.py +++ b/src/calibre/gui2/preferences/tweaks.py @@ -39,7 +39,7 @@ class Delegate(QStyledItemDelegate): # {{{ class Tweak(object): # {{{ - def __init__(self, name, doc, var_names, defaults, custom): + def __init__(self, name, doc, var_names, defaults, custom, tweak_num): translate = _ self.name = translate(name) self.doc = doc.strip() @@ -53,6 +53,7 @@ class Tweak(object): # {{{ for x in var_names: if x in custom: self.custom_values[x] = custom[x] + self.tweak_num = tweak_num def __str__(self): ans = ['#: ' + self.name] @@ -92,6 +93,10 @@ class Tweak(object): # {{{ def update(self, varmap): self.custom_values.update(varmap) + @property + def name_with_position_number(self): + return "%d: %s"%(self.tweak_num, self.name) + # }}} class Tweaks(QAbstractListModel, SearchQueryParser): # {{{ @@ -113,7 +118,7 @@ class Tweaks(QAbstractListModel, SearchQueryParser): # {{{ except: return NONE if role == Qt.DisplayRole: - return textwrap.fill(tweak.name, 40) + return textwrap.fill(tweak.name_with_position_number, 40) if role == Qt.FontRole and tweak.is_customized: ans = QFont() ans.setBold(True) @@ -182,7 +187,7 @@ class Tweaks(QAbstractListModel, SearchQueryParser): # {{{ pos += 1 if not var_names: raise ValueError('Failed to find any variables for %r'%name) - self.tweaks.append(Tweak(name, doc, var_names, defaults, custom)) + self.tweaks.append(Tweak(name, doc, var_names, defaults, custom, len(self.tweaks)+1)) #print '\n\n', self.tweaks[-1] return pos diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 0bc602622c..59d80373b1 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -260,7 +260,7 @@ class TagsModel(QAbstractItemModel): # {{{ db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories)) conts = contractions() - if len(conts) == 0: + if len(conts) == 0 or not tweaks['enable_multicharacters_in_tag_browser']: self.do_contraction = False else: self.do_contraction = True From 921ff8cf64a720cd00270e4c27771b3c2d622b82 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 6 Jul 2012 10:32:28 +0200 Subject: [PATCH 8/9] In tweaks, use the first variable name in the tweak block as the unique identifier for the tweak. You can find a tweak by searching for characters in this unique ID. --- src/calibre/gui2/preferences/tweaks.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/preferences/tweaks.py b/src/calibre/gui2/preferences/tweaks.py index 8169d799e0..633874ac7c 100644 --- a/src/calibre/gui2/preferences/tweaks.py +++ b/src/calibre/gui2/preferences/tweaks.py @@ -39,13 +39,15 @@ class Delegate(QStyledItemDelegate): # {{{ class Tweak(object): # {{{ - def __init__(self, name, doc, var_names, defaults, custom, tweak_num): + def __init__(self, name, doc, var_names, defaults, custom): translate = _ self.name = translate(name) self.doc = doc.strip() if self.doc: self.doc = translate(self.doc) self.var_names = var_names + if len(self.var_names) > 0: + self.doc = "%s: %s\n\n%s"%(_('ID'), self.var_names[-1], self.doc) self.default_values = {} for x in var_names: self.default_values[x] = defaults[x] @@ -53,7 +55,6 @@ class Tweak(object): # {{{ for x in var_names: if x in custom: self.custom_values[x] = custom[x] - self.tweak_num = tweak_num def __str__(self): ans = ['#: ' + self.name] @@ -94,8 +95,10 @@ class Tweak(object): # {{{ self.custom_values.update(varmap) @property - def name_with_position_number(self): - return "%d: %s"%(self.tweak_num, self.name) + def name_with_first_var(self): + if len(self.var_names) > 0: + return "%s (%s:%s)"%(self.name, _('ID'), self.var_names[-1]) + return self.name # }}} @@ -118,7 +121,7 @@ class Tweaks(QAbstractListModel, SearchQueryParser): # {{{ except: return NONE if role == Qt.DisplayRole: - return textwrap.fill(tweak.name_with_position_number, 40) + return textwrap.fill(tweak.name_with_first_var, 40) if role == Qt.FontRole and tweak.is_customized: ans = QFont() ans.setBold(True) @@ -187,7 +190,7 @@ class Tweaks(QAbstractListModel, SearchQueryParser): # {{{ pos += 1 if not var_names: raise ValueError('Failed to find any variables for %r'%name) - self.tweaks.append(Tweak(name, doc, var_names, defaults, custom, len(self.tweaks)+1)) + self.tweaks.append(Tweak(name, doc, var_names, defaults, custom)) #print '\n\n', self.tweaks[-1] return pos From 824dbc01a484e3f6692a3a5ab2c2417d005222ba Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 6 Jul 2012 11:01:55 +0200 Subject: [PATCH 9/9] Add copy-to-clipboard context menu to preferences -> tweaks left pane. --- src/calibre/gui2/preferences/tweaks.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/preferences/tweaks.py b/src/calibre/gui2/preferences/tweaks.py index 633874ac7c..99493ca1be 100644 --- a/src/calibre/gui2/preferences/tweaks.py +++ b/src/calibre/gui2/preferences/tweaks.py @@ -5,6 +5,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +from functools import partial import textwrap from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit @@ -18,8 +19,8 @@ from calibre.utils.search_query_parser import (ParseException, SearchQueryParser) from PyQt4.Qt import (QAbstractListModel, Qt, QStyledItemDelegate, QStyle, - QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog, - QVBoxLayout, QPlainTextEdit, QLabel, QModelIndex) + QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog, QApplication, + QVBoxLayout, QPlainTextEdit, QLabel, QModelIndex, QMenu, QIcon) ROOT = QModelIndex() @@ -334,7 +335,27 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.search.initialize('tweaks_search_history', help_text= _('Search for tweak')) self.search.search.connect(self.find) + self.view.setContextMenuPolicy(Qt.CustomContextMenu) + self.view.customContextMenuRequested.connect(self.show_context_menu) + self.copy_icon = QIcon(I('edit-copy.png')) + def show_context_menu(self, point): + idx = self.tweaks_view.currentIndex() + if not idx.isValid(): + return True + tweak = self.tweaks.data(idx, Qt.UserRole) + self.context_menu = QMenu(self) + self.context_menu.addAction(self.copy_icon, + _('Copy to clipboard'), + partial(self.copy_item_to_clipboard, + val=tweak.name_with_first_var)) + self.context_menu.popup(self.mapToGlobal(point)) + return True + + def copy_item_to_clipboard(self, val): + cb = QApplication.clipboard(); + cb.clear() + cb.setText(val) def plugin_tweaks(self): raw = self.tweaks.plugin_tweaks_string