From f20e426efb84353d9c74fe6a13bc671a01332453 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Wed, 19 Oct 2022 11:54:41 +0100 Subject: [PATCH] Make tools on the search bar customizable. Given that the saved searches action and the saved search button on the search bar now do the same thing, it seems reasonable to allow the user to choose whether the saved search button shows. I went from there to "Why not allow adding other buttons to the bar?" There is a lot of available real estate on that bar. This commit does that using a new toolbar in preferences / toolbars. I didn't use a QToolBar, instead constructing a 'classic' toolbar in an QHBoxLayout. That gave me more control over the look and placement. And also, I had no end of trouble trying to work through how to use a QToolBar that isn't attached to a QMainWIndow. In the process I removed the legacy Saved Search combo box, its associated buttons, and the tweak. I am sure there are some people still using the years-old interface and that they will complain. I can accept that, given that the button is better in almost every way. --- resources/default_tweaks.py | 6 - src/calibre/gui2/__init__.py | 3 + src/calibre/gui2/bars.py | 31 +++- src/calibre/gui2/layout.py | 42 +---- src/calibre/gui2/preferences/search.py | 2 + src/calibre/gui2/preferences/search.ui | 13 +- src/calibre/gui2/preferences/toolbar.py | 1 + src/calibre/gui2/search_box.py | 210 +++--------------------- 8 files changed, 78 insertions(+), 230 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index b7c02dbea7..3f8c016722 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -522,12 +522,6 @@ content_server_thumbnail_compression_quality = 75 # cover_drop_exclude = {'tiff', 'webp'} cover_drop_exclude = () -#: Show the Saved searches box in the Search bar -# In newer versions of calibre, only a single button that allows you to add a -# new Saved search is shown in the Search bar. If you would like to have the -# old Saved searches box with its two buttons back, set this tweak to True. -show_saved_search_box = False - #: Exclude fields when copy/pasting metadata # You can ask calibre to not paste some metadata fields when using the # Edit metadata->Copy metadata/Paste metadata actions. For example, diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 5b7ea82709..480b9e9581 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -284,6 +284,8 @@ def create_defs(): defs['action-layout-toolbar-child'] = () + defs['action-layout-searchbar'] = ('Saved searches',) + defs['action-layout-context-menu'] = ( 'Edit Metadata', 'Send To Device', 'Save To Disk', 'Connect Share', 'Copy To Library', None, @@ -395,6 +397,7 @@ def create_defs(): defs['edit_metadata_templates_only_F2_on_booklist'] = False # JSON dumps converts integer keys to strings, so do it explicitly defs['tb_search_order'] = {'0': 1, '1': 2, '2': 3, '3': 4, '4': 0} + defs['search_tool_bar_shows_text'] = True def migrate_tweak(tweak_name, pref_name): # If the tweak has been changed then leave the tweak in the file so diff --git a/src/calibre/gui2/bars.py b/src/calibre/gui2/bars.py index 14909aeb36..c3187be1c8 100644 --- a/src/calibre/gui2/bars.py +++ b/src/calibre/gui2/bars.py @@ -8,7 +8,8 @@ __docformat__ = 'restructuredtext en' from functools import partial from qt.core import ( Qt, QAction, QMenu, QObject, QToolBar, QToolButton, QSize, pyqtSignal, QKeySequence, QMenuBar, - QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty, QPainter, QWidget, QPalette, sip) + QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty, QPainter, QWidget, QPalette, sip, + QHBoxLayout) from calibre.constants import ismacos from calibre.gui2 import gprefs, native_menubar_defaults, config @@ -628,6 +629,7 @@ class BarsManager(QObject): self.main_bars = tuple(bars[:2]) self.child_bars = tuple(bars[2:]) self.reveal_bar = RevealBar(parent) + self.search_tool_bar = QHBoxLayout() self.menu_bar = MenuBar(self.location_manager, self.parent()) is_native_menubar = self.menu_bar.is_native_menubar @@ -636,6 +638,7 @@ class BarsManager(QObject): self.menubar_device_fallback = native_menubar_defaults['action-layout-menubar-device'] if is_native_menubar else () self.apply_settings() + self.search_tool_bar_actions = [] self.init_bars() def database_changed(self, db): @@ -669,6 +672,29 @@ class BarsManager(QObject): for bar, actions in zip(self.bars, self.bar_actions[:3]): bar.init_bar(actions) + # Build the layout containing the buttons to go into the search bar + self.build_search_tool_bar() + + def build_search_tool_bar(self): + for ac in self.search_tool_bar_actions: + self.search_tool_bar.removeWidget(ac) + + self.search_tool_bar_actions = [] + for what in gprefs['action-layout-searchbar']: + if what in self.parent().iactions: + qact = self.parent().iactions[what].qaction + tb = QToolButton() + tb.setDefaultAction(qact) + if not gprefs['search_tool_bar_shows_text']: + tb.setText(None) + else: + tb.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + tb.setCursor(Qt.CursorShape.PointingHandCursor) + tb.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + tb.setAutoRaise(True) + self.search_tool_bar.addWidget(tb) + self.search_tool_bar_actions.append(tb) + def update_bars(self, reveal_bar=False): ''' This shows the correct main toolbar and rebuilds the menubar based on @@ -693,6 +719,9 @@ class BarsManager(QObject): self.menu_bar.init_bar(self.bar_actions[4 if showing_device else 3]) self.menu_bar.update_lm_actions() self.menu_bar.setVisible(bool(self.menu_bar.added_actions)) + self.build_search_tool_bar() + from calibre.gui2.ui import get_gui + get_gui().search_bar.update() def apply_settings(self): sz = gprefs['toolbar_icon_size'] diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 1725dc7817..da49012515 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -14,8 +14,7 @@ from qt.core import ( from calibre import human_readable from calibre.constants import __appname__ from calibre.gui2.bars import BarsManager -from calibre.gui2.search_box import SavedSearchBox, SearchBox2 -from calibre.gui2.widgets2 import RightClickButton +from calibre.gui2.search_box import SearchBox2 from calibre.utils.config_base import tweaks @@ -265,40 +264,8 @@ class SearchBar(QFrame): # {{{ x.setIcon(QIcon.ic('arrow-down.png')) l.addWidget(x) - x = parent.saved_search = SavedSearchBox(self) - x.setObjectName("saved_search") - l.addWidget(x) - x.setVisible(tweaks['show_saved_search_box']) - - x = parent.copy_search_button = QToolButton(self) - x.setAutoRaise(True) - x.setCursor(Qt.CursorShape.PointingHandCursor) - x.setIcon(QIcon.ic("search_copy_saved.png")) - x.setObjectName("copy_search_button") - l.addWidget(x) - x.setToolTip(_("Copy current search text (instead of search name)")) - x.setVisible(tweaks['show_saved_search_box']) - - x = parent.save_search_button = RightClickButton(self) - x.setAutoRaise(True) - x.setCursor(Qt.CursorShape.PointingHandCursor) - x.setIcon(QIcon.ic("search_add_saved.png")) - x.setObjectName("save_search_button") - l.addWidget(x) - x.setVisible(tweaks['show_saved_search_box']) - - x = parent.add_saved_search_button = RightClickButton(self) - x.setToolTip(_( - 'Use an existing Saved search or create a new one' - )) - x.setText(_('Saved search')) - x.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) - x.setCursor(Qt.CursorShape.PointingHandCursor) - x.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) - x.setAutoRaise(True) - x.setIcon(QIcon.ic("folder_saved_search.png")) - l.addWidget(x) - x.setVisible(not tweaks['show_saved_search_box']) + # Add the searchbar tool buttons to the bar + l.addLayout(self.parent().bars_manager.search_tool_bar) def populate_sort_menu(self): from calibre.gui2.ui import get_gui @@ -342,9 +309,10 @@ class MainWindowMixin: # {{{ self.iactions['Fetch News'].init_scheduler() - self.search_bar = SearchBar(self) self.bars_manager = BarsManager(self.donate_action, self.location_manager, self) + # instantiating SearchBar must happen after setting bars manager + self.search_bar = SearchBar(self) for bar in self.bars_manager.main_bars: self.addToolBar(Qt.ToolBarArea.TopToolBarArea, bar) bar.setStyleSheet('QToolBar { border: 0px }') diff --git a/src/calibre/gui2/preferences/search.py b/src/calibre/gui2/preferences/search.py index 17ca258514..30a48a97c2 100644 --- a/src/calibre/gui2/preferences/search.py +++ b/src/calibre/gui2/preferences/search.py @@ -32,6 +32,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('show_highlight_toggle_button', gprefs) r('limit_search_columns', prefs) r('use_primary_find_in_search', prefs) + r('search_tool_bar_shows_text', gprefs) r('case_sensitive', prefs) fl = db.field_metadata.get_search_terms() r('limit_search_columns_to', prefs, setting=CommaSeparatedList, choices=fl) @@ -236,6 +237,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): return ConfigWidgetBase.commit(self) def refresh_gui(self, gui): + gui.refresh_search_bar_widgets() gui.current_db.new_api.clear_caches() set_use_primary_find_in_search(prefs['use_primary_find_in_search']) gui.set_highlight_only_button_icon() diff --git a/src/calibre/gui2/preferences/search.ui b/src/calibre/gui2/preferences/search.ui index 39768af479..ebc1957cc6 100644 --- a/src/calibre/gui2/preferences/search.ui +++ b/src/calibre/gui2/preferences/search.ui @@ -24,7 +24,7 @@ Genera&l - + @@ -40,7 +40,7 @@ - + What to search by default @@ -120,7 +120,7 @@ - + Qt::Vertical @@ -140,6 +140,13 @@ + + + + Show text next to buttons in the search bar + + + diff --git a/src/calibre/gui2/preferences/toolbar.py b/src/calibre/gui2/preferences/toolbar.py index 39fe7a223e..72f3acd2ef 100644 --- a/src/calibre/gui2/preferences/toolbar.py +++ b/src/calibre/gui2/preferences/toolbar.py @@ -245,6 +245,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): ('toolbar', _('The main toolbar')), ('toolbar-device', _('The main toolbar when a device is connected')), ('toolbar-child', _('The optional second toolbar')), + ('searchbar', ('The buttons on the search bar')), ('menubar', _('The menubar')), ('menubar-device', _('The menubar when a device is connected')), ('context-menu', _('The context menu for the books in the ' diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 13ba3a215d..546b1f7eb2 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -12,10 +12,9 @@ from functools import partial from qt.core import ( QComboBox, Qt, QLineEdit, pyqtSlot, QDialog, QEvent, pyqtSignal, QCompleter, QAction, QKeySequence, QTimer, - QIcon, QMenu, QApplication, QKeyEvent) + QIcon, QApplication, QKeyEvent) -from calibre.gui2 import config, error_dialog, question_dialog, gprefs, QT_HIDDEN_CLEAR_ACTION -from calibre.gui2.dialogs.confirm_delete import confirm +from calibre.gui2 import config, question_dialog, gprefs, QT_HIDDEN_CLEAR_ACTION from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor from calibre.gui2.dialogs.search import SearchDialog from calibre.utils.icu import primary_sort_key @@ -320,147 +319,6 @@ class SearchBox2(QComboBox): # {{{ # }}} -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. - ''' - - changed = pyqtSignal() - - def __init__(self, parent=None): - QComboBox.__init__(self, parent) - - self.line_edit = SearchLineEdit(self) - self.setLineEdit(self.line_edit) - self.line_edit.key_pressed.connect(self.key_pressed, type=Qt.ConnectionType.DirectConnection) - self.textActivated.connect(self.saved_search_selected) - - # Turn off auto-completion so that it doesn't interfere with typing - # names of new searches. - completer = QCompleter(self) - self.setCompleter(completer) - - self.setEditable(True) - self.setMaxVisibleItems(25) - self.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) - self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) - self.setMinimumContentsLength(25) - self.tool_tip_text = self.toolTip() - - def initialize(self, _search_box, colorize=False, help_text=_('Search')): - self.search_box = _search_box - try: - self.line_edit.setPlaceholderText(help_text) - except: - # Using Qt < 4.7 - pass - self.colorize = colorize - self.clear() - - def normalize_state(self): - # need this because line_edit will call it in some cases such as paste - pass - - def clear(self): - QComboBox.clear(self) - self.initialize_saved_search_names() - self.setEditText('') - self.setToolTip(self.tool_tip_text) - self.line_edit.home(False) - - def key_pressed(self, event): - if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): - self.saved_search_selected(self.currentText()) - - def saved_search_selected(self, qname): - from calibre.gui2.ui import get_gui - db = get_gui().current_db - qname = str(qname) - if qname is None or not qname.strip(): - self.search_box.clear() - return - if not db.saved_search_lookup(qname): - self.search_box.clear() - self.setEditText(qname) - return - self.search_box.set_search_string('search:"%s"' % qname, emit_changed=False) - self.setEditText(qname) - self.setToolTip(db.saved_search_lookup(qname)) - - def initialize_saved_search_names(self): - from calibre.gui2.ui import get_gui - gui = get_gui() - try: - names = gui.current_db.saved_search_names() - except AttributeError: - # Happens during gui initialization - names = [] - self.addItems(names) - self.setCurrentIndex(-1) - - # SIGNALed from the main UI - def save_search_button_clicked(self): - from calibre.gui2.ui import get_gui - db = get_gui().current_db - name = str(self.currentText()) - if not name.strip(): - name = str(self.search_box.text()).replace('"', '') - name = name.replace('\\', '') - if not name: - error_dialog(self, _('Create saved search'), - _('Invalid saved search name. ' - 'It must contain at least one letter or number'), show=True) - return - if not self.search_box.text(): - error_dialog(self, _('Create saved search'), - _('There is no search to save'), show=True) - return - db.saved_search_delete(name) - db.saved_search_add(name, str(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() - self.setCurrentIndex(self.findText(name)) - self.saved_search_selected(name) - self.changed.emit() - - def delete_current_search(self): - from calibre.gui2.ui import get_gui - db = get_gui().current_db - idx = self.currentIndex() - if idx <= 0: - error_dialog(self, _('Delete current search'), - _('No search is selected'), show=True) - return - if not confirm('

