class NumpyArrayDialog(QDialog): def __init__(self, parent=None, inline=True, offset=0, force_float=False): QDialog.__init__(self, parent=parent) self._parent = parent self._text = None self._valid = None self._offset = offset # TODO: add this as an option in the General Preferences? self._force_float = force_float self._help_inline = _(""" <b>Numpy Array/Matrix Helper</b><br> Type an array in Matlab : <code>[1 2;3 4]</code><br> or Spyder simplified syntax : <code>1 2;3 4</code> <br><br> Hit 'Enter' for array or 'Ctrl+Enter' for matrix. <br><br> <b>Hint:</b><br> Use two spaces or two tabs to generate a ';'. """) self._help_table = _(""" <b>Numpy Array/Matrix Helper</b><br> Enter an array in the table. <br> Use Tab to move between cells. <br><br> Hit 'Enter' for array or 'Ctrl+Enter' for matrix. <br><br> <b>Hint:</b><br> Use two tabs at the end of a row to move to the next row. """) # Widgets self._button_warning = QToolButton() self._button_help = HelperToolButton() self._button_help.setIcon(ima.icon('MessageBoxInformation')) style = """ QToolButton { border: 1px solid grey; padding:0px; border-radius: 2px; background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f6f7fa, stop: 1 #dadbde); } """ self._button_help.setStyleSheet(style) if inline: self._button_help.setToolTip(self._help_inline) self._text = NumpyArrayInline(self) self._widget = self._text else: self._button_help.setToolTip(self._help_table) self._table = NumpyArrayTable(self) self._widget = self._table style = """ QDialog { margin:0px; border: 1px solid grey; padding:0px; border-radius: 2px; }""" self.setStyleSheet(style) style = """ QToolButton { margin:1px; border: 0px solid grey; padding:0px; border-radius: 0px; }""" self._button_warning.setStyleSheet(style) # widget setup self.setWindowFlags(Qt.Window | Qt.Dialog | Qt.FramelessWindowHint) self.setModal(True) self.setWindowOpacity(0.90) self._widget.setMinimumWidth(200) # layout self._layout = QHBoxLayout() self._layout.addWidget(self._widget) self._layout.addWidget(self._button_warning, 1, Qt.AlignTop) self._layout.addWidget(self._button_help, 1, Qt.AlignTop) self.setLayout(self._layout) self._widget.setFocus() def keyPressEvent(self, event): """ Qt override. """ QToolTip.hideText() ctrl = event.modifiers() & Qt.ControlModifier if event.key() in [Qt.Key_Enter, Qt.Key_Return]: if ctrl: self.process_text(array=False) else: self.process_text(array=True) self.accept() else: QDialog.keyPressEvent(self, event) def event(self, event): """ Qt Override. Usefull when in line edit mode. """ if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Tab: return False return QWidget.event(self, event) def process_text(self, array=True): """ Construct the text based on the entered content in the widget. """ if array: prefix = 'np.array([[' else: prefix = 'np.matrix([[' suffix = ']])' values = self._widget.text().strip() if values != '': # cleans repeated spaces exp = r'(\s*)' + ROW_SEPARATOR + r'(\s*)' values = re.sub(exp, ROW_SEPARATOR, values) values = re.sub("\s+", " ", values) values = re.sub("]$", "", values) values = re.sub("^\[", "", values) values = re.sub(ROW_SEPARATOR + r'*$', '', values) # replaces spaces by commas values = values.replace(' ', ELEMENT_SEPARATOR) # iterate to find number of rows and columns new_values = [] rows = values.split(ROW_SEPARATOR) nrows = len(rows) ncols = [] for row in rows: new_row = [] elements = row.split(ELEMENT_SEPARATOR) ncols.append(len(elements)) for e in elements: num = e # replaces not defined values if num in NAN_VALUES: num = 'np.nan' # Convert numbers to floating point if self._force_float: try: num = str(float(e)) except: pass new_row.append(num) new_values.append(ELEMENT_SEPARATOR.join(new_row)) new_values = ROW_SEPARATOR.join(new_values) values = new_values # Check validity if len(set(ncols)) == 1: self._valid = True else: self._valid = False # Single rows are parsed as 1D arrays/matrices if nrows == 1: prefix = prefix[:-1] suffix = suffix.replace("]])", "])") # Fix offset offset = self._offset braces = BRACES.replace(' ', '\n' + ' '*(offset + len(prefix) - 1)) values = values.replace(ROW_SEPARATOR, braces) text = "{0}{1}{2}".format(prefix, values, suffix) self._text = text else: self._text = '' self.update_warning() def update_warning(self): """ Updates the icon and tip based on the validity of the array content. """ widget = self._button_warning if not self.is_valid(): tip = _('Array dimensions not valid') widget.setIcon(ima.icon('MessageBoxWarning')) widget.setToolTip(tip) QToolTip.showText(self._widget.mapToGlobal(QPoint(0, 5)), tip) else: self._button_warning.setToolTip('') def is_valid(self): """ Return if the current array state is valid. """ return self._valid def text(self): """ Return the parsed array/matrix text. """ return self._text @property def array_widget(self): """ Return the array builder widget. """ return self._widget
def setup(self): """Setup the ShortcutEditor with the provided arguments.""" # Widgets icon_info = HelperToolButton() icon_info.setIcon(get_std_icon('MessageBoxInformation')) layout_icon_info = QVBoxLayout() layout_icon_info.setContentsMargins(0, 0, 0, 0) layout_icon_info.setSpacing(0) layout_icon_info.addWidget(icon_info) layout_icon_info.addStretch(100) self.label_info = QLabel() self.label_info.setText( _("Press the new shortcut and select 'Ok' to confirm, " "click 'Cancel' to revert to the previous state, " "or use 'Clear' to unbind the command from a shortcut.")) self.label_info.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.label_info.setWordWrap(True) layout_info = QHBoxLayout() layout_info.setContentsMargins(0, 0, 0, 0) layout_info.addLayout(layout_icon_info) layout_info.addWidget(self.label_info) layout_info.setStretch(1, 100) self.label_current_sequence = QLabel(_("Current shortcut:")) self.text_current_sequence = QLabel(self.current_sequence) self.label_new_sequence = QLabel(_("New shortcut:")) self.text_new_sequence = ShortcutLineEdit(self) self.text_new_sequence.setPlaceholderText(_("Press shortcut.")) self.helper_button = HelperToolButton() self.helper_button.setIcon(QIcon()) self.label_warning = QLabel() self.label_warning.setWordWrap(True) self.label_warning.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.button_default = QPushButton(_('Default')) self.button_ok = QPushButton(_('Ok')) self.button_ok.setEnabled(False) self.button_clear = QPushButton(_('Clear')) self.button_cancel = QPushButton(_('Cancel')) button_box = QHBoxLayout() button_box.addWidget(self.button_default) button_box.addStretch(100) button_box.addWidget(self.button_ok) button_box.addWidget(self.button_clear) button_box.addWidget(self.button_cancel) # New Sequence button box self.btn_clear_sequence = create_toolbutton( self, icon=ima.icon('editclear'), tip=_("Clear all entered key sequences"), triggered=self.clear_new_sequence) self.button_back_sequence = create_toolbutton( self, icon=ima.icon('ArrowBack'), tip=_("Remove last key sequence entered"), triggered=self.back_new_sequence) newseq_btnbar = QHBoxLayout() newseq_btnbar.setSpacing(0) newseq_btnbar.setContentsMargins(0, 0, 0, 0) newseq_btnbar.addWidget(self.button_back_sequence) newseq_btnbar.addWidget(self.btn_clear_sequence) # Setup widgets self.setWindowTitle(_('Shortcut: {0}').format(self.name)) self.helper_button.setToolTip('') style = """ QToolButton { margin:1px; border: 0px solid grey; padding:0px; border-radius: 0px; }""" self.helper_button.setStyleSheet(style) icon_info.setToolTip('') icon_info.setStyleSheet(style) # Layout layout_sequence = QGridLayout() layout_sequence.setContentsMargins(0, 0, 0, 0) layout_sequence.addLayout(layout_info, 0, 0, 1, 4) layout_sequence.addItem(QSpacerItem(15, 15), 1, 0, 1, 4) layout_sequence.addWidget(self.label_current_sequence, 2, 0) layout_sequence.addWidget(self.text_current_sequence, 2, 2) layout_sequence.addWidget(self.label_new_sequence, 3, 0) layout_sequence.addWidget(self.helper_button, 3, 1) layout_sequence.addWidget(self.text_new_sequence, 3, 2) layout_sequence.addLayout(newseq_btnbar, 3, 3) layout_sequence.addWidget(self.label_warning, 4, 2, 1, 2) layout_sequence.setColumnStretch(2, 100) layout_sequence.setRowStretch(4, 100) layout = QVBoxLayout(self) layout.addLayout(layout_sequence) layout.addSpacing(10) layout.addLayout(button_box) layout.setSizeConstraint(layout.SetFixedSize) # Signals self.button_ok.clicked.connect(self.accept_override) self.button_clear.clicked.connect(self.unbind_shortcut) self.button_cancel.clicked.connect(self.reject) self.button_default.clicked.connect(self.set_sequence_to_default) # Set all widget to no focus so that we can register <Tab> key # press event. widgets = ( self.label_warning, self.helper_button, self.text_new_sequence, self.button_clear, self.button_default, self.button_cancel, self.button_ok, self.btn_clear_sequence, self.button_back_sequence) for w in widgets: w.setFocusPolicy(Qt.NoFocus) w.clearFocus()
class ShortcutEditor(QDialog): """A dialog for entering key sequences.""" def __init__(self, parent, context, name, sequence, shortcuts): super(ShortcutEditor, self).__init__(parent) self._parent = parent self.context = context self.npressed = 0 self.keys = set() self.key_modifiers = set() self.key_non_modifiers = list() self.key_text = list() self.sequence = sequence self.new_sequence = None self.edit_state = True self.shortcuts = shortcuts # Widgets self.label_info = QLabel() self.label_info.setText( _("Press the new shortcut and select 'Ok': \n" "(Press 'Tab' once to switch focus between the shortcut entry \n" "and the buttons below it)")) self.label_current_sequence = QLabel(_("Current shortcut:")) self.text_current_sequence = QLabel(sequence) self.label_new_sequence = QLabel(_("New shortcut:")) self.text_new_sequence = CustomLineEdit(self) self.text_new_sequence.setPlaceholderText(sequence) self.helper_button = HelperToolButton() self.helper_button.hide() self.label_warning = QLabel() self.label_warning.hide() bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_ok = bbox.button(QDialogButtonBox.Ok) self.button_cancel = bbox.button(QDialogButtonBox.Cancel) # Setup widgets self.setWindowTitle(_('Shortcut: {0}').format(name)) self.button_ok.setFocusPolicy(Qt.NoFocus) self.button_ok.setEnabled(False) self.button_cancel.setFocusPolicy(Qt.NoFocus) self.helper_button.setToolTip('') self.helper_button.setFocusPolicy(Qt.NoFocus) style = """ QToolButton { margin:1px; border: 0px solid grey; padding:0px; border-radius: 0px; }""" self.helper_button.setStyleSheet(style) self.text_new_sequence.setFocusPolicy(Qt.NoFocus) self.label_warning.setFocusPolicy(Qt.NoFocus) # Layout spacing = 5 layout_sequence = QGridLayout() layout_sequence.addWidget(self.label_info, 0, 0, 1, 3) layout_sequence.addItem(QSpacerItem(spacing, spacing), 1, 0, 1, 2) layout_sequence.addWidget(self.label_current_sequence, 2, 0) layout_sequence.addWidget(self.text_current_sequence, 2, 2) layout_sequence.addWidget(self.label_new_sequence, 3, 0) layout_sequence.addWidget(self.helper_button, 3, 1) layout_sequence.addWidget(self.text_new_sequence, 3, 2) layout_sequence.addWidget(self.label_warning, 4, 2, 1, 2) layout = QVBoxLayout() layout.addLayout(layout_sequence) layout.addSpacing(spacing) layout.addWidget(bbox) self.setLayout(layout) # Signals bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) def keyPressEvent(self, e): """Qt override.""" key = e.key() # Check if valid keys if key not in VALID_KEYS: self.invalid_key_flag = True return self.npressed += 1 self.key_non_modifiers.append(key) self.key_modifiers.add(key) self.key_text.append(e.text()) self.invalid_key_flag = False debug_print('key {0}, npressed: {1}'.format(key, self.npressed)) if key == Qt.Key_unknown: return # The user clicked just and only the special keys # Ctrl, Shift, Alt, Meta. if (key == Qt.Key_Control or key == Qt.Key_Shift or key == Qt.Key_Alt or key == Qt.Key_Meta): return modifiers = e.modifiers() if modifiers & Qt.ShiftModifier: key += Qt.SHIFT if modifiers & Qt.ControlModifier: key += Qt.CTRL if sys.platform == 'darwin': self.npressed -= 1 debug_print('decrementing') if modifiers & Qt.AltModifier: key += Qt.ALT if modifiers & Qt.MetaModifier: key += Qt.META self.keys.add(key) def toggle_state(self): """Switch between shortcut entry and Accept/Cancel shortcut mode.""" self.edit_state = not self.edit_state if not self.edit_state: self.text_new_sequence.setEnabled(False) if self.button_ok.isEnabled(): self.button_ok.setFocus() else: self.button_cancel.setFocus() else: self.text_new_sequence.setEnabled(True) self.text_new_sequence.setFocus() def nonedit_keyrelease(self, e): """Key release event for non-edit state.""" key = e.key() if key in [Qt.Key_Escape]: self.close() return if key in [Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down]: if self.button_ok.hasFocus(): self.button_cancel.setFocus() else: self.button_ok.setFocus() def keyReleaseEvent(self, e): """Qt override.""" self.npressed -= 1 if self.npressed <= 0: key = e.key() if len(self.keys) == 1 and key == Qt.Key_Tab: self.toggle_state() return if len(self.keys) == 1 and key == Qt.Key_Escape: self.set_sequence('') self.label_warning.setText( _("Please introduce a different " "shortcut")) if len(self.keys) == 1 and key in [Qt.Key_Return, Qt.Key_Enter]: self.toggle_state() return if not self.edit_state: self.nonedit_keyrelease(e) else: debug_print('keys: {}'.format(self.keys)) if self.keys and key != Qt.Key_Escape: self.validate_sequence() self.keys = set() self.key_modifiers = set() self.key_non_modifiers = list() self.key_text = list() self.npressed = 0 def check_conflicts(self): """Check shortcuts for conflicts.""" conflicts = [] for index, shortcut in enumerate(self.shortcuts): sequence = str(shortcut.key) if sequence == self.new_sequence and \ (shortcut.context == self.context or shortcut.context == '_' or self.context == '_'): conflicts.append(shortcut) return conflicts def update_warning(self, warning_type=NO_WARNING, conflicts=[]): """Update warning label to reflect conflict status of new shortcut""" if warning_type == NO_WARNING: warn = False tip = 'This shortcut is correct!' elif warning_type == SEQUENCE_CONFLICT: template = '<i>{0}<b>{1}</b></i>' tip_title = _('The new shorcut conflicts with:') + '<br>' tip_body = '' for s in conflicts: tip_body += ' - {0}: {1}<br>'.format(s.context, s.name) tip_body = tip_body[:-4] # Removing last <br> tip = template.format(tip_title, tip_body) warn = True elif warning_type == IN_BLACKLIST: template = '<i>{0}<b>{1}</b></i>' tip_title = _('Forbidden key sequence!') + '<br>' tip_body = '' use = BLACKLIST[self.new_sequence] if use is not None: tip_body = use tip = template.format(tip_title, tip_body) warn = True elif warning_type == SHIFT_BLACKLIST: template = '<i>{0}<b>{1}</b></i>' tip_title = _('Forbidden key sequence!') + '<br>' tip_body = '' use = BLACKLIST['Shift'] if use is not None: tip_body = use tip = template.format(tip_title, tip_body) warn = True elif warning_type == SEQUENCE_LENGTH: # Sequences with 5 keysequences (i.e. Ctrl+1, Ctrl+2, Ctrl+3, # Ctrl+4, Ctrl+5) are invalid template = '<i>{0}</i>' tip = _('A compound sequence can have {break} a maximum of ' '4 subsequences.{break}').format(**{'break': '<br>'}) warn = True elif warning_type == INVALID_KEY: template = '<i>{0}</i>' tip = _('Invalid key entered') + '<br>' warn = True self.helper_button.show() if warn: self.label_warning.show() self.helper_button.setIcon(get_std_icon('MessageBoxWarning')) self.button_ok.setEnabled(False) else: self.helper_button.setIcon(get_std_icon('DialogApplyButton')) self.label_warning.setText(tip) def set_sequence(self, sequence): """Set the new shortcut and update buttons.""" if not sequence or self.sequence == sequence: self.button_ok.setEnabled(False) different_sequence = False else: self.button_ok.setEnabled(True) different_sequence = True if sys.platform == 'darwin': if 'Meta+Ctrl' in sequence: shown_sequence = sequence.replace('Meta+Ctrl', 'Ctrl+Cmd') elif 'Ctrl+Meta' in sequence: shown_sequence = sequence.replace('Ctrl+Meta', 'Cmd+Ctrl') elif 'Ctrl' in sequence: shown_sequence = sequence.replace('Ctrl', 'Cmd') elif 'Meta' in sequence: shown_sequence = sequence.replace('Meta', 'Ctrl') else: shown_sequence = sequence else: shown_sequence = sequence self.text_new_sequence.setText(shown_sequence) self.new_sequence = sequence conflicts = self.check_conflicts() blacklist = self.new_sequence in BLACKLIST individual_keys = self.new_sequence.split('+') if conflicts and different_sequence: warning_type = SEQUENCE_CONFLICT elif blacklist: warning_type = IN_BLACKLIST elif len(individual_keys) == 2 and individual_keys[0] == 'Shift': warning_type = SHIFT_BLACKLIST else: warning_type = NO_WARNING self.update_warning(warning_type=warning_type, conflicts=conflicts) def validate_sequence(self): """Provide additional checks for accepting or rejecting shortcuts.""" if self.invalid_key_flag: self.update_warning(warning_type=INVALID_KEY) return for mod in MODIFIERS: non_mod = set(self.key_non_modifiers) non_mod.discard(mod) if mod in self.key_non_modifiers: self.key_non_modifiers.remove(mod) self.key_modifiers = self.key_modifiers - non_mod while u'' in self.key_text: self.key_text.remove(u'') self.key_text = [k.upper() for k in self.key_text] # Fix Backtab, Tab issue if os.name == 'nt': if Qt.Key_Backtab in self.key_non_modifiers: idx = self.key_non_modifiers.index(Qt.Key_Backtab) self.key_non_modifiers[idx] = Qt.Key_Tab if len(self.key_modifiers) == 0: # Filter single key allowed if self.key_non_modifiers[0] not in VALID_SINGLE_KEYS: return # Filter elif len(self.key_non_modifiers) > 1: return # QKeySequence accepts a maximum of 4 different sequences if len(self.keys) > 4: # Update warning self.update_warning(warning_type=SEQUENCE_LENGTH) return keys = [] for i in range(len(self.keys)): key_seq = 0 for m in self.key_modifiers: key_seq += MODIFIERS[m] key_seq += self.key_non_modifiers[i] keys.append(key_seq) sequence = QKeySequence(*keys) self.set_sequence(sequence.toString())
class ShortcutEditor(QDialog): """A dialog for entering key sequences.""" def __init__(self, parent, context, name, sequence, shortcuts): super(ShortcutEditor, self).__init__(parent) self._parent = parent self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.context = context self.name = name self.shortcuts = shortcuts self.current_sequence = sequence or _('<None>') self._qsequences = list() self.setup() self.update_warning() @property def new_sequence(self): """Return a string representation of the new key sequence.""" return ', '.join(self._qsequences) @property def new_qsequence(self): """Return the QKeySequence object of the new key sequence.""" return QKeySequence(self.new_sequence) def setup(self): """Setup the ShortcutEditor with the provided arguments.""" # Widgets icon_info = HelperToolButton() icon_info.setIcon(get_std_icon('MessageBoxInformation')) layout_icon_info = QVBoxLayout() layout_icon_info.setContentsMargins(0, 0, 0, 0) layout_icon_info.setSpacing(0) layout_icon_info.addWidget(icon_info) layout_icon_info.addStretch(100) self.label_info = QLabel() self.label_info.setText( _("Press the new shortcut and select 'Ok' to confirm, " "click 'Cancel' to revert to the previous state, " "or use 'Clear' to unbind the command from a shortcut.")) self.label_info.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.label_info.setWordWrap(True) layout_info = QHBoxLayout() layout_info.setContentsMargins(0, 0, 0, 0) layout_info.addLayout(layout_icon_info) layout_info.addWidget(self.label_info) layout_info.setStretch(1, 100) self.label_current_sequence = QLabel(_("Current shortcut:")) self.text_current_sequence = QLabel(self.current_sequence) self.label_new_sequence = QLabel(_("New shortcut:")) self.text_new_sequence = ShortcutLineEdit(self) self.text_new_sequence.setPlaceholderText(_("Press shortcut.")) self.helper_button = HelperToolButton() self.helper_button.setIcon(QIcon()) self.label_warning = QLabel() self.label_warning.setWordWrap(True) self.label_warning.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.button_default = QPushButton(_('Default')) self.button_ok = QPushButton(_('Ok')) self.button_ok.setEnabled(False) self.button_clear = QPushButton(_('Clear')) self.button_cancel = QPushButton(_('Cancel')) button_box = QHBoxLayout() button_box.addWidget(self.button_default) button_box.addStretch(100) button_box.addWidget(self.button_ok) button_box.addWidget(self.button_clear) button_box.addWidget(self.button_cancel) # New Sequence button box self.btn_clear_sequence = create_toolbutton( self, icon=ima.icon('editclear'), tip=_("Clear all entered key sequences"), triggered=self.clear_new_sequence) self.button_back_sequence = create_toolbutton( self, icon=ima.icon('ArrowBack'), tip=_("Remove last key sequence entered"), triggered=self.back_new_sequence) newseq_btnbar = QHBoxLayout() newseq_btnbar.setSpacing(0) newseq_btnbar.setContentsMargins(0, 0, 0, 0) newseq_btnbar.addWidget(self.button_back_sequence) newseq_btnbar.addWidget(self.btn_clear_sequence) # Setup widgets self.setWindowTitle(_('Shortcut: {0}').format(self.name)) self.helper_button.setToolTip('') style = """ QToolButton { margin:1px; border: 0px solid grey; padding:0px; border-radius: 0px; }""" self.helper_button.setStyleSheet(style) icon_info.setToolTip('') icon_info.setStyleSheet(style) # Layout layout_sequence = QGridLayout() layout_sequence.setContentsMargins(0, 0, 0, 0) layout_sequence.addLayout(layout_info, 0, 0, 1, 4) layout_sequence.addItem(QSpacerItem(15, 15), 1, 0, 1, 4) layout_sequence.addWidget(self.label_current_sequence, 2, 0) layout_sequence.addWidget(self.text_current_sequence, 2, 2) layout_sequence.addWidget(self.label_new_sequence, 3, 0) layout_sequence.addWidget(self.helper_button, 3, 1) layout_sequence.addWidget(self.text_new_sequence, 3, 2) layout_sequence.addLayout(newseq_btnbar, 3, 3) layout_sequence.addWidget(self.label_warning, 4, 2, 1, 2) layout_sequence.setColumnStretch(2, 100) layout_sequence.setRowStretch(4, 100) layout = QVBoxLayout(self) layout.addLayout(layout_sequence) layout.addSpacing(10) layout.addLayout(button_box) layout.setSizeConstraint(layout.SetFixedSize) # Signals self.button_ok.clicked.connect(self.accept_override) self.button_clear.clicked.connect(self.unbind_shortcut) self.button_cancel.clicked.connect(self.reject) self.button_default.clicked.connect(self.set_sequence_to_default) # Set all widget to no focus so that we can register <Tab> key # press event. widgets = ( self.label_warning, self.helper_button, self.text_new_sequence, self.button_clear, self.button_default, self.button_cancel, self.button_ok, self.btn_clear_sequence, self.button_back_sequence) for w in widgets: w.setFocusPolicy(Qt.NoFocus) w.clearFocus() @Slot() def reject(self): """Slot for rejected signal.""" # Added for spyder-ide/spyder#5426. Due to the focusPolicy of # Qt.NoFocus for the buttons, if the cancel button was clicked without # first setting focus to the button, it would cause a seg fault crash. self.button_cancel.setFocus() super(ShortcutEditor, self).reject() @Slot() def accept(self): """Slot for accepted signal.""" # Added for spyder-ide/spyder#5426. Due to the focusPolicy of # Qt.NoFocus for the buttons, if the cancel button was clicked without # first setting focus to the button, it would cause a seg fault crash. self.button_ok.setFocus() super(ShortcutEditor, self).accept() def event(self, event): """Qt method override.""" # We reroute all ShortcutOverride events to our keyPressEvent and block # any KeyPress and Shortcut event. This allows to register default # Qt shortcuts for which no key press event are emitted. # See spyder-ide/spyder/issues/10786. if event.type() == QEvent.ShortcutOverride: self.keyPressEvent(event) return True elif event.type() in [QEvent.KeyPress, QEvent.Shortcut]: return True else: return super(ShortcutEditor, self).event(event) def keyPressEvent(self, event): """Qt method override.""" event_key = event.key() if not event_key or event_key == Qt.Key_unknown: return if len(self._qsequences) == 4: # QKeySequence accepts a maximum of 4 different sequences. return if event_key in [Qt.Key_Control, Qt.Key_Shift, Qt.Key_Alt, Qt.Key_Meta]: # The event corresponds to just and only a special key. return translator = ShortcutTranslator() event_keyseq = translator.keyevent_to_keyseq(event) event_keystr = event_keyseq.toString(QKeySequence.PortableText) self._qsequences.append(event_keystr) self.update_warning() def check_conflicts(self): """Check shortcuts for conflicts.""" conflicts = [] if len(self._qsequences) == 0: return conflicts new_qsequence = self.new_qsequence for shortcut in self.shortcuts: shortcut_qsequence = QKeySequence.fromString(str(shortcut.key)) if shortcut_qsequence.isEmpty(): continue if (shortcut.context, shortcut.name) == (self.context, self.name): continue if shortcut.context in [self.context, '_'] or self.context == '_': if (shortcut_qsequence.matches(new_qsequence) or new_qsequence.matches(shortcut_qsequence)): conflicts.append(shortcut) return conflicts def check_ascii(self): """ Check that all characters in the new sequence are ascii or else the shortcut will not work. """ try: self.new_sequence.encode('ascii') except UnicodeEncodeError: return False else: return True def check_singlekey(self): """Check if the first sub-sequence of the new key sequence is valid.""" if len(self._qsequences) == 0: return True else: keystr = self._qsequences[0] valid_single_keys = (EDITOR_SINGLE_KEYS if self.context == 'editor' else SINGLE_KEYS) if any((m in keystr for m in ('Ctrl', 'Alt', 'Shift', 'Meta'))): return True else: # This means that the the first subsequence is composed of # a single key with no modifier. valid_single_keys = (EDITOR_SINGLE_KEYS if self.context == 'editor' else SINGLE_KEYS) if any((k == keystr for k in valid_single_keys)): return True else: return False def update_warning(self): """Update the warning label, buttons state and sequence text.""" new_qsequence = self.new_qsequence new_sequence = self.new_sequence self.text_new_sequence.setText( new_qsequence.toString(QKeySequence.NativeText)) conflicts = self.check_conflicts() if len(self._qsequences) == 0: warning = SEQUENCE_EMPTY tip = '' icon = QIcon() elif conflicts: warning = SEQUENCE_CONFLICT template = '<p style="margin-bottom: 0.3em">{0}</p>{1}{2}' tip_title = _('This key sequence conflicts with:') tip_body = '' for s in conflicts: tip_body += ' ' * 2 tip_body += ' - {0}: <b>{1}</b><br>'.format(s.context, s.name) tip_body += '<br>' if len(conflicts) == 1: tip_override = _("Press 'Ok' to unbind it and assign it to") else: tip_override = _("Press 'Ok' to unbind them and assign it to") tip_override += ' <b>{}</b>.'.format(self.name) tip = template.format(tip_title, tip_body, tip_override) icon = get_std_icon('MessageBoxWarning') elif new_sequence in BLACKLIST: warning = IN_BLACKLIST tip = _('This key sequence is forbidden.') icon = get_std_icon('MessageBoxWarning') elif self.check_singlekey() is False or self.check_ascii() is False: warning = INVALID_KEY tip = _('This key sequence is invalid.') icon = get_std_icon('MessageBoxWarning') else: warning = NO_WARNING tip = _('This key sequence is valid.') icon = get_std_icon('DialogApplyButton') self.warning = warning self.conflicts = conflicts self.helper_button.setIcon(icon) self.button_ok.setEnabled( self.warning in [NO_WARNING, SEQUENCE_CONFLICT]) self.label_warning.setText(tip) def set_sequence_from_str(self, sequence): """ This is a convenience method to set the new QKeySequence of the shortcut editor from a string. """ self._qsequences = [QKeySequence(s) for s in sequence.split(', ')] self.update_warning() def set_sequence_to_default(self): """Set the new sequence to the default value defined in the config.""" sequence = CONF.get_default( 'shortcuts', "{}/{}".format(self.context, self.name)) if sequence: self._qsequences = sequence.split(', ') self.update_warning() else: self.unbind_shortcut() def back_new_sequence(self): """Remove the last subsequence from the sequence compound.""" self._qsequences = self._qsequences[:-1] self.update_warning() def clear_new_sequence(self): """Clear the new sequence.""" self._qsequences = [] self.update_warning() def unbind_shortcut(self): """Unbind the shortcut.""" self._qsequences = [] self.accept() def accept_override(self): """Unbind all conflicted shortcuts, and accept the new one""" conflicts = self.check_conflicts() if conflicts: for shortcut in conflicts: shortcut.key = '' self.accept()
class NumpyArrayDialog(QDialog): def __init__(self, parent=None, inline=True, offset=0, force_float=False): QDialog.__init__(self, parent=parent) self._parent = parent self._text = None self._valid = None self._offset = offset # TODO: add this as an option in the General Preferences? self._force_float = force_float self._help_inline = _(""" <b>Numpy Array/Matrix Helper</b><br> Type an array in Matlab : <code>[1 2;3 4]</code><br> or Spyder simplified syntax : <code>1 2;3 4</code> <br><br> Hit 'Enter' for array or 'Ctrl+Enter' for matrix. <br><br> <b>Hint:</b><br> Use two spaces or two tabs to generate a ';'. """) self._help_table = _(""" <b>Numpy Array/Matrix Helper</b><br> Enter an array in the table. <br> Use Tab to move between cells. <br><br> Hit 'Enter' for array or 'Ctrl+Enter' for matrix. <br><br> <b>Hint:</b><br> Use two tabs at the end of a row to move to the next row. """) # Widgets self._button_warning = QToolButton() self._button_help = HelperToolButton() self._button_help.setIcon(ima.icon('MessageBoxInformation')) if is_dark_interface(): style = """ QToolButton { border: 1px solid grey; padding:0px; border-radius: 2px; background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #32414B, stop: 1 #16252B); } """ else: style = """ QToolButton { border: 1px solid grey; padding:0px; border-radius: 2px; background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f6f7fa, stop: 1 #dadbde); } """ self._button_help.setStyleSheet(style) if inline: self._button_help.setToolTip(self._help_inline) self._text = NumpyArrayInline(self) self._widget = self._text else: self._button_help.setToolTip(self._help_table) self._table = NumpyArrayTable(self) self._widget = self._table style = """ QDialog { margin:0px; border: 1px solid grey; padding:0px; border-radius: 2px; }""" self.setStyleSheet(style) style = """ QToolButton { margin:1px; border: 0px solid grey; padding:0px; border-radius: 0px; }""" self._button_warning.setStyleSheet(style) # widget setup self.setWindowFlags(Qt.Window | Qt.Dialog | Qt.FramelessWindowHint) self.setModal(True) self.setWindowOpacity(0.90) self._widget.setMinimumWidth(200) # layout self._layout = QHBoxLayout() self._layout.addWidget(self._widget) self._layout.addWidget(self._button_warning, 1, Qt.AlignTop) self._layout.addWidget(self._button_help, 1, Qt.AlignTop) self.setLayout(self._layout) self._widget.setFocus() def keyPressEvent(self, event): """ Qt override. """ QToolTip.hideText() ctrl = event.modifiers() & Qt.ControlModifier if event.key() in [Qt.Key_Enter, Qt.Key_Return]: if ctrl: self.process_text(array=False) else: self.process_text(array=True) self.accept() else: QDialog.keyPressEvent(self, event) def event(self, event): """ Qt Override. Usefull when in line edit mode. """ if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Tab: return False return QWidget.event(self, event) def process_text(self, array=True): """ Construct the text based on the entered content in the widget. """ if array: prefix = 'np.array([[' else: prefix = 'np.matrix([[' suffix = ']])' values = self._widget.text().strip() if values != '': # cleans repeated spaces exp = r'(\s*)' + ROW_SEPARATOR + r'(\s*)' values = re.sub(exp, ROW_SEPARATOR, values) values = re.sub(r"\s+", " ", values) values = re.sub(r"]$", "", values) values = re.sub(r"^\[", "", values) values = re.sub(ROW_SEPARATOR + r'*$', '', values) # replaces spaces by commas values = values.replace(' ', ELEMENT_SEPARATOR) # iterate to find number of rows and columns new_values = [] rows = values.split(ROW_SEPARATOR) nrows = len(rows) ncols = [] for row in rows: new_row = [] elements = row.split(ELEMENT_SEPARATOR) ncols.append(len(elements)) for e in elements: num = e # replaces not defined values if num in NAN_VALUES: num = 'np.nan' # Convert numbers to floating point if self._force_float: try: num = str(float(e)) except: pass new_row.append(num) new_values.append(ELEMENT_SEPARATOR.join(new_row)) new_values = ROW_SEPARATOR.join(new_values) values = new_values # Check validity if len(set(ncols)) == 1: self._valid = True else: self._valid = False # Single rows are parsed as 1D arrays/matrices if nrows == 1: prefix = prefix[:-1] suffix = suffix.replace("]])", "])") # Fix offset offset = self._offset braces = BRACES.replace(' ', '\n' + ' ' * (offset + len(prefix) - 1)) values = values.replace(ROW_SEPARATOR, braces) text = "{0}{1}{2}".format(prefix, values, suffix) self._text = text else: self._text = '' self.update_warning() def update_warning(self): """ Updates the icon and tip based on the validity of the array content. """ widget = self._button_warning if not self.is_valid(): tip = _('Array dimensions not valid') widget.setIcon(ima.icon('MessageBoxWarning')) widget.setToolTip(tip) QToolTip.showText(self._widget.mapToGlobal(QPoint(0, 5)), tip) else: self._button_warning.setToolTip('') def is_valid(self): """ Return if the current array state is valid. """ return self._valid def text(self): """ Return the parsed array/matrix text. """ return self._text @property def array_widget(self): """ Return the array builder widget. """ return self._widget
class ShortcutEditor(QDialog): """A dialog for entering key sequences.""" def __init__(self, parent, context, name, sequence, shortcuts): super(ShortcutEditor, self).__init__(parent) self._parent = parent self.context = context self.npressed = 0 self.keys = set() self.key_modifiers = set() self.key_non_modifiers = list() self.key_text = list() self.sequence = sequence self.new_sequence = None self.edit_state = True self.shortcuts = shortcuts # Widgets self.label_info = QLabel() self.label_info.setText(_("Press the new shortcut and select 'Ok': \n" "(Press 'Tab' once to switch focus between the shortcut entry \n" "and the buttons below it)")) self.label_current_sequence = QLabel(_("Current shortcut:")) self.text_current_sequence = QLabel(sequence) self.label_new_sequence = QLabel(_("New shortcut:")) self.text_new_sequence = CustomLineEdit(self) self.text_new_sequence.setPlaceholderText(sequence) self.helper_button = HelperToolButton() self.helper_button.hide() self.label_warning = QLabel() self.label_warning.hide() bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_ok = bbox.button(QDialogButtonBox.Ok) self.button_cancel = bbox.button(QDialogButtonBox.Cancel) # Setup widgets self.setWindowTitle(_('Shortcut: {0}').format(name)) self.button_ok.setFocusPolicy(Qt.NoFocus) self.button_ok.setEnabled(False) self.button_cancel.setFocusPolicy(Qt.NoFocus) self.helper_button.setToolTip('') self.helper_button.setFocusPolicy(Qt.NoFocus) style = """ QToolButton { margin:1px; border: 0px solid grey; padding:0px; border-radius: 0px; }""" self.helper_button.setStyleSheet(style) self.text_new_sequence.setFocusPolicy(Qt.NoFocus) self.label_warning.setFocusPolicy(Qt.NoFocus) # Layout spacing = 5 layout_sequence = QGridLayout() layout_sequence.addWidget(self.label_info, 0, 0, 1, 3) layout_sequence.addItem(QSpacerItem(spacing, spacing), 1, 0, 1, 2) layout_sequence.addWidget(self.label_current_sequence, 2, 0) layout_sequence.addWidget(self.text_current_sequence, 2, 2) layout_sequence.addWidget(self.label_new_sequence, 3, 0) layout_sequence.addWidget(self.helper_button, 3, 1) layout_sequence.addWidget(self.text_new_sequence, 3, 2) layout_sequence.addWidget(self.label_warning, 4, 2, 1, 2) layout = QVBoxLayout() layout.addLayout(layout_sequence) layout.addSpacing(spacing) layout.addWidget(bbox) self.setLayout(layout) # Signals bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) @Slot() def reject(self): """Slot for rejected signal.""" # Added for issue #5426. Due to the focusPolicy of Qt.NoFocus for the # buttons, if the cancel button was clicked without first setting focus # to the button, it would cause a seg fault crash. self.button_cancel.setFocus() super(ShortcutEditor, self).reject() @Slot() def accept(self): """Slot for accepted signal.""" # Added for issue #5426. Due to the focusPolicy of Qt.NoFocus for the # buttons, if the ok button was clicked without first setting focus to # the button, it would cause a seg fault crash. self.button_ok.setFocus() super(ShortcutEditor, self).accept() def keyPressEvent(self, e): """Qt override.""" key = e.key() # Check if valid keys if key not in VALID_KEYS: self.invalid_key_flag = True return self.npressed += 1 self.key_non_modifiers.append(key) self.key_modifiers.add(key) self.key_text.append(e.text()) self.invalid_key_flag = False debug_print('key {0}, npressed: {1}'.format(key, self.npressed)) if key == Qt.Key_unknown: return # The user clicked just and only the special keys # Ctrl, Shift, Alt, Meta. if (key == Qt.Key_Control or key == Qt.Key_Shift or key == Qt.Key_Alt or key == Qt.Key_Meta): return modifiers = e.modifiers() if modifiers & Qt.ShiftModifier: key += Qt.SHIFT if modifiers & Qt.ControlModifier: key += Qt.CTRL if sys.platform == 'darwin': self.npressed -= 1 debug_print('decrementing') if modifiers & Qt.AltModifier: key += Qt.ALT if modifiers & Qt.MetaModifier: key += Qt.META self.keys.add(key) def toggle_state(self): """Switch between shortcut entry and Accept/Cancel shortcut mode.""" self.edit_state = not self.edit_state if not self.edit_state: self.text_new_sequence.setEnabled(False) if self.button_ok.isEnabled(): self.button_ok.setFocus() else: self.button_cancel.setFocus() else: self.text_new_sequence.setEnabled(True) self.text_new_sequence.setFocus() def nonedit_keyrelease(self, e): """Key release event for non-edit state.""" key = e.key() if key in [Qt.Key_Escape]: self.close() return if key in [Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down]: if self.button_ok.hasFocus(): self.button_cancel.setFocus() else: self.button_ok.setFocus() def keyReleaseEvent(self, e): """Qt override.""" self.npressed -= 1 if self.npressed <= 0: key = e.key() if len(self.keys) == 1 and key == Qt.Key_Tab: self.toggle_state() return if len(self.keys) == 1 and key == Qt.Key_Escape: self.set_sequence('') self.label_warning.setText(_("Please introduce a different " "shortcut")) if len(self.keys) == 1 and key in [Qt.Key_Return, Qt.Key_Enter]: self.toggle_state() return if not self.edit_state: self.nonedit_keyrelease(e) else: debug_print('keys: {}'.format(self.keys)) if self.keys and key != Qt.Key_Escape: self.validate_sequence() self.keys = set() self.key_modifiers = set() self.key_non_modifiers = list() self.key_text = list() self.npressed = 0 def check_conflicts(self): """Check shortcuts for conflicts.""" conflicts = [] for index, shortcut in enumerate(self.shortcuts): sequence = str(shortcut.key) if sequence == self.new_sequence and \ (shortcut.context == self.context or shortcut.context == '_' or self.context == '_'): conflicts.append(shortcut) return conflicts def update_warning(self, warning_type=NO_WARNING, conflicts=[]): """Update warning label to reflect conflict status of new shortcut""" if warning_type == NO_WARNING: warn = False tip = 'This shortcut is correct!' elif warning_type == SEQUENCE_CONFLICT: template = '<i>{0}<b>{1}</b></i>' tip_title = _('The new shortcut conflicts with:') + '<br>' tip_body = '' for s in conflicts: tip_body += ' - {0}: {1}<br>'.format(s.context, s.name) tip_body = tip_body[:-4] # Removing last <br> tip = template.format(tip_title, tip_body) warn = True elif warning_type == IN_BLACKLIST: template = '<i>{0}<b>{1}</b></i>' tip_title = _('Forbidden key sequence!') + '<br>' tip_body = '' use = BLACKLIST[self.new_sequence] if use is not None: tip_body = use tip = template.format(tip_title, tip_body) warn = True elif warning_type == SHIFT_BLACKLIST: template = '<i>{0}<b>{1}</b></i>' tip_title = _('Forbidden key sequence!') + '<br>' tip_body = '' use = BLACKLIST['Shift'] if use is not None: tip_body = use tip = template.format(tip_title, tip_body) warn = True elif warning_type == SEQUENCE_LENGTH: # Sequences with 5 keysequences (i.e. Ctrl+1, Ctrl+2, Ctrl+3, # Ctrl+4, Ctrl+5) are invalid template = '<i>{0}</i>' tip = _('A compound sequence can have {break} a maximum of ' '4 subsequences.{break}').format(**{'break': '<br>'}) warn = True elif warning_type == INVALID_KEY: template = '<i>{0}</i>' tip = _('Invalid key entered') + '<br>' warn = True self.helper_button.show() if warn: self.label_warning.show() self.helper_button.setIcon(get_std_icon('MessageBoxWarning')) self.button_ok.setEnabled(False) else: self.helper_button.setIcon(get_std_icon('DialogApplyButton')) self.label_warning.setText(tip) def set_sequence(self, sequence): """Set the new shortcut and update buttons.""" if not sequence or self.sequence == sequence: self.button_ok.setEnabled(False) different_sequence = False else: self.button_ok.setEnabled(True) different_sequence = True if sys.platform == 'darwin': if 'Meta+Ctrl' in sequence: shown_sequence = sequence.replace('Meta+Ctrl', 'Ctrl+Cmd') elif 'Ctrl+Meta' in sequence: shown_sequence = sequence.replace('Ctrl+Meta', 'Cmd+Ctrl') elif 'Ctrl' in sequence: shown_sequence = sequence.replace('Ctrl', 'Cmd') elif 'Meta' in sequence: shown_sequence = sequence.replace('Meta', 'Ctrl') else: shown_sequence = sequence else: shown_sequence = sequence self.text_new_sequence.setText(shown_sequence) self.new_sequence = sequence conflicts = self.check_conflicts() blacklist = self.new_sequence in BLACKLIST individual_keys = self.new_sequence.split('+') if conflicts and different_sequence: warning_type = SEQUENCE_CONFLICT elif blacklist: warning_type = IN_BLACKLIST elif len(individual_keys) == 2 and individual_keys[0] == 'Shift': warning_type = SHIFT_BLACKLIST else: warning_type = NO_WARNING self.update_warning(warning_type=warning_type, conflicts=conflicts) def validate_sequence(self): """Provide additional checks for accepting or rejecting shortcuts.""" if self.invalid_key_flag: self.update_warning(warning_type=INVALID_KEY) return for mod in MODIFIERS: non_mod = set(self.key_non_modifiers) non_mod.discard(mod) if mod in self.key_non_modifiers: self.key_non_modifiers.remove(mod) self.key_modifiers = self.key_modifiers - non_mod while u'' in self.key_text: self.key_text.remove(u'') self.key_text = [k.upper() for k in self.key_text] # Fix Backtab, Tab issue if Qt.Key_Backtab in self.key_non_modifiers: idx = self.key_non_modifiers.index(Qt.Key_Backtab) self.key_non_modifiers[idx] = Qt.Key_Tab if len(self.key_modifiers) == 0: # Filter single key allowed if self.key_non_modifiers[0] not in VALID_SINGLE_KEYS: return # Filter elif len(self.key_non_modifiers) > 1: return # QKeySequence accepts a maximum of 4 different sequences if len(self.keys) > 4: # Update warning self.update_warning(warning_type=SEQUENCE_LENGTH) return keys = [] for i in range(len(self.keys)): key_seq = 0 for m in self.key_modifiers: key_seq += MODIFIERS[m] key_seq += self.key_non_modifiers[i] keys.append(key_seq) sequence = QKeySequence(*keys) self.set_sequence(sequence.toString())
def setup(self): """Setup the ShortcutEditor with the provided arguments.""" # Widgets icon_info = HelperToolButton() icon_info.setIcon(get_std_icon('MessageBoxInformation')) layout_icon_info = QVBoxLayout() layout_icon_info.setContentsMargins(0, 0, 0, 0) layout_icon_info.setSpacing(0) layout_icon_info.addWidget(icon_info) layout_icon_info.addStretch(100) self.label_info = QLabel() self.label_info.setText( _("Press the new shortcut and select 'Ok' to confirm, " "click 'Cancel' to revert to the previous state, " "or use 'Clear' to unbind the command from a shortcut.")) self.label_info.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.label_info.setWordWrap(True) layout_info = QHBoxLayout() layout_info.setContentsMargins(0, 0, 0, 0) layout_info.addLayout(layout_icon_info) layout_info.addWidget(self.label_info) layout_info.setStretch(1, 100) self.label_current_sequence = QLabel(_("Current shortcut:")) self.text_current_sequence = QLabel(self.current_sequence) self.label_new_sequence = QLabel(_("New shortcut:")) self.text_new_sequence = ShortcutLineEdit(self) self.text_new_sequence.setPlaceholderText(_("Press shortcut.")) self.helper_button = HelperToolButton() self.helper_button.setIcon(QIcon()) self.label_warning = QLabel() self.label_warning.setWordWrap(True) self.label_warning.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.button_default = QPushButton(_('Default')) self.button_ok = QPushButton(_('Ok')) self.button_ok.setEnabled(False) self.button_clear = QPushButton(_('Clear')) self.button_cancel = QPushButton(_('Cancel')) button_box = QHBoxLayout() button_box.addWidget(self.button_default) button_box.addStretch(100) button_box.addWidget(self.button_ok) button_box.addWidget(self.button_clear) button_box.addWidget(self.button_cancel) # New Sequence button box self.btn_clear_sequence = create_toolbutton( self, icon=ima.icon('editclear'), tip=_("Clear all entered key sequences"), triggered=self.clear_new_sequence) self.button_back_sequence = create_toolbutton( self, icon=ima.icon('ArrowBack'), tip=_("Remove last key sequence entered"), triggered=self.back_new_sequence) newseq_btnbar = QHBoxLayout() newseq_btnbar.setSpacing(0) newseq_btnbar.setContentsMargins(0, 0, 0, 0) newseq_btnbar.addWidget(self.button_back_sequence) newseq_btnbar.addWidget(self.btn_clear_sequence) # Setup widgets self.setWindowTitle(_('Shortcut: {0}').format(self.name)) self.helper_button.setToolTip('') style = """ QToolButton { margin:1px; border: 0px solid grey; padding:0px; border-radius: 0px; }""" self.helper_button.setStyleSheet(style) icon_info.setToolTip('') icon_info.setStyleSheet(style) # Layout layout_sequence = QGridLayout() layout_sequence.setContentsMargins(0, 0, 0, 0) layout_sequence.addLayout(layout_info, 0, 0, 1, 4) layout_sequence.addItem(QSpacerItem(15, 15), 1, 0, 1, 4) layout_sequence.addWidget(self.label_current_sequence, 2, 0) layout_sequence.addWidget(self.text_current_sequence, 2, 2) layout_sequence.addWidget(self.label_new_sequence, 3, 0) layout_sequence.addWidget(self.helper_button, 3, 1) layout_sequence.addWidget(self.text_new_sequence, 3, 2) layout_sequence.addLayout(newseq_btnbar, 3, 3) layout_sequence.addWidget(self.label_warning, 4, 2, 1, 2) layout_sequence.setColumnStretch(2, 100) layout_sequence.setRowStretch(4, 100) layout = QVBoxLayout() layout.addLayout(layout_sequence) layout.addSpacing(5) layout.addLayout(button_box) self.setLayout(layout) # Signals self.button_ok.clicked.connect(self.accept_override) self.button_clear.clicked.connect(self.unbind_shortcut) self.button_cancel.clicked.connect(self.reject) self.button_default.clicked.connect(self.set_sequence_to_default) # Set all widget to no focus so that we can register <Tab> key # press event. widgets = ( self.label_warning, self.helper_button, self.text_new_sequence, self.button_clear, self.button_default, self.button_cancel, self.button_ok, self.btn_clear_sequence, self.button_back_sequence) for w in widgets: w.setFocusPolicy(Qt.NoFocus) w.clearFocus()
class ShortcutEditor(QDialog): """A dialog for entering key sequences.""" def __init__(self, parent, context, name, sequence, shortcuts): super(ShortcutEditor, self).__init__(parent) self._parent = parent self.context = context self.name = name self.shortcuts = shortcuts self.current_sequence = sequence self._qsequences = list() self.setup() self.update_warning() @property def new_sequence(self): """Return a string representation of the new key sequence.""" return ', '.join(self._qsequences) @property def new_qsequence(self): """Return the QKeySequence object of the new key sequence.""" return QKeySequence(self.new_sequence) def setup(self): """Setup the ShortcutEditor with the provided arguments.""" # Widgets icon_info = HelperToolButton() icon_info.setIcon(get_std_icon('MessageBoxInformation')) layout_icon_info = QVBoxLayout() layout_icon_info.setContentsMargins(0, 0, 0, 0) layout_icon_info.setSpacing(0) layout_icon_info.addWidget(icon_info) layout_icon_info.addStretch(100) self.label_info = QLabel() self.label_info.setText( _("Press the new shortcut and select 'Ok' to confirm, " "click 'Cancel' to revert to the previous state, " "or use 'Clear' to unbind the command from a shortcut.")) self.label_info.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.label_info.setWordWrap(True) layout_info = QHBoxLayout() layout_info.setContentsMargins(0, 0, 0, 0) layout_info.addLayout(layout_icon_info) layout_info.addWidget(self.label_info) layout_info.setStretch(1, 100) self.label_current_sequence = QLabel(_("Current shortcut:")) self.text_current_sequence = QLabel(self.current_sequence) self.label_new_sequence = QLabel(_("New shortcut:")) self.text_new_sequence = ShortcutLineEdit(self) self.text_new_sequence.setPlaceholderText(_("Press shortcut.")) self.helper_button = HelperToolButton() self.helper_button.setIcon(QIcon()) self.label_warning = QLabel() self.label_warning.setWordWrap(True) self.label_warning.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.button_default = QPushButton(_('Default')) self.button_ok = QPushButton(_('Ok')) self.button_ok.setEnabled(False) self.button_clear = QPushButton(_('Clear')) self.button_cancel = QPushButton(_('Cancel')) button_box = QHBoxLayout() button_box.addWidget(self.button_default) button_box.addStretch(100) button_box.addWidget(self.button_ok) button_box.addWidget(self.button_clear) button_box.addWidget(self.button_cancel) # New Sequence button box self.btn_clear_sequence = create_toolbutton( self, icon=ima.icon('editclear'), tip=_("Clear all entered key sequences"), triggered=self.clear_new_sequence) self.button_back_sequence = create_toolbutton( self, icon=ima.icon('ArrowBack'), tip=_("Remove last key sequence entered"), triggered=self.back_new_sequence) newseq_btnbar = QHBoxLayout() newseq_btnbar.setSpacing(0) newseq_btnbar.setContentsMargins(0, 0, 0, 0) newseq_btnbar.addWidget(self.button_back_sequence) newseq_btnbar.addWidget(self.btn_clear_sequence) # Setup widgets self.setWindowTitle(_('Shortcut: {0}').format(self.name)) self.helper_button.setToolTip('') style = """ QToolButton { margin:1px; border: 0px solid grey; padding:0px; border-radius: 0px; }""" self.helper_button.setStyleSheet(style) icon_info.setToolTip('') icon_info.setStyleSheet(style) # Layout layout_sequence = QGridLayout() layout_sequence.setContentsMargins(0, 0, 0, 0) layout_sequence.addLayout(layout_info, 0, 0, 1, 4) layout_sequence.addItem(QSpacerItem(15, 15), 1, 0, 1, 4) layout_sequence.addWidget(self.label_current_sequence, 2, 0) layout_sequence.addWidget(self.text_current_sequence, 2, 2) layout_sequence.addWidget(self.label_new_sequence, 3, 0) layout_sequence.addWidget(self.helper_button, 3, 1) layout_sequence.addWidget(self.text_new_sequence, 3, 2) layout_sequence.addLayout(newseq_btnbar, 3, 3) layout_sequence.addWidget(self.label_warning, 4, 2, 1, 2) layout_sequence.setColumnStretch(2, 100) layout_sequence.setRowStretch(4, 100) layout = QVBoxLayout() layout.addLayout(layout_sequence) layout.addSpacing(5) layout.addLayout(button_box) self.setLayout(layout) # Signals self.button_ok.clicked.connect(self.accept_override) self.button_clear.clicked.connect(self.unbind_shortcut) self.button_cancel.clicked.connect(self.reject) self.button_default.clicked.connect(self.set_sequence_to_default) # Set all widget to no focus so that we can register <Tab> key # press event. widgets = ( self.label_warning, self.helper_button, self.text_new_sequence, self.button_clear, self.button_default, self.button_cancel, self.button_ok, self.btn_clear_sequence, self.button_back_sequence) for w in widgets: w.setFocusPolicy(Qt.NoFocus) w.clearFocus() @Slot() def reject(self): """Slot for rejected signal.""" # Added for issue #5426. Due to the focusPolicy of Qt.NoFocus for the # buttons, if the cancel button was clicked without first setting focus # to the button, it would cause a seg fault crash. self.button_cancel.setFocus() super(ShortcutEditor, self).reject() @Slot() def accept(self): """Slot for accepted signal.""" # Added for issue #5426. Due to the focusPolicy of Qt.NoFocus for the # buttons, if the ok button was clicked without first setting focus to # the button, it would cause a seg fault crash. self.button_ok.setFocus() super(ShortcutEditor, self).accept() def event(self, event): """Qt method override.""" if event.type() in (QEvent.Shortcut, QEvent.ShortcutOverride): return True else: return super(ShortcutEditor, self).event(event) def keyPressEvent(self, event): """Qt method override.""" event_key = event.key() if not event_key or event_key == Qt.Key_unknown: return if len(self._qsequences) == 4: # QKeySequence accepts a maximum of 4 different sequences. return if event_key in [Qt.Key_Control, Qt.Key_Shift, Qt.Key_Alt, Qt.Key_Meta]: # The event corresponds to just and only a special key. return translator = ShortcutTranslator() event_keyseq = translator.keyevent_to_keyseq(event) event_keystr = event_keyseq.toString(QKeySequence.PortableText) self._qsequences.append(event_keystr) self.update_warning() def check_conflicts(self): """Check shortcuts for conflicts.""" conflicts = [] if len(self._qsequences) == 0: return conflicts new_qsequence = self.new_qsequence for shortcut in self.shortcuts: shortcut_qsequence = QKeySequence.fromString(str(shortcut.key)) if shortcut_qsequence.isEmpty(): continue if (shortcut.context, shortcut.name) == (self.context, self.name): continue if shortcut.context in [self.context, '_'] or self.context == '_': if (shortcut_qsequence.matches(new_qsequence) or new_qsequence.matches(shortcut_qsequence)): conflicts.append(shortcut) return conflicts def check_ascii(self): """ Check that all characters in the new sequence are ascii or else the shortcut will not work. """ try: self.new_sequence.encode('ascii') except UnicodeEncodeError: return False else: return True def check_singlekey(self): """Check if the first sub-sequence of the new key sequence is valid.""" if len(self._qsequences) == 0: return True else: keystr = self._qsequences[0] valid_single_keys = (EDITOR_SINGLE_KEYS if self.context == 'editor' else SINGLE_KEYS) if any((m in keystr for m in ('Ctrl', 'Alt', 'Shift', 'Meta'))): return True else: # This means that the the first subsequence is composed of # a single key with no modifier. valid_single_keys = (EDITOR_SINGLE_KEYS if self.context == 'editor' else SINGLE_KEYS) if any((k == keystr for k in valid_single_keys)): return True else: return False def update_warning(self): """Update the warning label, buttons state and sequence text.""" new_qsequence = self.new_qsequence new_sequence = self.new_sequence self.text_new_sequence.setText( new_qsequence.toString(QKeySequence.NativeText)) conflicts = self.check_conflicts() if len(self._qsequences) == 0: warning = SEQUENCE_EMPTY tip = '' icon = QIcon() elif conflicts: warning = SEQUENCE_CONFLICT template = '<i>{0}<b>{1}</b>{2}</i>' tip_title = _('The new shortcut conflicts with:') + '<br>' tip_body = '' for s in conflicts: tip_body += ' - {0}: {1}<br>'.format(s.context, s.name) tip_body = tip_body[:-4] # Removing last <br> tip_override = '<br>Press <b>OK</b> to unbind ' tip_override += 'it' if len(conflicts) == 1 else 'them' tip_override += ' and assign it to <b>{}</b>'.format(self.name) tip = template.format(tip_title, tip_body, tip_override) icon = get_std_icon('MessageBoxWarning') elif new_sequence in BLACKLIST: warning = IN_BLACKLIST template = '<i>{0}<b>{1}</b></i>' tip_title = _('Forbidden key sequence!') + '<br>' tip_body = '' use = BLACKLIST[new_sequence] if use is not None: tip_body = use tip = template.format(tip_title, tip_body) icon = get_std_icon('MessageBoxWarning') elif self.check_singlekey() is False or self.check_ascii() is False: warning = INVALID_KEY template = '<i>{0}</i>' tip = _('Invalid key sequence entered') + '<br>' icon = get_std_icon('MessageBoxWarning') else: warning = NO_WARNING tip = 'This shortcut is valid.' icon = get_std_icon('DialogApplyButton') self.warning = warning self.conflicts = conflicts self.helper_button.setIcon(icon) self.button_ok.setEnabled( self.warning in [NO_WARNING, SEQUENCE_CONFLICT]) self.label_warning.setText(tip) # Everytime after update warning message, update the label height new_height = self.label_warning.sizeHint().height() self.label_warning.setMaximumHeight(new_height) def set_sequence_from_str(self, sequence): """ This is a convenience method to set the new QKeySequence of the shortcut editor from a string. """ self._qsequences = [QKeySequence(s) for s in sequence.split(', ')] self.update_warning() def set_sequence_to_default(self): """Set the new sequence to the default value defined in the config.""" sequence = CONF.get_default( 'shortcuts', "{}/{}".format(self.context, self.name)) self._qsequences = sequence.split(', ') self.update_warning() def back_new_sequence(self): """Remove the last subsequence from the sequence compound.""" self._qsequences = self._qsequences[:-1] self.update_warning() def clear_new_sequence(self): """Clear the new sequence.""" self._qsequences = [] self.update_warning() def unbind_shortcut(self): """Unbind the shortcut.""" self._qsequences = [] self.accept() def accept_override(self): """Unbind all conflicted shortcuts, and accept the new one""" conflicts = self.check_conflicts() if conflicts: for shortcut in conflicts: shortcut.key = '' self.accept()