Exemple #1
0
class OutlineMode(Mode, QtCore.QObject):
    """
    Generic mode that provides outline information through the
    document_changed signal and a specialised worker function.

    To use this mode, you need to write a worker function that returns a list
    of pyqode.core.share.Definition (see
    pyqode.python.backend.workers.defined_names() for an example of how to
    implement the worker function).
    """

    #: Signal emitted when the document structure changed.
    document_changed = QtCore.Signal()

    @property
    def definitions(self):
        """
        Gets the list of top level definitions.
        """
        return self._results

    def __init__(self, worker, delay=1000):
        Mode.__init__(self)
        QtCore.QObject.__init__(self)
        self._worker = worker
        self._jobRunner = DelayJobRunner(delay=delay)
        #: The list of definitions found in the file, each item is a
        #: pyqode.core.share.Definition.
        self._results = []

    def on_state_changed(self, state):
        if state:
            self.editor.new_text_set.connect(self._run_analysis)
            self.editor.textChanged.connect(self._request_analysis)
        else:
            self.editor.textChanged.disconnect(self._request_analysis)
            self.editor.new_text_set.disconnect(self._run_analysis)
            self._jobRunner.cancel_requests()

    def _request_analysis(self):
        self._jobRunner.request_job(self._run_analysis)

    def _run_analysis(self):
        try:
            self.editor.file
            self.editor.toPlainText()
        except (RuntimeError, AttributeError):
            # called by the timer after the editor got deleted
            return
        if self.enabled:
            request_data = {
                'code': self.editor.toPlainText(),
                'path': self.editor.file.path,
                'encoding': self.editor.file.encoding
            }
            try:
                self.editor.backend.send_request(
                    self._worker,
                    request_data,
                    on_receive=self._on_results_available)
            except NotRunning:
                QtCore.QTimer.singleShot(100, self._run_analysis)
        else:
            self._results = []
            self.document_changed.emit()

    def _on_results_available(self, results):
        if results:
            results = [Definition.from_dict(ddict) for ddict in results]
        self._results = results
        if self._results is not None:
            _logger().log(5, "Document structure changed")
            self.document_changed.emit()
Exemple #2
0
class OccurrencesHighlighterMode(Mode):
    """ Highlights occurrences of the word under the text text cursor.

    The ``delay`` before searching for occurrences is configurable.
    """
    @property
    def delay(self):
        """
        Delay before searching for occurrences. The timer is rearmed as soon
        as the cursor position changed.
        """
        return self.timer.delay

    @delay.setter
    def delay(self, value):
        self.timer.delay = value
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).delay = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def background(self):
        """
        Background or underline color (if underlined is True).
        """
        return self._background

    @background.setter
    def background(self, value):
        self._background = value
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).background = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def foreground(self):
        """
        Foreground color of occurences, not used if underlined is True.
        """
        return self._foreground

    @foreground.setter
    def foreground(self, value):
        self._foreground = value
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).foreground = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def underlined(self):
        """
        True to use to underlined occurrences instead of
        changing the background. Default is True.

        If this mode is ON, the foreground color is ignored, the
        background color is then used as the underline color.

        """
        return self._underlined

    @underlined.setter
    def underlined(self, value):
        self._underlined = value
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).underlined = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def case_sensitive(self):
        return self._case_sensitive

    @case_sensitive.setter
    def case_sensitive(self, value):
        self._case_sensitive = value
        self._request_highlight()

    def __init__(self):
        super(OccurrencesHighlighterMode, self).__init__()
        self._decorations = []
        #: Timer used to run the search request with a specific delay
        self.timer = DelayJobRunner(delay=1000)
        self._sub = None
        self._background = QtGui.QColor('#CCFFCC')
        self._foreground = None
        self._underlined = False
        self._case_sensitive = False

    def on_state_changed(self, state):
        if state:
            self.editor.cursorPositionChanged.connect(self._request_highlight)
        else:
            self.editor.cursorPositionChanged.disconnect(
                self._request_highlight)
            self.timer.cancel_requests()

    def _clear_decos(self):
        for d in self._decorations:
            self.editor.decorations.remove(d)
        self._decorations[:] = []

    def _request_highlight(self):
        if self.editor is not None:
            sub = TextHelper(self.editor).word_under_cursor(
                select_whole_word=True).selectedText()
            if sub != self._sub:
                self._clear_decos()
                if len(sub) > 1:
                    self.timer.request_job(self._send_request)

    def _send_request(self):
        if self.editor is None:
            return
        cursor = self.editor.textCursor()
        self._sub = TextHelper(self.editor).word_under_cursor(
            select_whole_word=True).selectedText()
        if not cursor.hasSelection() or cursor.selectedText() == self._sub:
            request_data = {
                'string': self.editor.toPlainText(),
                'sub': self._sub,
                'regex': False,
                'whole_word': True,
                'case_sensitive': self.case_sensitive
            }
            try:
                self.editor.backend.send_request(findall, request_data,
                                                 self._on_results_available)
            except NotRunning:
                self._request_highlight()

    def _on_results_available(self, results):
        if len(results) > 500:
            # limit number of results (on very big file where a lots of
            # occurrences can be found, this would totally freeze the editor
            # during a few seconds, with a limit of 500 we can make sure
            # the editor will always remain responsive).
            results = results[:500]
        current = self.editor.textCursor().position()
        if len(results) > 1:
            for start, end in results:
                if start <= current <= end:
                    continue
                deco = TextDecoration(self.editor.textCursor(),
                                      start_pos=start,
                                      end_pos=end)
                if self.underlined:
                    deco.set_as_underlined(self._background)
                else:
                    deco.set_background(QtGui.QBrush(self._background))
                    if self._foreground is not None:
                        deco.set_foreground(self._foreground)
                deco.draw_order = 3
                self.editor.decorations.append(deco)
                self._decorations.append(deco)

    def clone_settings(self, original):
        self.delay = original.delay
        self.background = original.background
        self.foreground = original.foreground
        self.underlined = original.underlined
class DocumentAnalyserMode(Mode, QtCore.QObject):
    """ Analyses the document outline as a tree of statements.

    This mode analyses the structure of a document (a tree of
    :class:`pyqode.python.backend.workers.Definition`.

    :attr:`pyqode.python.modes.DocumentAnalyserMode.document_changed`
    is emitted whenever the document structure changed.

    To keep good performances, the analysis task is run when the application is
    idle for more than 1 second (by default).
    """
    #: Signal emitted when the document structure changed.
    document_changed = QtCore.Signal()

    def __init__(self, delay=1000):
        Mode.__init__(self)
        QtCore.QObject.__init__(self)
        self._jobRunner = DelayJobRunner(delay=delay)
        #: The list of results (elements might have children; this is actually
        #: a tree).
        self.results = []

    def on_state_changed(self, state):
        if state:
            self.editor.new_text_set.connect(self._run_analysis)
            self.editor.textChanged.connect(self._request_analysis)
        else:
            self.editor.textChanged.disconnect(self._request_analysis)
            self.editor.new_text_set.disconnect(self._run_analysis)
            self._jobRunner.cancel_requests()

    def _request_analysis(self):
        self._jobRunner.request_job(self._run_analysis)

    def _run_analysis(self):
        if self.enabled and self.editor and self.editor.toPlainText() and \
                self.editor.file:
            request_data = {
                'code': self.editor.toPlainText(),
                'path': self.editor.file.path,
                'encoding': self.editor.file.encoding
            }
            try:
                self.editor.backend.send_request(
                    defined_names, request_data,
                    on_receive=self._on_results_available)
            except NotRunning:
                QtCore.QTimer.singleShot(100, self._run_analysis)
        else:
            self.results = []
            self.document_changed.emit()

    def _on_results_available(self, results):
        if results:
            results = [Definition().from_dict(ddict) for ddict in results]
        self.results = results
        if self.results is not None:
            _logger().debug("Document structure changed")
            self.document_changed.emit()

    @property
    def flattened_results(self):
        """
        Flattens the document structure tree as a simple sequential list.
        """
        ret_val = []
        for d in self.results:
            ret_val.append(d)
            for sub_d in d.children:
                nd = Definition(sub_d.name, sub_d.icon, sub_d.line,
                                sub_d.column, sub_d.full_name)
                nd.name = "  " + nd.name
                nd.full_name = "  " + nd.full_name
                ret_val.append(nd)
        return ret_val

    def to_tree_widget_items(self, to_collapse=None):
        """
        Returns the results as a list of top level QTreeWidgetItem.

        This is a convenience function that you can use to update a document
        tree widget wheneve the document changed.
        """
        def convert(name, editor, to_collapse):
            ti = QtWidgets.QTreeWidgetItem()
            ti.setText(0, name.name)
            ti.setIcon(0, QtGui.QIcon(name.icon))
            name.block = editor.document().findBlockByNumber(name.line)
            ti.setData(0, QtCore.Qt.UserRole, name)
            block_data = name.block.userData()
            if block_data is None:
                block_data = TextBlockUserData()
                name.block.setUserData(block_data)
            block_data.tree_item = ti

            if to_collapse is not None and \
                    TextBlockHelper.get_fold_trigger_state(name.block):
                to_collapse.append(ti)

            for ch in name.children:
                ti_ch, to_collapse = convert(ch, editor, to_collapse)
                if ti_ch:
                    ti.addChild(ti_ch)
            return ti, to_collapse

        items = []
        for d in self.results:
            value, to_collapse = convert(d, self.editor, to_collapse)
            items.append(value)
        if to_collapse is not None:
            return items, to_collapse
        return items
