13383629591964a77323bd095294e61624a36bd4
[gc-dialer] / src / util / qtpie.py
1 #!/usr/bin/env python
2
3 import math
4 import logging
5
6 import qt_compat
7 QtCore = qt_compat.QtCore
8 QtGui = qt_compat.import_module("QtGui")
9
10 import misc as misc_utils
11
12
13 _moduleLogger = logging.getLogger(__name__)
14
15
16 _TWOPI = 2 * math.pi
17
18
19 def _radius_at(center, pos):
20         delta = pos - center
21         xDelta = delta.x()
22         yDelta = delta.y()
23
24         radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
25         return radius
26
27
28 def _angle_at(center, pos):
29         delta = pos - center
30         xDelta = delta.x()
31         yDelta = delta.y()
32
33         radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
34         angle = math.acos(xDelta / radius)
35         if 0 <= yDelta:
36                 angle = _TWOPI - angle
37
38         return angle
39
40
41 class QActionPieItem(object):
42
43         def __init__(self, action, weight = 1):
44                 self._action = action
45                 self._weight = weight
46
47         def action(self):
48                 return self._action
49
50         def setWeight(self, weight):
51                 self._weight = weight
52
53         def weight(self):
54                 return self._weight
55
56         def setEnabled(self, enabled = True):
57                 self._action.setEnabled(enabled)
58
59         def isEnabled(self):
60                 return self._action.isEnabled()
61
62
63 class PieFiling(object):
64
65         INNER_RADIUS_DEFAULT = 64
66         OUTER_RADIUS_DEFAULT = 192
67
68         SELECTION_CENTER = -1
69         SELECTION_NONE = -2
70
71         NULL_CENTER = QActionPieItem(QtGui.QAction(None))
72
73         def __init__(self):
74                 self._innerRadius = self.INNER_RADIUS_DEFAULT
75                 self._outerRadius = self.OUTER_RADIUS_DEFAULT
76                 self._children = []
77                 self._center = self.NULL_CENTER
78
79                 self._cacheIndexToAngle = {}
80                 self._cacheTotalWeight = 0
81
82         def insertItem(self, item, index = -1):
83                 self._children.insert(index, item)
84                 self._invalidate_cache()
85
86         def removeItemAt(self, index):
87                 item = self._children.pop(index)
88                 self._invalidate_cache()
89
90         def set_center(self, item):
91                 if item is None:
92                         item = self.NULL_CENTER
93                 self._center = item
94
95         def center(self):
96                 return self._center
97
98         def clear(self):
99                 del self._children[:]
100                 self._center = self.NULL_CENTER
101                 self._invalidate_cache()
102
103         def itemAt(self, index):
104                 return self._children[index]
105
106         def indexAt(self, center, point):
107                 return self._angle_to_index(_angle_at(center, point))
108
109         def innerRadius(self):
110                 return self._innerRadius
111
112         def setInnerRadius(self, radius):
113                 self._innerRadius = radius
114
115         def outerRadius(self):
116                 return self._outerRadius
117
118         def setOuterRadius(self, radius):
119                 self._outerRadius = radius
120
121         def __iter__(self):
122                 return iter(self._children)
123
124         def __len__(self):
125                 return len(self._children)
126
127         def __getitem__(self, index):
128                 return self._children[index]
129
130         def _invalidate_cache(self):
131                 self._cacheIndexToAngle.clear()
132                 self._cacheTotalWeight = sum(child.weight() for child in self._children)
133                 if self._cacheTotalWeight == 0:
134                         self._cacheTotalWeight = 1
135
136         def _index_to_angle(self, index, isShifted):
137                 key = index, isShifted
138                 if key in self._cacheIndexToAngle:
139                         return self._cacheIndexToAngle[key]
140                 index = index % len(self._children)
141
142                 baseAngle = _TWOPI / self._cacheTotalWeight
143
144                 angle = math.pi / 2
145                 if isShifted:
146                         if self._children:
147                                 angle -= (self._children[0].weight() * baseAngle) / 2
148                         else:
149                                 angle -= baseAngle / 2
150                 while angle < 0:
151                         angle += _TWOPI
152
153                 for i, child in enumerate(self._children):
154                         if index < i:
155                                 break
156                         angle += child.weight() * baseAngle
157                 while _TWOPI < angle:
158                         angle -= _TWOPI
159
160                 self._cacheIndexToAngle[key] = angle
161                 return angle
162
163         def _angle_to_index(self, angle):
164                 numChildren = len(self._children)
165                 if numChildren == 0:
166                         return self.SELECTION_CENTER
167
168                 baseAngle = _TWOPI / self._cacheTotalWeight
169
170                 iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
171                 while iterAngle < 0:
172                         iterAngle += _TWOPI
173
174                 oldIterAngle = iterAngle
175                 for index, child in enumerate(self._children):
176                         iterAngle += child.weight() * baseAngle
177                         if oldIterAngle < angle and angle <= iterAngle:
178                                 return index - 1 if index != 0 else numChildren - 1
179                         elif oldIterAngle < (angle + _TWOPI) and (angle + _TWOPI <= iterAngle):
180                                 return index - 1 if index != 0 else numChildren - 1
181                         oldIterAngle = iterAngle
182
183
184 class PieArtist(object):
185
186         ICON_SIZE_DEFAULT = 48
187
188         SHAPE_CIRCLE = "circle"
189         SHAPE_SQUARE = "square"
190         DEFAULT_SHAPE = SHAPE_SQUARE
191
192         BACKGROUND_FILL = "fill"
193         BACKGROUND_NOFILL = "no fill"
194
195         def __init__(self, filing, background = BACKGROUND_FILL):
196                 self._filing = filing
197
198                 self._cachedOuterRadius = self._filing.outerRadius()
199                 self._cachedInnerRadius = self._filing.innerRadius()
200                 canvasSize = self._cachedOuterRadius * 2 + 1
201                 self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
202                 self._mask = None
203                 self._backgroundState = background
204                 self.palette = None
205
206         def pieSize(self):
207                 diameter = self._filing.outerRadius() * 2 + 1
208                 return QtCore.QSize(diameter, diameter)
209
210         def centerSize(self):
211                 painter = QtGui.QPainter(self._canvas)
212                 text = self._filing.center().action().text()
213                 fontMetrics = painter.fontMetrics()
214                 if text:
215                         textBoundingRect = fontMetrics.boundingRect(text)
216                 else:
217                         textBoundingRect = QtCore.QRect()
218                 textWidth = textBoundingRect.width()
219                 textHeight = textBoundingRect.height()
220
221                 return QtCore.QSize(
222                         textWidth + self.ICON_SIZE_DEFAULT,
223                         max(textHeight, self.ICON_SIZE_DEFAULT),
224                 )
225
226         def show(self, palette):
227                 self.palette = palette
228
229                 if (
230                         self._cachedOuterRadius != self._filing.outerRadius() or
231                         self._cachedInnerRadius != self._filing.innerRadius()
232                 ):
233                         self._cachedOuterRadius = self._filing.outerRadius()
234                         self._cachedInnerRadius = self._filing.innerRadius()
235                         self._canvas = self._canvas.scaled(self.pieSize())
236
237                 if self._mask is None:
238                         self._mask = QtGui.QBitmap(self._canvas.size())
239                         self._mask.fill(QtCore.Qt.color0)
240                         self._generate_mask(self._mask)
241                         self._canvas.setMask(self._mask)
242                 return self._mask
243
244         def hide(self):
245                 self.palette = None
246
247         def paint(self, selectionIndex):
248                 painter = QtGui.QPainter(self._canvas)
249                 painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
250
251                 self.paintPainter(selectionIndex, painter)
252
253                 return self._canvas
254
255         def paintPainter(self, selectionIndex, painter):
256                 adjustmentRect = painter.viewport().adjusted(0, 0, -1, -1)
257
258                 numChildren = len(self._filing)
259                 if numChildren == 0:
260                         self._paint_center_background(painter, adjustmentRect, selectionIndex)
261                         self._paint_center_foreground(painter, adjustmentRect, selectionIndex)
262                         return self._canvas
263                 else:
264                         for i in xrange(len(self._filing)):
265                                 self._paint_slice_background(painter, adjustmentRect, i, selectionIndex)
266
267                 self._paint_center_background(painter, adjustmentRect, selectionIndex)
268                 self._paint_center_foreground(painter, adjustmentRect, selectionIndex)
269
270                 for i in xrange(len(self._filing)):
271                         self._paint_slice_foreground(painter, adjustmentRect, i, selectionIndex)
272
273         def _generate_mask(self, mask):
274                 """
275                 Specifies on the mask the shape of the pie menu
276                 """
277                 painter = QtGui.QPainter(mask)
278                 painter.setPen(QtCore.Qt.color1)
279                 painter.setBrush(QtCore.Qt.color1)
280                 if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
281                         painter.drawRect(mask.rect())
282                 elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
283                         painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
284                 else:
285                         raise NotImplementedError(self.DEFAULT_SHAPE)
286
287         def _paint_slice_background(self, painter, adjustmentRect, i, selectionIndex):
288                 if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
289                         currentWidth = adjustmentRect.width()
290                         newWidth = math.sqrt(2) * currentWidth
291                         dx = (newWidth - currentWidth) / 2
292                         adjustmentRect = adjustmentRect.adjusted(-dx, -dx, dx, dx)
293                 elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
294                         pass
295                 else:
296                         raise NotImplementedError(self.DEFAULT_SHAPE)
297
298                 if self._backgroundState == self.BACKGROUND_NOFILL:
299                         painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent))
300                         painter.setPen(self.palette.highlight().color())
301                 else:
302                         if i == selectionIndex and self._filing[i].isEnabled():
303                                 painter.setBrush(self.palette.highlight())
304                                 painter.setPen(self.palette.highlight().color())
305                         else:
306                                 painter.setBrush(self.palette.window())
307                                 painter.setPen(self.palette.window().color())
308
309                 a = self._filing._index_to_angle(i, True)
310                 b = self._filing._index_to_angle(i + 1, True)
311                 if b < a:
312                         b += _TWOPI
313                 size = b - a
314                 if size < 0:
315                         size += _TWOPI
316
317                 startAngleInDeg = (a * 360 * 16) / _TWOPI
318                 sizeInDeg = (size * 360 * 16) / _TWOPI
319                 painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
320
321         def _paint_slice_foreground(self, painter, adjustmentRect, i, selectionIndex):
322                 child = self._filing[i]
323
324                 a = self._filing._index_to_angle(i, True)
325                 b = self._filing._index_to_angle(i + 1, True)
326                 if b < a:
327                         b += _TWOPI
328                 middleAngle = (a + b) / 2
329                 averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2
330
331                 sliceX = averageRadius * math.cos(middleAngle)
332                 sliceY = - averageRadius * math.sin(middleAngle)
333
334                 piePos = adjustmentRect.center()
335                 pieX = piePos.x()
336                 pieY = piePos.y()
337                 self._paint_label(
338                         painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
339                 )
340
341         def _paint_label(self, painter, action, isSelected, x, y):
342                 text = action.text()
343                 fontMetrics = painter.fontMetrics()
344                 if text:
345                         textBoundingRect = fontMetrics.boundingRect(text)
346                 else:
347                         textBoundingRect = QtCore.QRect()
348                 textWidth = textBoundingRect.width()
349                 textHeight = textBoundingRect.height()
350
351                 icon = action.icon().pixmap(
352                         QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
353                         QtGui.QIcon.Normal,
354                         QtGui.QIcon.On,
355                 )
356                 iconWidth = icon.width()
357                 iconHeight = icon.width()
358                 averageWidth = (iconWidth + textWidth)/2
359                 if not icon.isNull():
360                         iconRect = QtCore.QRect(
361                                 x - averageWidth,
362                                 y - iconHeight/2,
363                                 iconWidth,
364                                 iconHeight,
365                         )
366
367                         painter.drawPixmap(iconRect, icon)
368
369                 if text:
370                         if isSelected:
371                                 if action.isEnabled():
372                                         pen = self.palette.highlightedText()
373                                         brush = self.palette.highlight()
374                                 else:
375                                         pen = self.palette.mid()
376                                         brush = self.palette.window()
377                         else:
378                                 if action.isEnabled():
379                                         pen = self.palette.windowText()
380                                 else:
381                                         pen = self.palette.mid()
382                                 brush = self.palette.window()
383
384                         leftX = x - averageWidth + iconWidth
385                         topY = y + textHeight/2
386                         painter.setPen(pen.color())
387                         painter.setBrush(brush)
388                         painter.drawText(leftX, topY, text)
389
390         def _paint_center_background(self, painter, adjustmentRect, selectionIndex):
391                 if self._backgroundState == self.BACKGROUND_NOFILL:
392                         return
393                 if len(self._filing) == 0:
394                         if self._backgroundState == self.BACKGROUND_NOFILL:
395                                 painter.setBrush(QtGui.QBrush(QtCore.Qt.transparent))
396                         else:
397                                 if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
398                                         painter.setBrush(self.palette.highlight())
399                                 else:
400                                         painter.setBrush(self.palette.window())
401                         painter.setPen(self.palette.mid().color())
402
403                         painter.drawRect(adjustmentRect)
404                 else:
405                         dark = self.palette.mid().color()
406                         light = self.palette.light().color()
407                         if self._backgroundState == self.BACKGROUND_NOFILL:
408                                 background = QtGui.QBrush(QtCore.Qt.transparent)
409                         else:
410                                 if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
411                                         background = self.palette.highlight().color()
412                                 else:
413                                         background = self.palette.window().color()
414
415                         innerRadius = self._cachedInnerRadius
416                         adjustmentCenterPos = adjustmentRect.center()
417                         innerRect = QtCore.QRect(
418                                 adjustmentCenterPos.x() - innerRadius,
419                                 adjustmentCenterPos.y() - innerRadius,
420                                 innerRadius * 2 + 1,
421                                 innerRadius * 2 + 1,
422                         )
423
424                         painter.setPen(QtCore.Qt.NoPen)
425                         painter.setBrush(background)
426                         painter.drawPie(innerRect, 0, 360 * 16)
427
428                         if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
429                                 pass
430                         elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
431                                 painter.setPen(QtGui.QPen(dark, 1))
432                                 painter.setBrush(QtCore.Qt.NoBrush)
433                                 painter.drawEllipse(adjustmentRect)
434                         else:
435                                 raise NotImplementedError(self.DEFAULT_SHAPE)
436
437         def _paint_center_foreground(self, painter, adjustmentRect, selectionIndex):
438                 centerPos = adjustmentRect.center()
439                 pieX = centerPos.x()
440                 pieY = centerPos.y()
441
442                 x = pieX
443                 y = pieY
444
445                 self._paint_label(
446                         painter,
447                         self._filing.center().action(),
448                         selectionIndex == PieFiling.SELECTION_CENTER,
449                         x, y
450                 )
451
452
453 class QPieDisplay(QtGui.QWidget):
454
455         def __init__(self, filing, parent = None, flags = QtCore.Qt.Window):
456                 QtGui.QWidget.__init__(self, parent, flags)
457                 self._filing = filing
458                 self._artist = PieArtist(self._filing)
459                 self._selectionIndex = PieFiling.SELECTION_NONE
460
461         def popup(self, pos):
462                 self._update_selection(pos)
463                 self.show()
464
465         def sizeHint(self):
466                 return self._artist.pieSize()
467
468         @misc_utils.log_exception(_moduleLogger)
469         def showEvent(self, showEvent):
470                 mask = self._artist.show(self.palette())
471                 self.setMask(mask)
472
473                 QtGui.QWidget.showEvent(self, showEvent)
474
475         @misc_utils.log_exception(_moduleLogger)
476         def hideEvent(self, hideEvent):
477                 self._artist.hide()
478                 self._selectionIndex = PieFiling.SELECTION_NONE
479                 QtGui.QWidget.hideEvent(self, hideEvent)
480
481         @misc_utils.log_exception(_moduleLogger)
482         def paintEvent(self, paintEvent):
483                 canvas = self._artist.paint(self._selectionIndex)
484                 offset = (self.size() - canvas.size()) / 2
485
486                 screen = QtGui.QPainter(self)
487                 screen.drawPixmap(QtCore.QPoint(offset.width(), offset.height()), canvas)
488
489                 QtGui.QWidget.paintEvent(self, paintEvent)
490
491         def selectAt(self, index):
492                 oldIndex = self._selectionIndex
493                 self._selectionIndex = index
494                 if self.isVisible():
495                         self.update()
496
497
498 class QPieButton(QtGui.QWidget):
499
500         activated = qt_compat.Signal(int)
501         highlighted = qt_compat.Signal(int)
502         canceled = qt_compat.Signal()
503         aboutToShow = qt_compat.Signal()
504         aboutToHide = qt_compat.Signal()
505
506         BUTTON_RADIUS = 24
507         DELAY = 250
508
509         def __init__(self, buttonSlice, parent = None, buttonSlices = None):
510                 # @bug Artifacts on Maemo 5 due to window 3D effects, find way to disable them for just these?
511                 # @bug The pie's are being pushed back on screen on Maemo, leading to coordinate issues
512                 QtGui.QWidget.__init__(self, parent)
513                 self._cachedCenterPosition = self.rect().center()
514
515                 self._filing = PieFiling()
516                 self._display = QPieDisplay(self._filing, None, QtCore.Qt.SplashScreen)
517                 self._selectionIndex = PieFiling.SELECTION_NONE
518
519                 self._buttonFiling = PieFiling()
520                 self._buttonFiling.set_center(buttonSlice)
521                 if buttonSlices is not None:
522                         for slice in buttonSlices:
523                                 self._buttonFiling.insertItem(slice)
524                 self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS)
525                 self._buttonArtist = PieArtist(self._buttonFiling, PieArtist.BACKGROUND_NOFILL)
526                 self._poppedUp = False
527                 self._pressed = False
528
529                 self._delayPopupTimer = QtCore.QTimer()
530                 self._delayPopupTimer.setInterval(self.DELAY)
531                 self._delayPopupTimer.setSingleShot(True)
532                 self._delayPopupTimer.timeout.connect(self._on_delayed_popup)
533                 self._popupLocation = None
534
535                 self._mousePosition = None
536                 self.setFocusPolicy(QtCore.Qt.StrongFocus)
537                 self.setSizePolicy(
538                         QtGui.QSizePolicy(
539                                 QtGui.QSizePolicy.MinimumExpanding,
540                                 QtGui.QSizePolicy.MinimumExpanding,
541                         )
542                 )
543
544         def insertItem(self, item, index = -1):
545                 self._filing.insertItem(item, index)
546
547         def removeItemAt(self, index):
548                 self._filing.removeItemAt(index)
549
550         def set_center(self, item):
551                 self._filing.set_center(item)
552
553         def set_button(self, item):
554                 self.update()
555
556         def clear(self):
557                 self._filing.clear()
558
559         def itemAt(self, index):
560                 return self._filing.itemAt(index)
561
562         def indexAt(self, point):
563                 return self._filing.indexAt(self._cachedCenterPosition, point)
564
565         def innerRadius(self):
566                 return self._filing.innerRadius()
567
568         def setInnerRadius(self, radius):
569                 self._filing.setInnerRadius(radius)
570
571         def outerRadius(self):
572                 return self._filing.outerRadius()
573
574         def setOuterRadius(self, radius):
575                 self._filing.setOuterRadius(radius)
576
577         def buttonRadius(self):
578                 return self._buttonFiling.outerRadius()
579
580         def setButtonRadius(self, radius):
581                 self._buttonFiling.setOuterRadius(radius)
582                 self._buttonFiling.setInnerRadius(radius / 2)
583                 self._buttonArtist.show(self.palette())
584
585         def minimumSizeHint(self):
586                 return self._buttonArtist.centerSize()
587
588         @misc_utils.log_exception(_moduleLogger)
589         def mousePressEvent(self, mouseEvent):
590                 lastSelection = self._selectionIndex
591
592                 lastMousePos = mouseEvent.pos()
593                 self._mousePosition = lastMousePos
594                 self._update_selection(self._cachedCenterPosition)
595
596                 self.highlighted.emit(self._selectionIndex)
597
598                 self._display.selectAt(self._selectionIndex)
599                 self._pressed = True
600                 self.update()
601                 self._popupLocation = mouseEvent.globalPos()
602                 self._delayPopupTimer.start()
603
604         @misc_utils.log_exception(_moduleLogger)
605         def _on_delayed_popup(self):
606                 self.__on_delayed_popup()
607
608         def __on_delayed_popup(self):
609                 assert self._popupLocation is not None, "Widget location abuse"
610                 self._popup_child(self._popupLocation)
611
612         @misc_utils.log_exception(_moduleLogger)
613         def mouseMoveEvent(self, mouseEvent):
614                 lastSelection = self._selectionIndex
615
616                 lastMousePos = mouseEvent.pos()
617                 if self._mousePosition is None:
618                         # Absolute
619                         self._update_selection(lastMousePos)
620                 else:
621                         # Relative
622                         self._update_selection(
623                                 self._cachedCenterPosition + (lastMousePos - self._mousePosition),
624                                 ignoreOuter = True,
625                         )
626
627                 if lastSelection != self._selectionIndex:
628                         self.highlighted.emit(self._selectionIndex)
629                         self._display.selectAt(self._selectionIndex)
630
631                 if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive():
632                         self._on_delayed_popup()
633
634         @misc_utils.log_exception(_moduleLogger)
635         def mouseReleaseEvent(self, mouseEvent):
636                 self._delayPopupTimer.stop()
637                 self._popupLocation = None
638
639                 lastSelection = self._selectionIndex
640
641                 lastMousePos = mouseEvent.pos()
642                 if self._mousePosition is None:
643                         # Absolute
644                         self._update_selection(lastMousePos)
645                 else:
646                         # Relative
647                         self._update_selection(
648                                 self._cachedCenterPosition + (lastMousePos - self._mousePosition),
649                                 ignoreOuter = True,
650                         )
651                 self._mousePosition = None
652
653                 self._activate_at(self._selectionIndex)
654                 self._pressed = False
655                 self.update()
656                 self._hide_child()
657
658         @misc_utils.log_exception(_moduleLogger)
659         def keyPressEvent(self, keyEvent):
660                 if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
661                         self._popup_child(QtGui.QCursor.pos())
662                         if self._selectionIndex != len(self._filing) - 1:
663                                 nextSelection = self._selectionIndex + 1
664                         else:
665                                 nextSelection = 0
666                         self._select_at(nextSelection)
667                         self._display.selectAt(self._selectionIndex)
668                 elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
669                         self._popup_child(QtGui.QCursor.pos())
670                         if 0 < self._selectionIndex:
671                                 nextSelection = self._selectionIndex - 1
672                         else:
673                                 nextSelection = len(self._filing) - 1
674                         self._select_at(nextSelection)
675                         self._display.selectAt(self._selectionIndex)
676                 elif keyEvent.key() in [QtCore.Qt.Key_Space]:
677                         self._popup_child(QtGui.QCursor.pos())
678                         self._select_at(PieFiling.SELECTION_CENTER)
679                         self._display.selectAt(self._selectionIndex)
680                 elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
681                         self._delayPopupTimer.stop()
682                         self._popupLocation = None
683                         self._activate_at(self._selectionIndex)
684                         self._hide_child()
685                 elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
686                         self._delayPopupTimer.stop()
687                         self._popupLocation = None
688                         self._activate_at(PieFiling.SELECTION_NONE)
689                         self._hide_child()
690                 else:
691                         QtGui.QWidget.keyPressEvent(self, keyEvent)
692
693         @misc_utils.log_exception(_moduleLogger)
694         def resizeEvent(self, resizeEvent):
695                 self.setButtonRadius(min(resizeEvent.size().width(), resizeEvent.size().height()) / 2 - 1)
696                 QtGui.QWidget.resizeEvent(self, resizeEvent)
697
698         @misc_utils.log_exception(_moduleLogger)
699         def showEvent(self, showEvent):
700                 self._buttonArtist.show(self.palette())
701                 self._cachedCenterPosition = self.rect().center()
702
703                 QtGui.QWidget.showEvent(self, showEvent)
704
705         @misc_utils.log_exception(_moduleLogger)
706         def hideEvent(self, hideEvent):
707                 self._display.hide()
708                 self._select_at(PieFiling.SELECTION_NONE)
709                 QtGui.QWidget.hideEvent(self, hideEvent)
710
711         @misc_utils.log_exception(_moduleLogger)
712         def paintEvent(self, paintEvent):
713                 self.setButtonRadius(min(self.rect().width(), self.rect().height()) / 2 - 1)
714                 if self._poppedUp:
715                         selectionIndex = PieFiling.SELECTION_CENTER
716                 else:
717                         selectionIndex = PieFiling.SELECTION_NONE
718
719                 screen = QtGui.QStylePainter(self)
720                 screen.setRenderHint(QtGui.QPainter.Antialiasing, True)
721                 option = QtGui.QStyleOptionButton()
722                 option.initFrom(self)
723                 option.state = QtGui.QStyle.State_Sunken if self._pressed else QtGui.QStyle.State_Raised
724
725                 screen.drawControl(QtGui.QStyle.CE_PushButton, option)
726                 self._buttonArtist.paintPainter(selectionIndex, screen)
727
728                 QtGui.QWidget.paintEvent(self, paintEvent)
729
730         def __iter__(self):
731                 return iter(self._filing)
732
733         def __len__(self):
734                 return len(self._filing)
735
736         def _popup_child(self, position):
737                 self._poppedUp = True
738                 self.aboutToShow.emit()
739
740                 self._delayPopupTimer.stop()
741                 self._popupLocation = None
742
743                 position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius())
744                 self._display.move(position)
745                 self._display.show()
746
747                 self.update()
748
749         def _hide_child(self):
750                 self._poppedUp = False
751                 self.aboutToHide.emit()
752                 self._display.hide()
753                 self.update()
754
755         def _select_at(self, index):
756                 self._selectionIndex = index
757
758         def _update_selection(self, lastMousePos, ignoreOuter = False):
759                 radius = _radius_at(self._cachedCenterPosition, lastMousePos)
760                 if radius < self._filing.innerRadius():
761                         self._select_at(PieFiling.SELECTION_CENTER)
762                 elif radius <= self._filing.outerRadius() or ignoreOuter:
763                         self._select_at(self.indexAt(lastMousePos))
764                 else:
765                         self._select_at(PieFiling.SELECTION_NONE)
766
767         def _activate_at(self, index):
768                 if index == PieFiling.SELECTION_NONE:
769                         self.canceled.emit()
770                         return
771                 elif index == PieFiling.SELECTION_CENTER:
772                         child = self._filing.center()
773                 else:
774                         child = self.itemAt(index)
775
776                 if child.action().isEnabled():
777                         child.action().trigger()
778                         self.activated.emit(index)
779                 else:
780                         self.canceled.emit()
781
782
783 class QPieMenu(QtGui.QWidget):
784
785         activated = qt_compat.Signal(int)
786         highlighted = qt_compat.Signal(int)
787         canceled = qt_compat.Signal()
788         aboutToShow = qt_compat.Signal()
789         aboutToHide = qt_compat.Signal()
790
791         def __init__(self, parent = None):
792                 QtGui.QWidget.__init__(self, parent)
793                 self._cachedCenterPosition = self.rect().center()
794
795                 self._filing = PieFiling()
796                 self._artist = PieArtist(self._filing)
797                 self._selectionIndex = PieFiling.SELECTION_NONE
798
799                 self._mousePosition = ()
800                 self.setFocusPolicy(QtCore.Qt.StrongFocus)
801
802         def popup(self, pos):
803                 self._update_selection(pos)
804                 self.show()
805
806         def insertItem(self, item, index = -1):
807                 self._filing.insertItem(item, index)
808                 self.update()
809
810         def removeItemAt(self, index):
811                 self._filing.removeItemAt(index)
812                 self.update()
813
814         def set_center(self, item):
815                 self._filing.set_center(item)
816                 self.update()
817
818         def clear(self):
819                 self._filing.clear()
820                 self.update()
821
822         def itemAt(self, index):
823                 return self._filing.itemAt(index)
824
825         def indexAt(self, point):
826                 return self._filing.indexAt(self._cachedCenterPosition, point)
827
828         def innerRadius(self):
829                 return self._filing.innerRadius()
830
831         def setInnerRadius(self, radius):
832                 self._filing.setInnerRadius(radius)
833                 self.update()
834
835         def outerRadius(self):
836                 return self._filing.outerRadius()
837
838         def setOuterRadius(self, radius):
839                 self._filing.setOuterRadius(radius)
840                 self.update()
841
842         def sizeHint(self):
843                 return self._artist.pieSize()
844
845         @misc_utils.log_exception(_moduleLogger)
846         def mousePressEvent(self, mouseEvent):
847                 lastSelection = self._selectionIndex
848
849                 lastMousePos = mouseEvent.pos()
850                 self._update_selection(lastMousePos)
851                 self._mousePosition = lastMousePos
852
853                 if lastSelection != self._selectionIndex:
854                         self.highlighted.emit(self._selectionIndex)
855                         self.update()
856
857         @misc_utils.log_exception(_moduleLogger)
858         def mouseMoveEvent(self, mouseEvent):
859                 lastSelection = self._selectionIndex
860
861                 lastMousePos = mouseEvent.pos()
862                 self._update_selection(lastMousePos)
863
864                 if lastSelection != self._selectionIndex:
865                         self.highlighted.emit(self._selectionIndex)
866                         self.update()
867
868         @misc_utils.log_exception(_moduleLogger)
869         def mouseReleaseEvent(self, mouseEvent):
870                 lastSelection = self._selectionIndex
871
872                 lastMousePos = mouseEvent.pos()
873                 self._update_selection(lastMousePos)
874                 self._mousePosition = ()
875
876                 self._activate_at(self._selectionIndex)
877                 self.update()
878
879         @misc_utils.log_exception(_moduleLogger)
880         def keyPressEvent(self, keyEvent):
881                 if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
882                         if self._selectionIndex != len(self._filing) - 1:
883                                 nextSelection = self._selectionIndex + 1
884                         else:
885                                 nextSelection = 0
886                         self._select_at(nextSelection)
887                         self.update()
888                 elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
889                         if 0 < self._selectionIndex:
890                                 nextSelection = self._selectionIndex - 1
891                         else:
892                                 nextSelection = len(self._filing) - 1
893                         self._select_at(nextSelection)
894                         self.update()
895                 elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
896                         self._activate_at(self._selectionIndex)
897                 elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
898                         self._activate_at(PieFiling.SELECTION_NONE)
899                 else:
900                         QtGui.QWidget.keyPressEvent(self, keyEvent)
901
902         @misc_utils.log_exception(_moduleLogger)
903         def showEvent(self, showEvent):
904                 self.aboutToShow.emit()
905                 self._cachedCenterPosition = self.rect().center()
906
907                 mask = self._artist.show(self.palette())
908                 self.setMask(mask)
909
910                 lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
911                 self._update_selection(lastMousePos)
912
913                 QtGui.QWidget.showEvent(self, showEvent)
914
915         @misc_utils.log_exception(_moduleLogger)
916         def hideEvent(self, hideEvent):
917                 self._artist.hide()
918                 self._selectionIndex = PieFiling.SELECTION_NONE
919                 QtGui.QWidget.hideEvent(self, hideEvent)
920
921         @misc_utils.log_exception(_moduleLogger)
922         def paintEvent(self, paintEvent):
923                 canvas = self._artist.paint(self._selectionIndex)
924
925                 screen = QtGui.QPainter(self)
926                 screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
927
928                 QtGui.QWidget.paintEvent(self, paintEvent)
929
930         def __iter__(self):
931                 return iter(self._filing)
932
933         def __len__(self):
934                 return len(self._filing)
935
936         def _select_at(self, index):
937                 self._selectionIndex = index
938
939         def _update_selection(self, lastMousePos):
940                 radius = _radius_at(self._cachedCenterPosition, lastMousePos)
941                 if radius < self._filing.innerRadius():
942                         self._selectionIndex = PieFiling.SELECTION_CENTER
943                 elif radius <= self._filing.outerRadius():
944                         self._select_at(self.indexAt(lastMousePos))
945                 else:
946                         self._selectionIndex = PieFiling.SELECTION_NONE
947
948         def _activate_at(self, index):
949                 if index == PieFiling.SELECTION_NONE:
950                         self.canceled.emit()
951                         self.aboutToHide.emit()
952                         self.hide()
953                         return
954                 elif index == PieFiling.SELECTION_CENTER:
955                         child = self._filing.center()
956                 else:
957                         child = self.itemAt(index)
958
959                 if child.isEnabled():
960                         child.action().trigger()
961                         self.activated.emit(index)
962                 else:
963                         self.canceled.emit()
964                 self.aboutToHide.emit()
965                 self.hide()
966
967
968 def init_pies():
969         PieFiling.NULL_CENTER.setEnabled(False)
970
971
972 def _print(msg):
973         print msg
974
975
976 def _on_about_to_hide(app):
977         app.exit()
978
979
980 if __name__ == "__main__":
981         app = QtGui.QApplication([])
982         init_pies()
983
984         if False:
985                 pie = QPieMenu()
986                 pie.show()
987
988         if False:
989                 singleAction = QtGui.QAction(None)
990                 singleAction.setText("Boo")
991                 singleItem = QActionPieItem(singleAction)
992                 spie = QPieMenu()
993                 spie.insertItem(singleItem)
994                 spie.show()
995
996         if False:
997                 oneAction = QtGui.QAction(None)
998                 oneAction.setText("Chew")
999                 oneItem = QActionPieItem(oneAction)
1000                 twoAction = QtGui.QAction(None)
1001                 twoAction.setText("Foo")
1002                 twoItem = QActionPieItem(twoAction)
1003                 iconTextAction = QtGui.QAction(None)
1004                 iconTextAction.setText("Icon")
1005                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1006                 iconTextItem = QActionPieItem(iconTextAction)
1007                 mpie = QPieMenu()
1008                 mpie.insertItem(oneItem)
1009                 mpie.insertItem(twoItem)
1010                 mpie.insertItem(oneItem)
1011                 mpie.insertItem(iconTextItem)
1012                 mpie.show()
1013
1014         if True:
1015                 oneAction = QtGui.QAction(None)
1016                 oneAction.setText("Chew")
1017                 oneAction.triggered.connect(lambda: _print("Chew"))
1018                 oneItem = QActionPieItem(oneAction)
1019                 twoAction = QtGui.QAction(None)
1020                 twoAction.setText("Foo")
1021                 twoAction.triggered.connect(lambda: _print("Foo"))
1022                 twoItem = QActionPieItem(twoAction)
1023                 iconAction = QtGui.QAction(None)
1024                 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
1025                 iconAction.triggered.connect(lambda: _print("Icon"))
1026                 iconItem = QActionPieItem(iconAction)
1027                 iconTextAction = QtGui.QAction(None)
1028                 iconTextAction.setText("Icon")
1029                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1030                 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
1031                 iconTextItem = QActionPieItem(iconTextAction)
1032                 mpie = QPieMenu()
1033                 mpie.set_center(iconItem)
1034                 mpie.insertItem(oneItem)
1035                 mpie.insertItem(twoItem)
1036                 mpie.insertItem(oneItem)
1037                 mpie.insertItem(iconTextItem)
1038                 mpie.show()
1039                 mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
1040                 mpie.canceled.connect(lambda: _print("Canceled"))
1041
1042         if False:
1043                 oneAction = QtGui.QAction(None)
1044                 oneAction.setText("Chew")
1045                 oneAction.triggered.connect(lambda: _print("Chew"))
1046                 oneItem = QActionPieItem(oneAction)
1047                 twoAction = QtGui.QAction(None)
1048                 twoAction.setText("Foo")
1049                 twoAction.triggered.connect(lambda: _print("Foo"))
1050                 twoItem = QActionPieItem(twoAction)
1051                 iconAction = QtGui.QAction(None)
1052                 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
1053                 iconAction.triggered.connect(lambda: _print("Icon"))
1054                 iconItem = QActionPieItem(iconAction)
1055                 iconTextAction = QtGui.QAction(None)
1056                 iconTextAction.setText("Icon")
1057                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1058                 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
1059                 iconTextItem = QActionPieItem(iconTextAction)
1060                 pieFiling = PieFiling()
1061                 pieFiling.set_center(iconItem)
1062                 pieFiling.insertItem(oneItem)
1063                 pieFiling.insertItem(twoItem)
1064                 pieFiling.insertItem(oneItem)
1065                 pieFiling.insertItem(iconTextItem)
1066                 mpie = QPieDisplay(pieFiling)
1067                 mpie.show()
1068
1069         if False:
1070                 oneAction = QtGui.QAction(None)
1071                 oneAction.setText("Chew")
1072                 oneAction.triggered.connect(lambda: _print("Chew"))
1073                 oneItem = QActionPieItem(oneAction)
1074                 twoAction = QtGui.QAction(None)
1075                 twoAction.setText("Foo")
1076                 twoAction.triggered.connect(lambda: _print("Foo"))
1077                 twoItem = QActionPieItem(twoAction)
1078                 iconAction = QtGui.QAction(None)
1079                 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
1080                 iconAction.triggered.connect(lambda: _print("Icon"))
1081                 iconItem = QActionPieItem(iconAction)
1082                 iconTextAction = QtGui.QAction(None)
1083                 iconTextAction.setText("Icon")
1084                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1085                 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
1086                 iconTextItem = QActionPieItem(iconTextAction)
1087                 mpie = QPieButton(iconItem)
1088                 mpie.set_center(iconItem)
1089                 mpie.insertItem(oneItem)
1090                 mpie.insertItem(twoItem)
1091                 mpie.insertItem(oneItem)
1092                 mpie.insertItem(iconTextItem)
1093                 mpie.show()
1094                 mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
1095                 mpie.canceled.connect(lambda: _print("Canceled"))
1096
1097         app.exec_()