%PDF- %PDF-
Direktori : /usr/lib/calibre/calibre/gui2/tweak_book/ |
Current File : //usr/lib/calibre/calibre/gui2/tweak_book/text_search.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> from qt.core import ( QWidget, QHBoxLayout, QVBoxLayout, QLabel, QComboBox, QPushButton, QIcon, pyqtSignal, QFont, QCheckBox, QSizePolicy ) from lxml.etree import tostring from calibre import prepare_string_for_xml from calibre.gui2 import error_dialog from calibre.gui2.tweak_book import tprefs, editors, current_container from calibre.gui2.tweak_book.search import get_search_regex, InvalidRegex, initialize_search_request from calibre.gui2.tweak_book.widgets import BusyCursor from calibre.gui2.widgets2 import HistoryComboBox from polyglot.builtins import iteritems, error_message # UI {{{ class ModeBox(QComboBox): def __init__(self, parent): QComboBox.__init__(self, parent) self.addItems([_('Normal'), _('Regex')]) self.setToolTip('<style>dd {margin-bottom: 1.5ex}</style>' + _( '''Select how the search expression is interpreted <dl> <dt><b>Normal</b></dt> <dd>The search expression is treated as normal text, calibre will look for the exact text.</dd> <dt><b>Regex</b></dt> <dd>The search expression is interpreted as a regular expression. See the User Manual for more help on using regular expressions.</dd> </dl>''')) @property def mode(self): return ('normal', 'regex')[self.currentIndex()] @mode.setter def mode(self, val): self.setCurrentIndex({'regex':1}.get(val, 0)) class WhereBox(QComboBox): def __init__(self, parent, emphasize=False): QComboBox.__init__(self) self.addItems([_('Current file'), _('All text files'), _('Selected files'), _('Open files')]) self.setToolTip('<style>dd {margin-bottom: 1.5ex}</style>' + _( ''' Where to search/replace: <dl> <dt><b>Current file</b></dt> <dd>Search only inside the currently opened file</dd> <dt><b>All text files</b></dt> <dd>Search in all text (HTML) files</dd> <dt><b>Selected files</b></dt> <dd>Search in the files currently selected in the File browser</dd> <dt><b>Open files</b></dt> <dd>Search in the files currently open in the editor</dd> </dl>''')) self.emphasize = emphasize self.ofont = QFont(self.font()) if emphasize: f = self.emph_font = QFont(self.ofont) f.setBold(True), f.setItalic(True) self.setFont(f) @property def where(self): wm = {0:'current', 1:'text', 2:'selected', 3:'open'} return wm[self.currentIndex()] @where.setter def where(self, val): wm = {0:'current', 1:'text', 2:'selected', 3:'open'} self.setCurrentIndex({v:k for k, v in iteritems(wm)}[val]) def showPopup(self): # We do it like this so that the popup uses a normal font if self.emphasize: self.setFont(self.ofont) QComboBox.showPopup(self) def hidePopup(self): if self.emphasize: self.setFont(self.emph_font) QComboBox.hidePopup(self) class TextSearch(QWidget): find_text = pyqtSignal(object) def __init__(self, ui): QWidget.__init__(self, ui) self.l = l = QVBoxLayout(self) self.la = la = QLabel(_('&Find:')) self.find = ft = HistoryComboBox(self) ft.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) ft.initialize('tweak_book_text_search_history') la.setBuddy(ft) self.h = h = QHBoxLayout() h.addWidget(la), h.addWidget(ft), l.addLayout(h) self.h2 = h = QHBoxLayout() l.addLayout(h) self.mode = m = ModeBox(self) h.addWidget(m) self.where_box = wb = WhereBox(self) h.addWidget(wb) self.cs = cs = QCheckBox(_('&Case sensitive')) h.addWidget(cs) self.da = da = QCheckBox(_('&Dot all')) da.setToolTip('<p>'+_("Make the '.' special character match any character at all, including a newline")) h.addWidget(da) self.h3 = h = QHBoxLayout() l.addLayout(h) h.addStretch(10) self.next_button = b = QPushButton(QIcon(I('arrow-down.png')), _('&Next'), self) b.setToolTip(_('Find next match')) h.addWidget(b) connect_lambda(b.clicked, self, lambda self: self.do_search('down')) self.prev_button = b = QPushButton(QIcon(I('arrow-up.png')), _('&Previous'), self) b.setToolTip(_('Find previous match')) h.addWidget(b) connect_lambda(b.clicked, self, lambda self: self.do_search('up')) state = tprefs.get('text_search_widget_state') self.state = state or {} @property def state(self): return {'mode': self.mode.mode, 'where':self.where_box.where, 'case_sensitive':self.cs.isChecked(), 'dot_all':self.da.isChecked()} @state.setter def state(self, val): self.mode.mode = val.get('mode', 'normal') self.where_box.where = val.get('where', 'current') self.cs.setChecked(bool(val.get('case_sensitive'))) self.da.setChecked(bool(val.get('dot_all', True))) def save_state(self): tprefs['text_search_widget_state'] = self.state def do_search(self, direction='down'): state = self.state state['find'] = self.find.text() state['direction'] = direction self.find_text.emit(state) # }}} def file_matches_pattern(fname, pat): root = current_container().parsed(fname) if hasattr(root, 'xpath'): raw = tostring(root, method='text', encoding='unicode', with_tail=True) else: raw = current_container().raw_data(fname) return pat.search(raw) is not None def run_text_search(search, current_editor, current_editor_name, searchable_names, gui_parent, show_editor, edit_file): try: pat = get_search_regex(search) except InvalidRegex as e: return error_dialog(gui_parent, _('Invalid regex'), '<p>' + _( 'The regular expression you entered is invalid: <pre>{0}</pre>With error: {1}').format( prepare_string_for_xml(e.regex), error_message(e)), show=True) editor, where, files, do_all, marked = initialize_search_request(search, 'count', current_editor, current_editor_name, searchable_names) with BusyCursor(): if editor is not None: if editor.find_text(pat): return True if not files and editor.find_text(pat, wrap=True): return True for fname, syntax in iteritems(files): ed = editors.get(fname, None) if ed is not None: if ed.find_text(pat, complete=True): show_editor(fname) return True else: if file_matches_pattern(fname, pat): edit_file(fname, syntax) if editors[fname].find_text(pat, complete=True): return True msg = '<p>' + _('No matches were found for %s') % ('<pre style="font-style:italic">' + prepare_string_for_xml(search['find']) + '</pre>') return error_dialog(gui_parent, _('Not found'), msg, show=True) def find_text_in_chunks(pat, chunks): text = ''.join(x[0] for x in chunks) m = pat.search(text) if m is None: return -1, -1 start, after = m.span() def contains(clen, pt): return offset <= pt < offset + clen offset = 0 start_pos = end_pos = None for chunk, chunk_start in chunks: clen = len(chunk) if offset + clen < start: offset += clen continue # this chunk ends before start if start_pos is None: if contains(clen, start): start_pos = chunk_start + (start - offset) if start_pos is not None: if contains(clen, after-1): end_pos = chunk_start + (after - offset) return start_pos, end_pos offset += clen if offset > after: break # the next chunk starts after end return -1, -1