Skip to content
Merged
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
20 changes: 17 additions & 3 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
matrix:
unityVersion:
- 2021.3.45f2
- 2023.2.22f1
- 6000.6.6f1

steps:
- name: Checkout
Expand All @@ -71,17 +71,31 @@ jobs:
key: Library-${{ runner.os }}-${{ matrix.unityVersion }}-EditMode-${{ hashFiles('Packages/manifest.json', 'Packages/packages-lock.json', 'ProjectSettings/ProjectVersion.txt') }}
restore-keys: Library-${{ runner.os }}-${{ matrix.unityVersion }}-EditMode-

- name: Install window manager for Editor windows
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends openbox x11-utils

- name: Run EditMode tests
env:
LIBGL_ALWAYS_SOFTWARE: '1'
UNITY_EDITOR_VERSION: ${{ matrix.unityVersion }}
run: |
mkdir -p Logs
xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" \
unity --non-interactive test . \
--editor-version "${{ matrix.unityVersion }}" \
bash -euo pipefail <<'DISPLAY_SESSION'
# Xvfb supplies a display, but EditorWindow resize/focus also needs a WM.
openbox --sm-disable > Logs/window-manager.log 2>&1 &
window_manager_pid=$!
trap 'kill "$window_manager_pid" 2>/dev/null || true' EXIT
timeout 15 bash -c 'until xprop -root _NET_SUPPORTING_WM_CHECK | grep -q "window id"; do sleep 0.1; done'
unity --non-interactive test . \
--editor-version "$UNITY_EDITOR_VERSION" \
--mode EditMode \
--output Logs/TestResults/editmode.xml \
--timeout 900 \
-- -force-glcore -logFile Logs/editmode.log
DISPLAY_SESSION

- name: Return Unity license
if: always() && env.UNITY_LICENSE_ACTIVATION_ATTEMPTED == 'true'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

