Ejemplo n.º 1
0
class MenuRecentFiles(QtWidgets.QMenu):
    """
    Menu that manage the list of recent files.

    To use the menu, simply connect to the open_requested signal.

    """
    #: Signal emitted when the user clicked on a recent file action.
    #: The parameter is the path of the file to open.
    open_requested = QtCore.Signal(str)
    clear_requested = QtCore.Signal()

    def __init__(self,
                 parent,
                 recent_files_manager=None,
                 title='Recent files',
                 icon_provider=None,
                 clear_icon=None):
        """
        :param organisation: name of your organisation as used for your own
                             QSettings
        :param application: name of your application as used for your own
                            QSettings
        :param parent: parent object

        :param icon_provider: Object that provides icon based on the file path.
        :type icon_provider: QtWidgets.QFileIconProvider

        :param clear_icon: Clear action icon. This parameter is a tuple made up
            of the icon theme name and the fallback icon path (from your
            resources). Default is None, clear action has no icons.
        """
        super(MenuRecentFiles, self).__init__(title, parent)
        if icon_provider is None:
            self.icon_provider = QtWidgets.QFileIconProvider()
        else:
            self.icon_provider = icon_provider
        self.clear_icon = clear_icon
        #: Recent files manager
        self.manager = recent_files_manager
        #: List of recent files actions
        self.recent_files_actions = []
        self.update_actions()

    def update_actions(self):
        """
        Updates the list of actions.
        """
        self.clear()
        self.recent_files_actions[:] = []
        for file in self.manager.get_recent_files():
            action = QtWidgets.QAction(self)
            action.setText(os.path.split(file)[1])
            action.setToolTip(file)
            action.setStatusTip(file)
            action.setData(file)
            action.setIcon(self.icon_provider.icon(QtCore.QFileInfo(file)))
            action.triggered.connect(self._on_action_triggered)
            self.addAction(action)
            self.recent_files_actions.append(action)
        self.addSeparator()
        action_clear = QtWidgets.QAction(_('Clear list'), self)
        action_clear.triggered.connect(self.clear_recent_files)
        if isinstance(self.clear_icon, QtGui.QIcon):
            action_clear.setIcon(self.clear_icon)
        elif self.clear_icon:
            theme = ''
            if len(self.clear_icon) == 2:
                theme, path = self.clear_icon
            else:
                path = self.clear_icon
            icons.icon(theme, path, 'fa.times-circle')
        self.addAction(action_clear)

    def clear_recent_files(self):
        """ Clear recent files and menu. """
        self.manager.clear()
        self.update_actions()
        self.clear_requested.emit()

    def _on_action_triggered(self):
        """
        Emits open_requested when a recent file action has been triggered.
        """
        action = self.sender()
        assert isinstance(action, QtWidgets.QAction)
        path = action.data()
        self.open_requested.emit(path)
        self.update_actions()
