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') 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/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() diff --git a/shell/view/home/activitiesdonut.py b/shell/view/home/activitiesdonut.py index 746af4f5..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() @@ -129,7 +163,16 @@ 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 _resume_activate_cb(self, menuitem): + self.emit('resume') + + def _stop_activate_cb(self, menuitem): + self.emit('stop') def get_activity(self): return self._activity @@ -141,12 +184,15 @@ 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) 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,24 +216,154 @@ class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): def _add_activity(self, activity): icon = ActivityIcon(activity) - icon.connect('activated', self._activity_icon_clicked_cb) + 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_clicked_cb(self, icon): + 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() + 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 + + 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() @@ -251,9 +427,8 @@ 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) + 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 @@ -262,5 +437,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 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: 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