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 __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 __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 __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 __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('#80CC80') self._foreground = QtGui.QColor('#404040') self._underlined = True
def __init__(self): super(GoToAssignmentsMode, self).__init__() self._definitions = [] self._goto_requested = False self.action_goto = QtWidgets.QAction("Go to assignments", self) self.action_goto.setShortcut(self.shortcut) self.action_goto.triggered.connect(self.request_goto) self.word_clicked.connect(self._on_word_clicked) self._runner = DelayJobRunner(delay=1)
class HtmlPreviewWidget(QtWebWidgets.QWebView): hide_requested = QtCore.Signal() show_requested = QtCore.Signal() def __init__(self, parent=None): super(HtmlPreviewWidget, self).__init__(parent) self._editor = None self._timer = DelayJobRunner(delay=1000) try: # prevent opening internal links when using QtWebKit self.page().setLinkDelegationPolicy( QtWebWidgets.QWebPage.DelegateAllLinks) except (TypeError, AttributeError): # no needed with QtWebEngine, internal links are properly handled # by the default implementation pass def set_editor(self, editor): url = QtCore.QUrl('') if editor is not None: url = QtCore.QUrl.fromLocalFile(editor.file.path) try: self.setHtml(editor.to_html(), url) except (TypeError, AttributeError): self.setHtml('<center>No preview available...</center>', url) self._editor = None self.hide_requested.emit() else: if self._editor is not None and editor != self._editor: try: self._editor.textChanged.disconnect(self._on_text_changed) except TypeError: pass editor.textChanged.connect(self._on_text_changed) self._editor = proxy(editor) self.show_requested.emit() def _on_text_changed(self, *_): self._timer.request_job(self._update_preview) def _update_preview(self): url = QtCore.QUrl('') if self._editor is not None: url = QtCore.QUrl.fromLocalFile(self._editor.file.path) try: try: pos = self.page().mainFrame().scrollBarValue(QtCore.Qt.Vertical) self.setHtml(self._editor.to_html(), url) self.page().mainFrame().setScrollBarValue(QtCore.Qt.Vertical, pos) except AttributeError: # Not possible with QtWebEngine??? # self._scroll_pos = self.page().mainFrame().scrollBarValue( # QtCore.Qt.Vertical) self.setHtml(self._editor.to_html(), url) except (TypeError, AttributeError): self.setHtml('<center>No preview available...</center>', url) self.hide_requested.emit()
def __init__(self): super(GoToAssignmentsMode, self).__init__() self._definitions = [] self._goto_requested = False self.action_goto = QtWidgets.QAction(_("Go to assignments"), self) self.action_goto.setShortcut(self.shortcut) self.action_goto.triggered.connect(self.request_goto) icon = icons.icon(qta_name='fa.share') if icon: self.action_goto.setIcon(icon) self.word_clicked.connect(self._on_word_clicked) self._runner = DelayJobRunner(delay=1)
def __init__(self): QObject.__init__(self) Mode.__init__(self) self._previous_cursor_start = -1 self._previous_cursor_end = -1 self._definition = None self._deco = None self._pending = False self.action_goto = QAction(_("Go to assignments"), self) self.action_goto.setShortcut('F7') self.action_goto.triggered.connect(self.request_goto) self.word_clicked.connect(self.request_goto) self._timer = DelayJobRunner(delay=200)
class HtmlPreviewWidget(QtWidgets.QTextEdit): """ Display html preview of a document as rich text in a QTextEdit. """ hide_requested = QtCore.Signal() show_requested = QtCore.Signal() def __init__(self, parent=None): super(HtmlPreviewWidget, self).__init__(parent) self._editor = None self._timer = DelayJobRunner(delay=1000) def set_editor(self, editor): try: self.setHtml(editor.to_html()) except (TypeError, AttributeError): self.setHtml('<center>No preview available...</center>') self._editor = None self.hide_requested.emit() else: if self._editor is not None and editor != self._editor: try: self._editor.textChanged.disconnect(self._on_text_changed) except TypeError: pass editor.textChanged.connect(self._on_text_changed) self._editor = proxy(editor) self.show_requested.emit() def _on_text_changed(self, *_): self._timer.request_job(self._update_preview) def _update_preview(self): try: # remember cursor/scrollbar position p = self.textCursor().position() v = self.verticalScrollBar().value() # display new html self.setHtml(self._editor.to_html()) # restore cursor/scrollbar position c = self.textCursor() c.setPosition(p) self.setTextCursor(c) self.verticalScrollBar().setValue(v) except (TypeError, AttributeError): self.setHtml('<center>No preview available...</center>') self.hide_requested.emit()
def __init__(self): QObject.__init__(self) Mode.__init__(self) self._root_node = None self._vars = [] self._paragraphs = [] self._runner = DelayJobRunner()
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 = QtGui.QIcon.fromTheme( 'dialog-info', QtGui.QIcon(':pyqode-icons/rc/dialog-info.png')) #: Warning icon self.warning_icon = QtGui.QIcon.fromTheme( 'dialog-warning', QtGui.QIcon(':pyqode-icons/rc/dialog-warning.png')) #: Error icon self.error_icon = QtGui.QIcon.fromTheme( 'dialog-error', QtGui.QIcon(':pyqode-icons/rc/dialog-error.png'))
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 __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
def __init__(self, parent=None): super(HtmlPreviewWidget, self).__init__(parent) self._editor = None self._timer = DelayJobRunner(delay=1000) try: # prevent opening internal links when using QtWebKit self.page().setLinkDelegationPolicy( QtWebWidgets.QWebPage.DelegateAllLinks) except (TypeError, AttributeError): # no needed with QtWebEngine, internal links are properly handled # by the default implementation pass
class TabBar(QtWidgets.QTabBar): """ Tab bar specialized to allow the user to close a tab using mouse middle click. Also exposes a double clicked signal. """ double_clicked = QtCore.Signal() def __init__(self, parent): QtWidgets.QTabBar.__init__(self, parent) self.setTabsClosable(True) self._timer = DelayJobRunner(delay=1) def mousePressEvent(self, event): QtWidgets.QTabBar.mousePressEvent(self, event) if event.button() == QtCore.Qt.MiddleButton: tab = self.tabAt(event.pos()) self._timer.request_job(self.parentWidget().tabCloseRequested.emit, tab) def mouseDoubleClickEvent(self, event): self.double_clicked.emit()
class TabBar(QtWidgets.QTabBar): """ Tab bar specialized to allow the user to close a tab using mouse middle click. Also exposes a double clicked signal. """ double_clicked = QtCore.Signal() def __init__(self, parent): QtWidgets.QTabBar.__init__(self, parent) self.setTabsClosable(True) self._timer = DelayJobRunner(delay=1) def mousePressEvent(self, event): QtWidgets.QTabBar.mousePressEvent(self, event) if event.button() == QtCore.Qt.MiddleButton: tab = self.tabAt(event.pos()) self._timer.request_job( self.parentWidget().tabCloseRequested.emit, tab) def mouseDoubleClickEvent(self, event): if event.button() == QtCore.Qt.LeftButton: self.double_clicked.emit()
def __init__(self, window=None): super().__init__() self._query_thread = None self.icon_provider = widgets.FileIconProvider() self._runner = DelayJobRunner(delay=300) self.main_window = window self.ui = locator_ui.Ui_Frame() self.ui.setupUi(self) self.ui.lineEdit.textChanged.connect(self.request_search) self.ui.lineEdit.prompt_text = _('Type to locate...') self.ui.lineEdit.installEventFilter(self) self.ui.treeWidget.installEventFilter(self) self.ui.treeWidget.setItemDelegate(HTMLDelegate()) self.setWindowFlags(QtCore.Qt.Popup) self.ui.lineEdit.setFocus(True) self.ui.bt_close.clicked.connect(self.hide) self.ui.bt_infos.clicked.connect(self._show_help) self.mode = self.MODE_GOTO_FILE self.ui.treeWidget.currentItemChanged.connect( self._on_current_item_changed) self.ui.treeWidget.itemDoubleClicked.connect(self._activate) self.ui.cb_non_project_files.toggled.connect(self._search)
def __init__(self, window): super().__init__() self.main_window = window self.parser_plugins = [] self._indexors = [] self._locator = LocatorWidget(self.main_window) self._locator.activated.connect(self._on_locator_activated) self._locator.cancelled.connect(self._on_locator_cancelled) self._widget = QtWidgets.QWidget(self.main_window) self._job_runner = DelayJobRunner() self._cached_files = [] self._running_tasks = False self._widget.installEventFilter(self) self._setup_filesystem_treeview() self._setup_prj_selector_widget(self.main_window) self._setup_dock_widget() self._setup_tab_bar_context_menu(self.main_window) self._setup_locator() self._setup_project_menu() self._load_parser_plugins() api.signals.connect_slot(api.signals.CURRENT_EDITOR_CHANGED, self._on_current_editor_changed)
class GoToAssignmentsMode(WordClickMode): """ Goes to the assignments (using jedi.Script.goto_assignments) when the user execute the shortcut or click word. If there are more than one assignments, an input dialog is used to ask the user to choose the desired assignment. This mode will emit the :attr:`out_of_doc` signal if the definition can not be reached in the current document. IDE will typically connects a slot that open a new editor tab and goes to the definition position. """ #: Signal emitted when the definition cannot be reached in the current #: document out_of_doc = QtCore.Signal(Assignment) #: Signal emitted when no results could be found. no_results_found = QtCore.Signal() shortcut = 'Alt+F2' def __init__(self): super(GoToAssignmentsMode, self).__init__() self._definitions = [] self._goto_requested = False self.action_goto = QtWidgets.QAction("Go to assignments", self) self.action_goto.setShortcut(self.shortcut) self.action_goto.triggered.connect(self.request_goto) self.word_clicked.connect(self._on_word_clicked) self._runner = DelayJobRunner(delay=1) def on_state_changed(self, state): super(GoToAssignmentsMode, self).on_state_changed(state) if state: self.editor.add_action(self.action_goto) else: self.editor.remove_action(self.action_goto) def request_goto(self): """ Request a goto action for the word under the text cursor. """ self._goto_requested = True self._check_word_cursor() def _check_word_cursor(self, tc=None): """ Request a go to assignment. :param tc: Text cursor which contains the text that we must look for its assignment. Can be None to go to the text that is under the text cursor. :type tc: QtGui.QTextCursor """ if not tc: tc = TextHelper(self.editor).word_under_cursor() request_data = { 'code': self.editor.toPlainText(), 'line': tc.blockNumber(), 'column': tc.columnNumber(), 'path': self.editor.file.path, 'encoding': self.editor.file.encoding } try: self.editor.backend.send_request( workers.goto_assignments, request_data, on_receive=self._on_results_available) except NotRunning: pass def _goto(self, definition): fp = '' if self.editor.file.path: fp = os.path.normpath(self.editor.file.path.replace(".pyc", ".py")) if definition.module_path == fp: line = definition.line col = definition.column _logger().debug("Go to %s" % definition) self._runner.request_job( TextHelper(self.editor).goto_line, line, move=True, column=col) else: _logger().debug("Out of doc: %s" % definition) self.out_of_doc.emit(definition) def _unique(self, seq): """ Not performant but works. """ # order preserving checked = [] for e in seq: present = False for c in checked: if str(c) == str(e): present = True break if not present: checked.append(e) return checked def _clear_selection(self): super(GoToAssignmentsMode, self)._clear_selection() self._definitions[:] = [] def _validate_definitions(self, definitions): if definitions: if len(definitions) == 1: return definitions[0].line is not None return True return False def _on_results_available(self, definitions): _logger().debug("Got %r" % definitions) definitions = [Assignment(path, line, col, full_name) for path, line, col, full_name in definitions] definitions = self._unique(definitions) self._definitions = definitions if self._validate_definitions(definitions): if self._goto_requested: self._perform_goto(definitions) else: self._select_word_cursor() self.editor.set_mouse_cursor(QtCore.Qt.PointingHandCursor) else: self._clear_selection() self.editor.set_mouse_cursor(QtCore.Qt.IBeamCursor) self._goto_requested = False def _perform_goto(self, definitions): if len(definitions) == 1: definition = definitions[0] if definition: self._goto(definition) elif len(definitions) > 1: _logger().debug( "More than 1 assignments in different modules, user " "need to make a choice: %s" % definitions) def_str, result = QtWidgets.QInputDialog.getItem( self.editor, "Choose a definition", "Choose the definition you want to go to:", [str(d) for d in definitions]) if result: for definition in definitions: if definition and str(definition) == def_str: self._goto(definition) break def _on_word_clicked(self): self._perform_goto(self._definitions)
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 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)
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 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
def __init__(self, parent): QtWidgets.QTabBar.__init__(self, parent) self.setTabsClosable(True) self._timer = DelayJobRunner(delay=1)
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 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 GoToAssignmentsMode(WordClickMode): """ Goes to the assignments (using jedi.Script.goto_assignments) when the user execute the shortcut or click word. If there are more than one assignments, an input dialog is used to ask the user to choose the desired assignment. This mode will emit the :attr:`out_of_doc` signal if the definition can not be reached in the current document. IDE will typically connects a slot that open a new editor tab and goes to the definition position. """ #: Signal emitted when the definition cannot be reached in the current #: document out_of_doc = QtCore.Signal(Assignment) #: Signal emitted when no results could be found. no_results_found = QtCore.Signal() shortcut = 'Alt+F2' def __init__(self): super(GoToAssignmentsMode, self).__init__() self._definitions = [] self._goto_requested = False self.action_goto = QtWidgets.QAction("Go to assignments", self) self.action_goto.setShortcut(self.shortcut) self.action_goto.triggered.connect(self.request_goto) icon = icons.icon(qta_name='fa.share') if icon: self.action_goto.setIcon(icon) self.word_clicked.connect(self._on_word_clicked) self._runner = DelayJobRunner(delay=1) def on_state_changed(self, state): super(GoToAssignmentsMode, self).on_state_changed(state) if state: self.editor.add_action(self.action_goto) else: self.editor.remove_action(self.action_goto) def request_goto(self): """ Request a goto action for the word under the text cursor. """ self._goto_requested = True self._check_word_cursor() def _check_word_cursor(self, tc=None): """ Request a go to assignment. :param tc: Text cursor which contains the text that we must look for its assignment. Can be None to go to the text that is under the text cursor. :type tc: QtGui.QTextCursor """ if not tc: tc = TextHelper(self.editor).word_under_cursor() request_data = { 'code': self.editor.toPlainText(), 'line': tc.blockNumber(), 'column': tc.columnNumber(), 'path': self.editor.file.path, 'encoding': self.editor.file.encoding } try: self.editor.backend.send_request( workers.goto_assignments, request_data, on_receive=self._on_results_available) except NotRunning: pass def _goto(self, definition): fp = '' if self.editor.file.path: fp = os.path.normpath(self.editor.file.path.replace(".pyc", ".py")) if definition.module_path == fp: line = definition.line col = definition.column _logger().debug("Go to %s" % definition) self._runner.request_job( TextHelper(self.editor).goto_line, line, move=True, column=col) else: _logger().debug("Out of doc: %s" % definition) self.out_of_doc.emit(definition) def _unique(self, seq): """ Not performant but works. """ # order preserving checked = [] for e in seq: present = False for c in checked: if str(c) == str(e): present = True break if not present: checked.append(e) return checked def _clear_selection(self): super(GoToAssignmentsMode, self)._clear_selection() self._definitions[:] = [] def _validate_definitions(self, definitions): if definitions: if len(definitions) == 1: return definitions[0].line is not None return True return False def _on_results_available(self, definitions): _logger().debug("Got %r" % definitions) definitions = [Assignment(path, line, col, full_name) for path, line, col, full_name in definitions] definitions = self._unique(definitions) self._definitions = definitions if self._validate_definitions(definitions): if self._goto_requested: self._perform_goto(definitions) else: self._select_word_cursor() self.editor.set_mouse_cursor(QtCore.Qt.PointingHandCursor) else: self._clear_selection() self.editor.set_mouse_cursor(QtCore.Qt.IBeamCursor) self._goto_requested = False def _perform_goto(self, definitions): if len(definitions) == 1: definition = definitions[0] if definition: self._goto(definition) elif len(definitions) > 1: _logger().debug( "More than 1 assignments in different modules, user " "need to make a choice: %s" % definitions) def_str, result = QtWidgets.QInputDialog.getItem( self.editor, "Choose a definition", "Choose the definition you want to go to:", [str(d) for d in definitions]) if result: for definition in definitions: if definition and str(definition) == def_str: self._goto(definition) break def _on_word_clicked(self): self._perform_goto(self._definitions)
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 GoToDefinitionMode(Mode, QObject): """ Go to the definition of the symbol under the word cursor. """ #: Signal emitted when a word is clicked. The parameter is a #: QTextCursor with the clicked word set as the selected text. word_clicked = Signal(QTextCursor) def __init__(self): QObject.__init__(self) Mode.__init__(self) self._previous_cursor_start = -1 self._previous_cursor_end = -1 self._definition = None self._deco = None self._pending = False self.action_goto = QAction(_("Go to assignments"), self) self.action_goto.setShortcut('F7') self.action_goto.triggered.connect(self.request_goto) self.word_clicked.connect(self.request_goto) self._timer = DelayJobRunner(delay=200) def on_state_changed(self, state): """ Connects/disconnects slots to/from signals when the mode state changed. """ super(GoToDefinitionMode, self).on_state_changed(state) if state: self.editor.mouse_moved.connect(self._on_mouse_moved) self.editor.mouse_released.connect(self._on_mouse_released) self.editor.add_action(self.action_goto, sub_menu='COBOL') self.editor.mouse_double_clicked.connect( self._timer.cancel_requests) else: self.editor.mouse_moved.disconnect(self._on_mouse_moved) self.editor.mouse_released.disconnect(self._on_mouse_released) self.editor.remove_action(self.action_goto, sub_menu='Python') self.editor.mouse_double_clicked.disconnect( self._timer.cancel_requests) def _select_word_under_mouse_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 _on_mouse_moved(self, event): """ mouse moved callback """ if event.modifiers() & Qt.ControlModifier: self._select_word_under_mouse_cursor() else: self._remove_decoration() self.editor.set_mouse_cursor(Qt.IBeamCursor) self._previous_cursor_start = -1 self._previous_cursor_end = -1 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 find_definition(self, symbol, definition): if symbol.lower() == definition.name.lower().replace( " section", "").replace(" division", ""): return definition for ch in definition.children: d = self.find_definition(symbol, ch) if d is not None: return d return None def select_word(self, cursor): symbol = cursor.selectedText() analyser = self.editor.outline_mode for definition in analyser.definitions: node = self.find_definition(symbol, definition) if node is not None: break else: node = None self._definition = None if node and node.line != cursor.block().blockNumber(): self._definition = node if self._deco is None: if cursor.selectedText(): self._deco = TextDecoration(cursor) self._deco.set_foreground(Qt.blue) self._deco.set_as_underlined() self.editor.decorations.append(self._deco) return True return False def _add_decoration(self, cursor): """ Adds a decoration for the word under ``cursor``. """ if self.select_word(cursor): self.editor.set_mouse_cursor(Qt.PointingHandCursor) else: self.editor.set_mouse_cursor(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 def request_goto(self, tc=None): """ Request a go to assignment. :param tc: Text cursor which contains the text that we must look for its assignment. Can be None to go to the text that is under the text cursor. :type tc: QtGui.QTextCursor """ if not tc: tc = TextHelper( self.editor).word_under_cursor(select_whole_word=True) if not self._definition or isinstance(self.sender(), QAction): self.select_word(tc) if self._definition is not None: QTimer.singleShot(100, self._goto_def) def _goto_def(self): if self._definition: line = self._definition.line col = self._definition.column TextHelper(self.editor).goto_line(line, move=True, column=col)
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 GoToDefinitionMode(Mode, QObject): """ Go to the definition of the symbol under the word cursor. """ #: Signal emitted when a word is clicked. The parameter is a #: QTextCursor with the clicked word set as the selected text. word_clicked = Signal(QTextCursor) def __init__(self): QObject.__init__(self) Mode.__init__(self) self._previous_cursor_start = -1 self._previous_cursor_end = -1 self._definition = None self._deco = None self._pending = False self.action_goto = QAction(_("Go to assignments"), self) self.action_goto.setShortcut('F7') self.action_goto.triggered.connect(self.request_goto) self.word_clicked.connect(self.request_goto) self._timer = DelayJobRunner(delay=200) def on_state_changed(self, state): """ Connects/disconnects slots to/from signals when the mode state changed. """ super(GoToDefinitionMode, self).on_state_changed(state) if state: self.editor.mouse_moved.connect(self._on_mouse_moved) self.editor.mouse_pressed.connect(self._on_mouse_pressed) self.editor.add_action(self.action_goto, sub_menu='COBOL') self.editor.mouse_double_clicked.connect( self._timer.cancel_requests) else: self.editor.mouse_moved.disconnect(self._on_mouse_moved) self.editor.mouse_pressed.disconnect(self._on_mouse_pressed) self.editor.remove_action(self.action_goto, sub_menu='Python') self.editor.mouse_double_clicked.disconnect( self._timer.cancel_requests) def _select_word_under_mouse_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 _on_mouse_moved(self, event): """ mouse moved callback """ if event.modifiers() & Qt.ControlModifier: self._select_word_under_mouse_cursor() else: self._remove_decoration() self.editor.set_mouse_cursor(Qt.IBeamCursor) self._previous_cursor_start = -1 self._previous_cursor_end = -1 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 find_definition(self, symbol, definition): if symbol in TextHelper(self.editor).line_text(definition.line): return definition for ch in definition.children: d = self.find_definition(symbol, ch) if d is not None: return d return None def select_word(self, cursor): symbol = cursor.selectedText() analyser = self.editor.outline_mode for definition in analyser.definitions: node = self.find_definition(symbol, definition) if node is not None: break else: node = None self._definition = None if node and node.line != cursor.block().blockNumber(): self._definition = node if self._deco is None: if cursor.selectedText(): self._deco = TextDecoration(cursor) self._deco.set_foreground(Qt.blue) self._deco.set_as_underlined() self.editor.decorations.append(self._deco) return True return False def _add_decoration(self, cursor): """ Adds a decoration for the word under ``cursor``. """ if self.select_word(cursor): self.editor.set_mouse_cursor(Qt.PointingHandCursor) else: self.editor.set_mouse_cursor(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 def request_goto(self, tc=None): """ Request a go to assignment. :param tc: Text cursor which contains the text that we must look for its assignment. Can be None to go to the text that is under the text cursor. :type tc: QtGui.QTextCursor """ print('request goto') if not tc: tc = TextHelper(self.editor).word_under_cursor( select_whole_word=True) if not self._definition or isinstance(self.sender(), QAction): self.select_word(tc) if self._definition is not None: QTimer.singleShot(100, self._goto_def) def _goto_def(self): line = self._definition.line col = self._definition.column TextHelper(self.editor).goto_line(line, move=True, column=col)
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 LocatorWidget(QtWidgets.QFrame): """ Popup widget that let the user quickly locate a file in a project. The user can also locate a symbol in the current editor with the ``@`` operator or a symbol in the whole project with the ``#`` operator. The ``:`` operator can be used to specify the line number to go to. Note that all those operators are exclusive, you cannot mix ``@`` with ``:`` or with ``#``. """ activated = QtCore.pyqtSignal(str, int) cancelled = QtCore.pyqtSignal() GOTO_LINE_PATTERN = re.compile(r'^.*:.*') GOTO_SYMBOL_PATTERN = re.compile('^@.*') GOTO_SYMBOL_IN_PROJ_PATTERN = re.compile('^!.*') MODE_GOTO_FILE = 0 MODE_GOTO_SYMBOL = 1 MODE_GOTO_SYMBOL_IN_PROJECT = 2 MODE_GOTO_LINE = 3 def __init__(self, window=None): super().__init__() self._query_thread = None self.icon_provider = widgets.FileIconProvider() self._runner = DelayJobRunner(delay=300) self.main_window = window self.ui = locator_ui.Ui_Frame() self.ui.setupUi(self) self.ui.lineEdit.textChanged.connect(self.request_search) self.ui.lineEdit.prompt_text = _('Type to locate...') self.ui.lineEdit.installEventFilter(self) self.ui.treeWidget.installEventFilter(self) self.ui.treeWidget.setItemDelegate(HTMLDelegate()) self.setWindowFlags(QtCore.Qt.Popup) self.ui.lineEdit.setFocus(True) self.ui.bt_close.clicked.connect(self.hide) self.ui.bt_infos.clicked.connect(self._show_help) self.mode = self.MODE_GOTO_FILE self.ui.treeWidget.currentItemChanged.connect( self._on_current_item_changed) self.ui.treeWidget.itemDoubleClicked.connect(self._activate) self.ui.cb_non_project_files.toggled.connect(self._search) def showEvent(self, ev): self._activated = False self.ui.lineEdit.clear() if self.mode == self.MODE_GOTO_FILE: self.search_files() self.ui.cb_non_project_files.setVisible(True) elif self.mode == self.MODE_GOTO_SYMBOL: self.ui.lineEdit.setText('@') self.search_symbol() self.ui.cb_non_project_files.setVisible(False) elif self.mode == self.MODE_GOTO_SYMBOL_IN_PROJECT: self.ui.lineEdit.setText('!') self.search_symbol_in_project() self.ui.cb_non_project_files.setVisible(True) elif self.mode == self.MODE_GOTO_LINE: self.ui.lineEdit.setText(':') self.ui.treeWidget.hide() self.adjustSize() self.ui.cb_non_project_files.setVisible(False) self.ui.lineEdit.setFocus() super().showEvent(ev) def closeEvent(self, ev): if not self._activated: self.cancelled.emit() if self._query_thread: self._query_thread.quit() self._on_query_finished() super().closeEvent(ev) def eventFilter(self, obj, ev): if obj == self.ui.lineEdit and ev.type() == QtCore.QEvent.KeyPress: if ev.key() == QtCore.Qt.Key_Down: next_item = self.ui.treeWidget.itemBelow( self.ui.treeWidget.currentItem()) if next_item is None: next_item = self.ui.treeWidget.topLevelItem(0) self.ui.treeWidget.setCurrentItem(next_item) return True elif ev.key() == QtCore.Qt.Key_Up: next_item = self.ui.treeWidget.itemAbove( self.ui.treeWidget.currentItem()) if next_item is None: next_item = self.ui.treeWidget.topLevelItem( self.ui.treeWidget.topLevelItemCount() - 1) self.ui.treeWidget.setCurrentItem(next_item) return True if ev.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter] and \ ev.modifiers() & QtCore.Qt.ShiftModifier: self.ui.cb_non_project_files.setChecked( not self.ui.cb_non_project_files.isChecked()) self.ui.lineEdit.setFocus(True) return True elif ev.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]: self._activate() return True if ev.key() in [QtCore.Qt.Key_Tab, QtCore.Qt.Key_Backtab]: # tab should not have any effect return True elif obj == self.ui.treeWidget and ev.type() == QtCore.QEvent.KeyPress\ and ev.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]: self._activate() return True return super().eventFilter(obj, ev) def _activate(self, *_): current_item = self.ui.treeWidget.currentItem() if self.ui.treeWidget.isVisible() and current_item: data = current_item.data(0, QtCore.Qt.UserRole) else: data = self.main_window.current_tab.file.path if isinstance(data, str): text = self.ui.lineEdit.text() if self.GOTO_LINE_PATTERN.match(text): line = self._get_requested_line_nbr() else: line = -1 self.activated.emit(data, line) elif isinstance(data, tuple): line, path = data self.activated.emit(path, line) self._activated = True self.close() def request_search(self): self._runner.request_job(self._search) def _search(self): text = self.ui.lineEdit.text() # check the source to use for search (symbols or files). if self.GOTO_SYMBOL_PATTERN.match(text): self.ui.cb_non_project_files.setVisible(False) if self.GOTO_LINE_PATTERN.match(text): self.ui.treeWidget.hide() self.adjustSize() elif editor.get_current_editor(): self.search_symbol() elif self.GOTO_SYMBOL_IN_PROJ_PATTERN.match(text): if self.GOTO_LINE_PATTERN.match(text): self.ui.cb_non_project_files.setVisible(False) self.ui.treeWidget.hide() self.adjustSize() else: self.ui.cb_non_project_files.setVisible(True) self.search_symbol_in_project() else: if not text.startswith(':'): self.ui.cb_non_project_files.setVisible(True) self.search_files() else: self.ui.cb_non_project_files.setVisible(False) # will be used to goto line in the current editor. self.ui.treeWidget.hide() self.adjustSize() e = editor.get_current_editor() try: TextHelper(e).goto_line(self._get_requested_line_nbr() - 1) except (ValueError, AttributeError): _logger().debug('failed to go to line on editor %r', e) def _get_projects(self): if self.ui.cb_non_project_files.isChecked(): return None # all active project return project.get_projects() def search_symbol(self): search_term = self._get_search_term() fpath = editor.get_current_editor().file.path self._request_exec_query(index.get_symbols, self._display_search_symbol_results, name_filter=search_term, file=fpath) def _display_search_symbol_results(self, symbols): self.ui.treeWidget.clear() search_term = self._get_search_term() first_item = None for symbol_item, file_item in symbols: name = symbol_item.name line = symbol_item.line + 1 path = file_item.path enriched = self.get_enriched_text(name, search_term) if enriched == name and search_term: continue text = '%s<br><i>%s:%d</i>' % (enriched, path, line) item = QtWidgets.QTreeWidgetItem() item.setText(0, text) item.setIcon(0, self.icon_provider.icon(path)) item.setData(0, QtCore.Qt.UserRole, (line, path)) if first_item is None: first_item = item self.ui.treeWidget.addTopLevelItem(item) if self.ui.treeWidget.topLevelItemCount(): self.ui.treeWidget.show() self.ui.treeWidget.setCurrentItem(first_item) else: item = QtWidgets.QTreeWidgetItem() item.setText(0, _('No match found')) item.setIcon(0, QtGui.QIcon.fromTheme('dialog-warning')) self.ui.treeWidget.addTopLevelItem(item) self.adjustSize() def search_symbol_in_project(self): search_term = self._get_search_term() self._request_exec_query( index.get_symbols, self._display_search_symbol_in_project_results, name_filter=search_term, projects=self._get_projects()) def _display_search_symbol_in_project_results(self, symbols): search_term = self._get_search_term() # display self.ui.treeWidget.clear() first_item = None for symbol_item, file_item in symbols: name = symbol_item.name line = symbol_item.line + 1 path = file_item.path enriched = self.get_enriched_text(name, search_term) if enriched == name and search_term: continue text = '%s<br><i>%s:%d</i>' % (enriched, path, line) item = QtWidgets.QTreeWidgetItem() item.setText(0, text) item.setIcon(0, self.icon_provider.icon(path)) item.setData(0, QtCore.Qt.UserRole, (line, path)) if first_item is None: first_item = item self.ui.treeWidget.addTopLevelItem(item) if self.ui.treeWidget.topLevelItemCount(): self.ui.treeWidget.show() self.ui.treeWidget.setCurrentItem(first_item) else: item = QtWidgets.QTreeWidgetItem() item.setText(0, _('No match found')) item.setIcon(0, QtGui.QIcon.fromTheme('dialog-warning')) self.ui.treeWidget.addTopLevelItem(item) self.adjustSize() def get_definition_icon(self, icon): if isinstance(icon, list): icon = tuple(icon) return self._get_definition_icon(icon) @utils.memoized @staticmethod def _get_definition_icon(icon): if isinstance(icon, tuple): icon = QtGui.QIcon.fromTheme(icon[0], QtGui.QIcon(icon[1])) elif isinstance(icon, str): if QtGui.QIcon.hasThemeIcon(icon): icon = QtGui.QIcon.fromTheme(icon) else: icon = QtGui.QIcon(icon) return icon def _request_exec_query(self, query_fct, callback, **kwargs): class QueryThread(QtCore.QThread): results_available = QtCore.pyqtSignal(object) def __init__(self, query_fct, **kwargs): super().__init__() self.query_fct = query_fct self.kwargs = kwargs def run(self): generator = query_fct(**kwargs) ret = [] try: for i, value in enumerate(generator): if query_fct == index.get_files: if not os.path.exists(value.path): continue elif query_fct == index.get_symbols: s, f = value if not os.path.exists(f.path): continue ret.append(value) if i > LIMIT: break except sqlite3.OperationalError: _logger().exception('failed to execute sql query') ret = [] self.results_available.emit(ret) if self._query_thread is None: self._query_thread = QueryThread(query_fct, **kwargs) self._query_thread.setParent(self) self._query_thread.results_available.connect(callback) self._query_thread.finished.connect(self._on_query_finished) self._query_thread.start() def _on_query_finished(self): self._query_thread = None def search_files(self): name_filter = self._get_search_term() self._request_exec_query(index.get_files, self._display_search_results, name_filter=name_filter, projects=self._get_projects()) def _display_search_results(self, project_files): name_filter = self._get_search_term() self.ui.treeWidget.clear() first_item = None for file_item in project_files: path = file_item.path name = file_item.name enriched = self.get_enriched_text(name, name_filter) if enriched == name and name_filter: continue text = '%s<br><i>%s</i>' % (enriched, os.path.dirname(path)) item = QtWidgets.QTreeWidgetItem() item.setText(0, text) item.setIcon(0, self.icon_provider.icon(path)) item.setData(0, QtCore.Qt.UserRole, path) if first_item is None: first_item = item self.ui.treeWidget.addTopLevelItem(item) if self.ui.treeWidget.topLevelItemCount(): self.ui.treeWidget.show() self.ui.treeWidget.setCurrentItem(first_item) else: item = QtWidgets.QTreeWidgetItem() item.setText(0, _('No match found')) item.setIcon(0, QtGui.QIcon.fromTheme('dialog-information')) self.ui.treeWidget.addTopLevelItem(item) self.adjustSize() @staticmethod def get_match_spans(expr, item): spans = [] item = item.lower() search_expr = DbHelper._get_searchable_name(expr) for token in get_search_tokens(search_expr): if not token: continue try: start = item.index(token) length = len(token) spans.append((start, length)) to_replace = item[start:length] item = item.replace(to_replace, len(to_replace) * '*') except ValueError: pass return spans @staticmethod def get_enriched_text(item, expr): spans = LocatorWidget.get_match_spans(expr, item) offset = 0 enriched = '' for start, end in sorted(spans, key=lambda x: x[0]): enriched += item[offset:start] + '<b>' + \ item[start:start + end] + '</b>' offset = start + end enriched += item[offset:] return enriched def _get_search_term(self): text = self.ui.lineEdit.text() if text.startswith('@'): text = text[1:] elif text.startswith('!'): text = text[1:] if self.GOTO_LINE_PATTERN.match(text): text = text.split(':')[0] return text.strip().replace("'", '').replace('"', '').replace('*', '') def _get_requested_line_nbr(self): text = self.ui.lineEdit.text() try: return int(text.split(':')[1]) except (IndexError, ValueError): return -1 def _show_help(self): help_text = _('''<p>Use <i>Goto</i> to navigate your project’s files swiftly.</p> <p>Use the <i>arrow keys</i> to navigate into the list and press <i>ENTER</i> to open the selected entry. Press <i>ESCAPE</i> to close the popup window.<p> <p><i>Goto</i> accepts several operators: <ul> <li> <b>@</b> to locate a symbol in the current editor.</li> <li> <b>!</b> to locate a symbol in the opened project(s).</li> <li> <b>:</b> to specify the line number to go to.</li> </ul> </p> ''') QtWidgets.QMessageBox.information(self, _('Goto: help'), help_text) self.show() def _on_current_item_changed(self, item): text = self.ui.lineEdit.text() if item and self.GOTO_SYMBOL_PATTERN.match(text): line, path = item.data(0, QtCore.Qt.UserRole) TextHelper(editor.get_current_editor()).goto_line(line - 1)
def __init__(self, parent=None): super(HtmlPreviewWidget, self).__init__(parent) self._editor = None self._timer = DelayJobRunner(delay=1000)
class DocumentAnalyserMode(Mode, QtCore.QObject): """ 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.blockCountChanged.connect(self._on_line_count_changed) self.editor.new_text_set.connect(self._run_analysis) else: self.editor.blockCountChanged.disconnect( self._on_line_count_changed) self.editor.new_text_set.disconnect(self._run_analysis) def _on_line_count_changed(self, e): self._jobRunner.request_job(self._run_analysis) def _run_analysis(self): if 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 NotConnected: QtCore.QTimer.singleShot(100, self._run_analysis) else: self.results = [] self.document_changed.emit() def _on_results_available(self, status, 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