Ejemplo n.º 2
0
class SearchAndReplacePanel(Panel, Ui_SearchPanel):
    """ Lets the user search and replace text in the current document.

    It uses the backend API to search for some text. Search operation is
    performed in a background process (the backend process)..

    The search panel can also be used programatically. To do that, the client
    code must first requests a search using :meth:`requestSearch` and connects
    to :attr:`search_finished`. The results of the search can then be
    retrieved using :attr:`cpt_occurences` and :meth:`get_oOccurrences`. The
    client code may now navigate through occurrences using :meth:`select_next`
    or :meth:`select_previous`, or replace the occurrences with a specific
    text using :meth:`replace` or :meth:`replace_all`.
    """
    STYLESHEET = """SearchAndReplacePanel
    {
        background-color: %(bck)s;
        color: %(color)s;
    }

    QtoolButton
    {
        color: %(color)s;
        background-color: transparent;
        padding: 5px;
        min-height: 24px;
        min-width: 24px;
        border: none;
    }

    QtoolButton:hover
    {
        background-color: %(highlight)s;
        border: none;
        border-radius: 5px;
        color: %(color)s;
    }

    QtoolButton:pressed, QCheckBox:pressed
    {
        border: 1px solid %(bck)s;
    }

    QtoolButton:disabled
    {
        color: %(highlight)s;
    }

    QCheckBox
    {
        padding: 4px;
        color: %(color)s;
    }

    QCheckBox:hover
    {
            background-color: %(highlight)s;
            color: %(color)s;
            border-radius: 5px;
    }
    """
    _KEYS = [
        "panelBackground", "background", "panelForeground", "panelHighlight"
    ]

    #: Signal emitted when a search operation finished
    search_finished = QtCore.Signal()

    #: Define the maximum number of occurences that can be highlighted
    #: in the document.
    #:
    #: .. note:: The search operation itself is fast but the creation of all
    #:    the extra selection used to highlight search result can be slow.
    MAX_HIGHLIGHTED_OCCURENCES = 500

    @property
    def background(self):
        """ Text decoration background """
        return self._bg

    @background.setter
    def background(self, value):
        self._bg = value
        self._refresh_decorations()
        # propagate changes to every clone
        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):
        """ Text decoration foreground """
        return self._fg

    @foreground.setter
    def foreground(self, value):
        self._fg = value
        self._refresh_decorations()
        # propagate changes to every clone
        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

    def __init__(self):
        Panel.__init__(self, dynamic=True)
        self.job_runner = DelayJobRunner(delay=500)
        Ui_SearchPanel.__init__(self)
        self.setupUi(self)
        self.lineEditReplace.prompt_text = ' Replace'
        #: Occurrences counter
        self.cpt_occurences = 0
        self._previous_stylesheet = ""
        self._separator = None
        self._decorations = []
        self._occurrences = []
        self._current_occurrence_index = 0
        self._bg = None
        self._fg = None
        self._update_buttons(txt="")
        self.lineEditSearch.installEventFilter(self)
        self.lineEditReplace.installEventFilter(self)
        self._init_actions()
        self._init_style()
        self.checkBoxRegex.stateChanged.connect(
            self.checkBoxWholeWords.setDisabled)

    def _init_actions(self):
        icon_size = QtCore.QSize(16, 16)

        icon = icons.icon('edit-find', ':/pyqode-icons/rc/edit-find.png',
                          'fa.search')
        self.actionSearch.setIcon(icon)
        self.actionSearch.setShortcut('Ctrl+F')
        self.labelSearch.setPixmap(icon.pixmap(icon_size))

        icon = icons.icon('edit-find-replace',
                          ':/pyqode-icons/rc/edit-find-replace.png',
                          'fa.search-plus')
        self.actionActionSearchAndReplace.setShortcut('Ctrl+H')
        self.actionActionSearchAndReplace.setIcon(icon)
        self.labelReplace.setPixmap(icon.pixmap(icon_size))

        icon = icons.icon('go-up', ':/pyqode-icons/rc/go-up.png',
                          'fa.arrow-up')
        self.actionFindPrevious.setShortcut('Shift+F3')
        self.actionFindPrevious.setIcon(icon)
        self.toolButtonPrevious.setIcon(icon)
        self.toolButtonPrevious.setIconSize(icon_size)

        icon = icons.icon('go-down', ':/pyqode-icons/rc/go-down.png',
                          'fa.arrow-down')
        self.actionFindNext.setShortcut('F3')
        self.actionFindNext.setIcon(icon)
        self.toolButtonNext.setIcon(icon)
        self.toolButtonNext.setIconSize(icon_size)

        icon = icons.icon('window-close', ':/pyqode-icons/rc/close.png',
                          'fa.close')
        self.toolButtonClose.setIcon(icon)
        self.toolButtonClose.setIconSize(icon_size)

        self.menu = QtWidgets.QMenu(self.editor)
        self.menu.setTitle('Search')
        self.menu.menuAction().setIcon(self.actionSearch.icon())
        self.menu.addAction(self.actionSearch)
        self.actionSearch.setShortcutContext(QtCore.Qt.WidgetShortcut)
        self.menu.addAction(self.actionActionSearchAndReplace)
        self.actionActionSearchAndReplace.setShortcutContext(
            QtCore.Qt.WidgetShortcut)
        self.menu.addAction(self.actionFindNext)
        self.actionFindNext.setShortcutContext(QtCore.Qt.WidgetShortcut)
        self.menu.addAction(self.actionFindPrevious)
        self.actionFindPrevious.setShortcutContext(QtCore.Qt.WidgetShortcut)

    def _init_style(self):
        self._bg = QtGui.QColor('yellow')
        self._outline = QtGui.QPen(QtGui.QColor('gray'), 1)

    def on_install(self, editor):
        super(SearchAndReplacePanel, self).on_install(editor)
        self.hide()
        self.text_helper = TextHelper(editor)

    def _refresh_decorations(self):
        for deco in self._decorations:
            self.editor.decorations.remove(deco)
            deco.set_background(QtGui.QBrush(self.background))
            deco.set_outline(self._outline)
            self.editor.decorations.append(deco)

    def on_state_changed(self, state):
        super(SearchAndReplacePanel, self).on_state_changed(state)
        if state:
            # menu
            self.editor.add_action(self.menu.menuAction())
            # requestSearch slot
            self.editor.textChanged.connect(self.request_search)
            self.lineEditSearch.textChanged.connect(self.request_search)
            self.checkBoxCase.stateChanged.connect(self.request_search)
            self.checkBoxWholeWords.stateChanged.connect(self.request_search)
            self.checkBoxRegex.stateChanged.connect(self.request_search)
            self.checkBoxInSelection.stateChanged.connect(self.request_search)
            # navigation slots
            self.toolButtonNext.clicked.connect(self.select_next)
            self.actionFindNext.triggered.connect(self.select_next)
            self.toolButtonPrevious.clicked.connect(self.select_previous)
            self.actionFindPrevious.triggered.connect(self.select_previous)
            # replace slots
            self.toolButtonReplace.clicked.connect(self.replace)
            self.toolButtonReplaceAll.clicked.connect(self.replace_all)
            # internal updates slots
            self.lineEditReplace.textChanged.connect(self._update_buttons)
            self.search_finished.connect(self._on_search_finished)
        else:
            self.editor.remove_action(self.menu.menuAction())
            # requestSearch slot
            self.editor.textChanged.disconnect(self.request_search)
            self.lineEditSearch.textChanged.disconnect(self.request_search)
            self.checkBoxCase.stateChanged.disconnect(self.request_search)
            self.checkBoxWholeWords.stateChanged.disconnect(
                self.request_search)
            self.checkBoxRegex.stateChanged.disconnect(self.request_search)
            self.checkBoxInSelection.stateChanged.disconnect(
                self.request_search)
            # navigation slots
            self.toolButtonNext.clicked.disconnect(self.select_next)
            self.actionFindNext.triggered.disconnect(self.select_next)
            self.toolButtonPrevious.clicked.disconnect(self.select_previous)
            # replace slots
            self.toolButtonReplace.clicked.disconnect(self.replace)
            self.toolButtonReplaceAll.clicked.disconnect(self.replace_all)
            # internal updates slots
            self.lineEditReplace.textChanged.disconnect(self._update_buttons)
            self.search_finished.disconnect(self._on_search_finished)

    def close_panel(self):
        """
        Closes the panel
        """
        self.hide()
        self.lineEditReplace.clear()
        self.lineEditSearch.clear()

    @QtCore.Slot()
    def on_toolButtonClose_clicked(self):
        self.close_panel()

    @QtCore.Slot()
    def on_actionSearch_triggered(self):
        self.widgetSearch.show()
        self.widgetReplace.hide()
        self.show()
        new_text = self.text_helper.selected_text()
        old_text = self.lineEditSearch.text()
        text_changed = new_text != old_text
        self.lineEditSearch.setText(new_text)
        self.lineEditSearch.selectAll()
        self.lineEditSearch.setFocus()
        self.setFocusPolicy(QtCore.Qt.ClickFocus)
        if not text_changed:
            self.request_search(new_text)

    @QtCore.Slot()
    def on_actionActionSearchAndReplace_triggered(self):
        self.widgetSearch.show()
        self.widgetReplace.show()
        self.show()
        new_txt = self.text_helper.selected_text()
        old_txt = self.lineEditSearch.text()
        txt_changed = new_txt != old_txt
        self.lineEditSearch.setText(new_txt)
        self.lineEditReplace.clear()
        self.lineEditReplace.setFocus()
        if not txt_changed:
            self.request_search(new_txt)

    def focusOutEvent(self, event):
        self.job_runner.cancel_requests()
        Panel.focusOutEvent(self, event)

    def request_search(self, txt=None):
        """
        Requests a search operation.

        :param txt: The text to replace. If None, the content of lineEditSearch
                    is used instead.
        """
        if self.checkBoxRegex.isChecked():
            try:
                re.compile(self.lineEditSearch.text(), re.DOTALL)
            except sre_constants.error as e:
                self._show_error(e)
                return
            else:
                self._show_error(None)

        if txt is None or isinstance(txt, int):
            txt = self.lineEditSearch.text()
        if txt:
            self.job_runner.request_job(self._exec_search, txt,
                                        self._search_flags())
        else:
            self.job_runner.cancel_requests()
            self._clear_occurrences()
            self._on_search_finished()

    @staticmethod
    def _set_widget_background_color(widget, color):
        """
        Changes the base color of a widget (background).
        :param widget: widget to modify
        :param color: the color to apply
        """
        pal = widget.palette()
        pal.setColor(pal.Base, color)
        widget.setPalette(pal)

    def _show_error(self, error):
        if error:
            self._set_widget_background_color(self.lineEditSearch,
                                              QtGui.QColor('#FFCCCC'))
            self.lineEditSearch.setToolTip(str(error))
        else:
            self._set_widget_background_color(
                self.lineEditSearch,
                self.palette().color(self.palette().Base))
            self.lineEditSearch.setToolTip('')

    def get_occurences(self):
        """
        Returns the list of text occurrences.

        An occurrence is a tuple that contains start and end positions.

        :return: List of tuple(int, int)
        """
        return self._occurrences

    def select_next(self):
        """
        Selects the next occurrence.

        :return: True in case of success, false if no occurrence could be
                 selected.
        """
        current_occurence = self._current_occurrence()
        occurrences = self.get_occurences()
        if not occurrences:
            return
        current = self._occurrences[current_occurence]
        cursor_pos = self.editor.textCursor().position()
        if cursor_pos not in range(current[0], current[1] + 1) or \
                current_occurence == -1:
            # search first occurrence that occurs after the cursor position
            current_occurence = 0
            for i, (start, end) in enumerate(self._occurrences):
                if end > cursor_pos:
                    current_occurence = i
                    break
        else:
            if (current_occurence == -1
                    or current_occurence >= len(occurrences) - 1):
                current_occurence = 0
            else:
                current_occurence += 1
        self._set_current_occurrence(current_occurence)
        try:
            cursor = self.editor.textCursor()
            cursor.setPosition(occurrences[current_occurence][0])
            cursor.setPosition(occurrences[current_occurence][1],
                               cursor.KeepAnchor)
            self.editor.setTextCursor(cursor)
            return True
        except IndexError:
            return False

    def select_previous(self):
        """
        Selects previous occurrence.

        :return: True in case of success, false if no occurrence could be
                 selected.
        """
        current_occurence = self._current_occurrence()
        occurrences = self.get_occurences()
        if not occurrences:
            return
        current = self._occurrences[current_occurence]
        cursor_pos = self.editor.textCursor().position()
        if cursor_pos not in range(current[0], current[1] + 1) or \
                current_occurence == -1:
            # search first occurrence that occurs before the cursor position
            current_occurence = len(self._occurrences) - 1
            for i, (start, end) in enumerate(self._occurrences):
                if end >= cursor_pos:
                    current_occurence = i - 1
                    break
        else:
            if (current_occurence == -1 or current_occurence == 0):
                current_occurence = len(occurrences) - 1
            else:
                current_occurence -= 1
        self._set_current_occurrence(current_occurence)
        try:
            cursor = self.editor.textCursor()
            cursor.setPosition(occurrences[current_occurence][0])
            cursor.setPosition(occurrences[current_occurence][1],
                               cursor.KeepAnchor)
            self.editor.setTextCursor(cursor)
            return True
        except IndexError:
            return False

    def replace(self, text=None):
        """
        Replaces the selected occurrence.

        :param text: The replacement text. If it is None, the lineEditReplace's
                     text is used instead.

        :return True if the text could be replace properly, False if there is
                no more occurrences to replace.
        """
        if text is None or isinstance(text, bool):
            text = self.lineEditReplace.text()
        current_occurences = self._current_occurrence()
        occurrences = self.get_occurences()
        if current_occurences == -1:
            self.select_next()
            current_occurences = self._current_occurrence()
        try:
            # prevent search request due to editor textChanged
            try:
                self.editor.textChanged.disconnect(self.request_search)
            except (RuntimeError, TypeError):
                # already disconnected
                pass
            occ = occurrences[current_occurences]
            cursor = self.editor.textCursor()
            cursor.setPosition(occ[0])
            cursor.setPosition(occ[1], cursor.KeepAnchor)
            len_to_replace = len(cursor.selectedText())
            len_replacement = len(text)
            offset = len_replacement - len_to_replace
            cursor.insertText(text)
            self.editor.setTextCursor(cursor)
            self._remove_occurrence(current_occurences, offset)
            current_occurences -= 1
            self._set_current_occurrence(current_occurences)
            self.select_next()
            self.cpt_occurences = len(self.get_occurences())
            self._update_label_matches()
            self._update_buttons()
            return True
        except IndexError:
            return False
        finally:
            self.editor.textChanged.connect(self.request_search)

    def replace_all(self, text=None):
        """
        Replaces all occurrences in the editor's document.

        :param text: The replacement text. If None, the content of the lineEdit
                     replace will be used instead
        """
        cursor = self.editor.textCursor()
        cursor.beginEditBlock()
        remains = self.replace(text=text)
        while remains:
            remains = self.replace(text=text)
        cursor.endEditBlock()

    def eventFilter(self, obj, event):
        if event.type() == QtCore.QEvent.KeyPress:
            if (event.key() == QtCore.Qt.Key_Tab
                    or event.key() == QtCore.Qt.Key_Backtab):
                return True
            elif (event.key() == QtCore.Qt.Key_Return
                  or event.key() == QtCore.Qt.Key_Enter):
                if obj == self.lineEditReplace:
                    if event.modifiers() & QtCore.Qt.ControlModifier:
                        self.replace_all()
                    else:
                        self.replace()
                elif obj == self.lineEditSearch:
                    if event.modifiers() & QtCore.Qt.ShiftModifier:
                        self.select_previous()
                    else:
                        self.select_next()
                return True
            elif event.key() == QtCore.Qt.Key_Escape:
                self.on_toolButtonClose_clicked()
        return Panel.eventFilter(self, obj, event)

    def _search_flags(self):
        """
        Returns the user search flag: (regex, case_sensitive, whole_words).
        """
        return (self.checkBoxRegex.isChecked(), self.checkBoxCase.isChecked(),
                self.checkBoxWholeWords.isChecked(),
                self.checkBoxInSelection.isChecked())

    def _exec_search(self, sub, flags):
        if self.editor is None:
            return
        regex, case_sensitive, whole_word, in_selection = flags
        tc = self.editor.textCursor()
        assert isinstance(tc, QtGui.QTextCursor)
        if in_selection and tc.hasSelection():
            text = tc.selectedText()
            self._offset = tc.selectionStart()
        else:
            text = self.editor.toPlainText()
            self._offset = 0
        request_data = {
            'string': text,
            'sub': sub,
            'regex': regex,
            'whole_word': whole_word,
            'case_sensitive': case_sensitive
        }
        try:
            self.editor.backend.send_request(findall, request_data,
                                             self._on_results_available)
        except AttributeError:
            self._on_results_available(findall(request_data))

    def _on_results_available(self, results):
        self._occurrences = [(start + self._offset, end + self._offset)
                             for start, end in results]
        self._on_search_finished()

    def _update_label_matches(self):
        self.labelMatches.setText("{0} matches".format(self.cpt_occurences))
        color = "#DD0000"
        if self.cpt_occurences:
            color = "#00DD00"
        self.labelMatches.setStyleSheet("color: %s" % color)
        if self.lineEditSearch.text() == "":
            self.labelMatches.clear()

    def _on_search_finished(self):
        self._clear_decorations()
        all_occurences = self.get_occurences()
        occurrences = all_occurences[:self.MAX_HIGHLIGHTED_OCCURENCES]
        for i, occurrence in enumerate(occurrences):
            deco = self._create_decoration(occurrence[0], occurrence[1])
            self._decorations.append(deco)
            self.editor.decorations.append(deco)
        self.cpt_occurences = len(all_occurences)
        if not self.cpt_occurences:
            self._current_occurrence_index = -1
        else:
            self._current_occurrence_index = -1
        self._update_label_matches()
        self._update_buttons(txt=self.lineEditReplace.text())

    def _current_occurrence(self):
        ret_val = self._current_occurrence_index
        return ret_val

    def _clear_occurrences(self):
        self._occurrences[:] = []

    def _create_decoration(self, selection_start, selection_end):
        """ Creates the text occurences decoration """
        deco = TextDecoration(self.editor.document(), selection_start,
                              selection_end)
        deco.set_background(QtGui.QBrush(self.background))
        deco.set_outline(self._outline)
        deco.draw_order = 1
        return deco

    def _clear_decorations(self):
        """ Remove all decorations """
        for deco in self._decorations:
            self.editor.decorations.remove(deco)
        self._decorations[:] = []

    def _set_current_occurrence(self, current_occurence_index):
        self._current_occurrence_index = current_occurence_index

    def _remove_occurrence(self, i, offset=0):
        self._occurrences.pop(i)
        if offset:
            updated_occurences = []
            for j, occ in enumerate(self._occurrences):
                if j >= i:
                    updated_occurences.append(
                        (occ[0] + offset, occ[1] + offset))
                else:
                    updated_occurences.append((occ[0], occ[1]))
            self._occurrences = updated_occurences

    def _update_buttons(self, txt=""):
        enable = self.cpt_occurences > 1
        self.toolButtonNext.setEnabled(enable)
        self.toolButtonPrevious.setEnabled(enable)
        self.actionFindNext.setEnabled(enable)
        self.actionFindPrevious.setEnabled(enable)
        enable = (txt != self.lineEditSearch.text() and self.cpt_occurences)
        self.toolButtonReplace.setEnabled(enable)
        self.toolButtonReplaceAll.setEnabled(enable)

    def clone_settings(self, original):
        self.background = original.background
        self.foreground = original.foreground
