Merge lp:~codeforger/simplegc/SelectableList into lp:simplegc
- SelectableList
- Merge into trunk
Proposed by
Michael Rochester
Status: | Work in progress | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~codeforger/simplegc/SelectableList | ||||
Merge into: | lp:simplegc | ||||
Diff against target: |
534 lines (+519/-0) 2 files modified
sgc/__init__.py (+1/-0) sgc/widgets/list.py (+518/-0) |
||||
To merge this branch: | bzr merge lp:~codeforger/simplegc/SelectableList | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sam Bull | Pending | ||
Review via email:
|
Commit message
Description of the change
Adds a multiselectable list.
To post a comment you must log in.
Unmerged revisions
- 357. By Michael Rochester
-
cleaned the branch and made all the requested tweaks.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'sgc/__init__.py' |
2 | --- sgc/__init__.py 2013-03-23 16:16:35 +0000 |
3 | +++ sgc/__init__.py 2013-05-30 17:30:38 +0000 |
4 | @@ -31,6 +31,7 @@ |
5 | from widgets.fps_counter import FPSCounter |
6 | from widgets.input_box import InputBox |
7 | from widgets.label import Label |
8 | +from widgets.list import List |
9 | from widgets.radio_button import Radio |
10 | from widgets.scroll_box import ScrollBox |
11 | from widgets.settings import Keys |
12 | |
13 | === added file 'sgc/widgets/list.py' |
14 | --- sgc/widgets/list.py 1970-01-01 00:00:00 +0000 |
15 | +++ sgc/widgets/list.py 2013-05-30 17:30:38 +0000 |
16 | @@ -0,0 +1,518 @@ |
17 | +# Copyright 2010-2012 the SGC project developers. |
18 | +# See the LICENSE file at the top-level directory of this distribution |
19 | +# and at http://program.sambull.org/sgc/license.html. |
20 | + |
21 | +""" |
22 | +Selectable List. A container widget that provides a multiselectable list. |
23 | + |
24 | +""" |
25 | + |
26 | +import sgc |
27 | +from sgc.locals import * |
28 | + |
29 | +import pygame.mouse |
30 | +from pygame.locals import * |
31 | +from pygame import draw |
32 | + |
33 | +from _locals import * |
34 | +from base_widget import Simple |
35 | + |
36 | + |
37 | +class List(Simple): |
38 | + |
39 | + """ |
40 | + List |
41 | + |
42 | + A multicolumn list of text that can be multiselectable. |
43 | + """ |
44 | + |
45 | + _can_focus = True |
46 | + _settings_default = {"col": (10, 10, 10), |
47 | + "font": Font["widget"], |
48 | + "padding": 3, |
49 | + "offset": 0, |
50 | + "mult": False, |
51 | + "colwidth": [100], |
52 | + "headfont": Font["widget"], |
53 | + "header": False} |
54 | + |
55 | + _strings = pygame.cursors.sizer_x_strings |
56 | + _cursor = pygame.cursors.compile(_strings) |
57 | + _size = (len(_strings[0]), len(_strings)) |
58 | + _hotspot = (_size[0] / 2, _size[1] / 2) |
59 | + _cursor = (_size, _hotspot) + _cursor |
60 | + |
61 | + _cursor_set = False |
62 | + |
63 | + _move = False # Contains the index of the column being |
64 | + # resized. |
65 | + _mouse = False # Contains mouse position of last event. |
66 | + |
67 | + _resize = False # True if widget is resizing column. |
68 | + |
69 | + |
70 | + _focused = False # True if element has focus (draws dotted |
71 | + # line) |
72 | + _focus = 0 # Currently focused element (draws dotted line) |
73 | + |
74 | + _sort_column = False # Index of column that is currently sorted |
75 | + _sort_state = None # "top", "bottom" or none to denote current |
76 | + # sort direction. |
77 | + |
78 | + _lastclick = [[0, 0]] # stores the index of the last clicked |
79 | + # item and the time it was clicked, used |
80 | + # to check for double clicks. |
81 | + |
82 | + def _config(self, **kwargs): |
83 | + """ |
84 | + colwidth: A sequence of ints that describes the width of each |
85 | + column. |
86 | + multi_selection: ``bool`` Enable the ability to select more |
87 | + than one record at once. |
88 | + col: ``tuple`` (r,g,b) Text colour. |
89 | + font: Font object the label will render with. |
90 | + padding: ``int`` Vertical padding on list elements. |
91 | + content: The content to be displayed in the list. A sequence |
92 | + containing each row. Multi-column list will be another sequence |
93 | + for each row. |
94 | + For example: (("row0col0", "row0col1"), ("row1-0", "row1-1")) |
95 | + header: The header text of each column, nust be a seqauence of |
96 | + strings. |
97 | + headfont: The font to be used for the header. |
98 | + """ |
99 | + |
100 | + if "multi_selection" in kwargs: |
101 | + self._settings["mult"] = kwargs["multi_selection"] |
102 | + if "padding" in kwargs: |
103 | + self._settings["padding"] = kwargs["padding"] |
104 | + if "init" in kwargs: |
105 | + self._lbls = [] # List of Label widgets that make up the content. |
106 | + self._selected = [] # Currently selected items |
107 | + |
108 | + if "content" in kwargs: |
109 | + |
110 | + self._columns = [] |
111 | + self._content = list(kwargs["content"]) |
112 | + if not isinstance(self._content[0], list): |
113 | + for i, x in enumerate(self._content): |
114 | + self._content[i] = [x] |
115 | + if "col" in kwargs: |
116 | + self._settings["col"] = kwargs["col"] |
117 | + if "font" in kwargs: |
118 | + self._settings["font"] = kwargs["font"] |
119 | + if "colwidth" in kwargs: |
120 | + self._settings["colwidth"] = [c for c in kwargs["colwidth"]] |
121 | + elif self._settings["colwidth"][0] == 100: |
122 | + l = len(self._content[0]) |
123 | + self._settings["colwidth"] = [100 for c in range(l)] |
124 | + self.colpos = [0] |
125 | + for x in range(len(self._settings["colwidth"])): |
126 | + self.colpos.append((self.colpos[x] + |
127 | + self._settings["colwidth"][x])) |
128 | + if "headfont" in kwargs: |
129 | + self._setting["headfont"] = kwargs["headfont"] |
130 | + if "header" in kwargs: |
131 | + self._settings["header"] = kwargs["header"] |
132 | + font_height = self._settings["headfont"].get_ascent() +\ |
133 | + self._settings["headfont"].get_descent() + 6 |
134 | + self._settings["offset"] = font_height |
135 | + if "content" in kwargs: |
136 | + # not isinstance(list,self._content[0])) or\ |
137 | + # len(self._content[0] == 1: |
138 | + self._texth = self._settings["font"].get_ascent() + \ |
139 | + self._settings["font"].get_descent() + \ |
140 | + (self._settings["padding"] * 2) |
141 | + w = [] |
142 | + self._contain = None |
143 | + for x in (range(len(self._content[0]))): |
144 | + labels = [] |
145 | + p = 0 |
146 | + for y in range(len(self._content)): |
147 | + lbl = sgc.Label(pos=(0, p), col=self._settings["col"], |
148 | + text=self._content[y][x], |
149 | + font=self._settings["font"]) |
150 | + p += self._texth |
151 | + lbl._parent = self._contain |
152 | + labels.append(lbl) |
153 | + w.append(lbl) |
154 | + self._lbls.append(labels) |
155 | + if self._settings["colwidth"]: |
156 | + column = sgc.Container((self._settings["colwidth"][x] - |
157 | + self._settings["padding"] * 2, |
158 | + len(self._content) * self._texth), |
159 | + widgets=labels) |
160 | + else: |
161 | + column = sgc.Container(widgets=labels) |
162 | + self._columns.append(column) |
163 | + |
164 | + self._contain = sgc.HBox(border=self._settings["padding"], |
165 | + widgets=self._columns) |
166 | + self._contain._parent = self |
167 | + if not hasattr(self, "image"): |
168 | + self._create_base_images((self._contain.image.get_width(), |
169 | + self._contain.image.get_height())) |
170 | + self._scroll = sgc.ScrollBox((self.rect.w, |
171 | + self.rect.h - |
172 | + self._settings["offset"]), |
173 | + pos=(0, self._settings["offset"]), |
174 | + widget=self._contain) |
175 | + self._contain._parent = self._scroll |
176 | + self._scroll._parent = self |
177 | + self._anchor = False |
178 | + |
179 | + def _draw_final(self): |
180 | + if self._settings["header"]: |
181 | + self._header = sgc.Simple((self.colpos[-1] + 10, |
182 | + self._settings["offset"])) |
183 | + for p, header in enumerate(self._settings["header"]): |
184 | + lbl = sgc.Label((self._settings["colwidth"][p], |
185 | + self._settings["offset"]), |
186 | + pos=(0, 0), |
187 | + text=header, |
188 | + col=(10, 10, 10), |
189 | + font=self._settings["headfont"]) |
190 | + draw.rect(self._header.image, (255, 255, 255), |
191 | + ((self.colpos[p], 0), |
192 | + (self.colpos[p] + self._settings["colwidth"][p], |
193 | + self._settings["offset"])), 0) |
194 | + self._header.image.blit(lbl.image, (self.colpos[p] + 3, 1)) |
195 | + draw.line(self._header.image, (150, 150, 150), |
196 | + (self.colpos[p], 0), |
197 | + (self.colpos[p], self._settings["offset"])) |
198 | + |
199 | + draw.line(self._images["image"], (150, 150, 150), |
200 | + (1, 0), (self.image.get_width()-2, 0)) |
201 | + draw.line(self._images["image"], (150, 150, 150), |
202 | + (1, self.image.get_height() - 1), |
203 | + (self.image.get_width() - 2, self.image.get_height() - 1)) |
204 | + draw.line(self._images["image"], (150, 150, 150), |
205 | + (0, 1), (0, self.image.get_height() - 2)) |
206 | + draw.line(self._images["image"], (150, 150, 150), |
207 | + (self.image.get_width() - 1, 1), |
208 | + (self.image.get_width() - 1, self.image.get_height() - 2)) |
209 | + if self._sort_state != None: |
210 | + if self._sort_state == "top": |
211 | + draw.line(self._header.image, (150, 150, 150), |
212 | + (self.colpos[self._sort_column + 1] - 15, 3), |
213 | + (self.colpos[self._sort_column + 1] - 10, |
214 | + self._settings["offset"] - 3)) |
215 | + draw.line(self._header.image, (150, 150, 150), |
216 | + (self.colpos[self._sort_column + 1] - 5, 3), |
217 | + (self.colpos[self._sort_column + 1] - 10, |
218 | + self._settings["offset"] - 3)) |
219 | + else: |
220 | + draw.line(self._header.image, (150, 150, 150), |
221 | + (self.colpos[self._sort_column + 1] - 15, |
222 | + self._settings["offset"] - 3), |
223 | + (self.colpos[self._sort_column + 1] - 10, 3)) |
224 | + draw.line(self._header.image, (150, 150, 150), |
225 | + (self.colpos[self._sort_column + 1] - 5, |
226 | + self._settings["offset"] - 3), |
227 | + (self.colpos[self._sort_column + 1] - 10, 3)) |
228 | + |
229 | + def update(self, time): |
230 | + self.image.fill((255, 255, 255)) |
231 | + self._scroll.update(time) |
232 | + |
233 | + w = (self.image.get_width()) |
234 | + for sel in self._selected: |
235 | + selection = Simple((w, self._texth)) |
236 | + selection.pos = (0, (sel - 1) * self._texth + |
237 | + self._settings["padding"] + |
238 | + self._settings["offset"] + |
239 | + self._contain.rect.y) |
240 | + selection.image.fill(self._settings["col"]) |
241 | + selection.image.set_alpha(100) |
242 | + self.image.blit(selection.image, selection.pos) |
243 | + if self._focused: |
244 | + focus = Simple((w, self._texth)) |
245 | + focus.pos = (0, (self._focus - 1) * self._texth + |
246 | + self._settings["padding"] + |
247 | + self._settings["offset"] + |
248 | + self._contain.rect.y) |
249 | + focus.image.fill(Color(255, 255, 0)) |
250 | + focus.image.set_colorkey(Color(255, 255, 0)) |
251 | + self._dotted_rect(focus.image) |
252 | + self.image.blit(focus.image, focus.pos) |
253 | + |
254 | + self.image.blit(self._scroll.image, (0, self._settings["offset"])) |
255 | + if self._settings["header"]: |
256 | + self.image.blit(self._header.image, |
257 | + (self._scroll._settings["widget"].pos[0], 0)) |
258 | + |
259 | + def _event(self, event): |
260 | + if event.type == MOUSEBUTTONDOWN and event.button == 1: |
261 | + if (self._collide_resize(event.pos) is not False): |
262 | + self._move = self._collide_resize(event.pos) |
263 | + self._mouse = event.pos[0] |
264 | + elif self._collide_head(event.pos) is not False: |
265 | + old_sort_col = self._sort_column |
266 | + self._sort_column = self._collide_head(event.pos) |
267 | + if old_sort_col != self._sort_column: |
268 | + self._sort_state = None |
269 | + if self._sort_state == None: |
270 | + self._sort_state = "top" |
271 | + self.orr_content = list(self._content) |
272 | + self._content.sort(key=lambda data: data[self._sort_column]) |
273 | + elif self._sort_state == "top": |
274 | + self._sort_state = "bottom" |
275 | + self._content.reverse() |
276 | + else: |
277 | + self._sort_state = None |
278 | + self._content = self.orr_content |
279 | + self.config(content=self._content) |
280 | + self._scroll = sgc.ScrollBox((self.rect.w, |
281 | + self.rect.h - |
282 | + self._settings["offset"]), |
283 | + pos=(0, self._settings["offset"]), |
284 | + widget=self._contain) |
285 | + self._scroll._parent = self |
286 | + self._draw_final() |
287 | + else: |
288 | + sel = self._colide_item(event.pos) |
289 | + self._lastclick = [self._lastclick[-1], |
290 | + [sel, pygame.time.get_ticks()]] |
291 | + |
292 | + if sel > 0 and sel < len(self._content) + 1: |
293 | + if (self._lastclick[0][0] == self._lastclick[1][0] and |
294 | + (self._lastclick[1][1] - self._lastclick[0][1]) < 500): |
295 | + self.on_confirm() |
296 | + else: |
297 | + if (pygame.key.get_mods() & KMOD_CTRL and |
298 | + self._settings["mult"]): |
299 | + if sel in self._selected: |
300 | + self._selected.remove(sel) |
301 | + else: |
302 | + self._selected.append(sel) |
303 | + elif (pygame.key.get_mods() & KMOD_SHIFT |
304 | + and self._settings["mult"] and self._anchor): |
305 | + self._selected = [] |
306 | + for new in range(abs(self._anchor - sel) + 1): |
307 | + lowest_selected = min(self._anchor, sel) |
308 | + self._selected.append(new + lowest_selected) |
309 | + else: |
310 | + self._anchor = sel |
311 | + self._selected = [sel] |
312 | + self._focus = sel |
313 | + self._focused = False |
314 | + self.on_select() |
315 | + self._scroll._event(event) |
316 | + elif event.type == MOUSEBUTTONUP: |
317 | + if self._resize is not False and self._mouse is not False: |
318 | + self._scroll = sgc.ScrollBox((self.rect.w, |
319 | + self.rect.h - |
320 | + self._settings["offset"]), |
321 | + pos=(0, self._settings["offset"]), |
322 | + widget=self._contain) |
323 | + self._scroll._parent = self |
324 | + self._mouse = False |
325 | + self._resize = False |
326 | + self._scroll._event(event) |
327 | + elif event.type == MOUSEMOTION: |
328 | + if self._collide_resize(event.pos) is False and\ |
329 | + self._cursor_set == True: |
330 | + self._remove_cursor() |
331 | + self._cursor_set = False |
332 | + elif self._collide_resize(event.pos) is not False and\ |
333 | + self._cursor_set == False: |
334 | + self._set_cursor(*List._cursor) |
335 | + self._cursor_set = True |
336 | + if self._resize is not False and self._mouse is not False: |
337 | + dif = event.pos[0] - self._mouse |
338 | + self._mouse = event.pos[0] |
339 | + self._settings["colwidth"][self._move] += dif |
340 | + if self._settings["colwidth"][self._move] < 0: |
341 | + self._settings["colwidth"][self._move] = 0 |
342 | + self.config(columnwidth=self._settings["colwidth"], |
343 | + content=self._content) |
344 | + self._contain._create_base_images((self._contain.rect.w, |
345 | + self._contain.rect.h)) |
346 | + elif self._mouse is not False: |
347 | + if abs(event.pos[0] - self._mouse) > 4: |
348 | + self._resize = True |
349 | + self._scroll._event(event) |
350 | + else: |
351 | + self._scroll._event(event) |
352 | + if event.type == KEYDOWN: |
353 | + if event.key == K_SPACE or event.key == K_RETURN: |
354 | + if pygame.key.get_mods() & KMOD_CTRL: |
355 | + if self._focused: |
356 | + if self._focus not in self._selected: |
357 | + self._selected.append(self._focus) |
358 | + else: |
359 | + self._selected.remove(self._focus) |
360 | + self.on_select() |
361 | + else: |
362 | + self.on_confirm() |
363 | + |
364 | + if event.key == K_UP: |
365 | + if (pygame.key.get_mods() & KMOD_SHIFT |
366 | + and self._settings["mult"] and self._anchor): |
367 | + selrange = [min(self._selected), max(self._selected)] |
368 | + if selrange[1] == self._anchor and selrange[0] != 1: |
369 | + selrange[0] -= 1 |
370 | + elif selrange[0] != selrange[1] and\ |
371 | + selrange[1] != self._anchor: |
372 | + selrange[1] -= 1 |
373 | + self._selected = [] |
374 | + for x in range(selrange[0], selrange[1] + 1): |
375 | + self._selected.append(x) |
376 | + self.on_select() |
377 | + elif pygame.key.get_mods() & KMOD_CTRL: |
378 | + if self._focus != 1: |
379 | + self._focus -= 1 |
380 | + self._focused = True |
381 | + else: |
382 | + if self._anchor != 1: |
383 | + self._selected = [self._anchor - 1] |
384 | + else: |
385 | + self._selected = [1] |
386 | + self._anchor = self._selected[0] |
387 | + self.on_select() |
388 | + |
389 | + elif event.key == K_DOWN: |
390 | + if (pygame.key.get_mods() & KMOD_SHIFT and\ |
391 | + self._settings["mult"] and self._anchor): |
392 | + selrange = [min(self._selected), max(self._selected)] |
393 | + if selrange[0] == self._anchor and\ |
394 | + selrange[1] != len(self._content): |
395 | + selrange[1] += 1 |
396 | + elif selrange[1] != selrange[0] and\ |
397 | + selrange[0] != self._anchor: |
398 | + selrange[0] += 1 |
399 | + self._selected = [] |
400 | + for x in range(selrange[0], selrange[1] + 1): |
401 | + self._selected.append(x) |
402 | + self.on_select() |
403 | + elif pygame.key.get_mods() & KMOD_CTRL: |
404 | + if self._focus != len(self._content): |
405 | + self._focus += 1 |
406 | + self._focused = True |
407 | + else: |
408 | + if self._anchor != len(self._content): |
409 | + self._selected = [self._anchor + 1] |
410 | + self._anchor = self._selected[0] |
411 | + self.on_select() |
412 | + elif event.key == K_HOME: |
413 | + self._selected = [1] |
414 | + self._anchor = self._selected[0] |
415 | + self.on_select() |
416 | + elif event.key == K_END: |
417 | + self._selected = [len(self._content)] |
418 | + self._anchor = self._selected[0] |
419 | + self.on_select() |
420 | + elif event.key == K_PAGEUP: |
421 | + self._selected = [self._selected[0] - |
422 | + (self.rect.bottom / self._texth - 3)] |
423 | + if self._selected[0] < 1: |
424 | + self._selected = [1] |
425 | + self._anchor = self._selected[0] |
426 | + self.on_select() |
427 | + elif event.key == K_PAGEDOWN: |
428 | + self._selected = [self._selected[0] + |
429 | + self.rect.bottom / self._texth - 3] |
430 | + if self._selected[0] > len(self._content): |
431 | + self._selected = [len(self._content)] |
432 | + self._anchor = self._selected[0] |
433 | + self.on_select() |
434 | + elif event.key == K_BACKSLASH and\ |
435 | + pygame.key.get_mods() & KMOD_CTRL: |
436 | + self._anchor = 1 |
437 | + self._selected = [] |
438 | + self.on_select() |
439 | + |
440 | + def _collide_resize(self, pos): |
441 | + for p, x in enumerate(self.colpos[1:], 1): |
442 | + if isinstance(x, list): |
443 | + x = x[0] |
444 | + if (pos[0] - self.pos_abs[0] >= |
445 | + self._scroll._settings["widget"].pos[0] + x - 2 and |
446 | + pos[0] - self.pos_abs[0] <= |
447 | + self._scroll._settings["widget"].pos[0] + x + 2 and |
448 | + pos[1] < self.pos_abs[1] + self._settings["offset"] and |
449 | + pos[1] > self.pos_abs[1]): |
450 | + return p - 1 |
451 | + return False |
452 | + |
453 | + def _collide_head(self, pos): |
454 | + for p, x in enumerate(self.colpos[1:], 1): |
455 | + if isinstance(x, list): |
456 | + x = x[0] |
457 | + if (pos[0] - self.pos_abs[0] <= |
458 | + self._scroll._settings["widget"].pos[0] + x and |
459 | + pos[1] < self.pos_abs[1] + self._settings["offset"] and |
460 | + pos[1] > self.pos_abs[1]): |
461 | + return p - 1 |
462 | + return False |
463 | + |
464 | + def _dotted_rect(self, image, col=(0, 0, 0)): |
465 | + image.lock() |
466 | + for i in range(0, self.rect.w, 3): |
467 | + # Draw horizontal lines |
468 | + image.set_at((i, 0), col) |
469 | + image.set_at((i, self._texth - 1), col) |
470 | + for i in range(0, self._texth, 2): |
471 | + # Draw vertical lines |
472 | + image.set_at((0, i), col) |
473 | + image.set_at((self.rect.w - 1, i), col) |
474 | + image.unlock() |
475 | + |
476 | + def _colide_item(self, pos): |
477 | + if isinstance(self._lbls[0], list): |
478 | + for i, x in enumerate(self._lbls[0]): |
479 | + if(pos[1] < x.rect_abs.bottom): |
480 | + return i + 1 |
481 | + return -1 |
482 | + else: |
483 | + for i, x in enumerate(self._lbls): |
484 | + if(pos[1] < x.rect_abs.bottom): |
485 | + return i + 1 |
486 | + return -1 |
487 | + |
488 | + def on_select(self): |
489 | + """ |
490 | + Called when the selection is changed. Even when the new selection is |
491 | + the same as the old one. |
492 | + |
493 | + Emits an event with attribute 'gui_type' == "select". |
494 | + |
495 | + Override this function to use as a callback handler. |
496 | + |
497 | + """ |
498 | + pygame.event.post(self._create_event("select")) |
499 | + |
500 | + def on_confirm(self): |
501 | + """ |
502 | + Called when an item is double clicked, or the enter key is pressed with |
503 | + a selection. |
504 | + |
505 | + Emits an event with attribute 'gui_type' == "confirm". |
506 | + |
507 | + Override this function to use as a callback handler. |
508 | + |
509 | + """ |
510 | + pygame.event.post(self._create_event("confirm")) |
511 | + |
512 | + def _focus_enter(self, focus): |
513 | + """Draw rectangle when focus is gained from keyboard.""" |
514 | + self._scroll._focus_enter(focus) |
515 | + |
516 | + def _focus_exit(self): |
517 | + """Stop drawing rectangle when focus is lost.""" |
518 | + self._scroll._focus_exit() |
519 | + |
520 | + @property |
521 | + def selected_text(self): |
522 | + """ |
523 | + A property function that will return a list of strings from the |
524 | + selected rows. |
525 | + """ |
526 | + return [self._content[x-1] for x in self._selected] |
527 | + |
528 | + @property |
529 | + def selected_pos(self): |
530 | + """ |
531 | + A property function that will retrun a list of the indexes of the |
532 | + selected rows. |
533 | + """ |
534 | + return [x-1 for x in self._selected] |