5da43a882e4ce6200d16cd52e4fd0da8dc8ff25f
[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._mouseButtonPressed = False
57                 self._mousePosition = ()
58
59                 canvasSize = self._outerRadius * 2 + 1
60                 self._canvas = QtGui.QPixmap(canvasSize, canvasSize)
61                 self._mask = None
62
63         def popup(self, pos):
64                 index = self.indexAt(self.mapFromGlobal(QtGui.QCursor.pos()))
65                 self._mousePosition = pos
66                 self.show()
67
68         def insertItem(self, item, index = -1):
69                 self._children.insert(index, item)
70                 self.update()
71
72         def removeItemAt(self, index):
73                 item = self._children.pop(index)
74                 self.update()
75
76         def set_center(self, item):
77                 self._center = item
78
79         def clear(self):
80                 del self._children[:]
81                 self.update()
82
83         def itemAt(self, index):
84                 return self._children[index]
85
86         def indexAt(self, point):
87                 return self._angle_to_index(self._angle_at(point))
88
89         def innerRadius(self):
90                 return self._innerRadius
91
92         def setInnerRadius(self, radius):
93                 self._innerRadius = radius
94
95         def outerRadius(self):
96                 return self._outerRadius
97
98         def setOuterRadius(self, radius):
99                 self._outerRadius = radius
100                 self._canvas = self._canvas.scaled(self.sizeHint())
101
102         def sizeHint(self):
103                 diameter = self._outerRadius * 2 + 1
104                 return QtCore.QSize(diameter, diameter)
105
106         def mousePressEvent(self, mouseEvent):
107                 lastSelection = self._selectionIndex
108
109                 lastMousePos = mouseEvent.pos()
110                 self._update_selection(lastMousePos)
111                 self._mouseButtonPressed = True
112                 self._mousePosition = lastMousePos
113
114                 if lastSelection != self._selectionIndex:
115                         self.highlighted.emit(self._selectionIndex)
116                         self.update()
117
118         def mouseMoveEvent(self, mouseEvent):
119                 lastSelection = self._selectionIndex
120
121                 lastMousePos = mouseEvent.pos()
122                 self._update_selection(lastMousePos)
123
124                 if lastSelection != self._selectionIndex:
125                         self.highlighted.emit(self._selectionIndex)
126                         self.update()
127
128         def mouseReleaseEvent(self, mouseEvent):
129                 lastSelection = self._selectionIndex
130
131                 lastMousePos = mouseEvent.pos()
132                 self._update_selection(lastMousePos)
133                 self._mouseButtonPressed = False
134                 self._mousePosition = ()
135
136                 self._activate_at(self._selectionIndex)
137                 self.update()
138
139         def showEvent(self, showEvent):
140                 self.aboutToShow.emit()
141
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)
148
149                 lastMousePos = self.mapFromGlobal(QtGui.QCursor.pos())
150                 self._update_selection(lastMousePos)
151
152                 QtGui.QWidget.showEvent(self, showEvent)
153
154         def hideEvent(self, hideEvent):
155                 self.canceled.emit()
156                 self._selectionIndex = self.SELECTION_NONE
157                 QtGui.QWidget.hideEvent(self, hideEvent)
158
159         def paintEvent(self, paintEvent):
160                 painter = QtGui.QPainter(self._canvas)
161                 painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
162
163                 adjustmentRect = self._canvas.rect().adjusted(0, 0, -1, -1)
164
165                 numChildren = len(self._children)
166                 if numChildren < 2:
167                         if self._selectionIndex == 0 and self._children[0].isEnabled():
168                                 painter.setBrush(self.palette().highlight())
169                         else:
170                                 painter.setBrush(self.palette().background())
171
172                         painter.fillRect(self.rect(), painter.brush())
173                 else:
174                         for i in xrange(len(self._children)):
175                                 self._paint_slice_background(painter, adjustmentRect, i)
176
177                 self._paint_center_background(painter, adjustmentRect)
178                 self._paint_center_foreground(painter)
179
180                 for i in xrange(len(self._children)):
181                         self._paint_slice_foreground(painter, i)
182
183                 screen = QtGui.QPainter(self)
184                 screen.drawPixmap(QtCore.QPoint(0, 0), self._canvas)
185
186                 QtGui.QWidget.paintEvent(self, paintEvent)
187
188         def __len__(self):
189                 return len(self._children)
190
191         def _generate_mask(self, mask):
192                 """
193                 Specifies on the mask the shape of the pie menu
194                 """
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))
199
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())
203                 else:
204                         painter.setBrush(self.palette().background())
205                 painter.setPen(self.palette().mid().color())
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                 size = b - a
212                 if size < 0:
213                         size += 2*math.pi
214
215                 startAngleInDeg = (a * 360 * 16) / (2*math.pi)
216                 sizeInDeg = (size * 360 * 16) / (2*math.pi)
217                 painter.drawPie(adjustmentRect, int(startAngleInDeg), int(sizeInDeg))
218
219         def _paint_slice_foreground(self, painter, i):
220                 child = self._children[i]
221
222                 a = self._index_to_angle(i, True)
223                 b = self._index_to_angle(i + 1, True)
224                 if b < a:
225                         b += 2*math.pi
226                 middleAngle = (a + b) / 2
227                 averageRadius = (self._innerRadius + self._outerRadius) / 2
228
229                 sliceX = averageRadius * math.cos(middleAngle)
230                 sliceY = - averageRadius * math.sin(middleAngle)
231
232                 pieX = self._canvas.rect().center().x()
233                 pieY = self._canvas.rect().center().y()
234                 self._paint_label(
235                         painter, child.action(), i == self._selectionIndex, pieX+sliceX, pieY+sliceY
236                 )
237
238         def _paint_label(self, painter, action, isSelected, x, y):
239                 text = action.text()
240                 fontMetrics = painter.fontMetrics()
241                 if text:
242                         textBoundingRect = fontMetrics.boundingRect(text)
243                 else:
244                         textBoundingRect = QtCore.QRect()
245                 textWidth = textBoundingRect.width()
246                 textHeight = textBoundingRect.height()
247
248                 icon = action.icon().pixmap(
249                         QtCore.QSize(self.ICON_SIZE_DEFAULT, self.ICON_SIZE_DEFAULT),
250                         QtGui.QIcon.Normal,
251                         QtGui.QIcon.On,
252                 )
253                 averageWidth = (icon.width() + textWidth)/2
254                 if not icon.isNull():
255                         iconRect = QtCore.QRect(
256                                 x - averageWidth,
257                                 y - icon.height()/2,
258                                 icon.width(),
259                                 icon.height(),
260                         )
261
262                         painter.drawPixmap(iconRect, icon)
263
264                 if text:
265                         if isSelected:
266                                 if action.isEnabled():
267                                         pen = self.palette().highlightedText()
268                                         brush = self.palette().highlight()
269                                 else:
270                                         pen = self.palette().mid()
271                                         brush = self.palette().background()
272                         else:
273                                 if action.isEnabled():
274                                         pen = self.palette().text()
275                                 else:
276                                         pen = self.palette().mid()
277                                 brush = self.palette().background()
278
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)
284
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()
290                 else:
291                         background = self.palette().background().color()
292
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,
298                 )
299
300                 painter.setPen(QtCore.Qt.NoPen)
301                 painter.setBrush(background)
302                 painter.drawPie(innerRect, 0, 360 * 16)
303
304                 painter.setPen(QtGui.QPen(dark, 1))
305                 painter.setBrush(QtCore.Qt.NoBrush)
306                 painter.drawEllipse(innerRect)
307
308                 painter.setPen(QtGui.QPen(dark, 1))
309                 painter.setBrush(QtCore.Qt.NoBrush)
310                 painter.drawEllipse(adjustmentRect)
311
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)
317
318         def _paint_center_foreground(self, painter):
319                 pieX = self._canvas.rect().center().x()
320                 pieY = self._canvas.rect().center().y()
321
322                 x = pieX
323                 y = pieY
324
325                 self._paint_label(
326                         painter, self._center.action(), self._selectionIndex == self.SELECTION_CENTER, x, y
327                 )
328
329         def _select_at(self, index):
330                 self._selectionIndex = index
331
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
338
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)))
345                 else:
346                         self._selectionIndex = self.SELECTION_NONE
347
348         def _activate_at(self, index):
349                 if index == self.SELECTION_NONE:
350                         print "Nothing selected"
351                         return
352                 elif index == self.SELECTION_CENTER:
353                         child = self._center
354                 else:
355                         child = self.itemAt(index)
356                 if child.action().isEnabled():
357                         child.action().trigger()
358                 self.activated.emit(index)
359                 self.aboutToHide.emit()
360                 self.hide()
361
362         def _index_to_angle(self, index, isShifted):
363                 index = index % len(self._children)
364
365                 totalWeight = sum(child.weight() for child in self._children)
366                 if totalWeight == 0:
367                         totalWeight = 1
368                 baseAngle = (2 * math.pi) / totalWeight
369
370                 angle = math.pi / 2
371                 if isShifted:
372                         if self._children:
373                                 angle -= (self._children[0].weight() * baseAngle) / 2
374                         else:
375                                 angle -= baseAngle / 2
376                 while angle < 0:
377                         angle += 2*math.pi
378
379                 for i, child in enumerate(self._children):
380                         if index < i:
381                                 break
382                         angle += child.weight() * baseAngle
383                 while (2*math.pi) < angle:
384                         angle -= 2*math.pi
385
386                 return angle
387
388         def _angle_to_index(self, angle):
389                 numChildren = len(self._children)
390                 if numChildren == 0:
391                         return self.SELECTION_CENTER
392
393                 totalWeight = sum(child.weight() for child in self._children)
394                 if totalWeight == 0:
395                         totalWeight = 1
396                 baseAngle = (2 * math.pi) / totalWeight
397
398                 iterAngle = math.pi / 2 - (self.itemAt(0).weight() * baseAngle) / 2
399                 while iterAngle < 0:
400                         iterAngle += 2 * math.pi
401
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
410
411         def _radius_at(self, pos):
412                 xDelta = pos.x() - self.rect().center().x()
413                 yDelta = pos.y() - self.rect().center().y()
414
415                 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
416                 return radius
417
418         def _angle_at(self, pos):
419                 xDelta = pos.x() - self.rect().center().x()
420                 yDelta = pos.y() - self.rect().center().y()
421
422                 radius = math.sqrt(xDelta ** 2 + yDelta ** 2)
423                 angle = math.acos(xDelta / radius)
424                 if 0 <= yDelta:
425                         angle = 2*math.pi - angle
426
427                 return angle
428
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)
432                         self.update()
433                 elif keyEvent.key in [QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Backtab]:
434                         self._select_at(self._selectionIndex - 1)
435                         self.update()
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)
440
441         def _on_mouse_press(self, mouseEvent):
442                 self._mouseButtonPressed = True
443
444
445 def _print(msg):
446         print msg
447
448
449 def _on_about_to_hide(app):
450         app.exit()
451
452
453 if __name__ == "__main__":
454         app = QtGui.QApplication([])
455         QPieMenu.NULL_CENTER.setEnabled(False)
456
457         if False:
458                 pie = QPieMenu()
459                 pie.show()
460
461         if False:
462                 singleAction = QtGui.QAction(None)
463                 singleAction.setText("Boo")
464                 singleItem = QActionPieItem(singleAction)
465                 spie = QPieMenu()
466                 spie.insertItem(singleItem)
467                 spie.show()
468
469         if False:
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)
480                 mpie = QPieMenu()
481                 mpie.insertItem(oneItem)
482                 mpie.insertItem(twoItem)
483                 mpie.insertItem(oneItem)
484                 mpie.insertItem(iconTextItem)
485                 mpie.show()
486
487         if True:
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)
505                 mpie = QPieMenu()
506                 mpie.set_center(iconItem)
507                 mpie.insertItem(oneItem)
508                 mpie.insertItem(twoItem)
509                 mpie.insertItem(oneItem)
510                 mpie.insertItem(iconTextItem)
511                 mpie.show()
512                 mpie.aboutToHide.connect(lambda: _on_about_to_hide(app))
513
514         app.exec_()