Allow setting theme color theme from the menu bar

This functions essentially the same as the language selector. The language selector now also checks the currently set option on launch, and both the language and theme selectors spawn a popup prompting the user to restart NUSGet after selecting a new value.
This commit is contained in:
Campbell 2025-05-27 14:44:48 -04:00
parent db6dc65791
commit 109e3dc25a
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
5 changed files with 162 additions and 10 deletions

View File

@ -32,7 +32,7 @@ from qt.py.ui_MainMenu import Ui_MainWindow
from modules.config import * from modules.config import *
from modules.core import * from modules.core import *
from modules.language import * from modules.language import *
from modules.theme import is_dark_theme from modules.theme import *
from modules.tree import NUSGetTreeModel, TIDFilterProxyModel from modules.tree import NUSGetTreeModel, TIDFilterProxyModel
from modules.download_batch import run_nus_download_batch from modules.download_batch import run_nus_download_batch
from modules.download_wii import run_nus_download_wii from modules.download_wii import run_nus_download_wii
@ -135,9 +135,38 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# languages at once makes no sense. # languages at once makes no sense.
language_group = QActionGroup(self) language_group = QActionGroup(self)
language_group.setExclusive(True) language_group.setExclusive(True)
current_language = ""
try:
current_language = config_data["language"]
except KeyError:
pass
for action in self.ui.menu_options_language.actions(): for action in self.ui.menu_options_language.actions():
language_group.addAction(action) language_group.addAction(action)
language_group.triggered.connect(lambda lang=action: set_language(config_data, lang.text())) if current_language != "":
if LANGS[current_language] == action.text():
action.setChecked(True)
# There is no language set, so check the system language option.
if current_language == "":
self.ui.action_language_system.setChecked(True)
language_group.triggered.connect(lambda lang=action: self.change_language(lang.text()))
# Another QActionGroup used for the theme selector.
theme_group = QActionGroup(self)
theme_group.setExclusive(True)
for action in self.ui.menu_options_theme.actions():
theme_group.addAction(action)
self.ui.action_theme_system.triggered.connect(lambda: self.change_theme("system"))
self.ui.action_theme_light.triggered.connect(lambda: self.change_theme("light"))
self.ui.action_theme_dark.triggered.connect(lambda: self.change_theme("dark"))
try:
match config_data["theme"]:
case "light":
self.ui.action_theme_light.setChecked(True)
case "dark":
self.ui.action_theme_dark.setChecked(True)
case _:
self.ui.action_theme_system.setChecked(True)
except KeyError:
pass
# --------- # ---------
# Help Menu # Help Menu
# --------- # ---------
@ -146,7 +175,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.ui.action_about.triggered.connect(self.about_nusget) self.ui.action_about.triggered.connect(self.about_nusget)
self.ui.action_about_qt.triggered.connect(lambda: QMessageBox.aboutQt(self)) self.ui.action_about_qt.triggered.connect(lambda: QMessageBox.aboutQt(self))
# Save some light/dark theme values for later, including the appropriately colored info icon. # Save some light/dark theme values for later, including the appropriately colored info icon.
if is_dark_theme(): if is_dark_theme(config_data):
bg_color = "#2b2b2b" bg_color = "#2b2b2b"
icon = QIcon(os.path.join(os.path.dirname(__file__), "resources", "information_white.svg")) icon = QIcon(os.path.join(os.path.dirname(__file__), "resources", "information_white.svg"))
else: else:
@ -622,6 +651,26 @@ class MainWindow(QMainWindow, Ui_MainWindow):
about_box = AboutNUSGet([nusget_version, version("libWiiPy"), version("libTWLPy")]) about_box = AboutNUSGet([nusget_version, version("libWiiPy"), version("libTWLPy")])
about_box.exec() about_box.exec()
def change_language(self, new_lang):
set_language(config_data, new_lang)
msg_box = QMessageBox()
msg_box.setIcon(QMessageBox.Icon.Warning)
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
msg_box.setDefaultButton(QMessageBox.StandardButton.Ok)
msg_box.setWindowTitle(app.translate("MainWindow", "Restart Required"))
msg_box.setText(app.translate("MainWindow", "NUSGet must be restarted for the new language to take effect."))
msg_box.exec()
def change_theme(self, new_theme):
set_theme(config_data, new_theme)
msg_box = QMessageBox()
msg_box.setIcon(QMessageBox.Icon.Warning)
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
msg_box.setDefaultButton(QMessageBox.StandardButton.Ok)
msg_box.setWindowTitle(app.translate("MainWindow", "Restart Required"))
msg_box.setText(app.translate("MainWindow", "NUSGet must be restarted for the new theme to take effect."))
msg_box.exec()
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
@ -684,7 +733,7 @@ if __name__ == "__main__":
# NUSGet look nice and pretty. # NUSGet look nice and pretty.
app.setStyle("fusion") app.setStyle("fusion")
theme_sheet = "style_dark.qss" theme_sheet = "style_dark.qss"
if is_dark_theme(): if is_dark_theme(config_data):
theme_sheet = "style_dark.qss" theme_sheet = "style_dark.qss"
else: else:
theme_sheet = "style_light.qss" theme_sheet = "style_light.qss"

