Beispiel #1
0
    def __init__(self, parent, font=None, lexer=None):
        super(CodeWidget, self).__init__(parent)

        self.highlighter = PygmentsHighlighter(self.document(), lexer)
        self.line_number_widget = LineNumberWidget(self)
        self.breakpoints_widget = BreakpointsWidget(self)
        self.status_widget = StatusGutterWidget(self)

        if font is None:
            # Set a decent fixed width font for this platform.
            font = QtGui.QFont()
            if sys.platform == 'win32':
                # Prefer Consolas, but fall back to Courier if necessary.
                font.setFamily('Consolas')
                if not font.exactMatch():
                    font.setFamily('Courier')
            elif sys.platform == 'darwin':
                font.setFamily('Monaco')
            else:
                font.setFamily('Monospace')
            font.setStyleHint(QtGui.QFont.TypeWriter)
        self.set_font(font)

        # What that highlight color should be.
        self.line_highlight_color = QtGui.QColor(QtCore.Qt.yellow).lighter(160)

        # Auto-indentation behavior
        self.auto_indent = True
        self.smart_backspace = True

        # Tab settings
        self.tabs_as_spaces = True
        self.tab_width = 4

        self.indent_character = ':'
        self.comment_character = '#'

        # Set up gutter widget and current line highlighting
        self.blockCountChanged.connect(self.update_line_number_width)
        self.updateRequest.connect(self.update_line_numbers)

        # Set up breakpoint signals
        self.breakpoints_widget.gutterClicked.connect(self.breakpointClicked)

        self.update_line_number_width()

        # Don't wrap text
        self.setLineWrapMode(QtGui.QPlainTextEdit.NoWrap)

        # Key bindings
        self.indent_key = QtGui.QKeySequence(QtCore.Qt.Key_Tab)
        self.unindent_key = QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Backtab)
        self.comment_key = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Slash)
        self.backspace_key = QtGui.QKeySequence(QtCore.Qt.Key_Backspace)