class OutlineMode(Mode, QtCore.QObject):
    """
    Generic mode that provides outline information through the
    document_changed signal and a specialised worker function.

    To use this mode, you need to write a worker function that returns a list
    of pyqode.core.share.Definition (see
    pyqode.python.backend.workers.defined_names() for an example of how to
    implement the worker function).
    """

    #: Signal emitted when the document structure changed.
    document_changed = QtCore.Signal()

    @property
    def definitions(self):
        """
        Gets the list of top level definitions.
        """
        return self._results

    def __init__(self, worker, delay=1000):
        Mode.__init__(self)
        QtCore.QObject.__init__(self)
        self._worker = worker
        self._jobRunner = DelayJobRunner(delay=delay)
        #: The list of definitions found in the file, each item is a
        #: pyqode.core.share.Definition.
        self._results = []

    def on_state_changed(self, state):
        if state:
            self.editor.new_text_set.connect(self._run_analysis)
            self.editor.textChanged.connect(self._request_analysis)
        else:
            self.editor.textChanged.disconnect(self._request_analysis)
            self.editor.new_text_set.disconnect(self._run_analysis)
            self._jobRunner.cancel_requests()

    def _request_analysis(self):
        self._jobRunner.request_job(self._run_analysis)

    def _run_analysis(self):
        try:
            self.editor.file
            self.editor.toPlainText()
        except (RuntimeError, AttributeError):
            # called by the timer after the editor got deleted
            return
        if self.enabled:
            request_data = {
                'code': self.editor.toPlainText(),
                'path': self.editor.file.path,
                'encoding': self.editor.file.encoding
            }
            try:
                self.editor.backend.send_request(
                    self._worker, request_data,
                    on_receive=self._on_results_available)
            except NotRunning:
                QtCore.QTimer.singleShot(100, self._run_analysis)
        else:
            self._results = []
            self.document_changed.emit()

    def _on_results_available(self, results):
        if results:
            results = [Definition.from_dict(ddict) for ddict in results]
        self._results = results
        if self._results is not None:
            _logger().debug("Document structure changed")
            self.document_changed.emit()
Exemple #5
0
class WordClickMode(Mode, QtCore.QObject):
    """ Adds support for word click events.

    It will highlight the click-able word when the user press control and move
    the mouse over a word.

    Detecting whether a word is click-able is the responsability of the
    subclasses. You must override ``_check_word_cursor`` and call
    ``_select_word_cursor`` if this is a click-able word (this
    process might be asynchrone) otherwise _clear_selection.

    :attr:`pyqode.core.modes.WordClickMode.word_clicked` is emitted
    when the word is clicked by the user (while keeping control pressed).
    """
    #: Signal emitted when a word is clicked. The parameter is a
    #: QTextCursor with the clicked word set as the selected text.
    word_clicked = QtCore.Signal(QtGui.QTextCursor)

    def __init__(self):
        QtCore.QObject.__init__(self)
        Mode.__init__(self)
        self._previous_cursor_start = -1
        self._previous_cursor_end = -1
        self._deco = None
        self._cursor = None
        self._timer = DelayJobRunner(delay=200)

    def on_state_changed(self, state):
        if state:
            self.editor.mouse_moved.connect(self._on_mouse_moved)
            self.editor.mouse_pressed.connect(self._on_mouse_pressed)
            self.editor.key_released.connect(self._on_key_released)
            self.editor.mouse_double_clicked.connect(
                self._on_mouse_double_clicked)
        else:
            self.editor.mouse_moved.disconnect(self._on_mouse_moved)
            self.editor.mouse_pressed.disconnect(self._on_mouse_pressed)
            self.editor.key_released.disconnect(self._on_key_released)
            self.editor.mouse_double_clicked.disconnect(
                self._on_mouse_double_clicked)

    def _on_mouse_double_clicked(self):
        self._timer.cancel_requests()

    def _on_key_released(self, event):
        if event.key() == QtCore.Qt.Key_Control:
            self._clear_selection()
            self._cursor = None

    def _select_word_cursor(self):
        """ Selects the word under the mouse cursor. """
        cursor = TextHelper(self.editor).word_under_mouse_cursor()
        if (self._previous_cursor_start != cursor.selectionStart()
                and self._previous_cursor_end != cursor.selectionEnd()):
            self._remove_decoration()
            self._add_decoration(cursor)
        self._previous_cursor_start = cursor.selectionStart()
        self._previous_cursor_end = cursor.selectionEnd()

    def _clear_selection(self):
        try:
            self._remove_decoration()
        except ValueError:
            pass
        self.editor.set_mouse_cursor(QtCore.Qt.IBeamCursor)
        self._previous_cursor_start = -1
        self._previous_cursor_end = -1

    def _on_mouse_moved(self, event):
        """ mouse moved callback """
        if event.modifiers() & QtCore.Qt.ControlModifier:
            cursor = TextHelper(self.editor).word_under_mouse_cursor()
            if (not self._cursor
                    or cursor.position() != self._cursor.position()):
                self._check_word_cursor(cursor)
            self._cursor = cursor
        else:
            self._cursor = None
            self._clear_selection()

    def _check_word_cursor(self, cursor):
        pass

    def _on_mouse_pressed(self, event):
        """ mouse pressed callback """
        if event.button() == 1 and self._deco:
            cursor = TextHelper(self.editor).word_under_mouse_cursor()
            if cursor and cursor.selectedText():
                self._timer.request_job(self.word_clicked.emit, cursor)

    def _add_decoration(self, cursor):
        """
        Adds a decoration for the word under ``cursor``.
        """
        if self._deco is None:
            if cursor.selectedText():
                self._deco = TextDecoration(cursor)
                if self.editor.background.lightness() < 128:
                    self._deco.set_foreground(QtGui.QColor('#0681e0'))
                else:
                    self._deco.set_foreground(QtCore.Qt.blue)
                self._deco.set_as_underlined()
                self.editor.decorations.append(self._deco)
                self.editor.set_mouse_cursor(QtCore.Qt.PointingHandCursor)
            else:
                self.editor.set_mouse_cursor(QtCore.Qt.IBeamCursor)

    def _remove_decoration(self):
        """
        Removes the word under cursor's decoration
        """
        if self._deco is not None:
            self.editor.decorations.remove(self._deco)
            self._deco = None
