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