Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions TuneLab.GUI/GUI/Assets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ internal static class Assets
public static SvgIcon Part = new("<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n<rect x=\"0.5\" y=\"3.5\" width=\"23\" height=\"16\" rx=\"1.5\" stroke=\"white\"/>\r\n<path d=\"M0 5C0 3.89543 0.895431 3 2 3H22C23.1046 3 24 3.89543 24 5V7H0V5Z\" fill=\"white\"/>\r\n<rect x=\"3\" y=\"9\" width=\"6\" height=\"2\" fill=\"white\"/>\r\n<rect x=\"9\" y=\"15\" width=\"6\" height=\"2\" fill=\"white\"/>\r\n<rect x=\"15\" y=\"11\" width=\"6\" height=\"2\" fill=\"white\"/>\r\n</svg>");
// 音符(双符头 + 横梁),用作 Note 属性侧栏页签图标,与卡片状的 Part 图标区分。
public static SvgIcon Note = new("<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n<circle cx=\"7\" cy=\"17\" r=\"3\" fill=\"white\"/>\r\n<circle cx=\"17\" cy=\"15\" r=\"3\" fill=\"white\"/>\r\n<path d=\"M10 17V7L20 5V15\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\r\n</svg>");
// 历史(逆时针回转箭头 + 时钟指针),用于可导航的撤销/重做记录侧栏。
public static SvgIcon History = new("<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n<path d=\"M5.5 8H2.5V5\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\r\n<path d=\"M3.5 8C5 4.8 8.2 3 12 3C17 3 21 7 21 12C21 17 17 21 12 21C7.8 21 4.3 18.1 3.3 14.2\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\"/>\r\n<path d=\"M12 7V12L15.5 14\" stroke=\"white\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\r\n</svg>");
public static SvgIcon Properties = new("<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n<path d=\"M2 6C2 5.44772 2.44772 5 3 5H15V7H3C2.44772 7 2 6.55228 2 6V6Z\" fill=\"white\"/>\r\n<path d=\"M2 12C2 11.4477 2.44772 11 3 11H5V13H3C2.44772 13 2 12.5523 2 12V12Z\" fill=\"white\"/>\r\n<path d=\"M2 18C2 17.4477 2.44772 17 3 17H10V19H3C2.44772 19 2 18.5523 2 18V18Z\" fill=\"white\"/>\r\n<path d=\"M19 5H21C21.5523 5 22 5.44772 22 6V6C22 6.55228 21.5523 7 21 7H19V5Z\" fill=\"white\"/>\r\n<path d=\"M9 11H21C21.5523 11 22 11.4477 22 12V12C22 12.5523 21.5523 13 21 13H9V11Z\" fill=\"white\"/>\r\n<path d=\"M14 17H21C21.5523 17 22 17.4477 22 18V18C22 18.5523 21.5523 19 21 19H14V17Z\" fill=\"white\"/>\r\n<rect x=\"16\" y=\"3\" width=\"2\" height=\"6\" rx=\"1\" fill=\"white\"/>\r\n<rect x=\"6\" y=\"9\" width=\"2\" height=\"6\" rx=\"1\" fill=\"white\"/>\r\n<rect x=\"11\" y=\"15\" width=\"2\" height=\"6\" rx=\"1\" fill=\"white\"/>\r\n</svg>\r\n");
public static SvgIcon Track = new("<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n<g clip-path=\"url(#clip0_1327_14013)\">\r\n<path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M11 1C11 0.447716 11.4477 0 12 0C12.5523 0 13 0.447715 13 1V5V9V10V14V15V19V23C13 23.5523 12.5523 24 12 24C11.4477 24 11 23.5523 11 23V19V15V14V10V9V5V1ZM10 5H1C0.447715 5 0 5.44772 0 6V8C0 8.55229 0.447715 9 1 9H10V5ZM10 10H9C8.44771 10 8 10.4477 8 11V13C8 13.5523 8.44771 14 9 14H10V10ZM10 15H6C5.44772 15 5 15.4477 5 16V18C5 18.5523 5.44772 19 6 19H10V15ZM14 19H18C18.5523 19 19 18.5523 19 18V16C19 15.4477 18.5523 15 18 15H14V19ZM14 14H23C23.5523 14 24 13.5523 24 13V11C24 10.4477 23.5523 10 23 10H14V14ZM14 9H15C15.5523 9 16 8.55229 16 8V6C16 5.44772 15.5523 5 15 5H14V9Z\" fill=\"white\"/>\r\n</g>\r\n<defs>\r\n<clipPath id=\"clip0_1327_14013\">\r\n<rect width=\"24\" height=\"24\" fill=\"white\"/>\r\n</clipPath>\r\n</defs>\r\n</svg>");
public static SvgIcon Gain = new("<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n<path d=\"M14 13V4.41421C14 3.52331 12.9229 3.07714 12.2929 3.70711L3.70711 12.2929C3.07714 12.9229 3.52331 14 4.41421 14H13C13.5523 14 14 13.5523 14 13Z\" fill=\"white\"/>\r\n</svg>");
Expand Down
63 changes: 35 additions & 28 deletions TuneLab.GUI/GUI/Controllers/ArrayController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ protected ArrayControllerBase()
}

