diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index d0f986209c..22e4900740 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -705,13 +705,17 @@ class ActionTweakEpub(InterfaceActionBase): name = 'Tweak ePub' actual_plugin = 'calibre.gui2.actions.tweak_epub:TweakEpubAction' +class ActionNextMatch(InterfaceActionBase): + name = 'Next Match' + actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, ActionRestart, ActionOpenFolder, ActionConnectShare, ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary, - ActionCopyToLibrary, ActionTweakEpub] + ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch] # }}} diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index b54d346904..8801777953 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -111,7 +111,10 @@ class InterfaceAction(QObject): action.setWhatsThis(text) action.setAutoRepeat(False) if shortcut: - action.setShortcut(shortcut) + if isinstance(shortcut, list): + action.setShortcuts(shortcut) + else: + action.setShortcut(shortcut) setattr(self, attr, action) return action @@ -170,6 +173,14 @@ class InterfaceAction(QObject): ''' pass + def gui_layout_complete(self): + ''' + Called once per action when the layout of the main GUI is + completed. If your action needs to make changes to the layout, they + should be done here, rather than in :meth:`initialization_complete`. + ''' + pass + def initialization_complete(self): ''' Called once per action when the initialization of the main GUI is diff --git a/src/calibre/gui2/actions/next_match.py b/src/calibre/gui2/actions/next_match.py new file mode 100644 index 0000000000..79de6a2d9b --- /dev/null +++ b/src/calibre/gui2/actions/next_match.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from calibre.gui2.actions import InterfaceAction + +class NextMatchAction(InterfaceAction): + name = 'Move to next highlighted book' + action_spec = (_('Move to next match'), 'arrow-down.png', + _('Move to next highlighted match'), [_('N'), _('F3')]) + dont_add_to = frozenset(['toolbar-device', 'context-menu-device']) + action_type = 'current' + + def genesis(self): + ''' + Setup this plugin. Only called once during initialization. self.gui is + available. The action secified by :attr:`action_spec` is available as + ``self.qaction``. + ''' + self.can_move = None + self.qaction.triggered.connect(self.move_forward) + self.create_action(spec=(_('Move to previous item'), 'arrow-up.png', + _('Move to previous highlighted item'), [_('Shift+N'), + _('Shift+F3')]), attr='p_action') + self.gui.addAction(self.p_action) + self.p_action.triggered.connect(self.move_backward) + + def gui_layout_complete(self): + self.gui.search_highlight_only.setVisible(True) + + def location_selected(self, loc): + self.can_move = loc == 'library' + try: + self.gui.search_highlight_only.setVisible(self.can_move) + except: + import traceback + traceback.print_exc() + + def move_forward(self): + if self.can_move is None: + self.can_move = self.gui.current_view() is self.gui.library_view + self.gui.search_highlight_only.setVisible(self.can_move) + + if self.can_move: + self.gui.current_view().move_highlighted_row(forward=True) + + def move_backward(self): + if self.can_move is None: + self.can_move = self.gui.current_view() is self.gui.library_view + self.gui.search_highlight_only.setVisible(self.can_move) + + if self.can_move: + self.gui.current_view().move_highlighted_row(forward=False) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 2edf19d0c4..c1d9498075 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -196,9 +196,11 @@ class SearchBar(QWidget): # {{{ x = parent.search_highlight_only = QCheckBox() x.setText(_('&Highlight')) - x.setToolTip(_('Highlight matched books in the book list, instead ' - 'of restricting the book list to the matches.')) + x.setToolTip('

'+_('When searching, highlight matched books, instead ' + 'of restricting the book list to the matches.

