Source code for sugar.graphics.objectchooser

# Copyright (C) 2007, One Laptop Per Child
# 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.

"""
STABLE.
"""

import logging
import cairo
import io

from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
import dbus

from sugar.datastore import datastore
from sugar.activity.activity import PREVIEW_SIZE


J_DBUS_SERVICE = 'org.laptop.Journal'
J_DBUS_INTERFACE = 'org.laptop.Journal'
J_DBUS_PATH = '/org/laptop/Journal'

FILTER_TYPE_MIME_BY_ACTIVITY = 'mime_by_activity'
FILTER_TYPE_GENERIC_MIME = 'generic_mime'
FILTER_TYPE_ACTIVITY = 'activity'


[docs] def get_preview_pixbuf(preview_data, width=-1, height=-1): """ Retrieve a pixbuf with the content of the preview field Args: preview_data (bytes): preview data from the metadata dictionary. Can't be None. Returned from the sugar.datastore.datastore.DSObject.get_metadata() method. Keyword Args: width (int): the pixbuf width, if is not set, the default width will be used height (int): the pixbuf height, if is not set, the default height will be used Returns: GdkPixbuf.Pixbuf: the generated Pixbuf None: if it could not be created Example: pixbuf = get_preview_pixbuf(metadata.get('preview', '')) has_preview = pixbuf is not None if has_preview: im = Gtk.Image() im.set_from_pixbuf(pixbuf) box.append(im) im.show() """ if width == -1: width = PREVIEW_SIZE[0] if height == -1: height = PREVIEW_SIZE[1] pixbuf = None if len(preview_data) > 4: try: # Handle both base64 encoded and direct PNG data if isinstance(preview_data, str): import base64 preview_data = base64.b64decode(preview_data) elif isinstance(preview_data, bytes): # Check if it's base64 encoded if preview_data[1:4] != b'PNG': import base64 preview_data = base64.b64decode(preview_data) # Create pixbuf from PNG data png_file = io.BytesIO(preview_data) # Load image and scale to dimensions surface = cairo.ImageSurface.create_from_png(png_file) png_width = surface.get_width() png_height = surface.get_height() # Calculate scaling to fit within target dimensions scale_w = width / png_width scale_h = height / png_height scale = min(scale_w, scale_h) # Create scaled surface scaled_width = int(png_width * scale) scaled_height = int(png_height * scale) preview_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, scaled_width, scaled_height) cr = cairo.Context(preview_surface) cr.scale(scale, scale) cr.set_source_surface(surface) cr.paint() # Convert to pixbuf pixbuf = Gdk.pixbuf_get_from_surface(preview_surface, 0, 0, scaled_width, scaled_height) except Exception: logging.exception('Error while loading the preview') return pixbuf
[docs] class ObjectChooser(object): """ UI interface for object choosers. Object choosers can be used by activities to allow the user to select objects from the file system or from some other similar source. Keyword Args: parent (:class:`Gtk.Widget`): the widget calling ObjectChooser what_filter (str): an activity bundle_id or a generic mime type as defined in :mod:`sugar.mime` used to determine which objects will be presented in the object chooser filter_type (str): should be one of [None, FILTER_TYPE_GENERIC_MIME, FILTER_TYPE_ACTIVITY, FILTER_TYPE_MIME_BY_ACTIVITY] If filter_type is None, the default behavior of the what_filter is applied (for backward compatibility), this option is DEPRECATED. If filter_type is FILTER_TYPE_GENERIC_MIME, the what_filter should be a generic mime type defined in mime.py; the object chooser will filter based in the 'mime_type' metadata field. If filter_type is FILTER_TYPE_ACTIVITY, the what_filter should by an activity bundle_id; the object chooser will filter based in the 'activity' metadata field. If filter_type is FILTER_TYPE_MIME_BY_ACTIVITY, the what_filter should be an activity bundle_id; the object chooser will filter based on the 'mime_type' metadata field and the mime types defined by the activity in the activity.info file. show_preview (bool): if True will show the preview image associated with the object in the Journal. This option is only available if filter_type is selected. Examples: chooser = ObjectChooser(self._activity, what_filter='Image') chooser = ObjectChooser(parent=self, what_filter=self.get_bundle_id(), filter_type=FILTER_TYPE_ACTIVITY) """
[docs] def __init__(self, parent=None, what_filter=None, filter_type=None, show_preview=False): if parent is None: parent_xid = 0 elif hasattr(parent, 'get_surface'): # GTK4 uses surfaces instead of XID try: surface = parent.get_surface() if surface and hasattr(surface, 'get_handle'): parent_xid = surface.get_handle() else: parent_xid = 0 except (AttributeError, TypeError): parent_xid = 0 elif hasattr(parent, 'get_window') and hasattr(parent.get_window(), 'get_xid'): # Fallback for GTK3 compatibility parent_xid = parent.get_window().get_xid() else: parent_xid = parent if isinstance(parent, int) else 0 self._parent_xid = parent_xid self._show_preview = show_preview self._main_loop = None self._object_id = None self._bus = None self._chooser_id = None self._response_code = Gtk.ResponseType.NONE self._what_filter = what_filter if filter_type is not None: # verify is one of the available types if filter_type not in [FILTER_TYPE_MIME_BY_ACTIVITY, FILTER_TYPE_GENERIC_MIME, FILTER_TYPE_ACTIVITY]: raise ValueError('filter_type not implemented') self._filter_type = filter_type
[docs] def run(self): """ Runs the object chooser and displays it. Returns: Gtk.ResponseType constant, the response received from displaying the object chooser. """ self._object_id = None self._main_loop = GObject.MainLoop() # Use DBusGMainLoop for dbus.SessionBus mainloop integration (GTK4 compatible) from dbus.mainloop.glib import DBusGMainLoop dbus_loop = DBusGMainLoop(set_as_default=True) self._bus = dbus.SessionBus(mainloop=dbus_loop) self._bus.add_signal_receiver( self.__name_owner_changed_cb, signal_name='NameOwnerChanged', dbus_interface='org.freedesktop.DBus', arg0=J_DBUS_SERVICE) obj = self._bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) journal = dbus.Interface(obj, J_DBUS_INTERFACE) journal.connect_to_signal('ObjectChooserResponse', self.__chooser_response_cb) journal.connect_to_signal('ObjectChooserCancelled', self.__chooser_cancelled_cb) if self._what_filter is None: what_filter = '' else: what_filter = self._what_filter try: if self._filter_type is None: self._chooser_id = journal.ChooseObject( self._parent_xid, what_filter) else: self._chooser_id = journal.ChooseObjectWithFilter( self._parent_xid, what_filter, self._filter_type, self._show_preview) self._main_loop.run() except Exception as e: logging.error('Error running object chooser: %s', e) self._response_code = Gtk.ResponseType.CANCEL finally: self._main_loop = None return self._response_code
[docs] def get_selected_object(self): """ Gets the object selected using the object chooser. Returns: object: the object selected """ if self._object_id is None: return None else: return datastore.get(self._object_id)
[docs] def destroy(self): """ Destroys and cleans up (disposes) the object chooser. """ self._cleanup()
def _cleanup(self): if self._main_loop is not None and self._main_loop.is_running(): self._main_loop.quit() self._main_loop = None self._bus = None def __chooser_response_cb(self, chooser_id, object_id): if chooser_id != self._chooser_id: return logging.debug('ObjectChooser.__chooser_response_cb: %r', object_id) self._response_code = Gtk.ResponseType.ACCEPT self._object_id = object_id self._cleanup() def __chooser_cancelled_cb(self, chooser_id): if chooser_id != self._chooser_id: return logging.debug('ObjectChooser.__chooser_cancelled_cb: %r', chooser_id) self._response_code = Gtk.ResponseType.CANCEL self._cleanup() def __name_owner_changed_cb(self, name, old, new): logging.debug('ObjectChooser.__name_owner_changed_cb') # Journal service disappeared from the bus self._response_code = Gtk.ResponseType.CANCEL self._cleanup()