Source code for orangecanvas.gui.stackedwidget

"""
=====================
AnimatedStackedWidget
=====================

A widget similar to :class:`QStackedWidget` supporting animated
transitions between widgets.

"""

import logging

from AnyQt.QtWidgets import QWidget, QFrame, QStackedLayout, QSizePolicy
from AnyQt.QtGui import QPixmap, QPainter
from AnyQt.QtCore import Qt, QPoint, QRect, QSize, QPropertyAnimation
from AnyQt.QtCore import pyqtSignal as Signal, pyqtProperty as Property

from .utils import updates_disabled

log = logging.getLogger(__name__)


def clipMinMax(size, minSize, maxSize):
    """
    Clip the size so it is bigger then minSize but smaller than maxSize.
    """
    return size.expandedTo(minSize).boundedTo(maxSize)


def fixSizePolicy(size, hint, policy):
    """
    Fix size so it conforms to the size policy and the given size hint.
    """
    width, height = hint.width(), hint.height()
    expanding = policy.expandingDirections()
    hpolicy, vpolicy = policy.horizontalPolicy(), policy.verticalPolicy()

    if expanding & Qt.Horizontal:
        width = max(width, size.width())

    if hpolicy == QSizePolicy.Maximum:
        width = min(width, size.width())

    if expanding & Qt.Vertical:
        height = max(height, size.height())

    if vpolicy == QSizePolicy.Maximum:
        height = min(height, hint.height())

    return QSize(width, height).boundedTo(size)


