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