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" />
-
-
-
-
-
-
SetActivePage(1);
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
WireOptionsNavigation();
+ WireGameOptions();
// The settings page edits _settings live, so a launch started while
// it is open already uses the new values.
@@ -288,9 +293,9 @@ public partial class MainWindow : Window
CtxLaunch.Click += (_, _) => LaunchSelected();
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
CtxCopyPath.Click += async (_, _) =>
- await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Clipboard.Path");
+ await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path);
CtxCopyTitleId.Click += async (_, _) =>
- await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Clipboard.TitleId");
+ await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId);
CtxGameSettings.Click += (_, _) => OpenSelectedGameSettings();
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
@@ -340,6 +345,11 @@ public partial class MainWindow : Window
{
if (index == _activePageIndex)
{
+ if (index == 0 && _isGameSettingsOpen)
+ {
+ CloseGameSettings();
+ }
+
return;
}
@@ -348,12 +358,25 @@ public partial class MainWindow : Window
_settings.Save(); // leaving the Options page
}
+ if (_isGameSettingsOpen)
+ {
+ CloseGameSettings(restoreLibrary: false);
+ }
+
_activePageIndex = index;
SetActiveClass(LibraryTabButton, index == 0);
SetActiveClass(OptionsTabButton, index == 1);
LibraryPage.IsVisible = index == 0;
LibraryToolbar.IsVisible = index == 0;
+ OptionsPageSurface.IsVisible = index == 1;
OptionsPage.IsVisible = index == 1;
+
+ if (index == 1)
+ {
+ Dispatcher.UIThread.Post(
+ () => SetOptionsNavigationIndicator(_optionsSectionIndex, animate: false),
+ DispatcherPriority.Loaded);
+ }
}
private static void SetActiveClass(Button button, bool active)
@@ -446,17 +469,69 @@ public partial class MainWindow : Window
active ? KeyboardNavigationMode.Continue : KeyboardNavigationMode.None);
}
- private void SetOptionsNavigationIndicator(int section)
+ private void SetOptionsNavigationIndicator(int section, bool animate = true)
{
- if (OptionsNavIndicator.RenderTransform is not TranslateTransform transform)
+ var buttons = OptionsNavigationButtons();
+ var button = buttons[Math.Clamp(section, 0, buttons.Length - 1)];
+ MoveNavigationIndicator(
+ OptionsNavIndicator,
+ OptionsNavHost,
+ button,
+ section,
+ animate);
+ }
+
+ private static void ConfigureNavigationIndicatorAnimation(Border indicator)
+ {
+ if (ElementComposition.GetElementVisual(indicator) is not { } visual)
{
return;
}
- var buttons = OptionsNavigationButtons();
- var button = buttons[Math.Clamp(section, 0, buttons.Length - 1)];
- transform.Y = button.TranslatePoint(default, OptionsNavHost)?.Y
+ var translationAnimation = visual.Compositor.CreateVector3KeyFrameAnimation();
+ translationAnimation.Duration = NavigationIndicatorAnimationDuration;
+ translationAnimation.Target = nameof(CompositionVisual.Translation);
+ translationAnimation.InsertExpressionKeyFrame(
+ 1f,
+ "this.FinalValue",
+ new SineEaseInOut());
+
+ var animations = visual.Compositor.CreateImplicitAnimationCollection();
+ animations[nameof(CompositionVisual.Translation)] = translationAnimation;
+ visual.ImplicitAnimations = animations;
+ }
+
+ private static void MoveNavigationIndicator(
+ Border indicator,
+ Control host,
+ Button button,
+ int section,
+ bool animate = true)
+ {
+ if (ElementComposition.GetElementVisual(indicator) is not { } visual)
+ {
+ return;
+ }
+
+ var targetY = button.TranslatePoint(default, host)?.Y
?? section * button.Bounds.Height;
+
+ if (!animate)
+ {
+ visual.ImplicitAnimations = null;
+ visual.StopAnimation(nameof(CompositionVisual.Translation));
+ }
+ else if (visual.ImplicitAnimations is null)
+ {
+ ConfigureNavigationIndicatorAnimation(indicator);
+ }
+
+ visual.Translation = new Vector3D(0, targetY, 0);
+
+ if (!animate)
+ {
+ ConfigureNavigationIndicatorAnimation(indicator);
+ }
}
// ---- Github http client config ----
@@ -581,7 +656,7 @@ public partial class MainWindow : Window
SetActivePage(1);
}
- if (_activePageIndex != 0)
+ if (_activePageIndex != 0 || _isGameSettingsOpen)
{
_previousPadButtons = pad.Buttons;
return;
@@ -798,6 +873,13 @@ public partial class MainWindow : Window
private void OnKeyDown(object sender, KeyEventArgs args)
{
+ if (args.Key == Key.Escape && _isGameSettingsOpen)
+ {
+ CloseGameSettings();
+ args.Handled = true;
+ return;
+ }
+
if (args.Key == Key.F11 && !_isRunning)
{
WindowState = WindowState == WindowState.FullScreen
@@ -910,11 +992,6 @@ public partial class MainWindow : Window
titleBar.IsVisible = !isFullscreen;
}
- if (StatusBar is { } statusBar)
- {
- statusBar.IsVisible = !isFullscreen;
- }
-
if (ResizeHandles is { } handles)
{
handles.IsVisible = CanResize && WindowState == WindowState.Normal;
@@ -1302,9 +1379,6 @@ public partial class MainWindow : Window
? Path.GetFullPath(found)
: null;
- EmulatorPathText.Text = _emulatorExePath is not null
- ? Localization.Instance.Format("Status.EmulatorPath", _emulatorExePath)
- : Localization.Instance.Get("Status.EmulatorNotFound");
}
// ---- Game library ----
@@ -1379,11 +1453,6 @@ public partial class MainWindow : Window
var excluded = new HashSet(_settings.ExcludedGames, GameLibraryPath.Comparer);
_libraryWatcher.Watch(folders);
var showLoadingState = showProgress && _allGames.Count == 0;
- if (showProgress)
- {
- StatusBarRight.Text = Localization.Instance.Get("Status.ScanningLibrary");
- }
-
if (showLoadingState)
{
EmptyState.IsVisible = false;
@@ -1404,12 +1473,6 @@ public partial class MainWindow : Window
LoadingState.IsVisible = false;
LoadGameDetailsInBackground(reconciliation.CoversToLoad, reconciliation.Games);
UpdateDiscordPresence();
- if (showProgress)
- {
- StatusBarRight.Text = folders.Length == 0
- ? Localization.Instance.Get("Status.AddFolderPrompt")
- : Localization.Instance.Format("Status.LibraryScanned", games.Count, folders.Length);
- }
}
///
@@ -1806,24 +1869,6 @@ public partial class MainWindow : Window
CtxGameSettings.IsEnabled = !string.IsNullOrWhiteSpace(game.TitleId);
}
- 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;
- }
-
- _ = new PerGameSettingsDialog(game.TitleId, game.Name, _settings).ShowDialog(this);
- }
-
private void OpenSelectedGameFolder()
{
if (GameList.SelectedItem is not GameEntry game)
@@ -1854,12 +1899,13 @@ public partial class MainWindow : Window
}
catch (Exception ex)
{
- StatusBarRight.Text = Localization.Instance.Format("Status.CouldNotOpenFolder", ex.Message);
+ AppendConsoleLine(
+ Localization.Instance.Format("Status.CouldNotOpenFolder", ex.Message),
+ WarningLineBrush);
}
}
- /// Copies and reports it via , e.g. "Clipboard.Path".
- private async Task CopyToClipboardAsync(string? text, string whatKey)
+ private async Task CopyToClipboardAsync(string? text)
{
if (string.IsNullOrEmpty(text) || Clipboard is null)
{
@@ -1867,7 +1913,6 @@ public partial class MainWindow : Window
}
await Clipboard.SetTextAsync(text);
- StatusBarRight.Text = Localization.Instance.Format("Status.CopiedToClipboard", Localization.Instance.Get(whatKey));
}
private void RemoveSelectedFromLibrary()
@@ -1887,7 +1932,6 @@ public partial class MainWindow : Window
string.Equals(g.Path, game.Path, GameLibraryPath.Comparison));
GameList.SelectedItem = null;
RefreshVisibleGames();
- StatusBarRight.Text = Localization.Instance.Format("Status.RemovedFromLibrary", game.Name);
}
private void RefreshVisibleGames(IReadOnlySet? backgroundsChanged = null)
@@ -2197,7 +2241,6 @@ public partial class MainWindow : Window
_runningGameName = displayName;
_runningGameTitleId = resolvedTitleId;
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
- StatusBarRight.Text = Localization.Instance.Format("Status.Running", displayName);
UpdateRunButtons();
UpdateDiscordPresence();
@@ -2250,7 +2293,6 @@ public partial class MainWindow : Window
_emulator.Stop();
_runningGameName = null;
_runningGameTitleId = null;
- StatusBarRight.Text = Localization.Instance.Get("Status.Stopping");
UpdateDiscordPresence();
ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
Console.Error.WriteLine("[GUI][INFO] Waiting for the SDL game process to exit.");
@@ -2317,7 +2359,6 @@ public partial class MainWindow : Window
brush);
CloseFileLogSoon();
- StatusBarRight.Text = Localization.Instance.Get("Status.Idle");
_runningGameName = null;
_runningGameTitleId = null;
UpdateRunButtons();
@@ -2470,6 +2511,9 @@ public partial class MainWindow : Window
LaunchButton.IsEnabled = GameList.SelectedItem is GameEntry;
}
+ GameSettingsButton.IsEnabled =
+ GameList.SelectedItem is GameEntry game &&
+ !string.IsNullOrWhiteSpace(game.TitleId);
OpenFileButton.IsEnabled = !_isRunning;
}
diff --git a/src/SharpEmu.GUI/PerGameSettings.cs b/src/SharpEmu.GUI/PerGameSettings.cs
index ca932e3c..43846e41 100644
--- a/src/SharpEmu.GUI/PerGameSettings.cs
+++ b/src/SharpEmu.GUI/PerGameSettings.cs
@@ -92,6 +92,74 @@ public sealed class PerGameSettings
return settings;
}
+ ///
+ /// Removes values that already match the global configuration so the
+ /// remaining object contains only effective per-game overrides.
+ ///
+ internal void RemoveInheritedValues(GuiSettings global)
+ {
+ if (string.Equals(LogLevel, global.LogLevel, StringComparison.OrdinalIgnoreCase))
+ {
+ LogLevel = null;
+ }
+
+ if (ImportTraceLimit == global.ImportTraceLimit)
+ {
+ ImportTraceLimit = null;
+ }
+
+ if (StrictDynlibResolution == global.StrictDynlibResolution)
+ {
+ StrictDynlibResolution = null;
+ }
+
+ if (LogToFile == global.LogToFile)
+ {
+ LogToFile = null;
+ }
+
+ if (string.Equals(WindowMode, global.WindowMode, StringComparison.OrdinalIgnoreCase))
+ {
+ WindowMode = null;
+ }
+
+ if (string.Equals(Resolution, global.Resolution, StringComparison.OrdinalIgnoreCase))
+ {
+ Resolution = null;
+ }
+
+ if (DisplayIndex == global.DisplayIndex)
+ {
+ DisplayIndex = null;
+ }
+
+ if (RefreshRate == global.RefreshRate)
+ {
+ RefreshRate = null;
+ }
+
+ if (string.Equals(ScalingMode, global.ScalingMode, StringComparison.OrdinalIgnoreCase))
+ {
+ ScalingMode = null;
+ }
+
+ if (VSync == global.VSync)
+ {
+ VSync = null;
+ }
+
+ if (string.Equals(HdrMode, global.HdrMode, StringComparison.OrdinalIgnoreCase))
+ {
+ HdrMode = null;
+ }
+
+ if (EnvironmentToggles is { } environmentToggles &&
+ EnvironmentEntriesEqual(environmentToggles, global.EnvironmentToggles))
+ {
+ EnvironmentToggles = null;
+ }
+ }
+
public void Save(string titleId)
{
if (string.IsNullOrWhiteSpace(titleId))
@@ -130,6 +198,35 @@ public sealed class PerGameSettings
return trimmed.Length == 0 ? "UNKNOWN" : trimmed;
}
+
+ private static bool EnvironmentEntriesEqual(
+ IEnumerable left,
+ IEnumerable right) =>
+ NormalizeEnvironmentEntries(left).SetEquals(NormalizeEnvironmentEntries(right));
+
+ private static HashSet NormalizeEnvironmentEntries(IEnumerable entries)
+ {
+ var normalized = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var entry in entries)
+ {
+ var parts = entry.Split('=', 2, StringSplitOptions.TrimEntries);
+ if (parts.Length == 0 || string.IsNullOrWhiteSpace(parts[0]))
+ {
+ continue;
+ }
+
+ if (parts.Length == 2 && parts[1] == "0")
+ {
+ continue;
+ }
+
+ normalized.Add(parts.Length == 2 && parts[1] != "1"
+ ? $"{parts[0]}={parts[1]}"
+ : parts[0]);
+ }
+
+ return normalized;
+ }
}
public sealed record EffectiveLaunchSettings(
diff --git a/src/SharpEmu.GUI/PerGameSettingsDialog.cs b/src/SharpEmu.GUI/PerGameSettingsDialog.cs
deleted file mode 100644
index 4b81b7db..00000000
--- a/src/SharpEmu.GUI/PerGameSettingsDialog.cs
+++ /dev/null
@@ -1,426 +0,0 @@
-// Copyright (C) 2026 SharpEmu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Layout;
-using Avalonia.Media;
-using SharpEmu.Libs.VideoOut;
-
-namespace SharpEmu.GUI;
-
-public sealed class PerGameSettingsDialog : Window
-{
- private static readonly string[] LogLevels =
- { "Trace", "Debug", "Info", "Warning", "Error", "Critical" };
- private static readonly string[] WindowModes = { "Windowed", "Borderless", "Exclusive" };
- private static readonly string[] ScalingModes = { "Fit", "Cover", "Stretch", "Integer" };
- private static readonly string[] HdrModes = { "Auto", "On", "Off" };
-
- private static readonly string[] EnvToggles =
- {
- "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 string _titleId;
- private IReadOnlyList _hostDisplays = [];
- private bool _updatingHostDisplayOptions;
-
- private readonly SettingRow _logLevelRow;
- private readonly ComboBox _logLevel = new() { ItemsSource = LogLevels, Width = 160 };
-
- private readonly SettingRow _traceRow;
- private readonly NumericUpDown _trace = new()
- {
- Minimum = 0, Maximum = 4096, Increment = 16, Width = 160, FormatString = "0",
- };
-
- private readonly SettingRow _strictRow;
- private readonly ToggleSwitch _strict = new();
-
- private readonly SettingRow _logToFileRow;
- private readonly ToggleSwitch _logToFile = new();
-
- private readonly SettingRow _windowModeRow;
- private readonly ComboBox _windowMode = new() { ItemsSource = WindowModes, Width = 160 };
-
- private readonly SettingRow _resolutionRow;
- private readonly ComboBox _resolution = new() { Width = 160 };
-
- private readonly SettingRow _displayIndexRow;
- private readonly ComboBox _displayIndex = new() { Width = 240 };
-
- private readonly SettingRow _refreshRateRow;
- private readonly ComboBox _refreshRate = new() { Width = 160 };
-
- private readonly SettingRow _scalingModeRow;
- private readonly ComboBox _scalingMode = new() { ItemsSource = ScalingModes, Width = 160 };
-
- private readonly SettingRow _vsyncRow;
- private readonly ToggleSwitch _vsync = new();
-
- private readonly SettingRow _hdrModeRow;
- private readonly ComboBox _hdrMode = new() { ItemsSource = HdrModes, Width = 160 };
-
- private readonly SettingRow _envRow;
- private readonly StackPanel _envList = new() { Orientation = Orientation.Vertical, Spacing = 8, Margin = new(0, 4, 0, 0) };
- private readonly List<(string Name, ToggleSwitch Box)> _envBoxes = new();
-
- public PerGameSettingsDialog(string titleId, string displayName, GuiSettings global)
- {
- _titleId = titleId;
- var loc = Localization.Instance;
-
- Title = loc.Format("PerGame.Title", displayName, titleId);
- Width = 520;
- MaxHeight = 720;
- SizeToContent = SizeToContent.Height;
- WindowStartupLocation = WindowStartupLocation.CenterOwner;
- CanResize = false;
-
- Background = new SolidColorBrush(Color.Parse("#0D1017"));
-
- _strict.OnContent = _logToFile.OnContent = _vsync.OnContent = loc.Get("Common.On");
- _strict.OffContent = _logToFile.OffContent = _vsync.OffContent = loc.Get("Common.Off");
-
- _logLevelRow = Row(loc.Get("Options.LogLevel.Label"), loc.Get("Options.LogLevel.Desc"), _logLevel);
- _traceRow = Row(loc.Get("Options.TraceImports.Label"), loc.Get("Options.TraceImports.Desc"), _trace);
- _strictRow = Row(loc.Get("Options.Strict.Label"), loc.Get("Options.Strict.Desc"), _strict);
- _logToFileRow = Row(loc.Get("Options.LogToFile.Label"), loc.Get("Options.LogToFile.Desc"), _logToFile);
- _windowModeRow = Row(loc.Get("Options.WindowMode.Label"), loc.Get("Options.WindowMode.Desc"), _windowMode);
- _resolutionRow = Row(loc.Get("Options.Resolution.Label"), loc.Get("Options.Resolution.Desc"), _resolution);
- _displayIndexRow = Row(loc.Get("Options.Display.Label"), loc.Get("Options.Display.Desc"), _displayIndex);
- _refreshRateRow = Row(loc.Get("Options.RefreshRate.Label"), loc.Get("Options.RefreshRate.Desc"), _refreshRate);
- _scalingModeRow = Row(loc.Get("Options.Scaling.Label"), loc.Get("Options.Scaling.Desc"), _scalingMode);
- _vsyncRow = Row(loc.Get("Options.VSync.Label"), loc.Get("Options.VSync.Desc"), _vsync);
- _hdrModeRow = Row(loc.Get("Options.Hdr.Label"), loc.Get("Options.Hdr.Desc"), _hdrMode);
- _envRow = new SettingRow
- {
- Label = loc.Get("PerGame.EnvToggles.Label"),
- Description = loc.Get("PerGame.EnvToggles.Desc"),
- ShowOverride = true,
- };
-
- foreach (var name in EnvToggles)
- {
- var box = new ToggleSwitch { OnContent = name, OffContent = name };
- _envBoxes.Add((name, box));
- _envList.Children.Add(box);
- }
-
- var general = new StackPanel { Orientation = Orientation.Vertical, Spacing = 12, Margin = new(0, 12, 0, 0) };
- general.Children.Add(Card(loc.Get("Options.Section.Emulation"), _strictRow));
- general.Children.Add(Card(loc.Get("Options.Section.Logging"), _logLevelRow, _traceRow, _logToFileRow));
- general.Children.Add(Card(loc.Get("Options.Section.Environment"), _envRow, _envList));
-
- var graphics = new StackPanel { Orientation = Orientation.Vertical, Spacing = 12, Margin = new(0, 12, 0, 0) };
- graphics.Children.Add(Card(
- loc.Get("Options.Section.Display"),
- _windowModeRow,
- _resolutionRow,
- _displayIndexRow,
- _refreshRateRow,
- _scalingModeRow,
- _vsyncRow,
- _hdrModeRow));
-
- var content = new StackPanel { Orientation = Orientation.Vertical, Spacing = 12, Margin = new(16) };
- content.Children.Add(new TextBlock
- {
- Text = loc.Get("PerGame.InheritNote"),
- Foreground = new SolidColorBrush(Color.Parse("#8B94A7")),
- FontSize = 12,
- });
- content.Children.Add(new TabControl
- {
- ItemsSource = new[]
- {
- new TabItem { Header = loc.Get("PerGame.Tab.General"), Content = general },
- new TabItem { Header = loc.Get("PerGame.Tab.Graphics"), Content = graphics },
- },
- });
-
- var save = new Button { Content = loc.Get("Common.Save"), Classes = { "accent" } };
- var cancel = new Button { Content = loc.Get("Common.Cancel"), Classes = { "ghost" } };
- save.Click += (_, _) => { Persist(); Close(); };
- cancel.Click += (_, _) => Close();
-
- var buttonBar = new Border
- {
- BorderBrush = new SolidColorBrush(Color.Parse("#8B94A7")) { Opacity = 0.25 },
- BorderThickness = new Thickness(0, 1, 0, 0),
- Padding = new(16),
- Child = new StackPanel
- {
- Orientation = Orientation.Horizontal,
- Spacing = 8,
- HorizontalAlignment = HorizontalAlignment.Right,
- Children = { cancel, save },
- },
- };
-
- var root = new Grid { RowDefinitions = new RowDefinitions("*,Auto") };
- var scroller = new ScrollViewer { Content = content };
- Grid.SetRow(scroller, 0);
- Grid.SetRow(buttonBar, 1);
- root.Children.Add(scroller);
- root.Children.Add(buttonBar);
- Content = root;
-
- _displayIndex.SelectionChanged += (_, _) => OnHostDisplayChanged();
- _resolution.SelectionChanged += (_, _) => OnHostResolutionChanged();
- LoadValues(global);
- _envRow.PropertyChanged += (_, e) =>
- {
- if (e.Property == SettingRow.IsOverriddenProperty)
- {
- _envList.IsEnabled = _envRow.IsOverridden;
- }
- };
- _envList.IsEnabled = _envRow.IsOverridden;
- }
-
- private static SettingRow Row(string label, string description, Control value) => new()
- {
- Label = label,
- Description = description,
- ShowOverride = true,
- Content = value,
- };
-
- private static Border Card(string title, params Control[] rows)
- {
- var stack = new StackPanel { Orientation = Orientation.Vertical, Spacing = 14 };
- stack.Children.Add(new TextBlock { Text = title, Classes = { "sectionTitle" } });
- foreach (var row in rows)
- {
- stack.Children.Add(row);
- }
-
- var card = new Border { Child = stack };
- card.Classes.Add("card");
- return card;
- }
-
- private void LoadValues(GuiSettings global)
- {
- var existing = PerGameSettings.Load(_titleId);
- var displayIndex = Math.Max(0, existing?.DisplayIndex ?? global.DisplayIndex);
- var resolution = existing?.Resolution ?? global.Resolution;
- var refreshRate = Math.Clamp(existing?.RefreshRate ?? global.RefreshRate, 0, 1000);
-
- _updatingHostDisplayOptions = true;
- try
- {
- _hostDisplays = HostDisplayOptions.BuildDisplays(HostDisplayCatalog.Query(), displayIndex);
- _displayIndex.ItemsSource = _hostDisplays;
- var display = HostDisplayOptions.SelectDisplay(_hostDisplays, displayIndex);
- _displayIndex.SelectedItem = display;
- PopulateHostModes(display, resolution, refreshRate);
- }
- finally
- {
- _updatingHostDisplayOptions = false;
- }
-
- _logLevel.SelectedItem = Array.IndexOf(LogLevels, global.LogLevel) >= 0 ? global.LogLevel : "Info";
- _trace.Value = global.ImportTraceLimit;
- _strict.IsChecked = global.StrictDynlibResolution;
- _logToFile.IsChecked = global.LogToFile;
- _windowMode.SelectedItem = ChoiceOrDefault(WindowModes, global.WindowMode, "Windowed");
- _scalingMode.SelectedItem = ChoiceOrDefault(ScalingModes, global.ScalingMode, "Fit");
- _vsync.IsChecked = global.VSync;
- _hdrMode.SelectedItem = ChoiceOrDefault(HdrModes, global.HdrMode, "Auto");
- foreach (var (name, box) in _envBoxes)
- {
- box.IsChecked = IsEnvironmentEnabled(global.EnvironmentToggles, name, defaultValue: false);
- }
-
- if (existing is null)
- {
- return;
- }
-
- if (existing.LogLevel is { } level && Array.IndexOf(LogLevels, level) >= 0)
- {
- _logLevelRow.IsOverridden = true;
- _logLevel.SelectedItem = level;
- }
-
- if (existing.ImportTraceLimit is { } t) { _traceRow.IsOverridden = true; _trace.Value = t; }
- if (existing.StrictDynlibResolution is { } s) { _strictRow.IsOverridden = true; _strict.IsChecked = s; }
- if (existing.LogToFile is { } l) { _logToFileRow.IsOverridden = true; _logToFile.IsChecked = l; }
- if (existing.WindowMode is { } windowMode && WindowModes.Contains(windowMode, StringComparer.OrdinalIgnoreCase))
- {
- _windowModeRow.IsOverridden = true;
- _windowMode.SelectedItem = ChoiceOrDefault(WindowModes, windowMode, "Windowed");
- }
- if (existing.Resolution is not null)
- {
- _resolutionRow.IsOverridden = true;
- }
- if (existing.DisplayIndex is not null)
- {
- _displayIndexRow.IsOverridden = true;
- }
- if (existing.RefreshRate is not null)
- {
- _refreshRateRow.IsOverridden = true;
- }
- if (existing.ScalingMode is { } scalingMode && ScalingModes.Contains(scalingMode, StringComparer.OrdinalIgnoreCase))
- {
- _scalingModeRow.IsOverridden = true;
- _scalingMode.SelectedItem = ChoiceOrDefault(ScalingModes, scalingMode, "Fit");
- }
- if (existing.VSync is { } vsync)
- {
- _vsyncRow.IsOverridden = true;
- _vsync.IsChecked = vsync;
- }
- if (existing.HdrMode is { } hdrMode && HdrModes.Contains(hdrMode, StringComparer.OrdinalIgnoreCase))
- {
- _hdrModeRow.IsOverridden = true;
- _hdrMode.SelectedItem = ChoiceOrDefault(HdrModes, hdrMode, "Auto");
- }
- if (existing.EnvironmentToggles is { } env)
- {
- _envRow.IsOverridden = true;
- foreach (var (name, box) in _envBoxes)
- {
- box.IsChecked = IsEnvironmentEnabled(env, name, defaultValue: false);
- }
- }
- }
-
- private static string ChoiceOrDefault(string[] choices, string? value, string fallback) =>
- choices.FirstOrDefault(choice => string.Equals(choice, value, StringComparison.OrdinalIgnoreCase)) ?? fallback;
-
- private void OnHostDisplayChanged()
- {
- if (_updatingHostDisplayOptions || _displayIndex.SelectedItem is not HostDisplayOption display)
- {
- return;
- }
-
- _updatingHostDisplayOptions = true;
- try
- {
- PopulateHostModes(
- display,
- _resolution.SelectedItem as string ?? "1920x1080",
- SelectedRefreshRate());
- }
- finally
- {
- _updatingHostDisplayOptions = false;
- }
- }
-
- private void OnHostResolutionChanged()
- {
- if (_updatingHostDisplayOptions || _displayIndex.SelectedItem is not HostDisplayOption display)
- {
- return;
- }
-
- var selectedRefreshRate = SelectedRefreshRate();
- _updatingHostDisplayOptions = true;
- try
- {
- PopulateRefreshRates(display, _resolution.SelectedItem as string, selectedRefreshRate);
- }
- finally
- {
- _updatingHostDisplayOptions = false;
- }
- }
-
- private void PopulateHostModes(
- HostDisplayOption display,
- string selectedResolution,
- int selectedRefreshRate)
- {
- var resolutions = HostDisplayOptions.BuildResolutions(display, selectedResolution);
- _resolution.ItemsSource = resolutions;
- _resolution.SelectedItem = resolutions.FirstOrDefault(resolution =>
- string.Equals(resolution, selectedResolution, StringComparison.OrdinalIgnoreCase)) ?? resolutions[0];
- PopulateRefreshRates(display, _resolution.SelectedItem as string, selectedRefreshRate);
- }
-
- private void PopulateRefreshRates(
- HostDisplayOption display,
- string? resolution,
- int selectedRefreshRate)
- {
- var rates = HostDisplayOptions.BuildRefreshRates(
- display,
- resolution,
- selectedRefreshRate,
- Localization.Instance.Get("Options.RefreshRate.Automatic"));
- _refreshRate.ItemsSource = rates;
- _refreshRate.SelectedItem = rates.FirstOrDefault(rate => rate.Value == selectedRefreshRate) ?? rates[0];
- }
-
- private int SelectedRefreshRate() =>
- _refreshRate.SelectedItem is HostRefreshRateOption refreshRate ? refreshRate.Value : 0;
-
- private void Persist()
- {
- var settings = new PerGameSettings
- {
- LogLevel = _logLevelRow.IsOverridden ? _logLevel.SelectedItem as string : null,
- ImportTraceLimit = _traceRow.IsOverridden ? (int)(_trace.Value ?? 0) : null,
- StrictDynlibResolution = _strictRow.IsOverridden ? _strict.IsChecked == true : null,
- LogToFile = _logToFileRow.IsOverridden ? _logToFile.IsChecked == true : null,
- WindowMode = _windowModeRow.IsOverridden ? _windowMode.SelectedItem as string : null,
- Resolution = _resolutionRow.IsOverridden ? _resolution.SelectedItem as string : null,
- DisplayIndex = _displayIndexRow.IsOverridden && _displayIndex.SelectedItem is HostDisplayOption display
- ? display.Index
- : null,
- RefreshRate = _refreshRateRow.IsOverridden ? SelectedRefreshRate() : null,
- ScalingMode = _scalingModeRow.IsOverridden ? _scalingMode.SelectedItem as string : null,
- VSync = _vsyncRow.IsOverridden ? _vsync.IsChecked == true : null,
- HdrMode = _hdrModeRow.IsOverridden ? _hdrMode.SelectedItem as string : null,
- EnvironmentToggles = _envRow.IsOverridden ? BuildEnvironmentEntries() : null,
- };
- settings.Save(_titleId);
- }
-
- private List BuildEnvironmentEntries()
- {
- return _envBoxes
- .Where(entry => entry.Box.IsChecked == true)
- .Select(entry => entry.Name)
- .ToList();
- }
-
- private static bool IsEnvironmentEnabled(
- IEnumerable entries,
- string name,
- bool defaultValue)
- {
- foreach (var entry in entries)
- {
- if (string.Equals(entry, name + "=0", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
-
- if (string.Equals(entry, name, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(entry, name + "=1", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return defaultValue;
- }
-}
diff --git a/src/SharpEmu.GUI/SettingRow.cs b/src/SharpEmu.GUI/SettingRow.cs
index 83ab267b..503ff254 100644
--- a/src/SharpEmu.GUI/SettingRow.cs
+++ b/src/SharpEmu.GUI/SettingRow.cs
@@ -3,9 +3,7 @@
using Avalonia;
using Avalonia.Controls;
-using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
-using Avalonia.Data;
using Avalonia.Media;
namespace SharpEmu.GUI;
@@ -18,17 +16,9 @@ public sealed class SettingRow : ContentControl
public static readonly StyledProperty DescriptionProperty =
AvaloniaProperty.Register(nameof(Description));
- public static readonly StyledProperty ShowOverrideProperty =
- AvaloniaProperty.Register(nameof(ShowOverride));
-
- public static readonly StyledProperty IsOverriddenProperty =
- AvaloniaProperty.Register(
- nameof(IsOverridden), defaultBindingMode: BindingMode.TwoWay);
-
public static readonly StyledProperty LabelFontFamilyProperty =
AvaloniaProperty.Register(nameof(LabelFontFamily));
- private ContentPresenter? _slot;
private TextBlock? _label;
public string? Label
@@ -43,18 +33,6 @@ public sealed class SettingRow : ContentControl
set => SetValue(DescriptionProperty, value);
}
- public bool ShowOverride
- {
- get => GetValue(ShowOverrideProperty);
- set => SetValue(ShowOverrideProperty, value);
- }
-
- public bool IsOverridden
- {
- get => GetValue(IsOverriddenProperty);
- set => SetValue(IsOverriddenProperty, value);
- }
-
public FontFamily? LabelFontFamily
{
get => GetValue(LabelFontFamilyProperty);
@@ -64,20 +42,14 @@ public sealed class SettingRow : ContentControl
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
- _slot = e.NameScope.Find("PART_Slot");
_label = e.NameScope.Find("PART_Label");
- UpdateSlotEnabled();
UpdateLabelFont();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
- if (change.Property == ShowOverrideProperty || change.Property == IsOverriddenProperty)
- {
- UpdateSlotEnabled();
- }
- else if (change.Property == LabelFontFamilyProperty)
+ if (change.Property == LabelFontFamilyProperty)
{
UpdateLabelFont();
}
@@ -90,12 +62,4 @@ public sealed class SettingRow : ContentControl
_label.FontFamily = family;
}
}
-
- private void UpdateSlotEnabled()
- {
- if (_slot is not null)
- {
- _slot.IsEnabled = !ShowOverride || IsOverridden;
- }
- }
}
diff --git a/src/SharpEmu.GUI/Themes/Styles/GameOptions.axaml b/src/SharpEmu.GUI/Themes/Styles/GameOptions.axaml
new file mode 100644
index 00000000..41f4b019
--- /dev/null
+++ b/src/SharpEmu.GUI/Themes/Styles/GameOptions.axaml
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SharpEmu.GUI/Themes/Styles/Options.axaml b/src/SharpEmu.GUI/Themes/Styles/Options.axaml
index 288a69e0..878df3f7 100644
--- a/src/SharpEmu.GUI/Themes/Styles/Options.axaml
+++ b/src/SharpEmu.GUI/Themes/Styles/Options.axaml
@@ -1,108 +1,12 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SharpEmu.GUI/Themes/Templates/SettingRow.axaml b/src/SharpEmu.GUI/Themes/Templates/SettingRow.axaml
index 7b37ae87..771f2ec8 100644
--- a/src/SharpEmu.GUI/Themes/Templates/SettingRow.axaml
+++ b/src/SharpEmu.GUI/Themes/Templates/SettingRow.axaml
@@ -12,11 +12,22 @@ Control theme for the shared launcher settings row.
+
+
+
+
+
+
-
-
-
-
+
diff --git a/src/SharpEmu.GUI/Themes/Tokens.axaml b/src/SharpEmu.GUI/Themes/Tokens.axaml
index 3f9e228a..834e67c7 100644
--- a/src/SharpEmu.GUI/Themes/Tokens.axaml
+++ b/src/SharpEmu.GUI/Themes/Tokens.axaml
@@ -10,25 +10,23 @@ Shared colors and brushes used throughout the launcher.
#7C5CFC
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/tests/SharpEmu.Libs.Tests/GUI/PerGameSettingsTests.cs b/tests/SharpEmu.Libs.Tests/GUI/PerGameSettingsTests.cs
index bd905486..dc5364ae 100644
--- a/tests/SharpEmu.Libs.Tests/GUI/PerGameSettingsTests.cs
+++ b/tests/SharpEmu.Libs.Tests/GUI/PerGameSettingsTests.cs
@@ -57,4 +57,86 @@ public sealed class PerGameSettingsTests
Assert.NotNull(settings);
Assert.Equal(["SHARPEMU_TRACE", "SHARPEMU_NO_JIT"], settings.EnvironmentToggles);
}
+
+ [Fact]
+ public void RemoveInheritedValues_AllMatchingValues_ProducesEmptySettings()
+ {
+ var global = new GuiSettings
+ {
+ LogLevel = "Info",
+ ImportTraceLimit = 32,
+ StrictDynlibResolution = true,
+ LogToFile = false,
+ WindowMode = "Borderless",
+ Resolution = "2560x1440",
+ DisplayIndex = 1,
+ RefreshRate = 144,
+ ScalingMode = "Fit",
+ VSync = true,
+ HdrMode = "Auto",
+ EnvironmentToggles = ["SHARPEMU_LOG_IO", "SHARPEMU_VK_VALIDATION"],
+ };
+ var perGame = new PerGameSettings
+ {
+ LogLevel = "info",
+ ImportTraceLimit = 32,
+ StrictDynlibResolution = true,
+ LogToFile = false,
+ WindowMode = "borderless",
+ Resolution = "2560x1440",
+ DisplayIndex = 1,
+ RefreshRate = 144,
+ ScalingMode = "fit",
+ VSync = true,
+ HdrMode = "auto",
+ EnvironmentToggles = ["SHARPEMU_VK_VALIDATION=1", "sharpemu_log_io"],
+ };
+
+ perGame.RemoveInheritedValues(global);
+
+ Assert.True(perGame.IsEmpty);
+ }
+
+ [Fact]
+ public void RemoveInheritedValues_DifferentValues_RemainOverrides()
+ {
+ var global = new GuiSettings
+ {
+ LogLevel = "Info",
+ Resolution = "1920x1080",
+ VSync = true,
+ EnvironmentToggles = ["SHARPEMU_LOG_IO"],
+ };
+ var perGame = new PerGameSettings
+ {
+ LogLevel = "Debug",
+ Resolution = "2560x1440",
+ VSync = false,
+ EnvironmentToggles = ["SHARPEMU_VK_VALIDATION"],
+ };
+
+ perGame.RemoveInheritedValues(global);
+
+ Assert.Equal("Debug", perGame.LogLevel);
+ Assert.Equal("2560x1440", perGame.Resolution);
+ Assert.False(perGame.VSync);
+ Assert.Equal(["SHARPEMU_VK_VALIDATION"], perGame.EnvironmentToggles);
+ }
+
+ [Fact]
+ public void RemoveInheritedValues_DisabledEnvironmentEntry_MatchesMissingEntry()
+ {
+ var global = new GuiSettings
+ {
+ EnvironmentToggles = ["SHARPEMU_LOG_IO=0"],
+ };
+ var perGame = new PerGameSettings
+ {
+ EnvironmentToggles = [],
+ };
+
+ perGame.RemoveInheritedValues(global);
+
+ Assert.Null(perGame.EnvironmentToggles);
+ }
}