Example #1
0
class LoaderList(QtWidgets.QListView):  #WIP name

    emit_text = QtCore.Signal(str)
    emit_tab = QtCore.Signal(dict)

    def __init__(self):
        super(LoaderList, self).__init__()
        _model = QtGui.QStandardItemModel()
        self._model = QtGui.QStandardItemModel()
        self.setModel(self._model)

    def __setitem__(self, name, value):
        item = QtGui.QStandardItem(name)
        item.setData(value, role=QtCore.Qt.UserRole + 1)
        self._model.appendRow(item)

    def selectionChanged(self, selected, deselected):
        #print selected, deselected
        for index in selected.indexes():
            item = self._model.item(index.row(), index.column())
            #print item
            a = item.data(QtCore.Qt.UserRole + 1)
            print a
            #self.emit_text.emit(text)
            self.emit_tab.emit(a)
            #if index.column() == 0:
        super(LoaderList, self).selectionChanged(selected, deselected)
 def set_style(self, theme):
     self.styles = {
         feature: self.format(*style)
         for feature, style in theme.items()
     }
     self.tri_single = (QtCore.QRegExp("'''"), 1, self.styles['comment'])
     self.tri_double = (QtCore.QRegExp('"""'), 2, self.styles['comment'])
Example #3
0
class EditLine(QtWidgets.QLineEdit):
    """
    Base class for search/replace widgets.
    Emits signals on certain keystrokes
    and sets defaults common to both.
    """
    escape_signal = QtCore.Signal()
    ctrl_enter_signal = QtCore.Signal()

    def __init__(self, editor):
        super(EditLine, self).__init__(editor)
        self.editor = editor
        font = QtGui.QFont(constants.DEFAULT_FONT)
        font.setPointSize(10)
        self.setFont(font)

    def keyPressEvent(self, event):
        esc = QtCore.Qt.Key.Key_Escape
        if event.key() == esc:
            self.editor.setFocus(QtCore.Qt.MouseFocusReason)
            self.escape_signal.emit()
            return
        enter_keys = [QtCore.Qt.Key.Key_Return, QtCore.Qt.Key.Key_Enter]
        enter = event.key() in enter_keys
        ctrl = event.modifiers() == QtCore.Qt.ControlModifier
        if ctrl and enter:
            self.ctrl_enter_signal.emit()
        super(EditLine, self).keyPressEvent(event)
Example #4
0
class Signal(QtCore.QObject):
    s = QtCore.Signal(str)
    e = QtCore.Signal()
    receivers = []

    def customEvent(self, event):
        pass
        # from _fnpython import stderrRedirector, outputRedirector
        # try:
        #     outputRedirector(event.text)
        # except:
        #     pass
        for func in self.receivers:
            func(text=event.text)
 def set_list(self, stringlist):
     """
     Sets the list of completions.
     """
     qslm = QtCore.QStringListModel()
     qslm.setStringList(stringlist)
     self.setModel(qslm)
Example #6
0
 def sizeHint(self):
     self.ensurePolished()
     width = self.style().pixelMetric(
         QtWidgets.QStyle.PM_TabCloseIndicatorWidth, None, self)
     height = self.style().pixelMetric(
         QtWidgets.QStyle.PM_TabCloseIndicatorHeight, None, self)
     return QtCore.QSize(width, height)
Example #7
0
    def sizeHint(self, option, index):
        text = index.data()
        fontmetrics = option.fontMetrics

        try:
            get_width = fontmetrics.horizontalAdvance
        except AttributeError:
            # python 2.7 & earlier versions of PySide2
            get_width = fontmetrics.width

        width = get_width(text)

        column_width = self.column_width
        chars_per_line = 250
        while (width > column_width) and (chars_per_line > 10):
            width = get_width(text, chars_per_line)
            chars_per_line -= 10

        num_lines = math.ceil(len(text) / chars_per_line) + 0.5
        height = fontmetrics.height()
        height *= num_lines

        size = QtCore.QSize()
        size.setWidth(width)
        size.setHeight(height)

        # FIXME: the below causes recursion. we do want to figure out a way
        # to have the row height adjusted though. this just isn't it yet.
        # index.model().setData(index, size, QtCore.Qt.SizeHintRole)
        # index.model().dataChanged.emit(index, index)

        return size
Example #8
0
class Redirect(QtCore.QObject):
    signal = QtCore.Signal(str, object)

    def __init__(self, stream):
        super(Redirect, self).__init__()
        self.stream = stream
        self.queue = Queue(maxsize=2000)
        self.SERedirect = lambda x: None
        # self.receivers = []

        for a in dir(stream):
            try:
                getattr(self, a)
            except AttributeError:
                attr = getattr(stream, a)
                setattr(self, a, attr)

    def write(self, text):
        queue = self.queue
        receivers = self.receivers('2signal')
        if not receivers:
            queue.put(text)
        else:
            # if queue.empty():
            #     queue = None
            # for func in receivers:
            # func(text=text, queue=queue)
            self.signal.emit(text, queue)

        self.stream.write(text)
        self.SERedirect(text)
Example #9
0
class FileTree(QtWidgets.QTreeView):
    path_signal = QtCore.Signal(str)

    def __init__(self, path):
        super(FileTree, self).__init__()
        self.set_model(path)

    def set_model(self, path):
        model = QtWidgets.QFileSystemModel()
        model.setRootPath(path)
        model.setNameFilterDisables(False)
        model.setNameFilters(['*.py', '*.txt', '*.md'])

        self.setModel(model)
        RTC = QtWidgets.QHeaderView.ResizeToContents
        self.header().setResizeMode(RTC)
        self.setRootIndex(model.index(path))

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            super(FileTree, self).mousePressEvent(event)
        if event.button() == QtCore.Qt.RightButton:
            menu = QtWidgets.QMenu()
            menu.addAction('New', 'print "does nothing"')
            menu.addAction('Delete', 'print "does nothing"')
            cursor = QtGui.QCursor()
            pos = cursor.pos()
            menu.exec_(pos)

    def selectionChanged(self, selected, deselected):
        index_sel = selected.indexes()[0]
        item = self.model().filePath(index_sel)
        self.path_signal.emit(item)
Example #10
0
 def move_to_bottom(self):
     geo = self.parent().geometry()
     centre = geo.center()
     x = centre.x() - (self.width() / 2)
     y = geo.bottom() - 70
     pos = QtCore.QPoint(x, y)
     pos = self.parent().mapToGlobal(pos)
     self.move(pos)
Example #11
0
 def setup_save_timer(self, interval=500):
     """ Initialise the autosave timer.
     :param interval: autosave interval in milliseconds
     :type interval: int
     """
     self.autosave_timer = QtCore.QTimer()
     self.autosave_timer.setSingleShot(True)
     self.autosave_timer.setInterval(interval)
Example #12
0
 def start_timer(self):
     """
     Starts timer for
     self.check_update_globals
     """
     self.timer = QtCore.QTimer()
     self.timer.setInterval(100)
     self.timer.timeout.connect(self.check_update_globals)
     self.timer.start()
Example #13
0
def pythonKnobEdit():
    if nuke.thisKnob().name() == 'showPanel':  # TODO: is there a 'knob added' knobchanged?
        node = nuke.thisNode()
        global timer
        timer = QtCore.QTimer()
        timer.setSingleShot(True)
        timer.setInterval(10)
        timer.timeout.connect(partial(addTextKnobs, node))
        timer.start()
Example #14
0
 def anim_popup_bar(popup_bar):
     anim = QtCore.QPropertyAnimation(
         popup_bar, 
         'maximumHeight'
     )
     anim.setStartValue(0)
     anim.setEndValue(46)
     anim.setDuration(400)
     anim.start()
     anim_popup_bar.anim = anim
Example #15
0
    def __init__(self):
        super(Terminal, self).__init__()
        self.setReadOnly(True)
        self.setObjectName('Terminal')
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)

        self.queue = Queue()
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.check_queue)
        self.timer.setInterval(10)
        self.timer.start()

        self.interval = time.time()
Example #16
0
 def animate_popup(self, start=0, end=46):
     """
     FIXME: This works in the prototype, but
     not here yet. Perhaps because it's called
     via a few signals, or something to do with
     the parent object being the tabeditor while
     the class is instantiated on the tabeditor's
     parent class, pythoneditor.
     """
     anim = QtCore.QPropertyAnimation(self.popup_bar, 'maximumHeight')
     self._anim = anim
     anim.setStartValue(start)
     anim.setEndValue(end)
     anim.setDuration(400)
     anim.start()
Example #17
0
class Signal(QtCore.QObject):
    s = QtCore.Signal(str)
    receivers = {'<stdout>': [], '<stderr>': []}

    def customEvent(self, event):

        for func in self.receivers[event.print_type]:
            func(text=event.text, queue=event.queue)

        if event.text is None:
            return

        if event.print_type == '<stdout>':
            sys.outputRedirector(event.text)
        elif event.print_type == '<stderr>':
            sys.stderrRedirector(event.text)
