diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index b00097f5b2..9150172fc1 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -120,6 +120,8 @@ def _config():
help='Search history for the LRF viewer')
c.add_opt('scheduler_search_history', default=[],
help='Search history for the recipe scheduler')
+ c.add_opt('plugin_search_history', default=[],
+ help='Search history for the recipe scheduler')
c.add_opt('worker_limit', default=6,
help=_('Maximum number of waiting worker processes'))
c.add_opt('get_social_metadata', default=True,
@@ -138,6 +140,7 @@ def _config():
help=_('Show the average rating per item indication in the tag browser'))
c.add_opt('disable_animations', default=False,
help=_('Disable UI animations'))
+ c.add_opt
return ConfigProxy(c)
config = _config()
diff --git a/src/calibre/gui2/preferences/plugins.py b/src/calibre/gui2/preferences/plugins.py
index ba5b921d44..1edd4fe5f9 100644
--- a/src/calibre/gui2/preferences/plugins.py
+++ b/src/calibre/gui2/preferences/plugins.py
@@ -17,11 +17,14 @@ from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin
remove_plugin
from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, \
question_dialog
+from calibre.utils.search_query_parser import SearchQueryParser
+from calibre.utils.icu import lower
-class PluginModel(QAbstractItemModel): # {{{
+class PluginModel(QAbstractItemModel, SearchQueryParser): # {{{
def __init__(self, *args):
QAbstractItemModel.__init__(self, *args)
+ SearchQueryParser.__init__(self, ['all'])
self.icon = QVariant(QIcon(I('plugins.png')))
p = QIcon(self.icon).pixmap(32, 32, QIcon.Disabled, QIcon.On)
self.disabled_icon = QVariant(QIcon(p))
@@ -40,6 +43,72 @@ class PluginModel(QAbstractItemModel): # {{{
for plugins in self._data.values():
plugins.sort(cmp=lambda x, y: cmp(x.name.lower(), y.name.lower()))
+ def universal_set(self):
+ ans = set([])
+ for c, category in enumerate(self.categories):
+ ans.add((c, -1))
+ for p, plugin in enumerate(self._data[category]):
+ ans.add((c, p))
+ return ans
+
+ def get_matches(self, location, query, candidates=None):
+ if candidates is None:
+ candidates = self.universal_set()
+ ans = set([])
+ if not query:
+ return ans
+ query = lower(query)
+ for c, p in candidates:
+ if p < 0:
+ if query in lower(self.categories[c]):
+ ans.add((c, p))
+ else:
+ try:
+ plugin = self._data[self.categories[c]][p]
+ except:
+ continue
+ if query in lower(plugin.name) or query in lower(plugin.author) or \
+ query in lower(plugin.description):
+ ans.add((c, p))
+ return ans
+
+ def find(self, query):
+ query = query.strip()
+ matches = self.parse(query)
+ if not matches:
+ return QModelIndex()
+ matches = list(sorted(matches))
+ c, p = matches[0]
+ cat_idx = self.index(c, 0, QModelIndex())
+ if p == -1:
+ return cat_idx
+ return self.index(p, 0, cat_idx)
+
+ def find_next(self, idx, query, backwards=False):
+ query = query.strip()
+ matches = self.parse(query)
+ if not matches:
+ return idx
+ if idx.parent().isValid():
+ loc = (idx.parent().row(), idx.row())
+ else:
+ loc = (idx.row(), -1)
+ if loc not in matches:
+ return self.find(query)
+ if len(matches) == 1:
+ return QModelIndex()
+ matches = list(sorted(matches))
+ i = matches.index(loc)
+ if backwards:
+ ans = i - 1 if i - 1 >= 0 else len(matches)-1
+ else:
+ ans = i + 1 if i + 1 < len(matches) else 0
+
+ ans = matches[ans]
+
+ return self.index(ans[0], 0, QModelIndex()) if ans[1] < 0 else \
+ self.index(ans[1], 0, self.index(ans[0], 0, QModelIndex()))
+
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QModelIndex()
@@ -127,6 +196,7 @@ class PluginModel(QAbstractItemModel): # {{{
return plugin
return NONE
+
# }}}
class ConfigWidget(ConfigWidgetBase, Ui_Form):
@@ -144,6 +214,42 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.customize_plugin_button.clicked.connect(self.customize_plugin)
self.remove_plugin_button.clicked.connect(self.remove_plugin)
self.button_plugin_add.clicked.connect(self.add_plugin)
+ self.search.initialize('plugin_search_history',
+ help_text=_('Search for plugin'))
+ self.search.search.connect(self.find)
+ self.next_button.clicked.connect(self.find_next)
+ self.previous_button.clicked.connect(self.find_previous)
+
+ def find(self, query):
+ idx = self._plugin_model.find(query)
+ if not idx.isValid():
+ return info_dialog(self, _('No matches'),
+ _('Could not find any matching plugins'), show=True,
+ show_copy_button=False)
+ self.highlight_index(idx)
+
+ def highlight_index(self, idx):
+ self.plugin_view.scrollTo(idx)
+ self.plugin_view.selectionModel().select(idx,
+ self.plugin_view.selectionModel().ClearAndSelect)
+ self.plugin_view.setCurrentIndex(idx)
+
+ def find_next(self, *args):
+ idx = self.plugin_view.currentIndex()
+ if not idx.isValid():
+ idx = self._plugin_model.index(0, 0)
+ idx = self._plugin_model.find_next(idx,
+ unicode(self.search.currentText()))
+ self.highlight_index(idx)
+
+ def find_previous(self, *args):
+ idx = self.plugin_view.currentIndex()
+ if not idx.isValid():
+ idx = self._plugin_model.index(0, 0)
+ idx = self._plugin_model.find_next(idx,
+ unicode(self.search.currentText()), backwards=True)
+ self.highlight_index(idx)
+
def toggle_plugin(self, *args):
self.modify_plugin(op='toggle')
@@ -184,13 +290,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
show=True, show_copy_button=False)
idx = self._plugin_model.plugin_to_index_by_properties(plugin)
if idx.isValid():
- self.plugin_view.scrollTo(idx,
- self.plugin_view.PositionAtCenter)
- self.plugin_view.scrollTo(idx,
- self.plugin_view.PositionAtCenter)
- self.plugin_view.selectionModel().select(idx,
- self.plugin_view.selectionModel().ClearAndSelect)
- self.plugin_view.setCurrentIndex(idx)
+ self.highlight_index(idx)
else:
error_dialog(self, _('No valid plugin path'),
_('%s is not a valid plugin path')%path).exec_()
diff --git a/src/calibre/gui2/preferences/plugins.ui b/src/calibre/gui2/preferences/plugins.ui
index 83a904eb08..ebf422dfe3 100644
--- a/src/calibre/gui2/preferences/plugins.ui
+++ b/src/calibre/gui2/preferences/plugins.ui
@@ -24,6 +24,47 @@
+ -
+
+
-
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ &Next
+
+
+
+ :/images/arrow-down.png:/images/arrow-down.png
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ &Previous
+
+
+
+ :/images/arrow-up.png:/images/arrow-up.png
+
+
+
+
+
-
@@ -84,6 +125,13 @@
+
+
+ SearchBox2
+ QComboBox
+ calibre/gui2/search_box.h
+
+
diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py
index 4e4da9d1df..a50ca20fc1 100644
--- a/src/calibre/utils/search_query_parser.py
+++ b/src/calibre/utils/search_query_parser.py
@@ -260,12 +260,12 @@ class SearchQueryParser(object):
'''
Should return the set of matches for :param:'location` and :param:`query`.
- The search must be performed over all entries is :param:`candidates` is
+ The search must be performed over all entries if :param:`candidates` is
None otherwise only over the items in candidates.
:param:`location` is one of the items in :member:`SearchQueryParser.DEFAULT_LOCATIONS`.
:param:`query` is a string literal.
- :param: None or a subset of the set returned by :meth:`universal_set`.
+ :return: None or a subset of the set returned by :meth:`universal_set`.
'''
return set([])