# Copyright (C) 2009, Aleksey Lim, Simon Schampijer
# Copyright (C) 2012, Walter Bender
# Copyright (C) 2012, One Laptop Per Child
#
# 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.
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GObject
import gettext
from sugar4.debug import debug_print
from sugar4.graphics.toolbutton import ToolButton
from sugar4.graphics.toolbarbox import ToolbarButton
from sugar4.graphics.radiopalette import RadioPalette, RadioMenuButton
from sugar4.graphics.radiotoolbutton import RadioToolButton
from sugar4.graphics.xocolor import XoColor
from sugar4.graphics.icon import Icon
from sugar4.graphics import style
from sugar4.graphics.palettemenu import PaletteMenuBox
from sugar4 import profile
def _(msg):
return gettext.dgettext("sugar-toolkit-gtk4", msg)
def _create_activity_icon(metadata, icon_name=None):
"""Create an activity icon with appropriate color."""
import os
if metadata is not None and metadata.get("icon-color"):
color = XoColor(metadata["icon-color"])
else:
color = profile.get_color()
if icon_name is None:
icon_name = "activity-journal" # Default fallback icon
# If icon_name is an absolute SVG path, use file_name
if icon_name and os.path.isabs(icon_name) and icon_name.endswith(".svg"):
print(f"[_create_activity_icon] Using SVG file: {icon_name}")
icon = Icon(file_name=icon_name, xo_color=color)
else:
print(f"[_create_activity_icon] Using theme icon: {icon_name}")
icon = Icon(icon_name=icon_name, xo_color=color)
return icon
[docs]
class TitleEntry(Gtk.Box):
"""Entry widget for editing activity title."""
[docs]
__gsignals__ = {
"enter-key-press": (GObject.SignalFlags.RUN_FIRST, None, ([])),
}
[docs]
def __init__(self, activity, **kwargs):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
[docs]
self.entry = Gtk.Entry(**kwargs)
# Get display and calculate size
display = Gdk.Display.get_default()
if display:
monitor = display.get_monitors()[0] # Primary monitor
geometry = monitor.get_geometry()
width = int(geometry.width / 3)
else:
width = 300 # Fallback width
self.entry.set_size_request(width, -1)
self.entry.set_text(activity.metadata.get("title", ""))
# GTK4 uses different event handling
focus_controller = Gtk.EventControllerFocus()
focus_controller.connect("leave", self.__focus_out_event_cb, activity)
self.entry.add_controller(focus_controller)
self.entry.connect("activate", self.__activate_cb, activity)
click_gesture = Gtk.GestureClick()
click_gesture.connect("pressed", self.__button_press_event_cb)
self.entry.add_controller(click_gesture)
self.entry.show()
self.append(self.entry)
if hasattr(activity.metadata, "connect"):
activity.metadata.connect("updated", self.__jobject_updated_cb)
if hasattr(activity, "connect"):
activity.connect("closing", self.__closing_cb)
def __activate_cb(self, entry, activity):
self.save_title(activity)
entry.select_region(0, 0)
self.emit("enter-key-press")
return False
def __jobject_updated_cb(self, jobject):
if self.entry.has_focus():
return
title = jobject.get("title", "")
if self.entry.get_text() == title:
return
self.entry.set_text(title)
def __closing_cb(self, activity):
self.save_title(activity)
return False
def __focus_out_event_cb(self, controller, activity):
self.entry.select_region(0, 0)
self.save_title(activity)
return False
def __button_press_event_cb(self, gesture, n_press, x, y):
widget = gesture.get_widget()
if not widget.has_focus():
widget.grab_focus()
widget.select_region(0, -1)
return True
return False
[docs]
def save_title(self, activity):
title = self.entry.get_text()
current_title = activity.metadata.get("title", "")
if title == current_title:
return
activity.metadata["title"] = title
activity.metadata["title_set_by_user"] = "1"
if hasattr(activity, "save"):
activity.save()
if hasattr(activity, "set_title"):
activity.set_title(title)
if hasattr(activity, "get_shared_activity"):
shared_activity = activity.get_shared_activity()
if shared_activity is not None and hasattr(shared_activity.props, "name"):
shared_activity.props.name = title
[docs]
class DescriptionItem(ToolButton):
"""Button for editing activity description."""
[docs]
def __init__(self, activity, icon=None, **kwargs):
print(f"[DescriptionItem] kwargs: {kwargs}")
ToolButton.__init__(self, **kwargs)
import os
# Accept either icon name or file path
# TODO: Improve icon handling
if icon is None:
# Default to theme icon name
icon_name = "edit-description"
activity_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../../hello-world/activity")
)
file_name = os.path.join(activity_dir, "edit-description.svg")
elif os.path.isabs(icon):
icon_name = None
file_name = icon
elif icon.endswith(".svg"):
activity_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../../hello-world/activity")
)
file_name = os.path.join(activity_dir, icon)
icon_name = None
else:
icon_name = icon
activity_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../../hello-world/activity")
)
file_name = os.path.join(activity_dir, icon)
icon_widget = Icon(icon_name=icon_name, file_name=file_name)
icon_widget.set_pixel_size(48)
self.set_icon_widget(icon_widget)
icon_widget.show()
print(f"[DescriptionItem] icon_widget type: {type(icon_widget)}")
print(f"[DescriptionItem] icon_name: {icon_name}")
print(f"[DescriptionItem] file_name: {file_name}")
self.set_tooltip(_("Description"))
self.palette_invoker.props.toggle_palette = True
self.palette_invoker.props.lock_palette = True
self.props.hide_tooltip_on_click = False
self._palette = self.get_palette()
description_box = PaletteMenuBox()
sw = Gtk.ScrolledWindow()
# Get display and calculate size
display = Gdk.Display.get_default()
if display:
monitor = display.get_monitors()[0] # Primary monitor
geometry = monitor.get_geometry()
width = int(geometry.width / 2)
else:
width = 400 # Fallback width
sw.set_size_request(width, 2 * style.GRID_CELL_SIZE)
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
self._text_view = Gtk.TextView()
self._text_view.set_cursor_visible(True)
self._text_view.set_left_margin(style.DEFAULT_PADDING)
self._text_view.set_right_margin(style.DEFAULT_PADDING)
self._text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
text_buffer = Gtk.TextBuffer()
description = activity.metadata.get("description", "")
if description:
text_buffer.set_text(description)
self._text_view.set_buffer(text_buffer)
# GTK4 focus handling
focus_controller = Gtk.EventControllerFocus()
focus_controller.connect("leave", self.__description_changed_cb, activity)
self._text_view.add_controller(focus_controller)
sw.set_child(self._text_view)
description_box.append_item(sw, vertical_padding=0)
self._palette.set_content(description_box)
description_box.show()
if hasattr(activity.metadata, "connect"):
activity.metadata.connect("updated", self.__jobject_updated_cb)
[docs]
def set_expanded(self, expanded):
box = self.toolbar_box
if not box:
return
if not expanded:
self.palette_invoker.notify_popdown()
return
if hasattr(box, "expanded_button") and box.expanded_button is not None:
box.expanded_button.queue_draw()
if box.expanded_button != self:
box.expanded_button.set_expanded(False)
if hasattr(box, "expanded_button"):
box.expanded_button = self
def _get_text_from_buffer(self):
buf = self._text_view.get_buffer()
start_iter = buf.get_start_iter()
end_iter = buf.get_end_iter()
return buf.get_text(start_iter, end_iter, False)
def __jobject_updated_cb(self, jobject):
if self._text_view.has_focus():
return
description = jobject.get("description", "")
if not description:
return
if self._get_text_from_buffer() == description:
return
buf = self._text_view.get_buffer()
buf.set_text(description)
def __description_changed_cb(self, controller, activity):
description = self._get_text_from_buffer()
current_description = activity.metadata.get("description", "")
if description == current_description:
return
activity.metadata["description"] = description
if hasattr(activity, "save"):
activity.save()
return False