source: orange/Orange/OrangeCanvas/gui/stackedwidget.py @ 11366:7f9332b11252

Revision 11366:7f9332b11252, 10.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Added rst documentation for the canvas gui package.

Fixing docstrings in the process.

Line 
1"""
2=====================
3AnimatedStackedWidget
4=====================
5
6A widget similar to :class:`QStackedWidget` supporting animated
7transitions between widgets.
8
9"""
10
11import logging
12
13from PyQt4.QtGui import QWidget, QFrame, QStackedLayout, QPixmap, \
14                        QPainter, QSizePolicy
15
16from PyQt4.QtCore import Qt, QPoint, QRect, QSize, QPropertyAnimation
17
18from PyQt4.QtCore import pyqtSignal as Signal
19from PyQt4.QtCore import pyqtProperty as Property
20
21from .utils import updates_disabled
22
23log = logging.getLogger(__name__)
24
25
26def clipMinMax(size, minSize, maxSize):
27    """
28    Clip the size so it is bigger then minSize but smaller than maxSize.
29    """
30    return size.expandedTo(minSize).boundedTo(maxSize)
31
32
33def fixSizePolicy(size, hint, policy):
34    """
35    Fix size so it conforms to the size policy and the given size hint.
36    """
37    width, height = hint.width(), hint.height()
38    expanding = policy.expandingDirections()
39    hpolicy, vpolicy = policy.horizontalPolicy(), policy.verticalPolicy()
40
41    if expanding & Qt.Horizontal:
42        width = max(width, size.width())
43
44    if hpolicy == QSizePolicy.Maximum:
45        width = min(width, size.width())
46
47    if expanding & Qt.Vertical:
48        height = max(height, size.height())
49
50    if vpolicy == QSizePolicy.Maximum:
51        height = min(height, hint.height())
52
53    return QSize(width, height).boundedTo(size)
54
55
56class StackLayout(QStackedLayout):
57    """
58    A stacked layout with ``sizeHint`` always the same as that of the
59    `current` widget.
60
61    """
62    def __init__(self, parent=None):
63        QStackedLayout.__init__(self, parent)
64        self.currentChanged.connect(self._onCurrentChanged)
65
66    def sizeHint(self):
67        current = self.currentWidget()
68        if current:
69            hint = current.sizeHint()
70            # Clip the hint with min/max sizes.
71            hint = clipMinMax(hint, current.minimumSize(),
72                              current.maximumSize())
73            return hint
74        else:
75            return QStackedLayout.sizeHint(self)
76
77    def minimumSize(self):
78        current = self.currentWidget()
79        if current:
80            return current.minimumSize()
81        else:
82            return QStackedLayout.minimumSize(self)
83
84    def maximumSize(self):
85        current = self.currentWidget()
86        if current:
87            return current.maximumSize()
88        else:
89            return QStackedLayout.maximumSize(self)
90
91    def setGeometry(self, rect):
92        QStackedLayout.setGeometry(self, rect)
93        for i in range(self.count()):
94            w = self.widget(i)
95            hint = w.sizeHint()
96            geom = QRect(rect)
97            size = clipMinMax(rect.size(), w.minimumSize(), w.maximumSize())
98            size = fixSizePolicy(size, hint, w.sizePolicy())
99            geom.setSize(size)
100            if geom != w.geometry():
101                w.setGeometry(geom)
102
103    def _onCurrentChanged(self, index):
104        """
105        Current widget changed, invalidate the layout.
106        """
107        self.invalidate()
108
109
110class AnimatedStackedWidget(QFrame):
111    # Current widget has changed
112    currentChanged = Signal(int)
113
114    # Transition animation has started
115    transitionStarted = Signal()
116
117    # Transition animation has finished
118    transitionFinished = Signal()
119
120    def __init__(self, parent=None, animationEnabled=True):
121        QFrame.__init__(self, parent)
122        self.__animationEnabled = animationEnabled
123
124        layout = StackLayout()
125
126        self.__fadeWidget = CrossFadePixmapWidget(self)
127
128        self.transitionAnimation = \
129            QPropertyAnimation(self.__fadeWidget, "blendingFactor_", self)
130        self.transitionAnimation.setStartValue(0.0)
131        self.transitionAnimation.setEndValue(1.0)
132        self.transitionAnimation.setDuration(100 if animationEnabled else 0)
133        self.transitionAnimation.finished.connect(
134            self.__onTransitionFinished
135        )
136
137        layout.addWidget(self.__fadeWidget)
138        layout.currentChanged.connect(self.__onLayoutCurrentChanged)
139
140        self.setLayout(layout)
141
142        self.__widgets = []
143        self.__currentIndex = -1
144        self.__nextCurrentIndex = -1
145
146    def setAnimationEnabled(self, animationEnabled):
147        """
148        Enable/disable transition animations.
149        """
150        if self.__animationEnabled != animationEnabled:
151            self.__animationEnabled = animationEnabled
152            self.transitionAnimation.setDuration(
153                100 if animationEnabled else 0
154            )
155
156    def animationEnabled(self):
157        """
158        Is the transition animation enabled.
159        """
160        return self.__animationEnabled
161
162    def addWidget(self, widget):
163        """
164        Append the widget to the stack and return its index.
165        """
166        return self.insertWidget(self.layout().count(), widget)
167
168    def insertWidget(self, index, widget):
169        """
170        Insert `widget` into the stack at `index`.
171        """
172        index = min(index, self.count())
173        self.__widgets.insert(index, widget)
174        if index <= self.__currentIndex or self.__currentIndex == -1:
175            self.__currentIndex += 1
176        return self.layout().insertWidget(index, widget)
177
178    def removeWidget(self, widget):
179        """
180        Remove `widget` from the stack.
181
182        .. note:: The widget is hidden but is not deleted.
183
184        """
185        index = self.__widgets.index(widget)
186        self.layout().removeWidget(widget)
187        self.__widgets.pop(index)
188
189    def widget(self, index):
190        """
191        Return the widget at `index`
192        """
193        return self.__widgets[index]
194
195    def indexOf(self, widget):
196        """
197        Return the index of `widget` in the stack.
198        """
199        return self.__widgets.index(widget)
200
201    def count(self):
202        """
203        Return the number of widgets in the stack.
204        """
205        return max(self.layout().count() - 1, 0)
206
207    def setCurrentWidget(self, widget):
208        """
209        Set the current shown widget.
210        """
211        index = self.__widgets.index(widget)
212        self.setCurrentIndex(index)
213
214    def setCurrentIndex(self, index):
215        """
216        Set the current shown widget index.
217        """
218        index = max(min(index, self.count() - 1), 0)
219        if self.__currentIndex == -1:
220            self.layout().setCurrentIndex(index)
221            self.__currentIndex = index
222            return
223
224#        if not self.animationEnabled():
225#            self.layout().setCurrentIndex(index)
226#            self.__currentIndex = index
227#            return
228
229        # else start the animation
230        current = self.__widgets[self.__currentIndex]
231        next_widget = self.__widgets[index]
232
233        current_pix = QPixmap.grabWidget(current)
234        next_pix = QPixmap.grabWidget(next_widget)
235
236        with updates_disabled(self):
237            self.__fadeWidget.setPixmap(current_pix)
238            self.__fadeWidget.setPixmap2(next_pix)
239            self.__nextCurrentIndex = index
240            self.__transitionStart()
241
242    def currentIndex(self):
243        """
244        Return the current shown widget index.
245        """
246        return self.__currentIndex
247
248    def sizeHint(self):
249        hint = QFrame.sizeHint(self)
250        if hint.isEmpty():
251            hint = QSize(0, 0)
252        return hint
253
254    def __transitionStart(self):
255        """
256        Start the transition.
257        """
258        log.debug("Stack transition start (%s)", str(self.objectName()))
259        # Set the fade widget as the current widget
260        self.__fadeWidget.blendingFactor_ = 0.0
261        self.layout().setCurrentWidget(self.__fadeWidget)
262        self.transitionAnimation.start()
263        self.transitionStarted.emit()
264
265    def __onTransitionFinished(self):
266        """
267        Transition has finished.
268        """
269        log.debug("Stack transition finished (%s)" % str(self.objectName()))
270        self.__fadeWidget.blendingFactor_ = 1.0
271        self.__currentIndex = self.__nextCurrentIndex
272        with updates_disabled(self):
273            self.layout().setCurrentIndex(self.__currentIndex)
274        self.transitionFinished.emit()
275
276    def __onLayoutCurrentChanged(self, index):
277        # Suppress transitional __fadeWidget current widget
278        if index != self.count():
279            self.currentChanged.emit(index)
280
281
282class CrossFadePixmapWidget(QWidget):
283    """
284    A widget for cross fading between two pixmaps.
285    """
286    def __init__(self, parent=None, pixmap1=None, pixmap2=None):
287        QWidget.__init__(self, parent)
288        self.setPixmap(pixmap1)
289        self.setPixmap2(pixmap2)
290        self.blendingFactor_ = 0.0
291        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
292
293    def setPixmap(self, pixmap):
294        """
295        Set pixmap 1
296        """
297        self.pixmap1 = pixmap
298        self.updateGeometry()
299
300    def setPixmap2(self, pixmap):
301        """
302        Set pixmap 2
303        """
304        self.pixmap2 = pixmap
305        self.updateGeometry()
306
307    def setBlendingFactor(self, factor):
308        """
309        Set the blending factor between the two pixmaps.
310        """
311        self.__blendingFactor = factor
312        self.updateGeometry()
313
314    def blendingFactor(self):
315        """
316        Pixmap blending factor between 0.0 and 1.0
317        """
318        return self.__blendingFactor
319
320    blendingFactor_ = Property(float, fget=blendingFactor,
321                               fset=setBlendingFactor)
322
323    def sizeHint(self):
324        """
325        Return an interpolated size between pixmap1.size()
326        and pixmap2.size()
327
328        """
329        if self.pixmap1 and self.pixmap2:
330            size1 = self.pixmap1.size()
331            size2 = self.pixmap2.size()
332            return size1 + self.blendingFactor_ * (size2 - size1)
333        else:
334            return QWidget.sizeHint(self)
335
336    def paintEvent(self, event):
337        """
338        Paint the interpolated pixmap image.
339        """
340        p = QPainter(self)
341        p.setClipRect(event.rect())
342        factor = self.blendingFactor_ ** 2
343        if self.pixmap1 and 1. - factor:
344            p.setOpacity(1. - factor)
345            p.drawPixmap(QPoint(0, 0), self.pixmap1)
346        if self.pixmap2 and factor:
347            p.setOpacity(factor)
348            p.drawPixmap(QPoint(0, 0), self.pixmap2)
Note: See TracBrowser for help on using the repository browser.