source: orange/Orange/OrangeCanvas/gui/stackedwidget.py @ 11365:370f24214156

Revision 11365:370f24214156, 9.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Changed ToolBox.removeItem and StackedWidget.removeWidget to not delete the widget.

This follows standard Qt4's semantics.

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        self.layout().removeWidget(widget)
168        self.__widgets.pop(index)
169
170    def widget(self, index):
171        """Return the widget at `index`
172        """
173        return self.__widgets[index]
174
175    def indexOf(self, widget):
176        """Return the index of `widget` in the stack.
177        """
178        return self.__widgets.index(widget)
179
180    def count(self):
181        """Return the number of widgets in the stack.
182        """
183        return max(self.layout().count() - 1, 0)
184
185    def setCurrentWidget(self, widget):
186        """Set the current shown widget.
187        """
188        index = self.__widgets.index(widget)
189        self.setCurrentIndex(index)
190
191    def setCurrentIndex(self, index):
192        """Set the current shown widget index.
193        """
194        index = max(min(index, self.count() - 1), 0)
195        if self.__currentIndex == -1:
196            self.layout().setCurrentIndex(index)
197            self.__currentIndex = index
198            return
199
200#        if not self.animationEnabled():
201#            self.layout().setCurrentIndex(index)
202#            self.__currentIndex = index
203#            return
204
205        # else start the animation
206        current = self.__widgets[self.__currentIndex]
207        next_widget = self.__widgets[index]
208
209        current_pix = QPixmap.grabWidget(current)
210        next_pix = QPixmap.grabWidget(next_widget)
211
212        with updates_disabled(self):
213            self.__fadeWidget.setPixmap(current_pix)
214            self.__fadeWidget.setPixmap2(next_pix)
215            self.__nextCurrentIndex = index
216            self.__transitionStart()
217
218    def currentIndex(self):
219        return self.__currentIndex
220
221    def sizeHint(self):
222        hint = QFrame.sizeHint(self)
223        if hint.isEmpty():
224            hint = QSize(0, 0)
225        return hint
226
227    def __transitionStart(self):
228        """Start the transition.
229        """
230        log.debug("Stack transition start (%s)", str(self.objectName()))
231        # Set the fade widget as the current widget
232        self.__fadeWidget.blendingFactor_ = 0.0
233        self.layout().setCurrentWidget(self.__fadeWidget)
234        self.transitionAnimation.start()
235        self.transitionStarted.emit()
236
237    def __onTransitionFinished(self):
238        """Transition has finished.
239        """
240        log.debug("Stack transition finished (%s)" % str(self.objectName()))
241        self.__fadeWidget.blendingFactor_ = 1.0
242        self.__currentIndex = self.__nextCurrentIndex
243        with updates_disabled(self):
244            self.layout().setCurrentIndex(self.__currentIndex)
245        self.transitionFinished.emit()
246
247    def __onLayoutCurrentChanged(self, index):
248        # Suppress transitional __fadeWidget current widget
249        if index != self.count():
250            self.currentChanged.emit(index)
251
252
253class CrossFadePixmapWidget(QWidget):
254    """A widget for cross fading between two pixmaps.
255    """
256    def __init__(self, parent=None, pixmap1=None, pixmap2=None):
257        QWidget.__init__(self, parent)
258        self.setPixmap(pixmap1)
259        self.setPixmap2(pixmap2)
260        self.blendingFactor_ = 0.0
261        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
262
263    def setPixmap(self, pixmap):
264        """Set pixmap 1
265        """
266        self.pixmap1 = pixmap
267        self.updateGeometry()
268
269    def setPixmap2(self, pixmap):
270        """Set pixmap 2
271        """
272        self.pixmap2 = pixmap
273        self.updateGeometry()
274
275    def setBlendingFactor(self, factor):
276        """Set the blending factor between the two pixmaps.
277        """
278        self.__blendingFactor = factor
279        self.updateGeometry()
280
281    def blendingFactor(self):
282        """Pixmap blending factor between 0.0 and 1.0
283        """
284        return self.__blendingFactor
285
286    blendingFactor_ = Property(float, fget=blendingFactor,
287                               fset=setBlendingFactor)
288
289    def sizeHint(self):
290        """Return an interpolated size between pixmap1.size()
291        and pixmap2.size()
292
293        """
294        if self.pixmap1 and self.pixmap2:
295            size1 = self.pixmap1.size()
296            size2 = self.pixmap2.size()
297            return size1 + self.blendingFactor_ * (size2 - size1)
298        else:
299            return QWidget.sizeHint(self)
300
301    def paintEvent(self, event):
302        """Paint the interpolated pixmap image.
303        """
304        p = QPainter(self)
305        p.setClipRect(event.rect())
306        factor = self.blendingFactor_ ** 2
307        if self.pixmap1 and 1. - factor:
308            p.setOpacity(1. - factor)
309            p.drawPixmap(QPoint(0, 0), self.pixmap1)
310        if self.pixmap2 and factor:
311            p.setOpacity(factor)
312            p.drawPixmap(QPoint(0, 0), self.pixmap2)
Note: See TracBrowser for help on using the repository browser.