Examples

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

Basic Activity

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

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

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

Creative Studio Activity Example

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

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

Hello_World_Dodge Example

A simple dodging ball game made with new toolkit.

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

UI Components

Examples of various UI components:

Alert Example

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

Icon Example

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

Palette Example

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

Toolbar Examples

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

Other Examples