class WordClickMode(Mode, QtCore.QObject):
    """ Adds support for word click events.

    It will highlight the click-able word when the user press control and move
    the mouse over a word.

    Detecting whether a word is click-able is the responsability of the
    subclasses. You must override ``_check_word_cursor`` and call
    ``_select_word_cursor`` if this is a click-able word (this
    process might be asynchrone) otherwise _clear_selection.

    :attr:`pyqode.core.modes.WordClickMode.word_clicked` is emitted
    when the word is clicked by the user (while keeping control pressed).
    """
    #: Signal emitted when a word is clicked. The parameter is a
    #: QTextCursor with the clicked word set as the selected text.
    word_clicked = QtCore.Signal(QtGui.QTextCursor)

    def __init__(self):
        QtCore.QObject.__init__(self)
        Mode.__init__(self)
        self._previous_cursor_start = -1
        self._previous_cursor_end = -1
        self._deco = None
        self._cursor = None
        self._timer = DelayJobRunner(delay=200)

    def on_state_changed(self, state):
        if state:
            self.editor.mouse_moved.connect(self._on_mouse_moved)
            self.editor.mouse_released.connect(self._on_mouse_released)
            self.editor.key_released.connect(self._on_key_released)
            self.editor.mouse_double_clicked.connect(
                self._on_mouse_double_clicked)
        else:
            self.editor.mouse_moved.disconnect(self._on_mouse_moved)
            self.editor.mouse_released.disconnect(self._on_mouse_released)
            self.editor.key_released.disconnect(self._on_key_released)
            self.editor.mouse_double_clicked.disconnect(
                self._on_mouse_double_clicked)

    def _on_mouse_double_clicked(self):
        self._timer.cancel_requests()

    def _on_key_released(self, event):
        if event.key() == QtCore.Qt.Key_Control:
            self._clear_selection()
            self._cursor = None

    def _select_word_cursor(self):
        """ Selects the word under the mouse cursor. """
        cursor = TextHelper(self.editor).word_under_mouse_cursor()
        if (self._previous_cursor_start != cursor.selectionStart() and
                self._previous_cursor_end != cursor.selectionEnd()):
            self._remove_decoration()
            self._add_decoration(cursor)
        self._previous_cursor_start = cursor.selectionStart()
        self._previous_cursor_end = cursor.selectionEnd()

    def _clear_selection(self):
        try:
            self._remove_decoration()
        except ValueError:
            pass
        self.editor.set_mouse_cursor(QtCore.Qt.IBeamCursor)
        self._previous_cursor_start = -1
        self._previous_cursor_end = -1

    def _on_mouse_moved(self, event):
        """ mouse moved callback """
        if event.modifiers() & QtCore.Qt.ControlModifier:
            cursor = TextHelper(self.editor).word_under_mouse_cursor()
            if (not self._cursor or
                    cursor.position() != self._cursor.position()):
                self._check_word_cursor(cursor)
            self._cursor = cursor
        else:
            self._cursor = None
            self._clear_selection()

    def _check_word_cursor(self, cursor):
        pass

    def _on_mouse_released(self, event):
        """ mouse pressed callback """
        if event.button() == 1 and self._deco:
            cursor = TextHelper(self.editor).word_under_mouse_cursor()
            if cursor and cursor.selectedText():
                self._timer.request_job(
                    self.word_clicked.emit, cursor)

    def _add_decoration(self, cursor):
        """
        Adds a decoration for the word under ``cursor``.
        """
        if self._deco is None:
            if cursor.selectedText():
                self._deco = TextDecoration(cursor)
                if self.editor.background.lightness() < 128:
                    self._deco.set_foreground(QtGui.QColor('#0681e0'))
                else:
                    self._deco.set_foreground(QtCore.Qt.blue)
                self._deco.set_as_underlined()
                self.editor.decorations.append(self._deco)
                self.editor.set_mouse_cursor(QtCore.Qt.PointingHandCursor)
            else:
                self.editor.set_mouse_cursor(QtCore.Qt.IBeamCursor)

    def _remove_decoration(self):
        """
        Removes the word under cursor's decoration
        """
        if self._deco is not None:
            self.editor.decorations.remove(self._deco)
            self._deco = None
