diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index bccc4cec13..f3c908235e 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -52,6 +52,7 @@ class Boss(QObject): fl.edit_file.connect(self.edit_file_requested) self.gui.central.current_editor_changed.connect(self.apply_current_editor_state) self.gui.central.close_requested.connect(self.editor_close_requested) + self.gui.central.search_panel.search_triggered.connect(self.search) def mkdtemp(self, prefix=''): self.container_count += 1 @@ -258,6 +259,50 @@ class Boss(QObject): self.update_global_history_actions() # }}} + def search(self, action, overrides=None): + ' Run a search/replace ' + sp = self.gui.central.search_panel + # Ensure the search panel is visible + sp.setVisible(True) + ed = self.gui.central.current_editor + name = None + for n, x in editors.iteritems(): + if x is ed: + name = n + break + state = sp.state + if overrides: + state.update(overrides) + searchable_names = self.gui.file_list.searchable_names + where = state['where'] + err = None + if name is None and where in {'current', 'selected-text'}: + err = _('No file is being edited.') + elif where == 'selected' and not searchable_names['selected']: + err = _('No files are selected in the Files Browser') + if not err and not state['find']: + err = _('No search query specified') + if err: + return error_dialog(self.gui, _('Cannot search'), err, show=True) + del err + if where == 'current': + files = [name] + editor = ed + elif where in {'styles', 'text', 'selected'}: + files = searchable_names[where] + if name in files: + editor = ed + else: + common = set(editors).intersection(set(files)) + if common: + name = next(x for x in files if x in common) + editor = editors[name] + self.gui.central.show_editor(editor) + else: + pass # TODO: Find the first name with a match and open its editor + else: + pass # selected text TODO: Implement this + def save_book(self): c = current_container() for name, ed in editors.iteritems(): diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index 404b9128de..4260a12692 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -338,6 +338,26 @@ class FileList(QTreeWidget): syntax = {'text':'html', 'styles':'css'}.get(category, None) self.edit_file.emit(name, syntax, mime) + @property + def all_files(self): + return (category.child(i) for category in self.categories.itervalues() for i in xrange(category.childCount())) + + @property + def searchable_names(self): + ans = {'text':[], 'styles':[], 'selected':[]} + for item in self.all_files: + category = unicode(item.data(0, CATEGORY_ROLE).toString()) + mime = unicode(item.data(0, MIME_ROLE).toString()) + name = unicode(item.data(0, NAME_ROLE).toString()) + ok = category in {'text', 'styles'} + if ok: + ans[category].append(name) + if not ok and category == 'misc': + ok = mime in {guess_type('a.'+x) for x in ('opf', 'ncx', 'txt', 'xml')} + if ok and item.isSelected(): + ans['selected'].append(name) + return ans + class FileListWidget(QWidget): delete_requested = pyqtSignal(object, object) @@ -359,4 +379,7 @@ class FileListWidget(QWidget): def build(self, container, preserve_state=True): self.file_list.build(container, preserve_state=preserve_state) + @property + def searchable_names(self): + return self.file_list.searchable_names diff --git a/src/calibre/gui2/tweak_book/search.py b/src/calibre/gui2/tweak_book/search.py index cc4aabf58e..0e1e6b380d 100644 --- a/src/calibre/gui2/tweak_book/search.py +++ b/src/calibre/gui2/tweak_book/search.py @@ -21,11 +21,9 @@ REGEX_FLAGS = regex.VERSION1 | regex.WORD | regex.FULLCASE | regex.MULTILINE | r class PushButton(QPushButton): - triggered = pyqtSignal(object) - def __init__(self, text, action, parent): QPushButton.__init__(self, text, parent) - self.clicked.connect(lambda : self.triggered.emit(action)) + self.clicked.connect(lambda : parent.search_triggered.emit(action)) class SearchWidget(QWidget): @@ -38,6 +36,8 @@ class SearchWidget(QWidget): 'dot_all': False, } + search_triggered = pyqtSignal(object) + def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QGridLayout(self) @@ -168,7 +168,7 @@ class SearchWidget(QWidget): @dynamic_property def where(self): - wm = {0:'current', 1:'text', 2:'style', 3:'selected-files', 4:'selected-text'} + wm = {0:'current', 1:'text', 2:'styles', 3:'selected', 4:'selected-text'} def fget(self): return wm[self.where_box.currentIndex()] def fset(self, val): @@ -223,8 +223,12 @@ class SearchWidget(QWidget): def save_state(self): tprefs.set('find-widget-state', self.state) +# }}} + class SearchPanel(QWidget): + search_triggered = pyqtSignal(object) + def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QHBoxLayout() @@ -241,6 +245,7 @@ class SearchPanel(QWidget): self.widget = SearchWidget(self) l.addWidget(self.widget) self.restore_state, self.save_state = self.widget.restore_state, self.widget.save_state + self.widget.search_triggered.connect(self.search_triggered) def hide_panel(self): self.setVisible(False) @@ -248,5 +253,11 @@ class SearchPanel(QWidget): def show_panel(self): self.setVisible(True) self.widget.find_text.setFocus(Qt.OtherFocusReason) -# }}} + + @property + def state(self): + ans = self.widget.state + ans['find'] = self.widget.find + ans['replace'] = self.widget.replace + return ans diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 05649bc332..7075b0caa0 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -154,7 +154,7 @@ class Main(MainWindow): group = _('Global Actions') def reg(icon, text, target, sid, keys, description): - ac = actions[sid] = QAction(QIcon(I(icon)), text, self) + ac = actions[sid] = QAction(QIcon(I(icon)), text, self) if icon else QAction(text, self) ac.setObjectName('action-' + sid) if target is not None: ac.triggered.connect(target) @@ -212,11 +212,27 @@ class Main(MainWindow): # Preview actions group = _('Preview') self.action_auto_reload_preview = reg('auto-reload.png', _('Auto reload preview'), None, 'auto-reload-preview', (), _('Auto reload preview')) - self.action_reload_preview = reg('view-refresh.png', _('Refresh preview'), None, 'reload-preview', ('F5', 'Ctrl+R'), _('Refresh preview')) + self.action_reload_preview = reg('view-refresh.png', _('Refresh preview'), None, 'reload-preview', ('F5',), _('Refresh preview')) # Search actions group = _('Search') - self.action_find = reg('search.png', _('&Find/Replace'), self.central.show_find, 'find-replace', ('Ctrl+F',), _('Find/Replace')) + self.action_find = reg('search.png', _('&Find/Replace'), self.central.show_find, 'find-replace', ('Ctrl+F',), _('Show the Find/Replace panel')) + def sreg(name, text, action, overrides={}, keys=(), description=None, icon=None): + return reg(icon, text, partial(self.boss.search, action, overrides), name, keys, description or text.replace('&', '')) + self.action_find_next = sreg('find-next', _('Find &Next'), + 'find', {'direction':'down'}, ('F3', 'Ctrl+G'), _('Find next match')) + self.action_find_previous = sreg('find-previous', _('Find &Previous'), + 'find', {'direction':'up'}, ('Shift+F3', 'Shift+Ctrl+G'), _('Find previous match')) + self.action_replace = sreg('replace', _('Replace'), + 'replace', keys=('Ctrl+R'), description=_('Replace current match')) + self.action_replace_next = sreg('replace-next', _('&Replace and find next'), + 'replace-find', {'direction':'down'}, ('Ctrl+]'), _('Replace current match and find next')) + self.action_replace_previous = sreg('replace-previous', _('R&eplace and find previous'), + 'replace-find', {'direction':'up'}, ('Ctrl+['), _('Replace current match and find previous')) + self.action_replace_all = sreg('replace-all', _('Replace &all'), + 'replace-all', keys=('Ctrl+A'), description=_('Replace all matches')) + self.action_count = sreg('count-matches', _('&Count all'), + 'count', keys=('Ctrl+N'), description=_('Count number of matches')) def create_menubar(self): b = self.menuBar() @@ -252,6 +268,20 @@ class Main(MainWindow): elif name.endswith('-bar'): t.addAction(ac) + e = b.addMenu(_('&Search')) + a = e.addAction + a(self.action_find) + e.addSeparator() + a(self.action_find_next) + a(self.action_find_previous) + e.addSeparator() + a(self.action_replace) + a(self.action_replace_next) + a(self.action_replace_previous) + a(self.action_replace_all) + e.addSeparator() + a(self.action_count) + def create_toolbars(self): def create(text, name): name += '-bar'