From a7ec3d5a774ffd44a4f433811ce3a10c1e35d905 Mon Sep 17 00:00:00 2001 From: Daniel Freak Date: Fri, 31 Jul 2026 12:14:21 +0300 Subject: [PATCH] [GUI] Add inline per-game settings and unify options styling (#728) * [GUI] Add inline per-game settings and unify options styling * [GUI] Update options styling & add scrollable area * [GUI] Remove focus and pointover styles for options --- src/SharpEmu.GUI/App.axaml | 2 + src/SharpEmu.GUI/Languages/ar.json | 9 +- src/SharpEmu.GUI/Languages/br.json | 9 +- src/SharpEmu.GUI/Languages/de.json | 9 +- src/SharpEmu.GUI/Languages/dk.json | 9 +- src/SharpEmu.GUI/Languages/en.json | 11 +- src/SharpEmu.GUI/Languages/es.json | 9 +- src/SharpEmu.GUI/Languages/fr.json | 9 +- src/SharpEmu.GUI/Languages/hu.json | 9 +- src/SharpEmu.GUI/Languages/it.json | 9 +- src/SharpEmu.GUI/Languages/ja.json | 9 +- src/SharpEmu.GUI/Languages/ko.json | 9 +- src/SharpEmu.GUI/Languages/nl.json | 9 +- src/SharpEmu.GUI/Languages/pt.json | 9 +- src/SharpEmu.GUI/Languages/ru.json | 11 +- src/SharpEmu.GUI/Languages/tr.json | 11 +- src/SharpEmu.GUI/MainWindow.GameOptions.cs | 493 ++++++++++++ src/SharpEmu.GUI/MainWindow.axaml | 700 ++++++++++++++++-- src/SharpEmu.GUI/MainWindow.axaml.cs | 150 ++-- src/SharpEmu.GUI/PerGameSettings.cs | 97 +++ src/SharpEmu.GUI/PerGameSettingsDialog.cs | 426 ----------- src/SharpEmu.GUI/SettingRow.cs | 38 +- .../Themes/Styles/GameOptions.axaml | 156 ++++ src/SharpEmu.GUI/Themes/Styles/Options.axaml | 103 +-- .../Themes/Styles/OptionsNav.axaml | 100 +++ .../Themes/Templates/SettingRow.axaml | 30 +- src/SharpEmu.GUI/Themes/Tokens.axaml | 36 +- .../GUI/PerGameSettingsTests.cs | 82 ++ 28 files changed, 1782 insertions(+), 772 deletions(-) create mode 100644 src/SharpEmu.GUI/MainWindow.GameOptions.cs delete mode 100644 src/SharpEmu.GUI/PerGameSettingsDialog.cs create mode 100644 src/SharpEmu.GUI/Themes/Styles/GameOptions.axaml create mode 100644 src/SharpEmu.GUI/Themes/Styles/OptionsNav.axaml diff --git a/src/SharpEmu.GUI/App.axaml b/src/SharpEmu.GUI/App.axaml index 2af46780..3d6220d2 100644 --- a/src/SharpEmu.GUI/App.axaml +++ b/src/SharpEmu.GUI/App.axaml @@ -31,6 +31,8 @@ cascade order so individual launcher views do not redefine global visuals. + + diff --git a/src/SharpEmu.GUI/Languages/ar.json b/src/SharpEmu.GUI/Languages/ar.json index 519e7ef6..a520dc74 100644 --- a/src/SharpEmu.GUI/Languages/ar.json +++ b/src/SharpEmu.GUI/Languages/ar.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ إضافة مجلد ألعاب", "Library.Loading": "جارٍ تحميل المكتبة...", + "Library.Stat.Version": "الإصدار", + "Library.Stat.Installed": "المثبت", + "Library.Stat.TitleId": "معرّف اللعبة", + "Common.Back": "رجوع", "Options.General": "عام", + "Options.Logging": "التسجيل", "Options.Section.Emulation": "المحاكاة", "Options.Section.Logging": "التسجيل", "Options.Section.Launcher": "المُشغِّل", @@ -143,10 +148,6 @@ "Options.Env.GuestImageCpuSync.Desc": "إعادة رفع أسطح الضيف التي تعيد كتابتها شيفرة المعالج الخاصة باللعبة.\nاتركه مغلقًا عادة. شغّله للألعاب التي لا تصل أسطحها المرسومة بالمعالج إلى الشاشة.\nيكلّف أداءً ويسبب مشاكل في بعض الألعاب مثل GTA V.", "Common.Save": "حفظ", "Common.Cancel": "إلغاء", - "PerGame.Title": "إعدادات خاصة باللعبة — {0} ({1})", - "PerGame.InheritNote": "الصفوف غير المحددة ترث الإعدادات الافتراضية العامة.", - "PerGame.EnvToggles.Label": "مفاتيح البيئة", - "PerGame.EnvToggles.Desc": "تجاوز المجموعة العامة من مفاتيح ‎SHARPEMU_*‎ لهذه اللعبة.", "Options.About": "حول", "About.Github.Label": "GitHub", "About.Github.Desc": "الكود المصدري والمشكلات وتطوير المشروع.", diff --git a/src/SharpEmu.GUI/Languages/br.json b/src/SharpEmu.GUI/Languages/br.json index 9938ab67..3cb60d89 100644 --- a/src/SharpEmu.GUI/Languages/br.json +++ b/src/SharpEmu.GUI/Languages/br.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ Adicionar pasta do jogo", "Library.Loading": "Carregando biblioteca…", + "Library.Stat.Version": "Versão", + "Library.Stat.Installed": "Instalado", + "Library.Stat.TitleId": "ID do título", + "Common.Back": "Voltar", "Options.General": "Opções Gerais", + "Options.Logging": "Logs", "Options.Env.Tab": "Ambiente", "Options.Section.Environment": "VARIÁVEIS DE AMBIENTE", "Options.Env.Desc": "Variáveis de ambiente passadas ao emulador durante a inicialização.", @@ -151,10 +156,6 @@ "Options.Env.LogIo.Desc": "Registra no console a abertura e leitura de arquivos e a resolução de caminhos.\nUse quando um jogo não encontrar seus arquivos de dados durante a inicialização.", "Common.Save": "Salvar", "Common.Cancel": "Cancelar", - "PerGame.Title": "Configurações por jogo — {0} ({1})", - "PerGame.InheritNote": "As linhas desmarcadas herdam os padrões globais.", - "PerGame.EnvToggles.Label": "Variáveis de ambiente", - "PerGame.EnvToggles.Desc": "Substitui o conjunto global de opções SHARPEMU_* para este jogo.", "About.Github.LatestCommitLabel": "Último commit", "About.Github.LatestCommitDescription": "Último commit na branch main", "Updater.Auto.Label": "Verificar atualizações ao iniciar", diff --git a/src/SharpEmu.GUI/Languages/de.json b/src/SharpEmu.GUI/Languages/de.json index 481ab6a1..31d99967 100644 --- a/src/SharpEmu.GUI/Languages/de.json +++ b/src/SharpEmu.GUI/Languages/de.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ Spielordner hinzufügen", "Library.Loading": "Bibliothek wird geladen…", + "Library.Stat.Version": "Version", + "Library.Stat.Installed": "Installiert", + "Library.Stat.TitleId": "Titel-ID", + "Common.Back": "Zurück", "Options.General": "Allgemein", + "Options.Logging": "Protokollierung", "Options.Section.Emulation": "EMULATION", "Options.Section.Logging": "PROTOKOLLIERUNG", "Options.Section.Launcher": "LAUNCHER", @@ -143,10 +148,6 @@ "Options.Env.GuestImageCpuSync.Desc": "Gast-Oberflächen neu hochladen, die der eigene CPU-Code des Spiels überschreibt.\nNormalerweise aus lassen. Für Titel aktivieren, deren CPU-gezeichnete Oberflächen nie auf dem Bildschirm erscheinen.\nKostet Leistung und verursacht bei einigen Titeln wie GTA V Regressionen.", "Common.Save": "Speichern", "Common.Cancel": "Abbrechen", - "PerGame.Title": "Spielspezifische Einstellungen — {0} ({1})", - "PerGame.InheritNote": "Nicht angehakte Zeilen übernehmen die globalen Standardwerte.", - "PerGame.EnvToggles.Label": "Umgebungsschalter", - "PerGame.EnvToggles.Desc": "Die globalen SHARPEMU_*-Schalter für dieses Spiel überschreiben.", "Options.About": "Über", "About.Github.Label": "GitHub", "About.Github.Desc": "Quellcode, Issues und Projektentwicklung.", diff --git a/src/SharpEmu.GUI/Languages/dk.json b/src/SharpEmu.GUI/Languages/dk.json index a5ad45d7..b04cf078 100644 --- a/src/SharpEmu.GUI/Languages/dk.json +++ b/src/SharpEmu.GUI/Languages/dk.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ Tilføj spilmappe", "Library.Loading": "Indlæser bibliotek…", + "Library.Stat.Version": "Version", + "Library.Stat.Installed": "Installeret", + "Library.Stat.TitleId": "Titel-id", + "Common.Back": "Tilbage", "Options.General": "Generelt", + "Options.Logging": "Logging", "Options.Section.Emulation": "EMULERING", "Options.Section.Logging": "LOGGING", "Options.Section.Launcher": "LAUNCHER", @@ -143,10 +148,6 @@ "Options.Env.GuestImageCpuSync.Desc": "Genindlæs gæsteoverflader, som spillets egen CPU-kode omskriver.\nLad den være slået fra normalt. Slå til for titler, hvis CPU-tegnede overflader aldrig når skærmen.\nKoster ydeevne og giver regressioner i nogle titler, såsom GTA V.", "Common.Save": "Gem", "Common.Cancel": "Annuller", - "PerGame.Title": "Indstillinger pr. spil — {0} ({1})", - "PerGame.InheritNote": "Umarkerede rækker arver de globale standardværdier.", - "PerGame.EnvToggles.Label": "Miljøkontakter", - "PerGame.EnvToggles.Desc": "Tilsidesæt det globale sæt SHARPEMU_*-kontakter for dette spil.", "Options.About": "Om", "About.Github.Label": "GitHub", "About.Github.Desc": "Kildekode, issues og projektudvikling.", diff --git a/src/SharpEmu.GUI/Languages/en.json b/src/SharpEmu.GUI/Languages/en.json index f734c547..30bc6734 100644 --- a/src/SharpEmu.GUI/Languages/en.json +++ b/src/SharpEmu.GUI/Languages/en.json @@ -24,8 +24,13 @@ "Library.Empty.AddFolder": "+ Add game folder", "Library.Loading": "Loading library…", + "Library.Stat.Version": "Version", + "Library.Stat.Installed": "Installed", + "Library.Stat.TitleId": "Title ID", + "Common.Back": "Back", "Options.General": "General", + "Options.Logging": "Logging", "Options.Env.Tab": "Environment", "Options.Section.Environment": "ENVIRONMENT VARIABLES", "Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.", @@ -117,12 +122,6 @@ "Common.Save": "Save", "Common.Cancel": "Cancel", - "PerGame.Title": "Per-game settings — {0} ({1})", - "PerGame.InheritNote": "Unchecked rows inherit the global defaults.", - "PerGame.Tab.General": "General", - "PerGame.Tab.Graphics": "Graphics", - "PerGame.EnvToggles.Label": "Environment toggles", - "PerGame.EnvToggles.Desc": "Override the global set of SHARPEMU_* switches for this game.", "Console.Title": "CONSOLE", "Console.SearchWatermark": "Search...", diff --git a/src/SharpEmu.GUI/Languages/es.json b/src/SharpEmu.GUI/Languages/es.json index 8838260e..9aa3bd4a 100644 --- a/src/SharpEmu.GUI/Languages/es.json +++ b/src/SharpEmu.GUI/Languages/es.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ Añadir carpeta de juegos", "Library.Loading": "Cargando biblioteca…", + "Library.Stat.Version": "Versión", + "Library.Stat.Installed": "Instalado", + "Library.Stat.TitleId": "ID del título", + "Common.Back": "Volver", "Options.General": "General", + "Options.Logging": "Logs", "Options.Section.Emulation": "EMULACIÓN", "Options.Section.Logging": "LOGS", "Options.Section.Launcher": "LAUNCHER", @@ -153,10 +158,6 @@ "Options.Env.GuestImageCpuSync.Desc": "Volver a subir las superficies del invitado que reescribe el propio código de CPU del juego.\nDejar desactivado normalmente. Activar en títulos cuyas superficies dibujadas por CPU nunca llegan a la pantalla.\nCuesta rendimiento y causa regresiones en algunos títulos, como GTA V.", "Common.Save": "Guardar", "Common.Cancel": "Cancelar", - "PerGame.Title": "Ajustes por juego — {0} ({1})", - "PerGame.InheritNote": "Las filas sin marcar heredan los valores globales.", - "PerGame.EnvToggles.Label": "Variables de entorno", - "PerGame.EnvToggles.Desc": "Sustituir el conjunto global de opciones SHARPEMU_* para este juego.", "Updater.Auto.Label": "Buscar actualizaciones al iniciar", "Updater.Auto.Desc": "Consulta GitHub sin retrasar el arranque.", "Updater.Label": "Actualizaciones", diff --git a/src/SharpEmu.GUI/Languages/fr.json b/src/SharpEmu.GUI/Languages/fr.json index 8d753f24..a6e8caa0 100644 --- a/src/SharpEmu.GUI/Languages/fr.json +++ b/src/SharpEmu.GUI/Languages/fr.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ Ajouter un dossier de jeux", "Library.Loading": "Chargement de la bibliothèque…", + "Library.Stat.Version": "Version", + "Library.Stat.Installed": "Installé", + "Library.Stat.TitleId": "ID du titre", + "Common.Back": "Retour", "Options.General": "Général", + "Options.Logging": "Journalisation", "Options.Env.Tab": "Environnement", "Options.Section.Environment": "VARIABLES D’ENVIRONNEMENT", "Options.Env.Desc": "Paramètres passés à l’émulateur comme variables d’environnement au lancement.", @@ -151,10 +156,6 @@ "Options.Env.LogIo.Desc": "Journaliser l’ouverture et la lecture des fichiers ainsi que la résolution des chemins dans la console.\nÀ utiliser quand un jeu ne trouve pas ses fichiers de données au démarrage.", "Common.Save": "Enregistrer", "Common.Cancel": "Annuler", - "PerGame.Title": "Paramètres par jeu — {0} ({1})", - "PerGame.InheritNote": "Les lignes non cochées héritent des valeurs globales par défaut.", - "PerGame.EnvToggles.Label": "Variables d’environnement", - "PerGame.EnvToggles.Desc": "Remplacer l’ensemble global des options SHARPEMU_* pour ce jeu.", "About.Github.LatestCommitLabel": "Dernier commit", "About.Github.LatestCommitDescription": "Dernier commit sur la branche main", "Updater.Auto.Label": "Vérifier les mises à jour au démarrage", diff --git a/src/SharpEmu.GUI/Languages/hu.json b/src/SharpEmu.GUI/Languages/hu.json index 59e807af..d3ac437e 100644 --- a/src/SharpEmu.GUI/Languages/hu.json +++ b/src/SharpEmu.GUI/Languages/hu.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ Játékmappa hozzáadása", "Library.Loading": "Könyvtár betöltése", + "Library.Stat.Version": "Verzió", + "Library.Stat.Installed": "Telepítve", + "Library.Stat.TitleId": "Címazonosító", + "Common.Back": "Vissza", "Options.General": "Általános", + "Options.Logging": "Logolás", "Options.Env.Tab": "Környezet", "Options.Section.Environment": "KÖRNYEZETI VÁLTOZÓK", "Options.Env.Desc": "Indításkor környezeti változóként az emulátorhoz átadott kapcsolók.", @@ -151,10 +156,6 @@ "Options.Env.LogIo.Desc": "A fájlmegnyitások, olvasások és útvonal-feloldások naplózása a konzolra.\nAkkor használd, ha egy játék indításkor nem találja az adatfájljait.", "Common.Save": "Mentés", "Common.Cancel": "Mégse", - "PerGame.Title": "Játékonkénti beállítások — {0} ({1})", - "PerGame.InheritNote": "A be nem jelölt sorok a globális alapértelmezéseket öröklik.", - "PerGame.EnvToggles.Label": "Környezeti kapcsolók", - "PerGame.EnvToggles.Desc": "A globális SHARPEMU_* kapcsolókészlet felülírása ennél a játéknál.", "About.Github.LatestCommitLabel": "Legutóbbi commit", "About.Github.LatestCommitDescription": "A main ág legutóbbi commitja", "Updater.Auto.Label": "Frissítések keresése indításkor", diff --git a/src/SharpEmu.GUI/Languages/it.json b/src/SharpEmu.GUI/Languages/it.json index 54b8852d..7c13436d 100644 --- a/src/SharpEmu.GUI/Languages/it.json +++ b/src/SharpEmu.GUI/Languages/it.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ Aggiungi cartella giochi", "Library.Loading": "Caricamento libreria…", + "Library.Stat.Version": "Versione", + "Library.Stat.Installed": "Installato", + "Library.Stat.TitleId": "ID titolo", + "Common.Back": "Indietro", "Options.General": "Generale", + "Options.Logging": "Log", "Options.Section.Emulation": "EMULAZIONE", "Options.Section.Logging": "LOG", "Options.Section.Launcher": "LAUNCHER", @@ -148,10 +153,6 @@ "Options.Env.GuestImageCpuSync.Desc": "Ricarica le superfici guest riscritte dal codice CPU del gioco.\nLasciare disattivato normalmente. Attivare per i titoli le cui superfici disegnate dalla CPU non raggiungono mai lo schermo.\nCosta prestazioni e causa regressioni in alcuni titoli, come GTA V.", "Common.Save": "Salva", "Common.Cancel": "Annulla", - "PerGame.Title": "Impostazioni per gioco — {0} ({1})", - "PerGame.InheritNote": "Le righe non selezionate ereditano i valori globali.", - "PerGame.EnvToggles.Label": "Variabili d'ambiente", - "PerGame.EnvToggles.Desc": "Sovrascrivi l'insieme globale delle opzioni SHARPEMU_* per questo gioco.", "Options.About": "Informazioni", "About.Github.Label": "GitHub", "About.Github.Desc": "Codice sorgente, issue e sviluppo del progetto.", diff --git a/src/SharpEmu.GUI/Languages/ja.json b/src/SharpEmu.GUI/Languages/ja.json index c8e7c377..99e9677f 100644 --- a/src/SharpEmu.GUI/Languages/ja.json +++ b/src/SharpEmu.GUI/Languages/ja.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ ゲームフォルダーを追加", "Library.Loading": "ライブラリを読み込み中…", + "Library.Stat.Version": "バージョン", + "Library.Stat.Installed": "インストール済み", + "Library.Stat.TitleId": "タイトルID", + "Common.Back": "戻る", "Options.General": "一般", + "Options.Logging": "ロギング", "Options.Section.Emulation": "エミュレーション", "Options.Section.Logging": "ロギング", "Options.Section.Launcher": "ランチャー", @@ -143,10 +148,6 @@ "Options.Env.GuestImageCpuSync.Desc": "ゲーム自身の CPU コードが書き換えるゲスト表面を再アップロードします。\n通常はオフのままにしてください。CPU で描画した表面が画面に反映されないタイトルで有効にします。\n性能を犠牲にし、GTA V など一部のタイトルでは不具合が生じます。", "Common.Save": "保存", "Common.Cancel": "キャンセル", - "PerGame.Title": "ゲームごとの設定 — {0} ({1})", - "PerGame.InheritNote": "チェックされていない行はグローバルの既定値を継承します。", - "PerGame.EnvToggles.Label": "環境スイッチ", - "PerGame.EnvToggles.Desc": "このゲームに対してグローバルのSHARPEMU_*スイッチを上書きします。", "Options.About": "情報", "About.Github.Label": "GitHub", "About.Github.Desc": "ソースコード、Issue、プロジェクトの開発。", diff --git a/src/SharpEmu.GUI/Languages/ko.json b/src/SharpEmu.GUI/Languages/ko.json index 05f086bc..8c1a4917 100644 --- a/src/SharpEmu.GUI/Languages/ko.json +++ b/src/SharpEmu.GUI/Languages/ko.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ 게임 폴더 추가", "Library.Loading": "라이브러리 불러오는 중…", + "Library.Stat.Version": "버전", + "Library.Stat.Installed": "설치됨", + "Library.Stat.TitleId": "타이틀 ID", + "Common.Back": "뒤로", "Options.General": "일반", + "Options.Logging": "로깅", "Options.Section.Emulation": "에뮬레이션", "Options.Section.Logging": "로깅", "Options.Section.Launcher": "런처", @@ -143,10 +148,6 @@ "Options.Env.GuestImageCpuSync.Desc": "게임의 자체 CPU 코드가 다시 쓰는 게스트 표면을 다시 업로드합니다.\n평소에는 꺼 두세요. CPU로 그린 표면이 화면에 나타나지 않는 타이틀에서 켜세요.\n성능을 소모하며 GTA V 등 일부 타이틀에서는 문제가 생깁니다.", "Common.Save": "저장", "Common.Cancel": "취소", - "PerGame.Title": "게임별 설정 — {0} ({1})", - "PerGame.InheritNote": "선택하지 않은 항목은 전역 기본값을 따릅니다.", - "PerGame.EnvToggles.Label": "환경 스위치", - "PerGame.EnvToggles.Desc": "이 게임에 대해 전역 SHARPEMU_* 스위치 설정을 재정의합니다.", "Options.About": "정보", "About.Github.Label": "GitHub", "About.Github.Desc": "소스 코드, 이슈, 프로젝트 개발.", diff --git a/src/SharpEmu.GUI/Languages/nl.json b/src/SharpEmu.GUI/Languages/nl.json index 32653d3b..8d139f5e 100644 --- a/src/SharpEmu.GUI/Languages/nl.json +++ b/src/SharpEmu.GUI/Languages/nl.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ Gamemap toevoegen", "Library.Loading": "Bibliotheek laden…", + "Library.Stat.Version": "Versie", + "Library.Stat.Installed": "Geïnstalleerd", + "Library.Stat.TitleId": "Titel-ID", + "Common.Back": "Terug", "Options.General": "Algemeen", + "Options.Logging": "Logging", "Options.Section.Emulation": "EMULATIE", "Options.Section.Logging": "LOGGING", "Options.Section.Launcher": "LAUNCHER", @@ -143,10 +148,6 @@ "Options.Env.GuestImageCpuSync.Desc": "Gastoppervlakken opnieuw uploaden die de eigen CPU-code van de game herschrijft.\nNormaal uit laten. Inschakelen voor titels waarvan de door de CPU getekende oppervlakken nooit het scherm bereiken.\nKost prestaties en veroorzaakt regressies in sommige titels, zoals GTA V.", "Common.Save": "Opslaan", "Common.Cancel": "Annuleren", - "PerGame.Title": "Instellingen per game — {0} ({1})", - "PerGame.InheritNote": "Niet-aangevinkte rijen erven de globale standaardwaarden.", - "PerGame.EnvToggles.Label": "Omgevingsschakelaars", - "PerGame.EnvToggles.Desc": "Overschrijf de globale set SHARPEMU_*-schakelaars voor deze game.", "Options.About": "Over", "About.Github.Label": "GitHub", "About.Github.Desc": "Broncode, issues en projectontwikkeling.", diff --git a/src/SharpEmu.GUI/Languages/pt.json b/src/SharpEmu.GUI/Languages/pt.json index 838bd72f..b9cbe732 100644 --- a/src/SharpEmu.GUI/Languages/pt.json +++ b/src/SharpEmu.GUI/Languages/pt.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ Adicionar pasta de jogos", "Library.Loading": "A carregar biblioteca…", + "Library.Stat.Version": "Versão", + "Library.Stat.Installed": "Instalado", + "Library.Stat.TitleId": "ID do título", + "Common.Back": "Voltar", "Options.General": "Geral", + "Options.Logging": "Registos", "Options.Env.Tab": "Ambiente", "Options.Section.Environment": "VARIÁVEIS DE AMBIENTE", "Options.Env.Desc": "Switches passados ao emulador como variáveis de ambiente no arranque.", @@ -151,10 +156,6 @@ "Options.Env.LogIo.Desc": "Registar na consola a abertura e leitura de ficheiros e a resolução de caminhos.\nUtilize quando um jogo não encontrar os seus ficheiros de dados durante o arranque.", "Common.Save": "Guardar", "Common.Cancel": "Cancelar", - "PerGame.Title": "Definições por jogo — {0} ({1})", - "PerGame.InheritNote": "As linhas não assinaladas herdam as predefinições globais.", - "PerGame.EnvToggles.Label": "Variáveis de ambiente", - "PerGame.EnvToggles.Desc": "Substituir o conjunto global de opções SHARPEMU_* para este jogo.", "About.Github.LatestCommitLabel": "Último commit", "About.Github.LatestCommitDescription": "Último commit no ramo main", "Updater.Auto.Label": "Procurar atualizações no arranque", diff --git a/src/SharpEmu.GUI/Languages/ru.json b/src/SharpEmu.GUI/Languages/ru.json index 79d1858c..4d3dffe6 100644 --- a/src/SharpEmu.GUI/Languages/ru.json +++ b/src/SharpEmu.GUI/Languages/ru.json @@ -24,8 +24,13 @@ "Library.Empty.AddFolder": "+ Добавить папку с играми", "Library.Loading": "Загрузка библиотеки…", + "Library.Stat.Version": "Версия", + "Library.Stat.Installed": "Установлено", + "Library.Stat.TitleId": "ID игры", + "Common.Back": "Назад", "Options.General": "Основные", + "Options.Logging": "Логгирование", "Options.Env.Tab": "Окружение", "Options.Section.Environment": "ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ", "Options.Env.Desc": "Параметры, передаваемые эмулятору как переменные окружения при запуске.", @@ -117,12 +122,6 @@ "Common.Save": "Сохранить", "Common.Cancel": "Отмена", - "PerGame.Title": "Настройки игры — {0} ({1})", - "PerGame.InheritNote": "Неотмеченные строки наследуют глобальные настройки.", - "PerGame.Tab.General": "Основные", - "PerGame.Tab.Graphics": "Графика", - "PerGame.EnvToggles.Label": "Переключатели окружения", - "PerGame.EnvToggles.Desc": "Переопределить глобальный набор переключателей SHARPEMU_* для этой игры.", "Console.Title": "КОНСОЛЬ", "Console.SearchWatermark": "Поиск...", diff --git a/src/SharpEmu.GUI/Languages/tr.json b/src/SharpEmu.GUI/Languages/tr.json index c7c96ef9..ad074051 100644 --- a/src/SharpEmu.GUI/Languages/tr.json +++ b/src/SharpEmu.GUI/Languages/tr.json @@ -23,8 +23,13 @@ "Library.Empty.AddFolder": "+ Oyun klasörü ekle", "Library.Loading": "Kütüphane yükleniyor…", + "Library.Stat.Version": "Sürüm", + "Library.Stat.Installed": "Yüklü", + "Library.Stat.TitleId": "Başlık kimliği", + "Common.Back": "Geri", "Options.General": "Genel", + "Options.Logging": "Günlükleme", "Options.Section.Emulation": "EMÜLASYON", "Options.Section.Logging": "GÜNLÜKLEME", "Options.Section.Launcher": "BAŞLATICI", @@ -177,12 +182,6 @@ "Options.DefaultProfile.Desc": "Oyun metin girisi istediginde kullanilacak ad. Varsayilan deger Sharp'tir.", "Common.Save": "Kaydet", "Common.Cancel": "İptal", - "PerGame.Title": "Oyuna özel ayarlar — {0} ({1})", - "PerGame.InheritNote": "İşaretlenmemiş satırlar genel varsayılanları kullanır.", - "PerGame.Tab.General": "Genel", - "PerGame.Tab.Graphics": "Grafik", - "PerGame.EnvToggles.Label": "Ortam anahtarları", - "PerGame.EnvToggles.Desc": "Bu oyun için genel SHARPEMU_* anahtar kümesini geçersiz kıl.", "Options.About": "Hakkında", "About.Github.Label": "GitHub", "About.Github.Desc": "Kaynak kodu, hata kayıtları ve proje geliştirme.", diff --git a/src/SharpEmu.GUI/MainWindow.GameOptions.cs b/src/SharpEmu.GUI/MainWindow.GameOptions.cs new file mode 100644 index 00000000..ac0f6e95 --- /dev/null +++ b/src/SharpEmu.GUI/MainWindow.GameOptions.cs @@ -0,0 +1,493 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using Avalonia; +using Avalonia.Controls; +using Avalonia.VisualTree; +using SharpEmu.Libs.VideoOut; + +namespace SharpEmu.GUI; + +/// Inline per-game settings navigation, loading and persistence. +public partial class MainWindow +{ + private static readonly string[] GameEnvironmentToggleNames = + [ + "SHARPEMU_BTHID_UNAVAILABLE", + "SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", + "SHARPEMU_WRITABLE_APP0", + "SHARPEMU_VK_VALIDATION", + "SHARPEMU_DUMP_SPIRV", + "SHARPEMU_LOG_DIRECT_MEMORY", + "SHARPEMU_LOG_IO", + "SHARPEMU_LOG_NP", + "SHARPEMU_GUEST_IMAGE_CPU_SYNC", + ]; + + private readonly List _gameEnvironmentPassthrough = new(); + private IReadOnlyList _gameHostDisplays = []; + private bool _isGameSettingsOpen; + private bool _isLoadingGameSettings; + private bool _updatingGameHostDisplayOptions; + private int _gameOptionsIndicatorIndex; + private int _gameOptionsSectionIndex; + private string? _gameSettingsTitleId; + + private void WireGameOptions() + { + GameSettingsButton.Click += (_, _) => OpenSelectedGameSettings(); + + GameLogLevelBox.ItemsSource = _logLevelChoices; + GameWindowModeBox.ItemsSource = _windowModeChoices; + GameScalingModeBox.ItemsSource = _scalingModeChoices; + GameHdrModeBox.ItemsSource = _hdrModeChoices; + + var navigationButtons = GameOptionsNavigationButtons(); + for (var index = 0; index < navigationButtons.Length; index++) + { + var section = index; + navigationButtons[index].Click += (_, _) => + { + if (section < GameOptionsSectionPanels().Length) + { + SetGameOptionsSection(section); + } + else + { + CloseGameSettings(); + } + }; + navigationButtons[index].PointerEntered += (_, _) => + { + if (_isGameSettingsOpen) + { + SetGameOptionsNavigationIndicator(section); + } + }; + navigationButtons[index].GotFocus += (_, _) => + { + if (_isGameSettingsOpen) + { + SetGameOptionsNavigationIndicator(section); + } + }; + } + + GameOptionsNavHost.PointerExited += (_, _) => + { + if (_isGameSettingsOpen) + { + SetGameOptionsNavigationIndicator(_gameOptionsSectionIndex); + } + }; + + GameOptionsLaunchButton.Click += (_, _) => + { + CloseGameSettings(); + LaunchSelected(); + }; + GameOptionsOpenFolderButton.Click += (_, _) => OpenSelectedGameFolder(); + GameOptionsCopyPathButton.Click += async (_, _) => + await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path); + GameOptionsCopyTitleIdButton.Click += async (_, _) => + await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId); + GameOptionsRemoveButton.Click += (_, _) => + { + CloseGameSettings(); + RemoveSelectedFromLibrary(); + }; + + GameStrictToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings(); + GameLogLevelBox.SelectionChanged += (_, _) => PersistOpenGameSettings(); + GameTraceImportsBox.ValueChanged += (_, _) => PersistOpenGameSettings(); + GameLogToFileToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings(); + GameWindowModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings(); + GameDisplayBox.SelectionChanged += (_, _) => OnGameHostDisplayChanged(); + GameResolutionBox.SelectionChanged += (_, _) => OnGameHostResolutionChanged(); + GameRefreshRateBox.SelectionChanged += (_, _) => PersistOpenGameSettings(); + GameScalingModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings(); + GameVSyncToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings(); + GameHdrModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings(); + foreach (var (_, toggle) in GameEnvironmentToggles()) + { + toggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings(); + } + + SetGameOptionsSection(0, animateIndicator: false); + } + + private void OpenSelectedGameSettings() + { + if (GameList.SelectedItem is not GameEntry game) + { + return; + } + + if (string.IsNullOrWhiteSpace(game.TitleId)) + { + AppendConsoleLine( + "[GUI][WARN] Per-game settings require a title ID, which this game does not have.", + WarningLineBrush); + return; + } + + _gameSettingsTitleId = game.TitleId; + GameOptionsOverlay.DataContext = game; + LoadGameSettings(game.TitleId); + SetGameOptionsSection(0, animateIndicator: false); + GameOptionsLaunchButton.IsEnabled = !_isRunning; + GameOptionsCopyTitleIdButton.IsEnabled = + !string.IsNullOrWhiteSpace(game.TitleId); + + _isGameSettingsOpen = true; + SetGameOptionsOpenClass(BackdropLayer, active: true); + SetGameOptionsOpenClass(CarouselHost, active: true); + SetGameOptionsOpenClass(LibrarySelectedDetails, active: true); + SetGameOptionsOpenClass(GameOptionsOverlay, active: true); + GameList.IsHitTestVisible = false; + LibraryToolbar.IsHitTestVisible = false; + GameOptionsOverlay.IsHitTestVisible = true; + GameOptionsGeneralNav.Focus(); + } + + private void CloseGameSettings(bool restoreLibrary = true) + { + if (!_isGameSettingsOpen) + { + return; + } + + _isGameSettingsOpen = false; + SetGameOptionsNavigationIndicator(_gameOptionsIndicatorIndex, animate: false); + _gameSettingsTitleId = null; + _gameEnvironmentPassthrough.Clear(); + SetGameOptionsOpenClass(BackdropLayer, active: false); + SetGameOptionsOpenClass(CarouselHost, active: false); + SetGameOptionsOpenClass(LibrarySelectedDetails, active: false); + SetGameOptionsOpenClass(GameOptionsOverlay, active: false); + GameOptionsOverlay.IsHitTestVisible = false; + GameOptionsOverlay.DataContext = null; + GameList.IsHitTestVisible = true; + LibraryToolbar.IsHitTestVisible = true; + + if (restoreLibrary && _activePageIndex == 0) + { + GameList.Focus(); + } + } + + private void LoadGameSettings(string titleId) + { + var effective = EffectiveLaunchSettings.Resolve( + _settings, + PerGameSettings.Load(titleId)); + + _isLoadingGameSettings = true; + _updatingGameHostDisplayOptions = true; + try + { + GameStrictToggle.IsChecked = effective.StrictDynlibResolution; + GameLogLevelBox.SelectedItem = FindChoice( + _logLevelChoices, + effective.LogLevel, + "Info"); + GameTraceImportsBox.Value = Math.Clamp(effective.ImportTraceLimit, 0, 4096); + GameLogToFileToggle.IsChecked = effective.LogToFile; + GameWindowModeBox.SelectedItem = FindChoice( + _windowModeChoices, + effective.WindowMode, + "Windowed"); + GameScalingModeBox.SelectedItem = FindChoice( + _scalingModeChoices, + effective.ScalingMode, + "Fit"); + GameVSyncToggle.IsChecked = effective.VSync; + GameHdrModeBox.SelectedItem = FindChoice( + _hdrModeChoices, + effective.HdrMode, + "Auto"); + + _gameHostDisplays = HostDisplayOptions.BuildDisplays( + HostDisplayCatalog.Query(), + effective.DisplayIndex); + GameDisplayBox.ItemsSource = _gameHostDisplays; + var display = HostDisplayOptions.SelectDisplay( + _gameHostDisplays, + effective.DisplayIndex); + GameDisplayBox.SelectedItem = display; + PopulateGameHostModes( + display, + effective.Resolution, + effective.RefreshRate); + + _gameEnvironmentPassthrough.Clear(); + foreach (var entry in effective.EnvironmentToggles) + { + if (!IsKnownGameEnvironmentEntry(entry)) + { + _gameEnvironmentPassthrough.Add(entry); + } + } + + foreach (var (name, toggle) in GameEnvironmentToggles()) + { + toggle.IsChecked = IsEnvironmentEnabled( + effective.EnvironmentToggles, + name); + } + } + finally + { + _updatingGameHostDisplayOptions = false; + _isLoadingGameSettings = false; + } + } + + private void PersistOpenGameSettings() + { + if (!_isGameSettingsOpen || + _isLoadingGameSettings || + _updatingGameHostDisplayOptions || + string.IsNullOrWhiteSpace(_gameSettingsTitleId)) + { + return; + } + + var settings = new PerGameSettings + { + LogLevel = SelectedComboText(GameLogLevelBox, "Info"), + ImportTraceLimit = (int)(GameTraceImportsBox.Value ?? 0), + StrictDynlibResolution = GameStrictToggle.IsChecked == true, + LogToFile = GameLogToFileToggle.IsChecked == true, + WindowMode = SelectedComboText(GameWindowModeBox, "Windowed"), + Resolution = SelectedComboText(GameResolutionBox, "1920x1080"), + DisplayIndex = GameDisplayBox.SelectedItem is HostDisplayOption display + ? display.Index + : 0, + RefreshRate = SelectedGameRefreshRate(), + ScalingMode = SelectedComboText(GameScalingModeBox, "Fit"), + VSync = GameVSyncToggle.IsChecked == true, + HdrMode = SelectedComboText(GameHdrModeBox, "Auto"), + EnvironmentToggles = BuildGameEnvironmentEntries(), + }; + settings.RemoveInheritedValues(_settings); + settings.Save(_gameSettingsTitleId); + } + + private void OnGameHostDisplayChanged() + { + if (_isLoadingGameSettings || + _updatingGameHostDisplayOptions || + GameDisplayBox.SelectedItem is not HostDisplayOption display) + { + return; + } + + _updatingGameHostDisplayOptions = true; + try + { + PopulateGameHostModes( + display, + GameResolutionBox.SelectedItem as string ?? "1920x1080", + SelectedGameRefreshRate()); + } + finally + { + _updatingGameHostDisplayOptions = false; + } + + PersistOpenGameSettings(); + } + + private void OnGameHostResolutionChanged() + { + if (_isLoadingGameSettings || + _updatingGameHostDisplayOptions || + GameDisplayBox.SelectedItem is not HostDisplayOption display) + { + return; + } + + _updatingGameHostDisplayOptions = true; + try + { + PopulateGameRefreshRates( + display, + GameResolutionBox.SelectedItem as string, + SelectedGameRefreshRate()); + } + finally + { + _updatingGameHostDisplayOptions = false; + } + + PersistOpenGameSettings(); + } + + private void PopulateGameHostModes( + HostDisplayOption display, + string selectedResolution, + int selectedRefreshRate) + { + var resolutions = HostDisplayOptions.BuildResolutions(display, selectedResolution); + GameResolutionBox.ItemsSource = resolutions; + GameResolutionBox.SelectedItem = resolutions.FirstOrDefault(resolution => + string.Equals(resolution, selectedResolution, StringComparison.OrdinalIgnoreCase)) + ?? resolutions[0]; + PopulateGameRefreshRates( + display, + GameResolutionBox.SelectedItem as string, + selectedRefreshRate); + } + + private void PopulateGameRefreshRates( + HostDisplayOption display, + string? resolution, + int selectedRefreshRate) + { + var refreshRates = HostDisplayOptions.BuildRefreshRates( + display, + resolution, + selectedRefreshRate, + Localization.Instance.Get("Options.RefreshRate.Automatic")); + GameRefreshRateBox.ItemsSource = refreshRates; + GameRefreshRateBox.SelectedItem = refreshRates.FirstOrDefault( + refreshRate => refreshRate.Value == selectedRefreshRate) ?? refreshRates[0]; + } + + private int SelectedGameRefreshRate() => + GameRefreshRateBox.SelectedItem is HostRefreshRateOption refreshRate + ? refreshRate.Value + : 0; + + private List BuildGameEnvironmentEntries() + { + var entries = new List(_gameEnvironmentPassthrough); + foreach (var (name, toggle) in GameEnvironmentToggles()) + { + if (toggle.IsChecked == true) + { + entries.Add(name); + } + } + + return entries; + } + + private static LocalizedChoice FindChoice( + IEnumerable choices, + string value, + string fallback) => + choices.FirstOrDefault(choice => + string.Equals(choice.Value, value, StringComparison.OrdinalIgnoreCase)) + ?? choices.First(choice => + string.Equals(choice.Value, fallback, StringComparison.OrdinalIgnoreCase)); + + private static bool IsEnvironmentEnabled( + IEnumerable entries, + string name) + { + foreach (var entry in entries) + { + var parts = entry.Split('=', 2, StringSplitOptions.TrimEntries); + if (!string.Equals(parts[0], name, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + return parts.Length == 1 || parts[1] != "0"; + } + + return false; + } + + private static bool IsKnownGameEnvironmentEntry(string entry) + { + var name = entry.Split('=', 2, StringSplitOptions.TrimEntries)[0]; + return GameEnvironmentToggleNames.Contains(name, StringComparer.OrdinalIgnoreCase); + } + + private void SetGameOptionsSection(int section, bool animateIndicator = true) + { + var buttons = GameOptionsSectionButtons(); + var panels = GameOptionsSectionPanels(); + section = Math.Clamp(section, 0, buttons.Length - 1); + _gameOptionsSectionIndex = section; + SetGameOptionsNavigationIndicator(section, animateIndicator); + + for (var index = 0; index < buttons.Length; index++) + { + var active = index == section; + SetActiveClass(buttons[index], active); + SetOptionsPanelInteraction(panels[index], active); + } + + SetActiveClass(GameOptionsBackNav, active: false); + } + + private void SetGameOptionsNavigationIndicator(int section, bool animate = true) + { + var buttons = GameOptionsNavigationButtons(); + _gameOptionsIndicatorIndex = Math.Clamp(section, 0, buttons.Length - 1); + var button = buttons[_gameOptionsIndicatorIndex]; + MoveNavigationIndicator( + GameOptionsNavIndicator, + GameOptionsNavHost, + button, + _gameOptionsIndicatorIndex, + animate); + } + + private Button[] GameOptionsNavigationButtons() => + [ + GameOptionsGeneralNav, + GameOptionsLoggingNav, + GameOptionsRenderingNav, + GameOptionsEnvironmentNav, + GameOptionsBackNav, + ]; + + private Button[] GameOptionsSectionButtons() => + [ + GameOptionsGeneralNav, + GameOptionsLoggingNav, + GameOptionsRenderingNav, + GameOptionsEnvironmentNav, + ]; + + private Control[] GameOptionsSectionPanels() => + [ + GameOptionsGeneralPanel, + GameOptionsLoggingPanel, + GameOptionsRenderingPanel, + GameOptionsEnvironmentPanel, + ]; + + private (string Name, ToggleSwitch Toggle)[] GameEnvironmentToggles() => + [ + ("SHARPEMU_BTHID_UNAVAILABLE", GameEnvBthidToggle), + ("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", GameEnvLoopGuardToggle), + ("SHARPEMU_WRITABLE_APP0", GameEnvWritableApp0Toggle), + ("SHARPEMU_VK_VALIDATION", GameEnvVkValidationToggle), + ("SHARPEMU_DUMP_SPIRV", GameEnvDumpSpirvToggle), + ("SHARPEMU_LOG_DIRECT_MEMORY", GameEnvLogDirectMemoryToggle), + ("SHARPEMU_LOG_IO", GameEnvLogIoToggle), + ("SHARPEMU_LOG_NP", GameEnvLogNpToggle), + ("SHARPEMU_GUEST_IMAGE_CPU_SYNC", GameEnvGuestImageCpuSyncToggle), + ]; + + private static void SetGameOptionsOpenClass(Control control, bool active) + { + if (active) + { + if (!control.Classes.Contains("gameOptionsOpen")) + { + control.Classes.Add("gameOptionsOpen"); + } + } + else + { + control.Classes.Remove("gameOptionsOpen"); + } + } +} diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml index cd7fe0b5..9ba5ee1e 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml +++ b/src/SharpEmu.GUI/MainWindow.axaml @@ -26,11 +26,22 @@ SPDX-License-Identifier: GPL-2.0-or-later - + - + + + + + + @@ -42,16 +53,15 @@ SPDX-License-Identifier: GPL-2.0-or-later - - - + + + - + + + - + + + + + + + + - + + + + + + + + - + - + Classes="optionsCircle" + ToolTip.Tip="{Binding [Launch.Console], + Source={x:Static local:Localization.Instance}, + x:CompileBindings=False}" + AutomationProperties.Name="{Binding [Launch.Console], + Source={x:Static local:Localization.Instance}, + x:CompileBindings=False}"> + + + @@ -328,6 +379,569 @@ SPDX-License-Identifier: GPL-2.0-or-later + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + IsHitTestVisible="False" />