class FoldingPanel(Panel):

    """ Displays the document outline and lets the user collapse/expand blocks.

    The data represented by the panel come from the text block user state and
    is set by the SyntaxHighlighter mode.

    The panel does not expose any function that you can use directly. To
    interact with the fold tree, you need to modify text block fold level or
    trigger state using :class:`pyqode.core.api.utils.TextBlockHelper` or
    :mod:`pyqode.core.api.folding`
    """
    #: signal emitted when a fold trigger state has changed, parameters are
    #: the concerned text block and the new state (collapsed or not).
    trigger_state_changed = QtCore.Signal(QtGui.QTextBlock, bool)
    collapse_all_triggered = QtCore.Signal()
    expand_all_triggered = QtCore.Signal()

    @property
    def native_look(self):
        """
        Defines whether the panel will use native indicator icons and color or
        use custom one.

        If you want to use custom indicator icons and color, you must first
        set this flag to False.
        """
        return self._native

    @native_look.setter
    def native_look(self, value):
        self._native = value
        # propagate changes to every clone
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).native_look = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def custom_indicators_icons(self):
        """
        Gets/sets the custom icon for the fold indicators.

        The list of indicators is interpreted as follow::

            (COLLAPSED_OFF, COLLAPSED_ON, EXPANDED_OFF, EXPANDED_ON)

        To use this property you must first set `native_look` to False.

        :returns: tuple(str, str, str, str)
        """
        return self._custom_indicators

    @custom_indicators_icons.setter
    def custom_indicators_icons(self, value):
        if len(value) != 4:
            raise ValueError('The list of custom indicators must contains 4 '
                             'strings')
        self._custom_indicators = value
        if self.editor:
            # propagate changes to every clone
            for clone in self.editor.clones:
                try:
                    clone.modes.get(
                        self.__class__).custom_indicators_icons = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def custom_fold_region_background(self):
        """
        Custom base color for the fold region background

        :return: QColor
        """
        return self._custom_color

    @custom_fold_region_background.setter
    def custom_fold_region_background(self, value):
        self._custom_color = value
        # propagate changes to every clone
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(
                        self.__class__).custom_fold_region_background = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def highlight_caret_scope(self):
        """
        True to highlight the caret scope automatically.

        (Similar to the ``Highlight blocks in Qt Creator``.

        Default is False.
        """
        return self._highlight_caret

    @highlight_caret_scope.setter
    def highlight_caret_scope(self, value):
        if value != self._highlight_caret:
            self._highlight_caret = value
            if self.editor:
                if value:
                    self._block_nbr = -1
                    self.editor.cursorPositionChanged.connect(
                        self._highlight_caret_scope)
                else:
                    self._block_nbr = -1
                    self.editor.cursorPositionChanged.disconnect(
                        self._highlight_caret_scope)
                for clone in self.editor.clones:
                    try:
                        clone.modes.get(
                            self.__class__).highlight_caret_scope = value
                    except KeyError:
                        # this should never happen since we're working with
                        # clones
                        pass

    def __init__(self, highlight_caret_scope=False):
        Panel.__init__(self)
        self._native = True
        self._custom_indicators = (
            ':/pyqode-icons/rc/arrow_right_off.png',
            ':/pyqode-icons/rc/arrow_right_on.png',
            ':/pyqode-icons/rc/arrow_down_off.png',
            ':/pyqode-icons/rc/arrow_down_on.png'
        )
        self._custom_color = QtGui.QColor('gray')
        self._block_nbr = -1
        self._highlight_caret = False
        self.highlight_caret_scope = highlight_caret_scope
        self._indic_size = 16
        #: the list of deco used to highlight the current fold region (
        #: surrounding regions are darker)
        self._scope_decos = []
        #: the list of folded blocs decorations
        self._block_decos = []
        self.setMouseTracking(True)
        self.scrollable = True
        self._mouse_over_line = None
        self._current_scope = None
        self._prev_cursor = None
        self.context_menu = None
        self.action_collapse = None
        self.action_expand = None
        self.action_collapse_all = None
        self.action_expand_all = None
        self._original_background = None
        self._highlight_runner = DelayJobRunner(delay=250)

    def on_install(self, editor):
        """
        Add the folding menu to the editor, on install.

        :param editor: editor instance on which the mode has been installed to.
        """
        super(FoldingPanel, self).on_install(editor)
        self.context_menu = QtWidgets.QMenu('Folding', self.editor)
        action = self.action_collapse = QtWidgets.QAction(
            'Collapse', self.context_menu)
        action.setShortcut('Shift+-')
        action.triggered.connect(self._on_action_toggle)
        self.context_menu.addAction(action)
        action = self.action_expand = QtWidgets.QAction('Expand',
                                                        self.context_menu)
        action.setShortcut('Shift++')
        action.triggered.connect(self._on_action_toggle)
        self.context_menu.addAction(action)
        self.context_menu.addSeparator()
        action = self.action_collapse_all = QtWidgets.QAction(
            'Collapse all', self.context_menu)
        action.setShortcut('Ctrl+Shift+-')
        action.triggered.connect(self._on_action_collapse_all_triggered)
        self.context_menu.addAction(action)
        action = self.action_expand_all = QtWidgets.QAction(
            'Expand all', self.context_menu)
        action.setShortcut('Ctrl+Shift++')
        action.triggered.connect(self._on_action_expand_all_triggered)
        self.context_menu.addAction(action)
        self.editor.add_menu(self.context_menu)

    def sizeHint(self):
        """ Returns the widget size hint (based on the editor font size) """
        fm = QtGui.QFontMetricsF(self.editor.font())
        size_hint = QtCore.QSize(fm.height(), fm.height())
        if size_hint.width() > 16:
            size_hint.setWidth(16)
        return size_hint

    def paintEvent(self, event):
        # Paints the fold indicators and the possible fold region background
        # on the folding panel.
        super(FoldingPanel, self).paintEvent(event)
        painter = QtGui.QPainter(self)
        # Draw background over the selected non collapsed fold region
        if self._mouse_over_line is not None:
            block = self.editor.document().findBlockByNumber(
                self._mouse_over_line)
            try:
                self._draw_fold_region_background(block, painter)
            except ValueError:
                pass
        # Draw fold triggers
        for top_position, line_number, block in self.editor.visible_blocks:
            if TextBlockHelper.is_fold_trigger(block):
                collapsed = TextBlockHelper.get_fold_trigger_state(block)
                mouse_over = self._mouse_over_line == line_number
                self._draw_fold_indicator(
                    top_position, mouse_over, collapsed, painter)
                if collapsed:
                    # check if the block already has a decoration, it might
                    # have been folded by the parent editor/document in the
                    # case of cloned editor
                    for deco in self._block_decos:
                        if deco.block == block:
                            # no need to add a deco, just go to the next block
                            break
                    else:
                        self._add_fold_decoration(block, FoldScope(block))
                else:
                    for deco in self._block_decos:
                        # check if the block decoration has been removed, it
                        # might have been unfolded by the parent
                        # editor/document in the case of cloned editor
                        if deco.block == block:
                            # remove it and
                            self._block_decos.remove(deco)
                            self.editor.decorations.remove(deco)
                            del deco
                            break

    def _draw_fold_region_background(self, block, painter):
        """
        Draw the fold region when the mouse is over and non collapsed
        indicator.

        :param top: Top position
        :param block: Current block.
        :param painter: QPainter
        """
        r = folding.FoldScope(block)
        th = TextHelper(self.editor)
        start, end = r.get_range(ignore_blank_lines=True)
        if start > 0:
            top = th.line_pos_from_number(start)
        else:
            top = 0
        bottom = th.line_pos_from_number(end + 1)
        h = bottom - top
        if h == 0:
            h = self.sizeHint().height()
        w = self.sizeHint().width()
        self._draw_rect(QtCore.QRectF(0, top, w, h), painter)

    def _draw_rect(self, rect, painter):
        """
        Draw the background rectangle using the current style primitive color
        or foldIndicatorBackground if nativeFoldingIndicator is true.

        :param rect: The fold zone rect to draw

        :param painter: The widget's painter.
        """
        c = self._custom_color
        if self._native:
            c = self.get_system_bck_color()
        grad = QtGui.QLinearGradient(rect.topLeft(),
                                     rect.topRight())
        if sys.platform == 'darwin':
            grad.setColorAt(0, c.lighter(100))
            grad.setColorAt(1, c.lighter(110))
            outline = c.darker(110)
        else:
            grad.setColorAt(0, c.lighter(110))
            grad.setColorAt(1, c.lighter(130))
            outline = c.darker(100)
        painter.fillRect(rect, grad)
        painter.setPen(QtGui.QPen(outline))
        painter.drawLine(rect.topLeft() +
                         QtCore.QPointF(1, 0),
                         rect.topRight() -
                         QtCore.QPointF(1, 0))
        painter.drawLine(rect.bottomLeft() +
                         QtCore.QPointF(1, 0),
                         rect.bottomRight() -
                         QtCore.QPointF(1, 0))
        painter.drawLine(rect.topRight() +
                         QtCore.QPointF(0, 1),
                         rect.bottomRight() -
                         QtCore.QPointF(0, 1))
        painter.drawLine(rect.topLeft() +
                         QtCore.QPointF(0, 1),
                         rect.bottomLeft() -
                         QtCore.QPointF(0, 1))

    @staticmethod
    def get_system_bck_color():
        """
        Gets a system color for drawing the fold scope background.
        """
        def merged_colors(colorA, colorB, factor):
            maxFactor = 100
            colorA = QtGui.QColor(colorA)
            colorB = QtGui.QColor(colorB)
            tmp = colorA
            tmp.setRed((tmp.red() * factor) / maxFactor +
                       (colorB.red() * (maxFactor - factor)) / maxFactor)
            tmp.setGreen((tmp.green() * factor) / maxFactor +
                         (colorB.green() * (maxFactor - factor)) / maxFactor)
            tmp.setBlue((tmp.blue() * factor) / maxFactor +
                        (colorB.blue() * (maxFactor - factor)) / maxFactor)
            return tmp

        pal = QtWidgets.QApplication.instance().palette()
        b = pal.window().color()
        h = pal.highlight().color()
        return merged_colors(b, h, 50)

    def _draw_fold_indicator(self, top, mouse_over, collapsed, painter):
        """
        Draw the fold indicator/trigger (arrow).

        :param top: Top position
        :param mouse_over: Whether the mouse is over the indicator
        :param collapsed: Whether the trigger is collapsed or not.
        :param painter: QPainter
        """
        rect = QtCore.QRect(0, top, self.sizeHint().width(),
                            self.sizeHint().height())
        if self._native:
            if os.environ['QT_API'].lower() not in PYQT5_API:
                opt = QtGui.QStyleOptionViewItemV2()
            else:
                opt = QtWidgets.QStyleOptionViewItem()
            opt.rect = rect
            opt.state = (QtWidgets.QStyle.State_Active |
                         QtWidgets.QStyle.State_Item |
                         QtWidgets.QStyle.State_Children)
            if not collapsed:
                opt.state |= QtWidgets.QStyle.State_Open
            if mouse_over:
                opt.state |= (QtWidgets.QStyle.State_MouseOver |
                              QtWidgets.QStyle.State_Enabled |
                              QtWidgets.QStyle.State_Selected)
                opt.palette.setBrush(QtGui.QPalette.Window,
                                     self.palette().highlight())
            opt.rect.translate(-2, 0)
            self.style().drawPrimitive(QtWidgets.QStyle.PE_IndicatorBranch,
                                       opt, painter, self)
        else:
            index = 0
            if not collapsed:
                index = 2
            if mouse_over:
                index += 1
            QtGui.QIcon(self._custom_indicators[index]).paint(painter, rect)

    @staticmethod
    def find_parent_scope(block):
        """
        Find parent scope, if the block is not a fold trigger.

        """
        original = block
        if not TextBlockHelper.is_fold_trigger(block):
            # search level of next non blank line
            while block.text().strip() == '' and block.isValid():
                block = block.next()
            ref_lvl = TextBlockHelper.get_fold_lvl(block) - 1
            block = original
            while (block.blockNumber() and
                   (not TextBlockHelper.is_fold_trigger(block) or
                    TextBlockHelper.get_fold_lvl(block) > ref_lvl)):
                block = block.previous()
        return block

    def _clear_scope_decos(self):
        """
        Clear scope decorations (on the editor)

        """
        for deco in self._scope_decos:
            self.editor.decorations.remove(deco)
        self._scope_decos[:] = []

    def _get_scope_highlight_color(self):
        """
        Gets the base scope highlight color (derivated from the editor
        background)

        """
        color = self.editor.background
        if color.lightness() < 128:
            color = drift_color(color, 130)
        else:
            color = drift_color(color, 105)
        return color

    def _add_scope_deco(self, start, end, parent_start, parent_end, base_color,
                        factor):
        """
        Adds a scope decoration that enclose the current scope
        :param start: Start of the current scope
        :param end: End of the current scope
        :param parent_start: Start of the parent scope
        :param parent_end: End of the parent scope
        :param base_color: base color for scope decoration
        :param factor: color factor to apply on the base color (to make it
            darker).
        """
        color = drift_color(base_color, factor=factor)
        # upper part
        if start > 0:
            d = TextDecoration(self.editor.document(),
                               start_line=parent_start, end_line=start)
            d.set_full_width(True, clear=False)
            d.draw_order = 2
            d.set_background(color)
            self.editor.decorations.append(d)
            self._scope_decos.append(d)
        # lower part
        if end <= self.editor.document().blockCount():
            d = TextDecoration(self.editor.document(),
                               start_line=end, end_line=parent_end + 1)
            d.set_full_width(True, clear=False)
            d.draw_order = 2
            d.set_background(color)
            self.editor.decorations.append(d)
            self._scope_decos.append(d)

    def _add_scope_decorations(self, block, start, end):
        """
        Show a scope decoration on the editor widget

        :param start: Start line
        :param end: End line
        """
        try:
            parent = FoldScope(block).parent()
        except ValueError:
            parent = None
        if TextBlockHelper.is_fold_trigger(block):
            base_color = self._get_scope_highlight_color()
            factor_step = 5
            if base_color.lightness() < 128:
                factor_step = 10
                factor = 70
            else:
                factor = 100
            while parent:
                # highlight parent scope
                parent_start, parent_end = parent.get_range()
                self._add_scope_deco(
                    start, end + 1, parent_start, parent_end,
                    base_color, factor)
                # next parent scope
                start = parent_start
                end = parent_end
                parent = parent.parent()
                factor += factor_step
            # global scope
            parent_start = 0
            parent_end = self.editor.document().blockCount()
            self._add_scope_deco(
                start, end + 1, parent_start, parent_end, base_color,
                factor + factor_step)
        else:
            self._clear_scope_decos()

    def _highlight_surrounding_scopes(self, block):
        """
        Highlights the scopes surrounding the current fold scope.

        :param block: Block that starts the current fold scope.
        """
        scope = FoldScope(block)
        if (self._current_scope is None or
                self._current_scope.get_range() != scope.get_range()):
            self._current_scope = scope
            self._clear_scope_decos()
            # highlight surrounding parent scopes with a darker color
            start, end = scope.get_range()
            if not TextBlockHelper.get_fold_trigger_state(block):
                self._add_scope_decorations(block, start, end)

    def mouseMoveEvent(self, event):
        """
        Detect mouser over indicator and highlight the current scope in the
        editor (up and down decoration arround the foldable text when the mouse
        is over an indicator).

        :param event: event
        """
        super(FoldingPanel, self).mouseMoveEvent(event)
        th = TextHelper(self.editor)
        line = th.line_nbr_from_position(event.pos().y())
        if line >= 0:
            block = FoldScope.find_parent_scope(
                self.editor.document().findBlockByNumber(line))
            if TextBlockHelper.is_fold_trigger(block):
                if self._mouse_over_line is None:
                    # mouse enter fold scope
                    QtWidgets.QApplication.setOverrideCursor(
                        QtGui.QCursor(QtCore.Qt.PointingHandCursor))
                if self._mouse_over_line != block.blockNumber() and \
                        self._mouse_over_line is not None:
                    # fold scope changed, a previous block was highlighter so
                    # we quickly update our highlighting
                    self._mouse_over_line = block.blockNumber()
                    self._highlight_surrounding_scopes(block)
                else:
                    # same fold scope, request highlight
                    self._mouse_over_line = block.blockNumber()
                    self._highlight_runner.request_job(
                        self._highlight_surrounding_scopes, block)
                self._highight_block = block
            else:
                # no fold scope to highlight, cancel any pending requests
                self._highlight_runner.cancel_requests()
                self._mouse_over_line = None
                QtWidgets.QApplication.restoreOverrideCursor()
            self.repaint()

    def leaveEvent(self, event):
        """
        Removes scope decorations and background from the editor and the panel
        if highlight_caret_scope, else simply update the scope decorations to
        match the caret scope.

        """
        super(FoldingPanel, self).leaveEvent(event)
        QtWidgets.QApplication.restoreOverrideCursor()
        self._highlight_runner.cancel_requests()
        if not self.highlight_caret_scope:
            self._clear_scope_decos()
            self._mouse_over_line = None
            self._current_scope = None
        else:
            self._block_nbr = -1
            self._highlight_caret_scope()
        self.editor.repaint()

    def _add_fold_decoration(self, block, region):
        """
        Add fold decorations (boxes arround a folded block in the editor
        widget).
        """
        _logger().debug('add fold deco %r', block)
        deco = TextDecoration(block)
        deco.signals.clicked.connect(self._on_fold_deco_clicked)
        deco.tooltip = region.text(max_lines=25)
        deco.draw_order = 1
        deco.block = block
        deco.select_line()
        deco.set_outline(drift_color(
            self._get_scope_highlight_color(), 110))
        deco.set_background(self._get_scope_highlight_color())
        deco.set_foreground(QtGui.QColor('#808080'))
        self._block_decos.append(deco)
        self.editor.decorations.append(deco)

    def toggle_fold_trigger(self, block):
        """
        Toggle a fold trigger block (expand or collapse it).

        :param block: The QTextBlock to expand/collapse
        """
        if not TextBlockHelper.is_fold_trigger(block):
            return
        region = FoldScope(block)
        if region.collapsed:
            region.unfold()
            if self._mouse_over_line is not None:
                self._add_scope_decorations(
                    region._trigger, *region.get_range())
        else:
            region.fold()
            self._clear_scope_decos()
        self._refresh_editor_and_scrollbars()
        self.trigger_state_changed.emit(region._trigger, region.collapsed)

    def mousePressEvent(self, event):
        """ Folds/unfolds the pressed indicator if any. """
        if self._mouse_over_line is not None:
            block = self.editor.document().findBlockByNumber(
                self._mouse_over_line)
            self.toggle_fold_trigger(block)

    def _on_fold_deco_clicked(self, deco):
        """
        Unfold a folded block that has just been clicked by the user
        """
        self.toggle_fold_trigger(deco.block)

    def on_state_changed(self, state):
        """
        On state changed we (dis)connect to the cursorPositionChanged signal
        """
        if state:
            self.editor.key_pressed.connect(self._on_key_pressed)
            if self._highlight_caret:
                self.editor.cursorPositionChanged.connect(
                    self._highlight_caret_scope)
                self._block_nbr = -1
            self.editor.new_text_set.connect(self._clear_block_deco)
        else:
            self.editor.key_pressed.disconnect(self._on_key_pressed)
            if self._highlight_caret:
                self.editor.cursorPositionChanged.disconnect(
                    self._highlight_caret_scope)
                self._block_nbr = -1
            self.editor.new_text_set.disconnect(self._clear_block_deco)

    def _select_scope(self, block, c):
        """
        Select the content of a scope
        """
        start_block = block
        _, end = FoldScope(block).get_range()
        end_block = self.editor.document().findBlockByNumber(end)
        c.beginEditBlock()
        c.setPosition(start_block.position())
        c.setPosition(end_block.position(), c.KeepAnchor)
        c.deleteChar()
        c.endEditBlock()

    def _on_key_pressed(self, event):
        """
        Override key press to select the current scope if the user wants
        to deleted a folded scope (without selecting it).
        """
        keys = [QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace]
        if event.key() in keys:
            c = self.editor.textCursor()
            assert isinstance(c, QtGui.QTextCursor)
            if c.hasSelection():
                for deco in self._block_decos:
                    if c.selectedText() == deco.cursor.selectedText():
                        block = deco.block
                        self._select_scope(block, c)
                        event.accept()
                        break

    @staticmethod
    def _show_previous_blank_lines(block):
        """
        Show the block previous blank lines
        """
        # set previous blank lines visibles
        pblock = block.previous()
        while (pblock.text().strip() == '' and
               pblock.blockNumber() >= 0):
            pblock.setVisible(True)
            pblock = pblock.previous()

    def refresh_decorations(self, force=False):
        """
        Refresh decorations colors. This function is called by the syntax
        highlighter when the style changed so that we may update our
        decorations colors according to the new style.

        """
        cursor = self.editor.textCursor()
        if (self._prev_cursor is None or force or
                self._prev_cursor.blockNumber() != cursor.blockNumber()):
            for deco in self._block_decos:
                self.editor.decorations.remove(deco)
            for deco in self._block_decos:
                deco.set_outline(drift_color(
                    self._get_scope_highlight_color(), 110))
                deco.set_background(self._get_scope_highlight_color())
                self.editor.decorations.append(deco)
        self._prev_cursor = cursor

    def _refresh_editor_and_scrollbars(self):
        """
        Refrehes editor content and scollbars.

        We generate a fake resize event to refresh scroll bar.

        We have the same problem as described here:
        http://www.qtcentre.org/threads/44803 and we apply the same solution
        (don't worry, there is no visual effect, the editor does not grow up
        at all, even with a value = 500)
        """
        TextHelper(self.editor).mark_whole_doc_dirty()
        self.editor.repaint()
        s = self.editor.size()
        s.setWidth(s.width() + 1)
        self.editor.resizeEvent(QtGui.QResizeEvent(self.editor.size(), s))

    def collapse_all(self):
        """
        Collapses all triggers and makes all blocks with fold level > 0
        invisible.
        """
        self._clear_block_deco()
        block = self.editor.document().firstBlock()
        last = self.editor.document().lastBlock()
        while block.isValid():
            lvl = TextBlockHelper.get_fold_lvl(block)
            trigger = TextBlockHelper.is_fold_trigger(block)
            if trigger:
                if lvl == 0:
                    self._show_previous_blank_lines(block)
                TextBlockHelper.set_fold_trigger_state(block, True)
            block.setVisible(lvl == 0)
            if block == last and block.text().strip() == '':
                block.setVisible(True)
                self._show_previous_blank_lines(block)
            block = block.next()
        self._refresh_editor_and_scrollbars()
        tc = self.editor.textCursor()
        tc.movePosition(tc.Start)
        self.editor.setTextCursor(tc)
        self.collapse_all_triggered.emit()

    def _clear_block_deco(self):
        """
        Clear the folded block decorations.
        """
        for deco in self._block_decos:
            self.editor.decorations.remove(deco)
        self._block_decos[:] = []

    def expand_all(self):
        """
        Expands all fold triggers.
        """
        block = self.editor.document().firstBlock()
        while block.isValid():
            TextBlockHelper.set_fold_trigger_state(block, False)
            block.setVisible(True)
            block = block.next()
        self._clear_block_deco()
        self._refresh_editor_and_scrollbars()
        self.expand_all_triggered.emit()

    def _on_action_toggle(self):
        """
        Toggle the current fold trigger.
        """
        block = FoldScope.find_parent_scope(self.editor.textCursor().block())
        self.toggle_fold_trigger(block)

    def _on_action_collapse_all_triggered(self):
        """
        Closes all top levels fold triggers recursively
        """
        self.collapse_all()

    def _on_action_expand_all_triggered(self):
        """
        Expands all fold triggers
        :return:
        """
        self.expand_all()

    def _highlight_caret_scope(self):
        """
        Highlight the scope surrounding the current caret position.

        This get called only if :attr:`
        pyqode.core.panels.FoldingPanel.highlight_care_scope` is True.
        """
        cursor = self.editor.textCursor()
        block_nbr = cursor.blockNumber()
        if self._block_nbr != block_nbr:
            block = FoldScope.find_parent_scope(
                self.editor.textCursor().block())
            try:
                s = FoldScope(block)
            except ValueError:
                self._clear_scope_decos()
            else:
                self._mouse_over_line = block.blockNumber()
                if TextBlockHelper.is_fold_trigger(block):
                    self._highlight_surrounding_scopes(block)
        self._block_nbr = block_nbr

    def clone_settings(self, original):
        self.native_look = original.native_look
        self.custom_indicators_icons = original.custom_indicators_icons
        self.highlight_caret_scope = original.highlight_caret_scope
        self.custom_fold_region_background = \
            original.custom_fold_region_background
