Difference between revisions of "User:Walter/favoriteslayout.py"

From Sugar Labs
Jump to navigation Jump to search
(update for 0.82)
Line 19: Line 19:
 
import math
 
import math
 
import hashlib
 
import hashlib
 +
from gettext import gettext as _
  
 
import gobject
 
import gobject
Line 35: Line 36:
  
 
class FavoritesLayout(gobject.GObject, hippo.CanvasLayout):
 
class FavoritesLayout(gobject.GObject, hippo.CanvasLayout):
 +
    """Base class of the different layout types."""
 +
 
     __gtype_name__ = 'FavoritesLayout'
 
     __gtype_name__ = 'FavoritesLayout'
  
Line 92: Line 95:
  
 
class RandomLayout(FavoritesLayout):
 
class RandomLayout(FavoritesLayout):
 +
    """Lay out icons randomly; try to nudge them around to resolve overlaps."""
 +
 
     __gtype_name__ = 'RandomLayout'
 
     __gtype_name__ = 'RandomLayout'
 +
 +
    icon_name = 'view-freeform'
 +
    """Name of icon used in home view dropdown palette."""
 +
 +
    profile_key = 'random-layout'
 +
    """String used in profile to represent this view."""
  
 
     def __init__(self):
 
     def __init__(self):
Line 165: Line 176:
  
 
class RingLayout(FavoritesLayout):
 
class RingLayout(FavoritesLayout):
 +
    """Lay out icons in a ring around the XO man."""
 +
 
     __gtype_name__ = 'RingLayout'
 
     __gtype_name__ = 'RingLayout'
 +
    icon_name = 'view-radial'
 +
    """Name of icon used in home view dropdown palette."""
 +
    profile_key = 'ring-layout'
 +
    """String used in profile to represent this view."""
  
 
     def __init__(self):
 
     def __init__(self):
Line 190: Line 207:
  
 
     def _calculate_radius_and_icon_size(self, children_count):
 
     def _calculate_radius_and_icon_size(self, children_count):
        angle = 2 * math.pi / children_count
 
 
 
         # what's the radius required without downscaling?
 
         # what's the radius required without downscaling?
 
         distance = style.STANDARD_ICON_SIZE + style.DEFAULT_SPACING
 
         distance = style.STANDARD_ICON_SIZE + style.DEFAULT_SPACING
 
         icon_size = style.STANDARD_ICON_SIZE
 
         icon_size = style.STANDARD_ICON_SIZE
          
+
         # circumference is 2*pi*r; we want this to be at least
         if children_count == 1:
+
         # 'children_count * distance'
            radius = 0
+
         radius = children_count * distance / (2 * math.pi)
         else:
+
         # limit computed radius to reasonable bounds.
            radius = math.sqrt(distance ** 2 /
+
         radius = max(radius, _MINIMUM_RADIUS)
                    (math.sin(angle) ** 2 + (math.cos(angle) - 1) ** 2))
+
        radius = min(radius, _MAXIMUM_RADIUS)
          
+
        # recompute icon size from limited radius
         if radius < _MINIMUM_RADIUS:
+
        if children_count > 0:
            # we can upscale, if we want
+
             icon_size = (2 * math.pi * radius / children_count) \
            icon_size += style.STANDARD_ICON_SIZE * \
+
                        - style.DEFAULT_SPACING
                    (0.5 * (_MINIMUM_RADIUS - radius) / _MINIMUM_RADIUS)
+
        # limit adjusted icon size.
            radius = _MINIMUM_RADIUS
+
        icon_size = max(icon_size, style.SMALL_ICON_SIZE)
        elif radius > _MAXIMUM_RADIUS:
+
        icon_size = min(icon_size, style.MEDIUM_ICON_SIZE)
            radius = _MAXIMUM_RADIUS
 
            # need to downscale. what's the icon size required?
 
             distance = math.sqrt((radius * math.sin(angle)) ** 2 + \
 
                    (radius * (math.cos(angle) - 1)) ** 2)
 
            icon_size = distance - style.DEFAULT_SPACING
 
       
 
 
         return radius, icon_size
 
         return radius, icon_size
  
     def _calculate_position(self, radius, icon_size, index, children_count):
+
     def _calculate_position(self, radius, icon_size, index, children_count,
 +
                            sin=math.sin, cos=math.cos):
 
         width, height = self.box.get_allocation()
 
         width, height = self.box.get_allocation()
        # angle decreases as the radius increases
+
         angle = index * (2 * math.pi / children_count) - math.pi / 2
        inc = 12.0 + index / 6.0
+
         x = radius * cos(angle) + (width - icon_size) / 2
         angle = index * (2 * math.pi / inc) - math.pi / 2
+
         y = radius * sin(angle) + (height - icon_size -
         # radius is proportional to index/children_count
+
                                  (style.GRID_CELL_SIZE/2) ) / 2
        myminimum = _MINIMUM_RADIUS * .667
 
        newradius = ((radius - myminimum) * (index * 1.1) / children_count) +
 
                                        myminimum
 
        x = newradius * math.cos(angle) + (width - icon_size) / 2
 
         y = newradius * math.sin(angle) + (height - icon_size -
 
                                        style.GRID_CELL_SIZE) / 2
 
 
         return x, y
 
         return x, y
  
Line 236: Line 240:
  
 
     def _update_icon_sizes(self):
 
     def _update_icon_sizes(self):
 +
        # XXX: THIS METHOD IS NEVER CALLED
 
         children_in_ring = self._get_children_in_ring()
 
         children_in_ring = self._get_children_in_ring()
 
         radius_, icon_size = \
 
         radius_, icon_size = \
Line 255: Line 260:
 
                 x, y = self._calculate_position(radius, icon_size, n,
 
                 x, y = self._calculate_position(radius, icon_size, n,
 
                                                 len(children_in_ring))
 
                                                 len(children_in_ring))
 +
 
                 # We need to always get requests to not confuse hippo
 
                 # We need to always get requests to not confuse hippo
 
                 min_w_, child_width = child.get_width_request()
 
                 min_w_, child_width = child.get_width_request()
Line 261: Line 267:
 
                 child.allocate(int(x), int(y), child_width, child_height,
 
                 child.allocate(int(x), int(y), child_width, child_height,
 
                               origin_changed)
 
                               origin_changed)
 
+
                 child.item.props.size = icon_size
                 # decrease the radius slightly with each icon
 
                radius -= 6;
 
  
 
         for child in self._locked_children.keys():
 
         for child in self._locked_children.keys():
Line 281: Line 285:
 
         else:
 
         else:
 
             return 0
 
             return 0
 +
 +
_SUNFLOWER_CONSTANT = style.STANDARD_ICON_SIZE * .75
 +
"""Chose a constant such that STANDARD_ICON_SIZE icons are nicely spaced."""
 +
 +
_SUNFLOWER_OFFSET = \
 +
    math.pow((style.XLARGE_ICON_SIZE / 2 + style.STANDARD_ICON_SIZE) /
 +
            _SUNFLOWER_CONSTANT, 2)
 +
"""
 +
Compute a starting index for the `SunflowerLayout` which leaves space for
 +
the XO man in the center.  Since r = _SUNFLOWER_CONSTANT * sqrt(n),
 +
solve for n when r is (XLARGE_ICON_SIZE + STANDARD_ICON_SIZE)/2.
 +
"""
 +
 +
_GOLDEN_RATIO = 1.6180339887498949
 +
"""
 +
Golden ratio: http://en.wikipedia.org/wiki/Golden_ratio
 +
Calculation: (math.sqrt(5) + 1) / 2
 +
"""
 +
 +
_SUNFLOWER_ANGLE = 2.3999632297286531
 +
"""
 +
The sunflower angle is approximately 137.5 degrees.
 +
This is the golden angle: http://en.wikipedia.org/wiki/Golden_angle
 +
Calculation: math.radians(360) / ( _GOLDEN_RATIO * _GOLDEN_RATIO )
 +
"""
 +
 +
class SunflowerLayout(RingLayout):
 +
    """Spiral layout based on Fibonacci ratio in phyllotaxis.
 +
 +
    See http://algorithmicbotany.org/papers/abop/abop-ch4.pdf
 +
    for details of Vogel's model of florets in a sunflower head."""
 +
 +
    __gtype_name__ = 'SunflowerLayout'
 +
 +
    icon_name = 'view-spiral'
 +
    """Name of icon used in home view dropdown palette."""
 +
 +
    profile_key = 'spiral-layout'
 +
    """String used in profile to represent this view."""
 +
 +
    def __init__(self):
 +
        RingLayout.__init__(self)
 +
        self.skipped_indices = []
 +
 +
    def _calculate_radius_and_icon_size(self, children_count):
 +
        """Stub out this method; not used in `SunflowerLayout`."""
 +
        return None, style.STANDARD_ICON_SIZE
 +
 +
    def adjust_index(self, i):
 +
        """Skip floret indices which end up outside the desired bounding box."""
 +
        for idx in self.skipped_indices:
 +
            if i < idx: break
 +
            i += 1
 +
        return i
 +
 +
    def _calculate_position(self, radius, icon_size, oindex, children_count):
 +
        """Calculate the position of sunflower floret number 'oindex'.
 +
        If the result is outside the bounding box, use the next index which
 +
        is inside the bounding box."""
 +
 +
        width, height = self.box.get_allocation()
 +
 +
        while True:
 +
 +
            index = self.adjust_index(oindex)
 +
 +
            # tweak phi to get a nice gap lined up where the "active activity"
 +
            # icon is, below the central XO man.
 +
            phi = index * _SUNFLOWER_ANGLE + math.radians(-130)
 +
 +
            # we offset index when computing r to make space for the XO man.
 +
            r = _SUNFLOWER_CONSTANT * math.sqrt(index + _SUNFLOWER_OFFSET)
 +
 +
            # x,y are the top-left corner of the icon, so remove icon_size
 +
            # from width/height to compensate.  y has an extra GRID_CELL_SIZE/2
 +
            # removed to make room for the "active activity" icon.
 +
            x = r * math.cos(phi) + (width - icon_size) / 2
 +
            y = r * math.sin(phi) + (height - icon_size - \
 +
                                    (style.GRID_CELL_SIZE / 2) ) / 2
 +
 +
            # skip allocations outside the allocation box.
 +
            # give up once we can't fit
 +
            if r < math.hypot(width / 2, height / 2):
 +
                if y < 0 or y > (height - icon_size) or \
 +
                      x < 0 or x > (width - icon_size):
 +
                    self.skipped_indices.append(index)
 +
                    continue # try again
 +
 +
            return x, y
 +
 +
class BoxLayout(RingLayout):
 +
    """Lay out icons in a square around the XO man."""
 +
 +
    __gtype_name__ = 'BoxLayout'
 +
 +
    icon_name = 'view-box'
 +
    """Name of icon used in home view dropdown palette."""
 +
 +
    profile_key = 'box-layout'
 +
    """String used in profile to represent this view."""
 +
 +
    def __init__(self):
 +
        RingLayout.__init__(self)
 +
 +
    def _calculate_position(self, radius, icon_size, index, children_count):
 +
 +
        # use "orthogonal" versions of cos and sin in order to square the
 +
        # circle and turn the 'ring view' into a 'box view'
 +
        def cos_d(d):
 +
            while d < 0:
 +
                d += 360
 +
            if d < 45: return 1
 +
            if d < 135: return (90 - d) / 45.
 +
            if d < 225: return -1
 +
            return cos_d(360 - d) # mirror around 180
 +
 +
        cos = lambda r: cos_d(math.degrees(r))
 +
        sin = lambda r: cos_d(math.degrees(r) - 90)
 +
 +
        return RingLayout._calculate_position\
 +
              (self, radius, icon_size, index, children_count,
 +
                sin=sin, cos=cos)
 +
 +
