#!/usr/bin/env python
########################################################################
# Pie Menus for pygtk and OLPC Sugar.
# Copyright (C) 1986-2007 by Don Hopkins. All rights reserved.
#
# Designed and implemented by Don Hopkins (dhopkins@DonHopkins.com).
#
# 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.1 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
'''PieMenu
Pie menu component for pygtk and OLPC Sugar, by Don Hopkins.'''
########################################################################
import gtk
import cairo
import pango
import math
import time
########################################################################
# Math utilities.
def DegToRad(deg):
return (2 * math.pi * deg) / 360.0
def RadToDeg(rad):
return (360.0 * rad) / (2 * math.pi)
def NormalizeAngleRad(ang):
two_pi = 2 * math.pi
while ang < 0:
ang += two_pi
while ang >= two_pi:
ang -= two_pi
return ang
########################################################################
# Font cache.
FontCache = {}
def GetFont(s):
if FontCache.has_key(s):
return FontCache[s]
descr = pango.FontDescription(s)
FontCache[s] = descr
return descr
########################################################################
# Image cache.
ImageCache = {}
def GetImage(s):
if ImageCache.has_key(s):
return ImageCache[s]
image = None
if s[-4:].lower() == '.png':
try:
image = cairo.ImageSurface.create_from_png(s)
except: pass
elif s[-4:].lower() == '.svg':
try:
# FIXME: read SVG into image
print "SVG images not supported yet:", s
except: pass
else:
print "Don't know how to load image file type:", s
if image:
ImageCache[s] = image
return image
########################################################################
class PieItem:
def __init__(
self,
label=None,
description=None,
x=0,
y=0,
width=0,
height=0,
label_font='Helvetica 14',
label_padding=2,
label_x_adjust=0,
label_y_adjust=2,
icon=None,
icon_hilite=None,
icon_padding=2,
icon_side='top',
visible=True,
pie=None,
sub_pie=None,
fixed_radius=0,
linear=False,
linear_direction='down',
linear_order_reversed=False,
linear_gap=0,
hilite_fill_color=(1, 1, 0),
hilite_stroke_color=(0, 0, 1),
hilite_text_color=(0, 0, 1),
lolite_fill_color=(1, 1, 1),
lolite_stroke_color=(0, 0, 0),
lolite_text_color=(0, 0, 0),
scale=1.0,
**args):
#print "PIEITEM INIT", self, label, args
self.label = label
self.description = description
self.x = x
self.y = y
self.width = width
self.height = height
self.label_font = label_font
self.label_padding = label_padding
self.label_x_adjust = label_x_adjust
self.label_y_adjust = label_y_adjust
self.icon = icon
self.icon_hilite = icon_hilite
self.icon_padding = icon_padding
self.icon_side = icon_side
self.visible = visible
self.pie = pie
self.sub_pie = sub_pie
self.fixed_radius = fixed_radius
self.linear = linear
self.linear_direction = linear_direction
self.linear_order_reversed = linear_order_reversed
self.linear_gap = linear_gap
self.hilite_fill_color = hilite_fill_color
self.hilite_stroke_color = hilite_stroke_color
self.hilite_text_color = hilite_text_color
self.lolite_fill_color = lolite_fill_color
self.lolite_stroke_color = lolite_stroke_color
self.lolite_text_color = lolite_text_color
self.scale = scale
self.valid = False
self.index = -1
self.dx = 0
self.dy = 0
self.enter_time = 0
self.exit_time = 0
self.total_time = 0
self.final_x = 0
self.final_y = 0
self.ring_index = None
self.angle = 0
self.edge1dx = 0
self.edge1dy = 0
self.edge1angle = 0
self.edge2dx = 0
self.edge2dy = 0
self.edge2angle = 0
self.label_x = 0
self.label_y = 0
self.label_width = 0
self.label_height = 0
self.icon_x = 0
self.icon_y = 0
self.icon_width = 0
self.icon_height = 0
def measure(self, context, pcontext, playout):
label = self.label
width = 0
height = 0
label_x = 0
label_y = 0
label_width = 0
label_height = 0
icon_x = 0
icon_y = 0
icon_width = 0
icon_height = 0
label_padding = self.label_padding
icon_padding = self.icon_padding
if label != None:
font = GetFont(self.label_font)
playout.set_font_description(font)
playout.set_markup(label)
label_width, label_height = playout.get_pixel_size()
label_width += 2 * label_padding
label_height += 2 * label_padding
icon = self.icon
if icon != None:
image = GetImage(icon)
if image:
icon_width = image.get_width()
icon_height = image.get_height()
icon_width += 2 * icon_padding
icon_height += 2 * icon_padding
if not label:
if not icon:
# No icon, no label.
pass
else:
# Just an icon.
width = icon_width
height = icon_height
icon_x = icon_padding
icon_y = icon_padding
else:
if not icon:
# Just a label.
width = label_width
height = label_height
label_x = label_padding
label_y = label_padding
else:
# Icon and label.
icon_side = self.icon_side
if icon_side in ('n', 'up', 'top'):
width = max(label_width, icon_width)
height = label_height + icon_height
icon_x = int(math.floor((width - icon_width) / 2)) + icon_padding
icon_y = icon_padding
label_x = int(math.floor((width - label_width) / 2)) + label_padding
label_y = icon_height + label_padding
elif icon_side in ('nw', 'topleft'):
width = max(label_width, icon_width)
height = label_height + icon_height
icon_x = icon_padding
icon_y = icon_padding
label_x = label_padding
label_y = icon_height + label_padding
elif icon_side in ('ne', 'topright'):
width = max(label_width, icon_width)
height = label_height + icon_height
icon_x = width - icon_width + icon_padding
icon_y = icon_padding
label_x = width - label_width + label_padding
label_y = icon_height + label_padding
elif icon_side in ('s', 'down', 'bottom'):
width = max(label_width, icon_width)
height = label_height + icon_height
label_x = int(math.floor((width - label_width) / 2)) + label_padding
label_y = label_padding
icon_x = int(math.floor((width - icon_width) / 2)) + icon_padding
icon_y = label_height + icon_padding
elif icon_side in ('sw', 'bottomleft'):
width = max(label_width, icon_width)
height = label_height + icon_height
label_x = label_padding
label_y = label_padding
icon_x = icon_padding
icon_y = label_height + icon_padding
elif icon_side in ('se', 'bottomright'):
width = max(label_width, icon_width)
height = label_height + icon_height
label_x = width - label_width + label_padding
label_y = label_padding
icon_x = width - icon_width + icon_padding
icon_y = label_height + icon_padding
elif icon_side in ('w', 'left'):
width = label_width + icon_width
height = max(label_height, icon_height)
icon_x = icon_padding
icon_y = int(math.floor((height - icon_height) / 2)) + icon_padding
label_x = icon_width + label_padding
label_y = int(math.floor((height - label_height) / 2)) + label_padding
elif icon_side in ('e', 'right'):
width = label_width + icon_width
height = max(label_height, icon_height)
label_x = label_padding
label_y = int(math.floor((height - label_height) / 2)) + label_padding
icon_x = label_width + icon_padding
icon_y = int(math.floor((height - icon_height) / 2)) + icon_padding
else:
print "Invalid icon_side: " + repr(icon_side)
self.width = width
self.height = height
self.label_x = label_x
self.label_y = label_y
self.label_width = label_width
self.label_height = label_height
self.icon_x = icon_x
self.icon_y = icon_y
self.icon_width = icon_width
self.icon_height = icon_height
def layoutForPie(self, radius):
if self.fixed_radius > 0:
radius = self.fixed_radius;
gap = 1
rdx = radius * self.dx
rdy = radius * self.dy
# Special cases for top, bottom, left, and right
if (math.floor(rdx + 0.5)) == 0: # top or bottom
rdx -= (self.width / 2.0) + 1
if rdy > 0:
rdy += gap - 3; # top
else:
rdy -= self.height + gap - 2; # bottom
else:
if (math.floor(rdy + 0.5)) == 0: # left or right
rdy -= (self.height / 2.0) + 0.5
if rdx < 0:
rdx -= self.width + gap - 2
else:
rdx += gap - 3
else: # everwhere else
# Justify the label according to its quadrant.
fudge = 2
if rdx < 0:
rdx -= self.width - fudge
else:
rdx -= fudge
if rdy < 0:
rdy -= self.height - fudge
else:
rdy -= fudge
self.x = int(math.floor(rdx + 0.5))
self.y = int(math.floor(rdy + 0.5))
def layoutForLinear(self, min_x, min_y, max_x, max_y):
pie = self.pie
linear_direction = self.linear_direction
if ((linear_direction == None) or
(linear_direction == "")):
linear_direction = self.pie.linear_direction
pie = self.pie
gap = self.linear_gap
x_center_offset = 0
y_center_offset = 0
if linear_direction in ('c', 'center'):
other_item = pie.addItemDirection(self, 'center')
x_center_offset = int(math.floor(self.width / -2.0) - 1)
y_center_offset = int(math.floor(self.height / -2.0))
elif linear_direction in ('e', 'right'):
other_item = pie.addItemDirection(self, 0)
if other_item:
x_center_offset = int(math.floor(other_item.x + other_item.width) + gap)
y_center_offset = int(math.floor(self.height / -2.0))
else:
self.layoutForPie(pie.radius)
return
elif linear_direction in ('ne', 'topright'):
other_item = pie.addItemDirection(self, 45)
if other_item:
x_center_offset = int(math.floor(other_item.x + other_item.width) + gap)
y_center_offset = int(math.floor((other_item.y + other_item.height) - self.height))
else:
self.layoutForPie(pie.radius)
return
elif linear_direction in ('se', 'bottomright'):
other_item = pie.addItemDirection(self, 315)
if other_item:
x_center_offset = int(math.floor(other_item.x + other_item.width) + gap)
y_center_offset = int(math.floor(other_item.y))
else:
self.layoutForPie(pie.radius)
return
elif linear_direction in ('w', 'left'):
other_item = pie.addItemDirection(self, 180)
if other_item:
x_center_offset = int(math.floor(other_item.x - self.width) - gap)
y_center_offset = int(math.floor(self.height / -2.0))
else:
self.layoutForPie(pie.radius)
return
elif linear_direction in ('nw', 'topleft'):
other_item = pie.addItemDirection(self, 135)
if other_item:
x_center_offset = int(math.floor(other_item.x - other_item.width) - gap)
y_center_offset = int(math.floor((other_item.y + other_item.height) - self.height))
else:
self.layoutForPie(pie.radius)
return
elif linear_direction in ('sw', 'bottomleft'):
other_item = pie.addItemDirection(self, 225)
if other_item:
x_center_offset = int(math.floor(other_item.x - self.width) - gap)
y_center_offset = int(math.floor(other_item.y))
else:
self.layoutForPie(pie.radius)
return
elif linear_direction in ('n', 'up', 'top'):
other_item = pie.addItemDirection(self, 90)
if other_item:
x_center_offset = int(math.floor(self.width / -2.0) - 1)
y_center_offset = int(math.floor(other_item.y - self.height) - gap)
else:
self.layoutForPie(pie.radius)
return
elif linear_direction in ('s', 'down', 'bottom'):
other_item = pie.addItemDirection(self, 270)
if other_item:
x_center_offset = int(math.floor(self.width / -2.0) - 1)
y_center_offset = int(math.floor(other_item.y + other_item.height) + gap)
else:
self.layoutForPie(pie.radius)
return
self.x = int(math.floor(0.5 + x_center_offset))
self.y = int(math.floor(0.5 + y_center_offset))
self.ring_index = None
def itemsOverlap(self, item, fringe=2):
myLeft = self.x - fringe
myRight = self.x + self.width + fringe
yourLeft = item.x - fringe
yourRight = item.x + item.width + fringe
myTop = self.y - fringe
myBottom = self.y + self.height + fringe
yourTop = item.y - fringe
yourBottom = item.y + item.height + fringe
return ((myBottom > yourTop) and
(myTop < yourBottom) and
(myRight > yourLeft) and
(myLeft < yourRight))
def draw(self, rect, context, pcontext, playout):
x = self.x
y = self.y
width = self.width
height = self.height
hilited = self.index == self.pie.cur_item
if hilited:
fill_color = self.hilite_fill_color
stroke_color = self.hilite_stroke_color
text_color = self.hilite_text_color
else:
fill_color = self.lolite_fill_color
stroke_color = self.lolite_stroke_color
text_color = self.lolite_text_color
if fill_color or stroke_color:
context.rectangle(x, y, width, height)
if fill_color:
context.set_source_rgb(*fill_color)
if stroke_color:
context.fill_preserve()
else:
context.fill()
if stroke_color:
context.set_source_rgb(*stroke_color)
context.stroke()
label = self.label
if text_color and (label != None):
context.set_source_rgb(*text_color)
font = GetFont(self.label_font)
playout.set_font_description(font)
playout.set_markup(label)
context.move_to(
x + self.label_x,
y + self.label_y)
context.show_layout(playout)
hilited = self.index == self.pie.cur_item
if hilited:
icon = self.icon_hilite or self.icon
else:
icon = self.icon
if icon:
image = GetImage(icon)
if image:
context.set_source_surface(
image,
x + self.icon_x,
y + self.icon_y)
context.paint()
def handleMotion(self):
# TODO: notify menu item about mouse motion
pass
########################################################################
class PieMenu(gtk.Window):
def __init__(
self,
parent=None,
outside_fill_color=(.9, .9, .9),
outside_stroke_color=(0, 0, 0),
background_fill_color=(1, 1, 1),
background_stroke_color=(.5, .5, .5),
edge_stroke_color=(.5, .5, .5),
slice_hilite_fill_color=(0, 1, 0),
slice_hilite_stroke_color=(0, 0, 1),
neutral_radius=12,
neutral_hilite_fill_color=(1, 0, 0),
neutral_hilite_stroke_color=(0, 0, 1),
neutral_lolite_fill_color=(1, 1, 1),
neutral_lolite_stroke_color=(.5, .5, .5),
neutral_description=None,
popout_radius=1000,
ring_radius = 40,
header=None,
header_fill_color=(0, 0, 0),
header_stroke_color=None,
header_text_color=(1, 1, 1),
header_font='Helvetica 24',
header_padding=2,
header_margin=4,
header_gap=4,
header_x_adjust=0,
header_y_adjust=3,
footer=None,
footer_fill_color=(1, 1, 0),
footer_stroke_color=(0, 0, 1),
footer_text_color=(0, 0, 1),
footer_font='Helvetica 12',
footer_padding=2,
footer_margin=4,
footer_gap=4,
footer_x_adjust=0,
footer_y_adjust=3,
footer_fixed_height=0,
footer_descriptions=True,
clockwise=True,
initial_angle=90,
fixed_radius=0,
min_radius=0,
max_radius=0,
extra_radius=0,
label_gap_radius=8,
margin_radius=5,
radius_notch=2,
popupedness=1,
show_background=True,
background_image=None,
border=5,
max_pie_items=8,
linear=False,
linear_direction='down',
linear_order_reversed=False,
transparent_items=True,
item_border=2,
item_margin=2,
item_width=0,
item_height=0,
center_margin=16,
center_border_width=2,
center_visible=1,
parent_pie=None,
parent_item=None,
pin_x=0,
pin_y=0,
popup_animation_duration=500,
animate_popup_labels=True,
animate_popup_opacity=False,
animate_popup_background=True,
**args):
# Create the toplevel window
gtk.Window.__init__(
self,
type = gtk.WINDOW_POPUP,
**args)
try:
self.set_screen(parent.get_screen())
except AttributeError:
self.connect('destroy', lambda *w: gtk.main_quit())
d = PieMenuDrawingArea()
self.d = d
self.add(self.d)
self.connect("show", self.handle_show)
d.connect("expose_event", self.handle_expose)
d.connect("motion_notify_event", self.handle_motion_notify_event)
d.connect("button_press_event", self.handle_button_press_event)
d.connect("button_release_event", self.handle_button_release_event)
d.connect("proximity_in_event", self.handle_proximity_in_event)
d.connect("proximity_out_event", self.handle_proximity_out_event)
d.connect("grab_notify", self.handle_grab_notify)
d.connect("grab_broken_event", self.handle_grab_broken_event)
d.connect("key_press_event", self.handle_key_press_event)
d.connect("key_release_event", self.handle_key_release_event)
d.set_events(
gtk.gdk.EXPOSURE_MASK |
gtk.gdk.POINTER_MOTION_MASK |
gtk.gdk.POINTER_MOTION_HINT_MASK |
gtk.gdk.BUTTON_MOTION_MASK |
gtk.gdk.BUTTON_PRESS_MASK |
gtk.gdk.BUTTON_RELEASE_MASK |
gtk.gdk.KEY_PRESS_MASK |
gtk.gdk.KEY_RELEASE_MASK |
gtk.gdk.PROXIMITY_IN_MASK |
gtk.gdk.PROXIMITY_OUT_MASK)
self.outside_fill_color = outside_fill_color
self.outside_stroke_color = outside_stroke_color
self.background_fill_color = background_fill_color
self.background_stroke_color = background_stroke_color
self.edge_stroke_color = edge_stroke_color
self.slice_hilite_fill_color = slice_hilite_fill_color
self.slice_hilite_stroke_color = slice_hilite_stroke_color
self.neutral_radius = neutral_radius
self.neutral_hilite_fill_color = neutral_hilite_fill_color
self.neutral_hilite_stroke_color = neutral_hilite_stroke_color
self.neutral_lolite_fill_color = neutral_lolite_fill_color
self.neutral_lolite_stroke_color = neutral_lolite_stroke_color
self.neutral_description = neutral_description
self.popout_radius = popout_radius
self.ring_radius = ring_radius
self.header = header
self.header_fill_color = header_fill_color
self.header_stroke_color = header_stroke_color
self.header_text_color = header_text_color
self.header_font = header_font
self.header_padding = header_padding
self.header_margin = header_margin
self.header_gap = header_gap
self.header_x_adjust = header_x_adjust
self.header_y_adjust = header_y_adjust
self.footer = footer
self.footer_fill_color = footer_fill_color
self.footer_stroke_color = footer_stroke_color
self.footer_text_color = footer_text_color
self.footer_font = footer_font
self.footer_padding = footer_padding
self.footer_margin = footer_margin
self.footer_gap = footer_gap
self.footer_x_adjust = footer_x_adjust
self.footer_y_adjust = footer_y_adjust
self.footer_fixed_height = footer_fixed_height
self.footer_descriptions = footer_descriptions
self.clockwise = clockwise
self.initial_angle = initial_angle
self.fixed_radius = fixed_radius
self.min_radius = min_radius
self.max_radius = max_radius
self.extra_radius = extra_radius
self.label_gap_radius = label_gap_radius
self.margin_radius = margin_radius
self.radius_notch = radius_notch
self.popupedness = popupedness
self.show_background = show_background
self.background_image = background_image
self.border = border
self.max_pie_items = max_pie_items
self.linear = linear
self.linear_direction = linear_direction
self.linear_order_reversed = linear_order_reversed
self.transparent_items = transparent_items
self.item_border = item_border
self.item_margin = item_margin
self.item_width = item_width
self.item_height = item_height
self.center_margin = center_margin
self.center_border_width = center_border_width
self.center_visible = center_visible
self.parent_pie = parent_pie
self.parent_item = parent_item
self.popup_animation_duration = popup_animation_duration
self.animate_popup_labels = animate_popup_labels
self.animate_popup_opacity = animate_popup_opacity
self.animate_popup_background = animate_popup_background
self.radius = 0
self.center_x = 0
self.center_y = 0
self.inner_radius = 0
self.header_x = 0
self.header_y = 0
self.header_width = 0
self.header_height = 0
self.footer_x = 0
self.footer_y = 0
self.footer_width = 0
self.footer_height = 0
self.cur_x = -1
self.cur_y = -1
self.last_x = -1
self.last_y = -1
self.mouse_delta_x = 0
self.mouse_delta_y = 0
self.direction = 0
self.distance = 0
self.items = []
self.visible_items = []
self.pie_rings = []
self.linear_items = []
self.item_directions = {}
self.item = None
self.cur_ring_index = 0
self.cur_item = -1
self.last_item = -1
self.valid = False
self.min_x = 0
self.max_x = 0
self.min_y = 0
self.max_y = 0
self.tracking_flag = False
self.center_x = 0
self.center_y = 0
self.pinned = False
self.win_x = None
self.win_y = None
self.win_width = None
self.win_height = None
def add_item(self, item):
item.pie = self
self.items.append(item)
self.invalidate()
def invalidate(self):
self.valid = False
def validate(self, rect, context, pcontext, playout):
if self.valid:
return
self.valid = True
self.layout(rect, context, pcontext, playout)
def addItemDirection(self, item, deg):
# Add an item to the map of directions to items in that direction.
# Also set the item's dx and dy. Not sure why that's here.
if deg == "center":
dx = 0
dy = 0
else:
angle = DegToRad(deg)
deg = \
int(
math.floor(0.5 +
RadToDeg(
NormalizeAngleRad(
angle))))
dx = math.cos(angle)
dy = -math.sin(angle)
item.dx = dx
item.dy = dy
last_item = None
a = self.item_directions.get(deg, None)
if not a:
a = []
self.item_directions[deg] = a
else:
last_item = a[-1]
a.append(item)
return last_item
def layoutSelf(self):
context = self.window.cairo_create()
pcontext = self.create_pango_context()
playout = pango.Layout(pcontext)
rect = self.get_allocation()
self.layout(rect, context, pcontext, playout)
def layout(self, rect, context, pcontext, playout):
#print "PieMenu layout", self, rect, context, pcontext, playout
# Just the visible items.
visible_items = []
self.visible_items = visible_items
# The pie rings.
pie_rings = []
self.pie_rings = pie_rings
# Just the visible pie menus items.
pie_items = []
# Just the visible linear menu items.
linear_items = []
self.linear_items = linear_items
# Map of item direction to array of items in that direction.
item_directions = {}
self.item_directions = item_directions
# Put just the visible items from items into the visible_items array.
for item in self.items:
if item.visible:
visible_items.append(item)
# Count the visible items.
item_count = len(visible_items)
# We're done if no items.
if item_count == 0:
return
# Initialize constants and variables for Layout.
two_pi = 2 * math.pi
max_pie_items = self.max_pie_items
if max_pie_items == None:
max_pie_items = 1.0e+6
elif type(max_pie_items) not in (type(()), type([])):
max_pie_items = [max_pie_items]
max_pie_items_total = 0
for i in max_pie_items:
max_pie_items_total += i
# Categorize items into pie_items and linear_items arrays.
item_index = 0
pie_item_count = 0
max_ring_items = max_pie_items[0]
for item in visible_items:
item.index = item_index
item_index += 1
item.valid = False
# Limit the number of pie items to max_pie_items_total.
# Classify overflow items as linear.
if pie_item_count >= max_pie_items_total:
item.linear = True
if item.linear:
# Handle reverse ordering linear items.
if item.linear_order_reversed:
# Prepend the linear item to the beginning of the linearItems array.
linear_items.insert(0, item)
else:
# Append the linear item to the end of the linearItems array.
linear_items.append(item)
else:
# If this is the first item, then make the first ring.
if len(pie_rings) == 0:
pie_rings.append(pie_items)
# If this item will overflow the current ring, then make a new ring.
if len(pie_items) >= max_ring_items:
pie_items = []
pie_rings.append(pie_items)
ring_index = len(pie_rings) - 1
if ring_index < len(max_pie_items):
max_ring_items = max_pie_items[ring_index]
else:
print "Layout error: too many pie items, max_pie_items array did not add up right."
# Append the pie item to the end of the pie_items array.
pie_items.append(item)
pie_item_count += 1
# Measure the items.
for item in self.items:
item.x = 0
item.y = 0
item.measure(context, pcontext, playout)
ring_count = len(pie_rings)
linear_item_count = len(linear_items)
clockwise = self.clockwise
if type(clockwise) not in (type(()), type([])):
clockwise = [clockwise]
# If there are any pie items, then calculate the pie menu parameters.
if ring_count > 0:
ring_index = 0
for pie_items in pie_rings:
ring_item_count = len(pie_items)
# Calculate the subtend, angle, cosine, sine, quadrant, slope,
# and size of each pie menu item.
# Calculate twist, the angular width of each slice.
twist = two_pi / ring_item_count
# Twist the other way if clockwise.
ring_clockwise = clockwise[min(len(clockwise) - 1, ring_index)]
if ring_clockwise:
twist = -twist
# Point ang towards the center of the first slice.
ring_initial_angle = self.getRingInitialAngle(ring_index)
ang = DegToRad(ring_initial_angle)
# Twist backwards half a slice, to the edge of the slice.
ang -= twist / 2.0
# Now calculate the size and other properties of the pie items.
for item in pie_items:
# Calculate angle, the center of the current slice.
angle = ang + (twist / 2.0)
# Add self item to the piemenu's list of items in the same direction.
self.addItemDirection(item, RadToDeg(angle))
# Calculate the unit vectors of the slice edge directions.
# Calculate ang in the upside-down coordinate system, for drawing.
item.ring_index = ring_index
item.angle = ang
item.edge1dx = math.cos(ang)
item.edge1dy = -math.sin(ang)
item.edge1angle = math.atan2(item.edge1dy, item.edge1dx)
item.edge2dx = math.cos(ang + twist)
item.edge2dy = -math.sin(ang + twist)
item.edge2angle = math.atan2(item.edge2dy, item.edge2dx)
# Twist ang around to the edge of the next slice.
ang += twist
ring_index += 1
# Determine the radius for the inner ring.
radius = self.fixed_radius
label_gap_radius = self.label_gap_radius
# If the radius is not fixed, then calculate it dynamically.
if radius <= 0:
# Start with the min_radius.
radius = self.min_radius
# If there are any pie items, then make sure they don't overlap.
if ring_count > 0:
# Only apply this to the inner ring of the pie menu, for now.
pie_items = pie_rings[0]
# Increase the radius until there are no overlaps between
# any pie items.
# Start by wrapping last around to the end of the
# circular menu.
# Last index and last pie item, used to test for overlap.
# Only test for last item overlap if more than one pie item.
last_index = len(pie_items) - 1
last = None
if last_index > 0:
last = pie_items[last_index]
# Loop over all pie items testing for overlap with last adjacent
# pie item.
for item in pie_items:
# Ignore fixed_radius items.
# XXX: The behavior of mixing adjacent fixed_radius and not
# fixed_radius pie menu items is not well defined.
# XXX: fixed_radius should be inherited from the piemenu.
if item.fixed_radius > 0:
continue
# Push the radius out until there are no overlaps.
# Give up after a while.
max_loops = 200
radius_notch = self.radius_notch
for loop_count in range(max_loops):
# Lay out the item at the current radius.
item.layoutForPie(radius + label_gap_radius)
# If there is only one item, then we're done pushing out.
if last == None:
# Done pushing out.
break
# If there are more than one pie items, then test for adjacent overlaps.
# Lay out the last item at the current radius.
last.layoutForPie(radius + label_gap_radius)
# Test for overlap. Takes two to tango.
if not item.itemsOverlap(last):
# They don't overlap, so we're done pushing out.
break
# The two adjacent labels overlap, so we keep looping and
# pushing them out until they don't.
# Bump the radius_notch.
radius += radius_notch
last = item
# Add in the extra radius.
radius += self.extra_radius
# Done calculating the radius.
self.radius = radius
ring_radius = self.ring_radius
outer_ring_radius = radius + ((len(pie_rings) - 1) * ring_radius)
# Calculate the bounding box of the items, as we lay them out.
max_x = -1000000
min_x = 1000000
max_y = -1000000
min_y = 1000000
if ring_count > 0:
# If there are any pie items, then make sure the bounding box
# encompasses the radius.
min_x = -outer_ring_radius
min_y = -outer_ring_radius
max_x = outer_ring_radius
max_y = outer_ring_radius
# Calculate the maximum radius (squared).
max_radius = 0
# Loop over the pie items, and calculate their bounding box
# and max_radius.
ring_index = 0
for pie_items in pie_rings:
for item in pie_items:
# Lay out the pie item at the current radius.
item.layoutForPie(
radius + (ring_index * ring_radius) + label_gap_radius)
# Calculate the corners of the item bounding box.
itw = item.width
ith = item.height
itx0 = item.x
ity0 = item.y
itx1 = itx0 + itw
ity1 = ity0 + ith
# Update the bounding box.
if itx0 < min_x:
min_x = itx0
if ity0 < min_y:
min_y = ity0
if itx1 > max_x:
max_x = itx1
if ity1 > max_y:
max_y = ity1
# Update the max_radius.
farx = max(abs(itx0), abs(itx1))
fary = max(abs(ity0), abs(ity1))
rad = (farx * farx) + (fary * fary)
if rad > self.max_radius:
max_radius = rad;
ring_index += 1
# Loop over the linear items, lay them out,
# and calculate their bounding box and max_radius.
# Calculate the max width of the north and south linear items.
max_item_width_north = 0
max_item_width_south = 0
for item in linear_items:
# Lay out the linear item.
item.layoutForLinear(
min_x,
min_y,
max_x,
max_y)
# Calculate the max vertical item width.
if abs(item.dx) < 0.01:
if item.dy < 0:
if item.width > max_item_width_north:
max_item_width_north = item.width
#print "max_item_width_north", max_item_width_north
else:
if item.width > max_item_width_south:
max_item_width_south = item.width
#print "max_item_width_south", max_item_width_south
# Calculate the corners of the item bounding box.
itw = item.width
ith = item.height
itx0 = item.x
ity0 = item.y
itx1 = itx0 + itw
ity1 = ity0 + ith
# Update the bounding box.
if itx0 < min_x:
min_x = itx0
if ity0 < min_y:
min_y = ity0
if itx1 > max_x:
max_x = itx1
if ity1 > max_y:
max_y = ity1
# Update the max_radius.
farx = max(abs(itx0), abs(itx1))
fary = max(abs(ity0), abs(ity1))
rad = (farx * farx) + (fary * fary)
if rad > max_radius:
max_radius = rad;
# Go over the linear items and fix the x and width of all vertical items.
for item in linear_items:
#print "item.dx", item.dx, item
if abs(item.dx) < 0.01:
w = 0
if item.dy < 0:
w = max_item_width_north
else:
w = max_item_width_south
item.width = w
item.x = int(math.floor(-0.5 * w) - 1)
# Calculate the max_radius.
max_radius = (
int(
math.floor(
0.95 +
math.sqrt(
max_radius)) +
self.margin_radius))
self.max_radius = max_radius
# Expand the bounding box by the border.
border = self.border
min_x -= border
min_y -= border
max_x += border
max_y += border
# Expand the bounding box to integers.
min_x = int(math.floor(min_x))
min_y = int(math.floor(min_y))
max_x = int(math.ceil(max_x))
max_y = int(math.ceil(max_y))
# Measure the header and footer.
self.measureHeader(context, pcontext, playout)
self.measureFooter(context, pcontext, playout)
# Position the header.
header_width = self.header_width
header_height = self.header_height
header_margin = self.header_margin
if self.header == None:
header_x = 0
header_y = 0
else:
# Position the header horizontally.
header_x = int(math.floor(header_width / -2))
# Make vertical space above the bounding box for the header.
header_y = min_y - header_height - self.header_gap
min_x = int(math.floor(min(min_x, header_x - header_margin)))
max_x = int(math.ceil(max(max_x, header_x + header_width + header_margin)))
min_y = int(math.floor(min(min_y, header_y - header_margin)))
max_y = int(math.ceil(max(max_y, header_y + header_height + header_margin)))
self.header_x = header_x
self.header_y = header_y
# Position the footer.
footer_width = self.footer_width
footer_height = self.footer_height
footer_margin = self.footer_margin
if self.footer == None:
footer_x = 0
footer_y = 0
else:
# Position the footer horizontally.
footer_x = int(math.floor(footer_width / -2))
# Make vertical space above the bounding box for the footer.
footer_y = max_y + self.footer_gap
min_x = int(math.floor(min(min_x, footer_x - footer_margin)))
max_x = int(math.ceil(max(max_x, footer_x + footer_width + footer_margin)))
min_y = int(math.floor(min(min_y, footer_y - footer_margin)))
max_y = int(math.ceil(max(max_y, footer_y + footer_height + footer_margin)))
# If (always or) fixed height footer, then expand it out horizontally.
#if True or self.footer_fixed_height:
# footer_width = max_x - min_x
# self.footer_width = footer_width
self.footer_x = footer_x
self.footer_y = footer_y
# Done calculating the bounding box.
self.min_x = min_x
self.min_y = min_y
self.max_x = max_x
self.max_y = max_y
# Set the pie menu center.
center_x = int(math.floor(0.5 + -min_x))
center_y = int(math.floor(0.5 + -min_y))
self.center_x = center_x
self.center_y = center_y
# Set the window position and size.
width = max_x - min_x
height = max_y - min_y
if self.pie_rings:
# If it's a pie menu, then center in the middle of the menu.
x = self.pin_x + min_x
y = self.pin_y + min_y
else:
# If it's a linear menu, then center on its header, or just below the mouse if no header.
x = self.pin_x - (width / 2)
y = self.pin_y - (header_height / 2)
self.x = x
self.y = y
self.width = width
self.height = height
# Offset the header.
self.header_x -= min_x
self.header_y -= min_y
# Offset the footer.
self.footer_x -= min_x
self.footer_y -= min_y
# Offset the items.
for item in visible_items:
x = item.x
y = item.y
x -= min_x
y -= min_y
item.x = x
item.y = y
item.final_x = x
item.final_y = y
# Done laying out the pie menu. (Whew!)
# FIXME: Just do this after popup?
self.shapeWindow()
def getRingClockwise(self, ring=0):
clockwise = self.clockwise
if type(clockwise) not in (type(()), type([])):
clockwise = [clockwise]
return clockwise[min(ring, len(clockwise) - 1)]
def getRingInitialAngle(self, ring=0):
initial_angle = self.initial_angle
if type(initial_angle) not in (type(()), type([])):
initial_angle = [initial_angle]
return initial_angle[min(ring, len(initial_angle) - 1)]
def measureHeader(self, context, pcontext, playout):
header = self.header
if header == None:
self.header_width = 0
self.header_height = 0
return
font = GetFont(self.header_font)
playout.set_font_description(font)
playout.set_markup(header)
width, height = playout.get_pixel_size()
header_padding = self.header_padding
width += 2 * header_padding
height += 2 * header_padding
self.header_width = width
self.header_height = height
def measureFooter(self, context, pcontext, playout):
footer = self.footer
footer_fixed_height = self.footer_fixed_height
if (footer == None) and (footer_fixed_height > 0):
self.footer_width = 0
self.footer_height = 0
return
if not footer:
footer = ''
font = GetFont(self.footer_font)
playout.set_font_description(font)
playout.set_markup(footer)
width, height = playout.get_pixel_size()
footer_padding = self.footer_padding
width += 2 * footer_padding
height += 2 * footer_padding
if footer_fixed_height > 0:
height = footer_fixed_height
self.footer_width = width
self.footer_height = height
def shapeWindow(self):
#print "SHAPEWINDOW", self.x, self.y, self.width, self.height
x = int(self.x)
y = int(self.y)
width = int(self.width)
height = int(self.height)
if ((x != self.win_x) or
(y != self.win_y)):
self.move(x, y)
self.win_x = x
self.win_y = y
if ((width != self.win_width) or
(height != self.win_height)):
self.resize(width, height)
self.win_width = width
self.win_height = height
def popup(self, pin_x, pin_y, pinned=False):
self.pin_x = int(math.floor(pin_x + 0.5))
self.pin_y = int(math.floor(pin_y + 0.5))
self.pinned = pinned
self.cur_item = -1
self.cur_ring_index = 0
self.item = None
if self.footer_descriptions:
self.footer = self.neutral_description
self.invalidate()
self.queue_draw()
self.show_all()
d = self.d
d.grab_add()
d.grab_focus()
#print "W", self.window
gtk.gdk.pointer_grab(
d.window,
True,
gtk.gdk.BUTTON_PRESS_MASK |
gtk.gdk.BUTTON_RELEASE_MASK |
gtk.gdk.ENTER_NOTIFY_MASK |
gtk.gdk.LEAVE_NOTIFY_MASK |
gtk.gdk.POINTER_MOTION_MASK)
gtk.gdk.keyboard_grab(
d.window,
owner_events=True)
def popdown(self):
self.d.grab_remove()
gtk.gdk.pointer_ungrab()
self.hide()
# TODO: restore focus?
def handle_expose(self, widget, event):
self.draw(widget, event)
return False
def draw(self, widget, event):
context = widget.window.cairo_create()
pcontext = widget.create_pango_context()
playout = pango.Layout(pcontext)
rect = self.get_allocation()
self.validate(rect, context, pcontext, playout)
context.rectangle(
event.area.x,
event.area.y,
event.area.width,
event.area.height)
context.clip()
self.drawBackground(rect, context, pcontext, playout)
self.drawHeader(rect, context, pcontext, playout)
self.drawFooter(rect, context, pcontext, playout)
cur_item = self.cur_item
if cur_item != -1:
cur_item_obj = self.visible_items[cur_item]
else:
cur_item_obj = None
cur_ring_index = self.cur_ring_index
draw_item_later = None
for item in self.items:
if item.index == cur_item:
draw_item_later = item
else:
# Don't draw labels inside of current ring.
if ((not item.linear) and
(item.ring_index < cur_ring_index)):
continue
item.draw(rect, context, pcontext, playout)
# Draw the current item last so it overlaps all other items.
if draw_item_later:
draw_item_later.draw(rect, context, pcontext, playout)
self.drawOverlay(rect, context, pcontext, playout)
def drawBackground(self, rect, context, pcontext, playout):
center_x = self.center_x
center_y = self.center_y
context.rectangle(rect)
context.clip()
fill_color = self.outside_fill_color
if fill_color:
context.rectangle(rect)
context.set_source_rgb(*fill_color)
context.fill()
cur_item = self.cur_item
if cur_item != -1:
cur_item_obj = self.visible_items[cur_item]
else:
cur_item_obj = None
cur_ring_index = self.cur_ring_index
pie_rings = self.pie_rings
ring_radius = self.ring_radius
neutral_radius = self.neutral_radius
radius = self.radius
if pie_rings and radius:
fill_color = self.background_fill_color
stroke_color = self.background_stroke_color
edge_stroke_color = self.edge_stroke_color
for ring_index in range(len(pie_rings) - 1, -1, -1):
pie_items = pie_rings[ring_index]
if ring_index == 0:
inner_radius = neutral_radius
outer_radius = radius
else:
inner_radius = radius + ((ring_index - 1) * ring_radius)
outer_radius = radius + ((ring_index) * ring_radius)
if ring_index != cur_ring_index:
if ring_index < cur_ring_index:
continue
if stroke_color:
context.arc(
center_x,
center_y,
radius + (ring_index * ring_radius),
0,
2 * math.pi)
context.set_source_rgb(*stroke_color)
context.stroke()
continue
########################################################################
# Draw a circle around the radius.
if fill_color or stroke_color or edge_stroke_color:
if fill_color:
context.arc(
center_x,
center_y,
outer_radius,
0,
2 * math.pi)
context.set_source_rgb(*fill_color)
context.fill()
if (edge_stroke_color and
(len(pie_items) > 1)):
for item in pie_items:
edge1dx = item.edge1dx
edge1dy = item.edge1dy
edge2dx = item.edge2dx
edge2dy = item.edge2dy
context.move_to(
center_x + (edge1dx * neutral_radius),
center_y + (edge1dy * neutral_radius))
context.line_to(
center_x + (edge1dx * outer_radius),
center_y + (edge1dy * outer_radius))
context.set_source_rgb(*edge_stroke_color)
context.stroke()
if stroke_color:
context.arc(
center_x,
center_y,
outer_radius,
0,
2 * math.pi)
context.set_source_rgb(*stroke_color)
context.stroke()
########################################################################
# Draw a circle around the neutral_radius,
# and hilite if no item selected.
if cur_item == -1:
fill_color = self.neutral_hilite_fill_color
stroke_color = self.neutral_hilite_stroke_color
else:
fill_color = self.neutral_lolite_fill_color
stroke_color = self.neutral_lolite_stroke_color
if fill_color or stroke_color:
if fill_color:
context.arc(
center_x,
center_y,
neutral_radius,
0,
2 * math.pi)
context.set_source_rgb(*fill_color)
if stroke_color:
context.fill_preserve()
else:
context.fill()
if stroke_color:
context.arc(
center_x,
center_y,
neutral_radius,
0,
2 * math.pi)
context.set_source_rgb(*stroke_color)
context.stroke()
########################################################################
# Draw the hilited slice.
slice_hilite_fill_color = self.slice_hilite_fill_color
slice_hilite_stroke_color = self.slice_hilite_stroke_color
if ((slice_hilite_fill_color or slice_hilite_stroke_color) and
cur_item_obj):
if cur_item_obj.linear:
pass # TODO: hilite linear items
else:
edge1angle = cur_item_obj.edge1angle
edge2angle = cur_item_obj.edge2angle
ring_clockwise = self.getRingClockwise(cur_ring_index)
if not ring_clockwise:
temp = edge1angle
edge1ang = edge2angle
edge2ang = temp
if ring_index == 0:
inner_radius = self.neutral_radius
outer_radius = self.radius
else:
inner_radius = self.radius + ((ring_index - 1) * ring_radius)
outer_radius = self.radius + ((ring_index) * ring_radius)
# If outer ring, then extend highlight to screen edge.
if len(pie_rings) == 1:
inner_radius = self.neutral_radius
outer_radius = self.radius + self.popout_radius
else:
if cur_ring_index == 0:
inner_radius = self.neutral_radius
outer_radius = self.radius + ring_radius
else:
inner_radius = self.radius + (ring_radius * cur_ring_index)
outer_radius = self.radius + (ring_radius * (cur_ring_index + 1))
if cur_ring_index == (len(pie_rings) - 1):
outer_radius += self.popout_radius
context.arc(
center_x,
center_y,
outer_radius,
edge1angle,
edge2angle)
context.arc_negative(
center_x,
center_y,
inner_radius,
edge2angle,
edge1angle)
context.close_path()
if fill_color:
context.set_source_rgb(*slice_hilite_fill_color)
#context.set_source_rgb(*slice_hilite_fill_color + (0.5,))
if stroke_color:
context.fill_preserve()
else:
context.fill()
if stroke_color:
context.set_source_rgb(*slice_hilite_stroke_color)
#context.set_source_rgb(*slice_hilite_stroke_color + (0.5,))
context.stroke()
def drawHeader(self, rect, context, pcontext, playout):
header = self.header
if header == None:
return
x = self.header_x
y = self.header_y
width = self.header_width
height = self.header_height
context.rectangle(x, y, width, height)
fill_color = self.header_fill_color
if fill_color:
context.rectangle(x, y, width, height)
context.set_source_rgb(*fill_color)
context.fill()
stroke_color = self.header_stroke_color
if stroke_color:
context.rectangle(x, y, width, height)
context.set_source_rgb(*stroke_color)
context.stroke()
text_color = self.header_text_color
if text_color:
context.set_source_rgb(*text_color)
font = GetFont(self.header_font)
playout.set_font_description(font)
playout.set_markup(header)
header_padding = self.header_padding
context.move_to(
x + header_padding,
y + header_padding)
context.show_layout(playout)
def drawFooter(self, rect, context, pcontext, playout):
footer = self.footer
if footer == None:
return
x = self.footer_x
y = self.footer_y
width = self.footer_width
height = self.footer_height
context.rectangle(x, y, width, height)
fill_color = self.footer_fill_color
if fill_color:
context.rectangle(x, y, width, height)
context.set_source_rgb(*fill_color)
context.fill()
stroke_color = self.footer_stroke_color
if stroke_color:
context.rectangle(x, y, width, height)
context.set_source_rgb(*stroke_color)
context.stroke()
text_color = self.footer_text_color
if text_color:
context.set_source_rgb(*text_color)
font = GetFont(self.footer_font)
playout.set_font_description(font)
playout.set_markup(footer)
footer_padding = self.footer_padding
context.move_to(
x + footer_padding,
y + footer_padding)
context.show_layout(playout)
def drawOverlay(self, rect, context, pcontext, playout):
stroke_color = self.outside_stroke_color
if stroke_color:
context.rectangle(rect)
context.set_source_rgb(*stroke_color)
context.stroke()
def setFooter(self, footer):
self.footer = footer
self.layoutSelf()
def trackMouseMove(self, cx, cy):
cur_x = self.cur_x
cur_y = self.cur_y
if ((cx == cur_x) and
(cy == cur_y)):
return
self.last_x = cur_x
self.lasy_y = cur_y
self.cur_x = cx
self.cur_y = cy
# Track the selection based on the cursor offset from the menu center.
two_pi = 2 * math.pi
dx = cx - self.center_x
dy = cy - self.center_y
# Add in and clear out any virtual mouse motion.
dx += self.mouse_delta_x
dy += self.mouse_delta_y
self.mouse_delta_x = 0
self.mouse_delta_y = 0
self.dx = dx
self.dy = dy
self.distance = (
math.sqrt(
(dx * dx) +
(dy * dy)))
if self.distance <= 0:
self.direction = 0
else:
self.direction = NormalizeAngleRad(math.atan2(-dy, dx))
visible_items = self.visible_items
item_count = len(visible_items)
self.handleMotion()
# If there aren't any items, there's nothing to do.
if item_count == 0:
return
new_item = -1
last_item = self.cur_item
self.last_item = last_item
#print "trackMouseMove", "cx", cx, "cy", cy, "dx", dx, "dy", dy
pie_rings = self.pie_rings
cur_ring_index = (
max(0,
min(len(pie_rings) - 1,
int(math.floor(
(self.distance - self.radius) /
self.ring_radius)))))
self.cur_ring_index = cur_ring_index
cur_item_entered = -1
for i in range(0, item_count):
it = self.items[i]
# Ignore the item if
# there is more than one ring,
# the item is a pie item,
# and it's not in the currently selected ring.
if ((len(pie_rings) > 1) and
(not it.linear) and
(it.ring_index != cur_ring_index)):
continue
x0 = it.final_x
y0 = it.final_y
x1 = x0 + it.width
y1 = y0 + it.height
#print "trackMouseMove ITEM", "i", i, "cx", cx, "cy", cy, "x0", x0, "y0", y0, "x1", x1, "y1", y1
if ((cx >= x0) and (cx < x1) and
(cy >= y0) and (cy < y1)):
#print "ENTERED", i
cur_item_entered = i
break
if cur_item_entered != -1:
new_item = cur_item_entered
else:
if (pie_rings and
(self.distance > self.neutral_radius)):
pie_items = pie_rings[cur_ring_index]
pie_item_count = len(pie_items)
if pie_item_count == 1:
new_item = pie_items[0].index
else:
if cur_item_entered != -1:
new_item = cur_item_entered
else:
if pie_item_count > 0:
twist = math.pi / pie_item_count
ring_clockwise = self.getRingClockwise(cur_ring_index)
ring_initial_angle = self.getRingInitialAngle(cur_ring_index)
ang = DegToRad(ring_initial_angle)
if ring_clockwise:
ang = ang - self.direction + twist
else:
ang = ang + self.direction - twist
ang = NormalizeAngleRad(ang)
new_pie_item = int(math.floor((ang / two_pi) * pie_item_count))
if new_pie_item < 0:
new_pie_item = 0
elif new_pie_item >= pie_item_count:
new_pie_item = pie_item_count - 1
new_item = pie_items[new_pie_item].index
# Now we've figured out the selected new_item,
# so update the display if necessary.
if new_item >= len(self.visible_items):
new_item = item_count - 1
if new_item != last_item:
self.cur_item = new_item
item = None
for i in range(0, item_count):
it = self.items[i]
hilited = (i == new_item)
if hilited:
item = it
break
self.item = item
if self.footer_descriptions:
footer = None
if item == None:
footer = self.neutral_description
else:
footer = item.description
self.setFooter(footer)
now = time.time()
if last_item != -1:
it = self.items[last_item]
if it.enter_time == 0:
it.enter_time = now
it.exit_time = now
elapsed = now - it.enter_time
it.total_time += elapsed
if new_item != -1:
self.items[new_item].enter_time = now
self.queue_draw()
# TODO: Signal onchanged event.
if new_item != -1:
item = self.items[new_item]
item.handleMotion()
def handleMotion(self):
# TODO: notify menu about mouse motion
pass
def trackMouseDown(self):
pass
def trackMouseUp(self):
if ((self.cur_item == -1) and
(not self.pinned)):
self.pinned = True
return
self.popdown()
self.doAction()
def doAction(self):
cur_item = self.cur_item
if cur_item == -1:
return
item = self.visible_items[cur_item]
print "DOACTION", self, self.cur_item, item, item.label
sub_pie = item.sub_pie
if sub_pie:
x = self.cur_x + self.x
y = self.cur_y + self.y
sub_pie.popup(x, y, True)
def handle_show(self, widget):
#print "handle_show", self, widget
pass
def handle_motion_notify_event(self, widget, event, *args):
#print "handle_motion_notify_event", self, widget, event, args
if (hasattr(event, 'is_hint') and
event.is_hint):
x, y, state = event.window.get_pointer()
else:
x = event.x
y = event.y
state = event.state
self.trackMouseMove(x, y)
def handle_button_press_event(self, widget, event, *args):
#print "handle_button_press_event", self, widget, event, args
self.handle_motion_notify_event(widget, event, *args)
self.trackMouseDown()
def handle_button_release_event(self, widget, event, *args):
#print "handle_button_release_event", self, widget, event, args
self.handle_motion_notify_event(widget, event, *args)
self.trackMouseUp()
def handle_proximity_in_event(self, widget, event, *args):
#print "handle_proximity_in_event", self, widget, event, args
self.handle_motion_notify_event(widget, event, *args)
def handle_proximity_out_event(self, widget, event, *args):
#print "handle_proximity_out_event", self, widget, event, args
self.handle_motion_notify_event(widget, event, *args)
def handle_grab_notify(self, widget, event, *args):
#print "handle_grab_notify", self, widget, event, args
pass
def handle_grab_broken_event(self, widget, event, *args):
#print "handle_grab_broken_event", self, widget, event, args
pass
def handle_key_press_event(self, widget, event, *args):
print "handle_key_press_event", self, widget, event, args
print help(event)
pass
def handle_key_release_event(self, widget, event, *args):
print "handle_key_release_event", self, widget, event, args
pass
########################################################################
class PieMenuDrawingArea(gtk.DrawingArea):
def __init__(self):
gtk.DrawingArea.__init__(self)
def handle_expose(self, widget, event):
self.parent.handle_expose(widget, event)
return False
########################################################################
class PieMenuTarget(gtk.Button):
def __init__(
self,
label='',
**args):
gtk.Button.__init__(self, label=label, **args)
self.connect("button_press_event", self.handle_button_press_event)
self.connect("button_release_event", self.handle_button_release_event)
#print "INIT"
self.set_events(
gtk.gdk.EXPOSURE_MASK |
gtk.gdk.BUTTON_PRESS_MASK |
gtk.gdk.BUTTON_RELEASE_MASK)
self.pie = None
def handle_button_press_event(self, widget, event):
#print "PRESS"
pie = self.pie
if not pie:
return False
win_x, win_y, state = event.window.get_pointer()
#print "WIN", win_x, win_y
x, y = event.get_root_coords()
#print "ROOT", x, y
pie.popup(x, y, False)
return False
def handle_button_release_event(self, widget, event):
return False
def setPie(self, pie):
self.pie = pie
########################################################################
class LinearMenu(PieMenu):
def __init__(
self,
**args):
PieMenu.__init__(
self,
max_pie_items=0,
**args)
########################################################################
class PurePieMenu(PieMenu):
def __init__(
self,
**args):
PieMenu.__init__(
self,
max_pie_items=1.0e+6,
**args)
########################################################################
class DozenPieMenu(PieMenu):
def __init__(
self,
**args):
PieMenu.__init__(
self,
max_pie_items=12,
**args)
########################################################################
class DonutPieMenu(PieMenu):
def __init__(
self,
**args):
PieMenu.__init__(
self,
neutral_radius=60,
min_radius=180,
label_gap_radius=-85,
**args)
########################################################################
def main():
########################################################################
# Make Window and PieMenuTarget.
win = gtk.Window()
win.set_title("Pie Menus, by Don Hopkins")
target = PieMenuTarget(label="Pie Menus")
win.add(target)
########################################################################
# Make diag_switch_pie.
diag_switch_pie = PieMenu(
header="Switch",
initial_angle=45,
min_radius=40)
diag_switch_pie.add_item(
PieItem(
label='On'))
diag_switch_pie.add_item(
PieItem(
label='Off'))
########################################################################
# Make compass_pie with extra linear overflow items.
compass_pie = PieMenu(
header="Compass",
footer="This menu has eight pie items,\nplus three overflow linear items.")
for label in (
'North', 'NE',
'East', 'SE',
'South', 'SW',
'West', 'NW',
'Linear', 'Overflow', 'Items',
):
compass_pie.add_item(
PieItem(
label=label))
########################################################################
# Make clock_pie with clock face.
labels = (
'12:', '01:', '02:', '03:', '04:', '05:', '06:', '07:', '08:', '09:', '10:', '11:',
'24:', '13:', '14:', '15:', '16:', '17:', '18:', '19:', '20:', '21:', '22:', '23:',
':00', ':05', ':10', ':15', ':20', ':25', ':30', ':35', ':40', ':45', ':50', ':55'
)
clock_pie = PieMenu(
header="Clock",
max_pie_items=[12, 12, 12],
fixed_radius=70,
ring_radius=40)
for label in labels:
clock_pie.add_item(
PieItem(
label=label,
label_font='Helvetica 12',
lolite_fill_color=None,
lolite_stroke_color=None))
########################################################################
# Make two_ringed_pie with rings of items.
two_ringed_pie = PieMenu(
header="Two Ringed",
max_pie_items=[4, 8],
min_radius=20,
ring_radius=50)
labels = (
'Top', 'Next', 'Bottom', 'Back',
'North', 'NE', 'East', 'SE', 'South', 'SW', 'West', 'NW',
'Linear', 'Overflow', 'Items'
)
for label in labels:
two_ringed_pie.add_item(
PieItem(
label=label,
label_font='Helvetica 12',
lolite_fill_color=None,
lolite_stroke_color=None))
########################################################################
# Make three_ringed_pie with rings of items.
three_ringed_pie = PieMenu(
header="Three Ringed",
max_pie_items=[4, 8, 12],
min_radius=20,
ring_radius=50)
labels = (
'Top', 'Next', 'Bottom', 'Back',
'North', 'NE', 'East', 'SE', 'South', 'SW', 'West', 'NW',
'12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11',
'Linear', 'Overflow', 'Items'
)
for label in labels:
three_ringed_pie.add_item(
PieItem(
label=label,
label_font='Helvetica 12',
lolite_fill_color=None,
lolite_stroke_color=None))
########################################################################
# Make four_ringed_pie with rings of items.
labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,.?!/@#$%^&*()_-+=|\\`~<>[]{}'
four_ringed_pie = PieMenu(
header="Four Ringed",
max_pie_items=[4, 8, 24, 1000],
min_radius=20)
for label in labels:
# Quote the label since we're using markup.
label = label.replace('&', '&').replace('<', '<').replace('>', '>')
four_ringed_pie.add_item(
PieItem(
label=label,
label_font='Helvetica 10',
lolite_fill_color=None,
lolite_stroke_color=None))
########################################################################
# MAKE months_liner, months_pie.
months = (
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December')
months_linear = LinearMenu(
header="Months (Linear)")
months_pie = DozenPieMenu(
header="Months (Pie)")
for label in months:
months_linear.add_item(
PieItem(
label=label))
months_pie.add_item(
PieItem(
label=label))
########################################################################
# Make top level root_pie, with items with submenus.
root_pie = PieMenu(
min_radius=40,
header="Pie Menus",
neutral_description="This pie menu has submenus\nwith various pie menu demos!")
root_pie.add_item(
PieItem(
#icon='/home/simcity/sugar/sugar-jhbuild/source/pycairo/examples/cairo_snippets/data/romedalen.png',
icon_side='top',
label="Compass...",
description="Eight item compass pie menu,\nwith three overflow linear items.",
sub_pie=compass_pie))
root_pie.add_item(
PieItem(
label="Switch...",
description="Two item diagonal pie menu,\nalong same axis as submenu item.",
sub_pie=diag_switch_pie))
root_pie.add_item(
PieItem(
#icon='/home/simcity/sugar/sugar-jhbuild/source/pycairo/examples/cairo_snippets/data/romedalen.png',
icon_side='right',
label="Clock...",
description="Three ringed pie menu clock,\nwith 24 hours plus minutes.",
sub_pie=clock_pie))
root_pie.add_item(
PieItem(
label="Four Ringed...",
description="Four ringed pie menu,\nwith a whole bunch of items.",
sub_pie=four_ringed_pie))
root_pie.add_item(
PieItem(
#icon='/home/simcity/sugar/sugar-jhbuild/source/pycairo/examples/cairo_snippets/data/romedalen.png',
icon_side='bottom',
label="Three Ringed...",
description="Three ringed pie menu,\nwith lots of items.",
sub_pie=three_ringed_pie))
root_pie.add_item(
PieItem(
label="Two Ringed...",
description="Two ringed pie menu,\nwith a dozen items.",
sub_pie=two_ringed_pie))
root_pie.add_item(
PieItem(
#icon='/home/simcity/sugar/sugar-jhbuild/source/pycairo/examples/cairo_snippets/data/romedalen.png',
icon_side='left',
label="Pie Months...",
description="Twelve month pie menu.",
sub_pie=months_pie))
root_pie.add_item(
PieItem(
label="Linear Months...",
description="Twelve month linear menu.",
sub_pie=months_linear))
########################################################################
target.setPie(root_pie)
#target.setPie(compass_pie)
win.resize(300, 300)
win.show_all()
gtk.main()
########################################################################
if __name__ == '__main__':
main()
########################################################################