Search code examples
qtcursorselectionpysideqtextedit

Extending selection in either direction in a QTextEdit


Currently, QTextEdit permits selecting text and then altering that selection with shift-click-drag only on the side of the selection opposite the anchor. The anchor is placed where the selection started. If the user tries to alter the selection near the start, the selection pivots around the anchor point instead of extending. I'd like to permit changing the selection from either side.

My first attempt is to simply set the anchor on the opposite side from where the cursor is located. Say, for example, the selection is from 10 to 20. If the cursor is shift-click-dragged at position 8, then the anchor would be set to 20. If the cursor is shift-click-dragged at position 22, then the anchor would be set to 10. Later, I'll try something more robust, perhaps based on the center point of the selection.

I thought this code would work, but it does not seem to affect the default behavior at all. What have I missed?

import sys
from PySide.QtCore import *
from PySide.QtGui import *

class TextEditor(QTextEdit):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setReadOnly(True)
        self.setMouseTracking(True)

    def mouseMoveEvent(self, event):
        point = QPoint()
        x = event.x()    #these are relative to the upper left corner of the text edit window
        y = event.y()
        point.setX(x)
        point.setY(y)
        self.mousepos = self.cursorForPosition(point).position()  # get character position of current mouse using local window coordinates

        if event.buttons()==Qt.LeftButton:
            modifiers = QApplication.keyboardModifiers()
            if modifiers == Qt.ShiftModifier:
                start = -1 #initialize to something impossible
                end = -1
                cursor = self.textCursor()
                select_point1 = cursor.selectionStart()
                select_point2 = cursor.selectionEnd()
                if select_point1 < select_point2: # determine order of selection points
                    start = select_point1
                    end = select_point2
                elif select_point2 < select_point1:
                    start = select_point2
                    end = select_point1
                if self.mousepos > end: # if past end when shift-click then trying to extend right
                    cursor.setPosition(start, mode=QTextCursor.MoveAnchor)
                elif self.mousepos < start: # if before start when shift-click then trying to extend left
                    cursor.setPosition(end, mode=QTextCursor.MoveAnchor)
                if start != -1 and end != -1: #if selection exists then this should trigger
                    self.setTextCursor(cursor)

        super().mouseMoveEvent(event)

Solution

  • Here's a first stab at implementing shift+click extension of the current selection. It seems to work okay, but I have not tested it to death, so there may be one or two glitches. The intended behaviour is that a shift+click above or below the selection should extend the whole selection in that direction; and a shift+click with drag should do the same thing, only continuously.

    Note that I have also set the text-interaction flags so that the caret is visible in read-only mode, and the selection can also be manipulated with the keyboard in various ways (e.g. ctrl+shift+right extends the selection to the next word).

    import sys
    from PySide.QtCore import *
    from PySide.QtGui import *
    
    class TextEditor(QTextEdit):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.setReadOnly(True)
            self.setTextInteractionFlags(
                Qt.TextSelectableByMouse |
                Qt.TextSelectableByKeyboard)
    
        def mouseMoveEvent(self, event):
            if not self.setShiftSelection(event, True):
                super().mouseMoveEvent(event)
    
        def mousePressEvent(self, event):
            if not self.setShiftSelection(event):
                super().mousePressEvent(event)
    
        def setShiftSelection(self, event, moving=False):
            if (event.buttons() == Qt.LeftButton and
                QApplication.keyboardModifiers() == Qt.ShiftModifier):
                cursor = self.textCursor()
                start = cursor.selectionStart()
                end = cursor.selectionEnd()
                if not moving or start != end:
                    anchor = cursor.anchor()
                    pos = self.cursorForPosition(event.pos()).position()
                    if pos <= start:
                        start = pos
                    elif pos >= end:
                        end = pos
                    elif anchor == start:
                        end = pos
                    else:
                        start = pos
                    if pos <= anchor:
                        start, end = end, start
                    cursor.setPosition(start, QTextCursor.MoveAnchor)
                    cursor.setPosition(end, QTextCursor.KeepAnchor)
                    self.setTextCursor(cursor)
                    return True
            return False
    
    if __name__ == '__main__':
    
        app = QApplication(sys.argv)
        window = TextEditor()
        window.setText(open(__file__).read())
        window.setGeometry(600, 50, 800, 800)
        window.show()
        sys.exit(app.exec_())