From f52aa7a61594092ca0b0e05453573a650f2df613 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 22 Mar 2014 10:09:25 +0530 Subject: [PATCH] Saved searches: Add import/export functionality --- src/calibre/gui2/tweak_book/search.py | 74 ++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/tweak_book/search.py b/src/calibre/gui2/tweak_book/search.py index 13c79bc834..bf6d17f263 100644 --- a/src/calibre/gui2/tweak_book/search.py +++ b/src/calibre/gui2/tweak_book/search.py @@ -6,19 +6,20 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' +import json, copy from functools import partial from collections import OrderedDict from PyQt4.Qt import ( - QWidget, QToolBar, Qt, QHBoxLayout, QSize, QIcon, QGridLayout, QLabel, + QWidget, QToolBar, Qt, QHBoxLayout, QSize, QIcon, QGridLayout, QLabel, QTimer, QPushButton, pyqtSignal, QComboBox, QCheckBox, QSizePolicy, QVBoxLayout, QLineEdit, QToolButton, QListView, QFrame, QApplication, QStyledItemDelegate, - QAbstractListModel, QVariant, QFormLayout, QModelIndex) + QAbstractListModel, QVariant, QFormLayout, QModelIndex, QMenu, QItemSelection) import regex from calibre import prepare_string_for_xml -from calibre.gui2 import NONE, error_dialog, info_dialog +from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, choose_save_file from calibre.gui2.dialogs.message_box import MessageBox from calibre.gui2.widgets2 import HistoryLineEdit2 from calibre.gui2.tweak_book import tprefs, editors, current_container @@ -391,9 +392,9 @@ class SearchesModel(QAbstractListModel): self.dataChanged.emit(self.index(b), self.index(b)) tprefs['saved_searches'] = self.searches - def add_search(self): + def add_searches(self, count=1): self.searches = tprefs['saved_searches'] - self.filtered_searches.append(len(self.searches) - 1) + self.filtered_searches.extend(xrange(len(self.searches) - 1, len(self.searches) - 1 - count, -1)) self.reset() def remove_searches(self, rows): @@ -601,6 +602,13 @@ class SavedSearches(Dialog): l.addWidget(self.bb) self.bb.clear() self.bb.addButton(self.bb.Close) + self.ib = b = self.bb.addButton(_('&Import'), self.bb.ActionRole) + b.clicked.connect(self.import_searches) + self.eb = b = self.bb.addButton(_('E&xport'), self.bb.ActionRole) + 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))) + b.setMenu(m) self.searches.setFocus(Qt.OtherFocusReason) @@ -639,12 +647,13 @@ class SavedSearches(Dialog): continue seen.add(index.row()) search = SearchWidget.DEFAULT_STATE.copy() + del search['mode'] search_index, s = index.data(Qt.UserRole).toPyObject() search.update(s) search['wrap'] = self.wrap search['direction'] = self.direction search['where'] = self.where - search['mode'] = 'regex' + search['mode'] = search.get('mode', 'regex') searches.append(search) if not searches: return @@ -681,7 +690,7 @@ class SavedSearches(Dialog): def _add_search(self, d): if d.exec_() == d.Accepted: - self.model.add_search() + self.model.add_searches() index = self.model.index(self.model.rowCount() - 1) self.searches.scrollTo(index) sm = self.searches.selectionModel() @@ -702,6 +711,57 @@ class SavedSearches(Dialog): self.description.setText(_('{2} (Case sensitive: {3} Dot All: {4})\nFind: {0}\nReplace: {1}').format( search.get('find', ''), search.get('replace', ''), search.get('name', ''), cs, da)) + def import_searches(self): + path = choose_files(self, 'import_saved_searches', _('Choose file'), filters=[ + (_('Saved Searches'), ['json'])], all_files=False, select_only_single_file=True) + if path: + with open(path[0], 'rb') as f: + obj = json.loads(f.read()) + needed_keys = {'name', 'find', 'replace', 'case_sensitive', 'dot_all', 'mode'} + def err(): + error_dialog(self, _('Invalid data'), _( + 'The file %s does not contain valid saved searches') % path, show=True) + if not isinstance(obj, dict) or not 'version' in obj or not 'searches' in obj: + return err() + searches = [] + for item in obj['searches']: + if not isinstance(item, dict) or not set(item.iterkeys()).issuperset(needed_keys): + return err + searches.append({k:item[k] for k in needed_keys}) + + if searches: + tprefs['saved_searches'] = tprefs['saved_searches'] + searches + count = len(searches) + self.model.add_searches(count=count) + sm = self.searches.selectionModel() + top, bottom = self.model.index(self.model.rowCount() - count), self.model.index(self.model.rowCount() - 1) + sm.select(QItemSelection(top, bottom), sm.ClearAndSelect) + self.searches.scrollTo(bottom) + + def export_searches(self, all=True): + if all: + searches = copy.deepcopy(tprefs['saved_searches']) + if not searches: + return error_dialog(self, _('No searches'), _( + 'No searches available to be saved'), show=True) + else: + searches = [] + for index in self.searches.selectionModel().selectedIndexes(): + search = index.data(Qt.UserRole).toPyObject()[-1] + searches.append(search.copy()) + if not searches: + return error_dialog(self, _('No searches'), _( + 'No searches selected'), show=True) + [s.__setitem__('mode', s.get('mode', 'regex')) for s in searches] + path = choose_save_file(self, 'export-saved-searches', _('Choose file'), filters=[ + (_('Saved Searches'), ['json'])], all_files=False) + if path: + if not path.lower().endswith('.json'): + path += '.json' + raw = json.dumps({'version':1, 'searches':searches}, ensure_ascii=False, indent=2, sort_keys=True) + with open(path, 'wb') as f: + f.write(raw.encode('utf-8')) + def validate_search_request(name, searchable_names, has_marked_text, state, gui_parent): err = None where = state['where']