namespace MackySoft.SerializeReferenceExtensions.Editor
{
internal static class DecoratorGUI
{
#if !UNITY_2022_2_OR_NEWER
internal static VisualElement CreateGUI (VisualElement propertyGUI, SerializedProperty property) => CreateOwnedGUI(propertyGUI, property);
#else
internal static VisualElement CreateGUI (VisualElement propertyGUI, SerializedProperty property) => propertyGUI;
#endif

#if !UNITY_6000_0_OR_NEWER
internal static VisualElement CreateOwnedGUI (VisualElement propertyGUI, SerializedProperty property)
{
DecoratorDrawer[] decorators = UnityPropertyDrawerUtility.GetDecoratorDrawers(property);
if (decorators.Length == 0)
{
return propertyGUI;
}

var result = new VisualElement();
int firstIMGUI = 0;
for (int i = 0; i < decorators.Length; i++)
{
VisualElement gui = CreateDecoratorGUI(decorators[i]);
if (gui == null)
{
continue;
}
if (i > firstIMGUI)
{
result.Add(CreateIMGUI(decorators, firstIMGUI, i));
}
result.Add(gui);
firstIMGUI = i + 1;
}
if (firstIMGUI < decorators.Length)
{
result.Add(CreateIMGUI(decorators, firstIMGUI, decorators.Length));
}
result.Add(propertyGUI);
return result;
}

private static IMGUIContainer CreateIMGUI (DecoratorDrawer[] decorators, int first, int end)
{
return new IMGUIContainer(() =>
{
float height = 0;
for (int i = first; i < end; i++)
{
height += decorators[i].GetHeight();
}
Rect position = GUILayoutUtility.GetRect(0, height, GUILayout.ExpandWidth(true));
for (int i = first; i < end; i++)
{
DecoratorDrawer decorator = decorators[i];
position.height = decorator.GetHeight();
float labelWidth = EditorGUIUtility.labelWidth;
float fieldWidth = EditorGUIUtility.fieldWidth;
try
{
decorator.OnGUI(position);
}
finally
{
EditorGUIUtility.labelWidth = labelWidth;
EditorGUIUtility.fieldWidth = fieldWidth;
}
position.y += position.height;
}
});
}
#else
internal static VisualElement CreateOwnedGUI (VisualElement propertyGUI, SerializedProperty property) => propertyGUI;
#endif

#if UNITY_2022_2_OR_NEWER && !UNITY_6000_0_OR_NEWER
private static VisualElement CreateDecoratorGUI (DecoratorDrawer drawer) => drawer.CreatePropertyGUI();
#elif !UNITY_6000_0_OR_NEWER
private static VisualElement CreateDecoratorGUI (DecoratorDrawer drawer) => null;
#endif
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;

namespace MackySoft.SerializeReferenceExtensions.Editor
{
// Route the popup's real right-click to Unity's bound Foldout. Unity retains ownership
// of the complete property menu, including disabled fields and third-party additions.
internal sealed class FoldoutPropertyMenuManipulator : Manipulator
{
private readonly Toggle header;
private readonly Func<bool> hasCurrentProperty;
private VisualElement panelRoot;
private EventBase pendingEvent;
private bool sending;
private bool dispatched;

internal FoldoutPropertyMenuManipulator (Toggle header, Func<bool> hasCurrentProperty)
{
this.header = header;
this.hasCurrentProperty = hasCurrentProperty;
}

protected override void RegisterCallbacksOnTarget ()
{
target.RegisterCallback<AttachToPanelEvent>(OnAttach);
target.RegisterCallback<DetachFromPanelEvent>(OnDetach);
if (target.panel != null)
{
Connect();
}
}

protected override void UnregisterCallbacksFromTarget ()
{
Disconnect();
target.UnregisterCallback<AttachToPanelEvent>(OnAttach);
target.UnregisterCallback<DetachFromPanelEvent>(OnDetach);
}

private void OnAttach (AttachToPanelEvent evt) => Connect();
private void OnDetach (DetachFromPanelEvent evt) => Disconnect();

private void Connect ()
{
Disconnect();
// An enabled panel ancestor also receives events aimed at disabled inputs.
panelRoot = target.panel.visualTree;
RegisterMenuEvents();
}

private void Disconnect ()
{
CancelPendingEvent();
if (panelRoot != null)
{
UnregisterMenuEvents();
panelRoot = null;
}
}

private bool IsMenuTarget (Vector2 position)
{
if (sending || target.panel == null || header.panel != target.panel
|| !target.worldBound.Contains(position) || !hasCurrentProperty())
{
return false;
}
// Bounds alone would also intercept an unrelated overlay above this input.
// Pick can return the previous element cached at the same pointer position
// after rebinding moves the header. Ask Unity for a fresh hit instead.
VisualElement picked = target.panel.PickAll(position, null);
return picked != null && (picked == target || target.Contains(picked));
}

private void Forward (EventBase original, EventBase copy)
{
CancelPendingEvent();
// SendEvent queues while another event is being dispatched. Keep the pooled
// copy owned until it reaches this callback, so it cannot be pooled and reused.
pendingEvent = copy;
dispatched = false;
copy.target = header;
original.PreventDefault();
original.StopPropagation();
sending = true;
try
{
header.SendEvent(copy);
}
finally
{
sending = false;
if (dispatched)
{
ReleasePendingEvent();
}
}
}

private bool ConsumeForwardedEvent (EventBase evt)
{
if (evt != pendingEvent)
{
return false;
}
if (sending)
{
// Synchronous dispatch still needs the sender's reference until SendEvent returns.
dispatched = true;
}
else
{
// A queued dispatch retains its own reference through the remaining callbacks.
ReleasePendingEvent();
}
return true;
}

private void CancelPendingEvent ()
{
if (pendingEvent != null)
{
pendingEvent.StopImmediatePropagation();
if (sending)
{
dispatched = true;
}
else
{
ReleasePendingEvent();
}
}
}

private void ReleasePendingEvent ()
{
pendingEvent?.Dispose();
pendingEvent = null;
}

#if UNITY_2022_2_OR_NEWER
private void RegisterMenuEvents ()
{
panelRoot.RegisterCallback<PointerUpEvent>(OnPointerUp, TrickleDown.TrickleDown);
panelRoot.RegisterCallback<ContextClickEvent>(OnContextClick, TrickleDown.TrickleDown);
}

private void UnregisterMenuEvents ()
{
panelRoot.UnregisterCallback<PointerUpEvent>(OnPointerUp, TrickleDown.TrickleDown);
panelRoot.UnregisterCallback<ContextClickEvent>(OnContextClick, TrickleDown.TrickleDown);
}

private void OnPointerUp (PointerUpEvent evt)
{
if (!ConsumeForwardedEvent(evt) && evt.button == (int)MouseButton.RightMouse && IsMenuTarget(evt.position))
{
Forward(evt, PointerUpEvent.GetPooled((IPointerEvent)evt));
}
}

private void OnContextClick (ContextClickEvent evt)
{
if (IsMenuTarget(evt.mousePosition))
{
evt.StopImmediatePropagation();
}
}
#else
private void RegisterMenuEvents () => panelRoot.RegisterCallback<MouseUpEvent>(OnMouseUp, TrickleDown.TrickleDown);
private void UnregisterMenuEvents () => panelRoot.UnregisterCallback<MouseUpEvent>(OnMouseUp, TrickleDown.TrickleDown);

private void OnMouseUp (MouseUpEvent evt)
{
if (!ConsumeForwardedEvent(evt) && evt.button == (int)MouseButton.RightMouse && IsMenuTarget(evt.mousePosition))
{
Forward(evt, MouseUpEvent.GetPooled((IMouseEvent)evt));
}
}
#endif
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Reflection;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

namespace MackySoft.SerializeReferenceExtensions.Editor
{
internal static class FoldoutPropertyNotifications
{
private static readonly MethodInfo NativeBooleanChange = FindNativeBooleanChange();

internal static void Install (Foldout field)
{
// PropertyField registers its custom-drawer callbacks before attaching the view.
field.RegisterCallback<AttachToPanelEvent>(OnFirstAttach);
}

private static void OnFirstAttach (AttachToPanelEvent evt)
{
var field = (Foldout)evt.currentTarget;
field.UnregisterCallback<AttachToPanelEvent>(OnFirstAttach);
PropertyField owner = field.parent as PropertyField;
if (owner == null)
{
return;
}

var native = (EventCallback<ChangeEvent<bool>>)Delegate.CreateDelegate(
typeof(EventCallback<ChangeEvent<bool>>), owner, NativeBooleanChange);
// Delegate identity selects only Unity's value-notification callback. The native
// expansion binding and consumer callbacks on this Foldout remain registered.
field.UnregisterCallback(native);
field.RegisterCallback<ChangeEvent<bool>>(change =>
{
if (!(change.target is Foldout))
{
native(change);
}
});
}

private static MethodInfo FindNativeBooleanChange ()
{
foreach (MethodInfo method in typeof(PropertyField).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic))
{
if (!method.Name.StartsWith("<RegisterPropertyChangesOnCustomDrawerElement>", StringComparison.Ordinal))
{
continue;
}
ParameterInfo[] parameters = method.GetParameters();
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(ChangeEvent<bool>))
{
return method;
}
}
throw new MissingMethodException(typeof(PropertyField).FullName, "RegisterPropertyChangesOnCustomDrawerElement: bool callback");
}
}
}
Loading
Loading