openlp/openlp/core/ui/shortcutlistform.py

484 lines
23 KiB
Python

# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2022 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
The :mod:`~openlp.core.ui.shortcutlistform` module contains the form class
"""
import logging
import re
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.actions import ActionList
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.ui.shortcutlistdialog import Ui_ShortcutListDialog
REMOVE_AMPERSAND = re.compile(r'&')
log = logging.getLogger(__name__)
class ShortcutListForm(QtWidgets.QDialog, Ui_ShortcutListDialog, RegistryProperties):
"""
The shortcut list dialog
"""
def __init__(self, parent=None):
"""
Constructor
"""
super(ShortcutListForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.setup_ui(self)
self.changed_actions = {}
self.action_list = ActionList.get_instance()
self.dialog_was_shown = False
self.primary_push_button.toggled.connect(self.on_primary_push_button_clicked)
self.alternate_push_button.toggled.connect(self.on_alternate_push_button_clicked)
self.tree_widget.currentItemChanged.connect(self.on_current_item_changed)
self.tree_widget.itemDoubleClicked.connect(self.on_item_double_clicked)
self.clear_primary_button.clicked.connect(self.on_clear_primary_button_clicked)
self.clear_alternate_button.clicked.connect(self.on_clear_alternate_button_clicked)
self.button_box.clicked.connect(self.on_restore_defaults_clicked)
self.default_radio_button.clicked.connect(self.on_default_radio_button_clicked)
self.custom_radio_button.clicked.connect(self.on_custom_radio_button_clicked)
def keyPressEvent(self, event):
"""
Respond to certain key presses
"""
if event.key() == QtCore.Qt.Key_Space:
self.keyReleaseEvent(event)
elif self.primary_push_button.isChecked() or self.alternate_push_button.isChecked():
self.keyReleaseEvent(event)
elif event.key() == QtCore.Qt.Key_Escape:
event.accept()
self.close()
def keyReleaseEvent(self, event):
"""
Respond to certain key presses
"""
if not self.primary_push_button.isChecked() and not self.alternate_push_button.isChecked():
return
# Do not continue, as the event is for the dialog (close it).
if self.dialog_was_shown and event.key() in (QtCore.Qt.Key_Escape, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.dialog_was_shown = False
return
key = event.key()
if key in (QtCore.Qt.Key_Shift, QtCore.Qt.Key_Control, QtCore.Qt.Key_Meta, QtCore.Qt.Key_Alt):
return
key_string = QtGui.QKeySequence(key).toString()
if event.modifiers() & QtCore.Qt.ControlModifier == QtCore.Qt.ControlModifier:
key_string = 'Ctrl+' + key_string
if event.modifiers() & QtCore.Qt.AltModifier == QtCore.Qt.AltModifier:
key_string = 'Alt+' + key_string
if event.modifiers() & QtCore.Qt.ShiftModifier == QtCore.Qt.ShiftModifier:
key_string = 'Shift+' + key_string
if event.modifiers() & QtCore.Qt.MetaModifier == QtCore.Qt.MetaModifier:
key_string = 'Meta+' + key_string
key_sequence = QtGui.QKeySequence(key_string)
if self._validiate_shortcut(self._current_item_action(), key_sequence):
if self.primary_push_button.isChecked():
self._adjust_button(self.primary_push_button, False,
text=self.get_shortcut_string(key_sequence, for_display=True))
elif self.alternate_push_button.isChecked():
self._adjust_button(self.alternate_push_button, False,
text=self.get_shortcut_string(key_sequence, for_display=True))
def exec(self):
"""
Execute the dialog
"""
self.changed_actions = {}
self.reload_shortcut_list()
self._adjust_button(self.primary_push_button, False, False, '')
self._adjust_button(self.alternate_push_button, False, False, '')
return QtWidgets.QDialog.exec(self)
def reload_shortcut_list(self):
"""
Reload the ``tree_widget`` list to add new and remove old actions.
"""
self.tree_widget.clear()
for category in self.action_list.categories:
# Check if the category is for internal use only.
if category.name is None:
continue
item = QtWidgets.QTreeWidgetItem([category.name])
for action in category.actions:
action_text = REMOVE_AMPERSAND.sub('', action.text())
action_item = QtWidgets.QTreeWidgetItem([action_text])
action_item.setIcon(0, action.icon())
action_item.setData(0, QtCore.Qt.UserRole, action)
tool_tip_text = action.toolTip()
# Only display tool tips if they are helpful.
if tool_tip_text != action_text:
# Display the tool tip in all three colums.
action_item.setToolTip(0, tool_tip_text)
action_item.setToolTip(1, tool_tip_text)
action_item.setToolTip(2, tool_tip_text)
item.addChild(action_item)
self.tree_widget.addTopLevelItem(item)
item.setExpanded(True)
self.refresh_shortcut_list()
def refresh_shortcut_list(self):
"""
This refreshes the item's shortcuts shown in the list. Note, this neither adds new actions nor removes old
actions.
"""
iterator = QtWidgets.QTreeWidgetItemIterator(self.tree_widget)
while iterator.value():
item = iterator.value()
iterator += 1
action = self._current_item_action(item)
if action is None:
continue
shortcuts = self._action_shortcuts(action)
if not shortcuts:
item.setText(1, '')
item.setText(2, '')
elif len(shortcuts) == 1:
item.setText(1, self.get_shortcut_string(shortcuts[0], for_display=True))
item.setText(2, '')
else:
item.setText(1, self.get_shortcut_string(shortcuts[0], for_display=True))
item.setText(2, self.get_shortcut_string(shortcuts[1], for_display=True))
self.on_current_item_changed()
def on_primary_push_button_clicked(self, toggled):
"""
Save the new primary shortcut.
"""
self.custom_radio_button.setChecked(True)
if toggled:
self.alternate_push_button.setChecked(False)
self.primary_push_button.setText('')
return
action = self._current_item_action()
if action is None:
return
shortcuts = self._action_shortcuts(action)
new_shortcuts = [QtGui.QKeySequence(self.primary_push_button.text())]
if len(shortcuts) == 2:
new_shortcuts.append(shortcuts[1])
self.changed_actions[action] = new_shortcuts
self.refresh_shortcut_list()
def on_alternate_push_button_clicked(self, toggled):
"""
Save the new alternate shortcut.
"""
self.custom_radio_button.setChecked(True)
if toggled:
self.primary_push_button.setChecked(False)
self.alternate_push_button.setText('')
return
action = self._current_item_action()
if action is None:
return
shortcuts = self._action_shortcuts(action)
new_shortcuts = []
if shortcuts:
new_shortcuts.append(shortcuts[0])
new_shortcuts.append(QtGui.QKeySequence(self.alternate_push_button.text()))
self.changed_actions[action] = new_shortcuts
if not self.primary_push_button.text():
# When we do not have a primary shortcut, the just entered alternate shortcut will automatically become the
# primary shortcut. That is why we have to adjust the primary button's text.
self.primary_push_button.setText(self.alternate_push_button.text())
self.alternate_push_button.setText('')
self.refresh_shortcut_list()
def on_item_double_clicked(self, item, column):
"""
A item has been double clicked. The ``primaryPushButton`` will be checked and the item's shortcut will be
displayed.
"""
action = self._current_item_action(item)
if action is None:
return
self.primary_push_button.setChecked(column in [0, 1])
self.alternate_push_button.setChecked(column not in [0, 1])
if column in [0, 1]:
self.primary_push_button.setText('')
self.primary_push_button.setFocus()
else:
self.alternate_push_button.setText('')
self.alternate_push_button.setFocus()
def on_current_item_changed(self, item=None, previousItem=None):
"""
A item has been pressed. We adjust the button's text to the action's shortcut which is encapsulate in the item.
"""
action = self._current_item_action(item)
self.primary_push_button.setEnabled(action is not None)
self.alternate_push_button.setEnabled(action is not None)
primary_text = ''
alternate_text = ''
primary_label_text = ''
alternate_label_text = ''
if action is None:
self.primary_push_button.setChecked(False)
self.alternate_push_button.setChecked(False)
else:
if action.default_shortcuts:
primary_label_text = self.get_shortcut_string(action.default_shortcuts[0], for_display=True)
if len(action.default_shortcuts) == 2:
alternate_label_text = self.get_shortcut_string(action.default_shortcuts[1], for_display=True)
shortcuts = self._action_shortcuts(action)
# We do not want to loose pending changes, that is why we have to keep the text when, this function has not
# been triggered by a signal.
if item is None:
primary_text = self.primary_push_button.text()
alternate_text = self.alternate_push_button.text()
elif len(shortcuts) == 1:
primary_text = self.get_shortcut_string(shortcuts[0], for_display=True)
elif len(shortcuts) == 2:
primary_text = self.get_shortcut_string(shortcuts[0], for_display=True)
alternate_text = self.get_shortcut_string(shortcuts[1], for_display=True)
# When we are capturing a new shortcut, we do not want, the buttons to display the current shortcut.
if self.primary_push_button.isChecked():
primary_text = ''
if self.alternate_push_button.isChecked():
alternate_text = ''
self.primary_push_button.setText(primary_text)
self.alternate_push_button.setText(alternate_text)
self.primary_label.setText(primary_label_text)
self.alternate_label.setText(alternate_label_text)
# We do not want to toggle and radio button, as the function has not been triggered by a signal.
if item is None:
return
if primary_label_text == primary_text and alternate_label_text == alternate_text:
self.default_radio_button.toggle()
else:
self.custom_radio_button.toggle()
def on_restore_defaults_clicked(self, button):
"""
Restores all default shortcuts.
"""
if self.button_box.buttonRole(button) != QtWidgets.QDialogButtonBox.ResetRole:
return
if QtWidgets.QMessageBox.question(self, translate('OpenLP.ShortcutListDialog', 'Restore Default Shortcuts'),
translate('OpenLP.ShortcutListDialog', 'Do you want to restore all '
'shortcuts to their defaults?')
) == QtWidgets.QMessageBox.No:
return
self._adjust_button(self.primary_push_button, False, text='')
self._adjust_button(self.alternate_push_button, False, text='')
for category in self.action_list.categories:
for action in category.actions:
self.changed_actions[action] = action.default_shortcuts
self.refresh_shortcut_list()
def on_default_radio_button_clicked(self, toggled):
"""
The default radio button has been clicked, which means we have to make sure, that we use the default shortcuts
for the action.
"""
if not toggled:
return
action = self._current_item_action()
if action is None:
return
temp_shortcuts = self._action_shortcuts(action)
self.changed_actions[action] = action.default_shortcuts
self.refresh_shortcut_list()
primary_button_text = ''
alternate_button_text = ''
if temp_shortcuts:
primary_button_text = self.get_shortcut_string(temp_shortcuts[0], for_display=True)
if len(temp_shortcuts) == 2:
alternate_button_text = self.get_shortcut_string(temp_shortcuts[1], for_display=True)
self.primary_push_button.setText(primary_button_text)
self.alternate_push_button.setText(alternate_button_text)
def on_custom_radio_button_clicked(self, toggled):
"""
The custom shortcut radio button was clicked, thus we have to restore the custom shortcuts by calling those
functions triggered by button clicks.
"""
if not toggled:
return
action = self._current_item_action()
if action is None:
QtWidgets.QMessageBox.information(self, translate('OpenLP.ShortcutListForm', 'Select an Action'),
translate('OpenLP.ShortcutListForm', 'Select an action and click one '
'of the buttons below to start '
'capturing a new primary or alternate shortcut, respectively.'))
else:
shortcuts = self._action_shortcuts(action)
self.refresh_shortcut_list()
primary_button_text = ''
alternate_button_text = ''
if shortcuts:
primary_button_text = self.get_shortcut_string(shortcuts[0], for_display=True)
if len(shortcuts) == 2:
alternate_button_text = self.get_shortcut_string(shortcuts[1], for_display=True)
self.primary_push_button.setText(primary_button_text)
self.alternate_push_button.setText(alternate_button_text)
def save(self):
"""
Save the shortcuts. **Note**, that we do not have to load the shortcuts, as they are loaded in
:class:`~openlp.core.utils.ActionList`.
"""
for category in self.action_list.categories:
# Check if the category is for internal use only.
if category.name is None:
continue
for action in category.actions:
if action in self.changed_actions:
old_shortcuts = list(map(self.get_shortcut_string, action.shortcuts()))
action.setShortcuts(self.changed_actions[action])
self.action_list.update_shortcut_map(action, old_shortcuts)
self.settings.setValue('shortcuts/' + action.objectName(), action.shortcuts())
def on_clear_primary_button_clicked(self, toggled):
"""
Restore the defaults of this action.
"""
self.primary_push_button.setChecked(False)
action = self._current_item_action()
if action is None:
return
shortcuts = self._action_shortcuts(action)
new_shortcuts = []
if action.default_shortcuts:
new_shortcuts.append(action.default_shortcuts[0])
# We have to check if the primary default shortcut is available. But we only have to check, if the action
# has a default primary shortcut (an "empty" shortcut is always valid and if the action does not have a
# default primary shortcut, then the alternative shortcut (not the default one) will become primary
# shortcut, thus the check will assume that an action were going to have the same shortcut twice.
if not self._validiate_shortcut(action, new_shortcuts[0]) and new_shortcuts[0] != shortcuts[0]:
return
if len(shortcuts) == 2:
new_shortcuts.append(shortcuts[1])
self.changed_actions[action] = new_shortcuts
self.refresh_shortcut_list()
self.on_current_item_changed(self.tree_widget.currentItem())
def on_clear_alternate_button_clicked(self, toggled):
"""
Restore the defaults of this action.
"""
self.alternate_push_button.setChecked(False)
action = self._current_item_action()
if action is None:
return
shortcuts = self._action_shortcuts(action)
new_shortcuts = []
if shortcuts:
new_shortcuts.append(shortcuts[0])
if len(action.default_shortcuts) == 2:
new_shortcuts.append(action.default_shortcuts[1])
if len(new_shortcuts) == 2:
if not self._validiate_shortcut(action, new_shortcuts[1]):
return
self.changed_actions[action] = new_shortcuts
self.refresh_shortcut_list()
self.on_current_item_changed(self.tree_widget.currentItem())
def _validiate_shortcut(self, changing_action, key_sequence):
"""
Checks if the given ``changing_action `` can use the given ``key_sequence``. Returns ``True`` if the
``key_sequence`` can be used by the action, otherwise displays a dialog and returns ``False``.
:param changing_action: The action which wants to use the ``key_sequence``.
:param key_sequence: The key sequence which the action want so use.
"""
is_valid = True
for category in self.action_list.categories:
for action in category.actions:
shortcuts = self._action_shortcuts(action)
if key_sequence not in shortcuts:
continue
if action is changing_action:
if self.primary_push_button.isChecked() and shortcuts.index(key_sequence) == 0:
continue
if self.alternate_push_button.isChecked() and shortcuts.index(key_sequence) == 1:
continue
# Have the same parent, thus they cannot have the same shortcut.
if action.parent() is changing_action.parent():
is_valid = False
# The new shortcut is already assigned, but if both shortcuts are only valid in a different widget the
# new shortcut is valid, because they will not interfere.
if action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]:
is_valid = False
if changing_action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]:
is_valid = False
if not is_valid:
text = translate('OpenLP.ShortcutListDialog',
'The shortcut "{key}" is already assigned to another action,\n'
'please use a different shortcut.'
).format(key=self.get_shortcut_string(key_sequence))
self.main_window.warning_message(translate('OpenLP.ShortcutListDialog', 'Duplicate Shortcut'),
text)
self.dialog_was_shown = True
return is_valid
def _action_shortcuts(self, action):
"""
This returns the shortcuts for the given ``action``, which also includes those shortcuts which are not saved
yet but already assigned (as changes are applied when closing the dialog).
"""
if action in self.changed_actions:
return self.changed_actions[action]
return action.shortcuts()
def _current_item_action(self, item=None):
"""
Returns the action of the given ``item``. If no item is given, we return the action of the current item of
the ``tree_widget``.
"""
if item is None:
item = self.tree_widget.currentItem()
if item is None:
return
return item.data(0, QtCore.Qt.UserRole)
def _adjust_button(self, button, checked=None, enabled=None, text=None):
"""
Can be called to adjust more properties of the given ``button`` at once.
"""
# Set the text before checking the button, because this emits a signal.
if text is not None:
button.setText(text)
if checked is not None:
button.setChecked(checked)
if enabled is not None:
button.setEnabled(enabled)
@staticmethod
def get_shortcut_string(shortcut, for_display=False):
if for_display:
if any(modifier in shortcut.toString() for modifier in ['Ctrl', 'Alt', 'Meta', 'Shift']):
sequence_format = QtGui.QKeySequence.NativeText
else:
sequence_format = QtGui.QKeySequence.PortableText
else:
sequence_format = QtGui.QKeySequence.PortableText
return shortcut.toString(sequence_format)