File: rectangularselection.py

package info (click to toggle)
orange3 3.40.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 15,908 kB
  • sloc: python: 162,745; ansic: 622; makefile: 322; sh: 93; cpp: 77
file content (263 lines) | stat: -rw-r--r-- 11,098 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
"""
Adapted from a code editor component created
for Enki editor as replacement for QScintilla.
Copyright (C) 2020  Andrei Kopats

Originally licensed under the terms of GNU Lesser General Public License
as published by the Free Software Foundation, version 2.1 of the license.
This is compatible with Orange3's GPL-3.0 license.
"""
from AnyQt.QtCore import Qt, QMimeData
from AnyQt.QtWidgets import QApplication, QTextEdit
from AnyQt.QtGui import QKeyEvent, QKeySequence, QPalette, QTextCursor


class RectangularSelection:
    """This class does not replresent any object, but is part of Qutepart
    It just groups together Qutepart rectangular selection methods and fields
    """

    MIME_TYPE = 'text/rectangular-selection'

    # any of this modifiers with mouse select text
    MOUSE_MODIFIERS = (Qt.AltModifier | Qt.ControlModifier,
                       Qt.AltModifier | Qt.ShiftModifier,
                       Qt.AltModifier)

    _MAX_SIZE = 256

    def __init__(self, qpart):
        self._qpart = qpart
        self._start = None

        qpart.cursorPositionChanged.connect(self._reset)  # disconnected during Alt+Shift+...
        qpart.textChanged.connect(self._reset)
        qpart.selectionChanged.connect(self._reset)  # disconnected during Alt+Shift+...

    def _reset(self):
        """Cursor moved while Alt is not pressed, or text modified.
        Reset rectangular selection"""
        if self._start is not None:
            self._start = None
            self._qpart._updateExtraSelections()  # pylint: disable=protected-access

    def isDeleteKeyEvent(self, keyEvent):
        """Check if key event should be handled as Delete command"""
        return self._start is not None and \
               (keyEvent.matches(QKeySequence.Delete) or \
                (keyEvent.key() == Qt.Key_Backspace and keyEvent.modifiers() == Qt.NoModifier))

    def delete(self):
        """Del or Backspace pressed. Delete selection"""
        with self._qpart:
            for cursor in self.cursors():
                if cursor.hasSelection():
                    cursor.deleteChar()

    @staticmethod
    def isExpandKeyEvent(keyEvent):
        """Check if key event should expand rectangular selection"""
        return keyEvent.modifiers() & Qt.ShiftModifier and \
               keyEvent.modifiers() & Qt.AltModifier and \
               keyEvent.key() in (Qt.Key_Left, Qt.Key_Right, Qt.Key_Down, Qt.Key_Up,
                                  Qt.Key_PageUp, Qt.Key_PageDown, Qt.Key_Home, Qt.Key_End)

    def onExpandKeyEvent(self, keyEvent):
        """One of expand selection key events"""
        if self._start is None:
            currentBlockText = self._qpart.textCursor().block().text()
            line = self._qpart.cursorPosition[0]
            visibleColumn = self._realToVisibleColumn(currentBlockText,
                                                      self._qpart.cursorPosition[1])
            self._start = (line, visibleColumn)
        modifiersWithoutAltShift = keyEvent.modifiers() & (~(Qt.AltModifier | Qt.ShiftModifier))
        newEvent = QKeyEvent(QKeyEvent.Type(keyEvent.type()),
                             keyEvent.key(),
                             modifiersWithoutAltShift,
                             keyEvent.text(),
                             keyEvent.isAutoRepeat(),
                             keyEvent.count())

        self._qpart.cursorPositionChanged.disconnect(self._reset)
        self._qpart.selectionChanged.disconnect(self._reset)
        super(self._qpart.__class__, self._qpart).keyPressEvent(newEvent)
        self._qpart.cursorPositionChanged.connect(self._reset)
        self._qpart.selectionChanged.connect(self._reset)
        # extra selections will be updated, because cursor has been moved

    def _visibleCharPositionGenerator(self, text):
        currentPos = 0
        yield currentPos

        for char in text:
            if char == '\t':
                currentPos += self._qpart.indentWidth
                # trim reminder. If width('\t') == 4,   width('abc\t') == 4
                currentPos = currentPos // self._qpart.indentWidth * self._qpart.indentWidth
            else:
                currentPos += 1
            yield currentPos

    def _realToVisibleColumn(self, text, realColumn):
        """If \t is used, real position of symbol in block and visible position differs
        This function converts real to visible
        """
        generator = self._visibleCharPositionGenerator(text)
        for _ in range(realColumn):
            val = next(generator)
        val = next(generator)
        return val

    def _visibleToRealColumn(self, text, visiblePos):
        """If \t is used, real position of symbol in block and visible position differs
        This function converts visible to real.
        Bigger value is returned, if visiblePos is in the middle of \t, None if text is too short
        """
        if visiblePos == 0:
            return 0
        elif not '\t' in text:
            return visiblePos
        else:
            currentIndex = 1
            for currentVisiblePos in self._visibleCharPositionGenerator(text):
                if currentVisiblePos >= visiblePos:
                    return currentIndex - 1
                currentIndex += 1

            return None

    def cursors(self):
        """Cursors for rectangular selection.
        1 cursor for every line
        """
        cursors = []
        if self._start is not None:
            startLine, startVisibleCol = self._start
            currentLine, currentCol = self._qpart.cursorPosition
            if abs(startLine - currentLine) > self._MAX_SIZE or \
               abs(startVisibleCol - currentCol) > self._MAX_SIZE:
                # Too big rectangular selection freezes the GUI
                self._qpart.userWarning.emit('Rectangular selection area is too big')
                self._start = None
                return []

            currentBlockText = self._qpart.textCursor().block().text()
            currentVisibleCol = self._realToVisibleColumn(currentBlockText, currentCol)

            for lineNumber in range(min(startLine, currentLine),
                                    max(startLine, currentLine) + 1):
                block = self._qpart.document().findBlockByNumber(lineNumber)
                cursor = QTextCursor(block)
                realStartCol = self._visibleToRealColumn(block.text(), startVisibleCol)
                realCurrentCol = self._visibleToRealColumn(block.text(), currentVisibleCol)
                if realStartCol is None:
                    realStartCol = block.length()  # out of range value
                if realCurrentCol is None:
                    realCurrentCol = block.length()  # out of range value

                cursor.setPosition(cursor.block().position() +
                                   min(realStartCol, block.length() - 1))
                cursor.setPosition(cursor.block().position() +
                                   min(realCurrentCol, block.length() - 1),
                                   QTextCursor.KeepAnchor)
                cursors.append(cursor)

        return cursors

    def selections(self):
        """Build list of extra selections for rectangular selection"""
        selections = []
        cursors = self.cursors()
        if cursors:
            background = self._qpart.palette().color(QPalette.Highlight)
            foreground = self._qpart.palette().color(QPalette.HighlightedText)
            for cursor in cursors:
                selection = QTextEdit.ExtraSelection()
                selection.format.setBackground(background)
                selection.format.setForeground(foreground)
                selection.cursor = cursor

                selections.append(selection)

        return selections

    def isActive(self):
        """Some rectangle is selected"""
        return self._start is not None

    def copy(self):
        """Copy to the clipboard"""
        data = QMimeData()
        text = '\n'.join([cursor.selectedText() \
                            for cursor in self.cursors()])
        data.setText(text)
        data.setData(self.MIME_TYPE, text.encode('utf8'))
        QApplication.clipboard().setMimeData(data)

    def cut(self):
        """Cut action. Copy and delete
        """
        cursorPos = self._qpart.cursorPosition
        topLeft = (min(self._start[0], cursorPos[0]),
                   min(self._start[1], cursorPos[1]))
        self.copy()
        self.delete()

        # Move cursor to top-left corner of the selection,
        # so that if text gets pasted again, original text will be restored
        self._qpart.cursorPosition = topLeft

    def _indentUpTo(self, text, width):
        """Add space to text, so text width will be at least width.
        Return text, which must be added
        """
        visibleTextWidth = self._realToVisibleColumn(text, len(text))
        diff = width - visibleTextWidth
        if diff <= 0:
            return ''
        elif self._qpart.indentUseTabs and \
                all(char == '\t' for char in text):  # if using tabs and only tabs in text
            return '\t' * (diff // self._qpart.indentWidth) + \
                   ' ' * (diff % self._qpart.indentWidth)
        else:
            return ' ' * int(diff)

    def paste(self, mimeData):
        """Paste recrangular selection.
        Add space at the beginning of line, if necessary
        """
        if self.isActive():
            self.delete()
        elif self._qpart.textCursor().hasSelection():
            self._qpart.textCursor().deleteChar()

        text = bytes(mimeData.data(self.MIME_TYPE)).decode('utf8')
        lines = text.splitlines()
        cursorLine, cursorCol = self._qpart.cursorPosition
        if cursorLine + len(lines) > len(self._qpart.lines):
            for _ in range(cursorLine + len(lines) - len(self._qpart.lines)):
                self._qpart.lines.append('')

        with self._qpart:
            for index, line in enumerate(lines):
                currentLine = self._qpart.lines[cursorLine + index]
                newLine = currentLine[:cursorCol] + \
                          self._indentUpTo(currentLine, cursorCol) + \
                          line + \
                          currentLine[cursorCol:]
                self._qpart.lines[cursorLine + index] = newLine
        self._qpart.cursorPosition = cursorLine, cursorCol

    def mousePressEvent(self, mouseEvent):
        cursor = self._qpart.cursorForPosition(mouseEvent.pos())
        self._start = cursor.block().blockNumber(), cursor.positionInBlock()

    def mouseMoveEvent(self, mouseEvent):
        cursor = self._qpart.cursorForPosition(mouseEvent.pos())

        self._qpart.cursorPositionChanged.disconnect(self._reset)
        self._qpart.selectionChanged.disconnect(self._reset)
        self._qpart.setTextCursor(cursor)
        self._qpart.cursorPositionChanged.connect(self._reset)
        self._qpart.selectionChanged.connect(self._reset)
        # extra selections will be updated, because cursor has been moved