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)
186 painter.setPen(QtGui.QPen(light, 1))
187 painter.setBrush(QtCore.Qt.NoBrush)
188 painter.drawArc(innerRect, 225 * 16, 180 * 16)
190 painter.setPen(QtGui.QPen(dark, 1))
191 painter.drawArc(innerRect, 45 * 16, 180 * 16)
193 painter.setPen(QtGui.QPen(light, 1))
194 painter.setBrush(QtCore.Qt.NoBrush)
195 painter.drawArc(adjustmentRect, 45 * 16, 180 * 16)
196 painter.setPen(QtGui.QPen(dark, 1))
197 painter.drawArc(adjustmentRect, 225 * 16, 180 * 16)
199 r = QtCore.QRect(innerRect)
200 innerRect.setLeft(r.center().x() + ((r.left() - r.center().x()) / 3) * 1)
201 innerRect.setRight(r.center().x() + ((r.right() - r.center().x()) / 3) * 1)
202 innerRect.setTop(r.center().y() + ((r.top() - r.center().y()) / 3) * 1)
203 innerRect.setBottom(r.center().y() + ((r.bottom() - r.center().y()) / 3) * 1)
205 if self._selectionIndex == -1:
206 text = self.palette().highlightedText().color()
208 text = self.palette().text().color()
210 for i, child in enumerate(self._children):
211 text = child.action().text()
213 a = self._index_to_angle(i, True)
214 b = self._index_to_angle(i + 1, True)
217 middleAngle = (a + b) / 2
218 averageRadius = (self._innerRadius + self._outerRadius) / 2
220 sliceX = averageRadius * math.cos(middleAngle)
221 sliceY = - averageRadius * math.sin(middleAngle)
223 pieX = self._canvas.rect().center().x()
224 pieY = self._canvas.rect().center().y()
226 fontMetrics = painter.fontMetrics()
228 textBoundingRect = fontMetrics.boundingRect(text)
230 textBoundingRect = QtCore.QRect()
231 textWidth = textBoundingRect.width()
232 textHeight = textBoundingRect.height()
234 icon = child.action().icon().pixmap(
235 QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
239 averageWidth = (icon.width() + textWidth)/2
240 if not icon.isNull():
241 iconRect = QtCore.QRect(
242 pieX + sliceX - averageWidth,
243 pieY + sliceY - icon.height()/2,
248 painter.drawPixmap(iconRect, icon)
251 if i == self._selectionIndex:
252 if child.action().isEnabled():
253 pen = self.palette().highlightedText()
254 brush = self.palette().highlight()
256 pen = self.palette().mid()
257 brush = self.palette().background()
259 if child.action().isEnabled():
260 pen = self.palette().text()
262 pen = self.palette().mid()
263 brush = self.palette().background()
265 leftX = pieX + sliceX - averageWidth + icon.width()
266 topY = pieY + sliceY + textHeight/2
267 painter.setPen(pen.color())
268 painter.setBrush(brush)
269 painter.drawText(leftX, topY, text)
271 screen = QtGui.QPainter(self)
272 screen.drawPixmap(QtCore.QPoint(0, 0), self._canvas)
274 QtGui.QWidget.paintEvent(self, paintEvent)
277 return len(self._children)
279 def _invalidate_view(self):
282 def _generate_mask(self, mask):
284 Specifies on the mask the shape of the pie menu
286 painter = QtGui.QPainter(mask)
287 painter.setPen(QtCore.Qt.color1)
288 painter.setBrush(QtCore.Qt.color1)
289 painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
291 def _select_at(self, index):
292 self._selectionIndex = index
294 numChildren = len(self._children)
295 loopDelta = max(numChildren, 1)
296 while self._selectionIndex < 0:
297 self._selectionIndex += loopDelta
298 while numChildren <= self._selectionIndex:
299 self._selectionIndex -= loopDelta
301 def _activate_at(self, index):
302 child = self.itemAt(index)
303 if child.action.isEnabled:
304 child.action.trigger()
305 self.activated.emit()
306 self.aboutToHide.emit()
309 def _index_to_angle(self, index, isShifted):
310 index = index % len(self._children)
312 totalWeight = sum(child.weight() for child in self._children)
315 baseAngle = (2 * math.pi) / totalWeight
320 angle -= (self._children[0].weight() * baseAngle) / 2
322 angle -= baseAngle / 2
326 for i, child in enumerate(self._children):
329 angle += child.weight() * baseAngle
330 while (2*math.pi) < angle:
335 def _angle_to_index(self, angle):
336 numChildren = len(self._children)
340 totalWeight = sum(child.weight() for child in self._children)
343 baseAngle = (2 * math.pi) / totalWeight
345 iterAngle = math.pi / 2 - (self.itemAt(0).weight * baseAngle) / 2
347 iterAngle += 2 * math.pi
349 oldIterAngle = iterAngle
350 for index, child in enumerate(self._children):
351 iterAngle += child.weight * baseAngle
352 if oldIterAngle < iterAngle and angle <= iterAngle:
354 elif oldIterAngle < (iterAngle + 2*math.pi) and angle <= (iterAngle + 2*math.pi):
356 oldIterAngle = iterAngle
358 def _radius_at(self, pos):
359 xDelta = pos.x() - self.rect().center().x()
360 yDelta = pos.y() - self.rect().center().y()
362 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
365 def _angle_at(self, pos):
366 xDelta = pos.x() - self.rect().center().x()
367 yDelta = pos.y() - self.rect().center().y()
369 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
370 angle = math.acos(xDelta / radius)
372 angle = 2*math.pi - angle
376 def _on_key_press(self, keyEvent):
377 if keyEvent.key in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
378 self._select_at(self._selectionIndex + 1)
379 elif keyEvent.key in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
380 self._select_at(self._selectionIndex - 1)
381 elif keyEvent.key in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
383 self._activate_at(self._selectionIndex)
384 elif keyEvent.key in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
387 def _on_mouse_press(self, mouseEvent):
388 self._mouseButtonPressed = True
391 if __name__ == "__main__":
392 app = QtGui.QApplication([])
399 singleAction = QtGui.QAction(None)
400 singleAction.setText("Boo")
401 singleItem = QActionPieItem(singleAction)
403 spie.insertItem(singleItem)
407 oneAction = QtGui.QAction(None)
408 oneAction.setText("Chew")
409 oneItem = QActionPieItem(oneAction)
410 twoAction = QtGui.QAction(None)
411 twoAction.setText("Foo")
412 twoItem = QActionPieItem(twoAction)
414 mpie.insertItem(oneItem)
415 mpie.insertItem(twoItem)
416 mpie.insertItem(oneItem)
417 mpie.insertItem(twoItem)