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