Exemple #8
0
class CheckerPanel(Panel):
    """ Shows messages collected by one or more checker modes """
    def __init__(self):
        super(CheckerPanel, self).__init__()
        self._previous_line = -1
        self.scrollable = True
        self._job_runner = DelayJobRunner(delay=100)
        self.setMouseTracking(True)
        #: Info icon
        self.info_icon = icons.icon('dialog-info',
                                    ':pyqode-icons/rc/dialog-info.png',
                                    'fa.info-circle',
                                    qta_options={'color': '#4040DD'})
        self.warning_icon = icons.icon('dialog-warning',
                                       ':pyqode-icons/rc/dialog-warning.png',
                                       'fa.exclamation-triangle',
                                       qta_options={'color': '#DDDD40'})
        self.error_icon = icons.icon('dialog-error',
                                     ':pyqode-icons/rc/dialog-error.png',
                                     'fa.exclamation-circle',
                                     qta_options={'color': '#DD4040'})

    def marker_for_line(self, line):
        """
        Returns the marker that is displayed at the specified line number if
        any.

        :param line: The marker line.

        :return: Marker of None
        :rtype: pyqode.core.Marker
        """
        block = self.editor.document().findBlockByNumber(line)
        try:
            return block.userData().messages
        except AttributeError:
            return []

    def sizeHint(self):
        """
        Returns the panel size hint. (fixed with of 16px)
        """
        metrics = QtGui.QFontMetricsF(self.editor.font())
        size_hint = QtCore.QSize(metrics.height(), metrics.height())
        if size_hint.width() > 16:
            size_hint.setWidth(16)
        return size_hint

    def on_uninstall(self):
        self._job_runner.cancel_requests()
        super(CheckerPanel, self).on_uninstall()

    def paintEvent(self, event):
        super(CheckerPanel, self).paintEvent(event)
        painter = QtGui.QPainter(self)
        for top, block_nbr, block in self.editor.visible_blocks:
            user_data = block.userData()
            if user_data and user_data.messages:
                for msg in user_data.messages:
                    icon = self._icon_from_message(msg)
                    if icon:
                        rect = QtCore.QRect()
                        rect.setX(0)
                        rect.setY(top)
                        rect.setWidth(self.sizeHint().width())
                        rect.setHeight(self.sizeHint().height())
                        icon.paint(painter, rect)

    def _icon_from_message(self, message):
        icons = {
            CheckerMessages.INFO: self.info_icon,
            CheckerMessages.WARNING: self.warning_icon,
            CheckerMessages.ERROR: self.error_icon
        }
        return icons[message.status]

    def mouseMoveEvent(self, event):
        # Requests a tooltip if the cursor is currently over a marker.
        line = TextHelper(self.editor).line_nbr_from_position(event.pos().y())
        if line:
            markers = self.marker_for_line(line)
            text = '\n'.join([
                marker.description for marker in markers if marker.description
            ])
            if len(markers):
                if self._previous_line != line:
                    top = TextHelper(self.editor).line_pos_from_number(
                        markers[0].line)
                    if top:
                        self._job_runner.request_job(self._display_tooltip,
                                                     text, top)
            else:
                self._job_runner.cancel_requests()
            self._previous_line = line

    def leaveEvent(self, *args):
        """
        Hide tooltip when leaving the panel region.
        """
        QtWidgets.QToolTip.hideText()
        self._previous_line = -1

    def _display_tooltip(self, tooltip, top):
        """
        Display tooltip at the specified top position.
        """
        QtWidgets.QToolTip.showText(
            self.mapToGlobal(QtCore.QPoint(self.sizeHint().width(), top)),
            tooltip, self)
