Adding a delay to poping up the pie menu
[ejpi] / src / libraries / qtpie.py
index 66899ac..cab868b 100755 (executable)
@@ -1,10 +1,51 @@
 #!/usr/bin/env python
 
 import math
+import logging
 
 from PyQt4 import QtGui
 from PyQt4 import QtCore
 
+try:
+       from util import misc as misc_utils
+except ImportError:
+       class misc_utils(object):
+
+               @staticmethod
+               def log_exception(logger):
+
+                       def wrapper(func):
+                               return func
+                       return wrapper
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+_TWOPI = 2 * math.pi
+
+
+def _radius_at(center, pos):
+       delta = pos - center
+       xDelta = delta.x()
+       yDelta = delta.y()
+
+       radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
+       return radius
+
+
+def _angle_at(center, pos):
+       delta = pos - center
+       xDelta = delta.x()
+       yDelta = delta.y()
+
+       radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
+       angle = math.acos(xDelta / radius)
+       if 0 <= yDelta:
+               angle = _TWOPI - angle
+
+       return angle
+
 
 class QActionPieItem(object):
 
@@ -28,61 +69,51 @@ class QActionPieItem(object):
                return self._action.isEnabled()
 
 
-class QPieMenu(QtGui.QWidget):
+class PieFiling(object):
 
-       INNER_RADIUS_DEFAULT = 24
-       OUTER_RADIUS_DEFAULT = 64
-       ICON_SIZE_DEFAULT = 32
+       INNER_RADIUS_DEFAULT = 32
+       OUTER_RADIUS_DEFAULT = 128
 
-       activated = QtCore.pyqtSignal((), (int, ))
-       highlighted = QtCore.pyqtSignal(int)
-       canceled = QtCore.pyqtSignal()
-       aboutToShow = QtCore.pyqtSignal()
-       aboutToHide = QtCore.pyqtSignal()
+       SELECTION_CENTER = -1
+       SELECTION_NONE = -2
 
-       def __init__(self, parent = None):
-               QtGui.QWidget.__init__(self, parent)
+       NULL_CENTER = QActionPieItem(QtGui.QAction(None))
+
+       def __init__(self):
                self._innerRadius = self.INNER_RADIUS_DEFAULT
                self._outerRadius = self.OUTER_RADIUS_DEFAULT
                self._children = []
-               self._selectionIndex = -2
+               self._center = self.NULL_CENTER
 
-               self._motion = 0
-               self._mouseButtonPressed = True
-               self._mousePosition = ()
-
-               canvasSize = self._outerRadius * 2 + 1
-               self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
-               self._mask = None
-
-       def popup(self, pos):
-               index = self.indexAt(self.mapFromGlobal(QtGui.QCursor.pos()))
-               self._mousePosition = pos
-               self.show()
+               self._cacheIndexToAngle = {}
+               self._cacheTotalWeight = 0
 
        def insertItem(self, item, index = -1):
                self._children.insert(index, item)
-               self._invalidate_view()
+               self._invalidate_cache()
 
        def removeItemAt(self, index):
                item = self._children.pop(index)
-               self._invalidate_view()
+               self._invalidate_cache()
+
+       def set_center(self, item):
+               if item is None:
+                       item = self.NULL_CENTER
+               self._center = item
+
+       def center(self):
+               return self._center
 
        def clear(self):
                del self._children[:]
-               self._invalidate_view()
+               self._center = self.NULL_CENTER
+               self._invalidate_cache()
 
        def itemAt(self, index):
                return self._children[index]
 
-       def indexAt(self, point):
-               return self._angle_to_index(self._angle_at(point))
-
-       def setHighlightedItem(self, index):
-               pass
-
-       def highlightedItem(self):
-               pass
+       def indexAt(self, center, point):
+               return self._angle_to_index(_angle_at(center, point))
 
        def innerRadius(self):
                return self._innerRadius
@@ -95,87 +126,273 @@ class QPieMenu(QtGui.QWidget):
 
        def setOuterRadius(self, radius):
                self._outerRadius = radius
-               self._canvas = self._canvas.scaled(self.sizeHint())
 