class TriangleLayout(RingLayout):
 +
    """Lay out icons in a triangle around the XO man."""
 +
 +
    __gtype_name__ = 'TriangleLayout'
 +
 +
    icon_name = 'view-triangle'
 +
    """Name of icon used in home view dropdown palette."""
 +
 +
    profile_key = 'triangle-layout'
 +
    """String used in profile to represent this view."""
 +
 +
    def __init__(self):
 +
        RingLayout.__init__(self)
 +
 +
    def _calculate_radius_and_icon_size(self, children_count):
 +
        # use slightly larger minimum radius than parent, because sides
 +
        # of triangle come awful close to the center.
 +
        radius, icon_size = \
 +
            RingLayout._calculate_radius_and_icon_size(self, children_count)
 +
        return max(radius, _MINIMUM_RADIUS + style.MEDIUM_ICON_SIZE), icon_size
 +
 +
    def _calculate_position(self, radius, icon_size, index, children_count):
 +
        # tweak cos and sin in order to make the 'ring' into an equilateral
 +
        # triangle.
 +
 +
        def cos_d(d):
 +
            while d < -90:
 +
                d += 360
 +
            if d <= 30: return (d + 90) / 120.
 +
            if d <= 90: return (90 - d) / 60.
 +
            return -cos_d(180 - d) # mirror around 90
 +
 +
        sqrt_3 = math.sqrt(3)
 +
 +
        def sin_d(d):
 +
            while d < -90:
 +
                d += 360
 +
            if d <= 30: return ((d + 90) / 120.) * sqrt_3 - 1
 +
            if d <= 90: return sqrt_3 - 1
 +
            return sin_d(180 - d) # mirror around 90
 +
 +
        cos = lambda r: cos_d(math.degrees(r))
 +
        sin = lambda r: sin_d(math.degrees(r))
 +
 +
        return RingLayout._calculate_position\
 +
              (self, radius, icon_size, index, children_count,
 +
                sin=sin, cos=cos)
 +
 +
class MyLayout(RingLayout):
 +
    """Spiral layout based on Archimedean spiral: r = a + b*theta."""
 +
 +
    __gtype_name__ = 'MyLayout'
 +
 +
    icon_name = 'view-mylayout'
 +
    """Name of icon used in home view dropdown palette."""
 +
 +
    profile_key = 'my-layout'
 +
    """String used in profile to represent this view."""
 +
 +
    def __init__(self):
 +
        RingLayout.__init__(self)
 +
 +
    def _calculate_radius_and_icon_size(self, children_count):
 +
        """Stub out this method; not used in `My Layout`."""
 +
        return None, style.STANDARD_ICON_SIZE
 +
 +
    def _calculate_position(self, radius, icon_size, index, children_count):
 +
        """ Increment the radius as you go; decrease the angle of rotation
 +
        as the radius increases to keep the distance between icons constant."""
 +
        width, height = self.box.get_allocation()
 +
        # angle decreases as the radius increases
 +
        angle = index * (2 * math.pi / (12.0 + index / 6.0)) - math.pi / 2
 +
        # radius is proportional to index/children_count
 +
        myminimum = _MINIMUM_RADIUS * .67
 +
        newradius = ((_MAXIMUM_RADIUS - myminimum) * (index * 1.1) / children_count) + myminimum
 +
        x = newradius * math.cos(angle) + (width - icon_size) / 2
 +
        y = newradius * math.sin(angle) + (height - icon_size - style.GRID_CELL_SIZE) / 2
 +
        return x, y
  
 
</pre>
 
</pre>

Revision as of 19:03, 13 October 2008

# Copyright (C) 2008 One Laptop Per Child
#
# 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 logging
import math
import hashlib
from gettext import gettext as _

