sugar-toolkit-gtk3/shell/view/home/activitiesdonut.py

444 lines
15 KiB
Python

# 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 hippo
import gobject
import gtk
from sugar.graphics.canvasicon 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
# 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 = 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()
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, icon_name=icon_name, xo_color=color,
size=style.MEDIUM_ICON_SIZE, cache=True)
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 = MenuItem(_('Resume'), 'zoom-activity')
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 activity.props.launching:
self._start_pulsing()
else:
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
# dispose of all rendered icons from launch feedback
self._clear_buffers()
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
# 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
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._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)
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()
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.
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')
# 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):
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
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
[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)