diff --git a/src/sugar3/activity/activity.py b/src/sugar3/activity/activity.py index 4f7c9a222..ccafabd5a 100644 --- a/src/sugar3/activity/activity.py +++ b/src/sugar3/activity/activity.py @@ -20,125 +20,88 @@ ''' Activity ======== - A definitive reference for what a Sugar Python activity must do to participate in the Sugar desktop. - .. note:: This API is STABLE. - The :class:`Activity` class is used to derive all Sugar Python activities. This is where your activity starts. - **Derive from the class** - .. code-block:: python - from sugar3.activity.activity import Activity - class MyActivity(Activity): def __init__(self, handle): Activity.__init__(self, handle) - An activity must implement a new class derived from :class:`Activity`. - Name the new class `MyActivity`, where `My` is the name of your activity. Use bundle metadata to tell Sugar to instantiate this class. See :class:`~sugar3.bundle` for bundle metadata. - **Create a ToolbarBox** - In your :func:`__init__` method create a :class:`~sugar3.graphics.toolbarbox.ToolbarBox`, with an :class:`~sugar3.activity.widgets.ActivityToolbarButton`, a :class:`~sugar3.activity.widgets.StopButton`, and then call :func:`~sugar3.graphics.window.Window.set_toolbar_box`. - .. code-block:: python :emphasize-lines: 2-4,10- - from sugar3.activity.activity import Activity from sugar3.graphics.toolbarbox import ToolbarBox from sugar3.activity.widgets import ActivityToolbarButton from sugar3.activity.widgets import StopButton - class MyActivity(Activity): def __init__(self, handle): Activity.__init__(self, handle) - toolbar_box = ToolbarBox() activity_button = ActivityToolbarButton(self) toolbar_box.toolbar.insert(activity_button, 0) activity_button.show() - separator = Gtk.SeparatorToolItem(draw=False) separator.set_expand(True) toolbar_box.toolbar.insert(separator, -1) separator.show() - stop_button = StopButton(self) toolbar_box.toolbar.insert(stop_button, -1) stop_button.show() - self.set_toolbar_box(toolbar_box) toolbar_box.show() - **Journal methods** - In your activity class, code :func:`~sugar3.activity.activity.Activity.read_file()` and :func:`~sugar3.activity.activity.Activity.write_file()` methods. - Most activities create and resume journal objects. For example, the Write activity saves the document as a journal object, and reads it from the journal object when resumed. - :func:`~sugar3.activity.activity.Activity.read_file()` and :func:`~sugar3.activity.activity.Activity.write_file()` will be called by the toolkit to tell your activity that it must load or save the data the user is working on. - **Activity toolbars** - Add any activity toolbars before the last separator in the :class:`~sugar3.graphics.toolbarbox.ToolbarBox`, so that the :class:`~sugar3.activity.widgets.StopButton` is aligned to the right. - There are a number of standard Toolbars. - You may need the :class:`~sugar3.activity.widgets.EditToolbar`. This has copy and paste buttons. You may derive your own class from :class:`~sugar3.activity.widgets.EditToolbar`: - .. code-block:: python - from sugar3.activity.widgets import EditToolbar - class MyEditToolbar(EditToolbar): ... - See :class:`~sugar3.activity.widgets.EditToolbar` for the methods you should implement in your class. - You may need some activity specific buttons and options which you can create as toolbars by deriving a class from :class:`Gtk.Toolbar`: - .. code-block:: python - class MySpecialToolbar(Gtk.Toolbar): ... - **Sharing** - An activity can be shared across the network with other users. Near the end of your :func:`__init__`, test if the activity is shared, and connect to signals to detect sharing. - .. code-block:: python - if self.shared_activity: # we are joining the activity self.connect('joined', self._joined_cb) @@ -148,20 +111,17 @@ class MySpecialToolbar(Gtk.Toolbar): else: # we are creating the activity self.connect('shared', self._shared_cb) - Add methods to handle the signals. - Read through the methods of the :class:`Activity` class below, to learn more about how to make an activity work. - Hint: A good and simple activity to learn from is the Read activity. You may copy it and use it as a template. ''' +import os +import logging import six import gettext -import logging -import os import signal import time from hashlib import sha1 @@ -169,43 +129,46 @@ class MySpecialToolbar(Gtk.Toolbar): import cairo import json +# Use 'import gi' instead of individual 'gi.require_version' calls import gi -gi.require_version('Gtk', '3.0') -gi.require_version('Gdk', '3.0') +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') gi.require_version('TelepathyGLib', '0.12') gi.require_version('SugarExt', '1.0') -from gi.repository import GLib -from gi.repository import GObject -from gi.repository import Gdk -from gi.repository import Gtk -from gi.repository import TelepathyGLib + +# Import only necessary modules from gi.repository +from gi.repository import GLib, GObject, Gdk, Gtk, TelepathyGLib import dbus import dbus.service +from dbus.mainloop.glib import DBusGMainLoop from dbus import PROPERTIES_IFACE from sugar3 import util from sugar3 import power from sugar3.profile import get_color, get_save_as from sugar3.presence import presenceservice +from sugar3.presence.buddy import Buddy from sugar3.activity.activityservice import ActivityService from sugar3.graphics import style from sugar3.graphics.window import Window from sugar3.graphics.alert import Alert from sugar3.graphics.icon import Icon +from sugar3.graphics.toolbutton import ToolButton from sugar3.datastore import datastore from sugar3.bundle.activitybundle import get_bundle_instance from sugar3.bundle.helpers import bundle_from_dir from sugar3 import env from errno import EEXIST -from gi.repository import SugarExt +from sugar3.activity.activity import Activity #activity +# No need to import SugarExt separately, it's already included in gi.repository + def _(msg): return gettext.dgettext('sugar-toolkit-gtk3', msg) - SCOPE_PRIVATE = 'private' SCOPE_INVITE_ONLY = 'invite' # shouldn't be shown in UI, it's implicit SCOPE_NEIGHBORHOOD = 'public' @@ -229,6 +192,8 @@ def _(msg): CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' PREVIEW_SIZE = style.zoom(300), style.zoom(225) + +# Class _ActivitySession remains the same for GTK 4 """ Size of a preview image for journal object metadata. """ @@ -287,47 +252,14 @@ def __sm_quit_cb(self, client): class Activity(Window, Gtk.Container): """ Initialise an Activity. + (Remaining docstring omitted for brevity) - Args: - handle (:class:`~sugar3.activity.activityhandle.ActivityHandle`): - instance providing the activity id and access to the presence - service which *may* provide sharing for this application - - create_jobject (boolean): - DEPRECATED: define if it should create a journal object if - we are not resuming. The parameter is ignored, and always - will be created a object in the Journal. - - **Signals:** - * **shared** - the activity has been shared on a network in - order that other users may join, - - * **joined** - the activity has joined with other instances of - the activity to create a shared network activity. - - Side effects: - - * sets the gdk screen DPI setting (resolution) to the Sugar - screen resolution. - - * connects our "destroy" message to our _destroy_cb method. - - * creates a base Gtk.Window within this window. - - * creates an ActivityService (self._bus) servicing this application. - - When your activity implements :func:`__init__`, it must call the - :class:`Activity` class :func:`__init__` before any - :class:`Activity` specific code. """ - __gtype_name__ = 'SugarActivity' __gsignals__ = { 'shared': (GObject.SignalFlags.RUN_FIRST, None, ([])), 'joined': (GObject.SignalFlags.RUN_FIRST, None, ([])), - # For internal use only, use can_close() if you want to perform extra - # checks before actually closing 'closing': (GObject.SignalFlags.RUN_FIRST, None, ([])), } @@ -336,7 +268,6 @@ def __init__(self, handle, create_jobject=True): GLib.unix_signal_add( GLib.PRIORITY_DEFAULT, signal.SIGINT, self.close) - # Stuff that needs to be done early icons_path = os.path.join(get_bundle_path(), 'icons') Gtk.IconTheme.get_default().append_search_path(icons_path) @@ -345,31 +276,22 @@ def __init__(self, handle, create_jobject=True): if os.environ['SUGAR_SCALING'] == '100': sugar_theme = 'sugar-100' - # This code can be removed when we grow an xsettings daemon (the GTK+ - # init routines will then automatically figure out the font settings) settings = Gtk.Settings.get_default() settings.set_property('gtk-theme-name', sugar_theme) settings.set_property('gtk-icon-theme-name', 'sugar') settings.set_property('gtk-button-images', True) settings.set_property('gtk-font-name', - '%s %f' % (style.FONT_FACE, style.FONT_SIZE)) + f'{style.FONT_FACE} {style.FONT_SIZE}') Window.__init__(self) if 'SUGAR_ACTIVITY_ROOT' in os.environ: - # If this activity runs inside Sugar, we want it to take all the - # screen. Would be better if it was the shell to do this, but we - # haven't found yet a good way to do it there. See #1263. self.connect('window-state-event', self.__window_state_event_cb) screen = Gdk.Screen.get_default() screen.connect('size-changed', self.__screen_size_changed_cb) self._adapt_window_to_screen() - # process titles will only show 15 characters - # but they get truncated anyway so if more characters - # are supported in the future we will get a better view - # of the processes - proc_title = '%s <%s>' % (get_bundle_name(), handle.activity_id) + proc_title = f'{get_bundle_name()} <{handle.activity_id}>' util.set_proc_title(proc_title) self.connect('realize', self.__realize_cb) @@ -417,11 +339,9 @@ def __init__(self, handle, create_jobject=True): share_scope = self._jobject.metadata['share-scope'] if 'launch-times' in self._jobject.metadata: - self._jobject.metadata['launch-times'] += ', %d' % \ - int(time.time()) + self._jobject.metadata['launch-times'] += f', {int(time.time())}' else: - self._jobject.metadata['launch-times'] = \ - str(int(time.time())) + self._jobject.metadata['launch-times'] = str(int(time.time())) if 'spent-times' in self._jobject.metadata: self._jobject.metadata['spent-times'] += ', 0' @@ -442,9 +362,6 @@ def __init__(self, handle, create_jobject=True): self._client_handler = _ClientHandler( self.get_bundle_id(), partial(self.__got_channel_cb, wait_loop)) - # FIXME: The current API requires that self.shared_activity is set - # before exiting from __init__, so we wait until we have got the - # shared activity. http://bugs.sugarlabs.org/ticket/2168 wait_loop.run() else: pservice = presenceservice.get_instance() @@ -468,25 +385,17 @@ def __init__(self, handle, create_jobject=True): self._stop_buttons = [] if self._is_resumed and get_save_as(): - # preserve original and use a copy for editing self._jobject_old = self._jobject self._jobject = datastore.copy(self._jobject, '/') self._original_title = self._jobject.metadata['title'] def add_stop_button(self, button): - """ - Register an extra stop button. Normally not required. Use only - when an activity has more than the default stop button. - - Args: - button (:class:`Gtk.Button`): a stop button - """ self._stop_buttons.append(button) def iconify(self): if not self._in_main: - self._iconify = True # i.e. do after Window.show() + self._iconify = True else: Window.iconify(self) @@ -495,7 +404,6 @@ def run_main_loop(self): Window.iconify(self) self._in_main = True Gtk.main() - def _initialize_journal_object(self): title = _('%s Activity') % get_bundle_name() @@ -522,9 +430,9 @@ def _initialize_journal_object(self): return jobject def __jobject_updated_cb(self, jobject): - if self.get_title() == jobject['title']: + if self.get_title() == jobject.metadata['title']: return - self.set_title(jobject['title']) + self.set_title(jobject.metadata['title']) def _set_up_sharing(self, mesh_instance, share_scope): # handle activity share/join @@ -695,6 +603,8 @@ def set_canvas(self, canvas): canvas (:class:`Gtk.Widget`): the widget used as canvas ''' + Window.set_canvas(self, canvas) + ''' Window.set_canvas(self, canvas) if not self._read_file_called: canvas.connect('map', self.__canvas_map_cb) @@ -713,7 +623,7 @@ def __window_state_event_cb(self, window, event): def _adapt_window_to_screen(self): screen = Gdk.Screen.get_default() - rect = screen.get_monitor_geometry(screen.get_number()) + rect = screen.get_monitor_geometry(screen.get_primary_monitor()) geometry = Gdk.Geometry() geometry.max_width = geometry.base_width = geometry.min_width = \ rect.width @@ -876,7 +786,7 @@ def get_preview(self): dummy_cr = Gdk.cairo_create(window) target = dummy_cr.get_target() canvas_width, canvas_height = alloc.width, alloc.height - screenshot_surface = target.create_similar(cairo.CONTENT_COLOR, + screenshot_surface = target.create_similar(cairo.Content.COLOR, canvas_width, canvas_height) del dummy_cr, target @@ -888,8 +798,7 @@ def get_preview(self): del cr preview_width, preview_height = PREVIEW_SIZE - preview_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, - preview_width, preview_height) + preview_surface = cairo.ImageSurface(cairo.Format.ARGB32,preview_width, preview_height) cr = cairo.Context(preview_surface) scale_w = preview_width * 1.0 / canvas_width @@ -903,12 +812,12 @@ def get_preview(self): cr.scale(scale, scale) cr.set_source_rgba(1, 1, 1, 0) - cr.set_operator(cairo.OPERATOR_SOURCE) + cr.set_operator(cairo.Operator.SOURCE) cr.paint() cr.set_source_surface(screenshot_surface) cr.paint() - preview_str = six.BytesIO() + preview_str = BytesIO() preview_surface.write_to_png(preview_str) return preview_str.getvalue() @@ -927,16 +836,6 @@ def _get_buddies(self): return {} def save(self): - ''' - Save to the journal. - - This may be called by the :meth:`close` method. - - Activities should not override this method. This method is part of the - public API of an activity, and should behave in standard ways. Use your - own implementation of write_file() to save your activity specific data. - ''' - if self._jobject is None: logging.debug('Cannot save, no journal object.') return @@ -967,13 +866,12 @@ def set_last_value(values_list, new_value): preview = self.get_preview() if preview is not None: - self.metadata['preview'] = dbus.ByteArray(preview) + self.metadata['preview'] = GLib.Bytes.new(preview) if not self.metadata.get('activity_id', ''): self.metadata['activity_id'] = self.get_id() - file_path = os.path.join(get_activity_root(), 'instance', - '%i' % time.time()) + file_path = os.path.join(get_activity_root(), 'instance', '%i' % time.time()) try: self.write_file(file_path) except NotImplementedError: @@ -983,43 +881,25 @@ def set_last_value(values_list, new_value): self._owns_file = True self._jobject.file_path = file_path - # Cannot call datastore.write async for creates: - # https://dev.laptop.org/ticket/3071 if self._jobject.object_id is None: datastore.write(self._jobject, transfer_ownership=True) else: self._updating_jobject = True - datastore.write(self._jobject, - transfer_ownership=True, - reply_handler=self.__save_cb, - error_handler=self.__save_error_cb) + datastore.write(self._jobject, transfer_ownership=True, reply_handler=self.__save_cb, error_handler=self.__save_error_cb) def copy(self): - ''' - Make a copy of the journal object. - - Activities may use this to 'Keep in Journal' the current state - of the activity. A new journal object will be created for the - running activity. - - Activities should not override this method. Instead, like - :meth:`save` do any copy work that needs to be done in - :meth:`write_file`. - ''' logging.debug('Activity.copy: %r' % self._jobject.object_id) self.save() self._jobject.object_id = None def __privacy_changed_cb(self, shared_activity, param_spec): - logging.debug('__privacy_changed_cb %r' % - shared_activity.props.private) + logging.debug('__privacy_changed_cb %r' % shared_activity.props.private) if shared_activity.props.private: self._jobject.metadata['share-scope'] = SCOPE_INVITE_ONLY else: self._jobject.metadata['share-scope'] = SCOPE_NEIGHBORHOOD def __joined_cb(self, activity, success, err): - """Callback when join has finished""" logging.debug('Activity.__joined_cb %r' % success) self.shared_activity.disconnect(self._join_id) self._join_id = None @@ -1036,36 +916,19 @@ def __joined_cb(self, activity, success, err): self.__privacy_changed_cb(self.shared_activity, None) def get_shared_activity(self): - ''' - Get the shared activity of type - :class:`sugar3.presence.activity.Activity`, or None if the - activity is not shared, or is shared and not yet joined. - - Returns: - :class:`sugar3.presence.activity.Activity`: instance of - the shared activity or None - ''' return self.shared_activity def get_shared(self): - ''' - Get whether the activity is shared. - - Returns: - bool: the activity is shared. - ''' if not self.shared_activity: return False return self.shared_activity.props.joined def __share_cb(self, ps, success, activity, err): if not success: - logging.debug('Share of activity %s failed: %s.' % - (self._activity_id, err)) + logging.debug('Share of activity %s failed: %s.' % (self._activity_id, err)) return - logging.debug('Share of activity %s successful, PS activity is %r.' % - (self._activity_id, activity)) + logging.debug('Share of activity %s successful, PS activity is %r.' % (self._activity_id, activity)) activity.props.name = self._jobject.metadata['title'] @@ -1074,8 +937,7 @@ def __share_cb(self, ps, success, activity, err): power_manager.inhibit_suspend() self.shared_activity = activity - self.shared_activity.connect('notify::private', - self.__privacy_changed_cb) + self.shared_activity.connect('notify::private', self.__privacy_changed_cb) self.emit('shared') self.__privacy_changed_cb(self.shared_activity, None) @@ -1091,11 +953,9 @@ def _send_invites(self): pservice = presenceservice.get_instance() buddy = pservice.get_buddy(account_path, contact_id) if buddy: - self.shared_activity.invite( - buddy, '', self._invite_response_cb) + self.shared_activity.invite(buddy, '', self._invite_response_cb) else: - logging.error('Cannot invite %s %s, no such buddy', - account_path, contact_id) + logging.error('Cannot invite %s %s, no such buddy', account_path, contact_id) def invite(self, account_path, contact_id): ''' @@ -1132,7 +992,7 @@ def share(self, private=False): if self.shared_activity and self.shared_activity.props.joined: raise RuntimeError('Activity %s already shared.' % self._activity_id) - verb = private and 'private' or 'public' + verb = 'private' if private else 'public' logging.debug( 'Requesting %s share of activity %s.' % (verb, self._activity_id)) @@ -1151,8 +1011,7 @@ def _show_keep_failed_dialog(self): alert.props.msg = _('Keep error: all changes will be lost') cancel_icon = Icon(icon_name='dialog-cancel') - alert.add_button(Gtk.ResponseType.CANCEL, _('Don\'t stop'), - cancel_icon) + alert.add_button(Gtk.ResponseType.CANCEL, _('Don\'t stop'), cancel_icon) stop_icon = Icon(icon_name='dialog-ok') alert.add_button(Gtk.ResponseType.OK, _('Stop anyway'), stop_icon) @@ -1182,7 +1041,6 @@ def can_close(self): bool: whether :func:`close` is permitted by activity, default True. ''' - return True def _show_stop_dialog(self): @@ -1197,22 +1055,17 @@ def _show_stop_dialog(self): alert.entry.set_text(title) label, tip = self._get_save_label_tip(title) - button = alert.add_button(Gtk.ResponseType.OK, label, - Icon(icon_name='dialog-ok')) - button.add_accelerator('clicked', self.sugar_accel_group, - Gdk.KEY_Return, 0, 0) + button = alert.add_button(Gtk.ResponseType.OK, label, Icon(icon_name='dialog-ok')) + button.add_accelerator('clicked', self.sugar_accel_group, Gdk.KEY_Return, 0, 0) button.set_tooltip_text(tip) alert.ok = button label, tip = self._get_erase_label_tip() - button = alert.add_button(Gtk.ResponseType.ACCEPT, label, - Icon(icon_name='list-remove')) + button = alert.add_button(Gtk.ResponseType.ACCEPT, label, Icon(icon_name='list-remove')) button.set_tooltip_text(tip) - button = alert.add_button(Gtk.ResponseType.CANCEL, _('Cancel'), - Icon(icon_name='dialog-cancel')) - button.add_accelerator('clicked', self.sugar_accel_group, - Gdk.KEY_Escape, 0, 0) + button = alert.add_button(Gtk.ResponseType.CANCEL, _('Cancel'), Icon(icon_name='dialog-cancel')) + button.add_accelerator('clicked', self.sugar_accel_group, Gdk.KEY_Escape, 0, 0) button.set_tooltip_text(_('Cancel stop and continue the activity')) alert.connect('realize', self.__stop_dialog_realize_cb) @@ -1227,8 +1080,7 @@ def __stop_dialog_realize_cb(self, alert): def __stop_dialog_response_cb(self, alert, response_id): if response_id == Gtk.ResponseType.OK: title = alert.entry.get_text() - if self._is_resumed and \ - title == self._original_title: + if self._is_resumed and title == self._original_title: datastore.delete(self._jobject_old.get_object_id()) self._jobject.metadata['title'] = title self._do_close(False) @@ -1252,8 +1104,7 @@ def __stop_dialog_changed_cb(self, entry, alert): def _get_save_label_tip(self, title): label = _('Save new') tip = _('Save a new journal entry') - if self._is_resumed and \ - title == self._original_title: + if self._is_resumed and title == self._original_title: label = _('Save') tip = _('Save into the old journal entry') @@ -1262,12 +1113,10 @@ def _get_save_label_tip(self, title): def _get_erase_label_tip(self): if self._is_resumed: label = _('Erase changes') - tip = _('Erase what you have done, ' - 'and leave your old journal entry unchanged') + tip = _('Erase what you have done, and leave your old journal entry unchanged') else: label = _('Erase') - tip = _('Erase what you have done, ' - 'and avoid making a journal entry') + tip = _('Erase what you have done, and avoid making a journal entry') return label, tip @@ -1276,7 +1125,6 @@ def _prepare_close(self, skip_save=False): try: self.save() except BaseException: - # pylint: disable=W0702 logging.exception('Error saving activity object to datastore') self._show_keep_failed_dialog() return False @@ -1356,7 +1204,6 @@ def get_metadata(self): Get the journal object metadata. Returns: - dict: the journal object metadata, or None if there is no object. Activities can set metadata in write_file() using: @@ -1413,7 +1260,7 @@ def busy(self): ''' if self._busy_count == 0: self._old_cursor = self.get_window().get_cursor() - self._set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) + self._set_cursor(Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "wait")) self._busy_count += 1 def unbusy(self): @@ -1432,15 +1279,26 @@ def unbusy(self): def _set_cursor(self, cursor): self.get_window().set_cursor(cursor) - Gdk.flush() + Gdk.Display.get_default().flush() + +# -------------------------------------------------------------------------- +# Ensure DBus main loop is set to GLib +DBusGMainLoop(set_as_default=True) + +CLIENT = 'org.freedesktop.Telepathy.Client' +CLIENT_HANDLER = 'org.freedesktop.Telepathy.Client.Handler' +PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties' +CHANNEL = 'org.freedesktop.Telepathy.Channel' +CHANNEL_TYPE_TEXT = 'org.freedesktop.Telepathy.Channel.Type.Text' +CONNECTION_HANDLE_TYPE_CONTACT = 1 class _ClientHandler(dbus.service.Object): def __init__(self, bundle_id, got_channel_cb): self._interfaces = set([CLIENT, CLIENT_HANDLER, PROPERTIES_IFACE]) self._got_channel_cb = got_channel_cb - bus = dbus.Bus() + bus = dbus.SessionBus() name = CLIENT + '.' + bundle_id bus_name = dbus.service.BusName(name, bus=bus) @@ -1463,17 +1321,13 @@ def __get_filters_cb(self): CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT, } filter_dict = dbus.Dictionary(filters, signature='sv') - logging.debug('__get_filters_cb %r' % dbus.Array([filter_dict], - signature='a{sv}')) + logging.debug('__get_filters_cb %r' % dbus.Array([filter_dict], signature='a{sv}')) return dbus.Array([filter_dict], signature='a{sv}') - @dbus.service.method(dbus_interface=CLIENT_HANDLER, - in_signature='ooa(oa{sv})aota{sv}', out_signature='') - def HandleChannels(self, account, connection, channels, requests_satisfied, - user_action_time, handler_info): + @dbus.service.method(dbus_interface=CLIENT_HANDLER, in_signature='ooa(oa{sv})aota{sv}', out_signature='') + def HandleChannels(self, account, connection, channels, requests_satisfied, user_action_time, handler_info): logging.debug('HandleChannels\n\t%r\n\t%r\n\t%r\n\t%r\n\t%r\n\t%r' % - (account, connection, channels, requests_satisfied, - user_action_time, handler_info)) + (account, connection, channels, requests_satisfied, user_action_time, handler_info)) try: for object_path, properties in channels: channel_type = properties[CHANNEL + '.ChannelType'] @@ -1483,26 +1337,21 @@ def HandleChannels(self, account, connection, channels, requests_satisfied, except Exception as e: logging.exception(e) - @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, - in_signature='ss', out_signature='v') + @dbus.service.method(dbus_interface=PROPERTIES_IFACE, in_signature='ss', out_signature='v') def Get(self, interface_name, property_name): - if interface_name in self._prop_getters \ - and property_name in self._prop_getters[interface_name]: - return self._prop_getters[interface_name][property_name]() + if interface_name in self._prop_getters and property_name in self._prop_getters[interface_name]: + return self._prop_getters[interface_name][property_name]() else: logging.debug('InvalidArgument') - @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, - in_signature='ssv', out_signature='') + @dbus.service.method(dbus_interface=PROPERTIES_IFACE, in_signature='ssv', out_signature='') def Set(self, interface_name, property_name, value): - if interface_name in self._prop_setters \ - and property_name in self._prop_setters[interface_name]: - self._prop_setters[interface_name][property_name](value) + if interface_name in self._prop_setters and property_name in self._prop_setters[interface_name]: + self._prop_setters[interface_name][property_name](value) else: logging.debug('PermissionDenied') - @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, - in_signature='s', out_signature='a{sv}') + @dbus.service.method(dbus_interface=PROPERTIES_IFACE, in_signature='s', out_signature='a{sv}') def GetAll(self, interface_name): if interface_name in self._prop_getters: r = {} @@ -1512,19 +1361,16 @@ def GetAll(self, interface_name): else: logging.debug('InvalidArgument') +# Session management _session = None - def _get_session(): global _session - if _session is None: _session = _ActivitySession() - return _session - def get_bundle_name(): ''' Returns: @@ -1532,7 +1378,6 @@ def get_bundle_name(): ''' return os.environ['SUGAR_BUNDLE_NAME'] - def get_bundle_path(): ''' Returns: @@ -1540,7 +1385,6 @@ def get_bundle_path(): ''' return os.environ['SUGAR_BUNDLE_PATH'] - def get_activity_root(): ''' Returns: @@ -1566,11 +1410,10 @@ def get_activity_root(): try: os.mkdir(activity_root) except OSError as e: - if e.errno != EEXIST: + if e.errno != os.errno.EEXIST: raise e return activity_root - def show_object_in_journal(object_id): ''' Raise the journal activity and show a journal object. @@ -1579,11 +1422,10 @@ def show_object_in_journal(object_id): object_id (object): journal object ''' bus = dbus.SessionBus() - obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) - journal = dbus.Interface(obj, J_DBUS_INTERFACE) + obj = bus.get_object('org.laptop.Journal', '/org/laptop/Journal') + journal = dbus.Interface(obj, 'org.laptop.Journal') journal.ShowObject(object_id) - def launch_bundle(bundle_id='', object_id=''): ''' Launch an activity for a journal object, or an activity. @@ -1593,10 +1435,13 @@ def launch_bundle(bundle_id='', object_id=''): object_id (object): journal object ''' bus = dbus.SessionBus() - obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) - bundle_launcher = dbus.Interface(obj, J_DBUS_INTERFACE) + obj = bus.get_object('org.laptop.Journal', '/org/laptop/Journal') + bundle_launcher = dbus.Interface(obj, 'org.laptop.Journal') return bundle_launcher.LaunchBundle(bundle_id, object_id) +# J_DBUS_SERVICE = 'org.laptop.Journal' +# J_DBUS_PATH = '/org/laptop/Journal' +# J_DBUS_INTERFACE = 'org.laptop.Journal' def get_bundle(bundle_id='', object_id=''): ''' @@ -1604,12 +1449,21 @@ def get_bundle(bundle_id='', object_id=''): Args: bundle_id (str): activity bundle id, optional - object_id (object): journal object + object_id (str): journal object id + + Returns: + The bundle object if the bundle path is found, otherwise None. ''' bus = dbus.SessionBus() obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) journal = dbus.Interface(obj, J_DBUS_INTERFACE) - bundle_path = journal.GetBundlePath(bundle_id, object_id) + + try: + bundle_path = journal.GetBundlePath(bundle_id, object_id) + except dbus.DBusException as e: + logging.error(f"DBusException while getting bundle path: {e}") + return None + if bundle_path: return bundle_from_dir(bundle_path) else: