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