#!/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):
return self._action.isEnabled()
-class QPieMenu(QtGui.QWidget):
-
- INNER_RADIUS_DEFAULT = 24
- OUTER_RADIUS_DEFAULT = 64
- ICON_SIZE_DEFAULT = 32
+class PieFiling(object):
- activated = QtCore.pyqtSignal(int)
- highlighted = QtCore.pyqtSignal(int)
- canceled = QtCore.pyqtSignal()
- aboutToShow = QtCore.pyqtSignal()
- aboutToHide = QtCore.pyqtSignal()
+ INNER_RADIUS_DEFAULT = 32
+ OUTER_RADIUS_DEFAULT = 128
SELECTION_CENTER = -1
SELECTION_NONE = -2
- NULL_CENTER = QtGui.QAction(None)
+ NULL_CENTER = QActionPieItem(QtGui.QAction(None))
- def __init__(self, parent = None):
- QtGui.QWidget.__init__(self, parent)
+ def __init__(self):
self._innerRadius = self.INNER_RADIUS_DEFAULT
self._outerRadius = self.OUTER_RADIUS_DEFAULT
self._children = []
self._center = self.NULL_CENTER
- self._selectionIndex = self.SELECTION_NONE
-
- self._mouseButtonPressed = False
- 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.update()
+ self._invalidate_cache()
def removeItemAt(self, index):
item = self._children.pop(index)
- self.update()
+ 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.update()
+ 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 indexAt(self, center, point):
+ return self._angle_to_index(_angle_at(center, point))
def innerRadius(self):
return self._innerRadius
def setOuterRadius(self, radius):
self._outerRadius = radius
- self._canvas = self._canvas.scaled(self.sizeHint())
- def sizeHint(self):
- diameter = self._outerRadius * 2 + 1
- return QtCore.QSize(diameter, diameter)
+ def __iter__(self):
+ return iter(self._children)
- def mousePressEvent(self, mouseEvent):
- lastSelection = self._selectionIndex
+ def __len__(self):
+ return len(self._children)
- lastMousePos = mouseEvent.pos()
- self._update_selection(lastMousePos)
- self._mouseButtonPressed = True
- self._mousePosition = lastMousePos
+ def __getitem__(self, index):
+ return self._children[index]
- if lastSelection != self._selectionIndex:
- self.highlighted.emit(self._selectionIndex)
- self.update()
+ 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 mouseMoveEvent(self, mouseEvent):
- lastSelection = self._selectionIndex
+ def _index_to_angle(self, index, isShifted):
+ key = index, isShifted
+ if key in self._cacheIndexToAngle:
+ return self._cacheIndexToAngle[key]
+ index = index % len(self._children)
- lastMousePos = mouseEvent.pos()
- self._update_selection(lastMousePos)
+ baseAngle = _TWOPI / self._cacheTotalWeight
- if lastSelection != self._selectionIndex:
- self.highlighted.emit(self._selectionIndex)
- self.update()
+ 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
- def mouseReleaseEvent(self, mouseEvent):
- lastSelection = self._selectionIndex
+ for i, child in enumerate(self._children):
+ if index < i:
+ break
+ angle += child.weight() * baseAngle
+ while _TWOPI < angle:
+ angle -= _TWOPI
- lastMousePos = mouseEvent.pos()
- self._update_selection(lastMousePos)
- self._mouseButtonPressed = False
- self._mousePosition = ()
+ self._cacheIndexToAngle[key] = angle
+ return angle
- self._activate_at(self._selectionIndex)
- self.update()
+ def _angle_to_index(self, angle):
+ numChildren = len(self._children)
+ if numChildren == 0:
+ return self.SELECTION_CENTER
- def showEvent(self, showEvent):
- self.aboutToShow.emit()
+ 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 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
- lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
- self._update_selection(lastMousePos)
-
- QtGui.QWidget.showEvent(self, showEvent)
-
- def hideEvent(self, hideEvent):
- self.canceled.emit()
- self._selectionIndex = self.SELECTION_NONE
- QtGui.QWidget.hideEvent(self, hideEvent)
+ def hide(self):
+ self.palette = None
- 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 in xrange(len(self._children)):
- self._paint_slice_background(painter, adjustmentRect, i)
+ for i in xrange(len(self._filing)):
+ self._paint_slice_background(painter, adjustmentRect, i, selectionIndex)
- self._paint_center_background(painter, adjustmentRect)
- self._paint_center_foreground(painter)
-
- for i in xrange(len(self._children)):
- self._paint_slice_foreground(painter, i)
-
- screen = QtGui.QPainter(self)
- screen.drawPixmap(QtCore.QPoint(0, 0), self._canvas)
+ self._paint_center_background(painter, adjustmentRect, selectionIndex)
+ self._paint_center_foreground(painter, selectionIndex)
- QtGui.QWidget.paintEvent(self, paintEvent)
+ for i in xrange(len(self._filing)):
+ self._paint_slice_foreground(painter, i, selectionIndex)
- def __len__(self):
- return len(self._children)
+ return self._canvas
def _generate_mask(self, mask):
"""
painter.setBrush(QtCore.Qt.color1)
painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
- def _paint_slice_background(self, painter, adjustmentRect, i):
- if i == self._selectionIndex and self._children[i].isEnabled():
- painter.setBrush(self.palette().highlight())
+ 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())
+ 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)
+ 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):
- child = self._children[i]
+ def _paint_slice_foreground(self, painter, i, selectionIndex):
+ child = self._filing[i]
- a = self._index_to_angle(i, True)
- b = self._index_to_angle(i + 1, True)
+ 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._innerRadius + self._outerRadius) / 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 == self._selectionIndex, pieX+sliceX, pieY+sliceY
+ painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
)
def _paint_label(self, painter, action, isSelected, x, y):
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)
if text:
if isSelected:
if action.isEnabled():
- pen = self.palette().highlightedText()
- brush = self.palette().highlight()
+ pen = self.palette.highlightedText()
+ brush = self.palette.highlight()
else:
- pen = self.palette().mid()
- brush = self.palette().background()
+ pen = self.palette.mid()
+ brush = self.palette.background()
else:
if action.isEnabled():
- pen = self.palette().text()
+ pen = self.palette.text()
else:
- pen = self.palette().mid()
- brush = self.palette().background()
+ 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)
painter.drawText(leftX, topY, text)
- def _paint_center_background(self, painter, adjustmentRect):
- dark = self.palette().dark().color()
- light = self.palette().light().color()
- if self._selectionIndex == self.SELECTION_CENTER and self._center.isEnabled():
- background = self.palette().highlight().color()
+ 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)
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):
- pieX = self._canvas.rect().center().x()
- pieY = self._canvas.rect().center().y()
+ 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._center.action(), self._selectionIndex == self.SELECTION_CENTER, x, y
+ painter,
+ self._filing.center().action(),
+ selectionIndex == PieFiling.SELECTION_CENTER,
+ x, y
)
- def _select_at(self, index):
+
+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()
- numChildren = len(self._children)
- loopDelta = max(numChildren, 1)
- while self._selectionIndex < 0:
- self._selectionIndex += loopDelta
- while numChildren <= self._selectionIndex:
- self._selectionIndex -= loopDelta
+
+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()
+
+ 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:
+ # Relative
+ 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()
+ 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:
+ 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), canvas)
+
+ 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()
+
+ 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
def _update_selection(self, lastMousePos):
- radius = self._radius_at(lastMousePos)
- if radius < self._innerRadius:
- self._selectionIndex = self.SELECTION_CENTER
- elif radius <= self._outerRadius:
- self._select_at(self._angle_to_index(self._angle_at(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._selectionIndex = self.SELECTION_NONE
+ self._select_at(PieFiling.SELECTION_NONE)
def _activate_at(self, index):
- if index == self.SELECTION_NONE:
- print "Nothing selected"
+ if index == PieFiling.SELECTION_NONE:
+ self.canceled.emit()
return
- elif index == self.SELECTION_CENTER:
- child = self._center
+ elif index == PieFiling.SELECTION_CENTER:
+ child = self._filing.center()
else:
child = self.itemAt(index)
+
if child.action().isEnabled():
child.action().trigger()
- self.activated.emit(index)
- self.aboutToHide.emit()
- self.hide()
+ self.activated.emit(index)
+ else:
+ self.canceled.emit()
- def _index_to_angle(self, index, isShifted):
- index = index % len(self._children)
- totalWeight = sum(child.weight() for child in self._children)
- if totalWeight == 0:
- totalWeight = 1
- baseAngle = (2 * math.pi) / totalWeight
+class QPieMenu(QtGui.QWidget):
- 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
+ activated = QtCore.pyqtSignal(int)
+ highlighted = QtCore.pyqtSignal(int)
+ canceled = QtCore.pyqtSignal()
+ aboutToShow = QtCore.pyqtSignal()
+ aboutToHide = QtCore.pyqtSignal()
- for i, child in enumerate(self._children):
- if index < i:
- break
- angle += child.weight() * baseAngle
- while (2*math.pi) < angle:
- angle -= 2*math.pi
+ def __init__(self, parent = None):
+ QtGui.QWidget.__init__(self, parent)
+ self._cachedCenterPosition = self.rect().center()
- return angle
+ self._filing = PieFiling()
+ self._artist = PieArtist(self._filing)
+ self._selectionIndex = PieFiling.SELECTION_NONE
- def _angle_to_index(self, angle):
- numChildren = len(self._children)
- if numChildren == 0:
- return self.SELECTION_CENTER
+ self._mousePosition = ()
+ self.setFocusPolicy(QtCore.Qt.StrongFocus)
- totalWeight = sum(child.weight() for child in self._children)
- if totalWeight == 0:
- totalWeight = 1
- baseAngle = (2 * math.pi) / totalWeight
+ def popup(self, pos):
+ self._update_selection(pos)
+ self.show()
- iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
- while iterAngle < 0:
- iterAngle += 2 * math.pi
+ def insertItem(self, item, index = -1):
+ self._filing.insertItem(item, index)
+ self.update()
- 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):
- return index - 1 if index != 0 else numChildren - 1
- oldIterAngle = iterAngle
+ def removeItemAt(self, index):
+ self._filing.removeItemAt(index)
+ self.update()
+
+ def set_center(self, item):
+ self._filing.set_center(item)
+ self.update()
+
+ def clear(self):
+ self._filing.clear()
+ self.update()
- def _radius_at(self, pos):
- xDelta = pos.x() - self.rect().center().x()
- yDelta = pos.y() - self.rect().center().y()
+ def itemAt(self, index):
+ return self._filing.itemAt(index)
- radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
- return radius
+ def indexAt(self, point):
+ return self._filing.indexAt(self._cachedCenterPosition, point)
- def _angle_at(self, pos):
- xDelta = pos.x() - self.rect().center().x()
- yDelta = pos.y() - self.rect().center().y()
+ def innerRadius(self):
+ return self._filing.innerRadius()
- radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
- angle = math.acos(xDelta / radius)
- if 0 <= yDelta:
- angle = 2*math.pi - angle
+ def setInnerRadius(self, radius):
+ self._filing.setInnerRadius(radius)
+ self.update()
- return angle
+ def outerRadius(self):
+ return self._filing.outerRadius()
- 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)
+ 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)
+
+ 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]:
- self._select_at(self._selectionIndex - 1)
+ 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]:
+ 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]:
- self._activate_at(self.SELECTION_NONE)
+ 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 _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 _on_mouse_press(self, mouseEvent):
- self._mouseButtonPressed = True
+def init_pies():
+ PieFiling.NULL_CENTER.setEnabled(False)
def _print(msg):
if __name__ == "__main__":
app = QtGui.QApplication([])
- QPieMenu.NULL_CENTER.setEnabled(False)
+ init_pies()
if False:
pie = QPieMenu()
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_()