diff --git a/NEWS b/NEWS index 9c8e0960..72afe0ff 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,10 @@ * Turn off logging by default. Logs may be re-enabled on a per-module basis by adding environment variables like SHELL_DEBUG and RECORD_DEBUG to the sugar environment + +Snapshot 088c7612e3 + +* Don't follow the cursor when expanding to secondary palette. (marco) * #2370 Update spanish translation. (marco) * #2014 Add icons in the share dropdown in activities. (marco) 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..b9138e16 --- /dev/null +++ b/services/console/interface/ps_watcher.py @@ -0,0 +1,466 @@ +# 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 +from hashlib import sha1 + +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 +BUDDY_COL_KEY_ID = 8 + + +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) + + self.iface.GetId(reply_handler=self._on_get_id_success, + error_handler=self._on_get_id_failure) + + self.iface.GetColor(reply_handler=self._on_get_color_success, + error_handler=self._on_get_color_failure) + + self.iface.GetType(reply_handler=self._on_get_type_success, + error_handler=self._on_get_type_failure) + + self.iface.GetName(reply_handler=self._on_get_name_success, + error_handler=self._on_get_name_failure) + + def _on_get_id_success(self, ident): + self.id = ident + self.ps_watcher.activities_list_store.set(self.iter, ACT_COL_ID, ident) + + def _on_get_id_failure(self, e): + logger.warning('.GetId(): %s', self.object_path, e) + self.ps_watcher.activities_list_store.set(self.iter, ACT_COL_ID, + '!') + + def _on_get_color_success(self, color): + self.color = color + self.ps_watcher.activities_list_store.set(self.iter, ACT_COL_COLOR, + color) + + def _on_get_color_failure(self, e): + logger.warning('.GetColor(): %s', self.object_path, e) + self.ps_watcher.activities_list_store.set(self.iter, ACT_COL_COLOR, + '!') + + def _on_get_type_success(self, type_): + self.type = type_ + self.ps_watcher.activities_list_store.set(self.iter, ACT_COL_TYPE, + type_) + + def _on_get_type_failure(self, e): + logger.warning('.GetType(): %s', self.object_path, e) + self.ps_watcher.activities_list_store.set(self.iter, ACT_COL_TYPE, + '!') + + def _on_get_name_success(self, name): + self.name = name + self.ps_watcher.activities_list_store.set(self.iter, ACT_COL_NAME, + name) + + def _on_get_name_failure(self, e): + logger.warning('.GetName(): %s', self.object_path, e) + self.ps_watcher.activities_list_store.set(self.iter, ACT_COL_NAME, + '!') + + 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.keyid = '?' + + self.iter = self.ps_watcher.add_buddy(self) + + self.iface.GetProperties(reply_handler=self._on_get_props_success, + error_handler=self._on_get_props_failure, + byte_arrays=True) + + def _on_get_props_success(self, props): + # ignore key for now + self.nick = props.get('nick', '?') + self.owner = props.get('owner', False) + self.color = props.get('color', '?') + self.ipv4 = props.get('ip4-address', '?') + self.ipv4 = props.get('ip4-address', '?') + self.cur_act = props.get('current-activity', '?') + key = props.get('key', None) + if key is not None: + self.keyid = sha1(key).hexdigest()[:8] + '...' + else: + self.keyid = '?' + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_NICK, + self.nick) + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_OWNER, + self.owner) + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_COLOR, + self.color) + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_IP4, + self.ipv4) + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_CUR_ACT, + self.cur_act) + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_KEY_ID, + self.keyid) + + def _on_get_props_failure(self, e): + logger.warning('.GetProperties(): %s', self.object_path, e) + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_NICK, '!') + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_OWNER, + False) + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_COLOR, '!') + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_IP4, '!') + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_CUR_ACT, + '!') + self.ps_watcher.buddies_list_store.set(self.iter, BUDDY_COL_KEY_ID, + '!') + + + 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) + c = self.activities_list.insert_column_with_attributes(0, + 'Object path', CellRendererText(), text=ACT_COL_PATH, + weight=ACT_COL_WEIGHT, strikethrough=ACT_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(ACT_COL_PATH) + c = self.activities_list.insert_column_with_attributes(1, 'ID', + CellRendererText(), text=ACT_COL_ID, + weight=ACT_COL_WEIGHT, strikethrough=ACT_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(ACT_COL_ID) + c = self.activities_list.insert_column_with_attributes(2, 'Color', + CellRendererText(), text=ACT_COL_COLOR, + weight=ACT_COL_WEIGHT, strikethrough=ACT_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(ACT_COL_COLOR) + c = self.activities_list.insert_column_with_attributes(3, 'Type', + CellRendererText(), text=ACT_COL_TYPE, weight=ACT_COL_WEIGHT, + strikethrough=ACT_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(ACT_COL_TYPE) + c = self.activities_list.insert_column_with_attributes(4, 'Name', + CellRendererText(), text=ACT_COL_NAME, weight=ACT_COL_WEIGHT, + strikethrough=ACT_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(ACT_COL_NAME) + + 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, str) + + self.pack_start(Label('Buddies:'), False, False) + self.buddies_list = TreeView(self.buddies_list_store) + c = self.buddies_list.insert_column_with_attributes(0, 'Object path', + CellRendererText(), text=BUDDY_COL_PATH, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(BUDDY_COL_PATH) + c = self.buddies_list.insert_column_with_attributes(1, 'Key ID', + CellRendererText(), text=BUDDY_COL_KEY_ID, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(BUDDY_COL_KEY_ID) + c = self.buddies_list.insert_column_with_attributes(2, 'Nick', + CellRendererText(), text=BUDDY_COL_NICK, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(BUDDY_COL_NICK) + c = self.buddies_list.insert_column_with_attributes(3, 'Owner', + CellRendererToggle(), active=BUDDY_COL_OWNER) + c = self.buddies_list.insert_column_with_attributes(4, 'Color', + CellRendererText(), text=BUDDY_COL_COLOR, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(BUDDY_COL_OWNER) + c = self.buddies_list.insert_column_with_attributes(5, 'IPv4', + CellRendererText(), text=BUDDY_COL_IP4, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(BUDDY_COL_IP4) + c = self.buddies_list.insert_column_with_attributes(6, 'CurAct', + CellRendererText(), text=BUDDY_COL_CUR_ACT, + weight=BUDDY_COL_WEIGHT, strikethrough=BUDDY_COL_STRIKE) + c.set_resizable(True) + c.set_sort_column_id(BUDDY_COL_CUR_ACT) + + 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, b.keyid)) + + 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()) diff --git a/sugar/graphics/palette.py b/sugar/graphics/palette.py index 89961388..b779eec7 100644 --- a/sugar/graphics/palette.py +++ b/sugar/graphics/palette.py @@ -69,6 +69,8 @@ class Palette(gobject.GObject): gobject.GObject.__init__(self) self._full_request = [0, 0] + self._cursor_x = 0 + self._cursor_y = 0 self._state = self._SECONDARY self._invoker = None self._group_id = None @@ -250,6 +252,12 @@ class Palette(gobject.GObject): self._set_state(state) + def _update_cursor_position(self): + display = gtk.gdk.display_get_default() + screen, x, y, mask = display.get_pointer() + self._cursor_x = x + self._cursor_y = y + def _update_position(self): x = y = 0 @@ -259,11 +267,11 @@ class Palette(gobject.GObject): position = self._position if position == self.AT_CURSOR: - display = gtk.gdk.display_get_default() - screen, x, y, mask = display.get_pointer() dist = style.PALETTE_CURSOR_DISTANCE + rect = gtk.gdk.Rectangle(self._cursor_x - dist, + self._cursor_y - dist, + dist * 2, dist * 2) - rect = gtk.gdk.Rectangle(x - dist, y - dist, dist * 2, dist * 2) x, y = self._get_at_cursor_position(rect) elif position == self.AROUND: x, y = self._get_around_position() @@ -282,6 +290,7 @@ class Palette(gobject.GObject): if self._up: return + self._update_cursor_position() self._update_full_request() self._invoker.connect_to_parent()