import gobject
import gtk
import hippo

from sugar.graphics import style
from sugar import activity

from view.home.grid import Grid

_logger = logging.getLogger('FavoritesLayout')

_CELL_SIZE = 4
_BASE_SCALE = 1000

class FavoritesLayout(gobject.GObject, hippo.CanvasLayout):
    """Base class of the different layout types."""

    __gtype_name__ = 'FavoritesLayout'

    def __init__(self):
        gobject.GObject.__init__(self)
        self.box = None
        self.fixed_positions = {}

    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 compare_activities(self, icon_a, icon_b):
        return 0

    def append(self, icon, locked=False):
        self.box.insert_sorted(icon, 0, self.compare_activities)
        if hasattr(icon, 'fixed_position'):
            relative_x, relative_y = icon.fixed_position
            if relative_x >= 0 and relative_y >= 0:
                min_width_, width = self.box.get_width_request()
                min_height_, height = self.box.get_height_request(width)
                self.fixed_positions[icon] = \
                        (int(relative_x * _BASE_SCALE / float(width)),
                         int(relative_y * _BASE_SCALE / float(height)))

    def remove(self, icon):
        if icon in self.fixed_positions:
            del self.fixed_positions[icon]
        self.box.remove(icon)

    def move_icon(self, icon, x, y, locked=False):
        if icon not in self.box.get_children():
            raise ValueError('Child not in box.')

        if hasattr(icon, 'get_bundle_id') and hasattr(icon, 'get_version'):
            min_width_, width = self.box.get_width_request()
            min_height_, height = self.box.get_height_request(width)
            registry = activity.get_registry()
            registry.set_activity_position(
                    icon.get_bundle_id(), icon.get_version(),
                    x * width / float(_BASE_SCALE),
                    y * height / float(_BASE_SCALE))
            self.fixed_positions[icon] = (x, y)

    def do_allocate(self, x, y, width, height, req_width, req_height,
                    origin_changed):
        raise NotImplementedError()

    def allow_dnd(self):
        return False

class RandomLayout(FavoritesLayout):
    """Lay out icons randomly; try to nudge them around to resolve overlaps."""

    __gtype_name__ = 'RandomLayout'

    icon_name = 'view-freeform'
    """Name of icon used in home view dropdown palette."""

    profile_key = 'random-layout'
    """String used in profile to represent this view."""

    def __init__(self):
        FavoritesLayout.__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 __grid_child_changed_cb(self, grid, child):
        child.emit_request_changed()

    def append(self, icon, locked=False):
        FavoritesLayout.append(self, icon, locked)

        min_width_, child_width = icon.get_width_request()
        min_height_, child_height = icon.get_height_request(child_width)
        min_width_, width = self.box.get_width_request()
        min_height_, height = self.box.get_height_request(width)

        if icon in self.fixed_positions:
            x, y = self.fixed_positions[icon]
            x = min(x, width - child_width)
            y = min(y, height - child_height)
        elif hasattr(icon, 'get_bundle_id'):
            name_hash = hashlib.md5(icon.get_bundle_id())
            x = int(name_hash.hexdigest()[:5], 16) % (width - child_width)
            y = int(name_hash.hexdigest()[-5:], 16) % (height - child_height)
        else:
            x = None
            y = None

        if x is None or y is None:
            self._grid.add(icon,
                           child_width / _CELL_SIZE, child_height / _CELL_SIZE)
        else:
            self._grid.add(icon,
                           child_width / _CELL_SIZE, child_height / _CELL_SIZE,
                           x / _CELL_SIZE, y / _CELL_SIZE)

    def remove(self, icon):
        self._grid.remove(icon)
        FavoritesLayout.remove(self, icon)

    def move_icon(self, icon, x, y, locked=False):
        self._grid.move(icon, x / _CELL_SIZE, y / _CELL_SIZE, locked)
        FavoritesLayout.move_icon(self, icon, x, y, locked)

    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 = self._grid.get_child_rect(child.item)
            child.allocate(rect.x * _CELL_SIZE,
                           rect.y * _CELL_SIZE,
                           child_width,
                           child_height,
                           origin_changed)

    def allow_dnd(self):
        return True