-       def sizeHint(self):
-               diameter = self._outerRadius * 2 + 1
+       def __iter__(self):
+               return iter(self._children)
+
+       def __len__(self):
+               return len(self._children)
+
+       def __getitem__(self, index):
+               return self._children[index]
+
+       def _invalidate_cache(self):
+               self._cacheIndexToAngle.clear()
+               self._cacheTotalWeight = sum(child.weight() for child in self._children)
+               if self._cacheTotalWeight == 0:
+                       self._cacheTotalWeight = 1
+
+       def _index_to_angle(self, index, isShifted):
+               key = index, isShifted
+               if key in self._cacheIndexToAngle:
+                       return self._cacheIndexToAngle[key]
+               index = index % len(self._children)
+
+               baseAngle = _TWOPI / self._cacheTotalWeight
+
+               angle = math.pi / 2
+               if isShifted:
+                       if self._children:
+                               angle -= (self._children[0].weight() * baseAngle) / 2
+                       else:
+                               angle -= baseAngle / 2
+               while angle < 0:
+                       angle += _TWOPI
+
+               for i, child in enumerate(self._children):
+                       if index < i:
+                               break
+                       angle += child.weight() * baseAngle
+               while _TWOPI < angle:
+                       angle -= _TWOPI
+
+               self._cacheIndexToAngle[key] = angle
+               return angle
+
+       def _angle_to_index(self, angle):
+               numChildren = len(self._children)
+               if numChildren == 0:
+                       return self.SELECTION_CENTER
+
+               baseAngle = _TWOPI / self._cacheTotalWeight
+
+               iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
+               while iterAngle < 0:
+                       iterAngle += _TWOPI
+
+               oldIterAngle = iterAngle
+               for index, child in enumerate(self._children):
+                       iterAngle += child.weight() * baseAngle
+                       if oldIterAngle < angle and angle <= iterAngle:
+                               return index - 1 if index != 0 else numChildren - 1
+                       elif oldIterAngle < (angle + _TWOPI) and (angle + _TWOPI <= iterAngle):
+                               return index - 1 if index != 0 else numChildren - 1
+                       oldIterAngle = iterAngle
+
+
+class PieArtist(object):
+
+       ICON_SIZE_DEFAULT = 32
+
+       def __init__(self, filing):
+               self._filing = filing
+
+               self._cachedOuterRadius = self._filing.outerRadius()
+               self._cachedInnerRadius = self._filing.innerRadius()
+               canvasSize = self._cachedOuterRadius * 2 + 1
+               self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
+               self._mask = None
+               self.palette = None
+
+       def pieSize(self):
+               diameter = self._filing.outerRadius() * 2 + 1
                return QtCore.QSize(diameter, diameter)
 
-       def showEvent(self, showEvent):
-               self.aboutToShow.emit()
+       def centerSize(self):
+               painter = QtGui.QPainter(self._canvas)
+               text = self._filing.center().action().text()
+               fontMetrics = painter.fontMetrics()
+               if text:
+                       textBoundingRect = fontMetrics.boundingRect(text)
+               else:
+                       textBoundingRect = QtCore.QRect()
+               textWidth = textBoundingRect.width()
+               textHeight = textBoundingRect.height()
+
+               return QtCore.QSize(
+                       textWidth + self.ICON_SIZE_DEFAULT,
+                       max(textHeight, self.ICON_SIZE_DEFAULT),
+               )
+
+       def show(self, palette):
+               self.palette = palette
+
+               if (
+                       self._cachedOuterRadius != self._filing.outerRadius() or
+                       self._cachedInnerRadius != self._filing.innerRadius()
+               ):
+                       self._cachedOuterRadius = self._filing.outerRadius()
+                       self._cachedInnerRadius = self._filing.innerRadius()
+                       self._canvas = self._canvas.scaled(self.pieSize())
 
                if self._mask is None:
                        self._mask = QtGui.QBitmap(self._canvas.size())
                        self._mask.fill(QtCore.Qt.color0)
                        self._generate_mask(self._mask)
                        self._canvas.setMask(self._mask)
-                       self.setMask(self._mask)
+               return self._mask
 
-               self._motion = 0
+       def hide(self):
+               self.palette = None
 
-               lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
-               radius = self._radius_at(lastMousePos)
-               if self._innerRadius <= radius and radius <= self._outerRadius:
-                       self._select_at(self._angle_to_index(lastMousePos))
-               else:
-                       if radius < self._innerRadius:
-                               self._selectionIndex = -1
-                       else:
-                               self._selectionIndex = -2
-
-               QtGui.QWidget.showEvent(self, showEvent)
-
-       def hideEvent(self, hideEvent):
-               self.canceled.emit()
-               self._selectionIndex = -2
-               QtGui.QWidget.hideEvent(self, hideEvent)
-
-       def paintEvent(self, paintEvent):
+       def paint(self, selectionIndex):
                painter = QtGui.QPainter(self._canvas)
                painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
 
                adjustmentRect = self._canvas.rect().adjusted(0, 0, -1, -1)
 
-               numChildren = len(self._children)
-               if numChildren < 2:
-                       if self._selectionIndex == 0 and self._children[0].isEnabled():
-                               painter.setBrush(self.palette().highlight())
+               numChildren = len(self._filing)
+               if numChildren == 0:
+                       if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
+                               painter.setBrush(self.palette.highlight())
+                       else:
+                               painter.setBrush(self.palette.background())
+                       painter.setPen(self.palette.mid().color())
+
+                       painter.drawRect(self._canvas.rect())
+                       self._paint_center_foreground(painter, selectionIndex)
+                       return self._canvas
+               elif numChildren == 1:
+                       if selectionIndex == 0 and self._filing[0].isEnabled():
+                               painter.setBrush(self.palette.highlight())
                        else:
-                               painter.setBrush(self.palette().background())
+                               painter.setBrush(self.palette.background())
 
-                       painter.fillRect(self.rect(), painter.brush())
+                       painter.fillRect(self._canvas.rect(), painter.brush())
                else:
