Adding a delay to poping up the pie menu
[ejpi] / src / libraries / qtpie.py
index 1eda74f..cab868b 100755 (executable)
@@ -1,27 +1,48 @@
 #!/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):
-       xDelta = pos.x() - center.x()
-       yDelta = pos.y() - center.y()
+       delta = pos - center
+       xDelta = delta.x()
+       yDelta = delta.y()
 
        radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
        return radius
 
 
 def _angle_at(center, pos):
-       xDelta = pos.x() - center.x()
-       yDelta = pos.y() - center.y()
+       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 = 2*math.pi - angle
+               angle = _TWOPI - angle
 
        return angle
 
@@ -64,11 +85,16 @@ class PieFiling(object):
                self._children = []
                self._center = self.NULL_CENTER
 
+               self._cacheIndexToAngle = {}
+               self._cacheTotalWeight = 0
+
        def insertItem(self, item, index = -1):
                self._children.insert(index, item)
+               self._invalidate_cache()
 
        def removeItemAt(self, index):
                item = self._children.pop(index)
+               self._invalidate_cache()
 
        def set_center(self, item):
                if item is None:
@@ -80,6 +106,8 @@ class PieFiling(object):
 
        def clear(self):
                del self._children[:]
+               self._center = self.NULL_CENTER
+               self._invalidate_cache()
 
        def itemAt(self, index):
                return self._children[index]
@@ -108,13 +136,19 @@ class PieFiling(object):
        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)
 
-               totalWeight = sum(child.weight() for child in self._children)
-               if totalWeight == 0:
-                       totalWeight = 1
-               baseAngle = (2 * math.pi) / totalWeight
+               baseAngle = _TWOPI / self._cacheTotalWeight
 
                angle = math.pi / 2
                if isShifted:
@@ -123,15 +157,16 @@ class PieFiling(object):
                        else:
                                angle -= baseAngle / 2
                while angle < 0:
-                       angle += 2*math.pi
+                       angle += _TWOPI
 
                for i, child in enumerate(self._children):
                        if index < i:
                                break
                        angle += child.weight() * baseAngle
-               while (2*math.pi) < angle:
-                       angle -= 2*math.pi
+               while _TWOPI < angle:
+                       angle -= _TWOPI
 
+               self._cacheIndexToAngle[key] = angle
                return angle
 
        def _angle_to_index(self, angle):
@@ -139,21 +174,18 @@ class PieFiling(object):
                if numChildren == 0:
                        return self.SELECTION_CENTER
 
-               totalWeight = sum(child.weight() for child in self._children)
-               if totalWeight == 0:
-                       totalWeight = 1
-               baseAngle = (2 * math.pi) / totalWeight
+               baseAngle = _TWOPI / self._cacheTotalWeight
 
                iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
                while iterAngle < 0:
-                       iterAngle += 2 * math.pi
+                       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 + 2*math.pi) and (angle + 2*math.pi <= iterAngle):
+                       elif oldIterAngle < (angle + _TWOPI) and (angle + _TWOPI <= iterAngle):
                                return index - 1 if index != 0 else numChildren - 1
                        oldIterAngle = iterAngle
 
@@ -225,8 +257,9 @@ class PieArtist(object):
                                painter.setBrush(self.palette.highlight())
                        else:
                                painter.setBrush(self.palette.background())
+                       painter.setPen(self.palette.mid().color())
 
-                       painter.fillRect(self._canvas.rect(), painter.brush())
+                       painter.drawRect(self._canvas.rect())
                        self._paint_center_foreground(painter, selectionIndex)
                        return self._canvas
                elif numChildren == 1:
@@ -267,13 +300,13 @@ class PieArtist(object):
                a = self._filing._index_to_angle(i, True)
                b = self._filing._index_to_angle(i + 1, True)
                if b < a:
-                       b += 2*math.pi
+                       b += _TWOPI
                size = b - a
                if size < 0:
-                       size += 2*math.pi
+                       size += _TWOPI
 
-               startAngleInDeg = (a * 360 * 16) / (2*math.pi)
-               sizeInDeg = (size * 360 * 16) / (2*math.pi)
+               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):
@@ -282,15 +315,16 @@ class PieArtist(object):
                a = self._filing._index_to_angle(i, True)
                b = self._filing._index_to_angle(i + 1, True)
                if b < a:
-                       b += 2*math.pi
+                       b += _TWOPI
                middleAngle = (a + b) / 2
                averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2
 
                sliceX = averageRadius * math.cos(middleAngle)
                sliceY = - averageRadius * math.sin(middleAngle)
 
-               pieX = self._canvas.rect().center().x()
-               pieY = self._canvas.rect().center().y()
+               piePos = self._canvas.rect().center()
+               pieX = piePos.x()
+               pieY = piePos.y()
                self._paint_label(
                        painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
                )
@@ -310,13 +344,15 @@ class PieArtist(object):
                        QtGui.QIcon.Normal,
                        QtGui.QIcon.On,
                )