View File

@ -6,6 +6,18 @@ from modules.config import update_setting
from PySide6.QtCore import QLocale, QTranslator from PySide6.QtCore import QLocale, QTranslator
LANGS = {
"en": "English",
"es": "Español",
"de": "Deutsch",
"fr": "Français",
"it": "Italiano",
"no": "Norsk",
"ro": "Românǎ",
"ko": "한국어",
}
def set_language(config_data: dict, lang: str) -> None: def set_language(config_data: dict, lang: str) -> None:
# Match the selected language. These names will NOT be translated since they represent each language in that # Match the selected language. These names will NOT be translated since they represent each language in that
# language, but the "System (Default)" option will, so that will match the default case. # language, but the "System (Default)" option will, so that will match the default case.
@ -46,6 +58,9 @@ def get_language(translator: QTranslator, config_data: dict, path: str) -> QTran
lang = "" lang = ""
# A specific language was set in the app's settings. # A specific language was set in the app's settings.
if lang != "": if lang != "":
# If the target language is English, then return an empty translator because that's the default.
if lang == "en":
return translator
if translator.load(QLocale(lang), 'nusget', '_', path): if translator.load(QLocale(lang), 'nusget', '_', path):
return translator return translator
else: else:

View File

@ -5,6 +5,9 @@ import os
import platform import platform
import subprocess import subprocess
from modules.config import update_setting
def is_dark_theme_windows(): def is_dark_theme_windows():
# This has to be here so that Python doesn't try to import it on non-Windows. # This has to be here so that Python doesn't try to import it on non-Windows.
import winreg import winreg
@ -17,6 +20,7 @@ def is_dark_theme_windows():
except Exception: except Exception:
return False return False
def is_dark_theme_macos(): def is_dark_theme_macos():
# macOS is weird. If the dark theme is on, then `defaults read -g AppleInterfaceStyle` returns "Dark". If the light # macOS is weird. If the dark theme is on, then `defaults read -g AppleInterfaceStyle` returns "Dark". If the light
# theme is on, then trying to read this key fails and returns an error instead. # theme is on, then trying to read this key fails and returns an error instead.
@ -29,6 +33,7 @@ def is_dark_theme_macos():
except Exception: except Exception:
return False return False
def is_dark_theme_linux(): def is_dark_theme_linux():
try: try:
import subprocess import subprocess
@ -43,7 +48,14 @@ def is_dark_theme_linux():
except Exception: except Exception:
return False return False
def is_dark_theme():
def is_dark_theme(config_data: dict):
# Theming priority order:
# 1. `THEME` environment variable
# 2. Theme saved in config.json
# 3. The system theme
# 4. Light, if all else fails (though personally I'd prefer dark)
#
# First, check for an environment variable overriding the theme, and use that if it exists. # First, check for an environment variable overriding the theme, and use that if it exists.
try: try:
if os.environ["THEME"].lower() == "light": if os.environ["THEME"].lower() == "light":
@ -54,7 +66,18 @@ def is_dark_theme():
print(f"Unknown theme specified: \"{os.environ['THEME']}\"") print(f"Unknown theme specified: \"{os.environ['THEME']}\"")
except KeyError: except KeyError:
pass pass
# If the theme wasn't overridden, then check the current system theme. # If the theme wasn't overridden, read the user's preference in config.json.
try:
match config_data["theme"]:
case "light":
return False
case "dark":
return True
case _:
pass
except KeyError:
pass
# If a theme wasn't set (or the theme is "system"), then check for the system theme.
system = platform.system() system = platform.system()
if system == "Windows": if system == "Windows":
return is_dark_theme_windows() return is_dark_theme_windows()
@ -62,3 +85,16 @@ def is_dark_theme():
return is_dark_theme_macos() return is_dark_theme_macos()
else: else:
return is_dark_theme_linux() return is_dark_theme_linux()
def set_theme(config_data: dict, theme: str) -> None:
match theme:
case "light":
print("setting theme to light")
update_setting(config_data, "theme", "light")
case "dark":
print("setting theme to dark")
update_setting(config_data, "theme", "dark")
case _:
print("setting theme to system (default)")
update_setting(config_data, "theme", "")