Example #18
0
    def setup_new_tab_btn(self):
        """
        Adds a new tab [+] button to the right of the tabs.
        """
        widget = QtWidgets.QWidget()
        widget.setObjectName('Tab_Widget_New_Button')
        self.insertTab(0, widget, '')
        nb = self.new_btn = QtWidgets.QToolButton()
        nb.setMinimumSize(QtCore.QSize(50, 10))
        nb.setText('+')  # you could set an icon instead of text
        nb.setAutoRaise(True)
        nb.clicked.connect(self.new_tab)

        tabBar = self.tabBar()
        tabBar.setTabButton(0, QtWidgets.QTabBar.RightSide, nb)
        tabBar.setTabEnabled(0, False)
class Editor(QtWidgets.QPlainTextEdit):
    block_key_press = False
    key_pressed_signal = QtCore.Signal(QtGui.QKeyEvent)

    def __init__(self):
        super(Editor, self).__init__()
        self._completer = Completer(self)

    def keyPressEvent(self, event):

        self.block_key_press = False
        self.key_pressed_signal.emit(event)
        if self.block_key_press:
            return

        super(Editor, self).keyPressEvent(event)
Example #20
0
    def move_tab_close_button(self, pos):
        i = self.tabAt(pos)
        rect = self.tabRect(i)
        btn = self.tab_close_button
        x = rect.right() - btn.width() - 2
        y = rect.center().y() - (btn.height() / 2)
        if i != self.currentIndex():
            y += 2
        btn_pos = QtCore.QPoint(x, y)

        btn.move(btn_pos)
        btn.raise_()

        if not self.tab_only_rect().contains(btn_pos):
            btn.hide()
        elif not self.tab_pressed:
            btn.show()
Example #21
0
    def make_rules(self):
        # rules
        rules = []

        # function args/kwargs TODO: find correct regex pattern for separate
        # args and kwargs (words) between parentheses
        # rules += [('(?:def \w+\()([^)]+)', 1, self.styles['args'])]

        class_regex = r'(?:class\s+\w+\()([a-zA-Z\.]+)(?:\))'
        rules += [(class_regex, 1, self.styles['inherited'])]
        rules += [(r'\b%s\b' % i, 0, self.styles['arguments'])
                  for i in self.arguments]
        rules += [(r'\b%s\b' % i, 0, self.styles['keyword'])
                  for i in self.keywords]
        rules += [(i, 0, self.styles['keyword'])
                  for i in self.operatorKeywords]
        rules += [(r'\b%s\b' % i, 0, self.styles['numbers'])
                  for i in self.truthy]
        rules += [(r'\b%s\b' % i, 0, self.styles['instantiators'])
                  for i in self.instantiators]
        rules += [(r'\b%s\b' % i, 0, self.styles['exceptions'])
                  for i in self.exceptions + self.types]

        rules += [
            # function names
            (r'(?:def\s+|)(\w+)(?:\()', 1, self.styles['function_names']),
            # class names
            (r'(?:class\s+)(\w+)(?:\()', 1, self.styles['class_names']),
            # methods
            (r'(?:\.)([a-zA-Z\.]+)(?:\()', 1, self.styles['methods']),
            # decorators
            (r'(?:@)(\w+)', 1, self.styles['function_names']),
            # string formatters
            (r'([rfb])(?:\'|\")', 0, self.styles['formatters']),
            # integers
            (r'\b[0-9]+\b', 0, self.styles['numbers']),
            # Double-quoted string, possibly containing escape sequences
            (r'"[^"\\]*(\\.[^"\\]*)*"', 0, self.styles['string']),
            # Single-quoted string, possibly containing escape sequences
            (r"'[^'\\]*(\\.[^'\\]*)*'", 0, self.styles['string']),
        ]

        # Build a QRegExp for each pattern
        self.rules = [(QtCore.QRegExp(pat), index, fmt)
                      for (pat, index, fmt) in rules]
Example #22
0
class ItemWordWrap(QtWidgets.QStyledItemDelegate):
    update_viewport = QtCore.Signal()

    def __init__(self, parent=None):
        super(ItemWordWrap, self).__init__(parent=parent)
        self.column_width = 100

    @QtCore.Slot(int, int, int)
    def handle_column_resized(self, logicalIndex, oldSize, newSize):
        if logicalIndex == 2:
            self.column_width = newSize
            self.update_viewport.emit()

    def sizeHint(self, option, index):
        text = index.data()
        fontmetrics = option.fontMetrics

        try:
            get_width = fontmetrics.horizontalAdvance
        except AttributeError:
            # python 2.7 & earlier versions of PySide2
            get_width = fontmetrics.width

        width = get_width(text)

        column_width = self.column_width
        chars_per_line = 250
        while (width > column_width) and (chars_per_line > 10):
            width = get_width(text, chars_per_line)
            chars_per_line -= 10

        num_lines = math.ceil(len(text) / chars_per_line) + 0.5
        height = fontmetrics.height()
        height *= num_lines

        size = QtCore.QSize()
        size.setWidth(width)
        size.setHeight(height)

        # FIXME: the below causes recursion. we do want to figure out a way
        # to have the row height adjusted though. this just isn't it yet.
        # index.model().setData(index, size, QtCore.Qt.SizeHintRole)
        # index.model().dataChanged.emit(index, index)

        return size
Example #23
0
class Terminal(QtWidgets.QPlainTextEdit):
    """ Output text display widget """
    link_activated = QtCore.Signal(str)

    def __init__(self):
        super(Terminal, self).__init__()

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

    @QtCore.Slot(str)
    def receive(self, text):
        try:
            textCursor = self.textCursor()
            if bool(textCursor):
                self.moveCursor(QtGui.QTextCursor.End)
        except Exception:
            pass
        self.insertPlainText(text)

    def stop(self):
        sys.stdout.reset()
        sys.stderr.reset()

    def setup(self):
        """
        Checks for an existing stream wrapper
        for sys.stdout and connects to it. If
        not present, creates a new one.
        TODO:
        The FnRedirect sys.stdout is always active.
        With a singleton object on a thread,
        that reads off this stream, we can make it
        available to Python Editor even before opening
        the panel.
        """
        if hasattr(sys.stdout, '_signal'):
            self.speaker = sys.stdout._signal
        else:
            self.speaker = streams.Speaker()
            sys.stdout = streams.SESysStdOut(sys.stdout, self.speaker)
            sys.stderr = streams.SESysStdErr(sys.stderr, self.speaker)

        self.speaker.emitter.connect(self.receive)

    def mousePressEvent(self, e):
        if not hasattr(self, 'anchorAt'):
            # pyqt doesn't use anchorAt
            return super(Terminal, self).mousePressEvent(e)

        if (e.button() == QtCore.Qt.LeftButton):
            clickedAnchor = self.anchorAt(e.pos())
            if clickedAnchor:
                self.link_activated.emit(clickedAnchor)
        super(Terminal, self).mousePressEvent(e)
