Source code for orangecanvas.gui.stackedwidget

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

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

"""
from typing import Any, Union

import logging

from AnyQt.QtWidgets import (
    QWidget, QFrame, QStackedLayout, QSizePolicy, QLayout
)
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):
    # type: (QSize, QSize, QSize) -> QSize
    """
    Clip the size so it is bigger then minSize but smaller than maxSize.
    """
    return size.expandedTo(minSize).boundedTo(maxSize)


def fixSizePolicy(size, hint, policy):
    # type: (QSize, QSize, QSizePolicy) -> QSize
    """
    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, **kwargs): # type: (Union[QWidget, QLayout, None], Any) -> None self.__rect = QRect() if parent is not None: super().__init__(parent, **kwargs) else: super().__init__(**kwargs) self.currentChanged.connect(self._onCurrentChanged)
[docs] def sizeHint(self): # type: () -> QSize 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): # type: () -> QSize current = self.currentWidget() if current: return current.minimumSize() else: return super().minimumSize()
[docs] def maximumSize(self): # type: () -> QSize current = self.currentWidget() if current: return current.maximumSize() else: return super().maximumSize()
[docs] def hasHeightForWidth(self) -> bool: current = self.currentWidget() if current is not None: return current.hasHeightForWidth() else: return False
[docs] def heightForWidth(self, width: int) -> int: current = self.currentWidget() if current is not None: return current.heightForWidth(width) else: return -1
[docs] def geometry(self): # type: () -> QRect # Reimplemented due to QTBUG-47107. return QRect(self.__rect)
[docs] def setGeometry(self, rect): # type: (QRect) -> None 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)
[docs] def addWidget(self, w): QStackedLayout.addWidget(self, w) rect = self.__rect 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)