Total Complexity | 102 |
Total Lines | 426 |
Duplicated Lines | 6.34 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like pygameui.Text often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
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 | |||
429 |