Can now draw a basic pie
[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                 light.setAlpha(128)
186                 painter.setPen(QtGui.QPen(light, 1))
187                 painter.setBrush(QtCore.Qt.NoBrush)
188                 painter.drawArc(innerRect, 225 * 16, 180 * 16)
189
190                 painter.setPen(QtGui.QPen(dark, 1))
191                 painter.drawArc(innerRect, 45 * 16, 180 * 16)
192
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)
198
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)
204
205                 if self._selectionIndex == -1:
206                         text = self.palette().highlightedText().color()
207                 else:
208                         text = self.palette().text().color()
209
210                 for i, child in enumerate(self._children):
211                         text = child.action().text()
212
213                         a = self._index_to_angle(i, True)
214                         b = self._index_to_angle(i + 1, True)
215                         if b < a:
216                                 b += 2*math.pi
217                         middleAngle = (a + b) / 2
218                         averageRadius = (self._innerRadius + self._outerRadius) / 2
219
220                         sliceX = averageRadius * math.cos(middleAngle)
221                         sliceY = - averageRadius * math.sin(middleAngle)
222
223                         pieX = self._canvas.rect().center().x()
224                         pieY = self._canvas.rect().center().y()
225
226                         fontMetrics = painter.fontMetrics()
227                         if text:
228                                 textBoundingRect = fontMetrics.boundingRect(text)
229                         else:
230                                 textBoundingRect = QtCore.QRect()
231                         textWidth = textBoundingRect.width()
232                         textHeight = textBoundingRect.height()
233
234                         icon = child.action().icon().pixmap(
235                                 QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
236                                 QtGui.QIcon.Normal,
237                                 QtGui.QIcon.On,
238                         )
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,
244                                         icon.width(),
245                                         icon.height(),
246                                 )
247
248                                 painter.drawPixmap(iconRect, icon)
249
250                         if text:
251                                 if i == self._selectionIndex:
252                                         if child.action().isEnabled():
253                                                 pen = self.palette().highlightedText()
254                                                 brush = self.palette().highlight()
255                                         else:
256                                                 pen = self.palette().mid()
257                                                 brush = self.palette().background()
258                                 else:
259                                         if child.action().isEnabled():
260                                                 pen = self.palette().text()
261                                         else:
262                                                 pen = self.palette().mid()
263                                         brush = self.palette().background()
264
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)
270
271                 screen = QtGui.QPainter(self)
272                 screen.drawPixmap(QtCore.QPoint(0, 0), self._canvas)
273
274                 QtGui.QWidget.paintEvent(self, paintEvent)
275
276         def __len__(self):
277                 return len(self._children)
278
279         def _invalidate_view(self):
280                 pass
281
282         def _generate_mask(self, mask):
283                 """
284                 Specifies on the mask the shape of the pie menu
285                 """
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))
290
291         def _select_at(self, index):
292                 self._selectionIndex = index
293
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
300
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()
307                 self.hide()
308
309         def _index_to_angle(self, index, isShifted):
310                 index = index % len(self._children)
311
312                 totalWeight = sum(child.weight() for child in self._children)
313                 if totalWeight == 0:
314                         totalWeight = 1
315                 baseAngle = (2 * math.pi) / totalWeight
316
317                 angle = math.pi / 2
318                 if isShifted:
319                         if self._children:
320                                 angle -= (self._children[0].weight() * baseAngle) / 2
321                         else:
322                                 angle -= baseAngle / 2
323                 while angle < 0:
324                         angle += 2*math.pi
325
326                 for i, child in enumerate(self._children):
327                         if index < i:
328                                 break
329                         angle += child.weight() * baseAngle
330                 while (2*math.pi) < angle:
331                         angle -= 2*math.pi
332
333                 return angle
334
335         def _angle_to_index(self, angle):
336                 numChildren = len(self._children)
337                 if numChildren == 0:
338                         return -1
339
340                 totalWeight = sum(child.weight() for child in self._children)
341                 if totalWeight == 0:
342                         totalWeight = 1
343                 baseAngle = (2 * math.pi) / totalWeight
344
345                 iterAngle = math.pi / 2 - (self.itemAt(0).weight * baseAngle) / 2
346                 while iterAngle < 0:
347                         iterAngle += 2 * math.pi
348
349                 oldIterAngle = iterAngle
350                 for index, child in enumerate(self._children):
351                         iterAngle += child.weight * baseAngle
352                         if oldIterAngle < iterAngle and angle <= iterAngle:
353                                 return index
354                         elif oldIterAngle < (iterAngle + 2*math.pi) and angle <= (iterAngle + 2*math.pi):
355                                 return index
356                         oldIterAngle = iterAngle
357
358         def _radius_at(self, pos):
359                 xDelta = pos.x() - self.rect().center().x()
360                 yDelta = pos.y() - self.rect().center().y()
361
362                 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
363                 return radius
364
365         def _angle_at(self, pos):
366                 xDelta = pos.x() - self.rect().center().x()
367                 yDelta = pos.y() - self.rect().center().y()
368
369                 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
370                 angle = math.acos(xDelta / radius)
371                 if 0 <= yDelta:
372                         angle = 2*math.pi - angle
373
374                 return angle
375
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]:
382                         self._motion = 0
383                         self._activate_at(self._selectionIndex)
384                 elif keyEvent.key in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
385                         pass
386
387         def _on_mouse_press(self, mouseEvent):
388                 self._mouseButtonPressed = True
389
390
391 if __name__ == "__main__":
392         app = QtGui.QApplication([])
393
394         if False:
395                 pie = QPieMenu()
396                 pie.show()
397
398         if False:
399                 singleAction = QtGui.QAction(None)
400                 singleAction.setText("Boo")
401                 singleItem = QActionPieItem(singleAction)
402                 spie = QPieMenu()
403                 spie.insertItem(singleItem)
404                 spie.show()
405
406         if True:
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)
413                 mpie = QPieMenu()
414                 mpie.insertItem(oneItem)
415                 mpie.insertItem(twoItem)
416                 mpie.insertItem(oneItem)
417                 mpie.insertItem(twoItem)
418                 mpie.show()
419
420         app.exec_()