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