Example #24
0
class ShortcutHandler(QtCore.QObject):
    """
    Shortcut Manager with custom signals.

    :param editor: required `QPlainTextEdit` or `Editor` class.
    :param tabeditor: optional `QWidget` or `TabEditor`
    :param terminal: optional `QPlainTextEdit` or `Terminal` class.
    """
    def __init__(
        self,
        editor=None,
        tabeditor=None,
        terminal=None,
    ):
        super(ShortcutHandler, self).__init__()
        self.setObjectName('ShortcutHandler')
        self._installed = False

        if editor is None:
            raise Exception("""
            A text editor is necessary
            for this class.
            """.strip())
        self.editor = editor

        if tabeditor is not None:
            self.tabeditor = tabeditor
            self.tabs = tabeditor.tabs
            parent_widget = tabeditor
        else:
            parent_widget = editor

        if terminal is not None:
            self.terminal = terminal
        self.parent_widget = parent_widget
        self.setParent(parent_widget)

        self.shortcut_dict = {}

        self.register_shortcuts()
        self.connect_signals()

    def connect_signals(self):
        """
        Connects the current editor's
        signals to this class
        """
        self.editor.focus_in_signal.connect(self.install_event_filter,
                                            QtCore.Qt.DirectConnection)
        self.editor.focus_out_signal.connect(self.remove_event_filter,
                                             QtCore.Qt.DirectConnection)

    def install_event_filter(self):
        if self._installed:
            return
        app = QtWidgets.QApplication.instance()
        QtCore.QCoreApplication.installEventFilter(app, self)
        self._installed = True

    def remove_event_filter(self):
        app = QtWidgets.QApplication.instance()
        QtCore.QCoreApplication.removeEventFilter(app, self)
        self._installed = False

    def eventFilter(self, obj, event):
        try:
            if not self.editor.isVisible():
                self.remove_event_filter()
                return False
        except RuntimeError:
            self.remove_event_filter()
            return False

        if not self.editor.hasFocus():
            self.remove_event_filter()
            return False

        if QtCore is None:
            return False

        if not hasattr(event, 'type'):
            return False

        if event.type() == QtCore.QEvent.Shortcut:
            if isinstance(obj, QtWidgets.QAction):
                combo = obj.shortcut()
                shortcut = combo.toString()
                action = self.shortcut_dict.get(shortcut)
                if action is None:
                    return False
                action.trigger()
                return True

        if event.type() == QtCore.QEvent.KeyPress:
            # only let the editor receive keypress overrides
            if obj == self.editor:
                return self.handle_keypress(event)

        return False

    QtCore.Slot(QtGui.QKeyEvent)

    def handle_keypress(self, event):

        app = QtWidgets.QApplication
        held = app.keyboardModifiers()

        if (event.isAutoRepeat() and held == QtCore.Qt.NoModifier):
            return False

        key = event.key()
        if key in [
                QtCore.Qt.Key_Control,
                QtCore.Qt.Key_Shift,
                QtCore.Qt.Key_Alt,
                QtCore.Qt.Key_AltGr,
                QtCore.Qt.Key_Meta,
        ]:
            return False

        # is it a Tab after a dot?
        if key == QtCore.Qt.Key_Tab:
            cursor = self.editor.textCursor()
            cursor.select(cursor.LineUnderCursor)
            text = cursor.selectedText()
            if text.endswith('.'):
                # allow autocompletion to handle this
                return False

        # try with event.text() for things
        # like " and { which appear as
        # shift+2 and shift+[ respectively
        action = self.shortcut_dict.get(event.text())

        single_key = (action is not None)
        if not single_key:
            combo = key_to_sequence(key)
            shortcut = combo.toString()
            action = self.shortcut_dict.get(shortcut)

        if action is None:
            return False

        # need some way for the key to be
        # recognised, for example in wrap_text
        e = self.editor
        e.last_key_pressed = event.text()
        action.trigger()
        e.shortcut_overrode_keyevent = True
        if single_key:
            # it's a single key. let the
            # autocomplete do its thing
            e.post_key_pressed_signal.emit(event)
        return True

    def register_shortcuts(self, action_dict=None):
        """
        Use the shortcut register to apply
        shortcuts to actions that exist
        on the widget.
        """
        if action_dict is None:
            a = actions.load_actions_from_json
            action_dict = a()

        widgacts = action_dict.items()
        for widget_name, widget_actions in widgacts:
            if not hasattr(self, widget_name):
                continue

            widget = getattr(self, widget_name)
            if widget is None:
                continue

            acts = widget_actions.items()
            for action_name, attributes in acts:
                shortcuts = attributes['Shortcuts']
                if len(shortcuts) == 0:
                    continue
                for action in widget.actions():
                    if action.text() != action_name:
                        continue
                    break
                else:
                    continue
                key_seqs = []
                for shortcut in shortcuts:
                    key_seq = QtGui.QKeySequence(shortcut)

                    # convert to unicode again
                    # to make sure the format
                    # stays the same
                    s = key_seq.toString()
                    self.shortcut_dict[s] = action
                    key_seqs.append(key_seq)

                action.setShortcuts(key_seqs)
                action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
Example #25
0
class EditTabs(QtWidgets.QTabWidget):
    """
    QTabWidget containing Editor
    QPlainTextEdit widgets.
    """
    reset_tab_signal = QtCore.Signal()
    closed_tab_signal = QtCore.Signal(object)
    tab_switched_signal = QtCore.Signal(int, int, bool)
    contents_saved_signal = QtCore.Signal(object)
    tab_moved_signal = QtCore.Signal(object, int)

    def __init__(self):
        QtWidgets.QTabWidget.__init__(self)
        self.setTabBar(TabBar(self))

        self.setTabsClosable(True)
        self.user_cancelled_tab_close = False
        self.setTabShape(QtWidgets.QTabWidget.Rounded)

        self.tab_count = 0
        self.current_index = 0

        tabBar = self.tabBar()
        tabBar.setMovable(True)
        tabBar.tabMoved.connect(self.tab_restrict_move,
                                QtCore.Qt.DirectConnection)

        self.setup_new_tab_btn()
        self.tabCloseRequested.connect(self.close_tab)
        self.reset_tab_signal.connect(self.reset_tabs)
        self.currentChanged.connect(self.widget_changed)
        self.setStyleSheet("QTabBar::tab { height: 24px; }")

        # add tab list button
        self.corner_button = QtWidgets.QPushButton(':')
        self.corner_button.setFixedSize(24, 24)
        self.corner_button.setStyleSheet("border: 5px solid black")
        self.corner_button.clicked.connect(self.show_tab_menu)
        self.setCornerWidget(self.corner_button,
                             corner=QtCore.Qt.TopRightCorner)

    # currently not in use
    @property
    def editor(self):
        widget = self.currentWidget()
        if widget.objectName() != 'Editor':
            raise Exception('Current Widget is not an Editor')
        return widget

    # currently not in use
    @editor.setter
    def editor(self, widget):
        if widget.objectName() != 'Editor':
            raise Exception('Current Widget is not an Editor')
        self.setCurrentWidget(widget)

    def show_tab_menu(self):
        """
        Show a list of tabs and go to the tab clicked.
        """
        menu = QtWidgets.QMenu()
        from functools import partial
        for i in range(self.count()):
            tab_name = self.tabText(i)
            if not tab_name.strip():
                button = self.tabBar().tabButton(i, QtWidgets.QTabBar.LeftSide)
                if not isinstance(button, TabButton):
                    continue
                tab_name = button.text()
            action = partial(self.setCurrentIndex, i)
            menu.addAction(tab_name, action)
        menu.exec_(QtGui.QCursor().pos())

    @QtCore.Slot(int, int)
    def tab_restrict_move(self, from_index, to_index):
        """
        Prevents tabs from being moved beyond the +
        new tab button.
        """
        if from_index >= self.count()-1:
            self.tabBar().moveTab(to_index, from_index)
            return

        for index in from_index, to_index:
            widget = self.widget(index)
            widget.tab_index = index
            if hasattr(widget, 'name'):
                self.tab_moved_signal.emit(widget, index)

    def setup_new_tab_btn(self):
        """
        Adds a new tab [+] button to the right of the tabs.
        """
        widget = QtWidgets.QWidget()
        widget.setObjectName('Tab_Widget_New_Button')
        self.insertTab(0, widget, '')
        nb = self.new_btn = QtWidgets.QToolButton()
        nb.setMinimumSize(QtCore.QSize(50, 10))
        nb.setText('+')  # you could set an icon instead of text
        nb.setAutoRaise(True)
        nb.clicked.connect(self.new_tab)

        tabBar = self.tabBar()
        tabBar.setTabButton(0, QtWidgets.QTabBar.RightSide, nb)
        tabBar.setTabEnabled(0, False)

    @QtCore.Slot(str)
    def new_tab(self, tab_name=None, init_features=True):
        """
        Creates a new tab.
        """
        count = self.count()
        index = 0 if count == 0 else count - 1
        editor = EDITOR.Editor(handle_shortcuts=False, init_features=init_features)

        if (tab_name is None
                or not tab_name):
            tab_name = 'Tab {0}'.format(index)

        editor.name = tab_name
        editor.tab_index = index

        self.insertTab(index,
                       editor,
                       tab_name
                       )
        self.setCurrentIndex(index)

        # relay the contents saved signal
        editor.contents_saved_signal.connect(self.contents_saved_signal)

        self.tab_count = self.count()
        self.current_index = self.currentIndex()
        editor.setFocus()
        return editor

    def close_current_tab(self):
        """
        Closes the active tab. Called via shortcut key.
        """
        _index = self.currentIndex()
        self.tabCloseRequested.emit(_index)

    def close_tab(self, index):
        """
        Remove current tab if tab count is greater than 3 (so that the
        last tab left open is not the new button tab, although a better
        solution here is to open a new tab if the only tab left open is
        the 'new tab' tab). Also emits a close signal which is used by the
        autosave to determine if an editor's contents need saving.
        """
        if self.count() < 3:
            return
        _index = self.currentIndex()

        editor = self.widget(index)
        if editor.objectName() == 'Tab_Widget_New_Button':
            return

        self.closed_tab_signal.emit(editor)
        # the below attribute may be altered
        # by a slot connected with DirectConnection
        if self.user_cancelled_tab_close:
            return

        editor.deleteLater()

        tab_on_left = (index < _index)
        last_tab_before_end = (_index == self.count()-2)

        self.removeTab(index)
        if tab_on_left or last_tab_before_end:
            _index -= 1
        self.setCurrentIndex(_index)
        self.tab_count = self.count()

    def reset_tabs(self):
        for index in reversed(range(self.count())):
            widget = self.widget(index)
            if widget is None:
                continue
            if widget.objectName() == 'Editor':
                self.removeTab(index)
        self.new_tab()

    @QtCore.Slot(int)
    def widget_changed(self, index):
        """
        Emits tab_switched_signal with current widget.
        """
        tabremoved = self.count() < self.tab_count
        previous = self.current_index
        current = self.currentIndex()
        self.tab_switched_signal.emit(previous,
                                      current,
                                      tabremoved)
        self.current_index = self.currentIndex()
        self.tab_count = self.count()

    def addTab(self, *args):
        """
        Insurance override to point to insertTab.
        args may be:
        const QString &text
        const QIcon &icon, const QString &text

        TODO: Needs testing.
        """
        debug('Warning. addTab override feature needs testing.')
        if len(args) == 1:
            label, = args
            return self.insertTab(self.count(), None, label)
        elif len(args) == 2:
            icon, label = args
            return self.insertTab(self.count(), None, icon, label)
        else:
            return

        super(EditTabs, self).addTab(*args, **kwargs)

    def insertTab(self, *args):
        """
        Override overloaded method which can
        receive the following params:

        int index, QWidget widget, QString label
        int index, QWidget widget, QIcon icon, QString label
        """
        super(EditTabs, self).insertTab(*args)

        if len(args) == 3:
            index, widget, label = args
        elif len(args) == 4:
            index, widget, icon, label = args
        else:
            return

        if not isinstance(widget, EDITOR.Editor):
            return

        if not hasattr(self, 'tab_button_list'):
            # Note: button removed from set when tab removed.
            self.tab_button_list = set([])

        button = TabButton(label, widget)
        self.tab_button_list.add(button)
        self.setTabText(index, '')
        self.tabBar().setTabButton(index, QtWidgets.QTabBar.LeftSide, button)

    def removeTab(self, index):
        """
        Override QTabWidget method in order to also remove any buttons
        stored on the class. As this is the sole method for removing tabs
        uring a session, this should be sufficient to cleanup the
        list and prevent memory leaks.
        """
        button = self.tabBar().tabButton(index, QtWidgets.QTabBar.LeftSide)
        if button in self.tab_button_list:
            self.tab_button_list.remove(button)
        super(EditTabs, self).removeTab(index)

    def setTabText(self, index, label):
        """
        Override QTabWidget setTabText so that if the tab
        has a button, the text will be set on the button
        instead of the tab.
        """
        button = self.tabBar().tabButton(index, QtWidgets.QTabBar.LeftSide)
        if isinstance(button, TabButton):
            return button.setText(label)
        super(EditTabs, self).setTabText(index, label)