Exemple #9
0
class CheckerPanel(Panel):
    """ Shows messages collected by one or more checker modes """
    def __init__(self):
        super(CheckerPanel, self).__init__()
        self._previous_line = -1
        self.scrollable = True
        self._job_runner = DelayJobRunner(delay=100)
        self.setMouseTracking(True)
        #: Info icon
        self.info_icon = icons.icon(
            'dialog-info', ':pyqode-icons/rc/dialog-info.png',
            'fa.info-circle', qta_options={'color': '#4040DD'})
        self.warning_icon = icons.icon(
            'dialog-warning', ':pyqode-icons/rc/dialog-warning.png',
            'fa.exclamation-triangle', qta_options={'color': '#DDDD40'})
        self.error_icon = icons.icon(
            'dialog-error', ':pyqode-icons/rc/dialog-error.png',
            'fa.exclamation-circle', qta_options={'color': '#DD4040'})

    def marker_for_line(self, line):
        """
        Returns the marker that is displayed at the specified line number if
        any.

        :param line: The marker line.

        :return: Marker of None
        :rtype: pyqode.core.Marker
        """
        block = self.editor.document().findBlockByNumber(line)
        try:
            return block.userData().messages
        except AttributeError:
            return []

    def sizeHint(self):
        """
        Returns the panel size hint. (fixed with of 16px)
        """
        metrics = QtGui.QFontMetricsF(self.editor.font())
        size_hint = QtCore.QSize(metrics.height(), metrics.height())
        if size_hint.width() > 16:
            size_hint.setWidth(16)
        return size_hint

    def on_uninstall(self):
        self._job_runner.cancel_requests()
        super(CheckerPanel, self).on_uninstall()

    def paintEvent(self, event):
        super(CheckerPanel, self).paintEvent(event)
        painter = QtGui.QPainter(self)
        for top, block_nbr, block in self.editor.visible_blocks:
            user_data = block.userData()
            if user_data and user_data.messages:
                for msg in user_data.messages:
                    icon = self._icon_from_message(msg)
                    if icon:
                        rect = QtCore.QRect()
                        rect.setX(0)
                        rect.setY(top)
                        rect.setWidth(self.sizeHint().width())
                        rect.setHeight(self.sizeHint().height())
                        icon.paint(painter, rect)

    def _icon_from_message(self, message):
        icons = {
            CheckerMessages.INFO: self.info_icon,
            CheckerMessages.WARNING: self.warning_icon,
            CheckerMessages.ERROR: self.error_icon
        }
        return icons[message.status]

    def mouseMoveEvent(self, event):
        # Requests a tooltip if the cursor is currently over a marker.
        line = TextHelper(self.editor).line_nbr_from_position(event.pos().y())
        if line:
            markers = self.marker_for_line(line)
            text = '\n'.join([marker.description for marker in markers if
                              marker.description])
            if len(markers):
                if self._previous_line != line:
                    top = TextHelper(self.editor).line_pos_from_number(
                        markers[0].line)
                    if top:
                        self._job_runner.request_job(self._display_tooltip,
                                                     text, top)
            else:
                self._job_runner.cancel_requests()
            self._previous_line = line

    def leaveEvent(self, *args):
        """
        Hide tooltip when leaving the panel region.
        """
        QtWidgets.QToolTip.hideText()
        self._previous_line = -1

    def _display_tooltip(self, tooltip, top):
        """
        Display tooltip at the specified top position.
        """
        QtWidgets.QToolTip.showText(self.mapToGlobal(QtCore.QPoint(
            self.sizeHint().width(), top)), tooltip, self)
