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