8ab0a2803c0f7f637ff91101877d23c4702fb792
[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         SELECTION_CENTER = -1
44         SELECTION_NONE = -2
45
46         NULL_CENTER = QtGui.QAction(None)
47
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
52                 self._children = []
53                 self._center = self.NULL_CENTER
54                 self._selectionIndex = self.SELECTION_NONE
55
56                 self._motion = 0
57                 self._mouseButtonPressed = True
58                 self._mousePosition = ()
59
60                 canvasSize = self._outerRadius * 2 + 1
61                 self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
62                 self._mask = None
63
64         def popup(self, pos):
65                 index = self.indexAt(self.mapFromGlobal(QtGui.QCursor.pos()))
66                 self._mousePosition = pos
67                 self.show()
68
69         def insertItem(self, item, index = -1):
70                 self._children.insert(index, item)
71                 self._invalidate_view()
72
73         def removeItemAt(self, index):
74                 item = self._children.pop(index)
75                 self._invalidate_view()
76
77         def set_center(self, item):
78                 self._center = item
79
80         def clear(self):
81                 del self._children[:]
82                 self._invalidate_view()
83
84         def itemAt(self, index):
85                 return self._children[index]
86
87         def indexAt(self, point):
88                 return self._angle_to_index(self._angle_at(point))
89
90         def setHighlightedItem(self, index):
91                 pass
92
93         def highlightedItem(self):
94                 pass
95
96         def innerRadius(self):
97                 return self._innerRadius
98
99         def setInnerRadius(self, radius):
100                 self._innerRadius = radius
101
102         def outerRadius(self):
103                 return self._outerRadius
104
105         def setOuterRadius(self, radius):
106                 self._outerRadius = radius
107                 self._canvas = self._canvas.scaled(self.sizeHint())
108
109         def sizeHint(self):
110                 diameter = self._outerRadius * 2 + 1
111                 return QtCore.QSize(diameter, diameter)
112
113         def showEvent(self, showEvent):
114                 self.aboutToShow.emit()
115
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)
122
123                 self._motion = 0
124
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))
129                 else:
130                         if radius < self._innerRadius:
131                                 self._selectionIndex = self.SELECTION_CENTER
132                         else:
133                                 self._selectionIndex = self.SELECTION_NONE
134
135                 QtGui.QWidget.showEvent(self, showEvent)
136
137         def hideEvent(self, hideEvent):
138                 self.canceled.emit()
139                 self._selectionIndex = self.SELECTION_NONE
140                 QtGui.QWidget.hideEvent(self, hideEvent)
141
142         def paintEvent(self, paintEvent):
143                 painter = QtGui.QPainter(self._canvas)
144                 painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
145
146                 adjustmentRect = self._canvas.rect().adjusted(0, 0, -1, -1)
147
148                 numChildren = len(self._children)
149                 if numChildren < 2:
150                         if self._selectionIndex == 0 and self._children[0].isEnabled():
151                                 painter.setBrush(self.palette().highlight())
152                         else:
153                                 painter.setBrush(self.palette().background())
154
155                         painter.fillRect(self.rect(), painter.brush())
156                 else:
157                         for i in xrange(len(self._children)):
158                                 self._paint_slice_background(painter, adjustmentRect, i)
159
160                 self._paint_center_background(painter, adjustmentRect)
161                 self._paint_center_foreground(painter)
162
163                 for i in xrange(len(self._children)):
164                         self._paint_slice_foreground(painter, i)
165
166                 screen = QtGui.QPainter(self)
167                 screen.drawPixmap(QtCore.QPoint(0, 0), self._canvas)
168
169                 QtGui.QWidget.paintEvent(self, paintEvent)
170
171         def __len__(self):
172                 return len(self._children)
173
174         def _invalidate_view(self):
175                 pass
176
177         def _generate_mask(self, mask):
178                 """
179                 Specifies on the mask the shape of the pie menu
180                 """
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))
185
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())
189                 else:
190                         painter.setBrush(self.palette().background())
191                 painter.setPen(self.palette().mid().color())
192
193                 a = self._index_to_angle(i, True)
194                 b = self._index_to_angle(i + 1, True)
195                 if b < a:
196                         b += 2*math.pi
197                 size = b - a
198                 if size < 0:
199                         size += 2*math.pi
200
201                 startAngleInDeg = (a * 360 * 16) / (2*math.pi)
202                 sizeInDeg = (size * 360 * 16) / (2*math.pi)
203                 painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
204
205         def _paint_slice_foreground(self, painter, i):
206                 child = self._children[i]
207
208                 a = self._index_to_angle(i, True)
209                 b = self._index_to_angle(i + 1, True)
210                 if b < a:
211                         b += 2*math.pi
212                 middleAngle = (a + b) / 2
213                 averageRadius = (self._innerRadius + self._outerRadius) / 2
214
215                 sliceX = averageRadius * math.cos(middleAngle)
216                 sliceY = - averageRadius * math.sin(middleAngle)
217
218                 pieX = self._canvas.rect().center().x()
219                 pieY = self._canvas.rect().center().y()
220                 self._paint_label(
221                         painter, child.action(), i == self._selectionIndex, pieX+sliceX, pieY+sliceY
222                 )
223
224         def _paint_label(self, painter, action, isSelected, x, y):
225                 text = action.text()
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 = 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                                 x - averageWidth,
243                                 y - icon.height()/2,
244                                 icon.width(),
245                                 icon.height(),
246                         )
247
248                         painter.drawPixmap(iconRect, icon)
249
250                 if text:
251                         if isSelected:
252                                 if 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 action.isEnabled():
260                                         pen = self.palette().text()
261                                 else:
262                                         pen = self.palette().mid()
263                                 brush = self.palette().background()
264
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)
270
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()
276                 else:
277                         background = self.palette().background().color()
278
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,
284                 )
285
286                 painter.setPen(QtCore.Qt.NoPen)
287                 painter.setBrush(background)
288                 painter.drawPie(innerRect, 0, 360 * 16)
289
290                 painter.setPen(QtGui.QPen(dark, 1))
291                 painter.setBrush(QtCore.Qt.NoBrush)
292                 painter.drawEllipse(innerRect)
293
294                 painter.setPen(QtGui.QPen(dark, 1))
295                 painter.setBrush(QtCore.Qt.NoBrush)
296                 painter.drawEllipse(adjustmentRect)
297
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)
303
304         def _paint_center_foreground(self, painter):
305                 pieX = self._canvas.rect().center().x()
306                 pieY = self._canvas.rect().center().y()
307
308                 x = pieX
309                 y = pieY
310
311                 self._paint_label(
312                         painter, self._center.action(), self._selectionIndex == self.SELECTION_CENTER, x, y
313                 )
314
315         def _select_at(self, index):
316                 self._selectionIndex = index
317
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
324
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()
331                 self.hide()
332
333         def _index_to_angle(self, index, isShifted):
334                 index = index % len(self._children)
335
336                 totalWeight = sum(child.weight() for child in self._children)
337                 if totalWeight == 0:
338                         totalWeight = 1
339                 baseAngle = (2 * math.pi) / totalWeight
340
341                 angle = math.pi / 2
342                 if isShifted:
343                         if self._children:
344                                 angle -= (self._children[0].weight() * baseAngle) / 2
345                         else:
346                                 angle -= baseAngle / 2
347                 while angle < 0:
348                         angle += 2*math.pi
349
350                 for i, child in enumerate(self._children):
351                         if index < i:
352                                 break
353                         angle += child.weight() * baseAngle
354                 while (2*math.pi) < angle:
355                         angle -= 2*math.pi
356
357                 return angle
358
359         def _angle_to_index(self, angle):
360                 numChildren = len(self._children)
361                 if numChildren == 0:
362                         return self.SELECTION_CENTER
363
364                 totalWeight = sum(child.weight() for child in self._children)
365                 if totalWeight == 0:
366                         totalWeight = 1
367                 baseAngle = (2 * math.pi) / totalWeight
368
369                 iterAngle = math.pi / 2 - (self.itemAt(0).weight * baseAngle) / 2
370                 while iterAngle < 0:
371                         iterAngle += 2 * math.pi
372
373                 oldIterAngle = iterAngle
374                 for index, child in enumerate(self._children):
375                         iterAngle += child.weight * baseAngle
376                         if oldIterAngle < iterAngle and angle <= iterAngle:
377                                 return index
378                         elif oldIterAngle < (iterAngle + 2*math.pi) and angle <= (iterAngle + 2*math.pi):
379                                 return index
380                         oldIterAngle = iterAngle
381
382         def _radius_at(self, pos):
383                 xDelta = pos.x() - self.rect().center().x()
384                 yDelta = pos.y() - self.rect().center().y()
385
386                 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
387                 return radius
388
389         def _angle_at(self, pos):
390                 xDelta = pos.x() - self.rect().center().x()
391                 yDelta = pos.y() - self.rect().center().y()
392
393                 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
394                 angle = math.acos(xDelta / radius)
395                 if 0 <= yDelta:
396                         angle = 2*math.pi - angle
397
398                 return angle
399
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]:
406                         self._motion = 0
407                         self._activate_at(self._selectionIndex)
408                 elif keyEvent.key in [QtCore.Qt.Key_Escape, QtCore.Qt.Key_Backspace]:
409                         pass
410
411         def _on_mouse_press(self, mouseEvent):
412                 self._mouseButtonPressed = True
413
414
415 if __name__ == "__main__":
416         app = QtGui.QApplication([])
417         QPieMenu.NULL_CENTER.setEnabled(False)
418
419         if False:
420                 pie = QPieMenu()
421                 pie.show()
422
423         if False:
424                 singleAction = QtGui.QAction(None)
425                 singleAction.setText("Boo")
426                 singleItem = QActionPieItem(singleAction)
427                 spie = QPieMenu()
428                 spie.insertItem(singleItem)
429                 spie.show()
430
431         if False:
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)
442                 mpie = QPieMenu()
443                 mpie.insertItem(oneItem)
444                 mpie.insertItem(twoItem)
445                 mpie.insertItem(oneItem)
446                 mpie.insertItem(iconTextItem)
447                 mpie.show()
448
449         if True:
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)
463                 mpie = QPieMenu()
464                 mpie.set_center(iconItem)
465                 mpie.insertItem(oneItem)
466                 mpie.insertItem(twoItem)
467                 mpie.insertItem(oneItem)
468                 mpie.insertItem(iconTextItem)
469                 mpie.show()
470
471         app.exec_()