5 from PyQt4 import QtGui
6 from PyQt4 import QtCore
9 class QActionPieItem(object):
11 def __init__(self, action, weight = 1):
18 def setWeight(self, weight):
24 def setEnabled(self, enabled = True):
25 self._action.setEnabled(enabled)
28 return self._action.isEnabled()
31 class QPieMenu(QtGui.QWidget):
33 INNER_RADIUS_DEFAULT = 24
34 OUTER_RADIUS_DEFAULT = 64
35 ICON_SIZE_DEFAULT = 32
37 activated = QtCore.pyqtSignal((), (int, ))
38 highlighted = QtCore.pyqtSignal(int)
39 canceled = QtCore.pyqtSignal()
40 aboutToShow = QtCore.pyqtSignal()
41 aboutToHide = QtCore.pyqtSignal()
43 def __init__(self, parent = None):
44 QtGui.QWidget.__init__(self, parent)
45 self._innerRadius = self.INNER_RADIUS_DEFAULT
46 self._outerRadius = self.OUTER_RADIUS_DEFAULT
48 self._selectionIndex = -2
51 self._mouseButtonPressed = True
52 self._mousePosition = ()
54 canvasSize = self._outerRadius * 2 + 1
55 self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
59 index = self.indexAt(self.mapFromGlobal(QtGui.QCursor.pos()))
60 self._mousePosition = pos
63 def insertItem(self, item, index = -1):
64 self._children.insert(index, item)
65 self._invalidate_view()
67 def removeItemAt(self, index):
68 item = self._children.pop(index)
69 self._invalidate_view()
73 self._invalidate_view()
75 def itemAt(self, index):
76 return self._children[index]
78 def indexAt(self, point):
79 return self._angle_to_index(self._angle_at(point))
81 def setHighlightedItem(self, index):
84 def highlightedItem(self):
87 def innerRadius(self):
88 return self._innerRadius
90 def setInnerRadius(self, radius):
91 self._innerRadius = radius
93 def outerRadius(self):
94 return self._outerRadius
96 def setOuterRadius(self, radius):
97 self._outerRadius = radius
98 self._canvas = self._canvas.scaled(self.sizeHint())
101 diameter = self._outerRadius * 2 + 1
102 return QtCore.QSize(diameter, diameter)
104 def showEvent(self, showEvent):
105 self.aboutToShow.emit()
107 if self._mask is None:
108 self._mask = QtGui.QBitmap(self._canvas.size())
109 self._mask.fill(QtCore.Qt.color0)
110 self._generate_mask(self._mask)
111 self._canvas.setMask(self._mask)
112 self.setMask(self._mask)
116 lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
117 radius = self._radius_at(lastMousePos)
118 if self._innerRadius <= radius and radius <= self._outerRadius:
119 self._select_at(self._angle_to_index(lastMousePos))
121 if radius < self._innerRadius:
122 self._selectionIndex = -1
124 self._selectionIndex = -2
126 QtGui.QWidget.showEvent(self, showEvent)
128 def hideEvent(self, hideEvent):
130 self._selectionIndex = -2
131 QtGui.QWidget.hideEvent(self, hideEvent)
133 def paintEvent(self, paintEvent):
134 painter = QtGui.QPainter(self._canvas)
135 painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
137 adjustmentRect = self._canvas.rect().adjusted(0, 0, -1, -1)
139 numChildren = len(self._children)
141 if self._selectionIndex == 0 and self._children[0].isEnabled():
142 painter.setBrush(self.palette().highlight())
144 painter.setBrush(self.palette().background())
146 painter.fillRect(self.rect(), painter.brush())
148 for i, child in enumerate(self._children):
149 if i == self._selectionIndex:
150 painter.setBrush(self.palette().highlight())
152 painter.setBrush(self.palette().background())
153 painter.setPen(self.palette().mid().color())
155 a = self._index_to_angle(i, True)
156 b = self._index_to_angle(i + 1, True)
163 startAngleInDeg = (a * 360 * 16) / (2*math.pi)
164 sizeInDeg = (size * 360 * 16) / (2*math.pi)
165 painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
167 dark = self.palette().dark().color()
168 light = self.palette().light().color()
169 if self._selectionIndex == -1:
170 background = self.palette().highlight().color()
172 background = self.palette().background().color()
174 innerRect = QtCore.QRect(
175 adjustmentRect.center().x() - self._innerRadius,
176 adjustmentRect.center().y() - self._innerRadius,
177 self._innerRadius * 2 + 1,
178 self._innerRadius * 2 + 1,
181 painter.setPen(QtCore.Qt.NoPen)
182 painter.setBrush(background)
183 painter.drawPie(innerRect, 0, 360 * 16)
185 painter.setPen(QtGui.QPen(dark, 1))
186 painter.setBrush(QtCore.Qt.NoBrush)
187 painter.drawEllipse(innerRect)
189 painter.setPen(QtGui.QPen(dark, 1))
190 painter.setBrush(QtCore.Qt.NoBrush)
191 painter.drawEllipse(adjustmentRect)
193 r = QtCore.QRect(innerRect)
194 innerRect.setLeft(r.center().x() + ((r.left() - r.center().x()) / 3) * 1)
195 innerRect.setRight(r.center().x() + ((r.right() - r.center().x()) / 3) * 1)
196 innerRect.setTop(r.center().y() + ((r.top() - r.center().y()) / 3) * 1)
197 innerRect.setBottom(r.center().y() + ((r.bottom() - r.center().y()) / 3) * 1)
199 if self._selectionIndex == -1:
200 text = self.palette().highlightedText().color()
202 text = self.palette().text().color()
204 for i, child in enumerate(self._children):
205 text = child.action().text()
207 a = self._index_to_angle(i, True)
208 b = self._index_to_angle(i + 1, True)
211 middleAngle = (a + b) / 2
212 averageRadius = (self._innerRadius + self._outerRadius) / 2
214 sliceX = averageRadius * math.cos(middleAngle)
215 sliceY = - averageRadius * math.sin(middleAngle)
217 pieX = self._canvas.rect().center().x()
218 pieY = self._canvas.rect().center().y()
220 fontMetrics = painter.fontMetrics()
222 textBoundingRect = fontMetrics.boundingRect(text)
224 textBoundingRect = QtCore.QRect()
225 textWidth = textBoundingRect.width()
226 textHeight = textBoundingRect.height()
228 icon = child.action().icon().pixmap(
229 QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
233 averageWidth = (icon.width() + textWidth)/2
234 if not icon.isNull():
235 iconRect = QtCore.QRect(
236 pieX + sliceX - averageWidth,
237 pieY + sliceY - icon.height()/2,
242 painter.drawPixmap(iconRect, icon)
245 if i == self._selectionIndex:
246 if child.action().isEnabled():
247 pen = self.palette().highlightedText()
248 brush = self.palette().highlight()
250 pen = self.palette().mid()
251 brush = self.palette().background()
253 if child.action().isEnabled():
254 pen = self.palette().text()
256 pen = self.palette().mid()
257 brush = self.palette().background()
259 leftX = pieX + sliceX - averageWidth + icon.width()
260 topY = pieY + sliceY + textHeight/2
261 painter.setPen(pen.color())
262 painter.setBrush(brush)
263 painter.drawText(leftX, topY, text)
265 screen = QtGui.QPainter(self)
266 screen.drawPixmap(QtCore.QPoint(0, 0), self._canvas)
268 QtGui.QWidget.paintEvent(self, paintEvent)
271 return len(self._children)
273 def _invalidate_view(self):
276 def _generate_mask(self, mask):
278 Specifies on the mask the shape of the pie menu
280 painter = QtGui.QPainter(mask)
281 painter.setPen(QtCore.Qt.color1)
282 painter.setBrush(QtCore.Qt.color1)
283 painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
285 def _select_at(self, index):
286 self._selectionIndex = index
288 numChildren = len(self._children)
289 loopDelta = max(numChildren, 1)
290 while self._selectionIndex < 0:
291 self._selectionIndex += loopDelta
292 while numChildren <= self._selectionIndex:
293 self._selectionIndex -= loopDelta
295 def _activate_at(self, index):
296 child = self.itemAt(index)
297 if child.action.isEnabled:
298 child.action.trigger()
299 self.activated.emit()
300 self.aboutToHide.emit()
303 def _index_to_angle(self, index, isShifted):
304 index = index % len(self._children)
306 totalWeight = sum(child.weight() for child in self._children)
309 baseAngle = (2 * math.pi) / totalWeight
314 angle -= (self._children[0].weight() * baseAngle) / 2
316 angle -= baseAngle / 2
320 for i, child in enumerate(self._children):
323 angle += child.weight() * baseAngle
324 while (2*math.pi) < angle:
329 def _angle_to_index(self, angle):
330 numChildren = len(self._children)
334 totalWeight = sum(child.weight() for child in self._children)
337 baseAngle = (2 * math.pi) / totalWeight
339 iterAngle = math.pi / 2 - (self.itemAt(0).weight * baseAngle) / 2
341 iterAngle += 2 * math.pi
343 oldIterAngle = iterAngle
344 for index, child in enumerate(self._children):
345 iterAngle += child.weight * baseAngle
346 if oldIterAngle < iterAngle and angle <= iterAngle:
348 elif oldIterAngle < (iterAngle + 2*math.pi) and angle <= (iterAngle + 2*math.pi):
350 oldIterAngle = iterAngle
352 def _radius_at(self, pos):
353 xDelta = pos.x() - self.rect().center().x()
354 yDelta = pos.y() - self.rect().center().y()
356 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
359 def _angle_at(self, pos):
360 xDelta = pos.x() - self.rect().center().x()
361 yDelta = pos.y() - self.rect().center().y()
363 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
364 angle = math.acos(xDelta / radius)
366 angle = 2*math.pi - angle
370 def _on_key_press(self, keyEvent):
371 if keyEvent.key in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
372 self._select_at(self._selectionIndex + 1)
373 elif keyEvent.key in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
374 self._select_at(self._selectionIndex - 1)
375 elif keyEvent.key in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
377 self._activate_at(self._selectionIndex)
378 elif keyEvent.key in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
381 def _on_mouse_press(self, mouseEvent):
382 self._mouseButtonPressed = True
385 if __name__ == "__main__":
386 app = QtGui.QApplication([])
393 singleAction = QtGui.QAction(None)
394 singleAction.setText("Boo")
395 singleItem = QActionPieItem(singleAction)
397 spie.insertItem(singleItem)
401 oneAction = QtGui.QAction(None)
402 oneAction.setText("Chew")
403 oneItem = QActionPieItem(oneAction)
404 twoAction = QtGui.QAction(None)
405 twoAction.setText("Foo")
406 twoItem = QActionPieItem(twoAction)
408 mpie.insertItem(oneItem)
409 mpie.insertItem(twoItem)
410 mpie.insertItem(oneItem)
411 mpie.insertItem(twoItem)