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

1014 lines
40 KiB
C#

#nullable enable
using ImGuiNET;
using System.Diagnostics.CodeAnalysis;
using T3.Editor.Gui.Help;
using T3.Core.DataTypes.Vector;
using T3.Core.Operator;
using T3.Core.Operator.Slots;
using T3.Core.SystemUi;
using T3.Editor.App;
using T3.Editor.Gui.Graph.Dialogs;
using T3.Editor.Gui.Input;
using T3.Editor.Gui.Interaction.Variations;
using T3.Editor.Gui.Interaction.Variations.Model;
using T3.Editor.Gui.Styling;
using T3.Editor.Skills.Training;
using T3.Editor.Gui.Windows.Layouts;
using T3.Editor.Gui.Windows.Output;
using T3.Editor.Gui.Windows.RenderExport;
using T3.Editor.UiModel;
using T3.Editor.UiModel.Commands;
using T3.Editor.UiModel.Commands.Graph;
using T3.Editor.UiModel.InputsAndTypes;
using T3.Editor.Gui.Windows.Variations;
using T3.Editor.UiModel.Modification;
using T3.Editor.UiModel.ProjectHandling;
using T3.Editor.UiModel.Selection;
using T3.SystemUi;
namespace T3.Editor.Gui.Windows;
[HelpUiID("ParameterWindow")]
internal sealed class ParameterWindow : Window
{
public ParameterWindow()
{
_instanceCounter++;
Config.Title = LayoutHandling.ParametersPrefix + _instanceCounter;
Config.Visible = true;
MenuTitle = "Parameters";
WindowFlags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
_parameterWindowInstances.Add(this);
}
internal override List<Window> GetInstances()
{
return _parameterWindowInstances;
}
protected override void Close()
{
_parameterWindowInstances.Remove(this);
}
protected override void AddAnotherInstance()
{
// ReSharper disable once ObjectCreationAsStatement
new ParameterWindow(); // Required to call constructor
}
protected override void DrawContent()
{
// Insert invisible spill over input to catch accidental imgui focus attempts
{
ImGui.SetNextItemWidth(2);
var tmpBuffer = "";
ImGui.PushStyleColor(ImGuiCol.FrameBg, Color.Transparent.Rgba);
ImGui.InputText("##imgui workaround", ref tmpBuffer, 1);
ImGui.PopStyleColor();
ImGui.SameLine();
}
if (!NodeSelection.TryGetSelectedInstanceOrInput(out var instance, out var inputUi, out _selectionChanged))
{
var nodeSelection = ProjectView.Focused?.NodeSelection;
var sectionSettingsShown = nodeSelection != null && DrawSettingsForSelectedSections(nodeSelection);
if (!sectionSettingsShown
&& VariationHandling.ActivePoolForSnapshots != null
&& !SkillTraining.IsInPlayMode)
{
// Show the active composition's header (name/namespace) too, so entering an
// operator doesn't require a background click to reveal it.
var composition = ProjectView.Focused?.CompositionInstance;
if (composition != null)
{
if (DrawSymbolHeader(composition, composition.GetSymbolUi()))
composition.GetSymbolUi().FlagAsModified();
}
_snapshotControlView.Draw();
}
_lastSelectedInstanceId = Guid.Empty;
return;
}
if (inputUi != null)
{
_viewMode = ViewModes.Settings;
_parameterSettings.SelectInput(inputUi.Id);
}
else if (_selectionChanged)
{
_viewMode = ViewModes.Parameters; // Switch back from documentation
}
var symbol = instance.Symbol;
var parentSymbol = instance.Parent?.Symbol;
var path = instance.InstancePath;
// ReSharper disable once RedundantAssignment
instance = null; //allow to unload of instance type in case a recompilation occurs
// Draw dialogs
RenameInputDialog.Draw();
if (!symbol.TryGetOrCreateInstance(path, parentSymbol, out instance))
{
Log.Error($"Failed to get instance for {symbol.Name} with path {string.Join(",", path)}");
return;
}
// Clicking the graph background selects the composition itself. If it has no inputs
// to show, the snapshot control view is more useful than an empty parameter list.
// Handled before the ui-definition lookup, which requires a parent and would bail
// out for compositions at the home level.
if (_viewMode == ViewModes.Parameters
&& IsFocusedComposition(instance)
&& instance.Inputs.Count == 0
&& VariationHandling.ActivePoolForSnapshots != null
&& !SkillTraining.IsInPlayMode)
{
if (DrawSymbolHeader(instance, instance.GetSymbolUi()))
instance.GetSymbolUi().FlagAsModified();
_snapshotControlView.Draw();
return;
}
if (!TryGetUiDefinitions(instance, out var symbolUi, out var symbolChildUi))
return;
var modified = false;
modified |= DrawSymbolHeader(instance, symbolUi);
if (instance.Parent == null)
{
CustomComponents.EmptyWindowMessage("Home canvas.");
return;
}
switch (_viewMode)
{
case ViewModes.Parameters:
DrawChildNameAndFlags(instance);
DrawParametersArea(instance, symbolChildUi, symbolUi);
break;
case ViewModes.Settings:
modified |= _parameterSettings.DrawContent(symbolUi, ProjectView.Focused!.NodeSelection);
break;
}
if (modified)
symbolUi.FlagAsModified();
}
private bool DrawSymbolHeader(Instance op, SymbolUi symbolUi)
{
var modified = false;
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(5, 5));
ImGui.BeginChild("header", new Vector2(0, ImGui.GetFrameHeight() + 5),
ImGuiChildFlags.AlwaysUseWindowPadding,
ImGuiWindowFlags.NoScrollbar
| ImGuiWindowFlags.NoScrollWithMouse
| ImGuiWindowFlags.NoBackground);
{
ImGui.AlignTextToFramePadding();
// Namespace and symbol
ImGui.PushFont(Fonts.FontBold);
ImGui.TextUnformatted(op.Symbol.Name);
ImGui.PopFont();
ImGui.SameLine();
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextDisabled.Rgba);
ImGui.TextUnformatted(" in ");
ImGui.PopStyleColor();
/*ImGui.SameLine();
ImGui.TextUnformatted(op.Symbol.SymbolPackage.RootNamespace);*/ // This is redundant I keep it commented out for now just in case
ImGui.SameLine();
var namespaceForEdit = op.Symbol.Namespace ?? "";
var iconSize = ImGui.GetFrameHeight();
// 3 icons; the settings and help buttons each nudge themselves +2px right
var iconBlockWidth = 3 * iconSize + 2 * (ImGui.GetStyle().ItemSpacing.X + 2);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - iconBlockWidth - ImGui.GetStyle().ItemSpacing.X);
var symbol = op.Symbol;
var package = symbol.SymbolPackage;
if (!package.IsReadOnly)
{
var namespaceModified = InputWithTypeAheadSearch.Draw("##namespace",
EditorSymbolPackage.AllSymbols
.Select(i => i.Namespace)
.Distinct()
.OrderBy(i => i),
false,
ref namespaceForEdit,
out _,
out _);
if (namespaceModified && !string.IsNullOrEmpty(namespaceForEdit) && ImGui.IsKeyPressed(Key.Return.ToImGuiKey()))
{
if (!EditableSymbolProject.ChangeSymbolNamespace(symbol, namespaceForEdit, out var reason))
{
BlockingWindow.Instance.ShowMessageBox(reason, "Could not rename namespace");
}
else
{
modified = true;
}
}
}
else
{
ImGui.Text(op.Symbol.Namespace);
}
// Tags...
{
ImGui.SameLine();
// Hard right-align: read-only packages render the namespace as plain text,
// which ignores the item width set above.
ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetStyle().WindowPadding.X - iconBlockWidth);
modified |= DrawSymbolTagsButton(symbolUi);
}
// Settings Modes
{
var isSettingsMode = _viewMode == ViewModes.Settings;
if (_parameterSettings.DrawToggleIcon(symbolUi, ref isSettingsMode))
{
_viewMode = isSettingsMode ? ViewModes.Settings : ViewModes.Parameters;
}
CustomComponents.TooltipForLastItem("Click to toggle parameter settings.");
}
// Help icon — shows the doc in the Help window (tooltip while that window is closed).
OperatorHelp.DrawHelpIcon(symbolUi);
}
ImGui.EndChild();
ImGui.PopStyleVar();
return modified;
}
public static bool DrawSymbolTagsButton(SymbolUi symbolUi)
{
var modified = false;
var tagsPopupId = "##Tags" + symbolUi.Symbol.Id;
var symbolUiTags = symbolUi.Tags;
var state = GetButtonStatesForSymbolTags(symbolUiTags);
if (CustomComponents.IconButton(Icon.Bookmark, Vector2.Zero, state))
{
ImGui.OpenPopup(tagsPopupId);
UpdateTagColors();
}
if (ImGui.IsItemHovered())
{
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(7,7));
ImGui.SetNextWindowContentSize(new Vector2(250 * T3Ui.UiScaleFactor, 0));
if (ImGui.BeginTooltip())
{
CustomComponents.HelpText("Symbol Tags affect listing in the Symbol Library and Search widgets.");
FormInputs.AddVerticalSpace();
var hadTags = false;
foreach (SymbolUi.SymbolTags tagValue in Enum.GetValues(typeof(SymbolUi.SymbolTags)))
{
if (!symbolUiTags.HasFlag(tagValue) || tagValue == SymbolUi.SymbolTags.None)
continue;
hadTags = true;
DrawSymbolTag(symbolUiTags, tagValue);
ImGui.SameLine(0, 4);
if (ImGui.GetContentRegionAvail().X < 100)
ImGui.NewLine();
}
if (hadTags)
{
ImGui.NewLine();
FormInputs.AddVerticalSpace();
}
CustomComponents.HelpText("Click to Edit...");
ImGui.EndTooltip();
}
ImGui.PopStyleVar();
}
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(7,7));
if (ImGui.BeginPopup(tagsPopupId))
{
ImGui.Text("Symbol tags");
FormInputs.AddVerticalSpace(4 * T3Ui.UiScaleFactor);
foreach (SymbolUi.SymbolTags tagValue in Enum.GetValues(typeof(SymbolUi.SymbolTags)))
{
if (tagValue == SymbolUi.SymbolTags.None)
continue;
if (DrawSymbolTag(symbolUiTags, tagValue))
{
modified = true;
symbolUi.Tags ^= tagValue;
}
}
ImGui.EndPopup();
}
ImGui.PopStyleVar();
return modified;
}
public static CustomComponents.ButtonStates GetButtonStatesForSymbolTags(SymbolUi.SymbolTags symbolUiTags)
{
var state = (int)symbolUiTags == 0 ? CustomComponents.ButtonStates.Disabled : CustomComponents.ButtonStates.Emphasized;
if ((symbolUiTags & (SymbolUi.SymbolTags.Essential | SymbolUi.SymbolTags.Example | SymbolUi.SymbolTags.Project)) != 0)
state = CustomComponents.ButtonStates.Activated;
if ((symbolUiTags & (SymbolUi.SymbolTags.Obsolete | SymbolUi.SymbolTags.NeedsFix | SymbolUi.SymbolTags.HasUpdate)) != 0)
state = CustomComponents.ButtonStates.NeedsAttention;
return state;
}
private static bool DrawSymbolTag(SymbolUi.SymbolTags symbolUiTags, SymbolUi.SymbolTags tagValue)
{
ImGui.PushFont(Fonts.FontSmall);
var isClicked = false;
var drawList = ImGui.GetWindowDrawList();
var isActive = symbolUiTags.HasFlag(tagValue);
var tagName = tagValue.ToString().ToUpperInvariant();
var size = ImGui.CalcTextSize(tagName);
var padding = new Vector2(10, 4) * T3Ui.UiScaleFactor;
if (ImGui.InvisibleButton(tagName, size + padding * 2))
{
isClicked = true;
}
if (!_tagColors.TryGetValue(tagValue, out var color))
color = UiColors.TextMuted;
var textColor = isActive ? UiColors.ForegroundFull : ColorVariations.OperatorLabel.Apply(color).Fade(0.4f);
var bgColor = isActive ? color : ColorVariations.OperatorBackground.Apply(color).Fade(0.2f);
var isHovered = ImGui.IsItemHovered();
var hoverFade = isHovered ? 1 : 0.8f;
var pMin = ImGui.GetItemRectMin();
var pMax = ImGui.GetItemRectMax();
drawList.AddRectFilled(pMin, pMax, bgColor.Fade(hoverFade), 5);
drawList.AddText(pMin + padding, textColor.Fade(hoverFade), tagName);
ImGui.PopFont();
return isClicked;
}
/// <summary>
/// This is kind of annoying because the definition of the colors might change on theme switching.
/// So this list would need to be updated on popup open... :-/
/// </summary>
private static Dictionary<SymbolUi.SymbolTags, Color> UpdateTagColors()
{
return new Dictionary<SymbolUi.SymbolTags, Color>
{
{ SymbolUi.SymbolTags.Essential, UiColors.StatusActivated },
{ SymbolUi.SymbolTags.Example, UiColors.StatusActivated },
{ SymbolUi.SymbolTags.Project, UiColors.StatusActivated },
{ SymbolUi.SymbolTags.Obsolete, UiColors.StatusAttention },
{ SymbolUi.SymbolTags.NeedsFix, UiColors.StatusAttention },
{ SymbolUi.SymbolTags.HasUpdate, UiColors.StatusAttention },
};
}
private static Dictionary<SymbolUi.SymbolTags, Color> _tagColors = UpdateTagColors();
private void DrawParametersArea(Instance instance, SymbolUi.Child symbolChildUi, SymbolUi symbolUi)
{
var compositionSymbolUi = ProjectView.Focused?.InstView?.SymbolUi;
if (compositionSymbolUi == null)
return;
if (compositionSymbolUi == symbolUi)
{
compositionSymbolUi = instance.Parent?.GetSymbolUi();
if (compositionSymbolUi == null)
return;
}
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(5, 5));
ImGui.BeginChild("parameters", Vector2.Zero, ImGuiChildFlags.None | ImGuiChildFlags.AlwaysUseWindowPadding, ImGuiWindowFlags.NoBackground);
// Scroll back up on operator change
if (_selectionChanged)
ImGui.SetScrollY(0);
var selectedChildSymbolUi = instance.GetSymbolUi();
// Draw parameters
DrawParameters(instance, selectedChildSymbolUi, symbolChildUi, compositionSymbolUi, false, this);
FormInputs.AddVerticalSpace(15);
// With the Help window open the summary would only duplicate the doc shown there.
if (!HelpWindow.IsOpen && OperatorHelp.DrawHelpSummary(symbolUi))
HelpWindow.ShowTopic(HelpTopic.ForOperator(symbolUi.Symbol.Id), showWindow: true);
OperatorHelp.DocumentationRenderer.DrawLinksAndExamples(symbolUi);
CustomComponents.HandleDragScrolling(this);
ImGui.EndChild();
ImGui.PopStyleVar();
}
private void DrawChildNameAndFlags(Instance op)
{
var hideParameters = _parameterSettings.IsActive;
if (hideParameters)
return;
var symbolChildUi = op.GetChildUi();
if (symbolChildUi == null)
return;
// Add spacing before
FormInputs.AddVerticalSpace(3);
ImGui.Indent(5);
var scale = T3Ui.UiScaleFactor;
var frameHeight = ImGui.GetFrameHeight();
var spacing = ImGui.GetStyle().ItemSpacing.X;
var groupGap = 6 * scale;
// Icon group: bypass + disable + pin-to-output (like the parameter popup)
var iconGroupWidth = 3 * frameHeight + 2 * spacing;
// Preset selector only when the preset pool tracks this instance
var presetPool = VariationHandling.ActivePoolForPresets;
var showPresets = presetPool != null
&& VariationHandling.ActiveInstanceForPresets is { } presetInstance
&& presetInstance.SymbolChildId == op.SymbolChildId
&& presetInstance.Symbol.Id == op.Symbol.Id;
// Reserve exact widths so the icon group ends flush right, aligned with the
// symbol header above (whose child has 5px window padding).
var availWidth = ImGui.GetContentRegionAvail().X - 5 * scale;
float nameWidth;
var dropdownWidth = 0f;
if (showPresets)
{
// name | dropdown | new-preset | gap | icons
var flexibleWidth = availWidth - (frameHeight + iconGroupWidth + 3 * spacing + groupGap);
nameWidth = MathF.Max(60 * scale, flexibleWidth * 0.5f);
dropdownWidth = MathF.Max(60 * scale, flexibleWidth - nameWidth);
}
else
{
nameWidth = MathF.Max(60 * scale, availWidth - (iconGroupWidth + spacing + groupGap));
}
// SymbolChild Name
{
ImGui.SetNextItemWidth(nameWidth);
var nameForEdit = symbolChildUi.SymbolChild.Name;
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 5);
if (ImGui.InputText("##symbolChildName", ref nameForEdit, 128))
{
if (_symbolChildNameCommand != null)
_symbolChildNameCommand.NewName = nameForEdit;
symbolChildUi.SymbolChild.Name = nameForEdit;
}
ImGui.PopStyleVar();
if (ImGui.IsItemActivated() && op.Parent != null)
{
_symbolChildNameCommand = new ChangeSymbolChildNameCommand(symbolChildUi, op.Parent.Symbol);
}
if (ImGui.IsItemDeactivatedAfterEdit())
{
if (_symbolChildNameCommand != null)
UndoRedoStack.Add(_symbolChildNameCommand);
_symbolChildNameCommand = null;
}
// Fake placeholder text
if (string.IsNullOrEmpty(nameForEdit))
ImGui.GetWindowDrawList().AddText(ImGui.GetItemRectMin() + new Vector2(5, 4),
UiColors.TextMuted.Fade(0.5f),
"Untitled instance");
}
// Preset dropdown + new-preset button
if (showPresets)
{
CollectPresets(presetPool!);
// Which preset fully describes the current values? Scanning all presets (instead of only
// the last-activated one) keeps the indication correct after loading a project or when
// values were changed from elsewhere.
var matchingPreset = FindMatchingPreset(presetPool!, op);
var activePreset = presetPool!.ActiveVariation is { IsPreset: true } activeVariation
&& _presets.Contains(activeVariation)
? activeVariation
: null;
string label;
Color labelColor;
if (matchingPreset != null)
{
label = VariationPicker.GetTitle(matchingPreset);
labelColor = UiColors.ForegroundFull;
}
else if (activePreset != null)
{
// Based on a preset but modified — the emphasized new-preset button carries the hint
label = VariationPicker.GetTitle(activePreset);
labelColor = UiColors.TextMuted;
}
else if (_presets.Count == 0)
{
label = "No Presets";
labelColor = UiColors.TextMuted;
}
else
{
if (_presets.Count != _lastPresetCountForLabel)
{
_lastPresetCountForLabel = _presets.Count;
_presetCountLabel = _presets.Count == 1 ? "1 Preset" : $"{_presets.Count} Presets";
}
label = _presetCountLabel;
labelColor = UiColors.Text;
}
ImGui.SameLine();
var renamingPreset = TryGetPresetById(_renamingPresetId);
if (renamingPreset != null)
{
// Naming a fresh preset replaces the dropdown; Enter confirms, Escape keeps "untitled".
ImGui.SetNextItemWidth(dropdownWidth);
if (_focusPresetRename)
{
ImGui.SetKeyboardFocusHere();
_focusPresetRename = false;
}
ImGui.InputText("##presetName", ref renamingPreset.Title, 256, ImGuiInputTextFlags.AutoSelectAll);
if (ImGui.IsItemDeactivated())
{
_renamingPresetId = Guid.Empty;
presetPool.SaveVariationsToFile();
}
}
else
{
_renamingPresetId = Guid.Empty; // The named preset may vanish mid-edit (e.g. undo)
var selectedPreset = matchingPreset ?? activePreset;
var chosen = _presetPicker.Draw(_presets, presetPool, op, selectedPreset, label, dropdownWidth,
out var renameRequested,
labelColor: labelColor);
if (chosen != null)
{
presetPool.Apply(op, chosen);
}
else if (renameRequested && selectedPreset != null)
{
_renamingPresetId = selectedPreset.Id;
_focusPresetRename = true;
}
}
// Emphasize the new-preset button while there is something worth saving: non-default
// values that no existing preset describes. Dimmed (but still clickable) otherwise.
var hasNonDefaults = false;
foreach (var input in op.Inputs)
{
if (input.Input.IsDefault)
continue;
hasNonDefaults = true;
break;
}
var suggestSaving = hasNonDefaults && matchingPreset == null;
ImGui.SameLine();
if (CustomComponents.IconButton(Icon.Presets, Vector2.Zero,
suggestSaving
? CustomComponents.ButtonStates.Emphasized
: CustomComponents.ButtonStates.Default))
{
var newPreset = PresetCanvas.CreatePresetForActivePool();
if (newPreset != null)
{
VariationThumbnailRenderer.RequestRender(presetPool, op, newPreset.Id, toFile: true);
_renamingPresetId = newPreset.Id;
_focusPresetRename = true;
}
}
CustomComponents.TooltipForLastItem(suggestSaving
? "Parameters don't match any preset — save as new preset"
: "Create new preset from current values");
}
// Bypass / disable / pin-to-output icons
ImGui.SameLine(0, spacing + groupGap);
{
var bypassable = symbolChildUi.SymbolChild.IsBypassable();
var isBypassed = symbolChildUi.SymbolChild.IsBypassed;
if (CustomComponents.ToggleTwoIconsButton(ref isBypassed,
Icon.OperatorBypassOff,
Icon.OperatorBypassOn,
CustomComponents.ButtonStates.NeedsAttention,
CustomComponents.ButtonStates.Default,
isEnabled: bypassable)
&& bypassable)
{
UndoRedoStack.AddAndExecute(new ChangeInstanceBypassedCommand(symbolChildUi.SymbolChild, isBypassed));
}
CustomComponents.TooltipForLastItem(!bypassable
? "This operator cannot be bypassed"
: isBypassed
? "Bypassed — click to enable"
: "Bypass this operator");
ImGui.SameLine();
var isDisabled = symbolChildUi.SymbolChild.IsDisabled;
if (CustomComponents.ToggleIconButton(ref isDisabled,
Icon.OperatorDisabled,
Vector2.Zero,
CustomComponents.ButtonStates.NeedsAttention))
{
UndoRedoStack.AddAndExecute(new ChangeInstanceIsDisabledCommand(symbolChildUi, isDisabled));
}
CustomComponents.TooltipForLastItem(isDisabled ? "Disabled — click to enable" : "Disable this operator");
// Always drawn (disabled without an output window) so the icon row keeps its layout —
// skipping it would leave the preceding SameLine dangling and pull the parameter
// area up onto this line.
ImGui.SameLine();
var projectView = ProjectView.Focused;
var canPin = projectView != null && RenderProcess.OutputWindow != null && projectView.CompositionInstance != null;
if (!canPin)
{
CustomComponents.IconButton(Icon.PlayOutput, Vector2.Zero, CustomComponents.ButtonStates.Disabled);
CustomComponents.TooltipForLastItem("Pin to output — needs an output window");
}
else
{
var isPinned = RenderProcess.OutputWindow!.Pinning.TryGetPinnedEvaluationInstance(projectView!.Structure,
out var pinnedInstance)
&& pinnedInstance == op;
if (CustomComponents.ToggleIconButton(ref isPinned, Icon.PlayOutput, Vector2.Zero))
{
if (LayoutHandling.FocusMode)
{
var selectedImage = projectView.NodeSelection.GetFirstSelectedInstance();
if (selectedImage != null)
{
projectView.SetBackgroundOutput(selectedImage);
}
}
else
{
NodeActions.PinSelectedToOutputWindow(projectView,
projectView.NodeSelection,
projectView.CompositionInstance!,
true);
}
}
CustomComponents.TooltipForLastItem(isPinned
? "Shown as graph background — click to clear"
: "Show output as graph background");
}
}
ImGui.Unindent(5);
}
private void CollectPresets(SymbolVariationPool pool)
{
_presets.Clear();
foreach (var variation in pool.AllVariations)
{
if (variation.IsPreset)
_presets.Add(variation);
}
}
/// <summary>
/// Finds the preset that exactly describes the instance's current values, with an early-out on
/// the previous frame's match so the full scan only runs while values are changing.
/// </summary>
private Variation? FindMatchingPreset(SymbolVariationPool pool, Instance op)
{
var cached = TryGetPresetById(_matchedPresetId);
if (cached != null && SymbolVariationPool.DoesInstanceMatchPresetExactly(op, cached))
return cached;
_matchedPresetId = Guid.Empty;
// Prefer the last-activated preset when several store identical values
if (pool.ActiveVariation is { IsPreset: true } active
&& _presets.Contains(active)
&& SymbolVariationPool.DoesInstanceMatchPresetExactly(op, active))
{
_matchedPresetId = active.Id;
return active;
}
foreach (var preset in _presets)
{
if (!SymbolVariationPool.DoesInstanceMatchPresetExactly(op, preset))
continue;
_matchedPresetId = preset.Id;
return preset;
}
return null;
}
private Variation? TryGetPresetById(Guid id)
{
if (id == Guid.Empty)
return null;
foreach (var preset in _presets)
{
if (preset.Id == id)
return preset;
}
return null;
}
/// <summary>
/// Draw all parameters of the selected instance.
/// The actual implementation is done in <see cref="InputValueUi{T}.DrawParameterEdit"/>
/// </summary>
public static void DrawParameters(Instance instance, SymbolUi symbolUi, SymbolUi.Child symbolChildUi,
SymbolUi compositionSymbolUi, bool hideNonEssentials, ParameterWindow? parameterWindow = null)
{
var groupState = GroupState.None;
if (instance.Parent == null)
{
Log.Warning("can't draw parameters for root instance");
return;
}
ImGui.PushStyleColor(ImGuiCol.FrameBg, UiColors.BackgroundButton.Rgba);
ImGui.PushStyleColor(ImGuiCol.FrameBgHovered, UiColors.BackgroundHover.Rgba);
ImGui.PushID(instance.GetHashCode());
foreach (var inputSlot in instance.Inputs)
{
if (!symbolUi.InputUis.TryGetValue(inputSlot.Id, out var inputUi))
{
Log.Warning("Trying to access an non existing input, probably the op instance is not the actual one.");
continue;
}
InsertGroupsAndPadding(inputUi, ref groupState);
ImGui.PushID(inputSlot.Id.GetHashCode());
var skipIfDefault = groupState == GroupState.InsideClosed;
// Draw the actual parameter line implemented
// in the generic InputValueUi<T>.DrawParameterEdit() method
ImGui.PushID(inputSlot.Id.GetHashCode());
var editState = inputUi.DrawParameterEdit(inputSlot, compositionSymbolUi, symbolChildUi, hideNonEssentials: hideNonEssentials, skipIfDefault);
ImGui.PopID();
HandleInputEditState(instance, inputSlot, editState);
if (editState == InputEditStateFlags.ShowOptions && parameterWindow != null)
{
parameterWindow._viewMode = ViewModes.Settings;
parameterWindow._parameterSettings.SelectedInputId = inputUi.Id;
//NodeSelection.SetSelection(inputUi);
}
ImGui.PopID();
}
ImGui.PopID(); // Instance
ImGui.PopStyleColor(2);
if (groupState == GroupState.InsideOpened)
FormInputs.EndGroup();
}
/// <summary>
/// Translates the edit state of a parameter row into in-flight value commands so edits
/// are undoable. Shared by the regular parameter view and the snapshot control view.
/// </summary>
internal static void HandleInputEditState(Instance instance, IInputSlot inputSlot, InputEditStateFlags editState)
{
if (instance.Parent == null)
return;
if ((editState & InputEditStateFlags.Started) != 0)
{
_inputSlotForActiveCommand = inputSlot;
_inputValueCommandInFlight =
new ChangeInputValueCommand(instance.Parent.Symbol, instance.SymbolChildId, inputSlot.Input, inputSlot.Input.Value);
}
if ((editState & InputEditStateFlags.Modified) != 0)
{
if (_inputValueCommandInFlight == null || _inputSlotForActiveCommand != inputSlot)
{
_inputValueCommandInFlight =
new ChangeInputValueCommand(instance.Parent.Symbol, instance.SymbolChildId, inputSlot.Input, inputSlot.Input.Value);
_inputSlotForActiveCommand = inputSlot;
}
_inputValueCommandInFlight.AssignNewValue(inputSlot.Input.Value);
inputSlot.DirtyFlag.Invalidate();
}
if ((editState & InputEditStateFlags.Finished) != 0)
{
if (_inputValueCommandInFlight != null && _inputSlotForActiveCommand == inputSlot)
{
UndoRedoStack.Add(_inputValueCommandInFlight);
}
_inputValueCommandInFlight = null;
}
}
private static void InsertGroupsAndPadding(IInputUi inputUi, ref GroupState groupState)
{
// Layouts padding and groups
if (inputUi.AddPadding)
FormInputs.AddVerticalSpace(2);
if (string.IsNullOrEmpty(inputUi.GroupTitle))
return;
if (groupState == GroupState.InsideOpened)
FormInputs.EndGroup();
if (inputUi.GroupTitle.EndsWith("..."))
{
var isOpen = FormInputs.BeginGroup(inputUi.GroupTitle);
groupState = isOpen ? GroupState.InsideOpened : GroupState.InsideClosed;
}
else
{
groupState = GroupState.None;
FormInputs.AddVerticalSpace(5);
ImGui.PushFont(Fonts.FontSmall);
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.SetCursorPosX(4);
ImGui.TextUnformatted(inputUi.GroupTitle.ToUpperInvariant());
ImGui.PopStyleColor();
ImGui.PopFont();
FormInputs.AddVerticalSpace(2);
}
}
/// <summary>
/// The selected instance is re-resolved from its path each frame, so compare ids
/// rather than relying on reference identity with the focused composition.
/// </summary>
private static bool IsFocusedComposition(Instance instance)
{
var composition = ProjectView.Focused?.CompositionInstance;
if (composition == null)
return false;
return composition == instance
|| (composition.Symbol.Id == instance.Symbol.Id && composition.SymbolChildId == instance.SymbolChildId);
}
private static bool TryGetUiDefinitions(Instance instance,
[NotNullWhen(true)] out SymbolUi? symbolUi,
[NotNullWhen(true)] out SymbolUi.Child? symbolChildUi)
{
symbolUi = instance.GetSymbolUi();
symbolChildUi = null;
if (instance.Parent == null)
return false;
var parentUi = instance.Parent.GetSymbolUi();
symbolChildUi = parentUi.ChildUis[instance.SymbolChildId];
return true;
}
private static bool DrawSettingsForSelectedSections(NodeSelection nodeSelection)
{
var somethingVisible = false;
// Draw Section settings
foreach (var section in nodeSelection.GetSelectedNodes<Section>())
{
ImGui.PushID(section.Id.GetHashCode());
ImGui.Indent(10f * T3Ui.UiScaleFactor);
//ImGui.SetCursorPosX(10f*T3Ui.UiScaleFactor);
ImGui.PushFont(Fonts.FontLarge);
ImGui.TextUnformatted("Section settings");
ImGui.PopFont();
FormInputs.AddVerticalSpace();
ImGui.Separator();
FormInputs.AddVerticalSpace();
ImGui.ColorEdit4("color", ref section.Color.Rgba);
FormInputs.AddVerticalSpace();
CustomComponents.StylizedText("Label:", Fonts.FontBold, UiColors.TextMuted, true);
ImGui.TextWrapped(section.Label);
FormInputs.AddVerticalSpace();
CustomComponents.StylizedText("Description:", Fonts.FontBold, UiColors.TextMuted.Rgba, true);
ImGui.TextWrapped(section.Title);
FormInputs.AddVerticalSpace();
ImGui.Separator();
FormInputs.AddVerticalSpace();
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
CustomComponents.IconButton(Icon.Tip,new Vector2(20,20)* T3Ui.UiScaleFactor);
ImGui.SameLine();
ImGui.TextWrapped("How to create an Section:\n" +
"1.Select the operators you want to include in the section.\n" +
"2.Shift + A to add the section." );
FormInputs.AddVerticalSpace();
CustomComponents.IconButton(Icon.Tip, new Vector2(20, 20) * T3Ui.UiScaleFactor);
ImGui.SameLine();
ImGui.TextWrapped("How to edit an Section:\n" +
"1.Double click on the header.\n" +
"2.Type in label and/or description");
ImGui.PopStyleColor();
ImGui.Unindent();
ImGui.PopID();
somethingVisible = true;
}
return somethingVisible;
}
private enum GroupState
{
None,
InsideClosed,
InsideOpened,
}
private enum ViewModes
{
Parameters,
Settings,
}
private ViewModes _viewMode = ViewModes.Parameters;
public static bool IsAnyInstanceVisible()
{
return WindowManager.IsAnyInstanceVisible<ParameterWindow>();
}
private static readonly List<Window> _parameterWindowInstances = new();
private ChangeSymbolChildNameCommand? _symbolChildNameCommand;
private static ChangeInputValueCommand? _inputValueCommandInFlight;
private static IInputSlot? _inputSlotForActiveCommand;
private static int _instanceCounter;
private Guid _lastSelectedInstanceId;
private bool _selectionChanged;
private readonly ParameterSettings _parameterSettings = new();
private readonly SnapshotControlView _snapshotControlView = new();
private readonly VariationPicker _presetPicker = new();
private readonly List<Variation> _presets = new();
private int _lastPresetCountForLabel = -1;
private string _presetCountLabel = "";
private Guid _matchedPresetId;
private Guid _renamingPresetId;
private bool _focusPresetRename;
public static readonly RenameInputDialog RenameInputDialog = new();
}