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,
|
||||
)
|
||||
|
||||
from ..ytsage_smooth_tab_widget import SmoothTabWidget
|
||||
from ...core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from ...core.ytsage_utils import update_auto_update_settings
|
||||
from ...utils.ytsage_constants import YTDLP_DOCS_URL
|
||||
@@ -115,7 +116,7 @@ class CustomOptionsDialog(QDialog):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Create tab widget to organize content
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget = SmoothTabWidget()
|
||||
layout.addWidget(self.tab_widget)
|
||||
|
||||
# === Cookies Tab ===
|
||||
@@ -542,7 +543,7 @@ class CustomOptionsDialog(QDialog):
|
||||
QDialog {
|
||||
background-color: #15181b;
|
||||
}
|
||||
QTabWidget::pane {
|
||||
QFrame#tabContent {
|
||||
border: 1px solid #3d3d3d;
|
||||
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