Examples

The Sugar Toolkit GTK4 comes with several example activities that demonstrate how to use the various components of the toolkit.

Basic Activity

The most basic activity shows how to create a simple Sugar activity:

This example demonstrates how to create a simple Sugar activity using GTK4. It shows the basic structure and key features of a Sugar activity.

  1"""Basic Sugar GTK4 Activity Example."""
  2
  3import sys
  4import os
  5
  6sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
  7
  8import gi
  9
 10gi.require_version("Gtk", "4.0")
 11from gi.repository import Gtk
 12
 13from sugar.activity import SimpleActivity
 14from sugar.graphics.xocolor import XoColor
 15
 16
 17class BasicExampleActivity(SimpleActivity):
 18    """A basic example activity showing Sugar GTK4 features."""
 19
 20    def __init__(self):
 21        super().__init__()
 22        self.set_title("Basic Sugar GTK4 Example")
 23
 24        self._create_content()
 25
 26        self._show_color_info()
 27
 28    def _create_content(self):
 29        """Create the main content area."""
 30        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
 31        main_box.set_margin_start(20)
 32        main_box.set_margin_end(20)
 33        main_box.set_margin_top(20)
 34        main_box.set_margin_bottom(20)
 35
 36        # Title
 37        title = Gtk.Label()
 38        title.set_markup("<big><b>Welcome to Sugar GTK4!</b></big>")
 39        main_box.append(title)
 40
 41        # Description
 42        desc = Gtk.Label(
 43            label="This is a basic Sugar activity using GTK4.\n"
 44            "It demonstrates the new toolkit features."
 45        )
 46        desc.set_justify(Gtk.Justification.CENTER)
 47        main_box.append(desc)
 48
 49        # Color demo
 50        color_frame = Gtk.Frame(label="XO Color Demo")
 51        color_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
 52        color_box.set_margin_start(10)
 53        color_box.set_margin_end(10)
 54        color_box.set_margin_top(10)
 55        color_box.set_margin_bottom(10)
 56
 57        # Show current XO color
 58        self.xo_color = XoColor()
 59        self.color_info_label = Gtk.Label(
 60            label=f"Current XO Color: {self.xo_color.to_string()}"
 61        )
 62        color_box.append(self.color_info_label)
 63
 64        # Color preview boxes
 65        color_preview_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
 66        color_preview_box.set_halign(Gtk.Align.CENTER)
 67
 68        # Stroke color box
 69        self.stroke_box = Gtk.Box()
 70        self.stroke_box.set_size_request(100, 50)
 71        stroke_label = Gtk.Label(label="Stroke")
 72        stroke_label.set_halign(Gtk.Align.CENTER)
 73        stroke_label.set_valign(Gtk.Align.CENTER)
 74        self.stroke_box.append(stroke_label)
 75        color_preview_box.append(self.stroke_box)
 76
 77        # Fill color box
 78        self.fill_box = Gtk.Box()
 79        self.fill_box.set_size_request(100, 50)
 80        fill_label = Gtk.Label(label="Fill")
 81        fill_label.set_halign(Gtk.Align.CENTER)
 82        fill_label.set_valign(Gtk.Align.CENTER)
 83        self.fill_box.append(fill_label)
 84        color_preview_box.append(self.fill_box)
 85
 86        color_box.append(color_preview_box)
 87
 88        same_color_label = Gtk.Label(label="Same Color (Stroke & Fill)")
 89        same_color_label.set_halign(Gtk.Align.CENTER)
 90        color_box.append(same_color_label)
 91
 92        self.same_color_area = Gtk.DrawingArea()
 93        self.same_color_area.set_content_width(100)
 94        self.same_color_area.set_content_height(50)
 95        self.same_color_area.set_halign(Gtk.Align.CENTER)
 96        self.same_color_area.set_valign(Gtk.Align.CENTER)
 97        self.same_color_area.set_draw_func(self._draw_same_color_box)
 98        color_box.append(self.same_color_area)
 99
100        # Interact with the colors hahahaha
101        random_button = Gtk.Button(label="Get Random Color")
102        random_button.connect("clicked", self._on_random_color)
103        # setting up color as black
104        css_provider = Gtk.CssProvider()
105        css_provider.load_from_data(b"button { color: #000000; }")
106        random_button.get_style_context().add_provider(
107            css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER
108        )
109        color_box.append(random_button)
110
111        color_frame.set_child(color_box)
112        main_box.append(color_frame)
113
114        info_frame = Gtk.Frame(label="Activity Info")
115        info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
116        info_box.set_margin_start(10)
117        info_box.set_margin_end(10)
118        info_box.set_margin_top(10)
119        info_box.set_margin_bottom(10)
120
121        info_box.append(Gtk.Label(label=f"Activity ID: {self.get_id()[:8]}..."))
122        info_box.append(Gtk.Label(label=f"Title: {self.get_title()}"))
123        info_box.append(Gtk.Label(label=f"Active: {self.get_active()}"))
124
125        info_frame.set_child(info_box)
126        main_box.append(info_frame)
127
128        self.set_canvas(main_box)
129
130    def _show_color_info(self):
131        """Display color information in terminal."""
132        print(f"Activity Color: {self.xo_color.to_string()}")
133        print(f"Stroke: {self.xo_color.get_stroke_color()}")
134        print(f"Fill: {self.xo_color.get_fill_color()}")
135
136        rgba = self.xo_color.to_rgba_tuple()
137        print(f"RGBA - Stroke: {rgba[0]}, Fill: {rgba[1]}")
138
139    def _draw_same_color_box(self, area, cr, width, height):
140        """Draw a rectangle filled with fill color and stroked with stroke color."""
141        fill_rgba = self.xo_color.get_fill_color()
142        stroke_rgba = self.xo_color.get_stroke_color()
143        # Convert hex color to RGB
144        fill_rgb = [int(fill_rgba[i : i + 2], 16) / 255.0 for i in (1, 3, 5)]
145        stroke_rgb = [int(stroke_rgba[i : i + 2], 16) / 255.0 for i in (1, 3, 5)]
146        cr.set_source_rgb(*fill_rgb)
147        cr.rectangle(5, 5, width - 10, height - 10)
148        cr.fill_preserve()
149        cr.set_line_width(4)
150        cr.set_source_rgb(*stroke_rgb)
151        cr.stroke()
152
153    def _on_random_color(self, button):
154        """Handle random color button click."""
155        self.xo_color = XoColor.get_random_color()
156        self.color_info_label.set_text(f"Current XO Color: {self.xo_color.to_string()}")
157        print(f"New random color: {self.xo_color.to_string()}")
158        self.same_color_area.queue_draw()
159
160
161def main():
162    """Run the basic example activity."""
163    app = Gtk.Application(application_id="org.sugarlabs.BasicExample")
164
165    def on_activate(app):
166        activity = BasicExampleActivity()
167        app.add_window(activity)
168        activity.present()
169
170    app.connect("activate", on_activate)
171    return app.run(sys.argv)
172
173
174if __name__ == "__main__":
175    main()

Creative Studio Activity Example

This example demonstrates an advanced Sugar creative activity with a custom toolbar, support for keyboard shortcuts (accelerators), file handling, and other features.

  1"""
  2Creative Studio Activity Example
  3================================
  4
  5This example demonstrates an advanced Sugar creative activity featuring:
  6- Multiple creative tools (drawing, text, shapes)
  7- Keyboard shortcuts (Ctrl+Z/Y/S)
  8- Color selection with visual feedback
  9- File operations with auto-save
 10- Preview generation
 11- Flexible creative workspace
 12"""
 13
 14import os
 15import sys
 16import logging
 17import json
 18import tempfile
 19from datetime import datetime
 20
 21sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
 22
 23# Set up mock environment if not running in Sugar
 24if 'SUGAR_BUNDLE_ID' not in os.environ:
 25    os.environ['SUGAR_BUNDLE_ID'] = 'org.sugarlabs.CreativeStudio'
 26    os.environ['SUGAR_BUNDLE_NAME'] = 'Creative Studio'
 27    os.environ['SUGAR_BUNDLE_PATH'] = os.path.dirname(__file__)
 28    os.environ['SUGAR_ACTIVITY_ROOT'] = '/tmp/creative_studio'
 29
 30import gi
 31
 32gi.require_version("Gtk", "4.0")
 33gi.require_version("Gdk", "4.0")
 34
 35from gi.repository import Gtk,  Gio, Gdk
 36
 37from sugar.activity.activity import Activity
 38from sugar.activity.activityhandle import ActivityHandle
 39from sugar.graphics.toolbarbox import ToolbarBox
 40from sugar.activity.widgets import ActivityToolbarButton, StopButton
 41
 42
 43class CreativeCanvas(Gtk.DrawingArea):
 44    """A versatile creative canvas supporting multiple tools and media."""
 45
 46    def __init__(self):
 47        super().__init__()
 48        self.set_size_request(800, 600)
 49        self.set_draw_func(self._draw_func)
 50        self.set_focusable(True)
 51
 52        # Set up gesture for drawing
 53        self._gesture = Gtk.GestureDrag()
 54        self._gesture.connect('drag-begin', self._drag_begin_cb)
 55        self._gesture.connect('drag-update', self._drag_update_cb)
 56        self._gesture.connect('drag-end', self._drag_end_cb)
 57        self.add_controller(self._gesture)
 58
 59        # Set up key controller for keyboard shortcuts
 60        self._key_controller = Gtk.EventControllerKey()
 61        self._key_controller.connect('key-pressed', self._key_pressed_cb)
 62        self.add_controller(self._key_controller)
 63
 64        # Make sure canvas can receive focus for keyboard events
 65        self.set_can_focus(True)
 66
 67        self._elements = []  # All creative elements (strokes, text, shapes, etc.)
 68        self._current_stroke = []
 69
 70        self._current_color = (0, 0, 0)  # Black
 71        self._current_brush_size = 3
 72        self._current_tool = "brush"  # brush, eraser, line, rectangle, circle, spray
 73        self._current_fill = False  # Whether shapes should be filled
 74
 75        # Undo/Redo stacks
 76        self._undo_stack = []
 77        self._redo_stack = []
 78
 79        self._on_change_callback = None
 80
 81    def set_change_callback(self, callback):
 82        """Set callback function to call when canvas changes."""
 83        self._on_change_callback = callback
 84
 85    def _notify_change(self):
 86        """Notify that canvas has changed."""
 87        if self._on_change_callback:
 88            self._on_change_callback()
 89
 90    def _key_pressed_cb(self, controller, keyval, keycode, state):
 91        """Handle keyboard shortcuts."""
 92        # Check for Ctrl key
 93        if state & Gdk.ModifierType.CONTROL_MASK:
 94            if keyval == Gdk.KEY_z or keyval == Gdk.KEY_Z:
 95                if self.undo():
 96                    print("Undo triggered by keyboard")
 97                return True
 98            elif keyval == Gdk.KEY_y or keyval == Gdk.KEY_Y:
 99                if self.redo():
