Passed
Pull Request — master (#291)
by Marek
02:40
created

pygameui.Text.Selection.last()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
#
2
#  Copyright 2001 - 2016 Ludek Smid [http://www.ospace.net/]
3
#
4
#  This file is part of Pygame.UI.
5
#
6
#  Pygame.UI is free software; you can redistribute it and/or modify
7
#  it under the terms of the Lesser GNU General Public License as published by
8
#  the Free Software Foundation; either version 2.1 of the License, or
9
#  (at your option) any later version.
10
#
11
#  Pygame.UI is distributed in the hope that it will be useful,
12
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
13
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
#  Lesser GNU General Public License for more details.
15
#
16
#  You should have received a copy of the Lesser GNU General Public License
17
#  along with Pygame.UI; if not, write to the Free Software
18
#  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19
#
20
21
import Const
22
from WordUtils import *
23
from Widget import Widget, registerWidget
24
import pygame.key
25
26
# keys mapping
27
MAPPING = {
28
    pygame.K_KP0: '0', pygame.K_KP1: '1', pygame.K_KP2: '2', pygame.K_KP3: '3', pygame.K_KP4: '4',
29
    pygame.K_KP5: '5', pygame.K_KP6: '6', pygame.K_KP7: '7', pygame.K_KP8: '8', pygame.K_KP9: '9',
30
}
31
32
class Selection(object):
33
    """ Object to hold and pre-process information about selected area of the text.
34
    """
35
    def __init__(self):
36
        self._start = None
37
        self._end = None
38
39
    @property
40
    def first(self):
41
        """ Returns coordinates of the beginning of the selection """
42
        return min(self._start, self._end)
43
44
    @property
45
    def last(self):
46
        """ Returns coordinates of the ending of the selection """
47
        return max(self._start, self._end)
48
49
    def select(self, row, column):
50
        if self._start is None:
51
            self._start = (row, column)
52
        self._end = (row, column)
53
54
    def deselect(self):
55
        self._start = self._end = None
56
57
    def __nonzero__(self):
58
        return self._start is not None and self._end is not None
59
60
61
class Text(Widget):
62
    """Text edit widget."""
63
64
    def __init__(self, parent, **kwargs):
65
        Widget.__init__(self, parent)
66
        # data
67
        self.__dict__['text'] = [""]
68
        self.__dict__['offsetRow'] = 0
69
        self.__dict__['cursorRow'] = 0
70
        self.__dict__['cursorColumn'] = 0
71
        self.__dict__['action'] = None
72
        self.__dict__['editable'] = 1
73
        self.__dict__['vertScrollbar'] = None
74
        self.__dict__['selection'] = Selection()
75
        # flags
76
        self.processKWArguments(kwargs)
77
        parent.registerWidget(self)
78
79
    def draw(self, surface):
80
        self.theme.drawText(surface, self)
81
        return self.rect
82
83
    def attachVScrollbar(self, scrollbar):
84
        self.vertScrollbar = scrollbar
85
        scrollbar.subscribeAction("*", self)
86
        scrollbar.action = "onScroll"
87
        scrollbar.slider.min = 0
88
        scrollbar.slider.max = len(self.text) + 100
89
        scrollbar.slider.position = self.offsetRow
90
        scrollbar.slider.shown = 1
91
92
    def onScroll(self, widget, action, data):
93
        self.offsetRow = self.vertScrollbar.slider.position
94
95
    def onCursorChanged(self):
96
        # force redraw
97
        self.cursorColumn += 1
98
        self.cursorColumn -= 1
99
        # super
100
        Widget.onCursorChanged(self)
101
102
    def deleteSelection(self):
103
        # we have some selection
104
        if self.selection:
105
            # one-line selection
106
            if self.selection.first[0] == self.selection.last[0]:
107
                # text before selection
108
                textBefore = self.text[self.selection.last[0]][:self.selection.first[1]]
109
                # text after selection
110
                textAfter = self.text[self.selection.last[0]][self.selection.last[1]:]
111
                # store new text without selection
112
                self.text[self.selection.last[0]] = u'%s%s' % (textBefore, textAfter)
113
            else:
114
                # handle multi-line selection
115
                # delete end of selection
116
                self.text[self.selection.last[0]] = self.text[self.selection.last[0]][self.selection.last[1]:]
117
                # delete fully selected rows
118
                start = self.selection.first[0]+1
119
                for row in range(start,self.selection.last[0]):
120
                    self.text.pop(start)
121
                # delete selection on first row
122
                self.text[self.selection.first[0]] = self.text[self.selection.first[0]][:self.selection.first[1]]
123
                # join the rows that are spanned
124
                self.text[self.selection.first[0]] = u'%s%s' % (self.text[self.selection.first[0]], self.text[self.selection.first[0]+1])
125
                del self.text[self.selection.first[0]+1]
126
            # move cursor to selection begining
127
            self.cursorColumn = self.selection.first[1]
128
            self.cursorRow = self.selection.first[0]
129
            # clear selection
130
            self.selection.deselect()
131
132
    def _processBackspace(self, evt):
133
        if self.selection:
134
            self.deleteSelection()
135
        elif self.cursorColumn > 0:
136
            self.text[self.cursorRow] = u'%s%s' % (self.text[self.cursorRow][:self.cursorColumn - 1], self.text[self.cursorRow][self.cursorColumn:])
137
            self.cursorColumn -= 1
138
        else:
139
            if self.cursorRow > 0:
140
                self.cursorColumn = len(self.text[self.cursorRow - 1])
141
                self.text[self.cursorRow - 1] = u'%s%s' % (self.text[self.cursorRow - 1], self.text[self.cursorRow])
142
                del self.text[self.cursorRow]
143
                self.cursorRow -= 1
144
145
    def _processDelete(self, evt):
146
        if self.selection:
147
            self.deleteSelection()
148
        elif self.cursorColumn < len(self.text[self.cursorRow]):
149
            self.text[self.cursorRow] = u'%s%s' % (self.text[self.cursorRow][:self.cursorColumn], self.text[self.cursorRow][self.cursorColumn + 1:])
150
        elif self.cursorRow < len(self.text) - 1:
151
            self.text[self.cursorRow] = u'%s%s' % (self.text[self.cursorRow], self.text[self.cursorRow + 1])
152
            del self.text[self.cursorRow + 1]
153
154
    def _processLeft(self, evt):
155
        if evt.mod & pygame.KMOD_SHIFT:
156
            self.selection.select(self.cursorRow, self.cursorColumn)
157
        if evt.mod & pygame.KMOD_CTRL:
158
            # move one word left
159
            # take words on line
160
            words = splitter(self.text[self.cursorRow])
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable splitter does not seem to be defined.
Loading history...
161 View Code Duplication
            if len(words) == 0:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
162
                #move to previous line
163
                if self.cursorRow > 0:
164
                    self.cursorRow -= 1
165
                    words2 = splitter(self.text[self.cursorRow])
166
                    if len(words2) > 0:
167
                        #move cursor to begining of last word
168
                        self.cursorColumn = words2[-1][1]
169
                    else:
170
                        #no words on line, so move cursor to the end of line
171
                        self.cursorColumn = len(self.text[self.cursorRow])
172
                else:
173
                    #we are on first line, so move cursor to begining of line
174
                    self.cursorColumn = 0
175
            idxs = getIdxFromColumn(words, self.cursorColumn)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable getIdxFromColumn does not seem to be defined.
Loading history...
176
            if idxs != (-1, -1):
177
                if idxs[0] == -1:
178
                    #cursor is before first word, jump to beginig of last word on previous line
179
                    if self.cursorRow > 0:
180
                        self.cursorRow -= 1
181
                        words2 = splitter(self.text[self.cursorRow])
182
                        if len(words2) > 0:
183
                            #move cursor to begining of last word
184
                            self.cursorColumn = words2[-1][1]
185
                        else:
186
                            #no words on line, so move cursor to the end of line
187
                            self.cursorColumn = len(self.text[self.cursorRow])
188
                    else:
189
                        #we are on first line, so move cursor to begining of line
190
                        self.cursorColumn = 0
191
                elif idxs[0] == idxs[1]:
192
                    #we are inside word, so move cursor to begining of word
193
                    self.cursorColumn = words[idxs[0]][1]
194
                elif idxs[1] == -1:
195
                    #cursor is after last word, we must jump to begining of last word
196
                    self.cursorColumn = words[idxs[0]][1]
197
                else:
198
                    #cursor is between words, we must jump to begining of left word
199
                    self.cursorColumn = words[idxs[0]][1]
200
        elif self.cursorColumn > 0: self.cursorColumn -= 1
201
        elif self.cursorRow > 0:
202
            self.cursorRow -= 1
203
            self.cursorColumn = len(self.text[self.cursorRow])
204
        if evt.mod & pygame.KMOD_SHIFT:
205
            self.selection.select(self.cursorRow, self.cursorColumn)
206
        else:
207
            self.selection.deselect()
208
209
    def _processRight(self, evt):
210
        if evt.mod & pygame.KMOD_CTRL:
211
            # move one word right
212
            # take words on line
213
            words = splitter(self.text[self.cursorRow])
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable splitter does not seem to be defined.
Loading history...
214 View Code Duplication
            if len(words) == 0:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
215
                #move to next line
216
                if self.cursorRow < len(self.text) - 1:
217
                    self.cursorRow += 1
218
                    words2 = splitter(self.text[self.cursorRow])
219
                    if len(words2) > 0:
220
                        self.cursorColumn = words2[0][1]
221
                    else:
222
                        #on next line are only separators (or is empty), so move to column 0
223
                        self.cursorColumn = 0
224
                else:
225
                    #we are on last line, so move cursor to end of line
226
                    self.cursorColumn = len(self.text[self.cursorRow])
227
            idxs = getIdxFromColumn(words, self.cursorColumn)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable getIdxFromColumn does not seem to be defined.
Loading history...
228
            if idxs != (-1, -1):
229
                if idxs[0] == idxs[1] or self.cursorColumn == words[idxs[1]][1]:
230
                    #cursor is inside of word or is on begining of word, so move on begining of next word
231
                    if idxs[1] + 1 < len(words):
232
                        #there is next word on line
233
                        self.cursorColumn = words[idxs[1] + 1][1]
234
                    else:
235
                        #we must jump to begining first word on next line
236
                        if self.cursorRow < len(self.text) - 1:
237
                            self.cursorRow += 1
238
                            words2 = splitter(self.text[self.cursorRow])
239
                            if len(words2) > 0:
240
                                self.cursorColumn = words2[0][1]
241
                            else:
242
                                #on next line are only separators (or is empty), so move to column 0
243
                                self.cursorColumn = 0
244
                        else:
245
                            #we are on last line, so move cursor to end of line
246
                            self.cursorColumn = len(self.text[self.cursorRow])
247
                elif idxs[0] == -1:
248
                    #cursor is before first word, jump to beginig of fist word
249
                    self.cursorColumn = words[idxs[1]][1]
250
                elif idxs[1] == -1:
251
                    #cursor is after last word, we must jump to begining first word on next line
252
                    if self.cursorRow < len(self.text) - 1:
253
                        self.cursorRow += 1
254
                        words2 = splitter(self.text[self.cursorRow])
255
                        if len(words2) > 0:
256
                            self.cursorColumn = words2[0][1]
257
                        else:
258
                            #on next line are only separators (or is empty), so move to column 0
259
                            self.cursorColumn = 0
260
                    else:
261
                        #we are on last line, so move cursor to end of line
262
                        self.cursorColumn = len(self.text[self.cursorRow])
263
                else:
264
                    #cursor is between words
265
                    self.cursorColumn = words[idxs[1]][1]
266
        elif self.cursorColumn < len(self.text[self.cursorRow]): self.cursorColumn += 1
267
        elif self.cursorRow < len(self.text):
268
            # move to the next row
269
            if self.cursorRow < len(self.text) - 1:
270
                self.cursorRow += 1
271
                self.cursorColumn = 0
272
        if evt.mod & pygame.KMOD_SHIFT:
273
            self.selection.select(self.cursorRow,self.cursorColumn)
274
        else:
275
            self.selection.deselect()
276
277
    def _processUp(self, evt):
278
        if self.cursorRow > 0:
279
            self.cursorRow -= 1
280
            self.cursorColumn = min(self.cursorColumn, len(self.text[self.cursorRow]))
281
282
        if self.cursorRow - self.offsetRow < 0:
283
            self.vertScrollbar.onButton1(self, "", "")
284
285
        if evt.mod & pygame.KMOD_SHIFT:
286
            self.selection.select(self.cursorRow,self.cursorColumn)
287
        else:
288
            self.selection.deselect()
289
290
    def _processDown(self, evt):
291
        if self.cursorRow < len(self.text) - 1:
292
            self.cursorRow += 1
293
            self.cursorColumn = min(self.cursorColumn, len(self.text[self.cursorRow]))
294
295
        if self.cursorRow - self.offsetRow >= self.theme.getTextDrawLines(self):
296
            self.vertScrollbar.onButton2(self, "", "")
297
298
        if evt.mod & pygame.KMOD_SHIFT:
299
            self.selection.select(self.cursorRow,self.cursorColumn)
300
        else:
301
            self.selection.deselect()
302
303
    def _processHome(self, evt):
304
        self.cursorColumn = 0
305
        if evt.mod & pygame.KMOD_SHIFT:
306
            self.selection.select(self.cursorRow, self.cursorColumn)
307
        else:
308
            self.selection.deselect()
309
310
    def _processEnd(self, evt):
311
        self.cursorColumn = len(self.text[self.cursorRow])
312
        if evt.mod & pygame.KMOD_SHIFT:
313
            self.selection.select(self.cursorRow, self.cursorColumn)
314
        else:
315
            self.selection.deselect()
316
317
    def _processReturn(self, evt):
318
        text1 = self.text[self.cursorRow][self.cursorColumn:]
319
        text2 = self.text[self.cursorRow][:self.cursorColumn]
320
        self.text[self.cursorRow] = text1
321
        self.text.insert(self.cursorRow, text2)
322
        self.cursorRow += 1
323
        self.cursorColumn = 0
324
325
    def _processUnicode(self, evt):
326
        char = evt.unicode
327
        self.text[self.cursorRow] = u'%s%c%s' % (
328
            self.text[self.cursorRow][:self.cursorColumn], char, self.text[self.cursorRow][self.cursorColumn:]
329
        )
330
        self.cursorColumn += 1
331
332
    def _processNumKeyboard(self, evt):
333
        self.text[self.cursorRow] = u'%s%c%s' % (
334
            self.text[self.cursorRow][:self.cursorColumn], MAPPING[evt.key], self.text[self.cursorRow][self.cursorColumn:]
335
        )
336
        self.cursorColumn += 1
337
338
    def wrapDeleteSelection(self, func, evt, deleteOnly=False):
339
        if self.selection:
340
            self.deleteSelection()
341
            if deleteOnly: return
342
        func(evt)
343
344
    def wrapSelect(self, func, evt):
345
        if evt.mod & pygame.KMOD_SHIFT:
346
            self.selection.select(self.cursorRow, self.cursorColumn)
347
            func(evt)
348
            self.selection.select(self.cursorRow, self.cursorColumn)
349
        else:
350
            func(evt)
351
352
    def processKeyUp(self, evt):
353
        # consume pygame.K_RETURN (acceptButton on Window will not work)
354
        # can be choosable on construction?
355
        if evt.key == pygame.K_RETURN:
356
            return Const.NoEvent
357
        else:
358
            return Widget.processKeyUp(self, evt)
359
360
    def processKeyDown(self, evt):
361
        if not self.editable:
362
            return Widget.processKeyDown(self, evt)
363
364
        # process keys
365
        if evt.key == pygame.K_BACKSPACE:
366
            self.wrapDeleteSelection(self._processBackspace, evt, deleteOnly=True)
367
368
        elif evt.key == pygame.K_DELETE:
369
            self.wrapDeleteSelection(self._processDelete, evt, deleteOnly=True)
370
371
        elif evt.key == pygame.K_ESCAPE:
372
            self.app.setFocus(None)
373
374
        elif evt.key == pygame.K_LEFT:
375
            self.wrapSelect(self._processLeft, evt)
376
377
        elif evt.key == pygame.K_RIGHT:
378
            self.wrapSelect(self._processRight, evt)
379
380
        elif evt.key == pygame.K_UP:
381
            self.wrapSelect(self._processUp, evt)
382
383
        elif evt.key == pygame.K_DOWN:
384
            self.wrapSelect(self._processDown, evt)
385
386
        elif evt.key == pygame.K_TAB:
387
            pass
388
389
        elif evt.key == pygame.K_HOME:
390
            self.wrapSelect(self._processHome, evt)
391
392
        elif evt.key == pygame.K_END:
393
            self.wrapSelect(self._processEnd, evt)
394
395
        elif evt.key == pygame.K_RETURN:
396
            self.wrapDeleteSelection(self._processReturn, evt)
397
398
        elif hasattr(evt, 'unicode') and evt.unicode:
399
            self.wrapDeleteSelection(self._processUnicode, evt)
400
401
        elif evt.key in MAPPING:
402
            self.wrapDeleteSelection(self._processNumKeyboard, evt)
403
404
        return Widget.processKeyDown(self, Const.NoEvent)
405
406
    def onFocusGained(self):
407
        Widget.onFocusGained(self)
408
        self.cursorRow = len(self.text) - 1
409
        self.cursorColumn = len(self.text[self.cursorRow])
410
411
    def onFocusLost(self):
412
        Widget.onFocusLost(self)
413
        self.processAction(self.action)
414
415
    # redirect mouse wheel events to the scollbar
416
    def processMWUp(self, evt):
417
        if self.vertScrollbar:
418
            return self.vertScrollbar.processMWUp(evt)
419
420
    def processMWDown(self, evt):
421
        if self.vertScrollbar:
422
            return self.vertScrollbar.processMWDown(evt)
423
424
425
registerWidget(Text, 'text')
426
427
428
429