-                       for i, child in enumerate(self._children):
-                               if i == self._selectionIndex:
-                                       painter.setBrush(self.palette().highlight())
+                       for i in xrange(len(self._filing)):
+                               self._paint_slice_background(painter, adjustmentRect, i, selectionIndex)
+
+               self._paint_center_background(painter, adjustmentRect, selectionIndex)
+               self._paint_center_foreground(painter, selectionIndex)
+
+               for i in xrange(len(self._filing)):
+                       self._paint_slice_foreground(painter, i, selectionIndex)
+
+               return self._canvas
+
+       def _generate_mask(self, mask):
+               """
+               Specifies on the mask the shape of the pie menu
+               """
+               painter = QtGui.QPainter(mask)
+               painter.setPen(QtCore.Qt.color1)
+               painter.setBrush(QtCore.Qt.color1)
+               painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
+
+       def _paint_slice_background(self, painter, adjustmentRect, i, selectionIndex):
+               if i == selectionIndex and self._filing[i].isEnabled():
+                       painter.setBrush(self.palette.highlight())
+               else:
+                       painter.setBrush(self.palette.background())
+               painter.setPen(self.palette.mid().color())
+
+               a = self._filing._index_to_angle(i, True)
+               b = self._filing._index_to_angle(i + 1, True)
+               if b < a:
+                       b += _TWOPI
+               size = b - a
+               if size < 0:
+                       size += _TWOPI
+
+               startAngleInDeg = (a * 360 * 16) / _TWOPI
+               sizeInDeg = (size * 360 * 16) / _TWOPI
+               painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
+
+       def _paint_slice_foreground(self, painter, i, selectionIndex):
+               child = self._filing[i]
+
+               a = self._filing._index_to_angle(i, True)
+               b = self._filing._index_to_angle(i + 1, True)
+               if b < a:
+                       b += _TWOPI
+               middleAngle = (a + b) / 2
+               averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2
+
+               sliceX = averageRadius * math.cos(middleAngle)
+               sliceY = - averageRadius * math.sin(middleAngle)
+
+               piePos = self._canvas.rect().center()
+               pieX = piePos.x()
+               pieY = piePos.y()
+               self._paint_label(
+                       painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
+               )
+
+       def _paint_label(self, painter, action, isSelected, x, y):
+               text = action.text()
+               fontMetrics = painter.fontMetrics()
+               if text:
+                       textBoundingRect = fontMetrics.boundingRect(text)
+               else:
+                       textBoundingRect = QtCore.QRect()
+               textWidth = textBoundingRect.width()
+               textHeight = textBoundingRect.height()
+
+               icon = action.icon().pixmap(
+                       QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
+                       QtGui.QIcon.Normal,
+                       QtGui.QIcon.On,
+               )
+               iconWidth = icon.width()
+               iconHeight = icon.width()
+               averageWidth = (iconWidth + textWidth)/2
+               if not icon.isNull():
+                       iconRect = QtCore.QRect(
+                               x - averageWidth,
+                               y - iconHeight/2,
+                               iconWidth,
+                               iconHeight,
+                       )
+
+                       painter.drawPixmap(iconRect, icon)
+
+               if text:
+                       if isSelected:
+                               if action.isEnabled():
+                                       pen = self.palette.highlightedText()
+                                       brush = self.palette.highlight()
+                               else:
+                                       pen = self.palette.mid()
+                                       brush = self.palette.background()
+                       else:
+                               if action.isEnabled():
+                                       pen = self.palette.text()
                                else:
-                                       painter.setBrush(self.palette().background())
-                               painter.setPen(self.palette().mid().color())
-
-                               a = self._index_to_angle(i, True)
-                               b = self._index_to_angle(i + 1, True)
-                               if b < a:
-                                       b += 2*math.pi
-                               size = b - a
-                               if size < 0:
-                                       size += 2*math.pi
-
-                               startAngleInDeg = (a * 360 * 16) / (2*math.pi)
-                               sizeInDeg = (size * 360 * 16) / (2*math.pi)
-                               painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
-
-               dark = self.palette().dark().color()
-               light = self.palette().light().color()
-               if self._selectionIndex == -1:
-                       background = self.palette().highlight().color()
+                                       pen = self.palette.mid()
+                               brush = self.palette.background()
+
+                       leftX = x - averageWidth + iconWidth
+                       topY = y + textHeight/2
+                       painter.setPen(pen.color())
+                       painter.setBrush(brush)
+                       painter.drawText(leftX, topY, text)
+
+       def _paint_center_background(self, painter, adjustmentRect, selectionIndex):
+               dark = self.palette.dark().color()
+               light = self.palette.light().color()
+               if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
+                       background = self.palette.highlight().color()
                else:
-                       background = self.palette().background().color()
+                       background = self.palette.background().color()
 
+               innerRadius = self._cachedInnerRadius
+               adjustmentCenterPos = adjustmentRect.center()
                innerRect = QtCore.QRect(
-                       adjustmentRect.center().x() - self._innerRadius,
-                       adjustmentRect.center().y() - self._innerRadius,
-                       self._innerRadius * 2 + 1,
-                       self._innerRadius * 2 + 1,
+                       adjustmentCenterPos.x() - innerRadius,
+                       adjustmentCenterPos.y() - innerRadius,
+                       innerRadius * 2 + 1,
+                       innerRadius * 2 + 1,
                )
 
                painter.setPen(QtCore.Qt.NoPen)
