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
56 self._mouseButtonPressed = False
57 self._mousePosition = ()
59 canvasSize = self._outerRadius * 2 + 1
60 self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
64 index = self.indexAt(self.mapFromGlobal(QtGui.QCursor.pos()))
65 self._mousePosition = pos
68 def insertItem(self, item, index = -1):
69 self._children.insert(index, item)
72 def removeItemAt(self, index):
73 item = self._children.pop(index)
76 def set_center(self, item):
83 def itemAt(self, index):
84 return self._children[index]
86 def indexAt(self, point):
87 return self._angle_to_index(self._angle_at(point))
89 def innerRadius(self):
90 return self._innerRadius
92 def setInnerRadius(self, radius):
93 self._innerRadius = radius
95 def outerRadius(self):
96 return self._outerRadius
98 def setOuterRadius(self, radius):
99 self._outerRadius = radius
100 self._canvas = self._canvas.scaled(self.sizeHint())
103 diameter = self._outerRadius * 2 + 1
104 return QtCore.QSize(diameter, diameter)
106 def mousePressEvent(self, mouseEvent):
107 lastSelection = self._selectionIndex
109 lastMousePos = mouseEvent.pos()
110 self._update_selection(lastMousePos)
111 self._mouseButtonPressed = True
112 self._mousePosition = lastMousePos
114 if lastSelection != self._selectionIndex:
115 self.highlighted.emit(self._selectionIndex)
118 def mouseMoveEvent(self, mouseEvent):
119 lastSelection = self._selectionIndex
121 lastMousePos = mouseEvent.pos()
122 self._update_selection(lastMousePos)
124 if lastSelection != self._selectionIndex:
125 self.highlighted.emit(self._selectionIndex)
128 def mouseReleaseEvent(self, mouseEvent):
129 lastSelection = self._selectionIndex
131 lastMousePos = mouseEvent.pos()
132 self._update_selection(lastMousePos)
133 self._mouseButtonPressed = False
134 self._mousePosition = ()
136 self._activate_at(self._selectionIndex)
139 def showEvent(self, showEvent):
140 self.aboutToShow.emit()
142 if self._mask is None:
143 self._mask = QtGui.QBitmap(self._canvas.size())
144 self._mask.fill(QtCore.Qt.color0)
145 self._generate_mask(self._mask)
146 self._canvas.setMask(self._mask)
147 self.setMask(self._mask)
149 lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
150 self._update_selection(lastMousePos)
152 QtGui.QWidget.showEvent(self, showEvent)
154 def hideEvent(self, hideEvent):
156 self._selectionIndex = self.SELECTION_NONE
157 QtGui.QWidget.hideEvent(self, hideEvent)
159 def paintEvent(self, paintEvent):
160 painter = QtGui.QPainter(self._canvas)
161 painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
163 adjustmentRect = self._canvas.rect().adjusted(0, 0, -1, -1)
165 numChildren = len(self._children)
167 if self._selectionIndex == 0 and self._children[0].isEnabled():
168 painter.setBrush(self.palette().highlight())
170 painter.setBrush(self.palette().background())
172 painter.fillRect(self.rect(), painter.brush())
174 for i in xrange(len(self._children)):
175 self._paint_slice_background(painter, adjustmentRect, i)
177 self._paint_center_background(painter, adjustmentRect)
178 self._paint_center_foreground(painter)
180 for i in xrange(len(self._children)):
181 self._paint_slice_foreground(painter, i)
183 screen = QtGui.QPainter(self)
184 screen.drawPixmap(QtCore.QPoint(0, 0), self._canvas)
186 QtGui.QWidget.paintEvent(self, paintEvent)
189 return len(self._children)
191 def _generate_mask(self, mask):
193 Specifies on the mask the shape of the pie menu
195 painter = QtGui.QPainter(mask)
196 painter.setPen(QtCore.Qt.color1)
197 painter.setBrush(QtCore.Qt.color1)
198 painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
200 def _paint_slice_background(self, painter, adjustmentRect, i):
201 if i == self._selectionIndex and self._children[i].isEnabled():
202 painter.setBrush(self.palette().highlight())
204 painter.setBrush(self.palette().background())
205 painter.setPen(self.palette().mid().color())
207 a = self._index_to_angle(i, True)
208 b = self._index_to_angle(i + 1, True)
215 startAngleInDeg = (a * 360 * 16) / (2*math.pi)
216 sizeInDeg = (size * 360 * 16) / (2*math.pi)
217 painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
219 def _paint_slice_foreground(self, painter, i):
220 child = self._children[i]
222 a = self._index_to_angle(i, True)
223 b = self._index_to_angle(i + 1, True)
226 middleAngle = (a + b) / 2
227 averageRadius = (self._innerRadius + self._outerRadius) / 2
229 sliceX = averageRadius * math.cos(middleAngle)
230 sliceY = - averageRadius * math.sin(middleAngle)
232 pieX = self._canvas.rect().center().x()
233 pieY = self._canvas.rect().center().y()
235 painter, child.action(), i == self._selectionIndex, pieX+sliceX, pieY+sliceY
238 def _paint_label(self, painter, action, isSelected, x, y):
240 fontMetrics = painter.fontMetrics()
242 textBoundingRect = fontMetrics.boundingRect(text)
244 textBoundingRect = QtCore.QRect()
245 textWidth = textBoundingRect.width()
246 textHeight = textBoundingRect.height()
248 icon = action.icon().pixmap(
249 QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
253 averageWidth = (icon.width() + textWidth)/2
254 if not icon.isNull():
255 iconRect = QtCore.QRect(
262 painter.drawPixmap(iconRect, icon)
266 if action.isEnabled():
267 pen = self.palette().highlightedText()
268 brush = self.palette().highlight()
270 pen = self.palette().mid()
271 brush = self.palette().background()
273 if action.isEnabled():
274 pen = self.palette().text()
276 pen = self.palette().mid()
277 brush = self.palette().background()
279 leftX = x - averageWidth + icon.width()
280 topY = y + textHeight/2
281 painter.setPen(pen.color())
282 painter.setBrush(brush)
283 painter.drawText(leftX, topY, text)
285 def _paint_center_background(self, painter, adjustmentRect):
286 dark = self.palette().dark().color()
287 light = self.palette().light().color()
288 if self._selectionIndex == self.SELECTION_CENTER and self._center.isEnabled():
289 background = self.palette().highlight().color()
291 background = self.palette().background().color()
293 innerRect = QtCore.QRect(
294 adjustmentRect.center().x() - self._innerRadius,
295 adjustmentRect.center().y() - self._innerRadius,
296 self._innerRadius * 2 + 1,
297 self._innerRadius * 2 + 1,
300 painter.setPen(QtCore.Qt.NoPen)
301 painter.setBrush(background)
302 painter.drawPie(innerRect, 0, 360 * 16)
304 painter.setPen(QtGui.QPen(dark, 1))
305 painter.setBrush(QtCore.Qt.NoBrush)
306 painter.drawEllipse(innerRect)
308 painter.setPen(QtGui.QPen(dark, 1))
309 painter.setBrush(QtCore.Qt.NoBrush)
310 painter.drawEllipse(adjustmentRect)
312 r = QtCore.QRect(innerRect)
313 innerRect.setLeft(r.center().x() + ((r.left() - r.center().x()) / 3) * 1)
314 innerRect.setRight(r.center().x() + ((r.right() - r.center().x()) / 3) * 1)
315 innerRect.setTop(r.center().y() + ((r.top() - r.center().y()) / 3) * 1)
316 innerRect.setBottom(r.center().y() + ((r.bottom() - r.center().y()) / 3) * 1)
318 def _paint_center_foreground(self, painter):
319 pieX = self._canvas.rect().center().x()
320 pieY = self._canvas.rect().center().y()
326 painter, self._center.action(), self._selectionIndex == self.SELECTION_CENTER, x, y
329 def _select_at(self, index):
330 self._selectionIndex = index
332 numChildren = len(self._children)
333 loopDelta = max(numChildren, 1)
334 while self._selectionIndex < 0:
335 self._selectionIndex += loopDelta
336 while numChildren <= self._selectionIndex:
337 self._selectionIndex -= loopDelta
339 def _update_selection(self, lastMousePos):
340 radius = self._radius_at(lastMousePos)
341 if radius < self._innerRadius:
342 self._selectionIndex = self.SELECTION_CENTER
343 elif radius <= self._outerRadius:
344 self._select_at(self._angle_to_index(self._angle_at(lastMousePos)))
346 self._selectionIndex = self.SELECTION_NONE
348 def _activate_at(self, index):
349 if index == self.SELECTION_NONE:
350 print "Nothing selected"
352 elif index == self.SELECTION_CENTER:
355 child = self.itemAt(index)
356 if child.action().isEnabled():
357 child.action().trigger()
358 self.activated.emit(index)
359 self.aboutToHide.emit()
362 def _index_to_angle(self, index, isShifted):
363 index = index % len(self._children)
365 totalWeight = sum(child.weight() for child in self._children)
368 baseAngle = (2 * math.pi) / totalWeight
373 angle -= (self._children[0].weight() * baseAngle) / 2
375 angle -= baseAngle / 2
379 for i, child in enumerate(self._children):
382 angle += child.weight() * baseAngle
383 while (2*math.pi) < angle:
388 def _angle_to_index(self, angle):
389 numChildren = len(self._children)
391 return self.SELECTION_CENTER
393 totalWeight = sum(child.weight() for child in self._children)
396 baseAngle = (2 * math.pi) / totalWeight
398 iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
400 iterAngle += 2 * math.pi
402 oldIterAngle = iterAngle
403 for index, child in enumerate(self._children):
404 iterAngle += child.weight() * baseAngle
405 if oldIterAngle < angle and angle <= iterAngle:
406 return index - 1 if index != 0 else numChildren - 1
407 elif oldIterAngle < (angle + 2*math.pi) and (angle + 2*math.pi <= iterAngle):
408 return index - 1 if index != 0 else numChildren - 1
409 oldIterAngle = iterAngle
411 def _radius_at(self, pos):
412 xDelta = pos.x() - self.rect().center().x()
413 yDelta = pos.y() - self.rect().center().y()
415 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
418 def _angle_at(self, pos):
419 xDelta = pos.x() - self.rect().center().x()
420 yDelta = pos.y() - self.rect().center().y()
422 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
423 angle = math.acos(xDelta / radius)
425 angle = 2*math.pi - angle
429 def _on_key_press(self, keyEvent):
430 if keyEvent.key in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
431 self._select_at(self._selectionIndex + 1)
433 elif keyEvent.key in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
434 self._select_at(self._selectionIndex - 1)
436 elif keyEvent.key in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
437 self._activate_at(self._selectionIndex)
438 elif keyEvent.key in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
439 self._activate_at(self.SELECTION_NONE)
441 def _on_mouse_press(self, mouseEvent):
442 self._mouseButtonPressed = True
449 def _on_about_to_hide(app):
453 if __name__ == "__main__":
454 app = QtGui.QApplication([])
455 QPieMenu.NULL_CENTER.setEnabled(False)
462 singleAction = QtGui.QAction(None)
463 singleAction.setText("Boo")
464 singleItem = QActionPieItem(singleAction)
466 spie.insertItem(singleItem)
470 oneAction = QtGui.QAction(None)
471 oneAction.setText("Chew")
472 oneItem = QActionPieItem(oneAction)
473 twoAction = QtGui.QAction(None)
474 twoAction.setText("Foo")
475 twoItem = QActionPieItem(twoAction)
476 iconTextAction = QtGui.QAction(None)
477 iconTextAction.setText("Icon")
478 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
479 iconTextItem = QActionPieItem(iconTextAction)
481 mpie.insertItem(oneItem)
482 mpie.insertItem(twoItem)
483 mpie.insertItem(oneItem)
484 mpie.insertItem(iconTextItem)
488 oneAction = QtGui.QAction(None)
489 oneAction.setText("Chew")
490 oneAction.triggered.connect(lambda: _print("Chew"))
491 oneItem = QActionPieItem(oneAction)
492 twoAction = QtGui.QAction(None)
493 twoAction.setText("Foo")
494 twoAction.triggered.connect(lambda: _print("Foo"))
495 twoItem = QActionPieItem(twoAction)
496 iconAction = QtGui.QAction(None)
497 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
498 iconAction.triggered.connect(lambda: _print("Icon"))
499 iconItem = QActionPieItem(iconAction)
500 iconTextAction = QtGui.QAction(None)
501 iconTextAction.setText("Icon")
502 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
503 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
504 iconTextItem = QActionPieItem(iconTextAction)
506 mpie.set_center(iconItem)
507 mpie.insertItem(oneItem)
508 mpie.insertItem(twoItem)
509 mpie.insertItem(oneItem)
510 mpie.insertItem(iconTextItem)
512 mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))