Add searching to the charmap dialog

This commit is contained in:
Kovid Goyal 2014-01-07 15:05:59 +05:30
parent 305379f27f
commit db71b29026
2 changed files with 99 additions and 8 deletions

View File

@ -6,17 +6,20 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import unicodedata, re import unicodedata, re, os, cPickle
from bisect import bisect from bisect import bisect
from functools import partial from functools import partial
from collections import defaultdict
from PyQt4.Qt import ( from PyQt4.Qt import (
QAbstractItemModel, QModelIndex, Qt, QVariant, pyqtSignal, QApplication, QAbstractItemModel, QModelIndex, Qt, QVariant, pyqtSignal, QApplication,
QTreeView, QSize, QGridLayout, QAbstractListModel, QListView, QPen, QMenu, QTreeView, QSize, QGridLayout, QAbstractListModel, QListView, QPen, QMenu,
QStyledItemDelegate, QSplitter, QLabel, QSizePolicy, QIcon, QMimeData) QStyledItemDelegate, QSplitter, QLabel, QSizePolicy, QIcon, QMimeData,
QPushButton, QToolButton)
from calibre.constants import ispy3, plugins from calibre.constants import ispy3, plugins, cache_dir
from calibre.gui2 import NONE from calibre.gui2 import NONE
from calibre.gui2.widgets2 import HistoryLineEdit2
from calibre.gui2.tweak_book import tprefs from calibre.gui2.tweak_book import tprefs
from calibre.gui2.tweak_book.editor.insert_resource import Dialog from calibre.gui2.tweak_book.editor.insert_resource import Dialog
@ -32,6 +35,58 @@ non_printing = {
0x206e: 'nads', 0x206f: 'nods', 0x20: 'sp', 0x7f: 'del', 0x2e3a: '2m', 0x2e3b: '3m', 0xad: 'shy', 0x206e: 'nads', 0x206f: 'nods', 0x20: 'sp', 0x7f: 'del', 0x2e3a: '2m', 0x2e3b: '3m', 0xad: 'shy',
} }
# Searching {{{
def load_search_index():
ver = 1 # Increment this when you make any changes to the index
name_map = {}
path = os.path.join(cache_dir(), 'unicode-name-index.pickle')
if os.path.exists(path):
with open(path, 'rb') as f:
name_map = cPickle.load(f)
if name_map.pop('calibre-nm-version:', -1) != ver:
name_map = {}
if not name_map:
name_map = defaultdict(set)
from calibre.constants import ispy3
if not ispy3:
chr = unichr
for x in xrange(1, 0x10FFFF + 1):
for word in unicodedata.name(chr(x), '').split():
name_map[word.lower()].add(x)
from calibre.ebooks.html_entities import html5_entities
for name, char in html5_entities.iteritems():
try:
name_map[name.lower()].add(ord(char))
except TypeError:
continue
name_map['nnbsp'].add(0x202F)
name_map['calibre-nm-version:'] = ver
cPickle.dump(dict(name_map), open(path, 'wb'), -1)
del name_map['calibre-nm-version:']
return name_map
_index = None
def search_for_chars(query, and_tokens=False):
global _index
if _index is None:
_index = load_search_index()
ans = set()
for token in query.split():
token = token.lower()
m = re.match(r'(?:[u]\+)([a-f0-9]+)', token)
if m is not None:
chars = {int(m.group(1), 16)}
else:
chars = _index.get(token, None)
if chars is not None:
if and_tokens:
ans &= chars
else:
ans |= chars
return sorted(ans)
# }}}
class CategoryModel(QAbstractItemModel): class CategoryModel(QAbstractItemModel):
def __init__(self, parent=None): def __init__(self, parent=None):
@ -452,10 +507,16 @@ class CategoryView(QTreeView):
else: else:
self.expand(index) self.expand(index)
def get_chars(self):
ans = self._model.get_range(self.currentIndex())
if ans is not None:
self.category_selected.emit(*ans)
def initialize(self): def initialize(self):
if not self.initialized: if not self.initialized:
self._model = m = CategoryModel(self) self._model = m = CategoryModel(self)
self.setModel(m) self.setModel(m)
self.setCurrentIndex(m.index(0, 0))
self.item_activated(m.index(0, 0)) self.item_activated(m.index(0, 0))
self._delegate = CategoryDelegate(self) self._delegate = CategoryDelegate(self)
self.setItemDelegate(self._delegate) self.setItemDelegate(self._delegate)
@ -668,8 +729,24 @@ class CharSelect(Dialog):
self.splitter = s = QSplitter(self) self.splitter = s = QSplitter(self)
s.setChildrenCollapsible(False) s.setChildrenCollapsible(False)
self.search = h = HistoryLineEdit2(self)
h.setToolTip(_(
'Search for unicode characters by using the English names or nicknames.'
' You can also search directly using a character code. For example, the following'
' searches will all yield the no-break space character: U+A0, nbsp, no-break'))
h.initialize('charmap_search')
h.setPlaceholderText(_('Search by name, nickname or character code'))
self.search_button = b = QPushButton(_('&Search'))
h.returnPressed.connect(self.do_search)
b.clicked.connect(self.do_search)
self.clear_button = cb = QToolButton(self)
cb.setIcon(QIcon(I('clear_left.png')))
cb.setText(_('Clear search'))
cb.clicked.connect(self.clear_search)
l.addWidget(h), l.addWidget(b, 0, 1), l.addWidget(cb, 0, 2)
self.category_view = CategoryView(self) self.category_view = CategoryView(self)
l.addWidget(s) l.addWidget(s, 1, 0, 1, 3)
self.char_view = CharView(self) self.char_view = CharView(self)
self.rearrange_button.toggled[bool].connect(self.set_allow_drag_and_drop) self.rearrange_button.toggled[bool].connect(self.set_allow_drag_and_drop)
self.category_view.category_selected.connect(self.show_chars) self.category_view.category_selected.connect(self.show_chars)
@ -679,14 +756,26 @@ class CharSelect(Dialog):
self.char_info = la = QLabel('\xa0') self.char_info = la = QLabel('\xa0')
la.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) la.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
l.addWidget(la) l.addWidget(la, 2, 0, 1, 3)
self.rearrange_msg = la = QLabel(_( self.rearrange_msg = la = QLabel(_(
'Drag and drop characters to re-arrange them. Click the re-arrange button again when you are done.')) 'Drag and drop characters to re-arrange them. Click the re-arrange button again when you are done.'))
la.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) la.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
la.setVisible(False) la.setVisible(False)
l.addWidget(la) l.addWidget(la, 3, 0, 1, 3)
l.addWidget(self.bb) l.addWidget(self.bb, 4, 0, 1, 3)
self.char_view.setFocus(Qt.OtherFocusReason)
def do_search(self):
text = unicode(self.search.text()).strip()
if not text:
return self.clear_search()
chars = search_for_chars(text)
self.show_chars(_('Search'), chars)
def clear_search(self):
self.search.clear()
self.category_view.get_chars()
def set_allow_drag_and_drop(self, on): def set_allow_drag_and_drop(self, on):
self.char_view.set_allow_drag_and_drop(on) self.char_view.set_allow_drag_and_drop(on)
@ -711,7 +800,7 @@ class CharSelect(Dialog):
def show_char_info(self, char_code): def show_char_info(self, char_code):
if char_code > 0: if char_code > 0:
category_name, subcategory_name, character_name = self.category_view.model().get_char_info(char_code) category_name, subcategory_name, character_name = self.category_view.model().get_char_info(char_code)
self.char_info.setText('%s - %s - %s (%04X)' % (category_name, subcategory_name, character_name, char_code)) self.char_info.setText('%s - %s - %s (U+%04X)' % (category_name, subcategory_name, character_name, char_code))
else: else:
self.char_info.clear() self.char_info.clear()

View File

@ -270,6 +270,7 @@ class TextEdit(QPlainTextEdit):
c.setPosition(start) c.setPosition(start)
c.setPosition(end, c.KeepAnchor) c.setPosition(end, c.KeepAnchor)
self.setTextCursor(c) self.setTextCursor(c)
self.centerCursor()
return True return True
def all_in_marked(self, pat, template=None): def all_in_marked(self, pat, template=None):
@ -323,6 +324,7 @@ class TextEdit(QPlainTextEdit):
c.setPosition(start) c.setPosition(start)
c.setPosition(end, c.KeepAnchor) c.setPosition(end, c.KeepAnchor)
self.setTextCursor(c) self.setTextCursor(c)
self.centerCursor()
return True return True
def replace(self, pat, template): def replace(self, pat, template):