XDG Support
[gonvert] / gonvert / 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                 assert self._popupLocation is not None, "Widget location abuse"
607                 self._popup_child(self._popupLocation)
608
609         @misc_utils.log_exception(_moduleLogger)
610         def mouseMoveEvent(self, mouseEvent):
611                 lastSelection = self._selectionIndex
612
613                 lastMousePos = mouseEvent.pos()
614                 if self._mousePosition is None:
615                         # Absolute
616                         self._update_selection(lastMousePos)
617                 else:
618                         # Relative
619                         self._update_selection(
620                                 self._cachedCenterPosition + (lastMousePos - self._mousePosition),
621                                 ignoreOuter = True,
622                         )
623
624                 if lastSelection != self._selectionIndex:
625                         self.highlighted.emit(self._selectionIndex)
626                         self._display.selectAt(self._selectionIndex)
627
628                 if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive():
629                         self._on_delayed_popup()
630
631         @misc_utils.log_exception(_moduleLogger)
632         def mouseReleaseEvent(self, mouseEvent):
633                 self._delayPopupTimer.stop()
634                 self._popupLocation = None
635
636                 lastSelection = self._selectionIndex
637
638                 lastMousePos = mouseEvent.pos()
639                 if self._mousePosition is None:
640                         # Absolute
641                         self._update_selection(lastMousePos)
642                 else:
643                         # Relative
644                         self._update_selection(
645                                 self._cachedCenterPosition + (lastMousePos - self._mousePosition),
646                                 ignoreOuter = True,
647                         )
648                 self._mousePosition = None
649
650                 self._activate_at(self._selectionIndex)
651                 self._pressed = False
652                 self.update()
653                 self._hide_child()
654
655         @misc_utils.log_exception(_moduleLogger)
656         def keyPressEvent(self, keyEvent):
657                 if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
658                         self._popup_child(QtGui.QCursor.pos())
659                         if self._selectionIndex != len(self._filing) - 1:
660                                 nextSelection = self._selectionIndex + 1
661                         else:
662                                 nextSelection = 0
663                         self._select_at(nextSelection)
664                         self._display.selectAt(self._selectionIndex)
665                 elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
666                         self._popup_child(QtGui.QCursor.pos())
667                         if 0 < self._selectionIndex:
668                                 nextSelection = self._selectionIndex - 1
669                         else:
670                                 nextSelection = len(self._filing) - 1
671                         self._select_at(nextSelection)
672                         self._display.selectAt(self._selectionIndex)
673                 elif keyEvent.key() in [QtCore.Qt.Key_Space]:
674                         self._popup_child(QtGui.QCursor.pos())
675                         self._select_at(PieFiling.SELECTION_CENTER)
676                         self._display.selectAt(self._selectionIndex)
677                 elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
678                         self._delayPopupTimer.stop()
679                         self._popupLocation = None
680                         self._activate_at(self._selectionIndex)
681                         self._hide_child()
682                 elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
683                         self._delayPopupTimer.stop()
684                         self._popupLocation = None
685                         self._activate_at(PieFiling.SELECTION_NONE)
686                         self._hide_child()
687                 else:
688                         QtGui.QWidget.keyPressEvent(self, keyEvent)
689
690         @misc_utils.log_exception(_moduleLogger)
691         def resizeEvent(self, resizeEvent):
692                 self.setButtonRadius(min(resizeEvent.size().width(), resizeEvent.size().height()) / 2 - 1)
693                 QtGui.QWidget.resizeEvent(self, resizeEvent)
694
695         @misc_utils.log_exception(_moduleLogger)
696         def showEvent(self, showEvent):
697                 self._buttonArtist.show(self.palette())
698                 self._cachedCenterPosition = self.rect().center()
699
700                 QtGui.QWidget.showEvent(self, showEvent)
701
702         @misc_utils.log_exception(_moduleLogger)
703         def hideEvent(self, hideEvent):
704                 self._display.hide()
705                 self._select_at(PieFiling.SELECTION_NONE)
706                 QtGui.QWidget.hideEvent(self, hideEvent)
707
708         @misc_utils.log_exception(_moduleLogger)
709         def paintEvent(self, paintEvent):
710                 self.setButtonRadius(min(self.rect().width(), self.rect().height()) / 2 - 1)
711                 if self._poppedUp:
712                         selectionIndex = PieFiling.SELECTION_CENTER
713                 else:
714                         selectionIndex = PieFiling.SELECTION_NONE
715
716                 screen = QtGui.QStylePainter(self)
717                 screen.setRenderHint(QtGui.QPainter.Antialiasing, True)
718                 option = QtGui.QStyleOptionButton()
719                 option.initFrom(self)
720                 option.state = QtGui.QStyle.State_Sunken if self._pressed else QtGui.QStyle.State_Raised
721
722                 screen.drawControl(QtGui.QStyle.CE_PushButton, option)
723                 self._buttonArtist.paintPainter(selectionIndex, screen)
724
725                 QtGui.QWidget.paintEvent(self, paintEvent)
726
727         def __iter__(self):
728                 return iter(self._filing)
729
730         def __len__(self):
731                 return len(self._filing)
732
733         def _popup_child(self, position):
734                 self._poppedUp = True
735                 self.aboutToShow.emit()
736
737                 self._delayPopupTimer.stop()
738                 self._popupLocation = None
739
740                 position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius())
741                 self._display.move(position)
742                 self._display.show()
743
744                 self.update()
745
746         def _hide_child(self):
747                 self._poppedUp = False
748                 self.aboutToHide.emit()
749                 self._display.hide()
750                 self.update()
751
752         def _select_at(self, index):
753                 self._selectionIndex = index
754
755         def _update_selection(self, lastMousePos, ignoreOuter = False):
756                 radius = _radius_at(self._cachedCenterPosition, lastMousePos)
757                 if radius < self._filing.innerRadius():
758                         self._select_at(PieFiling.SELECTION_CENTER)
759                 elif radius <= self._filing.outerRadius() or ignoreOuter:
760                         self._select_at(self.indexAt(lastMousePos))
761                 else:
762                         self._select_at(PieFiling.SELECTION_NONE)
763
764         def _activate_at(self, index):
765                 if index == PieFiling.SELECTION_NONE:
766                         self.canceled.emit()
767                         return
768                 elif index == PieFiling.SELECTION_CENTER:
769                         child = self._filing.center()
770                 else:
771                         child = self.itemAt(index)
772
773                 if child.action().isEnabled():
774                         child.action().trigger()
775                         self.activated.emit(index)
776                 else:
777                         self.canceled.emit()
778
779
780 class QPieMenu(QtGui.QWidget):
781
782         activated = qt_compat.Signal(int)
783         highlighted = qt_compat.Signal(int)
784         canceled = qt_compat.Signal()
785         aboutToShow = qt_compat.Signal()
786         aboutToHide = qt_compat.Signal()
787
788         def __init__(self, parent = None):
789                 QtGui.QWidget.__init__(self, parent)
790                 self._cachedCenterPosition = self.rect().center()
791
792                 self._filing = PieFiling()
793                 self._artist = PieArtist(self._filing)
794                 self._selectionIndex = PieFiling.SELECTION_NONE
795
796                 self._mousePosition = ()
797                 self.setFocusPolicy(QtCore.Qt.StrongFocus)
798
799         def popup(self, pos):
800                 self._update_selection(pos)
801                 self.show()
802
803         def insertItem(self, item, index = -1):
804                 self._filing.insertItem(item, index)
805                 self.update()
806
807         def removeItemAt(self, index):
808                 self._filing.removeItemAt(index)
809                 self.update()
810
811         def set_center(self, item):
812                 self._filing.set_center(item)
813                 self.update()
814
815         def clear(self):
816                 self._filing.clear()
817                 self.update()
818
819         def itemAt(self, index):
820                 return self._filing.itemAt(index)
821
822         def indexAt(self, point):
823                 return self._filing.indexAt(self._cachedCenterPosition, point)
824
825         def innerRadius(self):
826                 return self._filing.innerRadius()
827
828         def setInnerRadius(self, radius):
829                 self._filing.setInnerRadius(radius)
830                 self.update()
831
832         def outerRadius(self):
833                 return self._filing.outerRadius()
834
835         def setOuterRadius(self, radius):
836                 self._filing.setOuterRadius(radius)
837                 self.update()
838
839         def sizeHint(self):
840                 return self._artist.pieSize()
841
842         @misc_utils.log_exception(_moduleLogger)
843         def mousePressEvent(self, mouseEvent):
844                 lastSelection = self._selectionIndex
845
846                 lastMousePos = mouseEvent.pos()
847                 self._update_selection(lastMousePos)
848                 self._mousePosition = lastMousePos
849
850                 if lastSelection != self._selectionIndex:
851                         self.highlighted.emit(self._selectionIndex)
852                         self.update()
853
854         @misc_utils.log_exception(_moduleLogger)
855         def mouseMoveEvent(self, mouseEvent):
856                 lastSelection = self._selectionIndex
857
858                 lastMousePos = mouseEvent.pos()
859                 self._update_selection(lastMousePos)
860
861                 if lastSelection != self._selectionIndex:
862                         self.highlighted.emit(self._selectionIndex)
863                         self.update()
864
865         @misc_utils.log_exception(_moduleLogger)
866         def mouseReleaseEvent(self, mouseEvent):
867                 lastSelection = self._selectionIndex
868
869                 lastMousePos = mouseEvent.pos()
870                 self._update_selection(lastMousePos)
871                 self._mousePosition = ()
872
873                 self._activate_at(self._selectionIndex)
874                 self.update()
875
876         @misc_utils.log_exception(_moduleLogger)
877         def keyPressEvent(self, keyEvent):
878                 if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
879                         if self._selectionIndex != len(self._filing) - 1:
880                                 nextSelection = self._selectionIndex + 1
881                         else:
882                                 nextSelection = 0
883                         self._select_at(nextSelection)
884                         self.update()
885                 elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
886                         if 0 < self._selectionIndex:
887                                 nextSelection = self._selectionIndex - 1
888                         else:
889                                 nextSelection = len(self._filing) - 1
890                         self._select_at(nextSelection)
891                         self.update()
892                 elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
893                         self._activate_at(self._selectionIndex)
894                 elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
895                         self._activate_at(PieFiling.SELECTION_NONE)
896                 else:
897                         QtGui.QWidget.keyPressEvent(self, keyEvent)
898
899         @misc_utils.log_exception(_moduleLogger)
900         def showEvent(self, showEvent):
901                 self.aboutToShow.emit()
902                 self._cachedCenterPosition = self.rect().center()
903
904                 mask = self._artist.show(self.palette())
905                 self.setMask(mask)
906
907                 lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
908                 self._update_selection(lastMousePos)
909
910                 QtGui.QWidget.showEvent(self, showEvent)
911
912         @misc_utils.log_exception(_moduleLogger)
913         def hideEvent(self, hideEvent):
914                 self._artist.hide()
915                 self._selectionIndex = PieFiling.SELECTION_NONE
916                 QtGui.QWidget.hideEvent(self, hideEvent)
917
918         @misc_utils.log_exception(_moduleLogger)
919         def paintEvent(self, paintEvent):
920                 canvas = self._artist.paint(self._selectionIndex)
921
922                 screen = QtGui.QPainter(self)
923                 screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
924
925                 QtGui.QWidget.paintEvent(self, paintEvent)
926
927         def __iter__(self):
928                 return iter(self._filing)
929
930         def __len__(self):
931                 return len(self._filing)
932
933         def _select_at(self, index):
934                 self._selectionIndex = index
935
936         def _update_selection(self, lastMousePos):
937                 radius = _radius_at(self._cachedCenterPosition, lastMousePos)
938                 if radius < self._filing.innerRadius():
939                         self._selectionIndex = PieFiling.SELECTION_CENTER
940                 elif radius <= self._filing.outerRadius():
941                         self._select_at(self.indexAt(lastMousePos))
942                 else:
943                         self._selectionIndex = PieFiling.SELECTION_NONE
944
945         def _activate_at(self, index):
946                 if index == PieFiling.SELECTION_NONE:
947                         self.canceled.emit()
948                         self.aboutToHide.emit()
949                         self.hide()
950                         return
951                 elif index == PieFiling.SELECTION_CENTER:
952                         child = self._filing.center()
953                 else:
954                         child = self.itemAt(index)
955
956                 if child.isEnabled():
957                         child.action().trigger()
958                         self.activated.emit(index)
959                 else:
960                         self.canceled.emit()
961                 self.aboutToHide.emit()
962                 self.hide()
963
964
965 def init_pies():
966         PieFiling.NULL_CENTER.setEnabled(False)
967
968
969 def _print(msg):
970         print msg
971
972
973 def _on_about_to_hide(app):
974         app.exit()
975
976
977 if __name__ == "__main__":
978         app = QtGui.QApplication([])
979         init_pies()
980
981         if False:
982                 pie = QPieMenu()
983                 pie.show()
984
985         if False:
986                 singleAction = QtGui.QAction(None)
987                 singleAction.setText("Boo")
988                 singleItem = QActionPieItem(singleAction)
989                 spie = QPieMenu()
990                 spie.insertItem(singleItem)
991                 spie.show()
992
993         if False:
994                 oneAction = QtGui.QAction(None)
995                 oneAction.setText("Chew")
996                 oneItem = QActionPieItem(oneAction)
997                 twoAction = QtGui.QAction(None)
998                 twoAction.setText("Foo")
999                 twoItem = QActionPieItem(twoAction)
1000                 iconTextAction = QtGui.QAction(None)
1001                 iconTextAction.setText("Icon")
1002                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1003                 iconTextItem = QActionPieItem(iconTextAction)
1004                 mpie = QPieMenu()
1005                 mpie.insertItem(oneItem)
1006                 mpie.insertItem(twoItem)
1007                 mpie.insertItem(oneItem)
1008                 mpie.insertItem(iconTextItem)
1009                 mpie.show()
1010
1011         if True:
1012                 oneAction = QtGui.QAction(None)
1013                 oneAction.setText("Chew")
1014                 oneAction.triggered.connect(lambda: _print("Chew"))
1015                 oneItem = QActionPieItem(oneAction)
1016                 twoAction = QtGui.QAction(None)
1017                 twoAction.setText("Foo")
1018                 twoAction.triggered.connect(lambda: _print("Foo"))
1019                 twoItem = QActionPieItem(twoAction)
1020                 iconAction = QtGui.QAction(None)
1021                 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
1022                 iconAction.triggered.connect(lambda: _print("Icon"))
1023                 iconItem = QActionPieItem(iconAction)
1024                 iconTextAction = QtGui.QAction(None)
1025                 iconTextAction.setText("Icon")
1026                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1027                 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
1028                 iconTextItem = QActionPieItem(iconTextAction)
1029                 mpie = QPieMenu()
1030                 mpie.set_center(iconItem)
1031                 mpie.insertItem(oneItem)
1032                 mpie.insertItem(twoItem)
1033                 mpie.insertItem(oneItem)
1034                 mpie.insertItem(iconTextItem)
1035                 mpie.show()
1036                 mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
1037                 mpie.canceled.connect(lambda: _print("Canceled"))
1038
1039         if False:
1040                 oneAction = QtGui.QAction(None)
1041                 oneAction.setText("Chew")
1042                 oneAction.triggered.connect(lambda: _print("Chew"))
1043                 oneItem = QActionPieItem(oneAction)
1044                 twoAction = QtGui.QAction(None)
1045                 twoAction.setText("Foo")
1046                 twoAction.triggered.connect(lambda: _print("Foo"))
1047                 twoItem = QActionPieItem(twoAction)
1048                 iconAction = QtGui.QAction(None)
1049                 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
1050                 iconAction.triggered.connect(lambda: _print("Icon"))
1051                 iconItem = QActionPieItem(iconAction)
1052                 iconTextAction = QtGui.QAction(None)
1053                 iconTextAction.setText("Icon")
1054                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1055                 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
1056                 iconTextItem = QActionPieItem(iconTextAction)
1057                 pieFiling = PieFiling()
1058                 pieFiling.set_center(iconItem)
1059                 pieFiling.insertItem(oneItem)
1060                 pieFiling.insertItem(twoItem)
1061                 pieFiling.insertItem(oneItem)
1062                 pieFiling.insertItem(iconTextItem)
1063                 mpie = QPieDisplay(pieFiling)
1064                 mpie.show()
1065
1066         if False:
1067                 oneAction = QtGui.QAction(None)
1068                 oneAction.setText("Chew")
1069                 oneAction.triggered.connect(lambda: _print("Chew"))
1070                 oneItem = QActionPieItem(oneAction)
1071                 twoAction = QtGui.QAction(None)
1072                 twoAction.setText("Foo")
1073                 twoAction.triggered.connect(lambda: _print("Foo"))
1074                 twoItem = QActionPieItem(twoAction)
1075                 iconAction = QtGui.QAction(None)
1076                 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
1077                 iconAction.triggered.connect(lambda: _print("Icon"))
1078                 iconItem = QActionPieItem(iconAction)
1079                 iconTextAction = QtGui.QAction(None)
1080                 iconTextAction.setText("Icon")
1081                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1082                 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
1083                 iconTextItem = QActionPieItem(iconTextAction)
1084                 mpie = QPieButton(iconItem)
1085                 mpie.set_center(iconItem)
1086                 mpie.insertItem(oneItem)
1087                 mpie.insertItem(twoItem)
1088                 mpie.insertItem(oneItem)
1089                 mpie.insertItem(iconTextItem)
1090                 mpie.show()
1091                 mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
1092                 mpie.canceled.connect(lambda: _print("Canceled"))
1093
1094         app.exec_()