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