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