_MINIMUM_RADIUS = style.XLARGE_ICON_SIZE / 2 + style.DEFAULT_SPACING + \
        style.STANDARD_ICON_SIZE * 2
_MAXIMUM_RADIUS = (gtk.gdk.screen_height() - style.GRID_CELL_SIZE) / 2 - \
        style.STANDARD_ICON_SIZE - style.DEFAULT_SPACING

class RingLayout(FavoritesLayout):
    """Lay out icons in a ring around the XO man."""

    __gtype_name__ = 'RingLayout'
    icon_name = 'view-radial'
    """Name of icon used in home view dropdown palette."""
    profile_key = 'ring-layout'
    """String used in profile to represent this view."""

    def __init__(self):
        FavoritesLayout.__init__(self)
        self._locked_children = {}

    def append(self, icon, locked=False):
        FavoritesLayout.append(self, icon, locked)
        if locked:
            child = self.box.find_box_child(icon)
            self._locked_children[child] = (0, 0)

    def remove(self, icon):
        child = self.box.find_box_child(icon)
        if child in self._locked_children:
            del self._locked_children[child]
        FavoritesLayout.remove(self, icon)

    def move_icon(self, icon, x, y, locked=False):
        FavoritesLayout.move_icon(self, icon, x, y, locked)
        if locked:
            child = self.box.find_box_child(icon)
            self._locked_children[child] = (x, y)

    def _calculate_radius_and_icon_size(self, children_count):
        # what's the radius required without downscaling?
        distance = style.STANDARD_ICON_SIZE + style.DEFAULT_SPACING
        icon_size = style.STANDARD_ICON_SIZE
        # circumference is 2*pi*r; we want this to be at least
        # 'children_count * distance'
        radius = children_count * distance / (2 * math.pi)
        # limit computed radius to reasonable bounds.
        radius = max(radius, _MINIMUM_RADIUS)
        radius = min(radius, _MAXIMUM_RADIUS)
        # recompute icon size from limited radius
        if children_count > 0:
            icon_size = (2 * math.pi * radius / children_count) \
                        - style.DEFAULT_SPACING
        # limit adjusted icon size.
        icon_size = max(icon_size, style.SMALL_ICON_SIZE)
        icon_size = min(icon_size, style.MEDIUM_ICON_SIZE)
        return radius, icon_size

    def _calculate_position(self, radius, icon_size, index, children_count,
                            sin=math.sin, cos=math.cos):
        width, height = self.box.get_allocation()
        angle = index * (2 * math.pi / children_count) - math.pi / 2
        x = radius * cos(angle) + (width - icon_size) / 2
        y = radius * sin(angle) + (height - icon_size -
                                   (style.GRID_CELL_SIZE/2) ) / 2
        return x, y

    def _get_children_in_ring(self):
        children_in_ring = [child for child in self.box.get_layout_children() \
                if child not in self._locked_children]
        return children_in_ring

    def _update_icon_sizes(self):
        # XXX: THIS METHOD IS NEVER CALLED
        children_in_ring = self._get_children_in_ring()
        radius_, icon_size = \
                self._calculate_radius_and_icon_size(len(children_in_ring))

        for child in children_in_ring:
            child.item.props.size = icon_size

    def do_allocate(self, x, y, width, height, req_width, req_height,
                    origin_changed):
        children_in_ring = self._get_children_in_ring()
        if children_in_ring:
            radius, icon_size = \
                    self._calculate_radius_and_icon_size(len(children_in_ring))

            for n in range(len(children_in_ring)):
                child = children_in_ring[n]

                x, y = self._calculate_position(radius, icon_size, n,
                                                len(children_in_ring))

                # 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)

                child.allocate(int(x), int(y), child_width, child_height,
                               origin_changed)
                child.item.props.size = icon_size

        for child in self._locked_children.keys():
            x, y = self._locked_children[child]

            # 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)

            child.allocate(int(x), int(y), child_width, child_height,
                            origin_changed)

    def compare_activities(self, icon_a, icon_b):
        if hasattr(icon_a, 'installation_time') and \
                hasattr(icon_b, 'installation_time'):
            return icon_b.installation_time - icon_a.installation_time
        else:
            return 0