class OccurrencesHighlighterMode(Mode):
    """ Highlights occurrences of the word under the text text cursor.

    The ``delay`` before searching for occurrences is configurable.
    """
    @property
    def delay(self):
        """
        Delay before searching for occurrences. The timer is rearmed as soon
        as the cursor position changed.
        """
        return self.timer.delay

    @delay.setter
    def delay(self, value):
        self.timer.delay = value
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).delay = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def background(self):
        """
        Background or underline color (if underlined is True).
        """
        return self._background

    @background.setter
    def background(self, value):
        self._background = value
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).background = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def foreground(self):
        """
        Foreground color of occurences, not used if underlined is True.
        """
        return self._foreground

    @foreground.setter
    def foreground(self, value):
        self._foreground = value
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).foreground = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def underlined(self):
        """
        True to use to underlined occurrences instead of
        changing the background. Default is True.

        If this mode is ON, the foreground color is ignored, the
        background color is then used as the underline color.

        """
        return self._underlined

    @underlined.setter
    def underlined(self, value):
        self._underlined = value
        if self.editor:
            for clone in self.editor.clones:
                try:
                    clone.modes.get(self.__class__).underlined = value
                except KeyError:
                    # this should never happen since we're working with clones
                    pass

    @property
    def case_sensitive(self):
        return self._case_sensitive

    @case_sensitive.setter
    def case_sensitive(self, value):
        self._case_sensitive = value
        self._request_highlight()

    def __init__(self):
        super(OccurrencesHighlighterMode, self).__init__()
        self._decorations = []
        #: Timer used to run the search request with a specific delay
        self.timer = DelayJobRunner(delay=1000)
        self._sub = None
        self._background = QtGui.QColor('#CCFFCC')
        self._foreground = None
        self._underlined = False
        self._case_sensitive = False

    def on_state_changed(self, state):
        if state:
            self.editor.cursorPositionChanged.connect(self._request_highlight)
        else:
            self.editor.cursorPositionChanged.disconnect(
                self._request_highlight)
            self.timer.cancel_requests()

    def _clear_decos(self):
        for d in self._decorations:
            self.editor.decorations.remove(d)
        self._decorations[:] = []

    def _request_highlight(self):
        if self.editor is not None:
            sub = TextHelper(self.editor).word_under_cursor(
                select_whole_word=True).selectedText()
            if sub != self._sub:
                self._clear_decos()
                if len(sub) > 1:
                    self.timer.request_job(self._send_request)

    def _send_request(self):
        if self.editor is None:
            return
        cursor = self.editor.textCursor()
        self._sub = TextHelper(self.editor).word_under_cursor(
            select_whole_word=True).selectedText()
        if not cursor.hasSelection() or cursor.selectedText() == self._sub:
            request_data = {
                'string': self.editor.toPlainText(),
                'sub': self._sub,
                'regex': False,
                'whole_word': True,
                'case_sensitive': self.case_sensitive
            }
            try:
                self.editor.backend.send_request(findall, request_data,
                                                 self._on_results_available)
            except NotRunning:
                self._request_highlight()

    def _on_results_available(self, results):
        if len(results) > 500:
            # limit number of results (on very big file where a lots of
            # occurrences can be found, this would totally freeze the editor
            # during a few seconds, with a limit of 500 we can make sure
            # the editor will always remain responsive).
            results = results[:500]
        current = self.editor.textCursor().position()
        if len(results) > 1:
            for start, end in results:
                if start <= current <= end:
                    continue
                deco = TextDecoration(self.editor.textCursor(),
                                      start_pos=start, end_pos=end)
                if self.underlined:
                    deco.set_as_underlined(self._background)
                else:
                    deco.set_background(QtGui.QBrush(self._background))
                    if self._foreground is not None:
                        deco.set_foreground(self._foreground)
                deco.draw_order = 3
                self.editor.decorations.append(deco)
                self._decorations.append(deco)

    def clone_settings(self, original):
        self.delay = original.delay
        self.background = original.background
        self.foreground = original.foreground
        self.underlined = original.underlined
