Beispiel #1
0
    def init_features(self):
        """
        Initialise custom Editor features.
        """
        if self._features_initialised:
            return
        self._features_initialised = True

        # QSyntaxHighlighter causes
        # textChanged to be emitted,
        # which we don't want.
        self.emit_text_changed = False
        syntaxhighlighter.Highlight(self.document(), self)

        def set_text_changed_enabled():
            self.emit_text_changed = True

        QTimer.singleShot(0, set_text_changed_enabled)

        CM = contextmenu.ContextMenu
        self.contextmenu = CM(self)

        # TODO: add a new autocompleter
        # that uses DirectConnection.
        self.autocomplete_overriding = True
        AC = autocompletion.AutoCompleter
        self.autocomplete = AC(self)

        if self._handle_shortcuts:
            actions.Actions(editor=self)
            shortcuts.ShortcutHandler(editor=self)
Beispiel #2
0
    def __init__(self,
                 parent=None,
                 handle_shortcuts=True,
                 uid=None,
                 init_features=True):
        super(Editor, self).__init__(parent)
        self.setObjectName('Editor')
        self.setAcceptDrops(True)

        DEFAULT_FONT = constants.DEFAULT_FONT
        df = 'PYTHONEDITOR_DEFAULT_FONT'
        if os.getenv(df) is not None:
            DEFAULT_FONT = os.environ[df]
        font = QFont(DEFAULT_FONT)
        font.setPointSize(10)
        self.setFont(font)
        self.setMouseTracking(True)
        self.setCursorWidth(2)
        self.setStyleSheet("""
        QToolTip {
        color: #F6F6F6;
        background-color: rgb(45, 42, 46);
        }
        """)

        # instance variables
        if uid is None:
            uid = str(uuid.uuid1())
        self.shortcut_overrode_keyevent = False
        self._changed = False
        self.autocomplete_overriding = False
        self._handle_shortcuts = handle_shortcuts
        self._features_initialised = False
        self._key_pressed = False
        self.last_key_pressed = ''

        self.emit_text_changed = True
        self.textChanged.connect(self._handle_textChanged)

        self._selection_timer = QTimer()
        self._selection_timer.setInterval(1000)
        self._selection_timer.setSingleShot(True)
        self._selection_timer.timeout.connect(self.selection_stopped.emit)
        self.selectionChanged.connect(self._handle_selectionChanged)

        linenumberarea.LineNumberArea(self)

        if init_features:
            self.init_features()
Beispiel #3
0
    def __init__(self):
        super(Terminal, self).__init__()

        self.setObjectName('Terminal')
        self.setWindowFlags(Qt.WindowStaysOnTopHint)
        self.setReadOnly(True)
        self.destroyed.connect(self.stop)
        font = QFont(DEFAULT_FONT)
        font.setPointSize(10)
        self.setFont(font)

        if os.getenv(STARTUP) == '1':
            self.setup()
        else:
            QTimer.singleShot(0, self.setup)
Beispiel #4
0
    def set_editor_focus(self):
        """ Set the focus inside the editor.
        """
        try:
            retries = self.retries
        except AttributeError:
            self.retries = 0

        if self.retries > 4:
            return

        if not hasattr(self, 'python_editor'):
            QTimer.singleShot(100, self.set_editor_focus)
            self.retries += 1
            return

        editor = self.python_editor.tabeditor.editor
        if not editor.isVisible():
            QTimer.singleShot(100, self.set_editor_focus)
            self.retries += 1
            return
        editor.focus_in_signal.emit()
Beispiel #5
0
    def reload_package(self):
        """ Reloads the whole package (except for
        this module), in an order that does not
        cause errors.
        """
        self.python_editor.terminal.stop()
        self.python_editor.deleteLater()
        del self.python_editor

        # reload modules in the order
        # that they were loaded in
        for name in PYTHON_EDITOR_MODULES:
            mod = sys.modules.get(name)
            if mod is None:
                continue

            path = mod.__file__
            if path.endswith('.pyc'):
                path = path.replace('.pyc', '.py')
            if not os.path.isfile(path):
                continue
            with open(path, 'r') as f:
                data = f.read()
            if '\x00' in data:
                msg = 'Cannot load {0} due to Null bytes. Path:\n{1}'
                print(msg.format(mod, path))
                continue
            try:
                code = compile(data, mod.__file__, 'exec')
            except SyntaxError:
                # This message only shows in terminal
                # if this environment variable is set:
                # PYTHONEDITOR_CAPTURE_STARTUP_STREAMS
                error = traceback.format_exc()
                msg = 'Could not reload due to the following error:'

                def print_error():
                    print(msg)
                    print(error)

                QTimer.singleShot(100, print_error)
                continue
            try:
                reload_module(mod)
            except ImportError:
                msg = 'could not reload {0}: {1}'
                print(msg.format(name, mod))

        QTimer.singleShot(1, self.buildUI)
        QTimer.singleShot(10, self.set_editor_focus)