You can use the ' + 'N or F3 keys to go to the next match.')) l.addWidget(x) + x.setVisible(False) x = parent.saved_search = SavedSearchBox(self) x.setMaximumSize(QSize(150, 16777215)) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index eea452c238..6fa23c2813 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -93,8 +93,9 @@ class BooksModel(QAbstractTableModel): # {{{ self.bool_no_icon = QIcon(I('list_remove.png')) self.bool_blank_icon = QIcon(I('blank.png')) self.device_connected = False - self.rows_matching = set() - self.lowest_row_matching = None + self.rows_to_highlight = [] + self.rows_to_highlight_set = set() + self.current_highlighted_row = None self.highlight_only = False self.read_config() @@ -130,6 +131,9 @@ class BooksModel(QAbstractTableModel): # {{{ self.book_on_device = func def set_database(self, db): + self.rows_to_highlight = [] + self.rows_to_highlight_set = set() + self.current_highlighted_row = None self.db = db self.custom_columns = self.db.field_metadata.custom_field_metadata() self.column_map = list(self.orig_headers.keys()) + \ @@ -237,21 +241,43 @@ class BooksModel(QAbstractTableModel): # {{{ if self.last_search: self.research() + def get_current_highlighted_row(self): + if len(self.rows_to_highlight) == 0 or self.current_highlighted_row is None: + return None + try: + return self.rows_to_highlight[self.current_highlighted_row] + except: + return None + + def get_next_highlighted_row(self, forward): + if len(self.rows_to_highlight) == 0 or self.current_highlighted_row is None: + return None + self.current_highlighted_row += 1 if forward else -1 + if self.current_highlighted_row < 0: + self.current_highlighted_row = len(self.rows_to_highlight) - 1; + elif self.current_highlighted_row >= len(self.rows_to_highlight): + self.current_highlighted_row = 0 + return self.get_current_highlighted_row() + def search(self, text, reset=True): try: if self.highlight_only: self.db.search('') if not text: - self.rows_matching = set() - self.lowest_row_matching = None + self.rows_to_highlight = [] + self.rows_to_highlight_set = set() + self.current_highlighted_row = None else: - self.rows_matching = self.db.search(text, return_matches=True) - if self.rows_matching: - self.lowest_row_matching = self.db.row(self.rows_matching[0]) - self.rows_matching = set(self.rows_matching) + self.rows_to_highlight = self.db.search(text, return_matches=True) + self.rows_to_highlight_set = set(self.rows_to_highlight) + if self.rows_to_highlight: + self.current_highlighted_row = 0 + else: + self.current_highlighted_row = None else: - self.rows_matching = set() - self.lowest_row_matching = None + self.rows_to_highlight = [] + self.rows_to_highlight_set = set() + self.current_highlighted_row = None self.db.search(text) except ParseException as e: self.searched.emit(e.msg) @@ -674,7 +700,7 @@ class BooksModel(QAbstractTableModel): # {{{ if role in (Qt.DisplayRole, Qt.EditRole): return self.column_to_dc_map[col](index.row()) elif role == Qt.BackgroundColorRole: - if self.id(index) in self.rows_matching: + if self.id(index) in self.rows_to_highlight_set: return QColor('lightgreen') elif role == Qt.DecorationRole: if self.column_to_dc_decorator_map[col] is not None: diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index ea2e03fdad..e5cc259244 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -680,10 +680,16 @@ class BooksView(QTableView): # {{{ def set_editable(self, editable, supports_backloading): self._model.set_editable(editable) + def move_highlighted_row(self, forward): + id_to_select = self._model.get_next_highlighted_row(forward) + if id_to_select is not None: + self.select_rows([id_to_select], using_ids=True) + def search_proxy(self, txt): self._model.search(txt) - if self._model.lowest_row_matching is not None: - self.select_rows([self._model.lowest_row_matching], using_ids=False) + id_to_select = self._model.get_current_highlighted_row() + if id_to_select is not None: + self.select_rows([id_to_select], using_ids=True) self.setFocus(Qt.OtherFocusReason) def connect_to_search_box(self, sb, search_done): diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index b2d6d329f5..79bd1decc5 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -256,6 +256,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.height()) self.resize(self.width(), self._calculated_available_height) + for ac in self.iactions.values(): + try: + ac.gui_layout_complete() + except: + import traceback + traceback.print_exc() + if ac.plugin_path is None: + raise if config['autolaunch_server']: self.start_content_server() diff --git a/src/calibre/manual/gui.rst b/src/calibre/manual/gui.rst index 28fd0307d3..9a65d80384 100644 --- a/src/calibre/manual/gui.rst +++ b/src/calibre/manual/gui.rst @@ -478,6 +478,10 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes - Focus the search bar * - :kbd:`Shift+Ctrl+F` - Open the advanced search dialog + * - :kbd:`N or F3` + - Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked) + * - :kbd:`Shift+N or Shift+F3` + - Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked) * - :kbd:`Ctrl+D` - Download metadata and shortcuts * - :kbd:`Ctrl+R`