Ejemplo n.º 3
0
class EncodingsMenu(QtWidgets.QMenu):
    """
    Implements a menu that show the user preferred encoding and emit
    reload_requested when the user changed the selected encoding.

    This menu is a general purpose menu that you can put anywhere in your
    application but you will have to manage the reload operation yourself.
    For an integrated context menu, prefer using
    :class:`pyqode.core.widgets.EncodingsContextMenu`.

    """
    #: Signal emitted when the user triggered an action.
    reload_requested = QtCore.Signal(str)

    @property
    def current_encoding(self):
        """
        Gets/Sets the current encoding
        """
        return self._current_encoding

    @current_encoding.setter
    def current_encoding(self, value):
        self._current_encoding = convert_to_codec_key(value)
        self._refresh()

    def __init__(self,
                 title=_('Encodings'),
                 parent=None,
                 selected_encoding=locale.getpreferredencoding()):
        super(EncodingsMenu, self).__init__(parent)
        self.setTitle(title)
        self._group = QtWidgets.QActionGroup(self)
        self._edit_action = None
        self._current_encoding = ''
        self.current_encoding = selected_encoding

    def _clear_actions(self):
        for action in self._group.actions():
            self._group.removeAction(action)
            self.removeAction(action)
        self.removeAction(self._edit_action)

    def _refresh(self):
        self._clear_actions()
        for i, encoding in enumerate(sorted(Cache().preferred_encodings)):
            encoding = convert_to_codec_key(encoding)
            try:
                alias, lang = ENCODINGS_MAP[encoding]
            except KeyError:
                _logger().warn('KeyError with encoding:', encoding)
            else:
                action = QtWidgets.QAction('%s (%s)' % (alias, lang), self)
                action.setData(encoding)
                action.setCheckable(True)
                if encoding == self._current_encoding or \
                        convert_to_codec_key(alias) == self._current_encoding:
                    action.setChecked(True)
                self.addAction(action)
                self._group.addAction(action)
        self._group.triggered.connect(self._on_encoding_triggered)
        self.addSeparator()
        self._edit_action = QtWidgets.QAction(_('Add or remove'), self)
        self._edit_action.triggered.connect(self._on_edit_requested)
        self.addAction(self._edit_action)

    def _on_edit_requested(self):
        from pyqode.core.dialogs import DlgPreferredEncodingsEditor
        if DlgPreferredEncodingsEditor.edit_encoding(self):
            self._refresh()

    def _on_encoding_triggered(self, action):
        self.reload_requested.emit(action.data())
Ejemplo n.º 4
0
class RecentFilesManager(QtCore.QObject):
    """
    Manages a list of recent files. The list of files is stored in your
    application QSettings.

    """
    #: Maximum number of files kept in the list.
    max_recent_files = 15
    updated = QtCore.Signal()

    def __init__(self, organisation, application):
        super(RecentFilesManager, self).__init__()
        self._settings = QtCore.QSettings(organisation, application)

    def clear(self):
        """ Clears recent files in QSettings """
        self.set_value('list', [])
        self.updated.emit()

    def remove(self, filename):
        """
        Remove a file path from the list of recent files.
        :param filename: Path of the file to remove
        """
        files = self.get_value('list', [])
        files.remove(filename)
        self.set_value('list', files)
        self.updated.emit()

    def get_value(self, key, default=None):
        """
        Reads value from QSettings
        :param key: value key
        :param default: default value.
        :return: value
        """
        def unique(seq, idfun=None):
            if idfun is None:

                def idfun(x):
                    return x

            # order preserving
            seen = {}
            result = []
            for item in seq:
                marker = idfun(item)
                if marker in seen:
                    continue
                seen[marker] = 1
                result.append(item)
            return result

        lst = self._settings.value('recent_files/%s' % key, default)
        # emtpy list
        if lst is None:
            lst = []
        # single file
        if isinstance(lst, str):
            lst = [lst]
        return unique([os.path.normpath(pth) for pth in lst])

    def set_value(self, key, value):
        """
        Set the recent files value in QSettings.
        :param key: value key
        :param value: new value
        """
        if value is None:
            value = []
        value = [os.path.normpath(pth) for pth in value]
        self._settings.setValue('recent_files/%s' % key, value)

    def get_recent_files(self):
        """
        Gets the list of recent files. (files that do not exists anymore
        are automatically filtered)
        """
        ret_val = []
        files = self.get_value('list', [])
        # filter files, remove files that do not exist anymore
        for file in files:
            if file is not None and os.path.exists(file):
                if os.path.ismount(file) and \
                        sys.platform == 'win32' and not file.endswith('\\'):
                    file += '\\'
                if file not in ret_val:
                    ret_val.append(file)
        return ret_val

    def open_file(self, file):
        """
        Adds a file to the list (and move it to the top of the list if the
        file already exists)

        :param file: file path to add the list of recent files.

        """
        files = self.get_recent_files()
        try:
            files.remove(file)
        except ValueError:
            pass
        files.insert(0, file)
        # discard old files
        del files[self.max_recent_files:]
        self.set_value('list', files)
        self.updated.emit()

    def last_file(self):
        """
        Returns the path to the last opened file.
        """
        files = self.get_recent_files()
        try:
            return files[0]
        except IndexError:
            return None
Ejemplo n.º 5
0
class FileSystemTreeView(QtWidgets.QTreeView):
    """
    Extends QtWidgets.QTreeView with a filterable file system model.

    To exclude directories or extension, just set
    :attr:`FilterProxyModel.ignored_directories` and
    :attr:`FilterProxyModel.ignored_extensions`.

    Provides methods to retrieve file info from model index.

    By default there is no context menu and no file operations are possible. We
    provide a standard context menu with basic file system operation (
    :class:`FileSystemContextMenu`) that you can extend and
    set on the tree view using :meth:`FileSystemTreeView.set_context_menu`

    """
    class FilterProxyModel(QtCore.QSortFilterProxyModel):
        """
        Excludes :attr:`ignored_directories` and :attr:`ignored_extensions`
        from the file system model.
        """
        #: The list of directories to exclude
        ignored_directories = ['__pycache__']
        #: The list of file extension to exclude
        ignored_extensions = [
            '.pyc', '.pyd', '.so', '.dll', '.exe', '.egg-info', '.coverage',
            '.DS_Store'
        ]

        def __init__(self):
            super(FileSystemTreeView.FilterProxyModel, self).__init__()
            self._ignored_unused = []

        def set_root_path(self, path):
            """
            Sets the root path to watch.
            :param path: root path (str).
            """
            self._ignored_unused[:] = []
            parent_dir = os.path.dirname(path)
            for item in os.listdir(parent_dir):
                item_path = os.path.join(parent_dir, item)
                if item_path != path:
                    self._ignored_unused.append(os.path.normpath(item_path))

        def filterAcceptsRow(self, row, parent):
            index0 = self.sourceModel().index(row, 0, parent)
            finfo = self.sourceModel().fileInfo(index0)
            fn = finfo.fileName()
            fp = os.path.normpath(finfo.filePath())
            extension = '.%s' % finfo.suffix()

            if fp in self._ignored_unused:
                _logger().debug('excluding directory (unused): %s',
                                finfo.filePath())
                return False
            if fn in self.ignored_directories:
                _logger().debug('excluding directory: %s', finfo.filePath())
                return False
            if extension in self.ignored_extensions:
                _logger().debug('excluding file: %s', finfo.filePath())
                return False
            _logger().debug('accepting %s', finfo.filePath())
            return True

    #: signal emitted when the user deleted a file
    #: Parameters:
    #: - path (str): path of the file that got deleted
    file_deleted = QtCore.Signal(str)
    #: signal emitted when the user renamed a file
    #: Parameters:
    #: - old (str): old path
    #: - new (str): new path
    file_renamed = QtCore.Signal(str, str)
    #: signal emitted when the user created a file
    #: Parameters:
    #: - path (str): path of the file that got created
    file_created = QtCore.Signal(str)

    def __init__(self, parent=None):
        super(FileSystemTreeView, self).__init__(parent)
        self.context_menu = None
        self.root_path = None
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self._show_context_menu)
        self.helper = FileSystemHelper(self)
        self.setSelectionMode(self.ExtendedSelection)

    @classmethod
    def ignore_directories(cls, *directories):
        """
        Adds the specified directories to the list of ignored directories.

        This must be done before calling set_root_path!

        :param directories: the directories to ignore
        """
        for d in directories:
            cls.FilterProxyModel.ignored_directories.append(d)

    @classmethod
    def ignore_extensions(cls, *extensions):
        """
        Adds the specified extensions to the list of ignored directories.

        This must be done before calling set_root_path!

        :param extensions: the extensions to ignore

        .. note:: extension must have the dot: '.py' and not 'py'
        """
        for d in extensions:
            cls.FilterProxyModel.ignored_extensions.append(d)

    def set_context_menu(self, context_menu):
        """
        Sets the context menu of the tree view.

        :param context_menu: QMenu
        """
        self.context_menu = context_menu
        self.context_menu.tree_view = self
        for action in self.context_menu.actions():
            self.addAction(action)

    def set_root_path(self, path, hide_extra_columns=True):
        """
        Sets the root path to watch
        :param path: root path - str
        :param hide_extra_columns: Hide extra column (size, paths,...)
        """
        if os.path.isfile(path):
            path = os.path.abspath(os.path.join(path, os.pardir))
        self._fs_model_source = QtWidgets.QFileSystemModel()
        self._fs_model_proxy = self.FilterProxyModel()
        self._fs_model_proxy.setSourceModel(self._fs_model_source)
        self.setModel(self._fs_model_proxy)
        self._fs_model_proxy.set_root_path(path)
        self.root_path = os.path.dirname(path)
        file_root_index = self._fs_model_source.setRootPath(self.root_path)
        root_index = self._fs_model_proxy.mapFromSource(file_root_index)
        self.setRootIndex(root_index)
        # Expand first entry (project name)
        file_parent_index = self._fs_model_source.index(path)
        self.setExpanded(self._fs_model_proxy.mapFromSource(file_parent_index),
                         True)
        if hide_extra_columns:
            self.setHeaderHidden(True)
            for i in range(1, 4):
                self.hideColumn(i)

    def filePath(self, index):
        """
        Gets the file path of the item at the specified ``index``.

        :param index: item index - QModelIndex
        :return: str
        """
        return self._fs_model_source.filePath(
            self._fs_model_proxy.mapToSource(index))

    def fileInfo(self, index):
        """
        Gets the file info of the item at the specified ``index``.

        :param index: item index - QModelIndex
        :return: QFileInfo
        """
        return self._fs_model_source.fileInfo(
            self._fs_model_proxy.mapToSource(index))

    def _show_context_menu(self, point):
        if self.context_menu:
            self.context_menu.exec_(self.mapToGlobal(point))
