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 PieFiling(object):
33 INNER_RADIUS_DEFAULT = 24
34 OUTER_RADIUS_DEFAULT = 64
39 NULL_CENTER = QtGui.QAction(None)
41 def __init__(self, centerPos):
42 self._innerRadius = self.INNER_RADIUS_DEFAULT
43 self._outerRadius = self.OUTER_RADIUS_DEFAULT
45 self._center = self.NULL_CENTER
47 self._centerPos = centerPos
49 def insertItem(self, item, index = -1):
50 self._children.insert(index, item)
52 def removeItemAt(self, index):
53 item = self._children.pop(index)
55 def set_center(self, item):
57 item = self.NULL_CENTER
66 def itemAt(self, index):
67 return self._children[index]
69 def indexAt(self, point):
70 return self._angle_to_index(self._angle_at(point))
72 def innerRadius(self):
73 return self._innerRadius
75 def setInnerRadius(self, radius):
76 self._innerRadius = radius
78 def setCenterPosition(self, centerPos):
79 self._centerPos = centerPos
81 def outerRadius(self):
82 return self._outerRadius
84 def setOuterRadius(self, radius):
85 self._outerRadius = radius
88 return iter(self._children)
91 return len(self._children)
93 def __getitem__(self, index):
94 return self._children[index]
96 def _index_to_angle(self, index, isShifted):
97 index = index % len(self._children)
99 totalWeight = sum(child.weight() for child in self._children)
102 baseAngle = (2 * math.pi) / totalWeight
107 angle -= (self._children[0].weight() * baseAngle) / 2
109 angle -= baseAngle / 2
113 for i, child in enumerate(self._children):
116 angle += child.weight() * baseAngle
117 while (2*math.pi) < angle:
122 def _angle_to_index(self, angle):
123 numChildren = len(self._children)
125 return self.SELECTION_CENTER
127 totalWeight = sum(child.weight() for child in self._children)
130 baseAngle = (2 * math.pi) / totalWeight
132 iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
134 iterAngle += 2 * math.pi
136 oldIterAngle = iterAngle
137 for index, child in enumerate(self._children):
138 iterAngle += child.weight() * baseAngle
139 if oldIterAngle < angle and angle <= iterAngle:
140 return index - 1 if index != 0 else numChildren - 1
141 elif oldIterAngle < (angle + 2*math.pi) and (angle + 2*math.pi <= iterAngle):
142 return index - 1 if index != 0 else numChildren - 1
143 oldIterAngle = iterAngle
145 def _radius_at(self, pos):
146 xDelta = pos.x() - self._centerPos.x()
147 yDelta = pos.y() - self._centerPos.y()
149 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
152 def _angle_at(self, pos):
153 xDelta = pos.x() - self._centerPos.x()
154 yDelta = pos.y() - self._centerPos.y()
156 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
157 angle = math.acos(xDelta / radius)
159 angle = 2*math.pi - angle
164 class PieArtist(object):
166 ICON_SIZE_DEFAULT = 32
168 def __init__(self, filing):
169 self._filing = filing
171 self._cachedOuterRadius = self._filing.outerRadius()
172 self._cachedInnerRadius = self._filing.innerRadius()
173 canvasSize = self._cachedOuterRadius * 2 + 1
174 self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
179 diameter = self._cachedOuterRadius * 2 + 1
180 return QtCore.QSize(diameter, diameter)
182 def show(self, palette):
183 self.palette = palette
186 self._cachedOuterRadius != self._filing.outerRadius() or
187 self._cachedInnerRadius != self._filing.innerRadius()
189 self._cachedOuterRadius = self._filing.outerRadius()
190 self._cachedInnerRadius = self._filing.innerRadius()
191 self._canvas = self._canvas.scaled(self.sizeHint())
193 if self._mask is None:
194 self._mask = QtGui.QBitmap(self._canvas.size())
195 self._mask.fill(QtCore.Qt.color0)
196 self._generate_mask(self._mask)
197 self._canvas.setMask(self._mask)
203 def paint(self, selectionIndex):
204 painter = QtGui.QPainter(self._canvas)
205 painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
207 adjustmentRect = self._canvas.rect().adjusted(0, 0, -1, -1)
209 numChildren = len(self._filing)
211 if selectionIndex == 0 and self._filing[0].isEnabled():
212 painter.setBrush(self.palette.highlight())
214 painter.setBrush(self.palette.background())
216 painter.fillRect(self.rect(), painter.brush())
218 for i in xrange(len(self._filing)):
219 self._paint_slice_background(painter, adjustmentRect, i, selectionIndex)
221 self._paint_center_background(painter, adjustmentRect, selectionIndex)
222 self._paint_center_foreground(painter, selectionIndex)
224 for i in xrange(len(self._filing)):
225 self._paint_slice_foreground(painter, i, selectionIndex)
229 def _generate_mask(self, mask):
231 Specifies on the mask the shape of the pie menu
233 painter = QtGui.QPainter(mask)
234 painter.setPen(QtCore.Qt.color1)
235 painter.setBrush(QtCore.Qt.color1)
236 painter.drawEllipse(mask.rect().adjusted(0, 0, -1, -1))
238 def _paint_slice_background(self, painter, adjustmentRect, i, selectionIndex):
239 if i == selectionIndex and self._filing[i].isEnabled():
240 painter.setBrush(self.palette.highlight())
242 painter.setBrush(self.palette.background())
243 painter.setPen(self.palette.mid().color())
245 a = self._filing._index_to_angle(i, True)
246 b = self._filing._index_to_angle(i + 1, True)
253 startAngleInDeg = (a * 360 * 16) / (2*math.pi)
254 sizeInDeg = (size * 360 * 16) / (2*math.pi)
255 painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
257 def _paint_slice_foreground(self, painter, i, selectionIndex):
258 child = self._filing[i]
260 a = self._filing._index_to_angle(i, True)
261 b = self._filing._index_to_angle(i + 1, True)
264 middleAngle = (a + b) / 2
265 averageRadius = (self._cachedInnerRadius + self._cachedOuterRadius) / 2
267 sliceX = averageRadius * math.cos(middleAngle)
268 sliceY = - averageRadius * math.sin(middleAngle)
270 pieX = self._canvas.rect().center().x()
271 pieY = self._canvas.rect().center().y()
273 painter, child.action(), i == selectionIndex, pieX+sliceX, pieY+sliceY
276 def _paint_label(self, painter, action, isSelected, x, y):
278 fontMetrics = painter.fontMetrics()
280 textBoundingRect = fontMetrics.boundingRect(text)
282 textBoundingRect = QtCore.QRect()
283 textWidth = textBoundingRect.width()
284 textHeight = textBoundingRect.height()
286 icon = action.icon().pixmap(
287 QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
291 averageWidth = (icon.width() + textWidth)/2
292 if not icon.isNull():
293 iconRect = QtCore.QRect(
300 painter.drawPixmap(iconRect, icon)
304 if action.isEnabled():
305 pen = self.palette.highlightedText()
306 brush = self.palette.highlight()
308 pen = self.palette.mid()
309 brush = self.palette.background()
311 if action.isEnabled():
312 pen = self.palette.text()
314 pen = self.palette.mid()
315 brush = self.palette.background()
317 leftX = x - averageWidth + icon.width()
318 topY = y + textHeight/2
319 painter.setPen(pen.color())
320 painter.setBrush(brush)
321 painter.drawText(leftX, topY, text)
323 def _paint_center_background(self, painter, adjustmentRect, selectionIndex):
324 dark = self.palette.dark().color()
325 light = self.palette.light().color()
326 if selectionIndex == PieFiling.SELECTION_CENTER and self._filing.center().isEnabled():
327 background = self.palette.highlight().color()
329 background = self.palette.background().color()
331 innerRadius = self._cachedInnerRadius
332 innerRect = QtCore.QRect(
333 adjustmentRect.center().x() - innerRadius,
334 adjustmentRect.center().y() - innerRadius,
339 painter.setPen(QtCore.Qt.NoPen)
340 painter.setBrush(background)
341 painter.drawPie(innerRect, 0, 360 * 16)
343 painter.setPen(QtGui.QPen(dark, 1))
344 painter.setBrush(QtCore.Qt.NoBrush)
345 painter.drawEllipse(innerRect)
347 painter.setPen(QtGui.QPen(dark, 1))
348 painter.setBrush(QtCore.Qt.NoBrush)
349 painter.drawEllipse(adjustmentRect)
351 r = QtCore.QRect(innerRect)
352 innerRect.setLeft(r.center().x() + ((r.left() - r.center().x()) / 3) * 1)
353 innerRect.setRight(r.center().x() + ((r.right() - r.center().x()) / 3) * 1)
354 innerRect.setTop(r.center().y() + ((r.top() - r.center().y()) / 3) * 1)
355 innerRect.setBottom(r.center().y() + ((r.bottom() - r.center().y()) / 3) * 1)
357 def _paint_center_foreground(self, painter, selectionIndex):
358 pieX = self._canvas.rect().center().x()
359 pieY = self._canvas.rect().center().y()
366 self._filing.center().action(),
367 selectionIndex == PieFiling.SELECTION_CENTER,
372 class QPieMenu(QtGui.QWidget):
374 activated = QtCore.pyqtSignal(int)
375 highlighted = QtCore.pyqtSignal(int)
376 canceled = QtCore.pyqtSignal()
377 aboutToShow = QtCore.pyqtSignal()
378 aboutToHide = QtCore.pyqtSignal()
380 def __init__(self, parent = None):
381 QtGui.QWidget.__init__(self, parent)
382 self._filing = PieFiling(self.rect().center())
383 self._artist = PieArtist(self._filing)
384 self._selectionIndex = PieFiling.SELECTION_NONE
386 self._mouseButtonPressed = False
387 self._mousePosition = ()
389 def popup(self, pos):
390 index = self.indexAt(self.mapFromGlobal(QtGui.QCursor.pos()))
391 self._mousePosition = pos
394 def insertItem(self, item, index = -1):
395 self._filing.insertItem(item, index)
398 def removeItemAt(self, index):
399 self._filing.removeItemAt(index)
402 def set_center(self, item):
403 self._filing.set_center(item)
409 def itemAt(self, index):
410 return self._filing.itemAt(index)
412 def indexAt(self, point):
413 return self._filing.indexAt(point)
415 def innerRadius(self):
416 return self._filing.innerRadius()
418 def setInnerRadius(self, radius):
419 self._filing.setInnerRadius(radius)
422 def outerRadius(self):
423 return self._filing.outerRadius()
425 def setOuterRadius(self, radius):
426 self._filing.setOuterRadius(radius)
430 return self._artist.sizeHint()
432 def mousePressEvent(self, mouseEvent):
433 lastSelection = self._selectionIndex
435 lastMousePos = mouseEvent.pos()
436 self._update_selection(lastMousePos)
437 self._mouseButtonPressed = True
438 self._mousePosition = lastMousePos
440 if lastSelection != self._selectionIndex:
441 self.highlighted.emit(self._selectionIndex)
444 def mouseMoveEvent(self, mouseEvent):
445 lastSelection = self._selectionIndex
447 lastMousePos = mouseEvent.pos()
448 self._update_selection(lastMousePos)
450 if lastSelection != self._selectionIndex:
451 self.highlighted.emit(self._selectionIndex)
454 def mouseReleaseEvent(self, mouseEvent):
455 lastSelection = self._selectionIndex
457 lastMousePos = mouseEvent.pos()
458 self._update_selection(lastMousePos)
459 self._mouseButtonPressed = False
460 self._mousePosition = ()
462 self._activate_at(self._selectionIndex)
465 def showEvent(self, showEvent):
466 self.aboutToShow.emit()
468 self._filing.setCenterPosition(self.rect().center())
469 mask = self._artist.show(self.palette())
472 lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
473 self._update_selection(lastMousePos)
475 QtGui.QWidget.showEvent(self, showEvent)
477 def hideEvent(self, hideEvent):
480 self._selectionIndex = PieFiling.SELECTION_NONE
481 QtGui.QWidget.hideEvent(self, hideEvent)
483 def paintEvent(self, paintEvent):
484 canvas = self._artist.paint(self._selectionIndex)
486 screen = QtGui.QPainter(self)
487 screen.drawPixmap(QtCore.QPoint(0, 0), canvas)
489 QtGui.QWidget.paintEvent(self, paintEvent)
491 def _select_at(self, index):
492 self._selectionIndex = index
494 numChildren = len(self._filing)
495 loopDelta = max(numChildren, 1)
496 while self._selectionIndex < 0:
497 self._selectionIndex += loopDelta
498 while numChildren <= self._selectionIndex:
499 self._selectionIndex -= loopDelta
501 def _update_selection(self, lastMousePos):
502 radius = self._filing._radius_at(lastMousePos)
503 if radius < self._filing.innerRadius():
504 self._selectionIndex = PieFiling.SELECTION_CENTER
505 elif radius <= self._filing.outerRadius():
506 self._select_at(self.indexAt(lastMousePos))
508 self._selectionIndex = PieFiling.SELECTION_NONE
510 def _activate_at(self, index):
511 if index == PieFiling.SELECTION_NONE:
512 print "Nothing selected"
514 elif index == PieFiling.SELECTION_CENTER:
515 child = self._filing.center()
517 child = self.itemAt(index)
518 if child.action().isEnabled():
519 child.action().trigger()
520 self.activated.emit(index)
521 self.aboutToHide.emit()
524 def _on_key_press(self, keyEvent):
525 if keyEvent.key in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Down, QtCore.Qt.Key_Tab]:
526 self._select_at(self._selectionIndex + 1)
528 elif keyEvent.key in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
529 self._select_at(self._selectionIndex - 1)
531 elif keyEvent.key in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Space]:
532 self._activate_at(self._selectionIndex)
533 elif keyEvent.key in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
534 self._activate_at(PieFiling.SELECTION_NONE)
541 def _on_about_to_hide(app):
545 if __name__ == "__main__":
546 app = QtGui.QApplication([])
547 PieFiling.NULL_CENTER.setEnabled(False)
554 singleAction = QtGui.QAction(None)
555 singleAction.setText("Boo")
556 singleItem = QActionPieItem(singleAction)
558 spie.insertItem(singleItem)
562 oneAction = QtGui.QAction(None)
563 oneAction.setText("Chew")
564 oneItem = QActionPieItem(oneAction)
565 twoAction = QtGui.QAction(None)
566 twoAction.setText("Foo")
567 twoItem = QActionPieItem(twoAction)
568 iconTextAction = QtGui.QAction(None)
569 iconTextAction.setText("Icon")
570 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
571 iconTextItem = QActionPieItem(iconTextAction)
573 mpie.insertItem(oneItem)
574 mpie.insertItem(twoItem)
575 mpie.insertItem(oneItem)
576 mpie.insertItem(iconTextItem)
580 oneAction = QtGui.QAction(None)
581 oneAction.setText("Chew")
582 oneAction.triggered.connect(lambda: _print("Chew"))
583 oneItem = QActionPieItem(oneAction)
584 twoAction = QtGui.QAction(None)
585 twoAction.setText("Foo")
586 twoAction.triggered.connect(lambda: _print("Foo"))
587 twoItem = QActionPieItem(twoAction)
588 iconAction = QtGui.QAction(None)
589 iconAction.setIcon(QtGui.QIcon.fromTheme("gtk-open"))
590 iconAction.triggered.connect(lambda: _print("Icon"))
591 iconItem = QActionPieItem(iconAction)
592 iconTextAction = QtGui.QAction(None)
593 iconTextAction.setText("Icon")
594 iconTextAction.setIcon(QtGui.QIcon.fromTheme("gtk-close"))
595 iconTextAction.triggered.connect(lambda: _print("Icon and text"))
596 iconTextItem = QActionPieItem(iconTextAction)
598 mpie.set_center(iconItem)
599 mpie.insertItem(oneItem)
600 mpie.insertItem(twoItem)
601 mpie.insertItem(oneItem)
602 mpie.insertItem(iconTextItem)
604 mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))