Example #26
0
 def sizeHint(self):
     return QtCore.QSize(self.lineNumberAreaWidth(), 0)
Example #27
0
 def resizeLineNo(self):
     cr = self.editor.contentsRect()
     rect = QtCore.QRect(cr.left(), cr.top(), self.lineNumberAreaWidth(),
                         cr.height())
     self.setGeometry(rect)
Example #28
0
class Speaker(QtCore.QObject):
    """ Used to relay sys stdout, stderr, stdin
    """
    emitter = QtCore.Signal(str)
Example #29
0
class Editor(QtWidgets.QPlainTextEdit):
    """
    Code Editor widget. Extends QPlainTextEdit to provide:
    - Line Number Area
    - Syntax Highlighting
    - Autocompletion (of Python code)
    - Shortcuts for code editing
    - New Context Menu
    - Signals for connecting the Editor to other UI elements.
    - Unique identifier to match Editor widget to file storage.
    """
    wrap_types = [
        '\'',
        '"',
        '[',
        ']',
        '(',
        ')',
        '{',
        '}',
    ]

    wrap_signal = QtCore.Signal(str)
    uuid_signal = QtCore.Signal(str)
    return_signal = QtCore.Signal(QtGui.QKeyEvent)
    focus_in_signal = QtCore.Signal(QtGui.QFocusEvent)
    post_key_pressed_signal = QtCore.Signal(QtGui.QKeyEvent)
    wheel_signal = QtCore.Signal(QtGui.QWheelEvent)
    key_pressed_signal = QtCore.Signal(QtGui.QKeyEvent)
    # key_release_signal        = QtCore.Signal(QtGui.QKeyEvent)
    shortcut_signal = QtCore.Signal(QtGui.QKeyEvent)
    resize_signal = QtCore.Signal(QtGui.QResizeEvent)
    context_menu_signal = QtCore.Signal(QtWidgets.QMenu)
    tab_signal = QtCore.Signal()
    home_key_signal = QtCore.Signal()
    ctrl_x_signal = QtCore.Signal()
    ctrl_n_signal = QtCore.Signal()
    ctrl_w_signal = QtCore.Signal()
    ctrl_s_signal = QtCore.Signal()
    ctrl_c_signal = QtCore.Signal()
    ctrl_enter_signal = QtCore.Signal()
    end_key_ctrl_alt_signal = QtCore.Signal()
    home_key_ctrl_alt_signal = QtCore.Signal()
    relay_clear_output_signal = QtCore.Signal()
    editingFinished = QtCore.Signal()
    text_changed_signal = QtCore.Signal()

    def __init__(self, handle_shortcuts=True, uid=None, init_features=True):
        super(Editor, self).__init__()
        self.setObjectName('Editor')
        self.setAcceptDrops(True)
        font = QtGui.QFont(DEFAULT_FONT)
        font.setPointSize(10)
        self.setFont(font)
        self.setMouseTracking(True)
        self.setStyleSheet("""
        QToolTip {
        color: #F6F6F6;
        background-color: rgb(45, 42, 46);
        }
        """)
        self.shortcut_overrode_keyevent = False

        if uid is None:
            uid = str(uuid.uuid4())
        self._uuid = uid

        self._changed = False
        self.wait_for_autocomplete = False
        self._handle_shortcuts = handle_shortcuts
        self._features_initialised = False

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

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

        def set_text_changed_enabled():
            self.emit_text_changed = True

        QtCore.QTimer.singleShot(0, set_text_changed_enabled)

        # self.emit_text_changed = True
        self.contextmenu = contextmenu.ContextMenu(self)

        # TOOD: add a new autocompleter that uses DirectConnection.
        self.wait_for_autocomplete = True
        self.autocomplete = autocompletion.AutoCompleter(self)

        if self._handle_shortcuts:
            sch = shortcuts.ShortcutHandler(editor=self, use_tabs=False)
            sch.clear_output_signal.connect(self.relay_clear_output_signal)
            self.shortcuteditor = shortcuteditor.ShortcutEditor(sch)

        self.selectionChanged.connect(self.highlight_same_words)

    def _handle_textChanged(self):
        self._changed = True

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

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

    def highlight_same_words(self):
        """
        Highlights other matching words in document
        when full word selected.
        TODO: implement this!
        """
        textCursor = self.textCursor()
        if not textCursor.hasSelection():
            return
        """
        text = textCursor.selection().toPlainText()
        textCursor.select(QtGui.QTextCursor.WordUnderCursor)
        word = textCursor.selection().toPlainText()
        print(text, word)
        if text == word:
            print(word)
        """

    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
        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
        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
        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.
        """
        FR = QtCore.Qt.FocusReason
        ignored_reasons = [FR.PopupFocusReason, FR.MouseFocusReason]
        if event.reason() not in ignored_reasons:
            self.focus_in_signal.emit(event)
        super(Editor, self).focusInEvent(event)

    def focusOutEvent(self, event):
        if self._changed:
            self.editingFinished.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.
        """
        # will this be enough to give focus back to the
        # script editor or rest of the application?
        if not self.hasFocus():
            event.ignore()
            return

        # self.wait_for_autocomplete = True
        # # QtCore.Qt.DirectConnection
        # self.key_pressed_signal.emit(event)

        # self.autocomplete_overrode_keyevent = False

        if self.wait_for_autocomplete:
            # TODO: Connect (in autocomplete) using
            # QtCore.Qt.DirectConnection to work synchronously
            self.key_pressed_signal.emit(event)
            return

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

        if event.modifiers() == QtCore.Qt.NoModifier:
            # Tab Key
            if event.key() == QtCore.Qt.Key_Tab:
                return self.tab_signal.emit()
            # Enter/Return Key
            if event.key() == QtCore.Qt.Key_Return:
                return self.return_signal.emit(event)

        # Ctrl+Enter/Return Key
        if (event.key() == QtCore.Qt.Key_Return and event.modifiers() == CTRL):
            return self.ctrl_enter_signal.emit()

        # Surround selected text in brackets or quotes
        if (event.text() in self.wrap_types
                and self.textCursor().hasSelection()):
            return self.wrap_signal.emit(event.text())

        if event.key() == QtCore.Qt.Key_Home:
            # Ctrl+Alt+Home
            if event.modifiers() == CTRL_ALT:
                self.home_key_ctrl_alt_signal.emit()
            # Home
            elif event.modifiers() == QtCore.Qt.NoModifier:
                return self.home_key_signal.emit()

        # Ctrl+Alt+End
        if (event.key() == QtCore.Qt.Key_End
                and event.modifiers() == CTRL_ALT):
            self.end_key_ctrl_alt_signal.emit()

        # Ctrl+X
        if (event.key() == QtCore.Qt.Key_X and event.modifiers() == CTRL):
            self.ctrl_x_signal.emit()
            self.text_changed_signal.emit()

        # Ctrl+N
        if (event.key() == QtCore.Qt.Key_N and event.modifiers() == CTRL):
            self.ctrl_n_signal.emit()

        # Ctrl+W
        if (event.key() == QtCore.Qt.Key_W and event.modifiers() == CTRL):
            self.ctrl_w_signal.emit()

        # Ctrl+S
        if (event.key() == QtCore.Qt.Key_S and event.modifiers() == CTRL):
            self.ctrl_s_signal.emit()

        # Ctrl+C
        if (event.key() == QtCore.Qt.Key_C and event.modifiers() == CTRL):
            self.ctrl_c_signal.emit()

        super(Editor, self).keyPressEvent(event)
        self.post_key_pressed_signal.emit(event)

    def keyReleaseEvent(self, event):
        if not isinstance(self, Editor):
            # when the key released is F5 (reload app)
            return
        self.wait_for_autocomplete = True
        # self.key_release_signal.emit(event)
        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 dragEnterEvent(self, e):
        mimeData = e.mimeData()
        if mimeData.hasUrls:
            e.accept()
        else:
            super(Editor, self).dragEnterEvent(e)

        # prevent mimedata from being displayed unless alt is held
        app = QtWidgets.QApplication.instance()
        if app.keyboardModifiers() != QtCore.Qt.AltModifier:
            return

        # let's see what the data contains, at least!
        # maybe restrict this to non-known formats...
        for f in mimeData.formats():
            data = str(mimeData.data(f)).replace(b'\0', b'')
            data = data.replace(b'\x12', b'')
            print(f, data)

    def dragMoveEvent(self, e):
        # prevent mimedata from being displayed unless alt is held
        app = QtWidgets.QApplication.instance()
        if app.keyboardModifiers() != QtCore.Qt.AltModifier:
            super(Editor, self).dragMoveEvent(e)
            return

        if e.mimeData().hasUrls:
            e.accept()
        else:
            super(Editor, self).dragMoveEvent(e)

    def dropEvent(self, e):
        """
        TODO: e.ignore() files and send to edittabs to
        create new tab instead?
        """
        mimeData = e.mimeData()
        if (mimeData.hasUrls and mimeData.urls()):
            urls = mimeData.urls()

            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:
            super(Editor, self).dropEvent(e)

    def wheelEvent(self, event):
        """
        Restore focus and, if ctrl held, emit signal
        """
        self.setFocus(QtCore.Qt.MouseFocusReason)
        vertical = QtCore.Qt.Orientation.Vertical
        is_vertical = (event.orientation() == vertical)
        is_ctrl = (event.modifiers() == CTRL)
        if is_ctrl 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()
        super(Editor, self).insertFromMimeData(mimeData)

    """ # Great idea, needs testing
Example #30
0
class ShortcutHandler(QtCore.QObject):
    """
    Shortcut Manager with custom signals.
    """
    clear_output_signal = QtCore.Signal()
    exec_text_signal = QtCore.Signal()

    def __init__(self, parent_widget, use_tabs=True):
        """
        :param use_tabs:
        If False, the parent_widget is the QPlainTextEdit (Editor)
        widget. If True, apply shortcuts to the QTabBar as well as
        the Editor.
        """
        super(ShortcutHandler, self).__init__()
        self.setObjectName('ShortcutHandler')
        self.setParent(parent_widget)
        self.parent_widget = parent_widget
        self.use_tabs = use_tabs

        if use_tabs:
            self.tabs = parent_widget.tabs
            self.editor = parent_widget.editor
        else:
            self.editor = parent_widget

        self.connect_signals()
        self.install_shortcuts()

    def connect_signals(self):
        """ Connects the current editor's signals to this class """
        editor = self.editor
        pairs = [
            (editor.tab_signal, self.tab_handler),
            (editor.return_signal, self.return_handler),
            (editor.wrap_signal, self.wrap_text),
            (editor.home_key_ctrl_alt_signal, self.move_to_top),
            (editor.end_key_ctrl_alt_signal, self.move_to_bottom),
            (editor.ctrl_x_signal, self.cut_line),
            (editor.ctrl_c_signal, self.copy_block_or_selection),
            (editor.ctrl_s_signal, self.save),
            (editor.home_key_signal, self.jump_to_start),
            (editor.wheel_signal, self.wheel_zoom),
            (editor.ctrl_enter_signal, self.exec_selected_text),
        ]
        self._connections = []
        for signal, slot in pairs:
            name, _, handle = connect(editor, signal, slot)
            self._connections.append((name, slot))

    def install_shortcuts(self):
        """
        Maps shortcuts on the QPlainTextEdit widget
        to methods on this class.
        """
        def notimp(msg):
            return partial(self.notimplemented, msg)

        editor_shortcuts = {
            'Ctrl+B': self.exec_current_line,
            'Ctrl+Shift+Return': self.new_line_above,
            'Ctrl+Alt+Return': self.new_line_below,
            'Ctrl+Shift+D': self.duplicate_lines,
            'Ctrl+Shift+T': self.print_type,
            'Ctrl+Shift+F': self.search_input,
            'Ctrl+H': self.print_help,
            'Ctrl+L': self.select_lines,
            'Ctrl+J': self.join_lines,
            'Ctrl+/': self.comment_toggle,
            'Ctrl+]': self.indent,
            'Ctrl+[': self.unindent,
            'Shift+Tab': self.unindent,
            'Ctrl+=': self.zoom_in,
            'Ctrl++': self.zoom_in,
            'Ctrl+-': self.zoom_out,
            'Ctrl+Shift+K': self.delete_lines,
            'Ctrl+D': self.select_word,
            'Ctrl+M': self.hop_brackets,
            'Ctrl+Shift+M': self.select_between_brackets,
            'Ctrl+Shift+Delete': self.delete_to_end_of_line,
            'Ctrl+Shift+Backspace': self.delete_to_start_of_line,
            'Ctrl+Shift+Up': self.move_blocks_up,
            'Ctrl+Shift+Down': self.move_blocks_down,
            'Ctrl+G': notimp('goto'),
            'Ctrl+P': notimp('palette'),
            'Ctrl+C': self.copy_block_or_selection,
            'Ctrl+E': self.open_module_file,
            # 'Ctrl+Shift+Alt+Up': notimp('duplicate cursor up'),
            # 'Ctrl+Shift+Alt+Down': notimp('duplicate cursor down'),
        }

        if self.use_tabs:
            editor_shortcuts['Ctrl+S'] = self.save
            editor_shortcuts[QtCore.Qt.Key_F5] = self.parent_widget.parent(
            ).parent().parent().reload_package

        terminal_shortcuts = {
            'Ctrl+Backspace': self.clear_output_signal.emit,
        }
        editor_shortcuts.update(terminal_shortcuts)

        if self.use_tabs:
            tab_shortcuts = {
                'Ctrl+T': self.tabs.new_tab,
                'Ctrl+Shift+N': self.tabs.new_tab,
                'Ctrl+Shift+W': self.tabs.remove_current_tab,
                'Ctrl+Tab': self.next_tab,
                'Ctrl+Shift+Tab': self.previous_tab,
                # 'Ctrl+Shift+T': notimp('reopen previous tab'),
            }
            editor_shortcuts.update(tab_shortcuts)

        def doc(f):
            return f.func_doc if hasattr(f, 'func_doc') else f.__doc__

        self.shortcut_dict = {
            key: doc(func)
            for key, func in editor_shortcuts.items()
        }

        signal_dict = {
            'Tab': self.tab_handler.__doc__,
            'Return/Enter': self.return_handler.__doc__,
            r'\' " ( ) [ ] \{ \}': self.wrap_text.__doc__,
            'Ctrl+Alt+Home': self.move_to_top.__doc__,
            'Ctrl+Alt+End': self.move_to_bottom.__doc__,
            'Ctrl+X': self.cut_line.__doc__,
            'Home': self.jump_to_start.__doc__,
            'Ctrl+Mouse Wheel': self.wheel_zoom.__doc__,
            'Ctrl+Backspace': '\n' + ' ' * 8 + 'Clear Output Terminal\n',
        }

        self.shortcut_dict.update(signal_dict)

        def add_action(action, widget, shortcut, func):
            """
            Add action to widget with a shortcut that
            triggers the given function.

            :action: QtWidgets.QAction
            :widget: QtWidgets.QWidget
            :shortcut: str (e.g. 'Ctrl+S') or Qt Key
            :func: a callable that gets executed
                   when triggering the action.
            """
            key_seq = QtGui.QKeySequence(shortcut)
            a.setShortcut(key_seq)
            a.setShortcutContext(QtCore.Qt.WidgetShortcut)
            a.triggered.connect(func)
            widget.addAction(a)

        for shortcut, func in editor_shortcuts.items():
            a = QtWidgets.QAction(self.editor)
            add_action(a, self.editor, shortcut, func)

        if self.use_tabs:
            terminal = self.parent_widget.parent().parent().terminal
            for shortcut, func in terminal_shortcuts.items():
                a = QtWidgets.QAction(terminal)
                add_action(a, terminal, shortcut, func)

            for shortcut, func in tab_shortcuts.items():
                a = QtWidgets.QAction(self.tabs)
                add_action(a, self.tabs, shortcut, func)

    def notimplemented(self, text):
        """ Development reminders to implement features """
        raise NotImplementedError(text)

    def get_selected_blocks(self, ignoreEmpty=True):
        """
        Utility method for getting lines in selection.
        """
        textCursor = self.editor.textCursor()
        doc = self.editor.document()
        start = textCursor.selectionStart()
        end = textCursor.selectionEnd()

        # get line numbers
        blockNumbers = set(
            [doc.findBlock(b).blockNumber() for b in range(start, end)])

        pos = textCursor.position()
        blockNumbers |= set([doc.findBlock(pos).blockNumber()])

        def isEmpty(b):
            return doc.findBlockByNumber(b).text().strip() != ''

        blocks = []
        for b in blockNumbers:
            bn = doc.findBlockByNumber(b)
            if not ignoreEmpty:
                blocks.append(bn)
            elif isEmpty(b):
                blocks.append(bn)

        return blocks

    def save(self):
        actions.save_action(self.tabs, self.editor)

    def open_module_file(self):
        textCursor = self.editor.textCursor()
        text = textCursor.selection().toPlainText()
        if not text.strip():
            return

        obj = actions.get_subobject(text)
        actions.open_module_file(obj)

    def offset_for_traceback(self, text=None):
        """
        Offset text using newlines to get proper line ref in tracebacks.
        """
        textCursor = self.editor.textCursor()

        if text is None:
            text = textCursor.selection().toPlainText()

        selection_offset = textCursor.selectionStart()
        doc = self.editor.document()
        block_num = doc.findBlock(selection_offset).blockNumber()
        text = '\n' * block_num + text
        return text

    def exec_text(self, text, whole_text):
        """
        Execute whatever text is passed into this function.

        :text: the actual text to be executed
        :whole_text: the whole text for context and full traceback
        """
        self.exec_text_signal.emit()
        error_line_numbers = execute.mainexec(text, whole_text)
        if error_line_numbers is None:
            return
        else:
            self.highlight_errored_lines(error_line_numbers)

    def exec_selected_text(self):
        """
        If text is selected, call exec on that text.
        If no text is selected, it will execute all
        text within boundaries demarcated by the symbols #&&
        """
        textCursor = self.editor.textCursor()
        whole_text = self.editor.toPlainText()

        if textCursor.hasSelection():
            text = self.offset_for_traceback()
            return self.exec_text(text, whole_text)

        if not '\n#&&' in whole_text:
            text = whole_text
            whole_text = '\n' + whole_text
            return self.exec_text(text, whole_text)

        text = whole_text
        whole_text = '\n' + whole_text

        pos = textCursor.position()
        text_before = text[:pos]
        text_after = text[pos:]
        symbol_pos = text_before.rfind('#&&')
        text_before = text_before.split('\n#&&')[-1]
        text_after = text_after.split('\n#&&')[0]
        text = text_before + text_after
        doc = self.editor.document()
        block_num = doc.findBlock(symbol_pos).blockNumber()
        text = '\n' * block_num + text

        self.exec_text(text, whole_text)

    def exec_current_line(self):
        """
        Calls exec with the text of the line the cursor is on.
        Calls lstrip on current line text to allow exec of indented text.
        """
        textCursor = self.editor.textCursor()
        whole_text = self.editor.toPlainText()

        if textCursor.hasSelection():
            return self.exec_selected_text()

        textCursor.select(QtGui.QTextCursor.BlockUnderCursor)
        text = textCursor.selection().toPlainText().lstrip()
        text = self.offset_for_traceback(text=text)

        whole_text = '\n' + whole_text
        error_line_numbers = execute.mainexec(text, whole_text, verbosity=1)
        if error_line_numbers is None:
            return
        else:
            self.highlight_errored_lines(error_line_numbers)

    def highlight_errored_lines(self, error_line_numbers):
        """
        Draw a red background on any lines that caused an error.
        """
        extraSelections = []

        cursor = self.editor.textCursor()
        doc = self.editor.document()
        for lineno in error_line_numbers:
            selection = QtWidgets.QTextEdit.ExtraSelection()
            lineColor = QtGui.QColor.fromRgbF(0.8, 0.1, 0, 0.2)

            selection.format.setBackground(lineColor)
            selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection,
                                         True)

            block = doc.findBlockByLineNumber(lineno - 1)
            cursor.setPosition(block.position())
            selection.cursor = cursor
            selection.cursor.clearSelection()
            extraSelections.append(selection)
        self.editor.setExtraSelections(extraSelections)

    @QtCore.Slot(QtGui.QKeyEvent)
    def return_handler(self, event):
        """
        New line with auto-indentation.
        """
        return self.indent_next_line()

    def indent_next_line(self):
        """
        Match next line indentation to current line
        If ':' is character in cursor position and
        current line contains non-whitespace
        characters, add an extra four spaces.
        """
        textCursor = self.editor.textCursor()
        text = textCursor.block().text()
        indentCount = len(text) - len(text.lstrip(' '))

        doc = self.editor.document()
        if doc.characterAt(textCursor.position() - 1) == ':':
            indentCount = indentCount + 4

        insertion = '\n' + ' ' * indentCount
        if len(text.strip()) == 0:
            insertion = '\n'

        if not self.editor.wait_for_autocomplete:
            textCursor.insertText(insertion)
            self.editor.setTextCursor(textCursor)

        return True

    @QtCore.Slot()
    def cut_line(self):
        """
        If no text selected, cut whole
        current line to clipboard.
        """
        textCursor = self.editor.textCursor()
        if textCursor.hasSelection():
            return

        textCursor.select(QtGui.QTextCursor.LineUnderCursor)
        text = textCursor.selectedText()
        textCursor.insertText('')

        QtGui.QClipboard().setText(text)

    def new_line_above(self):
        """
        Inserts new line above current.
        """
        textCursor = self.editor.textCursor()
        line = textCursor.block().text()
        indentCount = len(line) - len(line.lstrip(' '))
        indent = ' ' * indentCount
        textCursor.movePosition(textCursor.StartOfLine)
        self.editor.setTextCursor(textCursor)
        textCursor.insertText(indent + '\n')
        self.editor.moveCursor(textCursor.Left)

    def new_line_below(self):
        """
        Inserts new line below current.
        """
        textCursor = self.editor.textCursor()
        line = textCursor.block().text()
        indentCount = len(line) - len(line.lstrip(' '))
        indent = ' ' * indentCount
        textCursor.movePosition(textCursor.EndOfLine)
        self.editor.setTextCursor(textCursor)
        textCursor.insertText('\n' + indent)

    @QtCore.Slot()
    def tab_handler(self):
        """
        Indents selected text. If no text
        is selected, adds four spaces.
        """
        textCursor = self.editor.textCursor()
        if textCursor.hasSelection():
            self.indent()
        else:
            self.tab_space()

    def indent(self):
        """
        Indent Selected Text
        """
        blocks = self.get_selected_blocks()
        for block in blocks:
            cursor = QtGui.QTextCursor(block)
            cursor.movePosition(QtGui.QTextCursor.StartOfLine)
            cursor.insertText('    ')

    def unindent(self):
        """
        Unindent Selected Text
        TODO: Maintain original selection
        and cursor position.
        """
        blocks = self.get_selected_blocks(ignoreEmpty=False)
        for block in blocks:
            cursor = QtGui.QTextCursor(block)
            cursor.select(QtGui.QTextCursor.LineUnderCursor)
            lineText = cursor.selectedText()
            if lineText.startswith(' '):
                newText = str(lineText[:4]).replace(' ', '') + lineText[4:]
                cursor.insertText(newText)

    def tab_space(self):
        """ Insert spaces instead of tabs """
        self.editor.insertPlainText('    ')

    def next_tab(self):
        """
        Switch to the next tab.
        """
        if hasattr(self, 'tabs'):
            next_index = self.tabs.currentIndex() + 1
            if next_index <= self.tabs.count():
                self.tabs.setCurrentIndex(next_index)

    def previous_tab(self):
        """
        Switch to the next tab.
        """
        if hasattr(self, 'tabs'):
            self.tabs.setCurrentIndex(self.tabs.currentIndex() - 1)

    def jump_to_start(self):
        """
        Jump to first character in line.
        If at first character, jump to
        start of line.
        """
        textCursor = self.editor.textCursor()
        init_pos = textCursor.position()
        textCursor.select(QtGui.QTextCursor.LineUnderCursor)
        text = textCursor.selection().toPlainText()
        textCursor.movePosition(QtGui.QTextCursor.StartOfLine)
        pos = textCursor.position()
        offset = len(text) - len(text.lstrip())
        new_pos = pos + offset
        if new_pos != init_pos:
            textCursor.setPosition(new_pos, QtGui.QTextCursor.MoveAnchor)
        self.editor.setTextCursor(textCursor)

    def comment_toggle(self):
        """
        Toggles commenting out selected lines,
        or lines with cursor.
        """
        blocks = self.get_selected_blocks()

        # iterate through lines in doc commenting or uncommenting
        # based on whether everything is commented or not
        commentAllOut = any([
            not str(block.text()).lstrip().startswith('#') for block in blocks
        ])
        if commentAllOut:
            for block in blocks:
                cursor = QtGui.QTextCursor(block)
                cursor.select(QtGui.QTextCursor.LineUnderCursor)
                selectedText = cursor.selectedText()
                right_split = len(selectedText.lstrip())
                count = len(selectedText)
                split_index = count - right_split
                split_text = selectedText[split_index:]
                newText = ' ' * split_index + '#' + split_text
                cursor.insertText(newText)
        else:
            for block in blocks:
                cursor = QtGui.QTextCursor(block)
                cursor.select(QtGui.QTextCursor.LineUnderCursor)
                selectedText = cursor.selectedText()
                newText = str(selectedText).replace('#', '', 1)
                cursor.insertText(newText)

    @QtCore.Slot(str)
    def wrap_text(self, key):
        """
        Wrap selected text in brackets
        or quotes of type "key".
        """
        if key in ['\'', '"']:
            key_in = key
            key_out = key
        elif key in ['[', ']']:
            key_in = '['
            key_out = ']'
        elif key in ['(', ')']:
            key_in = '('
            key_out = ')'
        elif key in ['{', '}']:
            key_in = '{'
            key_out = '}'
        # elif key in ['<', '>']:
        #     key_in = '<'
        #     key_out = '>'

        textCursor = self.editor.textCursor()
        text = key_in + textCursor.selectedText() + key_out
        textCursor.insertText(text)

    def select_lines(self):
        """
        Sets current lines selected
        and moves cursor to beginning
        of next line.
        """
        textCursor = self.editor.textCursor()

        start = textCursor.selectionStart()
        end = textCursor.selectionEnd()

        textCursor.setPosition(end, QtGui.QTextCursor.MoveAnchor)
        textCursor.movePosition(QtGui.QTextCursor.EndOfLine)
        new_end = textCursor.position() + 1
        if new_end >= self.editor.document().characterCount():
            new_end = new_end - 1

        textCursor.setPosition(start, QtGui.QTextCursor.MoveAnchor)
        textCursor.movePosition(QtGui.QTextCursor.StartOfLine)

        textCursor.setPosition(new_end, QtGui.QTextCursor.KeepAnchor)
        self.editor.setTextCursor(textCursor)

    def join_lines(self):
        """
        Joins current line(s) with next by deleting the
        newline at the end of the current line(s).
        """
        textCursor = self.editor.textCursor()

        blocks = self.get_selected_blocks(ignoreEmpty=False)
        if len(blocks) > 1:
            text = textCursor.selectedText()
            text = ' '.join(ln.strip() for ln in text.splitlines())
            textCursor.insertText(text)
        else:
            block = textCursor.block()
            text = block.text()
            next_line = block.next().text().strip()
            new_text = text + ' ' + next_line

            textCursor.select(QtGui.QTextCursor.LineUnderCursor)
            textCursor.movePosition(QtGui.QTextCursor.EndOfLine)
            new_pos = textCursor.position() + 1
            if new_pos >= self.editor.document().characterCount():
                return
            textCursor.setPosition(new_pos, QtGui.QTextCursor.KeepAnchor)

            textCursor.insertText('')
            textCursor.select(QtGui.QTextCursor.LineUnderCursor)
            textCursor.insertText(new_text)

            self.editor.setTextCursor(textCursor)

    def delete_lines(self):
        """
        Deletes the contents of the current line(s).
        """
        textCursor = self.editor.textCursor()

        start = textCursor.selectionStart()
        end = textCursor.selectionEnd()
        textCursor.setPosition(start, QtGui.QTextCursor.MoveAnchor)
        textCursor.movePosition(QtGui.QTextCursor.StartOfLine)
        new_start = textCursor.position()

        textCursor.setPosition(end, QtGui.QTextCursor.MoveAnchor)
        textCursor.movePosition(QtGui.QTextCursor.EndOfLine)

        new_end = textCursor.position()

        textCursor.setPosition(new_start, QtGui.QTextCursor.KeepAnchor)

        if textCursor.selectedText() == '':
            textCursor.setPosition(start, QtGui.QTextCursor.MoveAnchor)
            next_line = new_end + 1
            if 0 < next_line >= self.editor.document().characterCount():
                next_line = next_line - 2
                if next_line == -1:
                    return
            textCursor.setPosition(next_line, QtGui.QTextCursor.KeepAnchor)

        textCursor.insertText('')

    def copy_block_or_selection(self):
        """
        If there's no text selected,
        copy the current block.
        """
        textCursor = self.editor.textCursor()
        selection = textCursor.selection()
        text = selection.toPlainText()
        if not text:
            textCursor.select(QtGui.QTextCursor.BlockUnderCursor)
            selection = textCursor.selection()
            text = selection.toPlainText()

        QtGui.QClipboard().setText(text)

    def select_word(self):
        """
        Selects the word under cursor if no selection.
        If selection, selects next occurence of the same word.
        TODO: 1 )could optionally highlight all occurences of the word
        and iterate to the next selection. 2) Would be nice if extra
        selections could be made editable. 3) Wrap around.
        """
        textCursor = self.editor.textCursor()
        if not textCursor.hasSelection():
            textCursor.select(QtGui.QTextCursor.WordUnderCursor)
            return self.editor.setTextCursor(textCursor)

        text = textCursor.selection().toPlainText()
        start_pos = textCursor.selectionStart()
        end_pos = textCursor.selectionEnd()
        word_len = abs(end_pos - start_pos)

        whole_text = self.editor.toPlainText()
        second_half = whole_text[end_pos:]
        next_pos = second_half.find(text)

        if next_pos == -1:
            return

        next_start = next_pos + start_pos + word_len
        next_end = next_start + word_len

        textCursor.setPosition(next_start, QtGui.QTextCursor.MoveAnchor)
        textCursor.setPosition(next_end, QtGui.QTextCursor.KeepAnchor)
        self.editor.setTextCursor(textCursor)

        extraSelections = []

        selection = QtWidgets.QTextEdit.ExtraSelection()

        lineColor = QtGui.QColor.fromRgbF(1, 1, 1, 0.3)
        selection.format.setBackground(lineColor)
        selection.cursor = self.editor.textCursor()
        selection.cursor.setPosition(start_pos, QtGui.QTextCursor.MoveAnchor)
        selection.cursor.setPosition(end_pos, QtGui.QTextCursor.KeepAnchor)
        extraSelections.append(selection)
        self.editor.setExtraSelections(extraSelections)

    def hop_brackets(self):
        """
        Jump to closest bracket, starting
        with closing bracket.
        """
        textCursor = self.editor.textCursor()
        pos = textCursor.position()
        whole_text = self.editor.toPlainText()

        first_half = whole_text[:pos]
        second_half = whole_text[pos:]
        first_pos = first_half.rfind('(')
        second_pos = second_half.find(')')

        first_pos = first_pos + 1
        second_pos = second_pos + pos

        new_pos = first_pos if whole_text[pos] == ')' else second_pos
        textCursor.setPosition(new_pos, QtGui.QTextCursor.MoveAnchor)
        self.editor.setTextCursor(textCursor)

    def select_between_brackets(self):
        """
        Selects text between [] {} ()
        TODO: implement [] and {}
        """
        textCursor = self.editor.textCursor()
        pos = textCursor.position()
        whole_text = self.editor.toPlainText()

        first_half = whole_text[:pos]
        second_half = whole_text[pos:]
        first_pos = first_half.rfind('(')
        second_pos = second_half.find(')')

        first_pos = first_pos + 1
        second_pos = second_pos + pos

        textCursor.setPosition(first_pos, QtGui.QTextCursor.MoveAnchor)
        textCursor.setPosition(second_pos, QtGui.QTextCursor.KeepAnchor)
        self.editor.setTextCursor(textCursor)

    def search_input(self):
        """
        Very basic search dialog.
        TODO: Create a QAction/util for this
        as it is also accessed through
        the right-click menu.
        """
        getText = QtWidgets.QInputDialog.getText
        dialog = getText(
            self.editor,
            'Search',
            '',
        )
        text, ok = dialog
        if not ok:
            return

        textCursor = self.editor.textCursor()
        original_pos = textCursor.position()

        # start the search from the beginning of the document
        textCursor.setPosition(0, QtGui.QTextCursor.MoveAnchor)
        document = self.editor.document()
        cursor = document.find(text, textCursor)
        pos = cursor.position()
        if pos != -1:
            self.editor.setTextCursor(cursor)

    def duplicate_lines(self):
        """
        Duplicates the current line or
        selected text downwards.
        """
        textCursor = self.editor.textCursor()
        if textCursor.hasSelection():
            selected_text = textCursor.selectedText()
            for i in range(2):
                textCursor.insertText(selected_text)
                self.editor.setTextCursor(textCursor)
        else:
            textCursor.movePosition(QtGui.QTextCursor.EndOfLine)
            end_pos = textCursor.position()
            textCursor.movePosition(QtGui.QTextCursor.StartOfLine)
            textCursor.setPosition(end_pos, QtGui.QTextCursor.KeepAnchor)
            selected_text = textCursor.selectedText()
            textCursor.insertText(selected_text + '\n' + selected_text)

    def delete_to_end_of_line(self):
        """
        Deletes characters from cursor
        position to end of line.
        """
        textCursor = self.editor.textCursor()
        pos = textCursor.position()
        textCursor.movePosition(QtGui.QTextCursor.EndOfLine)
        textCursor.setPosition(pos, QtGui.QTextCursor.KeepAnchor)
        textCursor.insertText('')

    def delete_to_start_of_line(self):
        """
        Deletes characters from cursor
        position to start of line.
        """
        textCursor = self.editor.textCursor()
        pos = textCursor.position()
        textCursor.movePosition(QtGui.QTextCursor.StartOfLine)
        textCursor.setPosition(pos, QtGui.QTextCursor.KeepAnchor)
        textCursor.insertText('')

    def print_help(self):
        """
        Prints documentation
        for selected object
        """
        cursor = self.editor.textCursor()
        selection = cursor.selection()
        text = selection.toPlainText().strip()
        if not text:
            return
        obj = __main__.__dict__.get(text)
        if obj is not None:
            print(obj.__doc__)
        elif text:
            exec('help(' + text + ')', __main__.__dict__)

    def print_type(self):
        """
        Prints type
        for selected object
        """
        cursor = self.editor.textCursor()
        selection = cursor.selection()
        text = selection.toPlainText().strip()
        if not text:
            return
        obj = __main__.__dict__.get(text)
        if obj is not None:
            print(type(obj))
        elif text:
            exec('print(type(' + text + '))', __main__.__dict__)

    def zoom_in(self):
        """
        Zooms in by changing the font size.
        """
        font = self.editor.font()
        size = font.pointSize()
        new_size = size + 1
        font.setPointSize(new_size)
        self.editor.setFont(font)

    def zoom_out(self):
        """
        Zooms out by changing the font size.
        """
        font = self.editor.font()
        size = font.pointSize()
        new_size = size - 1 if size > 1 else 1
        font.setPointSize(new_size)
        self.editor.setFont(font)

    def wheel_zoom(self, event):
        """
        Zooms by changing the font size
        according to the wheel zoom delta.
        """
        font = self.editor.font()
        size = font.pointSize()
        delta = event.delta()
        amount = int(delta / 10) if delta > 1 or delta < -1 else delta
        new_size = size + amount
        new_size = new_size if new_size > 0 else 1
        font.setPointSize(new_size)
        self.editor.setFont(font)

    def move_blocks_up(self):
        """
        Moves selected blocks upwards.
        """
        restoreSelection = False
        textCursor = self.editor.textCursor()
        if textCursor.hasSelection():
            restoreSelection = True

        start = textCursor.selectionStart()
        end = textCursor.selectionEnd()
        selection_length = end - start
        textCursor.setPosition(start, QtGui.QTextCursor.MoveAnchor)
        textCursor.movePosition(QtGui.QTextCursor.StartOfBlock)
        new_start = textCursor.position()

        textCursor.setPosition(end, QtGui.QTextCursor.MoveAnchor)
        textCursor.movePosition(QtGui.QTextCursor.EndOfBlock)

        start_offset = start - new_start

        if new_start == 0:
            return

        textCursor.setPosition(new_start, QtGui.QTextCursor.KeepAnchor)
        selectedText = textCursor.selectedText()

        textCursor.insertText('')
        textCursor.deletePreviousChar()
        textCursor.movePosition(QtGui.QTextCursor.StartOfBlock)
        pos = textCursor.position()
        textCursor.insertText(selectedText + '\n')
        textCursor.setPosition(pos, QtGui.QTextCursor.MoveAnchor)

        if restoreSelection:
            moved_start = textCursor.position() + start_offset
            textCursor.setPosition(moved_start, QtGui.QTextCursor.MoveAnchor)
            moved_end = textCursor.position() + selection_length
            textCursor.setPosition(moved_end, QtGui.QTextCursor.KeepAnchor)
        else:
            new_pos = pos + start_offset
            textCursor.setPosition(new_pos, QtGui.QTextCursor.MoveAnchor)

        self.editor.setTextCursor(textCursor)

    def move_blocks_down(self):
        """
        Moves selected blocks downwards.
        """
        restoreSelection = False

        textCursor = self.editor.textCursor()
        if textCursor.hasSelection():
            restoreSelection = True

        start = textCursor.selectionStart()
        end = textCursor.selectionEnd()
        selection_length = end - start

        textCursor.setPosition(start, QtGui.QTextCursor.MoveAnchor)
        textCursor.movePosition(QtGui.QTextCursor.StartOfBlock)
        new_start = textCursor.position()

        textCursor.setPosition(end, QtGui.QTextCursor.MoveAnchor)
        textCursor.movePosition(QtGui.QTextCursor.EndOfBlock)
        new_end = textCursor.position()

        if new_end + 1 >= self.editor.document().characterCount():
            return

        end_offset = new_end - end

        textCursor.setPosition(new_start, QtGui.QTextCursor.KeepAnchor)
        selectedText = textCursor.selectedText()
        textCursor.insertText('')
        textCursor.deleteChar()
        textCursor.movePosition(QtGui.QTextCursor.EndOfBlock)
        textCursor.insertText('\n' + selectedText)

        if restoreSelection:
            moved_end = textCursor.position() - end_offset
            textCursor.setPosition(moved_end, QtGui.QTextCursor.MoveAnchor)
            moved_start = moved_end - selection_length
            textCursor.setPosition(moved_start, QtGui.QTextCursor.KeepAnchor)
        else:
            pos = textCursor.position()
            new_pos = pos - end_offset
            textCursor.setPosition(new_pos, QtGui.QTextCursor.MoveAnchor)

        self.editor.setTextCursor(textCursor)

    def move_to_top(self):
        """
        Move selection or line if no
        selection to top of document.
        """
        textCursor = self.editor.textCursor()
        if not textCursor.hasSelection():
            textCursor.select(QtGui.QTextCursor.LineUnderCursor)
        text = textCursor.selectedText()
        textCursor.insertText('')
        textCursor.setPosition(0, QtGui.QTextCursor.MoveAnchor)
        textCursor.insertText(text)
        self.editor.setTextCursor(textCursor)

    def move_to_bottom(self):
        """
        Move selection or line if no
        selection to bottom of document.
        """
        textCursor = self.editor.textCursor()
        if not textCursor.hasSelection():
            textCursor.select(QtGui.QTextCursor.LineUnderCursor)
        text = textCursor.selectedText()
        textCursor.insertText('')
        end = len(self.editor.toPlainText())
        textCursor.setPosition(end, QtGui.QTextCursor.MoveAnchor)
        textCursor.insertText(text)
        self.editor.setTextCursor(textCursor)