-               averageWidth = (icon.width() + textWidth)/2
+               iconWidth = icon.width()
+               iconHeight = icon.width()
+               averageWidth = (iconWidth + textWidth)/2
                if not icon.isNull():
                        iconRect = QtCore.QRect(
                                x - averageWidth,
-                               y - icon.height()/2,
-                               icon.width(),
-                               icon.height(),
+                               y - iconHeight/2,
+                               iconWidth,
+                               iconHeight,
                        )
 
                        painter.drawPixmap(iconRect, icon)
@@ -336,7 +372,7 @@ class PieArtist(object):
                                        pen = self.palette.mid()
                                brush = self.palette.background()
 
-                       leftX = x - averageWidth + icon.width()
+                       leftX = x - averageWidth + iconWidth
                        topY = y + textHeight/2
                        painter.setPen(pen.color())
                        painter.setBrush(brush)
@@ -351,9 +387,10 @@ class PieArtist(object):
                        background = self.palette.background().color()
 
                innerRadius = self._cachedInnerRadius
+               adjustmentCenterPos = adjustmentRect.center()
                innerRect = QtCore.QRect(
-                       adjustmentRect.center().x() - innerRadius,
-                       adjustmentRect.center().y() - innerRadius,
+                       adjustmentCenterPos.x() - innerRadius,
+                       adjustmentCenterPos.y() - innerRadius,
                        innerRadius * 2 + 1,
                        innerRadius * 2 + 1,
                )
@@ -371,14 +408,16 @@ class PieArtist(object):
                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):
-               pieX = self._canvas.rect().center().x()
-               pieY = self._canvas.rect().center().y()
+               centerPos = self._canvas.rect().center()
+               pieX = centerPos.x()
+               pieY = centerPos.y()
 
                x = pieX
                y = pieY
@@ -393,7 +432,7 @@ class PieArtist(object):
 
 class QPieDisplay(QtGui.QWidget):
 
-       def __init__(self, filing, parent = None, flags = 0):
+       def __init__(self, filing, parent = None, flags = QtCore.Qt.Window):
                QtGui.QWidget.__init__(self, parent, flags)
                self._filing = filing
                self._artist = PieArtist(self._filing)
@@ -406,17 +445,20 @@ class QPieDisplay(QtGui.QWidget):
        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)
 
@@ -426,8 +468,10 @@ class QPieDisplay(QtGui.QWidget):
                QtGui.QWidget.paintEvent(self, paintEvent)
 
        def selectAt(self, index):
+               oldIndex = self._selectionIndex
                self._selectionIndex = index
-               self.update()
+               if self.isVisible():
+                       self.update()
 
 
 class QPieButton(QtGui.QWidget):
@@ -438,19 +482,30 @@ class QPieButton(QtGui.QWidget):
        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)
-               centerSize = self._buttonArtist.centerSize()
-               self._buttonFiling.setOuterRadius(max(centerSize.width(), centerSize.height()))
                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)
 
@@ -473,7 +528,7 @@ class QPieButton(QtGui.QWidget):
                return self._filing.itemAt(index)
 
        def indexAt(self, point):
-               return self._filing.indexAt(self.rect().center(), point)
+               return self._filing.indexAt(self._cachedCenterPosition, point)
 
        def innerRadius(self):
                return self._filing.innerRadius()
@@ -487,21 +542,35 @@ class QPieButton(QtGui.QWidget):
        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):
-               self._popup_child(mouseEvent.globalPos())
                lastSelection = self._selectionIndex
 
                lastMousePos = mouseEvent.pos()
                self._mousePosition = lastMousePos
-               self._update_selection(self.rect().center())
+               self._update_selection(self._cachedCenterPosition)
 
-               if lastSelection != self._selectionIndex:
-                       self.highlighted.emit(self._selectionIndex)
-                       self._display.selectAt(self._selectionIndex)
+               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
 
@@ -511,13 +580,20 @@ class QPieButton(QtGui.QWidget):
                        self._update_selection(lastMousePos)
                else:
                        # Relative
-                       self._update_selection(self.rect().center() + (lastMousePos - self._mousePosition))
+                       self._update_selection(self._cachedCenterPosition + (lastMousePos - self._mousePosition))
 
                if lastSelection != self._selectionIndex:
                        self.highlighted.emit(self._selectionIndex)
                        self._display.selectAt(self._selectionIndex)
 
+               if self._selectionIndex != PieFiling.SELECTION_CENTER and self._delayPopupTimer.isActive():
+                       self._on_delayed_popup()
+
+       @misc_utils.log_exception(_moduleLogger)
        def mouseReleaseEvent(self, mouseEvent):
+               self._delayPopupTimer.stop()
+               self._popupLocation = None
+
                lastSelection = self._selectionIndex
 
                lastMousePos = mouseEvent.pos()
@@ -526,12 +602,13 @@ class QPieButton(QtGui.QWidget):
                        self._update_selection(lastMousePos)
                else:
                        # Relative
