Changes

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>