100                    print("Redo triggered by keyboard")
101                return True
102            elif keyval == Gdk.KEY_s or keyval == Gdk.KEY_S:
103                # Trigger save through callback
104                if hasattr(self, '_save_callback') and self._save_callback:
105                    self._save_callback()
106                    print("Save triggered by keyboard")
107                return True
108        return False
109
110    def set_save_callback(self, callback):
111        """Set callback for save shortcut."""
112        self._save_callback = callback
113
114    def _draw_func(self, area, cr, width, height, user_data=None):
115        """Draw the canvas content."""
116        cr.set_source_rgb(1, 1, 1)  # White background
117        cr.paint()
118
119        for element in self._elements:
120            self._draw_element(cr, element)
121
122        # Draw current stroke being created
123        if self._current_stroke:
124            self._draw_current_stroke(cr)
125
126    def _draw_element(self, cr, element):
127        element_type = element.get('type', 'stroke')
128        color = element.get('color', (0, 0, 0))
129        size = element.get('size', 3)
130        points = element.get('points', [])
131
132        cr.set_source_rgb(*color)
133        cr.set_line_width(size)
134
135        if element_type == 'brush':
136            if len(points) > 1:
137                cr.move_to(points[0][0], points[0][1])
138                for point in points[1:]:
139                    cr.line_to(point[0], point[1])
140                cr.stroke()
141
142        elif element_type == 'eraser':
143            # Eraser removes content by painting white with a thicker line
144            cr.set_source_rgb(1, 1, 1)  # White for eraser
145            cr.set_line_width(size * 3)  # Make eraser more visible/effective
146            cr.set_line_cap(1)  # Round line caps
147            cr.set_line_join(1)  # Round line joins
148            if len(points) > 1:
149                cr.move_to(points[0][0], points[0][1])
150                for point in points[1:]:
151                    cr.line_to(point[0], point[1])
152                cr.stroke()
153
154        elif element_type == 'line':
155            if len(points) >= 2:
156                cr.move_to(points[0][0], points[0][1])
157                cr.line_to(points[-1][0], points[-1][1])
158                cr.stroke()
159
160        elif element_type == 'rectangle':
161            if len(points) >= 2:
162                x1, y1 = points[0]
163                x2, y2 = points[-1]
164                width = abs(x2 - x1)
165                height = abs(y2 - y1)
166                x = min(x1, x2)
167                y = min(y1, y2)
168
169                if element.get('fill', False):
170                    cr.rectangle(x, y, width, height)
171                    cr.fill()
172                else:
173                    cr.rectangle(x, y, width, height)
174                    cr.stroke()
175
176        elif element_type == 'circle':
177            if len(points) >= 2:
178                x1, y1 = points[0]
179                x2, y2 = points[-1]
180                radius = ((x2-x1)**2 + (y2-y1)**2)**0.5
181
182                if element.get('fill', False):
183                    cr.arc(x1, y1, radius, 0, 2 * 3.14159)
184                    cr.fill()
185                else:
186                    cr.arc(x1, y1, radius, 0, 2 * 3.14159)
187                    cr.stroke()
188
189    def _draw_current_stroke(self, cr):
190        """Draw the stroke currently being created."""
191        cr.set_source_rgb(*self._current_color)
192        cr.set_line_width(self._current_brush_size)
193
194        if self._current_tool == 'eraser':
195            cr.set_source_rgb(1, 1, 1)
196            cr.set_line_width(self._current_brush_size * 3)
197            cr.set_line_cap(1)  # Round line caps
198            cr.set_line_join(1)  # Round line joins
199
200        if self._current_tool in ['brush', 'eraser'] and len(self._current_stroke) > 1:
201            cr.move_to(self._current_stroke[0][0], self._current_stroke[0][1])
202            for point in self._current_stroke[1:]:
203                cr.line_to(point[0], point[1])
204            cr.stroke()
205        elif self._current_tool == 'line' and len(self._current_stroke) >= 2:
206            cr.move_to(self._current_stroke[0][0], self._current_stroke[0][1])
207            cr.line_to(self._current_stroke[-1][0], self._current_stroke[-1][1])
208            cr.stroke()
209        elif self._current_tool == 'rectangle' and len(self._current_stroke) >= 2:
210            x1, y1 = self._current_stroke[0]
211            x2, y2 = self._current_stroke[-1]
212            width = abs(x2 - x1)
213            height = abs(y2 - y1)
214            x = min(x1, x2)
215            y = min(y1, y2)
216            cr.rectangle(x, y, width, height)
217            if self._current_fill:
218                cr.fill()
219            else:
220                cr.stroke()
221        elif self._current_tool == 'circle' and len(self._current_stroke) >= 2:
222            x1, y1 = self._current_stroke[0]
223            x2, y2 = self._current_stroke[-1]
224            radius = ((x2-x1)**2 + (y2-y1)**2)**0.5
225            cr.arc(x1, y1, radius, 0, 2 * 3.14159)
226            if self._current_fill:
227                cr.fill()
228            else:
229                cr.stroke()
230
231    def _drag_begin_cb(self, gesture, x, y):
232        """Start a new creative action."""
233        self._current_stroke = [(x, y)]
234        self.queue_draw()
235
236    def _drag_update_cb(self, gesture, x, y):
237        """Continue the current action."""
238        result = gesture.get_start_point()
239        if len(result) == 3:
240            valid, start_x, start_y = result
241            if valid:
242                current_x = start_x + x
243                current_y = start_y + y
244                if self._current_tool in ['brush', 'eraser']:
245                    # For freehand tools, add all points
246                    self._current_stroke.append((current_x, current_y))
247                else:
248                    # For shape tools, only keep start and current point
249                    if len(self._current_stroke) == 1:
250                        self._current_stroke.append((current_x, current_y))
251                    else:
252                        self._current_stroke[-1] = (current_x, current_y)
253                self.queue_draw()
254
255    def _drag_end_cb(self, gesture, x, y):
256        """Finish the current action."""
257        if self._current_stroke:
258            self._save_state()
259
260            element_data = {
261                'type': self._current_tool,
262                'points': self._current_stroke[:],
263                'color': self._current_color,
264                'size': self._current_brush_size,
265                'fill': self._current_fill,
266                'timestamp': datetime.now().isoformat()
267            }
268
269            self._elements.append(element_data)
270            self._current_stroke = []
271
272            # Clear redo stack
273            self._redo_stack = []
274
275            self.queue_draw()
276            self._notify_change()
277
278    def _save_state(self):
279        """Save current state for undo."""
280        state = [element.copy() for element in self._elements]
281        self._undo_stack.append(state)
282        # Limit undo stack size
283        if len(self._undo_stack) > 50:
284            self._undo_stack.pop(0)
285
286    def set_color(self, color):
287        """Set the current color."""
288        self._current_color = color
289
290    def set_brush_size(self, size):
291        """Set the brush size."""
292        self._current_brush_size = size
293
294    def set_tool(self, tool):
295        """Set the current tool."""
296        self._current_tool = tool
297
298    def set_fill_mode(self, fill):
299        """Set whether shapes should be filled."""
300        self._current_fill = fill
301
302    def clear_canvas(self):
303        """Clear all content."""
304        self._save_state()
305        self._elements = []
306        self._current_stroke = []
307        self._redo_stack = []
308        self.queue_draw()
309        self._notify_change()
310
311    def undo(self):
312        """Undo last action."""
313        if self._undo_stack:
314            # Save current state to redo stack
315            current_state = [element.copy() for element in self._elements]
316            self._redo_stack.append(current_state)
317
318            # Restore previous state
319            self._elements = self._undo_stack.pop()
320            self.queue_draw()
321            self._notify_change()
322            return True
323        return False
324
325    def redo(self):
326        """Redo last undone action."""
327        if self._redo_stack:
328            # Save current state to undo stack
329            current_state = [element.copy() for element in self._elements]
330            self._undo_stack.append(current_state)
331
332            # Restore redone state
333            self._elements = self._redo_stack.pop()
334            self.queue_draw()
335            self._notify_change()
336            return True
337        return False
338
339    def get_canvas_data(self):
340        """Get all canvas data for saving."""
341        return {
342            'elements': self._elements,
343            'canvas_size': (self.get_width(), self.get_height()),
344            'version': '2.0'
345        }
346
347    def set_canvas_data(self, data):
348        """Set canvas data from saved file."""
349        self._elements = data.get('elements', [])
350        self._current_stroke = []
351        self._undo_stack = []
352        self._redo_stack = []
353        self.queue_draw()
354
355
356class CreativeStudioActivity(Activity):
357    """An advanced creative studio Sugar activity."""
358
359    def __init__(self, handle=None, application=None):
360        """Initialize the activity."""
361        # Create handle if not provided (for testing)
362        if handle is None:
363            handle = ActivityHandle('creative-studio-123')
364
365        Activity.__init__(self, handle, application=application)
366
367        self._initialize_document_data()
368
369        self._current_tool = "brush"
370        self._canvas_size = (800, 600)
371        self._preview_image_path = None
372        self._current_color = (0, 0, 0)
373        self._has_unsaved_changes = False
374
375        self._color_buttons = {}
376
377        self._setup_ui()
378
379        self.set_title("Creative Studio")
380        self.set_default_size(1200, 800)
381
382    def _initialize_document_data(self):
383        """Initialize document metadata."""
384        self._document_data = {
385            'created': datetime.now().isoformat(),
386            'modified': datetime.now().isoformat(),
387            'version': '2.0',
388            'author': 'Creative User',
389            'title': 'Untitled Creation',
390            'element_count': 0,
391            'last_tool': 'brush'
392        }
393
394    def _setup_ui(self):
395        """Set up the user interface."""
396        self._create_toolbar()
397        self._create_canvas()
398
399    def _create_toolbar(self):
400        """Create the activity toolbar with creative tools."""
401        toolbar_box = ToolbarBox()
402
403        # Activity button
404        activity_button = ActivityToolbarButton(self)
405        toolbar_box.toolbar.append(activity_button)
406
407        # Separator
408        separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
409        toolbar_box.toolbar.append(separator)
410
411        # Tool selection
412        tools_label = Gtk.Label()
413        tools_label.set_markup("<span color='white' weight='bold'>Tools:</span>")
414        toolbar_box.toolbar.append(tools_label)
415
416        # Brush tool
417        brush_btn = Gtk.ToggleButton()
418        brush_btn.set_label("Brush")
419        brush_btn.set_active(True)
420        brush_btn.connect('toggled', lambda btn: self._tool_selected(btn, 'brush'))
421        toolbar_box.toolbar.append(brush_btn)
422        self._brush_btn = brush_btn
423
424        # Eraser tool
425        eraser_btn = Gtk.ToggleButton()
426        eraser_btn.set_label("Eraser")
427        eraser_btn.connect('toggled', lambda btn: self._tool_selected(btn, 'eraser'))
428        toolbar_box.toolbar.append(eraser_btn)
429        self._eraser_btn = eraser_btn
430
431        # Line tool
432        line_btn = Gtk.ToggleButton()
433        line_btn.set_label("Line")
434        line_btn.connect('toggled', lambda btn: self._tool_selected(btn, 'line'))
435        toolbar_box.toolbar.append(line_btn)
436        self._line_btn = line_btn
437
438        # Rectangle tool
439        rect_btn = Gtk.ToggleButton()
440        rect_btn.set_label("Rectangle")
441        rect_btn.connect('toggled', lambda btn: self._tool_selected(btn, 'rectangle'))
442        toolbar_box.toolbar.append(rect_btn)
443        self._rect_btn = rect_btn
444
445        # Circle tool
446        circle_btn = Gtk.ToggleButton()
447        circle_btn.set_label("Circle")
448        circle_btn.connect('toggled', lambda btn: self._tool_selected(btn, 'circle'))
449        toolbar_box.toolbar.append(circle_btn)
450        self._circle_btn = circle_btn
451
452        # Separator
453        separator2 = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
454        toolbar_box.toolbar.append(separator2)
455
456        # Fill mode toggle
457        fill_btn = Gtk.ToggleButton()
458        fill_btn.set_label("Fill Mode")
459        fill_btn.connect('toggled', self._fill_mode_toggled)
460        toolbar_box.toolbar.append(fill_btn)
461        self._fill_btn = fill_btn
462
463        # Brush size
464        size_label = Gtk.Label()
465        size_label.set_markup("<span color='white' weight='bold'>Size:</span>")
466        toolbar_box.toolbar.append(size_label)
467
468        size_adjustment = Gtk.Adjustment(value=3, lower=1, upper=50, step_increment=1)
469        size_spin = Gtk.SpinButton()
470        size_spin.set_adjustment(size_adjustment)
471        size_spin.connect('value-changed', self._brush_size_changed)
472        toolbar_box.toolbar.append(size_spin)
473
474        # Separator
475        separator3 = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
476        toolbar_box.toolbar.append(separator3)
477
478        colors_label = Gtk.Label()
479        colors_label.set_markup("<span color='white' weight='bold'>Colors:</span>")
480        toolbar_box.toolbar.append(colors_label)
481
482        colors = [
483            ("Black", (0, 0, 0)),
484            ("Red", (1, 0, 0)),
485            ("Blue", (0, 0, 1)),
486            ("Green", (0, 0.8, 0)),
487            ("Yellow", (1, 1, 0)),
488            ("Purple", (0.8, 0, 0.8))
489        ]
490
491        for color_name, color_value in colors:
492            color_btn = Gtk.Button()
493            color_btn.set_tooltip_text(f"Select {color_name}")
494
495            color_box = Gtk.Box()
496            color_box.set_orientation(Gtk.Orientation.VERTICAL)
497            color_box.set_spacing(2)
498
499            # Create colored rectangle
500            color_area = Gtk.DrawingArea()
501            color_area.set_size_request(60, 20)
502
503            def draw_color(area, cr, width, height, color_val=color_value):
504                cr.set_source_rgb(*color_val)
505                cr.paint()
506                cr.set_source_rgb(0, 0, 0)
507                cr.set_line_width(1)
508                cr.rectangle(0.5, 0.5, width-1, height-1)
509                cr.stroke()
510
511            color_area.set_draw_func(draw_color)
512
513            color_label = Gtk.Label()
514            color_label.set_markup(f"<span color='black' size='small' weight='bold'>{color_name}</span>")
515
516            color_box.append(color_area)
517            color_box.append(color_label)
518            color_btn.set_child(color_box)
519
520            css_provider = Gtk.CssProvider()
521            css = "button { background-color: #2a2a2a; border: 1px solid #555; padding: 4px; }"
522            css_provider.load_from_data(css.encode())
523            color_btn.get_style_context().add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
524
525            color_btn.connect('clicked', lambda btn, c=color_value, name=color_name: self._color_selected(c, name))
526            toolbar_box.toolbar.append(color_btn)
527            self._color_buttons[color_value] = color_btn
528
529        self._highlight_color_button((0, 0, 0))
530
531        # Set up application accelerators for keyboard shortcuts
532        self._setup_accelerators()
533
534        separator4 = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
535        toolbar_box.toolbar.append(separator4)
536
537        # Action buttons
538        # Undo
539        undo_btn = Gtk.Button()
540        undo_btn.set_label("Undo")
541        undo_btn.connect('clicked', lambda btn: self._undo_action())
542        toolbar_box.toolbar.append(undo_btn)
543
544        # Redo
545        redo_btn = Gtk.Button()
546        redo_btn.set_label("Redo")
547        redo_btn.connect('clicked', lambda btn: self._redo_action())
548        toolbar_box.toolbar.append(redo_btn)
549
550        # Clear
551        clear_btn = Gtk.Button()
552        clear_btn.set_label("Clear")
553        clear_btn.connect('clicked', lambda btn: self.clear_canvas())
554        toolbar_box.toolbar.append(clear_btn)
555
556        # Separator
557        separator5 = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
558        toolbar_box.toolbar.append(separator5)
559
560        # Save
561        save_btn = Gtk.Button()
562        save_btn.set_label("Save")
563        save_btn.connect('clicked', lambda btn: self.save_creation())
564        toolbar_box.toolbar.append(save_btn)
565
566        # Preview
567        preview_btn = Gtk.Button()
568        preview_btn.set_label("Preview")
569        preview_btn.connect('clicked', lambda btn: self.show_preview())
570        toolbar_box.toolbar.append(preview_btn)
571
572        # Spacer
573        spacer = Gtk.Box()
574        spacer.set_hexpand(True)
575        toolbar_box.toolbar.append(spacer)
576
577        # Stop button
578        stop_button = StopButton(self)
579        toolbar_box.toolbar.append(stop_button)
580
581        self.set_toolbar_box(toolbar_box)
582
583    def _highlight_color_button(self, color):
584        """Highlight the selected color button."""
585        for btn in self._color_buttons.values():
586            btn.remove_css_class("suggested-action")
587
588        if color in self._color_buttons:
589            self._color_buttons[color].add_css_class("suggested-action")
590
591    def _tool_selected(self, button, tool):
592        """Handle tool selection."""
593        if button.get_active():
594            # Deactivate other tool buttons
595            tool_buttons = [self._brush_btn, self._eraser_btn, self._line_btn,
596                          self._rect_btn, self._circle_btn]
597            for btn in tool_buttons:
598                if btn != button:
599                    btn.set_active(False)
600
601            self._creative_canvas.set_tool(tool)
602            self._current_tool = tool
603            self._status_label.set_text(f"Selected tool: {tool.title()}")
604
605    def _fill_mode_toggled(self, button):
606        """Handle fill mode toggle."""
607        fill_mode = button.get_active()
608        self._creative_canvas.set_fill_mode(fill_mode)
609        mode_text = "Fill" if fill_mode else "Outline"
610        self._status_label.set_text(f"Shape mode: {mode_text}")
611
612    def _brush_size_changed(self, spin_button):
613        """Handle brush size change."""
614        size = int(spin_button.get_value())
615        self._creative_canvas.set_brush_size(size)
616        self._status_label.set_text(f"Brush size: {size}")
617
618    def _color_selected(self, color, color_name=None):
619        """Handle color selection."""
620        self._creative_canvas.set_color(color)
621        self._current_color = color
622        self._highlight_color_button(color)
623
624        if color_name is None:
625            color_names = {
626                (0, 0, 0): "Black",
627                (1, 0, 0): "Red",
628                (0, 0, 1): "Blue",
629                (0, 0.8, 0): "Green",
630                (1, 1, 0): "Yellow",
631                (0.8, 0, 0.8): "Purple"
632            }
633            color_name = color_names.get(color, "Custom")
634
635        self._status_label.set_text(f"Selected color: {color_name}")
636
637        # Give focus back to canvas for keyboard shortcuts
638        self._creative_canvas.grab_focus()
639
640    def _undo_action(self):
641        """Handle undo action."""
642        if self._creative_canvas.undo():
643            self._status_label.set_text("Undid last action")
644            self._has_unsaved_changes = True
645        else:
646            self._status_label.set_text("Nothing to undo")
647
648    def _redo_action(self):
649        """Handle redo action."""
650        if self._creative_canvas.redo():
651            self._status_label.set_text("Redid last action")
652            self._has_unsaved_changes = True
653        else:
654            self._status_label.set_text("Nothing to redo")
655
656    def _on_canvas_change(self):
657        """Called when canvas content changes."""
658        self._has_unsaved_changes = True
659        self._update_doc_info()
660
661    def _setup_accelerators(self):
662        """Set up application-level keyboard accelerators."""
663        # Create event controller for window-level shortcuts
664        key_controller = Gtk.EventControllerKey()
665
666        def on_key_pressed(controller, keyval, keycode, state):
667            if state & Gdk.ModifierType.CONTROL_MASK:
668                if keyval == Gdk.KEY_z or keyval == Gdk.KEY_Z:
669                    self._undo_action()
670                    return True
671                elif keyval == Gdk.KEY_y or keyval == Gdk.KEY_Y:
672                    self._redo_action()
673                    return True
674                elif keyval == Gdk.KEY_s or keyval == Gdk.KEY_S:
675                    self.save_creation()
676                    return True
677            return False
678
679        key_controller.connect('key-pressed', on_key_pressed)
680        self.add_controller(key_controller)
681
682    def _create_canvas(self):
683        """Create the main canvas area."""
684        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
685        main_box.set_margin_top(12)
686        main_box.set_margin_bottom(12)
687        main_box.set_margin_start(12)
688        main_box.set_margin_end(12)
689
690        # Title
691        title_label = Gtk.Label()
692        title_label.set_markup("<span size='large' weight='bold'>Creative Studio</span>")
693        title_label.set_halign(Gtk.Align.CENTER)
694        main_box.append(title_label)
695
696        # Status area
697        self._status_label = Gtk.Label()
698        self._status_label.set_text("Welcome to Creative Studio! Select a tool and start creating.")
699        self._status_label.set_halign(Gtk.Align.CENTER)
700        self._status_label.set_wrap(True)
701        main_box.append(self._status_label)
702
703        # Creative area
704        canvas_frame = Gtk.Frame()
705        canvas_frame.set_label("Creative Canvas")
706
707        self._creative_canvas = CreativeCanvas()
708        self._creative_canvas.set_change_callback(self._on_canvas_change)
709        self._creative_canvas.set_save_callback(self.save_creation)
710
711        # Make canvas focusable and give it initial focus
712        self._creative_canvas.set_can_focus(True)
713        self._creative_canvas.grab_focus()
714
715        canvas_frame.set_child(self._creative_canvas)
716        main_box.append(canvas_frame)
717
718        # Info area
719        info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
720
721        doc_frame = Gtk.Frame()
722        doc_frame.set_label("Project Info")
723
724        self._doc_info_label = Gtk.Label()
725        self._update_doc_info()
726        self._doc_info_label.set_margin_top(6)
727        self._doc_info_label.set_margin_bottom(6)
728        self._doc_info_label.set_margin_start(6)
729        self._doc_info_label.set_margin_end(6)
730
731        doc_frame.set_child(self._doc_info_label)
732        info_box.append(doc_frame)
733
734        main_box.append(info_box)
735
736        self.set_canvas(main_box)
737
738    def _update_doc_info(self):
739        """Update document info display."""
740        if hasattr(self, '_doc_info_label'):
741            element_count = len(self._creative_canvas._elements) if hasattr(self, '_creative_canvas') else 0
742            save_status = "Unsaved changes" if self._has_unsaved_changes else "Saved"
743
744            text = f"Created: {self._document_data['created'][:19]}\n"
745            text += f"Modified: {self._document_data['modified'][:19]}\n"
746            text += f"Elements: {element_count}\n"
747            text += f"Status: {save_status}\n"
748            text += f"Current Tool: {self._current_tool.title()}"
749            self._doc_info_label.set_text(text)
750
751    def clear_canvas(self):
752        """Clear the creative canvas."""
753        self._creative_canvas.clear_canvas()
754        self._document_data['modified'] = datetime.now().isoformat()
755        self._update_doc_info()
756        self._status_label.set_text("Canvas cleared")
757
758    def save_creation(self):
759        """Save the current creation."""
760        try:
761            self._document_data['modified'] = datetime.now().isoformat()
762            self._document_data['element_count'] = len(self._creative_canvas._elements)
763            self._document_data['last_tool'] = self._current_tool
764
765            # In a real Sugar activity, this would use the activity's write_file method
766            # For demo purposes, we'll save to a temp location
767            save_path = "/tmp/creative_studio_save.json"
768
769            canvas_data = self._creative_canvas.get_canvas_data()
770            data = {
771                'document_data': self._document_data,
772                'canvas_data': canvas_data,
773                'current_tool': self._current_tool,
774                'current_color': self._current_color,
775                'saved_at': datetime.now().isoformat()
776            }
777
778            with open(save_path, 'w') as f:
779                json.dump(data, f, indent=2)
780
781            self._has_unsaved_changes = False
782            self._update_doc_info()
783            self._status_label.set_text(f"Creation saved successfully!")
784
785        except Exception as e:
786            logging.error(f"Error saving creation: {e}")
787            self._status_label.set_text(f"Error saving: {e}")
788
789    def show_preview(self):
790        """Show a preview of the current creation."""
791        try:
792            preview_data = self.get_preview()
793            if preview_data:
794                # Save preview to temp file
795                preview_path = "/tmp/creative_studio_preview.png"
796                with open(preview_path, 'wb') as f:
797                    f.write(preview_data)
798
799                # Show preview dialog
800                self._show_preview_dialog(preview_path)
801                self._status_label.set_text("Preview shown")
802            else:
803                self._status_label.set_text("No content to preview")
804
805        except Exception as e:
806            logging.error(f"Error showing preview: {e}")
807            self._status_label.set_text(f"Error showing preview: {e}")
808
809    def _show_preview_dialog(self, image_path):
810        """Show preview image in a dialog."""
811        dialog = Gtk.Dialog()
812        dialog.set_title("Creation Preview")
813        dialog.set_transient_for(self)
814        dialog.set_modal(True)
815        dialog.set_default_size(850, 650)
816
817        dialog.add_button("Close", Gtk.ResponseType.CLOSE)
818
819        try:
820            # Create a scrolled window for the image
821            scrolled = Gtk.ScrolledWindow()
822            scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
823            scrolled.set_margin_top(10)
824            scrolled.set_margin_bottom(10)
825            scrolled.set_margin_start(10)
826            scrolled.set_margin_end(10)
827
828            image = Gtk.Image()
829            image.set_from_file(image_path)
830
831            scrolled.set_child(image)
832            dialog.get_content_area().append(scrolled)
833            dialog.present()
834
835            def on_response(dialog, response_id):
836                dialog.destroy()
837
838            dialog.connect('response', on_response)
839
840        except Exception as e:
841            logging.error(f"Error loading preview image: {e}")
842            dialog.destroy()
843
844    def get_preview(self):
845        """Generate a preview image of the current creation."""
846        try:
847            import cairo
848
849            preview_width, preview_height = 1200, 800
850            surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, preview_width, preview_height)
851            cr = cairo.Context(surface)
852
853            cr.set_source_rgb(1, 1, 1)
854            cr.paint()
855
856            cr.set_source_rgb(0.8, 0.8, 0.8)
857            cr.set_line_width(2)
858            cr.rectangle(2, 2, preview_width - 4, preview_height - 4)
859            cr.stroke()
860
861            if hasattr(self, '_creative_canvas') and self._creative_canvas._elements:
862                # Scale the creation to fit the preview
863                canvas_width, canvas_height = 800, 600
864                scale_x = (preview_width - 20) / canvas_width
865                scale_y = (preview_height - 20) / canvas_height
866                scale = min(scale_x, scale_y)
867
868                cr.save()
869                cr.translate(10, 10)
870                cr.scale(scale, scale)
871
872                # Draw all elements
873                for element in self._creative_canvas._elements:
874                    self._creative_canvas._draw_element(cr, element)
875
876                cr.restore()
877            else:
878                # No content, show placeholder
879                cr.set_source_rgb(0.5, 0.5, 0.5)
880                cr.select_font_face("Sans", 0, 0)
881                cr.set_font_size(24)
882
883                text = "Creative Studio"
884                text_extents = cr.text_extents(text)
885                x = (preview_width - text_extents.width) / 2
886                y = preview_height / 2 - 10
887
888                cr.move_to(x, y)
889                cr.show_text(text)
890
891                cr.set_font_size(16)
892                text2 = "Create something amazing!"
893                text_extents2 = cr.text_extents(text2)
894                x2 = (preview_width - text_extents2.width) / 2
895                y2 = y + 40
896
897                cr.move_to(x2, y2)
898                cr.show_text(text2)
899
900            # Convert to PNG
901            import io
902            preview_str = io.BytesIO()
903            surface.write_to_png(preview_str)
904            return preview_str.getvalue()
905
906        except Exception as e:
907            logging.error(f"Error generating preview: {e}")
908            return None
909
910    def read_file(self, file_path):
911        """Read creation data from file."""
912        try:
913            with open(file_path, 'r') as f:
914                data = json.load(f)
915
916            self._document_data = data.get('document_data', self._document_data)
917            canvas_data = data.get('canvas_data', {})
918            self._creative_canvas.set_canvas_data(canvas_data)
919
920            self._current_tool = data.get('current_tool', 'brush')
921            self._current_color = tuple(data.get('current_color', (0, 0, 0)))
922
923            self._has_unsaved_changes = False
924            self._update_doc_info()
925            self._status_label.set_text("Creation loaded successfully")
926
927        except Exception as e:
928            logging.error(f"Error reading file: {e}")
929            self._status_label.set_text(f"Error loading creation: {e}")
930
931    def write_file(self, file_path):
932        """Write creation data to file."""
933        try:
934            self._document_data['modified'] = datetime.now().isoformat()
935            self._document_data['element_count'] = len(self._creative_canvas._elements)
936
937            canvas_data = self._creative_canvas.get_canvas_data()
938            data = {
939                'document_data': self._document_data,
940                'canvas_data': canvas_data,
941                'current_tool': self._current_tool,
942                'current_color': self._current_color,
943                'activity_id': self.get_id(),
944                'bundle_id': self.get_bundle_id(),
945                'saved_at': datetime.now().isoformat()
946            }
947
948            with open(file_path, 'w') as f:
949                json.dump(data, f, indent=2)
950
951            self._has_unsaved_changes = False
952            logging.info(f"Creative studio data saved to {file_path}")
953
954        except Exception as e:
955            logging.error(f"Error writing file: {e}")
956            raise
957
958    def can_close(self):
959        """Check if the activity can be closed."""
960        return True
961
962
963class CreativeStudioApplication(Gtk.Application):
964    """Application wrapper for the creative studio activity."""
965
966    def __init__(self):
967        super().__init__(application_id='org.sugarlabs.CreativeStudio',
968                         flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
969        self.activity = None
970
971    def do_activate(self):
972        """Activate the application."""
973        if not self.activity:
974            handle = ActivityHandle('creative-studio-123')
975            self.activity = CreativeStudioActivity(handle, application=self)
976            self.activity.present()
977
978
979def main():
980    """Main entry point."""
981    logging.basicConfig(level=logging.DEBUG)
982    app = CreativeStudioApplication()
983    return app.run(sys.argv)
984
985
986if __name__ == "__main__":
987    main()

Hello_World_Dodge Example

A simple dodging ball game made with new toolkit.

  1"""
  2Hello World Dodge! - Animated Game Demo for sugar-toolkit-gtk4
  3
  4- Move the "Hello World!" ball with arrow keys, WASD, or buttons.
  5- Ball moves smoothly, bounces off walls (increasing speed), and changes color.
  6- Avoid obstacles, reach the goal to score!
  7- Uses: Toolbox, ToolButton, Icon, XoColor, style
  8
  9"""
 10
 11import sys
 12import os
 13import random
 14import math
 15
 16sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
 17
 18import gi
 19gi.require_version("Gtk", "4.0")
 20gi.require_version("Gdk", "4.0")
 21from gi.repository import Gtk, Gdk, GLib
 22
 23from sugar.activity import SimpleActivity
 24from sugar.graphics.toolbox import Toolbox
 25from sugar.graphics.toolbutton import ToolButton
 26from sugar.graphics.icon import Icon
 27from sugar.graphics.xocolor import XoColor
 28from sugar.graphics import style
 29
 30BALL_RADIUS = 28
 31GOAL_RADIUS = 20
 32OBSTACLE_RADIUS = 22
 33BALL_INIT_SPEED = 3.0
 34BALL_MAX_SPEED = 50.0
 35BALL_SPEED_INC = 0.7
 36OBSTACLE_COUNT = 3
 37
 38class HelloWorldDodgeActivity(SimpleActivity):
 39    """Animated Hello World Dodge Game."""
 40
 41    def __init__(self):
 42        super().__init__()
 43        self.set_title("Hello World Dodge!")
 44        self._create_content()
 45
 46    def _create_content(self):
 47        css_provider = Gtk.CssProvider()
 48        css_provider.load_from_data(b"""
 49            * { color: #000000; }
 50            .game-btn {
 51                background: #e0e0e0;
 52                border-radius: 16px;
 53                border: 2px solid #888;
 54                padding: 8px 16px;
 55                margin: 2px;
 56                transition: background 150ms, border-color 150ms;
 57            }
 58            .game-btn:hover {
 59                background: #b0e0ff;
 60                border-color: #0077cc;
 61            }
 62            .score-label {
 63                font-weight: bold;
 64                font-size: 18pt;
 65            }
 66            .header-label {
 67                font-weight: bold;
 68                font-size: 22pt;
 69            }
 70            .instructions-label {
 71                font-size: 13pt;
 72                color: #222;
 73            }
 74            .center-box {
 75                margin-left: auto;
 76                margin-right: auto;
 77            }
 78        """)
 79        Gtk.StyleContext.add_provider_for_display(
 80            Gdk.Display.get_default(), # type: ignore
 81            css_provider,
 82            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
 83        )
 84
 85        # Main vertical box
 86        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING)
 87        main_box.set_margin_top(style.DEFAULT_SPACING)
 88        main_box.set_margin_bottom(style.DEFAULT_SPACING)
 89        main_box.set_margin_start(style.DEFAULT_SPACING)
 90        main_box.set_margin_end(style.DEFAULT_SPACING)
 91
 92        # Instructions
 93        self.instructions_label = Gtk.Label()
 94        self.instructions_label.set_wrap(True)
 95        self.instructions_label.set_justify(Gtk.Justification.CENTER)
 96        self.instructions_label.set_margin_bottom(style.DEFAULT_SPACING // 2)
 97        self.instructions_label.set_markup(
 98            "<span size='large' weight='bold'>How to Play:</span>\n"
 99            "<span size='medium'>Move the ball with arrow keys, WASD, or the on-screen buttons. "
100            "Reach the <b>green</b> goal, avoid <b>red</b> obstacles. "
101            "Press <b>Reset</b> to restart. Each wall bounce increases speed!</span>"
102        )
103        self.instructions_label.get_style_context().add_class("instructions-label")
104        main_box.append(self.instructions_label)
105
106        # Welcome and Score
107        self.header_label = Gtk.Label()
108        self.header_label.set_markup("<span size='xx-large' weight='bold'>Sugar Ball Dodge!</span>")
109        self.header_label.set_margin_bottom(style.DEFAULT_SPACING // 2)
110        self.header_label.get_style_context().add_class("header-label")
111        main_box.append(self.header_label)
112
113        self.score = 0
114        self.score_label = Gtk.Label(label="Score: 0")
115        self.score_label.set_margin_bottom(style.DEFAULT_SPACING)
116        self.score_label.get_style_context().add_class("score-label")
117        main_box.append(self.score_label)
118
119
120        # Ball Name ( Default to Hello World Lol)
121        name_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
122        name_label = Gtk.Label(label="Your Name:")
123        self.name_entry = Gtk.Entry()
124        self.name_entry.set_placeholder_text("Enter your name")
125        self.name_entry.set_max_length(16)
126        self.name_entry.set_width_chars(12)
127        self.name_entry.set_text("Hello World!")
128        self.name_entry.connect("changed", self._on_name_changed)
129        name_box.append(name_label)
130        name_box.append(self.name_entry)
131        main_box.append(name_box)
132
133
134
135        # Toolbar with movement buttons, pause, and reset, centered
136        toolbox = Toolbox()
137        toolbar_outer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
138        toolbar_outer.set_halign(Gtk.Align.CENTER)
139        toolbar_outer.set_hexpand(True)
140
141        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=style.DEFAULT_SPACING)
142        toolbar.set_halign(Gtk.Align.CENTER)
143        toolbar.set_hexpand(False)
144
145        btn_left = ToolButton(tooltip="Left")
146        btn_left.set_icon_widget(Icon(icon_name="go-left", pixel_size=36))
147        btn_left.get_style_context().add_class("game-btn")
148        btn_right = ToolButton(tooltip="Right")
149        btn_right.set_icon_widget(Icon(icon_name="go-right", pixel_size=36))
150        btn_right.get_style_context().add_class("game-btn")
151        btn_up = ToolButton(tooltip="Up")
152        btn_up.set_icon_widget(Icon(icon_name="go-up", pixel_size=36))
153        btn_up.get_style_context().add_class("game-btn")
154        btn_down = ToolButton(tooltip="Down")
155        btn_down.set_icon_widget(Icon(icon_name="go-down", pixel_size=36))
156        btn_down.get_style_context().add_class("game-btn")
157        toolbar.append(btn_left)
158        toolbar.append(btn_up)
159        toolbar.append(btn_down)
160        toolbar.append(btn_right)
161
162        # Pause
163        self.btn_pause = ToolButton(tooltip="Pause/Resume")
164        self.btn_pause.set_icon_widget(Icon(icon_name="media-playback-pause", pixel_size=36))
165        self.btn_pause.get_style_context().add_class("game-btn")
166        self.btn_pause.connect("clicked", self._toggle_pause)
167        toolbar.append(self.btn_pause)
168
169        # Reset
170        btn_reset = ToolButton(tooltip="Reset")
171        btn_reset.set_icon_widget(Icon(icon_name="document-open", pixel_size=36))
172        btn_reset.get_style_context().add_class("game-btn")
173        toolbar.append(btn_reset)
174
175        toolbar_outer.append(toolbar)
176        toolbox.add_toolbar("Controls", toolbar_outer)
177        main_box.append(toolbox)
178
179        # Main Game Area
180        area_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
181        area_box.set_hexpand(True)
182        area_box.set_vexpand(True)
183        area_box.set_halign(Gtk.Align.CENTER)
184        area_box.set_valign(Gtk.Align.CENTER)
185
186        # Frame
187        frame = Gtk.Frame()
188        frame.set_margin_top(10)
189        frame.set_margin_bottom(10)
190        frame.set_margin_start(10)
191        frame.set_margin_end(10)
192
193        self.area = Gtk.DrawingArea()
194        self.area.set_content_width(800)
195        self.area.set_content_height(600)
196        self.area.set_hexpand(False)
197        self.area.set_vexpand(False)
198        self.area.set_halign(Gtk.Align.CENTER)
199        self.area.set_valign(Gtk.Align.CENTER)
200        self.area.set_draw_func(self._draw_area)
201        frame.set_child(self.area)
202
203        # Scrolled Window
204        scrolled = Gtk.ScrolledWindow()
205        scrolled.set_child(frame)
206        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
207        scrolled.set_propagate_natural_width(True)
208        scrolled.set_propagate_natural_height(True)
209        area_box.append(scrolled)
210        main_box.append(area_box)
211
212        # Status ( Instructions )
213        self.status_label = Gtk.Label(label="Use arrow keys, WASD, or buttons to move the ball! Get the green goal, avoid red obstacles.")
214        self.status_label.set_margin_top(style.DEFAULT_SPACING)
215        main_box.append(self.status_label)
216
217        self.set_canvas(main_box)
218        self.set_default_size(1600, 1100)
219
220        # Ball state
221        self.ball_pos = [700.0, 450.0]
222        self.ball_radius = BALL_RADIUS
223        self.ball_color = XoColor()
224        self.ball_text = self.name_entry.get_text()
225        self.ball_velocity = [BALL_INIT_SPEED, 0.0]
226        self.ball_speed = BALL_INIT_SPEED
227
228
229        # Goal and obstacles
230        self.goal_pos = self._random_pos(GOAL_RADIUS)
231        self.obstacles = [self._random_pos(OBSTACLE_RADIUS) for _ in range(OBSTACLE_COUNT)]
232
233        self.animating = False
234        self.running = True
235
236        # Keyboard controls
237        key_controller = Gtk.EventControllerKey()
238        key_controller.connect("key-pressed", self._on_key_pressed)
239        self.add_controller(key_controller)
240
241        # Button controls
242        btn_left.connect("clicked", lambda b: self._set_direction(-1, 0))
243        btn_right.connect("clicked", lambda b: self._set_direction(1, 0))
244        btn_up.connect("clicked", lambda b: self._set_direction(0, -1))
245        btn_down.connect("clicked", lambda b: self._set_direction(0, 1))
246        btn_reset.connect("clicked", lambda b: self._reset_game())
247
248
249        # Start game loop
250        GLib.timeout_add(16, self._game_tick)  # 60 FPS
251
252
253    def _draw_area(self, area, cr, width, height):
254        # Draw goal
255        cr.save()
256        cr.set_source_rgb(0.2, 0.8, 0.2)
257        cr.arc(self.goal_pos[0], self.goal_pos[1], GOAL_RADIUS, 0, 2*math.pi)
258        cr.fill()
259        cr.restore()
260
261        # Draw obstacles
262        for ox, oy in self.obstacles:
263            cr.save()
264            cr.set_source_rgb(0.85, 0.1, 0.1)
265            cr.arc(ox, oy, OBSTACLE_RADIUS, 0, 2*math.pi)
266            cr.fill()
267            cr.restore()
268
269        # Draw ball with current color and position
270        r, g, b = self._hex_to_rgb(self.ball_color.get_fill_color())
271        cr.save()
272        cr.set_source_rgb(r, g, b)
273        cr.arc(self.ball_pos[0], self.ball_pos[1], self.ball_radius, 0, 2*math.pi)
274        cr.fill()
275        cr.restore()
276
277        # Draw text centered in the ball
278        cr.save()
279        cr.set_source_rgb(0, 0, 0)
280        cr.select_font_face("Sans", 0, 0)
281        cr.set_font_size(16)
282        text = self.ball_text
283        xbearing, ybearing, tw, th, xadv, yadv = cr.text_extents(text)
284        cr.move_to(self.ball_pos[0] - tw/2, self.ball_pos[1] + th/2)
285        cr.show_text(text)
286        cr.restore()
287
288        # Draw boundary rectangle (border)
289        cr.save()
290        cr.set_line_width(6)
291        cr.set_source_rgb(0.2, 0.2, 0.2)
292        cr.rectangle(3, 3, width - 6, height - 6)
293        cr.stroke()
294        cr.restore()
295
296
297    def _hex_to_rgb(self, hex_color):
298        hex_color = hex_color.lstrip("#")
299        return tuple(int(hex_color[i:i+2], 16)/255.0 for i in (0, 2, 4))
300
301    def _set_direction(self, dx, dy):
302        if not self.running:
303            return
304        speed = self.ball_speed
305        norm = math.hypot(dx, dy)
306        if norm == 0:
307            return
308        self.ball_velocity = [speed * dx / norm, speed * dy / norm]
309
310    def _on_name_changed(self, entry):
311        self.ball_text = entry.get_text()
312        # Pause the game when editing the name
313        if self.running:
314            self.running = False
315            self.btn_pause.set_icon_widget(Icon(icon_name="media-playback-start", pixel_size=36))
316            self.status_label.set_text("Paused for name entry. Click on Pause/Resume Button. Press Enter to Finish Typing!")
317        self.area.queue_draw()
318        # Connect Enter key to remove focus (finish editing)
319        entry.connect("activate", self._on_entry_activate)
320
321    def _on_entry_activate(self, entry):
322        # Remove focus from entry so user can resume game with keyboard
323        entry.get_root().set_focus(None)
324
325    def _toggle_pause(self, button):
326        if self.running:
327            self.running = False
328            self.btn_pause.set_icon_widget(Icon(icon_name="media-playback-start", pixel_size=36))
329            self.status_label.set_text("Paused. Click Pause/Resume or press 'p' to continue.")
330        else:
331            self.running = True
332            self.btn_pause.set_icon_widget(Icon(icon_name="media-playback-pause", pixel_size=36))
333            self.status_label.set_text("Game resumed!")
334
335    def _on_key_pressed(self, controller, keyval, keycode, state):
336        # Only handle keys if name_entry is not focused
337        if self.name_entry.has_focus():
338            return False
339        key = Gdk.keyval_name(keyval)
340        if key in ("Left", "a", "A"):
341            self._set_direction(-1, 0)
342        elif key in ("Right", "d", "D"):
343            self._set_direction(1, 0)
344        elif key in ("Up", "w", "W"):
345            self._set_direction(0, -1)
346        elif key in ("Down", "s", "S"):
347            self._set_direction(0, 1)
348        elif key == "r":
349            self._reset_game()
350        elif key in ("Return", "KP_Enter", "Enter"):
351            self._reset_game()
352        elif key in ("p", "P"):
353            self._toggle_pause(None)
354        return True
355
356    def _random_pos(self, radius):
357        # TODO: Make sure they are away from the ball slightly along with velocity accomodation so direct hits are avoided
358        width = self.area.get_content_width()
359        height = self.area.get_content_height()
360        return [
361            random.uniform(radius + 10, width - radius - 10),
362            random.uniform(radius + 10, height - radius - 10),
363        ]
364
365    def _reset_game(self):
366        # self.ball_pos = [700.0, 450.0]
367        # TODO: start from width half, but this should be random?
368        self.ball_pos = [400.0,300.0]
369        self.ball_color = XoColor()
370        self.ball_velocity = [BALL_INIT_SPEED, 0.0]
371        self.ball_speed = BALL_INIT_SPEED
372        self.goal_pos = self._random_pos(GOAL_RADIUS)
373        self.obstacles = [self._random_pos(OBSTACLE_RADIUS) for _ in range(OBSTACLE_COUNT)]
374        self.score = 0
375        self.score_label.set_text("Score: 0")
376        self.status_label.set_text("Game reset! Use arrows, WASD, or buttons.")
377        self.running = True
378        self.btn_pause.set_icon_widget(Icon(icon_name="media-playback-pause", pixel_size=36))
379        self.area.queue_draw()
380
381    def _game_tick(self):
382        if not self.running:
383            return True
384        width = self.area.get_content_width()
385        height = self.area.get_content_height()
386        x, y = self.ball_pos
387        vx, vy = self.ball_velocity
388
389        # Move ball
390        x_new = x + vx
391        y_new = y + vy
392        bounced = False
393
394        # Bounce off walls, increase speed
395        if x_new - self.ball_radius < 0:
396            x_new = self.ball_radius
397            vx = abs(vx)
398            bounced = True
399        if x_new + self.ball_radius > width:
400            x_new = width - self.ball_radius
401            vx = -abs(vx)
402            bounced = True
403        if y_new - self.ball_radius < 0:
404            y_new = self.ball_radius
405            vy = abs(vy)
406            bounced = True
407        if y_new + self.ball_radius > height:
408            y_new = height - self.ball_radius
409            vy = -abs(vy)
410            bounced = True
411
412        if bounced:
413            self.ball_speed = min(self.ball_speed + BALL_SPEED_INC, BALL_MAX_SPEED)
414            norm = math.hypot(vx, vy)
415            if norm > 0:
416                vx = self.ball_speed * vx / norm
417                vy = self.ball_speed * vy / norm
418            self.ball_color = XoColor()
419            self.status_label.set_text("Bounced! Speed up!")
420        self.ball_pos = [x_new, y_new]
421        self.ball_velocity = [vx, vy]
422
423        # Check collision with goal
424        if self._distance(self.ball_pos, self.goal_pos) < self.ball_radius + GOAL_RADIUS:
425            self.score += 1
426            self.score_label.set_text(f"Score: {self.score}")
427            self.goal_pos = self._random_pos(GOAL_RADIUS)
428            self.status_label.set_text("Goal! +1 Score")
429            # Move obstacles too
430            self.obstacles = [self._random_pos(OBSTACLE_RADIUS) for _ in range(OBSTACLE_COUNT)]
431            self.area.queue_draw()
432            # IMP: Return to fix the overlap issue
433            return True
434
435        # ONLY after checking goal check obstacles
436        for ox, oy in self.obstacles:
437            if self._distance(self.ball_pos, [ox, oy]) < self.ball_radius + OBSTACLE_RADIUS:
438                self.status_label.set_text("Game Over! Hit an obstacle. Press Reset.")
439                self.running = False
440                return True
441
442        self.area.queue_draw()
443        return True
444
445    def _distance(self, a, b):
446        return math.hypot(a[0] - b[0], a[1] - b[1])
447
448def main():
449    app = Gtk.Application(application_id="org.sugarlabs.HelloWorldDodge")
450
451    def on_activate(app):
452        activity = HelloWorldDodgeActivity()
453        app.add_window(activity)
454        activity.present()
455
456    app.connect("activate", on_activate)
457    return app.run(sys.argv)
458
459if __name__ == "__main__":
460    main()

UI Components

Examples of various UI components:

Alert Example

 1import gi
 2gi.require_version('Gtk', '4.0')
 3from gi.repository import Gtk, GLib
 4import sys
 5import os
 6sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
 7from sugar.graphics.alert import Alert, ConfirmationAlert, ErrorAlert, TimeoutAlert, NotifyAlert
 8
 9class AlertExample(Gtk.ApplicationWindow):
10    def __init__(self, app):
11        super().__init__(application=app, title="Alert Example")
12        self.set_default_size(600, 400)
13        
14        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
15        vbox.set_margin_top(20)
16        vbox.set_margin_bottom(20)
17        vbox.set_margin_start(20)
18        vbox.set_margin_end(20)
19        self.set_child(vbox)
20        
21        self.alert_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
22        vbox.append(self.alert_box)
23        
24        btn_simple = Gtk.Button(label="Show Simple Alert")
25        btn_simple.connect("clicked", self.on_simple_alert)
26        vbox.append(btn_simple)
27        
28        btn_confirm = Gtk.Button(label="Show Confirmation Alert")
29        btn_confirm.connect("clicked", self.on_confirmation_alert)
30        vbox.append(btn_confirm)
31        
32        btn_timeout = Gtk.Button(label="Show Timeout Alert")
33        btn_timeout.connect("clicked", self.on_timeout_alert)
34        vbox.append(btn_timeout)
35
36    def on_simple_alert(self, button):
37        alert = Alert()
38        alert.props.title = "Simple Alert"
39        alert.props.msg = "This is a basic alert message."
40        alert.add_button(1, "OK")
41        alert.connect("response", self.on_alert_response)
42        self.alert_box.append(alert)
43
44    def on_confirmation_alert(self, button):
45        alert = ConfirmationAlert()
46        alert.props.title = "Confirm Action"
47        alert.props.msg = "Are you sure you want to continue?"
48        alert.connect("response", self.on_alert_response)
49        self.alert_box.append(alert)
50
51    def on_timeout_alert(self, button):
52        alert = TimeoutAlert(timeout=5)
53        alert.props.title = "Timeout Alert"
54        alert.props.msg = "This alert will disappear in 5 seconds."
55        alert.connect("response", self.on_alert_response)
56        self.alert_box.append(alert)
57
58    def on_alert_response(self, alert, response_id):
59        print(f"Alert response: {response_id}")
60        self.alert_box.remove(alert)
61
62class AlertApp(Gtk.Application):
63    def do_activate(self):
64        window = AlertExample(self)
65        window.present()
66
67if __name__ == "__main__":
68    app = AlertApp()
69    app.run()

Icon Example

  1"""Sugar GTK4 Icon Example - Complete Feature Demo."""
  2
  3import sys
  4import os
  5
  6sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
  7
  8import gi
  9
 10gi.require_version("Gtk", "4.0")
 11gi.require_version("Gdk", "4.0")
 12from gi.repository import Gtk, Gdk
 13
 14from sugar.activity import SimpleActivity
 15from sugar.graphics.icon import Icon, EventIcon, CanvasIcon
 16from sugar.graphics.xocolor import XoColor
 17
 18
 19class IconExampleActivity(SimpleActivity):
 20    """Example activity demonstrating all Sugar GTK4 icon features."""
 21
 22    def __init__(self):
 23        super().__init__()
 24        self.set_title("Sugar GTK4 Icon Example ")
 25        self._create_content()
 26
 27    def _create_content(self):
 28        """Create the main content showing all icon types and features."""
 29        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
 30        main_box.set_margin_start(20)
 31        main_box.set_margin_end(20)
 32        main_box.set_margin_top(20)
 33        main_box.set_margin_bottom(20)
 34        main_box.set_hexpand(True)
 35        main_box.set_vexpand(True)
 36
 37        # Scrolled window for all content
 38        scrolled = Gtk.ScrolledWindow()
 39        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
 40        scrolled.set_child(main_box)
 41        scrolled.set_hexpand(True)
 42        scrolled.set_vexpand(True)
 43
 44        # Add CSS provider for CanvasIcon hover/active states
 45        css_provider = Gtk.CssProvider()
 46        css_data = """
 47        .canvas-icon {
 48            background-color: transparent;
 49            border-radius: 8px;
 50            padding: 4px;
 51            transition: background-color 200ms ease;
 52        }
 53        .canvas-icon:hover {
 54            background-color: rgba(0, 0, 0, 0.15);
 55        }
 56        .canvas-icon:active {
 57            background-color: rgba(0, 0, 0, 0.25);
 58        }
 59        """
 60        try:
 61            css_provider.load_from_string(css_data)
 62            Gtk.StyleContext.add_provider_for_display(
 63                Gdk.Display.get_default(),
 64                css_provider,
 65                Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
 66            )
 67        except Exception as e:
 68            print(f"Warning: Could not load CSS provider: {e}")
 69
 70        # Title
 71        title = Gtk.Label()
 72        title.set_markup("<big><b>Sugar GTK4 Icon Examples - Complete</b></big>")
 73        title.set_hexpand(True)
 74        main_box.append(title)
 75
 76        # Add sections
 77        self._add_basic_icons(main_box)
 78        self._add_colored_icons(main_box)
 79        self._add_badge_icons(main_box)
 80        self._add_event_icons(main_box)
 81        self._add_canvas_icons(main_box)
 82        self._add_size_and_alpha_examples(main_box)
 83
 84        self.set_canvas(scrolled)
 85        self.set_default_size(900, 700)
 86
 87    def _add_basic_icons(self, container):
 88        """Add basic icon examples."""
 89        frame = Gtk.Frame(label="Basic Icons")
 90        frame.set_hexpand(True)
 91        box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
 92        box.set_margin_start(10)
 93        box.set_margin_end(10)
 94        box.set_margin_top(10)
 95        box.set_margin_bottom(10)
 96        box.set_hexpand(True)
 97        box.set_halign(Gtk.Align.CENTER)
 98
 99        # System icons
100        for icon_name in [
101            "document-new",
102            "document-open",
103            "document-save",
104            "edit-copy",
105            "edit-paste",
106        ]:
107            icon = Icon(icon_name=icon_name, pixel_size=48)
108            box.append(icon)
109
110        frame.set_child(box)
111        container.append(frame)
112
113    def _add_colored_icons(self, container):
114        """Add colored icon examples."""
115        frame = Gtk.Frame(label="Colored Icons")
116        frame.set_hexpand(True)
117        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
118        vbox.set_margin_start(10)
119        vbox.set_margin_end(10)
120        vbox.set_margin_top(10)
121        vbox.set_margin_bottom(10)
122        vbox.set_hexpand(True)
123
124        # XO Color examples using xotest.svg
125        hbox1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
126        hbox1.set_hexpand(True)
127        hbox1.set_halign(Gtk.Align.CENTER)
128        label1 = Gtk.Label(label="XO Colors (xotest.svg):")
129        label1.set_size_request(150, -1)
130        hbox1.append(label1)
131
132        xotest_svg = os.path.join(
133            os.path.dirname(__file__),
134            "..",
135            "src",
136            "sugar",
137            "graphics",
138            "icons",
139            "test.svg",
140        )
141        for i in range(3):
142            xo_color = XoColor.get_random_color()
143            icon = Icon(file_name=xotest_svg, pixel_size=48)
144            icon.set_xo_color(xo_color)
145            hbox1.append(icon)
146
147        vbox.append(hbox1)
148
149        # Manual color examples (still using xotest.svg)
150        hbox2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
151        hbox2.set_hexpand(True)
152        hbox2.set_halign(Gtk.Align.CENTER)
153        label2 = Gtk.Label(label="Manual Colors (xotest.svg):")
154        label2.set_size_request(150, -1)
155        hbox2.append(label2)
156
157        for fill, stroke in [
158            ("#FF0000", "#800000"),
159            ("#00FF00", "#008000"),
160            ("#0000FF", "#000080"),
161        ]:
162            icon = Icon(file_name=xotest_svg, pixel_size=48)
163            icon.set_fill_color(fill)
164            icon.set_stroke_color(stroke)
165            hbox2.append(icon)
166
167        vbox.append(hbox2)
168        frame.set_child(vbox)
169        container.append(frame)
170
171    def _add_badge_icons(self, container):
172        """Add badge icon examples."""
173        frame = Gtk.Frame(label="Badge Icons")
174        frame.set_hexpand(True)
175        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
176        vbox.set_margin_start(10)
177        vbox.set_margin_end(10)
178        vbox.set_margin_top(10)
179        vbox.set_margin_bottom(10)
180        vbox.set_hexpand(True)
181
182        # Info label
183        info_label = Gtk.Label(label="Icons with badges (small overlay icons):")
184        vbox.append(info_label)
185
186        # Badge examples
187        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
188        hbox.set_hexpand(True)
189        hbox.set_halign(Gtk.Align.CENTER)
190        badges = [
191            ("folder", "emblem-favorite"),
192            ("document-new", "emblem-important"),
193            ("network-wireless", "dialog-information"),
194        ]
195        for main_icon, badge_icon in badges:
196            vbox_item = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
197            vbox_item.set_halign(Gtk.Align.CENTER)
198            icon = Icon(icon_name=main_icon, pixel_size=64)
199            icon.set_badge_name(badge_icon)
200            icon.set_fill_color("#00AA00")
201            icon.set_stroke_color("#004400")
202            vbox_item.append(icon)
203            label = Gtk.Label(label=f"{main_icon}\n+ {badge_icon}")
204            label.set_justify(Gtk.Justification.CENTER)
205            vbox_item.append(label)
206            hbox.append(vbox_item)
207
208        vbox.append(hbox)
209        frame.set_child(vbox)
210        container.append(frame)
211
212    def _add_event_icons(self, container):
213        """Add event icon examples."""
214        frame = Gtk.Frame(label="Interactive Icons (EventIcon)")
215        frame.set_hexpand(True)
216        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
217        vbox.set_margin_start(10)
218        vbox.set_margin_end(10)
219        vbox.set_margin_top(10)
220        vbox.set_margin_bottom(10)
221        vbox.set_hexpand(True)
222
223        # Info label
224        info_label = Gtk.Label(label="Click these icons to see events:")
225        vbox.append(info_label)
226
227        # Status label
228        self.event_info = Gtk.Label(label="No events yet")
229        vbox.append(self.event_info)
230
231        # Event icons
232        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
233        hbox.set_hexpand(True)
234        hbox.set_halign(Gtk.Align.CENTER)
235
236        for i, icon_name in enumerate(
237            ["media-playback-start", "media-playback-pause", "media-playback-stop"]
238        ):
239            event_icon = EventIcon(icon_name=icon_name, pixel_size=64)
240            event_icon.connect("clicked", self._on_icon_clicked, icon_name)
241            event_icon.connect("pressed", self._on_icon_pressed, icon_name)
242            event_icon.connect("released", self._on_icon_released, icon_name)
243            event_icon.connect("activate", self._on_icon_activated, icon_name)
244            hbox.append(event_icon)
245
246        vbox.append(hbox)
247        frame.set_child(vbox)
248        container.append(frame)
249
250    def _add_canvas_icons(self, container):
251        """Add canvas icon examples with hover effects."""
252        frame = Gtk.Frame(label="Canvas Icons (Hover Effects)")
253        frame.set_hexpand(True)
254        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
255        vbox.set_margin_start(10)
256        vbox.set_margin_end(10)
257        vbox.set_margin_top(10)
258        vbox.set_margin_bottom(10)
259        vbox.set_hexpand(True)
260
261        info_label = Gtk.Label(label="Hover and click these icons for visual feedback:")
262        vbox.append(info_label)
263
264        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
265        hbox.set_hexpand(True)
266        hbox.set_halign(Gtk.Align.CENTER)
267
268        icons = [
269            ("system-search", "#FF8800", "#AA4400"),
270            ("edit-delete", "#FF0000", "#880000"),
271            ("dialog-information", "#0088FF", "#004488"),
272        ]
273        for icon_name, fill, stroke in icons:
274            # Create a wrapper box for the canvas icon to ensure proper CSS application
275            wrapper = Gtk.Box()
276            wrapper.add_css_class("canvas-icon")
277
278            canvas_icon = CanvasIcon(icon_name=icon_name, pixel_size=64)
279            canvas_icon.set_fill_color(fill)
280            canvas_icon.set_stroke_color(stroke)
281
282            wrapper.append(canvas_icon)
283            hbox.append(wrapper)
284
285        vbox.append(hbox)
286        frame.set_child(vbox)
287        container.append(frame)
288
289    def _add_size_and_alpha_examples(self, container):
290        """Add different size and transparency examples."""
291        frame = Gtk.Frame(label="Different Sizes and Transparency")
292        frame.set_hexpand(True)
293        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
294        hbox.set_margin_start(10)
295        hbox.set_margin_end(10)
296        hbox.set_margin_top(10)
297        hbox.set_margin_bottom(10)
298        hbox.set_hexpand(True)
299        hbox.set_halign(Gtk.Align.CENTER)
300
301        sizes = [16, 24, 32, 48, 64, 96]
302        for size in sizes:
303            vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
304            vbox.set_halign(Gtk.Align.CENTER)
305            icon = Icon(icon_name="applications-graphics", pixel_size=size)
306            vbox.append(icon)
307            label = Gtk.Label(label=f"{size}px")
308            vbox.append(label)
309            hbox.append(vbox)
310
311        # Transparency example
312        vbox_alpha = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
313        vbox_alpha.set_halign(Gtk.Align.CENTER)
314        label_alpha = Gtk.Label(label="Alpha (transparency):")
315        vbox_alpha.append(label_alpha)
316        for alpha in [1.0, 0.7, 0.4]:
317            icon = Icon(icon_name="applications-graphics", pixel_size=48)
318            icon.set_alpha(alpha)
319            vbox_alpha.append(icon)
320        hbox.append(vbox_alpha)
321
322        frame.set_child(hbox)
323        container.append(frame)
324
325    def _on_icon_clicked(self, icon, icon_name):
326        """Handle icon click events."""
327        self.event_info.set_text(f"Clicked: {icon_name}")
328
329    def _on_icon_pressed(self, icon, x, y, icon_name):
330        """Handle icon press events."""
331        self.event_info.set_text(f"Pressed: {icon_name} at ({x:.1f}, {y:.1f})")
332
333    def _on_icon_released(self, icon, x, y, icon_name):
334        """Handle icon release events."""
335        self.event_info.set_text(f"Released: {icon_name} at ({x:.1f}, {y:.1f})")
336
337    def _on_icon_activated(self, icon, icon_name):
338        """Handle icon activate events."""
339        self.event_info.set_text(f"Activated: {icon_name}")
340
341
342def main():
343    """Run the icon example activity."""
344    app = Gtk.Application(application_id="org.sugarlabs.IconExample")
345
346    def on_activate(app):
347        activity = IconExampleActivity()
348        app.add_window(activity)
349        activity.present()
350
351    app.connect("activate", on_activate)
352    return app.run(sys.argv)
353
354
355if __name__ == "__main__":
356    main()
357

Palette Example

  1"""
  2Complete Palette Demo
  3"""
  4
  5import gi
  6gi.require_version('Gtk', '4.0')
  7gi.require_version('Gdk', '4.0')
  8
  9import sys
 10import os
 11from gi.repository import Gtk
 12
 13sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
 14from sugar.graphics.palette import Palette
 15from sugar.graphics.palettewindow import (
 16    PaletteWindow, WidgetInvoker,  CursorInvoker,
 17)
 18from sugar.graphics.palettemenu import PaletteMenuItem, PaletteMenuItemSeparator
 19from sugar.graphics.palettegroup import get_group
 20from sugar.graphics.icon import Icon
 21from sugar.graphics import style
 22
 23
 24class PaletteDemo(Gtk.ApplicationWindow):
 25    """Main demo window showcasing all palette features."""
 26
 27    def __init__(self, app):
 28        super().__init__(application=app)
 29        self.set_title("Sugar Palette Complete Demo - GTK4")
 30        self.set_default_size(800, 600)
 31
 32        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
 33        main_box.set_margin_start(20)
 34        main_box.set_margin_end(20)
 35        main_box.set_margin_top(20)
 36        main_box.set_margin_bottom(20)
 37        self.set_child(main_box)
 38
 39        title = Gtk.Label()
 40        title.set_markup("<big><b>Sugar Palette Demo - GTK4</b></big>")
 41        main_box.append(title)
 42
 43        scrolled = Gtk.ScrolledWindow()
 44        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
 45        scrolled.set_vexpand(True)
 46        main_box.append(scrolled)
 47
 48        content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
 49        scrolled.set_child(content_box)
 50
 51        self._create_basic_palette_section(content_box)
 52        self._create_menu_palette_section(content_box)
 53        self._create_palette_window_section(content_box)
 54        self._create_invoker_section(content_box)
 55        self._create_treeview_section(content_box)
 56        self._create_palette_group_section(content_box)
 57
 58    def _create_section_header(self, parent, title, description):
 59        header_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
 60        parent.append(header_box)
 61
 62        title_label = Gtk.Label()
 63        title_label.set_markup(f"<b>{title}</b>")
 64        title_label.set_halign(Gtk.Align.START)
 65        header_box.append(title_label)
 66
 67        desc_label = Gtk.Label(label=description)
 68        desc_label.set_halign(Gtk.Align.START)
 69        desc_label.set_wrap(True)
 70        desc_label.add_css_class('dim-label')
 71        header_box.append(desc_label)
 72
 73        sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
 74        sep.set_margin_top(10)
 75        sep.set_margin_bottom(10)
 76        parent.append(sep)
 77
 78        return header_box
 79
 80    def _create_basic_palette_section(self, parent):
 81        section = self._create_section_header(
 82            parent,
 83            "Basic Palettes",
 84            "Basic palette widgets with text, icons, and content"
 85        )
 86
 87        demo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
 88        demo_box.set_margin_top(10)
 89        parent.append(demo_box)
 90
 91        simple_btn = Gtk.Button(label="Simple Palette")
 92        demo_box.append(simple_btn)
 93
 94        simple_palette = Palette(label="Simple Palette")
 95        simple_palette.props.secondary_text = "This is a simple palette with primary and secondary text."
 96
 97        close_btn1 = Gtk.Button(label="Close")
 98        close_btn1.connect('clicked', lambda btn: simple_palette.popdown(immediate=True))
 99        simple_palette.set_content(close_btn1)
100
101        simple_invoker = WidgetInvoker()
102        simple_invoker.attach(simple_btn)
103        simple_invoker.set_lock_palette(True)
104        simple_palette.set_invoker(simple_invoker)
105        simple_btn.connect('clicked', lambda btn: simple_palette.popup(immediate=True))
106
107        icon_btn = Gtk.Button(label="With Icon")
108        demo_box.append(icon_btn)
109
110        icon_palette = Palette(label="Palette with Icon")
111        icon_palette.props.secondary_text = "This palette includes an icon and action buttons."
112        icon_palette.set_icon(Icon(icon_name='dialog-information', pixel_size=style.STANDARD_ICON_SIZE))
113
114        close_btn2 = Gtk.Button(label="Close")
115        close_btn2.connect('clicked', lambda btn: icon_palette.popdown(immediate=True))
116        icon_palette.set_content(close_btn2)
117
118        icon_palette.action_bar.add_action("Action 1", "document-save")
119        icon_palette.action_bar.add_action("Action 2", "edit-copy")
120
121        icon_invoker = WidgetInvoker()
122        icon_invoker.attach(icon_btn)
123        icon_invoker.set_lock_palette(True)
124        icon_palette.set_invoker(icon_invoker)
125        icon_btn.connect('clicked', lambda btn: icon_palette.popup(immediate=True))
126
127        # custom content
128        content_btn = Gtk.Button(label="Custom Content")
129        demo_box.append(content_btn)
130
131        content_palette = Palette(label="Custom Content")
132        content_palette.props.secondary_text = "This palette contains custom widgets."
133
134        custom_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
135        custom_content.set_margin_start(10)
136        custom_content.set_margin_end(10)
137        custom_content.set_margin_top(5)
138        custom_content.set_margin_bottom(5)
139
140        entry = Gtk.Entry()
141        entry.set_placeholder_text("Type something...")
142        custom_content.append(entry)
143
144        scale = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 100, 1)
145        scale.set_value(50)
146        custom_content.append(scale)
147
148        check = Gtk.CheckButton(label="Enable feature")
149        custom_content.append(check)
150
151        close_btn3 = Gtk.Button(label="Close")
152        close_btn3.connect('clicked', lambda btn: content_palette.popdown(immediate=True))
153        custom_content.append(close_btn3)
154
155        content_palette.set_content(custom_content)
156
157        content_invoker = WidgetInvoker()
158        content_invoker.attach(content_btn)
159        content_invoker.set_lock_palette(True)
160        content_palette.set_invoker(content_invoker)
161        content_btn.connect('clicked', lambda btn: content_palette.popup(immediate=True))
162
163    def _create_menu_palette_section(self, parent):
164        """Create menu palette examples."""
165        section = self._create_section_header(
166            parent,
167            "Menu Palettes",
168            "Palettes that act as context menus with menu items"
169        )
170
171        demo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
172        demo_box.set_margin_top(10)
173        parent.append(demo_box)
174
175        menu_btn = Gtk.Button(label="Menu Palette")
176        demo_box.append(menu_btn)
177
178        self.menu_feedback_label = Gtk.Label(label="(No menu action yet)")
179        demo_box.append(self.menu_feedback_label)
180
181        menu_palette = Palette(label="Menu Options")
182        menu_palette.props.secondary_text = "Right-click or use menu property for options"
183        menu = menu_palette.menu
184
185        def feedback(msg):
186            self.menu_feedback_label.set_text(msg)
187
188        item1 = PaletteMenuItem("Open File", "document-open")
189        item1.connect('activate', lambda x: feedback("Open File clicked"))
190        menu.append(item1)
191
192        item2 = PaletteMenuItem("Save File", "document-save")
193        item2.connect('activate', lambda x: feedback("Save File clicked"))
194        menu.append(item2)
195
196        menu.append(PaletteMenuItemSeparator())
197
198        item3 = PaletteMenuItem("Settings", "preferences-system")
199        item3.connect('activate', lambda x: feedback("Settings clicked"))
200        menu.append(item3)
201
202        menu_invoker = WidgetInvoker()
203        menu_invoker.attach_widget(menu_btn)
204        menu_palette.set_invoker(menu_invoker)
205        menu_btn.connect('clicked', lambda btn: menu_palette.popup(immediate=True))
206
207    def _create_palette_window_section(self, parent):
208        """Create palette window examples."""
209        section = self._create_section_header(
210            parent,
211            "Palette Windows",
212            "Low-level palette window implementation"
213        )
214
215        demo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
216        demo_box.set_margin_top(10)
217        parent.append(demo_box)
218
219        window_btn = Gtk.Button(label="Palette Window")
220        demo_box.append(window_btn)
221
222        palette_window = PaletteWindow()
223
224        custom_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
225        custom_widget.set_margin_start(10)
226        custom_widget.set_margin_end(10)
227        custom_widget.set_margin_top(10)
228        custom_widget.set_margin_bottom(10)
229
230        label = Gtk.Label(label="Custom Palette Window")
231        label.add_css_class('heading')
232        custom_widget.append(label)
233
234        progress = Gtk.ProgressBar()
235        progress.set_fraction(0.7)
236        progress.set_text("Progress: 70%")
237        progress.set_show_text(True)
238        custom_widget.append(progress)
239
240        button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
241        button_box.set_halign(Gtk.Align.CENTER)
242        ok_btn = Gtk.Button(label="OK")
243        cancel_btn = Gtk.Button(label="Cancel")
244        button_box.append(ok_btn)
245        button_box.append(cancel_btn)
246        custom_widget.append(button_box)
247
248        palette_window.set_content(custom_widget)
249
250        window_invoker = WidgetInvoker()
251        window_invoker.attach(window_btn)
252        palette_window.set_invoker(window_invoker)
253        window_btn.connect('clicked', lambda btn: palette_window.popup(immediate=True))
254
255        ok_btn.connect('clicked', lambda btn: palette_window.popdown(immediate=True))
256        cancel_btn.connect('clicked', lambda btn: palette_window.popdown(immediate=True))
257
258    def _create_invoker_section(self, parent):
259        """Create different invoker type examples."""
260        section = self._create_section_header(
261            parent,
262            "Invoker Types",
263            "Different ways to trigger palette display"
264        )
265
266        demo_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
267        demo_box.set_margin_top(10)
268        parent.append(demo_box)
269
270        # Widget invoker (hover demo with box)
271        widget_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
272        hover_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
273        hover_box.set_size_request(120, 40)
274        hover_box.set_halign(Gtk.Align.START)
275        hover_box.set_valign(Gtk.Align.CENTER)
276        hover_box.set_margin_top(4)
277        hover_box.set_margin_bottom(4)
278        hover_box.set_margin_start(4)
279        hover_box.set_margin_end(4)
280        hover_box.add_css_class('suggested-action')
281        hover_label = Gtk.Label(label="Hover Me (Box)")
282        hover_box.append(hover_label)
283        widget_row.append(hover_box)
284        widget_row.append(Gtk.Label(label="← Hover to invoke palette"))
285        demo_box.append(widget_row)
286
287        widget_palette = Palette(label="Widget Invoker (Hover)")
288        widget_palette.props.secondary_text = "Triggered by hover on box"
289        widget_invoker = WidgetInvoker()
290        widget_invoker.attach_widget(hover_box)
291        widget_palette.set_invoker(widget_invoker)
292        def on_motion_enter(controller, x, y):
293            widget_palette.popup(immediate=True)
294        motion_controller = Gtk.EventControllerMotion()
295        motion_controller.connect('enter', on_motion_enter)
296        hover_box.add_controller(motion_controller)
297
298
299        cursor_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
300        cursor_btn = Gtk.Button(label="Cursor Invoker")
301        cursor_row.append(cursor_btn)
302        cursor_row.append(Gtk.Label(label="← Click to show at cursor position"))
303        demo_box.append(cursor_row)
304
305        cursor_palette = Palette(label="Cursor Invoker")
306        cursor_palette.props.secondary_text = "Shows at cursor position"
307        cursor_invoker = CursorInvoker()
308        cursor_invoker.attach(cursor_btn)
309        cursor_palette.set_invoker(cursor_invoker)
310
311        def update_pointer_position(motion_controller, x, y):
312            cursor_invoker._cursor_x = int(x)
313            cursor_invoker._cursor_y = int(y)
314        motion_controller = Gtk.EventControllerMotion()
315        motion_controller.connect('motion', update_pointer_position)
316        cursor_btn.add_controller(motion_controller)
317
318        def show_cursor_palette(btn):
319            cursor_palette.popup(immediate=True)
320        cursor_btn.connect('clicked', show_cursor_palette)
321
322    def _create_treeview_section(self, parent):
323        """Create TreeView invoker examples."""
324        section = self._create_section_header(
325            parent,
326            "TreeView Integration",
327            "Double-click a row to show a palette for that item."
328        )
329
330        demo_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
331        demo_box.set_margin_top(10)
332        parent.append(demo_box)
333
334        store = Gtk.ListStore(str, str)
335        store.append(["Item 1", "Description 1"])
336        store.append(["Item 2", "Description 2"])
337        store.append(["Item 3", "Description 3"])
338
339        tree_view = Gtk.TreeView(model=store)
340        tree_view.set_size_request(-1, 150)
341
342        renderer = Gtk.CellRendererText()
343        column1 = Gtk.TreeViewColumn("Name", renderer, text=0)
344        tree_view.append_column(column1)
345
346        column2 = Gtk.TreeViewColumn("Description", renderer, text=1)
347        tree_view.append_column(column2)
348
349        scrolled_tree = Gtk.ScrolledWindow()
350        scrolled_tree.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
351        scrolled_tree.set_child(tree_view)
352        demo_box.append(scrolled_tree)
353
354        #  only double-click (row-activated) opens palette
355        def show_row_palette(treeview, path, column=None):
356            row = store[path][0]
357            palette = Palette(label=f"Row: {row}")
358            palette.props.secondary_text = f"Palette for {row}"
359            close_btn = Gtk.Button(label="Close")
360            close_btn.connect('clicked', lambda btn: palette.popdown(immediate=True))
361            palette.set_content(close_btn)
362            invoker = WidgetInvoker()
363            invoker.attach(tree_view)
364            invoker.set_lock_palette(True)
365            palette.set_invoker(invoker)
366            palette.popup(immediate=True)
367        def on_row_activated(treeview, path, column):
368            show_row_palette(treeview, path, column)
369        tree_view.connect('row-activated', on_row_activated)
370
371    def _create_palette_group_section(self, parent):
372        """Create palette group examples."""
373        section = self._create_section_header(
374            parent,
375            "Palette Groups",
376            "Coordinated palettes - only one shows at a time"
377        )
378
379        demo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
380        demo_box.set_margin_top(10)
381        parent.append(demo_box)
382
383        group = get_group('demo_group')
384
385        for i in range(3):
386            btn = Gtk.Button(label=f"Group Palette {i+1}")
387            demo_box.append(btn)
388
389            palette = Palette(label=f"Grouped Palette {i+1}")
390            palette.props.secondary_text = f"This is palette {i+1} in the group. Only one group palette can be open at a time."
391
392            group.add(palette)
393
394            invoker = WidgetInvoker()
395            invoker.attach(btn)
396            palette.set_invoker(invoker)
397            btn.connect('clicked', lambda btn, p=palette: p.popup(immediate=True))
398
399        control_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
400        control_box.set_margin_top(10)
401        parent.append(control_box)
402
403        popdown_btn = Gtk.Button(label="Pop Down All Groups")
404        popdown_btn.connect('clicked', lambda btn: self._popdown_all_groups())
405        control_box.append(popdown_btn)
406
407    def _popdown_all_groups(self):
408        """Pop down all palette groups."""
409        from sugar.graphics.palettegroup import popdown_all
410        popdown_all()
411        print("All palette groups popped down")
412
413
414class PaletteDemoApp(Gtk.Application):
415
416    def __init__(self):
417        super().__init__(application_id='org.sugarlabs.PaletteDemo')
418
419    def do_activate(self):
420        window = PaletteDemo(self)
421        window.present()
422
423
424def main():
425    app = PaletteDemoApp()
426    return app.run([])
427
428
429if __name__ == '__main__':
430    sys.exit(main())

