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.
This commit is contained in:
Charles Haley 2022-10-19 11:54:41 +01:00
parent 3335ebafa3
commit f20e426efb
8 changed files with 78 additions and 230 deletions

View File

@ -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,

View File

@ -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

View File

@ -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']

View File

@ -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 }')

View File

@ -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()

View File

@ -24,7 +24,7 @@
<string>Genera&amp;l</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<item row="6" column="0">
<item row="11" column="0">
<widget class="QPushButton" name="clear_history_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
@ -40,7 +40,7 @@
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<item row="10" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>What to search by default</string>
@ -120,7 +120,7 @@
</property>
</widget>
</item>
<item row="7" column="0">
<item row="20" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -140,6 +140,13 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="opt_search_tool_bar_shows_text">
<property name="text">
<string>Show text next to buttons in the search bar</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">

View File

@ -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 '

View File

@ -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('<p>'+_('The selected search will be '
'<b>permanently deleted</b>. Are you sure?') +
'</p>', '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'))
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'))
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('<p>' +
_("Save current search under the name shown in the box. "
"Press and hold for a pop-up options menu.") + '</p>')
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