From 18477b56d998a354c21c5f1cb187f27f02aff9fb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 25 Sep 2014 18:33:21 +0530 Subject: [PATCH] Edit Book: Re-implement the saved searches pop-up as a dockable window --- src/calibre/gui2/tweak_book/boss.py | 6 +- src/calibre/gui2/tweak_book/search.py | 275 +++++++++++++++++--------- src/calibre/gui2/tweak_book/ui.py | 6 + 3 files changed, 194 insertions(+), 93 deletions(-) diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 6a3a07f606..94e7b8190c 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -837,16 +837,14 @@ class Boss(QObject): error_dialog(self.gui, _('Not found'), _( 'No file with the name %s was found in the book') % target, show=True) - def saved_searches(self): - self.gui.saved_searches.show(), self.gui.saved_searches.raise_() - def save_search(self): state = self.gui.central.search_panel.state self.show_saved_searches() self.gui.saved_searches.add_predefined_search(state) def show_saved_searches(self): - self.gui.saved_searches.show(), self.gui.saved_searches.raise_() + self.gui.saved_searches_dock.show() + saved_searches = show_saved_searches def run_saved_searches(self, searches, action): ed = self.gui.central.current_editor diff --git a/src/calibre/gui2/tweak_book/search.py b/src/calibre/gui2/tweak_book/search.py index 8da19a7eec..4dea52a4a0 100644 --- a/src/calibre/gui2/tweak_book/search.py +++ b/src/calibre/gui2/tweak_book/search.py @@ -10,12 +10,11 @@ import json, copy from functools import partial from collections import OrderedDict -import sip from PyQt5.Qt import ( QWidget, QToolBar, Qt, QHBoxLayout, QSize, QIcon, QGridLayout, QLabel, QTimer, QPushButton, pyqtSignal, QComboBox, QCheckBox, QSizePolicy, QVBoxLayout, QFont, QLineEdit, QToolButton, QListView, QFrame, QApplication, QStyledItemDelegate, - QAbstractListModel, QFormLayout, QModelIndex, QMenu, QItemSelection) + QAbstractListModel, QPlainTextEdit, QModelIndex, QMenu, QItemSelection, QStackedLayout) import regex @@ -24,7 +23,7 @@ from calibre.gui2 import error_dialog, info_dialog, choose_files, choose_save_fi from calibre.gui2.dialogs.message_box import MessageBox from calibre.gui2.widgets2 import HistoryComboBox from calibre.gui2.tweak_book import tprefs, editors, current_container -from calibre.gui2.tweak_book.widgets import Dialog, BusyCursor +from calibre.gui2.tweak_book.widgets import BusyCursor from calibre.utils.icu import primary_contains @@ -453,80 +452,134 @@ class SearchesModel(QAbstractListModel): del self.searches[idx] tprefs['saved_searches'] = self.searches -class EditSearch(Dialog): # {{{ +class EditSearch(QFrame): # {{{ - def __init__(self, search=None, search_index=-1, parent=None, state=None): + done = pyqtSignal(object) + + def __init__(self, parent=None): + QFrame.__init__(self, parent) + self.setFrameShape(self.StyledPanel) + self.search_index = -1 + self.search = {} + self.original_name = None + + self.l = l = QVBoxLayout(self) + self.title = la = QLabel('

Edit...') + self.ht = h = QHBoxLayout() + l.addLayout(h) + h.addWidget(la) + self.cb = cb = QToolButton(self) + cb.setIcon(QIcon(I('window-close.png'))) + cb.setToolTip(_('Abort editing of search')) + h.addWidget(cb) + cb.clicked.connect(self.abort_editing) + self.search_name = n = QLineEdit('', self) + n.setPlaceholderText(_('The name with which to save this search')) + self.la1 = la = QLabel(_('&Name:')) + la.setBuddy(n) + self.h3 = h = QHBoxLayout() + h.addWidget(la), h.addWidget(n) + l.addLayout(h) + + self.find = f = QPlainTextEdit('', self) + self.la2 = la = QLabel(_('&Find:')) + la.setBuddy(f) + l.addWidget(la), l.addWidget(f) + + self.replace = r = QPlainTextEdit('', self) + self.la3 = la = QLabel(_('&Replace:')) + la.setBuddy(r) + l.addWidget(la), l.addWidget(r) + + self.case_sensitive = c = QCheckBox(_('Case sensitive')) + self.h = h = QHBoxLayout() + l.addLayout(h) + h.addWidget(c) + + self.dot_all = d = QCheckBox(_('Dot matches all')) + h.addWidget(d), h.addStretch(2) + + self.h2 = h = QHBoxLayout() + l.addLayout(h) + self.mode_box = m = ModeBox(self) + m.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.la4 = la = QLabel(_('&Mode:')) + la.setBuddy(m) + h.addWidget(la), h.addWidget(m), h.addStretch(2) + + self.done_button = b = QPushButton(QIcon(I('ok.png')), _('&Done')) + b.setToolTip(_('Finish editing of search')) + h.addWidget(b) + b.clicked.connect(self.emit_done) + + def show_search(self, search=None, search_index=-1, state=None): + self.title.setText('

