From a197097164a898b3e5d498d218a492dba095bebe Mon Sep 17 00:00:00 2001 From: Tomeu Vizoso Date: Wed, 1 Aug 2007 16:31:33 +0200 Subject: [PATCH 1/7] Do a dict.copy() so we don't overwrite the original. --- sugar/datastore/datastore.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sugar/datastore/datastore.py b/sugar/datastore/datastore.py index e0b4afad..1fbe6154 100644 --- a/sugar/datastore/datastore.py +++ b/sugar/datastore/datastore.py @@ -200,6 +200,9 @@ def delete(object_id): def find(query, sorting=None, limit=None, offset=None, reply_handler=None, error_handler=None): + + query = query.copy() + if sorting: query['order_by'] = sorting if limit: From d22f00d89486da4d0ae79cbd7252c20e142e7a86 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Wed, 1 Aug 2007 14:22:11 -0400 Subject: [PATCH 2/7] Don't set a server by default outside the emulator --- shell/intro/intro.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shell/intro/intro.py b/shell/intro/intro.py index b760c81d..ba633e39 100644 --- a/shell/intro/intro.py +++ b/shell/intro/intro.py @@ -221,7 +221,10 @@ class IntroWindow(gtk.Window): section = 'Server' if not cp.has_section(section): cp.add_section(section) - cp.set(section, 'Server', 'olpc.collabora.co.uk') + if env.is_emulator(): + cp.set(section, 'Server', 'olpc.collabora.co.uk') + else: + cp.set(section, 'Server', '') cp.set(section, 'Registered', 'False') config_path = os.path.join(env.get_profile_path(), 'config') From 4aeee4caa9db371477378b1524ed5772a8c3b9ee Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 2 Aug 2007 07:57:41 -0400 Subject: [PATCH 3/7] Icon(): Add scaled icon support (SVG) with editable fill and stroke colors --- sugar/graphics/icon.py | 109 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 96 insertions(+), 13 deletions(-) diff --git a/sugar/graphics/icon.py b/sugar/graphics/icon.py index c4c9d80a..731412f6 100644 --- a/sugar/graphics/icon.py +++ b/sugar/graphics/icon.py @@ -16,32 +16,115 @@ # Boston, MA 02111-1307, USA. import os - +import gobject import gtk +import re + +from sugar.graphics.style import Color class Icon(gtk.Image): - def __init__(self, name, size=gtk.ICON_SIZE_LARGE_TOOLBAR): - gtk.Image.__init__(self) + __gtype_name__ = 'SugarIcon' + __gproperties__ = { + 'xo-color' : (object, None, None, + gobject.PARAM_WRITABLE), + 'fill-color' : (object, None, None, + gobject.PARAM_READWRITE), + 'stroke-color' : (object, None, None, + gobject.PARAM_READWRITE) + } + + def __init__(self, name, size=gtk.ICON_SIZE_LARGE_TOOLBAR, **kwargs): + self._fill_color = None + self._stroke_color = None + self._icon_name = name + self._size = size + self._theme = gtk.icon_theme_get_default() + gobject.GObject.__init__(self, **kwargs) + + # If we have a non-styled-icon + if not self._fill_color and not self._stroke_color: + self._update_normal_icon() + + def _get_pixbuf(self, data, width, height): + loader = gtk.gdk.PixbufLoader('svg') + loader.set_size(width, height) + loader.write(data, len(data)) + loader.close() + return loader.get_pixbuf() + + def _read_icon_data(self, icon_name): + filename = self._get_real_name(icon_name) + icon_file = open(filename, 'r') + data = icon_file.read() + icon_file.close() + + return data + + def _update_normal_icon(self): icon_theme = gtk.icon_theme_get_for_screen(self.get_screen()) icon_set = gtk.IconSet() - normal_name = name - if icon_theme.has_icon(normal_name): + if icon_theme.has_icon(self._icon_name): source = gtk.IconSource() - source.set_icon_name(normal_name) - icon_set.add_source(source) - elif os.path.exists(normal_name): - source = gtk.IconSource() - source.set_filename(normal_name) + source.set_icon_name(self._icon_name) icon_set.add_source(source) - inactive_name = name + '-inactive' + inactive_name = self._icon_name + '-inactive' if icon_theme.has_icon(inactive_name): source = gtk.IconSource() source.set_icon_name(inactive_name) source.set_state(gtk.STATE_INSENSITIVE) icon_set.add_source(source) - - self.set_from_icon_set(icon_set, size) + self.set_from_icon_set(icon_set, self._size) + + def _update_icon(self): + if not self._fill_color and not self._stroke_color: + self._update_normal_icon() + return + + if not self._data: + data = self._read_icon_data(self._icon_name) + else: + data = self._data + + if self._fill_color: + entity = '' % self._fill_color + data = re.sub('', entity, data) + + if self._stroke_color: + entity = '' % self._stroke_color + data = re.sub('', entity, data) + + self._data = data + + # Redraw pixbuf + [w, h] = gtk.icon_size_lookup(self._size) + pixbuf = self._get_pixbuf(self._data, w, h) + self.set_from_pixbuf(pixbuf) + + def _get_real_name(self, name): + info = self._theme.lookup_icon(name, self._size, 0) + if not info: + raise ValueError("Icon '" + name + "' not found.") + fname = info.get_filename() + del info + return fname + + def do_set_property(self, pspec, value): + if pspec.name == 'xo-color': + self.props.fill_color = value.get_fill_color() + self.props.stroke_color = value.get_stroke_color() + elif pspec.name == 'fill-color': + self._fill_color = value + self._update_icon() + elif pspec.name == 'stroke-color': + self._stroke_color = value + self._update_icon() + + def do_get_property(self, pspec): + if pspec.name == 'fill-color': + return self._fill_color + elif pspec.name == 'stroke-color': + return self._stroke_color From f52e55381a9a57b9f08e7f28a0bf5f4ffae104d2 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Thu, 2 Aug 2007 14:14:21 -0400 Subject: [PATCH 4/7] Make the whole wedge in the activity ring clickable. #2029 --- shell/view/home/activitiesdonut.py | 45 ++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/shell/view/home/activitiesdonut.py b/shell/view/home/activitiesdonut.py index 746af4f5..125d9b89 100644 --- a/shell/view/home/activitiesdonut.py +++ b/shell/view/home/activitiesdonut.py @@ -147,6 +147,8 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): self._model.connect('activity-removed', self._activity_removed_cb) self._model.connect('active-activity-changed', self._activity_changed_cb) + self.connect('button-release-event', self._button_release_event_cb) + def _get_icon_from_activity(self, activity): for icon in self._activities: if icon.get_activity().equals(activity): @@ -170,18 +172,49 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): def _add_activity(self, activity): icon = ActivityIcon(activity) - icon.connect('activated', self._activity_icon_clicked_cb) self.append(icon, hippo.PACK_FIXED) self._activities.append(icon) self.emit_paint_needed(0, 0, -1, -1) - def _activity_icon_clicked_cb(self, icon): - activity = icon.get_activity() + def _get_activity(self, x, y): + # Compute the distance from the center. + [width, height] = self.get_allocation() + x -= width / 2 + y -= height / 2 + r = math.hypot(x, y) + + # Ignore the click if it's not inside the donut + if r < self._get_inner_radius() or r > self._get_radius(): + return None + + # Now figure out where in the donut the click was. + angle = math.atan2(-y, -x) + math.pi + + # Unfortunately, _get_angles() doesn't count from 0 to 2pi, it + # counts from roughly pi/2 to roughly 5pi/2. So we have to + # compare its return values against both angle and angle+2pi + high_angle = angle + 2 * math.pi + + for index, activity in enumerate(self._model): + [angle_start, angle_end] = self._get_angles(index) + if angle_start < angle and angle_end > angle: + return activity + elif angle_start < high_angle and angle_end > high_angle: + return activity + + return None + + def _button_release_event_cb(self, item, event): + activity = self._get_activity(event.x, event.y) + if activity is None: + return False + activity_host = self._shell.get_activity(activity.get_activity_id()) if activity_host: activity_host.present() + return True def _get_angles(self, index): angle = 2 * math.pi / 8 @@ -251,9 +284,7 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): radius = (self._get_inner_radius() + self._get_radius()) / 2 - i = 0 - for h_activity in self._model: - icon = self._get_icon_from_activity(h_activity) + for i, icon in enumerate(self._activities): [angle_start, angle_end] = self._get_angles(i) angle = angle_start + (angle_end - angle_start) / 2 @@ -262,5 +293,3 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): x = int(radius * math.cos(angle)) - icon_width / 2 y = int(radius * math.sin(angle)) - icon_height / 2 self.set_position(icon, x + width / 2, y + height / 2) - - i += 1 From 88401d31b4fb53d1b82c76f978907e2d40ae552f Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Fri, 3 Aug 2007 11:31:21 -0400 Subject: [PATCH 5/7] Make activity ring wedges represent approximate size of the activity. #2030 --- shell/model/homeactivity.py | 6 ++ shell/view/home/activitiesdonut.py | 94 ++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/shell/model/homeactivity.py b/shell/model/homeactivity.py index e5e00cd8..c45e5c75 100644 --- a/shell/model/homeactivity.py +++ b/shell/model/homeactivity.py @@ -58,6 +58,7 @@ class HomeActivity(gobject.GObject): self._window = None self._xid = None + self._pid = None self._service = None self._activity_id = activity_id self._bundle = bundle @@ -81,6 +82,7 @@ class HomeActivity(gobject.GObject): self._window = window self._xid = window.get_xid() + self._pid = window.get_pid() def get_service(self): """Get the activity service @@ -168,6 +170,10 @@ class HomeActivity(gobject.GObject): """ return self._launch_time + def get_pid(self): + """Returns the activity's PID""" + return self._pid + def equals(self, activity): if self._activity_id and activity.get_activity_id(): return self._activity_id == activity.get_activity_id() diff --git a/shell/view/home/activitiesdonut.py b/shell/view/home/activitiesdonut.py index 125d9b89..0e959658 100644 --- a/shell/view/home/activitiesdonut.py +++ b/shell/view/home/activitiesdonut.py @@ -129,7 +129,10 @@ class ActivityIcon(CanvasIcon): self._cleanup() self._level = 100.0 self.props.xo_color = self._orig_color - self.emit_paint_needed(0, 0, -1, -1) + + # Force the donut to redraw now that we know how much memory + # the activity is using. + self.emit_request_changed() def get_activity(self): return self._activity @@ -141,6 +144,7 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): self._activities = [] self._shell = shell + self._angles = [] self._model = shell.get_model().get_home() self._model.connect('activity-added', self._activity_added_cb) @@ -216,11 +220,90 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): activity_host.present() return True + MAX_ACTIVITIES = 10 + MIN_ACTIVITY_WEDGE_SIZE = 1.0 / MAX_ACTIVITIES + + def _get_activity_sizes(self): + # First get the size of each process that hosts an activity, + # and the number of activities it hosts. + process_size = {} + num_activities = {} + total_activity_size = 0 + for activity in self._model: + pid = activity.get_pid() + if not pid: + # Still starting up, hasn't opened a window yet + continue + + if process_size.has_key(pid): + num_activities[pid] += 1 + continue + + try: + statm = open('/proc/%s/statm' % pid) + # We use "RSS" (the second field in /proc/PID/statm) + # for the activity size because that's what ps and top + # use for calculating "%MEM". We multiply by 4 to + # convert from pages to kb. + process_size[pid] = int(statm.readline().split()[1]) * 4 + total_activity_size += process_size[pid] + num_activities[pid] = 1 + statm.close() + except IOError: + logging.warn('ActivitiesDonut: could not read /proc/%s/statm' % + pid) + except (IndexError, ValueError): + logging.warn('ActivitiesDonut: /proc/%s/statm was not in ' + + 'expected format' % pid) + + # Next, see how much free memory is left. + try: + meminfo = open('/proc/meminfo') + meminfo.readline() + free_memory = int(meminfo.readline()[9:-3]) + meminfo.close() + except IOError: + logging.warn('ActivitiesDonut: could not read /proc/meminfo') + except (IndexError, ValueError): + logging.warn('ActivitiesDonut: /proc/meminfo was not in ' + + 'expected format') + + # Each activity starts with MIN_ACTIVITY_WEDGE_SIZE. The + # remaining space in the donut is allocated proportionately + # among the activities-of-known-size and the free space + used_space = ActivitiesDonut.MIN_ACTIVITY_WEDGE_SIZE * len(self._model) + remaining_space = max(0.0, 1.0 - used_space) + + total_memory = total_activity_size + free_memory + + activity_sizes = [] + for activity in self._model: + percent = ActivitiesDonut.MIN_ACTIVITY_WEDGE_SIZE + pid = activity.get_pid() + if process_size.has_key(pid): + size = process_size[pid] / num_activities[pid] + percent += remaining_space * size / total_memory + activity_sizes.append(percent) + + return activity_sizes + + def _compute_angles(self): + percentages = self._get_activity_sizes() + self._angles = [] + if len(percentages) == 0: + return + + # The first wedge (Journal) should be centered at 6 o'clock + size = percentages[0] * 2 * math.pi + angle = (math.pi - size) / 2 + self._angles.append(angle) + + for size in percentages: + self._angles.append(self._angles[-1] + size * 2 * math.pi) + def _get_angles(self, index): - angle = 2 * math.pi / 8 - bottom_align = (math.pi - angle) / 2 - return [index * angle + bottom_align, - (index + 1) * angle + bottom_align] + return [self._angles[index], + self._angles[(index + 1) % len(self._angles)]] def _get_radius(self): [width, height] = self.get_allocation() @@ -284,6 +367,7 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): radius = (self._get_inner_radius() + self._get_radius()) / 2 + self._compute_angles() for i, icon in enumerate(self._activities): [angle_start, angle_end] = self._get_angles(i) angle = angle_start + (angle_end - angle_start) / 2 From 56d8e5e748324cc1b2bc574803497b7c2432a37f Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Mon, 6 Aug 2007 09:23:45 -0400 Subject: [PATCH 6/7] Don't use gtk.get_current_event_time() as the timestamp for window.activate() gtk.get_current_event_time() will return 0 if there's no "current event" (which will happen if this is invoked from a SugarKeyGrabber callback, because that intercepts events below the gtk level). And libwnck will g_warn if we pass "0" to window.activate(). Since matchbox doesn't look at the timestamp anyway, it's simplest to just always pass "1" there. (Needed for part of #2159) --- shell/view/ActivityHost.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shell/view/ActivityHost.py b/shell/view/ActivityHost.py index 1d3a32d6..fa3aaf61 100644 --- a/shell/view/ActivityHost.py +++ b/shell/view/ActivityHost.py @@ -65,10 +65,15 @@ class ActivityHost: pass def present(self): - self._window.activate(gtk.get_current_event_time()) + # wnck.Window.activate() expects a timestamp, but we don't + # always have one, and libwnck will complain if we pass "0", + # and matchbox doesn't look at the timestamp anyway. So we + # just always pass "1". + self._window.activate(1) def close(self): - self._window.close(gtk.get_current_event_time()) + # The "1" is a fake timestamp as with present() + self._window.close(1) def show_dialog(self, dialog): dialog.show() From 2485e15030d2ae8b61272dbc16081e5e4f9c3e40 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Mon, 6 Aug 2007 15:33:27 -0400 Subject: [PATCH 7/7] Add a basic palette with "Resume" and "Stop" to activity ring icons. #2028 --- shell/view/home/activitiesdonut.py | 66 ++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/shell/view/home/activitiesdonut.py b/shell/view/home/activitiesdonut.py index 0e959658..0c690b2e 100644 --- a/shell/view/home/activitiesdonut.py +++ b/shell/view/home/activitiesdonut.py @@ -14,12 +14,16 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -import hippo -import math -import gobject import colorsys +from gettext import gettext as _ +import math + +import hippo +import gobject +import gtk from sugar.graphics.canvasicon import CanvasIcon +from sugar.graphics.palette import Palette from sugar.graphics import style from sugar.graphics import xocolor from sugar import profile @@ -45,6 +49,13 @@ def html_to_rgb(html_color): class ActivityIcon(CanvasIcon): _INTERVAL = 250 + __gsignals__ = { + 'resume': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'stop': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + def __init__(self, activity): icon_name = activity.get_icon_name() self._orig_color = activity.get_icon_color() @@ -61,15 +72,38 @@ class ActivityIcon(CanvasIcon): self._activity = activity self._pulse_id = 0 + palette = Palette(_('Starting...')) + self.set_palette(palette) + activity.connect('notify::launching', self._launching_changed_cb) if activity.props.launching: self._start_pulsing() + else: + self._setup_palette() + + def _setup_palette(self): + palette = self.get_palette() + + palette.set_primary_text(self._activity.get_title()) + + resume_menu_item = gtk.MenuItem(_('Resume')) + resume_menu_item.connect('activate', self._resume_activate_cb) + palette.append_menu_item(resume_menu_item) + resume_menu_item.show() + + # FIXME: kludge + if self._activity.get_type() != "org.laptop.JournalActivity": + stop_menu_item = gtk.MenuItem(_('Stop')) + stop_menu_item.connect('activate', self._stop_activate_cb) + palette.append_menu_item(stop_menu_item) + stop_menu_item.show() def _launching_changed_cb(self, activity, pspec): if activity.props.launching: self._start_pulsing() else: self._stop_pulsing() + self._setup_palette() def __del__(self): self._cleanup() @@ -134,6 +168,12 @@ class ActivityIcon(CanvasIcon): # the activity is using. self.emit_request_changed() + def _resume_activate_cb(self, menuitem): + self.emit('resume') + + def _stop_activate_cb(self, menuitem): + self.emit('stop') + def get_activity(self): return self._activity @@ -176,12 +216,32 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): def _add_activity(self, activity): icon = ActivityIcon(activity) + icon.connect('resume', self._activity_icon_resumed_cb) + icon.connect('stop', self._activity_icon_stop_cb) self.append(icon, hippo.PACK_FIXED) self._activities.append(icon) self.emit_paint_needed(0, 0, -1, -1) + def _activity_icon_resumed_cb(self, icon): + activity = icon.get_activity() + activity_host = self._shell.get_activity(activity.get_activity_id()) + if activity_host: + activity_host.present() + else: + logging.error("Could not find ActivityHost for activity %s" % + activity.get_activity_id()) + + def _activity_icon_stop_cb(self, icon): + activity = icon.get_activity() + activity_host = self._shell.get_activity(activity.get_activity_id()) + if activity_host: + activity_host.close() + else: + logging.error("Could not find ActivityHost for activity %s" % + activity.get_activity_id()) + def _get_activity(self, x, y): # Compute the distance from the center. [width, height] = self.get_allocation()