mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
When searching, allow use of un-accented characters to match accented characters. This is dont in a language dependent way. So if your calibre interface language is set to English, n will match ñ, but if it is set to Spanish, it will not. Tag browser: When grouping by first letter, handle languages that have 'letters' made of more than one character.
This commit is contained in:
commit
e1c7404e02
@ -515,3 +515,13 @@ compile_gpm_templates = True
|
|||||||
# default_tweak_format = 'remember'
|
# default_tweak_format = 'remember'
|
||||||
default_tweak_format = None
|
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
|
||||||
|
|
||||||
|
@ -5,7 +5,9 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
import textwrap
|
import textwrap
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit
|
||||||
from calibre.gui2.preferences.tweaks_ui import Ui_Form
|
from calibre.gui2.preferences.tweaks_ui import Ui_Form
|
||||||
@ -18,8 +20,8 @@ from calibre.utils.search_query_parser import (ParseException,
|
|||||||
SearchQueryParser)
|
SearchQueryParser)
|
||||||
|
|
||||||
from PyQt4.Qt import (QAbstractListModel, Qt, QStyledItemDelegate, QStyle,
|
from PyQt4.Qt import (QAbstractListModel, Qt, QStyledItemDelegate, QStyle,
|
||||||
QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog,
|
QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog, QApplication,
|
||||||
QVBoxLayout, QPlainTextEdit, QLabel, QModelIndex)
|
QVBoxLayout, QPlainTextEdit, QLabel, QModelIndex, QMenu, QIcon)
|
||||||
|
|
||||||
ROOT = QModelIndex()
|
ROOT = QModelIndex()
|
||||||
|
|
||||||
@ -46,10 +48,12 @@ class Tweak(object): # {{{
|
|||||||
if self.doc:
|
if self.doc:
|
||||||
self.doc = translate(self.doc)
|
self.doc = translate(self.doc)
|
||||||
self.var_names = var_names
|
self.var_names = var_names
|
||||||
self.default_values = {}
|
if self.var_names:
|
||||||
|
self.doc = u"%s: %s\n\n%s"%(_('ID'), self.var_names[0], self.doc)
|
||||||
|
self.default_values = OrderedDict()
|
||||||
for x in var_names:
|
for x in var_names:
|
||||||
self.default_values[x] = defaults[x]
|
self.default_values[x] = defaults[x]
|
||||||
self.custom_values = {}
|
self.custom_values = OrderedDict()
|
||||||
for x in var_names:
|
for x in var_names:
|
||||||
if x in custom:
|
if x in custom:
|
||||||
self.custom_values[x] = custom[x]
|
self.custom_values[x] = custom[x]
|
||||||
@ -326,7 +330,27 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
self.search.initialize('tweaks_search_history', help_text=
|
self.search.initialize('tweaks_search_history', help_text=
|
||||||
_('Search for tweak'))
|
_('Search for tweak'))
|
||||||
self.search.search.connect(self.find)
|
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))
|
||||||
|
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):
|
def plugin_tweaks(self):
|
||||||
raw = self.tweaks.plugin_tweaks_string
|
raw = self.tweaks.plugin_tweaks_string
|
||||||
@ -442,7 +466,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from PyQt4.Qt import QApplication
|
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
#Tweaks()
|
#Tweaks()
|
||||||
#test_widget
|
#test_widget
|
||||||
|
@ -17,7 +17,7 @@ from PyQt4.Qt import (QAbstractItemModel, QIcon, QVariant, QFont, Qt,
|
|||||||
from calibre.gui2 import NONE, gprefs, config, error_dialog
|
from calibre.gui2 import NONE, gprefs, config, error_dialog
|
||||||
from calibre.library.database2 import Tag
|
from calibre.library.database2 import Tag
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.icu import sort_key, lower, strcmp
|
from calibre.utils.icu import sort_key, lower, strcmp, contractions
|
||||||
from calibre.library.field_metadata import TagsIcons, category_icon_map
|
from calibre.library.field_metadata import TagsIcons, category_icon_map
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.utils.formatter import EvalFormatter
|
from calibre.utils.formatter import EvalFormatter
|
||||||
@ -258,6 +258,16 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.hidden_categories.add(cat)
|
self.hidden_categories.add(cat)
|
||||||
db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
|
db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
|
||||||
|
|
||||||
|
conts = contractions()
|
||||||
|
if len(conts) == 0 or not tweaks['enable_multicharacters_in_tag_browser']:
|
||||||
|
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.db = db
|
||||||
self._run_rebuild()
|
self._run_rebuild()
|
||||||
self.endResetModel()
|
self.endResetModel()
|
||||||
@ -416,7 +426,14 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if not tag.sort:
|
if not tag.sort:
|
||||||
c = ' '
|
c = ' '
|
||||||
else:
|
else:
|
||||||
c = icu_upper(tag.sort[0])
|
if not self.do_contraction:
|
||||||
|
c = icu_upper(tag.sort)[0]
|
||||||
|
else:
|
||||||
|
v = icu_upper(tag.sort)
|
||||||
|
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:
|
if c not in chardict:
|
||||||
chardict[c] = [idx, idx]
|
chardict[c] = [idx, idx]
|
||||||
else:
|
else:
|
||||||
|
@ -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 import title_sort, author_to_author_sort
|
||||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
|
from calibre.utils.icu import primary_find
|
||||||
|
|
||||||
class MetadataBackup(Thread): # {{{
|
class MetadataBackup(Thread): # {{{
|
||||||
'''
|
'''
|
||||||
@ -147,9 +148,10 @@ def _match(query, value, matchkind):
|
|||||||
return True
|
return True
|
||||||
elif query == t:
|
elif query == t:
|
||||||
return True
|
return True
|
||||||
elif ((matchkind == REGEXP_MATCH and re.search(query, t, re.I|re.UNICODE)) or ### search unanchored
|
elif matchkind == REGEXP_MATCH:
|
||||||
(matchkind == CONTAINS_MATCH and query in t)):
|
return re.search(query, t, re.I|re.UNICODE)
|
||||||
return True
|
elif matchkind == CONTAINS_MATCH:
|
||||||
|
return primary_find(query, t)[0] != -1
|
||||||
except re.error:
|
except re.error:
|
||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
@ -227,9 +229,6 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
def __init__(self, FIELD_MAP, field_metadata, db_prefs=None):
|
def __init__(self, FIELD_MAP, field_metadata, db_prefs=None):
|
||||||
self.FIELD_MAP = FIELD_MAP
|
self.FIELD_MAP = FIELD_MAP
|
||||||
l = get_lang()
|
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.db_prefs = db_prefs
|
||||||
self.composites = {}
|
self.composites = {}
|
||||||
self.udc = get_udc()
|
self.udc = get_udc()
|
||||||
@ -279,15 +278,6 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
|
|
||||||
# Search functions {{{
|
# 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):
|
def universal_set(self):
|
||||||
return set([i[0] for i in self._data if i is not None])
|
return set([i[0] for i in self._data if i is not None])
|
||||||
|
|
||||||
@ -805,9 +795,6 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
if loc not in exclude_fields: # time for text matching
|
if loc not in exclude_fields: # time for text matching
|
||||||
if is_multiple_cols[loc] is not None:
|
if is_multiple_cols[loc] is not None:
|
||||||
vals = [v.strip() for v in item[loc].split(is_multiple_cols[loc])]
|
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:
|
else:
|
||||||
vals = [item[loc]] ### make into list to make _match happy
|
vals = [item[loc]] ### make into list to make _match happy
|
||||||
if _match(q, vals, matchkind):
|
if _match(q, vals, matchkind):
|
||||||
|
@ -272,42 +272,6 @@ icu_Collator_contractions(icu_Collator *self, PyObject *args, PyObject *kwargs)
|
|||||||
return Py_BuildValue("O", ans);
|
return Py_BuildValue("O", ans);
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
// Collator.span_contractions {{{
|
|
||||||
#ifndef __APPLE__
|
|
||||||
// uset_span is not available in the version of ICU on Apple's idiotic OS
|
|
||||||
static PyObject *
|
|
||||||
icu_Collator_span_contractions(icu_Collator *self, PyObject *args, PyObject *kwargs) {
|
|
||||||
int span_type;
|
|
||||||
UErrorCode status = U_ZERO_ERROR;
|
|
||||||
PyObject *str;
|
|
||||||
size_t slen = 0;
|
|
||||||
wchar_t *buf;
|
|
||||||
UChar *s;
|
|
||||||
int32_t ret;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "Ui", &str, &span_type)) return NULL;
|
|
||||||
|
|
||||||
if (self->contractions == NULL) {
|
|
||||||
self->contractions = uset_open(1, 0);
|
|
||||||
if (self->contractions == NULL) return PyErr_NoMemory();
|
|
||||||
self->contractions = ucol_getTailoredSet(self->collator, &status);
|
|
||||||
}
|
|
||||||
status = U_ZERO_ERROR;
|
|
||||||
|
|
||||||
slen = PyUnicode_GetSize(str);
|
|
||||||
buf = (wchar_t*)calloc(slen*4 + 2, sizeof(wchar_t));
|
|
||||||
s = (UChar*)calloc(slen*4 + 2, sizeof(UChar));
|
|
||||||
if (buf == NULL || s == NULL) return PyErr_NoMemory();
|
|
||||||
PyUnicode_AsWideChar((PyUnicodeObject*)str, buf, slen);
|
|
||||||
u_strFromWCS(s, slen*4+1, NULL, buf, slen, &status);
|
|
||||||
|
|
||||||
ret = uset_span(self->contractions, s, slen, span_type);
|
|
||||||
free(s); free(buf);
|
|
||||||
return Py_BuildValue("i", ret);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
// }}}
|
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
icu_Collator_clone(icu_Collator *self, PyObject *args, PyObject *kwargs);
|
icu_Collator_clone(icu_Collator *self, PyObject *args, PyObject *kwargs);
|
||||||
|
|
||||||
@ -328,12 +292,6 @@ static PyMethodDef icu_Collator_methods[] = {
|
|||||||
"contractions() -> returns the contractions defined for this collator."
|
"contractions() -> returns the contractions defined for this collator."
|
||||||
},
|
},
|
||||||
|
|
||||||
#ifndef __APPLE__
|
|
||||||
{"span_contractions", (PyCFunction)icu_Collator_span_contractions, METH_VARARGS,
|
|
||||||
"span_contractions(src, span_condition) -> returns the length of the initial substring according to span_condition in the set of contractions for this collator. Returns 0 if src does not fit the span_condition. The span_condition can be one of USET_SPAN_NOT_CONTAINED, USET_SPAN_CONTAINED, USET_SPAN_SIMPLE."
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
|
|
||||||
{"clone", (PyCFunction)icu_Collator_clone, METH_VARARGS,
|
{"clone", (PyCFunction)icu_Collator_clone, METH_VARARGS,
|
||||||
"clone() -> returns a clone of this collator."
|
"clone() -> returns a clone of this collator."
|
||||||
},
|
},
|
||||||
|
@ -110,20 +110,6 @@ def icu_contractions(collator):
|
|||||||
_cmap[collator] = ans
|
_cmap[collator] = ans
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def py_span_contractions(*args, **kwargs):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def icu_span_contractions(src, span_type=None, collator=None):
|
|
||||||
global _collator
|
|
||||||
if collator is None:
|
|
||||||
collator = _collator
|
|
||||||
if span_type is None:
|
|
||||||
span_type = _icu.USET_SPAN_SIMPLE
|
|
||||||
try:
|
|
||||||
return collator.span_contractions(src, span_type)
|
|
||||||
except TypeError:
|
|
||||||
return collator.span_contractions(unicode(src), span_type)
|
|
||||||
|
|
||||||
load_icu()
|
load_icu()
|
||||||
load_collator()
|
load_collator()
|
||||||
_icu_not_ok = _icu is None or _collator is None
|
_icu_not_ok = _icu is None or _collator is None
|
||||||
@ -164,9 +150,6 @@ find = (py_find if _icu_not_ok else partial(icu_find, _collator))
|
|||||||
contractions = ((lambda : {}) if _icu_not_ok else (partial(icu_contractions,
|
contractions = ((lambda : {}) if _icu_not_ok else (partial(icu_contractions,
|
||||||
_collator)))
|
_collator)))
|
||||||
|
|
||||||
span_contractions = (py_span_contractions if _icu_not_ok else
|
|
||||||
icu_span_contractions)
|
|
||||||
|
|
||||||
def primary_strcmp(a, b):
|
def primary_strcmp(a, b):
|
||||||
'strcmp that ignores case and accents on letters'
|
'strcmp that ignores case and accents on letters'
|
||||||
if _icu_not_ok:
|
if _icu_not_ok:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user