diff --git a/src/calibre/gui2/tweak_book/search.py b/src/calibre/gui2/tweak_book/search.py new file mode 100644 index 0000000000..cc4aabf58e --- /dev/null +++ b/src/calibre/gui2/tweak_book/search.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2013, Kovid Goyal ' + +from PyQt4.Qt import ( + QWidget, QToolBar, Qt, QHBoxLayout, QSize, QIcon, QGridLayout, QLabel, + QPushButton, pyqtSignal, QComboBox, QCheckBox, QSizePolicy) + +import regex + +from calibre.gui2.widgets import HistoryLineEdit +from calibre.gui2.tweak_book import tprefs + +REGEX_FLAGS = regex.VERSION1 | regex.WORD | regex.FULLCASE | regex.MULTILINE | regex.UNICODE + +# The search panel {{{ + +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)) + +class SearchWidget(QWidget): + + DEFAULT_STATE = { + 'mode': 'normal', + 'where': 'current', + 'case_sensitive': False, + 'direction': 'down', + 'wrap': True, + 'dot_all': False, + } + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self.l = l = QGridLayout(self) + l.setContentsMargins(0, 0, 0, 0) + self.setLayout(l) + + self.fl = fl = QLabel(_('&Find:')) + fl.setAlignment(Qt.AlignRight | Qt.AlignCenter) + self.find_text = ft = HistoryLineEdit(self) + ft.initialize('tweak_book_find_edit') + fl.setBuddy(ft) + l.addWidget(fl, 0, 0) + l.addWidget(ft, 0, 1) + + self.rl = rl = QLabel(_('&Replace:')) + rl.setAlignment(Qt.AlignRight | Qt.AlignCenter) + self.replace_text = rt = HistoryLineEdit(self) + rt.initialize('tweak_book_replace_edit') + rl.setBuddy(rt) + l.addWidget(rl, 1, 0) + l.addWidget(rt, 1, 1) + l.setColumnStretch(1, 10) + + self.fb = fb = PushButton(_('&Find'), 'find', self) + self.rfb = rfb = PushButton(_('Replace a&nd Find'), 'replace-find', self) + self.rb = rb = PushButton(_('&Replace'), 'replace', self) + self.rab = rab = PushButton(_('Replace &all'), 'replace-all', self) + l.addWidget(fb, 0, 2) + l.addWidget(rfb, 0, 3) + l.addWidget(rb, 1, 2) + l.addWidget(rab, 1, 3) + + self.ml = ml = QLabel(_('&Mode:')) + self.ol = ol = QHBoxLayout() + ml.setAlignment(Qt.AlignRight | Qt.AlignCenter) + l.addWidget(ml, 2, 0) + l.addLayout(ol, 2, 1, 1, 3) + self.mode_box = mb = QComboBox(self) + mb.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + mb.addItems([_('Normal'), _('Regex')]) + mb.setToolTip('' + _( + '''Select how the search expression is interpreted +
+
Normal
+
The search expression is treated as normal text, calibre will look for the exact text.
+
Regex
+
The search expression is interpreted as a regular expression. See the User Manual for more help on using regular expressions.
+
''')) + ml.setBuddy(mb) + ol.addWidget(mb) + + self.where_box = wb = QComboBox(self) + wb.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + wb.addItems([_('Current file'), _('All text files'), _('All style files'), _('Selected files'), _('Selected text')]) + wb.setToolTip('' + _( + ''' + Where to search/replace: +
+
Current file
+
Search only inside the currently opened file
+
All text files
+
Search in all text (HTML) files
+
All style files
+
Search in all style (CSS) files
+
Selected files
+
Search in the files currently selected in the Files Browser
+
Selected text
+
Search only within the selected text in the currently opened file
+
''')) + ol.addWidget(wb) + + self.direction_box = db = QComboBox(self) + db.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + db.addItems([_('Down'), _('Up')]) + db.setToolTip('' + _( + ''' + Direction to search: +
+
Down
+
Search for the next match from your current position
+
Up
+
Search for the previous match from your current position
+
''')) + ol.addWidget(db) + + self.cs = cs = QCheckBox(_('&Case sensitive')) + cs.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + ol.addWidget(cs) + + self.wr = wr = QCheckBox(_('&Wrap')) + wr.setToolTip('

'+_('When searching reaches the end, wrap around to the beginning and continue the search')) + wr.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + ol.addWidget(wr) + + self.da = da = QCheckBox(_('&Dot all')) + da.setToolTip('

'+_("Make the '.' special character match any character at all, including a newline")) + da.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + ol.addWidget(da) + + self.mode_box.currentIndexChanged[int].connect(self.da.setVisible) + + ol.addStretch(10) + + @dynamic_property + def mode(self): + def fget(self): + return 'normal' if self.mode_box.currentIndex() == 0 else 'regex' + def fset(self, val): + self.mode_box.setCurrentIndex({'regex':1}.get(val, 0)) + self.da.setVisible(self.mode == 'regex') + return property(fget=fget, fset=fset) + + @dynamic_property + def find(self): + def fget(self): + return unicode(self.find_text.text()).strip() + def fset(self, val): + self.find_text.setText(val) + return property(fget=fget, fset=fset) + + @dynamic_property + def replace(self): + def fget(self): + return unicode(self.replace_text.text()).strip() + def fset(self, val): + self.replace_text.setText(val) + return property(fget=fget, fset=fset) + + @dynamic_property + def where(self): + wm = {0:'current', 1:'text', 2:'style', 3:'selected-files', 4:'selected-text'} + def fget(self): + return wm[self.where_box.currentIndex()] + def fset(self, val): + self.where_box.setCurrentIndex({v:k for k, v in wm.iteritems()}[val]) + return property(fget=fget, fset=fset) + + @dynamic_property + def case_sensitive(self): + def fget(self): + return self.cs.isChecked() + def fset(self, val): + self.cs.setChecked(bool(val)) + return property(fget=fget, fset=fset) + + @dynamic_property + def direction(self): + def fget(self): + return 'down' if self.direction_box.currentIndex() == 0 else 'up' + def fset(self, val): + self.direction_box.setCurrentIndex(1 if val == 'up' else 0) + return property(fget=fget, fset=fset) + + @dynamic_property + def wrap(self): + def fget(self): + return self.wr.isChecked() + def fset(self, val): + self.wr.setChecked(bool(val)) + return property(fget=fget, fset=fset) + + @dynamic_property + def dot_all(self): + def fget(self): + return self.da.isChecked() + def fset(self, val): + self.da.setChecked(bool(val)) + return property(fget=fget, fset=fset) + + @dynamic_property + def state(self): + def fget(self): + return {x:getattr(self, x) for x in self.DEFAULT_STATE} + def fset(self, val): + for x in self.DEFAULT_STATE: + if x in val: + setattr(self, x, val[x]) + return property(fget=fget, fset=fset) + + def restore_state(self): + self.state = tprefs.get('find-widget-state', self.DEFAULT_STATE) + + def save_state(self): + tprefs.set('find-widget-state', self.state) + +class SearchPanel(QWidget): + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self.l = l = QHBoxLayout() + self.setLayout(l) + l.setContentsMargins(0, 0, 0, 0) + self.t = t = QToolBar(self) + l.addWidget(t) + t.setOrientation(Qt.Vertical) + t.setIconSize(QSize(12, 12)) + t.setMovable(False) + t.setFloatable(False) + t.cl = ac = t.addAction(QIcon(I('window-close.png')), _('Close search panel')) + ac.triggered.connect(self.hide_panel) + self.widget = SearchWidget(self) + l.addWidget(self.widget) + self.restore_state, self.save_state = self.widget.restore_state, self.widget.save_state + + def hide_panel(self): + self.setVisible(False) + + def show_panel(self): + self.setVisible(True) + self.widget.find_text.setFocus(Qt.OtherFocusReason) +# }}} + diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index d69fbab793..05649bc332 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -20,6 +20,7 @@ from calibre.gui2.tweak_book.job import BlockingJob from calibre.gui2.tweak_book.boss import Boss from calibre.gui2.tweak_book.keyboard import KeyboardManager from calibre.gui2.tweak_book.preview import Preview +from calibre.gui2.tweak_book.search import SearchPanel class Central(QStackedWidget): @@ -56,6 +57,9 @@ class Central(QStackedWidget): self.modified_icon = QIcon(I('modified.png')) self.editor_tabs.currentChanged.connect(self.current_editor_changed) self.editor_tabs.tabCloseRequested.connect(self._close_requested) + self.search_panel = SearchPanel(self) + l.addWidget(self.search_panel) + self.restore_state() def _close_requested(self, index): editor = self.editor_tabs.widget(index) @@ -91,6 +95,17 @@ class Central(QStackedWidget): def current_editor(self): return self.editor_tabs.currentWidget() + def save_state(self): + tprefs.set('search-panel-visible', self.search_panel.isVisible()) + self.search_panel.save_state() + + def restore_state(self): + self.search_panel.setVisible(tprefs.get('search-panel-visible', False)) + self.search_panel.restore_state() + + def show_find(self): + self.search_panel.show_panel() + class Main(MainWindow): APP_NAME = _('Tweak Book') @@ -108,6 +123,9 @@ class Main(MainWindow): self.blocking_job = BlockingJob(self) self.keyboard = KeyboardManager() + self.central = Central(self) + self.setCentralWidget(self.central) + self.create_actions() self.create_toolbars() self.create_docks() @@ -120,9 +138,6 @@ class Main(MainWindow): f.setBold(True) self.status_bar.setFont(f) - self.central = Central(self) - self.setCentralWidget(self.central) - self.boss(self) g = QApplication.instance().desktop().availableGeometry(self) self.resize(g.width()-50, g.height()-50) @@ -199,6 +214,10 @@ class Main(MainWindow): 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')) + # Search actions + group = _('Search') + self.action_find = reg('search.png', _('&Find/Replace'), self.central.show_find, 'find-replace', ('Ctrl+F',), _('Find/Replace')) + def create_menubar(self): b = self.menuBar() @@ -303,6 +322,7 @@ class Main(MainWindow): def save_state(self): tprefs.set('main_window_geometry', bytearray(self.saveGeometry())) tprefs.set('main_window_state', bytearray(self.saveState(self.STATE_VERSION))) + self.central.save_state() def restore_state(self): geom = tprefs.get('main_window_geometry', None) @@ -311,6 +331,7 @@ class Main(MainWindow): state = tprefs.get('main_window_state', None) if state is not None: self.restoreState(state, self.STATE_VERSION) + self.central.restore_state() # We never want to start with the inspector showing self.inspector_dock.close()