diff options
Diffstat (limited to 'src/view/home/activitiesdonut.py')
-rwxr-xr-x | src/view/home/activitiesdonut.py | 556 |
1 files changed, 556 insertions, 0 deletions
diff --git a/src/view/home/activitiesdonut.py b/src/view/home/activitiesdonut.py new file mode 100755 index 0000000..8e09006 --- /dev/null +++ b/src/view/home/activitiesdonut.py @@ -0,0 +1,556 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# 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 colorsys +from gettext import gettext as _ +import logging +import math +import os + +import hippo +import gobject +import gtk + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.palette import Palette +from sugar.graphics import style +from sugar.graphics import xocolor +from sugar import profile +import proc_smaps + +_MAX_ACTIVITIES = 6 +_MIN_WEDGE_SIZE = 1.0 / _MAX_ACTIVITIES +_DONUT_SIZE = style.zoom(450) + +# TODO: rgb_to_html and html_to_rgb are useful elsewhere +# we should put this in a common module +def rgb_to_html(r, g, b): + """ (r, g, b) tuple (in float format) -> #RRGGBB """ + return '#%02x%02x%02x' % (int(r * 255), int(g * 255), int(b * 255)) + +def html_to_rgb(html_color): + """ #RRGGBB -> (r, g, b) tuple (in float format) """ + html_color = html_color.strip() + if html_color[0] == '#': + html_color = html_color[1:] + if len(html_color) != 6: + raise ValueError, "input #%s is not in #RRGGBB format" % html_color + r, g, b = html_color[:2], html_color[2:4], html_color[4:] + r, g, b = [int(n, 16) for n in (r, g, b)] + r, g, b = (r / 255.0, g / 255.0, b / 255.0) + return (r, g, b) + +class ActivityIcon(CanvasIcon): + _INTERVAL = 200 + + __gsignals__ = { + 'resume': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'stop': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self, activity): + self._orig_color = activity.get_icon_color() + self._icon_colors = self._compute_icon_colors() + + self._direction = 0 + self._level_max = len(self._icon_colors) - 1 + self._level = self._level_max + color = self._icon_colors[self._level] + + CanvasIcon.__init__(self, xo_color=color, cache=True, + size=style.MEDIUM_ICON_SIZE) + + icon_path = activity.get_icon_path() + if icon_path: + self.props.file_name = icon_path + else: + self.props.icon_name = 'image-missing' + + self._activity = activity + self._pulse_id = 0 + + self.size = _MIN_WEDGE_SIZE + + palette = Palette(_('Starting...')) + self.set_palette(palette) + + if activity.props.launching: + self._start_pulsing() + activity.connect('notify::launching', self._launching_changed_cb) + else: + self._setup_palette() + + def _setup_palette(self): + palette = self.get_palette() + + palette.set_primary_text(self._activity.get_title()) + + resume_menu_item = MenuItem(_('Resume'), 'activity-start') + resume_menu_item.connect('activate', self._resume_activate_cb) + palette.menu.append(resume_menu_item) + resume_menu_item.show() + + # FIXME: kludge + if self._activity.get_type() != "org.laptop.JournalActivity": + stop_menu_item = MenuItem(_('Stop'), 'activity-stop') + stop_menu_item.connect('activate', self._stop_activate_cb) + palette.menu.append(stop_menu_item) + stop_menu_item.show() + + def _launching_changed_cb(self, activity, pspec): + if not activity.props.launching: + self._stop_pulsing() + self._setup_palette() + + def __del__(self): + self._cleanup() + + def _cleanup(self): + if self._pulse_id: + gobject.source_remove(self._pulse_id) + self._pulse_id = 0 + + def _compute_icon_colors(self): + _LEVEL_MAX = 1.6 + _LEVEL_STEP = 0.16 + _LEVEL_MIN = 0.0 + icon_colors = {} + level = _LEVEL_MIN + for i in range(0, int(_LEVEL_MAX / _LEVEL_STEP)): + icon_colors[i] = self._get_icon_color_for_level(level) + level += _LEVEL_STEP + return icon_colors + + def _get_icon_color_for_level(self, level): + factor = math.sin(level) + h, s, v = colorsys.rgb_to_hsv(*html_to_rgb(self._orig_color.get_fill_color())) + new_fill = rgb_to_html(*colorsys.hsv_to_rgb(h, s * factor, v)) + h, s, v = colorsys.rgb_to_hsv(*html_to_rgb(self._orig_color.get_stroke_color())) + new_stroke = rgb_to_html(*colorsys.hsv_to_rgb(h, s * factor, v)) + return xocolor.XoColor("%s,%s" % (new_stroke, new_fill)) + + def _pulse_cb(self): + if self._direction == 1: + self._level += 1 + if self._level > self._level_max: + self._direction = 0 + self._level = self._level_max + elif self._direction == 0: + self._level -= 1 + if self._level <= 0: + self._direction = 1 + self._level = 0 + + self.props.xo_color = self._icon_colors[self._level] + self.emit_paint_needed(0, 0, -1, -1) + return True + + def _start_pulsing(self): + if self._pulse_id: + return + + self._pulse_id = gobject.timeout_add(self._INTERVAL, self._pulse_cb) + + def _stop_pulsing(self): + if not self._pulse_id: + return + + self._cleanup() + self._level = 100.0 + self.props.xo_color = self._orig_color + + 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 + +class ActivitiesDonut(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = 'SugarActivitiesDonut' + def __init__(self, shell, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + + self._activities = [] + self._shell = shell + self._angles = [] + self._shell_mappings = proc_smaps.get_shared_mapping_names(os.getpid()) + + self._layout = _Layout() + self.set_layout(self._layout) + + 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('pending-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): + return icon + + def _activity_added_cb(self, model, activity): + self._add_activity(activity) + + def _activity_removed_cb(self, model, activity): + self._remove_activity(activity) + + def _activity_changed_cb(self, model, activity): + self.emit_paint_needed(0, 0, -1, -1) + + def _remove_activity(self, activity): + icon = self._get_icon_from_activity(activity) + if icon: + self.remove(icon) + icon._cleanup() + self._activities.remove(icon) + self._compute_angles() + + 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._compute_angles() + + 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 + + def _set_fixed_arc_size(self): + """Set fixed arc size""" + + n = len(self._activities) + if n > _MAX_ACTIVITIES: + size = 1.0 / n + else: + size = 1.0 / _MAX_ACTIVITIES + + for act in self._activities: + act.size = size + + def _update_activity_sizes(self): + """Currently the size of an activity on the donut does not + represent it's memory usage. This is disabled because it was + either not working perfectly or a little confusing. See #3605""" + self._set_fixed_arc_size() + return + + # Get the memory mappings of each process that hosts an + # activity, and count how many activity instances each + # activity process hosts, and how many processes are mapping + # each shared library, etc + process_mappings = {} + num_activities = {} + num_mappings = {} + unknown_size_activities = 0 + for activity in self._model: + pid = activity.get_pid() + if not pid: + # Still starting up, hasn't opened a window yet + unknown_size_activities += 1 + continue + + if num_activities.has_key(pid): + num_activities[pid] += 1 + continue + + try: + mappings = proc_smaps.get_mappings(pid, self._shell_mappings) + for mapping in mappings: + if mapping.shared > 0: + if num_mappings.has_key(mapping.name): + num_mappings[mapping.name] += 1 + else: + num_mappings[mapping.name] = 1 + process_mappings[pid] = mappings + num_activities[pid] = 1 + except Exception, e: + logging.warn('ActivitiesDonut: could not read /proc/%s/smaps: %r' + % (pid, e)) + + # Compute total memory used per process + process_size = {} + total_activity_size = 0 + for activity in self._model: + pid = activity.get_pid() + if not process_mappings.has_key(pid): + continue + + mappings = process_mappings[pid] + size = 0 + for mapping in mappings: + size += mapping.private + if mapping.shared > 0: + num = num_mappings[mapping.name] + size += mapping.shared / num + process_size[pid] = size + total_activity_size += size / num_activities[pid] + + # Now, see how much free memory is left. + free_memory = 0 + try: + meminfo = open('/proc/meminfo') + for line in meminfo.readlines(): + if line.startswith('MemFree:') or line.startswith('SwapFree:'): + free_memory += int(line[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') + + total_memory = float(total_activity_size + free_memory) + + # Each activity has an ideal size of: + # process_size[pid] / num_activities[pid] / total_memory + # (And the free memory wedge is ideally free_memory / + # total_memory) However, no activity wedge is allowed to be + # smaller than _MIN_WEDGE_SIZE. This means the small + # activities will use up extra space, which would make the + # ring overflow. We fix that by reducing the large activities + # and the free space proportionately. If there are activities + # of unknown size, they are simply carved out of the free + # space. + + free_percent = free_memory / total_memory + activity_sizes = [] + overflow = 0.0 + reducible = free_percent + for icon in self._activities: + pid = icon.get_activity().get_pid() + if process_size.has_key(pid): + icon.size = (process_size[pid] / num_activities[pid] / + total_memory) + if icon.size < _MIN_WEDGE_SIZE: + overflow += _MIN_WEDGE_SIZE - icon.size + icon.size = _MIN_WEDGE_SIZE + else: + reducible += icon.size - _MIN_WEDGE_SIZE + else: + icon.size = _MIN_WEDGE_SIZE + + if reducible > 0.0: + reduction = overflow / reducible + if unknown_size_activities > 0: + unknown_percent = _MIN_WEDGE_SIZE * unknown_size_activities + if (free_percent * (1 - reduction) < unknown_percent): + # The free wedge won't be large enough to fit the + # unknown-size activities. So adjust things + overflow += unknown_percent - free_percent + reducible -= free_percent + reduction = overflow / reducible + + if reduction > 0.0: + for icon in self._activities: + if icon.size > _MIN_WEDGE_SIZE: + icon.size -= (icon.size - _MIN_WEDGE_SIZE) * reduction + + def _compute_angles(self): + self._angles = [] + if len(self._activities) == 0: + return + + # Normally we don't _update_activity_sizes() when launching a + # new activity; but if the new wedge would overflow the ring + # then we have no choice. + total = reduce(lambda s1,s2: s1 + s2, + [icon.size for icon in self._activities]) + if total > 1.0: + self._update_activity_sizes() + + # The first wedge (Journal) should be centered at 6 o'clock + size = self._activities[0].size or _MIN_WEDGE_SIZE + angle = (math.pi - size * 2 * math.pi) / 2 + self._angles.append(angle) + + for icon in self._activities: + size = icon.size or _MIN_WEDGE_SIZE + self._angles.append(self._angles[-1] + size * 2 * math.pi) + + def redraw(self): + self._update_activity_sizes() + self._compute_angles() + self.emit_request_changed() + + def _get_angles(self, index): + return [self._angles[index], + self._angles[(index + 1) % len(self._angles)]] + + def _get_radius(self): + [width, height] = self.get_allocation() + return min(width, height) / 2 + + def _get_inner_radius(self): + return self._get_radius() * 0.5 + + def do_paint_below_children(self, cr, damaged_box): + [width, height] = self.get_allocation() + + cr.translate(width / 2, height / 2) + + radius = self._get_radius() + + # Outer Ring + cr.set_source_rgb(0xf1 / 255.0, 0xf1 / 255.0, 0xf1 / 255.0) + cr.arc(0, 0, radius, 0, 2 * math.pi) + cr.fill() + + # Selected Wedge + current_activity = self._model.get_pending_activity() + if current_activity is not None: + selected_index = self._model.index(current_activity) + [angle_start, angle_end] = self._get_angles(selected_index) + + cr.new_path() + cr.move_to(0, 0) + cr.line_to(radius * math.cos(angle_start), + radius * math.sin(angle_start)) + cr.arc(0, 0, radius, angle_start, angle_end) + cr.line_to(0, 0) + cr.set_source_rgb(1, 1, 1) + cr.fill() + + # Edges + if len(self._model): + n_edges = len(self._model) + 1 + else: + n_edges = 0 + + for i in range(0, n_edges): + cr.new_path() + cr.move_to(0, 0) + [angle, unused_angle] = self._get_angles(i) + cr.line_to(radius * math.cos(angle), + radius * math.sin(angle)) + + cr.set_source_rgb(0xe2 / 255.0, 0xe2 / 255.0, 0xe2 / 255.0) + cr.set_line_width(4) + cr.stroke_preserve() + + # Inner Ring + cr.new_path() + cr.arc(0, 0, self._get_inner_radius(), 0, 2 * math.pi) + cr.set_source_rgb(0xe2 / 255.0, 0xe2 / 255.0, 0xe2 / 255.0) + cr.fill() + + def do_allocate(self, width, height, origin_changed): + hippo.CanvasBox.do_allocate(self, width, height, origin_changed) + + radius = (self._get_inner_radius() + self._get_radius()) / 2 + + for i, icon in enumerate(self._activities): + [angle_start, angle_end] = self._get_angles(i) + angle = angle_start + (angle_end - angle_start) / 2 + + [icon_width, icon_height] = icon.get_allocation() + + 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) + +class _Layout(gobject.GObject,hippo.CanvasLayout): + __gtype_name__ = 'SugarDonutLayout' + def __init__(self): + gobject.GObject.__init__(self) + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + return _DONUT_SIZE, _DONUT_SIZE + + def do_get_width_request(self): + return _DONUT_SIZE, _DONUT_SIZE + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + for child in self._box.get_layout_children(): + min_width, child_width = child.get_width_request() + min_height, child_height = child.get_height_request(child_width) + + [angle_start, angle_end] = self._box._get_angles(i) + angle = angle_start + (angle_end - angle_start) / 2 + + x = int(radius * math.cos(angle)) - icon_width / 2 + y = int(radius * math.sin(angle)) - icon_height / 2 + + child.allocate(x + (width - child_width) / 2, + y + (height - child_height) / 2, + icon_width, icon_height, origin_changed) |