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()
46 NULL_CENTER = QtGui.QAction(None)
48 def __init__(self, parent = None):
49 QtGui.QWidget.__init__(self, parent)
50 self._innerRadius = self.INNER_RADIUS_DEFAULT
51 self._outerRadius = self.OUTER_RADIUS_DEFAULT
53 self._center = self.NULL_CENTER
54 self._selectionIndex = self.SELECTION_NONE
57 self._mouseButtonPressed = True
58 self._mousePosition = ()
60 canvasSize = self._outerRadius * 2 + 1
61 self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
65 index = self.indexAt(self.mapFromGlobal(QtGui.QCursor.pos()))
66 self._mousePosition = pos
69 def insertItem(self, item, index = -1):
70 self._children.insert(index, item)
71 self._invalidate_view()
73 def removeItemAt(self, index):
74 item = self._children.pop(index)
75 self._invalidate_view()
77 def set_center(self, item):
82 self._invalidate_view()
84 def itemAt(self, index):
85 return self._children[index]
87 def indexAt(self, point):
88 return self._angle_to_index(self._angle_at(point))
90 def setHighlightedItem(self, index):
93 def highlightedItem(self):
96 def innerRadius(self):
97 return self._innerRadius
99 def setInnerRadius(self, radius):
100 self._innerRadius = radius
102 def outerRadius(self):
103 return self._outerRadius
105 def setOuterRadius(self, radius):
106 self._outerRadius = radius
107 self._canvas = self._canvas.scaled(self.sizeHint())
110 diameter = self._outerRadius * 2 + 1
111 return QtCore.QSize(diameter, diameter)
113 def showEvent(self, showEvent):
114 self.aboutToShow.emit()
116 if self._mask is None:
117 self._mask = QtGui.QBitmap(self._canvas.size())
118 self._mask.fill(QtCore.Qt.color0)
119 self._generate_mask(self._mask)
120 self._canvas.setMask(self._mask)
121 self.setMask(self._mask)
125 lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
126 radius = self._radius_at(lastMousePos)
127 if self._innerRadius <= radius and radius <= self._outerRadius:
128 self._select_at(self._angle_to_index(lastMousePos))
130 if radius < self._innerRadius:
131 self._selectionIndex = self.SELECTION_CENTER
133 self._selectionIndex = self.SELECTION_NONE
135 QtGui.QWidget.showEvent(self, showEvent)
137 def hideEvent(self, hideEvent):
139 self._selectionIndex = self.SELECTION_NONE
140 QtGui.QWidget.hideEvent(self, hideEvent)
142 def paintEvent(self, paintEvent):
143 painter = QtGui.QPainter(self._canvas)
144 painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
146 adjustmentRect = self._canvas.rect().adjusted(0, 0, -1, -1)
148 numChildren = len(self._children)
150 if self._selectionIndex == 0 and self._children[0].isEnabled():
151 painter.setBrush(self.palette().highlight())
153 painter.setBrush(self.palette().background())
155 painter.fillRect(self.rect(), painter.brush())
157 for i in xrange(len(self._children)):
158 self._paint_slice_background(painter, adjustmentRect, i)
160 self._paint_center_background(painter, adjustmentRect)
161 self._paint_center_foreground(painter)
163 for i in xrange(len(self._children)):
164 self._paint_slice_foreground(painter, i)
166 screen = QtGui.QPainter(self)
167 screen.drawPixmap(QtCore.QPoint(0, 0), self._canvas)
169 QtGui.QWidget.paintEvent(self, paintEvent)
172 return len(self._children)
174 def _invalidate_view(self):
177 def _generate_mask(self, mask):
179 Specifies on the mask the shape of the pie menu
181 painter = QtGui.QPainter(mask)
182 painter.setPen(QtCore.Qt.color1)
183 painter.setBrush(QtCore.Qt.color1)
184 painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
186 def _paint_slice_background(self, painter, adjustmentRect, i):
187 if i == self._selectionIndex and self._children[i].isEnabled():
188 painter.setBrush(self.palette().highlight())
190 painter.setBrush(self.palette().background())
191 painter.setPen(self.palette().mid().color())
193 a = self._index_to_angle(i, True)
194 b = self._index_to_angle(i + 1, True)
201 startAngleInDeg = (a * 360 * 16) / (2*math.pi)
202 sizeInDeg = (size * 360 * 16) / (2*math.pi)
203 painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
205 def _paint_slice_foreground(self, painter, i):
206 child = self._children[i]
208 a = self._index_to_angle(i, True)
209 b = self._index_to_angle(i + 1, True)
212 middleAngle = (a + b) / 2
213 averageRadius = (self._innerRadius + self._outerRadius) / 2
215 sliceX = averageRadius * math.cos(middleAngle)
216 sliceY = - averageRadius * math.sin(middleAngle)
218 pieX = self._canvas.rect().center().x()
219 pieY = self._canvas.rect().center().y()
221 painter, child.action(), i == self._selectionIndex, pieX+sliceX, pieY+sliceY
224 def _paint_label(self, painter, action, isSelected, x, y):
226 fontMetrics = painter.fontMetrics()
228 textBoundingRect = fontMetrics.boundingRect(text)
230 textBoundingRect = QtCore.QRect()
231 textWidth = textBoundingRect.width()
232 textHeight = textBoundingRect.height()
234 icon = 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(
248 painter.drawPixmap(iconRect, icon)
252 if action.isEnabled():
253 pen = self.palette().highlightedText()
254 brush = self.palette().highlight()
256 pen = self.palette().mid()
257 brush = self.palette().background()
259 if action.isEnabled():
260 pen = self.palette().text()
262 pen = self.palette().mid()
263 brush = self.palette().background()
265 leftX = x - averageWidth + icon.width()
266 topY = y + textHeight/2
267 painter.setPen(pen.color())
268 painter.setBrush(brush)
269 painter.drawText(leftX, topY, text)
271 def _paint_center_background(self, painter, adjustmentRect):
272 dark = self.palette().dark().color()
273 light = self.palette().light().color()
274 if self._selectionIndex == self.SELECTION_CENTER and self._center.isEnabled():
275 background = self.palette().highlight().color()
277 background = self.palette().background().color()
279 innerRect = QtCore.QRect(
280 adjustmentRect.center().x() - self._innerRadius,
281 adjustmentRect.center().y() - self._innerRadius,
282 self._innerRadius * 2 + 1,
283 self._innerRadius * 2 + 1,
286 painter.setPen(QtCore.Qt.NoPen)
287 painter.setBrush(background)
288 painter.drawPie(innerRect, 0, 360 * 16)
290 painter.setPen(QtGui.QPen(dark, 1))
291 painter.setBrush(QtCore.Qt.NoBrush)
292 painter.drawEllipse(innerRect)
294 painter.setPen(QtGui.QPen(dark, 1))
295 painter.setBrush(QtCore.Qt.NoBrush)
296 painter.drawEllipse(adjustmentRect)
298 r = QtCore.QRect(innerRect)
299 innerRect.setLeft(r.center().x() + ((r.left() - r.center().x()) / 3) * 1)
300 innerRect.setRight(r.center().x() + ((r.right() - r.center().x()) / 3) * 1)
301 innerRect.setTop(r.center().y() + ((r.top() - r.center().y()) / 3) * 1)
302 innerRect.setBottom(r.center().y() + ((r.bottom() - r.center().y()) / 3) * 1)
304 def _paint_center_foreground(self, painter):
305 pieX = self._canvas.rect().center().x()
306 pieY = self._canvas.rect().center().y()
312 painter, self._center.action(), self._selectionIndex == self.SELECTION_CENTER, x, y
315 def _select_at(self, index):
316 self._selectionIndex = index
318 numChildren = len(self._children)
319 loopDelta = max(numChildren, 1)
320 while self._selectionIndex < 0:
321 self._selectionIndex += loopDelta
322 while numChildren <= self._selectionIndex:
323 self._selectionIndex -= loopDelta
325 def _activate_at(self, index):
326 child = self.itemAt(index)
327 if child.action.isEnabled:
328 child.action.trigger()
329 self.activated.emit()
330 self.aboutToHide.emit()
333 def _index_to_angle(self, index, isShifted):
334 index = index % len(self._children)
336 totalWeight = sum(child.weight() for child in self._children)
339 baseAngle = (2 * math.pi) / totalWeight
344 angle -= (self._children[0].weight() * baseAngle) / 2
346 angle -= baseAngle / 2
350 for i, child in enumerate(self._children):
353 angle += child.weight() * baseAngle
354 while (2*math.pi) < angle:
359 def _angle_to_index(self, angle):
360 numChildren = len(self._children)
362 return self.SELECTION_CENTER
364 totalWeight = sum(child.weight() for child in self._children)
367 baseAngle = (2 * math.pi) / totalWeight
369 iterAngle = math.pi / 2 - (self.itemAt(0).weight * baseAngle) / 2
371 iterAngle += 2 * math.pi
373 oldIterAngle = iterAngle
374 for index, child in enumerate(self._children):
375 iterAngle += child.weight * baseAngle
376 if oldIterAngle < iterAngle and angle <= iterAngle:
378 elif oldIterAngle < (iterAngle + 2*math.pi) and angle <= (iterAngle + 2*math.pi):
380 oldIterAngle = iterAngle
382 def _radius_at(self, pos):
383 xDelta = pos.x() - self.rect().center().x()
384 yDelta = pos.y() - self.rect().center().y()
386 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
389 def _angle_at(self, pos):
390 xDelta = pos.x() - self.rect().center().x()
391 yDelta = pos.y() - self.rect().center().y()
393 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
394 angle = math.acos(xDelta / radius)
396 angle = 2*math.pi - angle
400 def _on_key_press(self, keyEvent):
401 if keyEvent.key in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
402 self._select_at(self._selectionIndex + 1)
403 elif keyEvent.key in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
404 self._select_at(self._selectionIndex - 1)
405 elif keyEvent.key in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
407 self._activate_at(self._selectionIndex)
408 elif keyEvent.key in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
411 def _on_mouse_press(self, mouseEvent):
412 self._mouseButtonPressed = True
415 if __name__ == "__main__":
416 app = QtGui.QApplication([])
417 QPieMenu.NULL_CENTER.setEnabled(False)
424 singleAction = QtGui.QAction(None)
425 singleAction.setText("Boo")
426 singleItem = QActionPieItem(singleAction)
428 spie.insertItem(singleItem)
432 oneAction = QtGui.QAction(None)
433 oneAction.setText("Chew")
434 oneItem = QActionPieItem(oneAction)
435 twoAction = QtGui.QAction(None)
436 twoAction.setText("Foo")
437 twoItem = QActionPieItem(twoAction)
438 iconTextAction = QtGui.QAction(None)
439 iconTextAction.setText("Icon")
440 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
441 iconTextItem = QActionPieItem(iconTextAction)
443 mpie.insertItem(oneItem)
444 mpie.insertItem(twoItem)
445 mpie.insertItem(oneItem)
446 mpie.insertItem(iconTextItem)
450 oneAction = QtGui.QAction(None)
451 oneAction.setText("Chew")
452 oneItem = QActionPieItem(oneAction)
453 twoAction = QtGui.QAction(None)
454 twoAction.setText("Foo")
455 twoItem = QActionPieItem(twoAction)
456 iconAction = QtGui.QAction(None)
457 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
458 iconItem = QActionPieItem(iconAction)
459 iconTextAction = QtGui.QAction(None)
460 iconTextAction.setText("Icon")
461 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
462 iconTextItem = QActionPieItem(iconTextAction)
464 mpie.set_center(iconItem)
465 mpie.insertItem(oneItem)
466 mpie.insertItem(twoItem)
467 mpie.insertItem(oneItem)
468 mpie.insertItem(iconTextItem)