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()
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()
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
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 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 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)