PCEF 0.1.1 documentation

pcef.code_edit

Contents

Source code for pcef.code_edit

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# PCEF - PySide Code Editing framework
# Copyright 2013, Colin Duquesnoy <colin.duquesnoy@gmail.com>
#
# This software is released under the LGPLv3 license.
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Contains the CodeEdit widget (an extension of the CodeEdit used as a promoted widget by the
:class:`pcef.core.CodeEditorWidget` ui)
"""
from PyQt4.QtGui import QKeyEvent
from PySide.QtCore import Qt
from PySide.QtCore import Signal
from PySide.QtCore import QRect
from PySide.QtGui import QTextEdit, QFocusEvent
from PySide.QtGui import QTextOption
from PySide.QtGui import QFont
from PySide.QtGui import QKeyEvent
from PySide.QtGui import QTextCursor
from PySide.QtGui import QMenu
from PySide.QtGui import QPaintEvent
from PySide.QtGui import QMouseEvent
from PySide.QtGui import QPlainTextEdit
from PySide.QtGui import QWheelEvent
from pygments.token import Token
from pcef.style import StyledObject


class VisibleBlock(object):
    """
    This class contains the geometry of currently visible blocks
    """

    def __init__(self, row, block, rect):
        """
        :param row: line number
        :param block: QTextBlock
        :param rect: QRect
        """
        self.row = row
        self.block = block
        (self.left, self.top, self.width, self.height) = rect
        self.rect = QRect(*rect)


[docs]class CodeEdit(QPlainTextEdit, StyledObject): """ The code editor text edit. This is a specialized QPlainTextEdit made to expose additional signals, styling and methods. It also provides a custom context menu and methods to add actions, separators and sub-menus. Most of the code editor functionnalities are provided by installing modes on the PCEF instance. Additional signals: - dirtyChanged(bool) - focusedIn(QFocusEvent) - keyPressed(QKeyEvent) - keyReleased(QKeyEvent) - mousePressed(QMouseEvent) - mouseReleased(QMouseEvent) - newTextSet() - prePainting(QPaintEvent) - postPainting(QPaintEvent) - visibleBlocksChanged() """ QSS = """QPlainTextEdit { background-color: %(b)s; color: %(t)s; selection-background-color: %(bs)s; selection-color: %(ts)s; } """ #--------------------------------------------------------------------------- # Signals #--------------------------------------------------------------------------- #: Signal emitted when the dirty state of the document changed dirtyChanged = Signal(bool) #: Signal emitted when a key is pressed keyPressed = Signal(QKeyEvent) #: Signal emitted when a key is released keyReleased = Signal(QKeyEvent) #: Signal emitted when a mouse button is pressed mousePressed = Signal(QMouseEvent) #: Signal emitted when a mouse button is released mouseReleased = Signal(QMouseEvent) #: Signal emitted on a wheel event mouseWheelActivated = Signal(QWheelEvent) #: Signal emitted before painting the core widget prePainting = Signal(QPaintEvent) #: Signal emitted after painting the core widget postPainting = Signal(QPaintEvent) #: Signal emitted at the end of the keyPressed event postKeyPressed = Signal(QKeyEvent) #: Signal emitted when the list of visible blocks changed visibleBlocksChanged = Signal() #: Signal emitted when setPlainText is invoked newTextSet = Signal() #: Signal emitted when focusInEvent is is called focusedIn = Signal(QFocusEvent) #--------------------------------------------------------------------------- # Properties #--------------------------------------------------------------------------- def __get_dirty(self): return self.__dirty def __set_dirty(self, dirty): if dirty != self.__dirty: self.__dirty = dirty self.dirtyChanged.emit(dirty) #: Tells whether the editor is dirty(changes have been made to the document) dirty = property(__get_dirty, __set_dirty) #--------------------------------------------------------------------------- # Methods #--------------------------------------------------------------------------- def __init__(self, parent=None): """ Creates the widget. :param parent: Optional parent widget """ QPlainTextEdit.__init__(self, parent) StyledObject.__init__(self) #: Tag member used to remeber the filename of the edited text if any self.tagFilename = None #: Tag member used to remeber the filename of the edited text if any self.tagEncoding = 'utf8' #: Weakref to the editor self.editor = None #: dirty flag self.__dirty = False #: our custom context menu self.__context_menu = QMenu() #: The list of active extra-selections (TextDecoration) self.__selections = [] self.__numBlocks = -1 #: Shortcut to the fontMetrics self.fm = self.fontMetrics() self.textChanged.connect(self.__onTextChanged) self.blockCountChanged.connect(self.__onBlocksChanged) self.verticalScrollBar().valueChanged.connect(self.__onBlocksChanged) self.newTextSet.connect(self.__onBlocksChanged) self.cursorPositionChanged.connect(self.__onBlocksChanged) self.setLineWrapMode(QPlainTextEdit.NoWrap) self._onStyleChanged()
[docs] def addAction(self, action): """ Adds an action to the text edit context menu :param action: QAction """ QTextEdit.addAction(self, action) self.__context_menu.addAction(action)
[docs] def addSeparator(self): """ Adds a separator to the context menu """ self.__context_menu.addSeparator()
[docs] def addDecoration(self, decoration): """ Add a text decoration :param decoration: Text decoration :type decoration: pcef.core.TextDecoration """ self.__selections.append(decoration) self.setExtraSelections(self.__selections)
[docs] def removeDecoration(self, decoration): """ Remove text decoration. :param decoration: The decoration to remove :type decoration: pcef.core.TextDecoration """ try: self.__selections.remove(decoration) self.setExtraSelections(self.__selections) except ValueError: pass
[docs] def setShowWhitespaces(self, show): """ Shows/Hides whitespaces. :param show: True to show whitespaces, False to hide them :type show: bool """ doc = self.document() options = doc.defaultTextOption() if show: options.setFlags(options.flags() | QTextOption.ShowTabsAndSpaces) else: options.setFlags(options.flags() & ~QTextOption.ShowTabsAndSpaces) doc.setDefaultTextOption(options)
[docs] def indent(self, size): """ Indent current line or selection :param size: indent size in spaces :type size: int """ cursor = self.textCursor() cursor.beginEditBlock() sel_start = cursor.selectionStart() sel_end = cursor.selectionEnd() has_selection = True if not cursor.hasSelection(): cursor.select(QTextCursor.LineUnderCursor) has_selection = False nb_lines = len(cursor.selection().toPlainText().splitlines()) cursor.setPosition(cursor.selectionStart()) for i in range(nb_lines): cursor.movePosition(QTextCursor.StartOfLine) cursor.insertText(" " * size) cursor.movePosition(QTextCursor.EndOfLine) cursor.setPosition(cursor.position() + 1) cursor.setPosition(sel_start + size) if has_selection: cursor.setPosition(sel_end + (nb_lines * size), QTextCursor.KeepAnchor) cursor.endEditBlock() self.setTextCursor(cursor)
[docs] def unIndent(self, size): """ Un-indent current line or selection by tab_size """ cursor = self.textCursor() assert isinstance(cursor, QTextCursor) cursor.beginEditBlock() pos = cursor.position() sel_start = cursor.selectionStart() sel_end = cursor.selectionEnd() has_selection = True if not cursor.hasSelection(): cursor.select(QTextCursor.LineUnderCursor) has_selection = False nb_lines = len(cursor.selection().toPlainText().splitlines()) cursor.setPosition(cursor.selectionStart()) cpt = 0 for i in range(nb_lines): cursor.select(QTextCursor.LineUnderCursor) if cursor.selectedText().startswith(" " * size): cursor.movePosition(QTextCursor.StartOfLine) [cursor.deleteChar() for _ in range(size)] pos = pos - size cpt += 1 else: cursor.clearSelection() # next line cursor.movePosition(QTextCursor.EndOfLine) cursor.setPosition(cursor.position() + 1) if cpt: cursor.setPosition(sel_start - size) else: cursor.setPosition(sel_start) if has_selection: cursor.setPosition(sel_end - (cpt * size), QTextCursor.KeepAnchor) cursor.endEditBlock() self.setTextCursor(cursor)
def _onStyleChanged(self): """ Updates widget style when style changed. """ self.setFont(QFont(self.currentStyle.fontName, self.currentStyle.fontSize)) self.fm = self.fontMetrics() qss = self.QSS % { 'b': self.currentStyle.backgroundColor, 't': self.currentStyle.tokenColor(Token), "bs": self.currentStyle.selectionBackgroundColor, "ts": self.currentStyle.selectionTextColor} self.setShowWhitespaces(self.currentStyle.showWhitespaces) self.setStyleSheet(qss)
[docs] def paintEvent(self, event): """ Emits prePainting and postPainting signals :param event: QPaintEvent """ self.prePainting.emit(event) QPlainTextEdit.paintEvent(self, event) self.postPainting.emit(event)
[docs] def keyPressEvent(self, event): """ Performs indentation if tab key presed, else emits the keyPressed signal :param event: QKeyEvent """ assert isinstance(event, QKeyEvent) event.setAccepted(False) # replace tabs by space if event.key() == Qt.Key_Tab: cursor = self.textCursor() assert isinstance(cursor, QTextCursor) if not cursor.hasSelection(): # insert tab at cursor pos cursor.insertText(" " * self.editor().TAB_SIZE) else: # indent whole selection self.indent(self.editor().TAB_SIZE) event.setAccepted(True) self.keyPressed.emit(event) if not event.isAccepted(): event.setAccepted(True) QPlainTextEdit.keyPressEvent(self, event) self.postKeyPressed.emit(event)
[docs] def keyReleaseEvent(self, event): """ Performs indentation if tab key pressed, else emits the keyPressed signal :param event: QKeyEvent """ assert isinstance(event, QKeyEvent) event.setAccepted(False) self.keyReleased.emit(event) if not event.isAccepted(): event.setAccepted(True) QPlainTextEdit.keyReleaseEvent(self, event)
[docs] def focusInEvent(self, event): """ Emits the focusedIn signal :param event: :return: """ self.focusedIn.emit(event) QPlainTextEdit.focusInEvent(self, event)
[docs] def mousePressEvent(self, event): """ Emits mousePressed signal :param event: QMouseEvent """ event.setAccepted(False) self.mousePressed.emit(event) if not event.isAccepted(): event.setAccepted(True) QPlainTextEdit.mousePressEvent(self, event)
[docs] def mouseReleaseEvent(self, event): """ Emits mouseReleased signal. :param event: QMouseEvent """ event.setAccepted(False) self.mouseReleased.emit(event) if not event.isAccepted(): event.setAccepted(True) QPlainTextEdit.mouseReleaseEvent(self, event)
[docs] def wheelEvent(self, event): """ Emits the mouseWheelActivated signal. :param event: QMouseEvent """ event.setAccepted(False) self.mouseWheelActivated.emit(event) if not event.isAccepted(): event.setAccepted(True) QPlainTextEdit.wheelEvent(self, event)
[docs] def contextMenuEvent(self, event): """ Shows our own context menu """ self.__context_menu.exec_(event.globalPos())
[docs] def resizeEvent(self, event): """ Updates visible blocks on resize """ self.__onBlocksChanged() QPlainTextEdit.resizeEvent(self, event)
def __onTextChanged(self): """ Sets dirty to true """ self.dirty = True
[docs] def setPlainText(self, txt): """ Sets the text edit content and emits newTextSet signal. :param txt: New text to display """ QPlainTextEdit.setPlainText(self, txt) self.newTextSet.emit()
def __onBlocksChanged(self): """ Updates the list of visible blocks and emits visibleBlocksChanged signal. """ visible_blocks = [] block = self.firstVisibleBlock() row = block.blockNumber() + 1 width = self.width() w = width - 2 h = self.fm.height() bbox = self.blockBoundingGeometry(block) top = bbox.translated(self.contentOffset()).top() bottom = top + bbox.height() zoneTop = 0 # event.rect().top() zoneBottom = self.height() # event.rect().bottom() visible_blocks_append = visible_blocks.append while block.isValid() and top <= zoneBottom: if block.isVisible() and bottom >= zoneTop: visible_blocks_append( VisibleBlock(row, block, (0, top, w, h)) ) block = block.next() row += 1 top = bottom bottom = top + self.blockBoundingRect(block).height() self.visible_blocks = visible_blocks self.visibleBlocksChanged.emit()
[docs] def fold(self, start, end, fold=True): """ Fold/Unfold a block of text delimitted by start/end line numbers :param start: Start folding line (this line is not fold, only the next ones) :param end: End folding line. :param fold: True to fold, False to unfold """ for i in range(start + 1, end): self.document().findBlockByNumber(i).setVisible(not fold) self.update() self.__onBlocksChanged()

Contents