Editor/ObjectGenericControlWidget.cs
using Nodebox.Attributes;
namespace Nodebox.Editor;
/// <summary>
/// This is a callback control widget, which is used to edit classes.
/// It shows key properties, and an edit button. On clicking the edit button
/// it'll show a propery sheet popup.
/// </summary>
[CustomEditor(typeof(object), WithAllAttributes = [typeof(ObjectGenericControlWidgetAttribute)])]
public partial class ObjectGenericControlWidget : ControlObjectWidget {
public override bool SupportsMultiEdit => true;
public override bool IsWideMode => _isInlineEditor;
public override bool IncludeLabel => !_isInlineEditor;
bool _isInlineEditor;
RealTimeSince _visibilityDebounce = 0;
Widget PlaceholderWidget;
Dictionary<SerializedProperty, ControlWidget> KeyPropertyWidgets;
public ObjectGenericControlWidget(SerializedProperty property) : base(property, true) {
PaintBackground = false;
if (property.TryGetAttribute<InlineEditorAttribute>(out var inlineEditor)) {
_isInlineEditor = true;
BuildInlineEditor(inlineEditor);
return;
}
Layout = Layout.Row();
Layout.Spacing = 2;
KeyPropertyWidgets = new();
var keys = SerializedObject?.Where(x => x.HasAttribute<KeyPropertyAttribute>()).ToArray();
if (keys is not null && keys.Length > 0) {
foreach (var key in keys) {
var widget = Layout.Add(ControlSheetRow.CreateEditor(key));
widget.Visible = key.ShouldShow();
KeyPropertyWidgets.Add(key, widget);
}
// Only show the full edit button if we have editable properties that aren't showing on the front bar.
var propertiesThatArentKeys = SerializedObject?.Where(x => x.IsEditable && x.ShouldShow() && !x.IsMethod && !x.HasAttribute<KeyPropertyAttribute>()).ToArray();
if (propertiesThatArentKeys.Length > 0) {
var row = Layout.AddColumn();
var popupButton = new IconButton("edit_note");
popupButton.OnClick = OpenPopup;
popupButton.ToolTip = $"Edit";
popupButton.Background = Theme.ControlBackground;
popupButton.Foreground = Theme.Text;
popupButton.IconSize = 16;
row.Add(popupButton);
row.AddStretchCell(1);
}
} else {
PlaceholderWidget = new Widget(this);
PlaceholderWidget.Size = Theme.RowHeight;
PlaceholderWidget.VerticalSizeMode = SizeMode.CanGrow;
PlaceholderWidget.HorizontalSizeMode = SizeMode.Flexible;
PlaceholderWidget.OnPaintOverride = PaintPlaceholder;
PlaceholderWidget.MouseClick = OpenPopup;
PlaceholderWidget.Cursor = CursorShape.Finger;
PlaceholderWidget.ToolTip = $"Edit";
Layout.Add(PlaceholderWidget, 1);
}
}
StickyPopup Popup;
public override void OnDestroyed() {
Popup?.Destroy();
Popup = null;
base.OnDestroyed();
}
protected void OpenPopup() {
if (Popup.IsValid()) {
Popup?.Destroy();
Popup = null;
return;
}
var obj = SerializedObject;
// if it's nullable, create for the actual value rather than the nullable container
if (SerializedProperty.IsNullable) {
// best way to do this?
obj = SerializedProperty.GetValue<object>().GetSerialized();
obj.ParentProperty = SerializedProperty;
}
if (obj is null) {
Log.Error("Cannot create ControlSheet for a null object");
return;
}
//
// Create a popup for the control sheet editor
//
{
var popup = new StickyPopup(null) {
Owner = this,
MinimumWidth = Width,
Position = ScreenRect.BottomLeft
};
var editor = EditorUtility.OpenControlSheet(obj, this, false);
popup.Layout.Add(editor);
popup.OnPaintOverride = PaintPopupBackground;
popup.Visible = true;
popup.Focus(true);
Popup = popup;
//
// Clear any unrelated popups
//
Popup.DestroyUnrelatedPopups();
}
}
bool PaintPopupBackground() {
Paint.ClearPen();
Paint.SetBrushLinear(0, Vector2.Down * 256, Theme.SurfaceBackground.Lighten(0.2f).WithAlpha(0.98f), Theme.SurfaceBackground.WithAlpha(0.95f));
Paint.DrawRect(Paint.LocalRect);
Paint.ClearBrush();
Paint.SetPen(Color.Black.WithAlpha(0.33f), 2, PenStyle.Solid);
Paint.DrawRect(Paint.LocalRect.Shrink(0, -10, 1, 1), 4);
return true;
}
bool PaintPlaceholder() {
var type = SerializedProperty.IsNullable ? SerializedProperty.NullableType : SerializedProperty.PropertyType;
var value = SerializedProperty.GetValue<object>();
type = value?.GetType() ?? type;
var displayInfo = DisplayInfo.ForType(type);
string labelText = displayInfo.Name;
// If the type has a custom ToString(), use that instead of the type name.
if (type.GetMethod("ToString").DeclaringType != typeof(object)) {
labelText = value?.ToString() ?? labelText;
}
Theme.DrawDropdown(LocalRect, labelText, displayInfo.Icon ?? "edit_note", Popup.IsValid(), IsControlDisabled);
return true;
}
/// <summary>
/// The property has [InlineEditor], so we want to unfold this and show it inline.
/// </summary>
void BuildInlineEditor(InlineEditorAttribute attribute) {
Layout = Layout.Column();
if (attribute.Label) {
Layout.Add(ControlSheet.CreateLabel(SerializedProperty));
}
var cs = new ControlSheet();
cs.AddObject(SerializedObject);
cs.Margin = 0;
Layout.Add(cs);
}
protected override void OnContextMenu(ContextMenuEvent e) {
if (!_isInlineEditor) return;
e.Accepted = true;
var property = SerializedProperty;
var menu = new ContextMenu(this);
menu.AddOption($"Copy {property.DisplayName}", "content_copy", () => {
var str = ToClipboardString();
EditorUtility.Clipboard.Copy(str);
});
menu.AddOption($"Paste as {property.DisplayName}", "content_paste", () => {
var str = EditorUtility.Clipboard.Paste();
FromClipboardString(str);
});
menu.AddOption("Reset to Default", "restart_alt", () => {
property.Parent.NoteStartEdit(property);
property.SetValue(property.GetDefault());
property.Parent.NoteFinishEdit(property);
});
menu.OpenAt(e.ScreenPosition, false);
}
[EditorEvent.Frame]
public void UpdateVisibility() {
if (KeyPropertyWidgets is null)
return;
if (_visibilityDebounce < 0.2f)
return;
_visibilityDebounce = Random.Shared.Float(0, 0.1f);
foreach (var kvp in KeyPropertyWidgets) {
kvp.Value.Visible = kvp.Key.ShouldShow();
}
}
}