1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
|
#------------------------------------------------------------------------------
# Copyright (c) 2013-2025, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
#------------------------------------------------------------------------------
from enum import IntEnum
from atom.api import Int, Typed
from enaml.widgets.stack import ProxyStack
from .QtCore import QTimer, QEvent, Signal
from .QtGui import QPixmap
from .QtWidgets import QStackedWidget
from .q_pixmap_painter import QPixmapPainter
from .q_pixmap_transition import (
QDirectedTransition, QSlideTransition, QWipeTransition, QIrisTransition,
QFadeTransition, QCrossFadeTransition
)
from .qt_constraints_widget import QtConstraintsWidget
from .qt_stack_item import QtStackItem
TRANSITION_TYPE = {
'slide': QSlideTransition,
'wipe': QWipeTransition,
'iris': QIrisTransition,
'fade': QFadeTransition,
'crossfade': QCrossFadeTransition,
}
TRANSITION_DIRECTION = {
'left_to_right': QDirectedTransition.LeftToRight,
'right_to_left': QDirectedTransition.RightToLeft,
'top_to_bottom': QDirectedTransition.TopToBottom,
'bottom_to_top': QDirectedTransition.BottomToTop,
}
def make_transition(transition):
""" Make a QPixmapTransition from an Enaml Transition.
Parameters
----------
transition : Transition
The Enaml Transition object.
Returns
-------
result : QPixmapTransition
A QPixmapTransition to use as the transition.
"""
qtransition = TRANSITION_TYPE[transition.type]()
qtransition.setDuration(transition.duration)
if isinstance(qtransition, QDirectedTransition):
qtransition.setDirection(TRANSITION_DIRECTION[transition.direction])
return qtransition
class QStack(QStackedWidget):
""" A QStackedWidget subclass which adds support for transitions.
"""
class SizeHintMode(IntEnum):
""" An int enum defining the size hint modes of the stack.
"""
#: The size hint is the union of all stack items.
Union = 0
#: The size hint is the size hint of the current stack item.
Current = 1
#: Proxy the SizeHintMode values as if it were an anonymous enum.
Union = SizeHintMode.Union
Current = SizeHintMode.Current
#: A signal emitted when a LayoutRequest event is posted to the
#: stack widget. This will typically occur when the size hint of
#: the stack is no longer valid.
layoutRequested = Signal()
def __init__(self, *args, **kwargs):
""" Initialize a QStack.
Parameters
----------
*args, **kwargs
The positional and keyword arguments needed to initalize
a QStackedWidget.
"""
super(QStack, self).__init__(*args, **kwargs)
self._painter = None
self._transition = None
self._transition_index = 0
self._size_hint_mode = QStack.Union
#--------------------------------------------------------------------------
# Private API
#--------------------------------------------------------------------------
def _onTransitionFinished(self):
""" A signal handler for the `finished` signal of the transition.
This method resets the internal painter and triggers the normal
index change for the stacked widget.
"""
painter = self._painter
if painter is not None:
painter.setTargetWidget(None)
self._painter = None
self.setCurrentIndex(self._transition_index)
# This final show() makes sure the underlyling widget is visible.
# If transitions are being fired rapidly, it's possible that the
# current index and the transition index will be the same when
# the call above is invoked. In such cases, Qt short circuits the
# evaluation and the current widget is not shown.
self.currentWidget().show()
def _runTransition(self):
""" A private method which runs the transition effect.
The `_transition_index` attribute should be set before calling
this method. If no transition object exists for this widget,
then it is equivalent to calling `setCurrentIndex`. If the new
index is not different from the current index the transition
will not be performed.
"""
from_index = self.currentIndex()
to_index = self._transition_index
# If the index hasn't changed, there is nothing to update.
if from_index == to_index:
return
# If there is no transition applied, just change the index.
transition = self._transition
if transition is None:
self.setCurrentIndex(to_index)
return
# Otherwise, grab the pixmaps for the start and ending states
# and set them on the transtion. The widgets are resized to the
# current size so that the pixmaps are grabbed in a good state.
src_widget = self.widget(from_index)
dst_widget = self.widget(to_index)
size = self.size()
src_widget.resize(size)
dst_widget.resize(size)
src_pixmap = QPixmap.grabWidget(src_widget)
dst_pixmap = QPixmap.grabWidget(dst_widget)
out_pixmap = QPixmap(size)
transition.setPixmaps(src_pixmap, dst_pixmap, out_pixmap)
# Hide both of the constituent widgets so that the painter has
# a clean widget on which to draw.
src_widget.setVisible(False)
dst_widget.setVisible(False)
# Hookup the pixmap painter and start the transition.
painter = self._painter = QPixmapPainter()
painter.setTargetWidget(self)
transition.pixmapUpdated.connect(painter.drawPixmap)
transition.start()
#--------------------------------------------------------------------------
# Public API
#--------------------------------------------------------------------------
def event(self, event):
""" A custom event handler which handles LayoutRequest events.
When a LayoutRequest event is posted to this widget, it will
emit the `layoutRequested` signal. This allows an external
consumer of this widget to update their external layout.
"""
res = super(QStack, self).event(event)
if event.type() == QEvent.LayoutRequest:
self.layoutRequested.emit()
return res
def sizeHint(self):
""" A reimplemented size hint handler.
This method will compute the size hint based on the size hint
of the current tab, instead of the default behavior which is
the maximum of all the size hints of the tabs.
"""
if self._size_hint_mode == QStack.Current:
curr = self.currentWidget()
if curr is not None:
return curr.sizeHint()
return super(QStack, self).sizeHint()
def minimumSizeHint(self):
""" A reimplemented minimum size hint handler.
This method will compute the size hint based on the size hint
of the current tab, instead of the default behavior which is
the maximum of all the minimum size hints of the tabs.
"""
if self._size_hint_mode == QStack.Current:
curr = self.currentWidget()
if curr is not None:
return curr.minimumSizeHint()
return super(QStack, self).minimumSizeHint()
def sizeHintMode(self):
""" Get the size hint mode of the stack.
Returns
-------
result : QStack.SizeHintMode
The size hint mode enum value for the stack.
"""
return self._size_hint_mode
def setSizeHintMode(self, mode):
""" Set the size hint mode of the stack.
Parameters
----------
mode : QStack.SizeHintMode
The size hint mode for the stack.
"""
assert isinstance(mode, QStack.SizeHintMode)
self._size_hint_mode = mode
def transition(self):
""" Get the transition installed on this widget.
Returns
-------
result : QPixmapTransition or None
The pixmap transition installed on this widget, or None if
no transition is being used.
"""
return self._transition
def setTransition(self, transition):
""" Set the transition to be used by this widget.
Parameters
----------
transition : QPixmapTransition or None
The transition to use when changing between widgets on this
stack or None if no transition should be used.
"""
old = self._transition
if old is not None:
old.finished.disconnect(self._onTransitionFinished)
self._transition = transition
if transition is not None:
transition.finished.connect(self._onTransitionFinished)
def transitionTo(self, index):
""" Transition the stack widget to the given index.
If there is no transition object is installed on the widget
this is equivalent to calling `setCurrentIndex`. Otherwise,
the change will be animated using the installed transition.
Parameters
----------
index : int
The index of the target transition widget.
"""
if index < 0 or index >= self.count():
return
self._transition_index = index
if self.transition() is not None:
QTimer.singleShot(0, self._runTransition)
else:
self.setCurrentIndex(index)
#: A mapping Enaml -> Qt size hint modes.
SIZE_HINT_MODE = {
'union': QStack.Union,
'current': QStack.Current,
}
#: Cyclic notification guard
INDEX_FLAG = 0x1
class QtStack(QtConstraintsWidget, ProxyStack):
""" A Qt implementation of an Enaml Stack.
"""
#: A reference to the widget created by the proxy.
widget = Typed(QStack)
#: Cyclic notification guards
_guard = Int(0)
#--------------------------------------------------------------------------
# Initialization API
#--------------------------------------------------------------------------
def create_widget(self):
""" Create the underlying QStack widget.
"""
self.widget = QStack(self.parent_widget())
def init_widget(self):
""" Initialize the underlying control.
"""
super(QtStack, self).init_widget()
d = self.declaration
self.set_transition(d.transition)
self.set_size_hint_mode(d.size_hint_mode, update=False)
def init_layout(self):
""" Initialize the layout of the underlying control.
"""
super(QtStack, self).init_layout()
widget = self.widget
for item in self.stack_items():
widget.addWidget(item)
# Bypass the transition effect during initialization.
widget.setCurrentIndex(self.declaration.index)
widget.layoutRequested.connect(self.on_layout_requested)
widget.currentChanged.connect(self.on_current_changed)
#--------------------------------------------------------------------------
# Utility Methods
#--------------------------------------------------------------------------
def stack_items(self):
""" Get the stack items defined on the control.
"""
for d in self.declaration.stack_items():
w = d.proxy.widget
if w is not None:
yield w
#--------------------------------------------------------------------------
# Child Events
#--------------------------------------------------------------------------
def child_added(self, child):
""" Handle the child added event for a QtStack.
"""
super(QtStack, self).child_added(child)
if isinstance(child, QtStackItem):
for index, dchild in enumerate(self.children()):
if child is dchild:
self.widget.insertWidget(index, child.widget)
def child_removed(self, child):
""" Handle the child removed event for a QtStack.
"""
super(QtStack, self).child_removed(child)
if isinstance(child, QtStackItem):
self.widget.removeWidget(child.widget)
#--------------------------------------------------------------------------
# Signal Handlers
#--------------------------------------------------------------------------
def on_layout_requested(self):
""" Handle the `layoutRequested` signal from the QStack.
"""
self.geometry_updated()
def on_current_changed(self):
""" Handle the `currentChanged` signal from the QStack.
"""
if not self._guard & INDEX_FLAG:
self._guard |= INDEX_FLAG
try:
self.declaration.index = self.widget.currentIndex()
finally:
self._guard &= ~INDEX_FLAG
#--------------------------------------------------------------------------
# Widget Update Methods
#--------------------------------------------------------------------------
def set_index(self, index):
""" Set the current index of the underlying widget.
"""
if not self._guard & INDEX_FLAG:
self._guard |= INDEX_FLAG
try:
self.widget.transitionTo(index)
finally:
self._guard &= ~INDEX_FLAG
def set_transition(self, transition):
""" Set the transition on the underlying widget.
"""
if transition:
self.widget.setTransition(make_transition(transition))
else:
self.widget.setTransition(None)
def set_size_hint_mode(self, mode, update=True):
""" Set the size hint mode for the widget.
"""
self.widget.setSizeHintMode(SIZE_HINT_MODE[mode])
if update:
self.geometry_updated()
|