Exemple #11
0
class DocumentOutlineMode(QObject, Mode):
    """
    Parses the current cobol document when the text changed and emit the
    changed event if any properties of any document node has changed.

    This mode can be used to implement a document outline widget.

    """
    #: Signal emitted when the document layout changed
    changed = Signal(Name, list, list)

    @property
    def root_node(self):
        """
        Returns the document root node.
        """
        return self._root_node

    @property
    def variables(self):
        """
        Returns the list of variable document nodes
        """
        return self._vars

    @property
    def paragraphs(self):
        """
        Returns the list of paragraphs document nodes
        """
        return self._paragraphs

    def __init__(self):
        QObject.__init__(self)
        Mode.__init__(self)
        self._root_node = None
        self._vars = []
        self._paragraphs = []
        self._runner = DelayJobRunner()

    def on_state_changed(self, state):
        """
        Called when the mode is activated/deactivated
        """
        if state:
            self.editor.new_text_set.connect(self.parse)
            self.editor.textChanged.connect(self._parse)
        else:
            self.editor.new_text_set.disconnect(self.parse)
            self.editor.textChanged.disconnect(self._parse)
            self._runner.cancel_requests()

    def _parse(self):
        self._runner.request_job(self.parse)

    def parse(self):
        """ Parse the document layout.

        To get the results, use the following properties:
            - root_node
            - variables
            - paragraphs
        """
        # preview in preferences dialog have no file path
        if not self.editor.file.path:
            return
        txt = self.editor.toPlainText()
        fmt = self.editor.free_format
        try:
            root_node, variables, paragraphs = defined_names(txt, fmt)
        except AttributeError:
            # this should never happen but we must exit gracefully
            _logger().exception("Failed to parse document, probably due to "
                                "a malformed syntax.")
        else:
            changed = False
            if self._root_node is None or cmp_name(root_node, self._root_node):
                changed = True
            self._root_node = root_node
            self._vars = variables
            self._paragraphs = paragraphs
            if changed:
                _logger().debug('changed')
                self.changed.emit(
                    self.root_node, self.variables, self.paragraphs)
Exemple #12
0
class DocumentAnalyserMode(Mode, QtCore.QObject):
    """ Analyses the document outline as a tree of statements.

    This mode analyses the structure of a document (a tree of
    :class:`pyqode.python.backend.workers.Definition`.

    :attr:`pyqode.python.modes.DocumentAnalyserMode.document_changed`
    is emitted whenever the document structure changed.

    To keep good performances, the analysis task is run when the application is
    idle for more than 1 second (by default).
    """
    #: Signal emitted when the document structure changed.
    document_changed = QtCore.Signal()

    def __init__(self, delay=1000):
        Mode.__init__(self)
        QtCore.QObject.__init__(self)
        self._jobRunner = DelayJobRunner(delay=delay)
        #: The list of results (elements might have children; this is actually
        #: a tree).
        self.results = []

    def on_state_changed(self, state):
        if state:
            self.editor.new_text_set.connect(self._run_analysis)
            self.editor.textChanged.connect(self._request_analysis)
        else:
            self.editor.textChanged.disconnect(self._request_analysis)
            self.editor.new_text_set.disconnect(self._run_analysis)
            self._jobRunner.cancel_requests()

    def _request_analysis(self):
        self._jobRunner.request_job(self._run_analysis)

    def _run_analysis(self):
        if self.enabled and self.editor and self.editor.toPlainText() and \
                self.editor.file:
            request_data = {
                'code': self.editor.toPlainText(),
                'path': self.editor.file.path,
                'encoding': self.editor.file.encoding
            }
            try:
                self.editor.backend.send_request(
                    defined_names,
                    request_data,
                    on_receive=self._on_results_available)
            except NotRunning:
                QtCore.QTimer.singleShot(100, self._run_analysis)
        else:
            self.results = []
            self.document_changed.emit()

    def _on_results_available(self, results):
        if results:
            results = [Definition().from_dict(ddict) for ddict in results]
        self.results = results
        if self.results is not None:
            _logger().debug("Document structure changed")
            self.document_changed.emit()

    @property
    def flattened_results(self):
        """
        Flattens the document structure tree as a simple sequential list.
        """
        ret_val = []
        for d in self.results:
            ret_val.append(d)
            for sub_d in d.children:
                nd = Definition(sub_d.name, sub_d.icon, sub_d.line,
                                sub_d.column, sub_d.full_name)
                nd.name = "  " + nd.name
                nd.full_name = "  " + nd.full_name
                ret_val.append(nd)
        return ret_val

    def to_tree_widget_items(self, to_collapse=None):
        """
        Returns the results as a list of top level QTreeWidgetItem.

        This is a convenience function that you can use to update a document
        tree widget wheneve the document changed.
        """
        def convert(name, editor, to_collapse):
            ti = QtWidgets.QTreeWidgetItem()
            ti.setText(0, name.name)
            ti.setIcon(0, QtGui.QIcon(name.icon))
            name.block = editor.document().findBlockByNumber(name.line)
            ti.setData(0, QtCore.Qt.UserRole, name)
            block_data = name.block.userData()
            if block_data is None:
                block_data = TextBlockUserData()
                name.block.setUserData(block_data)
            block_data.tree_item = ti

            if to_collapse is not None and \
                    TextBlockHelper.get_fold_trigger_state(name.block):
                to_collapse.append(ti)

            for ch in name.children:
                ti_ch, to_collapse = convert(ch, editor, to_collapse)
                if ti_ch:
                    ti.addChild(ti_ch)
            return ti, to_collapse

        items = []
        for d in self.results:
            value, to_collapse = convert(d, self.editor, to_collapse)
            items.append(value)
        if to_collapse is not None:
            return items, to_collapse
        return items