Edit Book: Re-implement the saved searches pop-up as a dockable window

This commit is contained in:
Kovid Goyal 2014-09-25 18:33:21 +05:30
parent 92cfab55a1
commit 18477b56d9
3 changed files with 194 additions and 93 deletions

View File

@ -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

View File

@ -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('<h2>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('<h2>' + (_('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_()

View File

@ -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)