Enhancement 2052897: save and restore GUI panel layouts.

I added this to the existing Layout Actions. In addition I added the ability to toggle panels on/off in addition to turning them on and off.

I am unable to test the docstrings.
This commit is contained in:
Charles Haley 2024-02-12 17:15:12 +00:00
parent 04adea1b10
commit 1f1c777886
3 changed files with 214 additions and 12 deletions

View File

@ -1084,7 +1084,7 @@ class ActionSavedSearches(InterfaceActionBase):
class ActionLayoutActions(InterfaceActionBase): class ActionLayoutActions(InterfaceActionBase):
name = 'Layout actions' name = 'Layout Actions'
author = 'Charles Haley' author = 'Charles Haley'
actual_plugin = 'calibre.gui2.actions.layout_actions:LayoutActions' actual_plugin = 'calibre.gui2.actions.layout_actions:LayoutActions'
description = _("Show a menu of actions to change calibre's layout") description = _("Show a menu of actions to change calibre's layout")

View File

@ -431,6 +431,7 @@ def create_defs():
defs['light_palette_name'] = '' defs['light_palette_name'] = ''
defs['dark_palettes'] = {} defs['dark_palettes'] = {}
defs['light_palettes'] = {} defs['light_palettes'] = {}
defs['saved_layouts'] = {}
def migrate_tweak(tweak_name, pref_name): def migrate_tweak(tweak_name, pref_name):
# If the tweak has been changed then leave the tweak in the file so # If the tweak has been changed then leave the tweak in the file so

View File

