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

Proposed by Michael Rochester
Status: Needs review
Proposed branch: lp:~codeforger/simplegc/TabController
Merge into: lp:simplegc
Diff against target: 561 lines (+482/-0)
6 files modified
docs/conf.py (+1/-0)
docs/sgc.widgets.rst (+13/-0)
example/test.py (+13/-0)
sgc/__init__.py (+2/-0)
sgc/widgets/progress.py (+168/-0)
sgc/widgets/tab_controller.py (+285/-0)
To merge this branch: bzr merge lp:~codeforger/simplegc/TabController
Reviewer Review Type Date Requested Status
Sam Bull Pending
Review via email: mp+219938@code.launchpad.net

Description of the change

I have added a TabController widget that allows the creation of a tab container where each tab has a title and a singe child widget.

To post a comment you must log in.

Unmerged revisions

357. By Michael Rochester

Created the TabController widget

356. By Sam Bull

Add ProgressBar widget.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'docs/conf.py'
2--- docs/conf.py 2013-03-23 16:16:35 +0000
3+++ docs/conf.py 2014-05-17 14:54:22 +0000
4@@ -12,6 +12,7 @@
5 # serve to show the default.
6
7 import sys, os
8+sys.path.insert(0, "..")
9
10 # Initalise pygame for sgc to function properly
11 import pygame
12
13=== modified file 'docs/sgc.widgets.rst'
14--- docs/sgc.widgets.rst 2013-03-22 17:46:43 +0000
15+++ docs/sgc.widgets.rst 2014-05-17 14:54:22 +0000
16@@ -116,6 +116,19 @@
17
18 .. automethod:: config
19
20+:mod:`progress` Module
21+--------------------------
22+
23+.. automodule:: sgc.widgets.progress
24+
25+ .. autoclass:: sgc.widgets.progress.ProgressBar
26+ :members:
27+ :undoc-members:
28+ :show-inheritance:
29+ :exclude-members: update, groups, selected
30+
31+ .. automethod:: config
32+
33 :mod:`radio_button` Module
34 --------------------------
35
36
37=== modified file 'example/test.py'
38--- example/test.py 2013-03-22 17:46:43 +0000
39+++ example/test.py 2014-05-17 14:54:22 +0000
40@@ -63,6 +63,10 @@
41 input_box = sgc.InputBox(label="Input Box", default="default text...")
42 input_box.config(pos=(30,120))
43 input_box.add(order=0)
44+# Create progress bar
45+progress_bar = sgc.ProgressBar()
46+progress_bar.config(pos=(400,120))
47+progress_bar.add()
48 # Change colour button, on_click event caught in event loop
49 button = sgc.Button(label="Change\ncolour", pos=(40,200))
50 # Create FPS counter
51@@ -129,6 +133,8 @@
52 label.rect.midtop = title.rect.midbottom
53 label.add()
54
55+progress_fraction = 0.
56+
57 while True:
58 time = clock.tick(30)
59 for event in pygame.event.get():
60@@ -139,6 +145,7 @@
61 print "Button event"
62 if event.widget is button and event.gui_type == "click":
63 button.config(col=[random.randrange(1,200) for x in range(3)])
64+ progress_fraction = 0. if progress_fraction is False else False
65 elif event.widget is input_box:
66 clear()
67 elif event.type == KEYDOWN:
68@@ -147,6 +154,12 @@
69 elif event.type == QUIT:
70 exit()
71
72+ if progress_fraction is not False:
73+ progress_fraction = (progress_fraction + .02) % 1
74+ progress_bar.set_fraction(progress_fraction)
75+ else:
76+ progress_bar.pulse()
77+
78 # Cleanup removed windows
79 for widget in dialogs:
80 if not widget.active():
81
82=== modified file 'sgc/__init__.py'
83--- sgc/__init__.py 2013-03-23 16:16:35 +0000
84+++ sgc/__init__.py 2014-05-17 14:54:22 +0000
85@@ -31,11 +31,13 @@
86 from widgets.fps_counter import FPSCounter
87 from widgets.input_box import InputBox
88 from widgets.label import Label
89+from widgets.progress import ProgressBar
90 from widgets.radio_button import Radio
91 from widgets.scroll_box import ScrollBox
92 from widgets.settings import Keys
93 from widgets.scale import Scale
94 from widgets.switch import Switch
95+from widgets.tab_controller import TabController
96
97 # Import Menu last, so it can import the other widgets from here.
98 from widgets.menu import Menu
99
100=== added file 'sgc/widgets/progress.py'
101--- sgc/widgets/progress.py 1970-01-01 00:00:00 +0000
102+++ sgc/widgets/progress.py 2014-05-17 14:54:22 +0000
103@@ -0,0 +1,168 @@
104+# Copyright 2013 the SGC project developers.
105+# See the LICENSE file at the top-level directory of this distribution
106+# and at http://program.sambull.org/sgc/license.html.
107+
108+"""
109+Progress Bar widget. Indicates progress to the user with either a progress
110+estimate or activity throbber.
111+
112+"""
113+
114+import pygame
115+from pygame.locals import *
116+from pygame import draw
117+
118+from _locals import *
119+from base_widget import Simple
120+
121+
122+class ProgressBar(Simple):
123+ """
124+ A Progress Bar.
125+
126+ Images:
127+ 'image': The background of the bar.
128+ 'bar': The image to be used for the percent bar
129+ (bar will be cropped, not stretched).
130+ 'throbber': The image used for the 'activity' throbber.
131+ 'overlay': This image will always be displayed on top of the widget
132+ regardless of mode, it is by default blank, however passing the
133+ keyword 'text' to the config function will draw this text onto this
134+ surface.
135+ """
136+
137+ _default_size = (100, 20)
138+ _extra_images = {"bar": ((1, -4), (1, -4)),
139+ "throbber": ((.2, -6), (1, -6)),
140+ "overlay": ((1, 0), (1, 0))}
141+ _settings_default = {"col": (118, 45, 215), "font": Font["widget"],
142+ "text": "", "text_col": (10, 10, 10), "total_time": 1.,
143+ "pulse_duration": .1}
144+ _surf_flags = SRCALPHA
145+
146+ _bar_fraction = 0.
147+ _throbber_move_timer = 0
148+ _activity_mode = False
149+
150+ def _config(self, **kwargs):
151+ """
152+ col: ``tuple`` (r,g,b) The color to be used for the bar, to avoid
153+ saturation of the throbber use values below 200
154+ pulse_duration: ``float`` time in seconds each pulse should last,
155+ useful if your update loop is slower than the default of .25.
156+ total_time: ``float`` time in seconds the throbber should take to
157+ travel the full distance of the bar.
158+ font: font to be used for the text.
159+ text: ``str`` the text to be shown in the center of the widget.
160+ text_col: the colour of the text.
161+
162+ """
163+ for key in ("col", "font", "total_time",
164+ "pulse_duration", "text_col"):
165+ if key in kwargs:
166+ self._settings[key] = kwargs[key]
167+
168+ if "text" in kwargs:
169+ self._settings["text"] = kwargs["text"]
170+ overlay = self._images["overlay"]
171+ overlay.image.fill((0, 0, 0, 0))
172+ label = self._settings["font"].render(self._settings["text"], True,
173+ self._settings["text_col"])
174+ overlay.image.blit(label, (overlay.rect.w / 2 -
175+ label.get_rect().w / 2,
176+ overlay.rect.h / 2 -
177+ label.get_rect().h / 2))
178+
179+ if "total_time" in kwargs or "pulse_duration" in kwargs:
180+ total_time_ms = self._settings["total_time"] * 1000.
181+ self._throbber_velocity = self.rect.w / total_time_ms
182+
183+ if "init" in kwargs:
184+ self._images["bar"]._show = False
185+ self._images["throbber"]._show = False
186+ self._images["throbber"].pos = [0, 3]
187+
188+ total_time_ms = self._settings["total_time"] * 1000.
189+ self._throbber_velocity = self.rect.w / total_time_ms
190+
191+ self._throbber_x = 0
192+
193+ def _draw_base(self):
194+ draw.rect(self._images["image"], (100, 100, 100),
195+ (0, 0, self.rect.w, self.rect.h))
196+ draw.rect(self._images["image"], (240, 240, 240),
197+ (1, 1, self.rect.w - 2, self.rect.h - 2))
198+
199+ def _draw_bar(self, image, size):
200+ draw.rect(image, self._settings["col"],
201+ (0, 0, size[0], size[1]))
202+
203+ def _draw_throbber(self, image, size):
204+ brightened_color = [x + 20 for x in self._settings["col"]]
205+ draw.rect(image, brightened_color, (0, 0, size[0], size[1]))
206+
207+ def _draw_overlay(self, image, size):
208+ pass
209+
210+ def update(self, time):
211+ if self._activity_mode:
212+ self._images["bar"]._show = False
213+ if self._throbber_move_timer > 0:
214+ self._throbber_move_timer -= time
215+ # Move throbber.
216+ self._throbber_x += self._throbber_velocity * time
217+ self._images["throbber"].rect.x = self._throbber_x
218+ # If outside box, change direction and move back in.
219+ left_edge = self._images["throbber"].rect.x - 3
220+ right_edge = left_edge + self._images["throbber"].rect.w + 6
221+ if left_edge < 0 or right_edge > self.rect.w:
222+ self._throbber_velocity = - self._throbber_velocity
223+ self._throbber_x += self._throbber_velocity * time
224+ self._images["throbber"].rect.x = self._throbber_x
225+ else:
226+ bar_rect = (0, 0, self.rect.w * self._bar_fraction, self.rect.h)
227+ draw.rect(self.image, (100, 100, 100),
228+ (0, 0, self.rect.w, self.rect.h))
229+ draw.rect(self.image, (240, 240, 240),
230+ (1, 1, self.rect.w - 2, self.rect.h - 2))
231+ self.image.set_clip(bar_rect)
232+ self.image.blit(self._images["bar"].image, (2, 2))
233+ self.image.set_clip(None)
234+
235+ def pulse(self):
236+ """
237+ Moves the activity mode throbber.
238+
239+ The throbber will move for the number of seconds described by the
240+ pulse_duration config argument.
241+
242+ Note: This will cause the Widget to change to activity mode if it
243+ was in percent mode before.
244+
245+ """
246+ if not self._activity_mode:
247+ self._activity_mode = True
248+ bar_rect = (0, 0, self.rect.w * self._bar_fraction, self.rect.h)
249+ draw.rect(self.image, (100, 100, 100),
250+ (0, 0, self.rect.w, self.rect.h))
251+ draw.rect(self.image, (240, 240, 240),
252+ (1, 1, self.rect.w - 2, self.rect.h - 2))
253+ self._images["throbber"]._show = True
254+ self._throbber_move_timer = self._settings["pulse_duration"] * 1000
255+
256+ @property
257+ def fraction(self):
258+ """Returns the current fraction the bar is at as a float."""
259+ return self._bar_fraction
260+
261+ def set_fraction(self, fraction):
262+ """
263+ Sets the fraction the bar should be as a float.
264+
265+ Note: This will cause the widget to change to percent mode if it
266+ was in activity mode before.
267+ """
268+ if self._activity_mode:
269+ self._activity_mode = False
270+ self._images["throbber"]._show = False
271+ self._bar_fraction = min(max(fraction, 0.), 1.)
272
273=== added file 'sgc/widgets/tab_controller.py'
274--- sgc/widgets/tab_controller.py 1970-01-01 00:00:00 +0000
275+++ sgc/widgets/tab_controller.py 2014-05-17 14:54:22 +0000
276@@ -0,0 +1,285 @@
277+# Copyright 2010-2014 the SGC project developers.
278+# See the LICENSE file at the top-level directory of this distribution
279+# and at http://program.sambull.org/sgc/license.html.
280+
281+"""
282+Tabbed Container. A container that swaps the content in its area based upon the
283+selected tab
284+
285+"""
286+
287+import pygame.mouse
288+from pygame.locals import *
289+from pygame import draw, gfxdraw
290+
291+from _locals import *
292+from base_widget import Simple
293+
294+
295+class TabController(Simple):
296+ """
297+ TabController
298+
299+ container that has multiple tabs and swaps content within its area.
300+ """
301+
302+ _surf_flags = SRCALPHA
303+ _can_focus = True
304+ _extra_images = {"back": ((0, 1), (0, 20)), "front": ((0, 1), (0, 20))}
305+ _settings_default = {"tab_col": (200, 200, 200), "col": (255, 255, 255),
306+ "font": Font["widget"], "text_col": (10, 10, 10),
307+ "max_width": 100, "min_width": 50}
308+
309+ _current_tab = False
310+
311+ def __len__(self):
312+ return len(self._ordered_tabs)
313+
314+ def _config(self, **kwargs):
315+ """
316+ col: ``tuple`` (r,g,b) background color of the content area.
317+ tab_col: ``tuple`` (r,g,b) color to be used in drawing front tabs
318+ font: Font object the tab title will render with.
319+ padding: ``int`` padding in content area
320+ text_col: ``tuple`` (r,g,b) color to draw the tab titles with
321+ """
322+ for key in ("col", "tab_col", "font", "text_col",
323+ "max_width", "min_width"):
324+ if key in kwargs:
325+ self._settings[key] = kwargs[key]
326+
327+ if "init" in kwargs:
328+ self._tab_widget = {}
329+ self._tab_images = {}
330+ self._tab_sizes = {}
331+ self._ordered_tabs = []
332+
333+ self._images["back"]._show = False
334+ self._images["front"]._show = False
335+
336+ def add_tab(self, tab_name, widget, pos=None):
337+ """
338+ Add a tab with a child widget.
339+
340+ tab_name: ``str`` The name of the tab that should be created.
341+ widget: The widget to be placed in the content area of this tab.
342+ """
343+ if pos is None:
344+ self._ordered_tabs.append(tab_name)
345+ else:
346+ self._ordered_tabs.insert(pos, tab_name)
347+
348+ widget.pos = (widget.pos[0] + 1, widget.pos[1] + 20)
349+
350+ self._tab_widget[tab_name] = widget
351+ self._tab_widget[tab_name]._parent = self
352+ if not self._current_tab:
353+ self._current_tab = self._ordered_tabs[0]
354+
355+ cut_tab_name, tab_width = self._cut_string(tab_name,
356+ self._settings["max_width"])
357+ if tab_width < self._settings["min_width"]:
358+ tab_width = self._settings["min_width"]
359+
360+ tab_images = self._tab_images[tab_name] = {}
361+
362+ tab_images["back"] = pygame.Surface((tab_width + 1, 20))
363+ self._draw_back(tab_images["back"], (tab_width, 20))
364+
365+ tab_images["front"] = pygame.Surface((tab_width + 1, 20))
366+ self._draw_front(tab_images["front"], (tab_width, 20))
367+
368+ tab_title = self._settings["font"].render(cut_tab_name, True,
369+ self._settings["text_col"])
370+
371+ tab_images["back"].blit(tab_title, (5, 3))
372+ tab_images["front"].blit(tab_title, (5, 3))
373+
374+ self._tab_sizes[tab_name] = tab_width
375+
376+ def _cut_string(self, s, w, ellipsis=True):
377+ size = self._settings["font"].size(s)[0]
378+ if size < w:
379+ return s, size + 10
380+ if ellipsis:
381+ for i in range(len(s)):
382+ size = self._settings["font"].size(s+"...")[0]
383+ if size < w:
384+ return s+"...", size + 10
385+ s = s[:-1]
386+ else:
387+ for i in range(len(s)):
388+ size = self._settings["font"].size(s)[0]
389+ if size < w:
390+ return s, size + 10
391+ s = s[:-1]
392+
393+ def remove_tab(self, pos):
394+ del self._tab_widget[self._ordered_tabs[pos]]
395+ del self._tab_images[self._ordered_tabs[pos]]
396+ del self._tab_sizes[self._ordered_tabs[pos]]
397+ del self._ordered_tabs[pos]
398+
399+ def _draw_back(self, image, size):
400+ image.fill((255, 255, 255))
401+ self._gradientline(image, 200, 0, size[1], 2)
402+ self._gradientline(image, 200, size[0], size[1], 2)
403+ draw.line(image, (200, 200, 200), (0, size[1] - 1),
404+ (size[0], size[1] - 1))
405+
406+ def _draw_front(self, image, size):
407+ image.fill(self._settings["col"])
408+ draw.rect(image, self._settings["tab_col"],
409+ Rect(0, 0, size[0] + 1, 30))
410+ draw.rect(image, self._settings["col"],
411+ Rect(1, 1, size[0] - 1, 30))
412+
413+ def _event(self, event):
414+ if event.type == MOUSEBUTTONDOWN and event.button == 1:
415+ internal_pos = [event.pos[0] - self.rect.x,
416+ event.pos[1] - self.rect.y]
417+ #if and internal_pos[0] < 300 and\
418+ # internal_pos[0] > 0:
419+ # self._current_tab = self._ordered_tabs[internal_pos[0] / 100]
420+ tab_hit = 0
421+ if internal_pos[1] < 20 and internal_pos[1] > 0:
422+ for tab in self._ordered_tabs:
423+ if (internal_pos[0] > tab_hit and
424+ internal_pos[0] < (tab_hit + self._tab_sizes[tab])):
425+ self._current_tab = tab
426+ break
427+ else:
428+ tab_hit += self._tab_sizes[tab]
429+
430+ if event.type == KEYDOWN:
431+ if (pygame.key.get_mods() & KMOD_CTRL) and\
432+ (pygame.key.get_mods() & KMOD_ALT):
433+ if event.key == K_PAGEUP:
434+ self.previous_tab()
435+ elif event.key == K_PAGEDOWN:
436+ self.next_tab()
437+ elif event.key >= K_0 and\
438+ event.key <= K_9 and\
439+ (pygame.key.get_mods() & KMOD_ALT):
440+ number = event.key - K_0
441+ if number <= len(self._ordered_tabs) and number >= 1:
442+ self._current_tab = self._ordered_tabs[number-1]
443+
444+ # pass event to child widget.
445+ if hasattr(event, "pos"):
446+ internal_pos = [event.pos[0] - self.rect.x,
447+ event.pos[1] - self.rect.y]
448+ if self._tab_widget[self._current_tab].rect.collidepoint(internal_pos):
449+ self._tab_widget[self._current_tab]._event(event)
450+ else:
451+ self._tab_widget[self._current_tab]._event(event)
452+
453+ def update(self, time):
454+ # fill content area with user defined color
455+ self.image.fill(self._settings["col"],
456+ rect=Rect(0, 20, self.rect.w, self.rect.h))
457+
458+ # update content and blit into content area
459+ if self._current_tab:
460+ self._tab_widget[self._current_tab].update(time)
461+ self.image.blit(self._tab_widget[self._current_tab].image,
462+ self._tab_widget[self._current_tab].pos)
463+
464+ # draw content area border
465+ draw.rect(self.image, (200, 200, 200),
466+ Rect(0, 19, self.rect.w, self.rect.h - 19), 1)
467+
468+ # force transparency and Draw all tabs
469+ self.image.fill((255, 255, 255, 0), rect=Rect(0, 0, self.rect.w, 19))
470+
471+ selected_tab_number = 0
472+ selected_tab_pos = 0
473+ tab_pos = 0
474+ for i, tab in enumerate(self._ordered_tabs):
475+ if tab == self._current_tab:
476+ selected_tab_number = i
477+ selected_tab_pos = tab_pos
478+ tab_pos += self._tab_sizes[tab]
479+ else:
480+ self.image.blit(self._tab_images[tab]["back"], (tab_pos, 0))
481+ tab_pos += self._tab_sizes[tab]
482+
483+ self.image.blit(self._tab_images[self._current_tab]["front"],
484+ (selected_tab_pos, 0))
485+
486+ def _gradientline(self, surface, color, x, start_y, end_y):
487+ alpha_gradient = 255 / abs(start_y - end_y)
488+ alpha = 255
489+ for i in range(start_y - end_y):
490+ pygame.gfxdraw.pixel(surface, x, start_y - i,
491+ (color, color, color, alpha))
492+ alpha -= alpha_gradient
493+
494+ def rename_tab(self, pos, tab_name):
495+ if tab_name not in self._ordered_tabs:
496+ self._tab_widget[tab_name] = self._tab_widget[self._ordered_tabs[pos]]
497+ del self._tab_widget[self._ordered_tabs[pos]]
498+ del self._tab_sizes[self._ordered_tabs[pos]]
499+
500+ del self._tab_images[self._ordered_tabs[pos]]
501+ self._tab_images[tab_name] = {}
502+
503+ cut_tab_name, tab_width = self._cut_string(tab_name, self._settings["max_width"])
504+
505+ if tab_width < self._settings["min_width"]:
506+ tab_width = self._settings["min_width"]
507+
508+ self._tab_images[tab_name]["back"] =\
509+ pygame.Surface((tab_width + 1, 20))
510+ self._draw_back(self._tab_images[tab_name]["back"], (tab_width,
511+ 20))
512+
513+ self._tab_images[tab_name]["front"] =\
514+ pygame.Surface((tab_width + 1, 20))
515+ self._draw_front(self._tab_images[tab_name]["front"], (tab_width,
516+ 20))
517+
518+ tab_title =\
519+ self._settings["font"].render(cut_tab_name, True,
520+ self._settings["text_col"])
521+
522+ self._tab_images[tab_name]["back"].blit(tab_title, (5, 3))
523+ self._tab_images[tab_name]["front"].blit(tab_title, (5, 3))
524+
525+ self._tab_sizes[tab_name] = self._settings["max_width"]
526+
527+ self._ordered_tabs[pos] = tab_name
528+
529+ def get_child(self, pos):
530+ return self._tab_widget[self._ordered_tabs[pos]]
531+
532+ def get_tab_name(self, pos):
533+ return self._ordered_tabs[pos]
534+
535+ def get_child_pos(self, child):
536+ for i, w in enumerate(self._tab_widget.values()):
537+ if w is child:
538+ return i
539+ return -1
540+
541+ def get_tab_name_pos(self, tab_name):
542+ return self._ordered_tabs.index(tab_name)
543+
544+ def next_tab(self):
545+ next = ((self._ordered_tabs.index(self._current_tab) + 1) %
546+ len(self._ordered_tabs))
547+ self._current_tab = self._ordered_tabs[next]
548+
549+ def previous_tab(self):
550+ prev = ((self._ordered_tabs.index(self._current_tab) - 1) %
551+ len(self._ordered_tabs))
552+ self._current_tab = self._ordered_tabs[prev]
553+
554+ @property
555+ def current_tab(self):
556+ return self._ordered_tabs.index(self._current_tab)
557+
558+ @current_tab.setter
559+ def current_tab(self, pos):
560+ if pos >= 0 and pos < len(self._ordered_tabs):
561+ self._current_tab = self._ordered_tabs[pos]

Subscribers

People subscribed via source and target branches