Files
2026-07-13 13:13:17 +08:00

522 lines
21 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Data;
using System.Globalization;
using System.Text.RegularExpressions;
using ImGuiNET;
using T3.Core.Utils;
using T3.Editor.App;
using T3.Editor.Gui.Interaction.Timing;
using T3.Editor.Gui.Styling;
using T3.Editor.Gui.UiHelpers;
using T3.Editor.UiModel.InputsAndTypes;
using T3.SystemUi;
using Vector2 = System.Numerics.Vector2;
namespace T3.Editor.Gui.Interaction;
/// <summary>
/// An alternative ImGui component for editing float values
/// </summary>
internal static class SingleValueEdit
{
/// <summary>
/// Wrapper coll for int type
/// </summary>
internal static InputEditStateFlags Draw(ref int value,
Vector2 size,
int min = int.MinValue,
int max = int.MaxValue,
bool clampMin = false,
bool clampMax = false,
float scale = 0.2f,
string format = "{0:0}",
int defaultValue = default,
float horizontalAlign = 0)
{
double doubleValue = value;
var result = Draw(ref doubleValue, size, min, max, clampMin, clampMax, scale: scale, format: format, useIntegers: true, defaultValue: defaultValue, horizontalAlign: horizontalAlign);
value = (int)doubleValue;
return result;
}
/// <summary>
/// Wrapper call for float type
/// </summary>
internal static InputEditStateFlags Draw(ref float value,
Vector2 size,
float min = float.NegativeInfinity,
float max = float.PositiveInfinity,
bool clampMin = false,
bool clampMax= false,
float scale = 0.01f,
string format = "{0:0.000}",
float defaultValue = default,
float horizontalAlign = 0)
{
double floatValue = value;
var result = Draw(ref floatValue, size, min, max, clampMin, clampMax, scale: scale, format: format, defaultValue: defaultValue,
horizontalAlign: horizontalAlign);
value = (float)floatValue;
return result;
}
private static int _editInteractionCounter = 0;
private static InputEditStateFlags Draw(ref double value,
Vector2 size,
double min = double.NegativeInfinity,
double max = double.PositiveInfinity,
bool clampMin = false,
bool clampMax= false,
float scale = 1,
string format = "{0:0.000}",
bool useIntegers = false,
double defaultValue = default,
float horizontalAlign = 0)
{
_numberFormat = format;
_currentTabIndex++;
var componentId = ImGui.GetID("valueEdit");
var shouldFocus = _currentTabIndex == _tabFocusIndex;
if (shouldFocus)
{
//Log.Debug(" ShouldFocus for index " + TabFocusIndex + " state " + _state );
SetState(InputStates.TextInput);
_activeJogDialId = componentId;
_startValue = value;
_jogDialText = FormatValueForButton(ref value, true);
}
var io = ImGui.GetIO();
if (componentId == _activeJogDialId)
{
var modified = false;
switch (_state)
{
case InputStates.Dialing:
modified = true;
ImGui.PushStyleColor(ImGuiCol.Button, UiColors.BackgroundActive.Rgba);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UiColors.BackgroundActive.Rgba);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UiColors.BackgroundActive.Rgba);
//DrawButtonWithDynamicLabel(FormatValueForButton(ref _editValue), ref size);
ImGui.PushStyleVar(ImGuiStyleVar.ButtonTextAlign, new Vector2(horizontalAlign,0.5f));
ImGui.Button(FormatValueForButton(ref _editValue) + "###button", size);
ImGui.PopStyleVar();
DrawValueRangeIndicator(value, min, max, true);
ImGui.PopStyleColor(3);
if (ImGui.IsMouseReleased(0))
{
var wasClick = ImGui.GetIO().MouseDragMaxDistanceSqr[0] < UserSettings.Config.ClickThreshold;
if (wasClick)
{
if (io.KeyCtrl)
{
SetState(InputStates.Inactive);
return InputEditStateFlags.ResetToDefault;
}
else
{
SetState(InputStates.StartedTextInput);
}
}
else
{
SetState(InputStates.Inactive);
}
break;
}
if (ImGui.IsItemDeactivated())
{
SetState(InputStates.Inactive);
break;
}
var restarted = (float)(ImGui.GetTime() - _timeOpened) < 0.1f;
DrawValueEditMethod(ref _editValue, restarted, _center, min, max, clampMin, clampMax, scale);
_dampedValue = _slidingAverage.UpdateAndCompute(_editValue);
break;
case InputStates.StartedTextInput:
_editInteractionCounter++;
ImGui.SetKeyboardFocusHere();
SetState(InputStates.TextInput);
goto case InputStates.TextInput;
case InputStates.TextInput:
ImGui.PushStyleColor(ImGuiCol.Text, double.IsNaN(_editValue)
? UiColors.StatusError.Rgba
: UiColors.ForegroundFull);
ImGui.SetNextItemWidth(size.X);
modified =ImGui.InputText("##dialInput" + _editInteractionCounter, ref _jogDialText, 20);
// Keep Focusing until Tab-Key released
if (shouldFocus)
{
ImGui.SetKeyboardFocusHere(-1);
if (ImGui.IsKeyReleased(Key.Tab.ToImGuiKey()))
{
_tabFocusIndex = -1;
}
}
ImGui.PopStyleColor();
var completedAfterTabbing = false;
if (ImGui.IsKeyPressed(Key.Tab.ToImGuiKey()) && _tabFocusIndex == -1)
{
_tabFocusIndex = _currentTabIndex + (ImGui.GetIO().KeyShift ? -1 : 1);
completedAfterTabbing = true;
}
var completedAfterFocusLoss = ImGui.IsKeyPressed(Key.Esc.ToImGuiKey()) || ImGui.IsItemDeactivated() || !ImGui.IsWindowFocused();
var cancelInputAfterFocusLoss = !shouldFocus && !ImGui.IsItemActive();
if (cancelInputAfterFocusLoss || completedAfterFocusLoss)
{
// NOTE: This happens after canceling editing by closing the input
// and reopen the state. Sadly there doesn't appear to be a simple fix for this.
_tabFocusIndex = -1;
modified = false;
}
if (completedAfterTabbing || completedAfterFocusLoss)
{
SetState(InputStates.Inactive);
if (double.IsNaN(_editValue))
_editValue = _startValue;
modified = false;
}
var valid = Evaluate(_jogDialText, ref _editValue);
// If the value is invalid, just revert it to what it was previously
if (!valid)
{
_editValue = _startValue;
_editValue = defaultValue;
_jogDialText = value.ToString(CultureInfo.InvariantCulture);
}
break;
}
//value = _editValue;
if (_state == InputStates.Dialing)
{
value = _dampedValue;
}
else
{
value = _editValue;
}
if (_state == InputStates.Inactive)
{
return InputEditStateFlags.Finished;
}
return modified ? InputEditStateFlags.Modified : InputEditStateFlags.Started;
}
ImGui.PushStyleVar(ImGuiStyleVar.ButtonTextAlign, new Vector2(horizontalAlign,0.5f));
ImGui.Button(FormatValueForButton(ref value) + "###button", size);
ImGui.PopStyleVar();
//DrawButtonWithDynamicLabel(FormatValueForButton(ref value), ref size);
DrawValueRangeIndicator(value, min, max);
if (ImGui.IsItemActivated())
{
_activeJogDialId = componentId;
_editValue = value;
_startValue = value;
_jogDialText = FormatValueForButton(ref value);
SetState(InputStates.Dialing);
}
else
{
var isHovered = ImGui.IsItemHovered();
if (_state == InputStates.Inactive)
{
var isHoveredComponent = _activeHoverComponentId == componentId;
if (isHoveredComponent)
{
if (isHovered && (!UserSettings.Config.MouseWheelEditsNeedCtrlKey || io.KeyCtrl))
{
T3Ui.MouseWheelFieldHovered = true;
ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeEW);
var dl = ImGui.GetForegroundDrawList();
dl.AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), UiColors.StatusActivated);
var wheel = io.MouseWheel;
if (wheel == 0)
return InputEditStateFlags.Nothing;
if (useIntegers)
{
value += wheel * (io.KeyAlt ? 10 : 1);
}
else
{
var factor = 1f;
if (io.KeyShift)
{
factor = 0.01f;
}
else if (io.KeyAlt)
{
factor = 10f;
}
value += wheel * scale * 10 * factor;
}
_hoveredComponentModifiedByWheel = true;
return InputEditStateFlags.Modified;
}
var didModify = _hoveredComponentModifiedByWheel;
_hoveredComponentModifiedByWheel = false;
return didModify
? InputEditStateFlags.ModifiedAndFinished
: InputEditStateFlags.Nothing;
}
if (isHovered)
{
T3Ui.MouseWheelFieldHovered = true;
_hoveredComponentModifiedByWheel = false;
_activeHoverComponentId = componentId;
return InputEditStateFlags.Started;
}
}
}
return InputEditStateFlags.Nothing;
}
internal static void DrawValueEditMethod(ref double editValue, bool restarted, Vector2 center, double min, double max, bool clampMin, bool clampMax,
float scale)
{
switch (UserSettings.Config.ValueEditMethod)
{
case UserSettings.ValueEditMethods.InfinitySlider:
InfinitySliderOverlay.Draw(ref editValue, restarted, center, min, max, scale, clampMin, clampMax);
break;
case UserSettings.ValueEditMethods.RadialSlider:
RadialSliderOverlay.Draw(ref editValue, restarted, center, min, max, scale, clampMin, clampMax);
break;
case UserSettings.ValueEditMethods.JogDial:
JogDialOverlay.Draw(ref editValue, restarted, center, min, max, scale, clampMin, clampMax);
break;
case UserSettings.ValueEditMethods.ValueLadder:
default:
SliderLadder.Draw(ref editValue, min, max, scale, (float)(ImGui.GetTime() - _timeOpened), clampMin, clampMax, center);
break;
}
}
private static bool _hoveredComponentModifiedByWheel;
private static readonly SlidingAverage _slidingAverage = new(UserSettings.Config.ValueEditSmoothing.Clamp(0,100));
private static double _dampedValue;
private static void SetState(InputStates newState)
{
switch (newState)
{
case InputStates.Inactive:
{
_activeJogDialId = 0;
break;
}
case InputStates.Dialing:
_slidingAverage.Clear(UserSettings.Config.ValueEditSmoothing);
_center = ImGui.GetMousePos();
_timeOpened = ImGui.GetTime();
break;
case InputStates.StartedTextInput:
break;
case InputStates.TextInput:
break;
}
_state = newState;
}
private static int _currentTabIndex;
private static int _lastMaxTabIndex;
private static int _tabFocusIndex = -1; // if not -1 tries to set keyboard focus to input field.
private static bool Evaluate(string expression, ref double editValue)
{
try
{
var table = new DataTable();
table.Columns.Add("expression", typeof(string), expression);
var row = table.NewRow();
table.Rows.Add(row);
var newValue = double.Parse((string)row["expression"]);
if (double.IsNaN(newValue) || double.IsInfinity(newValue))
return false;
editValue = newValue;
return true;
}
catch
{
return false;
}
}
private static readonly Regex FilterFormattingPattern = new(@".*(\{0:(.+)\}).*");
private static string FormatValueForButton(ref double value, bool hideSuffix = false)
{
// Don't use rounding for integers
if (_numberFormat == "{0:0}")
return "" + (int)value;
try
{
if (!hideSuffix)
return string.Format(_numberFormat, value);
// Special formatting suffices like ° or × will break the parse and lead to incorrect value input.
// So we filter them out here.
var match = FilterFormattingPattern.Match(_numberFormat);
return string.Format(
match.Success
? match.Groups[1].Value
: _numberFormat, value);
}
catch (FormatException)
{
Log.Warning($"Invalid value format '{_numberFormat}'");
return value.ToString(CultureInfo.InvariantCulture);
}
}
/// <summary>
/// A horrible ImGui work around to have button that stays active while its label changes.
/// </summary>
private static void DrawButtonWithDynamicLabel(string label, ref Vector2 size)
{
var color1 = ImGuiCol.Text.GetStyleColor().Fade(ImGui.GetStyle().Alpha);
var keepPos = ImGui.GetCursorScreenPos();
ImGui.Button("##dial", size);
if (string.IsNullOrEmpty(label))
return;
ImGui.GetWindowDrawList().AddText(keepPos + new Vector2(4, 4), color1, label);
}
private static void DrawValueRangeIndicator(double value, double min, double max, bool isActive = false)
{
if (double.IsInfinity(min) && double.IsInfinity(max))
return;
var itemSize = ImGui.GetItemRectSize();
var itemPos = ImGui.GetItemRectMin();
var center = 0.0;
if (Math.Abs(min) > 0.001f)
{
center = MathUtils.RemapAndClamp(0, min, max, 0, itemSize.X);
// center = MathUtils.RemapAndClamp((min + max) * 0.5, min, max, 0, itemSize.X);
}
var end = MathUtils.RemapAndClamp(value, min, max, 0, itemSize.X);
var orgCenter = center;
if (center > end)
{
(center, end) = (end, center);
}
var p1 = itemPos + new Vector2((float)center, 0);
var p2 = itemPos + new Vector2((float)end, itemSize.Y);
var areaColor = UiColors.ForegroundFull.Fade(isActive ? 0.5f : 0.05f);
var centerColor = UiColors.ForegroundFull.Fade(isActive ? 0.5f : 0.1f);
var symbolColor = UiColors.ForegroundFull.Fade(isActive ? 0.8f : 0.06f);
ImGui.GetWindowDrawList().AddRectFilled(p1, p2, areaColor);
// Indicate center
var alignment = center < orgCenter ? -1 : 0;
ImGui.GetWindowDrawList().AddRectFilled(
ImGui.GetItemRectMin() + new Vector2((float)orgCenter + alignment, 0),
ImGui.GetItemRectMin() + new Vector2((float)orgCenter + alignment + 1, itemSize.Y),
centerColor);
// Indicate overflow
if (value < min)
{
var triangleCenter = new Vector2(itemPos.X + 5, itemPos.Y + itemSize.Y - 5);
ImGui.GetWindowDrawList().AddTriangleFilled(
triangleCenter + new Vector2(-3, 0),
triangleCenter + new Vector2(2, -4),
triangleCenter + new Vector2(2, 4),
symbolColor
);
}
else if (value > max)
{
var triangleCenter = new Vector2(itemPos.X + itemSize.X - 3, itemPos.Y + itemSize.Y - 5);
ImGui.GetWindowDrawList().AddTriangleFilled(
triangleCenter + new Vector2(-2, -4),
triangleCenter + new Vector2(3, 0),
triangleCenter + new Vector2(-2, 4),
symbolColor
);
}
}
private enum InputStates
{
Inactive,
Dialing,
StartedTextInput,
TextInput,
}
private static uint _activeJogDialId;
private static uint _activeHoverComponentId;
private static Vector2 _center;
private static double _editValue;
private static double _startValue;
private static string _jogDialText = "";
private static InputStates _state = InputStates.Inactive;
private static string _numberFormat = "{0:0.000}";
private static double _timeOpened;
private const double DefaultValue = 0.0;
/// <summary>
/// This is a horrible attempt to work around imguis current limitation that button elements can't have a tab focus
/// </summary>
public static void StartNextFrame()
{
_lastMaxTabIndex = _currentTabIndex;
_currentTabIndex = 0;
if (_tabFocusIndex > _lastMaxTabIndex)
{
Log.Debug("fixing tab overflow");
_tabFocusIndex = -1;
}
}
}