From 930d253fd16c42767c184ccd931f843e87440e3a Mon Sep 17 00:00:00 2001 From: dervart Date: Sat, 13 Jun 2026 22:56:53 +0700 Subject: [PATCH 1/2] feat: new class AutoHideScrollArea for Library & Autoinstall tab --- portprotonqt/custom_widgets.py | 140 ++++++++++++++++++++++++++- portprotonqt/game_library_manager.py | 6 +- portprotonqt/main_window.py | 4 +- 3 files changed, 143 insertions(+), 7 deletions(-) diff --git a/portprotonqt/custom_widgets.py b/portprotonqt/custom_widgets.py index bcba5557..6de1cafb 100644 --- a/portprotonqt/custom_widgets.py +++ b/portprotonqt/custom_widgets.py @@ -1,5 +1,5 @@ -from PySide6.QtWidgets import QLabel, QPushButton, QStyle, QStyleOptionButton, QWidget, QLayout, QLayoutItem -from PySide6.QtCore import Qt, Signal, QRect, QRectF, QSize, Property, QPropertyAnimation, QEasingCurve +from PySide6.QtWidgets import QLabel, QPushButton, QStyle, QStyleOptionButton, QWidget, QLayout, QLayoutItem, QScrollArea, QGraphicsOpacityEffect +from PySide6.QtCore import Qt, Signal, QRect, QRectF, QSize, Property, QPropertyAnimation, QEasingCurve, QTimer, QEvent from PySide6.QtGui import QFont, QFontMetrics, QIcon, QPainter from PySide6.QtSvg import QSvgRenderer from portprotonqt.theme_manager import ThemeManager @@ -700,3 +700,139 @@ class NavLabel(QLabel): event.accept() else: super().mousePressEvent(event) + +class AutoHideScrollArea(QScrollArea): + def __init__( + self, + theme, + parent: QWidget | None = None, + hide_delay_ms: int = 1000, + fade_duration_ms: int = 200, + ): + self.theme = theme + self.theme_manager = ThemeManager() + + super().__init__(parent) + self.hide_delay_ms = hide_delay_ms + self.fade_duration_ms = fade_duration_ms + + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + self.setStyleSheet("background: transparent;") + + self._v_scrollbar = self.verticalScrollBar() + self._v_scrollbar.installEventFilter(self) + + self._opacity_effect = QGraphicsOpacityEffect(self._v_scrollbar) + self._opacity_effect.setOpacity(0.0) + self._v_scrollbar.setGraphicsEffect(self._opacity_effect) + + self._fade_animation = QPropertyAnimation(self._opacity_effect, b"opacity") + self._fade_animation.setDuration(self.fade_duration_ms) + self._fade_animation.setEasingCurve(QEasingCurve.Type.InOutCubic) + + self._is_visible = False + self._scroll_needed = False + + self._apply_visible_style() + + self._hide_timer = QTimer(self) + self._hide_timer.setSingleShot(True) + self._hide_timer.timeout.connect(self._start_fade_out) + + self._v_scrollbar.valueChanged.connect(self._on_scroll) + + self.viewport().installEventFilter(self) + initial_widget = self.widget() + if initial_widget is not None: + initial_widget.installEventFilter(self) + + QTimer.singleShot(0, self._update_scroll_needed) + + def _apply_visible_style(self) -> None: + self._v_scrollbar.setStyleSheet(self.theme.SCROLL_STYLE) + + def _start_fade_in(self) -> None: + if not self._scroll_needed: + return + self._fade_animation.stop() + self._fade_animation.setStartValue(self._opacity_effect.opacity()) + self._fade_animation.setEndValue(1.0) + self._fade_animation.start() + self._is_visible = True + + def _start_fade_out(self) -> None: + if not self._scroll_needed: + return + self._fade_animation.stop() + self._fade_animation.setStartValue(self._opacity_effect.opacity()) + self._fade_animation.setEndValue(0.0) + self._fade_animation.start() + self._is_visible = False + + def _set_opacity_immediately(self, opacity: float) -> None: + self._fade_animation.stop() + self._opacity_effect.setOpacity(opacity) + self._is_visible = opacity == 1.0 + + def _update_scroll_needed(self) -> None: + widget = self.widget() + if widget is None: + self._scroll_needed = False + self._set_opacity_immediately(0.0) + self._hide_timer.stop() + return + + content_height = widget.sizeHint().height() + viewport_height = self.viewport().height() + self._scroll_needed = content_height > viewport_height + + if not self._scroll_needed: + self._set_opacity_immediately(0.0) + self._hide_timer.stop() + else: + if not self._is_visible: + self._set_opacity_immediately(0.0) + + def _on_scroll(self, value: int) -> None: + if not self._scroll_needed: + return + self._start_fade_in() + self._hide_timer.start(self.hide_delay_ms) + + def enterEvent(self, event): + if self._scroll_needed: + self._start_fade_in() + self._hide_timer.start(self.hide_delay_ms) + + def leaveEvent(self, event): + if self._scroll_needed and self._is_visible: + self._hide_timer.start(self.hide_delay_ms) + + def eventFilter(self, obj, event): + if not hasattr(self, "_v_scrollbar"): + return super().eventFilter(obj, event) + + if event.type() == QEvent.Type.Resize: + self._update_scroll_needed() + elif obj == self._v_scrollbar: + if event.type() == QEvent.Type.Enter: + if self._scroll_needed: + self._start_fade_in() + self._hide_timer.start(self.hide_delay_ms) + elif event.type() == QEvent.Type.Leave: + if self._scroll_needed and self._is_visible: + self._hide_timer.start(self.hide_delay_ms) + return super().eventFilter(obj, event) + + def setWidget(self, widget: QWidget | None) -> None: + old_widget = self.widget() + if old_widget is not None: + old_widget.removeEventFilter(self) + + if widget is None: + super().setWidget(None) # type: ignore + else: + super().setWidget(widget) + widget.installEventFilter(self) + + self._update_scroll_needed() diff --git a/portprotonqt/game_library_manager.py b/portprotonqt/game_library_manager.py index 75f3b16f..9b3dc770 100644 --- a/portprotonqt/game_library_manager.py +++ b/portprotonqt/game_library_manager.py @@ -5,7 +5,7 @@ from PySide6.QtWidgets import QAbstractButton from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QSlider, QScroller, QStackedWidget from PySide6.QtCore import Qt, QTimer from PySide6.QtGui import QRegion -from portprotonqt.custom_widgets import FlowLayout +from portprotonqt.custom_widgets import FlowLayout, AutoHideScrollArea from portprotonqt.config import favorites_config, game_config, ui_config from portprotonqt.image_utils import load_pixmap_async from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit @@ -73,10 +73,10 @@ class GameLibraryManager: layout.addWidget(searchWidget) # Scroll area for game grid - scrollArea = QScrollArea() + scrollArea = AutoHideScrollArea(theme=self.theme) self.gamesScrollArea = scrollArea scrollArea.setWidgetResizable(True) - scrollArea.setStyleSheet(self.theme.SCROLL_STYLE + self.theme.TRANSPARENT_BACKGROUND_STYLE) + # scrollArea.setStyleSheet(self.theme.SCROLL_STYLE + self.theme.TRANSPARENT_BACKGROUND_STYLE) QScroller.grabGesture(scrollArea.viewport(), QScroller.ScrollerGestureType.LeftMouseButtonGesture) self.gamesListWidget = QWidget() diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index eb672ff9..8790ce3f 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -13,7 +13,7 @@ from portprotonqt.icon_extractor import generate_thumbnail from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog, ExeSettingsDialog from portprotonqt.game_card import GameCard from portprotonqt.animations import DetailPageAnimations, ExpandingSearchAnimation, LibraryControlsAnimation -from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout +from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout, AutoHideScrollArea from portprotonqt.detail_pages import DetailPageManager from portprotonqt.portproton_api import PortProtonAPI, get_user_conf_setting, set_user_conf_setting from portprotonqt.debug_utils import get_selectable_gpu_list, get_prefix_name @@ -1840,7 +1840,7 @@ class MainWindow(MainWindowControlHintsMixin, MainWindowSystemTabMixin, MainWind searchLayout.addWidget(self.autoInstallSearchLineEdit) autoInstallLayout.addWidget(searchWidget) - self.autoInstallScrollArea = QScrollArea() + self.autoInstallScrollArea = AutoHideScrollArea(theme=self.theme) self.autoInstallScrollArea.setWidgetResizable(True) self.autoInstallScrollArea.setStyleSheet(self.theme.SCROLL_STYLE + self.theme.TRANSPARENT_BACKGROUND_STYLE) QScroller.grabGesture(self.autoInstallScrollArea.viewport(), QScroller.ScrollerGestureType.LeftMouseButtonGesture) -- 2.54.0 From dcaba9bf1f20c23ed2928bbe6e93dff6063aab34 Mon Sep 17 00:00:00 2001 From: dervart Date: Sat, 13 Jun 2026 23:06:48 +0700 Subject: [PATCH 2/2] fix: remove commented styles for AutoHideScrollArea --- portprotonqt/game_library_manager.py | 1 - portprotonqt/main_window.py | 1 - 2 files changed, 2 deletions(-) diff --git a/portprotonqt/game_library_manager.py b/portprotonqt/game_library_manager.py index 9b3dc770..ee97a767 100644 --- a/portprotonqt/game_library_manager.py +++ b/portprotonqt/game_library_manager.py @@ -76,7 +76,6 @@ class GameLibraryManager: scrollArea = AutoHideScrollArea(theme=self.theme) self.gamesScrollArea = scrollArea scrollArea.setWidgetResizable(True) - # scrollArea.setStyleSheet(self.theme.SCROLL_STYLE + self.theme.TRANSPARENT_BACKGROUND_STYLE) QScroller.grabGesture(scrollArea.viewport(), QScroller.ScrollerGestureType.LeftMouseButtonGesture) self.gamesListWidget = QWidget() diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 8790ce3f..1ad34251 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -1842,7 +1842,6 @@ class MainWindow(MainWindowControlHintsMixin, MainWindowSystemTabMixin, MainWind self.autoInstallScrollArea = AutoHideScrollArea(theme=self.theme) self.autoInstallScrollArea.setWidgetResizable(True) - self.autoInstallScrollArea.setStyleSheet(self.theme.SCROLL_STYLE + self.theme.TRANSPARENT_BACKGROUND_STYLE) QScroller.grabGesture(self.autoInstallScrollArea.viewport(), QScroller.ScrollerGestureType.LeftMouseButtonGesture) self.autoInstallContainer = QWidget() -- 2.54.0