-                       self._update_selection(self.rect().center() + (lastMousePos - self._mousePosition))
+                       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())
@@ -554,24 +631,32 @@ class QPieButton(QtGui.QWidget):
                        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)
@@ -583,10 +668,19 @@ class QPieButton(QtGui.QWidget):
 
                QtGui.QWidget.paintEvent(self, paintEvent)
 
+       def __iter__(self):
+               return iter(self._filing)
+
+       def __len__(self):
+               return len(self._filing)
+
        def _popup_child(self, position):
                self._poppedUp = True
                self.aboutToShow.emit()
 
+               self._delayPopupTimer.stop()
+               self._popupLocation = None
+
                position = position - QtCore.QPoint(self._filing.outerRadius(), self._filing.outerRadius())
                self._display.move(position)
                self._display.show()
@@ -603,7 +697,7 @@ class QPieButton(QtGui.QWidget):
                self._selectionIndex = index
 
        def _update_selection(self, lastMousePos):
-               radius = _radius_at(self.rect().center(), lastMousePos)
+               radius = _radius_at(self._cachedCenterPosition, lastMousePos)
                if radius < self._filing.innerRadius():
                        self._select_at(PieFiling.SELECTION_CENTER)
                elif radius <= self._filing.outerRadius():
@@ -637,6 +731,8 @@ class QPieMenu(QtGui.QWidget):
 
        def __init__(self, parent = None):
                QtGui.QWidget.__init__(self, parent)
+               self._cachedCenterPosition = self.rect().center()
+
                self._filing = PieFiling()
                self._artist = PieArtist(self._filing)
                self._selectionIndex = PieFiling.SELECTION_NONE
@@ -668,7 +764,7 @@ class QPieMenu(QtGui.QWidget):
                return self._filing.itemAt(index)
 
        def indexAt(self, point):
-               return self._filing.indexAt(self.rect().center(), point)
+               return self._filing.indexAt(self._cachedCenterPosition, point)
 
        def innerRadius(self):
                return self._filing.innerRadius()
@@ -687,6 +783,7 @@ class QPieMenu(QtGui.QWidget):
        def sizeHint(self):
                return self._artist.pieSize()
 
+       @misc_utils.log_exception(_moduleLogger)
        def mousePressEvent(self, mouseEvent):
                lastSelection = self._selectionIndex
 
@@ -698,6 +795,7 @@ class QPieMenu(QtGui.QWidget):
                        self.highlighted.emit(self._selectionIndex)
                        self.update()
 
+       @misc_utils.log_exception(_moduleLogger)
        def mouseMoveEvent(self, mouseEvent):
                lastSelection = self._selectionIndex
 
@@ -708,6 +806,7 @@ class QPieMenu(QtGui.QWidget):
                        self.highlighted.emit(self._selectionIndex)
                        self.update()
 
+       @misc_utils.log_exception(_moduleLogger)
        def mouseReleaseEvent(self, mouseEvent):
                lastSelection = self._selectionIndex
 
@@ -718,12 +817,21 @@ class QPieMenu(QtGui.QWidget):
                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]:
-                       self._select_at(self._selectionIndex + 1)
+                       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]:
-                       self._select_at(self._selectionIndex - 1)
+                       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)
@@ -732,8 +840,10 @@ class QPieMenu(QtGui.QWidget):
                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)
@@ -743,11 +853,13 @@ class QPieMenu(QtGui.QWidget):
 
                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)
 
@@ -756,18 +868,17 @@ class QPieMenu(QtGui.QWidget):
 
                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
 
-               numChildren = len(self._filing)
-               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.rect().center(), lastMousePos)
+               radius = _radius_at(self._cachedCenterPosition, lastMousePos)
                if radius < self._filing.innerRadius():
                        self._selectionIndex = PieFiling.SELECTION_CENTER
                elif radius <= self._filing.outerRadius():
@@ -795,6 +906,10 @@ class QPieMenu(QtGui.QWidget):
                self.hide()
 
 
+def init_pies():
+       PieFiling.NULL_CENTER.setEnabled(False)
+
+
 def _print(msg):
        print msg
 
@@ -805,7 +920,7 @@ def _on_about_to_hide(app):
 
 if __name__ == "__main__":
        app = QtGui.QApplication([])
-       PieFiling.NULL_CENTER.setEnabled(False)
+       init_pies()
 
        if False:
                pie = QPieMenu()
@@ -837,7 +952,7 @@ if __name__ == "__main__":
                mpie.insertItem(iconTextItem)
                mpie.show()
 
-       if False:
+       if True:
                oneAction = QtGui.QAction(None)
                oneAction.setText("Chew")
                oneAction.triggered.connect(lambda: _print("Chew"))
@@ -892,7 +1007,7 @@ if __name__ == "__main__":
                mpie = QPieDisplay(pieFiling)
                mpie.show()
 
-       if True:
+       if False:
                oneAction = QtGui.QAction(None)
                oneAction.setText("Chew")
                oneAction.triggered.connect(lambda: _print("Chew"))