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