diff options
Diffstat (limited to 'src/view/home')
-rw-r--r-- | src/view/home/FriendView.py | 86 | ||||
-rw-r--r-- | src/view/home/FriendsBox.py | 67 | ||||
-rw-r--r-- | src/view/home/HomeBox.py | 287 | ||||
-rw-r--r-- | src/view/home/HomeWindow.py | 141 | ||||
-rw-r--r-- | src/view/home/Makefile.am | 14 | ||||
-rw-r--r-- | src/view/home/MeshBox.py | 615 | ||||
-rw-r--r-- | src/view/home/MyIcon.py | 24 | ||||
-rw-r--r-- | src/view/home/__init__.py | 16 | ||||
-rwxr-xr-x | src/view/home/activitiesdonut.py | 556 | ||||
-rwxr-xr-x | src/view/home/proc_smaps.py | 107 | ||||
-rw-r--r-- | src/view/home/snowflakelayout.py | 108 | ||||
-rw-r--r-- | src/view/home/spreadlayout.py | 246 | ||||
-rw-r--r-- | src/view/home/transitionbox.py | 93 |
13 files changed, 2360 insertions, 0 deletions
diff --git a/src/view/home/FriendView.py b/src/view/home/FriendView.py new file mode 100644 index 0000000..786589f --- /dev/null +++ b/src/view/home/FriendView.py @@ -0,0 +1,86 @@ +# 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 hippo +import gobject + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from sugar.presence import presenceservice +from sugar import activity + +from view.BuddyIcon import BuddyIcon + +class FriendView(hippo.CanvasBox): + def __init__(self, shell, buddy, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + + self._pservice = presenceservice.get_instance() + + self._buddy = buddy + self._buddy_icon = BuddyIcon(shell, buddy) + self._buddy_icon.props.size = style.LARGE_ICON_SIZE + self.append(self._buddy_icon) + + self._activity_icon = CanvasIcon(size=style.LARGE_ICON_SIZE) + self._activity_icon_visible = False + + if self._buddy.is_present(): + self._buddy_appeared_cb(buddy) + + self._buddy.connect('current-activity-changed', self._buddy_activity_changed_cb) + self._buddy.connect('appeared', self._buddy_appeared_cb) + self._buddy.connect('disappeared', self._buddy_disappeared_cb) + self._buddy.connect('color-changed', self._buddy_color_changed_cb) + + def _get_new_icon_name(self, ps_activity): + registry = activity.get_registry() + activity_info = registry.get_activity(ps_activity.props.type) + if activity_info: + return activity_info.icon + return None + + def _remove_activity_icon(self): + if self._activity_icon_visible: + self.remove(self._activity_icon) + self._activity_icon_visible = False + + def _buddy_activity_changed_cb(self, buddy, ps_activity=None): + if not ps_activity: + self._remove_activity_icon() + return + + # FIXME: use some sort of "unknown activity" icon rather + # than hiding the icon? + name = self._get_new_icon_name(ps_activity) + if name: + self._activity_icon.props.file_name = name + self._activity_icon.props.xo_color = buddy.get_color() + if not self._activity_icon_visible: + self.append(self._activity_icon, hippo.PACK_EXPAND) + self._activity_icon_visible = True + else: + self._remove_activity_icon() + + def _buddy_appeared_cb(self, buddy): + home_activity = self._buddy.get_current_activity() + self._buddy_activity_changed_cb(buddy, home_activity) + + def _buddy_disappeared_cb(self, buddy): + self._buddy_activity_changed_cb(buddy, None) + + def _buddy_color_changed_cb(self, buddy, color): + self._activity_icon.props.xo_color = buddy.get_color() diff --git a/src/view/home/FriendsBox.py b/src/view/home/FriendsBox.py new file mode 100644 index 0000000..e9efc57 --- /dev/null +++ b/src/view/home/FriendsBox.py @@ -0,0 +1,67 @@ +# 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 random + +import hippo +import gobject + +from sugar import profile +from sugar.graphics import style +from sugar.graphics.icon import CanvasIcon +from sugar.graphics.palette import Palette + +from view.home.FriendView import FriendView +from view.home.spreadlayout import SpreadLayout + +class FriendsBox(hippo.CanvasBox): + __gtype_name__ = 'SugarFriendsBox' + def __init__(self, shell): + hippo.CanvasBox.__init__(self, background_color=0xe2e2e2ff) + + self._shell = shell + self._friends = {} + + self._layout = SpreadLayout() + self.set_layout(self._layout) + + self._owner_icon = CanvasIcon(icon_name='computer-xo', cache=True, + xo_color=profile.get_color()) + self._owner_icon.props.size = style.LARGE_ICON_SIZE + palette = Palette(profile.get_nick_name()) + self._owner_icon.set_palette(palette) + self._layout.add_center(self._owner_icon) + + friends = self._shell.get_model().get_friends() + + for friend in friends: + self.add_friend(friend) + + friends.connect('friend-added', self._friend_added_cb) + friends.connect('friend-removed', self._friend_removed_cb) + + def add_friend(self, buddy_info): + icon = FriendView(self._shell, buddy_info) + self._layout.add(icon) + + self._friends[buddy_info.get_key()] = icon + + def _friend_added_cb(self, data_model, buddy_info): + self.add_friend(buddy_info) + + def _friend_removed_cb(self, data_model, key): + self._layout.remove(self._friends[key]) + del self._friends[key] diff --git a/src/view/home/HomeBox.py b/src/view/home/HomeBox.py new file mode 100644 index 0000000..8764887 --- /dev/null +++ b/src/view/home/HomeBox.py @@ -0,0 +1,287 @@ +# 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 os +import logging +import signal +from gettext import gettext as _ +import re + +import gobject +import gtk +import hippo +import dbus + +from hardware import hardwaremanager +from sugar.graphics import style +from sugar.graphics.palette import Palette +from sugar.profile import get_profile +from sugar import env + +from view.home.activitiesdonut import ActivitiesDonut +from view.devices import deviceview +from view.home.MyIcon import MyIcon +from model.shellmodel import ShellModel +from hardware import schoolserver + +_logger = logging.getLogger('HomeBox') + +class HomeBox(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = 'SugarHomeBox' + + def __init__(self, shell): + hippo.CanvasBox.__init__(self, background_color=0xe2e2e2ff) + + self._redraw_id = None + + shell_model = shell.get_model() + + top_box = hippo.CanvasBox(box_height=style.GRID_CELL_SIZE * 2.5) + self.append(top_box) + + center_box = hippo.CanvasBox(yalign=hippo.ALIGNMENT_CENTER) + self.append(center_box, hippo.PACK_EXPAND) + + bottom_box = hippo.CanvasBox(box_height=style.GRID_CELL_SIZE * 2.5) + self.append(bottom_box) + + self._donut = ActivitiesDonut(shell) + center_box.append(self._donut) + + self._my_icon = _MyIcon(shell, style.XLARGE_ICON_SIZE) + self.append(self._my_icon, hippo.PACK_FIXED) + + self._devices_box = _DevicesBox(shell_model.get_devices()) + bottom_box.append(self._devices_box) + + shell_model.connect('notify::state', + self._shell_state_changed_cb) + + def _shell_state_changed_cb(self, model, pspec): + # FIXME implement this + if model.props.state == ShellModel.STATE_SHUTDOWN: + pass + + def do_allocate(self, width, height, origin_changed): + hippo.CanvasBox.do_allocate(self, width, height, origin_changed) + + [icon_width, icon_height] = self._my_icon.get_allocation() + self.set_position(self._my_icon, (width - icon_width) / 2, + (height - icon_height) / 2) + + _REDRAW_TIMEOUT = 5 * 60 * 1000 # 5 minutes + + def resume(self): + if self._redraw_id is None: + self._redraw_id = gobject.timeout_add(self._REDRAW_TIMEOUT, + self._redraw_activity_ring) + self._redraw_activity_ring() + + def suspend(self): + if self._redraw_id is not None: + gobject.source_remove(self._redraw_id) + self._redraw_id = None + + def _redraw_activity_ring(self): + self._donut.redraw() + return True + + def has_activities(self): + return self._donut.has_activities() + + def enable_xo_palette(self): + self._my_icon.enable_palette() + + def grab_and_rotate(self): + pass + + def rotate(self): + pass + + def release(self): + pass + +class _DevicesBox(hippo.CanvasBox): + def __init__(self, devices_model): + gobject.GObject.__init__(self, + orientation=hippo.ORIENTATION_HORIZONTAL, + xalign=hippo.ALIGNMENT_CENTER) + + self._device_icons = {} + + for device in devices_model: + self._add_device(device) + + devices_model.connect('device-appeared', + self._device_appeared_cb) + devices_model.connect('device-disappeared', + self._device_disappeared_cb) + + def _add_device(self, device): + view = deviceview.create(device) + self.append(view) + self._device_icons[device.get_id()] = view + + def _remove_device(self, device): + self.remove(self._device_icons[device.get_id()]) + del self._device_icons[device.get_id()] + + def _device_appeared_cb(self, model, device): + self._add_device(device) + + def _device_disappeared_cb(self, model, device): + self._remove_device(device) + +class _MyIcon(MyIcon): + def __init__(self, shell, scale): + MyIcon.__init__(self, scale) + + self._power_manager = None + self._shell = shell + self._profile = get_profile() + + def enable_palette(self): + palette = Palette(self._profile.nick_name) + + item = gtk.MenuItem(_('Reboot')) + item.connect('activate', self._reboot_activate_cb) + palette.menu.append(item) + item.show() + + item = gtk.MenuItem(_('Shutdown')) + item.connect('activate', self._shutdown_activate_cb) + palette.menu.append(item) + item.show() + + if not self._profile.is_registered(): + item = gtk.MenuItem(_('Register')) + item.connect('activate', self._register_activate_cb) + palette.menu.append(item) + item.show() + + item = gtk.MenuItem(_('About this XO')) + item.connect('activate', self._about_activate_cb) + palette.menu.append(item) + item.show() + + self.set_palette(palette) + + def _reboot_activate_cb(self, menuitem): + model = self._shell.get_model() + model.props.state = ShellModel.STATE_SHUTDOWN + + pm = self._get_power_manager() + + hw_manager = hardwaremanager.get_manager() + hw_manager.shutdown() + + if env.is_emulator(): + self._close_emulator() + else: + pm.Reboot() + + def _shutdown_activate_cb(self, menuitem): + model = self._shell.get_model() + model.props.state = ShellModel.STATE_SHUTDOWN + + pm = self._get_power_manager() + + hw_manager = hardwaremanager.get_manager() + hw_manager.shutdown() + + if env.is_emulator(): + self._close_emulator() + else: + pm.Shutdown() + + def _register_activate_cb(self, menuitem): + schoolserver.register_laptop() + if self._profile.is_registered(): + self.get_palette().menu.remove(menuitem) + + def _about_activate_cb(self, menuitem): + dialog = gtk.Dialog(_('About this XO'), + self.palette, + gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_OK, gtk.RESPONSE_OK)) + + not_available = _('Not available') + build = self._read_file('/boot/olpc_build') + if build is None: + build = not_available + label_build = gtk.Label('Build: %s' % build) + label_build.set_alignment(0, 0.5) + label_build.show() + dialog.vbox.pack_start(label_build) + + firmware = self._read_file('/ofw/openprom/model') + if firmware is None: + firmware = not_available + else: + firmware = re.split(" +", firmware) + if len(firmware) == 3: + firmware = firmware[1] + label_firmware = gtk.Label('Firmware: %s' % firmware) + label_firmware.set_alignment(0, 0.5) + label_firmware.show() + dialog.vbox.pack_start(label_firmware) + + serial = self._read_file('/ofw/serial-number') + if serial is None: + serial = not_available + label_serial = gtk.Label('Serial Number: %s' % serial) + label_serial.set_alignment(0, 0.5) + label_serial.show() + dialog.vbox.pack_start(label_serial) + + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.connect('response', self._response_cb) + dialog.show() + + def _read_file(self, path): + if os.access(path, os.R_OK) == 0: + _logger.error('read_file() No such file or directory: %s', path) + return None + + fd = open(path, 'r') + value = fd.read() + fd.close() + if value: + value = value.strip('\n') + return value + else: + _logger.error('read_file() No information in file or directory: %s', path) + return None + + def _response_cb(self, widget, response_id): + if response_id == gtk.RESPONSE_OK: + widget.destroy() + + def _close_emulator(self): + if os.environ.has_key('SUGAR_EMULATOR_PID'): + pid = int(os.environ['SUGAR_EMULATOR_PID']) + os.kill(pid, signal.SIGTERM) + + def _get_power_manager(self): + if self._power_manager is None: + bus = dbus.SystemBus() + proxy = bus.get_object('org.freedesktop.Hal', + '/org/freedesktop/Hal/devices/computer') + self._power_manager = dbus.Interface(proxy, \ + 'org.freedesktop.Hal.Device.SystemPowerManagement') + + return self._power_manager diff --git a/src/view/home/HomeWindow.py b/src/view/home/HomeWindow.py new file mode 100644 index 0000000..f1f46e9 --- /dev/null +++ b/src/view/home/HomeWindow.py @@ -0,0 +1,141 @@ +# 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 gtk +import hippo +import cairo + +from sugar.graphics import style + +from view.home.MeshBox import MeshBox +from view.home.HomeBox import HomeBox +from view.home.FriendsBox import FriendsBox +from view.home.transitionbox import TransitionBox +from model.shellmodel import ShellModel + +_HOME_PAGE = 0 +_FRIENDS_PAGE = 1 +_MESH_PAGE = 2 +_TRANSITION_PAGE = 3 + +class HomeWindow(gtk.Window): + def __init__(self, shell): + gtk.Window.__init__(self) + + self._shell = shell + self._active = False + self._level = ShellModel.ZOOM_HOME + + self._canvas = hippo.Canvas() + self.add(self._canvas) + self._canvas.show() + + self.set_default_size(gtk.gdk.screen_width(), + gtk.gdk.screen_height()) + + self.realize() + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DESKTOP) + self.connect("key-release-event", self._key_release_cb) + self.connect('focus-in-event', self._focus_in_cb) + self.connect('focus-out-event', self._focus_out_cb) + + self._enter_sid = self.connect('enter-notify-event', + self._enter_notify_event_cb) + self._leave_sid = self.connect('leave-notify-event', + self._leave_notify_event_cb) + self._motion_sid = self.connect('motion-notify-event', + self._motion_notify_event_cb) + + self._home_box = HomeBox(shell) + self._friends_box = FriendsBox(shell) + self._mesh_box = MeshBox(shell) + self._transition_box = TransitionBox() + + self._activate_view() + self._canvas.set_root(self._home_box) + + self._transition_box.connect('completed', + self._transition_completed_cb) + + def _enter_notify_event_cb(self, window, event): + if event.x != gtk.gdk.screen_width() / 2 or \ + event.y != gtk.gdk.screen_height() / 2: + self._mouse_moved() + + def _leave_notify_event_cb(self, window, event): + self._mouse_moved() + + def _motion_notify_event_cb(self, window, event): + self._mouse_moved() + + # We want to enable the XO palette only when the user + # moved away from the default mouse position (screen center). + def _mouse_moved(self): + self._home_box.enable_xo_palette() + self.disconnect(self._leave_sid) + self.disconnect(self._motion_sid) + self.disconnect(self._enter_sid) + + def _key_release_cb(self, widget, event): + keyname = gtk.gdk.keyval_name(event.keyval) + if keyname == "Alt_L": + self._home_box.release() + + def _deactivate_view(self): + if self._level == ShellModel.ZOOM_HOME: + self._home_box.suspend() + elif self._level == ShellModel.ZOOM_MESH: + self._mesh_box.suspend() + + def _activate_view(self): + if self._level == ShellModel.ZOOM_HOME: + self._home_box.resume() + elif self._level == ShellModel.ZOOM_MESH: + self._mesh_box.resume() + + def _focus_in_cb(self, widget, event): + self._activate_view() + + def _focus_out_cb(self, widget, event): + self._deactivate_view() + + def set_zoom_level(self, level): + self._deactivate_view() + self._level = level + self._activate_view() + + self._canvas.set_root(self._transition_box) + + if level == ShellModel.ZOOM_HOME: + size = style.XLARGE_ICON_SIZE + elif level == ShellModel.ZOOM_FRIENDS: + size = style.LARGE_ICON_SIZE + elif level == ShellModel.ZOOM_MESH: + size = style.STANDARD_ICON_SIZE + + self._transition_box.set_size(size) + + def _transition_completed_cb(self, transition_box): + if self._level == ShellModel.ZOOM_HOME: + self._canvas.set_root(self._home_box) + elif self._level == ShellModel.ZOOM_FRIENDS: + self._canvas.set_root(self._friends_box) + elif self._level == ShellModel.ZOOM_MESH: + self._canvas.set_root(self._mesh_box) + self._mesh_box.focus_search_entry() + + def get_home_box(self): + return self._home_box diff --git a/src/view/home/Makefile.am b/src/view/home/Makefile.am new file mode 100644 index 0000000..6916806 --- /dev/null +++ b/src/view/home/Makefile.am @@ -0,0 +1,14 @@ +sugardir = $(pkgdatadir)/shell/view/home +sugar_PYTHON = \ + __init__.py \ + activitiesdonut.py \ + FriendView.py \ + FriendsBox.py \ + HomeBox.py \ + HomeWindow.py \ + MeshBox.py \ + MyIcon.py \ + proc_smaps.py \ + snowflakelayout.py \ + spreadlayout.py \ + transitionbox.py diff --git a/src/view/home/MeshBox.py b/src/view/home/MeshBox.py new file mode 100644 index 0000000..3b7c4a7 --- /dev/null +++ b/src/view/home/MeshBox.py @@ -0,0 +1,615 @@ +# 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 random +from gettext import gettext as _ +import logging + +import hippo +import gobject +import gtk + +from sugar.graphics.icon import CanvasIcon +from sugar.graphics import style +from sugar.graphics.icon import get_icon_state +from sugar.graphics import style +from sugar.graphics import palette +from sugar.graphics import iconentry +from sugar.graphics.menuitem import MenuItem +from sugar import profile + +from model import accesspointmodel +from model.devices.network import mesh +from model.devices.network import wireless +from hardware import hardwaremanager +from hardware import nmclient +from view.BuddyIcon import BuddyIcon +from view.pulsingicon import PulsingIcon +from view.home.snowflakelayout import SnowflakeLayout +from view.home.spreadlayout import SpreadLayout + +from hardware.nmclient import NM_802_11_CAP_PROTO_WEP, NM_802_11_CAP_PROTO_WPA, NM_802_11_CAP_PROTO_WPA2 + + +_ICON_NAME = 'network-wireless' + +class AccessPointView(PulsingIcon): + def __init__(self, model, mesh_device=None): + PulsingIcon.__init__(self, size=style.STANDARD_ICON_SIZE, cache=True) + self._model = model + self._meshdev = mesh_device + self._disconnect_item = None + self._greyed_out = False + + self.connect('activated', self._activate_cb) + + model.connect('notify::strength', self._strength_changed_cb) + model.connect('notify::name', self._name_changed_cb) + model.connect('notify::state', self._state_changed_cb) + + (stroke, fill) = model.get_nm_network().get_colors() + self._device_stroke = stroke + self._device_fill = fill + + self._palette = self._create_palette() + self.set_palette(self._palette) + + self._update_icon() + self._update_name() + self._update_state() + + # Update badge + caps = model.props.capabilities + if model.get_nm_network().is_favorite(): + self.props.badge_name = "emblem-favorite" + elif (caps & NM_802_11_CAP_PROTO_WEP) or (caps & NM_802_11_CAP_PROTO_WPA) or (caps & NM_802_11_CAP_PROTO_WPA2): + self.props.badge_name = "emblem-locked" + + def _create_palette(self): + p = palette.Palette(self._model.props.name, menu_after_content=True) + if not self._meshdev: + return p + + # Only show disconnect when there's a mesh device, because mesh takes + # priority over the normal wireless device. NM doesn't have a "disconnect" + # method for a device either (for various reasons) so this doesn't + # have a good mapping + self._disconnect_item = gtk.MenuItem(_('Disconnect...')) + self._disconnect_item.connect('activate', self._disconnect_activate_cb) + p.menu.append(self._disconnect_item) + if self._model.props.state == accesspointmodel.STATE_CONNECTED: + self._disconnect_item.show() + return p + + def _disconnect_activate_cb(self, menuitem): + # Disconnection for an AP means activating the default mesh device + network_manager = hardwaremanager.get_network_manager() + if network_manager and self._meshdev: + network_manager.set_active_device(self._meshdev) + + def _strength_changed_cb(self, model, pspec): + self._update_icon() + + def _name_changed_cb(self, model, pspec): + self._update_name() + + def _state_changed_cb(self, model, pspec): + self._update_state() + + def _activate_cb(self, icon): + network_manager = hardwaremanager.get_network_manager() + if network_manager: + device = self._model.get_nm_device() + network = self._model.get_nm_network() + network_manager.set_active_device(device, network) + + def _update_name(self): + self._palette.set_primary_text(self._model.props.name) + + def _update_icon(self): + icon_name = get_icon_state(_ICON_NAME, self._model.props.strength) + if icon_name: + self.props.icon_name = icon_name + + def _update_state(self): + if self._model.props.state == accesspointmodel.STATE_CONNECTING: + if self._disconnect_item: + self._disconnect_item.hide() + self.props.pulse_time = 1.0 + self.props.colors = [ + [ style.Color(self._device_stroke).get_svg(), + style.Color(self._device_fill).get_svg() ], + [ style.Color(self._device_stroke).get_svg(), + '#e2e2e2' ] + ] + elif self._model.props.state == accesspointmodel.STATE_CONNECTED: + if self._disconnect_item: + self._disconnect_item.show() + self.props.pulse_time = 0.0 + self.props.colors = [ + [ '#ffffff', + style.Color(self._device_fill).get_svg() ], + [ '#ffffff', + style.Color(self._device_fill).get_svg() ] + ] + elif self._model.props.state == accesspointmodel.STATE_NOTCONNECTED: + if self._disconnect_item: + self._disconnect_item.hide() + self.props.pulse_time = 0.0 + self.props.colors = [ + [ style.Color(self._device_stroke).get_svg(), + style.Color(self._device_fill).get_svg() ] + ] + + if self._greyed_out: + self.props.pulse_time = 0.0 + self.props.colors = [['#D5D5D5', '#D5D5D5']] + + def set_filter(self, query): + self._greyed_out = self._model.props.name.lower().find(query) == -1 + self._update_state() + +_MESH_ICON_NAME = 'network-mesh' + +class MeshDeviceView(PulsingIcon): + def __init__(self, nm_device, channel): + if not channel in [1, 6, 11]: + raise ValueError("Invalid channel %d" % channel) + + PulsingIcon.__init__(self, size=style.STANDARD_ICON_SIZE, + icon_name=_MESH_ICON_NAME, cache=True) + + self._nm_device = nm_device + self.channel = channel + self.props.badge_name = "badge-channel-%d" % self.channel + self._greyed_out = False + + self._disconnect_item = None + self._palette = self._create_palette() + self.set_palette(self._palette) + + mycolor = profile.get_color() + self._device_fill = mycolor.get_fill_color() + self._device_stroke = mycolor.get_stroke_color() + + self.connect('activated', self._activate_cb) + + self._nm_device.connect('state-changed', self._state_changed_cb) + self._nm_device.connect('activation-stage-changed', self._state_changed_cb) + self._update_state() + + def _create_palette(self): + p = palette.Palette(_("Mesh Network") + " " + str(self.channel), menu_after_content=True) + + self._disconnect_item = gtk.MenuItem(_('Disconnect...')) + self._disconnect_item.connect('activate', self._disconnect_activate_cb) + p.menu.append(self._disconnect_item) + + state = self._nm_device.get_state() + chan = wireless.freq_to_channel(self._nm_device.get_frequency()) + if state == nmclient.DEVICE_STATE_ACTIVATED and chan == self.channel: + self._disconnect_item.show() + return p + + def _disconnect_activate_cb(self, menuitem): + network_manager = hardwaremanager.get_network_manager() + if network_manager: + network_manager.set_active_device(self._nm_device) + + def _activate_cb(self, icon): + network_manager = hardwaremanager.get_network_manager() + if network_manager: + freq = wireless.channel_to_freq(self.channel) + network_manager.set_active_device(self._nm_device, mesh_freq=freq) + + def _state_changed_cb(self, model): + self._update_state() + + def _update_state(self): + state = self._nm_device.get_state() + chan = wireless.freq_to_channel(self._nm_device.get_frequency()) + if self._greyed_out: + self.props.colors = [['#D5D5D5', '#D5D5D5']] + elif state == nmclient.DEVICE_STATE_ACTIVATING and chan == self.channel: + self._disconnect_item.hide() + self.props.pulse_time = 0.75 + self.props.colors = [ + [ style.Color(self._device_stroke).get_svg(), + style.Color(self._device_fill).get_svg() ], + [ style.Color(self._device_stroke).get_svg(), + '#e2e2e2' ] + ] + elif state == nmclient.DEVICE_STATE_ACTIVATED and chan == self.channel: + self._disconnect_item.show() + self.props.pulse_time = 0.0 + self.props.colors = [ + [ '#ffffff', + style.Color(self._device_fill).get_svg() ], + [ '#ffffff', + style.Color(self._device_fill).get_svg() ] + ] + elif state == nmclient.DEVICE_STATE_INACTIVE or chan != self.channel: + self._disconnect_item.hide() + self.props.pulse_time = 0.0 + self.props.colors = [ + [ style.Color(self._device_stroke).get_svg(), + style.Color(self._device_fill).get_svg() ] + ] + else: + raise RuntimeError("Shouldn't get here") + + def set_filter(self, query): + self._greyed_out = (query != '') + self._update_state() + +class ActivityView(hippo.CanvasBox): + def __init__(self, shell, model): + hippo.CanvasBox.__init__(self) + + self._shell = shell + self._model = model + self._icons = {} + + self._layout = SnowflakeLayout() + self.set_layout(self._layout) + + self._icon = self._create_icon() + self._layout.add(self._icon, center=True) + + self._update_palette() + + activity = self._model.activity + activity.connect('notify::name', self._name_changed_cb) + activity.connect('notify::color', self._color_changed_cb) + activity.connect('notify::private', self._private_changed_cb) + activity.connect('joined', self._joined_changed_cb) + #FIXME: 'joined' signal not working, see #5032 + + def _create_icon(self): + icon = CanvasIcon(file_name=self._model.get_icon_name(), + xo_color=self._model.get_color(), cache=True, + size=style.STANDARD_ICON_SIZE) + icon.connect('activated', self._clicked_cb) + return icon + + def _create_palette(self): + p = palette.Palette(self._model.activity.props.name) + + private = self._model.activity.props.private + joined = self._model.activity.props.joined + + if joined: + item = MenuItem(_('Resume'), 'activity-start') + item.connect('activate', self._clicked_cb) + item.show() + p.menu.append(item) + elif not private: + item = MenuItem(_('Join'), 'activity-start') + item.connect('activate', self._clicked_cb) + item.show() + p.menu.append(item) + + return p + + def _update_palette(self): + self._palette = self._create_palette() + self._icon.set_palette(self._palette) + + def has_buddy_icon(self, key): + return self._icons.has_key(key) + + def add_buddy_icon(self, key, icon): + self._icons[key] = icon + self._layout.add(icon) + + def remove_buddy_icon(self, key): + icon = self._icons[key] + del self._icons[key] + icon.destroy() + + def _clicked_cb(self, item): + bundle_id = self._model.get_bundle_id() + self._shell.join_activity(bundle_id, self._model.get_id()) + + def set_filter(self, query): + text_to_check = self._model.activity.props.name.lower() + \ + self._model.activity.props.type.lower() + if text_to_check.find(query) == -1: + self._icon.props.stroke_color = '#D5D5D5' + self._icon.props.fill_color = '#E5E5E5' + else: + self._icon.props.xo_color = self._model.get_color() + + for key, icon in self._icons.iteritems(): + if hasattr(icon, 'set_filter'): + icon.set_filter(query) + + def _name_changed_cb(self, activity, pspec): + self._update_palette() + + def _color_changed_cb(self, activity, pspec): + self._layout.remove(self._icon) + self._icon = self._create_icon() + self._layout.add(self._icon, center=True) + self._icon.set_palette(self._palette) + + def _private_changed_cb(self, activity, pspec): + self._update_palette() + + def _joined_changed_cb(self, widget, event): + logging.debug('ActivityView._joined_changed_cb: AAAA!!!!') + +_AUTOSEARCH_TIMEOUT = 1000 + +class MeshToolbar(gtk.Toolbar): + __gtype_name__ = 'MeshToolbar' + + __gsignals__ = { + 'query-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])) + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self._query = None + self._autosearch_timer = None + + self._add_separator() + + tool_item = gtk.ToolItem() + tool_item.set_expand(True) + self.insert(tool_item, -1) + tool_item.show() + + self._search_entry = iconentry.IconEntry() + self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, 'system-search') + self._search_entry.add_clear_button() + self._search_entry.connect('activate', self._entry_activated_cb) + self._search_entry.connect('changed', self._entry_changed_cb) + tool_item.add(self._search_entry) + self._search_entry.show() + + self._add_separator() + + def _add_separator(self): + separator = gtk.SeparatorToolItem() + separator.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE) + separator.props.draw = False + self.insert(separator, -1) + separator.show() + + def _entry_activated_cb(self, entry): + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + new_query = entry.props.text + if self._query != new_query: + self._query = new_query + self.emit('query-changed', self._query) + + def _entry_changed_cb(self, entry): + if not entry.props.text: + entry.activate() + return + + if self._autosearch_timer: + gobject.source_remove(self._autosearch_timer) + self._autosearch_timer = gobject.timeout_add(_AUTOSEARCH_TIMEOUT, + self._autosearch_timer_cb) + + def _autosearch_timer_cb(self): + logging.debug('_autosearch_timer_cb') + self._autosearch_timer = None + self._search_entry.activate() + return False + +class MeshBox(hippo.CanvasBox): + def __init__(self, shell): + hippo.CanvasBox.__init__(self) + + self._shell = shell + self._model = shell.get_model().get_mesh() + self._buddies = {} + self._activities = {} + self._access_points = {} + self._mesh = {} + self._buddy_to_activity = {} + self._suspended = True + self._query = '' + + self._toolbar = MeshToolbar() + self._toolbar.connect('query-changed', self._toolbar_query_changed_cb) + self.append(hippo.CanvasWidget(widget=self._toolbar)) + + self._layout_box = hippo.CanvasBox(background_color=0xe2e2e2ff) + self.append(self._layout_box, hippo.PACK_EXPAND) + + self._layout = SpreadLayout() + self._layout_box.set_layout(self._layout) + + for buddy_model in self._model.get_buddies(): + self._add_alone_buddy(buddy_model) + + self._model.connect('buddy-added', self._buddy_added_cb) + self._model.connect('buddy-removed', self._buddy_removed_cb) + self._model.connect('buddy-moved', self._buddy_moved_cb) + + for activity_model in self._model.get_activities(): + self._add_activity(activity_model) + + self._model.connect('activity-added', self._activity_added_cb) + self._model.connect('activity-removed', self._activity_removed_cb) + + for ap_model in self._model.get_access_points(): + self._add_access_point(ap_model) + + self._model.connect('access-point-added', + self._access_point_added_cb) + self._model.connect('access-point-removed', + self._access_point_removed_cb) + + if self._model.get_mesh(): + self._mesh_added_cb(self._model, self._model.get_mesh()) + + self._model.connect('mesh-added', + self._mesh_added_cb) + self._model.connect('mesh-removed', + self._mesh_removed_cb) + + def _mesh_added_cb(self, model, meshdev): + self._add_mesh_icon(meshdev, 1) + self._add_mesh_icon(meshdev, 6) + self._add_mesh_icon(meshdev, 11) + + def _mesh_removed_cb(self, model): + self._remove_mesh_icon(1) + self._remove_mesh_icon(6) + self._remove_mesh_icon(11) + + def _buddy_added_cb(self, model, buddy_model): + self._add_alone_buddy(buddy_model) + + def _buddy_removed_cb(self, model, buddy_model): + self._remove_buddy(buddy_model) + + def _buddy_moved_cb(self, model, buddy_model, activity_model): + # Owner doesn't move from the center + if buddy_model.is_owner(): + return + self._move_buddy(buddy_model, activity_model) + + def _activity_added_cb(self, model, activity_model): + self._add_activity(activity_model) + + def _activity_removed_cb(self, model, activity_model): + self._remove_activity(activity_model) + + def _access_point_added_cb(self, model, ap_model): + self._add_access_point(ap_model) + + def _access_point_removed_cb(self, model, ap_model): + self._remove_access_point(ap_model) + + def _add_mesh_icon(self, meshdev, channel): + if self._mesh.has_key(channel): + self._remove_mesh_icon(channel) + if not meshdev: + return + self._mesh[channel] = MeshDeviceView(meshdev, channel) + self._layout.add(self._mesh[channel]) + + def _remove_mesh_icon(self, channel): + if not self._mesh.has_key(channel): + return + self._layout.remove(self._mesh[channel]) + del self._mesh[channel] + + def _add_alone_buddy(self, buddy_model): + icon = BuddyIcon(self._shell, buddy_model) + if buddy_model.is_owner(): + vertical_offset = - style.GRID_CELL_SIZE + self._layout.add_center(icon, vertical_offset) + else: + self._layout.add(icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + self._buddies[buddy_model.get_key()] = icon + + def _remove_alone_buddy(self, buddy_model): + icon = self._buddies[buddy_model.get_key()] + self._layout.remove(icon) + del self._buddies[buddy_model.get_key()] + icon.destroy() + + def _remove_buddy(self, buddy_model): + key = buddy_model.get_key() + if self._buddies.has_key(key): + self._remove_alone_buddy(buddy_model) + else: + for activity in self._activities.values(): + if activity.has_buddy_icon(key): + activity.remove_buddy_icon(key) + + def _move_buddy(self, buddy_model, activity_model): + key = buddy_model.get_key() + + self._remove_buddy(buddy_model) + + if activity_model == None: + self._add_alone_buddy(buddy_model) + elif activity_model.get_id() in self._activities: + activity = self._activities[activity_model.get_id()] + + icon = BuddyIcon(self._shell, buddy_model, + style.SMALL_ICON_SIZE) + activity.add_buddy_icon(buddy_model.get_key(), icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + def _add_activity(self, activity_model): + icon = ActivityView(self._shell, activity_model) + self._layout.add(icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + self._activities[activity_model.get_id()] = icon + + def _remove_activity(self, activity_model): + icon = self._activities[activity_model.get_id()] + self._layout.remove(icon) + del self._activities[activity_model.get_id()] + icon.destroy() + + def _add_access_point(self, ap_model): + meshdev = self._model.get_mesh() + icon = AccessPointView(ap_model, meshdev) + self._layout.add(icon) + + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + self._access_points[ap_model.get_id()] = icon + + def _remove_access_point(self, ap_model): + icon = self._access_points[ap_model.get_id()] + self._layout.remove(icon) + del self._access_points[ap_model.get_id()] + + def suspend(self): + if not self._suspended: + self._suspended = True + for ap in self._access_points.values(): + ap.props.paused = True + + def resume(self): + if self._suspended: + self._suspended = False + for ap in self._access_points.values(): + ap.props.paused = False + + def _toolbar_query_changed_cb(self, toolbar, query): + self._query = query.lower() + for icon in self._layout_box.get_children(): + if hasattr(icon, 'set_filter'): + icon.set_filter(self._query) + + def focus_search_entry(self): + self._toolbar._search_entry.grab_focus() diff --git a/src/view/home/MyIcon.py b/src/view/home/MyIcon.py new file mode 100644 index 0000000..af0f6ce --- /dev/null +++ b/src/view/home/MyIcon.py @@ -0,0 +1,24 @@ +# 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 + +from sugar.graphics.icon import CanvasIcon +from sugar import profile + +class MyIcon(CanvasIcon): + def __init__(self, size): + CanvasIcon.__init__(self, size=size, + icon_name='computer-xo', + xo_color=profile.get_color()) diff --git a/src/view/home/__init__.py b/src/view/home/__init__.py new file mode 100644 index 0000000..a9dd95a --- /dev/null +++ b/src/view/home/__init__.py @@ -0,0 +1,16 @@ +# 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 + 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) diff --git a/src/view/home/proc_smaps.py b/src/view/home/proc_smaps.py new file mode 100755 index 0000000..6e1680f --- /dev/null +++ b/src/view/home/proc_smaps.py @@ -0,0 +1,107 @@ +# Copyright (C) 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 os + +# /proc/PID/maps consists of a number of lines like this: +# 00400000-004b1000 r-xp 00000000 fd:00 5767206 /bin/bash +# 006b1000-006bb000 rw-p 000b1000 fd:00 5767206 /bin/bash +# 006bb000-006c0000 rw-p 006bb000 00:00 0 +# ... +# The fields are: address, permissions, offset, device, inode, and +# (for non-anonymous mappings) pathname. +# +# /proc/PID/smaps gives additional information for each mapping: +# 00400000-004b1000 r-xp 00000000 fd:00 5767206 /bin/bash +# Size: 708 kB +# Rss: 476 kB +# Shared_Clean: 468 kB +# Shared_Dirty: 0 kB +# Private_Clean: 8 kB +# Private_Dirty: 0 kB +# Referenced: 0 kb +# +# The "Referenced" line only appears in kernel 2.6.22 and later. + +def get_shared_mapping_names(pid): + """Returns a set of the files for which PID has a shared mapping""" + + mappings = set() + infile = open("/proc/%s/maps" % pid, "r") + for line in infile: + # sharable mappings are non-anonymous and either read-only + # (permissions "r-..") or writable but explicitly marked + # shared ("rw.s") + fields = line.split() + if len(fields) < 6 or not fields[5].startswith('/'): + continue + if fields[1][0] != 'r' or (fields[1][1] == 'w' and fields[1][3] != 's'): + continue + mappings.add(fields[5]) + infile.close() + return mappings + +_smaps_lines_per_entry = None + +def get_mappings(pid, ignored_shared_mappings): + """Returns a list of (name, private, shared) tuples describing the + memory mappings of PID. Shared mappings named in + ignored_shared_mappings are ignored + """ + + global _smaps_lines_per_entry + if _smaps_lines_per_entry is None: + if os.path.isfile('/proc/%s/clear_refs' % os.getpid()): + _smaps_lines_per_entry = 8 + else: + _smaps_lines_per_entry = 7 + + mappings = [] + + smapfile = "/proc/%s/smaps" % pid + infile = open(smapfile, "r") + input = infile.read() + infile.close() + lines = input.splitlines() + + for line_idx in range(0, len(lines), _smaps_lines_per_entry): + name_idx = lines[line_idx].find('/') + if name_idx == -1: + name = None + else: + name = lines[line_idx][name_idx:] + + private_clean = int(lines[line_idx + 5][14:-3]) + private_dirty = int(lines[line_idx + 6][14:-3]) + if name in ignored_shared_mappings: + shared_clean = 0 + shared_dirty = 0 + else: + shared_clean = int(lines[line_idx + 3][14:-3]) + shared_dirty = int(lines[line_idx + 4][14:-3]) + + mapping = Mapping(name, private_clean + private_dirty, + shared_clean + shared_dirty) + mappings.append (mapping) + + return mappings + +class Mapping: + def __init__ (self, name, private, shared): + self.name = name + self.private = private + self.shared = shared diff --git a/src/view/home/snowflakelayout.py b/src/view/home/snowflakelayout.py new file mode 100644 index 0000000..1eb58cf --- /dev/null +++ b/src/view/home/snowflakelayout.py @@ -0,0 +1,108 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import math + +import gobject +import hippo + +from sugar.graphics import style + +_BASE_DISTANCE = style.zoom(15) +_CHILDREN_FACTOR = style.zoom(3) + +class SnowflakeLayout(gobject.GObject,hippo.CanvasLayout): + __gtype_name__ = 'SugarSnowflakeLayout' + def __init__(self): + gobject.GObject.__init__(self) + self._nflakes = 0 + + def add(self, child, center=False): + if not center: + self._nflakes += 1 + + self._box.append(child) + + box_child = self._box.find_box_child(child) + box_child.is_center = center + + def remove(self, child): + box_child = self._box.find_box_child(child) + if not box_child.is_center: + self._nflakes -= 1 + + self._box.remove(child) + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + size = self._calculate_size() + return (size, size) + + def do_get_width_request(self): + size = self._calculate_size() + return (size, size) + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + r = self._get_radius() + index = 0 + + for child in self._box.get_layout_children(): + cx = x + width / 2 + cy = x + height / 2 + + min_width, child_width = child.get_width_request() + min_height, child_height = child.get_height_request(child_width) + + if child.is_center: + child.allocate(x + (width - child_width) / 2, + y + (height - child_height) / 2, + child_width, child_height, origin_changed) + else: + angle = 2 * math.pi * index / self._nflakes + + dx = math.cos(angle) * r + dy = math.sin(angle) * r + + child_x = int(x + (width - child_width) / 2 + dx) + child_y = int(y + (height - child_height) / 2 + dy) + + child.allocate(child_x, child_y, child_width, + child_height, origin_changed) + + index += 1 + + def _get_radius(self): + radius = int(_BASE_DISTANCE + _CHILDREN_FACTOR * self._nflakes) + for child in self._box.get_layout_children(): + if child.is_center: + [min_w, child_w] = child.get_width_request() + [min_h, child_h] = child.get_height_request(child_w) + radius += max(child_w, child_h) / 2 + + return radius + + def _calculate_size(self): + thickness = 0 + 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) + thickness = max(thickness, max(child_width, child_height)) + + return self._get_radius() * 2 + thickness diff --git a/src/view/home/spreadlayout.py b/src/view/home/spreadlayout.py new file mode 100644 index 0000000..3463169 --- /dev/null +++ b/src/view/home/spreadlayout.py @@ -0,0 +1,246 @@ +# Copyright (C) 2007 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +from numpy import array +from random import random + +import hippo +import gobject +import gtk + +from sugar.graphics import style + +_PLACE_TRIALS = 20 +_MAX_WEIGHT = 255 +_CELL_SIZE = 4 + +class _Grid(gobject.GObject): + __gsignals__ = { + 'child-changed' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])) + } + def __init__(self, width, height): + gobject.GObject.__init__(self) + + self.width = width + self.height = height + self._children = [] + self._collisions = [] + self._collisions_sid = 0 + + self._array = array([0], dtype='b') + self._array.resize(width * height) + + def add(self, child, width, height): + trials = _PLACE_TRIALS + weight = _MAX_WEIGHT + while trials > 0 and weight: + x = int(random() * (self.width - width)) + y = int(random() * (self.height - height)) + + rect = gtk.gdk.Rectangle(x, y, width, height) + new_weight = self._compute_weight(rect) + if weight > new_weight: + weight = new_weight + + trials -= 1 + + child.grid_rect = rect + child.locked = False + + self._add_child(child) + + if weight > 0: + self._detect_collisions(child) + + def remove(self, child): + self._children.remove(child) + self._remove_weight(child.grid_rect) + child.grid_rect = None + + def _add_child(self, child): + self._children.append(child) + self.add_weight(child.grid_rect) + + def _move_child(self, child, new_rect): + self._remove_weight(child.grid_rect) + self.add_weight(new_rect) + + child.grid_rect = new_rect + + self.emit('child-changed', child) + + def _shift_child(self, child): + rect = child.grid_rect + weight = self._compute_weight(rect) + new_rects = [] + + if (rect.x + rect.width < self.width - 1): + new_rects.append(gtk.gdk.Rectangle(rect.x + 1, rect.y, + rect.width, rect.height)) + + if (rect.x - 1 > 0): + new_rects.append(gtk.gdk.Rectangle(rect.x - 1, rect.y, + rect.width, rect.height)) + + if (rect.y + rect.height < self.height - 1): + new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y + 1, + rect.width, rect.height)) + + if (rect.y - 1 > 0): + new_rects.append(gtk.gdk.Rectangle(rect.x, rect.y - 1, + rect.width, rect.height)) + + best_rect = None + for new_rect in new_rects: + new_weight = self._compute_weight(new_rect) + if new_weight < weight: + best_rect = new_rect + weight = new_weight + + if best_rect: + self._move_child(child, best_rect) + + return weight + + + def _solve_collisions(self): + for collision in self._collisions[:]: + weight = self._shift_child(collision) + if not weight: + self._collisions.remove(collision) + + return (len(self._collisions) > 0) + + def _detect_collisions(self, child): + collision_found = False + for c in self._children: + intersection = child.grid_rect.intersect(c.grid_rect) + if c != child and intersection.width > 0: + if c not in self._collisions: + collision_found = True + self._collisions.append(c) + + if collision_found: + if child not in self._collisions: + self._collisions.append(child) + +# if len(self._collisions) and not self._collisions_sid: +# self._collisions_sid = gobject.idle_add(self._solve_collisions) + + def add_weight(self, rect): + for i in range(rect.x, rect.x + rect.width): + for j in range(rect.y, rect.y + rect.height): + self[j, i] += 1 + + def _remove_weight(self, rect): + for i in range(rect.x, rect.x + rect.width): + for j in range(rect.y, rect.y + rect.height): + self[j, i] -= 1 + + def _compute_weight(self, rect): + weight = 0 + + for i in range(rect.x, rect.x + rect.width): + for j in range(rect.y, rect.y + rect.height): + weight += self[j, i] + + return weight + + def __getitem__(self, (row, col)): + return self._array[col + row * self.width] + + def __setitem__(self, (row, col), value): + self._array[col + row * self.width] = value + + +class SpreadLayout(gobject.GObject, hippo.CanvasLayout): + __gtype_name__ = 'SugarSpreadLayout' + def __init__(self): + gobject.GObject.__init__(self) + + min_width, width = self.do_get_width_request() + min_height, height = self.do_get_height_request(width) + + self._grid = _Grid(width / _CELL_SIZE, height / _CELL_SIZE) + self._grid.connect('child-changed', self._grid_child_changed_cb) + + def add_center(self, child, vertical_offset=0): + self._box.append(child) + + width, height = self._get_child_grid_size(child) + rect = gtk.gdk.Rectangle(int((self._grid.width - width) / 2), + int((self._grid.height - height) / 2), + width + 1, height + 1) + self._grid.add_weight(rect) + + box_child = self._box.find_box_child(child) + box_child.grid_rect = None + box_child.vertical_offset = vertical_offset + + def add(self, child): + self._box.append(child) + + width, height = self._get_child_grid_size(child) + box_child = self._box.find_box_child(child) + self._grid.add(box_child, width, height) + + def remove(self, child): + box_child = self._box.find_box_child(child) + self._grid.remove(box_child) + + self._box.remove(child) + + def do_set_box(self, box): + self._box = box + + def do_get_height_request(self, for_width): + return 0, gtk.gdk.screen_height() - style.GRID_CELL_SIZE + + def do_get_width_request(self): + return 0, gtk.gdk.screen_width() + + def do_allocate(self, x, y, width, height, + req_width, req_height, origin_changed): + for child in self._box.get_layout_children(): + # We need to always get requests to not confuse hippo + min_w, child_width = child.get_width_request() + min_h, child_height = child.get_height_request(child_width) + + rect = child.grid_rect + if child.grid_rect: + child.allocate(rect.x * _CELL_SIZE, + rect.y * _CELL_SIZE, + rect.width * _CELL_SIZE, + rect.height * _CELL_SIZE, + origin_changed) + else: + vertical_offset = child.vertical_offset + child_x = x + (width - child_width) / 2 + child_y = y + (height - child_height + vertical_offset) / 2 + child.allocate(child_x, child_y, child_width, child_height, + origin_changed) + + def _get_child_grid_size(self, child): + min_width, width = child.get_width_request() + min_height, height = child.get_height_request(width) + + return int(width / _CELL_SIZE), int(height / _CELL_SIZE) + + def _grid_child_changed_cb(self, grid, box_child): + box_child.item.emit_request_changed() diff --git a/src/view/home/transitionbox.py b/src/view/home/transitionbox.py new file mode 100644 index 0000000..f1ba4fb --- /dev/null +++ b/src/view/home/transitionbox.py @@ -0,0 +1,93 @@ +# Copyright (C) 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 hippo +import gobject + +from sugar.graphics import style +from sugar.graphics import animator + +from view.home.MyIcon import MyIcon +from view.home.spreadlayout import SpreadLayout + +class _Animation(animator.Animation): + def __init__(self, icon, start_size, end_size): + animator.Animation.__init__(self, 0.0, 1.0) + + self._icon = icon + self.start_size = start_size + self.end_size = end_size + + def next_frame(self, current): + d = (self.end_size - self.start_size) * current + self._icon.props.size = self.start_size + d + +class _Layout(gobject.GObject,hippo.CanvasLayout): + __gtype_name__ = 'SugarTransitionBoxLayout' + 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 0, 0 + + def do_get_width_request(self): + return 0, 0 + + 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) + + child.allocate(x + (width - child_width) / 2, + y + (height - child_height) / 2, + child_width, child_height, origin_changed) + +class TransitionBox(hippo.CanvasBox): + __gtype_name__ = 'SugarTransitionBox' + + __gsignals__ = { + 'completed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])) + } + + def __init__(self): + hippo.CanvasBox.__init__(self, background_color=0xe2e2e2ff) + + self._size = style.XLARGE_ICON_SIZE + + self._layout = _Layout() + self.set_layout(self._layout) + + self._my_icon = MyIcon(self._size) + self.append(self._my_icon) + + self._animator = animator.Animator(0.3) + self._animator.connect('completed', self._animation_completed_cb) + + def _animation_completed_cb(self, anim): + self.emit('completed') + + def set_size(self, size): + self._animator.remove_all() + self._animator.add(_Animation(self._my_icon, self._size, size)) + self._animator.start() + + self._size = size + |