Merge lp:~codeforger/simplegc/SelectableList into lp:simplegc

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
Reviewer Review Type Date Requested Status
Sam Bull Pending
Review via email: mp+166553@code.launchpad.net

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]

Subscribers

People subscribed via source and target branches