def __init__(self, parent, plugin, tabs, data, icon): QDialog.__init__(self, parent) # Variables self.plugins_tabs = [] self.plugins_data = [] self.plugins_instances = [] self.add_plugin(plugin, tabs, data, icon) self.plugin = None # Last plugin with focus self.mode = self.FILE_MODE # By default start in this mode self.initial_cursors = None # {fullpath: QCursor} self.initial_path = None # Fullpath of initial active editor self.initial_widget = None # Initial active editor self.line_number = None # Selected line number in filer self.is_visible = False # Is the switcher visible? help_text = _("Press <b>Enter</b> to switch files or <b>Esc</b> to " "cancel.<br><br>Type to filter filenames.<br><br>" "Use <b>:number</b> to go to a line, e.g. " "<b><code>main:42</code></b><br>" "Use <b>@symbol_text</b> to go to a symbol, e.g. " "<b><code>@init</code></b>" "<br><br> Press <b>Ctrl+W</b> to close current tab.<br>") # Either allow searching for a line number or a symbol but not both regex = QRegExp("([A-Za-z0-9_]{0,100}@[A-Za-z0-9_]{0,100})|" + "([A-Za-z0-9_]{0,100}:{0,1}[0-9]{0,100})") # Widgets self.edit = FilesFilterLine(self) self.help = HelperToolButton() self.list = QListWidget(self) self.filter = KeyPressFilter() regex_validator = QRegExpValidator(regex, self.edit) # Widgets setup self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) self.setWindowOpacity(0.95) self.edit.installEventFilter(self.filter) self.edit.setValidator(regex_validator) self.help.setToolTip(help_text) self.list.setItemDelegate(HTMLDelegate(self)) # Layout edit_layout = QHBoxLayout() edit_layout.addWidget(self.edit) edit_layout.addWidget(self.help) layout = QVBoxLayout() layout.addLayout(edit_layout) layout.addWidget(self.list) self.setLayout(layout) # Signals self.rejected.connect(self.restore_initial_state) self.filter.sig_up_key_pressed.connect(self.previous_row) self.filter.sig_down_key_pressed.connect(self.next_row) self.edit.returnPressed.connect(self.accept) self.edit.textChanged.connect(self.setup) self.list.itemSelectionChanged.connect(self.item_selection_changed) self.list.clicked.connect(self.edit.setFocus)
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.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()
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()
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
def __init__(self, parent=None, inline=True, offset=0, force_float=False, language='python'): super(ArrayBuilderDialog, self).__init__(parent=parent) self._language = language self._options = _REGISTERED_ARRAY_BUILDERS.get('python', None) 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: 1, y1: 1, x2: 1, y2: 1, stop: 0 {stop_0}, stop: 1 {stop_1}); }} """).format(stop_0=QStylePalette.COLOR_BACKGROUND_4, stop_1=QStylePalette.COLOR_BACKGROUND_2)) self._button_help.setStyleSheet(style) if inline: self._button_help.setToolTip(self._help_inline) self._text = ArrayInline(self, options=self._options) self._widget = self._text else: self._button_help.setToolTip(self._help_table) self._table = ArrayTable(self, options=self._options) 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()
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 __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)
class FileSwitcher(QDialog): """A Sublime-like file switcher.""" sig_goto_file = Signal(int, object) # Constants that define the mode in which the list widget is working # FILE_MODE is for a list of files, SYMBOL_MODE if for a list of symbols # in a given file when using the '@' symbol. FILE_MODE, SYMBOL_MODE = [1, 2] MAX_WIDTH = 600 def __init__(self, parent, plugin, tabs, data, icon): QDialog.__init__(self, parent) # Variables self.plugins_tabs = [] self.plugins_data = [] self.plugins_instances = [] self.add_plugin(plugin, tabs, data, icon) self.plugin = None # Last plugin with focus self.mode = self.FILE_MODE # By default start in this mode self.initial_cursors = None # {fullpath: QCursor} self.initial_path = None # Fullpath of initial active editor self.initial_widget = None # Initial active editor self.line_number = None # Selected line number in filer self.is_visible = False # Is the switcher visible? help_text = _("Press <b>Enter</b> to switch files or <b>Esc</b> to " "cancel.<br><br>Type to filter filenames.<br><br>" "Use <b>:number</b> to go to a line, e.g. " "<b><code>main:42</code></b><br>" "Use <b>@symbol_text</b> to go to a symbol, e.g. " "<b><code>@init</code></b>" "<br><br> Press <b>Ctrl+W</b> to close current tab.<br>") # Either allow searching for a line number or a symbol but not both regex = QRegExp("([A-Za-z0-9_]{0,100}@[A-Za-z0-9_]{0,100})|" + "([A-Za-z0-9_]{0,100}:{0,1}[0-9]{0,100})") # Widgets self.edit = FilesFilterLine(self) self.help = HelperToolButton() self.list = QListWidget(self) self.filter = KeyPressFilter() regex_validator = QRegExpValidator(regex, self.edit) # Widgets setup self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) self.setWindowOpacity(0.95) self.edit.installEventFilter(self.filter) self.edit.setValidator(regex_validator) self.help.setToolTip(help_text) self.list.setItemDelegate(HTMLDelegate(self)) # Layout edit_layout = QHBoxLayout() edit_layout.addWidget(self.edit) edit_layout.addWidget(self.help) layout = QVBoxLayout() layout.addLayout(edit_layout) layout.addWidget(self.list) self.setLayout(layout) # Signals self.rejected.connect(self.restore_initial_state) self.filter.sig_up_key_pressed.connect(self.previous_row) self.filter.sig_down_key_pressed.connect(self.next_row) self.edit.returnPressed.connect(self.accept) self.edit.textChanged.connect(self.setup) self.list.itemSelectionChanged.connect(self.item_selection_changed) self.list.clicked.connect(self.edit.setFocus) # --- Properties @property def widgets(self): widgets = [] for tabs, plugin in self.plugins_tabs: widgets += [(tabs.widget(index), plugin) for index in range(tabs.count())] return widgets @property def line_count(self): line_count = [] for widget in self.widgets: try: current_line_count = widget[0].get_line_count() except AttributeError: current_line_count = 0 line_count.append(current_line_count) return line_count @property def save_status(self): save_status = [] for da, icon in self.plugins_data: save_status += [getattr(td, 'newly_created', False) for td in da] return save_status @property def paths(self): paths = [] for da, icon in self.plugins_data: paths += [getattr(td, 'filename', None) for td in da] return paths @property def filenames(self): filenames = [] for da, icon in self.plugins_data: filenames += [os.path.basename(getattr(td, 'filename', None)) for td in da] return filenames @property def icons(self): icons = [] for da, icon in self.plugins_data: icons += [icon for td in da] return icons @property def current_path(self): return self.paths_by_widget[self.get_widget()] @property def paths_by_widget(self): widgets = [w[0] for w in self.widgets] return dict(zip(widgets, self.paths)) @property def widgets_by_path(self): widgets = [w[0] for w in self.widgets] return dict(zip(self.paths, widgets)) @property def filter_text(self): """Get the normalized (lowecase) content of the filter text.""" return to_text_string(self.edit.text()).lower() def set_search_text(self, _str): self.edit.setText(_str) def save_initial_state(self): """Save initial cursors and initial active widget.""" paths = self.paths self.initial_widget = self.get_widget() self.initial_cursors = {} for i, editor in enumerate(self.widgets): if editor is self.initial_widget: self.initial_path = paths[i] # This try is needed to make the fileswitcher work with # plugins that does not have a textCursor. try: self.initial_cursors[paths[i]] = editor.textCursor() except AttributeError: pass def accept(self): self.is_visible = False QDialog.accept(self) self.list.clear() def restore_initial_state(self): """Restores initial cursors and initial active editor.""" self.list.clear() self.is_visible = False widgets = self.widgets_by_path if not self.edit.clicked_outside: for path in self.initial_cursors: cursor = self.initial_cursors[path] if path in widgets: self.set_editor_cursor(widgets[path], cursor) if self.initial_widget in self.paths_by_widget: index = self.paths.index(self.initial_path) self.sig_goto_file.emit(index) def set_dialog_position(self): """Positions the file switcher dialog.""" parent = self.parent() geo = parent.geometry() width = self.list.width() # This has been set in setup left = parent.geometry().width()/2 - width/2 # Note: the +1 pixel on the top makes it look better if isinstance(parent, QMainWindow): top = (parent.toolbars_menu.geometry().height() + parent.menuBar().geometry().height() + 1) else: top = self.plugins_tabs[0][0].tabBar().geometry().height() + 1 while parent: geo = parent.geometry() top += geo.top() left += geo.left() parent = parent.parent() self.move(left, top) def get_item_size(self, content): """ Get the max size (width and height) for the elements of a list of strings as a QLabel. """ strings = [] if content: for rich_text in content: label = QLabel(rich_text) label.setTextFormat(Qt.PlainText) strings.append(label.text()) fm = label.fontMetrics() return (max([fm.width(s) * 1.3 for s in strings]), fm.height()) def fix_size(self, content): """ Adjusts the width and height of the file switcher based on the relative size of the parent and content. """ # Update size of dialog based on relative size of the parent if content: width, height = self.get_item_size(content) # Width parent = self.parent() relative_width = parent.geometry().width() * 0.65 if relative_width > self.MAX_WIDTH: relative_width = self.MAX_WIDTH self.list.setMinimumWidth(relative_width) # Height if len(content) < 8: max_entries = len(content) else: max_entries = 8 max_height = height * max_entries * 2.5 self.list.setMinimumHeight(max_height) # Resize self.list.resize(relative_width, self.list.height()) # --- Helper methods: List widget def count(self): """Gets the item count in the list widget.""" return self.list.count() def current_row(self): """Returns the current selected row in the list widget.""" return self.list.currentRow() def set_current_row(self, row): """Sets the current selected row in the list widget.""" return self.list.setCurrentRow(row) def select_row(self, steps): """Select row in list widget based on a number of steps with direction. Steps can be positive (next rows) or negative (previous rows). """ row = self.current_row() + steps if 0 <= row < self.count(): self.set_current_row(row) def previous_row(self): """Select previous row in list widget.""" if self.mode == self.SYMBOL_MODE: self.select_row(-1) return prev_row = self.current_row() - 1 if prev_row >= 0: title = self.list.item(prev_row).text() else: title = '' if prev_row == 0 and '</b></big><br>' in title: self.list.scrollToTop() elif '</b></big><br>' in title: # Select the next previous row, the one following is a title self.select_row(-2) else: self.select_row(-1) def next_row(self): """Select next row in list widget.""" if self.mode == self.SYMBOL_MODE: self.select_row(+1) return next_row = self.current_row() + 1 if next_row < self.count(): if '</b></big><br>' in self.list.item(next_row).text(): # Select the next next row, the one following is a title self.select_row(+2) else: self.select_row(+1) def get_stack_index(self, stack_index, plugin_index): """Get the real index of the selected item.""" other_plugins_count = sum([other_tabs[0].count() \ for other_tabs in \ self.plugins_tabs[:plugin_index]]) real_index = stack_index - other_plugins_count return real_index # --- Helper methods: Widget def get_widget(self, index=None, path=None, tabs=None): """Get widget by index. If no tabs and index specified the current active widget is returned. """ if index and tabs: return tabs.widget(index) elif path and tabs: return tabs.widget(index) elif self.plugin: index = self.plugins_instances.index(self.plugin) return self.plugins_tabs[index][0].currentWidget() else: return self.plugins_tabs[0][0].currentWidget() def set_editor_cursor(self, editor, cursor): """Set the cursor of an editor.""" pos = cursor.position() anchor = cursor.anchor() new_cursor = QTextCursor() if pos == anchor: new_cursor.movePosition(pos) else: new_cursor.movePosition(anchor) new_cursor.movePosition(pos, QTextCursor.KeepAnchor) editor.setTextCursor(cursor) def goto_line(self, line_number): """Go to specified line number in current active editor.""" if line_number: line_number = int(line_number) editor = self.get_widget() try: editor.go_to_line(min(line_number, editor.get_line_count())) except AttributeError: pass # --- Helper methods: Outline explorer def get_symbol_list(self): """Get the list of symbols present in the file.""" try: oedata = self.get_widget().get_outlineexplorer_data() except AttributeError: oedata = {} return oedata # --- Handlers def item_selection_changed(self): """List widget item selection change handler.""" row = self.current_row() if self.count() and row >= 0: if '</b></big><br>' in self.list.currentItem().text() and row == 0: self.next_row() if self.mode == self.FILE_MODE: try: stack_index = self.paths.index(self.filtered_path[row]) self.plugin = self.widgets[stack_index][1] plugin_index = self.plugins_instances.index(self.plugin) # Count the real index in the tabWidget of the # current plugin real_index = self.get_stack_index(stack_index, plugin_index) self.sig_goto_file.emit(real_index, self.plugin.get_current_tab_manager()) self.goto_line(self.line_number) try: self.plugin.switch_to_plugin() self.raise_() except AttributeError: # The widget using the fileswitcher is not a plugin pass self.edit.setFocus() except ValueError: pass else: line_number = self.filtered_symbol_lines[row] self.goto_line(line_number) def setup_file_list(self, filter_text, current_path): """Setup list widget content for file list display.""" short_paths = shorten_paths(self.paths, self.save_status) paths = self.paths icons = self.icons results = [] trying_for_line_number = ':' in filter_text # Get optional line number if trying_for_line_number: filter_text, line_number = filter_text.split(':') # Get all the available filenames scores = get_search_scores('', self.filenames, template="<b>{0}</b>") else: line_number = None # Get all available filenames and get the scores for # "fuzzy" matching scores = get_search_scores(filter_text, self.filenames, template="<b>{0}</b>") # Get max width to determine if shortpaths should be used max_width = self.get_item_size(paths)[0] self.fix_size(paths) # Build the text that will appear on the list widget for index, score in enumerate(scores): text, rich_text, score_value = score if score_value != -1: text_item = '<big>' + rich_text.replace('&', '') + '</big>' if trying_for_line_number: text_item += " [{0:} {1:}]".format(self.line_count[index], _("lines")) if max_width > self.list.width(): text_item += u"<br><i>{0:}</i>".format(short_paths[index]) else: text_item += u"<br><i>{0:}</i>".format(paths[index]) if (trying_for_line_number and self.line_count[index] != 0 or not trying_for_line_number): results.append((score_value, index, text_item)) # Sort the obtained scores and populate the list widget self.filtered_path = [] plugin = None for result in sorted(results): index = result[1] path = paths[index] icon = icons[index] text = '' try: title = self.widgets[index][1].get_plugin_title().split(' - ') if plugin != title[0]: plugin = title[0] text += '<br><big><b>' + plugin + '</b></big><br>' item = QListWidgetItem(text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) item.setFlags(Qt.ItemIsEditable) self.list.addItem(item) self.filtered_path.append(path) except: # The widget using the fileswitcher is not a plugin pass text = '' text += result[-1] item = QListWidgetItem(icon, text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) self.list.addItem(item) self.filtered_path.append(path) # To adjust the delegate layout for KDE themes self.list.files_list = True # Move selected item in list accordingly and update list size if current_path in self.filtered_path: self.set_current_row(self.filtered_path.index(current_path)) elif self.filtered_path: self.set_current_row(0) # If a line number is searched look for it self.line_number = line_number self.goto_line(line_number) def setup_symbol_list(self, filter_text, current_path): """Setup list widget content for symbol list display.""" # Get optional symbol name filter_text, symbol_text = filter_text.split('@') # Fetch the Outline explorer data, get the icons and values oedata = self.get_symbol_list() icons = get_python_symbol_icons(oedata) # The list of paths here is needed in order to have the same # point of measurement for the list widget size as in the file list # See issue 4648 paths = self.paths # Update list size self.fix_size(paths) symbol_list = process_python_symbol_data(oedata) line_fold_token = [(item[0], item[2], item[3]) for item in symbol_list] choices = [item[1] for item in symbol_list] scores = get_search_scores(symbol_text, choices, template="<b>{0}</b>") # Build the text that will appear on the list widget results = [] lines = [] self.filtered_symbol_lines = [] for index, score in enumerate(scores): text, rich_text, score_value = score line, fold_level, token = line_fold_token[index] lines.append(text) if score_value != -1: results.append((score_value, line, text, rich_text, fold_level, icons[index], token)) template = '{0}{1}' for (score, line, text, rich_text, fold_level, icon, token) in sorted(results): fold_space = ' '*(fold_level) line_number = line + 1 self.filtered_symbol_lines.append(line_number) textline = template.format(fold_space, rich_text) item = QListWidgetItem(icon, textline) item.setSizeHint(QSize(0, 16)) self.list.addItem(item) # To adjust the delegate layout for KDE themes self.list.files_list = False # Move selected item in list accordingly # NOTE: Doing this is causing two problems: # 1. It makes the cursor to auto-jump to the last selected # symbol after opening or closing a different file # 2. It moves the cursor to the first symbol by default, # which is very distracting. # That's why this line is commented! # self.set_current_row(0) def setup(self): """Setup list widget content.""" if len(self.plugins_tabs) == 0: self.close() return self.list.clear() current_path = self.current_path filter_text = self.filter_text # Get optional line or symbol to define mode and method handler trying_for_symbol = ('@' in self.filter_text) if trying_for_symbol: self.mode = self.SYMBOL_MODE self.setup_symbol_list(filter_text, current_path) else: self.mode = self.FILE_MODE self.setup_file_list(filter_text, current_path) # Set position according to size self.set_dialog_position() def add_plugin(self, plugin, tabs, data, icon): """Add a plugin to display its files.""" self.plugins_tabs.append((tabs, plugin)) self.plugins_data.append((data, icon)) self.plugins_instances.append(plugin)
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()
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 FileSwitcher(QDialog): """A Sublime-like file switcher.""" sig_goto_file = Signal(int) sig_close_file = Signal(int) # Constants that define the mode in which the list widget is working # FILE_MODE is for a list of files, SYMBOL_MODE if for a list of symbols # in a given file when using the '@' symbol. FILE_MODE, SYMBOL_MODE = [1, 2] def __init__(self, parent, tabs, data): QDialog.__init__(self, parent) # Variables self.tabs = tabs # Editor stack tabs self.data = data # Editor data self.mode = self.FILE_MODE # By default start in this mode self.initial_cursors = None # {fullpath: QCursor} self.initial_path = None # Fullpath of initial active editor self.initial_editor = None # Initial active editor self.line_number = None # Selected line number in filer self.is_visible = False # Is the switcher visible? help_text = _("Press <b>Enter</b> to switch files or <b>Esc</b> to " "cancel.<br><br>Type to filter filenames.<br><br>" "Use <b>:number</b> to go to a line, e.g. " "<b><code>main:42</code></b><br>" "Use <b>@symbol_text</b> to go to a symbol, e.g. " "<b><code>@init</code></b>" "<br><br> Press <b>Ctrl+W</b> to close current tab.<br>") # Either allow searching for a line number or a symbol but not both regex = QRegExp("([A-Za-z0-9_]{0,100}@[A-Za-z0-9_]{0,100})|" + "([A-Za-z0-9_]{0,100}:{0,1}[0-9]{0,100})") # Widgets self.edit = FilesFilterLine(self) self.help = HelperToolButton() self.list = QListWidget(self) self.filter = KeyPressFilter() regex_validator = QRegExpValidator(regex, self.edit) # Widgets setup self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) self.setWindowOpacity(0.95) self.edit.installEventFilter(self.filter) self.edit.setValidator(regex_validator) self.help.setToolTip(help_text) self.list.setItemDelegate(HTMLDelegate(self)) # Layout edit_layout = QHBoxLayout() edit_layout.addWidget(self.edit) edit_layout.addWidget(self.help) layout = QVBoxLayout() layout.addLayout(edit_layout) layout.addWidget(self.list) self.setLayout(layout) # Signals self.rejected.connect(self.restore_initial_state) self.filter.sig_up_key_pressed.connect(self.previous_row) self.filter.sig_down_key_pressed.connect(self.next_row) self.edit.returnPressed.connect(self.accept) self.edit.textChanged.connect(self.setup) self.list.itemSelectionChanged.connect(self.item_selection_changed) self.list.clicked.connect(self.edit.setFocus) # Setup self.save_initial_state() self.set_dialog_position() self.setup() # --- Properties @property def editors(self): return [self.tabs.widget(index) for index in range(self.tabs.count())] @property def line_count(self): return [editor.get_line_count() for editor in self.editors] @property def save_status(self): return [getattr(td, 'newly_created', False) for td in self.data] @property def paths(self): return [getattr(td, 'filename', None) for td in self.data] @property def filenames(self): return [os.path.basename(getattr(td, 'filename', None)) for td in self.data] @property def current_path(self): return self.paths_by_editor[self.get_editor()] @property def paths_by_editor(self): return dict(zip(self.editors, self.paths)) @property def editors_by_path(self): return dict(zip(self.paths, self.editors)) @property def filter_text(self): """Get the normalized (lowecase) content of the filter text.""" return to_text_string(self.edit.text()).lower() def set_search_text(self, _str): self.edit.setText(_str) def save_initial_state(self): """Saves initial cursors and initial active editor.""" paths = self.paths self.initial_editor = self.get_editor() self.initial_cursors = {} for i, editor in enumerate(self.editors): if editor is self.initial_editor: self.initial_path = paths[i] # This try is needed to make the fileswitcher work with # plugins that does not have a textCursor. try: self.initial_cursors[paths[i]] = editor.textCursor() except AttributeError: pass def accept(self): self.is_visible = False QDialog.accept(self) self.list.clear() def restore_initial_state(self): """Restores initial cursors and initial active editor.""" self.list.clear() self.is_visible = False editors = self.editors_by_path if not self.edit.clicked_outside: for path in self.initial_cursors: cursor = self.initial_cursors[path] if path in editors: self.set_editor_cursor(editors[path], cursor) if self.initial_editor in self.paths_by_editor: index = self.paths.index(self.initial_path) self.sig_goto_file.emit(index) def set_dialog_position(self): """Positions the file switcher dialog in the center of the editor.""" parent = self.parent() geo = parent.geometry() width = self.list.width() # This has been set in setup left = parent.geometry().width()/2 - width/2 top = 0 while parent: geo = parent.geometry() top += geo.top() left += geo.left() parent = parent.parent() # Note: the +1 pixel on the top makes it look better self.move(left, top + self.tabs.tabBar().geometry().height() + 1) def fix_size(self, content, extra=50): """ Adjusts the width and height of the file switcher, based on its content. """ # Update size of dialog based on longest shortened path strings = [] if content: for rich_text in content: label = QLabel(rich_text) label.setTextFormat(Qt.PlainText) strings.append(label.text()) fm = label.fontMetrics() # Max width max_width = max([fm.width(s) * 1.3 for s in strings]) self.list.setMinimumWidth(max_width + extra) # Max height if len(strings) < 8: max_entries = len(strings) else: max_entries = 8 max_height = fm.height() * max_entries * 2.5 self.list.setMinimumHeight(max_height) # Set position according to size self.set_dialog_position() # --- Helper methods: List widget def count(self): """Gets the item count in the list widget.""" return self.list.count() def current_row(self): """Returns the current selected row in the list widget.""" return self.list.currentRow() def set_current_row(self, row): """Sets the current selected row in the list widget.""" return self.list.setCurrentRow(row) def select_row(self, steps): """Select row in list widget based on a number of steps with direction. Steps can be positive (next rows) or negative (previous rows). """ row = self.current_row() + steps if 0 <= row < self.count(): self.set_current_row(row) def previous_row(self): """Select previous row in list widget.""" self.select_row(-1) def next_row(self): """Select next row in list widget.""" self.select_row(+1) # --- Helper methods: Editor def get_editor(self, index=None, path=None): """Get editor by index or path. If no path or index specified the current active editor is returned """ if index: return self.tabs.widget(index) elif path: return self.tabs.widget(index) else: return self.tabs.currentWidget() def set_editor_cursor(self, editor, cursor): """Set the cursor of an editor.""" pos = cursor.position() anchor = cursor.anchor() new_cursor = QTextCursor() if pos == anchor: new_cursor.movePosition(pos) else: new_cursor.movePosition(anchor) new_cursor.movePosition(pos, QTextCursor.KeepAnchor) editor.setTextCursor(cursor) def goto_line(self, line_number): """Go to specified line number in current active editor.""" if line_number: line_number = int(line_number) editor = self.get_editor() editor.go_to_line(min(line_number, editor.get_line_count())) # --- Helper methods: Outline explorer def get_symbol_list(self): """Get the list of symbols present in the file.""" try: oedata = self.get_editor().get_outlineexplorer_data() except AttributeError: oedata = {} return oedata # --- Handlers def item_selection_changed(self): """List widget item selection change handler.""" row = self.current_row() if self.count() and row >= 0: if self.mode == self.FILE_MODE: try: stack_index = self.paths.index(self.filtered_path[row]) self.sig_goto_file.emit(stack_index) self.goto_line(self.line_number) self.edit.setFocus() except ValueError: pass else: line_number = self.filtered_symbol_lines[row] self.goto_line(line_number) def setup_file_list(self, filter_text, current_path): """Setup list widget content for file list display.""" short_paths = shorten_paths(self.paths, self.save_status) paths = self.paths results = [] trying_for_line_number = ':' in filter_text # Get optional line number if trying_for_line_number: filter_text, line_number = filter_text.split(':') else: line_number = None # Get all available filenames and get the scores for "fuzzy" matching scores = get_search_scores(filter_text, self.filenames, template="<b>{0}</b>") # Build the text that will appear on the list widget for index, score in enumerate(scores): text, rich_text, score_value = score if score_value != -1: text_item = '<big>' + rich_text.replace('&', '') + '</big>' if trying_for_line_number: text_item += " [{0:} {1:}]".format(self.line_count[index], _("lines")) text_item += u"<br><i>{0:}</i>".format(short_paths[index]) results.append((score_value, index, text_item)) # Sort the obtained scores and populate the list widget self.filtered_path = [] for result in sorted(results): index = result[1] text = result[-1] path = paths[index] item = QListWidgetItem(ima.icon('FileIcon'), text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) self.list.addItem(item) self.filtered_path.append(path) # To adjust the delegate layout for KDE themes self.list.files_list = True # Move selected item in list accordingly and update list size if current_path in self.filtered_path: self.set_current_row(self.filtered_path.index(current_path)) elif self.filtered_path: self.set_current_row(0) self.fix_size(short_paths, extra=200) # If a line number is searched look for it self.line_number = line_number self.goto_line(line_number) def setup_symbol_list(self, filter_text, current_path): """Setup list widget content for symbol list display.""" # Get optional symbol name filter_text, symbol_text = filter_text.split('@') # Fetch the Outline explorer data, get the icons and values oedata = self.get_symbol_list() icons = get_python_symbol_icons(oedata) symbol_list = process_python_symbol_data(oedata) line_fold_token = [(item[0], item[2], item[3]) for item in symbol_list] choices = [item[1] for item in symbol_list] scores = get_search_scores(symbol_text, choices, template="<b>{0}</b>") # Build the text that will appear on the list widget results = [] lines = [] self.filtered_symbol_lines = [] for index, score in enumerate(scores): text, rich_text, score_value = score line, fold_level, token = line_fold_token[index] lines.append(text) if score_value != -1: results.append((score_value, line, text, rich_text, fold_level, icons[index], token)) template = '{0}{1}' for (score, line, text, rich_text, fold_level, icon, token) in sorted(results): fold_space = ' '*(fold_level) line_number = line + 1 self.filtered_symbol_lines.append(line_number) textline = template.format(fold_space, rich_text) item = QListWidgetItem(icon, textline) item.setSizeHint(QSize(0, 16)) self.list.addItem(item) # To adjust the delegate layout for KDE themes self.list.files_list = False # Move selected item in list accordingly # NOTE: Doing this is causing two problems: # 1. It makes the cursor to auto-jump to the last selected # symbol after opening or closing a different file # 2. It moves the cursor to the first symbol by default, # which is very distracting. # That's why this line is commented! # self.set_current_row(0) # Update list size self.fix_size(lines, extra=125) def setup(self): """Setup list widget content.""" if not self.tabs.count(): self.close() return self.list.clear() current_path = self.current_path filter_text = self.filter_text # Get optional line or symbol to define mode and method handler trying_for_symbol = ('@' in self.filter_text) if trying_for_symbol: self.mode = self.SYMBOL_MODE self.setup_symbol_list(filter_text, current_path) else: self.mode = self.FILE_MODE self.setup_file_list(filter_text, current_path)
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)
class FileSwitcher(QDialog): """A Sublime-like file switcher.""" sig_goto_file = Signal(int, object) # Constants that define the mode in which the list widget is working # FILE_MODE is for a list of files, SYMBOL_MODE if for a list of symbols # in a given file when using the '@' symbol. FILE_MODE, SYMBOL_MODE = [1, 2] MAX_WIDTH = 600 def __init__(self, parent, plugin, tabs, data, icon): QDialog.__init__(self, parent) # Variables self.plugins_tabs = [] self.plugins_data = [] self.plugins_instances = [] self.add_plugin(plugin, tabs, data, icon) self.plugin = None # Last plugin with focus self.mode = self.FILE_MODE # By default start in this mode self.initial_cursors = None # {fullpath: QCursor} self.initial_path = None # Fullpath of initial active editor self.initial_widget = None # Initial active editor self.line_number = None # Selected line number in filer self.is_visible = False # Is the switcher visible? help_text = _("Press <b>Enter</b> to switch files or <b>Esc</b> to " "cancel.<br><br>Type to filter filenames.<br><br>" "Use <b>:number</b> to go to a line, e.g. " "<b><code>main:42</code></b><br>" "Use <b>@symbol_text</b> to go to a symbol, e.g. " "<b><code>@init</code></b>" "<br><br> Press <b>Ctrl+W</b> to close current tab.<br>") # Either allow searching for a line number or a symbol but not both regex = QRegExp("([A-Za-z0-9_]{0,100}@[A-Za-z0-9_]{0,100})|" + "([A-Za-z0-9_]{0,100}:{0,1}[0-9]{0,100})") # Widgets self.edit = FilesFilterLine(self) self.help = HelperToolButton() self.list = QListWidget(self) self.filter = KeyPressFilter() regex_validator = QRegExpValidator(regex, self.edit) # Widgets setup self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) self.setWindowOpacity(0.95) self.edit.installEventFilter(self.filter) self.edit.setValidator(regex_validator) self.help.setToolTip(help_text) self.list.setItemDelegate(HTMLDelegate(self)) # Layout edit_layout = QHBoxLayout() edit_layout.addWidget(self.edit) edit_layout.addWidget(self.help) layout = QVBoxLayout() layout.addLayout(edit_layout) layout.addWidget(self.list) self.setLayout(layout) # Signals self.rejected.connect(self.restore_initial_state) self.filter.sig_up_key_pressed.connect(self.previous_row) self.filter.sig_down_key_pressed.connect(self.next_row) self.edit.returnPressed.connect(self.accept) self.edit.textChanged.connect(self.setup) self.list.itemSelectionChanged.connect(self.item_selection_changed) self.list.clicked.connect(self.edit.setFocus) # --- Properties @property def widgets(self): widgets = [] for plugin in self.plugins_instances: tabs = self.get_plugin_tabwidget(plugin) widgets += [(tabs.widget(index), plugin) for index in range(tabs.count())] return widgets @property def line_count(self): line_count = [] for widget in self.widgets: try: current_line_count = widget[0].get_line_count() except AttributeError: current_line_count = 0 line_count.append(current_line_count) return line_count @property def save_status(self): save_status = [] for da, icon in self.plugins_data: save_status += [getattr(td, 'newly_created', False) for td in da] return save_status @property def paths(self): paths = [] for plugin in self.plugins_instances: da = self.get_plugin_data(plugin) paths += [getattr(td, 'filename', None) for td in da] return paths @property def filenames(self): filenames = [] for plugin in self.plugins_instances: da = self.get_plugin_data(plugin) filenames += [ os.path.basename(getattr(td, 'filename', None)) for td in da ] return filenames @property def icons(self): icons = [] for da, icon in self.plugins_data: icons += [icon for td in da] return icons @property def current_path(self): return self.paths_by_widget[self.get_widget()] @property def paths_by_widget(self): widgets = [w[0] for w in self.widgets] return dict(zip(widgets, self.paths)) @property def widgets_by_path(self): widgets = [w[0] for w in self.widgets] return dict(zip(self.paths, widgets)) @property def filter_text(self): """Get the normalized (lowecase) content of the filter text.""" return to_text_string(self.edit.text()).lower() def set_search_text(self, _str): self.edit.setText(_str) def save_initial_state(self): """Save initial cursors and initial active widget.""" paths = self.paths self.initial_widget = self.get_widget() self.initial_cursors = {} for i, editor in enumerate(self.widgets): if editor is self.initial_widget: self.initial_path = paths[i] # This try is needed to make the fileswitcher work with # plugins that does not have a textCursor. try: self.initial_cursors[paths[i]] = editor.textCursor() except AttributeError: pass def accept(self): self.is_visible = False QDialog.accept(self) self.list.clear() def restore_initial_state(self): """Restores initial cursors and initial active editor.""" self.list.clear() self.is_visible = False widgets = self.widgets_by_path if not self.edit.clicked_outside: for path in self.initial_cursors: cursor = self.initial_cursors[path] if path in widgets: self.set_editor_cursor(widgets[path], cursor) if self.initial_widget in self.paths_by_widget: index = self.paths.index(self.initial_path) self.sig_goto_file.emit(index) def set_dialog_position(self): """Positions the file switcher dialog.""" parent = self.parent() geo = parent.geometry() width = self.list.width() # This has been set in setup left = parent.geometry().width() / 2 - width / 2 # Note: the +1 pixel on the top makes it look better if isinstance(parent, QMainWindow): top = (parent.toolbars_menu.geometry().height() + parent.menuBar().geometry().height() + 1) else: top = self.plugins_tabs[0][0].tabBar().geometry().height() + 1 while parent: geo = parent.geometry() top += geo.top() left += geo.left() parent = parent.parent() self.move(left, top) def get_item_size(self, content): """ Get the max size (width and height) for the elements of a list of strings as a QLabel. """ strings = [] if content: for rich_text in content: label = QLabel(rich_text) label.setTextFormat(Qt.PlainText) strings.append(label.text()) fm = label.fontMetrics() return (max([fm.width(s) * 1.3 for s in strings]), fm.height()) def fix_size(self, content): """ Adjusts the width and height of the file switcher based on the relative size of the parent and content. """ # Update size of dialog based on relative size of the parent if content: width, height = self.get_item_size(content) # Width parent = self.parent() relative_width = parent.geometry().width() * 0.65 if relative_width > self.MAX_WIDTH: relative_width = self.MAX_WIDTH self.list.setMinimumWidth(relative_width) # Height if len(content) < 8: max_entries = len(content) else: max_entries = 8 max_height = height * max_entries * 2.5 self.list.setMinimumHeight(max_height) # Resize self.list.resize(relative_width, self.list.height()) # --- Helper methods: List widget def count(self): """Gets the item count in the list widget.""" return self.list.count() def current_row(self): """Returns the current selected row in the list widget.""" return self.list.currentRow() def set_current_row(self, row): """Sets the current selected row in the list widget.""" return self.list.setCurrentRow(row) def select_row(self, steps): """Select row in list widget based on a number of steps with direction. Steps can be positive (next rows) or negative (previous rows). """ row = self.current_row() + steps if 0 <= row < self.count(): self.set_current_row(row) def previous_row(self): """Select previous row in list widget.""" if self.mode == self.SYMBOL_MODE: self.select_row(-1) return prev_row = self.current_row() - 1 if prev_row >= 0: title = self.list.item(prev_row).text() else: title = '' if prev_row == 0 and '</b></big><br>' in title: self.list.scrollToTop() elif '</b></big><br>' in title: # Select the next previous row, the one following is a title self.select_row(-2) else: self.select_row(-1) def next_row(self): """Select next row in list widget.""" if self.mode == self.SYMBOL_MODE: self.select_row(+1) return next_row = self.current_row() + 1 if next_row < self.count(): if '</b></big><br>' in self.list.item(next_row).text(): # Select the next next row, the one following is a title self.select_row(+2) else: self.select_row(+1) def get_stack_index(self, stack_index, plugin_index): """Get the real index of the selected item.""" other_plugins_count = sum([other_tabs[0].count() \ for other_tabs in \ self.plugins_tabs[:plugin_index]]) real_index = stack_index - other_plugins_count return real_index # --- Helper methods: Widget def get_plugin_data(self, plugin): """Get the data object of the plugin's current tab manager.""" # The data object is named "data" in the editor plugin while it is # named "clients" in the notebook plugin. try: data = plugin.get_current_tab_manager().data except AttributeError: data = plugin.get_current_tab_manager().clients return data def get_plugin_tabwidget(self, plugin): """Get the tabwidget of the plugin's current tab manager.""" # The tab widget is named "tabs" in the editor plugin while it is # named "tabwidget" in the notebook plugin. try: tabwidget = plugin.get_current_tab_manager().tabs except AttributeError: tabwidget = plugin.get_current_tab_manager().tabwidget return tabwidget def get_widget(self, index=None, path=None, tabs=None): """Get widget by index. If no tabs and index specified the current active widget is returned. """ if (index and tabs) or (path and tabs): return tabs.widget(index) elif self.plugin: return self.get_plugin_tabwidget(self.plugin).currentWidget() else: return self.plugins_tabs[0][0].currentWidget() def set_editor_cursor(self, editor, cursor): """Set the cursor of an editor.""" pos = cursor.position() anchor = cursor.anchor() new_cursor = QTextCursor() if pos == anchor: new_cursor.movePosition(pos) else: new_cursor.movePosition(anchor) new_cursor.movePosition(pos, QTextCursor.KeepAnchor) editor.setTextCursor(cursor) def goto_line(self, line_number): """Go to specified line number in current active editor.""" if line_number: line_number = int(line_number) try: self.plugin.go_to_line(line_number) except AttributeError: pass # --- Helper methods: Outline explorer def get_symbol_list(self): """Get the list of symbols present in the file.""" try: oedata = self.get_widget().get_outlineexplorer_data() except AttributeError: oedata = {} return oedata # --- Handlers def item_selection_changed(self): """List widget item selection change handler.""" row = self.current_row() if self.count() and row >= 0: if '</b></big><br>' in self.list.currentItem().text() and row == 0: self.next_row() if self.mode == self.FILE_MODE: try: stack_index = self.paths.index(self.filtered_path[row]) self.plugin = self.widgets[stack_index][1] plugin_index = self.plugins_instances.index(self.plugin) # Count the real index in the tabWidget of the # current plugin real_index = self.get_stack_index(stack_index, plugin_index) self.sig_goto_file.emit( real_index, self.plugin.get_current_tab_manager()) self.goto_line(self.line_number) try: self.plugin.switch_to_plugin() self.raise_() except AttributeError: # The widget using the fileswitcher is not a plugin pass self.edit.setFocus() except ValueError: pass else: line_number = self.filtered_symbol_lines[row] self.goto_line(line_number) def setup_file_list(self, filter_text, current_path): """Setup list widget content for file list display.""" short_paths = shorten_paths(self.paths, self.save_status) paths = self.paths icons = self.icons results = [] trying_for_line_number = ':' in filter_text # Get optional line number if trying_for_line_number: filter_text, line_number = filter_text.split(':') if line_number == '': line_number = None # Get all the available filenames scores = get_search_scores('', self.filenames, template="<b>{0}</b>") else: line_number = None # Get all available filenames and get the scores for # "fuzzy" matching scores = get_search_scores(filter_text, self.filenames, template="<b>{0}</b>") # Get max width to determine if shortpaths should be used max_width = self.get_item_size(paths)[0] self.fix_size(paths) # Build the text that will appear on the list widget for index, score in enumerate(scores): text, rich_text, score_value = score if score_value != -1: text_item = '<big>' + rich_text.replace('&', '') + '</big>' if trying_for_line_number: text_item += " [{0:} {1:}]".format(self.line_count[index], _("lines")) if max_width > self.list.width(): text_item += u"<br><i>{0:}</i>".format(short_paths[index]) else: text_item += u"<br><i>{0:}</i>".format(paths[index]) if (trying_for_line_number and self.line_count[index] != 0 or not trying_for_line_number): results.append((score_value, index, text_item)) # Sort the obtained scores and populate the list widget self.filtered_path = [] plugin = None for result in sorted(results): index = result[1] path = paths[index] icon = icons[index] text = '' try: title = self.widgets[index][1].get_plugin_title().split(' - ') if plugin != title[0]: plugin = title[0] text += '<br><big><b>' + plugin + '</b></big><br>' item = QListWidgetItem(text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) item.setFlags(Qt.ItemIsEditable) self.list.addItem(item) self.filtered_path.append(path) except: # The widget using the fileswitcher is not a plugin pass text = '' text += result[-1] item = QListWidgetItem(icon, text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) self.list.addItem(item) self.filtered_path.append(path) # To adjust the delegate layout for KDE themes self.list.files_list = True # Move selected item in list accordingly and update list size if current_path in self.filtered_path: self.set_current_row(self.filtered_path.index(current_path)) elif self.filtered_path: self.set_current_row(0) # If a line number is searched look for it self.line_number = line_number self.goto_line(line_number) def setup_symbol_list(self, filter_text, current_path): """Setup list widget content for symbol list display.""" # Get optional symbol name filter_text, symbol_text = filter_text.split('@') # Fetch the Outline explorer data, get the icons and values oedata = self.get_symbol_list() icons = get_python_symbol_icons(oedata) # The list of paths here is needed in order to have the same # point of measurement for the list widget size as in the file list # See issue 4648 paths = self.paths # Update list size self.fix_size(paths) symbol_list = process_python_symbol_data(oedata) line_fold_token = [(item[0], item[2], item[3]) for item in symbol_list] choices = [item[1] for item in symbol_list] scores = get_search_scores(symbol_text, choices, template="<b>{0}</b>") # Build the text that will appear on the list widget results = [] lines = [] self.filtered_symbol_lines = [] for index, score in enumerate(scores): text, rich_text, score_value = score line, fold_level, token = line_fold_token[index] lines.append(text) if score_value != -1: results.append((score_value, line, text, rich_text, fold_level, icons[index], token)) template = '{0}{1}' for (score, line, text, rich_text, fold_level, icon, token) in sorted(results): fold_space = ' ' * (fold_level) line_number = line + 1 self.filtered_symbol_lines.append(line_number) textline = template.format(fold_space, rich_text) item = QListWidgetItem(icon, textline) item.setSizeHint(QSize(0, 16)) self.list.addItem(item) # To adjust the delegate layout for KDE themes self.list.files_list = False # Select edit line when using symbol search initially. # See issue 5661 self.edit.setFocus() # Move selected item in list accordingly # NOTE: Doing this is causing two problems: # 1. It makes the cursor to auto-jump to the last selected # symbol after opening or closing a different file # 2. It moves the cursor to the first symbol by default, # which is very distracting. # That's why this line is commented! # self.set_current_row(0) def setup(self): """Setup list widget content.""" if len(self.plugins_tabs) == 0: self.close() return self.list.clear() current_path = self.current_path filter_text = self.filter_text # Get optional line or symbol to define mode and method handler trying_for_symbol = ('@' in self.filter_text) if trying_for_symbol: self.mode = self.SYMBOL_MODE self.setup_symbol_list(filter_text, current_path) else: self.mode = self.FILE_MODE self.setup_file_list(filter_text, current_path) # Set position according to size self.set_dialog_position() def show(self): """ Override Qt method to force an update of the fileswitcher before showing it. See Issue #5317 and PR #5389. """ self.setup() super(FileSwitcher, self).show() def add_plugin(self, plugin, tabs, data, icon): """Add a plugin to display its files.""" self.plugins_tabs.append((tabs, plugin)) self.plugins_data.append((data, icon)) self.plugins_instances.append(plugin)
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()
class FileSwitcher(QDialog): """A Sublime-like file switcher.""" sig_goto_file = Signal(int) sig_close_file = Signal(int) # Constants that define the mode in which the list widget is working # FILE_MODE is for a list of files, SYMBOL_MODE if for a list of symbols # in a given file when using the '@' symbol. FILE_MODE, SYMBOL_MODE = [1, 2] def __init__(self, parent, tabs, data): QDialog.__init__(self, parent) # Variables self.tabs = tabs # Editor stack tabs self.data = data # Editor data self.mode = self.FILE_MODE # By default start in this mode self.initial_cursors = None # {fullpath: QCursor} self.initial_path = None # Fullpath of initial active editor self.initial_editor = None # Initial active editor self.line_number = None # Selected line number in filer self.is_visible = False # Is the switcher visible? help_text = _("Press <b>Enter</b> to switch files or <b>Esc</b> to " "cancel.<br><br>Type to filter filenames.<br><br>" "Use <b>:number</b> to go to a line, e.g. " "<b><code>main:42</code></b><br>" "Use <b>@symbol_text</b> to go to a symbol, e.g. " "<b><code>@init</code></b>" "<br><br> Press <b>Ctrl+W</b> to close current tab.<br>") # Either allow searching for a line number or a symbol but not both regex = QRegExp("([A-Za-z0-9_]{0,100}@[A-Za-z0-9_]{0,100})|" + "([A-Za-z]{0,100}:{0,1}[0-9]{0,100})") # Widgets self.edit = QLineEdit(self) self.help = HelperToolButton() self.list = QListWidget(self) self.filter = KeyPressFilter() regex_validator = QRegExpValidator(regex, self.edit) # Widgets setup self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) self.setWindowOpacity(0.95) self.edit.installEventFilter(self.filter) self.edit.setValidator(regex_validator) self.help.setToolTip(help_text) self.list.setItemDelegate(HTMLDelegate(self)) # Layout edit_layout = QHBoxLayout() edit_layout.addWidget(self.edit) edit_layout.addWidget(self.help) layout = QVBoxLayout() layout.addLayout(edit_layout) layout.addWidget(self.list) self.setLayout(layout) # Signals self.rejected.connect(self.restore_initial_state) self.filter.sig_up_key_pressed.connect(self.previous_row) self.filter.sig_down_key_pressed.connect(self.next_row) self.edit.returnPressed.connect(self.accept) self.edit.textChanged.connect(self.setup) self.list.itemSelectionChanged.connect(self.item_selection_changed) self.list.clicked.connect(self.edit.setFocus) # Setup self.save_initial_state() self.set_dialog_position() self.setup() # --- Properties @property def editors(self): return [self.tabs.widget(index) for index in range(self.tabs.count())] @property def line_count(self): return [editor.get_line_count() for editor in self.editors] @property def save_status(self): return [getattr(td, 'newly_created', False) for td in self.data] @property def paths(self): return [getattr(td, 'filename', None) for td in self.data] @property def filenames(self): return [self.tabs.tabText(index) for index in range(self.tabs.count())] @property def current_path(self): return self.paths_by_editor[self.get_editor()] @property def paths_by_editor(self): return dict(zip(self.editors, self.paths)) @property def editors_by_path(self): return dict(zip(self.paths, self.editors)) @property def filter_text(self): """Get the normalized (lowecase) content of the filter text.""" return to_text_string(self.edit.text()).lower() def save_initial_state(self): """Saves initial cursors and initial active editor.""" paths = self.paths self.initial_editor = self.get_editor() self.initial_cursors = {} for i, editor in enumerate(self.editors): if editor is self.initial_editor: self.initial_path = paths[i] self.initial_cursors[paths[i]] = editor.textCursor() def accept(self): self.is_visible = False QDialog.accept(self) self.list.clear() def restore_initial_state(self): """Restores initial cursors and initial active editor.""" self.list.clear() self.is_visible = False editors = self.editors_by_path for path in self.initial_cursors: cursor = self.initial_cursors[path] if path in editors: self.set_editor_cursor(editors[path], cursor) if self.initial_editor in self.paths_by_editor: index = self.paths.index(self.initial_path) self.sig_goto_file.emit(index) def set_dialog_position(self): """Positions the file switcher dialog in the center of the editor.""" parent = self.parent() geo = parent.geometry() width = self.list.width() # This has been set in setup left = parent.geometry().width()/2 - width/2 top = 0 while parent: geo = parent.geometry() top += geo.top() left += geo.left() parent = parent.parent() # Note: the +1 pixel on the top makes it look better self.move(left, top + self.tabs.tabBar().geometry().height() + 1) def fix_size(self, content, extra=50): """ Adjusts the width and height of the file switcher, based on its content. """ # Update size of dialog based on longest shortened path strings = [] if content: for rich_text in content: label = QLabel(rich_text) label.setTextFormat(Qt.PlainText) strings.append(label.text()) fm = label.fontMetrics() # Max width max_width = max([fm.width(s) * 1.3 for s in strings]) self.list.setMinimumWidth(max_width + extra) # Max height if len(strings) < 8: max_entries = len(strings) else: max_entries = 8 max_height = fm.height() * max_entries * 2.5 self.list.setMinimumHeight(max_height) # Set position according to size self.set_dialog_position() # --- Helper methods: List widget def count(self): """Gets the item count in the list widget.""" return self.list.count() def current_row(self): """Returns the current selected row in the list widget.""" return self.list.currentRow() def set_current_row(self, row): """Sets the current selected row in the list widget.""" return self.list.setCurrentRow(row) def select_row(self, steps): """Select row in list widget based on a number of steps with direction. Steps can be positive (next rows) or negative (previous rows). """ row = self.current_row() + steps if 0 <= row < self.count(): self.set_current_row(row) def previous_row(self): """Select previous row in list widget.""" self.select_row(-1) def next_row(self): """Select next row in list widget.""" self.select_row(+1) # --- Helper methods: Editor def get_editor(self, index=None, path=None): """Get editor by index or path. If no path or index specified the current active editor is returned """ if index: return self.tabs.widget(index) elif path: return self.tabs.widget(index) else: return self.parent().get_current_editor() def set_editor_cursor(self, editor, cursor): """Set the cursor of an editor.""" pos = cursor.position() anchor = cursor.anchor() new_cursor = QTextCursor() if pos == anchor: new_cursor.movePosition(pos) else: new_cursor.movePosition(anchor) new_cursor.movePosition(pos, QTextCursor.KeepAnchor) editor.setTextCursor(cursor) def goto_line(self, line_number): """Go to specified line number in current active editor.""" if line_number: line_number = int(line_number) editor = self.get_editor() editor.go_to_line(min(line_number, editor.get_line_count())) # --- Helper methods: Outline explorer def get_symbol_list(self): """Get the list of symbols present in the file.""" try: oedata = self.get_editor().get_outlineexplorer_data() except AttributeError: oedata = {} return oedata # --- Handlers def item_selection_changed(self): """List widget item selection change handler.""" row = self.current_row() if self.count() and row >= 0: if self.mode == self.FILE_MODE: try: stack_index = self.paths.index(self.filtered_path[row]) self.sig_goto_file.emit(stack_index) self.goto_line(self.line_number) self.edit.setFocus() except ValueError: pass else: line_number = self.filtered_symbol_lines[row] self.goto_line(line_number) def setup_file_list(self, filter_text, current_path): """Setup list widget content for file list display.""" short_paths = shorten_paths(self.paths, self.save_status) paths = self.paths results = [] trying_for_line_number = ':' in filter_text # Get optional line number if trying_for_line_number: filter_text, line_number = filter_text.split(':') else: line_number = None # Get all available filenames and get the scores for "fuzzy" matching scores = get_search_scores(filter_text, self.filenames, template="<b>{0}</b>") # Build the text that will appear on the list widget for index, score in enumerate(scores): text, rich_text, score_value = score if score_value != -1: text_item = '<big>' + rich_text.replace('&', '') + '</big>' if trying_for_line_number: text_item += " [{0:} {1:}]".format(self.line_count[index], _("lines")) text_item += "<br><i>{0:}</i>".format(short_paths[index]) results.append((score_value, index, text_item)) # Sort the obtained scores and populate the list widget self.filtered_path = [] for result in sorted(results): index = result[1] text = result[-1] path = paths[index] item = QListWidgetItem(ima.icon('FileIcon'), text) item.setToolTip(path) item.setSizeHint(QSize(0, 25)) self.list.addItem(item) self.filtered_path.append(path) # To adjust the delegate layout for KDE themes self.list.files_list = True # Move selected item in list accordingly and update list size if current_path in self.filtered_path: self.set_current_row(self.filtered_path.index(current_path)) elif self.filtered_path: self.set_current_row(0) self.fix_size(short_paths, extra=200) # If a line number is searched look for it self.line_number = line_number self.goto_line(line_number) def setup_symbol_list(self, filter_text, current_path): """Setup list widget content for symbol list display.""" # Get optional symbol name filter_text, symbol_text = filter_text.split('@') # Fetch the Outline explorer data, get the icons and values oedata = self.get_symbol_list() icons = get_python_symbol_icons(oedata) symbol_list = process_python_symbol_data(oedata) line_fold_token = [(item[0], item[2], item[3]) for item in symbol_list] choices = [item[1] for item in symbol_list] scores = get_search_scores(symbol_text, choices, template="<b>{0}</b>") # Build the text that will appear on the list widget results = [] lines = [] self.filtered_symbol_lines = [] for index, score in enumerate(scores): text, rich_text, score_value = score line, fold_level, token = line_fold_token[index] lines.append(text) if score_value != -1: results.append((score_value, line, text, rich_text, fold_level, icons[index], token)) template = '{0}{1}' for (score, line, text, rich_text, fold_level, icon, token) in sorted(results): fold_space = ' '*(fold_level) line_number = line + 1 self.filtered_symbol_lines.append(line_number) textline = template.format(fold_space, rich_text) item = QListWidgetItem(icon, textline) item.setSizeHint(QSize(0, 16)) self.list.addItem(item) # To adjust the delegate layout for KDE themes self.list.files_list = False # Move selected item in list accordingly # NOTE: Doing this is causing two problems: # 1. It makes the cursor to auto-jump to the last selected # symbol after opening or closing a different file # 2. It moves the cursor to the first symbol by default, # which is very distracting. # That's why this line is commented! # self.set_current_row(0) # Update list size self.fix_size(lines, extra=125) def setup(self): """Setup list widget content.""" if not self.tabs.count(): self.close() return self.list.clear() current_path = self.current_path filter_text = self.filter_text # Get optional line or symbol to define mode and method handler trying_for_symbol = ('@' in self.filter_text) if trying_for_symbol: self.mode = self.SYMBOL_MODE self.setup_symbol_list(filter_text, current_path) else: self.mode = self.FILE_MODE self.setup_file_list(filter_text, current_path)