_SUNFLOWER_CONSTANT = style.STANDARD_ICON_SIZE * .75
"""Chose a constant such that STANDARD_ICON_SIZE icons are nicely spaced."""

_SUNFLOWER_OFFSET = \
    math.pow((style.XLARGE_ICON_SIZE / 2 + style.STANDARD_ICON_SIZE) /
             _SUNFLOWER_CONSTANT, 2)
"""
Compute a starting index for the `SunflowerLayout` which leaves space for
the XO man in the center.  Since r = _SUNFLOWER_CONSTANT * sqrt(n),
solve for n when r is (XLARGE_ICON_SIZE + STANDARD_ICON_SIZE)/2.
"""

_GOLDEN_RATIO = 1.6180339887498949
"""
Golden ratio: http://en.wikipedia.org/wiki/Golden_ratio
Calculation: (math.sqrt(5) + 1) / 2
"""

_SUNFLOWER_ANGLE = 2.3999632297286531
"""
The sunflower angle is approximately 137.5 degrees.
This is the golden angle: http://en.wikipedia.org/wiki/Golden_angle
Calculation: math.radians(360) / ( _GOLDEN_RATIO * _GOLDEN_RATIO )
"""

class SunflowerLayout(RingLayout):
    """Spiral layout based on Fibonacci ratio in phyllotaxis.

    See http://algorithmicbotany.org/papers/abop/abop-ch4.pdf
    for details of Vogel's model of florets in a sunflower head."""

    __gtype_name__ = 'SunflowerLayout'

    icon_name = 'view-spiral'
    """Name of icon used in home view dropdown palette."""

    profile_key = 'spiral-layout'
    """String used in profile to represent this view."""

    def __init__(self):
        RingLayout.__init__(self)
        self.skipped_indices = []

    def _calculate_radius_and_icon_size(self, children_count):
        """Stub out this method; not used in `SunflowerLayout`."""
        return None, style.STANDARD_ICON_SIZE

    def adjust_index(self, i):
        """Skip floret indices which end up outside the desired bounding box."""
        for idx in self.skipped_indices:
            if i < idx: break
            i += 1
        return i

    def _calculate_position(self, radius, icon_size, oindex, children_count):
        """Calculate the position of sunflower floret number 'oindex'.
        If the result is outside the bounding box, use the next index which
        is inside the bounding box."""

        width, height = self.box.get_allocation()

        while True:

            index = self.adjust_index(oindex)

            # tweak phi to get a nice gap lined up where the "active activity"
            # icon is, below the central XO man.
            phi = index * _SUNFLOWER_ANGLE + math.radians(-130)

            # we offset index when computing r to make space for the XO man.
            r = _SUNFLOWER_CONSTANT * math.sqrt(index + _SUNFLOWER_OFFSET)

            # x,y are the top-left corner of the icon, so remove icon_size
            # from width/height to compensate.  y has an extra GRID_CELL_SIZE/2
            # removed to make room for the "active activity" icon.
            x = r * math.cos(phi) + (width - icon_size) / 2
            y = r * math.sin(phi) + (height - icon_size - \
                                     (style.GRID_CELL_SIZE / 2) ) / 2

            # skip allocations outside the allocation box.
            # give up once we can't fit
            if r < math.hypot(width / 2, height / 2):
                if y < 0 or y > (height - icon_size) or \
                       x < 0 or x > (width - icon_size):
                    self.skipped_indices.append(index)
                    continue # try again

            return x, y

