Implement Dynamic Localization in RenderConfigWindow

This commit is contained in:
copecone committed 2026-10-10 16:25:15 +09:00
1 parent 863dcd2357
commit 6f4c632b41
3 files changed
+415 -165

No files matched your search

+1 -1
View File
@@ -23,7 +23,7 @@
"ui.render_config.audio_encoding": "오디오 인코딩 설정", "ui.render_config.audio_encoding": "오디오 인코딩 설정",
"ui.render_config.global_settings": "전역 설정", "ui.render_config.global_settings": "전역 설정",
"ui.render_config.advanced_settings": "고급 설정", "ui.render_config.advanced_settings": "고급 설정",
"ui.render_config.go_render": "렌더 시작", "ui.render_config.go_render": "렌더링하러 가다",
"ui.render_config.codec": "코덱", "ui.render_config.codec": "코덱",
"ui.render_config.rate_control": "비트레이트 제어 모드", "ui.render_config.rate_control": "비트레이트 제어 모드",
"ui.render_config.bitrate": "비트레이트 (kbps)", "ui.render_config.bitrate": "비트레이트 (kbps)",
+112
View File
@@ -0,0 +1,112 @@
using System;
using ADOFAIRenderer.Localization;
using TMPro;
using UnityEngine;
namespace ADOFAIRenderer.UI;
/// <summary>
/// Component that binds a TextMeshProUGUI component to a localization key or dynamic string provider,
/// automatically updating the displayed text whenever the active language changes.
/// </summary>
internal class LocalizedText : MonoBehaviour
{
private TextMeshProUGUI? _textComponent;
private string? _key;
private Func<string>? _dynamicProvider;
/// <summary>
/// Gets or sets the localization key. Setting this will clear any dynamic provider.
/// </summary>
public string? Key
{
get => _key;
set
{
_key = value;
_dynamicProvider = null;
Refresh();
}
}
/// <summary>
/// Sets a dynamic string provider function.
/// </summary>
public void SetDynamic(Func<string> provider)
{
_dynamicProvider = provider;
_key = null;
Refresh();
}
private void Awake()
{
EnsureComponent();
LocalizationManager.OnLanguageChanged += HandleLanguageChanged;
}
private void OnEnable()
{
Refresh();
}
private void OnDestroy()
{
LocalizationManager.OnLanguageChanged -= HandleLanguageChanged;
}
private void HandleLanguageChanged(SystemLanguage language)
{
Refresh();
}
private void EnsureComponent()
{
if (_textComponent == null)
{
_textComponent = GetComponent<TextMeshProUGUI>();
}
}
/// <summary>
/// Refreshes the text using either the dynamic provider or the static localization key.
/// </summary>
public void Refresh()
{
EnsureComponent();
if (_textComponent == null) return;
if (_dynamicProvider != null)
{
_textComponent.text = _dynamicProvider();
}
else if (!string.IsNullOrEmpty(_key))
{
_textComponent.text = LocalizationManager.Get(_key);
}
}
/// <summary>
/// Binds a TextMeshProUGUI component to a localization key.
/// </summary>
public static LocalizedText Bind(TextMeshProUGUI target, string key)
{
if (target == null) throw new ArgumentNullException(nameof(target));
var component = target.GetComponent<LocalizedText>() ?? target.gameObject.AddComponent<LocalizedText>();
component._textComponent = target;
component.Key = key;
return component;
}
/// <summary>
/// Binds a TextMeshProUGUI component to a dynamic string provider function.
/// </summary>
public static LocalizedText Bind(TextMeshProUGUI target, Func<string> provider)
{
if (target == null) throw new ArgumentNullException(nameof(target));
var component = target.GetComponent<LocalizedText>() ?? target.gameObject.AddComponent<LocalizedText>();
component._textComponent = target;
component.SetDynamic(provider);
return component;
}
}
+302 -164
View File
@@ -87,6 +87,10 @@ internal static class RenderConfigWindow
Size = new Vector2(900, 600), Draggable = true, Resizable = true, Size = new Vector2(900, 600), Draggable = true, Resizable = true,
ShowCloseButton = true ShowCloseButton = true
}); });
if (_configWindow.TitleText != null)
{
LocalizedText.Bind(_configWindow.TitleText, "ui.render_config.window_title");
}
_configWindow.CloseRequested += win => _configWindow.CloseRequested += win =>
{ {
@@ -95,6 +99,7 @@ internal static class RenderConfigWindow
}; };
BuildUI(_configWindow.Content); BuildUI(_configWindow.Content);
LocalizationManager.OnLanguageChanged += OnLanguageChanged;
} }
else else
{ {
@@ -145,14 +150,9 @@ internal static class RenderConfigWindow
}; };
// Presets (Collapsible Card) // Presets (Collapsible Card)
var (_, presetContent) = O5Factory.Card( var (_, presetContent) = CreateCard(
ctx, scrollContent, ctx, scrollContent,
LocalizationManager.Get("ui.render_config.presets"), "ui.render_config.presets"
activeValue: true,
onActiveChanged: null,
onDeleteClick: null,
showDeleteButton: false,
showActiveToggle: false
); );
var presets = PresetManager.GetPresets(); var presets = PresetManager.GetPresets();
@@ -177,7 +177,7 @@ internal static class RenderConfigWindow
_presetDropdown = CreateDropdownField( _presetDropdown = CreateDropdownField(
ctx, presetContent, ctx, presetContent,
LocalizationManager.Get("ui.render_config.preset_select"), "ui.render_config.preset_select",
initialPresetName, initialPresetName, initialPresetName, initialPresetName,
presetNames, p => p, presetNames, p => p,
"dd_presets", "dd_presets",
@@ -213,18 +213,24 @@ internal static class RenderConfigWindow
icon: null, icon: null,
id: "input_preset_name" id: "input_preset_name"
); );
if (_presetNameInput.Placeholder != null)
{
LocalizedText.Bind(_presetNameInput.Placeholder, "ui.render_config.preset_name_placeholder");
}
// ReSharper disable once InconsistentNaming
var inputLE = _presetNameInput.Rect.GetComponent<LayoutElement>() ?? _presetNameInput.Rect.gameObject.AddComponent<LayoutElement>(); var inputLE = _presetNameInput.Rect.GetComponent<LayoutElement>() ?? _presetNameInput.Rect.gameObject.AddComponent<LayoutElement>();
inputLE.flexibleWidth = 1f; inputLE.flexibleWidth = 1f;
inputLE.preferredHeight = 40f; inputLE.preferredHeight = 40f;
_savePresetButton = O5Factory.Button( _savePresetButton = CreateButton(
ctx: ctx, ctx: ctx,
parent: actionsRowRect, parent: actionsRowRect,
onClick: () => OnSavePresetClicked(config), textKey: "ui.render_config.preset_save",
text: LocalizationManager.Get("ui.render_config.preset_save"),
id: "btn_preset_save", id: "btn_preset_save",
onClick: () => OnSavePresetClicked(config),
height: 40f height: 40f
); );
// ReSharper disable once InconsistentNaming
var saveLE = _savePresetButton.Rect.GetComponent<LayoutElement>() ?? _savePresetButton.Rect.gameObject.AddComponent<LayoutElement>(); var saveLE = _savePresetButton.Rect.GetComponent<LayoutElement>() ?? _savePresetButton.Rect.gameObject.AddComponent<LayoutElement>();
saveLE.preferredWidth = 90f; saveLE.preferredWidth = 90f;
saveLE.flexibleWidth = 0f; saveLE.flexibleWidth = 0f;
@@ -241,12 +247,12 @@ internal static class RenderConfigWindow
fonts: ctx.Fonts, fonts: ctx.Fonts,
tween: ctx.Tween tween: ctx.Tween
); );
_deletePresetButton = O5Factory.Button( _deletePresetButton = CreateButton(
ctx: redCtx, ctx: redCtx,
parent: actionsRowRect, parent: actionsRowRect,
onClick: () => OnDeletePresetClicked(config), textKey: "ui.render_config.preset_delete",
text: LocalizationManager.Get("ui.render_config.preset_delete"),
id: "btn_preset_delete", id: "btn_preset_delete",
onClick: () => OnDeletePresetClicked(config),
height: 40f height: 40f
); );
var deleteLE = _deletePresetButton.Rect.GetComponent<LayoutElement>() ?? _deletePresetButton.Rect.gameObject.AddComponent<LayoutElement>(); var deleteLE = _deletePresetButton.Rect.GetComponent<LayoutElement>() ?? _deletePresetButton.Rect.gameObject.AddComponent<LayoutElement>();
@@ -259,86 +265,72 @@ internal static class RenderConfigWindow
// 1-3. Top section: 좌/우 컨테이너 1개씩으로 3행을 쌓는다 (중앙선 1개) // 1-3. Top section: 좌/우 컨테이너 1개씩으로 3행을 쌓는다 (중앙선 1개)
var (topLeft, topRight) = CreateTwoColumnPanel(scrollContent, spacing: 8f, columnSpacing: 10f); var (topLeft, topRight) = CreateTwoColumnPanel(scrollContent, spacing: 8f, columnSpacing: 10f);
_renderFpsSlider = O5Factory.Slider( _renderFpsSlider = CreateSlider(
ctx: ctx, parent: CreateFieldSlot(topLeft), ctx: ctx, parent: CreateFieldSlot(topLeft),
labelKey: "ui.render_config.render_fps", id: "slider_render_fps",
defaultValue: 60, min: 0.01, max: 3000, value: 60, defaultValue: 60, min: 0.01, max: 3000, value: 60,
format: "F3", clampMode: ClampMode.All, format: "F3", clampMode: ClampMode.All,
sliderFilter: value => Snap(renderFpsSnap, value), sliderFilter: value => Snap(renderFpsSnap, value),
inputFilter: null, onComplete: null, onChanged: val => { config.RenderFPS = val; MarkConfigModified(); }, onChanged: val => { config.RenderFPS = val; MarkConfigModified(); }
text: LocalizationManager.Get("ui.render_config.render_fps"),
id: "slider_render_fps", showFill: true
); );
_videoFPSSlider = O5Factory.Slider( _videoFPSSlider = CreateSlider(
ctx: ctx, parent: CreateFieldSlot(topRight), ctx: ctx, parent: CreateFieldSlot(topRight),
labelKey: "ui.render_config.video_fps", id: "slider_video_fps",
defaultValue: 60, min: 0.01, max: 600, value: 60, defaultValue: 60, min: 0.01, max: 600, value: 60,
format: "F3", clampMode: ClampMode.All, format: "F3", clampMode: ClampMode.All,
sliderFilter: value => Snap(videoFpsSnap, value), sliderFilter: value => Snap(videoFpsSnap, value),
inputFilter: null, onComplete: null, onChanged: val => { config.VideoFPS = val; MarkConfigModified(); }, onChanged: val => { config.VideoFPS = val; MarkConfigModified(); }
text: LocalizationManager.Get("ui.render_config.video_fps"),
id: "slider_video_fps", showFill: true
); );
_renderWidthSlider = O5Factory.Slider( _renderWidthSlider = CreateSlider(
ctx: ctx, parent: CreateFieldSlot(topLeft), ctx: ctx, parent: CreateFieldSlot(topLeft),
labelKey: "ui.render_config.render_width", id: "slider_render_width",
defaultValue: 2560, min: 0, max: 15360, value: 2560, defaultValue: 2560, min: 0, max: 15360, value: 2560,
format: "F0", clampMode: ClampMode.All, format: "F0", clampMode: ClampMode.All,
sliderFilter: value => Snap(renderWidthSnap, value), inputFilter: value => Math.Max(value, 2), sliderFilter: value => Snap(renderWidthSnap, value), inputFilter: value => Math.Max(value, 2),
onComplete: null, onChanged: val => { config.RenderWidth = (int)Math.Round(val); MarkConfigModified(); }, onChanged: val => { config.RenderWidth = (int)Math.Round(val); MarkConfigModified(); }
text: LocalizationManager.Get("ui.render_config.render_width"),
id: "slider_render_width", showFill: true
); );
_renderHeightSlider = O5Factory.Slider( _renderHeightSlider = CreateSlider(
ctx: ctx, parent: CreateFieldSlot(topRight), ctx: ctx, parent: CreateFieldSlot(topRight),
labelKey: "ui.render_config.render_height", id: "slider_render_height",
defaultValue: 1440, min: 0, max: 8640, value: 1440, defaultValue: 1440, min: 0, max: 8640, value: 1440,
format: "F0", clampMode: ClampMode.All, format: "F0", clampMode: ClampMode.All,
sliderFilter: value => Snap(renderHeightSnap, value), inputFilter: value => Math.Max(value, 2), sliderFilter: value => Snap(renderHeightSnap, value), inputFilter: value => Math.Max(value, 2),
onComplete: null, onChanged: val => { config.RenderHeight = (int)Math.Round(val); MarkConfigModified(); }, onChanged: val => { config.RenderHeight = (int)Math.Round(val); MarkConfigModified(); }
text: LocalizationManager.Get("ui.render_config.render_height"),
id: "slider_render_height", showFill: true
); );
_delayAfterEndSlider = O5Factory.Slider( _delayAfterEndSlider = CreateSlider(
ctx: ctx, parent: CreateFieldSlot(topLeft), ctx: ctx, parent: CreateFieldSlot(topLeft),
labelKey: "ui.render_config.delay_after_end", id: "slider_delay_after_end",
defaultValue: 15, min: 0, max: 9999.999, value: 15, defaultValue: 15, min: 0, max: 9999.999, value: 15,
format: "F3", clampMode: ClampMode.All, format: "F3", clampMode: ClampMode.All,
onChanged: val => { config.DelayAfterEnd = val; MarkConfigModified(); }
sliderFilter: null, inputFilter: null,
onComplete: null, onChanged: val => { config.DelayAfterEnd = val; MarkConfigModified(); },
text: LocalizationManager.Get("ui.render_config.delay_after_end"),
id: "slider_delay_after_end", showFill: true
); );
_showJudgementToggle = CreateToggleField( _showJudgementToggle = CreateToggleField(
ctx, CreateFieldSlot(topRight), "", ctx, CreateFieldSlot(topRight),
false, config.ShowJudgementText, labelKey: "",
LocalizationManager.Get("ui.render_config.show_judgement"), defaultValue: false,
"toggle_show_judgement", value: config.ShowJudgementText,
val => { config.ShowJudgementText = val; MarkConfigModified(); }, toggleTextKey: "ui.render_config.show_judgement",
id: "toggle_show_judgement",
onChanged: val => { config.ShowJudgementText = val; MarkConfigModified(); },
generateLabel: false generateLabel: false
); );
// 4. Video Encoding Config (Collapsible Card) // 4. Video Encoding Config (Collapsible Card)
var (_, videoContent) = O5Factory.Card( var (_, videoContent) = CreateCard(
ctx, scrollContent, ctx, scrollContent,
LocalizationManager.Get("ui.render_config.video_encoding"), "ui.render_config.video_encoding"
activeValue: true,
onActiveChanged: null,
onDeleteClick: null,
showDeleteButton: false,
showActiveToggle: false
); );
var (videoLeft, videoRight) = CreateTwoColumnPanel(videoContent); var (videoLeft, videoRight) = CreateTwoColumnPanel(videoContent);
var codecs = (Codec[])Enum.GetValues(typeof(Codec)); var codecs = (Codec[])Enum.GetValues(typeof(Codec));
_codecDropdown = CreateDropdownField( _codecDropdown = CreateDropdownField(
ctx, CreateFieldSlot(videoLeft), ctx, CreateFieldSlot(videoLeft),
LocalizationManager.Get("ui.render_config.codec"), "ui.render_config.codec",
Codec.H264, config.VideoConfig.Codec, Codec.H264, config.VideoConfig.Codec,
codecs, c => c.ToString(), codecs, c => c.ToString(),
"dd_video_codec", "dd_video_codec",
@@ -351,7 +343,7 @@ internal static class RenderConfigWindow
_bitrateInput = CreateInputField( _bitrateInput = CreateInputField(
ctx, CreateFieldSlot(videoLeft), ctx, CreateFieldSlot(videoLeft),
LocalizationManager.Get("ui.render_config.bitrate"), "ui.render_config.bitrate",
"30000", config.VideoConfig.Bitrate.ToString(), "30000", config.VideoConfig.Bitrate.ToString(),
"input_bitrate", out _bitrateLabel, "input_bitrate", out _bitrateLabel,
TMPro.TMP_InputField.CharacterValidation.Integer, TMPro.TMP_InputField.CharacterValidation.Integer,
@@ -360,7 +352,7 @@ internal static class RenderConfigWindow
_qpInput = CreateInputField( _qpInput = CreateInputField(
ctx, CreateFieldSlot(videoLeft), ctx, CreateFieldSlot(videoLeft),
LocalizationManager.Get("ui.render_config.qp"), "ui.render_config.qp",
"18", config.VideoConfig.QP.ToString(), "18", config.VideoConfig.QP.ToString(),
"input_qp", out _qpLabel, "input_qp", out _qpLabel,
TMPro.TMP_InputField.CharacterValidation.Integer, TMPro.TMP_InputField.CharacterValidation.Integer,
@@ -370,7 +362,7 @@ internal static class RenderConfigWindow
var rateModes = (RateControlMode[])Enum.GetValues(typeof(RateControlMode)); var rateModes = (RateControlMode[])Enum.GetValues(typeof(RateControlMode));
_rateControlDropdown = CreateDropdownField( _rateControlDropdown = CreateDropdownField(
ctx, CreateFieldSlot(videoRight), ctx, CreateFieldSlot(videoRight),
LocalizationManager.Get("ui.render_config.rate_control"), "ui.render_config.rate_control",
RateControlMode.CBR, config.VideoConfig.RateControlMode, RateControlMode.CBR, config.VideoConfig.RateControlMode,
rateModes, m => m.ToString(), rateModes, m => m.ToString(),
"dd_rate_control", "dd_rate_control",
@@ -384,7 +376,7 @@ internal static class RenderConfigWindow
_maxBitrateInput = CreateInputField( _maxBitrateInput = CreateInputField(
ctx, CreateFieldSlot(videoRight), ctx, CreateFieldSlot(videoRight),
LocalizationManager.Get("ui.render_config.max_bitrate"), "ui.render_config.max_bitrate",
"50000", config.VideoConfig.MaxBitrate.ToString(), "50000", config.VideoConfig.MaxBitrate.ToString(),
"input_max_bitrate", out _maxBitrateLabel, "input_max_bitrate", out _maxBitrateLabel,
TMPro.TMP_InputField.CharacterValidation.Integer, TMPro.TMP_InputField.CharacterValidation.Integer,
@@ -392,61 +384,55 @@ internal static class RenderConfigWindow
); );
_forceSoftwareToggle = CreateToggleField( _forceSoftwareToggle = CreateToggleField(
ctx, CreateFieldSlot(videoRight), "", ctx, CreateFieldSlot(videoRight),
false, config.VideoConfig.ForceSoftwareEncoder, labelKey: "",
LocalizationManager.Get("ui.render_config.force_software"), defaultValue: false,
"toggle_force_software", value: config.VideoConfig.ForceSoftwareEncoder,
val => { config.VideoConfig.ForceSoftwareEncoder = val; MarkConfigModified(); }, toggleTextKey: "ui.render_config.force_software",
id: "toggle_force_software",
onChanged: val => { config.VideoConfig.ForceSoftwareEncoder = val; MarkConfigModified(); },
generateLabel: true generateLabel: true
); );
_keyframeIntervalSlider = O5Factory.Slider( _keyframeIntervalSlider = CreateSlider(
ctx: ctx, parent: videoContent, ctx: ctx, parent: videoContent,
labelKey: "ui.render_config.keyframe_interval", id: "slider_keyframe_interval",
defaultValue: 2, min: 0.1, max: 10, value: 2, defaultValue: 2, min: 0.1, max: 10, value: 2,
format: "F1", clampMode: ClampMode.All, format: "F1", clampMode: ClampMode.All,
onChanged: val => { config.VideoConfig.KeyframeInterval = val; MarkConfigModified(); }
sliderFilter: null, inputFilter: null,
onComplete: null, onChanged: val => { config.VideoConfig.KeyframeInterval = val; MarkConfigModified(); },
text: LocalizationManager.Get("ui.render_config.keyframe_interval"),
id: "slider_keyframe_interval", showFill: true
); );
// 5. Audio Encoding Config (Collapsible Card) // 5. Audio Encoding Config (Collapsible Card)
var (_, audioContent) = O5Factory.Card( var (_, audioContent) = CreateCard(
ctx, scrollContent, ctx, scrollContent,
LocalizationManager.Get("ui.render_config.audio_encoding"), "ui.render_config.audio_encoding"
activeValue: true,
onActiveChanged: null,
onDeleteClick: null,
showDeleteButton: false,
showActiveToggle: false
); );
var (audioLeft, audioRight) = CreateTwoColumnPanel(audioContent); var (audioLeft, audioRight) = CreateTwoColumnPanel(audioContent);
_renderAudioToggle = CreateToggleField( _renderAudioToggle = CreateToggleField(
ctx, CreateFieldSlot(audioLeft), "", ctx, CreateFieldSlot(audioLeft),
true, config.AudioConfig.RenderAudio, labelKey: "",
LocalizationManager.Get("ui.render_config.render_audio"), defaultValue: true,
"toggle_render_audio", value: config.AudioConfig.RenderAudio,
val => { config.AudioConfig.RenderAudio = val; MarkConfigModified(); }, toggleTextKey: "ui.render_config.render_audio",
id: "toggle_render_audio",
onChanged: val => { config.AudioConfig.RenderAudio = val; MarkConfigModified(); },
generateLabel: false generateLabel: false
); );
_audioBitrateSlider = O5Factory.Slider( _audioBitrateSlider = CreateSlider(
ctx: ctx, parent: CreateFieldSlot(audioRight), ctx: ctx, parent: CreateFieldSlot(audioRight),
labelKey: "ui.render_config.audio_bitrate", id: "slider_audio_bitrate",
defaultValue: 256, min: 64, max: 384, value: 256, defaultValue: 256, min: 64, max: 384, value: 256,
format: "F0", clampMode: ClampMode.All, format: "F0", clampMode: ClampMode.All,
sliderFilter: value => Snap(audioBitrateSnap, value), sliderFilter: value => Snap(audioBitrateSnap, value),
inputFilter: null, onComplete: null, onChanged: val => { config.AudioConfig.AudioBitrate = (int)Math.Round(val) * 1000; MarkConfigModified(); }, onChanged: val => { config.AudioConfig.AudioBitrate = (int)Math.Round(val) * 1000; MarkConfigModified(); }
text: LocalizationManager.Get("ui.render_config.audio_bitrate"),
id: "slider_audio_bitrate", showFill: true
); );
var channels = new[] { 1, 2 }; var channels = new[] { 1, 2 };
_channelsDropdown = CreateDropdownField( _channelsDropdown = CreateDropdownField(
ctx, CreateFieldSlot(audioLeft), ctx, CreateFieldSlot(audioLeft),
LocalizationManager.Get("ui.render_config.channels"), "ui.render_config.channels",
2, config.AudioConfig.AudioChannels, 2, config.AudioConfig.AudioChannels,
channels, c => c == 1 ? "1 (Mono)" : "2 (Stereo)", channels, c => c == 1 ? "1 (Mono)" : "2 (Stereo)",
"dd_channels", "dd_channels",
@@ -455,7 +441,7 @@ internal static class RenderConfigWindow
_sampleRateDropdown = CreateDropdownField( _sampleRateDropdown = CreateDropdownField(
ctx, CreateFieldSlot(audioRight), ctx, CreateFieldSlot(audioRight),
LocalizationManager.Get("ui.render_config.sample_rate"), "ui.render_config.sample_rate",
48000, config.AudioConfig.AudioSampleRate, 48000, config.AudioConfig.AudioSampleRate,
CodecUtil.AvailableSampleRate[CodecUtil.AudioCodecMatch[config.VideoConfig.Codec]], r => $"{r/1000.0} kHz", CodecUtil.AvailableSampleRate[CodecUtil.AudioCodecMatch[config.VideoConfig.Codec]], r => $"{r/1000.0} kHz",
"dd_sample_rate", "dd_sample_rate",
@@ -463,19 +449,14 @@ internal static class RenderConfigWindow
); );
// 6. Global Settings (Collapsible Card) // 6. Global Settings (Collapsible Card)
var (_, globalContent) = O5Factory.Card( var (_, globalContent) = CreateCard(
ctx, scrollContent, ctx, scrollContent,
LocalizationManager.Get("ui.render_config.global_settings"), "ui.render_config.global_settings"
activeValue: true,
onActiveChanged: null,
onDeleteClick: null,
showDeleteButton: false,
showActiveToggle: false
); );
_outputDirInput = CreateFullWidthInputField( _outputDirInput = CreateFullWidthInputField(
ctx, globalContent, ctx, globalContent,
LocalizationManager.Get("ui.render_config.output_dir"), "ui.render_config.output_dir",
GlobalSettings.DefaultOutputDirectory, GlobalSettings.DefaultOutputDirectory,
GlobalSettings.EffectiveOutputDirectory, GlobalSettings.EffectiveOutputDirectory,
"input_output_dir", "input_output_dir",
@@ -495,8 +476,10 @@ internal static class RenderConfigWindow
); );
var (globalLeft, globalRight) = CreateTwoColumnPanel(globalContent); var (globalLeft, globalRight) = CreateTwoColumnPanel(globalContent);
O5Factory.Button( CreateButton(
ctx, globalLeft, ctx, globalLeft,
"ui.render_config.browse",
"btn_browse_output",
() => () =>
{ {
var initial = string.IsNullOrWhiteSpace(_outputDirInput.Value) var initial = string.IsNullOrWhiteSpace(_outputDirInput.Value)
@@ -512,39 +495,32 @@ internal static class RenderConfigWindow
GlobalSettings.Save(); GlobalSettings.Save();
} }
}); });
}, }
LocalizationManager.Get("ui.render_config.browse"),
"btn_browse_output"
); );
O5Factory.Button( CreateButton(
ctx, globalRight, ctx, globalRight,
"ui.render_config.reset_default",
"btn_reset_output",
() => () =>
{ {
_outputDirInput.Set(GlobalSettings.DefaultOutputDirectory); _outputDirInput.Set(GlobalSettings.DefaultOutputDirectory);
GlobalSettings.Current.OutputDirectory = null; GlobalSettings.Current.OutputDirectory = null;
GlobalSettings.Save(); GlobalSettings.Save();
}, }
LocalizationManager.Get("ui.render_config.reset_default"),
"btn_reset_output"
); );
// 7. Advanced Settings (Collapsible Card) // 7. Advanced Settings (Collapsible Card)
var (_, advContent) = O5Factory.Card( var (_, advContent) = CreateCard(
ctx, scrollContent, ctx, scrollContent,
LocalizationManager.Get("ui.render_config.advanced_settings"), "ui.render_config.advanced_settings"
activeValue: true,
onActiveChanged: null,
onDeleteClick: null,
showDeleteButton: false,
showActiveToggle: false
); );
var (advLeft, advRight) = CreateTwoColumnPanel(advContent); var (advLeft, advRight) = CreateTwoColumnPanel(advContent);
var extensions = new[] { "mkv", "mp4", "mov", "webm", "avi" }; var extensions = new[] { "mkv", "mp4", "mov", "webm", "avi" };
_fileExtensionDropdown = CreateDropdownField( _fileExtensionDropdown = CreateDropdownField(
ctx, CreateFieldSlot(advLeft), ctx, CreateFieldSlot(advLeft),
LocalizationManager.Get("ui.render_config.file_extension"), "ui.render_config.file_extension",
"mkv", config.AdvancedConfig.FileExtension, "mkv", config.AdvancedConfig.FileExtension,
extensions, e => e, extensions, e => e,
"dd_adv_file_extension", "dd_adv_file_extension",
@@ -556,11 +532,13 @@ internal static class RenderConfigWindow
); );
_processAudioToggle = CreateToggleField( _processAudioToggle = CreateToggleField(
ctx, CreateFieldSlot(advLeft), "", ctx, CreateFieldSlot(advLeft),
true, config.AdvancedConfig.ProcessAudio, labelKey: "",
LocalizationManager.Get("ui.render_config.process_audio"), defaultValue: true,
"toggle_process_audio", value: config.AdvancedConfig.ProcessAudio,
val => toggleTextKey: "ui.render_config.process_audio",
id: "toggle_process_audio",
onChanged: val =>
{ {
config.AdvancedConfig.ProcessAudio = val; config.AdvancedConfig.ProcessAudio = val;
MarkConfigModified(); MarkConfigModified();
@@ -570,7 +548,7 @@ internal static class RenderConfigWindow
_randomSeedInput = CreateInputField( _randomSeedInput = CreateInputField(
ctx, CreateFieldSlot(advRight), ctx, CreateFieldSlot(advRight),
LocalizationManager.Get("ui.render_config.random_seed"), "ui.render_config.random_seed",
"", config.AdvancedConfig.RandomSeed?.ToString() ?? "", "", config.AdvancedConfig.RandomSeed?.ToString() ?? "",
"input_random_seed", "input_random_seed",
TMPro.TMP_InputField.CharacterValidation.Integer, TMPro.TMP_InputField.CharacterValidation.Integer,
@@ -583,11 +561,13 @@ internal static class RenderConfigWindow
); );
_nonTileToggle = CreateToggleField( _nonTileToggle = CreateToggleField(
ctx, CreateFieldSlot(advRight), "", ctx, CreateFieldSlot(advRight),
false, config.AdvancedConfig.NonTile, labelKey: "",
LocalizationManager.Get("ui.render_config.non_tile"), defaultValue: false,
"toggle_non_tile", value: config.AdvancedConfig.NonTile,
val => toggleTextKey: "ui.render_config.non_tile",
id: "toggle_non_tile",
onChanged: val =>
{ {
config.AdvancedConfig.NonTile = val; config.AdvancedConfig.NonTile = val;
MarkConfigModified(); MarkConfigModified();
@@ -596,9 +576,7 @@ internal static class RenderConfigWindow
); );
// 7-1. Planet Color Settings (Optional) // 7-1. Planet Color Settings (Optional)
var planetColorsLabel = O5Factory.ControlText(ctx, advContent, 13f, noPad: true); var planetColorsLabel = CreateLabel(ctx, advContent, "ui.render_config.planet_colors");
planetColorsLabel.text = LocalizationManager.Get("ui.render_config.planet_colors");
planetColorsLabel.color = ctx.Theme.TextDim;
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
var planetLabelLE = planetColorsLabel.gameObject.AddComponent<LayoutElement>(); var planetLabelLE = planetColorsLabel.gameObject.AddComponent<LayoutElement>();
planetLabelLE.preferredHeight = 18f; planetLabelLE.preferredHeight = 18f;
@@ -609,11 +587,13 @@ internal static class RenderConfigWindow
var fireSlot = CreateFieldSlot(fireCol); var fireSlot = CreateFieldSlot(fireCol);
_firePlanetToggle = CreateToggleField( _firePlanetToggle = CreateToggleField(
ctx, fireSlot, "", ctx, fireSlot,
false, config.AdvancedConfig.firePlanetColor.HasValue, labelKey: "",
LocalizationManager.Get("ui.render_config.fire_planet"), defaultValue: false,
"toggle_fire_planet", value: config.AdvancedConfig.firePlanetColor.HasValue,
val => toggleTextKey: "ui.render_config.fire_planet",
id: "toggle_fire_planet",
onChanged: val =>
{ {
_firePlanetPicker?.SetBlocked(!val); _firePlanetPicker?.SetBlocked(!val);
config.AdvancedConfig.firePlanetColor = val ? _firePlanetPicker?.Value : null; config.AdvancedConfig.firePlanetColor = val ? _firePlanetPicker?.Value : null;
@@ -643,11 +623,13 @@ internal static class RenderConfigWindow
var iceSlot = CreateFieldSlot(iceCol); var iceSlot = CreateFieldSlot(iceCol);
_icePlanetToggle = CreateToggleField( _icePlanetToggle = CreateToggleField(
ctx, iceSlot, "", ctx, iceSlot,
false, config.AdvancedConfig.icePlanetColor.HasValue, labelKey: "",
LocalizationManager.Get("ui.render_config.ice_planet"), defaultValue: false,
"toggle_ice_planet", value: config.AdvancedConfig.icePlanetColor.HasValue,
val => toggleTextKey: "ui.render_config.ice_planet",
id: "toggle_ice_planet",
onChanged: val =>
{ {
_icePlanetPicker?.SetBlocked(!val); _icePlanetPicker?.SetBlocked(!val);
config.AdvancedConfig.icePlanetColor = val ? _icePlanetPicker?.Value : null; config.AdvancedConfig.icePlanetColor = val ? _icePlanetPicker?.Value : null;
@@ -677,11 +659,13 @@ internal static class RenderConfigWindow
var windSlot = CreateFieldSlot(windCol); var windSlot = CreateFieldSlot(windCol);
_windPlanetToggle = CreateToggleField( _windPlanetToggle = CreateToggleField(
ctx, windSlot, "", ctx, windSlot,
false, config.AdvancedConfig.windPlanetColor.HasValue, labelKey: "",
LocalizationManager.Get("ui.render_config.wind_planet"), defaultValue: false,
"toggle_wind_planet", value: config.AdvancedConfig.windPlanetColor.HasValue,
val => toggleTextKey: "ui.render_config.wind_planet",
id: "toggle_wind_planet",
onChanged: val =>
{ {
_windPlanetPicker?.SetBlocked(!val); _windPlanetPicker?.SetBlocked(!val);
config.AdvancedConfig.windPlanetColor = val ? _windPlanetPicker?.Value : null; config.AdvancedConfig.windPlanetColor = val ? _windPlanetPicker?.Value : null;
@@ -710,20 +694,17 @@ internal static class RenderConfigWindow
} }
// 8. Go Render Button // 8. Go Render Button
O5Factory.Button( CreateButton(
ctx, scrollContent, ctx, scrollContent,
TryGoRender, "ui.render_config.go_render",
LocalizationManager.Get("ui.render_config.go_render"),
"btn_go_render", "btn_go_render",
onClick: TryGoRender,
height: 48f height: 48f
); );
// 9. Version Label // 9. Version Label
var versionLabel = O5Factory.ControlText(ctx, scrollContent, 12f, noPad: true); var versionLabel = O5Factory.ControlText(ctx, scrollContent, 12f, noPad: true);
var versionText = $"ADOFAI Renderer v{Melon<RenderMod>.Instance.Info.Version}"; LocalizedText.Bind(versionLabel, GetVersionText);
if (DateTime.UtcNow >= RenderMod.ExpirationTime) versionText += " " + LocalizationManager.Get("ui.render_config.expired");
versionLabel.text = versionText;
versionLabel.alignment = TMPro.TextAlignmentOptions.Center; versionLabel.alignment = TMPro.TextAlignmentOptions.Center;
versionLabel.color = ctx.Theme.TextDim; versionLabel.color = ctx.Theme.TextDim;
@@ -953,7 +934,7 @@ internal static class RenderConfigWindow
{ {
SyncUItoConfig(config); SyncUItoConfig(config);
var name = _presetNameInput?.Value?.Trim(); var name = _presetNameInput?.Value.Trim();
if (string.IsNullOrWhiteSpace(name) || name == CustomPresetLabel) if (string.IsNullOrWhiteSpace(name) || name == CustomPresetLabel)
{ {
CommonWarningDialogUI.ShowDialog( CommonWarningDialogUI.ShowDialog(
@@ -1087,6 +1068,26 @@ internal static class RenderConfigWindow
_deletePresetButton?.SetBlocked(!canDelete); _deletePresetButton?.SetBlocked(!canDelete);
} }
private static void OnLanguageChanged(SystemLanguage language)
{
if (_configWindow == null) return;
if (_presetDropdown != null)
{
var cur = _presetDropdown.Value;
var isCustom = cur == "(Modified)" || cur == "(수정됨)" || cur == CustomPresetLabel;
ReloadPresetDropdown(isCustom ? CustomPresetLabel : cur);
}
}
private static string GetVersionText()
{
var versionText = $"ADOFAI Renderer v{Melon<RenderMod>.Instance.Info.Version}";
if (DateTime.UtcNow >= RenderMod.ExpirationTime)
versionText += " " + LocalizationManager.Get("ui.render_config.expired");
return versionText;
}
private static void ShowValidationError(string message) private static void ShowValidationError(string message)
{ {
Melon<RenderMod>.Logger.Error(message); Melon<RenderMod>.Logger.Error(message);
@@ -1189,23 +1190,139 @@ internal static class RenderConfigWindow
return slotRect; return slotRect;
} }
// ReSharper disable once UnusedTupleComponentInReturnValue
private static (RectTransform card, RectTransform content) CreateCard(
O5Context ctx,
Transform parent,
string titleKey,
bool activeValue = true,
Action<bool>? onActiveChanged = null,
Action? onDeleteClick = null,
bool showDeleteButton = false,
bool showActiveToggle = false)
{
var (card, content) = O5Factory.Card(
ctx,
parent,
LocalizationManager.Get(titleKey),
activeValue,
onActiveChanged,
onDeleteClick,
showDeleteButton,
showActiveToggle
);
var titleTmp = card.Find("Header/TitleTrigger/TitleText")?.GetComponent<TMPro.TextMeshProUGUI>()
?? card.GetComponentsInChildren<TMPro.TextMeshProUGUI>(true).FirstOrDefault(t => t.gameObject.name == "TitleText");
if (titleTmp)
{
titleTmp.fontSize = 18f;
LocalizedText.Bind(titleTmp, titleKey);
}
return (card, content);
}
private static TMPro.TextMeshProUGUI CreateLabel(
O5Context ctx,
Transform parent,
string textKey,
float fontSize = 13f,
bool noPad = true,
Color? color = null)
{
var label = O5Factory.ControlText(ctx, parent, fontSize, noPad: noPad);
label.text = LocalizationManager.Get(textKey);
label.color = color ?? ctx.Theme.TextDim;
LocalizedText.Bind(label, textKey);
return label;
}
private static O5Slider CreateSlider(
O5Context ctx,
Transform parent,
string labelKey,
string id,
double defaultValue,
double min,
double max,
double value,
string format = "F0",
ClampMode clampMode = ClampMode.All,
Func<double, double>? sliderFilter = null,
Func<double, double>? inputFilter = null,
Action<double>? onChanged = null,
Action<double>? onComplete = null,
bool showFill = true)
{
var slider = O5Factory.Slider(
ctx: ctx,
parent: parent,
defaultValue: defaultValue,
min: min,
max: max,
value: value,
format: format,
clampMode: clampMode,
sliderFilter: sliderFilter,
inputFilter: inputFilter,
onComplete: onComplete,
onChanged: onChanged,
text: LocalizationManager.Get(labelKey),
id: id,
showFill: showFill
);
if (slider.Label != null)
{
LocalizedText.Bind(slider.Label, labelKey);
}
return slider;
}
private static O5Button CreateButton(
O5Context ctx,
Transform parent,
string textKey,
string id,
Action onClick,
float? height = null)
{
var button = O5Factory.Button(
ctx: ctx,
parent: parent,
onClick: onClick,
text: LocalizationManager.Get(textKey),
id: id,
height: height
);
if (button.Label != null)
{
LocalizedText.Bind(button.Label, textKey);
}
return button;
}
private static O5InputField CreateInputField( private static O5InputField CreateInputField(
O5Context ctx, O5Context ctx,
Transform colParent, Transform colParent,
string labelText, string labelKey,
string? defaultValue, string? defaultValue,
string value, string value,
string id, string id,
TMPro.TMP_InputField.CharacterValidation characterValidation = TMPro.TMP_InputField.CharacterValidation.None, TMPro.TMP_InputField.CharacterValidation characterValidation = TMPro.TMP_InputField.CharacterValidation.None,
Action<string>? onChanged = null) Action<string>? onChanged = null)
{ {
return CreateInputField(ctx, colParent, labelText, defaultValue, value, id, out _, characterValidation, onChanged); return CreateInputField(ctx, colParent, labelKey, defaultValue, value, id, out _, characterValidation, onChanged);
} }
private static O5InputField CreateInputField( private static O5InputField CreateInputField(
O5Context ctx, O5Context ctx,
Transform colParent, Transform colParent,
string labelText, string labelKey,
string? defaultValue, string? defaultValue,
string value, string value,
string id, string id,
@@ -1214,8 +1331,9 @@ internal static class RenderConfigWindow
Action<string>? onChanged = null) Action<string>? onChanged = null)
{ {
label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true);
label.text = labelText; label.text = LocalizationManager.Get(labelKey);
label.color = ctx.Theme.TextDim; label.color = ctx.Theme.TextDim;
LocalizedText.Bind(label, labelKey);
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
var labelLE = label.gameObject.AddComponent<LayoutElement>(); var labelLE = label.gameObject.AddComponent<LayoutElement>();
@@ -1228,11 +1346,16 @@ internal static class RenderConfigWindow
defaultValue: defaultValue, defaultValue: defaultValue,
value: value, value: value,
onChanged: onChanged, onChanged: onChanged,
placeholder: labelText, placeholder: LocalizationManager.Get(labelKey),
icon: null, icon: null,
id: id id: id
); );
if (inputField.Placeholder != null)
{
LocalizedText.Bind(inputField.Placeholder, labelKey);
}
inputField.InputField.characterValidation = characterValidation; inputField.InputField.characterValidation = characterValidation;
return inputField; return inputField;
} }
@@ -1291,7 +1414,7 @@ internal static class RenderConfigWindow
private static O5InputField CreateFullWidthInputField( private static O5InputField CreateFullWidthInputField(
O5Context ctx, O5Context ctx,
Transform parent, Transform parent,
string labelText, string labelKey,
string? defaultValue, string? defaultValue,
string value, string value,
string id, string id,
@@ -1309,8 +1432,9 @@ internal static class RenderConfigWindow
layout.childForceExpandHeight = false; layout.childForceExpandHeight = false;
var label = O5Factory.ControlText(ctx, containerRect, 13f, noPad: true); var label = O5Factory.ControlText(ctx, containerRect, 13f, noPad: true);
label.text = labelText; label.text = LocalizationManager.Get(labelKey);
label.color = ctx.Theme.TextDim; label.color = ctx.Theme.TextDim;
LocalizedText.Bind(label, labelKey);
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
var labelLE = label.gameObject.AddComponent<LayoutElement>(); var labelLE = label.gameObject.AddComponent<LayoutElement>();
@@ -1323,30 +1447,36 @@ internal static class RenderConfigWindow
defaultValue: defaultValue, defaultValue: defaultValue,
value: value, value: value,
onChanged: onChanged, onChanged: onChanged,
placeholder: labelText, placeholder: LocalizationManager.Get(labelKey),
icon: null, icon: null,
id: id id: id
); );
if (input.Placeholder != null)
{
LocalizedText.Bind(input.Placeholder, labelKey);
}
return input; return input;
} }
private static O5Toggle CreateToggleField( private static O5Toggle CreateToggleField(
O5Context ctx, O5Context ctx,
Transform colParent, Transform colParent,
string labelText, string labelKey,
bool? defaultValue, bool? defaultValue,
bool value, bool value,
string toggleText, string toggleTextKey,
string id, string id,
Action<bool>? onChanged = null, Action<bool>? onChanged = null,
bool generateLabel = true) bool generateLabel = true)
{ {
if (generateLabel) if (generateLabel && !string.IsNullOrEmpty(labelKey))
{ {
var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true);
label.text = labelText; label.text = LocalizationManager.Get(labelKey);
label.color = ctx.Theme.TextDim; label.color = ctx.Theme.TextDim;
LocalizedText.Bind(label, labelKey);
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
var labelLE = label.gameObject.AddComponent<LayoutElement>(); var labelLE = label.gameObject.AddComponent<LayoutElement>();
@@ -1354,21 +1484,28 @@ internal static class RenderConfigWindow
labelLE.minHeight = 18f; labelLE.minHeight = 18f;
} }
return O5Factory.Toggle( var toggle = O5Factory.Toggle(
ctx: ctx, ctx: ctx,
parent: colParent, parent: colParent,
defaultValue: defaultValue, defaultValue: defaultValue,
value: value, value: value,
onChanged: onChanged, onChanged: onChanged,
text: toggleText, text: !string.IsNullOrEmpty(toggleTextKey) ? LocalizationManager.Get(toggleTextKey) : "",
id: id id: id
); );
if (toggle.Label != null && !string.IsNullOrEmpty(toggleTextKey))
{
LocalizedText.Bind(toggle.Label, toggleTextKey);
}
return toggle;
} }
private static O5Dropdown<T> CreateDropdownField<T>( private static O5Dropdown<T> CreateDropdownField<T>(
O5Context ctx, O5Context ctx,
Transform colParent, Transform colParent,
string labelText, string labelKey,
T defaultValue, T defaultValue,
T value, T value,
IReadOnlyList<T> values, IReadOnlyList<T> values,
@@ -1377,8 +1514,9 @@ internal static class RenderConfigWindow
Action<T>? onChanged = null) Action<T>? onChanged = null)
{ {
var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true);
label.text = labelText; label.text = LocalizationManager.Get(labelKey);
label.color = ctx.Theme.TextDim; label.color = ctx.Theme.TextDim;
LocalizedText.Bind(label, labelKey);
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
var labelLE = label.gameObject.AddComponent<LayoutElement>(); var labelLE = label.gameObject.AddComponent<LayoutElement>();