mirror of https://gitlab.com/openlp/openlp.git
486 lines
23 KiB
Python
486 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
##########################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# ---------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2020 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`.
|
|
"""
|
|
self.settings.beginGroup('shortcuts')
|
|
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(action.objectName(), action.shortcuts())
|
|
self.settings.endGroup()
|
|
|
|
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)
|