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")