'+_('The selected search will be ' - 'permanently deleted. Are you sure?') + - '

', 'saved_search_delete', self): - return - ss = db.saved_search_lookup(str(self.currentText())) - if ss is None: - return - db.saved_search_delete(str(self.currentText())) - self.clear() - self.search_box.clear() - self.changed.emit() - - # SIGNALed from the main UI - def copy_search_button_clicked(self): - from calibre.gui2.ui import get_gui - db = get_gui().current_db - idx = self.currentIndex() - if idx < 0: - return - self.search_box.set_search_string(db.saved_search_lookup(str(self.currentText()))) - - # }}} - - class SearchBoxMixin: # {{{ def __init__(self, *args, **kwargs): @@ -497,6 +355,16 @@ class SearchBoxMixin: # {{{ self.highlight_only_action = ac = QAction(self) self.addAction(ac), ac.triggered.connect(self.highlight_only_clicked) self.keyboard.register_shortcut('highlight search results', _('Highlight search results'), action=self.highlight_only_action) + self.refresh_search_bar_widgets() + + def refresh_search_bar_widgets(self): + self.set_highlight_only_button_icon() + if gprefs['search_tool_bar_shows_text']: + self.search_bar.search_button.setText(_('Search')) + self.search_bar.search_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + else: + self.search_bar.search_button.setText(None) + self.search_bar.search_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) def highlight_only_clicked(self, state): if not config['highlight_search_matches'] and not question_dialog(self, _('Are you sure?'), @@ -513,10 +381,20 @@ class SearchBoxMixin: # {{{ b = self.highlight_only_button if config['highlight_search_matches']: b.setIcon(QIcon.ic('highlight_only_on.png')) - b.setText(_('Filter')) + if gprefs['search_tool_bar_shows_text']: + b.setText(_('Filter')) + b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + else: + b.setText(None) + b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) else: b.setIcon(QIcon.ic('highlight_only_off.png')) - b.setText(_('Highlight')) + if gprefs['search_tool_bar_shows_text']: + b.setText(_('Highlight')) + b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + else: + b.setText(None) + b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.highlight_only_button.setVisible(gprefs['show_highlight_toggle_button']) self.library_view.model().set_highlight_only(config['highlight_search_matches']) @@ -526,11 +404,9 @@ class SearchBoxMixin: # {{{ def search_box_cleared(self): self.tags_view.clear() - self.saved_search.clear() self.set_number_of_books_shown() def search_box_changed(self): - self.saved_search.clear() self.tags_view.conditional_clear(self.search.current_text) def do_advanced_search(self, *args): @@ -554,41 +430,10 @@ class SavedSearchBoxMixin: # {{{ pass def init_saved_seach_box_mixin(self): - self.saved_search.changed.connect(self.saved_searches_changed) - ac = self.search.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) - if ac is not None: - ac.triggered.connect(self.saved_search.clear) - self.save_search_button.clicked.connect( - self.saved_search.save_search_button_clicked) - self.copy_search_button.clicked.connect( - self.saved_search.copy_search_button_clicked) - # self.saved_searches_changed() - self.saved_search.initialize(self.search, colorize=True, - help_text=_('Saved searches')) - self.saved_search.tool_tip_text=_('Choose saved search or enter name for new saved search') - self.saved_search.setToolTip(self.saved_search.tool_tip_text) - self.saved_search.setStatusTip(self.saved_search.tool_tip_text) - for x in ('copy', 'save'): - b = getattr(self, x+'_search_button') - b.setStatusTip(b.toolTip()) - self.save_search_button.setToolTip('