Toolbar Examples

  1"""ToolbarBox Example"""
  2
  3import sys
  4import os
  5
  6sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
  7
  8import gi
  9
 10gi.require_version("Gtk", "4.0")
 11from gi.repository import Gtk
 12
 13from sugar.activity import SimpleActivity
 14from sugar.graphics.toolbarbox import ToolbarBox, ToolbarButton
 15from sugar.graphics.toolbutton import ToolButton
 16from sugar.graphics.icon import Icon
 17from sugar.graphics import style
 18
 19PROJECT_ROOT = os.path.join(os.path.dirname(__file__), "..")
 20SUGAR_ICONS_PATH = os.path.join(PROJECT_ROOT, "sugar-artwork", "icons", "scalable", "actions")
 21SUGAR_ICONS_PATH = os.path.abspath(SUGAR_ICONS_PATH)
 22
 23
 24class ToolbarBoxExampleActivity(SimpleActivity):
 25    """Example activity demonstrating Sugar GTK4 ToolbarBox features."""
 26
 27    def __init__(self):
 28        super().__init__()
 29        self.set_title("Sugar GTK4 ToolbarBox Example")
 30        
 31        
 32        self._create_content()
 33
 34    def _create_content(self):
 35        """Create the main content with expandable toolbars."""
 36        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
 37
 38        self._toolbarbox = ToolbarBox()
 39
 40        self._create_main_toolbar()
 41
 42        main_box.append(self._toolbarbox)
 43
 44        content_area = self._create_content_area()
 45        main_box.append(content_area)
 46
 47        self._status_bar = Gtk.Label(
 48            label="Click toolbar buttons to expand/collapse sections"
 49        )
 50        self._status_bar.set_margin_start(style.DEFAULT_PADDING)
 51        self._status_bar.set_margin_end(style.DEFAULT_PADDING)
 52        self._status_bar.set_margin_top(style.DEFAULT_PADDING // 2)
 53        self._status_bar.set_margin_bottom(style.DEFAULT_PADDING // 2)
 54        self._status_bar.add_css_class("dim-label")
 55        main_box.append(self._status_bar)
 56
 57        self.set_canvas(main_box)
 58        self.set_default_size(800, 600)
 59
 60    def _create_main_toolbar(self):
 61        """Create the main toolbar with expandable sections."""
 62        toolbar = self._toolbarbox.get_toolbar()
 63
 64        # Activity button (non-expandable)
 65        activity_button = ToolButton(icon_name=os.path.join(PROJECT_ROOT, "sugar-artwork", "icons", "scalable", "apps", "activity-journal.svg"))
 66        activity_button.set_tooltip("My Activity")
 67        toolbar.append(activity_button)
 68
 69        # Separator
 70        separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
 71        separator.set_margin_start(6)
 72        separator.set_margin_end(6)
 73        toolbar.append(separator)
 74
 75        # Edit tools (expandable)
 76        edit_button = ToolbarButton(
 77            page=self._create_edit_toolbar(), 
 78            icon_name=os.path.join(SUGAR_ICONS_PATH, "toolbar-edit.svg")
 79        )
 80        edit_button.set_tooltip("Edit Tools")
 81        toolbar.append(edit_button)
 82
 83        # View tools (expandable)
 84        view_button = ToolbarButton(
 85            page=self._create_view_toolbar(), 
 86            icon_name=os.path.join(SUGAR_ICONS_PATH, "toolbar-view.svg")
 87        )
 88        view_button.set_tooltip("View Tools")
 89        toolbar.append(view_button)
 90
 91        # Tools section (expandable)
 92        tools_button = ToolbarButton(
 93            page=self._create_tools_toolbar(), 
 94            icon_name=os.path.join(PROJECT_ROOT, "sugar-artwork", "icons", "scalable", "categories", "preferences-system.svg")
 95        )
 96        tools_button.set_tooltip("Tools")
 97        toolbar.append(tools_button)
 98
 99        # Spacer
100        spacer = Gtk.Box()
101        spacer.set_hexpand(True)
102        toolbar.append(spacer)
103
104        # Stop button (non-expandable)
105        stop_button = ToolButton(icon_name=os.path.join(SUGAR_ICONS_PATH, "activity-stop.svg"))
106        stop_button.set_tooltip("Stop Activity")
107        stop_button.connect("clicked", lambda w: self.close())
108        toolbar.append(stop_button)
109
110    def _create_edit_toolbar(self):
111        """Create the edit toolbar page."""
112        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
113        toolbar.set_margin_top(style.DEFAULT_PADDING)
114        toolbar.set_margin_bottom(style.DEFAULT_PADDING)
115
116        edit_buttons = [
117            ("New", os.path.join(SUGAR_ICONS_PATH, "document-open.svg")),
118            ("Save", os.path.join(SUGAR_ICONS_PATH, "document-save.svg")),
119            ("---", None),  # Separator
120            ("Copy", os.path.join(SUGAR_ICONS_PATH, "edit-copy.svg")),
121            ("Paste", os.path.join(SUGAR_ICONS_PATH, "edit-paste.svg")),
122            ("---", None),  # Separator
123            ("Undo", os.path.join(SUGAR_ICONS_PATH, "edit-undo.svg")),
124            ("Redo", os.path.join(SUGAR_ICONS_PATH, "edit-redo.svg")),
125        ]
126
127        for label, icon_name in edit_buttons:
128            if label == "---":
129                separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
130                separator.set_margin_start(6)
131                separator.set_margin_end(6)
132                toolbar.append(separator)
133            else:
134                button = ToolButton(icon_name=icon_name)
135                button.set_tooltip(label)
136                button.connect("clicked", self._on_toolbar_action, f"Edit: {label}")
137                toolbar.append(button)
138
139        return toolbar
140
141    def _create_view_toolbar(self):
142        """Create the view toolbar page."""
143        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
144        toolbar.set_margin_top(style.DEFAULT_PADDING)
145        toolbar.set_margin_bottom(style.DEFAULT_PADDING)
146
147        zoom_out = ToolButton(icon_name=os.path.join(SUGAR_ICONS_PATH, "zoom-out.svg"))
148        zoom_out.set_tooltip("Zoom Out")
149        zoom_out.connect("clicked", self._on_toolbar_action, "View: Zoom Out")
150        toolbar.append(zoom_out)
151
152        zoom_label = Gtk.Label(label="100%")
153        zoom_label.set_size_request(50, -1)
154        toolbar.append(zoom_label)
155
156        zoom_in = ToolButton(icon_name=os.path.join(SUGAR_ICONS_PATH, "zoom-in.svg"))
157        zoom_in.set_tooltip("Zoom In")
158        zoom_in.connect("clicked", self._on_toolbar_action, "View: Zoom In")
159        toolbar.append(zoom_in)
160
161        separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
162        separator.set_margin_start(6)
163        separator.set_margin_end(6)
164        toolbar.append(separator)
165
166        view_modes = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3)
167        view_modes.add_css_class("linked")
168
169        list_view = Gtk.ToggleButton()
170        list_view.set_child(
171            Icon(file_name=os.path.join(SUGAR_ICONS_PATH, "view-list.svg"), pixel_size=style.STANDARD_ICON_SIZE)
172        )
173        list_view.set_tooltip_text("List View")
174        list_view.set_active(True)
175        view_modes.append(list_view)
176
177        grid_view = Gtk.ToggleButton()
178        grid_view.set_child(
179            Icon(file_name=os.path.join(SUGAR_ICONS_PATH, "view-details.svg"), pixel_size=style.STANDARD_ICON_SIZE)
180        )
181        grid_view.set_tooltip_text("Details View")
182        view_modes.append(grid_view)
183
184        toolbar.append(view_modes)
185
186        return toolbar
187
188    def _create_tools_toolbar(self):
189        """Create the tools toolbar page."""
190        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
191        toolbar.set_margin_top(style.DEFAULT_PADDING)
192        toolbar.set_margin_bottom(style.DEFAULT_PADDING)
193
194        # Tool selection  
195        cursor_path = os.path.join(PROJECT_ROOT, "sugar-artwork", "cursor", "sugar", "pngs")
196        tools = [
197            ("Brush", os.path.join(cursor_path, "paintbrush.png")),
198            ("Text", os.path.join(SUGAR_ICONS_PATH, "format-text-bold.svg")),
199            ("Shape", os.path.join(SUGAR_ICONS_PATH, "view-triangle.svg")),
200            ("Select", os.path.join(SUGAR_ICONS_PATH, "select-all.svg")),
201        ]
202
203        for name, icon_path in tools:
204            tool_button = ToolButton(icon_name=icon_path)
205            tool_button.set_tooltip(name)
206            tool_button.connect("clicked", self._on_toolbar_action, f"Tool: {name}")
207            toolbar.append(tool_button)
208
209        separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
210        separator.set_margin_start(6)
211        separator.set_margin_end(6)
212        toolbar.append(separator)
213
214        color_button = Gtk.ColorButton()
215        color_button.set_tooltip_text("Choose Color")
216        toolbar.append(color_button)
217
218        properties_button = ToolButton(icon_name=os.path.join(PROJECT_ROOT, "sugar-artwork", "icons", "scalable", "categories", "preferences-system.svg"))
219        properties_button.set_tooltip("Properties")
220        properties_button.connect(
221            "clicked", self._on_toolbar_action, "Tool: Properties"
222        )
223        toolbar.append(properties_button)
224
225        return toolbar
226
227    def _create_content_area(self):
228        """Create main content area."""
229        content_frame = Gtk.Frame()
230        content_frame.set_margin_start(style.DEFAULT_PADDING)
231        content_frame.set_margin_end(style.DEFAULT_PADDING)
232        content_frame.set_margin_top(style.DEFAULT_PADDING)
233        content_frame.set_margin_bottom(style.DEFAULT_PADDING)
234
235        content_box = Gtk.Box(
236            orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING
237        )
238        content_box.set_margin_start(style.DEFAULT_PADDING)
239        content_box.set_margin_end(style.DEFAULT_PADDING)
240        content_box.set_margin_top(style.DEFAULT_PADDING)
241        content_box.set_margin_bottom(style.DEFAULT_PADDING)
242
243        description = Gtk.Label()
244        description.set_markup("""
245<b>Sugar GTK4 ToolbarBox Example</b>
246
247This example demonstrates the expandable toolbar functionality:
248
249- <b>Expandable Sections:</b> Click Edit, View, or Tools buttons to expand sections
250- <b>Inline Display:</b> Expanded toolbars appear below the main toolbar
251- <b>Palette Fallback:</b> On smaller screens, content may appear in palettes
252
253<i>Click the toolbar buttons above to see the expansion behavior.</i>
254        """)
255        description.set_halign(Gtk.Align.START)
256        content_box.append(description)
257
258        log_frame = Gtk.Frame(label="Action Log")
259        log_frame.set_margin_top(style.DEFAULT_SPACING)
260
261        scrolled = Gtk.ScrolledWindow()
262        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
263        scrolled.set_size_request(-1, 150)
264
265        self._action_log = Gtk.TextView()
266        self._action_log.set_editable(False)
267        self._action_log.set_cursor_visible(False)
268        scrolled.set_child(self._action_log)
269
270        log_frame.set_child(scrolled)
271        content_box.append(log_frame)
272
273        content_frame.set_child(content_box)
274        return content_frame
275
276    def _on_toolbar_action(self, button, action):
277        """Handle toolbar button clicks."""
278        self._log_action(action)
279
280    def _log_action(self, action):
281        """Add action to the log."""
282        buffer = self._action_log.get_buffer()
283
284        import datetime
285
286        timestamp = datetime.datetime.now().strftime("%H:%M:%S")
287        text = f"[{timestamp}] {action}\n"
288
289        end_iter = buffer.get_end_iter()
290        buffer.insert(end_iter, text)
291
292        mark = buffer.get_insert()
293        self._action_log.scroll_mark_onscreen(mark)
294
295
296def main():
297    """Run the ToolbarBox example activity."""
298    app = Gtk.Application(application_id="org.sugarlabs.ToolbarBoxExample")
299
300    def on_activate(app):
301        activity = ToolbarBoxExampleActivity()
302        activity.set_application(app)
303        activity.present()
304
305    app.connect("activate", on_activate)
306    return app.run(sys.argv)
307
308
309if __name__ == "__main__":
310    main()
311
  1"""Sugar GTK4 Toolbox Example """
  2
  3import sys
  4import os
  5
  6sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
  7
  8import gi
  9gi.require_version("Gtk", "4.0")
 10from gi.repository import Gtk
 11
 12from sugar.activity import SimpleActivity
 13from sugar.graphics.toolbox import Toolbox
 14from sugar.graphics import style
 15from sugar.graphics.icon import Icon
 16
 17
 18class ToolboxExampleActivity(SimpleActivity):
 19    """Example activity demonstrating Sugar GTK4 Toolbox features."""
 20
 21    def __init__(self):
 22        super().__init__()
 23        self.set_title("Sugar GTK4 Toolbox Example")
 24        self._create_content()
 25
 26    def _create_content(self):
 27        """Create the main content with toolbox."""
 28        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
 29
 30        # Create toolbox
 31        self._toolbox = Toolbox()
 32        self._toolbox.connect('current-toolbar-changed', self._on_toolbar_changed)
 33
 34        # Add various toolbars
 35        self._create_edit_toolbar()
 36        self._create_view_toolbar()
 37        self._create_tools_toolbar()
 38        self._create_help_toolbar()
 39
 40        main_box.append(self._toolbox)
 41
 42        # Add content area
 43        content_area = self._create_content_area()
 44        main_box.append(content_area)
 45
 46        # Status bar
 47        self._status_bar = Gtk.Label(label="Toolbox Example - Switch between toolbars using tabs")
 48        self._status_bar.set_margin_start(style.DEFAULT_PADDING)
 49        self._status_bar.set_margin_end(style.DEFAULT_PADDING)
 50        self._status_bar.set_margin_top(style.DEFAULT_PADDING // 2)
 51        self._status_bar.set_margin_bottom(style.DEFAULT_PADDING // 2)
 52        self._status_bar.add_css_class('dim-label')
 53        main_box.append(self._status_bar)
 54
 55        self.set_canvas(main_box)
 56        self.set_default_size(800, 600)
 57
 58    def _create_edit_toolbar(self):
 59        """Create edit toolbar with common editing tools."""
 60        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
 61        toolbar.set_margin_top(style.DEFAULT_PADDING)
 62        toolbar.set_margin_bottom(style.DEFAULT_PADDING)
 63
 64        # Common edit buttons
 65        edit_buttons = [
 66            ("New", "document-new"),
 67            ("Open", "document-open"),
 68            ("Save", "document-save"),
 69            ("---", None),  # Separator
 70            ("Cut", "edit-cut"),
 71            ("Copy", "edit-copy"),
 72            ("Paste", "edit-paste"),
 73            ("---", None),  # Separator
 74            ("Undo", "edit-undo"),
 75            ("Redo", "edit-redo"),
 76        ]
 77
 78        for label, icon_name in edit_buttons:
 79            if label == "---":
 80                separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
 81                separator.set_margin_start(6)
 82                separator.set_margin_end(6)
 83                toolbar.append(separator)
 84            else:
 85                button = Gtk.Button()
 86                if icon_name:
 87                    icon = Icon(icon_name=icon_name, pixel_size=style.STANDARD_ICON_SIZE)
 88                    button.set_child(icon)
 89                button.set_tooltip_text(label)
 90                button.connect('clicked', self._on_toolbar_button_clicked, f"Edit: {label}")
 91                toolbar.append(button)
 92
 93        # Add spacer
 94        spacer = Gtk.Box()
 95        spacer.set_hexpand(True)
 96        toolbar.append(spacer)
 97
 98        # Add text entry for demonstration
 99        entry = Gtk.Entry()
100        entry.set_placeholder_text("Type something...")
101        entry.set_size_request(200, -1)
102        toolbar.append(entry)
103
104        self._toolbox.add_toolbar("Edit", toolbar)
105
106    def _create_view_toolbar(self):
107        """Create view toolbar with view-related controls."""
108        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
109        toolbar.set_margin_top(style.DEFAULT_PADDING)
110        toolbar.set_margin_bottom(style.DEFAULT_PADDING)
111
112        # Zoom controls
113        zoom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3)
114
115        zoom_out = Gtk.Button()
116        zoom_out.set_child(Icon(icon_name="zoom-out", pixel_size=style.STANDARD_ICON_SIZE))
117        zoom_out.set_tooltip_text("Zoom Out")
118        zoom_out.connect('clicked', self._on_toolbar_button_clicked, "View: Zoom Out")
119        zoom_box.append(zoom_out)
120
121        zoom_label = Gtk.Label(label="100%")
122        zoom_label.set_size_request(50, -1)
123        zoom_box.append(zoom_label)
124
125        zoom_in = Gtk.Button()
126        zoom_in.set_child(Icon(icon_name="zoom-in", pixel_size=style.STANDARD_ICON_SIZE))
127        zoom_in.set_tooltip_text("Zoom In")
128        zoom_in.connect('clicked', self._on_toolbar_button_clicked, "View: Zoom In")
129        zoom_box.append(zoom_in)
130
131        toolbar.append(zoom_box)
132
133        # Separator
134        separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
135        separator.set_margin_start(6)
136        separator.set_margin_end(6)
137        toolbar.append(separator)
138
139        # View mode toggle buttons
140        view_modes = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3)
141        view_modes.add_css_class('linked')
142
143        list_view = Gtk.ToggleButton()
144        list_view.set_child(Icon(icon_name="view-list", pixel_size=style.STANDARD_ICON_SIZE))
145        list_view.set_tooltip_text("List View")
146        list_view.set_active(True)
147        list_view.connect('toggled', self._on_view_mode_toggled, "List View")
148        view_modes.append(list_view)
149
150        grid_view = Gtk.ToggleButton()
151        grid_view.set_child(Icon(icon_name="view-grid", pixel_size=style.STANDARD_ICON_SIZE))
152        grid_view.set_tooltip_text("Grid View")
153        grid_view.connect('toggled', self._on_view_mode_toggled, "Grid View")
154        view_modes.append(grid_view)
155
156        toolbar.append(view_modes)
157
158        # Spacer
159        spacer = Gtk.Box()
160        spacer.set_hexpand(True)
161        toolbar.append(spacer)
162
163        # Fullscreen button
164        fullscreen = Gtk.Button()
165        fullscreen.set_child(Icon(icon_name="view-fullscreen", pixel_size=style.STANDARD_ICON_SIZE))
166        fullscreen.set_tooltip_text("Fullscreen")
167        fullscreen.connect('clicked', self._on_toolbar_button_clicked, "View: Fullscreen")
168        toolbar.append(fullscreen)
169
170        self._toolbox.add_toolbar("View", toolbar)
171
172    def _create_tools_toolbar(self):
173        """Create tools toolbar with tool-specific controls."""
174        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
175        toolbar.set_margin_top(style.DEFAULT_PADDING)
176        toolbar.set_margin_bottom(style.DEFAULT_PADDING)
177
178        # Tool selection
179        tools_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3)
180        tools_box.add_css_class('linked')
181
182        tools = [
183            ("Pointer", "tool-pointer"),
184            ("Brush", "tool-brush"),
185            ("Text", "tool-text"),
186            ("Shape", "shape-rectangle"),
187        ]
188
189        for i, (name, icon_name) in enumerate(tools):
190            tool_button = Gtk.ToggleButton()
191            tool_button.set_child(Icon(icon_name=icon_name, pixel_size=style.STANDARD_ICON_SIZE))
192            tool_button.set_tooltip_text(name)
193            if i == 0:  # Select first tool by default
194                tool_button.set_active(True)
195            tool_button.connect('toggled', self._on_tool_selected, name)
196            tools_box.append(tool_button)
197
198        toolbar.append(tools_box)
199
200        # Separator
201        separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
202        separator.set_margin_start(6)
203        separator.set_margin_end(6)
204
205        toolbar.append(separator)
206
207        # Color picker
208        color_button = Gtk.ColorButton()
209        color_button.set_tooltip_text("Choose Color")
210        toolbar.append(color_button)
211
212        # Size adjustment
213        size_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
214        size_label = Gtk.Label(label="Size:")
215        size_box.append(size_label)
216
217        size_scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL)
218        size_scale.set_range(1, 20)
219        size_scale.set_value(5)
220        size_scale.set_size_request(100, -1)
221        size_scale.set_tooltip_text("Tool Size")
222        size_box.append(size_scale)
223
224        toolbar.append(size_box)
225
226        self._toolbox.add_toolbar("Tools", toolbar)
227
228    def _create_help_toolbar(self):
229        """Create help toolbar with help and information."""
230        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
231        toolbar.set_margin_top(style.DEFAULT_PADDING)
232        toolbar.set_margin_bottom(style.DEFAULT_PADDING)
233
234        # Help buttons
235        help_button = Gtk.Button()
236        help_button.set_child(Icon(icon_name="help-contents", pixel_size=style.STANDARD_ICON_SIZE))
237        help_button.set_tooltip_text("Help Contents")
238        help_button.connect('clicked', self._on_toolbar_button_clicked, "Help: Contents")
239        toolbar.append(help_button)
240
241        about_button = Gtk.Button()
242        about_button.set_child(Icon(icon_name="help-about", pixel_size=style.STANDARD_ICON_SIZE))
243        about_button.set_tooltip_text("About")
244        about_button.connect('clicked', self._on_toolbar_button_clicked, "Help: About")
245        toolbar.append(about_button)
246
247        # Spacer
248        spacer = Gtk.Box()
249        spacer.set_hexpand(True)
250        toolbar.append(spacer)
251
252        # Info display
253        info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
254        info_title = Gtk.Label(label="Toolbox Info")
255        info_title.add_css_class('heading')
256        info_box.append(info_title)
257
258        self._info_label = Gtk.Label(label=f"Total toolbars: {self._toolbox.get_toolbar_count()}")
259        self._info_label.add_css_class('dim-label')
260        info_box.append(self._info_label)
261
262        toolbar.append(info_box)
263
264        self._toolbox.add_toolbar("Help", toolbar)
265
266    def _create_content_area(self):
267        """Create main content area."""
268        content_frame = Gtk.Frame()
269        content_frame.set_hexpand(True)
270        content_frame.set_vexpand(True)
271        content_frame.set_margin_start(style.DEFAULT_PADDING)
272        content_frame.set_margin_end(style.DEFAULT_PADDING)
273        content_frame.set_margin_top(style.DEFAULT_PADDING)
274        content_frame.set_margin_bottom(style.DEFAULT_PADDING)
275
276        content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=style.DEFAULT_SPACING)
277        content_box.set_margin_start(style.DEFAULT_PADDING * 2)
278        content_box.set_margin_end(style.DEFAULT_PADDING * 2)
279        content_box.set_margin_top(style.DEFAULT_PADDING * 2)
280        content_box.set_margin_bottom(style.DEFAULT_PADDING * 2)
281
282        title = Gtk.Label()
283        title.set_markup("<big><b>Toolbox Demo Content Area</b></big>")
284        content_box.append(title)
285
286        description = Gtk.Label()
287        description.set_markup("""
288<i>This demonstrates the Sugar Toolbox component:</i>
289
290• <b>Multiple Toolbars:</b> Switch between Edit, View, Tools, and Help
291• <b>Tab Navigation:</b> Click tabs at the bottom to switch toolbars
292• <b>Dynamic Content:</b> Each toolbar can contain different widgets
293• <b>Sugar Styling:</b> Consistent with Sugar visual design
294• <b>Signal Handling:</b> Responds to toolbar changes
295
296<i>Click the buttons in the toolbars above to see actions.</i>
297        """)
298        description.set_halign(Gtk.Align.START)
299        content_box.append(description)
300
301        # Action log
302        log_frame = Gtk.Frame(label="Action Log")
303        log_frame.set_margin_top(style.DEFAULT_SPACING)
304
305        scrolled = Gtk.ScrolledWindow()
306        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
307        scrolled.set_size_request(-1, 150)
308
309        self._action_log = Gtk.TextView()
310        self._action_log.set_editable(False)
311        self._action_log.set_cursor_visible(False)
312        scrolled.set_child(self._action_log)
313
314        log_frame.set_child(scrolled)
315        content_box.append(log_frame)
316
317        content_frame.set_child(content_box)
318        return content_frame
319
320    def _on_toolbar_changed(self, toolbox, page_num):
321        """Handle toolbar change."""
322        toolbar_name = self._toolbox.get_toolbar_label(page_num)
323        self._log_action(f"Switched to {toolbar_name} toolbar")
324
325        # Update info in help toolbar
326        if hasattr(self, '_info_label'):
327            self._info_label.set_text(
328                f"Total toolbars: {self._toolbox.get_toolbar_count()}, "
329                f"Current: {page_num + 1} ({toolbar_name})"
330            )
331
332    def _on_toolbar_button_clicked(self, button, action):
333        """Handle toolbar button clicks."""
334        self._log_action(action)
335
336    def _on_view_mode_toggled(self, button, mode):
337        """Handle view mode toggle."""
338        if button.get_active():
339            self._log_action(f"Switched to {mode}")
340
341    def _on_tool_selected(self, button, tool_name):
342        """Handle tool selection."""
343        if button.get_active():
344            self._log_action(f"Selected {tool_name} tool")
345
346    def _log_action(self, action):
347        """Add action to the log."""
348        buffer = self._action_log.get_buffer()
349
350        # Add timestamp and action
351        import datetime
352        timestamp = datetime.datetime.now().strftime("%H:%M:%S")
353        text = f"[{timestamp}] {action}\n"
354
355        # Insert at end
356        end_iter = buffer.get_end_iter()
357        buffer.insert(end_iter, text)
358
359        # Scroll to end
360        mark = buffer.get_insert()
361        self._action_log.scroll_mark_onscreen(mark)
362
363
364def main():
365    """Run the Toolbox example activity."""
366    app = Gtk.Application(application_id='org.sugarlabs.ToolboxExample')
367
368    def on_activate(app):
369        activity = ToolboxExampleActivity()
370        app.add_window(activity)
371        activity.present()
372
373    app.connect('activate', on_activate)
374    return app.run()
375
376
377if __name__ == "__main__":
378    main()

Other Examples