Examples
The Sugar Toolkit GTK4 comes with several example activities that demonstrate how to use the various components of the toolkit.
Basic Activity
The most basic activity shows how to create a simple Sugar activity:
This example demonstrates how to create a simple Sugar activity using GTK4. It shows the basic structure and key features of a Sugar activity.
1"""Basic Sugar GTK4 Activity Example."""
2
3import sys
4import os
5
6sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
7
8import gi
9
10gi.require_version("Gtk", "4.0")
11from gi.repository import Gtk
12
13from sugar.activity import SimpleActivity
14from sugar.graphics.xocolor import XoColor
15
16
17class BasicExampleActivity(SimpleActivity):
18 """A basic example activity showing Sugar GTK4 features."""
19
20 def __init__(self):
21 super().__init__()
22 self.set_title("Basic Sugar GTK4 Example")
23
24 self._create_content()
25
26 self._show_color_info()
27
28 def _create_content(self):
29 """Create the main content area."""
30 main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
31 main_box.set_margin_start(20)
32 main_box.set_margin_end(20)
33 main_box.set_margin_top(20)
34 main_box.set_margin_bottom(20)
35
36 # Title
37 title = Gtk.Label()
38 title.set_markup("<big><b>Welcome to Sugar GTK4!</b></big>")
39 main_box.append(title)
40
41 # Description
42 desc = Gtk.Label(
43 label="This is a basic Sugar activity using GTK4.\n"
44 "It demonstrates the new toolkit features."
45 )
46 desc.set_justify(Gtk.Justification.CENTER)
47 main_box.append(desc)
48
49 # Color demo
50 color_frame = Gtk.Frame(label="XO Color Demo")
51 color_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
52 color_box.set_margin_start(10)
53 color_box.set_margin_end(10)
54 color_box.set_margin_top(10)
55 color_box.set_margin_bottom(10)
56
57 # Show current XO color
58 self.xo_color = XoColor()
59 self.color_info_label = Gtk.Label(
60 label=f"Current XO Color: {self.xo_color.to_string()}"
61 )
62 color_box.append(self.color_info_label)
63
64 # Color preview boxes
65 color_preview_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
66 color_preview_box.set_halign(Gtk.Align.CENTER)
67
68 # Stroke color box
69 self.stroke_box = Gtk.Box()
70 self.stroke_box.set_size_request(100, 50)
71 stroke_label = Gtk.Label(label="Stroke")
72 stroke_label.set_halign(Gtk.Align.CENTER)
73 stroke_label.set_valign(Gtk.Align.CENTER)
74 self.stroke_box.append(stroke_label)
75 color_preview_box.append(self.stroke_box)
76
77 # Fill color box
78 self.fill_box = Gtk.Box()
79 self.fill_box.set_size_request(100, 50)
80 fill_label = Gtk.Label(label="Fill")
81 fill_label.set_halign(Gtk.Align.CENTER)
82 fill_label.set_valign(Gtk.Align.CENTER)
83 self.fill_box.append(fill_label)
84 color_preview_box.append(self.fill_box)
85
86 color_box.append(color_preview_box)
87
88 same_color_label = Gtk.Label(label="Same Color (Stroke & Fill)")
89 same_color_label.set_halign(Gtk.Align.CENTER)
90 color_box.append(same_color_label)
91
92 self.same_color_area = Gtk.DrawingArea()
93 self.same_color_area.set_content_width(100)
94 self.same_color_area.set_content_height(50)
95 self.same_color_area.set_halign(Gtk.Align.CENTER)
96 self.same_color_area.set_valign(Gtk.Align.CENTER)
97 self.same_color_area.set_draw_func(self._draw_same_color_box)
98 color_box.append(self.same_color_area)
99
100 # Interact with the colors hahahaha
101 random_button = Gtk.Button(label="Get Random Color")
102 random_button.connect("clicked", self._on_random_color)
103 # setting up color as black
104 css_provider = Gtk.CssProvider()
105 css_provider.load_from_data(b"button { color: #000000; }")
106 random_button.get_style_context().add_provider(
107 css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER
108 )
109 color_box.append(random_button)
110
111 color_frame.set_child(color_box)
112 main_box.append(color_frame)
113
114 info_frame = Gtk.Frame(label="Activity Info")
115 info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
116 info_box.set_margin_start(10)
117 info_box.set_margin_end(10)
118 info_box.set_margin_top(10)
119 info_box.set_margin_bottom(10)
120
121 info_box.append(Gtk.Label(label=f"Activity ID: {self.get_id()[:8]}..."))
122 info_box.append(Gtk.Label(label=f"Title: {self.get_title()}"))
123 info_box.append(Gtk.Label(label=f"Active: {self.get_active()}"))
124
125 info_frame.set_child(info_box)
126 main_box.append(info_frame)
127
128 self.set_canvas(main_box)
129
130 def _show_color_info(self):
131 """Display color information in terminal."""
132 print(f"Activity Color: {self.xo_color.to_string()}")
133 print(f"Stroke: {self.xo_color.get_stroke_color()}")
134 print(f"Fill: {self.xo_color.get_fill_color()}")
135
136 rgba = self.xo_color.to_rgba_tuple()
137 print(f"RGBA - Stroke: {rgba[0]}, Fill: {rgba[1]}")
138
139 def _draw_same_color_box(self, area, cr, width, height):
140 """Draw a rectangle filled with fill color and stroked with stroke color."""
141 fill_rgba = self.xo_color.get_fill_color()
142 stroke_rgba = self.xo_color.get_stroke_color()
143 # Convert hex color to RGB
144 fill_rgb = [int(fill_rgba[i : i + 2], 16) / 255.0 for i in (1, 3, 5)]
145 stroke_rgb = [int(stroke_rgba[i : i + 2], 16) / 255.0 for i in (1, 3, 5)]
146 cr.set_source_rgb(*fill_rgb)
147 cr.rectangle(5, 5, width - 10, height - 10)
148 cr.fill_preserve()
149 cr.set_line_width(4)
150 cr.set_source_rgb(*stroke_rgb)
151 cr.stroke()
152
153 def _on_random_color(self, button):
154 """Handle random color button click."""
155 self.xo_color = XoColor.get_random_color()
156 self.color_info_label.set_text(f"Current XO Color: {self.xo_color.to_string()}")
157 print(f"New random color: {self.xo_color.to_string()}")
158 self.same_color_area.queue_draw()
159
160
161def main():
162 """Run the basic example activity."""
163 app = Gtk.Application(application_id="org.sugarlabs.BasicExample")
164
165 def on_activate(app):
166 activity = BasicExampleActivity()
167 app.add_window(activity)
168 activity.present()
169
170 app.connect("activate", on_activate)
171 return app.run(sys.argv)
172
173
174if __name__ == "__main__":
175 main()
Creative Studio Activity Example
This example demonstrates an advanced Sugar creative activity with a custom toolbar, support for keyboard shortcuts (accelerators), file handling, and other features.
1"""
2Creative Studio Activity Example
3================================
4
5This example demonstrates an advanced Sugar creative activity featuring:
6- Multiple creative tools (drawing, text, shapes)
7- Keyboard shortcuts (Ctrl+Z/Y/S)
8- Color selection with visual feedback
9- File operations with auto-save
10- Preview generation
11- Flexible creative workspace
12"""
13
14import os
15import sys
16import logging
17import json
18import tempfile
19from datetime import datetime
20
21sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
22
23# Set up mock environment if not running in Sugar
24if 'SUGAR_BUNDLE_ID' not in os.environ:
25 os.environ['SUGAR_BUNDLE_ID'] = 'org.sugarlabs.CreativeStudio'
26 os.environ['SUGAR_BUNDLE_NAME'] = 'Creative Studio'
27 os.environ['SUGAR_BUNDLE_PATH'] = os.path.dirname(__file__)
28 os.environ['SUGAR_ACTIVITY_ROOT'] = '/tmp/creative_studio'
29
30import gi
31
32gi.require_version("Gtk", "4.0")
33gi.require_version("Gdk", "4.0")
34
35from gi.repository import Gtk, Gio, Gdk
36
37from sugar.activity.activity import Activity
38from sugar.activity.activityhandle import ActivityHandle
39from sugar.graphics.toolbarbox import ToolbarBox
40from sugar.activity.widgets import ActivityToolbarButton, StopButton
41
42
43class CreativeCanvas(Gtk.DrawingArea):
44 """A versatile creative canvas supporting multiple tools and media."""
45
46 def __init__(self):
47 super().__init__()
48 self.set_size_request(800, 600)
49 self.set_draw_func(self._draw_func)
50 self.set_focusable(True)
51
52 # Set up gesture for drawing
53 self._gesture = Gtk.GestureDrag()
54 self._gesture.connect('drag-begin', self._drag_begin_cb)
55 self._gesture.connect('drag-update', self._drag_update_cb)
56 self._gesture.connect('drag-end', self._drag_end_cb)
57 self.add_controller(self._gesture)
58
59 # Set up key controller for keyboard shortcuts
60 self._key_controller = Gtk.EventControllerKey()
61 self._key_controller.connect('key-pressed', self._key_pressed_cb)
62 self.add_controller(self._key_controller)
63
64 # Make sure canvas can receive focus for keyboard events
65 self.set_can_focus(True)
66
67 self._elements = [] # All creative elements (strokes, text, shapes, etc.)
68 self._current_stroke = []
69
70 self._current_color = (0, 0, 0) # Black
71 self._current_brush_size = 3
72 self._current_tool = "brush" # brush, eraser, line, rectangle, circle, spray
73 self._current_fill = False # Whether shapes should be filled
74
75 # Undo/Redo stacks
76 self._undo_stack = []
77 self._redo_stack = []
78
79 self._on_change_callback = None
80
81 def set_change_callback(self, callback):
82 """Set callback function to call when canvas changes."""
83 self._on_change_callback = callback
84
85 def _notify_change(self):
86 """Notify that canvas has changed."""
87 if self._on_change_callback:
88 self._on_change_callback()
89
90 def _key_pressed_cb(self, controller, keyval, keycode, state):
91 """Handle keyboard shortcuts."""
92 # Check for Ctrl key
93 if state & Gdk.ModifierType.CONTROL_MASK:
94 if keyval == Gdk.KEY_z or keyval == Gdk.KEY_Z:
95 if self.undo():
96 print("Undo triggered by keyboard")
97 return True
98 elif keyval == Gdk.KEY_y or keyval == Gdk.KEY_Y:
99 if self.redo():
100 print("Redo triggered by keyboard")
101 return True
102 elif keyval == Gdk.KEY_s or keyval == Gdk.KEY_S:
103 # Trigger save through callback
104 if hasattr(self, '_save_callback') and self._save_callback:
105 self._save_callback()
106 print("Save triggered by keyboard")
107 return True
108 return False
109
110 def set_save_callback(self, callback):
111 """Set callback for save shortcut."""
112 self._save_callback = callback
113
114 def _draw_func(self, area, cr, width, height, user_data=None):
115 """Draw the canvas content."""
116 cr.set_source_rgb(1, 1, 1) # White background
117 cr.paint()
118
119 for element in self._elements:
120 self._draw_element(cr, element)
121
122 # Draw current stroke being created
123 if self._current_stroke:
124 self._draw_current_stroke(cr)
125
126 def _draw_element(self, cr, element):
127 element_type = element.get('type', 'stroke')
128 color = element.get('color', (0, 0, 0))
129 size = element.get('size', 3)
130 points = element.get('points', [])
131
132 cr.set_source_rgb(*color)
133 cr.set_line_width(size)
134
135 if element_type == 'brush':
136 if len(points) > 1:
137 cr.move_to(points[0][0], points[0][1])
138 for point in points[1:]:
139 cr.line_to(point[0], point[1])
140 cr.stroke()
141
142 elif element_type == 'eraser':
143 # Eraser removes content by painting white with a thicker line
144 cr.set_source_rgb(1, 1, 1) # White for eraser
145 cr.set_line_width(size * 3) # Make eraser more visible/effective
146 cr.set_line_cap(1) # Round line caps
147 cr.set_line_join(1) # Round line joins
148 if len(points) > 1:
149 cr.move_to(points[0][0], points[0][1])
150 for point in points[1:]:
151 cr.line_to(point[0], point[1])
152 cr.stroke()
153
154 elif element_type == 'line':
155 if len(points) >= 2:
156 cr.move_to(points[0][0], points[0][1])
157 cr.line_to(points[-1][0], points[-1][1])
158 cr.stroke()
159
160 elif element_type == 'rectangle':
161 if len(points) >= 2:
162 x1, y1 = points[0]
163 x2, y2 = points[-1]
164 width = abs(x2 - x1)
165 height = abs(y2 - y1)
166 x = min(x1, x2)
167 y = min(y1, y2)
168
169 if element.get('fill', False):
170 cr.rectangle(x, y, width, height)
171 cr.fill()
172 else:
173 cr.rectangle(x, y, width, height)
174 cr.stroke()
175
176 elif element_type == 'circle':
177 if len(points) >= 2:
178 x1, y1 = points[0]
179 x2, y2 = points[-1]
180 radius = ((x2-x1)**2 + (y2-y1)**2)**0.5
181
182 if element.get('fill', False):
183 cr.arc(x1, y1, radius, 0, 2 * 3.14159)
184 cr.fill()
185 else:
186 cr.arc(x1, y1, radius, 0, 2 * 3.14159)
187 cr.stroke()
188
189 def _draw_current_stroke(self, cr):
190 """Draw the stroke currently being created."""
191 cr.set_source_rgb(*self._current_color)
192 cr.set_line_width(self._current_brush_size)
193
194 if self._current_tool == 'eraser':
195 cr.set_source_rgb(1, 1, 1)
196 cr.set_line_width(self._current_brush_size * 3)
197 cr.set_line_cap(1) # Round line caps
198 cr.set_line_join(1) # Round line joins
199
200 if self._current_tool in ['brush', 'eraser'] and len(self._current_stroke) > 1:
201 cr.move_to(self._current_stroke[0][0], self._current_stroke[0][1])
202 for point in self._current_stroke[1:]:
203 cr.line_to(point[0], point[1])
204 cr.stroke()
205 elif self._current_tool == 'line' and len(self._current_stroke) >= 2:
206 cr.move_to(self._current_stroke[0][0], self._current_stroke[0][1])
207 cr.line_to(self._current_stroke[-1][0], self._current_stroke[-1][1])
208 cr.stroke()
209 elif self._current_tool == 'rectangle' and len(self._current_stroke) >= 2:
210 x1, y1 = self._current_stroke[0]
211 x2, y2 = self._current_stroke[-1]
212 width = abs(x2 - x1)
213 height = abs(y2 - y1)
214 x = min(x1, x2)
215 y = min(y1, y2)
216 cr.rectangle(x, y, width, height)
217 if self._current_fill:
218 cr.fill()
219 else:
220 cr.stroke()
221 elif self._current_tool == 'circle' and len(self._current_stroke) >= 2:
222 x1, y1 = self._current_stroke[0]
223 x2, y2 = self._current_stroke[-1]
224 radius = ((x2-x1)**2 + (y2-y1)**2)**0.5
225 cr.arc(x1, y1, radius, 0, 2 * 3.14159)
226 if self._current_fill:
227 cr.fill()
228 else:
229 cr.stroke()
230
231 def _drag_begin_cb(self, gesture, x, y):
232 """Start a new creative action."""
233 self._current_stroke = [(x, y)]
234 self.queue_draw()
235
236 def _drag_update_cb(self, gesture, x, y):
237 """Continue the current action."""
238 result = gesture.get_start_point()
239 if len(result) == 3:
240 valid, start_x, start_y = result
241 if valid:
242 current_x = start_x + x
243 current_y = start_y + y
244 if self._current_tool in ['brush', 'eraser']:
245 # For freehand tools, add all points
246 self._current_stroke.append((current_x, current_y))
247 else:
248 # For shape tools, only keep start and current point
249 if len(self._current_stroke) == 1:
250 self._current_stroke.append((current_x, current_y))
251 else:
252 self._current_stroke[-1] = (current_x, current_y)
253 self.queue_draw()
254
255 def _drag_end_cb(self, gesture, x, y):
256 """Finish the current action."""
257 if self._current_stroke:
258 self._save_state()
259
260 element_data = {
261 'type': self._current_tool,
262 'points': self._current_stroke[:],
263 'color': self._current_color,
264 'size': self._current_brush_size,
265 'fill': self._current_fill,
266 'timestamp': datetime.now().isoformat()
267 }
268
269 self._elements.append(element_data)
270 self._current_stroke = []
271
272 # Clear redo stack
273 self._redo_stack = []
274
275 self.queue_draw()
276 self._notify_change()
277
278 def _save_state(self):
279 """Save current state for undo."""
280 state = [element.copy() for element in self._elements]
281 self._undo_stack.append(state)
282 # Limit undo stack size
283 if len(self._undo_stack) > 50:
284 self._undo_stack.pop(0)
285
286 def set_color(self, color):
287 """Set the current color."""
288 self._current_color = color
289
290 def set_brush_size(self, size):
291 """Set the brush size."""
292 self._current_brush_size = size
293
294 def set_tool(self, tool):
295 """Set the current tool."""
296 self._current_tool = tool
297
298 def set_fill_mode(self, fill):
299 """Set whether shapes should be filled."""
300 self._current_fill = fill
301
302 def clear_canvas(self):
303 """Clear all content."""
304 self._save_state()
305 self._elements = []
306 self._current_stroke = []
307 self._redo_stack = []
308 self.queue_draw()
309 self._notify_change()
310
311 def undo(self):
312 """Undo last action."""
313 if self._undo_stack:
314 # Save current state to redo stack
315 current_state = [element.copy() for element in self._elements]
316 self._redo_stack.append(current_state)
317
318 # Restore previous state
319 self._elements = self._undo_stack.pop()
320 self.queue_draw()
321 self._notify_change()
322 return True
323 return False
324
325 def redo(self):
326 """Redo last undone action."""
327 if self._redo_stack:
328 # Save current state to undo stack
329 current_state = [element.copy() for element in self._elements]
330 self._undo_stack.append(current_state)
331
332 # Restore redone state
333 self._elements = self._redo_stack.pop()
334 self.queue_draw()
335 self._notify_change()
336 return True
337 return False
338
339 def get_canvas_data(self):
340 """Get all canvas data for saving."""
341 return {
342 'elements': self._elements,
343 'canvas_size': (self.get_width(), self.get_height()),
344 'version': '2.0'
345 }
346
347 def set_canvas_data(self, data):
348 """Set canvas data from saved file."""
349 self._elements = data.get('elements', [])
350 self._current_stroke = []
351 self._undo_stack = []
352 self._redo_stack = []
353 self.queue_draw()
354
355
356class CreativeStudioActivity(Activity):
357 """An advanced creative studio Sugar activity."""
358
359 def __init__(self, handle=None, application=None):
360 """Initialize the activity."""
361 # Create handle if not provided (for testing)
362 if handle is None:
363 handle = ActivityHandle('creative-studio-123')
364
365 Activity.__init__(self, handle, application=application)
366
367 self._initialize_document_data()
368
369 self._current_tool = "brush"
370 self._canvas_size = (800, 600)
371 self._preview_image_path = None
372 self._current_color = (0, 0, 0)
373 self._has_unsaved_changes = False
374
375 self._color_buttons = {}
376
377 self._setup_ui()
378
379 self.set_title("Creative Studio")
380 self.set_default_size(1200, 800)
381
382 def _initialize_document_data(self):
383 """Initialize document metadata."""
384 self._document_data = {
385 'created': datetime.now().isoformat(),
386 'modified': datetime.now().isoformat(),
387 'version': '2.0',
388 'author': 'Creative User',
389 'title': 'Untitled Creation',
390 'element_count': 0,
391 'last_tool': 'brush'
392 }
393
394 def _setup_ui(self):
395 """Set up the user interface."""
396 self._create_toolbar()
397 self._create_canvas()
398
399 def _create_toolbar(self):
400 """Create the activity toolbar with creative tools."""
401 toolbar_box = ToolbarBox()
402
403 # Activity button
404 activity_button = ActivityToolbarButton(self)
405 toolbar_box.toolbar.append(activity_button)
406
407 # Separator
408 separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
409 toolbar_box.toolbar.append(separator)
410
411 # Tool selection
412 tools_label = Gtk.Label()
413 tools_label.set_markup("<span color='white' weight='bold'>Tools:</span>")
414 toolbar_box.toolbar.append(tools_label)
415
416 # Brush tool
417 brush_btn = Gtk.ToggleButton()
418 brush_btn.set_label("Brush")
419 brush_btn.set_active(True)
420 brush_btn.connect('toggled', lambda btn: self._tool_selected(btn, 'brush'))
421 toolbar_box.toolbar.append(brush_btn)
422 self._brush_btn = brush_btn
423
424 # Eraser tool
425 eraser_btn = Gtk.ToggleButton()
426 eraser_btn.set_label("Eraser")
427 eraser_btn.connect('toggled', lambda btn: self._tool_selected(btn, 'eraser'))
428 toolbar_box.toolbar.append(eraser_btn)
429 self._eraser_btn = eraser_btn
430
431 # Line tool
432 line_btn = Gtk.ToggleButton()
433 line_btn.set_label("Line")
434 line_btn.connect('toggled', lambda btn: self._tool_selected(btn, 'line'))
435 toolbar_box.toolbar.append(line_btn)
436 self._line_btn = line_btn
437
438 # Rectangle tool
439 rect_btn = Gtk.ToggleButton()
440 rect_btn.set_label("Rectangle")
441 rect_btn.connect('toggled', lambda btn: self._tool_selected(btn, 'rectangle'))
442 toolbar_box.toolbar.append(rect_btn)
443 self._rect_btn = rect_btn
444
445 # Circle tool
446 circle_btn = Gtk.ToggleButton()
447 circle_btn.set_label("Circle")
448 circle_btn.connect('toggled', lambda btn: self._tool_selected(btn, 'circle'))
449 toolbar_box.toolbar.append(circle_btn)
450 self._circle_btn = circle_btn
451
452 # Separator
453 separator2 = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
454 toolbar_box.toolbar.append(separator2)
455
456 # Fill mode toggle
457 fill_btn = Gtk.ToggleButton()
458 fill_btn.set_label("Fill Mode")
459 fill_btn.connect('toggled', self._fill_mode_toggled)
460 toolbar_box.toolbar.append(fill_btn)
461 self._fill_btn = fill_btn
462
463 # Brush size
464 size_label = Gtk.Label()
465 size_label.set_markup("<span color='white' weight='bold'>Size:</span>")
466 toolbar_box.toolbar.append(size_label)
467
468 size_adjustment = Gtk.Adjustment(value=3, lower=1, upper=50, step_increment=1)
469 size_spin = Gtk.SpinButton()
470 size_spin.set_adjustment(size_adjustment)
471 size_spin.connect('value-changed', self._brush_size_changed)
472 toolbar_box.toolbar.append(size_spin)
473
474 # Separator
475 separator3 = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
476 toolbar_box.toolbar.append(separator3)
477
478 colors_label = Gtk.Label()
479 colors_label.set_markup("<span color='white' weight='bold'>Colors:</span>")
480 toolbar_box.toolbar.append(colors_label)
481
482 colors = [
483 ("Black", (0, 0, 0)),
484 ("Red", (1, 0, 0)),
485 ("Blue", (0, 0, 1)),
486 ("Green", (0, 0.8, 0)),
487 ("Yellow", (1, 1, 0)),
488 ("Purple", (0.8, 0, 0.8))
489 ]
490
491 for color_name, color_value in colors:
492 color_btn = Gtk.Button()
493 color_btn.set_tooltip_text(f"Select {color_name}")
494
495 color_box = Gtk.Box()
496 color_box.set_orientation(Gtk.Orientation.VERTICAL)
497 color_box.set_spacing(2)
498
499 # Create colored rectangle
500 color_area = Gtk.DrawingArea()
501 color_area.set_size_request(60, 20)
502
503 def draw_color(area, cr, width, height, color_val=color_value):
504 cr.set_source_rgb(*color_val)
505 cr.paint()
506 cr.set_source_rgb(0, 0, 0)
507 cr.set_line_width(1)
508 cr.rectangle(0.5, 0.5, width-1, height-1)
509 cr.stroke()
510
511 color_area.set_draw_func(draw_color)
512
513 color_label = Gtk.Label()
514 color_label.set_markup(f"<span color='black' size='small' weight='bold'>{color_name}</span>")
515
516 color_box.append(color_area)
517 color_box.append(color_label)
518 color_btn.set_child(color_box)
519
520 css_provider = Gtk.CssProvider()
521 css = "button { background-color: #2a2a2a; border: 1px solid #555; padding: 4px; }"
522 css_provider.load_from_data(css.encode())
523 color_btn.get_style_context().add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
524
525 color_btn.connect('clicked', lambda btn, c=color_value, name=color_name: self._color_selected(c, name))
526 toolbar_box.toolbar.append(color_btn)
527 self._color_buttons[color_value] = color_btn
528
529 self._highlight_color_button((0, 0, 0))
530
531 # Set up application accelerators for keyboard shortcuts
532 self._setup_accelerators()
533
534 separator4 = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
535 toolbar_box.toolbar.append(separator4)
536
537 # Action buttons
538 # Undo
539 undo_btn = Gtk.Button()
540 undo_btn.set_label("Undo")
541 undo_btn.connect('clicked', lambda btn: self._undo_action())
542 toolbar_box.toolbar.append(undo_btn)
543
544 # Redo
545 redo_btn = Gtk.Button()
546 redo_btn.set_label("Redo")
547 redo_btn.connect('clicked', lambda btn: self._redo_action())
548 toolbar_box.toolbar.append(redo_btn)
549
550 # Clear
551 clear_btn = Gtk.Button()
552 clear_btn.set_label("Clear")
553 clear_btn.connect('clicked', lambda btn: self.clear_canvas())
554 toolbar_box.toolbar.append(clear_btn)
555
556 # Separator
557 separator5 = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
558 toolbar_box.toolbar.append(separator5)
559
560 # Save
561 save_btn = Gtk.Button()
562 save_btn.set_label("Save")
563 save_btn.connect('clicked', lambda btn: self.save_creation())
564 toolbar_box.toolbar.append(save_btn)
565
566 # Preview
567 preview_btn = Gtk.Button()
568 preview_btn.set_label("Preview")
569 preview_btn.connect('clicked', lambda btn: self.show_preview())
570 toolbar_box.toolbar.append(preview_btn)
571
572 # Spacer
573 spacer = Gtk.Box()
574 spacer.set_hexpand(True)
575 toolbar_box.toolbar.append(spacer)
576
577 # Stop button
578 stop_button = StopButton(self)
579 toolbar_box.toolbar.append(stop_button)
580
581 self.set_toolbar_box(toolbar_box)
582
583 def _highlight_color_button(self, color):
584 """Highlight the selected color button."""
585 for btn in self._color_buttons.values():
586 btn.remove_css_class("suggested-action")
587
588 if color in self._color_buttons:
589 self._color_buttons[color].add_css_class("suggested-action")
590
591 def _tool_selected(self, button, tool):
592 """Handle tool selection."""
593 if button.get_active():
594 # Deactivate other tool buttons
595 tool_buttons = [self._brush_btn, self._eraser_btn, self._line_btn,
596 self._rect_btn, self._circle_btn]
597 for btn in tool_buttons:
598 if btn != button:
599 btn.set_active(False)
600
601 self._creative_canvas.set_tool(tool)
602 self._current_tool = tool
603 self._status_label.set_text(f"Selected tool: {tool.title()}")
604
605 def _fill_mode_toggled(self, button):
606 """Handle fill mode toggle."""
607 fill_mode = button.get_active()
608 self._creative_canvas.set_fill_mode(fill_mode)
609 mode_text = "Fill" if fill_mode else "Outline"
610 self._status_label.set_text(f"Shape mode: {mode_text}")
611
612 def _brush_size_changed(self, spin_button):
613 """Handle brush size change."""
614 size = int(spin_button.get_value())
615 self._creative_canvas.set_brush_size(size)
616 self._status_label.set_text(f"Brush size: {size}")
617
618 def _color_selected(self, color, color_name=None):
619 """Handle color selection."""
620 self._creative_canvas.set_color(color)
621 self._current_color = color
622 self._highlight_color_button(color)
623
624 if color_name is None:
625 color_names = {
626 (0, 0, 0): "Black",
627 (1, 0, 0): "Red",
628 (0, 0, 1): "Blue",
629 (0, 0.8, 0): "Green",
630 (1, 1, 0): "Yellow",
631 (0.8, 0, 0.8): "Purple"
632 }
633 color_name = color_names.get(color, "Custom")
634
635 self._status_label.set_text(f"Selected color: {color_name}")
636
637 # Give focus back to canvas for keyboard shortcuts
638 self._creative_canvas.grab_focus()
639
640 def _undo_action(self):
641 """Handle undo action."""
642 if self._creative_canvas.undo():
643 self._status_label.set_text("Undid last action")
644 self._has_unsaved_changes = True
645 else:
646 self._status_label.set_text("Nothing to undo")
647
648 def _redo_action(self):
649 """Handle redo action."""
650 if self._creative_canvas.redo():
651 self._status_label.set_text("Redid last action")
652 self._has_unsaved_changes = True
653 else:
654 self._status_label.set_text("Nothing to redo")
655
656 def _on_canvas_change(self):
657 """Called when canvas content changes."""
658 self._has_unsaved_changes = True
659 self._update_doc_info()
660
661 def _setup_accelerators(self):
662 """Set up application-level keyboard accelerators."""
663 # Create event controller for window-level shortcuts
664 key_controller = Gtk.EventControllerKey()
665
666 def on_key_pressed(controller, keyval, keycode, state):
667 if state & Gdk.ModifierType.CONTROL_MASK:
668 if keyval == Gdk.KEY_z or keyval == Gdk.KEY_Z:
669 self._undo_action()
670 return True
671 elif keyval == Gdk.KEY_y or keyval == Gdk.KEY_Y:
672 self._redo_action()
673 return True
674 elif keyval == Gdk.KEY_s or keyval == Gdk.KEY_S:
675 self.save_creation()
676 return True
677 return False
678
679 key_controller.connect('key-pressed', on_key_pressed)
680 self.add_controller(key_controller)
681
682 def _create_canvas(self):
683 """Create the main canvas area."""
684 main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
685 main_box.set_margin_top(12)
686 main_box.set_margin_bottom(12)
687 main_box.set_margin_start(12)
688 main_box.set_margin_end(12)
689
690 # Title
691 title_label = Gtk.Label()
692 title_label.set_markup("<span size='large' weight='bold'>Creative Studio</span>")
693 title_label.set_halign(Gtk.Align.CENTER)
694 main_box.append(title_label)
695
696 # Status area
697 self._status_label = Gtk.Label()
698 self._status_label.set_text("Welcome to Creative Studio! Select a tool and start creating.")
699 self._status_label.set_halign(Gtk.Align.CENTER)
700 self._status_label.set_wrap(True)
701 main_box.append(self._status_label)
702
703 # Creative area
704 canvas_frame = Gtk.Frame()
705 canvas_frame.set_label("Creative Canvas")
706
707 self._creative_canvas = CreativeCanvas()
708 self._creative_canvas.set_change_callback(self._on_canvas_change)
709 self._creative_canvas.set_save_callback(self.save_creation)
710
711 # Make canvas focusable and give it initial focus
712 self._creative_canvas.set_can_focus(True)
713 self._creative_canvas.grab_focus()
714
715 canvas_frame.set_child(self._creative_canvas)
716 main_box.append(canvas_frame)
717
718 # Info area
719 info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
720
721 doc_frame = Gtk.Frame()
722 doc_frame.set_label("Project Info")
723
724 self._doc_info_label = Gtk.Label()
725 self._update_doc_info()
726 self._doc_info_label.set_margin_top(6)
727 self._doc_info_label.set_margin_bottom(6)
728 self._doc_info_label.set_margin_start(6)
729 self._doc_info_label.set_margin_end(6)
730
731 doc_frame.set_child(self._doc_info_label)
732 info_box.append(doc_frame)
733
734 main_box.append(info_box)
735
736 self.set_canvas(main_box)
737
738 def _update_doc_info(self):
739 """Update document info display."""
740 if hasattr(self, '_doc_info_label'):
741 element_count = len(self._creative_canvas._elements) if hasattr(self, '_creative_canvas') else 0
742 save_status = "Unsaved changes" if self._has_unsaved_changes else "Saved"
743
744 text = f"Created: {self._document_data['created'][:19]}\n"
745 text += f"Modified: {self._document_data['modified'][:19]}\n"
746 text += f"Elements: {element_count}\n"
747 text += f"Status: {save_status}\n"
748 text += f"Current Tool: {self._current_tool.title()}"
749 self._doc_info_label.set_text(text)
750
751 def clear_canvas(self):
752 """Clear the creative canvas."""
753 self._creative_canvas.clear_canvas()
754 self._document_data['modified'] = datetime.now().isoformat()
755 self._update_doc_info()
756 self._status_label.set_text("Canvas cleared")
757
758 def save_creation(self):
759 """Save the current creation."""
760 try:
761 self._document_data['modified'] = datetime.now().isoformat()
762 self._document_data['element_count'] = len(self._creative_canvas._elements)
763 self._document_data['last_tool'] = self._current_tool
764
765 # In a real Sugar activity, this would use the activity's write_file method
766 # For demo purposes, we'll save to a temp location
767 save_path = "/tmp/creative_studio_save.json"
768
769 canvas_data = self._creative_canvas.get_canvas_data()
770 data = {
771 'document_data': self._document_data,
772 'canvas_data': canvas_data,
773 'current_tool': self._current_tool,
774 'current_color': self._current_color,
775 'saved_at': datetime.now().isoformat()
776 }
777
778 with open(save_path, 'w') as f:
779 json.dump(data, f, indent=2)
780
781 self._has_unsaved_changes = False
782 self._update_doc_info()
783 self._status_label.set_text(f"Creation saved successfully!")
784
785 except Exception as e:
786 logging.error(f"Error saving creation: {e}")
787 self._status_label.set_text(f"Error saving: {e}")
788
789 def show_preview(self):
790 """Show a preview of the current creation."""
791 try:
792 preview_data = self.get_preview()
793 if preview_data:
794 # Save preview to temp file
795 preview_path = "/tmp/creative_studio_preview.png"
796 with open(preview_path, 'wb') as f:
797 f.write(preview_data)
798
799 # Show preview dialog
800 self._show_preview_dialog(preview_path)
801 self._status_label.set_text("Preview shown")
802 else:
803 self._status_label.set_text("No content to preview")
804
805 except Exception as e:
806 logging.error(f"Error showing preview: {e}")
807 self._status_label.set_text(f"Error showing preview: {e}")
808
809 def _show_preview_dialog(self, image_path):
810 """Show preview image in a dialog."""
811 dialog = Gtk.Dialog()
812 dialog.set_title("Creation Preview")
813 dialog.set_transient_for(self)
814 dialog.set_modal(True)
815 dialog.set_default_size(850, 650)
816
817 dialog.add_button("Close", Gtk.ResponseType.CLOSE)
818
819 try:
820 # Create a scrolled window for the image
821 scrolled = Gtk.ScrolledWindow()
822 scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
823 scrolled.set_margin_top(10)
824 scrolled.set_margin_bottom(10)
825 scrolled.set_margin_start(10)
826 scrolled.set_margin_end(10)
827
828 image = Gtk.Image()
829 image.set_from_file(image_path)
830
831 scrolled.set_child(image)
832 dialog.get_content_area().append(scrolled)
833 dialog.present()
834
835 def on_response(dialog, response_id):
836 dialog.destroy()
837
838 dialog.connect('response', on_response)
839
840 except Exception as e:
841 logging.error(f"Error loading preview image: {e}")
842 dialog.destroy()
843
844 def get_preview(self):
845 """Generate a preview image of the current creation."""
846 try:
847 import cairo
848
849 preview_width, preview_height = 1200, 800
850 surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, preview_width, preview_height)
851 cr = cairo.Context(surface)
852
853 cr.set_source_rgb(1, 1, 1)
854 cr.paint()
855
856 cr.set_source_rgb(0.8, 0.8, 0.8)
857 cr.set_line_width(2)
858 cr.rectangle(2, 2, preview_width - 4, preview_height - 4)
859 cr.stroke()
860
861 if hasattr(self, '_creative_canvas') and self._creative_canvas._elements:
862 # Scale the creation to fit the preview
863 canvas_width, canvas_height = 800, 600
864 scale_x = (preview_width - 20) / canvas_width
865 scale_y = (preview_height - 20) / canvas_height
866 scale = min(scale_x, scale_y)
867
868 cr.save()
869 cr.translate(10, 10)
870 cr.scale(scale, scale)
871
872 # Draw all elements
873 for element in self._creative_canvas._elements:
874 self._creative_canvas._draw_element(cr, element)
875
876 cr.restore()
877 else:
878 # No content, show placeholder
879 cr.set_source_rgb(0.5, 0.5, 0.5)
880 cr.select_font_face("Sans", 0, 0)
881 cr.set_font_size(24)
882
883 text = "Creative Studio"
884 text_extents = cr.text_extents(text)
885 x = (preview_width - text_extents.width) / 2
886 y = preview_height / 2 - 10
887
888 cr.move_to(x, y)
889 cr.show_text(text)
890
891 cr.set_font_size(16)
892 text2 = "Create something amazing!"
893 text_extents2 = cr.text_extents(text2)
894 x2 = (preview_width - text_extents2.width) / 2
895 y2 = y + 40
896
897 cr.move_to(x2, y2)
898 cr.show_text(text2)
899
900 # Convert to PNG
901 import io
902 preview_str = io.BytesIO()
903 surface.write_to_png(preview_str)
904 return preview_str.getvalue()
905
906 except Exception as e:
907 logging.error(f"Error generating preview: {e}")
908 return None
909
910 def read_file(self, file_path):
911 """Read creation data from file."""
912 try:
913 with open(file_path, 'r') as f:
914 data = json.load(f)
915
916 self._document_data = data.get('document_data', self._document_data)
917 canvas_data = data.get('canvas_data', {})
918 self._creative_canvas.set_canvas_data(canvas_data)
919
920 self._current_tool = data.get('current_tool', 'brush')
921 self._current_color = tuple(data.get('current_color', (0, 0, 0)))
922
923 self._has_unsaved_changes = False
924 self._update_doc_info()
925 self._status_label.set_text("Creation loaded successfully")
926
927 except Exception as e:
928 logging.error(f"Error reading file: {e}")
929 self._status_label.set_text(f"Error loading creation: {e}")
930
931 def write_file(self, file_path):
932 """Write creation data to file."""
933 try:
934 self._document_data['modified'] = datetime.now().isoformat()
935 self._document_data['element_count'] = len(self._creative_canvas._elements)
936
937 canvas_data = self._creative_canvas.get_canvas_data()
938 data = {
939 'document_data': self._document_data,
940 'canvas_data': canvas_data,
941 'current_tool': self._current_tool,
942 'current_color': self._current_color,
943 'activity_id': self.get_id(),
944 'bundle_id': self.get_bundle_id(),
945 'saved_at': datetime.now().isoformat()
946 }
947
948 with open(file_path, 'w') as f:
949 json.dump(data, f, indent=2)
950
951 self._has_unsaved_changes = False
952 logging.info(f"Creative studio data saved to {file_path}")
953
954 except Exception as e:
955 logging.error(f"Error writing file: {e}")
956 raise
957
958 def can_close(self):
959 """Check if the activity can be closed."""
960 return True
961
962
963class CreativeStudioApplication(Gtk.Application):
964 """Application wrapper for the creative studio activity."""
965
966 def __init__(self):
967 super().__init__(application_id='org.sugarlabs.CreativeStudio',
968 flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
969 self.activity = None
970
971 def do_activate(self):
972 """Activate the application."""
973 if not self.activity:
974 handle = ActivityHandle('creative-studio-123')
975 self.activity = CreativeStudioActivity(handle, application=self)
976 self.activity.present()
977
978
979def main():
980 """Main entry point."""
981 logging.basicConfig(level=logging.DEBUG)
982 app = CreativeStudioApplication()
983 return app.run(sys.argv)
984
985
986if __name__ == "__main__":
987 main()
Hello_World_Dodge Example
A simple dodging ball game made with new toolkit.
1"""
2Hello World Dodge! - Animated Game Demo for sugar-toolkit-gtk4
3
4- Move the "Hello World!" ball with arrow keys, WASD, or buttons.
5- Ball moves smoothly, bounces off walls (increasing speed), and changes color.
6- Avoid obstacles, reach the goal to score!
7- Uses: Toolbox, ToolButton, Icon, XoColor, style
8
9"""
10
11import sys
12import os
13import random
14import math
15
16sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
17
18import gi
19gi.require_version("Gtk", "4.0")
20gi.require_version("Gdk", "4.0")
21from gi.repository import Gtk, Gdk, GLib
22
23from sugar.activity import SimpleActivity
24from sugar.graphics.toolbox import Toolbox
25from sugar.graphics.toolbutton import ToolButton
26from sugar.graphics.icon import Icon
27from sugar.graphics.xocolor import XoColor
28from sugar.graphics import style
29
30BALL_RADIUS = 28
31GOAL_RADIUS = 20
32OBSTACLE_RADIUS = 22
33BALL_INIT_SPEED = 3.0
34BALL_MAX_SPEED = 50.0
35BALL_SPEED_INC = 0.7
36OBSTACLE_COUNT = 3
37
38class HelloWorldDodgeActivity(SimpleActivity):
39 """Animated Hello World Dodge Game."""
40
41 def __init__(self):
42 super().__init__()
43 self.set_title("Hello World Dodge!")
44 self._create_content()
45
46 def _create_content(self):
47 css_provider = Gtk.CssProvider()
48 css_provider.load_from_data(b"""
49 * { color: #000000; }
50 .game-btn {
51 background: #e0e0e0;
52 border-radius: 16px;
53 border: 2px solid #888;
54 padding: 8px 16px;
55 margin: 2px;
56 transition: background 150ms, border-color 150ms;
57 }
58 .game-btn:hover {
59 background: #b0e0ff;
60 border-color: #0077cc;
61 }
62 .score-label {
63 font-weight: bold;
64 font-size: 18pt;
65 }
66 .header-label {
67 font-weight: bold;
68 font-size: 22pt;
69 }
70 .instructions-label {
71 font-size: 13pt;
72 color: #222;
73 }
74 .center-box {
75 margin-left: auto;
76 margin-right: auto;
77 }
78 """)
79 Gtk.StyleContext.add_provider_for_display(
80 Gdk.Display.get_default(), # type: ignore
81 css_provider,
82 Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
83 )
84
85 # Main vertical box
86 main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING)
87 main_box.set_margin_top(style.DEFAULT_SPACING)
88 main_box.set_margin_bottom(style.DEFAULT_SPACING)
89 main_box.set_margin_start(style.DEFAULT_SPACING)
90 main_box.set_margin_end(style.DEFAULT_SPACING)
91
92 # Instructions
93 self.instructions_label = Gtk.Label()
94 self.instructions_label.set_wrap(True)
95 self.instructions_label.set_justify(Gtk.Justification.CENTER)
96 self.instructions_label.set_margin_bottom(style.DEFAULT_SPACING // 2)
97 self.instructions_label.set_markup(
98 "<span size='large' weight='bold'>How to Play:</span>\n"
99 "<span size='medium'>Move the ball with arrow keys, WASD, or the on-screen buttons. "
100 "Reach the <b>green</b> goal, avoid <b>red</b> obstacles. "
101 "Press <b>Reset</b> to restart. Each wall bounce increases speed!</span>"
102 )
103 self.instructions_label.get_style_context().add_class("instructions-label")
104 main_box.append(self.instructions_label)
105
106 # Welcome and Score
107 self.header_label = Gtk.Label()
108 self.header_label.set_markup("<span size='xx-large' weight='bold'>Sugar Ball Dodge!</span>")
109 self.header_label.set_margin_bottom(style.DEFAULT_SPACING // 2)
110 self.header_label.get_style_context().add_class("header-label")
111 main_box.append(self.header_label)
112
113 self.score = 0
114 self.score_label = Gtk.Label(label="Score: 0")
115 self.score_label.set_margin_bottom(style.DEFAULT_SPACING)
116 self.score_label.get_style_context().add_class("score-label")
117 main_box.append(self.score_label)
118
119
120 # Ball Name ( Default to Hello World Lol)
121 name_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
122 name_label = Gtk.Label(label="Your Name:")
123 self.name_entry = Gtk.Entry()
124 self.name_entry.set_placeholder_text("Enter your name")
125 self.name_entry.set_max_length(16)
126 self.name_entry.set_width_chars(12)
127 self.name_entry.set_text("Hello World!")
128 self.name_entry.connect("changed", self._on_name_changed)
129 name_box.append(name_label)
130 name_box.append(self.name_entry)
131 main_box.append(name_box)
132
133
134
135 # Toolbar with movement buttons, pause, and reset, centered
136 toolbox = Toolbox()
137 toolbar_outer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
138 toolbar_outer.set_halign(Gtk.Align.CENTER)
139 toolbar_outer.set_hexpand(True)
140
141 toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING)
142 toolbar.set_halign(Gtk.Align.CENTER)
143 toolbar.set_hexpand(False)
144
145 btn_left = ToolButton(tooltip="Left")
146 btn_left.set_icon_widget(Icon(icon_name="go-left", pixel_size=36))
147 btn_left.get_style_context().add_class("game-btn")
148 btn_right = ToolButton(tooltip="Right")
149 btn_right.set_icon_widget(Icon(icon_name="go-right", pixel_size=36))
150 btn_right.get_style_context().add_class("game-btn")
151 btn_up = ToolButton(tooltip="Up")
152 btn_up.set_icon_widget(Icon(icon_name="go-up", pixel_size=36))
153 btn_up.get_style_context().add_class("game-btn")
154 btn_down = ToolButton(tooltip="Down")
155 btn_down.set_icon_widget(Icon(icon_name="go-down", pixel_size=36))
156 btn_down.get_style_context().add_class("game-btn")
157 toolbar.append(btn_left)
158 toolbar.append(btn_up)
159 toolbar.append(btn_down)
160 toolbar.append(btn_right)
161
162 # Pause
163 self.btn_pause = ToolButton(tooltip="Pause/Resume")
164 self.btn_pause.set_icon_widget(Icon(icon_name="media-playback-pause", pixel_size=36))
165 self.btn_pause.get_style_context().add_class("game-btn")
166 self.btn_pause.connect("clicked", self._toggle_pause)
167 toolbar.append(self.btn_pause)
168
169 # Reset
170 btn_reset = ToolButton(tooltip="Reset")
171 btn_reset.set_icon_widget(Icon(icon_name="document-open", pixel_size=36))
172 btn_reset.get_style_context().add_class("game-btn")
173 toolbar.append(btn_reset)
174
175 toolbar_outer.append(toolbar)
176 toolbox.add_toolbar("Controls", toolbar_outer)
177 main_box.append(toolbox)
178
179 # Main Game Area
180 area_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
181 area_box.set_hexpand(True)
182 area_box.set_vexpand(True)
183 area_box.set_halign(Gtk.Align.CENTER)
184 area_box.set_valign(Gtk.Align.CENTER)
185
186 # Frame
187 frame = Gtk.Frame()
188 frame.set_margin_top(10)
189 frame.set_margin_bottom(10)
190 frame.set_margin_start(10)
191 frame.set_margin_end(10)
192
193 self.area = Gtk.DrawingArea()
194 self.area.set_content_width(800)
195 self.area.set_content_height(600)
196 self.area.set_hexpand(False)
197 self.area.set_vexpand(False)
198 self.area.set_halign(Gtk.Align.CENTER)
199 self.area.set_valign(Gtk.Align.CENTER)
200 self.area.set_draw_func(self._draw_area)
201 frame.set_child(self.area)
202
203 # Scrolled Window
204 scrolled = Gtk.ScrolledWindow()
205 scrolled.set_child(frame)
206 scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
207 scrolled.set_propagate_natural_width(True)
208 scrolled.set_propagate_natural_height(True)
209 area_box.append(scrolled)
210 main_box.append(area_box)
211
212 # Status ( Instructions )
213 self.status_label = Gtk.Label(label="Use arrow keys, WASD, or buttons to move the ball! Get the green goal, avoid red obstacles.")
214 self.status_label.set_margin_top(style.DEFAULT_SPACING)
215 main_box.append(self.status_label)
216
217 self.set_canvas(main_box)
218 self.set_default_size(1600, 1100)
219
220 # Ball state
221 self.ball_pos = [700.0, 450.0]
222 self.ball_radius = BALL_RADIUS
223 self.ball_color = XoColor()
224 self.ball_text = self.name_entry.get_text()
225 self.ball_velocity = [BALL_INIT_SPEED, 0.0]
226 self.ball_speed = BALL_INIT_SPEED
227
228
229 # Goal and obstacles
230 self.goal_pos = self._random_pos(GOAL_RADIUS)
231 self.obstacles = [self._random_pos(OBSTACLE_RADIUS) for _ in range(OBSTACLE_COUNT)]
232
233 self.animating = False
234 self.running = True
235
236 # Keyboard controls
237 key_controller = Gtk.EventControllerKey()
238 key_controller.connect("key-pressed", self._on_key_pressed)
239 self.add_controller(key_controller)
240
241 # Button controls
242 btn_left.connect("clicked", lambda b: self._set_direction(-1, 0))
243 btn_right.connect("clicked", lambda b: self._set_direction(1, 0))
244 btn_up.connect("clicked", lambda b: self._set_direction(0, -1))
245 btn_down.connect("clicked", lambda b: self._set_direction(0, 1))
246 btn_reset.connect("clicked", lambda b: self._reset_game())
247
248
249 # Start game loop
250 GLib.timeout_add(16, self._game_tick) # 60 FPS
251
252
253 def _draw_area(self, area, cr, width, height):
254 # Draw goal
255 cr.save()
256 cr.set_source_rgb(0.2, 0.8, 0.2)
257 cr.arc(self.goal_pos[0], self.goal_pos[1], GOAL_RADIUS, 0, 2*math.pi)
258 cr.fill()
259 cr.restore()
260
261 # Draw obstacles
262 for ox, oy in self.obstacles:
263 cr.save()
264 cr.set_source_rgb(0.85, 0.1, 0.1)
265 cr.arc(ox, oy, OBSTACLE_RADIUS, 0, 2*math.pi)
266 cr.fill()
267 cr.restore()
268
269 # Draw ball with current color and position
270 r, g, b = self._hex_to_rgb(self.ball_color.get_fill_color())
271 cr.save()
272 cr.set_source_rgb(r, g, b)
273 cr.arc(self.ball_pos[0], self.ball_pos[1], self.ball_radius, 0, 2*math.pi)
274 cr.fill()
275 cr.restore()
276
277 # Draw text centered in the ball
278 cr.save()
279 cr.set_source_rgb(0, 0, 0)
280 cr.select_font_face("Sans", 0, 0)
281 cr.set_font_size(16)
282 text = self.ball_text
283 xbearing, ybearing, tw, th, xadv, yadv = cr.text_extents(text)
284 cr.move_to(self.ball_pos[0] - tw/2, self.ball_pos[1] + th/2)
285 cr.show_text(text)
286 cr.restore()
287
288 # Draw boundary rectangle (border)
289 cr.save()
290 cr.set_line_width(6)
291 cr.set_source_rgb(0.2, 0.2, 0.2)
292 cr.rectangle(3, 3, width - 6, height - 6)
293 cr.stroke()
294 cr.restore()
295
296
297 def _hex_to_rgb(self, hex_color):
298 hex_color = hex_color.lstrip("#")
299 return tuple(int(hex_color[i:i+2], 16)/255.0 for i in (0, 2, 4))
300
301 def _set_direction(self, dx, dy):
302 if not self.running:
303 return
304 speed = self.ball_speed
305 norm = math.hypot(dx, dy)
306 if norm == 0:
307 return
308 self.ball_velocity = [speed * dx / norm, speed * dy / norm]
309
310 def _on_name_changed(self, entry):
311 self.ball_text = entry.get_text()
312 # Pause the game when editing the name
313 if self.running:
314 self.running = False
315 self.btn_pause.set_icon_widget(Icon(icon_name="media-playback-start", pixel_size=36))
316 self.status_label.set_text("Paused for name entry. Click on Pause/Resume Button. Press Enter to Finish Typing!")
317 self.area.queue_draw()
318 # Connect Enter key to remove focus (finish editing)
319 entry.connect("activate", self._on_entry_activate)
320
321 def _on_entry_activate(self, entry):
322 # Remove focus from entry so user can resume game with keyboard
323 entry.get_root().set_focus(None)
324
325 def _toggle_pause(self, button):
326 if self.running:
327 self.running = False
328 self.btn_pause.set_icon_widget(Icon(icon_name="media-playback-start", pixel_size=36))
329 self.status_label.set_text("Paused. Click Pause/Resume or press 'p' to continue.")
330 else:
331 self.running = True
332 self.btn_pause.set_icon_widget(Icon(icon_name="media-playback-pause", pixel_size=36))
333 self.status_label.set_text("Game resumed!")
334
335 def _on_key_pressed(self, controller, keyval, keycode, state):
336 # Only handle keys if name_entry is not focused
337 if self.name_entry.has_focus():
338 return False
339 key = Gdk.keyval_name(keyval)
340 if key in ("Left", "a", "A"):
341 self._set_direction(-1, 0)
342 elif key in ("Right", "d", "D"):
343 self._set_direction(1, 0)
344 elif key in ("Up", "w", "W"):
345 self._set_direction(0, -1)
346 elif key in ("Down", "s", "S"):
347 self._set_direction(0, 1)
348 elif key == "r":
349 self._reset_game()
350 elif key in ("Return", "KP_Enter", "Enter"):
351 self._reset_game()
352 elif key in ("p", "P"):
353 self._toggle_pause(None)
354 return True
355
356 def _random_pos(self, radius):
357 # TODO: Make sure they are away from the ball slightly along with velocity accomodation so direct hits are avoided
358 width = self.area.get_content_width()
359 height = self.area.get_content_height()
360 return [
361 random.uniform(radius + 10, width - radius - 10),
362 random.uniform(radius + 10, height - radius - 10),
363 ]
364
365 def _reset_game(self):
366 # self.ball_pos = [700.0, 450.0]
367 # TODO: start from width half, but this should be random?
368 self.ball_pos = [400.0,300.0]
369 self.ball_color = XoColor()
370 self.ball_velocity = [BALL_INIT_SPEED, 0.0]
371 self.ball_speed = BALL_INIT_SPEED
372 self.goal_pos = self._random_pos(GOAL_RADIUS)
373 self.obstacles = [self._random_pos(OBSTACLE_RADIUS) for _ in range(OBSTACLE_COUNT)]
374 self.score = 0
375 self.score_label.set_text("Score: 0")
376 self.status_label.set_text("Game reset! Use arrows, WASD, or buttons.")
377 self.running = True
378 self.btn_pause.set_icon_widget(Icon(icon_name="media-playback-pause", pixel_size=36))
379 self.area.queue_draw()
380
381 def _game_tick(self):
382 if not self.running:
383 return True
384 width = self.area.get_content_width()
385 height = self.area.get_content_height()
386 x, y = self.ball_pos
387 vx, vy = self.ball_velocity
388
389 # Move ball
390 x_new = x + vx
391 y_new = y + vy
392 bounced = False
393
394 # Bounce off walls, increase speed
395 if x_new - self.ball_radius < 0:
396 x_new = self.ball_radius
397 vx = abs(vx)
398 bounced = True
399 if x_new + self.ball_radius > width:
400 x_new = width - self.ball_radius
401 vx = -abs(vx)
402 bounced = True
403 if y_new - self.ball_radius < 0:
404 y_new = self.ball_radius
405 vy = abs(vy)
406 bounced = True
407 if y_new + self.ball_radius > height:
408 y_new = height - self.ball_radius
409 vy = -abs(vy)
410 bounced = True
411
412 if bounced:
413 self.ball_speed = min(self.ball_speed + BALL_SPEED_INC, BALL_MAX_SPEED)
414 norm = math.hypot(vx, vy)
415 if norm > 0:
416 vx = self.ball_speed * vx / norm
417 vy = self.ball_speed * vy / norm
418 self.ball_color = XoColor()
419 self.status_label.set_text("Bounced! Speed up!")
420 self.ball_pos = [x_new, y_new]
421 self.ball_velocity = [vx, vy]
422
423 # Check collision with goal
424 if self._distance(self.ball_pos, self.goal_pos) < self.ball_radius + GOAL_RADIUS:
425 self.score += 1
426 self.score_label.set_text(f"Score: {self.score}")
427 self.goal_pos = self._random_pos(GOAL_RADIUS)
428 self.status_label.set_text("Goal! +1 Score")
429 # Move obstacles too
430 self.obstacles = [self._random_pos(OBSTACLE_RADIUS) for _ in range(OBSTACLE_COUNT)]
431 self.area.queue_draw()
432 # IMP: Return to fix the overlap issue
433 return True
434
435 # ONLY after checking goal check obstacles
436 for ox, oy in self.obstacles:
437 if self._distance(self.ball_pos, [ox, oy]) < self.ball_radius + OBSTACLE_RADIUS:
438 self.status_label.set_text("Game Over! Hit an obstacle. Press Reset.")
439 self.running = False
440 return True
441
442 self.area.queue_draw()
443 return True
444
445 def _distance(self, a, b):
446 return math.hypot(a[0] - b[0], a[1] - b[1])
447
448def main():
449 app = Gtk.Application(application_id="org.sugarlabs.HelloWorldDodge")
450
451 def on_activate(app):
452 activity = HelloWorldDodgeActivity()
453 app.add_window(activity)
454 activity.present()
455
456 app.connect("activate", on_activate)
457 return app.run(sys.argv)
458
459if __name__ == "__main__":
460 main()
UI Components
Examples of various UI components:
Alert Example
1import gi
2gi.require_version('Gtk', '4.0')
3from gi.repository import Gtk, GLib
4import sys
5import os
6sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
7from sugar.graphics.alert import Alert, ConfirmationAlert, ErrorAlert, TimeoutAlert, NotifyAlert
8
9class AlertExample(Gtk.ApplicationWindow):
10 def __init__(self, app):
11 super().__init__(application=app, title="Alert Example")
12 self.set_default_size(600, 400)
13
14 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
15 vbox.set_margin_top(20)
16 vbox.set_margin_bottom(20)
17 vbox.set_margin_start(20)
18 vbox.set_margin_end(20)
19 self.set_child(vbox)
20
21 self.alert_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
22 vbox.append(self.alert_box)
23
24 btn_simple = Gtk.Button(label="Show Simple Alert")
25 btn_simple.connect("clicked", self.on_simple_alert)
26 vbox.append(btn_simple)
27
28 btn_confirm = Gtk.Button(label="Show Confirmation Alert")
29 btn_confirm.connect("clicked", self.on_confirmation_alert)
30 vbox.append(btn_confirm)
31
32 btn_timeout = Gtk.Button(label="Show Timeout Alert")
33 btn_timeout.connect("clicked", self.on_timeout_alert)
34 vbox.append(btn_timeout)
35
36 def on_simple_alert(self, button):
37 alert = Alert()
38 alert.props.title = "Simple Alert"
39 alert.props.msg = "This is a basic alert message."
40 alert.add_button(1, "OK")
41 alert.connect("response", self.on_alert_response)
42 self.alert_box.append(alert)
43
44 def on_confirmation_alert(self, button):
45 alert = ConfirmationAlert()
46 alert.props.title = "Confirm Action"
47 alert.props.msg = "Are you sure you want to continue?"
48 alert.connect("response", self.on_alert_response)
49 self.alert_box.append(alert)
50
51 def on_timeout_alert(self, button):
52 alert = TimeoutAlert(timeout=5)
53 alert.props.title = "Timeout Alert"
54 alert.props.msg = "This alert will disappear in 5 seconds."
55 alert.connect("response", self.on_alert_response)
56 self.alert_box.append(alert)
57
58 def on_alert_response(self, alert, response_id):
59 print(f"Alert response: {response_id}")
60 self.alert_box.remove(alert)
61
62class AlertApp(Gtk.Application):
63 def do_activate(self):
64 window = AlertExample(self)
65 window.present()
66
67if __name__ == "__main__":
68 app = AlertApp()
69 app.run()
Icon Example
1"""Sugar GTK4 Icon Example - Complete Feature Demo."""
2
3import sys
4import os
5
6sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
7
8import gi
9
10gi.require_version("Gtk", "4.0")
11gi.require_version("Gdk", "4.0")
12from gi.repository import Gtk, Gdk
13
14from sugar.activity import SimpleActivity
15from sugar.graphics.icon import Icon, EventIcon, CanvasIcon
16from sugar.graphics.xocolor import XoColor
17
18
19class IconExampleActivity(SimpleActivity):
20 """Example activity demonstrating all Sugar GTK4 icon features."""
21
22 def __init__(self):
23 super().__init__()
24 self.set_title("Sugar GTK4 Icon Example ")
25 self._create_content()
26
27 def _create_content(self):
28 """Create the main content showing all icon types and features."""
29 main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
30 main_box.set_margin_start(20)
31 main_box.set_margin_end(20)
32 main_box.set_margin_top(20)
33 main_box.set_margin_bottom(20)
34 main_box.set_hexpand(True)
35 main_box.set_vexpand(True)
36
37 # Scrolled window for all content
38 scrolled = Gtk.ScrolledWindow()
39 scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
40 scrolled.set_child(main_box)
41 scrolled.set_hexpand(True)
42 scrolled.set_vexpand(True)
43
44 # Add CSS provider for CanvasIcon hover/active states
45 css_provider = Gtk.CssProvider()
46 css_data = """
47 .canvas-icon {
48 background-color: transparent;
49 border-radius: 8px;
50 padding: 4px;
51 transition: background-color 200ms ease;
52 }
53 .canvas-icon:hover {
54 background-color: rgba(0, 0, 0, 0.15);
55 }
56 .canvas-icon:active {
57 background-color: rgba(0, 0, 0, 0.25);
58 }
59 """
60 try:
61 css_provider.load_from_string(css_data)
62 Gtk.StyleContext.add_provider_for_display(
63 Gdk.Display.get_default(),
64 css_provider,
65 Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
66 )
67 except Exception as e:
68 print(f"Warning: Could not load CSS provider: {e}")
69
70 # Title
71 title = Gtk.Label()
72 title.set_markup("<big><b>Sugar GTK4 Icon Examples - Complete</b></big>")
73 title.set_hexpand(True)
74 main_box.append(title)
75
76 # Add sections
77 self._add_basic_icons(main_box)
78 self._add_colored_icons(main_box)
79 self._add_badge_icons(main_box)
80 self._add_event_icons(main_box)
81 self._add_canvas_icons(main_box)
82 self._add_size_and_alpha_examples(main_box)
83
84 self.set_canvas(scrolled)
85 self.set_default_size(900, 700)
86
87 def _add_basic_icons(self, container):
88 """Add basic icon examples."""
89 frame = Gtk.Frame(label="Basic Icons")
90 frame.set_hexpand(True)
91 box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
92 box.set_margin_start(10)
93 box.set_margin_end(10)
94 box.set_margin_top(10)
95 box.set_margin_bottom(10)
96 box.set_hexpand(True)
97 box.set_halign(Gtk.Align.CENTER)
98
99 # System icons
100 for icon_name in [
101 "document-new",
102 "document-open",
103 "document-save",
104 "edit-copy",
105 "edit-paste",
106 ]:
107 icon = Icon(icon_name=icon_name, pixel_size=48)
108 box.append(icon)
109
110 frame.set_child(box)
111 container.append(frame)
112
113 def _add_colored_icons(self, container):
114 """Add colored icon examples."""
115 frame = Gtk.Frame(label="Colored Icons")
116 frame.set_hexpand(True)
117 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
118 vbox.set_margin_start(10)
119 vbox.set_margin_end(10)
120 vbox.set_margin_top(10)
121 vbox.set_margin_bottom(10)
122 vbox.set_hexpand(True)
123
124 # XO Color examples using xotest.svg
125 hbox1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
126 hbox1.set_hexpand(True)
127 hbox1.set_halign(Gtk.Align.CENTER)
128 label1 = Gtk.Label(label="XO Colors (xotest.svg):")
129 label1.set_size_request(150, -1)
130 hbox1.append(label1)
131
132 xotest_svg = os.path.join(
133 os.path.dirname(__file__),
134 "..",
135 "src",
136 "sugar",
137 "graphics",
138 "icons",
139 "test.svg",
140 )
141 for i in range(3):
142 xo_color = XoColor.get_random_color()
143 icon = Icon(file_name=xotest_svg, pixel_size=48)
144 icon.set_xo_color(xo_color)
145 hbox1.append(icon)
146
147 vbox.append(hbox1)
148
149 # Manual color examples (still using xotest.svg)
150 hbox2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
151 hbox2.set_hexpand(True)
152 hbox2.set_halign(Gtk.Align.CENTER)
153 label2 = Gtk.Label(label="Manual Colors (xotest.svg):")
154 label2.set_size_request(150, -1)
155 hbox2.append(label2)
156
157 for fill, stroke in [
158 ("#FF0000", "#800000"),
159 ("#00FF00", "#008000"),
160 ("#0000FF", "#000080"),
161 ]:
162 icon = Icon(file_name=xotest_svg, pixel_size=48)
163 icon.set_fill_color(fill)
164 icon.set_stroke_color(stroke)
165 hbox2.append(icon)
166
167 vbox.append(hbox2)
168 frame.set_child(vbox)
169 container.append(frame)
170
171 def _add_badge_icons(self, container):
172 """Add badge icon examples."""
173 frame = Gtk.Frame(label="Badge Icons")
174 frame.set_hexpand(True)
175 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
176 vbox.set_margin_start(10)
177 vbox.set_margin_end(10)
178 vbox.set_margin_top(10)
179 vbox.set_margin_bottom(10)
180 vbox.set_hexpand(True)
181
182 # Info label
183 info_label = Gtk.Label(label="Icons with badges (small overlay icons):")
184 vbox.append(info_label)
185
186 # Badge examples
187 hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
188 hbox.set_hexpand(True)
189 hbox.set_halign(Gtk.Align.CENTER)
190 badges = [
191 ("folder", "emblem-favorite"),
192 ("document-new", "emblem-important"),
193 ("network-wireless", "dialog-information"),
194 ]
195 for main_icon, badge_icon in badges:
196 vbox_item = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
197 vbox_item.set_halign(Gtk.Align.CENTER)
198 icon = Icon(icon_name=main_icon, pixel_size=64)
199 icon.set_badge_name(badge_icon)
200 icon.set_fill_color("#00AA00")
201 icon.set_stroke_color("#004400")
202 vbox_item.append(icon)
203 label = Gtk.Label(label=f"{main_icon}\n+ {badge_icon}")
204 label.set_justify(Gtk.Justification.CENTER)
205 vbox_item.append(label)
206 hbox.append(vbox_item)
207
208 vbox.append(hbox)
209 frame.set_child(vbox)
210 container.append(frame)
211
212 def _add_event_icons(self, container):
213 """Add event icon examples."""
214 frame = Gtk.Frame(label="Interactive Icons (EventIcon)")
215 frame.set_hexpand(True)
216 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
217 vbox.set_margin_start(10)
218 vbox.set_margin_end(10)
219 vbox.set_margin_top(10)
220 vbox.set_margin_bottom(10)
221 vbox.set_hexpand(True)
222
223 # Info label
224 info_label = Gtk.Label(label="Click these icons to see events:")
225 vbox.append(info_label)
226
227 # Status label
228 self.event_info = Gtk.Label(label="No events yet")
229 vbox.append(self.event_info)
230
231 # Event icons
232 hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
233 hbox.set_hexpand(True)
234 hbox.set_halign(Gtk.Align.CENTER)
235
236 for i, icon_name in enumerate(
237 ["media-playback-start", "media-playback-pause", "media-playback-stop"]
238 ):
239 event_icon = EventIcon(icon_name=icon_name, pixel_size=64)
240 event_icon.connect("clicked", self._on_icon_clicked, icon_name)
241 event_icon.connect("pressed", self._on_icon_pressed, icon_name)
242 event_icon.connect("released", self._on_icon_released, icon_name)
243 event_icon.connect("activate", self._on_icon_activated, icon_name)
244 hbox.append(event_icon)
245
246 vbox.append(hbox)
247 frame.set_child(vbox)
248 container.append(frame)
249
250 def _add_canvas_icons(self, container):
251 """Add canvas icon examples with hover effects."""
252 frame = Gtk.Frame(label="Canvas Icons (Hover Effects)")
253 frame.set_hexpand(True)
254 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
255 vbox.set_margin_start(10)
256 vbox.set_margin_end(10)
257 vbox.set_margin_top(10)
258 vbox.set_margin_bottom(10)
259 vbox.set_hexpand(True)
260
261 info_label = Gtk.Label(label="Hover and click these icons for visual feedback:")
262 vbox.append(info_label)
263
264 hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
265 hbox.set_hexpand(True)
266 hbox.set_halign(Gtk.Align.CENTER)
267
268 icons = [
269 ("system-search", "#FF8800", "#AA4400"),
270 ("edit-delete", "#FF0000", "#880000"),
271 ("dialog-information", "#0088FF", "#004488"),
272 ]
273 for icon_name, fill, stroke in icons:
274 # Create a wrapper box for the canvas icon to ensure proper CSS application
275 wrapper = Gtk.Box()
276 wrapper.add_css_class("canvas-icon")
277
278 canvas_icon = CanvasIcon(icon_name=icon_name, pixel_size=64)
279 canvas_icon.set_fill_color(fill)
280 canvas_icon.set_stroke_color(stroke)
281
282 wrapper.append(canvas_icon)
283 hbox.append(wrapper)
284
285 vbox.append(hbox)
286 frame.set_child(vbox)
287 container.append(frame)
288
289 def _add_size_and_alpha_examples(self, container):
290 """Add different size and transparency examples."""
291 frame = Gtk.Frame(label="Different Sizes and Transparency")
292 frame.set_hexpand(True)
293 hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
294 hbox.set_margin_start(10)
295 hbox.set_margin_end(10)
296 hbox.set_margin_top(10)
297 hbox.set_margin_bottom(10)
298 hbox.set_hexpand(True)
299 hbox.set_halign(Gtk.Align.CENTER)
300
301 sizes = [16, 24, 32, 48, 64, 96]
302 for size in sizes:
303 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
304 vbox.set_halign(Gtk.Align.CENTER)
305 icon = Icon(icon_name="applications-graphics", pixel_size=size)
306 vbox.append(icon)
307 label = Gtk.Label(label=f"{size}px")
308 vbox.append(label)
309 hbox.append(vbox)
310
311 # Transparency example
312 vbox_alpha = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
313 vbox_alpha.set_halign(Gtk.Align.CENTER)
314 label_alpha = Gtk.Label(label="Alpha (transparency):")
315 vbox_alpha.append(label_alpha)
316 for alpha in [1.0, 0.7, 0.4]:
317 icon = Icon(icon_name="applications-graphics", pixel_size=48)
318 icon.set_alpha(alpha)
319 vbox_alpha.append(icon)
320 hbox.append(vbox_alpha)
321
322 frame.set_child(hbox)
323 container.append(frame)
324
325 def _on_icon_clicked(self, icon, icon_name):
326 """Handle icon click events."""
327 self.event_info.set_text(f"Clicked: {icon_name}")
328
329 def _on_icon_pressed(self, icon, x, y, icon_name):
330 """Handle icon press events."""
331 self.event_info.set_text(f"Pressed: {icon_name} at ({x:.1f}, {y:.1f})")
332
333 def _on_icon_released(self, icon, x, y, icon_name):
334 """Handle icon release events."""
335 self.event_info.set_text(f"Released: {icon_name} at ({x:.1f}, {y:.1f})")
336
337 def _on_icon_activated(self, icon, icon_name):
338 """Handle icon activate events."""
339 self.event_info.set_text(f"Activated: {icon_name}")
340
341
342def main():
343 """Run the icon example activity."""
344 app = Gtk.Application(application_id="org.sugarlabs.IconExample")
345
346 def on_activate(app):
347 activity = IconExampleActivity()
348 app.add_window(activity)
349 activity.present()
350
351 app.connect("activate", on_activate)
352 return app.run(sys.argv)
353
354
355if __name__ == "__main__":
356 main()
357
Palette Example
1"""
2Complete Palette Demo
3"""
4
5import gi
6gi.require_version('Gtk', '4.0')
7gi.require_version('Gdk', '4.0')
8
9import sys
10import os
11from gi.repository import Gtk
12
13sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
14from sugar.graphics.palette import Palette
15from sugar.graphics.palettewindow import (
16 PaletteWindow, WidgetInvoker, CursorInvoker,
17)
18from sugar.graphics.palettemenu import PaletteMenuItem, PaletteMenuItemSeparator
19from sugar.graphics.palettegroup import get_group
20from sugar.graphics.icon import Icon
21from sugar.graphics import style
22
23
24class PaletteDemo(Gtk.ApplicationWindow):
25 """Main demo window showcasing all palette features."""
26
27 def __init__(self, app):
28 super().__init__(application=app)
29 self.set_title("Sugar Palette Complete Demo - GTK4")
30 self.set_default_size(800, 600)
31
32 main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
33 main_box.set_margin_start(20)
34 main_box.set_margin_end(20)
35 main_box.set_margin_top(20)
36 main_box.set_margin_bottom(20)
37 self.set_child(main_box)
38
39 title = Gtk.Label()
40 title.set_markup("<big><b>Sugar Palette Demo - GTK4</b></big>")
41 main_box.append(title)
42
43 scrolled = Gtk.ScrolledWindow()
44 scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
45 scrolled.set_vexpand(True)
46 main_box.append(scrolled)
47
48 content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
49 scrolled.set_child(content_box)
50
51 self._create_basic_palette_section(content_box)
52 self._create_menu_palette_section(content_box)
53 self._create_palette_window_section(content_box)
54 self._create_invoker_section(content_box)
55 self._create_treeview_section(content_box)
56 self._create_palette_group_section(content_box)
57
58 def _create_section_header(self, parent, title, description):
59 header_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
60 parent.append(header_box)
61
62 title_label = Gtk.Label()
63 title_label.set_markup(f"<b>{title}</b>")
64 title_label.set_halign(Gtk.Align.START)
65 header_box.append(title_label)
66
67 desc_label = Gtk.Label(label=description)
68 desc_label.set_halign(Gtk.Align.START)
69 desc_label.set_wrap(True)
70 desc_label.add_css_class('dim-label')
71 header_box.append(desc_label)
72
73 sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
74 sep.set_margin_top(10)
75 sep.set_margin_bottom(10)
76 parent.append(sep)
77
78 return header_box
79
80 def _create_basic_palette_section(self, parent):
81 section = self._create_section_header(
82 parent,
83 "Basic Palettes",
84 "Basic palette widgets with text, icons, and content"
85 )
86
87 demo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
88 demo_box.set_margin_top(10)
89 parent.append(demo_box)
90
91 simple_btn = Gtk.Button(label="Simple Palette")
92 demo_box.append(simple_btn)
93
94 simple_palette = Palette(label="Simple Palette")
95 simple_palette.props.secondary_text = "This is a simple palette with primary and secondary text."
96
97 close_btn1 = Gtk.Button(label="Close")
98 close_btn1.connect('clicked', lambda btn: simple_palette.popdown(immediate=True))
99 simple_palette.set_content(close_btn1)
100
101 simple_invoker = WidgetInvoker()
102 simple_invoker.attach(simple_btn)
103 simple_invoker.set_lock_palette(True)
104 simple_palette.set_invoker(simple_invoker)
105 simple_btn.connect('clicked', lambda btn: simple_palette.popup(immediate=True))
106
107 icon_btn = Gtk.Button(label="With Icon")
108 demo_box.append(icon_btn)
109
110 icon_palette = Palette(label="Palette with Icon")
111 icon_palette.props.secondary_text = "This palette includes an icon and action buttons."
112 icon_palette.set_icon(Icon(icon_name='dialog-information', pixel_size=style.STANDARD_ICON_SIZE))
113
114 close_btn2 = Gtk.Button(label="Close")
115 close_btn2.connect('clicked', lambda btn: icon_palette.popdown(immediate=True))
116 icon_palette.set_content(close_btn2)
117
118 icon_palette.action_bar.add_action("Action 1", "document-save")
119 icon_palette.action_bar.add_action("Action 2", "edit-copy")
120
121 icon_invoker = WidgetInvoker()
122 icon_invoker.attach(icon_btn)
123 icon_invoker.set_lock_palette(True)
124 icon_palette.set_invoker(icon_invoker)
125 icon_btn.connect('clicked', lambda btn: icon_palette.popup(immediate=True))
126
127 # custom content
128 content_btn = Gtk.Button(label="Custom Content")
129 demo_box.append(content_btn)
130
131 content_palette = Palette(label="Custom Content")
132 content_palette.props.secondary_text = "This palette contains custom widgets."
133
134 custom_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
135 custom_content.set_margin_start(10)
136 custom_content.set_margin_end(10)
137 custom_content.set_margin_top(5)
138 custom_content.set_margin_bottom(5)
139
140 entry = Gtk.Entry()
141 entry.set_placeholder_text("Type something...")
142 custom_content.append(entry)
143
144 scale = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 100, 1)
145 scale.set_value(50)
146 custom_content.append(scale)
147
148 check = Gtk.CheckButton(label="Enable feature")
149 custom_content.append(check)
150
151 close_btn3 = Gtk.Button(label="Close")
152 close_btn3.connect('clicked', lambda btn: content_palette.popdown(immediate=True))
153 custom_content.append(close_btn3)
154
155 content_palette.set_content(custom_content)
156
157 content_invoker = WidgetInvoker()
158 content_invoker.attach(content_btn)
159 content_invoker.set_lock_palette(True)
160 content_palette.set_invoker(content_invoker)
161 content_btn.connect('clicked', lambda btn: content_palette.popup(immediate=True))
162
163 def _create_menu_palette_section(self, parent):
164 """Create menu palette examples."""
165 section = self._create_section_header(
166 parent,
167 "Menu Palettes",
168 "Palettes that act as context menus with menu items"
169 )
170
171 demo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
172 demo_box.set_margin_top(10)
173 parent.append(demo_box)
174
175 menu_btn = Gtk.Button(label="Menu Palette")
176 demo_box.append(menu_btn)
177
178 self.menu_feedback_label = Gtk.Label(label="(No menu action yet)")
179 demo_box.append(self.menu_feedback_label)
180
181 menu_palette = Palette(label="Menu Options")
182 menu_palette.props.secondary_text = "Right-click or use menu property for options"
183 menu = menu_palette.menu
184
185 def feedback(msg):
186 self.menu_feedback_label.set_text(msg)
187
188 item1 = PaletteMenuItem("Open File", "document-open")
189 item1.connect('activate', lambda x: feedback("Open File clicked"))
190 menu.append(item1)
191
192 item2 = PaletteMenuItem("Save File", "document-save")
193 item2.connect('activate', lambda x: feedback("Save File clicked"))
194 menu.append(item2)
195
196 menu.append(PaletteMenuItemSeparator())
197
198 item3 = PaletteMenuItem("Settings", "preferences-system")
199 item3.connect('activate', lambda x: feedback("Settings clicked"))
200 menu.append(item3)
201
202 menu_invoker = WidgetInvoker()
203 menu_invoker.attach_widget(menu_btn)
204 menu_palette.set_invoker(menu_invoker)
205 menu_btn.connect('clicked', lambda btn: menu_palette.popup(immediate=True))
206
207 def _create_palette_window_section(self, parent):
208 """Create palette window examples."""
209 section = self._create_section_header(
210 parent,
211 "Palette Windows",
212 "Low-level palette window implementation"
213 )
214
215 demo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
216 demo_box.set_margin_top(10)
217 parent.append(demo_box)
218
219 window_btn = Gtk.Button(label="Palette Window")
220 demo_box.append(window_btn)
221
222 palette_window = PaletteWindow()
223
224 custom_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
225 custom_widget.set_margin_start(10)
226 custom_widget.set_margin_end(10)
227 custom_widget.set_margin_top(10)
228 custom_widget.set_margin_bottom(10)
229
230 label = Gtk.Label(label="Custom Palette Window")
231 label.add_css_class('heading')
232 custom_widget.append(label)
233
234 progress = Gtk.ProgressBar()
235 progress.set_fraction(0.7)
236 progress.set_text("Progress: 70%")
237 progress.set_show_text(True)
238 custom_widget.append(progress)
239
240 button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
241 button_box.set_halign(Gtk.Align.CENTER)
242 ok_btn = Gtk.Button(label="OK")
243 cancel_btn = Gtk.Button(label="Cancel")
244 button_box.append(ok_btn)
245 button_box.append(cancel_btn)
246 custom_widget.append(button_box)
247
248 palette_window.set_content(custom_widget)
249
250 window_invoker = WidgetInvoker()
251 window_invoker.attach(window_btn)
252 palette_window.set_invoker(window_invoker)
253 window_btn.connect('clicked', lambda btn: palette_window.popup(immediate=True))
254
255 ok_btn.connect('clicked', lambda btn: palette_window.popdown(immediate=True))
256 cancel_btn.connect('clicked', lambda btn: palette_window.popdown(immediate=True))
257
258 def _create_invoker_section(self, parent):
259 """Create different invoker type examples."""
260 section = self._create_section_header(
261 parent,
262 "Invoker Types",
263 "Different ways to trigger palette display"
264 )
265
266 demo_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
267 demo_box.set_margin_top(10)
268 parent.append(demo_box)
269
270 # Widget invoker (hover demo with box)
271 widget_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
272 hover_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
273 hover_box.set_size_request(120, 40)
274 hover_box.set_halign(Gtk.Align.START)
275 hover_box.set_valign(Gtk.Align.CENTER)
276 hover_box.set_margin_top(4)
277 hover_box.set_margin_bottom(4)
278 hover_box.set_margin_start(4)
279 hover_box.set_margin_end(4)
280 hover_box.add_css_class('suggested-action')
281 hover_label = Gtk.Label(label="Hover Me (Box)")
282 hover_box.append(hover_label)
283 widget_row.append(hover_box)
284 widget_row.append(Gtk.Label(label="← Hover to invoke palette"))
285 demo_box.append(widget_row)
286
287 widget_palette = Palette(label="Widget Invoker (Hover)")
288 widget_palette.props.secondary_text = "Triggered by hover on box"
289 widget_invoker = WidgetInvoker()
290 widget_invoker.attach_widget(hover_box)
291 widget_palette.set_invoker(widget_invoker)
292 def on_motion_enter(controller, x, y):
293 widget_palette.popup(immediate=True)
294 motion_controller = Gtk.EventControllerMotion()
295 motion_controller.connect('enter', on_motion_enter)
296 hover_box.add_controller(motion_controller)
297
298
299 cursor_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
300 cursor_btn = Gtk.Button(label="Cursor Invoker")
301 cursor_row.append(cursor_btn)
302 cursor_row.append(Gtk.Label(label="← Click to show at cursor position"))
303 demo_box.append(cursor_row)
304
305 cursor_palette = Palette(label="Cursor Invoker")
306 cursor_palette.props.secondary_text = "Shows at cursor position"
307 cursor_invoker = CursorInvoker()
308 cursor_invoker.attach(cursor_btn)
309 cursor_palette.set_invoker(cursor_invoker)
310
311 def update_pointer_position(motion_controller, x, y):
312 cursor_invoker._cursor_x = int(x)
313 cursor_invoker._cursor_y = int(y)
314 motion_controller = Gtk.EventControllerMotion()
315 motion_controller.connect('motion', update_pointer_position)
316 cursor_btn.add_controller(motion_controller)
317
318 def show_cursor_palette(btn):
319 cursor_palette.popup(immediate=True)
320 cursor_btn.connect('clicked', show_cursor_palette)
321
322 def _create_treeview_section(self, parent):
323 """Create TreeView invoker examples."""
324 section = self._create_section_header(
325 parent,
326 "TreeView Integration",
327 "Double-click a row to show a palette for that item."
328 )
329
330 demo_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
331 demo_box.set_margin_top(10)
332 parent.append(demo_box)
333
334 store = Gtk.ListStore(str, str)
335 store.append(["Item 1", "Description 1"])
336 store.append(["Item 2", "Description 2"])
337 store.append(["Item 3", "Description 3"])
338
339 tree_view = Gtk.TreeView(model=store)
340 tree_view.set_size_request(-1, 150)
341
342 renderer = Gtk.CellRendererText()
343 column1 = Gtk.TreeViewColumn("Name", renderer, text=0)
344 tree_view.append_column(column1)
345
346 column2 = Gtk.TreeViewColumn("Description", renderer, text=1)
347 tree_view.append_column(column2)
348
349 scrolled_tree = Gtk.ScrolledWindow()
350 scrolled_tree.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
351 scrolled_tree.set_child(tree_view)
352 demo_box.append(scrolled_tree)
353
354 # only double-click (row-activated) opens palette
355 def show_row_palette(treeview, path, column=None):
356 row = store[path][0]
357 palette = Palette(label=f"Row: {row}")
358 palette.props.secondary_text = f"Palette for {row}"
359 close_btn = Gtk.Button(label="Close")
360 close_btn.connect('clicked', lambda btn: palette.popdown(immediate=True))
361 palette.set_content(close_btn)
362 invoker = WidgetInvoker()
363 invoker.attach(tree_view)
364 invoker.set_lock_palette(True)
365 palette.set_invoker(invoker)
366 palette.popup(immediate=True)
367 def on_row_activated(treeview, path, column):
368 show_row_palette(treeview, path, column)
369 tree_view.connect('row-activated', on_row_activated)
370
371 def _create_palette_group_section(self, parent):
372 """Create palette group examples."""
373 section = self._create_section_header(
374 parent,
375 "Palette Groups",
376 "Coordinated palettes - only one shows at a time"
377 )
378
379 demo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
380 demo_box.set_margin_top(10)
381 parent.append(demo_box)
382
383 group = get_group('demo_group')
384
385 for i in range(3):
386 btn = Gtk.Button(label=f"Group Palette {i+1}")
387 demo_box.append(btn)
388
389 palette = Palette(label=f"Grouped Palette {i+1}")
390 palette.props.secondary_text = f"This is palette {i+1} in the group. Only one group palette can be open at a time."
391
392 group.add(palette)
393
394 invoker = WidgetInvoker()
395 invoker.attach(btn)
396 palette.set_invoker(invoker)
397 btn.connect('clicked', lambda btn, p=palette: p.popup(immediate=True))
398
399 control_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
400 control_box.set_margin_top(10)
401 parent.append(control_box)
402
403 popdown_btn = Gtk.Button(label="Pop Down All Groups")
404 popdown_btn.connect('clicked', lambda btn: self._popdown_all_groups())
405 control_box.append(popdown_btn)
406
407 def _popdown_all_groups(self):
408 """Pop down all palette groups."""
409 from sugar.graphics.palettegroup import popdown_all
410 popdown_all()
411 print("All palette groups popped down")
412
413
414class PaletteDemoApp(Gtk.Application):
415
416 def __init__(self):
417 super().__init__(application_id='org.sugarlabs.PaletteDemo')
418
419 def do_activate(self):
420 window = PaletteDemo(self)
421 window.present()
422
423
424def main():
425 app = PaletteDemoApp()
426 return app.run([])
427
428
429if __name__ == '__main__':
430 sys.exit(main())
Toolbar Examples
1"""ToolbarBox Example"""
2
3import sys
4import os
5
6sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
7
8import gi
9
10gi.require_version("Gtk", "4.0")
11from gi.repository import Gtk
12
13from sugar.activity import SimpleActivity
14from sugar.graphics.toolbarbox import ToolbarBox, ToolbarButton
15from sugar.graphics.toolbutton import ToolButton
16from sugar.graphics.icon import Icon
17from sugar.graphics import style
18
19PROJECT_ROOT = os.path.join(os.path.dirname(__file__), "..")
20SUGAR_ICONS_PATH = os.path.join(PROJECT_ROOT, "sugar-artwork", "icons", "scalable", "actions")
21SUGAR_ICONS_PATH = os.path.abspath(SUGAR_ICONS_PATH)
22
23
24class ToolbarBoxExampleActivity(SimpleActivity):
25 """Example activity demonstrating Sugar GTK4 ToolbarBox features."""
26
27 def __init__(self):
28 super().__init__()
29 self.set_title("Sugar GTK4 ToolbarBox Example")
30
31
32 self._create_content()
33
34 def _create_content(self):
35 """Create the main content with expandable toolbars."""
36 main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
37
38 self._toolbarbox = ToolbarBox()
39
40 self._create_main_toolbar()
41
42 main_box.append(self._toolbarbox)
43
44 content_area = self._create_content_area()
45 main_box.append(content_area)
46
47 self._status_bar = Gtk.Label(
48 label="Click toolbar buttons to expand/collapse sections"
49 )
50 self._status_bar.set_margin_start(style.DEFAULT_PADDING)
51 self._status_bar.set_margin_end(style.DEFAULT_PADDING)
52 self._status_bar.set_margin_top(style.DEFAULT_PADDING // 2)
53 self._status_bar.set_margin_bottom(style.DEFAULT_PADDING // 2)
54 self._status_bar.add_css_class("dim-label")
55 main_box.append(self._status_bar)
56
57 self.set_canvas(main_box)
58 self.set_default_size(800, 600)
59
60 def _create_main_toolbar(self):
61 """Create the main toolbar with expandable sections."""
62 toolbar = self._toolbarbox.get_toolbar()
63
64 # Activity button (non-expandable)
65 activity_button = ToolButton(icon_name=os.path.join(PROJECT_ROOT, "sugar-artwork", "icons", "scalable", "apps", "activity-journal.svg"))
66 activity_button.set_tooltip("My Activity")
67 toolbar.append(activity_button)
68
69 # Separator
70 separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
71 separator.set_margin_start(6)
72 separator.set_margin_end(6)
73 toolbar.append(separator)
74
75 # Edit tools (expandable)
76 edit_button = ToolbarButton(
77 page=self._create_edit_toolbar(),
78 icon_name=os.path.join(SUGAR_ICONS_PATH, "toolbar-edit.svg")
79 )
80 edit_button.set_tooltip("Edit Tools")
81 toolbar.append(edit_button)
82
83 # View tools (expandable)
84 view_button = ToolbarButton(
85 page=self._create_view_toolbar(),
86 icon_name=os.path.join(SUGAR_ICONS_PATH, "toolbar-view.svg")
87 )
88 view_button.set_tooltip("View Tools")
89 toolbar.append(view_button)
90
91 # Tools section (expandable)
92 tools_button = ToolbarButton(
93 page=self._create_tools_toolbar(),
94 icon_name=os.path.join(PROJECT_ROOT, "sugar-artwork", "icons", "scalable", "categories", "preferences-system.svg")
95 )
96 tools_button.set_tooltip("Tools")
97 toolbar.append(tools_button)
98
99 # Spacer
100 spacer = Gtk.Box()
101 spacer.set_hexpand(True)
102 toolbar.append(spacer)
103
104 # Stop button (non-expandable)
105 stop_button = ToolButton(icon_name=os.path.join(SUGAR_ICONS_PATH, "activity-stop.svg"))
106 stop_button.set_tooltip("Stop Activity")
107 stop_button.connect("clicked", lambda w: self.close())
108 toolbar.append(stop_button)
109
110 def _create_edit_toolbar(self):
111 """Create the edit toolbar page."""
112 toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
113 toolbar.set_margin_top(style.DEFAULT_PADDING)
114 toolbar.set_margin_bottom(style.DEFAULT_PADDING)
115
116 edit_buttons = [
117 ("New", os.path.join(SUGAR_ICONS_PATH, "document-open.svg")),
118 ("Save", os.path.join(SUGAR_ICONS_PATH, "document-save.svg")),
119 ("---", None), # Separator
120 ("Copy", os.path.join(SUGAR_ICONS_PATH, "edit-copy.svg")),
121 ("Paste", os.path.join(SUGAR_ICONS_PATH, "edit-paste.svg")),
122 ("---", None), # Separator
123 ("Undo", os.path.join(SUGAR_ICONS_PATH, "edit-undo.svg")),
124 ("Redo", os.path.join(SUGAR_ICONS_PATH, "edit-redo.svg")),
125 ]
126
127 for label, icon_name in edit_buttons:
128 if label == "---":
129 separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
130 separator.set_margin_start(6)
131 separator.set_margin_end(6)
132 toolbar.append(separator)
133 else:
134 button = ToolButton(icon_name=icon_name)
135 button.set_tooltip(label)
136 button.connect("clicked", self._on_toolbar_action, f"Edit: {label}")
137 toolbar.append(button)
138
139 return toolbar
140
141 def _create_view_toolbar(self):
142 """Create the view toolbar page."""
143 toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
144 toolbar.set_margin_top(style.DEFAULT_PADDING)
145 toolbar.set_margin_bottom(style.DEFAULT_PADDING)
146
147 zoom_out = ToolButton(icon_name=os.path.join(SUGAR_ICONS_PATH, "zoom-out.svg"))
148 zoom_out.set_tooltip("Zoom Out")
149 zoom_out.connect("clicked", self._on_toolbar_action, "View: Zoom Out")
150 toolbar.append(zoom_out)
151
152 zoom_label = Gtk.Label(label="100%")
153 zoom_label.set_size_request(50, -1)
154 toolbar.append(zoom_label)
155
156 zoom_in = ToolButton(icon_name=os.path.join(SUGAR_ICONS_PATH, "zoom-in.svg"))
157 zoom_in.set_tooltip("Zoom In")
158 zoom_in.connect("clicked", self._on_toolbar_action, "View: Zoom In")
159 toolbar.append(zoom_in)
160
161 separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
162 separator.set_margin_start(6)
163 separator.set_margin_end(6)
164 toolbar.append(separator)
165
166 view_modes = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3)
167 view_modes.add_css_class("linked")
168
169 list_view = Gtk.ToggleButton()
170 list_view.set_child(
171 Icon(file_name=os.path.join(SUGAR_ICONS_PATH, "view-list.svg"), pixel_size=style.STANDARD_ICON_SIZE)
172 )
173 list_view.set_tooltip_text("List View")
174 list_view.set_active(True)
175 view_modes.append(list_view)
176
177 grid_view = Gtk.ToggleButton()
178 grid_view.set_child(
179 Icon(file_name=os.path.join(SUGAR_ICONS_PATH, "view-details.svg"), pixel_size=style.STANDARD_ICON_SIZE)
180 )
181 grid_view.set_tooltip_text("Details View")
182 view_modes.append(grid_view)
183
184 toolbar.append(view_modes)
185
186 return toolbar
187
188 def _create_tools_toolbar(self):
189 """Create the tools toolbar page."""
190 toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
191 toolbar.set_margin_top(style.DEFAULT_PADDING)
192 toolbar.set_margin_bottom(style.DEFAULT_PADDING)
193
194 # Tool selection
195 cursor_path = os.path.join(PROJECT_ROOT, "sugar-artwork", "cursor", "sugar", "pngs")
196 tools = [
197 ("Brush", os.path.join(cursor_path, "paintbrush.png")),
198 ("Text", os.path.join(SUGAR_ICONS_PATH, "format-text-bold.svg")),
199 ("Shape", os.path.join(SUGAR_ICONS_PATH, "view-triangle.svg")),
200 ("Select", os.path.join(SUGAR_ICONS_PATH, "select-all.svg")),
201 ]
202
203 for name, icon_path in tools:
204 tool_button = ToolButton(icon_name=icon_path)
205 tool_button.set_tooltip(name)
206 tool_button.connect("clicked", self._on_toolbar_action, f"Tool: {name}")
207 toolbar.append(tool_button)
208
209 separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
210 separator.set_margin_start(6)
211 separator.set_margin_end(6)
212 toolbar.append(separator)
213
214 color_button = Gtk.ColorButton()
215 color_button.set_tooltip_text("Choose Color")
216 toolbar.append(color_button)
217
218 properties_button = ToolButton(icon_name=os.path.join(PROJECT_ROOT, "sugar-artwork", "icons", "scalable", "categories", "preferences-system.svg"))
219 properties_button.set_tooltip("Properties")
220 properties_button.connect(
221 "clicked", self._on_toolbar_action, "Tool: Properties"
222 )
223 toolbar.append(properties_button)
224
225 return toolbar
226
227 def _create_content_area(self):
228 """Create main content area."""
229 content_frame = Gtk.Frame()
230 content_frame.set_margin_start(style.DEFAULT_PADDING)
231 content_frame.set_margin_end(style.DEFAULT_PADDING)
232 content_frame.set_margin_top(style.DEFAULT_PADDING)
233 content_frame.set_margin_bottom(style.DEFAULT_PADDING)
234
235 content_box = Gtk.Box(
236 orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING
237 )
238 content_box.set_margin_start(style.DEFAULT_PADDING)
239 content_box.set_margin_end(style.DEFAULT_PADDING)
240 content_box.set_margin_top(style.DEFAULT_PADDING)
241 content_box.set_margin_bottom(style.DEFAULT_PADDING)
242
243 description = Gtk.Label()
244 description.set_markup("""
245<b>Sugar GTK4 ToolbarBox Example</b>
246
247This example demonstrates the expandable toolbar functionality:
248
249- <b>Expandable Sections:</b> Click Edit, View, or Tools buttons to expand sections
250- <b>Inline Display:</b> Expanded toolbars appear below the main toolbar
251- <b>Palette Fallback:</b> On smaller screens, content may appear in palettes
252
253<i>Click the toolbar buttons above to see the expansion behavior.</i>
254 """)
255 description.set_halign(Gtk.Align.START)
256 content_box.append(description)
257
258 log_frame = Gtk.Frame(label="Action Log")
259 log_frame.set_margin_top(style.DEFAULT_SPACING)
260
261 scrolled = Gtk.ScrolledWindow()
262 scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
263 scrolled.set_size_request(-1, 150)
264
265 self._action_log = Gtk.TextView()
266 self._action_log.set_editable(False)
267 self._action_log.set_cursor_visible(False)
268 scrolled.set_child(self._action_log)
269
270 log_frame.set_child(scrolled)
271 content_box.append(log_frame)
272
273 content_frame.set_child(content_box)
274 return content_frame
275
276 def _on_toolbar_action(self, button, action):
277 """Handle toolbar button clicks."""
278 self._log_action(action)
279
280 def _log_action(self, action):
281 """Add action to the log."""
282 buffer = self._action_log.get_buffer()
283
284 import datetime
285
286 timestamp = datetime.datetime.now().strftime("%H:%M:%S")
287 text = f"[{timestamp}] {action}\n"
288
289 end_iter = buffer.get_end_iter()
290 buffer.insert(end_iter, text)
291
292 mark = buffer.get_insert()
293 self._action_log.scroll_mark_onscreen(mark)
294
295
296def main():
297 """Run the ToolbarBox example activity."""
298 app = Gtk.Application(application_id="org.sugarlabs.ToolbarBoxExample")
299
300 def on_activate(app):
301 activity = ToolbarBoxExampleActivity()
302 activity.set_application(app)
303 activity.present()
304
305 app.connect("activate", on_activate)
306 return app.run(sys.argv)
307
308
309if __name__ == "__main__":
310 main()
311
1"""Sugar GTK4 Toolbox Example """
2
3import sys
4import os
5
6sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
7
8import gi
9gi.require_version("Gtk", "4.0")
10from gi.repository import Gtk
11
12from sugar.activity import SimpleActivity
13from sugar.graphics.toolbox import Toolbox
14from sugar.graphics import style
15from sugar.graphics.icon import Icon
16
17
18class ToolboxExampleActivity(SimpleActivity):
19 """Example activity demonstrating Sugar GTK4 Toolbox features."""
20
21 def __init__(self):
22 super().__init__()
23 self.set_title("Sugar GTK4 Toolbox Example")
24 self._create_content()
25
26 def _create_content(self):
27 """Create the main content with toolbox."""
28 main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
29
30 # Create toolbox
31 self._toolbox = Toolbox()
32 self._toolbox.connect('current-toolbar-changed', self._on_toolbar_changed)
33
34 # Add various toolbars
35 self._create_edit_toolbar()
36 self._create_view_toolbar()
37 self._create_tools_toolbar()
38 self._create_help_toolbar()
39
40 main_box.append(self._toolbox)
41
42 # Add content area
43 content_area = self._create_content_area()
44 main_box.append(content_area)
45
46 # Status bar
47 self._status_bar = Gtk.Label(label="Toolbox Example - Switch between toolbars using tabs")
48 self._status_bar.set_margin_start(style.DEFAULT_PADDING)
49 self._status_bar.set_margin_end(style.DEFAULT_PADDING)
50 self._status_bar.set_margin_top(style.DEFAULT_PADDING // 2)
51 self._status_bar.set_margin_bottom(style.DEFAULT_PADDING // 2)
52 self._status_bar.add_css_class('dim-label')
53 main_box.append(self._status_bar)
54
55 self.set_canvas(main_box)
56 self.set_default_size(800, 600)
57
58 def _create_edit_toolbar(self):
59 """Create edit toolbar with common editing tools."""
60 toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
61 toolbar.set_margin_top(style.DEFAULT_PADDING)
62 toolbar.set_margin_bottom(style.DEFAULT_PADDING)
63
64 # Common edit buttons
65 edit_buttons = [
66 ("New", "document-new"),
67 ("Open", "document-open"),
68 ("Save", "document-save"),
69 ("---", None), # Separator
70 ("Cut", "edit-cut"),
71 ("Copy", "edit-copy"),
72 ("Paste", "edit-paste"),
73 ("---", None), # Separator
74 ("Undo", "edit-undo"),
75 ("Redo", "edit-redo"),
76 ]
77
78 for label, icon_name in edit_buttons:
79 if label == "---":
80 separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
81 separator.set_margin_start(6)
82 separator.set_margin_end(6)
83 toolbar.append(separator)
84 else:
85 button = Gtk.Button()
86 if icon_name:
87 icon = Icon(icon_name=icon_name, pixel_size=style.STANDARD_ICON_SIZE)
88 button.set_child(icon)
89 button.set_tooltip_text(label)
90 button.connect('clicked', self._on_toolbar_button_clicked, f"Edit: {label}")
91 toolbar.append(button)
92
93 # Add spacer
94 spacer = Gtk.Box()
95 spacer.set_hexpand(True)
96 toolbar.append(spacer)
97
98 # Add text entry for demonstration
99 entry = Gtk.Entry()
100 entry.set_placeholder_text("Type something...")
101 entry.set_size_request(200, -1)
102 toolbar.append(entry)
103
104 self._toolbox.add_toolbar("Edit", toolbar)
105
106 def _create_view_toolbar(self):
107 """Create view toolbar with view-related controls."""
108 toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
109 toolbar.set_margin_top(style.DEFAULT_PADDING)
110 toolbar.set_margin_bottom(style.DEFAULT_PADDING)
111
112 # Zoom controls
113 zoom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3)
114
115 zoom_out = Gtk.Button()
116 zoom_out.set_child(Icon(icon_name="zoom-out", pixel_size=style.STANDARD_ICON_SIZE))
117 zoom_out.set_tooltip_text("Zoom Out")
118 zoom_out.connect('clicked', self._on_toolbar_button_clicked, "View: Zoom Out")
119 zoom_box.append(zoom_out)
120
121 zoom_label = Gtk.Label(label="100%")
122 zoom_label.set_size_request(50, -1)
123 zoom_box.append(zoom_label)
124
125 zoom_in = Gtk.Button()
126 zoom_in.set_child(Icon(icon_name="zoom-in", pixel_size=style.STANDARD_ICON_SIZE))
127 zoom_in.set_tooltip_text("Zoom In")
128 zoom_in.connect('clicked', self._on_toolbar_button_clicked, "View: Zoom In")
129 zoom_box.append(zoom_in)
130
131 toolbar.append(zoom_box)
132
133 # Separator
134 separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
135 separator.set_margin_start(6)
136 separator.set_margin_end(6)
137 toolbar.append(separator)
138
139 # View mode toggle buttons
140 view_modes = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3)
141 view_modes.add_css_class('linked')
142
143 list_view = Gtk.ToggleButton()
144 list_view.set_child(Icon(icon_name="view-list", pixel_size=style.STANDARD_ICON_SIZE))
145 list_view.set_tooltip_text("List View")
146 list_view.set_active(True)
147 list_view.connect('toggled', self._on_view_mode_toggled, "List View")
148 view_modes.append(list_view)
149
150 grid_view = Gtk.ToggleButton()
151 grid_view.set_child(Icon(icon_name="view-grid", pixel_size=style.STANDARD_ICON_SIZE))
152 grid_view.set_tooltip_text("Grid View")
153 grid_view.connect('toggled', self._on_view_mode_toggled, "Grid View")
154 view_modes.append(grid_view)
155
156 toolbar.append(view_modes)
157
158 # Spacer
159 spacer = Gtk.Box()
160 spacer.set_hexpand(True)
161 toolbar.append(spacer)
162
163 # Fullscreen button
164 fullscreen = Gtk.Button()
165 fullscreen.set_child(Icon(icon_name="view-fullscreen", pixel_size=style.STANDARD_ICON_SIZE))
166 fullscreen.set_tooltip_text("Fullscreen")
167 fullscreen.connect('clicked', self._on_toolbar_button_clicked, "View: Fullscreen")
168 toolbar.append(fullscreen)
169
170 self._toolbox.add_toolbar("View", toolbar)
171
172 def _create_tools_toolbar(self):
173 """Create tools toolbar with tool-specific controls."""
174 toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
175 toolbar.set_margin_top(style.DEFAULT_PADDING)
176 toolbar.set_margin_bottom(style.DEFAULT_PADDING)
177
178 # Tool selection
179 tools_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3)
180 tools_box.add_css_class('linked')
181
182 tools = [
183 ("Pointer", "tool-pointer"),
184 ("Brush", "tool-brush"),
185 ("Text", "tool-text"),
186 ("Shape", "shape-rectangle"),
187 ]
188
189 for i, (name, icon_name) in enumerate(tools):
190 tool_button = Gtk.ToggleButton()
191 tool_button.set_child(Icon(icon_name=icon_name, pixel_size=style.STANDARD_ICON_SIZE))
192 tool_button.set_tooltip_text(name)
193 if i == 0: # Select first tool by default
194 tool_button.set_active(True)
195 tool_button.connect('toggled', self._on_tool_selected, name)
196 tools_box.append(tool_button)
197
198 toolbar.append(tools_box)
199
200 # Separator
201 separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
202 separator.set_margin_start(6)
203 separator.set_margin_end(6)
204
205 toolbar.append(separator)
206
207 # Color picker
208 color_button = Gtk.ColorButton()
209 color_button.set_tooltip_text("Choose Color")
210 toolbar.append(color_button)
211
212 # Size adjustment
213 size_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
214 size_label = Gtk.Label(label="Size:")
215 size_box.append(size_label)
216
217 size_scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL)
218 size_scale.set_range(1, 20)
219 size_scale.set_value(5)
220 size_scale.set_size_request(100, -1)
221 size_scale.set_tooltip_text("Tool Size")
222 size_box.append(size_scale)
223
224 toolbar.append(size_box)
225
226 self._toolbox.add_toolbar("Tools", toolbar)
227
228 def _create_help_toolbar(self):
229 """Create help toolbar with help and information."""
230 toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
231 toolbar.set_margin_top(style.DEFAULT_PADDING)
232 toolbar.set_margin_bottom(style.DEFAULT_PADDING)
233
234 # Help buttons
235 help_button = Gtk.Button()
236 help_button.set_child(Icon(icon_name="help-contents", pixel_size=style.STANDARD_ICON_SIZE))
237 help_button.set_tooltip_text("Help Contents")
238 help_button.connect('clicked', self._on_toolbar_button_clicked, "Help: Contents")
239 toolbar.append(help_button)
240
241 about_button = Gtk.Button()
242 about_button.set_child(Icon(icon_name="help-about", pixel_size=style.STANDARD_ICON_SIZE))
243 about_button.set_tooltip_text("About")
244 about_button.connect('clicked', self._on_toolbar_button_clicked, "Help: About")
245 toolbar.append(about_button)
246
247 # Spacer
248 spacer = Gtk.Box()
249 spacer.set_hexpand(True)
250 toolbar.append(spacer)
251
252 # Info display
253 info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
254 info_title = Gtk.Label(label="Toolbox Info")
255 info_title.add_css_class('heading')
256 info_box.append(info_title)
257
258 self._info_label = Gtk.Label(label=f"Total toolbars: {self._toolbox.get_toolbar_count()}")
259 self._info_label.add_css_class('dim-label')
260 info_box.append(self._info_label)
261
262 toolbar.append(info_box)
263
264 self._toolbox.add_toolbar("Help", toolbar)
265
266 def _create_content_area(self):
267 """Create main content area."""
268 content_frame = Gtk.Frame()
269 content_frame.set_hexpand(True)
270 content_frame.set_vexpand(True)
271 content_frame.set_margin_start(style.DEFAULT_PADDING)
272 content_frame.set_margin_end(style.DEFAULT_PADDING)
273 content_frame.set_margin_top(style.DEFAULT_PADDING)
274 content_frame.set_margin_bottom(style.DEFAULT_PADDING)
275
276 content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING)
277 content_box.set_margin_start(style.DEFAULT_PADDING * 2)
278 content_box.set_margin_end(style.DEFAULT_PADDING * 2)
279 content_box.set_margin_top(style.DEFAULT_PADDING * 2)
280 content_box.set_margin_bottom(style.DEFAULT_PADDING * 2)
281
282 title = Gtk.Label()
283 title.set_markup("<big><b>Toolbox Demo Content Area</b></big>")
284 content_box.append(title)
285
286 description = Gtk.Label()
287 description.set_markup("""
288<i>This demonstrates the Sugar Toolbox component:</i>
289
290• <b>Multiple Toolbars:</b> Switch between Edit, View, Tools, and Help
291• <b>Tab Navigation:</b> Click tabs at the bottom to switch toolbars
292• <b>Dynamic Content:</b> Each toolbar can contain different widgets
293• <b>Sugar Styling:</b> Consistent with Sugar visual design
294• <b>Signal Handling:</b> Responds to toolbar changes
295
296<i>Click the buttons in the toolbars above to see actions.</i>
297 """)
298 description.set_halign(Gtk.Align.START)
299 content_box.append(description)
300
301 # Action log
302 log_frame = Gtk.Frame(label="Action Log")
303 log_frame.set_margin_top(style.DEFAULT_SPACING)
304
305 scrolled = Gtk.ScrolledWindow()
306 scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
307 scrolled.set_size_request(-1, 150)
308
309 self._action_log = Gtk.TextView()
310 self._action_log.set_editable(False)
311 self._action_log.set_cursor_visible(False)
312 scrolled.set_child(self._action_log)
313
314 log_frame.set_child(scrolled)
315 content_box.append(log_frame)
316
317 content_frame.set_child(content_box)
318 return content_frame
319
320 def _on_toolbar_changed(self, toolbox, page_num):
321 """Handle toolbar change."""
322 toolbar_name = self._toolbox.get_toolbar_label(page_num)
323 self._log_action(f"Switched to {toolbar_name} toolbar")
324
325 # Update info in help toolbar
326 if hasattr(self, '_info_label'):
327 self._info_label.set_text(
328 f"Total toolbars: {self._toolbox.get_toolbar_count()}, "
329 f"Current: {page_num + 1} ({toolbar_name})"
330 )
331
332 def _on_toolbar_button_clicked(self, button, action):
333 """Handle toolbar button clicks."""
334 self._log_action(action)
335
336 def _on_view_mode_toggled(self, button, mode):
337 """Handle view mode toggle."""
338 if button.get_active():
339 self._log_action(f"Switched to {mode}")
340
341 def _on_tool_selected(self, button, tool_name):
342 """Handle tool selection."""
343 if button.get_active():
344 self._log_action(f"Selected {tool_name} tool")
345
346 def _log_action(self, action):
347 """Add action to the log."""
348 buffer = self._action_log.get_buffer()
349
350 # Add timestamp and action
351 import datetime
352 timestamp = datetime.datetime.now().strftime("%H:%M:%S")
353 text = f"[{timestamp}] {action}\n"
354
355 # Insert at end
356 end_iter = buffer.get_end_iter()
357 buffer.insert(end_iter, text)
358
359 # Scroll to end
360 mark = buffer.get_insert()
361 self._action_log.scroll_mark_onscreen(mark)
362
363
364def main():
365 """Run the Toolbox example activity."""
366 app = Gtk.Application(application_id='org.sugarlabs.ToolboxExample')
367
368 def on_activate(app):
369 activity = ToolboxExampleActivity()
370 app.add_window(activity)
371 activity.present()
372
373 app.connect('activate', on_activate)
374 return app.run()
375
376
377if __name__ == "__main__":
378 main()
Other Examples
Animator:
animator_example.py
Hello World:
hello_world_dodge.py
Menu Item:
menuitem_example.py
Object Chooser:
objectchooser_example.py
Radio Palette:
radio_palette_example.py
Radio Tool Button:
radiotoolbutton_example.py
Style:
style_example.py
Tray:
tray_example.py
Window:
window_example.py