' + - _("Save current search under the name shown in the box. " - "Press and hold for a pop-up options menu.") + '

') - self.save_search_button.setMenu(QMenu(self.save_search_button)) - self.save_search_button.menu().addAction( - QIcon.ic('search_add_saved.png'), - _('Create Saved search'), - self.saved_search.save_search_button_clicked) - self.save_search_button.menu().addAction( - QIcon.ic('search_delete_saved.png'), _('Delete Saved search'), self.saved_search.delete_current_search) - self.save_search_button.menu().addAction( - QIcon.ic('search.png'), _('Manage Saved searches'), partial(self.do_saved_search_edit, None)) - self.add_saved_search_button.setMenu(QMenu(self.add_saved_search_button)) - self.add_saved_search_button.menu().aboutToShow.connect(self.populate_add_saved_search_menu) + pass - - def populate_add_saved_search_menu(self, to_menu=None): - m = to_menu if to_menu is not None else self.add_saved_search_button.menu() + def populate_add_saved_search_menu(self, to_menu): + m = to_menu m.clear() m.clear() m.addAction(QIcon.ic('search_add_saved.png'), _('Add Saved search'), self.add_saved_search) @@ -631,7 +476,6 @@ class SavedSearchBoxMixin: # {{{ def do_rebuild_saved_searches(self): self.saved_searches_changed() - self.saved_search.clear() def add_saved_search(self): from calibre.gui2.dialogs.saved_search_editor import AddSavedSearch