mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-31 14:33:54 -04:00
Support for saved searches
This commit is contained in:
parent
374569abb0
commit
001eca4196
3544
resources/images/search_add_saved.svg
Normal file
3544
resources/images/search_add_saved.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 134 KiB |
3547
resources/images/search_copy_saved.svg
Normal file
3547
resources/images/search_copy_saved.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 151 KiB |
3544
resources/images/search_delete_saved.svg
Normal file
3544
resources/images/search_delete_saved.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 134 KiB |
@ -220,6 +220,64 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="SavedSearchBox" name="saved_search">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Choose saved search or enter name for new saved search</string>
|
||||||
|
</property>
|
||||||
|
<property name="minimumContentsLength">
|
||||||
|
<number>15</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>150</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="copy_search_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Copy current search text (instead of search name)</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/search_copy_saved.svg</normaloff>:/images/search_copy_saved.svg</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="save_search_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Save current search under the name shown in the box</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/search_add_saved.svg</normaloff>:/images/search_add_saved.svg</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="delete_search_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Delete current search and clear search box</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/search_delete_saved.svg</normaloff>:/images/search_delete_saved.svg</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="2" column="0">
|
||||||
@ -686,6 +744,11 @@
|
|||||||
<extends>QComboBox</extends>
|
<extends>QComboBox</extends>
|
||||||
<header>calibre.gui2.search_box</header>
|
<header>calibre.gui2.search_box</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>SavedSearchBox</class>
|
||||||
|
<extends>QComboBox</extends>
|
||||||
|
<header>calibre.gui2.search_box</header>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../resources/images.qrc"/>
|
<include location="../../../resources/images.qrc"/>
|
||||||
|
@ -7,6 +7,7 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from PyQt4.Qt import QComboBox, SIGNAL, Qt, QLineEdit, QStringList, pyqtSlot
|
from PyQt4.Qt import QComboBox, SIGNAL, Qt, QLineEdit, QStringList, pyqtSlot
|
||||||
|
from PyQt4.QtGui import QCompleter
|
||||||
|
|
||||||
from calibre.gui2 import config
|
from calibre.gui2 import config
|
||||||
|
|
||||||
@ -20,6 +21,10 @@ class SearchLineEdit(QLineEdit):
|
|||||||
self.emit(SIGNAL('mouse_released(PyQt_PyObject)'), event)
|
self.emit(SIGNAL('mouse_released(PyQt_PyObject)'), event)
|
||||||
QLineEdit.mouseReleaseEvent(self, event)
|
QLineEdit.mouseReleaseEvent(self, event)
|
||||||
|
|
||||||
|
def focusOutEvent(self, event):
|
||||||
|
self.emit(SIGNAL('focus_out(PyQt_PyObject)'), event)
|
||||||
|
QLineEdit.focusOutEvent(self, event)
|
||||||
|
|
||||||
def dropEvent(self, ev):
|
def dropEvent(self, ev):
|
||||||
if self.parent().help_state:
|
if self.parent().help_state:
|
||||||
self.parent().normalize_state()
|
self.parent().normalize_state()
|
||||||
@ -176,3 +181,128 @@ class SearchBox2(QComboBox):
|
|||||||
def search_as_you_type(self, enabled):
|
def search_as_you_type(self, enabled):
|
||||||
self.as_you_type = enabled
|
self.as_you_type = enabled
|
||||||
|
|
||||||
|
|
||||||
|
class SavedSearchBox(QComboBox):
|
||||||
|
|
||||||
|
'''
|
||||||
|
To use this class:
|
||||||
|
* Call initialize()
|
||||||
|
* Connect to the changed() signal from this widget
|
||||||
|
if you care about changes to the list of saved searches.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QComboBox.__init__(self, parent)
|
||||||
|
self.normal_background = 'rgb(255, 255, 255, 0%)'
|
||||||
|
|
||||||
|
self.line_edit = SearchLineEdit(self)
|
||||||
|
self.setLineEdit(self.line_edit)
|
||||||
|
self.connect(self.line_edit, SIGNAL('key_pressed(PyQt_PyObject)'),
|
||||||
|
self.key_pressed, Qt.DirectConnection)
|
||||||
|
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
|
||||||
|
self.mouse_released, Qt.DirectConnection)
|
||||||
|
self.connect(self.line_edit, SIGNAL('focus_out(PyQt_PyObject)'),
|
||||||
|
self.focus_out, Qt.DirectConnection)
|
||||||
|
self.connect(self, SIGNAL('activated(const QString&)'),
|
||||||
|
self.saved_search_selected)
|
||||||
|
|
||||||
|
completer = QCompleter(self) # turn off auto-completion
|
||||||
|
self.setCompleter(completer)
|
||||||
|
self.setEditable(True)
|
||||||
|
self.help_state = True
|
||||||
|
self.prev_search = ''
|
||||||
|
self.setInsertPolicy(self.NoInsert)
|
||||||
|
|
||||||
|
def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')):
|
||||||
|
self.tool_tip_text = self.toolTip()
|
||||||
|
self.saved_searches = _saved_searches
|
||||||
|
self.search_box = _search_box
|
||||||
|
self.help_text = help_text
|
||||||
|
self.colorize = colorize
|
||||||
|
self.clear_to_help()
|
||||||
|
|
||||||
|
def normalize_state(self):
|
||||||
|
#print 'in normalize_state'
|
||||||
|
self.setEditText('')
|
||||||
|
self.line_edit.setStyleSheet(
|
||||||
|
'QLineEdit { color: black; background-color: %s; }' %
|
||||||
|
self.normal_background)
|
||||||
|
self.help_state = False
|
||||||
|
|
||||||
|
def clear_to_help(self):
|
||||||
|
#print 'in clear_to_help'
|
||||||
|
self.setToolTip(self.tool_tip_text)
|
||||||
|
self.initialize_saved_search_names()
|
||||||
|
self.setEditText(self.help_text)
|
||||||
|
self.line_edit.home(False)
|
||||||
|
self.help_state = True
|
||||||
|
self.line_edit.setStyleSheet(
|
||||||
|
'QLineEdit { color: gray; background-color: %s; }' %
|
||||||
|
self.normal_background)
|
||||||
|
|
||||||
|
def focus_out(self, event):
|
||||||
|
#print 'in focus_out'
|
||||||
|
if self.currentText() == '':
|
||||||
|
self.clear_to_help()
|
||||||
|
|
||||||
|
def key_pressed(self, event):
|
||||||
|
#print 'in key_pressed'
|
||||||
|
if self.help_state:
|
||||||
|
self.normalize_state()
|
||||||
|
|
||||||
|
def mouse_released(self, event):
|
||||||
|
if self.help_state:
|
||||||
|
self.normalize_state()
|
||||||
|
|
||||||
|
def saved_search_selected (self, qname):
|
||||||
|
#print 'in saved_search_selected'
|
||||||
|
if qname is None or qname == '':
|
||||||
|
return
|
||||||
|
self.normalize_state()
|
||||||
|
self.search_box.set_search_string ('search:"'+unicode(qname)+'"')
|
||||||
|
self.setEditText(qname)
|
||||||
|
self.setToolTip(self.saved_searches.lookup(qname))
|
||||||
|
|
||||||
|
def initialize_saved_search_names(self):
|
||||||
|
#print 'in initialize_saved_search_names'
|
||||||
|
self.clear()
|
||||||
|
qnames = self.saved_searches.names()
|
||||||
|
self.addItems(qnames)
|
||||||
|
self.setCurrentIndex(-1)
|
||||||
|
|
||||||
|
# SIGNALed from the main UI
|
||||||
|
def delete_search_button_clicked(self):
|
||||||
|
#print 'in delete_search_button_clicked'
|
||||||
|
idx = self.currentIndex
|
||||||
|
if idx < 0:
|
||||||
|
return
|
||||||
|
self.saved_searches.delete (unicode(self.currentText()))
|
||||||
|
self.clear_to_help()
|
||||||
|
self.search_box.set_search_string ('')
|
||||||
|
self.emit(SIGNAL('changed()'))
|
||||||
|
|
||||||
|
# SIGNALed from the main UI
|
||||||
|
def save_search_button_clicked(self):
|
||||||
|
#print 'in save_search_button_clicked'
|
||||||
|
name = self.currentText()
|
||||||
|
if self.help_state or name == '':
|
||||||
|
name = self.search_box.text()
|
||||||
|
self.saved_searches.add(name, self.search_box.text())
|
||||||
|
# now go through an initialization cycle to ensure that the combobox has
|
||||||
|
# the new search in it, that it is selected, and that the search box
|
||||||
|
# references the new search instead of the text in the search.
|
||||||
|
self.clear_to_help()
|
||||||
|
self.normalize_state()
|
||||||
|
self.setCurrentIndex(self.findText(name))
|
||||||
|
self.saved_search_selected (name)
|
||||||
|
self.emit(SIGNAL('changed()'))
|
||||||
|
|
||||||
|
# SIGNALed from the main UI
|
||||||
|
def copy_search_button_clicked (self):
|
||||||
|
#print 'in copy_search_button_clicked'
|
||||||
|
idx = self.currentIndex();
|
||||||
|
if idx < 0:
|
||||||
|
return
|
||||||
|
self.search_box.set_search_string (self.saved_searches.lookup(self.currentText()))
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, \
|
|||||||
QFont, SIGNAL, QSize, QIcon, QPoint, \
|
QFont, SIGNAL, QSize, QIcon, QPoint, \
|
||||||
QAbstractItemModel, QVariant, QModelIndex
|
QAbstractItemModel, QVariant, QModelIndex
|
||||||
from calibre.gui2 import config, NONE
|
from calibre.gui2 import config, NONE
|
||||||
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
|
from calibre.library.database2 import Tag
|
||||||
|
|
||||||
class TagsView(QTreeView):
|
class TagsView(QTreeView):
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ class TagsView(QTreeView):
|
|||||||
self.connect(self, SIGNAL('clicked(QModelIndex)'), self.toggle)
|
self.connect(self, SIGNAL('clicked(QModelIndex)'), self.toggle)
|
||||||
self.popularity.setChecked(config['sort_by_popularity'])
|
self.popularity.setChecked(config['sort_by_popularity'])
|
||||||
self.connect(self.popularity, SIGNAL('stateChanged(int)'), self.sort_changed)
|
self.connect(self.popularity, SIGNAL('stateChanged(int)'), self.sort_changed)
|
||||||
|
self.connect(self, SIGNAL('need_refresh()'), self.recount, Qt.QueuedConnection)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def match_all(self):
|
def match_all(self):
|
||||||
@ -119,9 +122,14 @@ class TagTreeItem(object):
|
|||||||
|
|
||||||
def tag_data(self, role):
|
def tag_data(self, role):
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
|
if self.tag.count == 0:
|
||||||
|
return QVariant('%s'%(self.tag.name))
|
||||||
|
else:
|
||||||
|
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
|
||||||
if role == Qt.DecorationRole:
|
if role == Qt.DecorationRole:
|
||||||
return self.icon_map[self.tag.state]
|
return self.icon_map[self.tag.state]
|
||||||
|
if role == Qt.ToolTipRole and self.tag.tooltip:
|
||||||
|
return self.tag.tooltip
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def toggle(self):
|
def toggle(self):
|
||||||
@ -129,36 +137,44 @@ class TagTreeItem(object):
|
|||||||
self.tag.state = (self.tag.state + 1)%3
|
self.tag.state = (self.tag.state + 1)%3
|
||||||
|
|
||||||
class TagsModel(QAbstractItemModel):
|
class TagsModel(QAbstractItemModel):
|
||||||
categories = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('Tags')]
|
categories = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('Tags'), _('Searches')]
|
||||||
row_map = ['author', 'series', 'format', 'publisher', 'news', 'tag']
|
row_map = ['author', 'series', 'format', 'publisher', 'news', 'tag', 'search']
|
||||||
|
|
||||||
def __init__(self, db, parent=None):
|
def __init__(self, db, parent=None):
|
||||||
QAbstractItemModel.__init__(self, parent)
|
QAbstractItemModel.__init__(self, parent)
|
||||||
self.cmap = tuple(map(QIcon, [I('user_profile.svg'),
|
self.cmap = tuple(map(QIcon, [I('user_profile.svg'),
|
||||||
I('series.svg'), I('book.svg'), I('publisher.png'),
|
I('series.svg'), I('book.svg'), I('publisher.png'),
|
||||||
I('news.svg'), I('tags.svg')]))
|
I('news.svg'), I('tags.svg'), I('search.svg')]))
|
||||||
self.icon_map = [QIcon(), QIcon(I('plus.svg')),
|
self.icon_map = [QIcon(), QIcon(I('plus.svg')),
|
||||||
QIcon(I('minus.svg'))]
|
QIcon(I('minus.svg'))]
|
||||||
self.db = db
|
self.db = db
|
||||||
self.ignore_next_search = 0
|
self.ignore_next_search = 0
|
||||||
self.root_item = TagTreeItem()
|
self.root_item = TagTreeItem()
|
||||||
data = self.db.get_categories(config['sort_by_popularity'])
|
data = self.db.get_categories(config['sort_by_popularity'])
|
||||||
|
data['search'] = self.get_search_nodes()
|
||||||
|
|
||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
c = TagTreeItem(parent=self.root_item,
|
c = TagTreeItem(parent=self.root_item,
|
||||||
data=self.categories[i], category_icon=self.cmap[i])
|
data=self.categories[i], category_icon=self.cmap[i])
|
||||||
for tag in data[r]:
|
for tag in data[r]:
|
||||||
t = TagTreeItem(parent=c, data=tag, icon_map=self.icon_map)
|
TagTreeItem(parent=c, data=tag, icon_map=self.icon_map)
|
||||||
t
|
|
||||||
|
|
||||||
self.db.add_listener(self.database_changed)
|
self.db.add_listener(self.database_changed)
|
||||||
self.connect(self, SIGNAL('need_refresh()'), self.refresh,
|
self.connect(self, SIGNAL('need_refresh()'), self.refresh,
|
||||||
Qt.QueuedConnection)
|
Qt.QueuedConnection)
|
||||||
|
|
||||||
|
def get_search_nodes(self):
|
||||||
|
l = []
|
||||||
|
for i in saved_searches.names():
|
||||||
|
l.append(Tag(i, tooltip=saved_searches.lookup(i)))
|
||||||
|
return l
|
||||||
|
|
||||||
def database_changed(self, event, ids):
|
def database_changed(self, event, ids):
|
||||||
self.emit(SIGNAL('need_refresh()'))
|
self.emit(SIGNAL('need_refresh()'))
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
data = self.db.get_categories(config['sort_by_popularity'])
|
data = self.db.get_categories(config['sort_by_popularity'])
|
||||||
|
data['search'] = self.get_search_nodes()
|
||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
category = self.root_item.children[i]
|
category = self.root_item.children[i]
|
||||||
names = [t.tag.name for t in category.children]
|
names = [t.tag.name for t in category.children]
|
||||||
|
@ -29,6 +29,7 @@ from calibre.utils.filenames import ascii_filename
|
|||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import prefs, dynamic
|
from calibre.utils.config import prefs, dynamic
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
from calibre.gui2 import warning_dialog, choose_files, error_dialog, \
|
from calibre.gui2 import warning_dialog, choose_files, error_dialog, \
|
||||||
question_dialog,\
|
question_dialog,\
|
||||||
pixmap_to_data, choose_dir, \
|
pixmap_to_data, choose_dir, \
|
||||||
@ -140,9 +141,21 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
Ui_MainWindow.__init__(self)
|
Ui_MainWindow.__init__(self)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.setWindowTitle(__appname__)
|
self.setWindowTitle(__appname__)
|
||||||
|
|
||||||
self.search.initialize('main_search_history', colorize=True,
|
self.search.initialize('main_search_history', colorize=True,
|
||||||
help_text=_('Search (For Advanced Search click the button to the left)'))
|
help_text=_('Search (For Advanced Search click the button to the left)'))
|
||||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
|
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
|
||||||
|
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
|
||||||
|
|
||||||
|
self.saved_search.initialize(saved_searches, self.search, colorize=True,
|
||||||
|
help_text=_('Saved Searches'))
|
||||||
|
self.connect(self.save_search_button, SIGNAL('clicked()'),
|
||||||
|
self.saved_search.save_search_button_clicked)
|
||||||
|
self.connect(self.delete_search_button, SIGNAL('clicked()'),
|
||||||
|
self.saved_search.delete_search_button_clicked)
|
||||||
|
self.connect(self.copy_search_button, SIGNAL('clicked()'),
|
||||||
|
self.saved_search.copy_search_button_clicked)
|
||||||
|
|
||||||
self.progress_indicator = ProgressIndicator(self)
|
self.progress_indicator = ProgressIndicator(self)
|
||||||
self.verbose = opts.verbose
|
self.verbose = opts.verbose
|
||||||
self.get_metadata = GetMetadata()
|
self.get_metadata = GetMetadata()
|
||||||
@ -511,6 +524,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.connect(self.tags_view,
|
self.connect(self.tags_view,
|
||||||
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
||||||
self.search.search_from_tags)
|
self.search.search_from_tags)
|
||||||
|
self.connect(self.tags_view,
|
||||||
|
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
||||||
|
self.saved_search.clear_to_help)
|
||||||
self.connect(self.status_bar.tag_view_button,
|
self.connect(self.status_bar.tag_view_button,
|
||||||
SIGNAL('toggled(bool)'), self.toggle_tags_view)
|
SIGNAL('toggled(bool)'), self.toggle_tags_view)
|
||||||
self.connect(self.search,
|
self.connect(self.search,
|
||||||
@ -521,6 +537,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
|
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
|
||||||
self.tags_view.recount)
|
self.tags_view.recount)
|
||||||
self.connect(self.search, SIGNAL('cleared()'), self.tags_view.clear)
|
self.connect(self.search, SIGNAL('cleared()'), self.tags_view.clear)
|
||||||
|
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.recount)
|
||||||
if not gprefs.get('quick_start_guide_added', False):
|
if not gprefs.get('quick_start_guide_added', False):
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
||||||
|
@ -409,14 +409,15 @@ class ResultCache(SearchQueryParser):
|
|||||||
|
|
||||||
class Tag(object):
|
class Tag(object):
|
||||||
|
|
||||||
def __init__(self, name, id=None, count=0, state=0):
|
def __init__(self, name, id=None, count=0, state=0, tooltip=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.id = id
|
self.id = id
|
||||||
self.count = count
|
self.count = count
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self.tooltip = tooltip
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u'%s:%s:%s:%s'%(self.name, self.count, self.id, self.state)
|
return u'%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state, self.tooltip)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return unicode(self).encode('utf-8')
|
return unicode(self).encode('utf-8')
|
||||||
|
@ -673,6 +673,9 @@ def _prefs():
|
|||||||
c.add_opt('add_formats_to_existing', default=False,
|
c.add_opt('add_formats_to_existing', default=False,
|
||||||
help=_('Add new formats to existing book records'))
|
help=_('Add new formats to existing book records'))
|
||||||
|
|
||||||
|
# this is here instead of the gui preferences because calibredb can execute searches
|
||||||
|
c.add_opt('saved_searches', default={}, help=_('List of named saved searches'))
|
||||||
|
|
||||||
c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.')
|
c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.')
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
@ -19,7 +19,49 @@ If this module is run, it will perform a series of unit tests.
|
|||||||
import sys, string, operator
|
import sys, string, operator
|
||||||
|
|
||||||
from calibre.utils.pyparsing import Keyword, Group, Forward, CharsNotIn, Suppress, \
|
from calibre.utils.pyparsing import Keyword, Group, Forward, CharsNotIn, Suppress, \
|
||||||
OneOrMore, oneOf, CaselessLiteral, Optional, NoMatch
|
OneOrMore, oneOf, CaselessLiteral, Optional, NoMatch, ParseException
|
||||||
|
from calibre.constants import preferred_encoding
|
||||||
|
from calibre.utils.config import prefs
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
This class manages access to the preference holding the saved search queries.
|
||||||
|
It exists to ensure that unicode is used throughout, and also to permit
|
||||||
|
adding other fields, such as whether the search is a 'favorite'
|
||||||
|
'''
|
||||||
|
class SavedSearchQueries(object):
|
||||||
|
queries = {}
|
||||||
|
opt_name = ''
|
||||||
|
|
||||||
|
def __init__(self, _opt_name):
|
||||||
|
self.opt_name = _opt_name;
|
||||||
|
self.queries = prefs[self.opt_name]
|
||||||
|
|
||||||
|
def force_unicode(self, x):
|
||||||
|
if not isinstance(x, unicode):
|
||||||
|
x = x.decode(preferred_encoding, 'replace')
|
||||||
|
return x
|
||||||
|
|
||||||
|
def add(self, name, value):
|
||||||
|
self.queries[self.force_unicode(name)] = self.force_unicode(value).strip()
|
||||||
|
prefs[self.opt_name] = self.queries
|
||||||
|
|
||||||
|
def lookup(self, name):
|
||||||
|
return self.queries.get(self.force_unicode(name), None)
|
||||||
|
|
||||||
|
def delete(self, name):
|
||||||
|
self.queries.pop(self.force_unicode(name), False)
|
||||||
|
prefs[self.opt_name] = self.queries
|
||||||
|
|
||||||
|
def names(self):
|
||||||
|
return sorted(self.queries.keys(),
|
||||||
|
cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
||||||
|
|
||||||
|
'''
|
||||||
|
Create a global instance of the saved searches. It is global so that the searches
|
||||||
|
are common across all instances of the parser (devices, library, etc).
|
||||||
|
'''
|
||||||
|
saved_searches = SavedSearchQueries('saved_searches')
|
||||||
|
|
||||||
|
|
||||||
class SearchQueryParser(object):
|
class SearchQueryParser(object):
|
||||||
@ -55,6 +97,7 @@ class SearchQueryParser(object):
|
|||||||
'comments',
|
'comments',
|
||||||
'format',
|
'format',
|
||||||
'isbn',
|
'isbn',
|
||||||
|
'search',
|
||||||
'all',
|
'all',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -130,6 +173,13 @@ class SearchQueryParser(object):
|
|||||||
|
|
||||||
|
|
||||||
def parse(self, query):
|
def parse(self, query):
|
||||||
|
# empty the list of searches used for recursion testing
|
||||||
|
self.searches_seen = set([])
|
||||||
|
return self._parse(query)
|
||||||
|
|
||||||
|
# this parse is used internally because it doesn't clear the
|
||||||
|
# recursive search test list
|
||||||
|
def _parse(self, query):
|
||||||
res = self._parser.parseString(query)[0]
|
res = self._parser.parseString(query)[0]
|
||||||
return self.evaluate(res)
|
return self.evaluate(res)
|
||||||
|
|
||||||
@ -152,7 +202,20 @@ class SearchQueryParser(object):
|
|||||||
return self.evaluate(argument[0])
|
return self.evaluate(argument[0])
|
||||||
|
|
||||||
def evaluate_token(self, argument):
|
def evaluate_token(self, argument):
|
||||||
return self.get_matches(argument[0], argument[1])
|
location = argument[0]
|
||||||
|
query = argument[1]
|
||||||
|
if location.lower() == 'search':
|
||||||
|
# print "looking for named search " + query
|
||||||
|
if query.startswith('='):
|
||||||
|
query = query[1:]
|
||||||
|
try:
|
||||||
|
if query in self.searches_seen:
|
||||||
|
raise ParseException(query, len(query), 'undefined saved search', self)
|
||||||
|
self.searches_seen.add(query)
|
||||||
|
return self._parse(saved_searches.lookup(query))
|
||||||
|
except: # convert all exceptions (e.g., missing key) to a parse error
|
||||||
|
raise ParseException(query, len(query), 'undefined saved search', self)
|
||||||
|
return self.get_matches(location, query)
|
||||||
|
|
||||||
def get_matches(self, location, query):
|
def get_matches(self, location, query):
|
||||||
'''
|
'''
|
||||||
|
Loading…
x
Reference in New Issue
Block a user