Consolidating the role of some code
[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         def __init__(self, filing):
202                 self._filing = filing
203
204                 self._cachedOuterRadius = self._filing.outerRadius()
205                 self._cachedInnerRadius = self._filing.innerRadius()
206                 canvasSize = self._cachedOuterRadius * 2 + 1
207                 self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
208                 self._mask = None
209                 self.palette = None
210
211         def pieSize(self):
212                 diameter = self._filing.outerRadius() * 2 + 1
213                 return QtCore.QSize(diameter, diameter)
214
215         def centerSize(self):
216                 painter = QtGui.QPainter(self._canvas)
217                 text = self._filing.center().action().text()
218                 fontMetrics = painter.fontMetrics()
219                 if text:
220                         textBoundingRect = fontMetrics.boundingRect(text)
221                 else:
222                         textBoundingRect = QtCore.QRect()
223                 textWidth = textBoundingRect.width()
224                 textHeight = textBoundingRect.height()
225
226                 return QtCore.QSize(
227                         textWidth + self.ICON_SIZE_DEFAULT,
228                         max(textHeight, self.ICON_SIZE_DEFAULT),
229                 )
230
231         def show(self, palette):
232                 self.palette = palette
233
234                 if (
235                         self._cachedOuterRadius != self._filing.outerRadius() or
236                         self._cachedInnerRadius != self._filing.innerRadius()
237                 ):
238                         self._cachedOuterRadius = self._filing.outerRadius()
239                         self._cachedInnerRadius = self._filing.innerRadius()
240                         self._canvas = self._canvas.scaled(self.pieSize())
241
242                 if self._mask is None:
243                         self._mask = QtGui.QBitmap(self._canvas.size())
244                         self._mask.fill(QtCore.Qt.color0)
245                         self._generate_mask(self._mask)
246                         self._canvas.setMask(self._mask)
247                 return self._mask
248
249         def hide(self):
250                 self.palette = None
251
252         def paint(self, selectionIndex):
253                 painter = QtGui.QPainter(self._canvas)
254                 painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
255
256                 adjustmentRect = self._canvas.rect().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, selectionIndex)
262                         return self._canvas
263                 elif numChildren == 1:
264                         if selectionIndex == 0 and self._filing[0].isEnabled():
265                                 painter.setBrush(self.palette.highlight())
266                         else:
267                                 painter.setBrush(self.palette.window())
268
269                         painter.fillRect(self._canvas.rect(), painter.brush())
270                 else:
271                         for i in xrange(len(self._filing)):
272                                 self._paint_slice_background(painter, adjustmentRect, i, selectionIndex)
273
274                 self._paint_center_background(painter, adjustmentRect, selectionIndex)
275                 self._paint_center_foreground(painter, selectionIndex)
276
277                 for i in xrange(len(self._filing)):
278                         self._paint_slice_foreground(painter, i, selectionIndex)
279
280                 return self._canvas
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 i == selectionIndex and self._filing[i].isEnabled():
308                         painter.setBrush(self.palette.highlight())
309                         painter.setPen(self.palette.highlight().color())
310                 else:
311                         painter.setBrush(self.palette.window())
312                         painter.setPen(self.palette.window().color())
313
314                 a = self._filing._index_to_angle(i, True)
315                 b = self._filing._index_to_angle(i + 1, True)
316                 if b < a:
317                         b += _TWOPI
318                 size = b - a
319                 if size < 0:
320                         size += _TWOPI
321
322                 startAngleInDeg = (a * 360 * 16) / _TWOPI
323                 sizeInDeg = (size * 360 * 16) / _TWOPI
324                 painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
325
326         def _paint_slice_foreground(self, painter, i, selectionIndex):
327                 child = self._filing[i]
328
329                 a = self._filing._index_to_angle(i, True)
330                 b = self._filing._index_to_angle(i + 1, True)
331                 if b < a:
332                         b += _TWOPI
333                 middleAngle = (a + b) / 2
334                 averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2
335
336                 sliceX = averageRadius * math.cos(middleAngle)
337                 sliceY = - averageRadius * math.sin(middleAngle)
338
339                 piePos = self._canvas.rect().center()
340                 pieX = piePos.x()
341                 pieY = piePos.y()
342                 self._paint_label(
343                         painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
344                 )
345
346         def _paint_label(self, painter, action, isSelected, x, y):
347                 text = action.text()
348                 fontMetrics = painter.fontMetrics()
349                 if text:
350                         textBoundingRect = fontMetrics.boundingRect(text)
351                 else:
352                         textBoundingRect = QtCore.QRect()
353                 textWidth = textBoundingRect.width()
354                 textHeight = textBoundingRect.height()
355
356                 icon = action.icon().pixmap(
357                         QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
358                         QtGui.QIcon.Normal,
359                         QtGui.QIcon.On,
360                 )
361                 iconWidth = icon.width()
362                 iconHeight = icon.width()
363                 averageWidth = (iconWidth + textWidth)/2
364                 if not icon.isNull():
365                         iconRect = QtCore.QRect(
366                                 x - averageWidth,
367                                 y - iconHeight/2,
368                                 iconWidth,
369                                 iconHeight,
370                         )
371
372                         painter.drawPixmap(iconRect, icon)
373
374                 if text:
375                         if isSelected:
376                                 if action.isEnabled():
377                                         pen = self.palette.highlightedText()
378                                         brush = self.palette.highlight()
379                                 else:
380                                         pen = self.palette.mid()
381                                         brush = self.palette.window()
382                         else:
383                                 if action.isEnabled():
384                                         pen = self.palette.windowText()
385                                 else:
386                                         pen = self.palette.mid()
387                                 brush = self.palette.window()
388
389                         leftX = x - averageWidth + iconWidth
390                         topY = y + textHeight/2
391                         painter.setPen(pen.color())
392                         painter.setBrush(brush)
393                         painter.drawText(leftX, topY, text)
394
395         def _paint_center_background(self, painter, adjustmentRect, selectionIndex):
396                 if len(self._filing) == 0:
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(self._canvas.rect())
404                 else:
405                         dark = self.palette.mid().color()
406                         light = self.palette.light().color()
407                         if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
408                                 background = self.palette.highlight().color()
409                         else:
410                                 background = self.palette.window().color()
411
412                         innerRadius = self._cachedInnerRadius
413                         adjustmentCenterPos = adjustmentRect.center()
414                         innerRect = QtCore.QRect(
415                                 adjustmentCenterPos.x() - innerRadius,
416                                 adjustmentCenterPos.y() - innerRadius,
417                                 innerRadius * 2 + 1,
418                                 innerRadius * 2 + 1,
419                         )
420
421                         painter.setPen(QtCore.Qt.NoPen)
422                         painter.setBrush(background)
423                         painter.drawPie(innerRect, 0, 360 * 16)
424
425                         if self.DEFAULT_SHAPE == self.SHAPE_SQUARE:
426                                 pass
427                         elif self.DEFAULT_SHAPE == self.SHAPE_CIRCLE:
428                                 painter.setPen(QtGui.QPen(dark, 1))
429                                 painter.setBrush(QtCore.Qt.NoBrush)
430                                 painter.drawEllipse(adjustmentRect)
431                         else:
432                                 raise NotImplementedError(self.DEFAULT_SHAPE)
433
434         def _paint_center_foreground(self, painter, selectionIndex):
435                 centerPos = self._canvas.rect().center()
436                 pieX = centerPos.x()
437                 pieY = centerPos.y()
438
439                 x = pieX
440                 y = pieY
441
442                 self._paint_label(
443                         painter,
444                         self._filing.center().action(),
445                         selectionIndex == PieFiling.SELECTION_CENTER,
446                         x, y
447                 )
448
449
450 class QPieDisplay(QtGui.QWidget):
451
452         def __init__(self, filing, parent = None, flags = QtCore.Qt.Window):
453                 QtGui.QWidget.__init__(self, parent, flags)
454                 self._filing = filing
455                 self._artist = PieArtist(self._filing)
456                 self._selectionIndex = PieFiling.SELECTION_NONE
457
458         def popup(self, pos):
459                 self._update_selection(pos)
460                 self.show()
461
462         def sizeHint(self):
463                 return self._artist.pieSize()
464
465         @misc_utils.log_exception(_moduleLogger)
466         def showEvent(self, showEvent):
467                 mask = self._artist.show(self.palette())
468                 self.setMask(mask)
469
470                 QtGui.QWidget.showEvent(self, showEvent)
471
472         @misc_utils.log_exception(_moduleLogger)
473         def hideEvent(self, hideEvent):
474                 self._artist.hide()
475                 self._selectionIndex = PieFiling.SELECTION_NONE
476                 QtGui.QWidget.hideEvent(self, hideEvent)
477
478         @misc_utils.log_exception(_moduleLogger)
479         def paintEvent(self, paintEvent):
480                 canvas = self._artist.paint(self._selectionIndex)
481                 offset = (self.size() - canvas.size()) / 2
482
483                 screen = QtGui.QPainter(self)
484                 screen.drawPixmap(QtCore.QPoint(offset.width(), offset.height()), canvas)
485
486                 QtGui.QWidget.paintEvent(self, paintEvent)
487
488         def selectAt(self, index):
489                 oldIndex = self._selectionIndex
490                 self._selectionIndex = index
491                 if self.isVisible():
492                         self.update()
493
494
495 class QPieButton(QtGui.QWidget):
496
497         activated = QtCore.pyqtSignal(int)
498         highlighted = QtCore.pyqtSignal(int)
499         canceled = QtCore.pyqtSignal()
500         aboutToShow = QtCore.pyqtSignal()
501         aboutToHide = QtCore.pyqtSignal()
502
503         BUTTON_RADIUS = 24
504         DELAY = 250
505
506         def __init__(self, buttonSlice, parent = None, buttonSlices = None):
507                 # @bug Artifacts on Maemo 5 due to window 3D effects, find way to disable them for just these?
508                 # @bug The pie's are being pushed back on screen on Maemo, leading to coordinate issues
509                 QtGui.QWidget.__init__(self, parent)
510                 self._cachedCenterPosition = self.rect().center()
511
512                 self._filing = PieFiling()
513                 self._display = QPieDisplay(self._filing, None, QtCore.Qt.SplashScreen)
514                 self._selectionIndex = PieFiling.SELECTION_NONE
515
516                 self._buttonFiling = PieFiling()
517                 self._buttonFiling.set_center(buttonSlice)
518                 if buttonSlices is not None:
519                         for slice in buttonSlices:
520                                 self._buttonFiling.insertItem(slice)
521                 self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS)
522                 self._buttonArtist = PieArtist(self._buttonFiling)
523                 self._poppedUp = False
524
525                 self._delayPopupTimer = QtCore.QTimer()
526                 self._delayPopupTimer.setInterval(self.DELAY)
527                 self._delayPopupTimer.setSingleShot(True)
528                 self._delayPopupTimer.timeout.connect(self._on_delayed_popup)
529                 self._popupLocation = None
530
531                 self._mousePosition = None
532                 self.setFocusPolicy(QtCore.Qt.StrongFocus)
533                 self.setSizePolicy(
534                         QtGui.QSizePolicy(
535                                 QtGui.QSizePolicy.MinimumExpanding,
536                                 QtGui.QSizePolicy.MinimumExpanding,
537                         )
538                 )
539
540         def insertItem(self, item, index = -1):
541                 self._filing.insertItem(item, index)
542
543         def removeItemAt(self, index):
544                 self._filing.removeItemAt(index)
545
546         def set_center(self, item):
547                 self._filing.set_center(item)
548
549         def set_button(self, item):
550                 self.update()
551
552         def clear(self):
553                 self._filing.clear()
554
555         def itemAt(self, index):
556                 return self._filing.itemAt(index)
557
558         def indexAt(self, point):
559                 return self._filing.indexAt(self._cachedCenterPosition, point)
560
561         def innerRadius(self):
562                 return self._filing.innerRadius()
563
564         def setInnerRadius(self, radius):
565                 self._filing.setInnerRadius(radius)
566
567         def outerRadius(self):
568                 return self._filing.outerRadius()
569
570         def setOuterRadius(self, radius):
571                 self._filing.setOuterRadius(radius)
572
573         def buttonRadius(self):
574                 return self._buttonFiling.outerRadius()
575
576         def setButtonRadius(self, radius):
577                 self._buttonFiling.setOuterRadius(radius)
578                 self._buttonFiling.setInnerRadius(radius / 2)
579                 self._buttonArtist.show(self.palette())
580
581         def minimumSizeHint(self):
582                 return self._buttonArtist.centerSize()
583
584         @misc_utils.log_exception(_moduleLogger)
585         def mousePressEvent(self, mouseEvent):
586                 lastSelection = self._selectionIndex
587
588                 lastMousePos = mouseEvent.pos()
589                 self._mousePosition = lastMousePos
590                 self._update_selection(self._cachedCenterPosition)
591
592                 self.highlighted.emit(self._selectionIndex)
593
594                 self._display.selectAt(self._selectionIndex)
595                 self._popupLocation = mouseEvent.globalPos()
596                 self._delayPopupTimer.start()
597
598         @misc_utils.log_exception(_moduleLogger)
599         def _on_delayed_popup(self):
600                 assert self._popupLocation is not None, "Widget location abuse"
601                 self._popup_child(self._popupLocation)
602
603         @misc_utils.log_exception(_moduleLogger)
604         def mouseMoveEvent(self, mouseEvent):
605                 lastSelection = self._selectionIndex
606
607                 lastMousePos = mouseEvent.pos()
608                 if self._mousePosition is None:
609                         # Absolute
610                         self._update_selection(lastMousePos)
611                 else:
612                         # Relative
613                         self._update_selection(
614                                 self._cachedCenterPosition + (lastMousePos - self._mousePosition),
615                                 ignoreOuter = True,
616                         )
617
618                 if lastSelection != self._selectionIndex:
619                         self.highlighted.emit(self._selectionIndex)
620                         self._display.selectAt(self._selectionIndex)
621
622                 if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive():
623                         self._on_delayed_popup()
624
625         @misc_utils.log_exception(_moduleLogger)
626         def mouseReleaseEvent(self, mouseEvent):
627                 self._delayPopupTimer.stop()
628                 self._popupLocation = None
629
630                 lastSelection = self._selectionIndex
631
632                 lastMousePos = mouseEvent.pos()
633                 if self._mousePosition is None:
634                         # Absolute
635                         self._update_selection(lastMousePos)
636                 else:
637                         # Relative
638                         self._update_selection(
639                                 self._cachedCenterPosition + (lastMousePos - self._mousePosition),
640                                 ignoreOuter = True,
641                         )
642                 self._mousePosition = None
643
644                 self._activate_at(self._selectionIndex)
645                 self._hide_child()
646
647         @misc_utils.log_exception(_moduleLogger)
648         def keyPressEvent(self, keyEvent):
649                 if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
650                         self._popup_child(QtGui.QCursor.pos())
651                         if self._selectionIndex != len(self._filing) - 1:
652                                 nextSelection = self._selectionIndex + 1
653                         else:
654                                 nextSelection = 0
655                         self._select_at(nextSelection)
656                         self._display.selectAt(self._selectionIndex)
657                 elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
658                         self._popup_child(QtGui.QCursor.pos())
659                         if 0 < self._selectionIndex:
660                                 nextSelection = self._selectionIndex - 1
661                         else:
662                                 nextSelection = len(self._filing) - 1
663                         self._select_at(nextSelection)
664                         self._display.selectAt(self._selectionIndex)
665                 elif keyEvent.key() in [QtCore.Qt.Key_Space]:
666                         self._popup_child(QtGui.QCursor.pos())
667                         self._select_at(PieFiling.SELECTION_CENTER)
668                         self._display.selectAt(self._selectionIndex)
669                 elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
670                         self._delayPopupTimer.stop()
671                         self._popupLocation = None
672                         self._activate_at(self._selectionIndex)
673                         self._hide_child()
674                 elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
675                         self._delayPopupTimer.stop()
676                         self._popupLocation = None
677                         self._activate_at(PieFiling.SELECTION_NONE)
678                         self._hide_child()
679                 else:
680                         QtGui.QWidget.keyPressEvent(self, keyEvent)
681
682         @misc_utils.log_exception(_moduleLogger)
683         def resizeEvent(self, resizeEvent):
684                 self.setButtonRadius(min(resizeEvent.size().width(), resizeEvent.size().height()) / 2 - 1)
685                 QtGui.QWidget.resizeEvent(self, resizeEvent)
686
687         @misc_utils.log_exception(_moduleLogger)
688         def showEvent(self, showEvent):
689                 self._buttonArtist.show(self.palette())
690                 self._cachedCenterPosition = self.rect().center()
691
692                 QtGui.QWidget.showEvent(self, showEvent)
693
694         @misc_utils.log_exception(_moduleLogger)
695         def hideEvent(self, hideEvent):
696                 self._display.hide()
697                 self._select_at(PieFiling.SELECTION_NONE)
698                 QtGui.QWidget.hideEvent(self, hideEvent)
699
700         @misc_utils.log_exception(_moduleLogger)
701         def paintEvent(self, paintEvent):
702                 self.setButtonRadius(min(self.rect().width(), self.rect().height()) / 2 - 1)
703                 if self._poppedUp:
704                         canvas = self._buttonArtist.paint(PieFiling.SELECTION_CENTER)
705                 else:
706                         canvas = self._buttonArtist.paint(PieFiling.SELECTION_NONE)
707                 offset = (self.size() - canvas.size()) / 2
708
709                 screen = QtGui.QPainter(self)
710                 screen.drawPixmap(QtCore.QPoint(offset.width(), offset.height()), canvas)
711
712                 QtGui.QWidget.paintEvent(self, paintEvent)
713
714         def __iter__(self):
715                 return iter(self._filing)
716
717         def __len__(self):
718                 return len(self._filing)
719
720         def _popup_child(self, position):
721                 self._poppedUp = True
722                 self.aboutToShow.emit()
723
724                 self._delayPopupTimer.stop()
725                 self._popupLocation = None
726
727                 position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius())
728                 self._display.move(position)
729                 self._display.show()
730
731                 self.update()
732
733         def _hide_child(self):
734                 self._poppedUp = False
735                 self.aboutToHide.emit()
736                 self._display.hide()
737                 self.update()
738
739         def _select_at(self, index):
740                 self._selectionIndex = index
741
742         def _update_selection(self, lastMousePos, ignoreOuter = False):
743                 radius = _radius_at(self._cachedCenterPosition, lastMousePos)
744                 if radius < self._filing.innerRadius():
745                         self._select_at(PieFiling.SELECTION_CENTER)
746                 elif radius <= self._filing.outerRadius() or ignoreOuter:
747                         self._select_at(self.indexAt(lastMousePos))
748                 else:
749                         self._select_at(PieFiling.SELECTION_NONE)
750
751         def _activate_at(self, index):
752                 if index == PieFiling.SELECTION_NONE:
753                         self.canceled.emit()
754                         return
755                 elif index == PieFiling.SELECTION_CENTER:
756                         child = self._filing.center()
757                 else:
758                         child = self.itemAt(index)
759
760                 if child.action().isEnabled():
761                         child.action().trigger()
762                         self.activated.emit(index)
763                 else:
764                         self.canceled.emit()
765
766
767 class QPieMenu(QtGui.QWidget):
768
769         activated = QtCore.pyqtSignal(int)
770         highlighted = QtCore.pyqtSignal(int)
771         canceled = QtCore.pyqtSignal()
772         aboutToShow = QtCore.pyqtSignal()
773         aboutToHide = QtCore.pyqtSignal()
774
775         def __init__(self, parent = None):
776                 QtGui.QWidget.__init__(self, parent)
777                 self._cachedCenterPosition = self.rect().center()
778
779                 self._filing = PieFiling()
780                 self._artist = PieArtist(self._filing)
781                 self._selectionIndex = PieFiling.SELECTION_NONE
782
783                 self._mousePosition = ()
784                 self.setFocusPolicy(QtCore.Qt.StrongFocus)
785
786         def popup(self, pos):
787                 self._update_selection(pos)
788                 self.show()
789
790         def insertItem(self, item, index = -1):
791                 self._filing.insertItem(item, index)
792                 self.update()
793
794         def removeItemAt(self, index):
795                 self._filing.removeItemAt(index)
796                 self.update()
797
798         def set_center(self, item):
799                 self._filing.set_center(item)
800                 self.update()
801
802         def clear(self):
803                 self._filing.clear()
804                 self.update()
805
806         def itemAt(self, index):
807                 return self._filing.itemAt(index)
808
809         def indexAt(self, point):
810                 return self._filing.indexAt(self._cachedCenterPosition, point)
811
812         def innerRadius(self):
813                 return self._filing.innerRadius()
814
815         def setInnerRadius(self, radius):
816                 self._filing.setInnerRadius(radius)
817                 self.update()
818
819         def outerRadius(self):
820                 return self._filing.outerRadius()
821
822         def setOuterRadius(self, radius):
823                 self._filing.setOuterRadius(radius)
824                 self.update()
825
826         def sizeHint(self):
827                 return self._artist.pieSize()
828
829         @misc_utils.log_exception(_moduleLogger)
830         def mousePressEvent(self, mouseEvent):
831                 lastSelection = self._selectionIndex
832
833                 lastMousePos = mouseEvent.pos()
834                 self._update_selection(lastMousePos)
835                 self._mousePosition = lastMousePos
836
837                 if lastSelection != self._selectionIndex:
838                         self.highlighted.emit(self._selectionIndex)
839                         self.update()
840
841         @misc_utils.log_exception(_moduleLogger)
842         def mouseMoveEvent(self, mouseEvent):
843                 lastSelection = self._selectionIndex
844
845                 lastMousePos = mouseEvent.pos()
846                 self._update_selection(lastMousePos)
847
848                 if lastSelection != self._selectionIndex:
849                         self.highlighted.emit(self._selectionIndex)
850                         self.update()
851
852         @misc_utils.log_exception(_moduleLogger)
853         def mouseReleaseEvent(self, mouseEvent):
854                 lastSelection = self._selectionIndex
855
856                 lastMousePos = mouseEvent.pos()
857                 self._update_selection(lastMousePos)
858                 self._mousePosition = ()
859
860                 self._activate_at(self._selectionIndex)
861                 self.update()
862
863         @misc_utils.log_exception(_moduleLogger)
864         def keyPressEvent(self, keyEvent):
865                 if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
866                         if self._selectionIndex != len(self._filing) - 1:
867                                 nextSelection = self._selectionIndex + 1
868                         else:
869                                 nextSelection = 0
870                         self._select_at(nextSelection)
871                         self.update()
872                 elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
873                         if 0 < self._selectionIndex:
874                                 nextSelection = self._selectionIndex - 1
875                         else:
876                                 nextSelection = len(self._filing) - 1
877                         self._select_at(nextSelection)
878                         self.update()
879                 elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
880                         self._activate_at(self._selectionIndex)
881                 elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
882                         self._activate_at(PieFiling.SELECTION_NONE)
883                 else:
884                         QtGui.QWidget.keyPressEvent(self, keyEvent)
885
886         @misc_utils.log_exception(_moduleLogger)
887         def showEvent(self, showEvent):
888                 self.aboutToShow.emit()
889                 self._cachedCenterPosition = self.rect().center()
890
891                 mask = self._artist.show(self.palette())
892                 self.setMask(mask)
893
894                 lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
895                 self._update_selection(lastMousePos)
896
897                 QtGui.QWidget.showEvent(self, showEvent)
898
899         @misc_utils.log_exception(_moduleLogger)
900         def hideEvent(self, hideEvent):
901                 self._artist.hide()
902                 self._selectionIndex = PieFiling.SELECTION_NONE
903                 QtGui.QWidget.hideEvent(self, hideEvent)
904
905         @misc_utils.log_exception(_moduleLogger)
906         def paintEvent(self, paintEvent):
907                 canvas = self._artist.paint(self._selectionIndex)
908
909                 screen = QtGui.QPainter(self)
910                 screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
911
912                 QtGui.QWidget.paintEvent(self, paintEvent)
913
914         def __iter__(self):
915                 return iter(self._filing)
916
917         def __len__(self):
918                 return len(self._filing)
919
920         def _select_at(self, index):
921                 self._selectionIndex = index
922
923         def _update_selection(self, lastMousePos):
924                 radius = _radius_at(self._cachedCenterPosition, lastMousePos)
925                 if radius < self._filing.innerRadius():
926                         self._selectionIndex = PieFiling.SELECTION_CENTER
927                 elif radius <= self._filing.outerRadius():
928                         self._select_at(self.indexAt(lastMousePos))
929                 else:
930                         self._selectionIndex = PieFiling.SELECTION_NONE
931
932         def _activate_at(self, index):
933                 if index == PieFiling.SELECTION_NONE:
934                         self.canceled.emit()
935                         self.aboutToHide.emit()
936                         self.hide()
937                         return
938                 elif index == PieFiling.SELECTION_CENTER:
939                         child = self._filing.center()
940                 else:
941                         child = self.itemAt(index)
942
943                 if child.isEnabled():
944                         child.action().trigger()
945                         self.activated.emit(index)
946                 else:
947                         self.canceled.emit()
948                 self.aboutToHide.emit()
949                 self.hide()
950
951
952 def init_pies():
953         PieFiling.NULL_CENTER.setEnabled(False)
954
955
956 def _print(msg):
957         print msg
958
959
960 def _on_about_to_hide(app):
961         app.exit()
962
963
964 if __name__ == "__main__":
965         app = QtGui.QApplication([])
966         init_pies()
967
968         if False:
969                 pie = QPieMenu()
970                 pie.show()
971
972         if False:
973                 singleAction = QtGui.QAction(None)
974                 singleAction.setText("Boo")
975                 singleItem = QActionPieItem(singleAction)
976                 spie = QPieMenu()
977                 spie.insertItem(singleItem)
978                 spie.show()
979
980         if False:
981                 oneAction = QtGui.QAction(None)
982                 oneAction.setText("Chew")
983                 oneItem = QActionPieItem(oneAction)
984                 twoAction = QtGui.QAction(None)
985                 twoAction.setText("Foo")
986                 twoItem = QActionPieItem(twoAction)
987                 iconTextAction = QtGui.QAction(None)
988                 iconTextAction.setText("Icon")
989                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
990                 iconTextItem = QActionPieItem(iconTextAction)
991                 mpie = QPieMenu()
992                 mpie.insertItem(oneItem)
993                 mpie.insertItem(twoItem)
994                 mpie.insertItem(oneItem)
995                 mpie.insertItem(iconTextItem)
996                 mpie.show()
997
998         if True:
999                 oneAction = QtGui.QAction(None)
1000                 oneAction.setText("Chew")
1001                 oneAction.triggered.connect(lambda: _print("Chew"))
1002                 oneItem = QActionPieItem(oneAction)
1003                 twoAction = QtGui.QAction(None)
1004                 twoAction.setText("Foo")
1005                 twoAction.triggered.connect(lambda: _print("Foo"))
1006                 twoItem = QActionPieItem(twoAction)
1007                 iconAction = QtGui.QAction(None)
1008                 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
1009                 iconAction.triggered.connect(lambda: _print("Icon"))
1010                 iconItem = QActionPieItem(iconAction)
1011                 iconTextAction = QtGui.QAction(None)
1012                 iconTextAction.setText("Icon")
1013                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1014                 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
1015                 iconTextItem = QActionPieItem(iconTextAction)
1016                 mpie = QPieMenu()
1017                 mpie.set_center(iconItem)
1018                 mpie.insertItem(oneItem)
1019                 mpie.insertItem(twoItem)
1020                 mpie.insertItem(oneItem)
1021                 mpie.insertItem(iconTextItem)
1022                 mpie.show()
1023                 mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
1024                 mpie.canceled.connect(lambda: _print("Canceled"))
1025
1026         if False:
1027                 oneAction = QtGui.QAction(None)
1028                 oneAction.setText("Chew")
1029                 oneAction.triggered.connect(lambda: _print("Chew"))
1030                 oneItem = QActionPieItem(oneAction)
1031                 twoAction = QtGui.QAction(None)
1032                 twoAction.setText("Foo")
1033                 twoAction.triggered.connect(lambda: _print("Foo"))
1034                 twoItem = QActionPieItem(twoAction)
1035                 iconAction = QtGui.QAction(None)
1036                 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
1037                 iconAction.triggered.connect(lambda: _print("Icon"))
1038                 iconItem = QActionPieItem(iconAction)
1039                 iconTextAction = QtGui.QAction(None)
1040                 iconTextAction.setText("Icon")
1041                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1042                 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
1043                 iconTextItem = QActionPieItem(iconTextAction)
1044                 pieFiling = PieFiling()
1045                 pieFiling.set_center(iconItem)
1046                 pieFiling.insertItem(oneItem)
1047                 pieFiling.insertItem(twoItem)
1048                 pieFiling.insertItem(oneItem)
1049                 pieFiling.insertItem(iconTextItem)
1050                 mpie = QPieDisplay(pieFiling)
1051                 mpie.show()
1052
1053         if False:
1054                 oneAction = QtGui.QAction(None)
1055                 oneAction.setText("Chew")
1056                 oneAction.triggered.connect(lambda: _print("Chew"))
1057                 oneItem = QActionPieItem(oneAction)
1058                 twoAction = QtGui.QAction(None)
1059                 twoAction.setText("Foo")
1060                 twoAction.triggered.connect(lambda: _print("Foo"))
1061                 twoItem = QActionPieItem(twoAction)
1062                 iconAction = QtGui.QAction(None)
1063                 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
1064                 iconAction.triggered.connect(lambda: _print("Icon"))
1065                 iconItem = QActionPieItem(iconAction)
1066                 iconTextAction = QtGui.QAction(None)
1067                 iconTextAction.setText("Icon")
1068                 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
1069                 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
1070                 iconTextItem = QActionPieItem(iconTextAction)
1071                 mpie = QPieButton(iconItem)
1072                 mpie.set_center(iconItem)
1073                 mpie.insertItem(oneItem)
1074                 mpie.insertItem(twoItem)
1075                 mpie.insertItem(oneItem)
1076                 mpie.insertItem(iconTextItem)
1077                 mpie.show()
1078                 mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
1079                 mpie.canceled.connect(lambda: _print("Canceled"))
1080
1081         app.exec_()