Add smooth tab widget with fade transitions
Introduce SmoothTabWidget and a FadingStackedWidget to provide cross-fade transitions between tabs. Added ytsage/gui/ytsage_smooth_tab_widget.py which implements a QTabBar + FadingStackedWidget (default 300ms fade) and overlay-based fade logic. Updated ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py to use SmoothTabWidget instead of QTabWidget and adjusted the stylesheet selector to target the new content frame (#tabContent) so existing styles continue to apply.
This commit is contained in:
@@ -29,6 +29,7 @@ from PySide6.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ..ytsage_smooth_tab_widget import SmoothTabWidget
|
||||||
from ...core.ytsage_yt_dlp import get_yt_dlp_path
|
from ...core.ytsage_yt_dlp import get_yt_dlp_path
|
||||||
from ...core.ytsage_utils import update_auto_update_settings
|
from ...core.ytsage_utils import update_auto_update_settings
|
||||||
from ...utils.ytsage_constants import YTDLP_DOCS_URL
|
from ...utils.ytsage_constants import YTDLP_DOCS_URL
|
||||||
@@ -115,7 +116,7 @@ class CustomOptionsDialog(QDialog):
|
|||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
# Create tab widget to organize content
|
# Create tab widget to organize content
|
||||||
self.tab_widget = QTabWidget()
|
self.tab_widget = SmoothTabWidget()
|
||||||
layout.addWidget(self.tab_widget)
|
layout.addWidget(self.tab_widget)
|
||||||
|
|
||||||
# === Cookies Tab ===
|
# === Cookies Tab ===
|
||||||
@@ -542,7 +543,7 @@ class CustomOptionsDialog(QDialog):
|
|||||||
QDialog {
|
QDialog {
|
||||||
background-color: #15181b;
|
background-color: #15181b;
|
||||||
}
|
}
|
||||||
QTabWidget::pane {
|
QFrame#tabContent {
|
||||||
border: 1px solid #3d3d3d;
|
border: 1px solid #3d3d3d;
|
||||||
background-color: #15181b;
|
background-color: #15181b;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
from PySide6.QtCore import QEasingCurve, QPropertyAnimation, Qt
|
||||||
|
from PySide6.QtGui import QPixmap
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QFrame,
|
||||||
|
QGraphicsOpacityEffect,
|
||||||
|
QLabel,
|
||||||
|
QStackedWidget,
|
||||||
|
QTabBar,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
class FadingStackedWidget(QStackedWidget):
|
||||||
|
"""
|
||||||
|
A QStackedWidget that cross-fades between widgets.
|
||||||
|
"""
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.fade_duration = 300
|
||||||
|
self.fade_easing = QEasingCurve.Type.OutQuad
|
||||||
|
|
||||||
|
def setCurrentIndex(self, index):
|
||||||
|
curr_index = self.currentIndex()
|
||||||
|
if index == curr_index:
|
||||||
|
return
|
||||||
|
|
||||||
|
widget = self.widget(index)
|
||||||
|
curr_widget = self.widget(curr_index)
|
||||||
|
|
||||||
|
# If widget isn't visible or valid, just swap
|
||||||
|
if not self.isVisible() or not curr_widget:
|
||||||
|
super().setCurrentIndex(index)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1. Capture the current view (the "old" tab)
|
||||||
|
# Use grab() for simplicity and reliability in PySide6
|
||||||
|
pixmap = self.grab()
|
||||||
|
|
||||||
|
# 2. Create an overlay label to hold this "old" view
|
||||||
|
overlay = QLabel(self)
|
||||||
|
overlay.setPixmap(pixmap)
|
||||||
|
overlay.setGeometry(0, 0, self.width(), self.height())
|
||||||
|
overlay.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) # Don't block clicks
|
||||||
|
overlay.show()
|
||||||
|
|
||||||
|
# 3. Switch the actual stack to the "new" view
|
||||||
|
super().setCurrentIndex(index)
|
||||||
|
|
||||||
|
# CRITICAL: Ensure overlay stays on top of the new widget
|
||||||
|
overlay.raise_()
|
||||||
|
|
||||||
|
# 4. Fade OUT the overlay, revealing the new view
|
||||||
|
effect = QGraphicsOpacityEffect(overlay)
|
||||||
|
overlay.setGraphicsEffect(effect)
|
||||||
|
|
||||||
|
|
||||||
|
anim = QPropertyAnimation(effect, b"opacity", overlay)
|
||||||
|
anim.setDuration(self.fade_duration)
|
||||||
|
anim.setStartValue(1.0)
|
||||||
|
anim.setEndValue(0.0)
|
||||||
|
anim.setEasingCurve(self.fade_easing)
|
||||||
|
|
||||||
|
# Cleanup when done
|
||||||
|
anim.finished.connect(lambda: self._cleanup(overlay))
|
||||||
|
|
||||||
|
# Keep reference to prevent GC
|
||||||
|
self._active_anim = anim
|
||||||
|
anim.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
|
||||||
|
def _cleanup(self, overlay):
|
||||||
|
overlay.hide()
|
||||||
|
overlay.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
|
class SmoothTabWidget(QWidget):
|
||||||
|
"""
|
||||||
|
A unified Widget that behaves like a QTabWidget but uses smooth fading transitions.
|
||||||
|
Includes a QTabBar and a FadingStackedWidget.
|
||||||
|
"""
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
# Main Layout
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.layout.setSpacing(0)
|
||||||
|
|
||||||
|
# Tab Bar
|
||||||
|
self.tab_bar = QTabBar(self)
|
||||||
|
self.tab_bar.setDrawBase(False) # We draw border on content instead
|
||||||
|
self.tab_bar.currentChanged.connect(self.set_current_index)
|
||||||
|
self.layout.addWidget(self.tab_bar)
|
||||||
|
|
||||||
|
# Content Area (Frame) - Mimics QTabWidget::pane
|
||||||
|
self.content_frame = QFrame(self)
|
||||||
|
self.content_frame.setObjectName("tabContent")
|
||||||
|
|
||||||
|
# Layout inside the content frame
|
||||||
|
self.content_layout = QVBoxLayout(self.content_frame)
|
||||||
|
self.content_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.content_layout.setSpacing(0)
|
||||||
|
|
||||||
|
# The Stack
|
||||||
|
self.stack = FadingStackedWidget(self.content_frame)
|
||||||
|
self.content_layout.addWidget(self.stack)
|
||||||
|
|
||||||
|
self.layout.addWidget(self.content_frame)
|
||||||
|
|
||||||
|
def addTab(self, widget, label):
|
||||||
|
"""Add a tab with the given widget and label."""
|
||||||
|
self.stack.addWidget(widget)
|
||||||
|
self.tab_bar.addTab(label)
|
||||||
|
|
||||||
|
def set_current_index(self, index):
|
||||||
|
"""Slot to handle tab bar clicks."""
|
||||||
|
self.tab_bar.setCurrentIndex(index)
|
||||||
|
self.stack.setCurrentIndex(index)
|
||||||
|
|
||||||
|
def currentWidget(self):
|
||||||
|
return self.stack.currentWidget()
|
||||||
|
|
||||||
|
def currentIndex(self):
|
||||||
|
return self.stack.currentIndex()
|
||||||
Reference in New Issue
Block a user