Consoliating background drawing for non-active slice and removed clipping which I...
[ejpi] / src / libraries / gtkpie.py
1 #!/usr/bin/env python
2
3 """
4 @todo Handle sizing in a better manner http://www.gtkmm.org/docs/gtkmm-2.4/docs/tutorial/html/sec-custom-widgets.html
5 """
6
7
8 from __future__ import division
9
10 import os
11 import weakref
12 import math
13 import copy
14 import warnings
15
16 import gobject
17 import gtk
18 import cairo
19 import pango
20
21 try:
22         import rsvg
23 except ImportError:
24         rsvg = None
25
26
27 def deg_to_rad(deg):
28         return (2 * math.pi * deg) / 360.0
29
30
31 def rad_to_deg(rad):
32         return (360.0 * rad) / (2 * math.pi)
33
34
35 def normalize_radian_angle(radAng):
36         """
37         Restricts @param radAng to the range [0..2pi)
38         """
39         twoPi = 2 * math.pi
40
41         while radAng < 0:
42                 radAng += twoPi
43         while twoPi <= radAng:
44                 radAng -= twoPi
45
46         return radAng
47
48
49 def delta_to_rtheta(dx, dy):
50         distance = math.sqrt(dx**2 + dy**2)
51
52         angleInRads = math.atan2(-dy, dx)
53         if angleInRads < 0:
54                 angleInRads = 2*math.pi + angleInRads
55         return distance, angleInRads
56
57
58 class FontCache(object):
59
60         def __init__(self):
61                 self.__fontCache = {}
62
63         def get_font(self, s):
64                 if s in self.__fontCache:
65                         return self.__fontCache[s]
66
67                 descr = pango.FontDescription(s)
68                 self.__fontCache[s] = descr
69
70                 return descr
71
72
73 FONTS = FontCache()
74
75
76 class ImageCache(object):
77
78         def __init__(self):
79                 self.__imageCache = {}
80                 self.__imagePaths = [
81                         os.path.join(os.path.dirname(__file__), "images"),
82                 ]
83
84         def add_path(self, path):
85                 self.__imagePaths.append(path)
86
87         def get_image(self, s):
88                 if s in self.__imageCache:
89                         return self.__imageCache[s]
90
91                 image = None
92
93                 if s.lower().endswith(".png"):
94                         for path in self.__imagePaths:
95                                 imagePath = os.path.join(path, s)
96                                 try:
97                                         image = cairo.ImageSurface.create_from_png(imagePath)
98                                         break
99                                 except:
100                                         warnings.warn("Unable to load image %s" % imagePath)
101                 elif s.lower().endswith(".svg") and rsvg is not None:
102                         for path in self.__imagePaths:
103                                 imagePath = os.path.join(path, s)
104                                 try:
105                                         image = rsvg.Handle(file=imagePath)
106                                 except:
107                                         warnings.warn("Unable to load image %s" % imagePath)
108                 else:
109                         print "Don't know how to load image file type:", s
110
111                 if image is not None:
112                         self.__imageCache[s] = image
113
114                 return image
115
116
117 IMAGES = ImageCache()
118
119
120 def convert_color(gtkColor):
121         r = gtkColor.red / 65535
122         g = gtkColor.green / 65535
123         b = gtkColor.blue / 65535
124         return r, g, b
125
126
127 def generate_pie_style(widget):
128         """
129         @bug This seems to always pick the same colors irregardless of the theme
130         """
131         # GTK states:
132         # * gtk.STATE_NORMAL - The state of a sensitive widget that is not active and does not have the focus
133         # * gtk.STATE_ACTIVE - The state of a sensitive widget when it is active e.g. a button that is pressed but not yet released
134         # * gtk.STATE_PRELIGHT - The state of a sensitive widget that has the focus e.g. a button that has the mouse pointer over it.
135         # * gtk.STATE_SELECTED - The state of a widget that is selected e.g. selected text in a gtk.Entry widget
136         # * gtk.STATE_INSENSITIVE - The state of a widget that is insensitive and will not respond to any events e.g. cannot be activated, selected or prelit.
137
138         gtkStyle = widget.get_style()
139         sliceStyle = dict(
140                 (gtkStyleState, {
141                         "text": convert_color(gtkStyle.text[gtkStyleState]),
142                         "fill": convert_color(gtkStyle.bg[gtkStyleState]),
143                         "stroke": None,
144                 })
145                 for gtkStyleState in (
146                         gtk.STATE_NORMAL, gtk.STATE_ACTIVE, gtk.STATE_PRELIGHT, gtk.STATE_SELECTED, gtk.STATE_INSENSITIVE
147                 )
148         )
149
150         return sliceStyle
151
152
153 class PieSlice(object):
154
155         SLICE_CENTER = 0
156         SLICE_EAST = 1
157         SLICE_SOUTH_EAST = 2
158         SLICE_SOUTH = 3
159         SLICE_SOUTH_WEST = 4
160         SLICE_WEST = 5
161         SLICE_NORTH_WEST = 6
162         SLICE_NORTH = 7
163         SLICE_NORTH_EAST = 8
164
165         MAX_ANGULAR_SLICES = 8
166
167         SLICE_DIRECTIONS = [
168                 SLICE_CENTER,
169                 SLICE_EAST,
170                 SLICE_SOUTH_EAST,
171                 SLICE_SOUTH,
172                 SLICE_SOUTH_WEST,
173                 SLICE_WEST,
174                 SLICE_NORTH_WEST,
175                 SLICE_NORTH,
176                 SLICE_NORTH_EAST,
177         ]
178
179         SLICE_DIRECTION_NAMES = [
180                 "CENTER",
181                 "EAST",
182                 "SOUTH_EAST",
183                 "SOUTH",
184                 "SOUTH_WEST",
185                 "WEST",
186                 "NORTH_WEST",
187                 "NORTH",
188                 "NORTH_EAST",
189         ]
190
191         def __init__(self, handler = (lambda p, s, d: None)):
192                 self._direction = self.SLICE_CENTER
193                 self._pie = None
194                 self._style = None
195                 self._handler = handler
196
197         def menu_init(self, pie, direction):
198                 self._direction = direction
199                 self._pie = weakref.ref(pie)
200                 self._style = pie.sliceStyle
201
202         def calculate_minimum_radius(self, context, textLayout):
203                 return 0
204
205         def draw_fg(self, styleState, isSelected, context, textLayout):
206                 if isSelected:
207                         styleState = gtk.STATE_ACTIVE
208                 self._draw_fg(styleState, context, textLayout)
209
210         def draw_bg(self, styleState, isSelected, context, textLayout):
211                 if isSelected:
212                         styleState = gtk.STATE_ACTIVE
213                 self._draw_bg(styleState, context, textLayout)
214
215         def _draw_fg(self, styleState, context, textLayout):
216                 pass
217
218         def _draw_bg(self, styleState, context, textLayout):
219                 centerPosition = self._pie().centerPosition
220                 radius = max(self._pie().radius, self.calculate_minimum_radius(context, textLayout))
221                 outerRadius = self._pie().outerRadius
222
223                 fillColor = self._style[styleState]["fill"]
224                 if not fillColor:
225                         return
226
227                 if self._direction == self.SLICE_CENTER:
228                         context.arc(
229                                 centerPosition[0],
230                                 centerPosition[1],
231                                 radius,
232                                 0,
233                                 2 * math.pi
234                         )
235
236                         context.set_source_rgb(*fillColor)
237                         context.fill()
238                 else:
239                         sliceCenterAngle = self.quadrant_to_theta(self._direction)
240                         sliceArcWidth = 2*math.pi / self.MAX_ANGULAR_SLICES
241                         sliceStartAngle = sliceCenterAngle - sliceArcWidth/2
242                         sliceEndAngle = sliceCenterAngle + sliceArcWidth/2
243
244                         context.arc(
245                                 centerPosition[0],
246                                 centerPosition[1],
247                                 radius,
248                                 sliceStartAngle,
249                                 sliceEndAngle,
250                         )
251                         context.arc_negative(
252                                 centerPosition[0],
253                                 centerPosition[1],
254                                 outerRadius,
255                                 sliceEndAngle,
256                                 sliceStartAngle,
257                         )
258                         context.close_path()
259
260                         context.set_source_rgb(*fillColor)
261                         context.fill()
262
263         def activate(self):
264                 self._handler(self._pie(), self, self._direction)
265
266         @classmethod
267         def rtheta_to_quadrant(cls, distance, angleInRads, innerRadius):
268                 if distance < innerRadius:
269                         quadrant = 0
270                 else:
271                         gradians = angleInRads / (2*math.pi)
272                         preciseQuadrant = gradians * cls.MAX_ANGULAR_SLICES + cls.MAX_ANGULAR_SLICES / (2 * 2*math.pi)
273                         quadrantWithWrap = int(preciseQuadrant)
274                         quadrant = quadrantWithWrap % cls.MAX_ANGULAR_SLICES
275                         quadrant += 1
276
277                 return quadrant
278
279         @classmethod
280         def quadrant_to_theta(cls, quadrant):
281                 assert quadrant != 0
282                 quadrant -= 1
283
284                 gradians = quadrant / cls.MAX_ANGULAR_SLICES
285                 radians = gradians * 2*math.pi
286
287                 return radians
288
289
290 class NullPieSlice(PieSlice):
291
292         def draw_bg(self, styleState, isSelected, context, textLayout):
293                 super(NullPieSlice, self).draw_bg(styleState, False, context, textLayout)
294
295
296 class LabelPieSlice(PieSlice):
297
298         def _align_label(self, labelWidth, labelHeight):
299                 centerPosition = self._pie().centerPosition
300                 if self._direction == PieSlice.SLICE_CENTER:
301                         labelX = centerPosition[0] - labelWidth/2
302                         labelY = centerPosition[1] - labelHeight/2
303                 else:
304                         if self._direction in (PieSlice.SLICE_NORTH_WEST, PieSlice.SLICE_WEST, PieSlice.SLICE_SOUTH_WEST):
305                                 outerX = 0
306                                 labelX = outerX
307                         elif self._direction in (PieSlice.SLICE_SOUTH, PieSlice.SLICE_NORTH):
308                                 outerX = centerPosition[0]
309                                 labelX = outerX - labelWidth/2
310                         elif self._direction in (PieSlice.SLICE_NORTH_EAST, PieSlice.SLICE_EAST, PieSlice.SLICE_SOUTH_EAST):
311                                 outerX = centerPosition[0] * 2
312                                 labelX = outerX - labelWidth
313                         else:
314                                 assert False, "Direction %d is incorrect" % self._direction
315
316                         if self._direction in (PieSlice.SLICE_NORTH_EAST, PieSlice.SLICE_NORTH, PieSlice.SLICE_NORTH_WEST):
317                                 outerY = 0
318                                 labelY = outerY
319                         elif self._direction in (PieSlice.SLICE_EAST, PieSlice.SLICE_WEST):
320                                 outerY = centerPosition[1]
321                                 labelY = outerY - labelHeight/2
322                         elif self._direction in (PieSlice.SLICE_SOUTH_EAST, PieSlice.SLICE_SOUTH, PieSlice.SLICE_SOUTH_WEST):
323                                 outerY = centerPosition[1] * 2
324                                 labelY = outerY - labelHeight
325                         else:
326                                 assert False, "Direction %d is incorrect" % self._direction
327
328                 return int(labelX), int(labelY)
329
330
331 class TextLabelPieSlice(LabelPieSlice):
332
333         def __init__(self, text, fontName = 'Helvetica 12', handler = (lambda p, s, d: None)):
334                 super(TextLabelPieSlice, self).__init__(handler = handler)
335                 self.__text = text
336                 self.__fontName = fontName
337
338         def calculate_minimum_radius(self, context, textLayout):
339                 font = FONTS.get_font(self.__fontName)
340                 textLayout.set_font_description(font)
341                 textLayout.set_markup(self.__text)
342
343                 labelWidth, labelHeight = textLayout.get_pixel_size()
344                 return min(labelWidth, labelHeight) / 2
345
346         def _draw_fg(self, styleState, context, textLayout):
347                 super(TextLabelPieSlice, self)._draw_fg(styleState, context, textLayout)
348
349                 textColor = self._style[styleState]["text"]
350                 font = FONTS.get_font(self.__fontName)
351
352                 context.set_source_rgb(*textColor)
353                 textLayout.set_font_description(font)
354                 textLayout.set_markup(self.__text)
355                 labelWidth, labelHeight = textLayout.get_pixel_size()
356                 labelX, labelY = self._align_label(labelWidth, labelHeight)
357
358                 context.move_to(
359                         labelX,
360                         labelY,
361                 )
362
363                 context.show_layout(textLayout)
364
365
366 class ImageLabelPieSlice(LabelPieSlice):
367
368         def __init__(self, imagePath, handler = (lambda p, s, d: None)):
369                 super(ImageLabelPieSlice, self).__init__(handler = handler)
370                 self.__imagePath = imagePath
371
372         def calculate_minimum_radius(self, context, textLayout):
373                 image = IMAGES.get_image(self.__imagePath)
374                 if image is None:
375                         return
376                 labelWidth, labelHeight = image.get_width(), image.get_height()
377                 return min(labelWidth, labelHeight) / 2
378
379         def _draw_fg(self, styleState, context, textLayout):
380                 super(ImageLabelPieSlice, self)._draw_fg(styleState, context, textLayout)
381
382                 image = IMAGES.get_image(self.__imagePath)
383                 if image is None:
384                         return
385
386                 labelWidth, labelHeight = image.get_width(), image.get_height()
387                 labelX, labelY = self._align_label(labelWidth, labelHeight)
388
389                 context.set_source_surface(
390                         image,
391                         labelX,
392                         labelY,
393                 )
394
395                 context.paint()
396
397
398 class PieMenu(gtk.DrawingArea):
399
400         def __init__(self, style = None, **kwds):
401                 super(PieMenu, self).__init__()
402
403                 self.sliceStyle = style
404                 self.centerPosition = 0, 0
405                 self.radius = 20
406                 self.outerRadius = self.radius * 2
407
408                 self.connect("expose_event", self._on_expose)
409                 self.connect("motion_notify_event", self._on_motion_notify)
410                 self.connect("leave_notify_event", self._on_leave_notify)
411                 self.connect("proximity_in_event", self._on_motion_notify)
412                 self.connect("proximity_out_event", self._on_leave_notify)
413                 self.connect("button_press_event", self._on_button_press)
414                 self.connect("button_release_event", self._on_button_release)
415
416                 self.set_events(
417                         gtk.gdk.EXPOSURE_MASK |
418                         gtk.gdk.POINTER_MOTION_MASK |
419                         gtk.gdk.POINTER_MOTION_HINT_MASK |
420                         gtk.gdk.BUTTON_MOTION_MASK |
421                         gtk.gdk.BUTTON_PRESS_MASK |
422                         gtk.gdk.BUTTON_RELEASE_MASK |
423                         gtk.gdk.PROXIMITY_IN_MASK |
424                         gtk.gdk.PROXIMITY_OUT_MASK |
425                         gtk.gdk.LEAVE_NOTIFY_MASK
426                 )
427
428                 self.__activeSlice = None
429                 self.__slices = {}
430                 for direction in PieSlice.SLICE_DIRECTIONS:
431                         self.add_slice(NullPieSlice(), direction)
432
433                 self.__clickPosition = 0, 0
434                 self.__styleState = gtk.STATE_NORMAL
435
436         def add_slice(self, slice, direction):
437                 assert direction in PieSlice.SLICE_DIRECTIONS
438
439                 slice.menu_init(self, direction)
440                 self.__slices[direction] = slice
441
442                 if direction == PieSlice.SLICE_CENTER:
443                         self.__activeSlice = self.__slices[PieSlice.SLICE_CENTER]
444
445         def __update_state(self, mousePosition):
446                 rect = self.get_allocation()
447                 newStyleState = self.__styleState
448
449                 if (
450                         0 <= mousePosition[0] and mousePosition[1] < rect.width and
451                         0 <= mousePosition[1] and mousePosition[1] < rect.height
452                 ):
453                         if self.__clickPosition == (0, 0):
454                                 newStyleState = gtk.STATE_PRELIGHT
455                 else:
456                         if self.__clickPosition != (0, 0):
457                                 newStyleState = gtk.STATE_PRELIGHT
458
459                 if newStyleState != self.__styleState:
460                         self.__generate_draw_event()
461                         self.__styleState = newStyleState
462
463         def __process_mouse_position(self, mousePosition):
464                 self.__update_state(mousePosition)
465                 if self.__clickPosition == (0, 0):
466                         return
467
468                 delta = (
469                         mousePosition[0] - self.centerPosition[0],
470                         - (mousePosition[1] - self.centerPosition[1])
471                 )
472                 distance, angleInRads = delta_to_rtheta(delta[0], delta[1])
473                 quadrant = PieSlice.rtheta_to_quadrant(distance, angleInRads, self.radius)
474                 self.__select_slice(self.__slices[quadrant])
475
476         def __select_slice(self, newSlice):
477                 if newSlice is self.__activeSlice:
478                         return
479
480                 oldSlice = self.__activeSlice
481                 self.__activeSlice = newSlice
482                 self.__generate_draw_event()
483
484         def __generate_draw_event(self):
485                 if self.window is None:
486                         return
487
488                 rect = self.get_allocation()
489                 self.window.invalidate_rect(rect, True)
490
491         def _on_expose(self, widget, event):
492                 # @bug Not getting all of the invalidation events needed on Hildon
493                 # @bug Not highlighting proper slice besides center on Hildon
494                 cairoContext = self.window.cairo_create()
495                 pangoContext = self.create_pango_context()
496                 textLayout = pango.Layout(pangoContext)
497
498                 rect = self.get_allocation()
499
500                 self.centerPosition = event.area.x + event.area.width / 2, event.area.y + event.area.height / 2
501                 self.radius = max(rect.width, rect.width) / 3 / 2
502                 self.outerRadius = max(rect.width, rect.height)
503
504                 # Draw Background
505                 cairoContext.rectangle(
506                         event.area.x,
507                         event.area.y,
508                         event.area.width,
509                         event.area.height,
510                 )
511                 cairoContext.set_source_rgb(*self.sliceStyle[self.__styleState]["fill"])
512                 cairoContext.fill()
513
514                 isSelected = self.__clickPosition != (0, 0)
515                 self.__activeSlice.draw_bg(self.__styleState, isSelected, cairoContext, textLayout)
516
517                 # Draw Foreground
518                 for slice in self.__slices.itervalues():
519                         isSelected = (slice is self.__activeSlice)
520                         if not isSelected:
521                                 slice.draw_fg(self.__styleState, isSelected, cairoContext, textLayout)
522
523                 isSelected = self.__clickPosition != (0, 0)
524                 self.__activeSlice.draw_fg(self.__styleState, isSelected, cairoContext, textLayout)
525
526         def _on_leave_notify(self, widget, event):
527                 newStyleState = gtk.STATE_NORMAL
528                 if newStyleState != self.__styleState:
529                         self.__generate_draw_event()
530                         self.__styleState = newStyleState
531
532                 mousePosition = event.get_coords()
533                 self.__process_mouse_position(mousePosition)
534
535         def _on_motion_notify(self, widget, event):
536                 mousePosition = event.get_coords()
537                 self.__process_mouse_position(mousePosition)
538
539         def _on_button_press(self, widget, event):
540                 self.__clickPosition = event.get_coords()
541
542                 self._on_motion_notify(widget, event)
543                 self.__generate_draw_event()
544
545         def _on_button_release(self, widget, event):
546                 self._on_motion_notify(widget, event)
547
548                 self.__activeSlice.activate()
549                 self.__activeSlice = self.__slices[PieSlice.SLICE_CENTER]
550                 self.__clickPosition = 0, 0
551
552                 self.__generate_draw_event()
553
554
555 gobject.type_register(PieMenu)
556
557
558 class FakeEvent(object):
559
560         def __init__(self, x, y, isHint):
561                 self.x = x
562                 self.y = y
563                 self.is_hint = isHint
564
565         def get_coords(self):
566                 return self.x, self.y
567
568
569 class PiePopup(gtk.DrawingArea):
570
571         def __init__(self, style = None, **kwds):
572                 super(PiePopup, self).__init__()
573
574                 self.showAllSlices = True
575                 self.sliceStyle = style
576                 self.centerPosition = 0, 0
577                 self.radius = 20
578                 self.outerRadius = self.radius * 2
579
580                 self.connect("expose_event", self._on_expose)
581                 self.connect("motion_notify_event", self._on_motion_notify)
582                 self.connect("proximity_in_event", self._on_motion_notify)
583                 self.connect("proximity_out_event", self._on_leave_notify)
584                 self.connect("leave_notify_event", self._on_leave_notify)
585                 self.connect("button_press_event", self._on_button_press)
586                 self.connect("button_release_event", self._on_button_release)
587
588                 self.set_events(
589                         gtk.gdk.EXPOSURE_MASK |
590                         gtk.gdk.POINTER_MOTION_MASK |
591                         gtk.gdk.POINTER_MOTION_HINT_MASK |
592                         gtk.gdk.BUTTON_MOTION_MASK |
593                         gtk.gdk.BUTTON_PRESS_MASK |
594                         gtk.gdk.BUTTON_RELEASE_MASK |
595                         gtk.gdk.PROXIMITY_IN_MASK |
596                         gtk.gdk.PROXIMITY_OUT_MASK |
597                         gtk.gdk.LEAVE_NOTIFY_MASK
598                 )
599
600                 self.__popped = False
601                 self.__styleState = gtk.STATE_NORMAL
602                 self.__activeSlice = None
603                 self.__slices = {}
604                 self.__localSlices = {}
605
606                 self.__clickPosition = 0, 0
607                 self.__popupTimeDelay = None
608
609                 self.__pie = None
610                 self.__pie = PieMenu(self.sliceStyle)
611                 self.__pie.connect("button_release_event", self._on_button_release)
612                 self.__pie.show()
613
614                 self.__popupWindow = gtk.Window(type = gtk.WINDOW_POPUP)
615                 self.__popupWindow.set_title("")
616                 self.__popupWindow.add(self.__pie)
617
618                 for direction in PieSlice.SLICE_DIRECTIONS:
619                         self.add_slice(NullPieSlice(), direction)
620
621         def add_slice(self, slice, direction):
622                 assert direction in PieSlice.SLICE_DIRECTIONS
623
624                 self.__slices[direction] = slice
625                 self.__pie.add_slice(copy.copy(slice), direction)
626
627                 if self.showAllSlices or direction == PieSlice.SLICE_CENTER:
628                         self.__localSlices[direction] = copy.copy(slice)
629                         self.__localSlices[direction].menu_init(self, direction)
630                 if direction == PieSlice.SLICE_CENTER:
631                         self.__activeSlice = self.__localSlices[PieSlice.SLICE_CENTER]
632
633         def __update_state(self, mousePosition):
634                 rect = self.get_allocation()
635                 newStyleState = self.__styleState
636
637                 if (
638                         0 <= mousePosition[0] and mousePosition[0] < rect.width and
639                         0 <= mousePosition[1] and mousePosition[1] < rect.height
640                 ):
641                         if self.__clickPosition == (0, 0):
642                                 newStyleState = gtk.STATE_PRELIGHT
643                 else:
644                         if self.__clickPosition != (0, 0):
645                                 newStyleState = gtk.STATE_PRELIGHT
646
647                 if newStyleState != self.__styleState:
648                         self.__styleState = newStyleState
649                         self.__generate_draw_event()
650
651         def __generate_draw_event(self):
652                 rect = self.get_allocation()
653                 rect.x = 0
654                 rect.y = 0
655                 self.window.invalidate_rect(rect, True)
656
657         def _on_expose(self, widget, event):
658                 cairoContext = self.window.cairo_create()
659                 pangoContext = self.create_pango_context()
660                 textLayout = pango.Layout(pangoContext)
661
662                 rect = self.get_allocation()
663
664                 self.centerPosition = event.area.x + event.area.width / 2, event.area.y + event.area.height / 2
665                 self.radius = max(rect.width, rect.width) / 3 / 2
666                 self.outerRadius = max(rect.width, rect.height)
667
668                 # Draw Background
669                 cairoContext.rectangle(
670                         event.area.x,
671                         event.area.y,
672                         event.area.width,
673                         event.area.height,
674                 )
675                 cairoContext.set_source_rgb(*self.sliceStyle[self.__styleState]["fill"])
676                 cairoContext.fill()
677
678                 isSelected = self.__clickPosition != (0, 0)
679                 self.__activeSlice.draw_bg(self.__styleState, isSelected, cairoContext, textLayout)
680
681                 # Draw Foreground
682                 for slice in self.__localSlices.itervalues():
683                         isSelected = (slice is self.__activeSlice)
684                         if not isSelected:
685                                 slice.draw_fg(self.__styleState, isSelected, cairoContext, textLayout)
686
687                 isSelected = self.__clickPosition != (0, 0)
688                 self.__activeSlice.draw_fg(self.__styleState, isSelected, cairoContext, textLayout)
689
690         def _on_leave_notify(self, widget, event):
691                 newStyleState = gtk.STATE_NORMAL
692                 if newStyleState != self.__styleState:
693                         self.__styleState = newStyleState
694                         self.__generate_draw_event()
695
696                 self._on_motion_notify(widget, event)
697
698         def _on_motion_notify(self, widget, event):
699                 self.__update_state(event.get_coords())
700                 if not self.__popped:
701                         return
702
703                 mousePosition = event.get_root_coords()
704                 piePosition = self.__popupWindow.get_position()
705                 event.x = mousePosition[0] - piePosition[0]
706                 event.y = mousePosition[1] - piePosition[1]
707                 self.__pie._on_motion_notify(self.__pie, event)
708
709         def _on_button_press(self, widget, event):
710                 if len(self.__slices) == 0:
711                         return
712
713                 self.__clickPosition = event.get_root_coords()
714                 self.__generate_draw_event()
715                 self.__popupTimeDelay = gobject.timeout_add(100, self._on_delayed_popup)
716
717         def _on_delayed_popup(self):
718                 self.__popup(self.__clickPosition)
719                 gobject.source_remove(self.__popupTimeDelay)
720                 self.__popupTimeDelay = None
721                 return False
722
723         def _on_button_release(self, widget, event):
724                 if len(self.__slices) == 0:
725                         return
726
727                 if self.__popupTimeDelay is None:
728                         mousePosition = event.get_root_coords()
729                         piePosition = self.__popupWindow.get_position()
730                         eventX = mousePosition[0] - piePosition[0]
731                         eventY = mousePosition[1] - piePosition[1]
732                         pieRelease = FakeEvent(eventX, eventY, False)
733                         self.__pie._on_button_release(self.__pie, pieRelease)
734
735                         self.__unpop()
736                 else:
737                         gobject.source_remove(self.__popupTimeDelay)
738                         self.__popupTimeDelay = None
739                         self.__activeSlice.activate()
740
741                 self.__clickPosition = 0, 0
742                 self.__generate_draw_event()
743
744         def __popup(self, position):
745                 assert not self.__popped
746                 self.__popped = True
747
748                 width, height = 256, 256
749                 popupX, popupY = position[0] - width/2, position[1] - height/2
750
751                 self.__popupWindow.move(int(popupX), int(popupY))
752                 self.__popupWindow.resize(width, height)
753                 pieClick = FakeEvent(width/2, height/2, False)
754                 self.__pie._on_button_press(self.__pie, pieClick)
755                 self.__pie.grab_focus()
756
757                 self.__popupWindow.show()
758
759         def __unpop(self):
760                 assert self.__popped
761                 self.__popped = False
762
763                 piePosition = self.__popupWindow.get_position()
764                 self.grab_focus()
765
766                 self.__popupWindow.hide()
767
768
769 gobject.type_register(PiePopup)
770
771
772 def pie_main(isPop):
773         win = gtk.Window()
774         win.set_title("Pie Menu Test")
775
776         sliceStyle = generate_pie_style(win)
777         if isPop:
778                 target = PiePopup(sliceStyle)
779         else:
780                 target = PieMenu(sliceStyle)
781
782         def handler(pie, slice, direction):
783                 print pie, slice, direction
784         target.add_slice(TextLabelPieSlice("C", handler=handler), PieSlice.SLICE_CENTER)
785         target.add_slice(TextLabelPieSlice("N", handler=handler), PieSlice.SLICE_NORTH)
786         target.add_slice(TextLabelPieSlice("S", handler=handler), PieSlice.SLICE_SOUTH)
787         target.add_slice(TextLabelPieSlice("E", handler=handler), PieSlice.SLICE_EAST)
788         target.add_slice(TextLabelPieSlice("W", handler=handler), PieSlice.SLICE_WEST)
789
790         win.add(target)
791         win.resize(300, 300)
792         win.connect("destroy", lambda w: gtk.main_quit())
793         win.show_all()
794
795
796 if __name__ == "__main__":
797         pie_main(False)
798         pie_main(True)
799         gtk.main()