View File

@ -68,6 +68,15 @@ class Ui_MainWindow(object):
self.action_language_korean = QAction(MainWindow) self.action_language_korean = QAction(MainWindow)
self.action_language_korean.setObjectName(u"action_language_korean") self.action_language_korean.setObjectName(u"action_language_korean")
self.action_language_korean.setCheckable(True) self.action_language_korean.setCheckable(True)
self.action_theme_system = QAction(MainWindow)
self.action_theme_system.setObjectName(u"action_theme_system")
self.action_theme_system.setCheckable(True)
self.action_theme_light = QAction(MainWindow)
self.action_theme_light.setObjectName(u"action_theme_light")
self.action_theme_light.setCheckable(True)
self.action_theme_dark = QAction(MainWindow)
self.action_theme_dark.setObjectName(u"action_theme_dark")
self.action_theme_dark.setCheckable(True)
self.centralwidget = QWidget(MainWindow) self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName(u"centralwidget") self.centralwidget.setObjectName(u"centralwidget")
self.horizontalLayout_3 = QHBoxLayout(self.centralwidget) self.horizontalLayout_3 = QHBoxLayout(self.centralwidget)
@ -354,13 +363,15 @@ class Ui_MainWindow(object):
MainWindow.setCentralWidget(self.centralwidget) MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow) self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar") self.menubar.setObjectName(u"menubar")
self.menubar.setGeometry(QRect(0, 0, 1010, 30)) self.menubar.setGeometry(QRect(0, 0, 1010, 21))
self.menu_help = QMenu(self.menubar) self.menu_help = QMenu(self.menubar)
self.menu_help.setObjectName(u"menu_help") self.menu_help.setObjectName(u"menu_help")
self.menu_options = QMenu(self.menubar) self.menu_options = QMenu(self.menubar)
self.menu_options.setObjectName(u"menu_options") self.menu_options.setObjectName(u"menu_options")
self.menu_options_language = QMenu(self.menu_options) self.menu_options_language = QMenu(self.menu_options)
self.menu_options_language.setObjectName(u"menu_options_language") self.menu_options_language.setObjectName(u"menu_options_language")
self.menu_options_theme = QMenu(self.menu_options)
self.menu_options_theme.setObjectName(u"menu_options_theme")
MainWindow.setMenuBar(self.menubar) MainWindow.setMenuBar(self.menubar)
self.menubar.addAction(self.menu_options.menuAction()) self.menubar.addAction(self.menu_options.menuAction())
@ -369,6 +380,7 @@ class Ui_MainWindow(object):
self.menu_help.addAction(self.action_about_qt) self.menu_help.addAction(self.action_about_qt)
self.menu_help.addSeparator() self.menu_help.addSeparator()
self.menu_options.addAction(self.menu_options_language.menuAction()) self.menu_options.addAction(self.menu_options_language.menuAction())
self.menu_options.addAction(self.menu_options_theme.menuAction())
self.menu_options_language.addAction(self.action_language_system) self.menu_options_language.addAction(self.action_language_system)
self.menu_options_language.addAction(self.action_language_english) self.menu_options_language.addAction(self.action_language_english)
self.menu_options_language.addAction(self.action_language_spanish) self.menu_options_language.addAction(self.action_language_spanish)
@ -378,6 +390,9 @@ class Ui_MainWindow(object):
self.menu_options_language.addAction(self.action_language_norwegian) self.menu_options_language.addAction(self.action_language_norwegian)
self.menu_options_language.addAction(self.action_language_romanian) self.menu_options_language.addAction(self.action_language_romanian)
self.menu_options_language.addAction(self.action_language_korean) self.menu_options_language.addAction(self.action_language_korean)
self.menu_options_theme.addAction(self.action_theme_system)
self.menu_options_theme.addAction(self.action_theme_light)
self.menu_options_theme.addAction(self.action_theme_dark)
self.retranslateUi(MainWindow) self.retranslateUi(MainWindow)
@ -401,6 +416,9 @@ class Ui_MainWindow(object):
self.action_language_norwegian.setText(QCoreApplication.translate("MainWindow", u"Norsk", None)) self.action_language_norwegian.setText(QCoreApplication.translate("MainWindow", u"Norsk", None))
self.action_language_romanian.setText(QCoreApplication.translate("MainWindow", u"Rom\u00e2n\u0103", None)) self.action_language_romanian.setText(QCoreApplication.translate("MainWindow", u"Rom\u00e2n\u0103", None))
self.action_language_korean.setText(QCoreApplication.translate("MainWindow", u"\ud55c\uad6d\uc5b4", None)) self.action_language_korean.setText(QCoreApplication.translate("MainWindow", u"\ud55c\uad6d\uc5b4", None))
self.action_theme_system.setText(QCoreApplication.translate("MainWindow", u"System (Default)", None))
self.action_theme_light.setText(QCoreApplication.translate("MainWindow", u"Light", None))
self.action_theme_dark.setText(QCoreApplication.translate("MainWindow", u"Dark", None))
self.tree_filter_input.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Search", None)) self.tree_filter_input.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Search", None))
self.tree_filter_reset_btn.setText(QCoreApplication.translate("MainWindow", u"Clear", None)) self.tree_filter_reset_btn.setText(QCoreApplication.translate("MainWindow", u"Clear", None))
self.platform_tabs.setTabText(self.platform_tabs.indexOf(self.wii_tab), QCoreApplication.translate("MainWindow", u"Wii", None)) self.platform_tabs.setTabText(self.platform_tabs.indexOf(self.wii_tab), QCoreApplication.translate("MainWindow", u"Wii", None))
@ -427,10 +445,11 @@ class Ui_MainWindow(object):
"hr { height: 1px; border-width: 0; }\n" "hr { height: 1px; border-width: 0; }\n"
"li.unchecked::marker { content: \"\\2610\"; }\n" "li.unchecked::marker { content: \"\\2610\"; }\n"
"li.checked::marker { content: \"\\2612\"; }\n" "li.checked::marker { content: \"\\2612\"; }\n"
"</style></head><body style=\" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;\">\n" "</style></head><body style=\" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;\">\n"
"<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;\"><br /></p></body></html>", None)) "<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;\"><br /></p></body></html>", None))
self.menu_help.setTitle(QCoreApplication.translate("MainWindow", u"Help", None)) self.menu_help.setTitle(QCoreApplication.translate("MainWindow", u"Help", None))
self.menu_options.setTitle(QCoreApplication.translate("MainWindow", u"Options", None)) self.menu_options.setTitle(QCoreApplication.translate("MainWindow", u"Options", None))
self.menu_options_language.setTitle(QCoreApplication.translate("MainWindow", u"Language", None)) self.menu_options_language.setTitle(QCoreApplication.translate("MainWindow", u"Language", None))
self.menu_options_theme.setTitle(QCoreApplication.translate("MainWindow", u"Theme", None))
# retranslateUi # retranslateUi