' + (_('Add search') if search_index == -1 else _('Edit search'))) self.search = search or {} self.original_name = self.search.get('name', None) self.search_index = search_index - Dialog.__init__(self, _('Edit search'), 'edit-saved-search', parent=parent) + + self.search_name.setText(self.search.get('name', '')) + self.find.setPlainText(self.search.get('find', '')) + self.replace.setPlainText(self.search.get('replace', '')) + self.case_sensitive.setChecked(self.search.get('case_sensitive', SearchWidget.DEFAULT_STATE['case_sensitive'])) + self.dot_all.setChecked(self.search.get('dot_all', SearchWidget.DEFAULT_STATE['dot_all'])) + self.mode_box.mode = self.search.get('mode', 'regex') + if state is not None: - self.find.setText(state['find']) - self.replace.setText(state['replace']) + self.find.setPlainText(state['find']) + self.replace.setPlainText(state['replace']) self.case_sensitive.setChecked(state['case_sensitive']) self.dot_all.setChecked(state['dot_all']) self.mode_box.mode = state.get('mode') - def show(self): - Dialog.show(self), self.raise_(), self.activateWindow() + def emit_done(self): + self.done.emit(True) - def sizeHint(self): - ans = Dialog.sizeHint(self) - ans.setWidth(600) - return ans + def keyPressEvent(self, ev): + if ev.key() == Qt.Key_Escape: + self.abort_editing() + ev.accept() + return + return QFrame.keyPressEvent(self, ev) - def setup_ui(self): - self.l = l = QFormLayout(self) - l.setFieldGrowthPolicy(l.AllNonFixedFieldsGrow) - self.setLayout(l) + def abort_editing(self): + self.done.emit(False) - self.search_name = n = QLineEdit(self.search.get('name', ''), self) - n.setPlaceholderText(_('The name with which to save this search')) - l.addRow(_('&Name:'), n) + @property + def current_search(self): + search = self.search.copy() + f = unicode(self.find.toPlainText()) + search['find'] = f + r = unicode(self.replace.toPlainText()) + search['replace'] = r + search['dot_all'] = bool(self.dot_all.isChecked()) + search['case_sensitive'] = bool(self.case_sensitive.isChecked()) + search['mode'] = self.mode_box.mode + return search - self.find = f = QLineEdit(self.search.get('find', ''), self) - f.setPlaceholderText(_('The expression to search for')) - l.addRow(_('&Find:'), f) - - self.replace = r = QLineEdit(self.search.get('replace', ''), self) - r.setPlaceholderText(_('The replace expression')) - l.addRow(_('&Replace:'), r) - - self.case_sensitive = c = QCheckBox(_('Case sensitive')) - c.setChecked(self.search.get('case_sensitive', SearchWidget.DEFAULT_STATE['case_sensitive'])) - l.addRow(c) - - self.dot_all = d = QCheckBox(_('Dot matches all')) - d.setChecked(self.search.get('dot_all', SearchWidget.DEFAULT_STATE['dot_all'])) - l.addRow(d) - - self.mode_box = m = ModeBox(self) - self.mode_box.mode = self.search.get('mode', 'regex') - m.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - l.addRow(_('&Mode:'), m) - - l.addRow(self.bb) - - def accept(self): + def save_changes(self): searches = tprefs['saved_searches'] all_names = {x['name'] for x in searches} - {self.original_name} - n = unicode(self.search_name.text()).strip() - search = self.search + n = self.search_name.text().strip() if not n: - return error_dialog(self, _('Must specify name'), _( + error_dialog(self, _('Must specify name'), _( 'You must specify a search name'), show=True) + return False if n in all_names: - return error_dialog(self, _('Name exists'), _( + error_dialog(self, _('Name exists'), _( 'Another search with the name %s already exists') % n, show=True) + return False + search = self.search search['name'] = n - f = unicode(self.find.text()) + f = unicode(self.find.toPlainText()) if not f: - return error_dialog(self, _('Must specify find'), _( + error_dialog(self, _('Must specify find'), _( 'You must specify a find expression'), show=True) + return False search['find'] = f - r = unicode(self.replace.text()) + r = unicode(self.replace.toPlainText()) search['replace'] = r search['dot_all'] = bool(self.dot_all.isChecked()) @@ -538,8 +591,8 @@ class EditSearch(Dialog): # {{{ else: searches[self.search_index] = search tprefs.set('saved_searches', searches) + return True - Dialog.accept(self) # }}} class SearchDelegate(QStyledItemDelegate): @@ -549,15 +602,13 @@ class SearchDelegate(QStyledItemDelegate): ans.setHeight(ans.height() + 4) return ans -class SavedSearches(Dialog): +class SavedSearches(QWidget): run_saved_searches = pyqtSignal(object, object) def __init__(self, parent=None): - Dialog.__init__(self, _('Saved Searches'), 'saved-searches', parent=parent) - - def sizeHint(self): - return QSize(800, 675) + QWidget.__init__(self, parent) + self.setup_ui() def setup_ui(self): self.l = l = QVBoxLayout(self) @@ -576,6 +627,15 @@ class SavedSearches(Dialog): self.h2 = h = QHBoxLayout() self.searches = searches = QListView(self) + self.stack = stack = QStackedLayout() + self.main_widget = mw = QWidget(self) + stack.addWidget(mw) + self.edit_search_widget = es = EditSearch(mw) + stack.addWidget(es) + es.done.connect(self.search_editing_done) + mw.v = QVBoxLayout(mw) + mw.v.setContentsMargins(0, 0, 0, 0) + mw.v.addWidget(searches) searches.doubleClicked.connect(self.edit_search) self.model = SearchesModel(self.searches) self.model.dataChanged.connect(self.show_details) @@ -585,10 +645,11 @@ class SavedSearches(Dialog): self.delegate = SearchDelegate(searches) searches.setItemDelegate(self.delegate) searches.setAlternatingRowColors(True) - h.addWidget(searches, stretch=10) + h.addLayout(stack, stretch=10) self.v = v = QVBoxLayout() h.addLayout(v) l.addLayout(h) + stack.currentChanged.connect(self.stack_current_changed) def pb(text, tooltip=None): b = QPushButton(text, self) @@ -627,11 +688,11 @@ class SavedSearches(Dialog): b.clicked.connect(self.edit_search) v.addWidget(b) - self.eb = b = pb(_('Re&move search'), _('Remove the currently selected searches')) + self.rb = b = pb(_('Re&move search'), _('Remove the currently selected searches')) b.clicked.connect(self.remove_search) v.addWidget(b) - self.eb = b = pb(_('&Add search'), _('Add a new saved search')) + self.ab = b = pb(_('&Add search'), _('Add a new saved search')) b.clicked.connect(self.add_search) v.addWidget(b) @@ -651,17 +712,21 @@ class SavedSearches(Dialog): self.wr.setChecked(SearchWidget.DEFAULT_STATE['wrap']) v.addWidget(wr) + self.d3 = d = QFrame(self) + d.setFrameStyle(QFrame.HLine) + v.addWidget(d) + self.description = d = QLabel(' \n \n ') d.setTextFormat(Qt.PlainText) d.setWordWrap(True) - l.addWidget(d) + mw.v.addWidget(d) - l.addWidget(self.bb) - self.bb.clear() - self.bb.addButton(self.bb.Close) - self.ib = b = self.bb.addButton(_('&Import'), self.bb.ActionRole) + self.ib = b = pb(_('&Import'), _('Import saved searches')) b.clicked.connect(self.import_searches) - self.eb = b = self.bb.addButton(_('E&xport'), self.bb.ActionRole) + v.addWidget(b) + + self.eb2 = b = pb(_('E&xport'), _('Export saved searches')) + v.addWidget(b) self.em = m = QMenu(_('Export')) m.addAction(_('Export All'), lambda : QTimer.singleShot(0, partial(self.export_searches, all=True))) m.addAction(_('Export Selected'), lambda : QTimer.singleShot(0, partial(self.export_searches, all=False))) @@ -669,6 +734,11 @@ class SavedSearches(Dialog): self.searches.setFocus(Qt.OtherFocusReason) + def stack_current_changed(self, index): + visible = index == 0 + for x in ('eb', 'ab', 'rb', 'upb', 'dnb', 'd2', 'filter_text', 'cft', 'd3', 'ib', 'eb2'): + getattr(self, x).setVisible(visible) + @dynamic_property def where(self): def fget(self): @@ -698,28 +768,39 @@ class SavedSearches(Dialog): self.searches.scrollTo(self.model.index(0)) def run_search(self, action): - searches, seen = [], set() - for index in self.searches.selectionModel().selectedIndexes(): - if index.row() in seen: - continue - seen.add(index.row()) - search = SearchWidget.DEFAULT_STATE.copy() - del search['mode'] - search_index, s = index.data(Qt.UserRole) - search.update(s) + searches = [] + + def fill_in_search(search): search['wrap'] = self.wrap search['direction'] = self.direction search['where'] = self.where search['mode'] = search.get('mode', 'regex') + + if self.editing_search: + search = SearchWidget.DEFAULT_STATE.copy() + del search['mode'] + search.update(self.edit_search_widget.current_search) + fill_in_search(search) searches.append(search) + else: + seen = set() + for index in self.searches.selectionModel().selectedIndexes(): + if index.row() in seen: + continue + seen.add(index.row()) + search = SearchWidget.DEFAULT_STATE.copy() + del search['mode'] + search_index, s = index.data(Qt.UserRole) + search.update(s) + fill_in_search(search) + searches.append(search) if not searches: return self.run_saved_searches.emit(searches, action) @property def editing_search(self): - d = getattr(self, 'current_edit_search', None) - return d is not None and not sip.isdeleted(d) and d.isVisible() + return self.stack.currentIndex() != 0 def move_entry(self, delta): if self.editing_search: @@ -735,13 +816,28 @@ class SavedSearches(Dialog): sm = self.searches.selectionModel() sm.setCurrentIndex(index, sm.ClearAndSelect) + def search_editing_done(self, save_changes): + if save_changes and not self.edit_search_widget.save_changes(): + return + self.stack.setCurrentIndex(0) + if save_changes: + if self.edit_search_widget.search_index == -1: + self._add_search() + else: + index = self.searches.currentIndex() + if index.isValid(): + self.model.dataChanged.emit(index, index) + def edit_search(self): index = self.searches.currentIndex() - if index.isValid() and not self.editing_search: + if not index.isValid(): + return error_dialog(self, _('Cannot edit'), _( + 'Cannot edit search - no search selected.'), show=True) + if not self.editing_search: search_index, search = index.data(Qt.UserRole) - d = self.current_edit_search = EditSearch(search=search, search_index=search_index, parent=self) - d.accepted.connect(partial(self.model.dataChanged.emit, index, index)) - d.show() + self.edit_search_widget.show_search(search=search, search_index=search_index) + self.stack.setCurrentIndex(1) + self.edit_search_widget.find.setFocus(Qt.OtherFocusReason) def remove_search(self): if self.editing_search: @@ -753,9 +849,9 @@ class SavedSearches(Dialog): def add_search(self): if self.editing_search: return - self.current_edit_search = d = EditSearch(parent=self) - d.accepted.connect(self._add_search) - d.show() + self.edit_search_widget.show_search() + self.stack.setCurrentIndex(1) + self.edit_search_widget.search_name.setFocus(Qt.OtherFocusReason) def _add_search(self): self.model.add_searches() @@ -768,9 +864,9 @@ class SavedSearches(Dialog): def add_predefined_search(self, state): if self.editing_search: return - self.current_edit_search = d = EditSearch(parent=self, state=state) - d.accepted.connect(self._add_search) - d.show() + self.edit_search_widget.show_search(state=state) + self.stack.setCurrentIndex(1) + self.edit_search_widget.search_name.setFocus(Qt.OtherFocusReason) def show_details(self): self.description.setText(' \n \n ') @@ -1059,4 +1155,5 @@ def run_search( if __name__ == '__main__': app = QApplication([]) d = SavedSearches() - d.exec_() + d.show() + app.exec_() diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 57f8be81e5..ad078ed828 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -679,6 +679,12 @@ class Main(MainWindow): self.addDockWidget(Qt.LeftDockWidgetArea, d) d.close() # Hidden by default + d = create(_('Saved Searches'), 'saved-searches') + d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea) + d.setWidget(self.saved_searches) + self.addDockWidget(Qt.LeftDockWidgetArea, d) + d.close() # Hidden by default + def resizeEvent(self, ev): self.blocking_job.resize(ev.size()) return super(Main, self).resizeEvent(ev)