예제 #1
0
    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)
예제 #2
0
    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)
예제 #3
0
    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()
예제 #4
0
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 += '&nbsp;' * 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()
예제 #5
0
    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()
예제 #6
0
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
예제 #7
0
    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()
예제 #8
0
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
예제 #9
0
    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)
예제 #10
0
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 = '&nbsp;'*(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)
예제 #11
0
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())
예제 #12
0
    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()
예제 #13
0
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()
예제 #14
0
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())
예제 #15
0
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 = '&nbsp;'*(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)
예제 #16
0
    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)
예제 #17
0
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 = '&nbsp;' * (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)
예제 #18
0
    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()
예제 #19
0
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 = '&nbsp;'*(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)