@@ -191,199 +408,519 @@ class QPieMenu(QtGui.QWidget):
                painter.drawEllipse(adjustmentRect)
 
                r = QtCore.QRect(innerRect)
-               innerRect.setLeft(r.center().x() + ((r.left() - r.center().x()) / 3) * 1)
-               innerRect.setRight(r.center().x() + ((r.right() - r.center().x()) / 3) * 1)
-               innerRect.setTop(r.center().y() + ((r.top() - r.center().y()) / 3) * 1)
-               innerRect.setBottom(r.center().y() + ((r.bottom() - r.center().y()) / 3) * 1)
+               innerCenter = r.center()
+               innerRect.setLeft(innerCenter.x() + ((r.left() - innerCenter.x()) / 3) * 1)
+               innerRect.setRight(innerCenter.x() + ((r.right() - innerCenter.x()) / 3) * 1)
+               innerRect.setTop(innerCenter.y() + ((r.top() - innerCenter.y()) / 3) * 1)
+               innerRect.setBottom(innerCenter.y() + ((r.bottom() - innerCenter.y()) / 3) * 1)
+
+       def _paint_center_foreground(self, painter, selectionIndex):
+               centerPos = self._canvas.rect().center()
+               pieX = centerPos.x()
+               pieY = centerPos.y()
+
+               x = pieX
+               y = pieY
+
+               self._paint_label(
+                       painter,
+                       self._filing.center().action(),
+                       selectionIndex == PieFiling.SELECTION_CENTER,
+                       x, y
+               )
+
+
+class QPieDisplay(QtGui.QWidget):
+
+       def __init__(self, filing, parent = None, flags = QtCore.Qt.Window):
+               QtGui.QWidget.__init__(self, parent, flags)
+               self._filing = filing
+               self._artist = PieArtist(self._filing)
+               self._selectionIndex = PieFiling.SELECTION_NONE
+
+       def popup(self, pos):
+               self._update_selection(pos)
+               self.show()
+
+       def sizeHint(self):
+               return self._artist.pieSize()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def showEvent(self, showEvent):
+               mask = self._artist.show(self.palette())
+               self.setMask(mask)
+
+               QtGui.QWidget.showEvent(self, showEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def hideEvent(self, hideEvent):
+               self._artist.hide()
+               self._selectionIndex = PieFiling.SELECTION_NONE
+               QtGui.QWidget.hideEvent(self, hideEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def paintEvent(self, paintEvent):
+               canvas = self._artist.paint(self._selectionIndex)
+
+               screen = QtGui.QPainter(self)
+               screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
+
+               QtGui.QWidget.paintEvent(self, paintEvent)
+
+       def selectAt(self, index):
+               oldIndex = self._selectionIndex
+               self._selectionIndex = index
+               if self.isVisible():
+                       self.update()
+
+
+class QPieButton(QtGui.QWidget):
+
+       activated = QtCore.pyqtSignal(int)
+       highlighted = QtCore.pyqtSignal(int)
+       canceled = QtCore.pyqtSignal()
+       aboutToShow = QtCore.pyqtSignal()
+       aboutToHide = QtCore.pyqtSignal()
+
+       BUTTON_RADIUS = 24
+       DELAY = 250
+
+       def __init__(self, buttonSlice, parent = None):
+               QtGui.QWidget.__init__(self, parent)
+               self._cachedCenterPosition = self.rect().center()
+
+               self._filing = PieFiling()
+               self._display = QPieDisplay(self._filing, None, QtCore.Qt.SplashScreen)
+               self._selectionIndex = PieFiling.SELECTION_NONE
+
+               self._buttonFiling = PieFiling()
+               self._buttonFiling.set_center(buttonSlice)
+               # @todo Figure out how to make the button auto-fill to content
+               self._buttonFiling.setOuterRadius(self.BUTTON_RADIUS)
+               self._buttonArtist = PieArtist(self._buttonFiling)
+               self._poppedUp = False
+
+               self._delayPopupTimer = QtCore.QTimer()
+               self._delayPopupTimer.setInterval(self.DELAY)
+               self._delayPopupTimer.setSingleShot(True)
+               self._delayPopupTimer.timeout.connect(self._on_delayed_popup)
+               self._popupLocation = None
+
+               self._mousePosition = None
+               self.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+       def insertItem(self, item, index = -1):
+               self._filing.insertItem(item, index)
+
+       def removeItemAt(self, index):
+               self._filing.removeItemAt(index)
+
+       def set_center(self, item):
+               self._filing.set_center(item)
+
+       def set_button(self, item):
+               self.update()
+
+       def clear(self):
+               self._filing.clear()
+
+       def itemAt(self, index):
+               return self._filing.itemAt(index)
+
+       def indexAt(self, point):
+               return self._filing.indexAt(self._cachedCenterPosition, point)
+
+       def innerRadius(self):
+               return self._filing.innerRadius()
+
+       def setInnerRadius(self, radius):
+               self._filing.setInnerRadius(radius)
+
+       def outerRadius(self):
+               return self._filing.outerRadius()
 
-               if self._selectionIndex == -1:
-                       text = self.palette().highlightedText().color()
+       def setOuterRadius(self, radius):
+               self._filing.setOuterRadius(radius)
+
+       def buttonRadius(self):
+               return self._buttonFiling.outerRadius()
+
+       def setButtonRadius(self, radius):
+               self._buttonFiling.setOuterRadius(radius)
+
+       def sizeHint(self):
+               return self._buttonArtist.pieSize()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mousePressEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._mousePosition = lastMousePos
+               self._update_selection(self._cachedCenterPosition)
+
+               self.highlighted.emit(self._selectionIndex)
+
+               self._display.selectAt(self._selectionIndex)
+               self._popupLocation = mouseEvent.globalPos()
+               self._delayPopupTimer.start()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_delayed_popup(self):
+               assert self._popupLocation is not None
+               self._popup_child(self._popupLocation)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseMoveEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               if self._mousePosition is None:
+                       # Absolute
+                       self._update_selection(lastMousePos)
                else:
-                       text = self.palette().text().color()
+                       # Relative
+                       self._update_selection(self._cachedCenterPosition + (lastMousePos - self._mousePosition))
 
-               for i, child in enumerate(self._children):
-                       text = child.action().text()
+               if lastSelection != self._selectionIndex:
+                       self.highlighted.emit(self._selectionIndex)
+                       self._display.selectAt(self._selectionIndex)
 
-                       a = self._index_to_angle(i, True)
-                       b = self._index_to_angle(i + 1, True)
-                       if b < a:
-                               b += 2*math.pi
-                       middleAngle = (a + b) / 2
-                       averageRadius = (self._innerRadius + self._outerRadius) / 2
+               if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive():
+                       self._on_delayed_popup()
 
-                       sliceX = averageRadius * math.cos(middleAngle)
-                       sliceY = - averageRadius * math.sin(middleAngle)
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseReleaseEvent(self, mouseEvent):
+               self._delayPopupTimer.stop()
+               self._popupLocation = None
 
-                       pieX = self._canvas.rect().center().x()
-                       pieY = self._canvas.rect().center().y()
+               lastSelection = self._selectionIndex
 
-                       fontMetrics = painter.fontMetrics()
-                       if text:
-                               textBoundingRect = fontMetrics.boundingRect(text)
+               lastMousePos = mouseEvent.pos()
+               if self._mousePosition is None:
+                       # Absolute
+                       self._update_selection(lastMousePos)
+               else:
+                       # Relative
+                       self._update_selection(self._cachedCenterPosition + (lastMousePos - self._mousePosition))
+               self._mousePosition = None
+
+               self._activate_at(self._selectionIndex)
+               self._hide_child()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def keyPressEvent(self, keyEvent):
+               if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
+                       self._popup_child(QtGui.QCursor.pos())
+                       if self._selectionIndex != len(self._filing) - 1:
+                               nextSelection = self._selectionIndex + 1
                        else:
-                               textBoundingRect = QtCore.QRect()
-                       textWidth = textBoundingRect.width()
-                       textHeight = textBoundingRect.height()
-
-                       icon = child.action().icon().pixmap(
-                               QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
-                               QtGui.QIcon.Normal,
-                               QtGui.QIcon.On,
-                       )
-                       averageWidth = (icon.width() + textWidth)/2
-                       if not icon.isNull():
-                               iconRect = QtCore.QRect(
-                                       pieX + sliceX - averageWidth,
-                                       pieY + sliceY - icon.height()/2,
-                                       icon.width(),
-                                       icon.height(),
-                               )
-
-                               painter.drawPixmap(iconRect, icon)
-
-                       if text:
-                               if i == self._selectionIndex:
-                                       if child.action().isEnabled():
-                                               pen = self.palette().highlightedText()
-                                               brush = self.palette().highlight()
-                                       else:
-                                               pen = self.palette().mid()
-                                               brush = self.palette().background()
-                               else:
-                                       if child.action().isEnabled():
-                                               pen = self.palette().text()
-                                       else:
-                                               pen = self.palette().mid()
-                                       brush = self.palette().background()
-
-                               leftX = pieX + sliceX - averageWidth + icon.width()
-                               topY = pieY + sliceY + textHeight/2
-                               painter.setPen(pen.color())
-                               painter.setBrush(brush)
-                               painter.drawText(leftX, topY, text)
+                               nextSelection = 0
+                       self._select_at(nextSelection)
+                       self._display.selectAt(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
+                       self._popup_child(QtGui.QCursor.pos())
+                       if 0 < self._selectionIndex:
+                               nextSelection = self._selectionIndex - 1
+                       else:
+                               nextSelection = len(self._filing) - 1
+                       self._select_at(nextSelection)
+                       self._display.selectAt(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Space]:
+                       self._popup_child(QtGui.QCursor.pos())
+                       self._select_at(PieFiling.SELECTION_CENTER)
+                       self._display.selectAt(self._selectionIndex)
+               elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
+                       self._delayPopupTimer.stop()
+                       self._popupLocation = None
+                       self._activate_at(self._selectionIndex)
+                       self._hide_child()
+               elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
+                       self._delayPopupTimer.stop()
+                       self._popupLocation = None
+                       self._activate_at(PieFiling.SELECTION_NONE)
+                       self._hide_child()
+               else:
+                       QtGui.QWidget.keyPressEvent(self, keyEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def showEvent(self, showEvent):
+               self._buttonArtist.show(self.palette())
+               self._cachedCenterPosition = self.rect().center()
+
+               QtGui.QWidget.showEvent(self, showEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def hideEvent(self, hideEvent):
+               self._display.hide()
+               self._select_at(PieFiling.SELECTION_NONE)
+               QtGui.QWidget.hideEvent(self, hideEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def paintEvent(self, paintEvent):
+               if self._poppedUp:
+                       canvas = self._buttonArtist.paint(PieFiling.SELECTION_CENTER)
+               else:
+                       canvas = self._buttonArtist.paint(PieFiling.SELECTION_NONE)
 
                screen = QtGui.QPainter(self)
-               screen.drawPixmap(QtCore.QPoint(0, 0), self._canvas)
+               screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
 
                QtGui.QWidget.paintEvent(self, paintEvent)
 
+       def __iter__(self):
+               return iter(self._filing)
+
        def __len__(self):
-               return len(self._children)
+               return len(self._filing)
 
-       def _invalidate_view(self):
-               pass
+       def _popup_child(self, position):
+               self._poppedUp = True
+               self.aboutToShow.emit()
 
-       def _generate_mask(self, mask):
-               """
-               Specifies on the mask the shape of the pie menu
-               """
-               painter = QtGui.QPainter(mask)
-               painter.setPen(QtCore.Qt.color1)
-               painter.setBrush(QtCore.Qt.color1)
-               painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
+               self._delayPopupTimer.stop()
+               self._popupLocation = None
+
+               position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius())
+               self._display.move(position)
+               self._display.show()
+
+               self.update()
+
+       def _hide_child(self):
+               self._poppedUp = False
+               self.aboutToHide.emit()
+               self._display.hide()
+               self.update()
 
        def _select_at(self, index):
                self._selectionIndex = index
 
-               numChildren = len(self._children)
-               loopDelta = max(numChildren, 1)
-               while self._selectionIndex < 0:
-                       self._selectionIndex += loopDelta
-               while numChildren <= self._selectionIndex:
-                       self._selectionIndex -= loopDelta
+       def _update_selection(self, lastMousePos):
+               radius = _radius_at(self._cachedCenterPosition, lastMousePos)
+               if radius < self._filing.innerRadius():
+                       self._select_at(PieFiling.SELECTION_CENTER)
+               elif radius <= self._filing.outerRadius():
+                       self._select_at(self.indexAt(lastMousePos))
+               else:
+                       self._select_at(PieFiling.SELECTION_NONE)
 
        def _activate_at(self, index):
-               child = self.itemAt(index)
-               if child.action.isEnabled:
-                       child.action.trigger()
-               self.activated.emit()
-               self.aboutToHide.emit()
-               self.hide()
+               if index == PieFiling.SELECTION_NONE:
+                       self.canceled.emit()
+                       return
+               elif index == PieFiling.SELECTION_CENTER:
+                       child = self._filing.center()
+               else:
+                       child = self.itemAt(index)
 
-       def _index_to_angle(self, index, isShifted):
-               index = index % len(self._children)
+               if child.action().isEnabled():
+                       child.action().trigger()
+                       self.activated.emit(index)
+               else:
+                       self.canceled.emit()
 
-               totalWeight = sum(child.weight() for child in self._children)
-               if totalWeight == 0:
-                       totalWeight = 1
-               baseAngle = (2 * math.pi) / totalWeight
 
-               angle = math.pi / 2
-               if isShifted:
-                       if self._children:
-                               angle -= (self._children[0].weight() * baseAngle) / 2
-                       else:
-                               angle -= baseAngle / 2
-               while angle < 0:
-                       angle += 2*math.pi
+class QPieMenu(QtGui.QWidget):
 
-               for i, child in enumerate(self._children):
-                       if index < i:
-                               break
-                       angle += child.weight() * baseAngle
-               while (2*math.pi) < angle:
-                       angle -= 2*math.pi
+       activated = QtCore.pyqtSignal(int)
+       highlighted = QtCore.pyqtSignal(int)
+       canceled = QtCore.pyqtSignal()
+       aboutToShow = QtCore.pyqtSignal()
+       aboutToHide = QtCore.pyqtSignal()
 
-               return angle
+       def __init__(self, parent = None):
+               QtGui.QWidget.__init__(self, parent)
+               self._cachedCenterPosition = self.rect().center()
 
-       def _angle_to_index(self, angle):
-               numChildren = len(self._children)
-               if numChildren == 0:
-                       return -1
+               self._filing = PieFiling()
+               self._artist = PieArtist(self._filing)
+               self._selectionIndex = PieFiling.SELECTION_NONE
 
-               totalWeight = sum(child.weight() for child in self._children)
-               if totalWeight == 0:
-                       totalWeight = 1
-               baseAngle = (2 * math.pi) / totalWeight
+               self._mousePosition = ()
+               self.setFocusPolicy(QtCore.Qt.StrongFocus)
 
-               iterAngle = math.pi / 2 - (self.itemAt(0).weight * baseAngle) / 2
-               while iterAngle < 0:
-                       iterAngle += 2 * math.pi
+       def popup(self, pos):
+               self._update_selection(pos)
+               self.show()
 
-               oldIterAngle = iterAngle
-               for index, child in enumerate(self._children):
-                       iterAngle += child.weight * baseAngle
-                       if oldIterAngle < iterAngle and angle <= iterAngle:
-                               return index
-                       elif oldIterAngle < (iterAngle + 2*math.pi) and angle <= (iterAngle + 2*math.pi):
-                               return index
-                       oldIterAngle = iterAngle
+       def insertItem(self, item, index = -1):
+               self._filing.insertItem(item, index)
+               self.update()
 
-       def _radius_at(self, pos):
-               xDelta = pos.x() - self.rect().center().x()
-               yDelta = pos.y() - self.rect().center().y()
+       def removeItemAt(self, index):
+               self._filing.removeItemAt(index)
+               self.update()
 
-               radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
-               return radius
+       def set_center(self, item):
+               self._filing.set_center(item)
+               self.update()
 
-       def _angle_at(self, pos):
-               xDelta = pos.x() - self.rect().center().x()
-               yDelta = pos.y() - self.rect().center().y()
+       def clear(self):
+               self._filing.clear()
+               self.update()
 
-               radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
-               angle = math.acos(xDelta / radius)
-               if 0 <= yDelta:
-                       angle = 2*math.pi - angle
+       def itemAt(self, index):
+               return self._filing.itemAt(index)
 
-               return angle
+       def indexAt(self, point):
+               return self._filing.indexAt(self._cachedCenterPosition, point)
+
+       def innerRadius(self):
+               return self._filing.innerRadius()
+
+       def setInnerRadius(self, radius):
+               self._filing.setInnerRadius(radius)
+               self.update()
+
+       def outerRadius(self):
+               return self._filing.outerRadius()
+
+       def setOuterRadius(self, radius):
+               self._filing.setOuterRadius(radius)
+               self.update()
+
+       def sizeHint(self):
+               return self._artist.pieSize()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mousePressEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._update_selection(lastMousePos)
+               self._mousePosition = lastMousePos
+
+               if lastSelection != self._selectionIndex:
+                       self.highlighted.emit(self._selectionIndex)
+                       self.update()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseMoveEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._update_selection(lastMousePos)
 
-       def _on_key_press(self, keyEvent):
-               if keyEvent.key in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
-                       self._select_at(self._selectionIndex + 1)
-               elif keyEvent.key in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
-                       self._select_at(self._selectionIndex - 1)
-               elif keyEvent.key in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
-                       self._motion = 0
+               if lastSelection != self._selectionIndex:
+                       self.highlighted.emit(self._selectionIndex)
+                       self.update()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def mouseReleaseEvent(self, mouseEvent):
+               lastSelection = self._selectionIndex
+
+               lastMousePos = mouseEvent.pos()
+               self._update_selection(lastMousePos)
+               self._mousePosition = ()
+
+               self._activate_at(self._selectionIndex)
+               self.update()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def keyPressEvent(self, keyEvent):
+               if keyEvent.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
+                       if self._selectionIndex != len(self._filing) - 1:
+                               nextSelection = self._selectionIndex + 1
+                       else:
+                               nextSelection = 0
+                       self._select_at(nextSelection)
+                       self.update()
+               elif keyEvent.key() in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
+                       if 0 < self._selectionIndex:
+                               nextSelection = self._selectionIndex - 1
+                       else:
+                               nextSelection = len(self._filing) - 1
+                       self._select_at(nextSelection)
+                       self.update()
+               elif keyEvent.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
                        self._activate_at(self._selectionIndex)
-               elif keyEvent.key in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
-                       pass
+               elif keyEvent.key() in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
+                       self._activate_at(PieFiling.SELECTION_NONE)
+               else:
+                       QtGui.QWidget.keyPressEvent(self, keyEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def showEvent(self, showEvent):
+               self.aboutToShow.emit()
+               self._cachedCenterPosition = self.rect().center()
+
+               mask = self._artist.show(self.palette())
+               self.setMask(mask)
+
+               lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
+               self._update_selection(lastMousePos)
+
+               QtGui.QWidget.showEvent(self, showEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def hideEvent(self, hideEvent):
+               self._artist.hide()
+               self._selectionIndex = PieFiling.SELECTION_NONE
+               QtGui.QWidget.hideEvent(self, hideEvent)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def paintEvent(self, paintEvent):
+               canvas = self._artist.paint(self._selectionIndex)
+
+               screen = QtGui.QPainter(self)
+               screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
+
+               QtGui.QWidget.paintEvent(self, paintEvent)
+
+       def __iter__(self):
+               return iter(self._filing)
+
+       def __len__(self):
+               return len(self._filing)
+
+       def _select_at(self, index):
+               self._selectionIndex = index
+
+       def _update_selection(self, lastMousePos):
+               radius = _radius_at(self._cachedCenterPosition, lastMousePos)
+               if radius < self._filing.innerRadius():
+                       self._selectionIndex = PieFiling.SELECTION_CENTER
+               elif radius <= self._filing.outerRadius():
+                       self._select_at(self.indexAt(lastMousePos))
+               else:
+                       self._selectionIndex = PieFiling.SELECTION_NONE
 
-       def _on_mouse_press(self, mouseEvent):
-               self._mouseButtonPressed = True
+       def _activate_at(self, index):
+               if index == PieFiling.SELECTION_NONE:
+                       self.canceled.emit()
+                       self.aboutToHide.emit()
+                       self.hide()
+                       return
+               elif index == PieFiling.SELECTION_CENTER:
+                       child = self._filing.center()
+               else:
+                       child = self.itemAt(index)
+
+               if child.isEnabled():
+                       child.action().trigger()
+                       self.activated.emit(index)
+               else:
+                       self.canceled.emit()
+               self.aboutToHide.emit()
+               self.hide()
+
+
+def init_pies():
+       PieFiling.NULL_CENTER.setEnabled(False)
+
+
+def _print(msg):
+       print msg
+
+
+def _on_about_to_hide(app):
+       app.exit()
 
 
 if __name__ == "__main__":
        app = QtGui.QApplication([])
+       init_pies()
 
        if False:
                pie = QPieMenu()
@@ -397,18 +934,105 @@ if __name__ == "__main__":
                spie.insertItem(singleItem)
                spie.show()
 
+       if False:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoItem = QActionPieItem(twoAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               mpie = QPieMenu()
+               mpie.insertItem(oneItem)
+               mpie.insertItem(twoItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(iconTextItem)
+               mpie.show()
+
        if True:
                oneAction = QtGui.QAction(None)
                oneAction.setText("Chew")
+               oneAction.triggered.connect(lambda: _print("Chew"))
                oneItem = QActionPieItem(oneAction)
                twoAction = QtGui.QAction(None)
                twoAction.setText("Foo")
+               twoAction.triggered.connect(lambda: _print("Foo"))
                twoItem = QActionPieItem(twoAction)
+               iconAction = QtGui.QAction(None)
+               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+               iconAction.triggered.connect(lambda: _print("Icon"))
+               iconItem = QActionPieItem(iconAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+               iconTextItem = QActionPieItem(iconTextAction)
                mpie = QPieMenu()
+               mpie.set_center(iconItem)
                mpie.insertItem(oneItem)
                mpie.insertItem(twoItem)
                mpie.insertItem(oneItem)
+               mpie.insertItem(iconTextItem)
+               mpie.show()
+               mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
+               mpie.canceled.connect(lambda: _print("Canceled"))
+
+       if False:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneAction.triggered.connect(lambda: _print("Chew"))
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoAction.triggered.connect(lambda: _print("Foo"))
+               twoItem = QActionPieItem(twoAction)
+               iconAction = QtGui.QAction(None)
+               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+               iconAction.triggered.connect(lambda: _print("Icon"))
+               iconItem = QActionPieItem(iconAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               pieFiling = PieFiling()
+               pieFiling.set_center(iconItem)
+               pieFiling.insertItem(oneItem)
+               pieFiling.insertItem(twoItem)
+               pieFiling.insertItem(oneItem)
+               pieFiling.insertItem(iconTextItem)
+               mpie = QPieDisplay(pieFiling)
+               mpie.show()
+
+       if False:
+               oneAction = QtGui.QAction(None)
+               oneAction.setText("Chew")
+               oneAction.triggered.connect(lambda: _print("Chew"))
+               oneItem = QActionPieItem(oneAction)
+               twoAction = QtGui.QAction(None)
+               twoAction.setText("Foo")
+               twoAction.triggered.connect(lambda: _print("Foo"))
+               twoItem = QActionPieItem(twoAction)
+               iconAction = QtGui.QAction(None)
+               iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
+               iconAction.triggered.connect(lambda: _print("Icon"))
+               iconItem = QActionPieItem(iconAction)
+               iconTextAction = QtGui.QAction(None)
+               iconTextAction.setText("Icon")
+               iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
+               iconTextAction.triggered.connect(lambda: _print("Icon and text"))
+               iconTextItem = QActionPieItem(iconTextAction)
+               mpie = QPieButton(iconItem)
+               mpie.set_center(iconItem)
+               mpie.insertItem(oneItem)
                mpie.insertItem(twoItem)
+               mpie.insertItem(oneItem)
+               mpie.insertItem(iconTextItem)
                mpie.show()
+               mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
+               mpie.canceled.connect(lambda: _print("Canceled"))
 
        app.exec_()