diff --git a/NUSGet.py b/NUSGet.py index f8620a4..9fadf48 100644 --- a/NUSGet.py +++ b/NUSGet.py @@ -22,14 +22,16 @@ import sys import webbrowser from importlib.metadata import version -from PySide6.QtGui import QIcon +from PySide6.QtGui import QActionGroup, QIcon from PySide6.QtWidgets import QApplication, QMainWindow, QMessageBox, QFileDialog, QListView -from PySide6.QtCore import QRunnable, Slot, QThreadPool, Signal, QObject, QLibraryInfo, QTranslator, QLocale +from PySide6.QtCore import QRunnable, Slot, QThreadPool, Signal, QObject, QLibraryInfo from qt.py.ui_AboutDialog import AboutNUSGet from qt.py.ui_MainMenu import Ui_MainWindow +from modules.config import * from modules.core import * +from modules.language import * from modules.theme import is_dark_theme from modules.tree import NUSGetTreeModel, TIDFilterProxyModel from modules.download_batch import run_nus_download_batch @@ -81,9 +83,6 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.ui.download_btn.clicked.connect(self.download_btn_pressed) self.ui.script_btn.clicked.connect(self.script_btn_pressed) self.ui.custom_out_dir_btn.clicked.connect(self.choose_output_dir) - # About and About Qt Buttons - self.ui.actionAbout.triggered.connect(self.about_nusget) - self.ui.actionAbout_Qt.triggered.connect(lambda: QMessageBox.aboutQt(self)) self.ui.pack_archive_checkbox.toggled.connect( lambda: connect_is_enabled_to_checkbox([self.ui.archive_file_entry], self.ui.pack_archive_checkbox)) self.ui.custom_out_dir_checkbox.toggled.connect( @@ -126,10 +125,26 @@ class MainWindow(QMainWindow, Ui_MainWindow): dropdown_delegate = ComboBoxItemDelegate() self.ui.console_select_dropdown.setItemDelegate(dropdown_delegate) self.ui.console_select_dropdown.currentIndexChanged.connect(self.selected_console_changed) - # Fix the annoying background on the help menu items. - self.ui.menuHelp.setWindowFlags(self.ui.menuHelp.windowFlags() | Qt.FramelessWindowHint) - self.ui.menuHelp.setWindowFlags(self.ui.menuHelp.windowFlags() | Qt.NoDropShadowWindowHint) - self.ui.menuHelp.setAttribute(Qt.WA_TranslucentBackground) + # ------------ + # Options Menu + # ------------ + # Fix the annoying background on the option menu items and submenus. + fixup_qmenu_background(self.ui.menu_options) + fixup_qmenu_background(self.ui.menu_options_language) + # Build a QActionGroup so that the language options function like radio buttons, because selecting multiple + # languages at once makes no sense. + language_group = QActionGroup(self) + language_group.setExclusive(True) + for action in self.ui.menu_options_language.actions(): + language_group.addAction(action) + language_group.triggered.connect(lambda lang=action: set_language(config_data, lang.text())) + # --------- + # Help Menu + # --------- + # Same fix for help menu items. + fixup_qmenu_background(self.ui.menu_help) + self.ui.action_about.triggered.connect(self.about_nusget) + 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. if is_dark_theme(): bg_color = "#2b2b2b" @@ -137,8 +152,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): else: bg_color = "#e3e3e3" icon = QIcon(os.path.join(os.path.dirname(__file__), "resources", "information_black.svg")) - self.ui.actionAbout.setIcon(icon) - self.ui.actionAbout_Qt.setIcon(icon) + self.ui.action_about.setIcon(icon) + self.ui.action_about_qt.setIcon(icon) # Title tree loading code. Now powered by Models:tm: wii_model = NUSGetTreeModel(wii_database, root_name="Wii Titles") vwii_model = NUSGetTreeModel(vwii_database, root_name="vWii Titles") @@ -683,16 +698,10 @@ if __name__ == "__main__": translator = QTranslator(app) if translator.load(QLocale.system(), 'qtbase', '_', path): app.installTranslator(translator) - translator = QTranslator(app) + # Get the translation path, and call get_language() to find the appropriate translations to load based on the + # settings and system language. path = os.path.join(os.path.dirname(__file__), "resources", "translations") - # Unix-likes and Windows handle this differently, apparently. Unix-likes will try `nusget_xx_XX.qm` and then fall - # back on just `nusget_xx.qm` if the region-specific translation for the language can't be found. On Windows, no - # such fallback exists, and so this code manually implements that fallback, since for languages like Spanish NUSGet - # doesn't use region-specific translations. - locale = QLocale.system() - if not translator.load(QLocale.system(), 'nusget', '_', path): - base_locale = QLocale(locale.language()) - translator.load(base_locale, 'nusget', '_', path) + translator = get_language(QTranslator(app), config_data, path) app.installTranslator(translator) window = MainWindow() diff --git a/modules/config.py b/modules/config.py new file mode 100644 index 0000000..5b97bba --- /dev/null +++ b/modules/config.py @@ -0,0 +1,27 @@ +# "modules/config.py", licensed under the MIT license +# Copyright 2024-2025 NinjaCheetah & Contributors + +import os +import json +import pathlib + +def get_config_file() -> pathlib.Path: + config_dir = pathlib.Path(os.path.join( + os.environ.get('APPDATA') or + os.environ.get('XDG_CONFIG_HOME') or + os.path.join(os.environ['HOME'], '.config'), + "NUSGet" + )) + config_dir.mkdir(exist_ok=True) + return config_dir.joinpath("config.json") + + +def save_config(config_data: dict) -> None: + config_file = get_config_file() + print(f"writing data: {config_data}") + open(config_file, "w").write(json.dumps(config_data, indent=4)) + + +def update_setting(config_data: dict, setting: str, value: any) -> None: + config_data[setting] = value + save_config(config_data) diff --git a/modules/core.py b/modules/core.py index d149293..093b2a7 100644 --- a/modules/core.py +++ b/modules/core.py @@ -1,15 +1,12 @@ # "modules/core.py", licensed under the MIT license # Copyright 2024-2025 NinjaCheetah & Contributors -import os -import json -import pathlib import requests from dataclasses import dataclass from typing import List from PySide6.QtCore import Qt, QSize -from PySide6.QtWidgets import QStyledItemDelegate, QSizePolicy +from PySide6.QtWidgets import QStyledItemDelegate # This is required to make the dropdown look correct with the custom styling. A little fuzzy on the why, but it has to @@ -63,6 +60,13 @@ def connect_is_enabled_to_checkbox(items, chkbox): item.setEnabled(False) +def fixup_qmenu_background(menu): + # These fixes are required to not have a square background poking out from behind the rounded corners of a QMenu. + menu.setWindowFlags(menu.windowFlags() | Qt.FramelessWindowHint) + menu.setWindowFlags(menu.windowFlags() | Qt.NoDropShadowWindowHint) + menu.setAttribute(Qt.WA_TranslucentBackground) + + def check_nusget_updates(app, current_version: str, progress_callback=None) -> str | None: # Simple function to make a request to the GitHub API and then check if the latest available version is newer. gh_api_request = requests.get(url="https://api.github.com/repos/NinjaCheetah/NUSGet/releases/latest", stream=True) @@ -81,25 +85,3 @@ def check_nusget_updates(app, current_version: str, progress_callback=None) -> s return new_version progress_callback.emit(app.translate("MainWindow", "\n\nYou're running the latest release of NUSGet.")) return None - - -def get_config_file() -> pathlib.Path: - config_dir = pathlib.Path(os.path.join( - os.environ.get('APPDATA') or - os.environ.get('XDG_CONFIG_HOME') or - os.path.join(os.environ['HOME'], '.config'), - "NUSGet" - )) - config_dir.mkdir(exist_ok=True) - return config_dir.joinpath("config.json") - - -def save_config(config_data: dict) -> None: - config_file = get_config_file() - print(f"writing data: {config_data}") - open(config_file, "w").write(json.dumps(config_data)) - - -def update_setting(config_data: dict, setting: str, value: any) -> None: - config_data[setting] = value - save_config(config_data) diff --git a/modules/language.py b/modules/language.py new file mode 100644 index 0000000..c246b52 --- /dev/null +++ b/modules/language.py @@ -0,0 +1,64 @@ +# "modules/language.py", licensed under the MIT license +# Copyright 2024-2025 NinjaCheetah & Contributors + +from modules.config import update_setting + +from PySide6.QtCore import QLocale, QTranslator + + +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 + # language, but the "System (Default)" option will, so that will match the default case. + match lang: + case "English": + print("setting language to English") + update_setting(config_data, "language", "en") + case "Español": + print("setting language to Spanish") + update_setting(config_data, "language", "es") + case "Deutsch": + print("setting language to German") + update_setting(config_data, "language", "de") + case "Français": + print("setting language to French") + update_setting(config_data, "language", "fr") + case "Italiano": + print("setting language to Italian") + update_setting(config_data, "language", "it") + case "Norsk": + print("setting language to Norwegian") + update_setting(config_data, "language", "no") + case "Română": + print("setting language to Romanian") + update_setting(config_data, "language", "ro") + case "한국어": + print("setting language to Korean") + update_setting(config_data, "language", "ko") + case _: + print("setting language to system (default)") + update_setting(config_data, "language", "") + + +def get_language(translator: QTranslator, config_data: dict, path: str) -> QTranslator: + try: + lang = config_data["language"] + except KeyError: + lang = "" + # A specific language was set in the app's settings. + if lang != "": + if translator.load(QLocale(lang), 'nusget', '_', path): + return translator + else: + # If we get here, then the saved language is invalid, so clear it and run again to use the system language. + update_setting(config_data, "language", "") + return get_language(translator, config_data, path) + else: + # Unix-likes and Windows handle this differently, apparently. Unix-likes will try `nusget_xx_XX.qm` and then + # fall back on just `nusget_xx.qm` if the region-specific translation for the language can't be found. On + # Windows, no such fallback exists, and so this code manually implements that fallback, since for languages like + # Spanish NUSGet doesn't use region-specific translations. + locale = QLocale.system() + if not translator.load(QLocale.system(), 'nusget', '_', path): + base_locale = QLocale(locale.language()) + translator.load(base_locale, 'nusget', '_', path) + return translator diff --git a/qt/py/ui_MainMenu.py b/qt/py/ui_MainMenu.py index 09a3bc8..f731a80 100644 --- a/qt/py/ui_MainMenu.py +++ b/qt/py/ui_MainMenu.py @@ -31,15 +31,43 @@ class Ui_MainWindow(object): MainWindow.resize(1010, 675) MainWindow.setMinimumSize(QSize(1010, 675)) MainWindow.setMaximumSize(QSize(1010, 675)) - self.actionAbout = QAction(MainWindow) - self.actionAbout.setObjectName(u"actionAbout") + self.action_about = QAction(MainWindow) + self.action_about.setObjectName(u"action_about") icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.HelpAbout)) - self.actionAbout.setIcon(icon) - self.actionAbout.setMenuRole(QAction.MenuRole.ApplicationSpecificRole) - self.actionAbout_Qt = QAction(MainWindow) - self.actionAbout_Qt.setObjectName(u"actionAbout_Qt") - self.actionAbout_Qt.setIcon(icon) - self.actionAbout_Qt.setMenuRole(QAction.MenuRole.ApplicationSpecificRole) + self.action_about.setIcon(icon) + self.action_about.setMenuRole(QAction.MenuRole.ApplicationSpecificRole) + self.action_about_qt = QAction(MainWindow) + self.action_about_qt.setObjectName(u"action_about_qt") + self.action_about_qt.setIcon(icon) + self.action_about_qt.setMenuRole(QAction.MenuRole.ApplicationSpecificRole) + self.action_language_system = QAction(MainWindow) + self.action_language_system.setObjectName(u"action_language_system") + self.action_language_system.setCheckable(True) + self.action_language_system.setChecked(False) + self.action_language_english = QAction(MainWindow) + self.action_language_english.setObjectName(u"action_language_english") + self.action_language_english.setCheckable(True) + self.action_language_spanish = QAction(MainWindow) + self.action_language_spanish.setObjectName(u"action_language_spanish") + self.action_language_spanish.setCheckable(True) + self.action_language_german = QAction(MainWindow) + self.action_language_german.setObjectName(u"action_language_german") + self.action_language_german.setCheckable(True) + self.action_language_french = QAction(MainWindow) + self.action_language_french.setObjectName(u"action_language_french") + self.action_language_french.setCheckable(True) + self.action_language_italian = QAction(MainWindow) + self.action_language_italian.setObjectName(u"action_language_italian") + self.action_language_italian.setCheckable(True) + self.action_language_norwegian = QAction(MainWindow) + self.action_language_norwegian.setObjectName(u"action_language_norwegian") + self.action_language_norwegian.setCheckable(True) + self.action_language_romanian = QAction(MainWindow) + self.action_language_romanian.setObjectName(u"action_language_romanian") + self.action_language_romanian.setCheckable(True) + self.action_language_korean = QAction(MainWindow) + self.action_language_korean.setObjectName(u"action_language_korean") + self.action_language_korean.setCheckable(True) self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") self.horizontalLayout_3 = QHBoxLayout(self.centralwidget) @@ -327,14 +355,29 @@ class Ui_MainWindow(object): self.menubar = QMenuBar(MainWindow) self.menubar.setObjectName(u"menubar") self.menubar.setGeometry(QRect(0, 0, 1010, 30)) - self.menuHelp = QMenu(self.menubar) - self.menuHelp.setObjectName(u"menuHelp") + self.menu_help = QMenu(self.menubar) + self.menu_help.setObjectName(u"menu_help") + self.menu_options = QMenu(self.menubar) + self.menu_options.setObjectName(u"menu_options") + self.menu_options_language = QMenu(self.menu_options) + self.menu_options_language.setObjectName(u"menu_options_language") MainWindow.setMenuBar(self.menubar) - self.menubar.addAction(self.menuHelp.menuAction()) - self.menuHelp.addAction(self.actionAbout) - self.menuHelp.addAction(self.actionAbout_Qt) - self.menuHelp.addSeparator() + self.menubar.addAction(self.menu_options.menuAction()) + self.menubar.addAction(self.menu_help.menuAction()) + self.menu_help.addAction(self.action_about) + self.menu_help.addAction(self.action_about_qt) + self.menu_help.addSeparator() + self.menu_options.addAction(self.menu_options_language.menuAction()) + 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_spanish) + self.menu_options_language.addAction(self.action_language_german) + self.menu_options_language.addAction(self.action_language_french) + self.menu_options_language.addAction(self.action_language_italian) + 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_korean) self.retranslateUi(MainWindow) @@ -347,8 +390,17 @@ class Ui_MainWindow(object): def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) - self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"About NUSGet", None)) - self.actionAbout_Qt.setText(QCoreApplication.translate("MainWindow", u"About Qt", None)) + self.action_about.setText(QCoreApplication.translate("MainWindow", u"About NUSGet", None)) + self.action_about_qt.setText(QCoreApplication.translate("MainWindow", u"About Qt", None)) + self.action_language_system.setText(QCoreApplication.translate("MainWindow", u"System (Default)", None)) + self.action_language_english.setText(QCoreApplication.translate("MainWindow", u"English", None)) + self.action_language_spanish.setText(QCoreApplication.translate("MainWindow", u"Espa\u00f1ol", None)) + self.action_language_german.setText(QCoreApplication.translate("MainWindow", u"Deutsch", None)) + self.action_language_french.setText(QCoreApplication.translate("MainWindow", u"Fran\u00e7ais", None)) + self.action_language_italian.setText(QCoreApplication.translate("MainWindow", u"Italiano", 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_korean.setText(QCoreApplication.translate("MainWindow", u"\ud55c\uad6d\uc5b4", 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.platform_tabs.setTabText(self.platform_tabs.indexOf(self.wii_tab), QCoreApplication.translate("MainWindow", u"Wii", None)) @@ -377,6 +429,8 @@ class Ui_MainWindow(object): "li.checked::marker { content: \"\\2612\"; }\n" "
\n" "