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