Beispiel #6
0
    def __init__(self, parent=None):
        super(TabEditor, self).__init__(parent)
        if parent is not None:
            self.setParent(parent)

        self.setLayout(
            QVBoxLayout(self)
        )
        self.layout().setContentsMargins(0,0,0,0)

        self.tab_widget = QWidget()
        twl = QHBoxLayout(
            self.tab_widget
        )
        self.tab_widget_layout = twl
        self.tab_widget_layout.setContentsMargins(
            0,0,0,0
        )

        lb = QToolButton()
        self.tab_left_button = lb
        lb.setArrowType(Qt.LeftArrow)
        lb.setAutoRaise(True)
        lb.setToolTip('Go to previous tab.')
        self.tab_widget_layout.addWidget(lb)
        self.tab_left_button.clicked.connect(self.tab_left)

        self.tabs = Tabs()
        self.tab_widget_layout.addWidget(self.tabs)

        rb = QToolButton()
        self.tab_right_button = rb
        rb.setArrowType(Qt.RightArrow)
        rb.setAutoRaise(True)
        rb.setToolTip('Go to next tab.')
        self.tab_widget_layout.addWidget(rb)
        self.tab_right_button.clicked.connect(self.tab_right)

        # add corner buttons
        tb = QToolButton()
        self.tab_list_button = tb
        tb.setArrowType(Qt.DownArrow)
        tb.setToolTip(
            'Click for a list of tabs.'
        )
        tb.setAutoRaise(True)
        self.tab_list_button.clicked.connect(
            self.show_tab_menu
        )

        nb = QToolButton()
        self.new_tab_button = nb
        nb.setToolTip(
            'Click to add a new tab.'
        )
        nb.setText('+')
        nb.setAutoRaise(True)
        self.new_tab_button.clicked.connect(
            self.new_tab
        )

        for button in [
                self.new_tab_button,
                self.tab_list_button
            ]:
            self.tab_widget_layout.addWidget(button)

        self.editor = editor.Editor(
            parent=self,
            handle_shortcuts=False
        )

        for widget in self.tab_widget, self.editor:
            self.layout().addWidget(widget)

        # Give the autosave a chance to load all
        # tabs before connecting signals between
        # tabs and editor.
        QTimer.singleShot(
            0,
            self.connect_signals
        )

        self.setStyleSheet(TAB_STYLESHEET)
