Ejemplo n.º 1
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)
Ejemplo n.º 2
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)
Ejemplo n.º 3
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)
Ejemplo n.º 4
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)
Ejemplo n.º 5
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)
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)
Ejemplo n.º 7
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)
Ejemplo n.º 8
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
Ejemplo n.º 9
0
class Speaker(QtCore.QObject):
    """ Used to relay sys stdout, stderr, stdin
    """
    emitter = QtCore.Signal(str)
Ejemplo n.º 10
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)
Ejemplo n.º 11
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)
Ejemplo n.º 12
0
class PopupBarTest(QtCore.QObject):
    """
    Test class for the popup_bar    
    """
    show_popup_signal = QtCore.Signal()
    def __init__(self, tabs):
        super(PopupBarTest, self).__init__()
        #self.setParent(tabs)
        
        self.tabeditor = tabs
        self.editor = tabs.editor
        self.tabs = tabs.tabs
        self.setParent(tabs)
        self.show_popup_signal.connect(self.show_popup_bar)
        
    def show_popup_bar(self):
        #editor = _ide.python_editor.editor
        #layout = _ide.python_editor.tabeditor.layout()
        
        editor = self.editor
        layout = self.tabeditor.layout()

        # first remove any previous widgets
        name = 'Document out of sync warning'
        for i in range(layout.count()):
            item = layout.itemAt(i)
            if item is None:
                continue
            widget = item.widget()
            if widget.objectName() != name:
                continue
            layout.removeItem(item)
            widget.deleteLater()

        popup_bar = QtWidgets.QWidget()
        popup_bar.setObjectName('Document out of sync warning')
        bar_layout = QtWidgets.QHBoxLayout(popup_bar)

        l = QtWidgets.QLabel()
        l.setText('This tab is out of sync with the autosave.')
        new_button     = QtWidgets.QPushButton('Load into New Tab')
        save_button    = QtWidgets.QPushButton('Save This Version')
        update_button  = QtWidgets.QPushButton('Update From Autosave')
        diff_button    = QtWidgets.QPushButton('Show Diff')

        stylesheet = """
        QPushButton { background-color: #444; }
        QPushButton:hover { background-color: orange; }
        """

        for b in new_button, save_button, update_button, diff_button:
            #b.setFlat(True)
            b.setStyleSheet(stylesheet)

        for b in l, new_button, save_button, update_button, diff_button:
            bar_layout.addWidget(b)

        layout.insertWidget(1, popup_bar)
        popup_bar.setMaximumHeight(0)

        #print popup_bar.maximumHeight()
        #popup_bar.setMaximumHeight(46)
        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

        anim_popup_bar(popup_bar)
