# -*- coding: utf-8 -*- # # Copyright © 2010 Pierre Raybaut # Licensed under the terms of the MIT License # (see spyderlib/__init__.py for details) """Shortcut management""" from __future__ import print_function from spyderlib.qt.QtGui import (QVBoxLayout, QComboBox, QItemDelegate, QTableView, QMessageBox, QPushButton) from spyderlib.qt.QtCore import (Qt, QSize, QAbstractTableModel, QModelIndex, SIGNAL) from spyderlib.qt.compat import to_qvariant, from_qvariant import sys # Local imports from spyderlib.baseconfig import _ from spyderlib.guiconfig import (get_shortcut, set_shortcut, iter_shortcuts, reset_shortcuts) from spyderlib.plugins.configdialog import GeneralConfigPage from spyderlib.py3compat import to_text_string, is_text_string KEYSTRINGS = ["Escape", "Tab", "Backtab", "Backspace", "Return", "Enter", "Delete", "Pause", "Print", "Clear", "Home", "End", "Left", "Up", "Right", "Down", "PageUp", "PageDown"] + \ ["F%d" % _i for _i in range(1, 36)] + \ ["Space", "Exclam", "QuoteDbl", "NumberSign", "Dollar", "Percent", "Ampersand", "Apostrophe", "ParenLeft", "ParenRight", "Asterisk", "Plus", "Comma", "Minus", "Period", "Slash"] + \ [str(_i) for _i in range(10)] + \ ["Colon", "Semicolon", "Less", "Equal", "Greater", "Question", "At"] + [chr(_i) for _i in range(65, 91)] + \ ["BracketLeft", "Backslash", "BracketRight", "Underscore"] class Key(object): MODIFIERS = {Qt.NoModifier: "", Qt.ShiftModifier: "Shift", Qt.ControlModifier: "Ctrl", Qt.AltModifier: "Alt", Qt.MetaModifier: "Meta"} if sys.platform == 'darwin': MODIFIERNAMES = {Qt.NoModifier: "", Qt.ShiftModifier: "Shift", Qt.ControlModifier: "Cmd", Qt.AltModifier: "Alt", Qt.MetaModifier: "Ctrl"} elif sys.platform == 'win32': MODIFIERNAMES = {Qt.NoModifier: "", Qt.ShiftModifier: "Shift", Qt.ControlModifier: "Ctrl", Qt.AltModifier: "Alt", Qt.MetaModifier: "Win"} else: MODIFIERNAMES = {Qt.NoModifier: "", Qt.ShiftModifier: "Shift", Qt.ControlModifier: "Ctrl", Qt.AltModifier: "Alt", Qt.MetaModifier: "Meta"} KEYS = {} for attr in KEYSTRINGS: KEYS[getattr(Qt, "Key_"+attr)] = attr def __init__(self, key, mod1=Qt.NoModifier, mod2=Qt.NoModifier, mod3=Qt.NoModifier): modifiers = [mod1, mod2, mod3] assert all([mod in self.MODIFIERS for mod in modifiers]) self.modifiers = sorted(modifiers) assert key in self.KEYS self.key = key def __str__(self): tlist = [] for mod in sorted(list(set(self.modifiers))): if mod != Qt.NoModifier: tlist.append(self.MODIFIERS[mod]) tlist.append(self.KEYS[self.key]) return "+".join(tlist) def __unicode__(self): return to_text_string(self.__str__()) @staticmethod def modifier_from_str(modstr): for k, v in list(Key.MODIFIERS.items()): if v.lower() == modstr.lower(): return k @staticmethod def key_from_str(keystr): for k, v in list(Key.KEYS.items()): if v.lower() == keystr.lower(): return k @staticmethod def modifier_from_name(modname): for k, v in list(Key.MODIFIERNAMES.items()): if v.lower() == modname.lower(): return k def keystr2key(keystr): keylist = keystr.split("+") mods = [] if len(keylist) > 1: for modstr in keylist[:-1]: mods.append(Key.modifier_from_str(modstr)) return Key(Key.key_from_str(keylist[-1]), *mods) class Shortcut(object): def __init__(self, context, name, key=None): self.context = context self.name = name if is_text_string(key): key = keystr2key(key) self.key = key def __str__(self): return "%s/%s: %s" % (self.context, self.name, self.key) def load(self): self.key = keystr2key(get_shortcut(self.context, self.name)) def save(self): set_shortcut(self.context, self.name, str(self.key)) CONTEXT, NAME, MOD1, MOD2, MOD3, KEY = list(range(6)) class ShortcutsModel(QAbstractTableModel): def __init__(self): QAbstractTableModel.__init__(self) self.shortcuts = [] def sortByName(self): self.shortcuts = sorted(self.shortcuts, key=lambda x: x.context+'/'+x.name) self.reset() def flags(self, index): if not index.isValid(): return Qt.ItemIsEnabled column = index.column() if column in (MOD1, MOD2, MOD3, KEY): return Qt.ItemFlags(QAbstractTableModel.flags(self, index)| Qt.ItemIsEditable) else: return Qt.ItemFlags(QAbstractTableModel.flags(self, index)) def data(self, index, role=Qt.DisplayRole): if not index.isValid() or \ not (0 <= index.row() < len(self.shortcuts)): return to_qvariant() shortcut = self.shortcuts[index.row()] key = shortcut.key column = index.column() if role == Qt.DisplayRole: if column == CONTEXT: return to_qvariant(shortcut.context) elif column == NAME: return to_qvariant(shortcut.name) elif column == MOD1: return to_qvariant(Key.MODIFIERNAMES[key.modifiers[0]]) elif column == MOD2: return to_qvariant(Key.MODIFIERNAMES[key.modifiers[1]]) elif column == MOD3: return to_qvariant(Key.MODIFIERNAMES[key.modifiers[2]]) elif column == KEY: return to_qvariant(Key.KEYS[key.key]) elif role == Qt.TextAlignmentRole: return to_qvariant(int(Qt.AlignHCenter|Qt.AlignVCenter)) return to_qvariant() def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.TextAlignmentRole: if orientation == Qt.Horizontal: return to_qvariant(int(Qt.AlignHCenter|Qt.AlignVCenter)) return to_qvariant(int(Qt.AlignRight|Qt.AlignVCenter)) if role != Qt.DisplayRole: return to_qvariant() if orientation == Qt.Horizontal: if section == CONTEXT: return to_qvariant(_("Context")) elif section == NAME: return to_qvariant(_("Name")) elif section == MOD1: return to_qvariant(_("Mod1")) elif section == MOD2: return to_qvariant(_("Mod2")) elif section == MOD3: return to_qvariant(_("Mod3")) elif section == KEY: return to_qvariant(_("Key")) return to_qvariant() def rowCount(self, index=QModelIndex()): return len(self.shortcuts) def columnCount(self, index=QModelIndex()): return 6 def setData(self, index, value, role=Qt.EditRole): if index.isValid() and 0 <= index.row() < len(self.shortcuts): shortcut = self.shortcuts[index.row()] key = shortcut.key column = index.column() text = from_qvariant(value, str) if column == MOD1: key.modifiers[0] = Key.modifier_from_name(text) elif column == MOD2: key.modifiers[1] = Key.modifier_from_name(text) elif column == MOD3: key.modifiers[2] = Key.modifier_from_name(text) elif column == KEY: key.key = Key.key_from_str(text) self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index) return True return False class ShortcutsDelegate(QItemDelegate): def __init__(self, parent=None): QItemDelegate.__init__(self, parent) self.modifiers = sorted(Key.MODIFIERNAMES.values()) self.mod = None self.keys = sorted(Key.KEYS.values()) self.key = None def sizeHint(self, option, index): fm = option.fontMetrics if index.column() in (MOD1, MOD2, MOD3): if self.mod is None: w = 0 for mod in self.modifiers: cw = fm.width(mod) if cw > w: w = cw self.mod = mod else: w = fm.width(self.mod) return QSize(w+20, fm.height()) elif index.column() == KEY: if self.key is None: w = 0 for key in self.keys: cw = fm.width(key) if cw > w: w = cw self.key = key else: w = fm.width(self.key) return QSize(w+20, fm.height()) return QItemDelegate.sizeHint(self, option, index) def createEditor(self, parent, option, index): if index.column() in (MOD1, MOD2, MOD3): combobox = QComboBox(parent) combobox.addItems(self.modifiers) return combobox elif index.column() == KEY: combobox = QComboBox(parent) combobox.addItems(self.keys) return combobox else: return QItemDelegate.createEditor(self, parent, option, index) def setEditorData(self, editor, index): text = from_qvariant(index.model().data(index, Qt.DisplayRole), str) if index.column() in (MOD1, MOD2, MOD3, KEY): i = editor.findText(text) if i == -1: i = 0 editor.setCurrentIndex(i) else: QItemDelegate.setEditorData(self, editor, index) def setModelData(self, editor, model, index): if index.column() in (MOD1, MOD2, MOD3, KEY): model.setData(index, to_qvariant(editor.currentText())) else: QItemDelegate.setModelData(self, editor, model, index) class ShortcutsTable(QTableView): def __init__(self, parent=None): QTableView.__init__(self, parent) self.model = ShortcutsModel() self.setModel(self.model) self.setItemDelegate(ShortcutsDelegate(self)) self.load_shortcuts() def adjust_cells(self): self.resizeColumnsToContents() # self.resizeRowsToContents() self.horizontalHeader().setStretchLastSection(True) def load_shortcuts(self): shortcuts = [] for context, name, keystr in iter_shortcuts(): shortcut = Shortcut(context, name, keystr) shortcuts.append(shortcut) shortcuts = sorted(shortcuts, key=lambda x: x.context+x.name) self.model.shortcuts = shortcuts self.model.reset() self.adjust_cells() def check_shortcuts(self): """Check shortcuts for conflicts""" conflicts = [] for index, sh1 in enumerate(self.model.shortcuts): if index == len(self.model.shortcuts)-1: break for sh2 in self.model.shortcuts[index+1:]: if sh2 is sh1: continue if str(sh2.key) == str(sh1.key) \ and (sh1.context == sh2.context or sh1.context == '_' or sh2.context == '_'): conflicts.append((sh1, sh2)) if conflicts: self.parent().emit(SIGNAL('show_this_page()')) cstr = "\n".join(['%s <---> %s' % (sh1, sh2) for sh1, sh2 in conflicts]) QMessageBox.warning(self, _( "Conflicts"), _("The following conflicts have been " "detected:")+"\n"+cstr, QMessageBox.Ok) def save_shortcuts(self): self.check_shortcuts() for shortcut in self.model.shortcuts: shortcut.save() class ShortcutsConfigPage(GeneralConfigPage): CONF_SECTION = "shortcuts" NAME = _("Keyboard shortcuts") ICON = "genprefs.png" def setup_page(self): self.table = ShortcutsTable(self) self.connect(self.table.model, SIGNAL("dataChanged(QModelIndex,QModelIndex)"), lambda i1, i2, opt='': self.has_been_modified(opt)) vlayout = QVBoxLayout() vlayout.addWidget(self.table) reset_btn = QPushButton(_("Reset to default values")) self.connect(reset_btn, SIGNAL('clicked()'), self.reset_to_default) vlayout.addWidget(reset_btn) self.setLayout(vlayout) def check_settings(self): self.table.check_shortcuts() def reset_to_default(self): reset_shortcuts() self.main.apply_shortcuts() self.table.load_shortcuts() self.load_from_conf() self.set_modified(False) def apply_settings(self, options): self.table.save_shortcuts() self.main.apply_shortcuts() def test(): from spyderlib.utils.qthelpers import qapplication app = qapplication() table = ShortcutsTable() table.show() app.exec_() print([str(s) for s in table.model.shortcuts]) table.check_shortcuts() if __name__ == '__main__': test()