Files
pymol-open-source/modules/pmg_qt/shortcut_menu_gui.py
Jarrett Johnson 87d27daafb port over qt6 migrations
Fixes #498
2026-02-24 01:35:00 -05:00

416 lines
17 KiB
Python

import json
import os
from textwrap import fill
from pymol import setting
from pymol import save_shortcut
from pymol.Qt import QtGui, QtWidgets
from pymol.Qt import QtCore, QtCoreModels
from pymol.shortcut_manager import ShortcutManager, ShortcutIndex
from pymol.keyboard import get_default_keys
Qt = QtCore.Qt
QSI = QtGui.QStandardItem # For brevity
def get_shortcut_key_map():
shortcut_key_map = {}
for key, value in vars(Qt).items():
if isinstance(value, Qt.Key):
shortcut_key_map[value] = key.partition('_')[2]
return shortcut_key_map
_SHORTCUT_KEY_MAP = get_shortcut_key_map()
_SHORTCUT_MODIFIER_MAP = {
Qt.KeyboardModifier.ControlModifier: _SHORTCUT_KEY_MAP[Qt.Key.Key_Control],
Qt.KeyboardModifier.AltModifier: _SHORTCUT_KEY_MAP[Qt.Key.Key_Alt],
Qt.KeyboardModifier.ShiftModifier: _SHORTCUT_KEY_MAP[Qt.Key.Key_Shift],
Qt.KeyboardModifier.MetaModifier: _SHORTCUT_KEY_MAP[Qt.Key.Key_Meta],
}
_REPLACE_KEYS = {
'PageUp': 'pgup',
'PageDown': 'pgdn',
'Home': 'home',
'Insert': 'insert',
'Up': 'up',
'Down': 'down',
'Left': 'left',
'Right': 'right',
'End':'end'
}
class PyMOLShortcutMenu(QtWidgets.QWidget):
'''
Keyboard shortcut dialog for PyMOL. This displays all assigned shortcuts
and allows them to be changed or new shortcuts to be created.
'''
def __init__(self, parent, saved_shortcuts, cmd):
QtWidgets.QWidget.__init__(self, parent, Qt.WindowType.Window)
self.resize(700, 700)
self.cmd = cmd
self.shortcut_manager = ShortcutManager(saved_shortcuts, cmd)
self.build_panel_elements(parent)
def build_panel_elements(self, parent):
'''
Responsible for creating all panel elements in order and adding them to the layout.
'''
self.create_new_form = parent.load_form("create_shortcut", None)
self.help_form = parent.load_form("help_shortcut", None)
self.confirm_change = parent.load_form("change_confirm", None)
self.model = QtGui.QStandardItemModel(self)
self.proxy_model = QtCoreModels.QSortFilterProxyModel(self)
self.proxy_model.setSourceModel(self.model)
self.proxy_model.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
self.proxy_model.setFilterKeyColumn(-1)
self.setWindowTitle('Keyboard Shortcut Menu')
layout = QtWidgets.QVBoxLayout(self)
self.setLayout(layout)
# Create layout for filter bar and refresh button
top_layout = QtWidgets.QGridLayout()
layout.addLayout(top_layout)
# Filter
self.filter_le = QtWidgets.QLineEdit(self)
top_layout.addWidget(self.filter_le)
self.filter_le.setPlaceholderText("Filter")
self.filter_le.textChanged.connect(self.proxy_model.setFilterRegularExpression)
self.refresh_button = QtWidgets.QPushButton(self)
self.refresh_button.resize(26, 26)
top_layout.addWidget(self.refresh_button, 0, 1)
# themed icons only available by default on X11
if self.refresh_button.icon().isNull():
self.refresh_button.setIcon(QtGui.QIcon(
os.path.expandvars('$PYMOL_DATA/pmg_qt/icons/refresh.svg')))
self.refresh_button.setToolTip(
"Refresh the table to reflect any external changes")
self.refresh_button.clicked.connect(self.refresh_populate)
# Table
self.table = QtWidgets.QTableView(self)
self.table.setModel(self.proxy_model)
layout.addWidget(self.table)
self.intial_populate()
self.formatTable()
# Add layout for buttons
button_layout = QtWidgets.QGridLayout()
layout.addLayout(button_layout)
# Buttons
self.create_new_button = QtWidgets.QPushButton(self)
button_layout.addWidget(self.create_new_button, 0, 0)
self.create_new_button.setText("Create New")
self.create_new_button.setToolTip(
"Add a key binding that does not currently appear on the table")
self.create_new_button.clicked.connect(
lambda: self.create_new_form._dialog.show())
self.delete_selected_button = QtWidgets.QPushButton(self)
button_layout.addWidget(self.delete_selected_button, 0, 1)
self.delete_selected_button.setText("Delete Selected")
self.delete_selected_button.setToolTip(
"Unbind selected key bindings and remove any that have been created")
self.delete_selected_button.clicked.connect(self.delete_selected)
self.delete_selected_button.setEnabled(False)
self.reset_selected_button = QtWidgets.QPushButton(self)
button_layout.addWidget(self.reset_selected_button, 0, 2)
self.reset_selected_button.setText("Reset Selected")
self.reset_selected_button.setToolTip(
"Restore selected key bindings to their default values")
self.reset_selected_button.clicked.connect(self.reset_selected)
self.reset_selected_button.setEnabled(False)
self.reset_all_button = QtWidgets.QPushButton(self)
button_layout.addWidget(self.reset_all_button, 0, 3)
self.reset_all_button.setText("Reset All")
self.reset_all_button.setToolTip(
"Restore all key bindings to their default values and remove any that have been created")
self.reset_all_button.clicked.connect(self.reset_all_default)
self.save_button = QtWidgets.QPushButton(self)
button_layout.addWidget(self.save_button, 0, 4)
self.save_button.setText("Save")
self.save_button.setToolTip(
"Save the current key bindings to be loaded automatically when opening PyMOL")
self.save_button.clicked.connect(self.shortcut_manager.save_shortcuts)
# Ensuring that confirmed key and binding remain in scope
self.confirm_new_key = ''
self.confirm_new_binding = ''
# Connect create new and confirm menus
self.create_new_shortcut_menu_connect()
self.confirm_menu_connect()
self.model.itemChanged.connect(self.itemChanged)
def populateData(self):
'''
Fill the model with data from shortcut_dict.
'''
self.model.clear()
self.model.setHorizontalHeaderLabels(
['Key', 'Command (click to edit)', 'Description'])
for key, shortcut_list in self.shortcut_manager.cmd.shortcut_dict.items():
key_item = QSI(key)
command_item = QSI()
descript_item = QSI()
key_item.setFlags(Qt.ItemFlag.ItemIsEnabled)
descript_item.setFlags(Qt.ItemFlag.ItemIsEditable)
if shortcut_list[ShortcutIndex.USER_DEF]:
if shortcut_list[ShortcutIndex.USER_DEF] != "Deleted":
command_text = shortcut_list[ShortcutIndex.USER_DEF]
descript_text = "user defined"
else:
command_text = "Deleted"
descript_text = "Deleted"
else:
command_text = shortcut_list[ShortcutIndex.COMMAND]
descript_text = shortcut_list[ShortcutIndex.DESCRIPT]
command_item.setText(command_text)
descript_item.setText(descript_text)
self.model.appendRow([key_item, command_item, descript_item])
self.formatTable()
self.table.selectionModel().selectionChanged.connect(self.selection_changed)
def selection_changed(self):
item_selected = bool(self.table.selectionModel().selectedIndexes())
self.delete_selected_button.setEnabled(item_selected)
self.reset_selected_button.setEnabled(item_selected)
def intial_populate(self):
'''
Runs when the menu is first opened. Separate from populateData so that
the saved dictionary and current state of key_mappings can be checked.
'''
self.shortcut_manager.check_saved_dict()
self.shortcut_manager.check_key_mappings()
self.populateData()
def refresh_populate(self):
'''
Called through the refresh button.
'''
self.shortcut_manager.check_key_mappings()
self.populateData()
def reset_all_default(self):
'''
Iterates over all values to restore their default commands.
This will restore them to the values from keyboard.py
'''
self.shortcut_manager.reset_all_default()
self.populateData()
def delete_selected(self):
'''
Removes selected keybindings and updates table to say "Deleted".
Keys that don't have default values will be removed completely.
'''
selection_model = self.table.selectionModel()
list_indexes = selection_model.selectedIndexes()
delete_keys = []
for ind, table_index in enumerate(list_indexes):
table_colm_key = self.table.model().index(table_index.row(), 0)
table_colm_command = self.table.model().index(table_index.row(), 1)
table_colm_descipt = self.table.model().index(table_index.row(), 2)
delete_key = table_colm_key.data()
self.cmd.set_key(delete_key, '')
if delete_key in self.shortcut_manager.default_bindings:
self.table.model().setData(table_colm_command, 'Deleted')
self.table.model().setData(table_colm_descipt, 'Deleted')
self.shortcut_manager.cmd.shortcut_dict[delete_key][ShortcutIndex.USER_DEF] = 'Deleted'
else:
self.model.removeRow(table_index.row())
print(delete_key, " has been deleted and will be removed from the table")
delete_keys.append(delete_key)
for key in delete_keys:
del self.shortcut_manager.cmd.shortcut_dict[key]
def reset_selected(self):
'''
Restores default key bindings for items selected in the tables selection model.
'''
selection_model = self.table.selectionModel()
list_indexes = selection_model.selectedIndexes()
for ind, table_index in enumerate(list_indexes):
table_colm_key = self.table.model().index(table_index.row(), 0)
table_colm_command = self.table.model().index(table_index.row(), 1)
table_colm_descipt = self.table.model().index(table_index.row(), 2)
reset_key = table_colm_key.data()
if reset_key not in self.shortcut_manager.default_bindings:
print("This key does not have a default value.")
else:
reset_binding = self.shortcut_manager.default_bindings[reset_key]
reset_command = self.shortcut_manager.cmd.shortcut_dict[
reset_key][ShortcutIndex.COMMAND]
reset_description = self.shortcut_manager.cmd.shortcut_dict[
reset_key][ShortcutIndex.DESCRIPT]
self.table.model().setData(table_colm_command, reset_command)
self.table.model().setData(table_colm_descipt, reset_description)
self.shortcut_manager.cmd.shortcut_dict[reset_key][ShortcutIndex.USER_DEF] = ''
self.cmd.set_key(reset_key, reset_binding)
def create_new_shortcut_menu_connect(self):
self.create_new_form.createButton.clicked.connect(
self.create_new_shortcut_caller)
self.create_new_form.helpButton.clicked.connect(
self.help_menu_shortcut)
self.create_new_form.keyEdit.installEventFilter(self)
self.create_new_form.helpButton.setDefault(False)
self.create_new_form.helpButton.setAutoDefault(False)
def eventFilter(self, source, event):
'''
Event filter for creating new shortcuts. Processes the key event before passing it on.
'''
if (event.type() == QtCore.QEvent.Type.KeyPress and source is self.create_new_form.keyEdit):
raw_string = self.keyevent_to_string(event)
processed_string = self.process_keyevent_string(raw_string)
if processed_string in self.shortcut_manager.reserved_keys:
return 0
if processed_string:
self.create_new_form.keyEdit.setText(processed_string)
return super().eventFilter(source, event)
def keyevent_to_string(self, event):
'''
Generates string from captured key event for process_keyevent_string.
'''
keyevent_list = []
for mod, event_text in _SHORTCUT_MODIFIER_MAP.items():
if event.modifiers() & mod:
keyevent_list.append(event_text)
key = _SHORTCUT_KEY_MAP.get(event.key(), event.text())
if key not in keyevent_list:
keyevent_list.append(key)
return ' '.join(keyevent_list)
def process_keyevent_string(self, raw_string):
'''
Returns string of keyevent used to populate create key menu.
Provides filtering to the keyevents, returning a list of the accepted strings.
raw_string: string from keyevent_to_string
'''
split_string = raw_string.split()
process_list = []
prefix_key = split_string[0]
if len(split_string) >= 2:
prefix_key = split_string[0]
suffix_key = split_string[1]
if (prefix_key == 'Control' or prefix_key == 'Meta') and split_string[1]:
if suffix_key == 'Shift' and len(split_string) > 2:
process_list.append('CTSH')
suffix_key = split_string[2]
else:
process_list.append('CTRL')
elif prefix_key == 'Alt':
process_list.append('ALT')
elif prefix_key == 'Shift':
process_list.append('SHFT')
if suffix_key in _REPLACE_KEYS:
suffix_key = _REPLACE_KEYS[suffix_key]
process_list.append(suffix_key)
elif prefix_key in _REPLACE_KEYS:
process_list.append(_REPLACE_KEYS[prefix_key])
return('-'.join(process_list))
def help_menu_shortcut(self):
self.help_form._dialog.show()
def confirm_menu_connect(self):
self.confirm_change.confirmButton.clicked.connect(
lambda: self.shortcut_manager.create_new_shortcut(self.confirm_new_key, self.confirm_new_binding))
self.confirm_change.confirmButton.clicked.connect(lambda: self.populateData())
self.confirm_change.confirmButton.clicked.connect(
lambda: self.confirm_change._dialog.hide())
self.confirm_change.cancelButton.clicked.connect(
lambda: self.confirm_change._dialog.hide())
def create_new_shortcut_caller(self):
'''
Creates a new shortcut after checking existing and reserved keys.
'''
new_key = self.create_new_form.keyEdit.text()
new_binding = self.create_new_form.commandEdit.text()
if new_key == '':
pass
elif new_key in self.shortcut_manager.cmd.shortcut_dict:
hide_confirm_menu = self.confirm_change.doNotShowCheckBox.isChecked()
if not hide_confirm_menu:
self.confirm_new_key = new_key
self.confirm_new_binding = new_binding
self.confirm_change._dialog.show()
else:
self.shortcut_manager.create_new_shortcut(new_key, new_binding)
self.populateData()
else:
self.shortcut_manager.create_new_shortcut(new_key, new_binding)
self.populateData()
self.table.scrollToBottom()
def formatTable(self):
'''
Set up the table to look appropriately
'''
hh = self.table.horizontalHeader()
hh.setStretchLastSection(True)
self.table.verticalHeader().setVisible(False)
self.table.setFocus()
self.table.hide()
self.table.resizeColumnsToContents()
self.table.show()
def itemChanged(self, item):
"""
Called every time an item in the table is changed, only command items
are changeable.
@param item: The item which has changed
@type item: QStandardItem
"""
try:
if item.column() == 1 and item.text() != "Deleted":
changed_key = self.model.index(item.row(), 0).data()
changed_index = self.table.model().index(item.row(), 0)
self.cmd.set_key(changed_key, item.text())
self.shortcut_manager.cmd.shortcut_dict[changed_key][2] = item.text()
filter_active = bool(self.filter_le.text())
if not filter_active:
self.table.model().setData(self.table.model().index(item.row(), 2), 'user defined')
else:
pass
except Exception as e:
print(e)
print("Failed to change key binding")