Ejemplo n.º 6
0
class CodeEdit(QtWidgets.QPlainTextEdit):
    """
    The editor widget is a simple extension to QPlainTextEdit.

    It adds a few utility signals/methods and introduces the concepts of
    **Managers, Modes and Panels**.

    A **mode/panel** is an editor extension that, once added to a CodeEdit
    instance, may modify its behaviour and appearance:

      * **Modes** are simple objects which connect to the editor signals to
        append new behaviours (such as automatic indentation, code completion,
        syntax checking,...)

      * **Panels** are the combination of a **Mode** and a **QWidget**.
        They are displayed in the CodeEdit's content margins.

        When you install a Panel on a CodeEdit, you can choose to install it in
        one of the four following zones:

            .. image:: _static/editor_widget.png
                :align: center
                :width: 600
                :height: 450

    A **manager** is an object that literally manage a specific aspect of
    :class:`pyqode.core.api.CodeEdit`. There are managers to manage the list of
    modes/panels, to open/save file and to control the backend:

        - :attr:`pyqode.core.api.CodeEdit.file`:
            File manager. Use it to open/save files or access the opened file
            attribute.
        - :attr:`pyqode.core.api.CodeEdit.backend`:
            Backend manager. Use it to start/stop the backend or send a work
            request.
        - :attr:`pyqode.core.api.CodeEdit.modes`:
            Modes manager. Use it to append/remove modes on the editor.
        - :attr:`pyqode.core.api.CodeEdit.panels`:
            Modes manager. Use it to append/remove panels on the editor.

    Starting from version 2.1, CodeEdit defines the
    :attr:`pyqode.core.api.CodeEdit.mimetypes` class attribute that can be used
    by IDE to determine which editor to use for a given mime type. This
    property is a list of supported mimetypes. An empty list means the
    CodeEdit is generic. **Code editors specialised for a specific language
    should define the mime types they support!**
    """
    #: Paint hook
    painted = QtCore.Signal(QtGui.QPaintEvent)
    #: Signal emitted when a new text is set on the widget
    new_text_set = QtCore.Signal()
    #: Signal emitted when the text is saved to file
    text_saved = QtCore.Signal(str)
    #: Signal emitted before the text is saved to file
    text_saving = QtCore.Signal(str)
    #: Signal emitted when the dirty state changed
    dirty_changed = QtCore.Signal(bool)
    #: Signal emitted when a key is pressed
    key_pressed = QtCore.Signal(QtGui.QKeyEvent)
    #: Signal emitted when a key is released
    key_released = QtCore.Signal(QtGui.QKeyEvent)
    #: Signal emitted when a mouse button is pressed
    mouse_pressed = QtCore.Signal(QtGui.QMouseEvent)
    #: Signal emitted when a mouse button is released
    mouse_released = QtCore.Signal(QtGui.QMouseEvent)
    #: Signal emitted when a mouse double click event occured
    mouse_double_clicked = QtCore.Signal(QtGui.QMouseEvent)
    #: Signal emitted on a wheel event
    mouse_wheel_activated = QtCore.Signal(QtGui.QWheelEvent)
    #: Signal emitted at the end of the key_pressed event
    post_key_pressed = QtCore.Signal(QtGui.QKeyEvent)
    #: Signal emitted when focusInEvent is is called
    focused_in = QtCore.Signal(QtGui.QFocusEvent)
    #: Signal emitted when the mouse_moved
    mouse_moved = QtCore.Signal(QtGui.QMouseEvent)
    #: Signal emitted when the user press the TAB key
    indent_requested = QtCore.Signal()
    #: Signal emitted when the user press the BACK-TAB (Shift+TAB) key
    unindent_requested = QtCore.Signal()

    #: Store the list of mimetypes associated with the editor, for
    #: specialised editors.
    mimetypes = []

    _DEFAULT_FONT = 'Source Code Pro' if sys.platform != 'darwin' else 'Monaco'

    @property
    def use_spaces_instead_of_tabs(self):
        """ Use spaces instead of tabulations. Default is True. """
        return self._use_spaces_instead_of_tabs

    @use_spaces_instead_of_tabs.setter
    def use_spaces_instead_of_tabs(self, value):
        self._use_spaces_instead_of_tabs = value
        for c in self.clones:
            c.use_spaces_instead_of_tabs = value

    @property
    def tab_length(self):
        """ Tab length, number of spaces. """
        return self._tab_length

    @tab_length.setter
    def tab_length(self, value):
        if value < 2:
            value = 2
        self._tab_length = value
        for c in self.clones:
            c.tab_length = value

    @property
    def save_on_focus_out(self):
        """
        Automatically saves editor content on focus out.

        Default is False.
        """
        return self._save_on_focus_out

    @save_on_focus_out.setter
    def save_on_focus_out(self, value):
        self._save_on_focus_out = value
        for c in self.clones:
            c.save_on_focus_out = value

    @property
    def show_whitespaces(self):
        """
        Shows/Hides virtual white spaces.
        """
        return self._show_whitespaces

    @show_whitespaces.setter
    def show_whitespaces(self, value):
        if self._show_whitespaces != value:
            self._show_whitespaces = value
            self._set_whitespaces_flags(value)
            for c in self.clones:
                c.show_whitespaces = value
            self.rehighlight()

    @property
    def font_name(self):
        """
        The editor font family name.
        """
        return self._font_family

    @font_name.setter
    def font_name(self, value):
        if value == "":
            value = self._DEFAULT_FONT
        self._font_family = value
        self._reset_stylesheet()
        for c in self.clones:
            c.font_name = value

    @property
    def zoom_level(self):
        """
        Gets/Sets the editor zoom level.

        The zoom level is a value that is added to the current editor font
        size. Negative values are used to zoom out the editor, positive values
        are used to zoom in the editor.
        """
        return self._zoom_level

    @zoom_level.setter
    def zoom_level(self, value):
        self._zoom_level = value

    @property
    def font_size(self):
        """
        The font point size.

        .. note:: Please, **never use setFontPointSize/setFontFamily functions
            directly** as the values you define there will be overwritten as
            soon as the user zoom the editor or as soon as a stylesheet
            property has changed.
        """
        return self._font_size

    @font_size.setter
    def font_size(self, value):
        self._font_size = value
        self._reset_stylesheet()
        for c in self.clones:
            c.font_size = value

    @property
    def background(self):
        """
        The editor background color (QColor)
        """
        return self._background

    @background.setter
    def background(self, value):
        self._background = value
        self._reset_stylesheet()
        for c in self.clones:
            c.background = value

    @property
    def foreground(self):
        """
        The editor foreground color (QColor)
        """
        return self._foreground

    @foreground.setter
    def foreground(self, value):
        self._foreground = value
        self._reset_stylesheet()
        for c in self.clones:
            c.foreground = value

    @property
    def whitespaces_foreground(self):
        """
        The editor white spaces' foreground color. White spaces are highlighted
        by the syntax highlighter. You should call rehighlight to update their
        color. This is not done automatically to prevent multiple, useless
        call to ``rehighlight`` which can take some time on big files.
        """
        return self._whitespaces_foreground

    @whitespaces_foreground.setter
    def whitespaces_foreground(self, value):
        self._whitespaces_foreground = value
        for c in self.clones:
            c.whitespaces_foreground = value

    @property
    def selection_background(self):
        """
        The editor selection's background color.
        """
        return self._sel_background

    @selection_background.setter
    def selection_background(self, value):
        self._sel_background = value
        self._reset_stylesheet()
        for c in self.clones:
            c.selection_background = value

    @property
    def selection_foreground(self):
        """
        The editor selection's foreground color.
        """
        return self._sel_foreground

    @selection_foreground.setter
    def selection_foreground(self, value):
        self._sel_foreground = value
        for c in self.clones:
            c.selection_foreground = value

    @property
    def word_separators(self):
        """
        The list of word separators used by the code completion mode
        and the word clicked mode.
        """
        return self._word_separators

    @word_separators.setter
    def word_separators(self, value):
        self._word_separators = value
        for c in self.clones:
            c._word_separators = value

    @property
    def dirty(self):
        """
        Tells whethere the content of editor has been modified.

        (this is just a shortcut to QTextDocument.isModified

        :type: bool
        """
        return self.document().isModified()

    @property
    def visible_blocks(self):
        """
        Returns the list of visible blocks.

        Each element in the list is a tuple made up of the line top position,
        the line number and the QTextBlock itself.

        :return: A list of tuple(top_position, line_number, block)
        :rtype: List of tuple(int, int, QtWidgets.QTextBlock)
        """
        return self._visible_blocks

    @property
    def file(self):
        """
        Returns a reference to the :class:`pyqode.core.managers.FileManager`
        used to open/save file on the editor
        """
        return self._file

    @file.setter
    def file(self, file_manager):
        """
        Sets a custom file manager.

        :param file_manager: custom file manager instance.
        """
        self._file = file_manager

    @property
    def backend(self):
        """
        Returns a reference to the :class:`pyqode.core.managers.BackendManager`
        used to control the backend process.
        """
        return self._backend

    @property
    def modes(self):
        """
        Returns a reference to the :class:`pyqode.core.managers.ModesManager`
        used to manage the collection of installed modes.
        """
        return self._modes

    @property
    def panels(self):
        """
        Returns a reference to the :class:`pyqode.core.managers.PanelsManager`
        used to manage the collection of installed panels
        """
        return self._panels

    @property
    def decorations(self):
        """
        Returns a reference to the
        :class:`pyqode.core.managers.TextDecorationManager` used to manage the
        list of :class:`pyqode.core.api.TextDecoration`
        """
        return self._decorations

    @property
    def syntax_highlighter(self):
        """
        Returns a reference to the syntax highlighter mode currently used to
        highlight the editor content.

        :return: :class:`pyqode.core.api.SyntaxHighlighter`
        """
        for mode in self.modes:
            if hasattr(mode, 'highlightBlock'):
                return mode
        return None

    @property
    def show_context_menu(self):
        """
        Specifies whether we should display the context menu or not.

        Default is True
        """
        return self._show_ctx_mnu

    @show_context_menu.setter
    def show_context_menu(self, value):
        self._show_ctx_mnu = value

    @property
    def select_line_on_copy_empty(self):
        """
        :return: state of "whole line selecting" on copy with empty selection
        :rtype: bool
        """
        return self._select_line_on_copy_empty

    @select_line_on_copy_empty.setter
    def select_line_on_copy_empty(self, value):
        """
        To turn on/off selecting the whole line when copy with empty selection is triggered

        Default is True
        """
        self._select_line_on_copy_empty = value

    def __init__(self, parent=None, create_default_actions=True):
        """
        :param parent: Parent widget

        :param create_default_actions: True to create the action for the
            standard shortcuts (copy, paste, delete, undo, redo,...).
            Non-standard actions will always get created. If you would like
            to prevent the context menu from showing, just set the
            :attr:`show_menu_enabled` to False.
        """
        super(CodeEdit, self).__init__(parent)
        self.installEventFilter(self)
        self.clones = []
        self._show_ctx_mnu = True
        self._default_font_size = 10
        self._backend = BackendManager(self)
        self._file = FileManager(self)
        self._modes = ModesManager(self)
        self._panels = PanelsManager(self)
        self._decorations = TextDecorationsManager(self)
        self.document().modificationChanged.connect(self._emit_dirty_changed)

        self._word_separators = [
            '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '{',
            '}', '|', ':', '"', "'", "<", ">", "?", ",", ".", "/", ";", '[',
            ']', '\\', '\n', '\t', '=', '-', ' '
        ]
        self._save_on_focus_out = False
        self._use_spaces_instead_of_tabs = True
        self._whitespaces_foreground = None
        self._sel_background = None
        self._show_whitespaces = False
        self._foreground = None
        self._sel_foreground = None
        self._tab_length = 4
        self._zoom_level = 0
        self._font_size = 10
        self._background = None
        QtGui.QFontDatabase.addApplicationFont(
            ':/fonts/rc/SourceCodePro-Regular.ttf')
        QtGui.QFontDatabase.addApplicationFont(
            ':/fonts/rc/SourceCodePro-Bold.ttf')
        self._font_family = self._DEFAULT_FONT
        self._mimetypes = []
        self._select_line_on_copy_empty = True

        # Flags/Working variables
        self._last_mouse_pos = QtCore.QPoint(0, 0)
        self._modified_lines = set()
        self._cleaning = False
        self._visible_blocks = []
        self._tooltips_runner = DelayJobRunner(delay=700)
        self._prev_tooltip_block_nbr = -1
        self._original_text = ""

        self._dirty = False

        # setup context menu
        self._actions = []
        self._menus = []
        self._init_actions(create_default_actions)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self._show_context_menu)
        self._mnu = None  # bug with PySide (github #63)

        # init settings and styles from global settings/style modules
        self._init_settings()
        self._init_style()

        # connect slots
        self.textChanged.connect(self._on_text_changed)
        self.blockCountChanged.connect(self.update)
        self.cursorPositionChanged.connect(self.update)
        self.selectionChanged.connect(self.update)

        self.setMouseTracking(True)
        self.setCenterOnScroll(True)
        self.setLineWrapMode(self.NoWrap)
        self.setCursorWidth(2)

    def __repr__(self):
        return '%s(path=%r)' % (self.__class__.__name__, self.file.path)

    def split(self):
        """
        Split the code editor widget, return a clone of the widget ready to
        be used (and synchronised with its original).

        Splitting the widget is done in 2 steps:
            - first we clone the widget, you can override ``clone`` if your
              widget needs additional arguments.

            - then we link the two text document and disable some modes on the
                cloned instance (such as the watcher mode).
        """
        # cache cursor position so that the clone open at the current cursor
        # pos
        l, c = TextHelper(self).cursor_position()
        clone = self.clone()
        self.link(clone)
        TextHelper(clone).goto_line(l, c)
        clone.verticalScrollBar().setValue(self.verticalScrollBar().value())
        self.clones.append(clone)
        return clone

    def clone(self):
        """
        Clone ourselves, return an instance of the same class, using the
        default QWidget constructor.
        """
        clone = self.__class__(parent=self.parent())
        return clone

    def link(self, clone):
        """
        Links the clone with its original. We copy the file manager infos
        (path, mimetype, ...) and setup the clone text document as reference
        to our text document.

        :param clone: clone to link.
        """
        clone.file._path = self.file.path
        clone.file._encoding = self.file.encoding
        clone.file._mimetype = self.file.mimetype
        clone.setDocument(self.document())
        for original_mode, mode in zip(list(self.modes), list(clone.modes)):
            mode.enabled = original_mode.enabled
            mode.clone_settings(original_mode)
        for original_panel, panel in zip(list(self.panels),
                                         list(clone.panels)):
            panel.enabled = original_panel.isEnabled()
            panel.clone_settings(original_panel)
            if not original_panel.isVisible():
                panel.setVisible(False)
        clone.use_spaces_instead_of_tabs = self.use_spaces_instead_of_tabs
        clone.tab_length = self.tab_length
        clone._save_on_focus_out = self._save_on_focus_out
        clone.show_whitespaces = self.show_whitespaces
        clone.font_name = self.font_name
        clone.font_size = self.font_size
        clone.zoom_level = self.zoom_level
        clone.background = self.background
        clone.foreground = self.foreground
        clone.whitespaces_foreground = self.whitespaces_foreground
        clone.selection_background = self.selection_background
        clone.selection_foreground = self.selection_foreground
        clone.word_separators = self.word_separators
        clone.file.clone_settings(self.file)

    def close(self, clear=True):
        """
        Closes the editor, stops the backend and removes any installed
        mode/panel.

        This is also where we cache the cursor position.

        :param clear: True to clear the editor content before closing.
        """
        if self._tooltips_runner:
            self._tooltips_runner.cancel_requests()
            self._tooltips_runner = None
        self.decorations.clear()
        self.modes.clear()
        self.panels.clear()
        self.backend.stop()
        Cache().set_cursor_position(self.file.path,
                                    self.textCursor().position())
        super(CodeEdit, self).close()

    def set_mouse_cursor(self, cursor):
        """
        Changes the viewport's cursor

        :param cursor: the mouse cursor to set.
        :type cursor: QtWidgets.QCursor
        """
        self.viewport().setCursor(cursor)

    def show_tooltip(self, pos, tooltip, _sender_deco=None):
        """
        Show a tool tip at the specified position

        :param pos: Tooltip position
        :param tooltip: Tooltip text

        :param _sender_deco: TextDecoration which is the sender of the show
            tooltip request. (for internal use only).
        """
        if _sender_deco is not None and _sender_deco not in self.decorations:
            return
        QtWidgets.QToolTip.showText(pos, tooltip[0:1024], self)

    def setPlainText(self, txt, mime_type, encoding):
        """
        Extends setPlainText to force the user to setup an encoding and a
        mime type.

        Emits the new_text_set signal.

        :param txt: The new text to set.
        :param mime_type: Associated mimetype. Setting the mime will update the
                          pygments lexer.
        :param encoding: text encoding
        """
        self.file.mimetype = mime_type
        self.file._encoding = encoding
        self._original_text = txt
        self._modified_lines.clear()
        import time
        t = time.time()
        super(CodeEdit, self).setPlainText(txt)
        _logger().log(5, 'setPlainText duration: %fs' % (time.time() - t))
        self.new_text_set.emit()
        self.redoAvailable.emit(False)
        self.undoAvailable.emit(False)

    def add_action(self, action, sub_menu='Advanced'):
        """
        Adds an action to the editor's context menu.

        :param action: QAction to add to the context menu.
        :param sub_menu: The name of a sub menu where to put the action.
            'Advanced' by default. If None or empty, the action will be added
            at the root of the submenu.
        """
        if sub_menu:
            try:
                mnu = self._sub_menus[sub_menu]
            except KeyError:
                mnu = QtWidgets.QMenu(sub_menu)
                self.add_menu(mnu)
                self._sub_menus[sub_menu] = mnu
            finally:
                mnu.addAction(action)
        else:
            self._actions.append(action)
        action.setShortcutContext(QtCore.Qt.WidgetShortcut)
        self.addAction(action)

    def insert_action(self, action, prev_action):
        """
        Inserts an action to the editor's context menu.

        :param action: action to insert
        :param prev_action: the action after which the new action must be
            inserted or the insert index
        """
        if isinstance(prev_action, QtWidgets.QAction):
            index = self._actions.index(prev_action)
        else:
            index = prev_action
        action.setShortcutContext(QtCore.Qt.WidgetShortcut)
        self._actions.insert(index, action)

    def actions(self):
        """
        Returns the list of actions/sepqrators of the context menu.

        """
        return self._actions

    def add_separator(self, sub_menu='Advanced'):
        """
        Adds a sepqrator to the editor's context menu.

        :return: The sepator that has been added.
        :rtype: QtWidgets.QAction
        """
        action = QtWidgets.QAction(self)
        action.setSeparator(True)
        if sub_menu:
            try:
                mnu = self._sub_menus[sub_menu]
            except KeyError:
                pass
            else:
                mnu.addAction(action)
        else:
            self._actions.append(action)
        return action

    def remove_action(self, action, sub_menu='Advanced'):
        """
        Removes an action/separator from the editor's context menu.

        :param action: Action/seprator to remove.
        :param advanced: True to remove the action from the advanced submenu.
        """
        if sub_menu:
            try:
                mnu = self._sub_menus[sub_menu]
            except KeyError:
                pass
            else:
                mnu.removeAction(action)
        else:
            try:
                self._actions.remove(action)
            except ValueError:
                pass
        self.removeAction(action)

    def add_menu(self, menu):
        """
        Adds a sub-menu to the editor context menu.

        Menu are put at the bottom of the context menu.

        .. note:: to add a menu in the middle of the context menu, you can
            always add its menuAction().

        :param menu: menu to add
        """
        self._menus.append(menu)
        self._menus = sorted(list(set(self._menus)), key=lambda x: x.title())
        for action in menu.actions():
            action.setShortcutContext(QtCore.Qt.WidgetShortcut)
        self.addActions(menu.actions())

    def remove_menu(self, menu):
        """
        Removes a sub-menu from the context menu.
        :param menu: Sub-menu to remove.
        """
        self._menus.remove(menu)
        for action in menu.actions():
            self.removeAction(action)

    def menus(self):
        """
        Returns the list of sub context menus.
        """
        return self._menus

    def delete(self):
        """ Deletes the selected text """
        self.textCursor().removeSelectedText()

    def goto_line(self):
        """
        Shows the *go to line dialog* and go to the selected line.
        """
        helper = TextHelper(self)
        line, result = DlgGotoLine.get_line(self, helper.current_line_nbr(),
                                            helper.line_count())
        if not result:
            return
        return helper.goto_line(line, move=True)

    def rehighlight(self):
        """
        Calls ``rehighlight`` on the installed syntax highlighter mode.
        """
        if self.syntax_highlighter:
            self.syntax_highlighter.rehighlight()

    def reset_zoom(self):
        """
        Resets the zoom level.
        """
        self._zoom_level = 0
        self._reset_stylesheet()

    def zoom_in(self, increment=1):
        """
        Zooms in the editor (makes the font bigger).

        :param increment: zoom level increment. Default is 1.
        """
        # When called through an action, the first argument is a bool
        if isinstance(increment, bool):
            increment = 1
        self.zoom_level += increment
        TextHelper(self).mark_whole_doc_dirty()
        self._reset_stylesheet()

    def zoom_out(self, decrement=1):
        """
        Zooms out the editor (makes the font smaller).

        :param decrement: zoom level decrement. Default is 1. The value is
            given as an absolute value.
        """
        # When called through an action, the first argument is a bool
        if isinstance(decrement, bool):
            decrement = 1
        self.zoom_level -= decrement
        # make sure font size remains > 0
        if self.font_size + self.zoom_level <= 0:
            self.zoom_level = -self._font_size + 1
        TextHelper(self).mark_whole_doc_dirty()
        self._reset_stylesheet()

    def duplicate_line(self):
        """
        Duplicates the line under the cursor. If multiple lines are selected,
        only the last one is duplicated.
        """
        cursor = self.textCursor()
        assert isinstance(cursor, QtGui.QTextCursor)
        has_selection = True
        if not cursor.hasSelection():
            cursor.select(cursor.LineUnderCursor)
            has_selection = False
        line = cursor.selectedText()
        line = '\n'.join(line.split('\u2029'))
        end = cursor.selectionEnd()
        cursor.setPosition(end)
        cursor.beginEditBlock()
        cursor.insertText('\n')
        cursor.insertText(line)
        cursor.endEditBlock()
        if has_selection:
            pos = cursor.position()
            cursor.setPosition(end + 1)
            cursor.setPosition(pos, cursor.KeepAnchor)
        self.setTextCursor(cursor)

    def indent(self):
        """
        Indents the text cursor or the selection.

        Emits the :attr:`pyqode.core.api.CodeEdit.indent_requested`
        signal, the :class:`pyqode.core.modes.IndenterMode` will
        perform the actual indentation.
        """
        self.indent_requested.emit()

    def un_indent(self):
        """
        Un-indents the text cursor or the selection.

        Emits the :attr:`pyqode.core.api.CodeEdit.unindent_requested`
        signal, the :class:`pyqode.core.modes.IndenterMode` will
        perform the actual un-indentation.
        """
        self.unindent_requested.emit()

    def eventFilter(self, obj, event):
        if obj == self and event.type() == QtCore.QEvent.KeyPress:
            if event.key() == QtCore.Qt.Key_X and \
                    int(event.modifiers()) == QtCore.Qt.ControlModifier:
                self.cut()
                return True
            if event.key() == QtCore.Qt.Key_C and \
                    int(event.modifiers()) == QtCore.Qt.ControlModifier:
                self.copy()
                return True
        return False

    def cut(self):
        """
        Cuts the selected text or the whole line if no text was selected.
        """
        tc = self.textCursor()
        helper = TextHelper(self)
        tc.beginEditBlock()
        no_selection = False
        sText = tc.selection().toPlainText()
        # If only whitespace is selected, simply delete it
        if not helper.current_line_text() and not sText.strip():
            tc.deleteChar()
        else:
            if not self.textCursor().hasSelection():
                no_selection = True
                TextHelper(self).select_whole_line()
            super(CodeEdit, self).cut()
            if no_selection:
                tc.deleteChar()
        tc.endEditBlock()
        self.setTextCursor(tc)

    def copy(self):
        """
        Copy the selected text to the clipboard. If no text was selected, the
        entire line is copied (this feature can be turned off by
        setting :attr:`select_line_on_copy_empty` to False.
        """
        if self.select_line_on_copy_empty and not self.textCursor(
        ).hasSelection():
            TextHelper(self).select_whole_line()
        super(CodeEdit, self).copy()

    def swapLineUp(self):
        self.__swapLine(True)

    def swapLineDown(self):
        self.__swapLine(False)

    def __swapLine(self, up):
        has_selection = self.textCursor().hasSelection()
        helper = TextHelper(self)
        # Remember the cursor position so that we can restore it later
        line, column = helper.cursor_position()
        # Check the range that we're going to move and verify that it stays
        # within the document boundaries
        start_index, end_index = helper.selection_range()
        if up:
            start_index -= 1
            if start_index < 0:
                return
        else:
            end_index += 1
            if end_index >= helper.line_count():
                return
        # Select the current lines and the line that will be swapped, turn
        # them into a list, and then perform the swap on this list
        helper.select_lines(start_index, end_index)
        lines = helper.selected_text().replace(u'\u2029', u'\n').split(u'\n')
        if up:
            lines = lines[1:] + [lines[0]]
        else:
            lines = [lines[-1]] + lines[:-1]
        # Replace the selected text by the swapped text in a single undo action
        cursor = self.textCursor()
        cursor.beginEditBlock()
        cursor.insertText(u'\n'.join(lines))
        cursor.endEditBlock()
        self.setTextCursor(cursor)
        if has_selection:
            # If text was originally selected, select the range again
            if up:
                helper.select_lines(start_index, end_index - 1)
            else:
                helper.select_lines(start_index + 1, end_index)
        else:
            # Else restore cursor position, while moving with the swap
            helper.goto_line(line - 1 if up else line + 1, column)

    def resizeEvent(self, e):
        """
        Overrides resize event to resize the editor's panels.

        :param e: resize event
        """
        super(CodeEdit, self).resizeEvent(e)
        self.panels.resize()

    def closeEvent(self, e):
        self.close()
        super(CodeEdit, self).closeEvent(e)

    def paintEvent(self, e):
        """
        Overrides paint event to update the list of visible blocks and emit
        the painted event.

        :param e: paint event
        """
        self._update_visible_blocks(e)
        super(CodeEdit, self).paintEvent(e)
        self.painted.emit(e)

    def keyPressEvent(self, event):
        """
        Overrides the keyPressEvent to emit the key_pressed signal.

        Also takes care of indenting and handling smarter home key.

        :param event: QKeyEvent
        """
        if self.isReadOnly():
            return
        initial_state = event.isAccepted()
        event.ignore()
        self.key_pressed.emit(event)
        state = event.isAccepted()
        if not event.isAccepted():
            if event.key() == QtCore.Qt.Key_Tab and event.modifiers() == \
                    QtCore.Qt.NoModifier:
                self.indent()
                event.accept()
            elif event.key() == QtCore.Qt.Key_Backtab and \
                    event.modifiers() == QtCore.Qt.NoModifier:
                self.un_indent()
                event.accept()
            elif event.key() == QtCore.Qt.Key_Home and \
                    int(event.modifiers()) & QtCore.Qt.ControlModifier == 0:
                self._do_home_key(
                    event,
                    int(event.modifiers()) & QtCore.Qt.ShiftModifier)
            if not event.isAccepted():
                event.setAccepted(initial_state)
                super(CodeEdit, self).keyPressEvent(event)
        new_state = event.isAccepted()
        event.setAccepted(state)
        self.post_key_pressed.emit(event)
        event.setAccepted(new_state)

    def keyReleaseEvent(self, event):
        """
        Overrides keyReleaseEvent to emit the key_released signal.

        :param event: QKeyEvent
        """
        if self.isReadOnly():
            return
        initial_state = event.isAccepted()
        event.ignore()
        self.key_released.emit(event)
        if not event.isAccepted():
            event.setAccepted(initial_state)
            super(CodeEdit, self).keyReleaseEvent(event)

    def mouseDoubleClickEvent(self, event):
        initial_state = event.isAccepted()
        event.ignore()
        self.mouse_double_clicked.emit(event)
        if not event.isAccepted():
            event.setAccepted(initial_state)
            super(CodeEdit, self).mouseDoubleClickEvent(event)

    def focusInEvent(self, event):
        """
        Overrides focusInEvent to emits the focused_in signal

        :param event: QFocusEvent
        """
        self.focused_in.emit(event)
        super(CodeEdit, self).focusInEvent(event)

    def focusOutEvent(self, event):
        # Saves content if save_on_focus_out is True.
        if self._save_on_focus_out and self.dirty and self.file.path:
            self.file.save()
        super(CodeEdit, self).focusOutEvent(event)

    def mousePressEvent(self, event):
        """
        Overrides mousePressEvent to emits mouse_pressed signal

        :param event: QMouseEvent
        """
        initial_state = event.isAccepted()
        event.ignore()
        self.mouse_pressed.emit(event)
        if event.button() == QtCore.Qt.LeftButton:
            cursor = self.cursorForPosition(event.pos())
            for sel in self.decorations:
                if sel.cursor.blockNumber() == cursor.blockNumber():
                    if sel.contains_cursor(cursor):
                        sel.signals.clicked.emit(sel)
        if not event.isAccepted():
            event.setAccepted(initial_state)
            super(CodeEdit, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        """
        Emits mouse_released signal.

        :param event: QMouseEvent
        """
        initial_state = event.isAccepted()
        event.ignore()
        self.mouse_released.emit(event)
        if not event.isAccepted():
            event.setAccepted(initial_state)
            super(CodeEdit, self).mouseReleaseEvent(event)

    def wheelEvent(self, event):
        """
        Emits the mouse_wheel_activated signal.

        :param event: QMouseEvent
        """
        initial_state = event.isAccepted()
        event.ignore()
        self.mouse_wheel_activated.emit(event)
        if not event.isAccepted():
            event.setAccepted(initial_state)
            super(CodeEdit, self).wheelEvent(event)

    def mouseMoveEvent(self, event):
        """
        Overrides mouseMovedEvent to display any decoration tooltip and emits
        the mouse_moved event.

        :param event: QMouseEvent
        """
        cursor = self.cursorForPosition(event.pos())
        self._last_mouse_pos = event.pos()
        block_found = False
        for sel in self.decorations:
            if sel.contains_cursor(cursor) and sel.tooltip:
                if (self._prev_tooltip_block_nbr != cursor.blockNumber()
                        or not QtWidgets.QToolTip.isVisible()):
                    pos = event.pos()
                    # add left margin
                    pos.setX(pos.x() + self.panels.margin_size())
                    # add top margin
                    pos.setY(pos.y() + self.panels.margin_size(0))
                    self._tooltips_runner.request_job(self.show_tooltip,
                                                      self.mapToGlobal(pos),
                                                      sel.tooltip[0:1024], sel)
                    self._prev_tooltip_block_nbr = cursor.blockNumber()
                block_found = True
                break
        if not block_found and self._prev_tooltip_block_nbr != -1:
            QtWidgets.QToolTip.hideText()
            self._prev_tooltip_block_nbr = -1
            self._tooltips_runner.cancel_requests()
        self.mouse_moved.emit(event)
        super(CodeEdit, self).mouseMoveEvent(event)

    def showEvent(self, event):
        """ Overrides showEvent to update the viewport margins """
        super(CodeEdit, self).showEvent(event)
        self.panels.refresh()

    def setReadOnly(self, read_only):
        if read_only != self.isReadOnly():
            super(CodeEdit, self).setReadOnly(read_only)
            from pyqode.core.panels import ReadOnlyPanel
            try:
                panel = self.panels.get(ReadOnlyPanel)
            except KeyError:
                self.panels.append(ReadOnlyPanel(), ReadOnlyPanel.Position.TOP)
            else:
                panel.setVisible(read_only)

    def get_context_menu(self):
        """
        Gets the editor context menu.

        :return: QMenu
        """
        mnu = QtWidgets.QMenu()
        mnu.addActions(self._actions)
        mnu.addSeparator()
        for menu in self._menus:
            mnu.addMenu(menu)
        return mnu

    def _show_context_menu(self, point):
        """ Shows the context menu """
        tc = self.textCursor()
        nc = self.cursorForPosition(point)
        if not nc.position() in range(tc.selectionStart(), tc.selectionEnd()):
            self.setTextCursor(nc)
        self._mnu = self.get_context_menu()
        if len(self._mnu.actions()) > 1 and self.show_context_menu:
            self._mnu.popup(self.mapToGlobal(point))

    def _set_whitespaces_flags(self, show):
        """ Sets show white spaces flag """
        doc = self.document()
        options = doc.defaultTextOption()
        if show:
            options.setFlags(options.flags()
                             | QtGui.QTextOption.ShowTabsAndSpaces)
        else:
            options.setFlags(options.flags()
                             & ~QtGui.QTextOption.ShowTabsAndSpaces)
        doc.setDefaultTextOption(options)

    def _init_actions(self, create_standard_actions):
        """ Init context menu action """
        menu_advanced = QtWidgets.QMenu(_('Advanced'))
        self.add_menu(menu_advanced)
        self._sub_menus = {'Advanced': menu_advanced}
        if create_standard_actions:
            # Undo
            action = QtWidgets.QAction(_('Undo'), self)
            action.setShortcut('Ctrl+Z')
            action.setIcon(
                icons.icon('edit-undo', ':/pyqode-icons/rc/edit-undo.png',
                           'fa.undo'))
            action.triggered.connect(self.undo)
            self.undoAvailable.connect(action.setVisible)
            action.setVisible(False)
            self.add_action(action, sub_menu=None)
            self.action_undo = action
            # Redo
            action = QtWidgets.QAction(_('Redo'), self)
            action.setShortcut('Ctrl+Y')
            action.setIcon(
                icons.icon('edit-redo', ':/pyqode-icons/rc/edit-redo.png',
                           'fa.repeat'))
            action.triggered.connect(self.redo)
            self.redoAvailable.connect(action.setVisible)
            action.setVisible(False)
            self.add_action(action, sub_menu=None)
            self.action_redo = action
            # Copy
            action = QtWidgets.QAction(_('Copy'), self)
            action.setShortcut(QtGui.QKeySequence.Copy)
            action.setIcon(
                icons.icon('edit-copy', ':/pyqode-icons/rc/edit-copy.png',
                           'fa.copy'))
            action.triggered.connect(self.copy)
            self.add_action(action, sub_menu=None)
            self.action_copy = action
            # cut
            action = QtWidgets.QAction(_('Cut'), self)
            action.setShortcut(QtGui.QKeySequence.Cut)
            action.setIcon(
                icons.icon('edit-cut', ':/pyqode-icons/rc/edit-cut.png',
                           'fa.cut'))
            action.triggered.connect(self.cut)
            self.add_action(action, sub_menu=None)
            self.action_cut = action
            # paste
            action = QtWidgets.QAction(_('Paste'), self)
            action.setShortcut(QtGui.QKeySequence.Paste)
            action.setIcon(
                icons.icon('edit-paste', ':/pyqode-icons/rc/edit-paste.png',
                           'fa.paste'))
            action.triggered.connect(self.paste)
            self.add_action(action, sub_menu=None)
            self.action_paste = action
        # duplicate line
        action = QtWidgets.QAction(_('Duplicate line'), self)
        action.setShortcut('Ctrl+D')
        action.triggered.connect(self.duplicate_line)
        self.add_action(action, sub_menu=None)
        self.action_duplicate_line = action
        # swap line up
        action = QtWidgets.QAction(_('Swap line up'), self)
        action.setShortcut(
            QtGui.QKeySequence(QtCore.Qt.AltModifier + QtCore.Qt.Key_Up))
        action.triggered.connect(self.swapLineUp)
        self.add_action(action, sub_menu=None)
        self.action_swap_line_up = action
        # swap line down
        action = QtWidgets.QAction(_('Swap line down'), self)
        action.setShortcut(
            QtGui.QKeySequence(QtCore.Qt.AltModifier + QtCore.Qt.Key_Down))
        action.triggered.connect(self.swapLineDown)
        self.add_action(action, sub_menu=None)
        self.action_swap_line_down = action
        # select all
        action = QtWidgets.QAction(_('Select all'), self)
        action.setShortcut(QtGui.QKeySequence.SelectAll)
        action.triggered.connect(self.selectAll)
        self.action_select_all = action
        self.add_action(self.action_select_all, sub_menu=None)
        self.add_separator(sub_menu=None)
        if create_standard_actions:
            # indent
            action = QtWidgets.QAction(_('Indent'), self)
            action.setShortcut('Tab')
            action.setIcon(
                icons.icon('format-indent-more',
                           ':/pyqode-icons/rc/format-indent-more.png',
                           'fa.indent'))
            action.triggered.connect(self.indent)
            self.add_action(action)
            self.action_indent = action
            # unindent
            action = QtWidgets.QAction(_('Un-indent'), self)
            action.setShortcut('Shift+Tab')
            action.setIcon(
                icons.icon('format-indent-less',
                           ':/pyqode-icons/rc/format-indent-less.png',
                           'fa.dedent'))
            action.triggered.connect(self.un_indent)
            self.add_action(action)
            self.action_un_indent = action
            self.add_separator()
        # goto
        action = QtWidgets.QAction(_('Go to line'), self)
        action.setShortcut('Ctrl+G')
        action.setIcon(
            icons.icon('go-jump', ':/pyqode-icons/rc/goto-line.png',
                       'fa.share'))
        action.triggered.connect(self.goto_line)
        self.add_action(action)
        self.action_goto_line = action

    def _init_settings(self):
        """ Init setting """
        self._show_whitespaces = False
        self._tab_length = 4
        self._use_spaces_instead_of_tabs = True
        self.setTabStopWidth(self._tab_length * self.fontMetrics().width(" "))
        self._set_whitespaces_flags(self._show_whitespaces)

    def _init_style(self):
        """ Inits style options """
        self._background = QtGui.QColor('white')
        self._foreground = QtGui.QColor('black')
        self._whitespaces_foreground = QtGui.QColor('light gray')
        app = QtWidgets.QApplication.instance()
        self._sel_background = app.palette().highlight().color()
        self._sel_foreground = app.palette().highlightedText().color()
        self._font_size = 10
        self.font_name = ""

    def _update_visible_blocks(self, *args):
        """ Updates the list of visible blocks """
        self._visible_blocks[:] = []
        block = self.firstVisibleBlock()
        block_nbr = block.blockNumber()
        top = int(
            self.blockBoundingGeometry(block).translated(
                self.contentOffset()).top())
        bottom = top + int(self.blockBoundingRect(block).height())
        ebottom_top = 0
        ebottom_bottom = self.height()
        while block.isValid():
            visible = (top >= ebottom_top and bottom <= ebottom_bottom)
            if not visible:
                break
            if block.isVisible():
                self._visible_blocks.append((top, block_nbr, block))
            block = block.next()
            top = bottom
            bottom = top + int(self.blockBoundingRect(block).height())
            block_nbr = block.blockNumber()

    def _on_text_changed(self):
        """ Adjust dirty flag depending on editor's content """
        if not self._cleaning:
            ln = TextHelper(self).cursor_position()[0]
            self._modified_lines.add(ln)

    def _reset_stylesheet(self):
        """ Resets stylesheet"""
        self.setFont(
            QtGui.QFont(self._font_family, self._font_size + self._zoom_level))
        flg_stylesheet = hasattr(self, '_flg_stylesheet')
        if QtWidgets.QApplication.instance().styleSheet() or flg_stylesheet:
            self._flg_stylesheet = True
            # On Window, if the application once had a stylesheet, we must
            # keep on using a stylesheet otherwise strange colors appear
            # see https://github.com/OpenCobolIDE/OpenCobolIDE/issues/65
            # Also happen on plasma 5
            try:
                plasma = os.environ['DESKTOP_SESSION'] == 'plasma'
            except KeyError:
                plasma = False
            if sys.platform == 'win32' or plasma:
                self.setStyleSheet('''QPlainTextEdit
                {
                    background-color: %s;
                    color: %s;
                }
                ''' % (self.background.name(), self.foreground.name()))
            else:
                # on linux/osx we just have to set an empty stylesheet to
                # cancel any previous stylesheet and still keep a correct
                # style for scrollbars
                self.setStyleSheet('')
        else:
            p = self.palette()
            p.setColor(QtGui.QPalette.Base, self.background)
            p.setColor(QtGui.QPalette.Text, self.foreground)
            p.setColor(QtGui.QPalette.Highlight, self.selection_background)
            p.setColor(QtGui.QPalette.HighlightedText,
                       self.selection_foreground)
            self.setPalette(p)
        self.repaint()

    def _do_home_key(self, event=None, select=False):
        """ Performs home key action """
        # get nb char to first significative char
        delta = (self.textCursor().positionInBlock() -
                 TextHelper(self).line_indent())
        cursor = self.textCursor()
        move = QtGui.QTextCursor.MoveAnchor
        if select:
            move = QtGui.QTextCursor.KeepAnchor
        if delta > 0:
            cursor.movePosition(QtGui.QTextCursor.Left, move, delta)
        else:
            cursor.movePosition(QtGui.QTextCursor.StartOfBlock, move)
        self.setTextCursor(cursor)
        if event:
            event.accept()

    def _emit_dirty_changed(self, state):
        self.dirty_changed.emit(state)
        for c in self.clones:
            c.dirty_changed.emit(state)
Ejemplo n.º 7
0
class JsonTcpClient(QtNetwork.QTcpSocket):
    """
    A json tcp client socket used to start and communicate with the pyqode
    backend.

    It uses a simple message protocol. A message is made up of two parts.
    parts:
      - header: contains the length of the payload. (4bytes)
      - payload: data as a json string.

    """
    #: Internal signal emitted when the backend request finished and the
    #: socket can be removed from the list of sockets maintained by the
    #: backend manager
    finished = QtCore.Signal(QtNetwork.QTcpSocket)

    def __init__(self, parent, port, worker_class_or_function, args,
                 on_receive=None):
        super(JsonTcpClient, self).__init__(parent)
        self._port = port
        self._worker = worker_class_or_function
        self._args = args
        self._header_complete = False
        self._header_buf = bytes()
        self._to_read = 0
        self._data_buf = bytes()
        if on_receive:
            try:
                self._callback = WeakMethod(on_receive)
            except TypeError:
                # unbound method (i.e. free function)
                self._callback = ref(on_receive)
        else:
            self._callback = None
        self.is_connected = False
        self._closed = False
        self.connected.connect(self._on_connected)
        self.error.connect(self._on_error)
        self.disconnected.connect(self._on_disconnected)
        self.readyRead.connect(self._on_ready_read)
        self._connect()

    def close(self):
        self._closed = True  # fix issue with QTimer.singleShot
        super(JsonTcpClient, self).close()
        self._callback = None

    def _send_request(self):
        """
        Sends the request to the backend.
        """
        if isinstance(self._worker, str):
            classname = self._worker
        else:
            classname = '%s.%s' % (self._worker.__module__,
                                   self._worker.__name__)
        self.request_id = str(uuid.uuid4())
        self.send({'request_id': self.request_id, 'worker': classname,
                   'data': self._args})

    def send(self, obj, encoding='utf-8'):
        """
        Sends a python object to the backend. The object **must be JSON
        serialisable**.

        :param obj: object to send
        :param encoding: encoding used to encode the json message into a
            bytes array, this should match CodeEdit.file.encoding.
        """
        comm('sending request: %r', obj)
        msg = json.dumps(obj)
        msg = msg.encode(encoding)
        header = struct.pack('=I', len(msg))
        self.write(header)
        self.write(msg)

    @staticmethod
    def pick_free_port():
        """ Picks a free port """
        test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        test_socket.bind(('127.0.0.1', 0))
        free_port = int(test_socket.getsockname()[1])
        test_socket.close()
        return free_port

    def _connect(self):
        """ Connects our client socket to the backend socket """
        if self is None:
            return
        comm('connecting to 127.0.0.1:%d', self._port)
        address = QtNetwork.QHostAddress('127.0.0.1')
        self.connectToHost(address, self._port)
        if sys.platform == 'darwin':
            self.waitForConnected()

    def _on_connected(self):
        comm('connected to backend: %s:%d', self.peerName(), self.peerPort())
        self.is_connected = True
        self._send_request()

    def _on_error(self, error):
        if error not in SOCKET_ERROR_STRINGS:  # pragma: no cover
            error = -1
        if error == 1 and self.is_connected or (
                not self.is_connected and error == 0 and not self._closed):
            log_fct = comm
        else:
            log_fct = _logger().warning

        if error == 0 and not self.is_connected and not self._closed:
            QtCore.QTimer.singleShot(100, self._connect)

        log_fct(SOCKET_ERROR_STRINGS[error])

    def _on_disconnected(self):
        try:
            comm('disconnected from backend: %s:%d', self.peerName(),
                 self.peerPort())
        except (AttributeError, RuntimeError):
            # logger might be None if for some reason qt deletes the socket
            # after python global exit
            pass
        try:
            self.is_connected = False
        except AttributeError:
            pass

    def _read_header(self):
        comm('reading header')
        self._header_buf += self.read(4)
        if len(self._header_buf) == 4:
            self._header_complete = True
            try:
                header = struct.unpack('=I', self._header_buf)
            except TypeError:
                # pyside
                header = struct.unpack('=I', self._header_buf.data())
            self._to_read = header[0]
            self._header_buf = bytes()
            comm('header content: %d', self._to_read)

    def _read_payload(self):
        """ Reads the payload (=data) """
        comm('reading payload data')
        comm('remaining bytes to read: %d', self._to_read)
        data_read = self.read(self._to_read)
        nb_bytes_read = len(data_read)
        comm('%d bytes read', nb_bytes_read)
        self._data_buf += data_read
        self._to_read -= nb_bytes_read
        if self._to_read <= 0:
            try:
                data = self._data_buf.decode('utf-8')
            except AttributeError:
                data = bytes(self._data_buf.data()).decode('utf-8')
            comm('payload read: %r', data)
            comm('payload length: %r', len(self._data_buf))
            comm('decoding payload as json object')
            obj = json.loads(data)
            comm('response received: %r', obj)
            try:
                results = obj['results']
            except (KeyError, TypeError):
                results = None
            # possible callback
            if self._callback and self._callback():
                self._callback()(results)
            self._header_complete = False
            self._data_buf = bytes()
            self.finished.emit(self)

    def _on_ready_read(self):
        """ Read bytes when ready read """
        while self.bytesAvailable():
            if not self._header_complete:
                self._read_header()
            else:
                self._read_payload()
Ejemplo n.º 8
0
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, sub_menu='Python')
        else:
            self.editor.remove_action(self.action_goto, sub_menu='Python')

    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)