[docs]class StackLayout(QStackedLayout): """ A stacked layout with ``sizeHint`` always the same as that of the `current` widget. """ def __init__(self, parent=None): self.__rect = QRect() super().__init__(parent) self.currentChanged.connect(self._onCurrentChanged)
[docs] def sizeHint(self): current = self.currentWidget() if current: hint = current.sizeHint() # Clip the hint with min/max sizes. hint = clipMinMax(hint, current.minimumSize(), current.maximumSize()) return hint else: return super().sizeHint()
[docs] def minimumSize(self): current = self.currentWidget() if current: return current.minimumSize() else: return super().minimumSize()
[docs] def maximumSize(self): current = self.currentWidget() if current: return current.maximumSize() else: return super().maximumSize()
[docs] def geometry(self): # Reimplemented due to QTBUG-47107. return QRect(self.__rect)
[docs] def setGeometry(self, rect): if not isinstance(rect, QRect): raise TypeError("QRect required") if rect == self.__rect: return self.__rect = QRect(rect) super().setGeometry(rect) for i in range(self.count()): w = self.widget(i) hint = w.sizeHint() geom = QRect(rect) size = clipMinMax(rect.size(), w.minimumSize(), w.maximumSize()) size = fixSizePolicy(size, hint, w.sizePolicy()) geom.setSize(size) if geom != w.geometry(): w.setGeometry(geom)
def _onCurrentChanged(self, index): """ Current widget changed, invalidate the layout. """ self.invalidate()
[docs]class AnimatedStackedWidget(QFrame): # Current widget has changed currentChanged = Signal(int) # Transition animation has started transitionStarted = Signal() # Transition animation has finished transitionFinished = Signal() def __init__(self, parent=None, animationEnabled=True): super().__init__(parent) self.__animationEnabled = animationEnabled layout = StackLayout() self.__fadeWidget = CrossFadePixmapWidget(self) self.transitionAnimation = \ QPropertyAnimation(self.__fadeWidget, b"blendingFactor_", self) self.transitionAnimation.setStartValue(0.0) self.transitionAnimation.setEndValue(1.0) self.transitionAnimation.setDuration(100 if animationEnabled else 0) self.transitionAnimation.finished.connect( self.__onTransitionFinished ) layout.addWidget(self.__fadeWidget) layout.currentChanged.connect(self.__onLayoutCurrentChanged) self.setLayout(layout) self.__widgets = [] self.__currentIndex = -1 self.__nextCurrentIndex = -1
[docs] def setAnimationEnabled(self, animationEnabled): """ Enable/disable transition animations. """ if self.__animationEnabled != animationEnabled: self.__animationEnabled = animationEnabled self.transitionAnimation.setDuration( 100 if animationEnabled else 0 )
[docs] def animationEnabled(self): """ Is the transition animation enabled. """ return self.__animationEnabled
[docs] def addWidget(self, widget): """ Append the widget to the stack and return its index. """ return self.insertWidget(self.layout().count(), widget)
[docs] def insertWidget(self, index, widget): """ Insert `widget` into the stack at `index`. """ index = min(index, self.count()) self.__widgets.insert(index, widget) if index <= self.__currentIndex or self.__currentIndex == -1: self.__currentIndex += 1 return self.layout().insertWidget(index, widget)
[docs] def removeWidget(self, widget): """ Remove `widget` from the stack. .. note:: The widget is hidden but is not deleted. """ index = self.__widgets.index(widget) self.layout().removeWidget(widget) self.__widgets.pop(index)
[docs] def widget(self, index): """ Return the widget at `index` """ return self.__widgets[index]
[docs] def indexOf(self, widget): """ Return the index of `widget` in the stack. """ return self.__widgets.index(widget)
[docs] def count(self): """ Return the number of widgets in the stack. """ return max(self.layout().count() - 1, 0)
[docs] def setCurrentWidget(self, widget): """ Set the current shown widget. """ index = self.__widgets.index(widget) self.setCurrentIndex(index)
[docs] def setCurrentIndex(self, index): """ Set the current shown widget index. """ index = max(min(index, self.count() - 1), 0) if self.__currentIndex == -1: self.layout().setCurrentIndex(index) self.__currentIndex = index return # if not self.animationEnabled(): # self.layout().setCurrentIndex(index) # self.__currentIndex = index # return # else start the animation current = self.__widgets[self.__currentIndex] next_widget = self.__widgets[index] def has_pending_resize(widget): return widget.testAttribute(Qt.WA_PendingResizeEvent) or \ not widget.testAttribute(Qt.WA_WState_Created) current_pix = next_pix = None if not has_pending_resize(current): current_pix = current.grab() if not has_pending_resize(next_widget): next_pix = next_widget.grab() with updates_disabled(self): self.__fadeWidget.setPixmap(current_pix) self.__fadeWidget.setPixmap2(next_pix) self.__nextCurrentIndex = index self.__transitionStart()
[docs] def currentIndex(self): """ Return the current shown widget index. """ return self.__currentIndex
[docs] def sizeHint(self): hint = super().sizeHint() if hint.isEmpty(): hint = QSize(0, 0) return hint
def __transitionStart(self): """ Start the transition. """ log.debug("Stack transition start (%s)", str(self.objectName())) # Set the fade widget as the current widget self.__fadeWidget.blendingFactor_ = 0.0 self.layout().setCurrentWidget(self.__fadeWidget) self.transitionAnimation.start() self.transitionStarted.emit() def __onTransitionFinished(self): """ Transition has finished. """ log.debug("Stack transition finished (%s)" % str(self.objectName())) self.__fadeWidget.blendingFactor_ = 1.0 self.__currentIndex = self.__nextCurrentIndex with updates_disabled(self): self.layout().setCurrentIndex(self.__currentIndex) self.transitionFinished.emit() def __onLayoutCurrentChanged(self, index): # Suppress transitional __fadeWidget current widget if index != self.count(): self.currentChanged.emit(index)
class CrossFadePixmapWidget(QWidget): """ A widget for cross fading between two pixmaps. """ def __init__(self, parent=None, pixmap1=None, pixmap2=None): super().__init__(parent) self.setPixmap(pixmap1) self.setPixmap2(pixmap2) self.blendingFactor_ = 0.0 self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) def setPixmap(self, pixmap): """ Set pixmap 1 """ self.pixmap1 = pixmap self.updateGeometry() def setPixmap2(self, pixmap): """ Set pixmap 2 """ self.pixmap2 = pixmap self.updateGeometry() def setBlendingFactor(self, factor): """ Set the blending factor between the two pixmaps. """ self.__blendingFactor = factor self.updateGeometry() def blendingFactor(self): """ Pixmap blending factor between 0.0 and 1.0 """ return self.__blendingFactor blendingFactor_ = Property(float, fget=blendingFactor, fset=setBlendingFactor) def sizeHint(self): """ Return an interpolated size between pixmap1.size() and pixmap2.size() """ if self.pixmap1 and self.pixmap2: size1 = self.pixmap1.size() size2 = self.pixmap2.size() return size1 + self.blendingFactor_ * (size2 - size1) else: return super().sizeHint() def paintEvent(self, event): """ Paint the interpolated pixmap image. """ p = QPainter(self) p.setClipRect(event.rect()) factor = self.blendingFactor_ ** 2 if self.pixmap1 and 1. - factor: p.setOpacity(1. - factor) p.drawPixmap(QPoint(0, 0), self.pixmap1) if self.pixmap2 and factor: p.setOpacity(factor) p.drawPixmap(QPoint(0, 0), self.pixmap2)