diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py index 49de1fd..5cd798c 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py @@ -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; } diff --git a/ytsage/gui/ytsage_smooth_tab_widget.py b/ytsage/gui/ytsage_smooth_tab_widget.py new file mode 100644 index 0000000..375b2ba --- /dev/null +++ b/ytsage/gui/ytsage_smooth_tab_widget.py @@ -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()