Ejemplo n.º 9
0
class FileWatcherMode(Mode, QtCore.QObject):
    """ Watches the current file for external modifications.

    FileWatcher mode, check if the opened file has changed externally.

    """
    #: Signal emitted when the file has been deleted. The Signal is emitted
    #: with the current editor instance so that user have a chance to close
    #: the editor.
    file_deleted = QtCore.Signal(object)

    #: Signal emitted when the file has been reloaded in the editor.
    file_reloaded = QtCore.Signal()

    @property
    def auto_reload(self):
        """
        Automatically reloads changed files
        """
        return self._auto_reload

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

    def __init__(self):
        QtCore.QObject.__init__(self)
        Mode.__init__(self)
        self._auto_reload = False
        self._flg_notify = False
        self._data = (None, None)
        self._timer = QtCore.QTimer()
        self._timer.setInterval(1000)
        self._timer.timeout.connect(self._check_file)
        self._mtime = 0
        self._notification_pending = False
        self._processing = False

    def on_state_changed(self, state):
        if state:
            self.editor.new_text_set.connect(self._update_mtime)
            self.editor.new_text_set.connect(self._timer.start)
            self.editor.text_saving.connect(self._cancel_next_change)
            self.editor.text_saved.connect(self._update_mtime)
            self.editor.text_saved.connect(self._restart_monitoring)
            self.editor.focused_in.connect(self._check_for_pending)
        else:
            self._timer.stop()
            self.editor.new_text_set.connect(self._update_mtime)
            self.editor.new_text_set.connect(self._timer.start)
            self.editor.text_saving.disconnect(self._cancel_next_change)
            self.editor.text_saved.disconnect(self._restart_monitoring)
            self.editor.focused_in.disconnect(self._check_for_pending)
            self._timer.stop()

    def _cancel_next_change(self):
        self._timer.stop()
        for e in self.editor.clones:
            try:
                w = e.modes.get(self.__class__)
            except KeyError:
                pass
            else:
                w._cancel_next_change()

    def _restart_monitoring(self):
        self._update_mtime()
        for e in self.editor.clones:
            try:
                w = e.modes.get(self.__class__)
            except KeyError:
                pass
            else:
                w._restart_monitoring()
        self._timer.start()

    def _update_mtime(self):
        """ Updates modif time """
        try:
            self._mtime = os.path.getmtime(self.editor.file.path)
        except OSError:
            # file_path does not exists.
            self._mtime = 0
            self._timer.stop()
        except (TypeError, AttributeError):
            # file path is none, this happen if you use setPlainText instead of
            # openFile. This is perfectly fine, we just do not have anything to
            # watch
            try:
                self._timer.stop()
            except AttributeError:
                pass

    def _check_file(self):
        """
        Checks watched file moficiation time and permission changes.
        """
        try:
            self.editor.toPlainText()
        except RuntimeError:
            self._timer.stop()
            return
        if self.editor and self.editor.file.path:
            if not os.path.exists(self.editor.file.path) and self._mtime:
                self._notify_deleted_file()
            else:
                mtime = os.path.getmtime(self.editor.file.path)
                if mtime > self._mtime:
                    self._mtime = mtime
                    self._notify_change()
                # check for permission change
                writeable = os.access(self.editor.file.path, os.W_OK)
                self.editor.setReadOnly(not writeable)

    def _notify(self, title, message, expected_action=None):
        """
        Notify user from external event
        """
        inital_value = self.editor.save_on_focus_out
        self.editor.save_on_focus_out = False
        self._flg_notify = True
        dlg_type = (QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
        expected_action = (
            lambda *x: None) if not expected_action else expected_action
        if (self._auto_reload or QtWidgets.QMessageBox.question(
                self.editor, title, message, dlg_type,
                QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes):
            expected_action(self.editor.file.path)
        self._update_mtime()
        self.editor.save_on_focus_out = inital_value

    def _notify_change(self):
        """
        Notify user from external change if autoReloadChangedFiles is False
        then reload the changed file in the editor
        """
        def inner_action(*args):
            """ Inner action: open file """
            # cache cursor position before reloading so that the cursor
            # position is restored automatically after reload has finished.
            # See OpenCobolIDE/OpenCobolIDE#97
            Cache().set_cursor_position(self.editor.file.path,
                                        self.editor.textCursor().position())
            self.editor.file.open(self.editor.file.path)
            self.file_reloaded.emit()

        args = ("File changed",
                "The file <i>%s</i> has changed externally.\nDo you want to "
                "reload it?" % os.path.basename(self.editor.file.path))
        kwargs = {"expected_action": inner_action}
        if self.editor.hasFocus() or self.auto_reload:
            self._notify(*args, **kwargs)
        else:
            # show the reload prompt as soon as the editor has focus
            self._notification_pending = True
            self._data = (args, kwargs)

    def _check_for_pending(self, *args, **kwargs):
        """
        Checks if a notification is pending.
        """
        if self._notification_pending and not self._processing:
            self._processing = True
            args, kwargs = self._data
            self._notify(*args, **kwargs)
            self._notification_pending = False
            self._processing = False

    def _notify_deleted_file(self):
        """
        Notify user from external file deletion.
        """
        self.file_deleted.emit(self.editor)
        # file deleted, disable file watcher
        self.enabled = False

    def clone_settings(self, original):
        self.auto_reload = original.auto_reload
Ejemplo n.º 10
0
class EncodingPanel(Panel):
    """ Displays a warning when an encoding error occured and let you reload.

    This panel display a warning in case encoding/decoding error and
    give the user the possibility to try out another encoding, to edit any way
    or to close the editor.

    The panel is automatically shown by
    :class:`pyqode.core.managers.FileManager` in case of error so that you
    don't have to worry about encoding issues. The only think you might do
    is to provide to your user a way to specify the default encoding, i.e. the
    one that is tried before showing this panel.

    The panel is a simple widget with a label describing the error, an encoding
    menu and 3 buttons: ``Retry``, ``Edit`` anyway and ``Cancel``. It is
    strongly inspired by the GEdit encoding panel.

    You can change the background color and the label foreground color by
    setting up the ``color`` and ``foreground`` properties.

    It's up to the client code to handle cancel requests. To do that simply
    connect ``cancel_requested`` signal to remove the editor from your
    application.

    """
    #: Signal emitted when the user pressed on cancel. It is up to the client
    #: code to handle this event.
    cancel_requested = QtCore.Signal(object)

    _description = ('<html><head/><body><p><span style=" font-weight:600;">%s'
                    '</span></p><p><span style=" font-size:9pt;">'
                    'The file you opened has some invalid characters. '
                    'If you continue editing this file you could corrupt this '
                    'document. You can also choose another character encoding '
                    'and try again.</span></p></body></html>')

    @property
    def color(self):
        """
        Returns the panel color.
        """
        return self._color

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

    @property
    def foreground(self):
        return self._foreground

    @foreground.setter
    def foreground(self, value):
        self._foreground = value
        self._refresh_stylesheet()
        # propagate changes to every clone
        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

    def _refresh_stylesheet(self):
        try:
            self._lbl_stylesheet = (
                'color: %s;background: %s' %
                (self._foreground.name(), self._color.name()))
            for lbl in self._labels:
                lbl.setStyleSheet(self._lbl_stylesheet)
        except AttributeError:
            pass

    def __init__(self, add_context_menu=True):
        super(EncodingPanel, self).__init__(dynamic=True)
        # leave it here otherwise you will have circular import errors
        from pyqode.core._forms.pnl_encoding_ui import Ui_Form
        self.ui = Ui_Form()
        self.ui.setupUi(self)
        self.__add_ctx_mnu = add_context_menu
        self._labels = [self.ui.label, self.ui.lblDescription]
        self._color = None
        self.color = QtGui.QColor('#8AADD4')
        self._foreground = None
        self.foreground = QtGui.QColor('#FFFFFF')
        self._deco = None
        self.ui.pushButtonRetry.clicked.connect(self._reload)
        self.ui.pushButtonEdit.clicked.connect(self._edit_anyway)
        self.ui.pushButtonCancel.clicked.connect(self.cancel)
        self.hide()

    def enable_caret_line(self, value=True):
        try:
            from pyqode.core.modes import CaretLineHighlighterMode

            mode = self.editor.modes.get(CaretLineHighlighterMode)
        except KeyError:
            pass
        else:
            mode.enabled = value

    def on_open_failed(self, path, encoding):
        self.enable_caret_line(False)
        self.ui.comboBoxEncodings.current_encoding = encoding
        self.ui.lblDescription.setText(
            self._description %
            (_('There was a problem opening the file %r') % path))
        # load text as binary and mark it as red, user might make use the
        # binary to recognize the original encoding
        try:
            with open(path, 'rb') as file:
                content = str(file.read(16))
        except OSError:
            content = ''
        # set plain text
        self.editor.setPlainText(content, self.editor.file.get_mimetype(path),
                                 self.editor.file.encoding)
        self.editor.setDocumentTitle(self.editor.file.name)
        self.editor.setWindowTitle(self.editor.file.name)

        # Delay because the editor might not have been shown yet
        QtCore.QTimer.singleShot(1, self.show)

    def show(self):
        super(EncodingPanel, self).show()
        self.editor.selectAll()
        self._deco = TextDecoration(self.editor.textCursor())
        self._deco.set_background(QtCore.Qt.red)
        self._deco.set_foreground(QtCore.Qt.black)
        self.editor.decorations.append(self._deco)
        cursor = self.editor.textCursor()
        cursor.clearSelection()
        cursor.setPosition(0)
        self.editor.setTextCursor(cursor)
        self.editor.setReadOnly(True)

    def paintEvent(self, event):
        """ Fills the panel background. """
        super(EncodingPanel, self).paintEvent(event)
        if self.isVisible():
            # fill background
            painter = QtGui.QPainter(self)
            self._background_brush = QtGui.QBrush(self._color)
            painter.fillRect(event.rect(), self._background_brush)

    def on_install(self, editor):
        super(EncodingPanel, self).on_install(editor)
        if self.__add_ctx_mnu:
            # add context menu
            from pyqode.core.widgets import EncodingsContextMenu
            EncodingsContextMenu(parent=editor)

    def _reload(self):
        self.hide()
        self._rm_deco()
        self.editor.setReadOnly(False)
        self.enable_caret_line(True)
        self.editor.file.reload(self.ui.comboBoxEncodings.current_encoding)

    def _edit_anyway(self):
        self._rm_deco()
        self.editor.setReadOnly(False)
        self.enable_caret_line(True)
        self.hide()

    def _rm_deco(self):
        if self._deco:
            self.editor.decorations.remove(self._deco)
            self._deco = None

    def cancel(self):
        if self.sender():
            self.editor.clear()
        self._rm_deco()
        self.enable_caret_line(True)
        self.cancel_requested.emit(self.editor)
        self.hide()

    def clone_settings(self, original):
        self.color = original.color
        self.foreground = original.foreground
Ejemplo n.º 11
0
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.is_collapsed(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[0], PYSIDE2_API[0]]:
                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.is_collapsed(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).
        """
        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 _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).
        """
        delete_request = event.key() in [QtCore.Qt.Key_Backspace,
                                         QtCore.Qt.Key_Delete]
        if event.text() or delete_request:
            cursor = self.editor.textCursor()
            if cursor.hasSelection():
                # change selection to encompass the whole scope.
                positions_to_check = cursor.selectionStart(), cursor.selectionEnd()
            else:
                positions_to_check = (cursor.position(), )
            for pos in positions_to_check:
                block = self.editor.document().findBlock(pos)
                th = TextBlockHelper()
                if th.is_fold_trigger(block) and th.is_collapsed(block):
                    self.toggle_fold_trigger(block)
                    if delete_request and cursor.hasSelection():
                        scope = FoldScope(self.find_parent_scope(block))
                        tc = TextHelper(self.editor).select_lines(*scope.get_range())
                        if tc.selectionStart() > cursor.selectionStart():
                            start = cursor.selectionStart()
                        else:
                            start = tc.selectionStart()
                        if tc.selectionEnd() < cursor.selectionEnd():
                            end = cursor.selectionEnd()
                        else:
                            end = tc.selectionEnd()
                        tc.setPosition(start)
                        tc.setPosition(end, tc.KeepAnchor)
                        self.editor.setTextCursor(tc)

    @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_collapsed(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_collapsed(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