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]) |
|
|
|
|
161
|
|
View Code Duplication |
if len(words) == 0: |
|
|
|
|
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) |
|
|
|
|
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]) |
|
|
|
|
214
|
|
View Code Duplication |
if len(words) == 0: |
|
|
|
|
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) |
|
|
|
|
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
|
|
|
|