// 绑定到(新的)数组数据外观;切换数据时清空旧行(复用的 widget 仍绑在旧 token 上)。
public void Bind(IDataPropertyArray array)
public void Bind(IDataPropertyArray array, string? detail = null)
{
ResetRows();
mArray = array;
mDetail = detail;
}

public void Unbind()
{
ResetRows();
mArray = null;
mDetail = null;
}

protected virtual bool Deletable => false;
Expand Down Expand Up @@ -66,7 +68,7 @@ void RemoveRow(int position, string? token)
return;

mArray.RemoveAt(index);
mArray.Commit();
mArray.Commit("Delete List Item", mDetail);
}

// 行的删除回调(Deletable 时非 null):真实行按 token、seed 行按位置。
Expand Down Expand Up @@ -131,7 +133,7 @@ protected void ReconcileRows(IReadOnlyList<IControllerConfig> elements)
}
else
{
nextByKey.Add(key, new ElementRow(host, key, cfg, onDelete));
nextByKey.Add(key, new ElementRow(host, key, cfg, mDetail, onDelete));
structureChanged = true; // 新建(含同 key 换类型 / seed 位物化为真实位)
}
nextOrder.Add(key);
Expand Down Expand Up @@ -195,6 +197,8 @@ void ResetRows()
}

protected IDataPropertyArray? mArray;
protected string? Detail => mDetail;
string? mDetail;
IReadOnlyList<IControllerConfig> mElements = []; // 当前各元素 config(用于 seed 物化默认值)
readonly StackPanel mRowsPanel = new() { Orientation = Orientation.Vertical };
Dictionary<string, ElementRow> mRowsByKey = new();
Expand Down Expand Up @@ -254,7 +258,7 @@ void AddElement(AddableElement addable)
{
MaterializeSeed(); // 先把当前展示的 seed 行物化为真实元素,再追加——否则其余 seed 行会塌掉
Array.Add(addable.Template.GetDefaultValue());
Array.Commit();
Array.Commit("Add List Item", Detail);
}

readonly Button mAddButton;
Expand All @@ -268,10 +272,10 @@ sealed class ElementRow : IDisposable
public Control Root => mRoot;
public Type ConfigType => mWidget.ConfigType;

