source: orange/Orange/OrangeCanvas/gui/stackedwidget.py @ 11100:cf6f6744dd9b

Revision 11100:cf6f6744dd9b, 9.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 19 months ago (diff)

Added gui widget toolkit.

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