Beispiel #2
0
class CodeWidget(QtGui.QPlainTextEdit):
    """ A widget for viewing and editing code.
    """

    breakpointClicked = QtCore.Signal(int)

    ###########################################################################
    # CodeWidget interface
    ###########################################################################

    def __init__(self, parent, font=None, lexer=None):
        super(CodeWidget, self).__init__(parent)

        self.highlighter = PygmentsHighlighter(self.document(), lexer)
        self.line_number_widget = LineNumberWidget(self)
        self.breakpoints_widget = BreakpointsWidget(self)
        self.status_widget = StatusGutterWidget(self)

        if font is None:
            # Set a decent fixed width font for this platform.
            font = QtGui.QFont()
            if sys.platform == 'win32':
                # Prefer Consolas, but fall back to Courier if necessary.
                font.setFamily('Consolas')
                if not font.exactMatch():
                    font.setFamily('Courier')
            elif sys.platform == 'darwin':
                font.setFamily('Monaco')
            else:
                font.setFamily('Monospace')
            font.setStyleHint(QtGui.QFont.TypeWriter)
        self.set_font(font)

        # What that highlight color should be.
        self.line_highlight_color = QtGui.QColor(QtCore.Qt.yellow).lighter(160)

        # Auto-indentation behavior
        self.auto_indent = True
        self.smart_backspace = True

        # Tab settings
        self.tabs_as_spaces = True
        self.tab_width = 4

        self.indent_character = ':'
        self.comment_character = '#'

        # Set up gutter widget and current line highlighting
        self.blockCountChanged.connect(self.update_line_number_width)
        self.updateRequest.connect(self.update_line_numbers)

        # Set up breakpoint signals
        self.breakpoints_widget.gutterClicked.connect(self.breakpointClicked)

        self.update_line_number_width()

        # Don't wrap text
        self.setLineWrapMode(QtGui.QPlainTextEdit.NoWrap)

        # Key bindings
        self.indent_key = QtGui.QKeySequence(QtCore.Qt.Key_Tab)
        self.unindent_key = QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Backtab)
        self.comment_key = QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Slash)
        self.backspace_key = QtGui.QKeySequence(QtCore.Qt.Key_Backspace)

    def lines(self):
        """ Return the number of lines.
        """
        return self.blockCount()

    def set_line_column(self, line, column):
        """ Move the cursor to a particular line/column number.

        These line and column numbers are 1-indexed.
        """
        # Allow the caller to ignore either line or column by passing None.
        line0, col0 = self.get_line_column()
        if line is None:
            line = line0
        if column is None:
            column = col0
        line -= 1
        #column -= 1
        block = self.document().findBlockByLineNumber(line)
        line_start = block.position()
        position = line_start + column
        cursor = self.textCursor()
        cursor.setPosition(position)
        self.setTextCursor(cursor)

    def get_line_column(self):
        """ Get the current line and column numbers.

        These line and column numbers are 1-indexed.
        """
        cursor = self.textCursor()
        pos = cursor.position()
        line = cursor.blockNumber() + 1
        line_start = cursor.block().position()
        column = pos - line_start + 1
        return line, column

    def get_selected_text(self):
        """ Return the currently selected text.
        """
        return unicode(self.textCursor().selectedText())

    def set_font(self, font):
        """ Set the new QFont.
        """
        self.document().setDefaultFont(font)
        self.line_number_widget.set_font(font)
        self.update_line_number_width()

    def update_line_number_width(self, nblocks=0):
        """ Update the width of the line number widget.
        """
        left = 0
        if not self.breakpoints_widget.isHidden():
            left += self.breakpoints_widget.gutter_width()
        if not self.line_number_widget.isHidden():
            left += self.line_number_widget.gutter_width()
        self.setViewportMargins(left, 0, 0, 0)

    def update_line_numbers(self, rect, dy):
        """ Update the line numbers.
        """
        if dy:
            self.line_number_widget.scroll(0, dy)
            self.breakpoints_widget.scroll(0, dy)
        self.line_number_widget.update(
            0, rect.y(), self.line_number_widget.width(), rect.height())
        self.breakpoints_widget.update(
            0, rect.y(), self.breakpoints_widget.width(), rect.height())
        if rect.contains(self.viewport().rect()):
            self.update_line_number_width()

    def set_info_lines(self, info_lines):
        self.status_widget.info_lines = info_lines
        self.status_widget.update()

    def set_warn_lines(self, warn_lines):
        self.status_widget.warn_lines = warn_lines
        self.status_widget.update()

    def set_error_lines(self, error_lines):
        self.status_widget.error_lines = error_lines
        self.status_widget.update()

    def autoindent_newline(self):
        tab = '\t'
        if self.tabs_as_spaces:
            tab = ' '*self.tab_width

        cursor = self.textCursor()
        text = cursor.block().text()
        trimmed = text.rstrip()
        current_indent_pos = self._get_indent_position(text)

        cursor.beginEditBlock()

        # Create the new line. There is no need to move to the new block, as
        # the insertBlock will do that automatically
        cursor.insertBlock()

        # Remove any leading whitespace from the current line
        after = cursor.block().text()
        trimmed_after = after.rstrip()
        pos = after.index(trimmed_after)
        for i in range(pos):
            cursor.deleteChar()

        if self.indent_character and trimmed.endswith(self.indent_character):
            # indent one level
            indent = text[:current_indent_pos] + tab
        else:
            # indent to the same level
            indent = text[:current_indent_pos]
        cursor.insertText(indent)

        cursor.endEditBlock()
        self.ensureCursorVisible()

    def block_indent(self):
        cursor = self.textCursor()

        if not cursor.hasSelection():
            # Insert a tabulator
            self.line_indent(cursor)

        else:
            # Indent every selected line
            sel_blocks = self._get_selected_blocks()

            cursor.clearSelection()
            cursor.beginEditBlock()

            for block in sel_blocks:
                cursor.setPosition(block.position())
                self.line_indent(cursor)

            cursor.endEditBlock()
            self._show_selected_blocks(sel_blocks)

    def block_unindent(self):
        cursor = self.textCursor()

        if not cursor.hasSelection():
            # Unindent current line
            position = cursor.position()
            cursor.beginEditBlock()

            removed = self.line_unindent(cursor)
            position = max(position-removed, 0)

            cursor.endEditBlock()
            cursor.setPosition(position)
            self.setTextCursor(cursor)

        else:
            # Unindent every selected line
            sel_blocks = self._get_selected_blocks()

            cursor.clearSelection()
            cursor.beginEditBlock()

            for block in sel_blocks:
                cursor.setPosition(block.position())
                self.line_unindent(cursor)

            cursor.endEditBlock()
            self._show_selected_blocks(sel_blocks)

    def block_comment(self):
        """the comment char will be placed at the first non-whitespace
            char of the first line. For example:
                if foo:
                    bar
            will be commented as:
                #if foo:
                #    bar
        """
        cursor = self.textCursor()

        if not cursor.hasSelection():
            text = cursor.block().text()
            current_indent_pos = self._get_indent_position(text)

            if text[current_indent_pos] == self.comment_character:
                self.line_uncomment(cursor, current_indent_pos)
            else:
                self.line_comment(cursor, current_indent_pos)

        else:
            sel_blocks = self._get_selected_blocks()
            text = sel_blocks[0].text()
            indent_pos = self._get_indent_position(text)

            comment = True
            for block in sel_blocks:
                text = block.text()
                if len(text) > indent_pos and \
                        text[indent_pos] == self.comment_character:
                    # Already commented.
                    comment = False
                    break

            cursor.clearSelection()
            cursor.beginEditBlock()

            for block in sel_blocks:
                cursor.setPosition(block.position())
                if comment:
                    if block.length() < indent_pos:
                        cursor.insertText(' ' * indent_pos)
                    self.line_comment(cursor, indent_pos)
                else:
                    self.line_uncomment(cursor, indent_pos)
            cursor.endEditBlock()
            self._show_selected_blocks(sel_blocks)

    def line_comment(self, cursor, position):
        cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
        cursor.movePosition(QtGui.QTextCursor.Right,
                            QtGui.QTextCursor.MoveAnchor, position)
        cursor.insertText(self.comment_character)

    def line_uncomment(self, cursor, position=0):
        cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
        text = cursor.block().text()
        new_text = text[:position] + text[position+1:]
        cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
                            QtGui.QTextCursor.KeepAnchor)
        cursor.removeSelectedText()
        cursor.insertText(new_text)

    def line_indent(self, cursor):
        tab = '\t'
        if self.tabs_as_spaces:
            tab = '    '

        cursor.insertText(tab)

    def line_unindent(self, cursor):
        """ Unindents the cursor's line. Returns the number of characters
            removed.
        """
        tab = '\t'
        if self.tabs_as_spaces:
            tab = '    '

        cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
        if cursor.block().text().startswith(tab):
            new_text = cursor.block().text()[len(tab):]
            cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
                                QtGui.QTextCursor.KeepAnchor)
            cursor.removeSelectedText()
            cursor.insertText(new_text)
            return len(tab)
        else:
            return 0

    def word_under_cursor(self):
        """ Return the word under the cursor.
        """
        cursor = self.textCursor()
        cursor.select(QtGui.QTextCursor.WordUnderCursor)
        return unicode(cursor.selectedText())

    ###########################################################################
    # QWidget interface
    ###########################################################################

    # FIXME: This is a quick hack to be able to access the keyPressEvent
    # from the rest editor. This should be changed to work within the traits
    # framework.
    def keyPressEvent_action(self, event):
        pass

    def keyPressEvent(self, event):
        key_sequence = QtGui.QKeySequence(event.key() + int(event.modifiers()))

        self.keyPressEvent_action(event) # FIXME: see above

        # If the cursor is in the middle of the first line, pressing the "up"
        # key causes the cursor to go to the start of the first line, i.e. the
        # beginning of the document. Likewise, if the cursor is somewhere in the
        # last line, the "down" key causes it to go to the end.
        cursor = self.textCursor()
        if key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.Key_Up)):
            cursor.movePosition(QtGui.QTextCursor.StartOfLine)
            if cursor.atStart():
                self.setTextCursor(cursor)
                event.accept()
        elif key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.Key_Down)):
            cursor.movePosition(QtGui.QTextCursor.EndOfLine)
            if cursor.atEnd():
                self.setTextCursor(cursor)
                event.accept()

        elif self.auto_indent and \
                key_sequence.matches(QtGui.QKeySequence(QtCore.Qt.Key_Return)):
            event.accept()
            return self.autoindent_newline()
        elif key_sequence.matches(self.indent_key):
            event.accept()
            return self.block_indent()
        elif key_sequence.matches(self.unindent_key):
            event.accept()
            return self.block_unindent()
        elif key_sequence.matches(self.comment_key):
            event.accept()
            return self.block_comment()
        elif self.auto_indent and self.smart_backspace and \
                key_sequence.matches(self.backspace_key) and \
                self._backspace_should_unindent():
            event.accept()
            return self.block_unindent()

        return super(CodeWidget, self).keyPressEvent(event)

    def resizeEvent(self, event):
        QtGui.QPlainTextEdit.resizeEvent(self, event)
        contents = self.contentsRect()
        left = contents.left()
        self.breakpoints_widget.setGeometry(QtCore.QRect(left,
            contents.top(), self.breakpoints_widget.gutter_width(),
            contents.height()))
        left += self.breakpoints_widget.gutter_width()
        self.line_number_widget.setGeometry(QtCore.QRect(left,
            contents.top(), self.line_number_widget.gutter_width(),
            contents.height()))

        # use the viewport width to determine the right edge. This allows for
        # the propper placement w/ and w/o the scrollbar
        right_pos = self.viewport().width() + self.line_number_widget.width() + \
                    self.breakpoints_widget.width() + 1\
                    - self.status_widget.sizeHint().width()
        self.status_widget.setGeometry(QtCore.QRect(right_pos,
            contents.top(), self.status_widget.sizeHint().width(),
            contents.height()))

    def sizeHint(self):
        # Suggest a size that is 80 characters wide and 40 lines tall.
        style = self.style()
        opt = QtGui.QStyleOptionHeader()
        font_metrics = QtGui.QFontMetrics(self.document().defaultFont())
        width = font_metrics.width(' ') * 80
        width += self.breakpoints_widget.sizeHint().width()
        width += self.line_number_widget.sizeHint().width()
        width += self.status_widget.sizeHint().width()
        width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
        height = font_metrics.height() * 40
        return QtCore.QSize(width, height)

    def setTextCursor(self, cursor):
        cursor.setVisualNavigation(True)
        super(CodeWidget, self).setTextCursor(cursor)

    ###########################################################################
    # Private methods
    ###########################################################################

    def _get_indent_position(self, line):
        trimmed = line.rstrip()
        if len(trimmed) != 0:
            return line.index(trimmed)
        else:
            # if line is all spaces, treat it as the indent position
            return len(line)

    def _show_selected_blocks(self, selected_blocks):
        """ Assumes contiguous blocks
        """
        cursor = self.textCursor()
        cursor.clearSelection()
        cursor.setPosition(selected_blocks[0].position())
        cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
        cursor.movePosition(QtGui.QTextCursor.NextBlock,
                            QtGui.QTextCursor.KeepAnchor, len(selected_blocks))
        cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
                            QtGui.QTextCursor.KeepAnchor)

        self.setTextCursor(cursor)

    def _get_selected_blocks(self):
        cursor = self.textCursor()
        if cursor.position() > cursor.anchor():
            move_op = QtGui.QTextCursor.PreviousBlock
            start_pos = cursor.anchor()
            end_pos = cursor.position()
        else:
            move_op = QtGui.QTextCursor.NextBlock
            start_pos = cursor.position()
            end_pos = cursor.anchor()

        cursor.setPosition(start_pos)
        cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
        blocks = [cursor.block()]

        while cursor.movePosition(QtGui.QTextCursor.NextBlock):
            block = cursor.block()
            if block.position() < end_pos:
                blocks.append(block)

        return blocks

    def _backspace_should_unindent(self):
        cursor = self.textCursor()
        # Don't unindent if we have a selection.
        if cursor.hasSelection():
            return False
        column = cursor.columnNumber()
        # Don't unindent if we are at the beggining of the line
        if column < self.tab_width:
            return False
        else:
            # Unindent if we are at the indent position
            return column == self._get_indent_position(cursor.block().text())