mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Ordering, editing, adding, removing for saved searches
This commit is contained in:
parent
74f4bafae2
commit
c59bf51c70
@ -41,6 +41,7 @@ d['charmap_favorites'] = list(map(ord, '\xa0\u2002\u2003\u2009\xad' '‘’“
|
|||||||
d['folders_for_types'] = {'style':'styles', 'image':'images', 'font':'fonts', 'audio':'audio', 'video':'video'}
|
d['folders_for_types'] = {'style':'styles', 'image':'images', 'font':'fonts', 'audio':'audio', 'video':'video'}
|
||||||
d['pretty_print_on_open'] = False
|
d['pretty_print_on_open'] = False
|
||||||
d['disable_completion_popup_for_search'] = False
|
d['disable_completion_popup_for_search'] = False
|
||||||
|
d['saved_searches'] = []
|
||||||
|
|
||||||
del d
|
del d
|
||||||
|
|
||||||
|
@ -11,14 +11,18 @@ from functools import partial
|
|||||||
from PyQt4.Qt import (
|
from PyQt4.Qt import (
|
||||||
QWidget, QToolBar, Qt, QHBoxLayout, QSize, QIcon, QGridLayout, QLabel,
|
QWidget, QToolBar, Qt, QHBoxLayout, QSize, QIcon, QGridLayout, QLabel,
|
||||||
QPushButton, pyqtSignal, QComboBox, QCheckBox, QSizePolicy, QVBoxLayout,
|
QPushButton, pyqtSignal, QComboBox, QCheckBox, QSizePolicy, QVBoxLayout,
|
||||||
QLineEdit, QToolButton, QListView, QFrame, QApplication)
|
QLineEdit, QToolButton, QListView, QFrame, QApplication, QStyledItemDelegate,
|
||||||
|
QAbstractListModel, QVariant, QFormLayout, QModelIndex)
|
||||||
|
|
||||||
import regex
|
import regex
|
||||||
|
|
||||||
|
from calibre.gui2 import NONE, error_dialog
|
||||||
from calibre.gui2.widgets2 import HistoryLineEdit2
|
from calibre.gui2.widgets2 import HistoryLineEdit2
|
||||||
from calibre.gui2.tweak_book import tprefs
|
from calibre.gui2.tweak_book import tprefs
|
||||||
from calibre.gui2.tweak_book.widgets import Dialog
|
from calibre.gui2.tweak_book.widgets import Dialog
|
||||||
|
|
||||||
|
from calibre.utils.icu import primary_contains
|
||||||
|
|
||||||
REGEX_FLAGS = regex.VERSION1 | regex.WORD | regex.FULLCASE | regex.MULTILINE | regex.UNICODE
|
REGEX_FLAGS = regex.VERSION1 | regex.WORD | regex.FULLCASE | regex.MULTILINE | regex.UNICODE
|
||||||
|
|
||||||
# The search panel {{{
|
# The search panel {{{
|
||||||
@ -288,7 +292,7 @@ class SearchWidget(QWidget):
|
|||||||
|
|
||||||
regex_cache = {}
|
regex_cache = {}
|
||||||
|
|
||||||
class SearchPanel(QWidget):
|
class SearchPanel(QWidget): # {{{
|
||||||
|
|
||||||
search_triggered = pyqtSignal(object)
|
search_triggered = pyqtSignal(object)
|
||||||
|
|
||||||
@ -350,6 +354,143 @@ class SearchPanel(QWidget):
|
|||||||
ev.accept()
|
ev.accept()
|
||||||
else:
|
else:
|
||||||
return QWidget.keyPressEvent(self, ev)
|
return QWidget.keyPressEvent(self, ev)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class SearchesModel(QAbstractListModel):
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QAbstractListModel.__init__(self, parent)
|
||||||
|
self.searches = tprefs['saved_searches']
|
||||||
|
self.filtered_searches = list(xrange(len(self.searches)))
|
||||||
|
|
||||||
|
def rowCount(self, parent=QModelIndex()):
|
||||||
|
return len(self.filtered_searches)
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
|
search = self.searches[self.filtered_searches[index.row()]]
|
||||||
|
return QVariant(search['name'])
|
||||||
|
if role == Qt.ToolTipRole:
|
||||||
|
search = self.searches[self.filtered_searches[index.row()]]
|
||||||
|
tt = '\n'.join((search['find'], search['replace']))
|
||||||
|
return QVariant(tt)
|
||||||
|
if role == Qt.UserRole:
|
||||||
|
search = self.searches[self.filtered_searches[index.row()]]
|
||||||
|
return QVariant((self.filtered_searches[index.row()], search))
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def do_filter(self, text):
|
||||||
|
text = unicode(text)
|
||||||
|
self.filtered_searches = []
|
||||||
|
for i, search in enumerate(self.searches):
|
||||||
|
if primary_contains(text, search['name']):
|
||||||
|
self.filtered_searches.append(i)
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def move_entry(self, row, delta):
|
||||||
|
a, b = row, row + delta
|
||||||
|
if 0 <= b < len(self.filtered_searches):
|
||||||
|
ai, bi = self.filtered_searches[a], self.filtered_searches[b]
|
||||||
|
self.searches[ai], self.searches[bi] = self.searches[bi], self.searches[ai]
|
||||||
|
self.dataChanged.emit(self.index(a), self.index(a))
|
||||||
|
self.dataChanged.emit(self.index(b), self.index(b))
|
||||||
|
tprefs['saved_searches'] = self.searches
|
||||||
|
|
||||||
|
def add_search(self):
|
||||||
|
self.searches = tprefs['saved_searches']
|
||||||
|
self.filtered_searches.append(len(self.searches) - 1)
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def remove_searches(self, rows):
|
||||||
|
rows = sorted(set(rows), reverse=True)
|
||||||
|
indices = [self.filtered_searches[row] for row in rows]
|
||||||
|
for row in rows:
|
||||||
|
self.beginRemoveRows(QModelIndex(), row, row)
|
||||||
|
del self.filtered_searches[row]
|
||||||
|
self.endRemoveRows()
|
||||||
|
for idx in sorted(indices, reverse=True):
|
||||||
|
del self.searches[idx]
|
||||||
|
tprefs['saved_searches'] = self.searches
|
||||||
|
|
||||||
|
class EditSearch(Dialog): # {{{
|
||||||
|
|
||||||
|
def __init__(self, search=None, search_index=-1, parent=None):
|
||||||
|
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)
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
ans = Dialog.sizeHint(self)
|
||||||
|
ans.setWidth(600)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
self.l = l = QFormLayout(self)
|
||||||
|
self.setLayout(l)
|
||||||
|
|
||||||
|
self.search_name = n = QLineEdit(self.search.get('name', ''), self)
|
||||||
|
n.setPlaceholderText(_('The name with which to save this search'))
|
||||||
|
l.addRow(_('&Name:'), n)
|
||||||
|
|
||||||
|
self.find = f = QLineEdit(self.search.get('find', ''), self)
|
||||||
|
f.setPlaceholderText(_('The regular 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)
|
||||||
|
|
||||||
|
l.addRow(self.bb)
|
||||||
|
|
||||||
|
def accept(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
|
||||||
|
if not n:
|
||||||
|
return error_dialog(self, _('Must specify name'), _(
|
||||||
|
'You must specify a search name'), show=True)
|
||||||
|
if n in all_names:
|
||||||
|
return error_dialog(self, _('Name exists'), _(
|
||||||
|
'Another search with the name %s already exists') % n, show=True)
|
||||||
|
search['name'] = n
|
||||||
|
|
||||||
|
f = unicode(self.find.text())
|
||||||
|
if not f:
|
||||||
|
return error_dialog(self, _('Must specify find'), _(
|
||||||
|
'You must specify a find expression'), show=True)
|
||||||
|
search['find'] = f
|
||||||
|
|
||||||
|
r = unicode(self.replace.text())
|
||||||
|
search['replace'] = r
|
||||||
|
|
||||||
|
search['dot_all'] = bool(self.dot_all.isChecked())
|
||||||
|
search['case_sensitive'] = bool(self.case_sensitive.isChecked())
|
||||||
|
|
||||||
|
if self.search_index == -1:
|
||||||
|
searches.append(search)
|
||||||
|
else:
|
||||||
|
searches[self.search_index] = search
|
||||||
|
tprefs.set('saved_searches', searches)
|
||||||
|
|
||||||
|
Dialog.accept(self)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class SearchDelegate(QStyledItemDelegate):
|
||||||
|
|
||||||
|
def sizeHint(self, *args):
|
||||||
|
ans = QStyledItemDelegate.sizeHint(self, *args)
|
||||||
|
ans.setHeight(ans.height() + 4)
|
||||||
|
return ans
|
||||||
|
|
||||||
class SavedSearches(Dialog):
|
class SavedSearches(Dialog):
|
||||||
|
|
||||||
@ -357,7 +498,7 @@ class SavedSearches(Dialog):
|
|||||||
Dialog.__init__(self, _('Saved Searches'), 'saved-searches', parent=parent)
|
Dialog.__init__(self, _('Saved Searches'), 'saved-searches', parent=parent)
|
||||||
|
|
||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
return QSize(800, 650)
|
return QSize(800, 675)
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
self.l = l = QVBoxLayout(self)
|
self.l = l = QVBoxLayout(self)
|
||||||
@ -376,6 +517,15 @@ class SavedSearches(Dialog):
|
|||||||
|
|
||||||
self.h2 = h = QHBoxLayout()
|
self.h2 = h = QHBoxLayout()
|
||||||
self.searches = searches = QListView(self)
|
self.searches = searches = QListView(self)
|
||||||
|
searches.doubleClicked.connect(self.edit_search)
|
||||||
|
self.model = SearchesModel(self.searches)
|
||||||
|
self.model.dataChanged.connect(self.show_details)
|
||||||
|
searches.setModel(self.model)
|
||||||
|
searches.selectionModel().currentChanged.connect(self.show_details)
|
||||||
|
searches.setSelectionMode(searches.ExtendedSelection)
|
||||||
|
self.delegate = SearchDelegate(searches)
|
||||||
|
searches.setItemDelegate(self.delegate)
|
||||||
|
searches.setAlternatingRowColors(True)
|
||||||
h.addWidget(searches, stretch=10)
|
h.addWidget(searches, stretch=10)
|
||||||
self.v = v = QVBoxLayout()
|
self.v = v = QVBoxLayout()
|
||||||
h.addLayout(v)
|
h.addLayout(v)
|
||||||
@ -431,14 +581,21 @@ class SavedSearches(Dialog):
|
|||||||
v.addWidget(d)
|
v.addWidget(d)
|
||||||
|
|
||||||
self.where_box = wb = WhereBox(self)
|
self.where_box = wb = WhereBox(self)
|
||||||
|
self.where = SearchWidget.DEFAULT_STATE['where']
|
||||||
v.addWidget(wb)
|
v.addWidget(wb)
|
||||||
self.direction_box = db = DirectionBox(self)
|
self.direction_box = db = DirectionBox(self)
|
||||||
|
self.direction = SearchWidget.DEFAULT_STATE['direction']
|
||||||
v.addWidget(db)
|
v.addWidget(db)
|
||||||
|
|
||||||
self.wr = wr = QCheckBox(_('&Wrap'))
|
self.wr = wr = QCheckBox(_('&Wrap'))
|
||||||
wr.setToolTip('<p>'+_('When searching reaches the end, wrap around to the beginning and continue the search'))
|
wr.setToolTip('<p>'+_('When searching reaches the end, wrap around to the beginning and continue the search'))
|
||||||
|
self.wr.setChecked(SearchWidget.DEFAULT_STATE['wrap'])
|
||||||
v.addWidget(wr)
|
v.addWidget(wr)
|
||||||
|
|
||||||
|
self.description = d = QLabel(' \n \n ')
|
||||||
|
d.setTextFormat(Qt.PlainText)
|
||||||
|
l.addWidget(d)
|
||||||
|
|
||||||
l.addWidget(self.bb)
|
l.addWidget(self.bb)
|
||||||
self.bb.clear()
|
self.bb.clear()
|
||||||
self.bb.addButton(self.bb.Close)
|
self.bb.addButton(self.bb.Close)
|
||||||
@ -470,22 +627,67 @@ class SavedSearches(Dialog):
|
|||||||
return property(fget=fget, fset=fset)
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
def do_filter(self, text):
|
def do_filter(self, text):
|
||||||
pass
|
self.model.do_filter(text)
|
||||||
|
self.searches.scrollTo(self.model.index(0))
|
||||||
|
|
||||||
def run_search(self, action):
|
def run_search(self, action):
|
||||||
pass
|
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()
|
||||||
|
search_index, s = index.data(Qt.UserRole).toPyObject()
|
||||||
|
search.update(s)
|
||||||
|
search['wrap'] = self.wrap
|
||||||
|
search['direction'] = self.direction
|
||||||
|
search['where'] = self.where
|
||||||
|
searches.append(search)
|
||||||
|
if not searches:
|
||||||
|
return
|
||||||
|
|
||||||
def move_entry(self, delta):
|
def move_entry(self, delta):
|
||||||
pass
|
rows = {index.row() for index in self.searches.selectionModel().selectedIndexes()} - {-1}
|
||||||
|
if rows:
|
||||||
|
with tprefs:
|
||||||
|
for row in sorted(rows, reverse=delta > 0):
|
||||||
|
self.model.move_entry(row, delta)
|
||||||
|
nrow = row + delta
|
||||||
|
index = self.model.index(nrow)
|
||||||
|
if index.isValid():
|
||||||
|
sm = self.searches.selectionModel()
|
||||||
|
sm.setCurrentIndex(index, sm.ClearAndSelect)
|
||||||
|
|
||||||
def edit_search(self):
|
def edit_search(self):
|
||||||
pass
|
index = self.searches.currentIndex()
|
||||||
|
if index.isValid():
|
||||||
|
search_index, search = index.data(Qt.UserRole).toPyObject()
|
||||||
|
d = EditSearch(search=search, search_index=search_index, parent=self)
|
||||||
|
if d.exec_() == d.Accepted:
|
||||||
|
self.model.dataChanged.emit(index, index)
|
||||||
|
|
||||||
def remove_search(self):
|
def remove_search(self):
|
||||||
pass
|
rows = {index.row() for index in self.searches.selectionModel().selectedIndexes()} - {-1}
|
||||||
|
self.model.remove_searches(rows)
|
||||||
|
self.show_details()
|
||||||
|
|
||||||
def add_search(self):
|
def add_search(self):
|
||||||
pass
|
d = EditSearch(parent=self)
|
||||||
|
if d.exec_() == d.Accepted:
|
||||||
|
self.model.add_search()
|
||||||
|
index = self.model.index(self.model.rowCount() - 1)
|
||||||
|
self.searches.scrollTo(index)
|
||||||
|
sm = self.searches.selectionModel()
|
||||||
|
sm.setCurrentIndex(index, sm.ClearAndSelect)
|
||||||
|
self.show_details()
|
||||||
|
|
||||||
|
def show_details(self):
|
||||||
|
self.description.setText(' \n \n ')
|
||||||
|
i = self.searches.currentIndex()
|
||||||
|
if i.isValid():
|
||||||
|
search_index, search = i.data(Qt.UserRole).toPyObject()
|
||||||
|
self.description.setText(_('{2}\nFind: {0}\nReplace: {1}').format(
|
||||||
|
search.get('find', ''), search.get('replace', ''), search.get('name', '')))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user