Source code for sugar.graphics.radiopalette
# Copyright (C) 2009, Aleksey Lim
# Copyright (C) 2025 MostlyK
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
#
# SPDX-License-Identifier: LGPL-2.1-or-later
"""
RadioPalette
=========================
Radio palette provides a way to create radio button groups within palettes,
allowing users to select one option from multiple choices.
This GTK4 port modernizes the radio palette system while maintaining
compatibility with Sugar's palette interface patterns.
Example:
Create a radio palette with multiple tool options.
.. code-block:: python
from gi.repository import Gtk
from sugar.graphics.radiopalette import RadioToolsButton, RadioPalette
from sugar.graphics.toolbutton import ToolButton
# Create the main radio button
radio_button = RadioToolsButton(
icon_name='tool-brush',
tooltip='Drawing Tools'
)
# Create the palette
palette = RadioPalette(primary_text='Drawing Tools')
radio_button.set_palette(palette)
# Add tool options
brush_btn = ToolButton(icon_name='tool-brush')
palette.append(brush_btn, 'Brush')
pen_btn = ToolButton(icon_name='tool-pen')
palette.append(pen_btn, 'Pen')
"""
import gi
import os
gi.require_version("Gtk", "4.0")
gi.require_version("GObject", "2.0")
from gi.repository import Gtk
import logging
from sugar.graphics.toolbutton import ToolButton
from sugar.graphics.palette import Palette
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
SUGAR_DEBUG = os.environ.get('SUGAR_DEBUG', '0') == '1'
[docs]
class RadioMenuButton(ToolButton):
"""
A toolbar button that shows a radio palette when clicked.
"""
[docs]
def __init__(self, **kwargs):
print(f"RadioMenuButton.__init__ called with kwargs: {kwargs}")
# Don't create a default palette by removing tooltip
# This prevents ToolButton from auto-creating a regular Palette
tooltip = kwargs.pop('tooltip', None)
super().__init__(**kwargs)
self.selected_button = None
invoker = self.get_palette_invoker()
print(f"Got palette invoker: {invoker}")
if invoker:
# In GTK4, we handle toggle behavior differently
invoker.set_toggle_palette(True)
print("Set toggle_palette to True")
self.set_hide_tooltip_on_click(False)
self.connect("notify::palette", self._on_palette_changed)
# Set tooltip after everything is set up
if tooltip:
self.set_tooltip_text(tooltip)
if self.get_palette():
print("Palette already exists, calling _on_palette_changed")
self._on_palette_changed(self, None)
def _on_palette_changed(self, widget, pspec):
print("RadioMenuButton._on_palette_changed called")
palette = self.get_palette()
print(f"Current palette: {palette}")
if not isinstance(palette, RadioPalette):
print("Palette is not a RadioPalette instance")
return
print("Calling palette.update_button()")
palette.update_button()
[docs]
class RadioToolsButton(RadioMenuButton):
[docs]
def do_clicked(self):
if not self.selected_button:
logger.warning("RadioToolsButton clicked but no button selected")
return
self.selected_button.emit("clicked")
[docs]
class RadioPalette(Palette):
"""
A palette containing radio button options.
"""
[docs]
def __init__(self, **kwargs):
print(f"RadioPalette.__init__ called with kwargs: {kwargs}")
super().__init__(**kwargs)
self.button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.button_box.set_spacing(6)
self.button_box.set_homogeneous(True)
self.set_content(self.button_box)
print("RadioPalette created successfully")
[docs]
def append(self, button, label):
"""
Add a button option to the radio palette.
Args:
button: The ToolButton to add to the radio group
label: The label text for this option
"""
print(f"RadioPalette.append called with button: {button}, label: {label}")
if not isinstance(button, ToolButton):
raise TypeError("Button must be a ToolButton instance")
if button.get_palette() is not None:
raise RuntimeError("Radio palette buttons should not have sub-palettes")
button.palette_label = label
button.connect("clicked", self._on_button_clicked)
self.button_box.append(button)
children_count = 0
child = self.button_box.get_first_child()
while child:
children_count += 1
child = child.get_next_sibling()
print(f"RadioPalette now has {children_count} buttons")
if children_count == 1:
print("First button added, setting as selected but not calling _on_button_clicked yet")
# Don't call _on_button_clicked immediately - wait for the palette to be attached to a button
if hasattr(button, "set_active"):
button.set_active(True)
button.palette_label = label # Ensure label is set
[docs]
def update_button(self):
print("RadioPalette.update_button called")
print(f"Current invoker: {self.get_invoker()}")
# Find the first button or any active button
selected_button = None
child = self.button_box.get_first_child()
while child:
if hasattr(child, "get_active") and child.get_active():
print(f"Found active button: {child}")
selected_button = child
break
child = child.get_next_sibling()
# If no active button found, use the first button
if not selected_button:
selected_button = self.button_box.get_first_child()
if selected_button:
print(f"No active button found, using first button: {selected_button}")
if hasattr(selected_button, "set_active"):
selected_button.set_active(True)
if selected_button:
# Update the RadioMenuButton to reflect the selected tool
invoker = self.get_invoker()
if invoker and hasattr(invoker, '_widget'):
radio_button = invoker._widget # This should be our RadioMenuButton
print(f"Updating radio button: {radio_button}")
if hasattr(selected_button, "palette_label"):
print(f"Setting primary text to: {selected_button.palette_label}")
self.set_primary_text(selected_button.palette_label)
if isinstance(radio_button, RadioMenuButton):
icon_name = selected_button.get_icon_name()
if icon_name:
print(f"Setting icon to: {icon_name}")
radio_button.set_icon_name(icon_name)
radio_button.set_selected_button(selected_button)
def _on_button_clicked(self, button):
print(f"RadioPalette._on_button_clicked called with button: {button}")
# First, make sure this button is active and others are not
child = self.button_box.get_first_child()
while child:
if hasattr(child, "set_active"):
child.set_active(child == button)
child = child.get_next_sibling()
if hasattr(button, "palette_label"):
print(f"Setting primary text to: {button.palette_label}")
self.set_primary_text(button.palette_label)
print("Calling popdown")
self.popdown(immediate=True)
# Update the RadioMenuButton
invoker = self.get_invoker()
print(f"Got invoker: {invoker}")
if invoker and hasattr(invoker, '_widget'):
radio_button = invoker._widget
print(f"Radio button from invoker: {radio_button}")
if isinstance(radio_button, RadioMenuButton):
if hasattr(button, "palette_label"):
# Don't set label on the radio button, just the tooltip
pass
icon_name = button.get_icon_name()
if icon_name:
print(f"Setting radio button icon to: {icon_name}")
radio_button.set_icon_name(icon_name)
radio_button.set_selected_button(button)
print(f"Set selected button to: {button}")
[docs]
def popdown(self, immediate=True):
if SUGAR_DEBUG:
print(f"RadioPalette.popdown called with immediate={immediate}")
# Call the parent class's popdown method
super().popdown(immediate=immediate)
if SUGAR_DEBUG:
print(f"RadioPalette.popdown: is_up={self.is_up()}, widget={self._widget}")
[docs]
def popup(self, immediate=True):
if SUGAR_DEBUG:
print(f"RadioPalette.popup called with immediate={immediate}")
# Call the parent class's popup method
super().popup(immediate=immediate)
if SUGAR_DEBUG:
print(f"RadioPalette.popup: is_up={self.is_up()}, widget={self._widget}")
[docs]
def get_buttons(self):
buttons = []
child = self.button_box.get_first_child()
while child:
if isinstance(child, ToolButton):
buttons.append(child)
child = child.get_next_sibling()
return buttons
[docs]
def get_selected_button(self):
child = self.button_box.get_first_child()
while child:
if hasattr(child, "get_active") and child.get_active():
return child
child = child.get_next_sibling()
return None