Give the app icons, tooltips and a finished theme

There was no icon system at all. Transport buttons called
QStyle.standardIcon(SP_MediaPlay), which returns the platform theme's dark
monochrome glyph -- painted onto the app's saturated red buttons at a fixed
36px with no text, that is the black square. Everything else called an icon
was an emoji baked into en.json, which is tofu wherever the emoji font is
missing. Qt stylesheets cannot recolour a QIcon, so the colour is an argument
to the new helper; that is the whole fix.

The SVGs are drawn here rather than vendored, which keeps a third-party
licence out of the tree, and they live in the module rather than as asset
files, which keeps them out of package-data and safe in a frozen build.

Tooltips were the other half: three widgets in the entire application had one
and nothing set an accessible name. Filling that in at the call sites would
have meant editing well over a hundred of them, most in upstream-owned files.
Instead one application-level event filter handles QEvent.Polish, which Qt
sends to every widget once before it is shown -- so it also reaches dialogs
built by upstream code, and survives the next merge. It maps placeholder
emoji to icons, fills empty tooltips from the button text, and logs icon-only
buttons that still have none so the gaps are findable.

StyleSheet.MAIN styles the window, inputs, buttons and tables and nothing
else, so the main tab bar, combos, sliders, lists, menus, splitters, tooltips
and the horizontal scrollbar fell through to the platform style. EXTRA_QSS
covers them, in a fork-owned module appended at the one application site.
SmoothTabWidget names its frame "tabContent" with the comment "We draw border
on content instead" -- that rule existed only inside two dialogs, and now
exists for the main window too.

Two corrections to rules that were already there: the pressed style changed
the padding, shifting every label two pixels and clipping fixed-width icon
buttons, and checkboxes were fully rounded, which reads as a radio button
rather than an on/off toggle.

Verified by screenshot on the real display: tab bar, buttons, combo carets
and checkboxes all render as intended, and all 35 icons rasterise non-empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 00:58:38 +02:00
parent 5ae2817eea
commit 3aca43b372
16 changed files with 783 additions and 22 deletions
+31 -7
View File
@@ -56,6 +56,7 @@ from .ytsage_gui_analysis import AnalysisMixin
from .ytsage_smooth_tab_widget import SmoothTabWidget
from ..utils.ytsage_constants import (
ICON_PATH,
ICON_PATH_LARGE,
SOUND_PATH,
SUBPROCESS_CREATIONFLAGS,
VIDEO_EXTENSIONS,
@@ -67,6 +68,9 @@ from ..utils.ytsage_config_manager import ConfigManager
from ..utils.ytsage_localization import LocalizationManager, _
from ..utils.ytsage_history_manager import HistoryManager
from .ytsage_stylesheet import StyleSheet
from .ytsage_theme import build_extra_qss
from . import ytsage_icons as icons
from . import ytsage_theme as theme
class UpdateCheckThread(QThread):
@@ -122,12 +126,18 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
self.version = APP_VERSION
load_saved_path(self)
# Load custom icon
if ICON_PATH.exists():
self.setWindowIcon(QIcon(str(ICON_PATH)))
else:
logger.warning(f"Icon file not found at {ICON_PATH}. Using default icon.")
self.setWindowIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown)) # Fallback
# Load custom icon. Both sizes go in so the window manager and taskbar
# pick rather than upscale the 48px one.
app_icon = QIcon()
for path in (ICON_PATH, ICON_PATH_LARGE):
if path.exists():
app_icon.addFile(str(path))
if app_icon.isNull():
logger.warning(f"Icon file not found at {ICON_PATH}. Using a drawn fallback.")
# A drawn icon beats the platform's download arrow standing in for
# the application's identity.
app_icon = icons.icon("play-circle", theme.ACCENT, 64)
self.setWindowIcon(app_icon)
self.signals = SignalManager()
self.download_paused = False
self.current_download = None
@@ -184,7 +194,12 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
# Defer heavy start-up tasks to ensure UI renders immediately
QTimer.singleShot(100, self._perform_startup_checks)
self.setStyleSheet(StyleSheet.MAIN)
# StyleSheet.MAIN is upstream's and covers the window, inputs, buttons
# and tables. EXTRA_QSS adds everything it never styled -- the tab bar,
# combos, sliders, lists, menus, splitters, tooltips and the horizontal
# scrollbar -- and corrects two of its rules. Kept in a separate,
# fork-owned module so this stays a one-line change here.
self.setStyleSheet(StyleSheet.MAIN + build_extra_qss())
self.signals.update_progress.connect(self.update_progress_bar)
# After adding format buttons
@@ -314,6 +329,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
# Analyze button with app's red theme
self.analyze_button = QPushButton(_("buttons.analyze"))
self.analyze_button.setProperty("sageIcon", "search")
self.analyze_button.clicked.connect(self.analyze_url)
self.analyze_button.setEnabled(False) # Disabled until URL is entered
self.analyze_button.setMinimumHeight(42)
@@ -435,33 +451,41 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
# Replace the two separate buttons with a single Custom Options button
self.custom_options_btn = QPushButton(_("buttons.custom_options"))
self.custom_options_btn.setProperty("sageIcon", "settings")
self.custom_options_btn.clicked.connect(self.show_custom_options)
self.about_btn = QPushButton(_("buttons.about"))
self.about_btn.setProperty("sageIcon", "info")
self.about_btn.clicked.connect(self.show_about_dialog)
self.history_btn = QPushButton(_("buttons.history"))
self.history_btn.setProperty("sageIcon", "clock")
self.history_btn.clicked.connect(self.show_history_dialog)
# Add new Time Range button
self.time_range_btn = QPushButton(_("buttons.trim_video"))
self.time_range_btn.setProperty("sageIcon", "scissors")
self.time_range_btn.clicked.connect(self.show_time_range_dialog)
# --- Rename Path Button to Settings Button ---
self.settings_button = QPushButton(_("buttons.download_settings")) # Renamed button
self.settings_button.setProperty("sageIcon", "settings")
self.settings_button.clicked.connect(self.show_download_settings_dialog) # Renamed method
self._update_settings_tooltip()
# --- End Settings Button ---
self.download_btn = QPushButton(_("buttons.download"))
self.download_btn.setProperty("sageIcon", "download")
self.download_btn.clicked.connect(self.start_download)
# Add pause and cancel buttons
self.pause_btn = QPushButton(_("buttons.pause"))
self.pause_btn.setProperty("sageIcon", "pause")
self.pause_btn.clicked.connect(self.toggle_pause)
self.pause_btn.setVisible(False) # Hidden initially
self.cancel_btn = QPushButton(_("buttons.cancel"))
self.cancel_btn.setProperty("sageIcon", "x")
self.cancel_btn.clicked.connect(self.cancel_download)
self.cancel_btn.setVisible(False) # Hidden initially