public ElementRow(IDataPropertyObject host, string bindKey, IControllerConfig config, Action? onDelete)
public ElementRow(IDataPropertyObject host, string bindKey, IControllerConfig config, string? detail, Action? onDelete)
{
// host = 现存位的数组外观(bindKey = token)或越界位的 SeedPositionView(bindKey 仅作行身份、视图按 position 寻址)。
mWidget = ElementWidget.Create(host, bindKey, config);
mWidget = ElementWidget.Create(host, bindKey, config, detail);

mRoot = new LayerPanel { Background = Brushes.Transparent, Margin = new(24, 6, 24, 6) };
mRoot.Children.Add(mWidget.View); // 底层:控件填满整行
Expand Down Expand Up @@ -313,26 +317,26 @@ internal abstract class ElementWidget : IDisposable

protected readonly DisposableManager s = new();

public static ElementWidget Create(IDataPropertyObject dataObject, string token, IControllerConfig config) => config switch
public static ElementWidget Create(IDataPropertyObject dataObject, string token, IControllerConfig config, string? detail = null) => config switch
{
SliderConfig c => new SliderElement(dataObject, token, c),
TextBoxConfig c => new TextElement(dataObject, token, c),
ComboBoxConfig c => new ComboElement(dataObject, token, c),
CheckBoxConfig c => new CheckElement(dataObject, token, c),
SliderConfig c => new SliderElement(dataObject, token, c, detail),
TextBoxConfig c => new TextElement(dataObject, token, c, detail),
ComboBoxConfig c => new ComboElement(dataObject, token, c, detail),
CheckBoxConfig c => new CheckElement(dataObject, token, c, detail),
ObjectConfig c => new ObjectElement(dataObject, token, c),
ArrayConfig c => new NestedArrayElement(dataObject, token, c),
ListConfig c => new NestedListElement(dataObject, token, c),
ExtensibleObjectConfig c => new NestedExtensibleObjectElement(dataObject, token, c),
ArrayConfig c => new NestedArrayElement(dataObject, token, c, detail),
ListConfig c => new NestedListElement(dataObject, token, c, detail),
ExtensibleObjectConfig c => new NestedExtensibleObjectElement(dataObject, token, c, detail),
_ => new UnknownElement(config),
};

sealed class SliderElement : ElementWidget
{
public SliderElement(IDataPropertyObject dataObject, string token, SliderConfig config)
public SliderElement(IDataPropertyObject dataObject, string token, SliderConfig config, string? detail)
{
mController = new SliderController { HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch };
Apply(config);
mController.BindDataProperty(dataObject.DoubleField(token, config.DefaultValue), s);
mController.BindDataProperty(dataObject.DoubleField(token, config.DefaultValue), s, detail: detail);
}

void Apply(SliderConfig config)
Expand All @@ -352,10 +356,10 @@ void Apply(SliderConfig config)

sealed class TextElement : ElementWidget
{
public TextElement(IDataPropertyObject dataObject, string token, TextBoxConfig config)
public TextElement(IDataPropertyObject dataObject, string token, TextBoxConfig config, string? detail)
{
mController = new SingleLineTextController { HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, IsPassword = config.IsPassword };
mController.BindDataProperty(dataObject.StringField(token, config.DefaultValue), s);
mController.BindDataProperty(dataObject.StringField(token, config.DefaultValue), s, detail: detail);
}

public override Control View => mController;
Expand All @@ -367,10 +371,11 @@ public TextElement(IDataPropertyObject dataObject, string token, TextBoxConfig c

sealed class ComboElement : ElementWidget
{
public ComboElement(IDataPropertyObject dataObject, string token, ComboBoxConfig config)
public ComboElement(IDataPropertyObject dataObject, string token, ComboBoxConfig config, string? detail)
{
mDataObject = dataObject;
mToken = token;
mDetail = detail;
mConfig = config;
mController = new ComboBoxController { HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch };
BindWith(config);
Expand All @@ -380,7 +385,7 @@ public ComboElement(IDataPropertyObject dataObject, string token, ComboBoxConfig
void BindWith(ComboBoxConfig config)
{
mController.SetConfig(config);
mController.BindDataProperty(mDataObject.ValueField(mToken, config.DefaultOption.Value), s);
mController.BindDataProperty(mDataObject.ValueField(mToken, config.DefaultOption.Value), s, detail: mDetail);
}

public override Control View => mController;
Expand All @@ -399,16 +404,17 @@ public override void Update(IControllerConfig config)

readonly IDataPropertyObject mDataObject;
readonly string mToken;
readonly string? mDetail;
readonly ComboBoxController mController;
ComboBoxConfig mConfig;
}

sealed class CheckElement : ElementWidget
{
public CheckElement(IDataPropertyObject dataObject, string token, CheckBoxConfig config)
public CheckElement(IDataPropertyObject dataObject, string token, CheckBoxConfig config, string? detail)
{
mController = new CheckBox { VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center };
mController.BindDataProperty(dataObject.BooleanField(token, config.DefaultValue), s);
mController.BindDataProperty(dataObject.BooleanField(token, config.DefaultValue), s, detail: detail);
}

public override Control View => mController;
Expand Down Expand Up @@ -442,9 +448,9 @@ public override void Dispose()
// 数组套数组元素:嵌 ArrayController/ListController,导航进 dataObject.Array(token)。
sealed class NestedArrayElement : ElementWidget
{
public NestedArrayElement(IDataPropertyObject dataObject, string token, ArrayConfig config)
public NestedArrayElement(IDataPropertyObject dataObject, string token, ArrayConfig config, string? detail)
{
mController.Bind(dataObject.Array(token));
mController.Bind(dataObject.Array(token), detail);
mController.Apply(config);
}

Expand All @@ -462,9 +468,9 @@ public override void Dispose()

sealed class NestedListElement : ElementWidget
{
public NestedListElement(IDataPropertyObject dataObject, string token, ListConfig config)
public NestedListElement(IDataPropertyObject dataObject, string token, ListConfig config, string? detail)
{
mController.Bind(dataObject.Array(token));
mController.Bind(dataObject.Array(token), detail);
mController.Apply(config);
}

Expand All @@ -483,9 +489,9 @@ public override void Dispose()
// 数组套变长键控对象元素:嵌 ExtensibleObjectController,导航进 dataObject.Object(token)。
sealed class NestedExtensibleObjectElement : ElementWidget
{
public NestedExtensibleObjectElement(IDataPropertyObject dataObject, string token, ExtensibleObjectConfig config)
public NestedExtensibleObjectElement(IDataPropertyObject dataObject, string token, ExtensibleObjectConfig config, string? detail)
{
mController.Bind(dataObject.Object(token));
mController.Bind(dataObject.Object(token), detail);
mController.Apply(config);
}

Expand Down Expand Up @@ -532,6 +538,7 @@ abstract class ForwardingDataObject(IDataObject inner) : IDataObject
public void EndMergeNotify() => inner.EndMergeNotify();
public bool Pushable() => inner.Pushable();
public bool Commit() => inner.Commit();
public bool Commit(string description, string? detail = null) => inner.Commit(description, detail);
public bool Discard() => inner.Discard();
public bool DiscardTo(Head head) => inner.DiscardTo(head);
public bool Undo() => inner.Undo();
Expand Down
20 changes: 14 additions & 6 deletions TuneLab.GUI/GUI/Controllers/ExtensibleObjectController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,18 @@ public ExtensibleObjectController()
Children.Add(mAddButton);
}

public void Bind(IDataPropertyObject dataObject)
public void Bind(IDataPropertyObject dataObject, string? detail = null)
{
ResetRows();
mDataObject = dataObject;
mDetail = detail;
}

public void Unbind()
{
ResetRows();
mDataObject = null;
mDetail = null;
}

public void Apply(ExtensibleObjectConfig config)
Expand Down Expand Up @@ -164,16 +166,18 @@ void AddKey(AddableKey addable)
if (mDataObject == null)
return;
mDataObject.SetValue(addable.Key.Id, addable.Template.GetDefaultValue());
mDataObject.Commit();
var detail = addable.Key.DisplayText ?? addable.Key.Id;
mDataObject.Commit("Add Property", string.IsNullOrWhiteSpace(detail) ? mDetail : detail);
}

// 删键 = 移除该键(presence 翻回 absent),提交。
void RemoveKey(string id)
{
if (mDataObject == null)
return;
var detail = mRowsByKey.TryGetValue(id, out var row) ? row.Detail : id;
mDataObject.RemoveValue(id);
mDataObject.Commit();
mDataObject.Commit("Delete Property", string.IsNullOrWhiteSpace(detail) ? mDetail : detail);
}

void ResetRows()
Expand All @@ -187,6 +191,7 @@ void ResetRows()
}

IDataPropertyObject? mDataObject;
string? mDetail;
IReadOnlyList<AddableKey> mAddableElements = [];
HashSet<string> mPresentKeys = new();
readonly StackPanel mRowsPanel = new() { Orientation = Orientation.Vertical };
Expand All @@ -200,11 +205,13 @@ sealed class KeyedRow : IDisposable
{
public Control Root => mRoot;
public Type ConfigType => mWidget.ConfigType;
public string Detail { get; private set; }

public KeyedRow(IDataPropertyObject dataObject, PropertyKey key, IControllerConfig config, Action onDelete)
{
mWidget = ElementWidget.Create(dataObject, key.Id, config);
mTitle = ArrayControlsFactory.MakeRowTitle(key.DisplayText ?? key.Id);
Detail = key.DisplayText ?? key.Id;
mWidget = ElementWidget.Create(dataObject, key.Id, config, Detail);
mTitle = ArrayControlsFactory.MakeRowTitle(Detail);

var content = new StackPanel { Orientation = Orientation.Vertical };
content.Children.Add(mTitle);
Expand All @@ -228,7 +235,8 @@ public KeyedRow(IDataPropertyObject dataObject, PropertyKey key, IControllerConf

public void Update(PropertyKey key, IControllerConfig config)
{
mTitle.Content = key.DisplayText ?? key.Id; // 语言切换等仅 DisplayText 变:重贴标签、不重建
Detail = key.DisplayText ?? key.Id;
mTitle.Content = Detail;
mWidget.Update(config);
}

Expand Down
Loading