View File

@ -435,7 +435,7 @@ p, li { white-space: pre-wrap; }
hr { height: 1px; border-width: 0; } hr { height: 1px; border-width: 0; }
li.unchecked::marker { content: &quot;\2610&quot;; } li.unchecked::marker { content: &quot;\2610&quot;; }
li.checked::marker { content: &quot;\2612&quot;; } li.checked::marker { content: &quot;\2612&quot;; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt; &lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> &lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
</widget> </widget>
@ -469,7 +469,7 @@ li.checked::marker { content: &quot;\2612&quot;; }
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1010</width> <width>1010</width>
<height>30</height> <height>21</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menu_help"> <widget class="QMenu" name="menu_help">
@ -498,7 +498,16 @@ li.checked::marker { content: &quot;\2612&quot;; }
<addaction name="action_language_romanian"/> <addaction name="action_language_romanian"/>
<addaction name="action_language_korean"/> <addaction name="action_language_korean"/>
</widget> </widget>
<widget class="QMenu" name="menu_options_theme">
<property name="title">
<string>Theme</string>
</property>
<addaction name="action_theme_system"/>
<addaction name="action_theme_light"/>
<addaction name="action_theme_dark"/>
</widget>
<addaction name="menu_options_language"/> <addaction name="menu_options_language"/>
<addaction name="menu_options_theme"/>
</widget> </widget>
<addaction name="menu_options"/> <addaction name="menu_options"/>
<addaction name="menu_help"/> <addaction name="menu_help"/>
@ -600,6 +609,30 @@ li.checked::marker { content: &quot;\2612&quot;; }
<string>한국어</string> <string>한국어</string>
</property> </property>
</action> </action>
<action name="action_theme_system">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>System (Default)</string>
</property>
</action>
<action name="action_theme_light">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Light</string>
</property>
</action>
<action name="action_theme_dark">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Dark</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>