Ejemplo n.º 13
0
class TabEditor(QtWidgets.QWidget):
    """
    A psuedo-QTabWidget that contains
    a QTabBar and a single Editor.
    """
    tab_switched_signal = QtCore.Signal()

    def __init__(self, parent=None):
        super(TabEditor, self).__init__(parent)
        if parent is not None:
            self.setParent(parent)

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

        self.tab_widget = QtWidgets.QWidget()
        twl = QtWidgets.QHBoxLayout(self.tab_widget)
        self.tab_widget_layout = twl
        self.tab_widget_layout.setContentsMargins(0, 0, 0, 0)
        self.tabs = Tabs()
        self.tab_widget_layout.addWidget(self.tabs)

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

        nb = QtWidgets.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(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.
        QtCore.QTimer.singleShot(0, self.connect_signals)

    def connect_signals(self):
        """
        We probably want to run this after tabs
        are loaded.
        """
        self.tabs.currentChanged.connect(self.set_editor_contents)
        self.tabs.tab_close_signal.connect(self.empty_if_last_tab_closed)

        self.editor.cursorPositionChanged.connect(self.store_cursor_position)
        self.editor.selectionChanged.connect(self.store_selection)

        # this is something we're going to want
        # only when tab already set (and not
        # when switching)
        self.editor.text_changed_signal.connect(self.save_text_in_tab)

    def show_tab_menu(self):
        """
        Show a list of tabs and go to
        the tab clicked.
        """
        menu = QtWidgets.QMenu()
        current_index = self.tabs.currentIndex()
        from functools import partial
        for i in range(self.tabs.count()):
            tab_name = self.tabs.tabText(i)
            if not tab_name.strip():
                continue
            func = partial(self.tabs.setCurrentIndex, i)
            action = menu.addAction(tab_name, func)
            if i == current_index:
                font = action.font()
                font.setBold(True)
                font.setUnderline(True)
                action.setFont(font)
                menu.setActiveAction(action)
        menu.exec_(QtGui.QCursor().pos())

    def new_tab(self, tab_name=None, tab_data={}):
        return self.tabs.new_tab(tab_name=tab_name, tab_data=tab_data)

    def close_current_tab(self):
        raise NotImplementedError

    @QtCore.Slot(str)
    def empty_if_last_tab_closed(self, uid):
        if self.tabs.count() == 0:
            self.editor.setPlainText('')

    @QtCore.Slot(int)
    def set_editor_contents(self, index):
        """
        Set editor contents to the data
        found in tab #index
        """
        data = self.tabs.tabData(index)

        if not data:
            # this will be a new
            # empty tab, ignore.
            return

        text = data['text']

        if text is None or not text.strip():
            path = data.get('path')
            text = self.updata_text(data, path)

        # collect data before setting editor text
        cursor_pos = self.tabs.get('cursor_pos')
        selection = self.tabs.get('selection')

        self.editor.setPlainText(text)

        if cursor_pos is not None:
            block_pos, cursor_pos = cursor_pos

            # set first block visible
            cursor = self.editor.textCursor()
            self.editor.moveCursor(cursor.End)
            cursor.setPosition(block_pos)
            self.editor.setTextCursor(cursor)

            # restore cursor position
            cursor = self.editor.textCursor()
            cursor.setPosition(cursor_pos)
            self.editor.setTextCursor(cursor)

        if selection is not None:
            # TODO: this won't restore a selection
            # that starts from below and selects
            # upwards :( (yet)
            cursor = self.editor.textCursor()
            has, start, end = selection
            if has:
                cursor.setPosition(start, QtGui.QTextCursor.MoveAnchor)
                cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
            self.editor.setTextCursor(cursor)
        self.editor.setFocus(QtCore.Qt.MouseFocusReason)

        # for the autosave check_document_modified
        self.tab_switched_signal.emit()

    def updata_text(self, data, path):
        if path is None or path is not None and not os.path.isfile(path):
            text = ''
        else:
            with open(path, 'r') as f:
                text = f.read()
            data['text'] = text
        return text

    def store_cursor_position(self):
        editor = self.editor
        cursor = editor.textCursor()
        block = editor.firstVisibleBlock()
        self.tabs['cursor_pos'] = (block.position(), cursor.position())

    def store_selection(self):
        tc = self.editor.textCursor()
        status = (tc.hasSelection(), tc.selectionStart(), tc.selectionEnd())
        self.tabs['selection'] = status

    def save_text_in_tab(self):
        """
        Store the editor's current text
        in the current tab.
        Strangely appears to be called twice
        on current editor's textChanged and
        backspace key...
        """
        if self.tabs.count() == 0:
            self.new_tab()

        saved = self.tabs.get('saved')
        original_text = self.tabs.get('original_text')
        if saved and not original_text:
            # keep original text in case
            # revert is required
            text = self.tabs['text']
            self.tabs['original_text'] = text
            self.tabs['saved'] = False
            self.tabs.repaint()
        elif original_text is not None:
            text = self.editor.toPlainText()
            if original_text == text:
                self.tabs['saved'] = True
                self.tabs.repaint()

        self.tabs['text'] = self.editor.toPlainText()
Ejemplo n.º 14
0
class CloseButton(QtWidgets.QAbstractButton):
    close_clicked_signal = QtCore.Signal(QtCore.QPoint)

    def __init__(self, parent=None):
        super(CloseButton, self).__init__(parent=parent)
        self.setFocusPolicy(QtCore.Qt.NoFocus)
        self.setToolTip('Close Tab')
        self.resize(self.sizeHint())
        self.tab_saved = False

    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)

    def enterEvent(self, event):
        if self.isEnabled():
            self.update()
        super(CloseButton, self).enterEvent(event)

    def leaveEvent(self, event):
        if self.isEnabled():
            self.update()
        super(CloseButton, self).leaveEvent(event)

    def mousePressEvent(self, event):
        super(CloseButton, self).mousePressEvent(event)
        parent_pos = self.mapToParent(event.pos())
        self.close_clicked_signal.emit(parent_pos)

    def paintEvent(self, event):
        """
        Adapted from qttabbar.cpp
        """
        p = QtGui.QPainter(self)
        opt = QtWidgets.QStyleOption()
        opt.initFrom(self)
        QStyle = QtWidgets.QStyle
        opt.state |= QStyle.State_AutoRaise
        hovered = False
        if (self.isEnabled() and self.underMouse() and (not self.isChecked())
                and (not self.isDown())):
            hovered = True
            opt.state |= QStyle.State_Raised
        if self.isChecked():
            opt.state |= QStyle.State_On
        if self.isDown():
            opt.state |= QStyle.State_Sunken

        if isinstance(self.parent(), QtWidgets.QTabBar):
            tb = self.parent()
            index = tb.currentIndex()
            BP = QtWidgets.QTabBar.ButtonPosition
            position = BP(tb.style().styleHint(
                QStyle.SH_TabBar_CloseButtonPosition, opt, tb))
            tab_button = tb.tabButton(index, position)
            if (self == tab_button):
                opt.state |= QStyle.State_Selected

        self.style().drawPrimitive(QtWidgets.QStyle.PE_IndicatorTabClose, opt,
                                   p, self)

        # the below is all good, but wait
        # until 'saved' status is properly
        # locked down.
        """
Ejemplo n.º 15
0
class Tabs(QtWidgets.QTabBar):
    """
    Make tabs fast by overriding the
    paintEvent to draw close buttons.

    Current tab data can be easily
    indexed out of this class via Tabs[key].

    FIXME: This is a GUI class. The data management
    should happen within a data model. This class
    should only serve as a view into that model,
    to permit other views to similarly display the
    model's content.
    """
    pen = QtGui.QPen()
    brush = QtGui.QBrush()
    mouse_over_rect = False
    over_button = -1
    start_move_index = -1

    # for autosave purposes:
    contents_saved_signal = QtCore.Signal(str)
    # in case we receive a ctrl+shift+w signal
    # to close the tab:
    tab_close_signal = QtCore.Signal(str)
    tab_renamed_signal = QtCore.Signal(str, str, str, str, object)
    tab_repositioned_signal = QtCore.Signal(int, int)
    reset_tab_signal = QtCore.Signal()

    def __init__(self, parent=None):
        super(Tabs, self).__init__(parent)
        self.tab_pressed = False
        self.setStyleSheet(TAB_STYLESHEET)
        self.setMovable(True)
        self.setExpanding(False)
        self.pressed_uid = ''
        self._hovered_index = -2

        # # a stack for navigating positions
        # # `list` of `tuples`
        # # [(`str` tab_uid, `int` cursor_pos),]
        # self.cursor_previous = []
        # self.cursor_next = []

        cb = CloseButton(self)
        self.tab_close_button = cb
        cb.hide()
        cb.close_clicked_signal.connect(self.clicked_close)

    @QtCore.Slot(str)
    def new_tab(self, tab_name=None, tab_data={}):
        """
        Creates a new tab.
        """
        index = self.currentIndex() + 1

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

        self.insertTab(index, tab_name)
        data = {
            'uuid': str(uuid.uuid4()),
            'name': tab_name,
            'text': '',
            'path': '',
            'date': '',
            'saved': False,
        }
        data.update(**tab_data)
        self.setTabData(index, data)
        self.setCurrentIndex(index)

    def __getitem__(self, name):
        """
        Allow easy lookup for
        the current tab's data.
        """
        index = self.currentIndex()
        if index == -1:
            raise KeyError('No current tab.')

        data = self.tabData(index)
        if data is None:
            raise KeyError('No tab data available for index %i.' % index)

        return data[name]

    def get(self, name):
        try:
            return self[name]
        except KeyError:
            return None

    def __setitem__(self, name, value):
        """
        Easily set current tab's value.
        """
        if self.count() == 0:
            return
        index = self.currentIndex()
        tab_data = self.tabData(index)
        tab_data[name] = value
        return self.setTabData(index, tab_data)

    def tab_only_rect(self):
        """
        self.rect() without the <> buttons.
        """
        rect = self.rect()
        lB, rB = [
            c for c in self.children() if isinstance(c, QtWidgets.QToolButton)
        ]
        side_button_width = lB.width() + rB.width() + 15
        rect.adjust(0, 0, -side_button_width, 0)
        return rect

    def event(self, event):
        try:
            # Check class (after reload, opening a new window, etc)
            # this can raise TypeError:
            # super(type, obj): obj must be an instance or subtype of type
            if not issubclass(Tabs, self.__class__):
                return False
        except TypeError:
            return False

        try:
            QE = QtCore.QEvent
        except AttributeError:
            return True
        if not hasattr(event, 'type'):
            return
        if event.type() in [
                QE.HoverEnter,
                QE.HoverMove,
                QE.HoverLeave,
                QE.Paint,
        ]:
            self.handle_close_button_display(event)

        elif event.type() == QtCore.QEvent.ToolTip:
            pos = self.mapFromGlobal(self.cursor().pos())
            if self.rect().contains(pos):
                i = self.tabAt(pos)
                data = self.tabData(i)
                if data is not None:
                    path = data.get('path')
                    if path:
                        self.setTabToolTip(i, path)
                    else:
                        self.setTabToolTip(i, data.get('name'))

        return super(Tabs, self).event(event)

    def handle_close_button_display(self, e):  # sourcery no-metrics

        if self.tab_pressed:
            if self.tab_close_button.isVisible():
                self.tab_close_button.hide()
            return

        if e.type() in [e.HoverEnter, e.MouseButtonRelease]:
            pos = e.pos()
            self._hovered_index = i = self.tabAt(pos)
            self.tab_close_button.show()
            self.tab_close_button.raise_()
            self.move_tab_close_button(pos)

            data = self.tabData(i)
            if data is not None:
                ts = data.get('saved')
                tcb = self.tab_close_button
                tcb.tab_saved = ts

        elif e.type() == QtCore.QEvent.HoverMove:
            pos = e.pos()
            i = self.tabAt(pos)
            if i != self._hovered_index:
                self.tab_close_button.show()
                self.move_tab_close_button(pos)
                self._hovered_index = i

                data = self.tabData(i)
                if data is not None:
                    ts = data.get('saved')
                    tcb = self.tab_close_button
                    tcb.tab_saved = ts

        elif e.type() == QtCore.QEvent.HoverLeave:
            self.tab_close_button.hide()

        elif e.type() == QtCore.QEvent.Paint:
            if hasattr(self, 'name_edit') and self.name_edit.isVisible():
                self.tab_close_button.hide()
                return
            pos = self.mapFromGlobal(self.cursor().pos())

            if not self.rect().contains(pos):
                return

            if not self.tab_only_rect().contains(pos):
                return

            i = self.tabAt(pos)
            if (i != self._hovered_index):
                self.move_tab_close_button(pos)
                self._hovered_index = i

            if self.tab_close_button.isVisible():
                data = self.tabData(i)
                if data is not None:
                    ts = data.get('saved')
                    tcb = self.tab_close_button
                    tcb.tab_saved = ts

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

    def mousePressEvent(self, event):
        """
        """
        # this doesn't cover wacom...
        # might need tabletPressEvent
        if event.button() == QtCore.Qt.LeftButton:
            self.tab_pressed = True
            pt = event.pos()
            i = self.tabAt(pt)
            self.pressedIndex = i
            self.start_move_index = i
            data = self.tabData(i)
            if data is not None:
                self.pressed_uid = data['uuid']
            self.dragStartPosition = pt

            # handle name edit still being visible
            if hasattr(self, 'name_edit'):
                try:
                    if self.name_edit.isVisible():
                        ti = self.name_edit.tab_index
                        if ti != i:
                            self.rename_tab()
                except RuntimeError:
                    # likely that the lineedit
                    # has been deleted
                    del self.name_edit

        # if not returned, handle clicking on tab
        return super(Tabs, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        """
        TODO: make close buttons follow tabs
        when they're moving!
        """
        if self.count() == 0:
            return
        if event.buttons() == QtCore.Qt.LeftButton:
            i = self.currentIndex()
            if (not hasattr(self, 'pressedIndex') or self.pressedIndex != i):
                self.pressedIndex = i
            data = self.tabData(self.pressedIndex)
            if data['uuid'] != self.pressed_uid:
                debug('wrong tab!')

        return super(Tabs, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        self.tab_pressed = False
        i = self.tabAt(event.pos())
        if event.button() == QtCore.Qt.LeftButton:
            if i == -1:
                i = self.currentIndex()
            if (i != self.start_move_index):
                self.tab_repositioned_signal.emit(i, self.start_move_index)
            self.handle_close_button_display(event)

        elif event.button() == QtCore.Qt.RightButton:
            menu = QtWidgets.QMenu()

            rename = partial(self._show_name_edit, i)
            menu.addAction('Rename', rename)

            move_to_first = partial(self.move_to_first, i)
            menu.addAction('Move Tab to First', move_to_first)

            move_to_last = partial(self.move_to_last, i)
            menu.addAction('Move Tab to Last', move_to_last)

            close_tab_func = partial(self.removeTab, i)
            menu.addAction('Close Tab', close_tab_func)

            copy_file_path = partial(self.copy_tab_file_path, i)
            menu.addAction('Copy File Path', copy_file_path)

            # Other ideas (TODO)
            """
            menu.addAction('Close Other Tabs', )
            menu.addAction('Close Tabs to Right', )
            menu.addAction('Close Tabs to Left', )
            menu.addAction('Pin Tab', )
            """

            menu.exec_(QtGui.QCursor().pos())

        elif event.button() == QtCore.Qt.MiddleButton:
            if i != -1:
                self.removeTab(i)

        return super(Tabs, self).mouseReleaseEvent(event)

    def mouseDoubleClickEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            self.show_name_edit(event)
        return super(Tabs, self).mouseDoubleClickEvent(event)

    def show_name_edit(self, event):
        """
        Shows a QLineEdit widget where the tab
        text is, allowing renaming of tabs.
        """
        try:
            self.rename_tab()
        except RuntimeError:  # likely that the lineedit has been deleted
            del self.name_edit
            return

        index = self.tabAt(event.pos())
        self._show_name_edit(index)

    def _show_name_edit(self, index):
        """
        Shows a QLineEdit widget where the tab
        text is, allowing renaming of tabs.
        """
        rect = self.tabRect(index)

        label = self.tabText(index)
        self.renaming_label = label

        self.tab_text = label
        self.tab_index = index

        self.name_edit = QtWidgets.QLineEdit(self)
        self.name_edit.resize(rect.width(), rect.height() - 7)
        self.name_edit.tab_index = index
        self.name_edit.tab_text = label
        self.name_edit.editingFinished.connect(self.rename_tab)
        self.name_edit.setText(label.strip())
        self.name_edit.selectAll()
        self.name_edit.show()
        self.name_edit.raise_()
        p = rect.topLeft()
        self.name_edit.move(p.x(), p.y() + 5)

        self.name_edit.setFocus(QtCore.Qt.MouseFocusReason)

    def copy_tab_file_path(self, index):
        """
        Copy the current tab's file path
        (if it has one) to the clipboard.
        """
        data = self.tabData(index)
        path = data.get('path')
        if path is None or not path.strip():
            print('No file path for "{0}".'.format(data['name']))
            return
        clipboard = QtGui.QClipboard()
        clipboard.setText(path)
        print('Path copied to clipboard:')
        print(path)

    def move_to_first(self, index):
        """
        Move the current tab to the first position.
        """
        self.setCurrentIndex(0)
        self.moveTab(index, 0)
        self.setCurrentIndex(0)
        self.tab_repositioned_signal.emit(index, 0)

    def move_to_last(self, index):
        """
        Move the current tab to the last position.
        """
        last = self.count() - 1
        self.setCurrentIndex(last)
        self.moveTab(index, last)
        self.setCurrentIndex(last)
        self.tab_repositioned_signal.emit(index, last)

    def rename_tab(self):
        """
        Sets the label of the tab the QLineEdit was
        spawned over.
        """
        if not (hasattr(self, 'name_edit') and self.name_edit.isVisible()):
            return

        self.name_edit.hide()

        label = self.name_edit.text().strip()
        if not bool(label):
            label = self.name_edit.tab_text

        index = self.name_edit.tab_index

        if self.renaming_label == label:
            return

        # FIXME: if the tab is not
        # positioned to the right,
        # this can cause a jump.
        self.setTabText(index, label)

        data = self.tabData(index)
        data['name'] = label
        self.tab_renamed_signal.emit(data['uuid'], data['name'], data['text'],
                                     str(index), data.get('path'))
        self.setTabData(index, data)

    @QtCore.Slot()
    def remove_current_tab(self):
        self.removeTab(self.currentIndex())

    @QtCore.Slot(QtCore.QPoint)
    def clicked_close(self, pos):
        for i in range(self.count()):
            rect = self.tabRect(i)
            if rect.contains(pos):
                label = self.tabText(i)
                self.removeTab(i)

    def removeTab(self, index):
        """
        The only way to remove a tab.

        If the tab's 'saved' property is not set to True,
        the user will be prompted to save.

        Setting 'saved' to True can only happen via:
        A) actually saving the file, or
        B) loading a stored tab which is just a saved file
        with no autosave text contents.

        If the tab is removed, its uuid is emitted which will notify
        the autosave handler to also remove the autosave.
        """
        data = self.tabData(index)
        if not isinstance(data, dict):
            return

        text = data.get('text')
        has_text = False
        if hasattr(text, 'strip'):
            has_text = bool(text.strip())

        if has_text:
            path = data.get('path')

            self.updata_saved(data, text, path)

            saved = (data.get('saved') is True)
            if not saved:
                i = index
                if not self.prompt_user_to_save(i):
                    return

        super(Tabs, self).removeTab(index)

        self.tab_close_signal.emit(data['uuid'])

    def updata_saved(self, data, text, path):
        if path is None or path is not None and not os.path.isfile(path):
            # can't be sure it's saved
            # if it has no path
            data['saved'] = False
        else:
            with open(path, 'r') as f:
                saved_text = f.read()
            if saved_text != text:
                data['saved'] = False

    def prompt_user_to_save(self, index):
        """ Ask the user if they wish to close
        a tab that has unsaved contents.
        """
        name = self.tabText(index)
        msg_box = QtWidgets.QMessageBox()
        msg_box.setWindowTitle('Save changes?')
        msg_box.setText('%s has not been saved to a file.' % name)
        msg_box.setInformativeText('Do you want to save your changes?')
        buttons = (msg_box.Save | msg_box.Discard | msg_box.Cancel)
        msg_box.setStandardButtons(buttons)
        msg_box.setDefaultButton(msg_box.Save)
        ret = msg_box.exec_()

        user_cancelled = (ret == msg_box.Cancel)

        if (ret == msg_box.Save):
            data = self.tabData(index)
            path = save.save(data['text'], data['path'])
            if path is None:
                user_cancelled = True

        return not user_cancelled
Ejemplo n.º 16
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)
Ejemplo n.º 17
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
Ejemplo n.º 18
0
class VB(QtCore.QObject):
    s = QtCore.Signal()