66899ac1756040709070c5bc9d07cc55dcc858bc
[ejpi] / src / libraries / qtpie.py
1 #!/usr/bin/env python
2
3 import math
4
5 from PyQt4 import QtGui
6 from PyQt4 import QtCore
7
8
9 class QActionPieItem(object):
10
11         def __init__(self, action, weight = 1):
12                 self._action = action
13                 self._weight = weight
14
15         def action(self):
16                 return self._action
17
18         def setWeight(self, weight):
19                 self._weight = weight
20
21         def weight(self):
22                 return self._weight
23
24         def setEnabled(self, enabled = True):
25                 self._action.setEnabled(enabled)
26
27         def isEnabled(self):
28                 return self._action.isEnabled()
29
30
31 class QPieMenu(QtGui.QWidget):
32
33         INNER_RADIUS_DEFAULT = 24
34         OUTER_RADIUS_DEFAULT = 64
35         ICON_SIZE_DEFAULT = 32
36
37         activated = QtCore.pyqtSignal((), (int, ))
38         highlighted = QtCore.pyqtSignal(int)
39         canceled = QtCore.pyqtSignal()
40         aboutToShow = QtCore.pyqtSignal()
41         aboutToHide = QtCore.pyqtSignal()
42
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
47                 self._children = []
48                 self._selectionIndex = -2
49
50                 self._motion = 0
51                 self._mouseButtonPressed = True
52                 self._mousePosition = ()
53
54                 canvasSize = self._outerRadius * 2 + 1
55                 self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
56                 self._mask = None
57
58         def popup(self, pos):
59                 index = self.indexAt(self.mapFromGlobal(QtGui.QCursor.pos()))
60                 self._mousePosition = pos
61                 self.show()
62
63         def insertItem(self, item, index = -1):
64                 self._children.insert(index, item)
65                 self._invalidate_view()
66
67         def removeItemAt(self, index):
68                 item = self._children.pop(index)
69                 self._invalidate_view()
70
71         def clear(self):
72                 del self._children[:]
73                 self._invalidate_view()
74
75         def itemAt(self, index):
76                 return self._children[index]
77
78         def indexAt(self, point):
79                 return self._angle_to_index(self._angle_at(point))
80
81         def setHighlightedItem(self, index):
82                 pass
83
84         def highlightedItem(self):
85                 pass
86
87         def innerRadius(self):
88                 return self._innerRadius
89
90         def setInnerRadius(self, radius):
91                 self._innerRadius = radius
92
93         def outerRadius(self):
94                 return self._outerRadius
95
96         def setOuterRadius(self, radius):
97                 self._outerRadius = radius
98                 self._canvas = self._canvas.scaled(self.sizeHint())
99
100         def sizeHint(self):
101                 diameter = self._outerRadius * 2 + 1
102                 return QtCore.QSize(diameter, diameter)
103
104         def showEvent(self, showEvent):
105                 self.aboutToShow.emit()
106
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)
113
114                 self._motion = 0
115
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))
120                 else:
121                         if radius < self._innerRadius:
122                                 self._selectionIndex = -1
123                         else:
124                                 self._selectionIndex = -2
125
126                 QtGui.QWidget.showEvent(self, showEvent)
127
128         def hideEvent(self, hideEvent):
129                 self.canceled.emit()
130                 self._selectionIndex = -2
131                 QtGui.QWidget.hideEvent(self, hideEvent)
132
133         def paintEvent(self, paintEvent):
134                 painter = QtGui.QPainter(self._canvas)
135                 painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
136
137                 adjustmentRect = self._canvas.rect().adjusted(0, 0, -1, -1)
138
139                 numChildren = len(self._children)
140                 if numChildren < 2:
141                         if self._selectionIndex == 0 and self._children[0].isEnabled():
142                                 painter.setBrush(self.palette().highlight())
143                         else:
144                                 painter.setBrush(self.palette().background())
145
146                         painter.fillRect(self.rect(), painter.brush())
147                 else:
148                         for i, child in enumerate(self._children):
149                                 if i == self._selectionIndex:
150                                         painter.setBrush(self.palette().highlight())
151                                 else:
152                                         painter.setBrush(self.palette().background())
153                                 painter.setPen(self.palette().mid().color())
154
155                                 a = self._index_to_angle(i, True)
156                                 b = self._index_to_angle(i + 1, True)
157                                 if b < a:
158                                         b += 2*math.pi
159                                 size = b - a
160                                 if size < 0:
161                                         size += 2*math.pi
162
163                                 startAngleInDeg = (a * 360 * 16) / (2*math.pi)
164                                 sizeInDeg = (size * 360 * 16) / (2*math.pi)
165                                 painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
166
167                 dark = self.palette().dark().color()
168                 light = self.palette().light().color()
169                 if self._selectionIndex == -1:
170                         background = self.palette().highlight().color()
171                 else:
172                         background = self.palette().background().color()
173
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,
179                 )
180
181                 painter.setPen(QtCore.Qt.NoPen)
182                 painter.setBrush(background)
183                 painter.drawPie(innerRect, 0, 360 * 16)
184
185                 painter.setPen(QtGui.QPen(dark, 1))
186                 painter.setBrush(QtCore.Qt.NoBrush)
187                 painter.drawEllipse(innerRect)
188
189                 painter.setPen(QtGui.QPen(dark, 1))
190                 painter.setBrush(QtCore.Qt.NoBrush)
191                 painter.drawEllipse(adjustmentRect)
192
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)
198
199                 if self._selectionIndex == -1:
200                         text = self.palette().highlightedText().color()
201                 else:
202                         text = self.palette().text().color()
203
204                 for i, child in enumerate(self._children):
205                         text = child.action().text()
206
207                         a = self._index_to_angle(i, True)
208                         b = self._index_to_angle(i + 1, True)
209                         if b < a:
210                                 b += 2*math.pi
211                         middleAngle = (a + b) / 2
212                         averageRadius = (self._innerRadius + self._outerRadius) / 2
213
214                         sliceX = averageRadius * math.cos(middleAngle)
215                         sliceY = - averageRadius * math.sin(middleAngle)
216
217                         pieX = self._canvas.rect().center().x()
218                         pieY = self._canvas.rect().center().y()
219
220                         fontMetrics = painter.fontMetrics()
221                         if text:
222                                 textBoundingRect = fontMetrics.boundingRect(text)
223                         else:
224                                 textBoundingRect = QtCore.QRect()
225                         textWidth = textBoundingRect.width()
226                         textHeight = textBoundingRect.height()
227
228                         icon = child.action().icon().pixmap(
229                                 QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
230                                 QtGui.QIcon.Normal,
231                                 QtGui.QIcon.On,
232                         )
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,
238                                         icon.width(),
239                                         icon.height(),
240                                 )
241
242                                 painter.drawPixmap(iconRect, icon)
243
244                         if text:
245                                 if i == self._selectionIndex:
246                                         if child.action().isEnabled():
247                                                 pen = self.palette().highlightedText()
248                                                 brush = self.palette().highlight()
249                                         else:
250                                                 pen = self.palette().mid()
251                                                 brush = self.palette().background()
252                                 else:
253                                         if child.action().isEnabled():
254                                                 pen = self.palette().text()
255                                         else:
256                                                 pen = self.palette().mid()
257                                         brush = self.palette().background()
258
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)
264
265                 screen = QtGui.QPainter(self)
266                 screen.drawPixmap(QtCore.QPoint(0, 0), self._canvas)
267
268                 QtGui.QWidget.paintEvent(self, paintEvent)
269
270         def __len__(self):
271                 return len(self._children)
272
273         def _invalidate_view(self):
274                 pass
275
276         def _generate_mask(self, mask):
277                 """
278                 Specifies on the mask the shape of the pie menu
279                 """
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))
284
285         def _select_at(self, index):
286                 self._selectionIndex = index
287
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
294
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()
301                 self.hide()
302
303         def _index_to_angle(self, index, isShifted):
304                 index = index % len(self._children)
305
306                 totalWeight = sum(child.weight() for child in self._children)
307                 if totalWeight == 0:
308                         totalWeight = 1
309                 baseAngle = (2 * math.pi) / totalWeight
310
311                 angle = math.pi / 2
312                 if isShifted:
313                         if self._children:
314                                 angle -= (self._children[0].weight() * baseAngle) / 2
315                         else:
316                                 angle -= baseAngle / 2
317                 while angle < 0:
318                         angle += 2*math.pi
319
320                 for i, child in enumerate(self._children):
321                         if index < i:
322                                 break
323                         angle += child.weight() * baseAngle
324                 while (2*math.pi) < angle:
325                         angle -= 2*math.pi
326
327                 return angle
328
329         def _angle_to_index(self, angle):
330                 numChildren = len(self._children)
331                 if numChildren == 0:
332                         return -1
333
334                 totalWeight = sum(child.weight() for child in self._children)
335                 if totalWeight == 0:
336                         totalWeight = 1
337                 baseAngle = (2 * math.pi) / totalWeight
338
339                 iterAngle = math.pi / 2 - (self.itemAt(0).weight * baseAngle) / 2
340                 while iterAngle < 0:
341                         iterAngle += 2 * math.pi
342
343                 oldIterAngle = iterAngle
344                 for index, child in enumerate(self._children):
345                         iterAngle += child.weight * baseAngle
346                         if oldIterAngle < iterAngle and angle <= iterAngle:
347                                 return index
348                         elif oldIterAngle < (iterAngle + 2*math.pi) and angle <= (iterAngle + 2*math.pi):
349                                 return index
350                         oldIterAngle = iterAngle
351
352         def _radius_at(self, pos):
353                 xDelta = pos.x() - self.rect().center().x()
354                 yDelta = pos.y() - self.rect().center().y()
355
356                 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
357                 return radius
358
359         def _angle_at(self, pos):
360                 xDelta = pos.x() - self.rect().center().x()
361                 yDelta = pos.y() - self.rect().center().y()
362
363                 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
364                 angle = math.acos(xDelta / radius)
365                 if 0 <= yDelta:
366                         angle = 2*math.pi - angle
367
368                 return angle
369
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]:
376                         self._motion = 0
377                         self._activate_at(self._selectionIndex)
378                 elif keyEvent.key in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
379                         pass
380
381         def _on_mouse_press(self, mouseEvent):
382                 self._mouseButtonPressed = True
383
384
385 if __name__ == "__main__":
386         app = QtGui.QApplication([])
387
388         if False:
389                 pie = QPieMenu()
390                 pie.show()
391
392         if False:
393                 singleAction = QtGui.QAction(None)
394                 singleAction.setText("Boo")
395                 singleItem = QActionPieItem(singleAction)
396                 spie = QPieMenu()
397                 spie.insertItem(singleItem)
398                 spie.show()
399
400         if True:
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)
407                 mpie = QPieMenu()
408                 mpie.insertItem(oneItem)
409                 mpie.insertItem(twoItem)
410                 mpie.insertItem(oneItem)
411                 mpie.insertItem(twoItem)
412                 mpie.show()
413
414         app.exec_()