Beispiel #7
0
class Editor(QPlainTextEdit):
    """Code Editor widget. Extends QPlainTextEdit to
    provide (through separate modules):
    - Line Number Area
    - Syntax Highlighting
    - Autocompletion (of Python code)
    - Shortcuts for code editing
    - Custom Context Menu
    - Signals for connecting the Editor to other
        UI elements.
    """
    wrap_types = [
        '\'',
        '"',
        '[',
        ']',
        '(',
        ')',
        '{',
        '}',
    ]

    wrap_signal = Signal(str)
    uuid_signal = Signal(str)
    return_signal = Signal(QKeyEvent)
    focus_in_signal = Signal()
    focus_out_signal = Signal()
    post_key_pressed_signal = Signal(QKeyEvent)
    wheel_signal = Signal(QWheelEvent)
    key_pressed_signal = Signal(QKeyEvent)
    shortcut_signal = Signal(QKeyEvent)
    resize_signal = Signal(QResizeEvent)
    context_menu_signal = Signal(QMenu)
    tab_signal = Signal()
    home_key_signal = Signal()
    relay_clear_output_signal = Signal()
    editingFinished = Signal()
    text_changed_signal = Signal()
    selection_stopped = Signal()

    def __init__(self,
                 parent=None,
                 handle_shortcuts=True,
                 uid=None,
                 init_features=True):
        super(Editor, self).__init__(parent)
        self.setObjectName('Editor')
        self.setAcceptDrops(True)

        DEFAULT_FONT = constants.DEFAULT_FONT
        df = 'PYTHONEDITOR_DEFAULT_FONT'
        if os.getenv(df) is not None:
            DEFAULT_FONT = os.environ[df]
        font = QFont(DEFAULT_FONT)
        font.setPointSize(10)
        self.setFont(font)
        self.setMouseTracking(True)
        self.setCursorWidth(2)
        self.setStyleSheet("""
        QToolTip {
        color: #F6F6F6;
        background-color: rgb(45, 42, 46);
        }
        """)

        # instance variables
        if uid is None:
            uid = str(uuid.uuid1())
        self.shortcut_overrode_keyevent = False
        self._changed = False
        self.autocomplete_overriding = False
        self._handle_shortcuts = handle_shortcuts
        self._features_initialised = False
        self._key_pressed = False
        self.last_key_pressed = ''

        self.emit_text_changed = True
        self.textChanged.connect(self._handle_textChanged)

        self._selection_timer = QTimer()
        self._selection_timer.setInterval(1000)
        self._selection_timer.setSingleShot(True)
        self._selection_timer.timeout.connect(self.selection_stopped.emit)
        self.selectionChanged.connect(self._handle_selectionChanged)

        linenumberarea.LineNumberArea(self)

        if init_features:
            self.init_features()

    def init_features(self):
        """Initialise custom Editor features.
        """
        if self._features_initialised:
            return
        self._features_initialised = True

        # QSyntaxHighlighter causes
        # textChanged to be emitted,
        # which we don't want.
        self.emit_text_changed = False
        syntaxhighlighter.Highlight(self.document(), self)

        def set_text_changed_enabled():
            self.emit_text_changed = True

        QTimer.singleShot(0, set_text_changed_enabled)

        CM = contextmenu.ContextMenu
        self.contextmenu = CM(self)

        # TODO: add a new autocompleter
        # that uses an eventfilter.
        self.autocomplete_overriding = True
        AC = autocompletion.AutoCompleter
        self.autocomplete = AC(self)

        if self._handle_shortcuts:
            actions.Actions(editor=self)
            shortcuts.ShortcutHandler(editor=self)

    @Slot()
    def _handle_textChanged(self):
        self._changed = True

        # emit custom textChanged when desired.
        if self.emit_text_changed:
            self.text_changed_signal.emit()

    @Slot()
    def _handle_selectionChanged(self):
        """Emit a selection_stopped signal once
        the selection has stopped changing after a
        certain time interval defined on the
        _selection_timer.
        """
        if not self._selection_timer.isActive():
            self._selection_timer.start()

    def setTextChanged(self, state=True):
        self._changed = state

    def selected_text(self):
        return self.textCursor().selection().toPlainText()

    def replace_text(self, text):
        """Set the text programmatically
        but allow an undo. Works around
        setPlainText automatically
        resetting the undo stack.
        """
        tc = self.textCursor()
        tc.beginEditBlock()
        tc.select(tc.Document)
        tc.removeSelectedText()
        text = sanitize_text(text)
        self.appendPlainText(text)
        tc.endEditBlock()

    def setPlainText(self, text):
        """Override original method to prevent
        textChanged signal being emitted.
        WARNING: textCursor can still be used
        to setPlainText.
        """
        self.emit_text_changed = False
        text = sanitize_text(text)
        super(Editor, self).setPlainText(text)
        self.emit_text_changed = True

    def insertPlainText(self, text):
        """Override original method to prevent
        textChanged signal being emitted.
        """
        self.emit_text_changed = False
        text = sanitize_text(text)
        super(Editor, self).insertPlainText(text)
        self.emit_text_changed = True

    def appendPlainText(self, text):
        """Override original method to prevent
        textChanged signal being emitted.
        """
        self.emit_text_changed = False
        text = sanitize_text(text)
        super(Editor, self).appendPlainText(text)
        self.emit_text_changed = True

    def focusInEvent(self, event):
        """Emit a signal when focusing in a window.
        When there used to be an editor per tab,
        this would work well to check that the tab's
        contents had not been changed. Now, we'll
        also want to signal from the tab switched
        signal.
        """
        # ignore PopupFocusReason as the
        # autocomplete QListView triggers it.
        ignored_reasons = [
            Qt.PopupFocusReason,
        ]
        if event.reason() not in ignored_reasons:
            # FIXME:
            # AttributeError: 'PySide2.QtCore.QEvent' object has no attribute 'reason'
            self.focus_in_signal.emit()
        super(Editor, self).focusInEvent(event)

    def focusOutEvent(self, event):
        if not isinstance(event, QFocusEvent):
            if os.getenv('USER') == 'mlast':
                # why would a focus out handler
                # be receiving an event of the
                # wrong type?
                print(dir(event), event.type())
                # AttributeError: 'PySide2.QtCore.QEvent' object has no attribute 'sender'
            return

        if self._changed:
            self.editingFinished.emit()

        # emit text changed to store the
        # latest text within the tab
        # FIXME: I don't like that this is here. at the very least it should be a more generic request_autosave signal.
        self.text_changed_signal.emit()

        ignored_reasons = [
            Qt.PopupFocusReason,
        ]
        if event.reason() not in ignored_reasons:
            self.focus_out_signal.emit()

        super(Editor, self).focusOutEvent(event)

    def resizeEvent(self, event):
        """Emit signal on resize so that the
        LineNumberArea has a chance to update.
        """
        super(Editor, self).resizeEvent(event)
        self.resize_signal.emit(event)

    def keyPressEvent(self, event):
        """Emit signals for key events
        that QShortcut cannot override.
        """
        self._key_pressed = True

        if not self.hasFocus():
            event.ignore()
            return

        # print('Editor: {!r} has been pressed.'.format(event.text()))
        if self.autocomplete_overriding:
            # let the autocomplete handle the
            # key press (i.e. complete the text)
            self.key_pressed_signal.emit(event)
            return

        self.shortcut_overrode_keyevent = False
        self.shortcut_signal.emit(event)
        if self.shortcut_overrode_keyevent:
            event.accept()
            return

        # print('Editor: {!r} will be entered.'.format(event.text()))
        super(Editor, self).keyPressEvent(event)
        self.post_key_pressed_signal.emit(event)

    def keyReleaseEvent(self, event):
        self._key_pressed = False
        if not isinstance(self, Editor):
            # when the key released is F5
            # (reload app)
            return
        self.autocomplete_overriding = True
        super(Editor, self).keyReleaseEvent(event)

    def contextMenuEvent(self, event):
        """Creates a standard context menu
        and emits it for futher changes
        and execution elsewhere.
        """
        menu = self.createStandardContextMenu()
        self.context_menu_signal.emit(menu)

    def event(self, event):
        """Drop to open files implemented as a filter
        instead of dragEnterEvent and dropEvent
        because it is the only way to make it work
        on windows.
        """
        if event.type() == event.DragEnter:
            mimeData = event.mimeData()
            if mimeData.hasUrls():
                event.accept()
                return True
        elif event.type() == event.Drop:
            mimeData = event.mimeData()
            if mimeData.hasUrls():
                event.accept()
                urls = mimeData.urls()
                self.drop_files(urls)
                return True
        try:
            return super(Editor, self).event(event)
        except TypeError:
            return False

    def drop_files(self, urls):
        """When dragging and dropping files onto the
        editor from a source with urls (file paths),
        if there are tabs, open the files in new
        tabs. If the tabs are not present just insert
        the text into the editor.
        """
        if self._handle_shortcuts:
            # if we're handling shortcuts
            # it means there are no tabs.
            # just insert the text
            text_list = []
            for url in urls:
                path = url.toLocalFile()
                with open(path, 'r') as f:
                    text_list.append(f.read())

            self.textCursor().insertText('\n'.join(text_list))
        else:
            tabeditor = self.parent()
            for url in urls:
                path = url.toLocalFile()
                actions.open_action(tabeditor.tabs, self, path)

    def wheelEvent(self, event):
        """Restore focus and, if ctrl held, emit signal
        """
        self.setFocus(Qt.MouseFocusReason)
        vertical = Qt.Orientation.Vertical
        is_vertical = (event.orientation() == vertical)
        CTRL = Qt.ControlModifier
        ctrl_held = (event.modifiers() == CTRL)
        if ctrl_held and is_vertical:
            return self.wheel_signal.emit(event)
        super(Editor, self).wheelEvent(event)

    def insertFromMimeData(self, mimedata):
        """Override to emit text_changed_signal
        (which triggers autosave) when text
        is pasted or dragged in.
        """
        self.text_changed_signal.emit()
        mimedata = sanitize_mimedata(mimedata)
        super(Editor, self).insertFromMimeData(mimedata)

    def showEvent(self, event):
        """Override to automatically set the
        focus on the editor when shown.
        """
        super(Editor, self).showEvent(event)

        # Previously, this used PopupFocusReason,
        # which doesn't trigger the autosave via
        # the focus_in_signal.
        self.setFocus(Qt.ShortcutFocusReason)