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