SHA256
Add Segmented Control UI for Output Mode Option
This commit is contained in:
6 files changed
+681
-69
No files matched your search
@@ -11,6 +11,11 @@ namespace ADOFAIRenderer.Data;
|
|||||||
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
||||||
public class RenderConfig
|
public class RenderConfig
|
||||||
{
|
{
|
||||||
|
public enum RenderOutputMode
|
||||||
|
{
|
||||||
|
Video, PNGSequence
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Game frames simulated per second of render time (the game clock advances 1 / RenderFPS seconds per frame).
|
/// Game frames simulated per second of render time (the game clock advances 1 / RenderFPS seconds per frame).
|
||||||
/// Values above <see cref="VideoFPS"/> render extra frames between video frames, which are not captured
|
/// Values above <see cref="VideoFPS"/> render extra frames between video frames, which are not captured
|
||||||
@@ -44,6 +49,9 @@ public class RenderConfig
|
|||||||
/// <summary>Whether the judgement texts (hit accuracy texts) are shown during the render (scrHitTextManagerPatch). Default false.</summary>
|
/// <summary>Whether the judgement texts (hit accuracy texts) are shown during the render (scrHitTextManagerPatch). Default false.</summary>
|
||||||
public bool ShowJudgementText { get; set; } = false;
|
public bool ShowJudgementText { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>Determines whether to export the rendered output as an encoded video or a PNG image sequence.</summary>
|
||||||
|
public RenderOutputMode OutputMode { get; set; } = RenderOutputMode.Video;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copies every value of <paramref name="source"/> into this object; the nested configs are copied value by value
|
/// Copies every value of <paramref name="source"/> into this object; the nested configs are copied value by value
|
||||||
/// into the existing nested objects. Does nothing when <paramref name="source"/> is null.
|
/// into the existing nested objects. Does nothing when <paramref name="source"/> is null.
|
||||||
@@ -57,6 +65,7 @@ public class RenderConfig
|
|||||||
RenderHeight = source.RenderHeight;
|
RenderHeight = source.RenderHeight;
|
||||||
DelayAfterEnd = source.DelayAfterEnd;
|
DelayAfterEnd = source.DelayAfterEnd;
|
||||||
ShowJudgementText = source.ShowJudgementText;
|
ShowJudgementText = source.ShowJudgementText;
|
||||||
|
OutputMode = source.OutputMode;
|
||||||
|
|
||||||
VideoConfig.CopyFrom(source.VideoConfig);
|
VideoConfig.CopyFrom(source.VideoConfig);
|
||||||
AudioConfig.CopyFrom(source.AudioConfig);
|
AudioConfig.CopyFrom(source.AudioConfig);
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
"ui.render_config.sample_rate": "Sample Rate (Hz)",
|
"ui.render_config.sample_rate": "Sample Rate (Hz)",
|
||||||
"ui.render_config.channels": "Audio Channels",
|
"ui.render_config.channels": "Audio Channels",
|
||||||
"ui.render_config.audio_bitrate": "Audio Bitrate (kbps)",
|
"ui.render_config.audio_bitrate": "Audio Bitrate (kbps)",
|
||||||
|
"ui.render_config.output_mode": "Output Mode",
|
||||||
|
"ui.render_config.output_mode_video": "Video",
|
||||||
|
"ui.render_config.output_mode_png_sequence": "PNG Sequence",
|
||||||
"ui.render_config.output_dir": "Output Directory",
|
"ui.render_config.output_dir": "Output Directory",
|
||||||
"ui.render_config.browse": "Browse...",
|
"ui.render_config.browse": "Browse...",
|
||||||
"ui.render_config.reset_default": "Reset to Default",
|
"ui.render_config.reset_default": "Reset to Default",
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
"ui.render_config.sample_rate": "샘플레이트 (Hz)",
|
"ui.render_config.sample_rate": "샘플레이트 (Hz)",
|
||||||
"ui.render_config.channels": "오디오 채널",
|
"ui.render_config.channels": "오디오 채널",
|
||||||
"ui.render_config.audio_bitrate": "오디오 비트레이트 (kbps)",
|
"ui.render_config.audio_bitrate": "오디오 비트레이트 (kbps)",
|
||||||
|
"ui.render_config.output_mode": "출력 모드",
|
||||||
|
"ui.render_config.output_mode_video": "동영상",
|
||||||
|
"ui.render_config.output_mode_png_sequence": "PNG 시퀀스",
|
||||||
"ui.render_config.output_dir": "출력 디렉토리",
|
"ui.render_config.output_dir": "출력 디렉토리",
|
||||||
"ui.render_config.browse": "찾아보기...",
|
"ui.render_config.browse": "찾아보기...",
|
||||||
"ui.render_config.reset_default": "기본값으로 초기화",
|
"ui.render_config.reset_default": "기본값으로 초기화",
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ internal class RendererUIManager : MonoBehaviour
|
|||||||
ObjectActiveBright = new Color(1.000f, 0.294f, 0.079f, 1f),
|
ObjectActiveBright = new Color(1.000f, 0.294f, 0.079f, 1f),
|
||||||
ControlOutlineIdle = new Color(1f, 1f, 1f, 0.4f),
|
ControlOutlineIdle = new Color(1f, 1f, 1f, 0.4f),
|
||||||
ButtonHover = new Color(0.133f, 0.133f, 0.133f, 1.0f),
|
ButtonHover = new Color(0.133f, 0.133f, 0.133f, 1.0f),
|
||||||
|
ButtonPressed = new Color(0.133f, 0.133f, 0.133f, 1.0f),
|
||||||
ControlOutlineHover = new Color(1f, 1f, 1f, 0.4f),
|
ControlOutlineHover = new Color(1f, 1f, 1f, 0.4f),
|
||||||
CardHeader = new Color(0.157f, 0f, 0f, 1f),
|
CardHeader = new Color(0.157f, 0f, 0f, 1f),
|
||||||
CardPanel = new Color(0, 0, 0, 0f),
|
CardPanel = new Color(0, 0, 0, 0f),
|
||||||
|
|||||||
@@ -0,0 +1,405 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using O5Kit.Behaviour;
|
||||||
|
using O5Kit.Core;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.EventSystems;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace ADOFAIRenderer.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Definition of a single option within a <see cref="SegmentedControl{T}"/>.
|
||||||
|
/// Supports static strings, dynamic providers, or localization keys.
|
||||||
|
/// </summary>
|
||||||
|
internal class SegmentOption<T>
|
||||||
|
{
|
||||||
|
public T Value { get; set; }
|
||||||
|
public string? LabelKey { get; set; }
|
||||||
|
public Func<string>? DynamicLabel { get; set; }
|
||||||
|
public string? StaticLabel { get; set; }
|
||||||
|
|
||||||
|
public SegmentOption(T value, string labelKey)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
LabelKey = labelKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SegmentOption(T value, Func<string> dynamicLabel)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
DynamicLabel = dynamicLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SegmentOption(T value, string? labelKey = null, string? staticLabel = null)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
LabelKey = labelKey;
|
||||||
|
StaticLabel = staticLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator SegmentOption<T>((T value, string labelKey) tuple)
|
||||||
|
=> new(tuple.value, tuple.labelKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A generalized rounded segmented button group / picker UI component.
|
||||||
|
/// Visual styling, colors, and dimensions are pulled directly from <see cref="O5Theme"/> (matching <see cref="O5Kit.Control.O5Button"/>).
|
||||||
|
/// Automatically updates localized labels via <see cref="LocalizedText"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal class SegmentedControl<T> : O5Object
|
||||||
|
{
|
||||||
|
private readonly List<SegmentItem> _items = new();
|
||||||
|
private T _value;
|
||||||
|
private bool _blocked;
|
||||||
|
|
||||||
|
public T Value => _value;
|
||||||
|
public T DefaultValue { get; }
|
||||||
|
public IReadOnlyList<SegmentOption<T>> Options { get; }
|
||||||
|
public Action<T>? OnChanged { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Resting background color for unselected segments (defaults to Theme.ObjectButton).</summary>
|
||||||
|
public Color NormalColor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Resting background color for the selected segment (defaults to Theme.ObjectActive).</summary>
|
||||||
|
public Color ActiveColor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Border and outline color (defaults to Theme.ControlOutlineIdle).</summary>
|
||||||
|
public Color BorderColor { get; set; }
|
||||||
|
|
||||||
|
private class SegmentItem
|
||||||
|
{
|
||||||
|
public SegmentOption<T> Option { get; }
|
||||||
|
public GameObject RootObject { get; }
|
||||||
|
public Image Background { get; }
|
||||||
|
public TextMeshProUGUI Label { get; }
|
||||||
|
public ITweenHandle? HoverTween { get; set; }
|
||||||
|
public bool Hovered { get; set; }
|
||||||
|
public bool Pressed { get; set; }
|
||||||
|
|
||||||
|
public SegmentItem(SegmentOption<T> option, GameObject root, Image bg, TextMeshProUGUI label)
|
||||||
|
{
|
||||||
|
Option = option;
|
||||||
|
RootObject = root;
|
||||||
|
Background = bg;
|
||||||
|
Label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Tint(O5Context ctx, Color target, float duration)
|
||||||
|
{
|
||||||
|
HoverTween?.Kill();
|
||||||
|
var bg = Background;
|
||||||
|
HoverTween = ctx.Tween.TweenColor(() => bg.color, v => bg.color = v, target, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SegmentedControl(
|
||||||
|
O5Context ctx,
|
||||||
|
string id,
|
||||||
|
RectTransform rect,
|
||||||
|
T defaultValue,
|
||||||
|
T initialValue,
|
||||||
|
IReadOnlyList<SegmentOption<T>> options,
|
||||||
|
Action<T>? onChanged = null)
|
||||||
|
: base(ctx, id, rect)
|
||||||
|
{
|
||||||
|
DefaultValue = defaultValue;
|
||||||
|
_value = initialValue;
|
||||||
|
Options = options;
|
||||||
|
OnChanged = onChanged;
|
||||||
|
|
||||||
|
// Pull colors directly from O5Theme, identical to O5Button / O5Toggle
|
||||||
|
NormalColor = Ctx.Theme.ObjectButton;
|
||||||
|
ActiveColor = Ctx.Theme.ObjectActive;
|
||||||
|
BorderColor = Ctx.Theme.ControlOutlineIdle;
|
||||||
|
|
||||||
|
BuildSegments();
|
||||||
|
UpdateVisual(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates and attaches a new rounded <see cref="SegmentedControl{T}"/> to <paramref name="parent"/>.
|
||||||
|
/// Height matches standard buttons and sliders (<see cref="O5Theme.ControlHeightFor"/>).
|
||||||
|
/// </summary>
|
||||||
|
public static SegmentedControl<T> Create(
|
||||||
|
O5Context ctx,
|
||||||
|
Transform parent,
|
||||||
|
string id,
|
||||||
|
T defaultValue,
|
||||||
|
T initialValue,
|
||||||
|
IReadOnlyList<SegmentOption<T>> options,
|
||||||
|
float? height = null,
|
||||||
|
Action<T>? onChanged = null)
|
||||||
|
{
|
||||||
|
var h = height ?? ctx.Theme.ControlHeightFor(ctx.Theme.FontSizeBody);
|
||||||
|
var controlObj = new GameObject($"SegmentedControl_{id}");
|
||||||
|
controlObj.transform.SetParent(parent, false);
|
||||||
|
|
||||||
|
var rect = controlObj.AddComponent<RectTransform>();
|
||||||
|
var le = controlObj.AddComponent<LayoutElement>();
|
||||||
|
le.minHeight = h;
|
||||||
|
le.preferredHeight = h;
|
||||||
|
le.flexibleWidth = 1f;
|
||||||
|
|
||||||
|
return new SegmentedControl<T>(ctx, id, rect, defaultValue, initialValue, options, onChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildSegments()
|
||||||
|
{
|
||||||
|
var theme = Ctx.Theme;
|
||||||
|
|
||||||
|
// 1. Masked container for rounded corners
|
||||||
|
var maskObj = new GameObject("MaskContainer");
|
||||||
|
maskObj.transform.SetParent(Rect, false);
|
||||||
|
|
||||||
|
var maskRect = maskObj.AddComponent<RectTransform>();
|
||||||
|
maskRect.anchorMin = Vector2.zero;
|
||||||
|
maskRect.anchorMax = Vector2.one;
|
||||||
|
maskRect.offsetMin = Vector2.zero;
|
||||||
|
maskRect.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
var maskImg = maskObj.AddComponent<Image>();
|
||||||
|
maskImg.sprite = Ctx.Sprites.RoundedControl;
|
||||||
|
maskImg.type = Image.Type.Sliced;
|
||||||
|
maskImg.color = NormalColor;
|
||||||
|
|
||||||
|
var mask = maskObj.AddComponent<Mask>();
|
||||||
|
mask.showMaskGraphic = true;
|
||||||
|
|
||||||
|
// Continuous horizontal layout with no middle divider line
|
||||||
|
var layout = maskObj.AddComponent<HorizontalLayoutGroup>();
|
||||||
|
layout.padding = new RectOffset(0, 0, 0, 0);
|
||||||
|
layout.spacing = 0f;
|
||||||
|
layout.childControlWidth = true;
|
||||||
|
layout.childControlHeight = true;
|
||||||
|
layout.childForceExpandWidth = true;
|
||||||
|
layout.childForceExpandHeight = true;
|
||||||
|
|
||||||
|
for (var i = 0; i < Options.Count; i++)
|
||||||
|
{
|
||||||
|
var opt = Options[i];
|
||||||
|
var segmentObj = new GameObject($"Segment_{i}_{opt.Value}");
|
||||||
|
segmentObj.transform.SetParent(maskRect, false);
|
||||||
|
|
||||||
|
var segRect = segmentObj.AddComponent<RectTransform>();
|
||||||
|
var segLE = segmentObj.AddComponent<LayoutElement>();
|
||||||
|
segLE.minWidth = 0f;
|
||||||
|
segLE.preferredWidth = 0f;
|
||||||
|
segLE.flexibleWidth = 1f;
|
||||||
|
|
||||||
|
var bgImage = segmentObj.AddComponent<Image>();
|
||||||
|
bgImage.color = NormalColor;
|
||||||
|
bgImage.raycastTarget = true;
|
||||||
|
|
||||||
|
var textObj = new GameObject("Text");
|
||||||
|
textObj.transform.SetParent(segmentObj.transform, false);
|
||||||
|
|
||||||
|
var textRect = textObj.AddComponent<RectTransform>();
|
||||||
|
textRect.anchorMin = Vector2.zero;
|
||||||
|
textRect.anchorMax = Vector2.one;
|
||||||
|
textRect.offsetMin = Vector2.zero;
|
||||||
|
textRect.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
var tmp = textObj.AddComponent<TextMeshProUGUI>();
|
||||||
|
tmp.font = Ctx.Fonts.Regular;
|
||||||
|
tmp.fontSize = theme.FontSizeBody;
|
||||||
|
tmp.alignment = TextAlignmentOptions.Center;
|
||||||
|
tmp.characterSpacing = -1f;
|
||||||
|
tmp.raycastTarget = false;
|
||||||
|
|
||||||
|
// Bind localization
|
||||||
|
if (!string.IsNullOrEmpty(opt.LabelKey))
|
||||||
|
{
|
||||||
|
LocalizedText.Bind(tmp, opt.LabelKey);
|
||||||
|
}
|
||||||
|
else if (opt.DynamicLabel != null)
|
||||||
|
{
|
||||||
|
LocalizedText.Bind(tmp, opt.DynamicLabel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tmp.text = opt.StaticLabel ?? opt.Value?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new SegmentItem(opt, segmentObj, bgImage, tmp);
|
||||||
|
_items.Add(item);
|
||||||
|
|
||||||
|
// Wire pointer events like O5Button
|
||||||
|
var trigger = segmentObj.AddComponent<EventTrigger>();
|
||||||
|
UnityUtils.AddEvents(trigger,
|
||||||
|
(EventTriggerType.PointerEnter, _ => OnSegmentHoverEnter(item)),
|
||||||
|
(EventTriggerType.PointerExit, _ => OnSegmentHoverExit(item)),
|
||||||
|
(EventTriggerType.PointerDown, e =>
|
||||||
|
{
|
||||||
|
if (IsLeftPress(e))
|
||||||
|
{
|
||||||
|
OnSegmentPressEnter(item);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(EventTriggerType.PointerUp, _ => OnSegmentPressExit(item)),
|
||||||
|
(EventTriggerType.PointerClick, e =>
|
||||||
|
{
|
||||||
|
if (IsLeftPress(e))
|
||||||
|
{
|
||||||
|
SelectOption(item.Option.Value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Rounded outline overlay on top of masked container
|
||||||
|
var outlineObj = new GameObject("Outline");
|
||||||
|
outlineObj.transform.SetParent(Rect, false);
|
||||||
|
|
||||||
|
var outlineRect = outlineObj.AddComponent<RectTransform>();
|
||||||
|
outlineRect.anchorMin = Vector2.zero;
|
||||||
|
outlineRect.anchorMax = Vector2.one;
|
||||||
|
outlineRect.offsetMin = Vector2.zero;
|
||||||
|
outlineRect.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
var outlineImg = outlineObj.AddComponent<Image>();
|
||||||
|
outlineImg.sprite = Ctx.Sprites.RoundedOutline;
|
||||||
|
outlineImg.type = Image.Type.Sliced;
|
||||||
|
outlineImg.color = BorderColor;
|
||||||
|
outlineImg.raycastTarget = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsLeftPress(BaseEventData e) =>
|
||||||
|
e is PointerEventData ped && ped.button == PointerEventData.InputButton.Left;
|
||||||
|
|
||||||
|
private void OnSegmentHoverEnter(SegmentItem item)
|
||||||
|
{
|
||||||
|
if (IsDisposed || _blocked) return;
|
||||||
|
item.Hovered = true;
|
||||||
|
if (item.Pressed) return;
|
||||||
|
|
||||||
|
var isSelected = EqualityComparer<T>.Default.Equals(item.Option.Value, _value);
|
||||||
|
var targetColor = isSelected ? Ctx.Theme.ObjectActiveBright : Ctx.Theme.ButtonHover;
|
||||||
|
item.Tint(Ctx, targetColor, 0.12f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSegmentHoverExit(SegmentItem item)
|
||||||
|
{
|
||||||
|
if (IsDisposed) return;
|
||||||
|
item.Hovered = false;
|
||||||
|
if (item.Pressed) return;
|
||||||
|
|
||||||
|
var isSelected = EqualityComparer<T>.Default.Equals(item.Option.Value, _value);
|
||||||
|
var targetColor = isSelected ? ActiveColor : NormalColor;
|
||||||
|
item.Tint(Ctx, targetColor, 0.12f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSegmentPressEnter(SegmentItem item)
|
||||||
|
{
|
||||||
|
if (IsDisposed || _blocked) return;
|
||||||
|
item.Pressed = true;
|
||||||
|
var isSelected = EqualityComparer<T>.Default.Equals(item.Option.Value, _value);
|
||||||
|
var targetColor = isSelected ? Ctx.Theme.ObjectActiveBright : Ctx.Theme.ButtonHover;
|
||||||
|
item.Tint(Ctx, targetColor, 0.08f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSegmentPressExit(SegmentItem item)
|
||||||
|
{
|
||||||
|
if (IsDisposed || !item.Pressed) return;
|
||||||
|
item.Pressed = false;
|
||||||
|
|
||||||
|
var isSelected = EqualityComparer<T>.Default.Equals(item.Option.Value, _value);
|
||||||
|
Color settle;
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
settle = item.Hovered ? Ctx.Theme.ObjectActiveBright : ActiveColor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
settle = item.Hovered ? Ctx.Theme.ButtonHover : NormalColor;
|
||||||
|
}
|
||||||
|
item.Tint(Ctx, settle, 0.12f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SelectOption(T newValue, bool notify = true)
|
||||||
|
{
|
||||||
|
if (IsDisposed) return;
|
||||||
|
if (EqualityComparer<T>.Default.Equals(_value, newValue)) return;
|
||||||
|
|
||||||
|
_value = newValue;
|
||||||
|
UpdateVisual(false);
|
||||||
|
|
||||||
|
if (notify)
|
||||||
|
{
|
||||||
|
OnChanged?.Invoke(_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(T value, bool notify = true)
|
||||||
|
{
|
||||||
|
if (IsDisposed) return;
|
||||||
|
if (EqualityComparer<T>.Default.Equals(_value, value)) return;
|
||||||
|
|
||||||
|
_value = value;
|
||||||
|
UpdateVisual(false);
|
||||||
|
|
||||||
|
if (notify)
|
||||||
|
{
|
||||||
|
OnChanged?.Invoke(_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset(bool notify = true)
|
||||||
|
{
|
||||||
|
Set(DefaultValue, notify);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetBlocked(bool blocked, bool noAnimate = false)
|
||||||
|
{
|
||||||
|
_blocked = blocked;
|
||||||
|
base.SetBlocked(blocked, noAnimate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateVisual(bool noAnimate = false)
|
||||||
|
{
|
||||||
|
if (IsDisposed) return;
|
||||||
|
|
||||||
|
foreach (var item in _items)
|
||||||
|
{
|
||||||
|
item.Pressed = false;
|
||||||
|
var isSelected = EqualityComparer<T>.Default.Equals(item.Option.Value, _value);
|
||||||
|
Color targetBg;
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
targetBg = item.Hovered ? Ctx.Theme.ObjectActiveBright : ActiveColor;
|
||||||
|
item.Label.color = Ctx.Theme.Text;
|
||||||
|
item.Label.fontStyle = FontStyles.Bold;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetBg = item.Hovered ? Ctx.Theme.ButtonHover : NormalColor;
|
||||||
|
item.Label.color = Ctx.Theme.TextDim;
|
||||||
|
item.Label.fontStyle = FontStyles.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noAnimate)
|
||||||
|
{
|
||||||
|
item.HoverTween?.Kill();
|
||||||
|
item.Background.color = targetBg;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
item.Tint(Ctx, targetBg, 0.15f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
if (IsDisposed) return;
|
||||||
|
|
||||||
|
foreach (var item in _items)
|
||||||
|
{
|
||||||
|
item.HoverTween?.Kill();
|
||||||
|
item.HoverTween = null;
|
||||||
|
}
|
||||||
|
OnChanged = null;
|
||||||
|
base.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
+260
-69
@@ -13,6 +13,7 @@ using Newtonsoft.Json;
|
|||||||
using O5Kit.Control;
|
using O5Kit.Control;
|
||||||
using O5Kit.Core;
|
using O5Kit.Core;
|
||||||
using O5Kit.Factory;
|
using O5Kit.Factory;
|
||||||
|
using TMPro;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
|
||||||
@@ -105,25 +106,34 @@ internal static class RenderConfigWindow
|
|||||||
private static O5Slider? _renderHeightSlider;
|
private static O5Slider? _renderHeightSlider;
|
||||||
private static O5Slider? _delayAfterEndSlider;
|
private static O5Slider? _delayAfterEndSlider;
|
||||||
private static O5Toggle? _showJudgementToggle;
|
private static O5Toggle? _showJudgementToggle;
|
||||||
|
private static SegmentedControl<RenderConfig.RenderOutputMode>? _outputModeControl;
|
||||||
|
|
||||||
private static O5Dropdown<Codec>? _codecDropdown;
|
private static O5Dropdown<Codec>? _codecDropdown;
|
||||||
|
private static TextMeshProUGUI? _codecLabel;
|
||||||
|
private static ITweenHandle? _codecLabelTween;
|
||||||
|
|
||||||
private static O5Dropdown<RateControlMode>? _rateControlDropdown;
|
private static O5Dropdown<RateControlMode>? _rateControlDropdown;
|
||||||
|
private static TextMeshProUGUI? _rateControlLabel;
|
||||||
|
private static ITweenHandle? _rateControlLabelTween;
|
||||||
// Bitrate / max bitrate / QP inputs keep their label and the running label color tween, so UpdateRateControlUI
|
// Bitrate / max bitrate / QP inputs keep their label and the running label color tween, so UpdateRateControlUI
|
||||||
// can dim the label together with the blocked input (see SetFieldBlocked).
|
// can dim the label together with the blocked input (see SetFieldBlocked).
|
||||||
private static O5InputField? _bitrateInput;
|
private static O5InputField? _bitrateInput;
|
||||||
private static TMPro.TextMeshProUGUI? _bitrateLabel;
|
private static TextMeshProUGUI? _bitrateLabel;
|
||||||
private static ITweenHandle? _bitrateLabelTween;
|
private static ITweenHandle? _bitrateLabelTween;
|
||||||
|
|
||||||
private static O5InputField? _maxBitrateInput;
|
private static O5InputField? _maxBitrateInput;
|
||||||
private static TMPro.TextMeshProUGUI? _maxBitrateLabel;
|
private static TextMeshProUGUI? _maxBitrateLabel;
|
||||||
private static ITweenHandle? _maxBitrateLabelTween;
|
private static ITweenHandle? _maxBitrateLabelTween;
|
||||||
|
|
||||||
private static O5InputField? _qpInput;
|
private static O5InputField? _qpInput;
|
||||||
private static TMPro.TextMeshProUGUI? _qpLabel;
|
private static TextMeshProUGUI? _qpLabel;
|
||||||
private static ITweenHandle? _qpLabelTween;
|
private static ITweenHandle? _qpLabelTween;
|
||||||
|
|
||||||
private static O5Slider? _keyframeIntervalSlider;
|
|
||||||
private static O5Toggle? _forceSoftwareToggle;
|
private static O5Toggle? _forceSoftwareToggle;
|
||||||
|
private static TextMeshProUGUI? _forceSoftwareLabel;
|
||||||
|
private static ITweenHandle? _forceSoftwareLabelTween;
|
||||||
|
|
||||||
|
private static O5Slider? _keyframeIntervalSlider;
|
||||||
|
|
||||||
private static O5Toggle? _renderAudioToggle;
|
private static O5Toggle? _renderAudioToggle;
|
||||||
private static O5Dropdown<int>? _sampleRateDropdown;
|
private static O5Dropdown<int>? _sampleRateDropdown;
|
||||||
@@ -376,7 +386,8 @@ internal static class RenderConfigWindow
|
|||||||
var redTheme = ctx.Theme with
|
var redTheme = ctx.Theme with
|
||||||
{
|
{
|
||||||
ObjectButton = ctx.Theme.SoftRed,
|
ObjectButton = ctx.Theme.SoftRed,
|
||||||
ButtonHover = ctx.Theme.SoftRed * 0.8f
|
ButtonHover = ctx.Theme.SoftRed * 0.8f,
|
||||||
|
ButtonPressed = ctx.Theme.SoftRed * 0.8f
|
||||||
};
|
};
|
||||||
var redCtx = new O5Context(
|
var redCtx = new O5Context(
|
||||||
config: ctx.Config,
|
config: ctx.Config,
|
||||||
@@ -421,7 +432,7 @@ internal static class RenderConfigWindow
|
|||||||
_renderFpsSlider = CreateSlider(
|
_renderFpsSlider = CreateSlider(
|
||||||
ctx: ctx, parent: CreateFieldSlot(topLeft),
|
ctx: ctx, parent: CreateFieldSlot(topLeft),
|
||||||
labelKey: "ui.render_config.render_fps", id: "slider_render_fps",
|
labelKey: "ui.render_config.render_fps", id: "slider_render_fps",
|
||||||
defaultValue: 60, min: 0.01, max: 3000, value: 60,
|
defaultValue: 60, min: 0.001, max: 3000, value: 60,
|
||||||
format: "F3", clampMode: ClampMode.All,
|
format: "F3", clampMode: ClampMode.All,
|
||||||
sliderFilter: value => Snap(renderFpsSnap, value),
|
sliderFilter: value => Snap(renderFpsSnap, value),
|
||||||
onChanged: val => { config.RenderFPS = val; MarkConfigModified(); }
|
onChanged: val => { config.RenderFPS = val; MarkConfigModified(); }
|
||||||
@@ -431,7 +442,7 @@ internal static class RenderConfigWindow
|
|||||||
_videoFPSSlider = CreateSlider(
|
_videoFPSSlider = CreateSlider(
|
||||||
ctx: ctx, parent: CreateFieldSlot(topRight),
|
ctx: ctx, parent: CreateFieldSlot(topRight),
|
||||||
labelKey: "ui.render_config.video_fps", id: "slider_video_fps",
|
labelKey: "ui.render_config.video_fps", id: "slider_video_fps",
|
||||||
defaultValue: 60, min: 0.01, max: 600, value: 60,
|
defaultValue: 60, min: 0.001, max: 600, value: 60,
|
||||||
format: "F3", clampMode: ClampMode.All,
|
format: "F3", clampMode: ClampMode.All,
|
||||||
sliderFilter: value => Snap(videoFpsSnap, value),
|
sliderFilter: value => Snap(videoFpsSnap, value),
|
||||||
onChanged: val => { config.VideoFPS = val; MarkConfigModified(); }
|
onChanged: val => { config.VideoFPS = val; MarkConfigModified(); }
|
||||||
@@ -479,6 +490,28 @@ internal static class RenderConfigWindow
|
|||||||
generateLabel: false
|
generateLabel: false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Output Mode (RenderConfig.OutputMode): Video vs PNG Sequence
|
||||||
|
_outputModeControl = CreateSegmentedField(
|
||||||
|
ctx: ctx,
|
||||||
|
parent: scrollContent,
|
||||||
|
labelKey: "ui.render_config.output_mode",
|
||||||
|
defaultValue: RenderConfig.RenderOutputMode.Video,
|
||||||
|
value: config.OutputMode,
|
||||||
|
options: new SegmentOption<RenderConfig.RenderOutputMode>[]
|
||||||
|
{
|
||||||
|
new(RenderConfig.RenderOutputMode.Video, "ui.render_config.output_mode_video"),
|
||||||
|
new(RenderConfig.RenderOutputMode.PNGSequence, "ui.render_config.output_mode_png_sequence")
|
||||||
|
},
|
||||||
|
id: "seg_output_mode",
|
||||||
|
onChanged: val =>
|
||||||
|
{
|
||||||
|
config.OutputMode = val;
|
||||||
|
UpdateUIBlock(config);
|
||||||
|
MarkConfigModified();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
// Section 3: Video Encoding card (RenderConfig.VideoConfig)
|
// Section 3: Video Encoding card (RenderConfig.VideoConfig)
|
||||||
// Codec | Rate Control Mode
|
// Codec | Rate Control Mode
|
||||||
@@ -502,11 +535,11 @@ internal static class RenderConfigWindow
|
|||||||
"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", out _codecLabel,
|
||||||
c =>
|
c =>
|
||||||
{
|
{
|
||||||
config.VideoConfig.Codec = c;
|
config.VideoConfig.Codec = c;
|
||||||
UpdateVideoCodecUI(c, config);
|
UpdateSampleRateUI(c, config);
|
||||||
MarkConfigModified();
|
MarkConfigModified();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -517,7 +550,7 @@ internal static class RenderConfigWindow
|
|||||||
"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,
|
TMP_InputField.CharacterValidation.Integer,
|
||||||
val => { if (int.TryParse(val, out var b) && b > 0) config.VideoConfig.Bitrate = b; MarkConfigModified(); }
|
val => { if (int.TryParse(val, out var b) && b > 0) config.VideoConfig.Bitrate = b; MarkConfigModified(); }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -526,7 +559,7 @@ internal static class RenderConfigWindow
|
|||||||
"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,
|
TMP_InputField.CharacterValidation.Integer,
|
||||||
val => { if (int.TryParse(val, out var qp) && qp >= 0) config.VideoConfig.QP = qp; MarkConfigModified(); }
|
val => { if (int.TryParse(val, out var qp) && qp >= 0) config.VideoConfig.QP = qp; MarkConfigModified(); }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -536,11 +569,11 @@ internal static class RenderConfigWindow
|
|||||||
"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", out _rateControlLabel,
|
||||||
m =>
|
m =>
|
||||||
{
|
{
|
||||||
config.VideoConfig.RateControlMode = m;
|
config.VideoConfig.RateControlMode = m;
|
||||||
UpdateRateControlUI(m);
|
UpdateUIBlock(config);
|
||||||
MarkConfigModified();
|
MarkConfigModified();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -550,7 +583,7 @@ internal static class RenderConfigWindow
|
|||||||
"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,
|
TMP_InputField.CharacterValidation.Integer,
|
||||||
val => { if (int.TryParse(val, out var mb) && mb > 0) config.VideoConfig.MaxBitrate = mb; MarkConfigModified(); }
|
val => { if (int.TryParse(val, out var mb) && mb > 0) config.VideoConfig.MaxBitrate = mb; MarkConfigModified(); }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -560,7 +593,7 @@ internal static class RenderConfigWindow
|
|||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
value: config.VideoConfig.ForceSoftwareEncoder,
|
value: config.VideoConfig.ForceSoftwareEncoder,
|
||||||
toggleTextKey: "ui.render_config.force_software",
|
toggleTextKey: "ui.render_config.force_software",
|
||||||
id: "toggle_force_software",
|
id: "toggle_force_software", out _forceSoftwareLabel,
|
||||||
onChanged: val => { config.VideoConfig.ForceSoftwareEncoder = val; MarkConfigModified(); },
|
onChanged: val => { config.VideoConfig.ForceSoftwareEncoder = val; MarkConfigModified(); },
|
||||||
generateLabel: true
|
generateLabel: true
|
||||||
);
|
);
|
||||||
@@ -826,7 +859,7 @@ internal static class RenderConfigWindow
|
|||||||
"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,
|
TMP_InputField.CharacterValidation.Integer,
|
||||||
val =>
|
val =>
|
||||||
{
|
{
|
||||||
if (int.TryParse(val, out var s)) config.AdvancedConfig.RandomSeed = s;
|
if (int.TryParse(val, out var s)) config.AdvancedConfig.RandomSeed = s;
|
||||||
@@ -1021,7 +1054,7 @@ internal static class RenderConfigWindow
|
|||||||
// RenderMod.ExpirationTime (GetVersionText)
|
// RenderMod.ExpirationTime (GetVersionText)
|
||||||
var versionLabel = O5Factory.ControlText(ctx, scrollContent, 12f, noPad: true);
|
var versionLabel = O5Factory.ControlText(ctx, scrollContent, 12f, noPad: true);
|
||||||
LocalizedText.Bind(versionLabel, GetVersionText);
|
LocalizedText.Bind(versionLabel, GetVersionText);
|
||||||
versionLabel.alignment = TMPro.TextAlignmentOptions.Center;
|
versionLabel.alignment = TextAlignmentOptions.Center;
|
||||||
versionLabel.color = ctx.Theme.TextDim;
|
versionLabel.color = ctx.Theme.TextDim;
|
||||||
|
|
||||||
// ReSharper disable once InconsistentNaming
|
// ReSharper disable once InconsistentNaming
|
||||||
@@ -1071,6 +1104,7 @@ internal static class RenderConfigWindow
|
|||||||
_renderHeightSlider?.Set(config.RenderHeight, false, noFilter: true);
|
_renderHeightSlider?.Set(config.RenderHeight, false, noFilter: true);
|
||||||
_delayAfterEndSlider?.Set(config.DelayAfterEnd, false, noFilter: true);
|
_delayAfterEndSlider?.Set(config.DelayAfterEnd, false, noFilter: true);
|
||||||
_showJudgementToggle?.Set(config.ShowJudgementText, false);
|
_showJudgementToggle?.Set(config.ShowJudgementText, false);
|
||||||
|
_outputModeControl?.Set(config.OutputMode, false);
|
||||||
|
|
||||||
_codecDropdown?.Set(config.VideoConfig.Codec, false);
|
_codecDropdown?.Set(config.VideoConfig.Codec, false);
|
||||||
_rateControlDropdown?.Set(config.VideoConfig.RateControlMode, false);
|
_rateControlDropdown?.Set(config.VideoConfig.RateControlMode, false);
|
||||||
@@ -1118,8 +1152,8 @@ internal static class RenderConfigWindow
|
|||||||
// The sample rate list depends on the codec. UpdateVideoCodecUI writes the dropdown's resulting value back into
|
// The sample rate list depends on the codec. UpdateVideoCodecUI writes the dropdown's resulting value back into
|
||||||
// the config: O5Dropdown.SetValues selects the first listed rate when the current one is not in the new list,
|
// the config: O5Dropdown.SetValues selects the first listed rate when the current one is not in the new list,
|
||||||
// so a rate the codec does not support becomes the codec's first rate.
|
// so a rate the codec does not support becomes the codec's first rate.
|
||||||
UpdateVideoCodecUI(config.VideoConfig.Codec, config);
|
UpdateSampleRateUI(config.VideoConfig.Codec, config);
|
||||||
UpdateRateControlUI(config.VideoConfig.RateControlMode, noAnimate: true);
|
UpdateUIBlock(config, noAnimate: true);
|
||||||
|
|
||||||
UpdatePresetLabel(config);
|
UpdatePresetLabel(config);
|
||||||
}
|
}
|
||||||
@@ -1252,6 +1286,7 @@ internal static class RenderConfigWindow
|
|||||||
if (_renderHeightSlider != null) target.RenderHeight = (int)Math.Round(_renderHeightSlider.Value);
|
if (_renderHeightSlider != null) target.RenderHeight = (int)Math.Round(_renderHeightSlider.Value);
|
||||||
if (_delayAfterEndSlider != null) target.DelayAfterEnd = _delayAfterEndSlider.Value;
|
if (_delayAfterEndSlider != null) target.DelayAfterEnd = _delayAfterEndSlider.Value;
|
||||||
if (_showJudgementToggle != null) target.ShowJudgementText = _showJudgementToggle.Value;
|
if (_showJudgementToggle != null) target.ShowJudgementText = _showJudgementToggle.Value;
|
||||||
|
if (_outputModeControl != null) target.OutputMode = _outputModeControl.Value;
|
||||||
|
|
||||||
if (_codecDropdown != null) target.VideoConfig.Codec = _codecDropdown.Value;
|
if (_codecDropdown != null) target.VideoConfig.Codec = _codecDropdown.Value;
|
||||||
if (_rateControlDropdown != null) target.VideoConfig.RateControlMode = _rateControlDropdown.Value;
|
if (_rateControlDropdown != null) target.VideoConfig.RateControlMode = _rateControlDropdown.Value;
|
||||||
@@ -1721,10 +1756,8 @@ internal static class RenderConfigWindow
|
|||||||
hLayout.spacing = spacing;
|
hLayout.spacing = spacing;
|
||||||
hLayout.padding = new RectOffset(0, 0, 0, 0);
|
hLayout.padding = new RectOffset(0, 0, 0, 0);
|
||||||
hLayout.childAlignment = TextAnchor.UpperCenter;
|
hLayout.childAlignment = TextAnchor.UpperCenter;
|
||||||
hLayout.childControlWidth = true;
|
hLayout.childControlWidth = hLayout.childControlHeight = true;
|
||||||
hLayout.childControlHeight = true;
|
hLayout.childForceExpandWidth = true; hLayout.childForceExpandHeight = false;
|
||||||
hLayout.childForceExpandWidth = true;
|
|
||||||
hLayout.childForceExpandHeight = false;
|
|
||||||
|
|
||||||
// ReSharper disable once InconsistentNaming
|
// ReSharper disable once InconsistentNaming
|
||||||
var panelLE = panelObj.AddComponent<LayoutElement>();
|
var panelLE = panelObj.AddComponent<LayoutElement>();
|
||||||
@@ -1830,8 +1863,8 @@ internal static class RenderConfigWindow
|
|||||||
showActiveToggle
|
showActiveToggle
|
||||||
);
|
);
|
||||||
|
|
||||||
var titleTmp = card.Find("Header/TitleTrigger/TitleText")?.GetComponent<TMPro.TextMeshProUGUI>()
|
var titleTmp = card.Find("Header/TitleTrigger/TitleText")?.GetComponent<TextMeshProUGUI>()
|
||||||
?? card.GetComponentsInChildren<TMPro.TextMeshProUGUI>(true).FirstOrDefault(t => t.gameObject.name == "TitleText");
|
?? card.GetComponentsInChildren<TextMeshProUGUI>(true).FirstOrDefault(t => t.gameObject.name == "TitleText");
|
||||||
if (titleTmp)
|
if (titleTmp)
|
||||||
{
|
{
|
||||||
titleTmp.fontSize = 18f;
|
titleTmp.fontSize = 18f;
|
||||||
@@ -1848,7 +1881,7 @@ internal static class RenderConfigWindow
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a localized text label (default 13 pt, theme dim text color unless <paramref name="color"/> is given).
|
/// Creates a localized text label (default 13 pt, theme dim text color unless <paramref name="color"/> is given).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static TMPro.TextMeshProUGUI CreateLabel(
|
private static TextMeshProUGUI CreateLabel(
|
||||||
O5Context ctx,
|
O5Context ctx,
|
||||||
Transform parent,
|
Transform parent,
|
||||||
string textKey,
|
string textKey,
|
||||||
@@ -1960,7 +1993,7 @@ internal static class RenderConfigWindow
|
|||||||
string? defaultValue,
|
string? defaultValue,
|
||||||
string value,
|
string value,
|
||||||
string id,
|
string id,
|
||||||
TMPro.TMP_InputField.CharacterValidation characterValidation = TMPro.TMP_InputField.CharacterValidation.None,
|
TMP_InputField.CharacterValidation characterValidation = TMP_InputField.CharacterValidation.None,
|
||||||
Action<string>? onChanged = null)
|
Action<string>? onChanged = null)
|
||||||
{
|
{
|
||||||
return CreateInputField(ctx, colParent, labelKey, defaultValue, value, id, out _, characterValidation, onChanged);
|
return CreateInputField(ctx, colParent, labelKey, defaultValue, value, id, out _, characterValidation, onChanged);
|
||||||
@@ -1987,8 +2020,8 @@ internal static class RenderConfigWindow
|
|||||||
string? defaultValue,
|
string? defaultValue,
|
||||||
string value,
|
string value,
|
||||||
string id,
|
string id,
|
||||||
out TMPro.TextMeshProUGUI label,
|
out TextMeshProUGUI label,
|
||||||
TMPro.TMP_InputField.CharacterValidation characterValidation = TMPro.TMP_InputField.CharacterValidation.None,
|
TMP_InputField.CharacterValidation characterValidation = TMP_InputField.CharacterValidation.None,
|
||||||
Action<string>? onChanged = null)
|
Action<string>? onChanged = null)
|
||||||
{
|
{
|
||||||
// Labeled text input: an 18-unit label row (13 pt, dim) above an O5Kit input. The label key also serves as the
|
// Labeled text input: an 18-unit label row (13 pt, dim) above an O5Kit input. The label key also serves as the
|
||||||
@@ -2027,19 +2060,9 @@ internal static class RenderConfigWindow
|
|||||||
// Dependent field rules (rate control, codec)
|
// Dependent field rules (rate control, codec)
|
||||||
// ===================================================================================================
|
// ===================================================================================================
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Blocks or unblocks an input and dims its label: a blocked label uses the theme dim text color at 40% of its
|
|
||||||
/// alpha. The label color changes with a 0.2-second tween (any running tween for that label is killed first), or
|
|
||||||
/// immediately with <paramref name="noAnimate"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">Input to block; may be null.</param>
|
|
||||||
/// <param name="label">Label above the input; may be null.</param>
|
|
||||||
/// <param name="tweenHandle">Stored tween handle of this label, replaced by the new tween.</param>
|
|
||||||
/// <param name="blocked">True to block (disable) the field.</param>
|
|
||||||
/// <param name="noAnimate">True to apply the change without animation.</param>
|
|
||||||
private static void SetFieldBlocked(
|
private static void SetFieldBlocked(
|
||||||
O5InputField? input,
|
O5InputField? input,
|
||||||
TMPro.TextMeshProUGUI? label,
|
TextMeshProUGUI? label,
|
||||||
ref ITweenHandle? tweenHandle,
|
ref ITweenHandle? tweenHandle,
|
||||||
bool blocked,
|
bool blocked,
|
||||||
bool noAnimate = false)
|
bool noAnimate = false)
|
||||||
@@ -2047,7 +2070,37 @@ internal static class RenderConfigWindow
|
|||||||
input?.SetBlocked(blocked, noAnimate);
|
input?.SetBlocked(blocked, noAnimate);
|
||||||
|
|
||||||
if (label == null) return;
|
if (label == null) return;
|
||||||
|
UpdateLabelDim(label, ref tweenHandle, blocked, noAnimate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetToggleBlocked(
|
||||||
|
O5Toggle? toggle,
|
||||||
|
TextMeshProUGUI? label,
|
||||||
|
ref ITweenHandle? tweenHandle,
|
||||||
|
bool blocked,
|
||||||
|
bool noAnimate = false)
|
||||||
|
{
|
||||||
|
toggle?.SetBlocked(blocked, noAnimate);
|
||||||
|
|
||||||
|
if (label == null) return;
|
||||||
|
UpdateLabelDim(label, ref tweenHandle, blocked, noAnimate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetDropdownBlocked<T>(
|
||||||
|
O5Dropdown<T>? dropdown,
|
||||||
|
TextMeshProUGUI? label,
|
||||||
|
ref ITweenHandle? tweenHandle,
|
||||||
|
bool blocked,
|
||||||
|
bool noAnimate = false)
|
||||||
|
{
|
||||||
|
dropdown?.SetBlocked(blocked, noAnimate);
|
||||||
|
|
||||||
|
if (label == null) return;
|
||||||
|
UpdateLabelDim(label, ref tweenHandle, blocked, noAnimate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateLabelDim(TextMeshProUGUI label, ref ITweenHandle? tweenHandle, bool blocked, bool noAnimate = false)
|
||||||
|
{
|
||||||
tweenHandle?.Kill();
|
tweenHandle?.Kill();
|
||||||
var ctx = RendererUIManager.Context;
|
var ctx = RendererUIManager.Context;
|
||||||
var baseColor = ctx.Theme.TextDim;
|
var baseColor = ctx.Theme.TextDim;
|
||||||
@@ -2055,41 +2108,20 @@ internal static class RenderConfigWindow
|
|||||||
? new Color(baseColor.r, baseColor.g, baseColor.b, baseColor.a * 0.4f)
|
? new Color(baseColor.r, baseColor.g, baseColor.b, baseColor.a * 0.4f)
|
||||||
: baseColor;
|
: baseColor;
|
||||||
|
|
||||||
if (noAnimate)
|
if (noAnimate) label.color = targetColor;
|
||||||
{
|
else tweenHandle = ctx.Tween.TweenColor(label, targetColor, 0.2f);
|
||||||
label.color = targetColor;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tweenHandle = ctx.Tween.TweenColor(label, targetColor, 0.2f);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enables only the video fields used by the rate control mode:
|
/// Replaces the sample rate choices with those supported by the audio codec paired with <paramref name="videoCodec"/>.
|
||||||
/// CBR - Bitrate; VBR - Bitrate and Max Bitrate; CQP - QP. Values of blocked fields stay in the config.
|
|
||||||
/// </summary>
|
|
||||||
private static void UpdateRateControlUI(RateControlMode mode, bool noAnimate = false)
|
|
||||||
{
|
|
||||||
bool blockBitrate = mode is RateControlMode.CQP;
|
|
||||||
bool blockMaxBitrate = mode is RateControlMode.CBR or RateControlMode.CQP;
|
|
||||||
bool blockQp = mode is RateControlMode.CBR or RateControlMode.VBR;
|
|
||||||
|
|
||||||
SetFieldBlocked(_bitrateInput, _bitrateLabel, ref _bitrateLabelTween, blockBitrate, noAnimate);
|
|
||||||
SetFieldBlocked(_maxBitrateInput, _maxBitrateLabel, ref _maxBitrateLabelTween, blockMaxBitrate, noAnimate);
|
|
||||||
SetFieldBlocked(_qpInput, _qpLabel, ref _qpLabelTween, blockQp, noAnimate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Replaces the sample rate choices with those supported by the audio codec paired with <paramref name="codec"/>.
|
|
||||||
/// O5Dropdown.SetValues keeps the current rate when it is in the new list and otherwise selects the first entry;
|
/// O5Dropdown.SetValues keeps the current rate when it is in the new list and otherwise selects the first entry;
|
||||||
/// the resulting rate is written to <paramref name="config"/> when one is given.
|
/// the resulting rate is written to <paramref name="config"/> when one is given.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void UpdateVideoCodecUI(Codec codec, RenderConfig? config = null)
|
private static void UpdateSampleRateUI(Codec videoCodec, RenderConfig? config = null)
|
||||||
{
|
{
|
||||||
if (_sampleRateDropdown == null) return;
|
if (_sampleRateDropdown == null) return;
|
||||||
|
|
||||||
_sampleRateDropdown.SetValues(CodecUtil.AvailableSampleRate[CodecUtil.AudioCodecMatch[codec]]);
|
_sampleRateDropdown.SetValues(CodecUtil.AvailableSampleRate[CodecUtil.AudioCodecMatch[videoCodec]]);
|
||||||
|
|
||||||
if (config != null && _sampleRateDropdown.Value is var currentRate)
|
if (config != null && _sampleRateDropdown.Value is var currentRate)
|
||||||
{
|
{
|
||||||
@@ -2097,6 +2129,35 @@ internal static class RenderConfigWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void UpdateUIBlock(RenderConfig config, bool noAnimate = false)
|
||||||
|
{
|
||||||
|
var blockCodecDropdown = false;
|
||||||
|
var blockRateControlModeDropdown = false;
|
||||||
|
var blockBitrateInput = config.VideoConfig.RateControlMode is RateControlMode.CQP;
|
||||||
|
var blockMaxBitrateInput = config.VideoConfig.RateControlMode is RateControlMode.CBR or RateControlMode.CQP;
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
var blockQPInput = config.VideoConfig.RateControlMode is RateControlMode.CBR or RateControlMode.VBR;
|
||||||
|
var blockForceSoftwareToggle = false;
|
||||||
|
var blockKeyframeIntervalSlider = false;
|
||||||
|
|
||||||
|
var blockVideoEncodingSettings = config.OutputMode is RenderConfig.RenderOutputMode.PNGSequence;
|
||||||
|
blockCodecDropdown |= blockVideoEncodingSettings;
|
||||||
|
blockRateControlModeDropdown |= blockVideoEncodingSettings;
|
||||||
|
blockBitrateInput |= blockVideoEncodingSettings;
|
||||||
|
blockMaxBitrateInput |= blockVideoEncodingSettings;
|
||||||
|
blockQPInput |= blockVideoEncodingSettings;
|
||||||
|
blockForceSoftwareToggle |= blockVideoEncodingSettings;
|
||||||
|
blockKeyframeIntervalSlider |= blockVideoEncodingSettings;
|
||||||
|
|
||||||
|
SetDropdownBlocked(_codecDropdown, _codecLabel, ref _codecLabelTween, blockCodecDropdown, noAnimate);
|
||||||
|
SetDropdownBlocked(_rateControlDropdown, _rateControlLabel, ref _rateControlLabelTween, blockRateControlModeDropdown, noAnimate);
|
||||||
|
SetFieldBlocked(_bitrateInput, _bitrateLabel, ref _bitrateLabelTween, blockBitrateInput, noAnimate);
|
||||||
|
SetFieldBlocked(_maxBitrateInput, _maxBitrateLabel, ref _maxBitrateLabelTween, blockMaxBitrateInput, noAnimate);
|
||||||
|
SetFieldBlocked(_qpInput, _qpLabel, ref _qpLabelTween, blockQPInput, noAnimate);
|
||||||
|
SetToggleBlocked(_forceSoftwareToggle, _forceSoftwareLabel, ref _forceSoftwareLabelTween, blockForceSoftwareToggle, noAnimate);
|
||||||
|
_keyframeIntervalSlider?.SetBlocked(blockKeyframeIntervalSlider, noAnimate);
|
||||||
|
}
|
||||||
|
|
||||||
// ===================================================================================================
|
// ===================================================================================================
|
||||||
// More control factory helpers
|
// More control factory helpers
|
||||||
// ===================================================================================================
|
// ===================================================================================================
|
||||||
@@ -2154,6 +2215,21 @@ internal static class RenderConfigWindow
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Same as the CreateToggleField overload with an <c>out</c> label parameter, without returning the label.</summary>
|
||||||
|
private static O5Toggle CreateToggleField(
|
||||||
|
O5Context ctx,
|
||||||
|
Transform colParent,
|
||||||
|
string labelKey,
|
||||||
|
bool? defaultValue,
|
||||||
|
bool value,
|
||||||
|
string toggleTextKey,
|
||||||
|
string id,
|
||||||
|
Action<bool>? onChanged = null,
|
||||||
|
bool generateLabel = true)
|
||||||
|
{
|
||||||
|
return CreateToggleField(ctx, colParent, labelKey, defaultValue, value, toggleTextKey, id, out _, onChanged, generateLabel);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an O5Kit toggle whose text is localized with <paramref name="toggleTextKey"/>. With
|
/// Creates an O5Kit toggle whose text is localized with <paramref name="toggleTextKey"/>. With
|
||||||
/// <paramref name="generateLabel"/> an 18-unit label row is added above it (text from <paramref name="labelKey"/>,
|
/// <paramref name="generateLabel"/> an 18-unit label row is added above it (text from <paramref name="labelKey"/>,
|
||||||
@@ -2167,12 +2243,14 @@ internal static class RenderConfigWindow
|
|||||||
bool value,
|
bool value,
|
||||||
string toggleTextKey,
|
string toggleTextKey,
|
||||||
string id,
|
string id,
|
||||||
|
out TextMeshProUGUI? label,
|
||||||
Action<bool>? onChanged = null,
|
Action<bool>? onChanged = null,
|
||||||
bool generateLabel = true)
|
bool generateLabel = true)
|
||||||
{
|
{
|
||||||
|
label = null;
|
||||||
if (generateLabel)
|
if (generateLabel)
|
||||||
{
|
{
|
||||||
var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true);
|
label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true);
|
||||||
label.text = string.IsNullOrEmpty(labelKey) ? "" : LocalizationManager.Get(labelKey);
|
label.text = string.IsNullOrEmpty(labelKey) ? "" : LocalizationManager.Get(labelKey);
|
||||||
label.color = ctx.Theme.TextDim;
|
label.color = ctx.Theme.TextDim;
|
||||||
LocalizedText.Bind(label, labelKey);
|
LocalizedText.Bind(label, labelKey);
|
||||||
@@ -2201,6 +2279,21 @@ internal static class RenderConfigWindow
|
|||||||
return toggle;
|
return toggle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Same as the CreateDropdownField overload with an <c>out</c> label parameter, without returning the label.</summary>
|
||||||
|
private static O5Dropdown<T> CreateDropdownField<T>(
|
||||||
|
O5Context ctx,
|
||||||
|
Transform colParent,
|
||||||
|
string labelKey,
|
||||||
|
T defaultValue,
|
||||||
|
T value,
|
||||||
|
IReadOnlyList<T> values,
|
||||||
|
Func<T, string> display,
|
||||||
|
string id,
|
||||||
|
Action<T>? onChanged = null)
|
||||||
|
{
|
||||||
|
return CreateDropdownField(ctx, colParent, labelKey, defaultValue, value, values, display, id, out _, onChanged);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a labeled dropdown: an 18-unit label row and a container of preferred height 50 units holding the O5Kit
|
/// Creates a labeled dropdown: an 18-unit label row and a container of preferred height 50 units holding the O5Kit
|
||||||
/// dropdown. <paramref name="display"/> converts values to their shown text. The change callback is not invoked for
|
/// dropdown. <paramref name="display"/> converts values to their shown text. The change callback is not invoked for
|
||||||
@@ -2215,9 +2308,10 @@ internal static class RenderConfigWindow
|
|||||||
IReadOnlyList<T> values,
|
IReadOnlyList<T> values,
|
||||||
Func<T, string> display,
|
Func<T, string> display,
|
||||||
string id,
|
string id,
|
||||||
|
out TextMeshProUGUI label,
|
||||||
Action<T>? onChanged = null)
|
Action<T>? onChanged = null)
|
||||||
{
|
{
|
||||||
var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true);
|
label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true);
|
||||||
label.text = LocalizationManager.Get(labelKey);
|
label.text = LocalizationManager.Get(labelKey);
|
||||||
label.color = ctx.Theme.TextDim;
|
label.color = ctx.Theme.TextDim;
|
||||||
LocalizedText.Bind(label, labelKey);
|
LocalizedText.Bind(label, labelKey);
|
||||||
@@ -2302,7 +2396,7 @@ internal static class RenderConfigWindow
|
|||||||
if (header.Find("HexInput") is RectTransform hexInput)
|
if (header.Find("HexInput") is RectTransform hexInput)
|
||||||
{
|
{
|
||||||
hexInput.anchorMin = new Vector2(0.32f, 0f);
|
hexInput.anchorMin = new Vector2(0.32f, 0f);
|
||||||
var tmpInput = hexInput.GetComponentInChildren<TMPro.TMP_InputField>();
|
var tmpInput = hexInput.GetComponentInChildren<TMP_InputField>();
|
||||||
if (tmpInput != null && tmpInput.textComponent != null)
|
if (tmpInput != null && tmpInput.textComponent != null)
|
||||||
{
|
{
|
||||||
tmpInput.textComponent.fontSize = 15f;
|
tmpInput.textComponent.fontSize = 15f;
|
||||||
@@ -2313,6 +2407,103 @@ internal static class RenderConfigWindow
|
|||||||
return picker;
|
return picker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a labeled segmented control field. When <paramref name="horizontal"/> is true,
|
||||||
|
/// places the label on the left and the segmented control on the right; otherwise stacks them vertically.
|
||||||
|
/// Both the label and segment buttons automatically track language changes via <see cref="LocalizedText"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static SegmentedControl<T> CreateSegmentedField<T>(
|
||||||
|
O5Context ctx,
|
||||||
|
Transform parent,
|
||||||
|
string? labelKey,
|
||||||
|
T defaultValue,
|
||||||
|
T value,
|
||||||
|
IReadOnlyList<SegmentOption<T>> options,
|
||||||
|
string id,
|
||||||
|
Action<T>? onChanged = null,
|
||||||
|
bool horizontal = true,
|
||||||
|
float? height = null)
|
||||||
|
{
|
||||||
|
var controlHeight = height ?? ctx.Theme.ControlHeightFor(ctx.Theme.FontSizeBody);
|
||||||
|
|
||||||
|
var fieldObj = new GameObject($"Field_Seg_{id}");
|
||||||
|
fieldObj.transform.SetParent(parent, false);
|
||||||
|
var fieldRect = fieldObj.AddComponent<RectTransform>();
|
||||||
|
|
||||||
|
if (horizontal)
|
||||||
|
{
|
||||||
|
var hLayout = fieldObj.AddComponent<HorizontalLayoutGroup>();
|
||||||
|
hLayout.spacing = 16f;
|
||||||
|
hLayout.childAlignment = TextAnchor.MiddleLeft;
|
||||||
|
hLayout.childControlWidth = true;
|
||||||
|
hLayout.childControlHeight = true;
|
||||||
|
hLayout.childForceExpandWidth = false;
|
||||||
|
hLayout.childForceExpandHeight = false;
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
var fieldLE = fieldObj.AddComponent<LayoutElement>();
|
||||||
|
fieldLE.preferredHeight = controlHeight;
|
||||||
|
fieldLE.flexibleWidth = 1f;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(labelKey))
|
||||||
|
{
|
||||||
|
var label = O5Factory.ControlText(ctx, fieldRect, ctx.Theme.FontSizeBody, noPad: true);
|
||||||
|
label.text = LocalizationManager.Get(labelKey);
|
||||||
|
label.color = ctx.Theme.TextDim;
|
||||||
|
LocalizedText.Bind(label, labelKey);
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
var labelLE = label.gameObject.AddComponent<LayoutElement>();
|
||||||
|
var csf = label.gameObject.AddComponent<ContentSizeFitter>();
|
||||||
|
csf.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||||
|
csf.verticalFit = ContentSizeFitter.FitMode.Unconstrained;
|
||||||
|
labelLE.flexibleWidth = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segControl = SegmentedControl<T>.Create(ctx, fieldRect, id, defaultValue, value, options, controlHeight, onChanged);
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
var controlLE = segControl.Rect.GetComponent<LayoutElement>() ?? segControl.Rect.gameObject.AddComponent<LayoutElement>();
|
||||||
|
controlLE.flexibleWidth = 1f;
|
||||||
|
controlLE.preferredHeight = controlHeight;
|
||||||
|
|
||||||
|
return segControl;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var vLayout = fieldObj.AddComponent<VerticalLayoutGroup>();
|
||||||
|
vLayout.spacing = 3f;
|
||||||
|
vLayout.childControlWidth = true;
|
||||||
|
vLayout.childControlHeight = true;
|
||||||
|
vLayout.childForceExpandWidth = true;
|
||||||
|
vLayout.childForceExpandHeight = false;
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
var fieldLE = fieldObj.AddComponent<LayoutElement>();
|
||||||
|
fieldLE.flexibleWidth = 1f;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(labelKey))
|
||||||
|
{
|
||||||
|
var label = O5Factory.ControlText(ctx, fieldRect, ctx.Theme.FontSizeBody, noPad: true);
|
||||||
|
label.text = LocalizationManager.Get(labelKey);
|
||||||
|
label.color = ctx.Theme.TextDim;
|
||||||
|
LocalizedText.Bind(label, labelKey);
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
var labelLE = label.gameObject.AddComponent<LayoutElement>();
|
||||||
|
labelLE.preferredHeight = 18f;
|
||||||
|
labelLE.minHeight = 18f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segControl = SegmentedControl<T>.Create(ctx, fieldRect, id, defaultValue, value, options, controlHeight, onChanged);
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
var controlLE = segControl.Rect.GetComponent<LayoutElement>() ?? segControl.Rect.gameObject.AddComponent<LayoutElement>();
|
||||||
|
controlLE.flexibleWidth = 1f;
|
||||||
|
controlLE.preferredHeight = controlHeight;
|
||||||
|
|
||||||
|
return segControl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===================================================================================================
|
// ===================================================================================================
|
||||||
// Window visibility
|
// Window visibility
|
||||||
// ===================================================================================================
|
// ===================================================================================================
|
||||||
|
|||||||
Reference in new issue
Block a user