From 91c33ec6d8fc8973a2c5ee4b4d8ee6a9c81ac16a Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 14 Oct 2024 15:47:16 -0400 Subject: [PATCH] PythonEditor : Add line numbers --- Changes.md | 9 +++ python/GafferUI/CodeWidget.py | 4 +- python/GafferUI/MultiLineTextWidget.py | 107 ++++++++++++++++++++++++- python/GafferUI/PythonEditor.py | 2 +- 4 files changed, 116 insertions(+), 6 deletions(-) diff --git a/Changes.md b/Changes.md index 68eb292032..bfcb6b9ff6 100644 --- a/Changes.md +++ b/Changes.md @@ -6,6 +6,15 @@ Improvements - Light Editor : Added `is_sphere` column for Cycles lights. - Windows : Gaffer now uses the TBB memory allocator for significantly better performance. +- Python Editor : Added line numbers + +API +--- + +- MultiLineTextWidget : + - Added the ability to show line numbers by passing `lineNumbers = True` to the constructor. + - Added `setLineNumbersVisible()` and `getLineNumbersVisible()` +- CodeWidget : Added the ability to show line numbers by passing `lineNumbers = True` to the constructor. 1.5.0.0a3 (relative to 1.5.0.0a2) diff --git a/python/GafferUI/CodeWidget.py b/python/GafferUI/CodeWidget.py index 7bb2eb7a98..74f8811382 100644 --- a/python/GafferUI/CodeWidget.py +++ b/python/GafferUI/CodeWidget.py @@ -55,9 +55,9 @@ class CodeWidget( GafferUI.MultiLineTextWidget ) : - def __init__( self, text="", editable=True, fixedLineHeight=None, **kw ) : + def __init__( self, text="", editable=True, fixedLineHeight=None, lineNumbers = False, **kw ) : - GafferUI.MultiLineTextWidget.__init__( self, text, editable, fixedLineHeight = fixedLineHeight, wrapMode = self.WrapMode.None_, role = self.Role.Code, **kw ) + GafferUI.MultiLineTextWidget.__init__( self, text, editable, fixedLineHeight = fixedLineHeight, wrapMode = self.WrapMode.None_, role = self.Role.Code, lineNumbers = lineNumbers, **kw ) self.__completer = None self.__completionMenu = None diff --git a/python/GafferUI/MultiLineTextWidget.py b/python/GafferUI/MultiLineTextWidget.py index 81ed08ee94..512938132b 100644 --- a/python/GafferUI/MultiLineTextWidget.py +++ b/python/GafferUI/MultiLineTextWidget.py @@ -43,6 +43,7 @@ import Gaffer import GafferUI +from ._StyleSheet import _styleColors from Qt import QtGui from Qt import QtWidgets @@ -53,9 +54,9 @@ class MultiLineTextWidget( GafferUI.Widget ) : WrapMode = enum.Enum( "WrapNode", [ "None_", "Word", "Character", "WordOrCharacter" ] ) Role = enum.Enum( "Role", [ "Text", "Code" ] ) - def __init__( self, text="", editable=True, wrapMode=WrapMode.WordOrCharacter, fixedLineHeight=None, role=Role.Text, placeholderText = "", **kw ) : + def __init__( self, text="", editable=True, wrapMode=WrapMode.WordOrCharacter, fixedLineHeight=None, role=Role.Text, placeholderText = "", lineNumbers = False, **kw ) : - GafferUI.Widget.__init__( self, _PlainTextEdit(), **kw ) + GafferUI.Widget.__init__( self, _PlainTextEdit( lineNumbers ), **kw ) ## \todo This should come from the Style when we get Styles applied to Widgets # (and not just Gadgets as we have currently). @@ -416,13 +417,34 @@ def __drop( self, widget, event ) : self.insertText( self.__dropText( event.data ) ) return True +class _QLineNumberArea( QtWidgets.QWidget ) : + + def __init__( self, codeEditor ) : + + assert( isinstance( codeEditor, _PlainTextEdit ) ) + QtWidgets.QWidget.__init__( self, codeEditor ) + + def sizeHint( self ) : + + return QtCore.QSize( self.parentWidget().lineNumberAreaWidth(), 0 ) + + def paintEvent( self, event ) : + + self.parentWidget().lineNumberAreaPaintEvent( event ) + class _PlainTextEdit( QtWidgets.QPlainTextEdit ) : - def __init__( self, parent = None ) : + def __init__( self, lineNumbers, parent = None ) : QtWidgets.QPlainTextEdit.__init__( self, parent ) + + self.__lineNumberAreaWidget = _QLineNumberArea( self ) + self.setLineNumbersVisible( lineNumbers ) + self.__fixedLineHeight = None + self.blockCountChanged.connect( self.__updateLineNumberAreaWidth ) + self.updateRequest.connect( self.__updateLineNumberArea ) self.document().modificationChanged.connect( self.update ) def setFixedLineHeight( self, fixedLineHeight ) : @@ -440,6 +462,56 @@ def getFixedLineHeight( self ) : return self.__fixedLineHeight + def setLineNumbersVisible( self, visible ) : + + self.__lineNumbersVisible = visible + self.__lineNumberAreaWidget.setVisible( visible ) + self.__updateLineNumberAreaWidth( 0 ) + + def getLineNumbersVisible( self ) : + + return self.__lineNumbersVisible + + def lineNumberAreaWidth( self ) : + + if not self.getLineNumbersVisible() : + return 0 + + digits = 1 + value = max( 1, self.blockCount() ) + while value >= 10 : + value *= 0.1 + digits += 1 + + return 3 + self.fontMetrics().horizontalAdvance( '9' ) * digits + + def lineNumberAreaPaintEvent( self, event ) : + + block = self.firstVisibleBlock() + + top = round( self.blockBoundingGeometry( block ).translated( self.contentOffset() ).top() ) + bottom = top + round( self.blockBoundingRect( block ).height() ) + + width = self.lineNumberAreaWidth() + + painter = QtGui.QPainter( self.__lineNumberAreaWidget ) + painter.setPen( QtGui.QColor( *( _styleColors["backgroundLight"] ) ) ) + painter.setFont( self.property( "font" ) ) + + while block.isValid() and top <= event.rect().bottom() : + if block.isVisible() : + painter.drawText( + 0, + top, + width, + self.fontMetrics().height(), + QtCore.Qt.AlignRight, + str( block.blockNumber() + 1 ) + ) + block = block.next() + top = bottom + bottom = top + round( self.blockBoundingRect( block ).height() ) + def __computeHeight( self, size ) : fixedLineHeight = self.getFixedLineHeight() @@ -483,6 +555,20 @@ def event( self, event ) : return QtWidgets.QPlainTextEdit.event( self, event ) + def resizeEvent ( self, event ) : + + QtWidgets.QPlainTextEdit.resizeEvent( self, event ) + + contentsRect = self.contentsRect() + self.__lineNumberAreaWidget.setGeometry( + QtCore.QRect( + contentsRect.left(), + contentsRect.top(), + self.lineNumberAreaWidth(), + contentsRect.height() + ) + ) + def focusOutEvent( self, event ) : widget = GafferUI.Widget._owner( self ) @@ -523,3 +609,18 @@ def __paintActivationHint( self, painter ) : pixmap = GafferUI.Image._qtPixmapFromFile( "ctrlEnter.png" ) painter.setOpacity( 0.75 ) painter.drawPixmap( viewport.width() - ( pixmap.width() + 4 ), viewport.height() - ( pixmap.height() + 4 ), pixmap ) + + def __updateLineNumberAreaWidth( self, newBlockCount ) : + + lineNumberAreaWidth = self.lineNumberAreaWidth() + self.setViewportMargins( lineNumberAreaWidth + ( 8 if lineNumberAreaWidth > 0 else 0 ), 0, 0, 0 ) + + def __updateLineNumberArea( self, rect, scrollY ) : + + if scrollY != 0 : + self.__lineNumberAreaWidget.scroll( 0, scrollY ) + else : + self.__lineNumberAreaWidget.update( 0, rect.y(), self.__lineNumberAreaWidget.width(), rect.height() ) + + if rect.contains( self.viewport().rect() ) : + self.__updateLineNumberAreaWidth( 0 ) \ No newline at end of file diff --git a/python/GafferUI/PythonEditor.py b/python/GafferUI/PythonEditor.py index 86078531ad..3374e66a8d 100644 --- a/python/GafferUI/PythonEditor.py +++ b/python/GafferUI/PythonEditor.py @@ -71,7 +71,7 @@ def __init__( self, scriptNode, **kw ) : Gaffer.WeakMethod( self.__contextMenu ) ) - self.__inputWidget = GafferUI.CodeWidget() + self.__inputWidget = GafferUI.CodeWidget( lineNumbers = True ) self.__splittable.append( self.__outputWidget ) self.__splittable.append( self.__inputWidget )