class BoxLayout(RingLayout):
    """Lay out icons in a square around the XO man."""

    __gtype_name__ = 'BoxLayout'

    icon_name = 'view-box'
    """Name of icon used in home view dropdown palette."""

    profile_key = 'box-layout'
    """String used in profile to represent this view."""

    def __init__(self):
        RingLayout.__init__(self)

    def _calculate_position(self, radius, icon_size, index, children_count):

        # use "orthogonal" versions of cos and sin in order to square the
        # circle and turn the 'ring view' into a 'box view'
        def cos_d(d):
            while d < 0:
                d += 360
            if d < 45: return 1
            if d < 135: return (90 - d) / 45.
            if d < 225: return -1
            return cos_d(360 - d) # mirror around 180

        cos = lambda r: cos_d(math.degrees(r))
        sin = lambda r: cos_d(math.degrees(r) - 90)

        return RingLayout._calculate_position\
               (self, radius, icon_size, index, children_count,
                sin=sin, cos=cos)

class TriangleLayout(RingLayout):
    """Lay out icons in a triangle around the XO man."""

    __gtype_name__ = 'TriangleLayout'

    icon_name = 'view-triangle'
    """Name of icon used in home view dropdown palette."""

    profile_key = 'triangle-layout'
    """String used in profile to represent this view."""

    def __init__(self):
        RingLayout.__init__(self)

    def _calculate_radius_and_icon_size(self, children_count):
        # use slightly larger minimum radius than parent, because sides
        # of triangle come awful close to the center.
        radius, icon_size = \
            RingLayout._calculate_radius_and_icon_size(self, children_count)
        return max(radius, _MINIMUM_RADIUS + style.MEDIUM_ICON_SIZE), icon_size

    def _calculate_position(self, radius, icon_size, index, children_count):
        # tweak cos and sin in order to make the 'ring' into an equilateral
        # triangle.

        def cos_d(d):
            while d < -90:
                d += 360
            if d <= 30: return (d + 90) / 120.
            if d <= 90: return (90 - d) / 60.
            return -cos_d(180 - d) # mirror around 90

        sqrt_3 = math.sqrt(3)

        def sin_d(d):
            while d < -90:
                d += 360
            if d <= 30: return ((d + 90) / 120.) * sqrt_3 - 1
            if d <= 90: return sqrt_3 - 1
            return sin_d(180 - d) # mirror around 90

        cos = lambda r: cos_d(math.degrees(r))
        sin = lambda r: sin_d(math.degrees(r))

        return RingLayout._calculate_position\
               (self, radius, icon_size, index, children_count,
                sin=sin, cos=cos)

class MyLayout(RingLayout):
    """Spiral layout based on Archimedean spiral: r = a + b*theta."""

    __gtype_name__ = 'MyLayout'

    icon_name = 'view-mylayout'
    """Name of icon used in home view dropdown palette."""

    profile_key = 'my-layout'
    """String used in profile to represent this view."""

    def __init__(self):
        RingLayout.__init__(self)

    def _calculate_radius_and_icon_size(self, children_count):
        """Stub out this method; not used in `My Layout`."""
        return None, style.STANDARD_ICON_SIZE

    def _calculate_position(self, radius, icon_size, index, children_count):
        """ Increment the radius as you go; decrease the angle of rotation
        as the radius increases to keep the distance between icons constant."""
        width, height = self.box.get_allocation()
        # angle decreases as the radius increases
        angle = index * (2 * math.pi / (12.0 + index / 6.0)) - math.pi / 2
        # radius is proportional to index/children_count
        myminimum = _MINIMUM_RADIUS * .67
        newradius = ((_MAXIMUM_RADIUS - myminimum) * (index * 1.1) / children_count) + myminimum
        x = newradius * math.cos(angle) + (width - icon_size) / 2
        y = newradius * math.sin(angle) + (height - icon_size - style.GRID_CELL_SIZE) / 2
        return x, y