@ -3,9 +3,12 @@
from enum import Enum from enum import Enum
from functools import partial from functools import partial
from qt.core import QToolButton from qt.core import (QComboBox, QDialog, QDialogButtonBox, QFormLayout, QIcon,
QLabel, QMenu, QToolButton, QVBoxLayout)
from calibre.gui2.actions import InterfaceAction from calibre.gui2 import error_dialog, gprefs, question_dialog
from calibre.gui2.actions import InterfaceAction, show_menu_under_widget
from calibre.utils.icu import sort_key
class Panel(Enum): class Panel(Enum):
@ -18,32 +21,127 @@ class Panel(Enum):
QUICKVIEW = 'qv' QUICKVIEW = 'qv'
class SaveLayoutDialog(QDialog):
def __init__(self, parent, names):
QDialog.__init__(self, parent)
self.names = names
l = QVBoxLayout(self)
fl = QFormLayout()
l.addLayout(fl)
self.cb = cb = QComboBox()
cb.setEditable(True)
cb.setMinimumWidth(200)
cb.addItem('')
cb.addItems(sorted(names, key=sort_key))
fl.addRow(QLabel(_('Layout name')), cb)
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
l.addWidget(bb)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
def current_name(self):
return self.cb.currentText().strip()
def accept(self):
n = self.current_name()
if not n:
error_dialog(self, _('Invalid name'), _('The settings name cannot be blank'),
show=True, show_copy_button=False)
return
if self.current_name() in self.names:
r = question_dialog(self, _('Replace saved layout'),
_('Do you really want to overwrite the saved layout {0}?').format(self.current_name()))
if r == QDialog.DialogCode.Accepted:
super().accept()
else:
return
super().accept()
class LayoutActions(InterfaceAction): class LayoutActions(InterfaceAction):
name = 'Layout Actions' name = 'Layout Actions'
action_spec = (_('Layout actions'), 'layout.png', action_spec = (_('Layout actions'), 'layout.png',
_('Add/remove layout items: search bar, tag browser, etc.'), None) _("Save and restore layout item sizes, and add/remove/toggle "
"layout items such as the search bar, tag browser, etc. "
"Item sizes in saved layouts are saved as a percentage of "
"the window size. Restoring a layout doesn't change the "
"window size, instead fitting the items into the current window."), None)
action_type = 'current' action_type = 'current'
popup_type = QToolButton.ToolButtonPopupMode.InstantPopup popup_type = QToolButton.ToolButtonPopupMode.InstantPopup
action_add_menu = True action_add_menu = True
dont_add_to = frozenset({'context-menu-device', 'menubar-device'}) dont_add_to = frozenset({'context-menu-device', 'menubar-device'})
def genesis(self):
self.layout_icon = QIcon.ic('layout.png')
self.menu = m = self.qaction.menu()
m.aboutToShow.connect(self.about_to_show_menu)
# Create a "hidden" menu that can have a shortcut.
self.hidden_menu = QMenu()
self.shortcut_action = self.create_menu_action(
menu=self.hidden_menu,
unique_name='Main window layout',
shortcut=None,
text=_("Save and restore layout item sizes, and add/remove/toggle "
"layout items such as the search bar, tag browser, etc. "),
icon='layout.png',
triggered=self.show_menu)
# We want to show the menu when a shortcut is used. Apparently the only way
# to do that is to scan the toolbar(s) for the action button then exec the
# associated menu. The search is done here to take adding and removing the
# action from toolbars into account.
#
# If a shortcut is triggered and there isn't a toolbar button visible then
# show the menu in the upper left corner of the library view pane. Yes, this
# is a bit weird but it works as well as a popping up a dialog.
def show_menu(self):
show_menu_under_widget(self.gui, self.menu, self.qaction, self.name)
def toggle_layout(self): def toggle_layout(self):
self.gui.layout_container.toggle_layout() self.gui.layout_container.toggle_layout()
def gui_layout_complete(self): def gui_layout_complete(self):
m = self.qaction.menu() m = self.qaction.menu()
m.aboutToShow.connect(self.populate_layout_menu) m.aboutToShow.connect(self.about_to_show_menu)
def populate_layout_menu(self): def initialization_complete(self):
self.populate_menu()
def about_to_show_menu(self):
self.populate_menu()
def populate_menu(self):
m = self.qaction.menu() m = self.qaction.menu()
m.clear() m.clear()
lm = m.addMenu(self.layout_icon, _('Restore saved layout'))
layouts = gprefs['saved_layouts']
if layouts:
for l in sorted(layouts, key=sort_key):
lm.addAction(self.layout_icon, l, partial(self.apply_layout, l))
else:
lm.setEnabled(False)
lm = m.addAction(self.layout_icon, _('Save current layout'))
lm.triggered.connect(self.save_current_layout)
lm = m.addMenu(self.layout_icon, _('Delete saved layout'))
layouts = gprefs['saved_layouts']
if layouts:
for l in sorted(layouts, key=sort_key):
lm.addAction(self.layout_icon, l, partial(self.delete_layout, l))
else:
lm.setEnabled(False)
m.addSeparator()
m.addAction(_('Hide all'), self.hide_all) m.addAction(_('Hide all'), self.hide_all)
for button, name in zip(self.gui.layout_buttons, self.gui.button_order): for button, name in zip(self.gui.layout_buttons, self.gui.button_order):
m.addSeparator() m.addSeparator()
ic = button.icon() ic = button.icon()
m.addAction(ic, _('Show {}').format(button.label), partial(self.set_visible, Panel(name), True)) m.addAction(ic, _('Show {}').format(button.label), partial(self.set_visible, Panel(name), True))
m.addAction(ic, _('Hide {}').format(button.label), partial(self.set_visible, Panel(name), False)) m.addAction(ic, _('Hide {}').format(button.label), partial(self.set_visible, Panel(name), False))
m.addAction(ic, _('Toggle {}').format(button.label), partial(self.toggle_item, Panel(name)))
def _change_item(self, button, show=True): def _change_item(self, button, show=True):
if button.isChecked() and not show: if button.isChecked() and not show:
@ -51,26 +149,129 @@ class LayoutActions(InterfaceAction):
elif not button.isChecked() and show: elif not button.isChecked() and show:
button.click() button.click()
def _toggle_item(self, button):
button.click()
def _button_from_enum(self, name: Panel): def _button_from_enum(self, name: Panel):
for q, b in zip(self.gui.button_order, self.gui.layout_buttons): for q, b in zip(self.gui.button_order, self.gui.layout_buttons):
if q == name.value: if q == name.value:
return b return b
def set_visible(self, name: Panel, show=True): # Public API
def apply_layout(self, name):
'''apply_layout()
Apply a saved GUI panel layout.
:param:`name` The name of the saved layout
Throws KeyError if the name doesn't exist.
''' '''
Show or hide the panel. Does nothing if the panel is already in the layouts = gprefs['saved_layouts']
# This can be called by plugins so let the exception fly
settings = layouts[name]
# Order is important here. change_layout() must be called before
# unserializing the settings or panes like book details won't display
# properly.
self.gui.layout_container.change_layout(self.gui, settings['layout'] == 'wide')
self.gui.layout_container.unserialize_settings(settings)
self.gui.layout_container.relayout()
def save_current_layout(self):
'''save_current_layout()
Opens a dialog asking for the name to use to save the current layout.
Saves the current settings under the provided name.
'''
layouts = gprefs['saved_layouts']
d = SaveLayoutDialog(self.gui, layouts.keys())
if d.exec() == QDialog.DialogCode.Accepted:
self.save_named_layout(d.current_name(), self.current_settings())
def current_settings(self):
'''current_settings()
:return: the current gui layout settings.
'''
return self.gui.layout_container.serialized_settings()
def save_named_layout(self, name, settings):
'''save_named_layout()
Saves the settings under the provided name.
:param:`name` The name for the settings.
:param:`settings`: The gui layout settings to save.
'''
layouts = gprefs['saved_layouts']
layouts.update({name: settings})
gprefs['saved_layouts'] = layouts
self.populate_menu()
def delete_layout(self, name, show_warning=True):
'''delete_layout()
Delete a saved layout.
:param:`name` The name of the layout to delete
:param:`show_warning`: If True a warning dialog will be shown before deleting the layout.
'''
if show_warning:
if not question_dialog(self.gui, _('Are you sure?'),
_('Do you really want to delete the saved layout {0}').format(name),
skip_dialog_name='delete_saved_gui_layout'):
return
layouts = gprefs['saved_layouts']
layouts.pop(name, None)
self.populate_menu()
def saved_layout_names(self):
'''saved_layout_names()
Get a list of saved layout names
:return: the sorted list of names. The list is empty if there are no names.
'''
layouts = gprefs['saved_layouts']
return sorted(layouts.keys(), key=sort_key)
def toggle_item(self, name):
'''toggle_item()
Toggle the visibility of the panel.
:param name: specifies which panel to toggle. Valid names are
SEARCH_BAR: 'sb'
TAG_BROWSER: 'tb'
BOOK_DETAILS: 'bd'
GRID_VIEW: 'gv'
COVER_BROWSER: 'cb'
QUICKVIEW: 'qv'
'''
self._toggle_item(self._button_from_enum(name))
def set_visible(self, name: Panel, show=True):
'''set_visible()
Show or hide a panel. Does nothing if the panel is already in the
desired state. desired state.
:param name: specifies which panel using a Panel enum :param name: specifies which panel to show. Valid names are
SEARCH_BAR: 'sb'
TAG_BROWSER: 'tb'
BOOK_DETAILS: 'bd'
GRID_VIEW: 'gv'
COVER_BROWSER: 'cb'
QUICKVIEW: 'qv'
:param show: If True, show the panel, otherwise hide the panel :param show: If True, show the panel, otherwise hide the panel
''' '''
self._change_item(self._button_from_enum(name), show) self._change_item(self._button_from_enum(name), show)
def is_visible(self, name: Panel): def is_visible(self, name: Panel):
''' '''is_visible()
Returns True if the panel is visible. Returns True if the panel is visible.
:param name: specifies which panel using a Panel enum :param name: specifies which panel. Valid names are
SEARCH_BAR: 'sb'
TAG_BROWSER: 'tb'
BOOK_DETAILS: 'bd'
GRID_VIEW: 'gv'
COVER_BROWSER: 'cb'
QUICKVIEW: 'qv'
''' '''
self._button_from_enum(name).isChecked() self._button_from_enum(name).isChecked()
@ -83,7 +284,7 @@ class LayoutActions(InterfaceAction):
self.set_visible(Panel(name), show=True) self.set_visible(Panel(name), show=True)
def panel_titles(self): def panel_titles(self):
''' '''panel_titles()
Return a dictionary of Panel Enum items to translated human readable title. Return a dictionary of Panel Enum items to translated human readable title.
Simplifies building dialogs, for example combo boxes of all the panel Simplifies building dialogs, for example combo boxes of all the panel
names or check boxes for each panel. names or check boxes for each panel.