From 0afad46773a52e8ccfcd5dd555e212ff6bb97f6d Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Wed, 25 Jul 2007 10:30:15 -0400 Subject: [PATCH] Add a basic Presence Service tab to the developer console. * Newly added activities and buddies appear in bold for 5 seconds. * Removed activities and buddies stay in the list with strikethrough applied for 5 seconds, after which they vanish. * Columns other than "object path" don't do anything. * In the object path column, the common prefix is replaced by ".../". --- services/console/README | 12 + services/console/console.py | 2 + services/console/interface/Makefile.am | 6 +- services/console/interface/ps_watcher.py | 344 +++++++++++++++++++++++ 4 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 services/console/README create mode 100644 services/console/interface/ps_watcher.py diff --git a/services/console/README b/services/console/README new file mode 100644 index 00000000..a3aa6e1a --- /dev/null +++ b/services/console/README @@ -0,0 +1,12 @@ +Defining new tabs in the developer console +========================================== + +The tabs are top-level packages inside 'interface/'. + +Each package used as a tab must have a class Interface, instantiatable +with no arguments, with an attribute 'widget' that is a Gtk widget to be +placed in the tab. That's it. + +Tabs are automatically run under the GLib main loop, dbus-python is set up +to use it, and the shared dbus-python session-bus connection is expected to +exist. diff --git a/services/console/console.py b/services/console/console.py index ec74b8df..32ff1032 100755 --- a/services/console/console.py +++ b/services/console/console.py @@ -53,6 +53,7 @@ class Console: self._load_interface('memphis', 'Memphis') self._load_interface('logviewer', 'Log Viewer') self._load_interface('terminal', 'Terminal') + self._load_interface('ps_watcher', 'Presence') main_hbox = gtk.HBox() main_hbox.pack_start(self.notebook, True, True, 0) @@ -86,6 +87,7 @@ class Service(dbus.service.Object): bus = dbus.SessionBus() name = dbus.service.BusName(CONSOLE_BUS, bus) + obj = Service(name) gtk.main() diff --git a/services/console/interface/Makefile.am b/services/console/interface/Makefile.am index 2654a4b0..3328c591 100644 --- a/services/console/interface/Makefile.am +++ b/services/console/interface/Makefile.am @@ -1,6 +1,6 @@ SUBDIRS = memphis logviewer terminal xo -sugardir = $(pkgdatadir)/shell/console/interface +sugardir = $(pkgdatadir)/services/console/interface sugar_PYTHON = \ - __init__.py - + __init__.py \ + ps_watcher.py diff --git a/services/console/interface/ps_watcher.py b/services/console/interface/ps_watcher.py new file mode 100644 index 00000000..9c47a0d3 --- /dev/null +++ b/services/console/interface/ps_watcher.py @@ -0,0 +1,344 @@ +# Copyright (C) 2007 Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +from gtk import VBox, Label, TreeView, Expander, ListStore, CellRendererText,\ + ScrolledWindow, CellRendererToggle +from gobject import timeout_add + + +logger = logging.getLogger('ps_watcher') +logging.basicConfig(filename='/tmp/ps_watcher.log') +logging.getLogger().setLevel(1) + + +PS_NAME = 'org.laptop.Sugar.Presence' +PS_PATH = '/org/laptop/Sugar/Presence' +PS_IFACE = PS_NAME +ACTIVITY_IFACE = PS_IFACE + '.Activity' +BUDDY_IFACE = PS_IFACE + '.Buddy' + +# keep these in sync with the calls to ListStore() +ACT_COL_PATH = 0 +ACT_COL_WEIGHT = 1 +ACT_COL_STRIKE = 2 +ACT_COL_ID = 3 +ACT_COL_COLOR = 4 +ACT_COL_TYPE = 5 +ACT_COL_NAME = 6 +BUDDY_COL_PATH = 0 +BUDDY_COL_WEIGHT = 1 +BUDDY_COL_STRIKE = 2 +BUDDY_COL_NICK = 3 +BUDDY_COL_OWNER = 4 +BUDDY_COL_COLOR = 5 +BUDDY_COL_IP4 = 6 +BUDDY_COL_CUR_ACT = 7 + + +class ActivityWatcher(object): + + def __init__(self, ps_watcher, object_path): + self.ps_watcher = ps_watcher + self.bus = ps_watcher.bus + self.proxy = self.bus.get_object(self.ps_watcher.unique_name, + object_path) + self.iface = dbus.Interface(self.proxy, ACTIVITY_IFACE) + self.object_path = object_path + self.appearing = True + self.disappearing = False + timeout_add(5000, self._finish_appearing) + + self.id = '?' + self.color = '?' + self.type = '?' + self.name = '?' + + self.iter = self.ps_watcher.add_activity(self) + + def _finish_appearing(self): + self.appearing = False + self.ps_watcher.activities_list_store.set(self.iter, ACT_COL_WEIGHT, + 400) + return False + + def disappear(self): + self.disappearing = True + self.ps_watcher.activities_list_store.set(self.iter, ACT_COL_STRIKE, + True) + timeout_add(5000, self._finish_disappearing) + + def _finish_disappearing(self): + self.ps_watcher.remove_activity(self) + return False + + +class BuddyWatcher(object): + + def __init__(self, ps_watcher, object_path): + self.ps_watcher = ps_watcher + self.bus = ps_watcher.bus + self.proxy = self.bus.get_object(self.ps_watcher.unique_name, + object_path) + self.iface = dbus.Interface(self.proxy, BUDDY_IFACE) + self.object_path = object_path + self.appearing = True + self.disappearing = False + timeout_add(5000, self._finish_appearing) + + self.nick = '?' + self.owner = False + self.color = '?' + self.ipv4 = '?' + self.cur_act = '?' + + self.iter = self.ps_watcher.add_buddy(self) + + def _finish_appearing(self): + self.appearing = False + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_WEIGHT, + 400) + return False + + def disappear(self): + self.disappearing = True + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_STRIKE, + True) + timeout_add(5000, self._finish_disappearing) + + def _finish_disappearing(self): + self.ps_watcher.remove_buddy(self) + return False + + +class PresenceServiceWatcher(VBox): + + def __init__(self, bus, unique_name): + VBox.__init__(self) + + logger.debug('Starting up PresenceServiceWatcher...') + self.bus = bus + self.unique_name = unique_name + self.proxy = bus.get_object(unique_name, PS_PATH) + self.iface = dbus.Interface(self.proxy, PS_IFACE) + + logger.debug('Starting up PresenceServiceWatcher (2)...') + + self.activities = None + self.iface.connect_to_signal('ActivityAppeared', + self._on_activity_appeared) + self.iface.connect_to_signal('ActivityDisappeared', + self._on_activity_disappeared) + self.iface.GetActivities(reply_handler=self._on_get_activities_success, + error_handler=self._on_get_activities_failure) + + self.buddies = None + self.iface.connect_to_signal('BuddyAppeared', + self._on_buddy_appeared) + self.iface.connect_to_signal('BuddyDisappeared', + self._on_buddy_disappeared) + self.iface.GetBuddies(reply_handler=self._on_get_buddies_success, + error_handler=self._on_get_buddies_failure) + + # keep this in sync with the ACT_COL_ constants + self.activities_list_store = ListStore(str, # object path + int, # weight (bold if new) + bool, # strikethrough (dead) + str, # ID + str, # color + str, # type + str, # name + ) + + self.pack_start(Label('Activities:'), False, False) + + self.activities_list = TreeView(self.activities_list_store) + self.activities_list.insert_column_with_attributes(0, 'Object path', + CellRendererText(), text=ACT_COL_PATH, + weight=ACT_COL_WEIGHT, strikethrough=ACT_COL_STRIKE) + self.activities_list.insert_column_with_attributes(1, 'ID', + CellRendererText(), text=ACT_COL_ID, + weight=ACT_COL_WEIGHT, strikethrough=ACT_COL_STRIKE) + self.activities_list.insert_column_with_attributes(2, 'Color', + CellRendererText(), text=ACT_COL_COLOR, + weight=ACT_COL_WEIGHT, strikethrough=ACT_COL_STRIKE) + self.activities_list.insert_column_with_attributes(3, 'Type', + CellRendererText(), text=ACT_COL_TYPE, weight=ACT_COL_WEIGHT, + strikethrough=ACT_COL_STRIKE) + self.activities_list.insert_column_with_attributes(4, 'Name', + CellRendererText(), text=ACT_COL_NAME, weight=ACT_COL_WEIGHT, + strikethrough=ACT_COL_STRIKE) + + scroller = ScrolledWindow() + scroller.add(self.activities_list) + self.pack_start(scroller) + + # keep this in sync with the BUDDY_COL_ constants + self.buddies_list_store = ListStore(str, int, bool, str, bool, + str, str, str) + + self.pack_start(Label('Buddies:'), False, False) + self.buddies_list = TreeView(self.buddies_list_store) + self.buddies_list.insert_column_with_attributes(0, 'Object path', + CellRendererText(), text=BUDDY_COL_PATH, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + self.buddies_list.insert_column_with_attributes(1, 'Nick', + CellRendererText(), text=BUDDY_COL_NICK, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + self.buddies_list.insert_column_with_attributes(2, 'Owner', + CellRendererToggle(), active=BUDDY_COL_OWNER) + self.buddies_list.insert_column_with_attributes(3, 'Color', + CellRendererText(), text=BUDDY_COL_COLOR, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + self.buddies_list.insert_column_with_attributes(4, 'IPv4', + CellRendererText(), text=BUDDY_COL_IP4, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + self.buddies_list.insert_column_with_attributes(5, 'CurAct', + CellRendererText(), text=BUDDY_COL_CUR_ACT, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + + scroller = ScrolledWindow() + scroller.add(self.buddies_list) + self.pack_start(scroller) + + self.iface.connect_to_signal('ActivityInvitation', + self._on_activity_invitation) + self.iface.connect_to_signal('PrivateInvitation', + self._on_private_invitation) + + def _on_get_activities_success(self, paths): + logger.debug('PS GetActivities() returned %r', paths) + self.activities = {} + for path in paths: + self.activities[path] = ActivityWatcher(self, path) + + def _on_get_activities_failure(self, e): + logger.warning('PS GetActivities() failed with %s', e) + + def add_activity(self, act): + path = act.object_path + if path.startswith('/org/laptop/Sugar/Presence/Activities/'): + path = '.../' + path[38:] + return self.activities_list_store.append((path, 700, False, + act.id, act.color, act.type, act.name)) + + def remove_activity(self, act): + self.activities.pop(act.object_path, None) + self.activities_list_store.remove(act.iter) + + def _on_activity_appeared(self, path): + if self.activities is None: + return + logger.debug('PS emitted ActivityAppeared("%s")', path) + self.activities[path] = ActivityWatcher(self, path) + + def _on_activity_disappeared(self, path): + if self.activities is None: + return + logger.debug('PS emitted ActivityDisappeared("%s")', path) + act = self.activities.get(path) + if act is None: + logger.warning('Trying to remove activity "%s" which is already ' + 'absent', path) + else: + # we don't remove the activity straight away, just cross it out + act.disappear() + + def _on_activity_invitation(self, path): + logger.debug('PS emitted ActivityInvitation("%s")', path) + + def _on_private_invitation(self, bus_name, conn, channel): + logger.debug('PS emitted PrivateInvitation("%s", "%s", "%s")', + bus_name, conn, channel) + + def _on_get_buddies_success(self, paths): + logger.debug('PS GetBuddies() returned %r', paths) + self.buddies = {} + for path in paths: + self.buddies[path] = BuddyWatcher(self, path) + + def _on_get_buddies_failure(self, e): + logger.warning('PS GetBuddies() failed with %s', e) + + def add_buddy(self, b): + path = b.object_path + if path.startswith('/org/laptop/Sugar/Presence/Buddies/'): + path = '.../' + path[35:] + return self.buddies_list_store.append((path, 700, False, + b.nick, b.owner, b.color, b.ipv4, b.cur_act)) + + def remove_buddy(self, b): + self.buddies.pop(b.object_path, None) + self.buddies_list_store.remove(b.iter) + + def _on_buddy_appeared(self, path): + if self.buddies is None: + return + logger.debug('PS emitted BuddyAppeared("%s")', path) + self.buddies[path] = BuddyWatcher(self, path) + + def _on_buddy_disappeared(self, path): + if self.buddies is None: + return + logger.debug('PS emitted BuddyDisappeared("%s")', path) + b = self.buddies.get(path) + if b is None: + logger.warning('Trying to remove buddy "%s" which is already ' + 'absent', path) + else: + # we don't remove the activity straight away, just cross it out + b.disappear() + + +class PresenceServiceNameWatcher(VBox): + + def __init__(self, bus): + VBox.__init__(self) + + self.bus = bus + + self.label = Label('Looking for Presence Service...') + bus.watch_name_owner(PS_NAME, self.on_name_owner_change) + + self.pack_start(self.label, False, False) + self.ps_watcher = None + + self.show_all() + + def on_name_owner_change(self, owner): + try: + if owner: + self.label.set_text('Presence Service running: unique name %s' + % owner) + if self.ps_watcher is not None: + self.remove(self.ps_watcher) + self.ps_watcher = PresenceServiceWatcher(self.bus, owner) + self.pack_start(self.ps_watcher) + self.show_all() + else: + self.label.set_text('Presence Service not running') + if self.ps_watcher is not None: + self.remove(self.ps_watcher) + self.ps_watcher = None + except Exception, e: + logger.warning('%s', e) + + +class Interface(object): + def __init__(self): + self.widget = PresenceServiceNameWatcher(dbus.SessionBus())