From f69959ff692e8b1a4f1f2f54ad085f8ba07a2169 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Thu, 16 Nov 2023 17:50:25 +0100 Subject: [PATCH 01/30] Implement popup menu headings --- Assets/Resources/Prefabs/UI/PopupMenu.prefab | 2 +- .../Prefabs/UI/PopupMenuHeading.prefab | 137 ++++++++++++++++++ .../Prefabs/UI/PopupMenuHeading.prefab.meta | 7 + Assets/SEE/UI/PopupMenu/PopupMenu.cs | 91 +++++++++--- Assets/SEE/UI/PopupMenu/PopupMenuAction.cs | 16 -- Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs | 31 ++++ ...uAction.cs.meta => PopupMenuEntry.cs.meta} | 0 7 files changed, 244 insertions(+), 40 deletions(-) create mode 100644 Assets/Resources/Prefabs/UI/PopupMenuHeading.prefab create mode 100644 Assets/Resources/Prefabs/UI/PopupMenuHeading.prefab.meta delete mode 100644 Assets/SEE/UI/PopupMenu/PopupMenuAction.cs create mode 100644 Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs rename Assets/SEE/UI/PopupMenu/{PopupMenuAction.cs.meta => PopupMenuEntry.cs.meta} (100%) diff --git a/Assets/Resources/Prefabs/UI/PopupMenu.prefab b/Assets/Resources/Prefabs/UI/PopupMenu.prefab index 2865898c28..dd03a134c3 100644 --- a/Assets/Resources/Prefabs/UI/PopupMenu.prefab +++ b/Assets/Resources/Prefabs/UI/PopupMenu.prefab @@ -160,7 +160,7 @@ MonoBehaviour: m_Top: 0 m_Bottom: 0 m_ChildAlignment: 0 - m_Spacing: 3 + m_Spacing: 0 m_ChildForceExpandWidth: 1 m_ChildForceExpandHeight: 0 m_ChildControlWidth: 1 diff --git a/Assets/Resources/Prefabs/UI/PopupMenuHeading.prefab b/Assets/Resources/Prefabs/UI/PopupMenuHeading.prefab new file mode 100644 index 0000000000..b2c04b9731 --- /dev/null +++ b/Assets/Resources/Prefabs/UI/PopupMenuHeading.prefab @@ -0,0 +1,137 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &4296809060982174892 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8706819387747019311} + - component: {fileID: 2060764360537592436} + - component: {fileID: 3611351358325543688} + m_Layer: 5 + m_Name: PopupMenuHeading + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8706819387747019311 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4296809060982174892} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &2060764360537592436 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4296809060982174892} + m_CullTransparentMesh: 1 +--- !u!114 &3611351358325543688 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4296809060982174892} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Node Properties + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: d10d3fbe67cb68d41930a013bc4e2e43, type: 2} + m_sharedMaterial: {fileID: 21041790971390992, guid: d10d3fbe67cb68d41930a013bc4e2e43, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 18 + m_fontSizeBase: 18 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} diff --git a/Assets/Resources/Prefabs/UI/PopupMenuHeading.prefab.meta b/Assets/Resources/Prefabs/UI/PopupMenuHeading.prefab.meta new file mode 100644 index 0000000000..c147ed91f6 --- /dev/null +++ b/Assets/Resources/Prefabs/UI/PopupMenuHeading.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3ea654c8c3f555028adf5c7eb6451e83 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SEE/UI/PopupMenu/PopupMenu.cs b/Assets/SEE/UI/PopupMenu/PopupMenu.cs index 8976b6f7d8..5a1adbf104 100644 --- a/Assets/SEE/UI/PopupMenu/PopupMenu.cs +++ b/Assets/SEE/UI/PopupMenu/PopupMenu.cs @@ -30,15 +30,15 @@ public class PopupMenu : PlatformDependentComponent private CanvasGroup MenuCanvasGroup; /// - /// The transform under which the actions are listed. + /// The transform under which the entries are listed. /// - private RectTransform ActionList; + private RectTransform EntryList; /// - /// A queue of actions that were added before the menu was started. - /// These actions will be added to the menu once it is started. + /// A queue of entries that were added before the menu was started. + /// These entries will be added to the menu once it is started. /// - private readonly Queue ActionsBeforeStart = new(); + private readonly Queue EntriesBeforeStart = new(); /// /// Whether the menu should currently be shown. @@ -60,16 +60,16 @@ protected override void StartDesktop() // Instantiate the menu. Menu = (RectTransform)PrefabInstantiator.InstantiatePrefab(MenuPrefabPath, Canvas.transform, false).transform; MenuCanvasGroup = Menu.gameObject.MustGetComponent(); - ActionList = (RectTransform)Menu.Find("Action List"); + EntryList = (RectTransform)Menu.Find("Action List"); // The menu should be hidden when the user moves the mouse away from it. PointerHelper pointerHelper = Menu.gameObject.MustGetComponent(); pointerHelper.ExitEvent.AddListener(_ => HideMenu().Forget()); - // We add all actions that were added before the menu was started. - while (ActionsBeforeStart.Count > 0) + // We add all entries that were added before the menu was started. + while (EntriesBeforeStart.Count > 0) { - AddAction(ActionsBeforeStart.Dequeue()); + AddEntry(EntriesBeforeStart.Dequeue()); } // FIXME (#632): On the first appearance of the menu, it lacks a background. @@ -79,19 +79,39 @@ protected override void StartDesktop() } /// - /// Adds a new to the menu. + /// Adds a new to the menu. /// - /// The action to be added. - public void AddAction(PopupMenuAction action) + /// The entry to be added. + public void AddEntry(PopupMenuEntry entry) { if (Menu is null) { - ActionsBeforeStart.Enqueue(action); + EntriesBeforeStart.Enqueue(entry); return; } + switch (entry) + { + case PopupMenuAction action: + AddAction(action); + break; + case PopupMenuHeading heading: + AddHeading(heading); + break; + default: + throw new System.ArgumentException($"Unknown entry type: {entry.GetType()}"); + } + // TODO (#668): Respect priority - GameObject actionItem = PrefabInstantiator.InstantiatePrefab("Prefabs/UI/PopupMenuButton", ActionList, false); + } + + /// + /// Adds a new to the menu. + /// + /// The action to be added. + private void AddAction(PopupMenuAction action) + { + GameObject actionItem = PrefabInstantiator.InstantiatePrefab("Prefabs/UI/PopupMenuButton", EntryList, false); ButtonManagerBasic button = actionItem.MustGetComponent(); button.buttonText = action.Name; button.clickEvent.AddListener(() => @@ -106,29 +126,40 @@ public void AddAction(PopupMenuAction action) } /// - /// Adds all given to the menu. + /// Adds a new to the menu. + /// + /// The heading to be added. + private void AddHeading(PopupMenuHeading heading) + { + GameObject headingItem = PrefabInstantiator.InstantiatePrefab("Prefabs/UI/PopupMenuHeading", EntryList, false); + TextMeshProUGUI text = headingItem.MustGetComponent(); + text.text = heading.Text; + } + + /// + /// Adds all given to the menu. /// - /// The actions to be added. - public void AddActions(IEnumerable actions) + /// The entries to be added. + public void AddEntries(IEnumerable entries) { - foreach (PopupMenuAction action in actions) + foreach (PopupMenuEntry entry in entries) { - AddAction(action); + AddEntry(entry); } } /// - /// Removes all actions from the menu. + /// Removes all entries from the menu. /// - public void ClearActions() + public void ClearEntries() { if (Menu is null) { - ActionsBeforeStart.Clear(); + EntriesBeforeStart.Clear(); return; } - foreach (Transform child in ActionList) + foreach (Transform child in EntryList) { Destroyer.Destroy(child.gameObject); } @@ -185,5 +216,19 @@ public async UniTaskVoid HideMenu() Menu.gameObject.SetActive(false); } } + + /// + /// Convenience method that shows the menu with the given + /// at the given . + /// + /// The entries to be shown in the menu. + /// The position at which the menu should be shown. + public void ShowWith(IEnumerable entries, Vector2 position) + { + ClearEntries(); + AddEntries(entries); + MoveTo(position); + ShowMenu().Forget(); + } } } diff --git a/Assets/SEE/UI/PopupMenu/PopupMenuAction.cs b/Assets/SEE/UI/PopupMenu/PopupMenuAction.cs deleted file mode 100644 index f7c45953e1..0000000000 --- a/Assets/SEE/UI/PopupMenu/PopupMenuAction.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using UnityEngine.Events; - -namespace SEE.UI.PopupMenu -{ - /// - /// An action that can be added to a . - /// - /// The name of the action. - /// The action to be executed when the user clicks on the action. - /// The unicode glyph of the FontAwesome v6 icon - /// that should be displayed next to the action. - /// The priority of the action. Actions with a higher priority - /// are displayed first. - public record PopupMenuAction(string Name, Action Action, char IconGlyph, int priority = default); -} diff --git a/Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs b/Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs new file mode 100644 index 0000000000..4c87a7ed6b --- /dev/null +++ b/Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs @@ -0,0 +1,31 @@ +using System; +using UnityEngine.Events; + +namespace SEE.UI.PopupMenu +{ + /// + /// An entry in a . + /// The priority of the entry. Entries with a higher priority + /// are displayed first. + /// + public abstract record PopupMenuEntry(int Priority); + + /// + /// An action that can be added to a . + /// + /// The name of the action. + /// The action to be executed when the user clicks on the action. + /// The unicode glyph of the FontAwesome v6 icon + /// that should be displayed next to the action. + /// The priority of the entry. Entries with a higher priority + /// are displayed first. + public record PopupMenuAction(string Name, Action Action, char IconGlyph, int Priority = default) : PopupMenuEntry(Priority); + + /// + /// A heading that can be added to a . + /// + /// The text of the heading. + /// The priority of the entry. Entries with a higher priority + /// are displayed first. + public record PopupMenuHeading(string Text, int Priority = default) : PopupMenuEntry(Priority); +} diff --git a/Assets/SEE/UI/PopupMenu/PopupMenuAction.cs.meta b/Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs.meta similarity index 100% rename from Assets/SEE/UI/PopupMenu/PopupMenuAction.cs.meta rename to Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs.meta From a9229c1dc93a5d3571eaeaa28c7155333a6c75a0 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Thu, 16 Nov 2023 18:34:34 +0100 Subject: [PATCH 02/30] Fix PopupMenu background being invisible sometimes --- Assets/Resources/Prefabs/UI/PopupMenu.prefab | 4 ++-- Assets/SEE/UI/PopupMenu/PopupMenu.cs | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Assets/Resources/Prefabs/UI/PopupMenu.prefab b/Assets/Resources/Prefabs/UI/PopupMenu.prefab index dd03a134c3..fa77fd5e53 100644 --- a/Assets/Resources/Prefabs/UI/PopupMenu.prefab +++ b/Assets/Resources/Prefabs/UI/PopupMenu.prefab @@ -129,8 +129,8 @@ RectTransform: m_Children: [] m_Father: {fileID: 5912560621427071452} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} m_AnchoredPosition: {x: 125, y: 0} m_SizeDelta: {x: 250, y: 0} m_Pivot: {x: 0.5, y: 1} diff --git a/Assets/SEE/UI/PopupMenu/PopupMenu.cs b/Assets/SEE/UI/PopupMenu/PopupMenu.cs index 5a1adbf104..4cc96c4906 100644 --- a/Assets/SEE/UI/PopupMenu/PopupMenu.cs +++ b/Assets/SEE/UI/PopupMenu/PopupMenu.cs @@ -6,6 +6,7 @@ using SEE.Utils; using TMPro; using UnityEngine; +using UnityEngine.UI; namespace SEE.UI.PopupMenu { @@ -34,6 +35,11 @@ public class PopupMenu : PlatformDependentComponent /// private RectTransform EntryList; + /// + /// The content size fitter of the popup menu. + /// + private ContentSizeFitter contentSizeFitter; + /// /// A queue of entries that were added before the menu was started. /// These entries will be added to the menu once it is started. @@ -59,6 +65,7 @@ protected override void StartDesktop() { // Instantiate the menu. Menu = (RectTransform)PrefabInstantiator.InstantiatePrefab(MenuPrefabPath, Canvas.transform, false).transform; + contentSizeFitter = Menu.gameObject.MustGetComponent(); MenuCanvasGroup = Menu.gameObject.MustGetComponent(); EntryList = (RectTransform)Menu.Find("Action List"); @@ -72,8 +79,6 @@ protected override void StartDesktop() AddEntry(EntriesBeforeStart.Dequeue()); } - // FIXME (#632): On the first appearance of the menu, it lacks a background. - // We hide the menu by default. Menu.gameObject.SetActive(false); } @@ -198,6 +203,12 @@ public async UniTaskVoid ShowMenu() ShouldShowMenu = true; Menu.gameObject.SetActive(true); Menu.localScale = Vector3.zero; + // This may seem stupid, but unfortunately, due to a Unity bug, + // this appears to be the only way to make the content size fitter update. + // See https://forum.unity.com/threads/content-size-fitter-refresh-problem.498536/ + contentSizeFitter.enabled = false; + await UniTask.WaitForEndOfFrame(); + contentSizeFitter.enabled = true; await UniTask.WhenAll(Menu.DOScale(1, AnimationDuration).AsyncWaitForCompletion().AsUniTask(), MenuCanvasGroup.DOFade(1, AnimationDuration / 2).AsyncWaitForCompletion().AsUniTask()); } From 43a63a267661b0eb69de76b698282177534c3d2a Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 15:49:25 +0100 Subject: [PATCH 03/30] Implement GraphFilter This is a class that allows its users to filter graph elements by various criteria. --- .../SEE/DataModel/GraphSearch/GraphFilter.cs | 68 +++++++++++++++++++ .../DataModel/GraphSearch/GraphFilter.cs.meta | 3 + 2 files changed, 71 insertions(+) create mode 100644 Assets/SEE/DataModel/GraphSearch/GraphFilter.cs create mode 100644 Assets/SEE/DataModel/GraphSearch/GraphFilter.cs.meta diff --git a/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs new file mode 100644 index 0000000000..e6bd222427 --- /dev/null +++ b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using SEE.DataModel.DG; + +namespace SEE.DataModel.GraphSearch +{ + /// + /// A configurable filter for graph elements, mainly intended for use with . + /// + public class GraphFilter + { + // if empty, include all. otherwise, *at least one* must be present. + /// + /// A set of toggle attributes of which at least one must be present in a graph element for it to be included. + /// + public readonly ISet IncludeToggleAttributes = new HashSet(); + + /// + /// A set of toggle attributes of which none must be present in a graph element for it to be included. + /// + public readonly ISet ExcludeToggleAttributes = new HashSet(); + + /// + /// Elements that should always be excluded. + /// + public readonly ISet ExcludeElements = new HashSet(); + + /// + /// Whether to include nodes. + /// + public bool IncludeNodes = true; + + /// + /// Whether to include edges. + /// + public bool IncludeEdges = true; + + /// + /// Returns whether the given element should be included in the search results. + /// + /// The element to check. + /// The type of the element. + /// Whether the element should be included. + public bool Includes(T element) where T : GraphElement + { + return element switch + { + Node => IncludeNodes, + Edge => IncludeEdges, + _ => false + } + && (IncludeToggleAttributes.Count == 0 || IncludeToggleAttributes.Overlaps(element.ToggleAttributes)) + && !ExcludeElements.Contains(element) + && !ExcludeToggleAttributes.Overlaps(element.ToggleAttributes); + } + + /// + /// Applies the filter to the given elements, that is, returns only those elements that are included. + /// + /// The elements to filter. + /// The type of the elements. + /// The filtered elements. + public IEnumerable Apply(IEnumerable elements) where T : GraphElement + { + return elements.Where(Includes); + } + } +} diff --git a/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs.meta b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs.meta new file mode 100644 index 0000000000..c6e41a8396 --- /dev/null +++ b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f40d8a0e41f0452d833e5d9df6c481b2 +timeCreated: 1700171708 \ No newline at end of file From b533195a00ac17ba7a0e6280f8bb5299aa240a2a Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 15:49:50 +0100 Subject: [PATCH 04/30] Implement `Toggle` extension method --- ...tExtensions.cs => CollectionExtensions.cs} | 40 ++++++++++--------- ...s.cs.meta => CollectionExtensions.cs.meta} | 0 2 files changed, 21 insertions(+), 19 deletions(-) rename Assets/SEE/Utils/{ListExtensions.cs => CollectionExtensions.cs} (65%) rename Assets/SEE/Utils/{ListExtensions.cs.meta => CollectionExtensions.cs.meta} (100%) diff --git a/Assets/SEE/Utils/ListExtensions.cs b/Assets/SEE/Utils/CollectionExtensions.cs similarity index 65% rename from Assets/SEE/Utils/ListExtensions.cs rename to Assets/SEE/Utils/CollectionExtensions.cs index c3bbcb57ac..88b3a99e19 100644 --- a/Assets/SEE/Utils/ListExtensions.cs +++ b/Assets/SEE/Utils/CollectionExtensions.cs @@ -4,25 +4,8 @@ namespace SEE.Utils { - public static class ListExtensions + public static class CollectionExtensions { - public static void Resize(this List list, int count) - { - if (list.Count < count) - { - if (list.Capacity < count) - { - list.Capacity = count; - } - - int end = count - list.Count; - for (int i = 0; i < end; i++) - { - list.Add(default); - } - } - } - /// /// Returns all permutations of this . /// @@ -49,5 +32,24 @@ public static ISet> Permutations(this IList inputList) return result; } + + /// + /// Toggles the given in the given , + /// that is, if the set contains the element, it will be removed, otherwise it will be added. + /// + /// The set in which the element shall be toggled. + /// The element which shall be toggled. + /// The type of the elements in the set. + public static void Toggle(this ISet set, T element) + { + if (set.Contains(element)) + { + set.Remove(element); + } + else + { + set.Add(element); + } + } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Utils/ListExtensions.cs.meta b/Assets/SEE/Utils/CollectionExtensions.cs.meta similarity index 100% rename from Assets/SEE/Utils/ListExtensions.cs.meta rename to Assets/SEE/Utils/CollectionExtensions.cs.meta From 6e73a6b1b1057be4534a055f35efb2ee39e53bc6 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 15:50:57 +0100 Subject: [PATCH 05/30] Apply GraphFilter to GraphSearch --- Assets/SEE/DataModel/DG/GraphSearch.cs.meta | 3 --- Assets/SEE/DataModel/GraphSearch.meta | 3 +++ .../SEE/DataModel/{DG => GraphSearch}/GraphSearch.cs | 10 ++++++++-- Assets/SEE/DataModel/GraphSearch/GraphSearch.cs.meta | 3 +++ 4 files changed, 14 insertions(+), 5 deletions(-) delete mode 100644 Assets/SEE/DataModel/DG/GraphSearch.cs.meta create mode 100644 Assets/SEE/DataModel/GraphSearch.meta rename Assets/SEE/DataModel/{DG => GraphSearch}/GraphSearch.cs (94%) create mode 100644 Assets/SEE/DataModel/GraphSearch/GraphSearch.cs.meta diff --git a/Assets/SEE/DataModel/DG/GraphSearch.cs.meta b/Assets/SEE/DataModel/DG/GraphSearch.cs.meta deleted file mode 100644 index e655afaa6b..0000000000 --- a/Assets/SEE/DataModel/DG/GraphSearch.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 15f8fbf868d74f6d940b2d0d614f5dd7 -timeCreated: 1697754642 \ No newline at end of file diff --git a/Assets/SEE/DataModel/GraphSearch.meta b/Assets/SEE/DataModel/GraphSearch.meta new file mode 100644 index 0000000000..3dc0c05a73 --- /dev/null +++ b/Assets/SEE/DataModel/GraphSearch.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5576bb27322b498189cc83975680c415 +timeCreated: 1700171649 \ No newline at end of file diff --git a/Assets/SEE/DataModel/DG/GraphSearch.cs b/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs similarity index 94% rename from Assets/SEE/DataModel/DG/GraphSearch.cs rename to Assets/SEE/DataModel/GraphSearch/GraphSearch.cs index 44fb8300b8..21a34f89fd 100644 --- a/Assets/SEE/DataModel/DG/GraphSearch.cs +++ b/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using System.Linq; using FuzzySharp; +using SEE.DataModel.DG; -namespace SEE.DataModel.DG +namespace SEE.DataModel.GraphSearch { /// /// Allows searching for nodes by their source name. @@ -28,6 +29,11 @@ public class GraphSearch : IObserver /// private readonly Graph graph; + /// + /// The filter that is applied to the graph elements before they are searched. + /// + public GraphFilter Filter { get; } = new(); + /// /// Creates a new instance of for the given . /// @@ -49,7 +55,7 @@ public GraphSearch(Graph graph) public IEnumerable Search(string query, int limit = 10, int cutoff = 40) { return Process.ExtractTop(FilterString(query), elements.Keys, limit: limit, cutoff: cutoff) - .SelectMany(x => elements[x.Value].Select(Element => (x.Score, Element))) + .SelectMany(x => Filter.Apply(elements[x.Value]).Select(Element => (x.Score, Element))) .OrderByDescending(x => x.Score) .Select(x => x.Element); } diff --git a/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs.meta b/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs.meta new file mode 100644 index 0000000000..eb6e0e3de4 --- /dev/null +++ b/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e2bc5cd2ba5f498dbb66dc802641b47d +timeCreated: 1700171671 \ No newline at end of file From 8de9de50c53d3c72883c24157f1bef486202caf4 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 15:51:29 +0100 Subject: [PATCH 06/30] Implement static Icons class --- Assets/SEE/Utils/Icons.cs | 33 +++++++++++++++++++++++++++++++++ Assets/SEE/Utils/Icons.cs.meta | 3 +++ 2 files changed, 36 insertions(+) create mode 100644 Assets/SEE/Utils/Icons.cs create mode 100644 Assets/SEE/Utils/Icons.cs.meta diff --git a/Assets/SEE/Utils/Icons.cs b/Assets/SEE/Utils/Icons.cs new file mode 100644 index 0000000000..e68129e858 --- /dev/null +++ b/Assets/SEE/Utils/Icons.cs @@ -0,0 +1,33 @@ +namespace SEE.Utils +{ + /// + /// Contains unicode characters for icons in the FontAwesome 6 free font. + /// + /// If you need to find out what a given icon looks like, search for the Unicode sequence + /// (e.g. "F1B2") on https://fontawesome.com/icons. The first result should be the icon you're looking for. + /// + /// See https://github.com/uni-bremen-agst/SEE/wiki/Icons for more information. + /// + public static class Icons + { + public const char Node = '\uF1B2'; + public const char Edge = '\uF542'; + public const char OutgoingEdge = '\uF2F5'; + public const char IncomingEdge = '\uF2F6'; + public const char LiftedIncomingEdge = '\uF090'; + public const char LiftedOutgoingEdge = '\uF08B'; + public const char EmptyCheckbox = '\uF0C8'; + public const char CheckedCheckbox = '\uF14A'; + public const char MinusCheckbox = '\uF146'; + public const char DoubleCheckmark = '\uF560'; + public const char Checkmark = '\uF00C'; + public const char Trash = '\uF1F8'; + public const char Info = '\uF05A'; + public const char LightBulb = '\uF0EB'; + public const char Code = '\uF121'; + public const char TreeView = '\uF802'; + public const char Compare = '\uE13A'; + public const char Hide = '\uF070'; + public const char Show = '\uF06E'; + } +} diff --git a/Assets/SEE/Utils/Icons.cs.meta b/Assets/SEE/Utils/Icons.cs.meta new file mode 100644 index 0000000000..cf3de511cd --- /dev/null +++ b/Assets/SEE/Utils/Icons.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 82bfa0ee71124c189c85ddd900596c50 +timeCreated: 1700173761 \ No newline at end of file From 1132f8adc226f435c77f52bd8e70fa3012f187ef Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 15:54:51 +0100 Subject: [PATCH 07/30] Allow keeping PopupMenu open after click --- Assets/Resources/Prefabs/UI/PopupMenu.prefab | 4 +- Assets/SEE/UI/PopupMenu/PopupMenu.cs | 53 ++++++++++++++------ Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs | 4 +- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/Assets/Resources/Prefabs/UI/PopupMenu.prefab b/Assets/Resources/Prefabs/UI/PopupMenu.prefab index fa77fd5e53..dd03a134c3 100644 --- a/Assets/Resources/Prefabs/UI/PopupMenu.prefab +++ b/Assets/Resources/Prefabs/UI/PopupMenu.prefab @@ -129,8 +129,8 @@ RectTransform: m_Children: [] m_Father: {fileID: 5912560621427071452} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 1} - m_AnchorMax: {x: 0, y: 1} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 125, y: 0} m_SizeDelta: {x: 250, y: 0} m_Pivot: {x: 0.5, y: 1} diff --git a/Assets/SEE/UI/PopupMenu/PopupMenu.cs b/Assets/SEE/UI/PopupMenu/PopupMenu.cs index 4cc96c4906..ac03affff2 100644 --- a/Assets/SEE/UI/PopupMenu/PopupMenu.cs +++ b/Assets/SEE/UI/PopupMenu/PopupMenu.cs @@ -71,7 +71,15 @@ protected override void StartDesktop() // The menu should be hidden when the user moves the mouse away from it. PointerHelper pointerHelper = Menu.gameObject.MustGetComponent(); - pointerHelper.ExitEvent.AddListener(_ => HideMenu().Forget()); + pointerHelper.ExitEvent.AddListener(x => + { + // If the mouse is not moving, this may indicate that the trigger has just been + // menu entries being rebuilt instead of the mouse moving outside of the menu. + if (x.IsPointerMoving()) + { + HideMenu().Forget(); + } + }); // We add all entries that were added before the menu was started. while (EntriesBeforeStart.Count > 0) @@ -81,6 +89,8 @@ protected override void StartDesktop() // We hide the menu by default. Menu.gameObject.SetActive(false); + + // TODO: Make this scrollable once it gets too big. } /// @@ -119,15 +129,22 @@ private void AddAction(PopupMenuAction action) GameObject actionItem = PrefabInstantiator.InstantiatePrefab("Prefabs/UI/PopupMenuButton", EntryList, false); ButtonManagerBasic button = actionItem.MustGetComponent(); button.buttonText = action.Name; - button.clickEvent.AddListener(() => - { - action.Action(); - HideMenu().Forget(); - }); + + button.clickEvent.AddListener(OnClick); if (action.IconGlyph != default) { actionItem.transform.Find("Icon").gameObject.MustGetComponent().text = action.IconGlyph.ToString(); } + return; + + void OnClick() + { + action.Action(); + if (action.CloseAfterClick) + { + HideMenu().Forget(); + } + } } /// @@ -198,7 +215,7 @@ public void MoveTo(Vector2 position) /// Activates the menu and fades it in. /// This asynchronous method will return once the menu is fully shown. /// - public async UniTaskVoid ShowMenu() + public async UniTask ShowMenu() { ShouldShowMenu = true; Menu.gameObject.SetActive(true); @@ -217,7 +234,7 @@ await UniTask.WhenAll(Menu.DOScale(1, AnimationDuration).AsyncWaitForCompletion( /// Hides the menu and fades it out. /// This asynchronous method will return once the menu is fully hidden and deactivated. /// - public async UniTaskVoid HideMenu() + public async UniTask HideMenu() { ShouldShowMenu = false; // We use a fade effect rather than DOScale because it looks better. @@ -232,13 +249,21 @@ public async UniTaskVoid HideMenu() /// Convenience method that shows the menu with the given /// at the given . /// - /// The entries to be shown in the menu. - /// The position at which the menu should be shown. - public void ShowWith(IEnumerable entries, Vector2 position) + /// The entries to be shown in the menu. + /// If null, entries will not be modified. + /// The position at which the menu should be shown. + /// If null, menu will not be moved. + public void ShowWith(IEnumerable entries = null, Vector2? position = null) { - ClearEntries(); - AddEntries(entries); - MoveTo(position); + if (entries != null) + { + ClearEntries(); + AddEntries(entries); + } + if (position.HasValue) + { + MoveTo(position.Value); + } ShowMenu().Forget(); } } diff --git a/Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs b/Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs index 4c87a7ed6b..a40808e7d2 100644 --- a/Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs +++ b/Assets/SEE/UI/PopupMenu/PopupMenuEntry.cs @@ -17,9 +17,11 @@ public abstract record PopupMenuEntry(int Priority); /// The action to be executed when the user clicks on the action. /// The unicode glyph of the FontAwesome v6 icon /// that should be displayed next to the action. + /// Whether the menu should be closed after the action is executed. /// The priority of the entry. Entries with a higher priority /// are displayed first. - public record PopupMenuAction(string Name, Action Action, char IconGlyph, int Priority = default) : PopupMenuEntry(Priority); + public record PopupMenuAction(string Name, Action Action, char IconGlyph, bool CloseAfterClick = true, + int Priority = default) : PopupMenuEntry(Priority); /// /// A heading that can be added to a . From d90073296eb69ed9d07c0e608d2f6daed1945ef0 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 15:56:30 +0100 Subject: [PATCH 08/30] Don't throw exception when closed window is closed --- Assets/SEE/UI/Window/DesktopWindowSpace.cs | 6 +++++- Assets/SEE/UI/Window/WindowSpace.cs | 7 ++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Assets/SEE/UI/Window/DesktopWindowSpace.cs b/Assets/SEE/UI/Window/DesktopWindowSpace.cs index 5e5e35317e..d5769bd6af 100644 --- a/Assets/SEE/UI/Window/DesktopWindowSpace.cs +++ b/Assets/SEE/UI/Window/DesktopWindowSpace.cs @@ -154,7 +154,11 @@ void CloseTab(PanelTab panelTab) { if (panelTab.Panel == panel) { - CloseWindow(windows.First(x => x.Window.GetInstanceID() == panelTab.Content.gameObject.GetInstanceID())); + BaseWindow window = windows.FirstOrDefault(x => x.Window.GetInstanceID() == panelTab.Content.gameObject.GetInstanceID()); + if (window != null) + { + CloseWindow(window); + } if (panelTab.Panel.NumberOfTabs <= 1) { // All tabs were closed, so we send out an event diff --git a/Assets/SEE/UI/Window/WindowSpace.cs b/Assets/SEE/UI/Window/WindowSpace.cs index f2c9288fcf..52bbb19351 100644 --- a/Assets/SEE/UI/Window/WindowSpace.cs +++ b/Assets/SEE/UI/Window/WindowSpace.cs @@ -88,7 +88,6 @@ public void AddWindow(BaseWindow window) /// Closes a previously opened window. /// /// The window which should be closed. - /// If the given is already closed. /// If the given is null. public void CloseWindow(BaseWindow window) { @@ -96,12 +95,10 @@ public void CloseWindow(BaseWindow window) { throw new ArgumentNullException(nameof(window)); } - else if (!windows.Contains(window)) + else if (windows.Contains(window)) { - throw new ArgumentException("Given window is already closed."); + windows.Remove(window); } - - windows.Remove(window); } /// From dd0debe3ae98a60ee00fc79537db883c00b1edd2 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 15:58:57 +0100 Subject: [PATCH 09/30] Minor code style improvements --- .../SEE/Controls/Actions/ContextMenuAction.cs | 26 +++++++------------ .../Game/CityRendering/AbstractLayoutNode.cs | 12 ++++----- Assets/SEE/UI/Menu/NestedMenu.cs | 3 +-- Assets/SEE/Utils/MathExtensions.cs | 22 ++++++++-------- 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/Assets/SEE/Controls/Actions/ContextMenuAction.cs b/Assets/SEE/Controls/Actions/ContextMenuAction.cs index f301c1a5c0..9e8b785df1 100644 --- a/Assets/SEE/Controls/Actions/ContextMenuAction.cs +++ b/Assets/SEE/Controls/Actions/ContextMenuAction.cs @@ -42,13 +42,7 @@ private void Update() } IEnumerable actions = GetApplicableOptions(o.GraphElemRef.Elem, o.gameObject); - - PopupMenu.ClearActions(); - PopupMenu.AddActions(actions); - - // We want to move the popup menu to the cursor position before showing it. - PopupMenu.MoveTo(Input.mousePosition); - PopupMenu.ShowMenu().Forget(); + PopupMenu.ShowWith(actions, Input.mousePosition); } } @@ -81,19 +75,19 @@ private static IEnumerable GetCommonOptions(GraphElement graphE IList actions = new List { // TODO (#665): Ask for confirmation or allow undo. - new("Delete", DeleteElement, '\uF1F8'), + new("Delete", DeleteElement, Icons.Trash), // TODO (#666): Better properties view - new("Properties", ShowProperties, '\uF05A'), + new("Properties", ShowProperties, Icons.Info), }; if (gameObject != null) { - actions.Add(new("Highlight", Highlight, '\uF0EB')); + actions.Add(new("Highlight", Highlight, Icons.LightBulb)); } if (graphElement.Filename() != null) { - actions.Add(new("Show Code", ShowCode, '\uF121')); + actions.Add(new("Show Code", ShowCode, Icons.Code)); } return actions; @@ -179,7 +173,7 @@ private static IEnumerable GetNodeOptions(Node node, GameObject { IList actions = new List { - new("Show in TreeView", RevealInTreeView, '\uF802'), + new("Show in TreeView", RevealInTreeView, Icons.TreeView), }; return actions; @@ -200,18 +194,18 @@ private static IEnumerable GetEdgeOptions(Edge edge, GameObject { IList actions = new List { - new("Show at Source (TreeView)", RevealAtSource, '\uF802'), - new("Show at Target (TreeView)", RevealAtTarget, '\uF802'), + new("Show at Source (TreeView)", RevealAtSource, Icons.TreeView), + new("Show at Target (TreeView)", RevealAtTarget, Icons.TreeView), }; if (edge.Type == "Clone") { - actions.Add(new("Show Unified Diff", ShowUnifiedDiff, '\uE13A')); + actions.Add(new("Show Unified Diff", ShowUnifiedDiff, Icons.Compare)); } if (edge.IsInImplementation() && ReflexionGraph.IsDivergent(edge)) { - actions.Add(new("Accept Divergence", AcceptDivergence, '\uF00C')); + actions.Add(new("Accept Divergence", AcceptDivergence, Icons.Checkmark)); } return actions; diff --git a/Assets/SEE/Game/CityRendering/AbstractLayoutNode.cs b/Assets/SEE/Game/CityRendering/AbstractLayoutNode.cs index 0798a02cfa..8dcc11e059 100644 --- a/Assets/SEE/Game/CityRendering/AbstractLayoutNode.cs +++ b/Assets/SEE/Game/CityRendering/AbstractLayoutNode.cs @@ -36,9 +36,9 @@ public abstract class AbstractLayoutNode : ILayoutNode /// the mapping of graph nodes onto LayoutNodes this node should be added to protected AbstractLayoutNode(Node node, IDictionary toLayoutNode) { - this.Node = node; - this.ToLayoutNode = toLayoutNode; - this.ToLayoutNode[node] = this; + Node = node; + ToLayoutNode = toLayoutNode; + ToLayoutNode[node] = this; } /// @@ -179,8 +179,8 @@ public ICollection Successors public abstract Vector3 Ground { get; } private Vector3 relativePosition; - private bool isSublayoutNode = false; - private bool isSublayoutRoot = false; + private bool isSublayoutNode; + private bool isSublayoutRoot; private Sublayout sublayout; private ILayoutNode sublayoutRoot; @@ -199,4 +199,4 @@ public override string ToString() } } -} \ No newline at end of file +} diff --git a/Assets/SEE/UI/Menu/NestedMenu.cs b/Assets/SEE/UI/Menu/NestedMenu.cs index dfd57f1297..eb4cfb5eb6 100644 --- a/Assets/SEE/UI/Menu/NestedMenu.cs +++ b/Assets/SEE/UI/Menu/NestedMenu.cs @@ -3,9 +3,8 @@ using System.Linq; using FuzzySharp; using SEE.Controls; -using SEE.DataModel.DG; +using SEE.DataModel.GraphSearch; using SEE.GO; -using SEE.GO.Menu; using TMPro; using UnityEngine; using UnityEngine.Windows.Speech; diff --git a/Assets/SEE/Utils/MathExtensions.cs b/Assets/SEE/Utils/MathExtensions.cs index 857a24b583..27166cbed0 100644 --- a/Assets/SEE/Utils/MathExtensions.cs +++ b/Assets/SEE/Utils/MathExtensions.cs @@ -320,32 +320,32 @@ public static void TestCircleAABB(Vector2 center, float radius, Vector2 min, Vec public static Vector2 ZW(this Vector4 a) => new(a.z, a.w); /// - /// Returns the given , replacing any of its components with - /// , , and/or , respectively, if they were given. + /// Returns the given , replacing any of its components with + /// , and/or respectively, if they were given. /// If no parameters are given, this method will be equivalent to the identity function. /// - /// The vector whose components shall be replaced + /// The vector whose components shall be replaced /// New X component /// New Y component - /// New Z component /// with its components replaced - public static Vector3 WithXYZ(this Vector3 vector3, float? x = null, float? y = null, float? z = null) + public static Vector2 WithXY(this Vector2 vector2, float? x = null, float? y = null) { - return new Vector3(x ?? vector3.x, y ?? vector3.y, z ?? vector3.z); + return new Vector2(x ?? vector2.x, y ?? vector2.y); } /// - /// Returns the given , replacing any of its components with - /// , and/or respectively, if they were given. + /// Returns the given , replacing any of its components with + /// , , and/or , respectively, if they were given. /// If no parameters are given, this method will be equivalent to the identity function. /// - /// The vector whose components shall be replaced + /// The vector whose components shall be replaced /// New X component /// New Y component + /// New Z component /// with its components replaced - public static Vector2 WithXY(this Vector2 vector2, float? x = null, float? y = null) + public static Vector3 WithXYZ(this Vector3 vector3, float? x = null, float? y = null, float? z = null) { - return new Vector2(x ?? vector2.x, y ?? vector2.y); + return new Vector3(x ?? vector3.x, y ?? vector3.y, z ?? vector3.z); } /// From cee09c4f417c139fac7a61fa6ab25465fd186433 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 16:11:56 +0100 Subject: [PATCH 10/30] Make TreeWindow filterable --- Assets/Resources/Prefabs/UI/TreeView.prefab | 1461 ++++++++++++++++- .../UI/Window/TreeWindow/DesktopTreeWindow.cs | 322 +++- Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs | 50 +- 3 files changed, 1714 insertions(+), 119 deletions(-) diff --git a/Assets/Resources/Prefabs/UI/TreeView.prefab b/Assets/Resources/Prefabs/UI/TreeView.prefab index dddc220848..d49f18321b 100644 --- a/Assets/Resources/Prefabs/UI/TreeView.prefab +++ b/Assets/Resources/Prefabs/UI/TreeView.prefab @@ -1,5 +1,187 @@ %YAML 1.1 %TAG !u! tag:unity3d.com,2011: +--- !u!1 &429727636846351680 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5000712621006847538} + - component: {fileID: 8552963848894541773} + - component: {fileID: 2697686805765614399} + - component: {fileID: 2678860440501321340} + - component: {fileID: 6642898023618199802} + - component: {fileID: 3052445492809702230} + m_Layer: 5 + m_Name: Group + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5000712621006847538 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 429727636846351680} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 8967932099786957271} + - {fileID: 8680346654750948472} + m_Father: {fileID: 7868881898829307179} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 55} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8552963848894541773 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 429727636846351680} + m_CullTransparentMesh: 0 +--- !u!114 &2697686805765614399 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 429727636846351680} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 0 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1} + m_DisabledColor: {r: 1, g: 1, b: 1, a: 0.39215687} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 2678860440501321340} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &2678860440501321340 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 429727636846351680} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.33333334, g: 0.37254903, b: 0.4509804, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 5e16c7aea118d68498053518146c9cf9, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 13 +--- !u!114 &6642898023618199802 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 429727636846351680} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1a12ddbc47b17cd478cb447d1113a22b, type: 3} + m_Name: + m_EditorClassIdentifier: + buttonText: "\uF5FD" + clickEvent: + m_PersistentCalls: + m_Calls: [] + hoverEvent: + m_PersistentCalls: + m_Calls: [] + hoverSound: {fileID: 0} + clickSound: {fileID: 0} + buttonVar: {fileID: 0} + normalText: {fileID: 8361415602960380083} + soundSource: {fileID: 0} + rippleParent: {fileID: 6267150986429446833} + useCustomContent: 0 + enableButtonSounds: 0 + useHoverSound: 1 + useClickSound: 1 + useRipple: 1 + rippleUpdateMode: 1 + rippleShape: {fileID: 0} + speed: 1 + maxSize: 4 + startColor: {r: 1, g: 1, b: 1, a: 1} + transitionColor: {r: 1, g: 1, b: 1, a: 0} + renderOnTop: 0 + centered: 0 +--- !u!114 &3052445492809702230 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 429727636846351680} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: 55 + m_MinHeight: -1 + m_PreferredWidth: 55 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 --- !u!1 &842951385601223108 GameObject: m_ObjectHideFlags: 0 @@ -14,6 +196,7 @@ GameObject: - component: {fileID: 5081112725215650259} - component: {fileID: 8918530655449257547} - component: {fileID: 3444143981090016800} + - component: {fileID: 4789464308854986259} m_Layer: 5 m_Name: SearchField m_TagString: Untagged @@ -38,10 +221,10 @@ RectTransform: - {fileID: 3073758193502664746} m_Father: {fileID: 7868881898829307179} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0.5} - m_AnchorMax: {x: 1, y: 0.5} - m_AnchoredPosition: {x: -3.749939, y: 0} - m_SizeDelta: {x: -7.500131, y: 55} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 55} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &6756889094794557856 CanvasRenderer: @@ -204,6 +387,26 @@ MonoBehaviour: texts: - {fileID: 3548788532928508177} - {fileID: 1512550248131604402} +--- !u!114 &4789464308854986259 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 842951385601223108} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: 100 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: 100 + m_FlexibleHeight: -1 + m_LayoutPriority: 3 --- !u!1 &894369973855324406 GameObject: m_ObjectHideFlags: 0 @@ -279,6 +482,141 @@ MonoBehaviour: m_FillOrigin: 0 m_UseSpriteMesh: 0 m_PixelsPerUnitMultiplier: 15 +--- !u!1 &1391077163789461073 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5693542113043864410} + - component: {fileID: 4202743844568988407} + - component: {fileID: 2494380858770498350} + m_Layer: 5 + m_Name: Icon + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5693542113043864410 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1391077163789461073} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 7683108501662694733} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -25, y: -25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4202743844568988407 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1391077163789461073} + m_CullTransparentMesh: 0 +--- !u!114 &2494380858770498350 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1391077163789461073} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: "\uF0B0" + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 4ebb98a3c87fa521a888029274c92b79, type: 2} + m_sharedMaterial: {fileID: -8620075009897487826, guid: 4ebb98a3c87fa521a888029274c92b79, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 36 + m_fontSizeBase: 36 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} --- !u!1 &1512550248131604402 GameObject: m_ObjectHideFlags: 0 @@ -414,7 +752,7 @@ MonoBehaviour: m_hasFontAssetChanged: 0 m_baseMaterial: {fileID: 0} m_maskOffset: {x: 0, y: 0, z: 0, w: 0} ---- !u!1 &2851445155489688032 +--- !u!1 &2476908023509198667 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -422,34 +760,169 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 8846550913904393515} - - component: {fileID: 115155201325811548} - - component: {fileID: 2881518792493254415} + - component: {fileID: 9195632984993411493} + - component: {fileID: 7822887819180324640} + - component: {fileID: 4605941648409354567} m_Layer: 5 - m_Name: Background + m_Name: Icon m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!224 &8846550913904393515 +--- !u!224 &9195632984993411493 RectTransform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2851445155489688032} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_GameObject: {fileID: 2476908023509198667} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] - m_Father: {fileID: 2059283171776054673} + m_Father: {fileID: 3416948009511138372} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: -10, y: -10} + m_SizeDelta: {x: -25, y: -25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7822887819180324640 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2476908023509198667} + m_CullTransparentMesh: 0 +--- !u!114 &4605941648409354567 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2476908023509198667} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: "\uF0DC" + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 4ebb98a3c87fa521a888029274c92b79, type: 2} + m_sharedMaterial: {fileID: -8620075009897487826, guid: 4ebb98a3c87fa521a888029274c92b79, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 36 + m_fontSizeBase: 36 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &2851445155489688032 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8846550913904393515} + - component: {fileID: 115155201325811548} + - component: {fileID: 2881518792493254415} + m_Layer: 5 + m_Name: Background + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8846550913904393515 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2851445155489688032} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2059283171776054673} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -10, y: -10} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &115155201325811548 CanvasRenderer: @@ -736,6 +1209,141 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: -10, y: -10} m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &4280109293398571001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8967932099786957271} + - component: {fileID: 177693816970757469} + - component: {fileID: 8361415602960380083} + m_Layer: 5 + m_Name: Icon + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8967932099786957271 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4280109293398571001} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 5000712621006847538} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -25, y: -25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &177693816970757469 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4280109293398571001} + m_CullTransparentMesh: 0 +--- !u!114 &8361415602960380083 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4280109293398571001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: "\uF5FD" + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 4ebb98a3c87fa521a888029274c92b79, type: 2} + m_sharedMaterial: {fileID: -8620075009897487826, guid: 4ebb98a3c87fa521a888029274c92b79, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 36 + m_fontSizeBase: 36 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} --- !u!1 &4431518105055718632 GameObject: m_ObjectHideFlags: 0 @@ -811,6 +1419,188 @@ MonoBehaviour: m_FillOrigin: 0 m_UseSpriteMesh: 0 m_PixelsPerUnitMultiplier: 10 +--- !u!1 &5043279403120338259 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7683108501662694733} + - component: {fileID: 1994657440880389828} + - component: {fileID: 4745195452347766396} + - component: {fileID: 479753717333530281} + - component: {fileID: 6777758793146567997} + - component: {fileID: 3913614832241006454} + m_Layer: 5 + m_Name: Filter + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7683108501662694733 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5043279403120338259} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 5693542113043864410} + - {fileID: 3512017188549587436} + m_Father: {fileID: 7868881898829307179} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 55} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1994657440880389828 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5043279403120338259} + m_CullTransparentMesh: 0 +--- !u!114 &4745195452347766396 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5043279403120338259} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 0 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1} + m_DisabledColor: {r: 1, g: 1, b: 1, a: 0.39215687} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 479753717333530281} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &479753717333530281 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5043279403120338259} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.33333334, g: 0.37254903, b: 0.4509804, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 5e16c7aea118d68498053518146c9cf9, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 13 +--- !u!114 &6777758793146567997 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5043279403120338259} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1a12ddbc47b17cd478cb447d1113a22b, type: 3} + m_Name: + m_EditorClassIdentifier: + buttonText: "\uF0B0" + clickEvent: + m_PersistentCalls: + m_Calls: [] + hoverEvent: + m_PersistentCalls: + m_Calls: [] + hoverSound: {fileID: 0} + clickSound: {fileID: 0} + buttonVar: {fileID: 0} + normalText: {fileID: 2494380858770498350} + soundSource: {fileID: 0} + rippleParent: {fileID: 9139761773161578503} + useCustomContent: 0 + enableButtonSounds: 0 + useHoverSound: 1 + useClickSound: 1 + useRipple: 1 + rippleUpdateMode: 1 + rippleShape: {fileID: 0} + speed: 1 + maxSize: 4 + startColor: {r: 1, g: 1, b: 1, a: 1} + transitionColor: {r: 1, g: 1, b: 1, a: 0} + renderOnTop: 0 + centered: 0 +--- !u!114 &3913614832241006454 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5043279403120338259} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: 55 + m_MinHeight: -1 + m_PreferredWidth: 55 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 --- !u!1 &5181310319957536929 GameObject: m_ObjectHideFlags: 0 @@ -820,6 +1610,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 7868881898829307179} + - component: {fileID: 3493787011031423823} m_Layer: 5 m_Name: Search m_TagString: Untagged @@ -840,14 +1631,177 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 6206375982450128193} + - {fileID: 5000712621006847538} + - {fileID: 7683108501662694733} + - {fileID: 3416948009511138372} m_Father: {fileID: 8823517661499080595} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 482.25, y: -30} - m_SizeDelta: {x: 954.5, y: 50} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -10} + m_SizeDelta: {x: 0, y: 50} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &3493787011031423823 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5181310319957536929} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 5 + m_Right: 5 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 3 + m_Spacing: 5 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!1 &5673117406039939321 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5705055065613871648} + - component: {fileID: 1794437172805276588} + - component: {fileID: 6681791915537918917} + - component: {fileID: 6091450123876866387} + m_Layer: 5 + m_Name: Ripple + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5705055065613871648 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5673117406039939321} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 3416948009511138372} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1794437172805276588 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5673117406039939321} + m_CullTransparentMesh: 0 +--- !u!114 &6681791915537918917 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5673117406039939321} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 951352f31055aae46b6e9786313c632d, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 12 +--- !u!114 &6091450123876866387 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5673117406039939321} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_ShowMaskGraphic: 0 +--- !u!1 &5756927395396884001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3073758193502664746} + - component: {fileID: 5446393462633531186} + m_Layer: 5 + m_Name: Text Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3073758193502664746 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5756927395396884001} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 3777093290825092734} + m_Father: {fileID: 6206375982450128193} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} ---- !u!1 &5756927395396884001 +--- !u!222 &5446393462633531186 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5756927395396884001} + m_CullTransparentMesh: 0 +--- !u!1 &6148420533256328260 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -855,43 +1809,180 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 3073758193502664746} - - component: {fileID: 5446393462633531186} + - component: {fileID: 3416948009511138372} + - component: {fileID: 3163452583473062481} + - component: {fileID: 6425330908644344478} + - component: {fileID: 4311322798690598591} + - component: {fileID: 3761970443406164293} + - component: {fileID: 3784183319226496380} m_Layer: 5 - m_Name: Text Area + m_Name: Sort m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!224 &3073758193502664746 +--- !u!224 &3416948009511138372 RectTransform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 5756927395396884001} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_GameObject: {fileID: 6148420533256328260} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: - - {fileID: 3777093290825092734} - m_Father: {fileID: 6206375982450128193} + - {fileID: 9195632984993411493} + - {fileID: 5705055065613871648} + m_Father: {fileID: 7868881898829307179} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 1, y: 1} + m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 55} m_Pivot: {x: 0.5, y: 0.5} ---- !u!222 &5446393462633531186 +--- !u!222 &3163452583473062481 CanvasRenderer: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 5756927395396884001} + m_GameObject: {fileID: 6148420533256328260} m_CullTransparentMesh: 0 +--- !u!114 &6425330908644344478 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6148420533256328260} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 0 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1} + m_DisabledColor: {r: 1, g: 1, b: 1, a: 0.39215687} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 4311322798690598591} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &4311322798690598591 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6148420533256328260} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.33333334, g: 0.37254903, b: 0.4509804, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 5e16c7aea118d68498053518146c9cf9, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 13 +--- !u!114 &3761970443406164293 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6148420533256328260} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1a12ddbc47b17cd478cb447d1113a22b, type: 3} + m_Name: + m_EditorClassIdentifier: + buttonText: "\uF0DC" + clickEvent: + m_PersistentCalls: + m_Calls: [] + hoverEvent: + m_PersistentCalls: + m_Calls: [] + hoverSound: {fileID: 0} + clickSound: {fileID: 0} + buttonVar: {fileID: 0} + normalText: {fileID: 4605941648409354567} + soundSource: {fileID: 0} + rippleParent: {fileID: 5673117406039939321} + useCustomContent: 0 + enableButtonSounds: 0 + useHoverSound: 1 + useClickSound: 1 + useRipple: 1 + rippleUpdateMode: 1 + rippleShape: {fileID: 0} + speed: 1 + maxSize: 4 + startColor: {r: 1, g: 1, b: 1, a: 1} + transitionColor: {r: 1, g: 1, b: 1, a: 0} + renderOnTop: 0 + centered: 0 +--- !u!114 &3784183319226496380 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6148420533256328260} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: 55 + m_MinHeight: -1 + m_PreferredWidth: 55 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 --- !u!1 &6265431863627329327 GameObject: m_ObjectHideFlags: 0 @@ -1005,6 +2096,95 @@ MonoBehaviour: webglMode: 0 background: {fileID: 2881518792493254415} bar: {fileID: 7370997065605906984} +--- !u!1 &6267150986429446833 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8680346654750948472} + - component: {fileID: 6840524763941592153} + - component: {fileID: 8535146521443516864} + - component: {fileID: 7576912764321325546} + m_Layer: 5 + m_Name: Ripple + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8680346654750948472 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6267150986429446833} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 5000712621006847538} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &6840524763941592153 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6267150986429446833} + m_CullTransparentMesh: 0 +--- !u!114 &8535146521443516864 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6267150986429446833} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 951352f31055aae46b6e9786313c632d, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 12 +--- !u!114 &7576912764321325546 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6267150986429446833} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_ShowMaskGraphic: 0 --- !u!1 &6821221533684094758 GameObject: m_ObjectHideFlags: 0 @@ -1126,7 +2306,8 @@ RectTransform: m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 - m_Children: [] + m_Children: + - {fileID: 7041327802215014214} m_Father: {fileID: 6104944427379572325} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} @@ -1639,3 +2820,221 @@ MonoBehaviour: callback: m_PersistentCalls: m_Calls: [] +--- !u!1 &9139761773161578503 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3512017188549587436} + - component: {fileID: 4731213525127919656} + - component: {fileID: 588219030636976623} + - component: {fileID: 521566120197899752} + m_Layer: 5 + m_Name: Ripple + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3512017188549587436 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9139761773161578503} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 7683108501662694733} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4731213525127919656 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9139761773161578503} + m_CullTransparentMesh: 0 +--- !u!114 &588219030636976623 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9139761773161578503} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 951352f31055aae46b6e9786313c632d, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 12 +--- !u!114 &521566120197899752 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9139761773161578503} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_ShowMaskGraphic: 0 +--- !u!1001 &6193129183857894167 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 2472446831714888479} + m_Modifications: + - target: {fileID: 3221561598132462901, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_Name + value: TreeViewItem + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_Pivot.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_Pivot.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_SizeDelta.x + value: 797 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_SizeDelta.y + value: 50 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 5 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -5 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8844280093565481701, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + propertyPath: m_fontStyle + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 4e7329bce1107abf6a795e345a0b2270, type: 3} +--- !u!224 &7041327802215014214 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 3766619498664164433, guid: 4e7329bce1107abf6a795e345a0b2270, + type: 3} + m_PrefabInstance: {fileID: 6193129183857894167} + m_PrefabAsset: {fileID: 0} diff --git a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs index e1123752be..de0e9e9992 100644 --- a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs @@ -59,6 +59,21 @@ public partial class TreeWindow /// private TMP_InputField SearchField; + /// + /// The button that opens the filter menu. + /// + private ButtonManagerBasic FilterButton; + + /// + /// The button that opens the grouping menu. + /// + private ButtonManagerBasic GroupButton; + + /// + /// The button that opens the sorting menu. + /// + private ButtonManagerBasic SortButton; + /// /// Orders the tree below the given node according to the graph hierarchy. /// This needs to be called whenever the tree is expanded. @@ -66,53 +81,66 @@ public partial class TreeWindow /// The node below which the tree should be ordered. private void OrderTree(Node orderBelow) { - int index = items.Find(CleanupID(orderBelow.ID)).GetSiblingIndex(); - OrderTreeRecursive(orderBelow); + Transform nodeItem = items.Find(CleanupID(orderBelow.ID)); + // We determine the node level based on the indent of the foreground. + int nodeLevel = Mathf.RoundToInt(((RectTransform)nodeItem.Find("Foreground")).offsetMin.x) / indentShift; + int index = nodeItem.GetSiblingIndex(); + + OrderTreeRecursive(orderBelow, nodeLevel); return; // Orders the item with the given id to the current index and increments the index. - void OrderItemHere(string id) + void OrderItemHere(string id, int level) { Transform item = items.Find(id); - if (item == null) - { - Debug.LogError($"Item {id} not found."); - } - else + if (item != null) { item.SetSiblingIndex(index++); + RectTransform foreground = (RectTransform)item.Find("Foreground"); + RectTransform background = (RectTransform)item.Find("Background"); + foreground.offsetMin = foreground.offsetMin.WithXY(x: indentShift * level); + background.offsetMin = background.offsetMin.WithXY(x: indentShift * level); } } // Recurses over the tree in pre-order and assigns indices to each node. - void OrderTreeRecursive(Node node) + void OrderTreeRecursive(Node node, int level) { string id = CleanupID(node.ID); - OrderItemHere(id); + OrderItemHere(id, level); if (expandedItems.Contains(id)) { - foreach (Node child in node.Children().OrderBy(x => x.SourceName)) + IList children = WithHiddenChildren(node.Children()).OrderBy(x => x.SourceName).ToList(); + foreach (Node child in children) { - OrderTreeRecursive(child); + OrderTreeRecursive(child, level + 1); } - HandleEdges($"{id}#Outgoing", node.Outgoings); - HandleEdges($"{id}#Incoming", node.Incomings); + List outgoings = Searcher.Filter.Apply(node.Outgoings).ToList(); + List incomings = Searcher.Filter.Apply(node.Incomings).ToList(); + // We need to handle lifted edges separately, since they are not children of the node. + List hiddenChildren = HiddenChildren(node.Children()).ToList(); + List liftedOutgoings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Outgoings)).ToList(); + List liftedIncomings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Incomings)).ToList(); + HandleEdges($"{id}#Outgoing", outgoings, level + 1); + HandleEdges($"{id}#Incoming", incomings, level + 1); + HandleEdges($"{id}#Lifted Outgoing", liftedOutgoings, level + 1); + HandleEdges($"{id}#Lifted Incoming", liftedIncomings, level + 1); } } // Orders the edges under the given id (outgoing/incoming) to the current index and increments the index. - void HandleEdges(string edgesId, ICollection edges) + void HandleEdges(string edgesId, ICollection edges, int level) { if (edges.Count > 0) { - OrderItemHere(edgesId); + OrderItemHere(edgesId, level); if (expandedItems.Contains(edgesId)) { foreach (Edge edge in edges) { - OrderItemHere($"{edgesId}#{CleanupID(edge.ID)}"); + OrderItemHere($"{edgesId}#{CleanupID(edge.ID)}", level + 1); } } } @@ -128,8 +156,17 @@ private void AddNode(Node node) GameObject nodeGameObject = GraphElementIDMap.Find(node.ID); int children = node.NumberOfChildren() + Mathf.Min(node.Outgoings.Count, 1) + Mathf.Min(node.Incomings.Count, 1); - AddItem(CleanupID(node.ID), children, node.ToShortString(), node.Level, nodeTypeUnicode, nodeGameObject, node, - item => CollapseNode(node, item), (item, order) => ExpandNode(node, item, orderTree: order)); + if (Searcher.Filter.Includes(node)) + { + AddItem(CleanupID(node.ID), children, node.ToShortString(), Icons.Node, nodeGameObject, node, + item => CollapseNode(node, item), (item, order) => ExpandNode(node, item, orderTree: order)); + } + else + { + // The node itself may not be included, but its children (or edges) could be. + // Thus, we assume this invisible node to be expanded by default and add its children. + ExpandNode(node, null); + } } /// @@ -138,7 +175,6 @@ private void AddNode(Node node) /// The ID of the item to be added. /// The number of children of the item to be added. /// The text of the item to be added. - /// The level of the item to be added. /// The icon of the item to be added, given as a unicode character. /// The game object of the element represented by the item. May be null. /// The graph element represented by the item. May be null. @@ -147,8 +183,8 @@ private void AddNode(Node node) /// A function that expands the item. /// It takes the item that was expanded and a boolean indicating whether the /// tree should be ordered after expanding the item as arguments. - private void AddItem(string id, int children, string text, int level, - char icon, GameObject representedGameObject, GraphElement representedGraphElement, + private void AddItem(string id, int children, string text, char icon, + GameObject representedGameObject, GraphElement representedGraphElement, Action collapseItem, Action expandItem) { GameObject item = PrefabInstantiator.InstantiatePrefab(treeItemPrefab, items, false); @@ -161,16 +197,13 @@ private void AddItem(string id, int children, string text, int level, textMesh.text = text; iconMesh.text = icon.ToString(); - foreground.localPosition += new Vector3(indentShift * level, 0, 0); - background.localPosition += new Vector3(indentShift * level, 0, 0); - ColorItem(); - // Slashes will cause problems later on, so we replace them with backslashes. + // Slashes will cause problems later on in the `transform.Find` method, so we replace them with backslashes. // NOTE: This becomes a problem if two nodes A and B exist where node A's name contains slashes and node B // has an identical name, except for all slashes being replaced by backslashes. // I hope this is unlikely enough to not be a problem for now. - item.name = CleanupID(id); + item.name = id; if (children <= 0) { expandIcon.SetActive(false); @@ -252,10 +285,12 @@ void RegisterClickHandler() .GetApplicableOptions(representedGraphElement, representedGameObject) .Where(x => !x.Name.Contains("TreeView")); - ContextMenu.ClearActions(); - ContextMenu.AddActions(actions); - ContextMenu.MoveTo(e.position); - ContextMenu.ShowMenu().Forget(); + actions = actions.Append(new PopupMenuAction("Hide in TreeView", () => + { + Searcher.Filter.ExcludeElements.Add(representedGraphElement); + Rebuild(); + }, Icons.Hide)); + ContextMenu.ShowWith(actions, e.position); } else { @@ -273,6 +308,29 @@ void RegisterClickHandler() } } + /// + /// Returns those nodes within which are included in the current filter, + /// and transitively adds all children of those nodes within + /// which are not included in the current filter. + /// + /// The nodes to be filtered. + /// The filtered nodes with any hidden transitive children. + private IEnumerable WithHiddenChildren(IList nodes) + { + return nodes.Where(Searcher.Filter.Includes).Concat(nodes.Where(x => !Searcher.Filter.Includes(x)).SelectMany(x => WithHiddenChildren(x.Children()))); + } + + /// + /// Returns those nodes within which are not included in the current filter, + /// transitively including all hidden children of those nodes. + /// + /// The nodes to be filtered. + /// The transitive hidden children of the given nodes. + private IEnumerable HiddenChildren(IEnumerable nodes) + { + return nodes.Where(x => !Searcher.Filter.Includes(x)).SelectMany(x => HiddenChildren(x.Children()).Append(x)); + } + /// /// Removes the given 's children from the tree window. /// @@ -288,7 +346,7 @@ private void RemoveNodeChildren(Node node) IEnumerable<(string ID, Node child)> GetChildItems(Node n) { string cleanId = CleanupID(n.ID); - IEnumerable<(string, Node)> children = n.Children().Select(x => (CleanupID(x.ID), x)); + IEnumerable<(string, Node)> children = WithHiddenChildren(n.Children()).Select(x => (CleanupID(x.ID), x)); // We need to remove the "Outgoing" and "Incoming" buttons if they exist, along with their children. if (n.Outgoings.Count > 0) { @@ -374,39 +432,67 @@ private void CollapseItem(GameObject item) /// Its children will be added to the tree window. /// /// The node represented by the item. - /// The item to be expanded. + /// The item to be expanded. If this is null + /// (i.e., no item actually exists in the TreeWindow) + /// only the children of the node will be added, not its connected edges. /// Whether to order the tree after expanding the node. private void ExpandNode(Node node, GameObject item, bool orderTree = false) { - ExpandItem(item); - foreach (Node child in node.Children()) { AddNode(child); } - if (node.Outgoings.Count > 0) - { - AddEdgeButton("Outgoing", outgoingEdgeUnicode, node.Outgoings); - } - if (node.Incomings.Count > 0) - { - AddEdgeButton("Incoming", incomingEdgeUnicode, node.Incomings); - } - if (orderTree) + if (item != null) { - OrderTree(node); + ExpandItem(item); + + List outgoings = Searcher.Filter.Apply(node.Outgoings).ToList(); + List incomings = Searcher.Filter.Apply(node.Incomings).ToList(); + // We need to lift edges of any hidden children upwards to the first visible parent, which is + // this node. We then need to filter them again, since they may have been hidden by the filter. + List hiddenChildren = HiddenChildren(node.Children()).ToList(); + List liftedOutgoings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Outgoings)).ToList(); + List liftedIncomings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Incomings)).ToList(); + + if (outgoings.Count > 0) + { + AddEdgeButton(outgoings, incoming: false, lifted: false); + } + if (incomings.Count > 0) + { + AddEdgeButton(incomings, incoming: true, lifted: false); + } + if (liftedOutgoings.Count > 0) + { + AddEdgeButton(liftedOutgoings, incoming: false, lifted: true); + } + if (liftedIncomings.Count > 0) + { + AddEdgeButton(liftedIncomings, incoming: true, lifted: true); + } + if (orderTree) + { + OrderTree(node); + } } return; - void AddEdgeButton(string edgesType, char icon, ICollection edges) + void AddEdgeButton(ICollection edges, bool incoming, bool lifted) { + (string edgesType, char icon) = (incoming, lifted) switch + { + (true, false) => ("Incoming", Icons.IncomingEdge), + (false, false) => ("Outgoing", Icons.OutgoingEdge), + (true, true) => ("Lifted Incoming", Icons.LiftedIncomingEdge), + (false, true) => ("Lifted Outgoing", Icons.LiftedOutgoingEdge), + }; string cleanedId = CleanupID(node.ID); string id = $"{cleanedId}#{edgesType}"; // Note that an edge may appear multiple times in the tree view, // hence we make its ID dependent on the node it is connected to, // and whether it is an incoming or outgoing edge (to cover self-loops). - AddItem(id, edges.Count, $"{edgesType} Edges", node.Level + 1, icon, + AddItem(id, edges.Count, $"{edgesType} Edges", icon, representedGameObject: null, representedGraphElement: null, collapsedItem => { @@ -418,17 +504,27 @@ void AddEdgeButton(string edgesType, char icon, ICollection edges) }, (expandedItem, order) => { ExpandItem(expandedItem); - foreach (Edge edge in edges) - { - GameObject edgeObject = GraphElementIDMap.Find(edge.ID); - AddItem($"{id}#{CleanupID(edge.ID)}", 0, edge.ToShortString(), node.Level + 2, edgeTypeUnicode, edgeObject, edge, null, null); - } + AddEdges(id, edges, lifted); if (order) { OrderTree(node); } }); } + + void AddEdges(string id, IEnumerable edges, bool lifted) + { + foreach (Edge edge in edges) + { + GameObject edgeObject = GraphElementIDMap.Find(edge.ID); + string title = edge.ToShortString(); + if (lifted) + { + title = $"{title}"; + } + AddItem($"{id}#{CleanupID(edge.ID)}", 0, title, Icons.Edge, edgeObject, edge, null, null); + } + } } /// @@ -453,15 +549,15 @@ private void SearchFor(string searchTerm) ClearTree(); if (searchTerm == null || searchTerm.Trim().Length == 0) { - AddRoots(); + AddRoots().Forget(); return; } - foreach (Node node in searcher.Search(searchTerm)) + foreach (Node node in Searcher.Search(searchTerm)) { GameObject nodeGameObject = GraphElementIDMap.Find(node.ID, mustFindElement: true); AddItem(CleanupID(node.ID), - 0, node.ToShortString(), 0, nodeTypeUnicode, nodeGameObject, node, + 0, node.ToShortString(), Icons.Node, nodeGameObject, node, null, (_, _) => RevealElement(node).Forget()); } @@ -484,6 +580,12 @@ public async UniTaskVoid RevealElement(GraphElement element, bool viaSource = fa // This case may occur when the method is called from the outside. await UniTask.WaitUntil(() => SearchField != null); } + if (!Searcher.Filter.Includes(element)) + { + ShowNotification.Warn("Element filtered out", + "Element is not included in the current filter and thus can't be shown."); + return; + } SearchField.onValueChanged.RemoveListener(SearchFor); SearchField.text = string.Empty; SearchField.ReleaseSelection(); @@ -513,10 +615,9 @@ public async UniTaskVoid RevealElement(GraphElement element, bool viaSource = fa expandedItems.Add(CleanupID(current.ID)); } - AddRoots(); + // We need to wait until the transform actually exists, hence the await. + await AddRoots(); - // We need to wait until the transform actually exists, hence the yield. - await UniTask.Yield(); RectTransform item = (RectTransform)items.Find(transformID); scrollRect.ScrollTo(item, duration: 1f); @@ -555,7 +656,110 @@ protected override void StartDesktop() SearchField.onDeselect.AddListener(_ => SEEInput.KeyboardShortcutsEnabled = true); SearchField.onValueChanged.AddListener(SearchFor); - AddRoots(); + FilterButton = root.Find("Search/Filter").gameObject.MustGetComponent(); + FilterButton.clickEvent.AddListener(ShowFilterMenu); + + AddRoots().Forget(); + return; + + // Constructs the menu for the filter button. + void UpdateFilterMenuEntries() + { + ISet nodeToggles = Graph.AllToggleNodeAttributes(); + ISet edgeToggles = Graph.AllToggleEdgeAttributes(); + ISet commonToggles = nodeToggles.Intersect(edgeToggles).ToHashSet(); + // Don't include common toggles in node/edge toggles. + nodeToggles.ExceptWith(commonToggles); + edgeToggles.ExceptWith(commonToggles); + // TODO: Allow filtering by node type. + + List entries = new() + { + new PopupMenuAction("Edges", + () => + { + Searcher.Filter.IncludeEdges = !Searcher.Filter.IncludeEdges; + UpdateFilterMenuEntries(); + Rebuild(); + }, + Checkbox(Searcher.Filter.IncludeEdges), CloseAfterClick: false), + }; + + if (Searcher.Filter.ExcludeElements.Count > 0) + { + entries.Insert(0, new PopupMenuAction("Show hidden elements", + () => + { + Searcher.Filter.ExcludeElements.Clear(); + Rebuild(); + }, + Icons.Show)); + } + + if (commonToggles.Count > 0) + { + entries.Add(new PopupMenuHeading("Common properties")); + entries.AddRange(commonToggles.Select(FilterActionFor)); + } + if (nodeToggles.Count > 0) + { + entries.Add(new PopupMenuHeading("Node properties")); + entries.AddRange(nodeToggles.Select(FilterActionFor)); + } + if (edgeToggles.Count > 0) + { + entries.Add(new PopupMenuHeading("Edge properties")); + entries.AddRange(edgeToggles.Select(FilterActionFor)); + } + + ContextMenu.ClearEntries(); + ContextMenu.AddEntries(entries); + } + + void ShowFilterMenu() + { + UpdateFilterMenuEntries(); + ContextMenu.ShowWith(position: FilterButton.transform.position); + } + + PopupMenuAction FilterActionFor(string toggleAttribute) + { + return new PopupMenuAction(toggleAttribute, + () => + { + // Toggle from include->exclude->none->include. + if (Searcher.Filter.IncludeToggleAttributes.Contains(toggleAttribute)) + { + Searcher.Filter.IncludeToggleAttributes.Remove(toggleAttribute); + Searcher.Filter.ExcludeToggleAttributes.Add(toggleAttribute); + } + else if (Searcher.Filter.ExcludeToggleAttributes.Contains(toggleAttribute)) + { + Searcher.Filter.ExcludeToggleAttributes.Remove(toggleAttribute); + } + else + { + Searcher.Filter.IncludeToggleAttributes.Add(toggleAttribute); + } + UpdateFilterMenuEntries(); + Rebuild(); + }, + Searcher.Filter.ExcludeToggleAttributes.Contains(toggleAttribute) + ? Icons.MinusCheckbox + : Checkbox(Searcher.Filter.IncludeToggleAttributes.Contains(toggleAttribute)), + CloseAfterClick: false); + } + + char Checkbox(bool value) => value ? Icons.CheckedCheckbox : Icons.EmptyCheckbox; + } + + /// + /// Rebuilds the tree window. + /// + private void Rebuild() + { + ClearTree(); + AddRoots().Forget(); } } } diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs index b3b79d0e4a..79610581b0 100644 --- a/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; using SEE.DataModel; using SEE.DataModel.DG; +using SEE.DataModel.GraphSearch; using SEE.UI.Notification; using SEE.Utils; using UnityEngine; @@ -27,27 +30,6 @@ public partial class TreeWindow : BaseWindow, IObserver /// private const string treeItemPrefab = "Prefabs/UI/TreeViewItem"; - // TODO: In the future, distinguish by node/edge type as well for the icons. - /// - /// The unicode character for a node. - /// - private const char nodeTypeUnicode = '\uf1b2'; - - /// - /// The unicode character for an edge. - /// - private const char edgeTypeUnicode = '\uf542'; - - /// - /// The unicode character for outgoing edges. - /// - private const char outgoingEdgeUnicode = '\uf2f5'; - - /// - /// The unicode character for incoming edges. - /// - private const char incomingEdgeUnicode = '\uf2f6'; - /// /// The graph to be displayed. /// Must be set before starting the window. @@ -56,8 +38,9 @@ public partial class TreeWindow : BaseWindow, IObserver /// /// The search helper used to search for elements in the graph. + /// We also use this to keep track of the current filter, sort, and group settings. /// - private GraphSearch searcher; + private GraphSearch Searcher; /// /// The context menu that is displayed when the user right-clicks on an item. @@ -71,7 +54,8 @@ public partial class TreeWindow : BaseWindow, IObserver protected override void Start() { - searcher = new GraphSearch(Graph); + Searcher = new GraphSearch(Graph); + Searcher.Filter.IncludeToggleAttributes.UnionWith(Graph.AllToggleGraphElementAttributes()); ContextMenu = gameObject.AddComponent(); Graph.Subscribe(this); base.Start(); @@ -79,19 +63,27 @@ protected override void Start() /// /// Adds the roots of the graph to the tree view. + /// It may take up to a frame to add and reorder all items, hence this method is asynchronous. /// - private void AddRoots() + private async UniTask AddRoots() { // We will traverse the graph and add each node to the tree view. - IList roots = Graph.GetRoots(); + IList roots = WithHiddenChildren(Graph.GetRoots()).ToList(); + + if (roots.Count == 0) + { + ShowNotification.Warn("Empty graph", "Graph has no roots. TreeView will be empty."); + return; + } + foreach (Node root in roots) { AddNode(root); } - - if (roots.Count == 0) + await UniTask.Yield(); + foreach (Node root in roots) { - ShowNotification.Warn("Empty graph", "Graph has no roots. TreeView will be empty."); + OrderTree(root); } } @@ -149,7 +141,7 @@ public void OnNext(ChangeEvent value) case HierarchyEvent: case NodeEvent: ClearTree(); - AddRoots(); + AddRoots().Forget(); break; } } From 97360a5665b981f49e9e66ea6b89c9fd7f6f1687 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 18:05:42 +0100 Subject: [PATCH 11/30] Properly dispose of TreeWindow's subscription --- .../UI/Window/TreeWindow/DesktopTreeWindow.cs | 5 +++++ Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs index de0e9e9992..ad4e1a67db 100644 --- a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs @@ -82,6 +82,11 @@ public partial class TreeWindow private void OrderTree(Node orderBelow) { Transform nodeItem = items.Find(CleanupID(orderBelow.ID)); + if (nodeItem == null) + { + return; + } + // We determine the node level based on the indent of the foreground. int nodeLevel = Mathf.RoundToInt(((RectTransform)nodeItem.Find("Foreground")).offsetMin.x) / indentShift; int index = nodeItem.GetSiblingIndex(); diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs index 79610581b0..6b9ae81f5a 100644 --- a/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs @@ -52,15 +52,25 @@ public partial class TreeWindow : BaseWindow, IObserver /// private RectTransform items; + /// + /// The subscription to the graph observable. + /// + private IDisposable subscription; + protected override void Start() { Searcher = new GraphSearch(Graph); Searcher.Filter.IncludeToggleAttributes.UnionWith(Graph.AllToggleGraphElementAttributes()); ContextMenu = gameObject.AddComponent(); - Graph.Subscribe(this); + subscription = Graph.Subscribe(this); base.Start(); } + private void OnDestroy() + { + subscription.Dispose(); + } + /// /// Adds the roots of the graph to the tree view. /// It may take up to a frame to add and reorder all items, hence this method is asynchronous. @@ -94,7 +104,10 @@ private void ClearTree() { foreach (Transform child in items) { - Destroyer.Destroy(child.gameObject); + if (child != null) + { + Destroyer.Destroy(child.gameObject); + } } } From 0f2aa09384fdadbfcd7a17d27d649fe68f8a9c55 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 18:08:53 +0100 Subject: [PATCH 12/30] Unreparent nodes before reparenting them This is because the `Reparent` method may not actually reparent the node again, e.g., in a reflexion graph, which leads to the node having the wrong parent. --- Assets/SEE/Controls/Actions/MoveAction.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Assets/SEE/Controls/Actions/MoveAction.cs b/Assets/SEE/Controls/Actions/MoveAction.cs index 9ae50c3fa8..919b1cf2db 100644 --- a/Assets/SEE/Controls/Actions/MoveAction.cs +++ b/Assets/SEE/Controls/Actions/MoveAction.cs @@ -588,17 +588,14 @@ private void UpdateHierarchy() if (Raycasting.RaycastLowestNode(out RaycastHit? raycastHit, out Node _, grabbedObject.Node)) { // Note: the root node can never be grabbed. See above. + // We need to undo the reparenting of the grabbed node if it is + // currently reparented onto another node. + grabbedObject.UnReparent(); if (raycastHit.HasValue) { // The user is currently aiming at a node. The grabbed node is reparented onto this aimed node. grabbedObject.Reparent(raycastHit.Value.transform.gameObject); } - else - { - // The user is currently not aiming at a node. The reparenting - // of the grabbed must be reverted. - grabbedObject.UnReparent(); - } } } } From 67b520f7a3161356b711bfd3c5705d5e70c13730 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 22 Nov 2023 18:32:01 +0100 Subject: [PATCH 13/30] Clean up lifted edges in TreeWindow correctly --- .../UI/Window/TreeWindow/DesktopTreeWindow.cs | 103 ++++++++---------- Assets/SEE/Utils/Icons.cs | 1 + 2 files changed, 47 insertions(+), 57 deletions(-) diff --git a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs index ad4e1a67db..7c5996655c 100644 --- a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs @@ -122,16 +122,10 @@ void OrderTreeRecursive(Node node, int level) OrderTreeRecursive(child, level + 1); } - List outgoings = Searcher.Filter.Apply(node.Outgoings).ToList(); - List incomings = Searcher.Filter.Apply(node.Incomings).ToList(); - // We need to handle lifted edges separately, since they are not children of the node. - List hiddenChildren = HiddenChildren(node.Children()).ToList(); - List liftedOutgoings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Outgoings)).ToList(); - List liftedIncomings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Incomings)).ToList(); - HandleEdges($"{id}#Outgoing", outgoings, level + 1); - HandleEdges($"{id}#Incoming", incomings, level + 1); - HandleEdges($"{id}#Lifted Outgoing", liftedOutgoings, level + 1); - HandleEdges($"{id}#Lifted Incoming", liftedIncomings, level + 1); + foreach ((List edges, string edgesType) in RelevantEdges(node)) + { + HandleEdges($"{id}#{edgesType}", edges, level + 1); + } } } @@ -352,22 +346,13 @@ private void RemoveNodeChildren(Node node) { string cleanId = CleanupID(n.ID); IEnumerable<(string, Node)> children = WithHiddenChildren(n.Children()).Select(x => (CleanupID(x.ID), x)); - // We need to remove the "Outgoing" and "Incoming" buttons if they exist, along with their children. - if (n.Outgoings.Count > 0) - { - children = AppendEdgeChildren("Outgoing", n.Outgoings); - } - if (n.Incomings.Count > 0) + foreach ((List edges, string edgesType) in RelevantEdges(n)) { - children = AppendEdgeChildren("Incoming", n.Incomings); + // We need to remove the "Outgoing" and "Incoming" buttons if they exist, along with their children. + children = children.Append((cleanId + "#" + edgesType, null)) + .Concat(edges.Select(x => ($"{cleanId}#{edgesType}#{CleanupID(x.ID)}", null))); } return children; - - IEnumerable<(string, Node)> AppendEdgeChildren(string edgeType, IEnumerable edges) - { - return children.Append((cleanId + "#" + edgeType, null)) - .Concat(edges.Select(x => ($"{cleanId}#{edgeType}#{CleanupID(x.ID)}", null))); - } } } @@ -432,6 +417,32 @@ private void CollapseItem(GameObject item) } } + /// + /// Returns connected and lifted edges of the given . + /// The edges are grouped by their type (outgoing, incoming, lifted outgoing, lifted incoming) + /// and each is returned as a list along with its type. Only those types are included that + /// have at least one edge. + /// + /// The node whose edges are requested. + /// The edges of the node, grouped by their type. + private IEnumerable<(List edges, string edgesType)> RelevantEdges(Node node) + { + List outgoings = Searcher.Filter.Apply(node.Outgoings).ToList(); + List incomings = Searcher.Filter.Apply(node.Incomings).ToList(); + // We need to lift edges of any hidden children upwards to the first visible parent, which is + // this node. We then need to filter them again, since they may have been hidden by the filter. + List hiddenChildren = HiddenChildren(node.Children()).ToList(); + List liftedOutgoings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Outgoings)).ToList(); + List liftedIncomings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Incomings)).ToList(); + return new[] + { + (outgoings, "Outgoing"), + (incomings, "Incoming"), + (liftedOutgoings, "Lifted Outgoing"), + (liftedIncomings, "Lifted Incoming") + }.Where(x => x.Item1.Count > 0); + } + /// /// Expands the given . /// Its children will be added to the tree window. @@ -452,30 +463,11 @@ private void ExpandNode(Node node, GameObject item, bool orderTree = false) { ExpandItem(item); - List outgoings = Searcher.Filter.Apply(node.Outgoings).ToList(); - List incomings = Searcher.Filter.Apply(node.Incomings).ToList(); - // We need to lift edges of any hidden children upwards to the first visible parent, which is - // this node. We then need to filter them again, since they may have been hidden by the filter. - List hiddenChildren = HiddenChildren(node.Children()).ToList(); - List liftedOutgoings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Outgoings)).ToList(); - List liftedIncomings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Incomings)).ToList(); - - if (outgoings.Count > 0) - { - AddEdgeButton(outgoings, incoming: false, lifted: false); - } - if (incomings.Count > 0) - { - AddEdgeButton(incomings, incoming: true, lifted: false); - } - if (liftedOutgoings.Count > 0) + foreach ((List edges, string edgesType) in RelevantEdges(node)) { - AddEdgeButton(liftedOutgoings, incoming: false, lifted: true); - } - if (liftedIncomings.Count > 0) - { - AddEdgeButton(liftedIncomings, incoming: true, lifted: true); + AddEdgeButton(edges, edgesType); } + if (orderTree) { OrderTree(node); @@ -483,14 +475,15 @@ private void ExpandNode(Node node, GameObject item, bool orderTree = false) } return; - void AddEdgeButton(ICollection edges, bool incoming, bool lifted) + void AddEdgeButton(ICollection edges, string edgesType) { - (string edgesType, char icon) = (incoming, lifted) switch + char icon = edgesType switch { - (true, false) => ("Incoming", Icons.IncomingEdge), - (false, false) => ("Outgoing", Icons.OutgoingEdge), - (true, true) => ("Lifted Incoming", Icons.LiftedIncomingEdge), - (false, true) => ("Lifted Outgoing", Icons.LiftedOutgoingEdge), + "Incoming" => Icons.IncomingEdge, + "Outgoing" => Icons.OutgoingEdge, + "Lifted Incoming" => Icons.LiftedIncomingEdge, + "Lifted Outgoing" => Icons.LiftedOutgoingEdge, + _ => Icons.QuestionMark }; string cleanedId = CleanupID(node.ID); string id = $"{cleanedId}#{edgesType}"; @@ -509,7 +502,7 @@ void AddEdgeButton(ICollection edges, bool incoming, bool lifted) }, (expandedItem, order) => { ExpandItem(expandedItem); - AddEdges(id, edges, lifted); + AddEdges(id, edges); if (order) { OrderTree(node); @@ -517,16 +510,12 @@ void AddEdgeButton(ICollection edges, bool incoming, bool lifted) }); } - void AddEdges(string id, IEnumerable edges, bool lifted) + void AddEdges(string id, IEnumerable edges) { foreach (Edge edge in edges) { GameObject edgeObject = GraphElementIDMap.Find(edge.ID); string title = edge.ToShortString(); - if (lifted) - { - title = $"{title}"; - } AddItem($"{id}#{CleanupID(edge.ID)}", 0, title, Icons.Edge, edgeObject, edge, null, null); } } diff --git a/Assets/SEE/Utils/Icons.cs b/Assets/SEE/Utils/Icons.cs index e68129e858..27c8602662 100644 --- a/Assets/SEE/Utils/Icons.cs +++ b/Assets/SEE/Utils/Icons.cs @@ -29,5 +29,6 @@ public static class Icons public const char Compare = '\uE13A'; public const char Hide = '\uF070'; public const char Show = '\uF06E'; + public const char QuestionMark = '?'; } } From 704a2202a3caaea69d428bcacddc99b21db9da18 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Fri, 24 Nov 2023 14:09:12 +0100 Subject: [PATCH 14/30] Make TreeWindow sortable --- Assets/SEE/DataModel/DG/Attributable.cs | 83 ++++- .../DataModel/DG/GraphElementsAttributes.cs | 14 +- Assets/SEE/DataModel/DG/GraphExtensions.cs | 16 +- .../SEE/DataModel/GraphSearch/GraphFilter.cs | 14 +- .../SEE/DataModel/GraphSearch/GraphSearch.cs | 25 +- .../SEE/DataModel/GraphSearch/GraphSorter.cs | 62 ++++ .../DataModel/GraphSearch/GraphSorter.cs.meta | 3 + .../DataModel/GraphSearch/IGraphModifier.cs | 31 ++ .../GraphSearch/IGraphModifier.cs.meta | 3 + .../UI/Window/TreeWindow/DesktopTreeWindow.cs | 97 +----- Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs | 11 +- .../TreeWindow/TreeWindowContextMenu.cs | 291 ++++++++++++++++++ .../TreeWindow/TreeWindowContextMenu.cs.meta | 3 + Assets/SEE/Utils/Icons.cs | 5 + 14 files changed, 547 insertions(+), 111 deletions(-) create mode 100644 Assets/SEE/DataModel/GraphSearch/GraphSorter.cs create mode 100644 Assets/SEE/DataModel/GraphSearch/GraphSorter.cs.meta create mode 100644 Assets/SEE/DataModel/GraphSearch/IGraphModifier.cs create mode 100644 Assets/SEE/DataModel/GraphSearch/IGraphModifier.cs.meta create mode 100644 Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs create mode 100644 Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs.meta diff --git a/Assets/SEE/DataModel/DG/Attributable.cs b/Assets/SEE/DataModel/DG/Attributable.cs index af3451de34..2de26ba4ec 100644 --- a/Assets/SEE/DataModel/DG/Attributable.cs +++ b/Assets/SEE/DataModel/DG/Attributable.cs @@ -198,7 +198,7 @@ public bool TryGetNumeric(string attributeName, out float value) /// /// Returns the value of a numeric (integer or float) attribute for the - /// attributed named if it exists. + /// attribute named if it exists. /// Otherwise an exception is thrown. /// /// Note: It could happen that the same name is given to a float and @@ -222,6 +222,85 @@ public float GetNumeric(string attributeName) } } + /// + /// Returns the value of an attribute of any type (integer, float, string, or toggle) + /// for the attribute named if it exists. + /// Otherwise an exception is thrown. + /// + /// In case of a toggle attribute, the value is always + /// if the attribute exists. + /// + /// Note: It could happen that the same name is given to different attribute types, + /// in which case the preference is as follows: float, integer, string, toggle. + /// + /// name of an attribute + /// value of attribute + /// if is not an attribute of this node + public object GetAny(string attributeName) + { + if (FloatAttributes.TryGetValue(attributeName, out float floatValue)) + { + return floatValue; + } + else if (IntAttributes.TryGetValue(attributeName, out int intValue)) + { + return intValue; + } + else if (StringAttributes.TryGetValue(attributeName, out string stringValue)) + { + return stringValue; + } + else if (toggleAttributes.Contains(attributeName)) + { + return UnitType.Unit; + } + else + { + throw new UnknownAttribute(attributeName); + } + } + + /// + /// Returns true if is the name of an attribute + /// of any type (integer, float, string, or toggle) of this node, and if so, + /// sets to the value of that attribute. + /// Otherwise is set to null and false is returned. + /// + /// Note: It could happen that the same name is given to different attribute types, + /// in which case the preference is as follows: float, integer, string, toggle. + /// + /// name of an attribute + /// value of attribute + /// whether is the name of an attribute of this node + public bool TryGetAny(string attributeName, out object value) + { + if (FloatAttributes.TryGetValue(attributeName, out float floatValue)) + { + value = floatValue; + return true; + } + else if (IntAttributes.TryGetValue(attributeName, out int intValue)) + { + value = intValue; + return true; + } + else if (StringAttributes.TryGetValue(attributeName, out string stringValue)) + { + value = stringValue; + return true; + } + else if (toggleAttributes.Contains(attributeName)) + { + value = UnitType.Unit; + return true; + } + else + { + value = null; + return false; + } + } + /// /// Returns the values of all numeric (int and float) attributes of this node. /// @@ -455,4 +534,4 @@ protected virtual void HandleCloned(object clone) target.IntAttributes = new Dictionary(IntAttributes); } } -} \ No newline at end of file +} diff --git a/Assets/SEE/DataModel/DG/GraphElementsAttributes.cs b/Assets/SEE/DataModel/DG/GraphElementsAttributes.cs index 2dde2deaaa..94ebd13654 100644 --- a/Assets/SEE/DataModel/DG/GraphElementsAttributes.cs +++ b/Assets/SEE/DataModel/DG/GraphElementsAttributes.cs @@ -173,7 +173,7 @@ public ISet AllNumericEdgeAttributes() /// Returns the union of /// and . /// - /// names of all numeric (int or float) node attributes + /// names of all numeric (int or float) attributes public ISet AllNumericAttributes() { ISet result = AllNumericNodeAttributes(); @@ -181,6 +181,18 @@ public ISet AllNumericAttributes() return result; } + /// + /// Returns the union of + /// and . + /// + /// names of all string attributes + public ISet AllStringAttributes() + { + ISet result = AllStringNodeAttributes(); + result.UnionWith(AllStringEdgeAttributes()); + return result; + } + /// /// Returns the union of the names of all numeric node attributes of the given . /// diff --git a/Assets/SEE/DataModel/DG/GraphExtensions.cs b/Assets/SEE/DataModel/DG/GraphExtensions.cs index 4ef91a2f4f..bb0be090e9 100644 --- a/Assets/SEE/DataModel/DG/GraphExtensions.cs +++ b/Assets/SEE/DataModel/DG/GraphExtensions.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using SEE.DataModel.GraphSearch; namespace SEE.DataModel.DG { /// - /// Provides extensions to . + /// Provides extensions to and related classes. /// public static class GraphExtensions { @@ -125,5 +126,18 @@ public static IGraphElementDiff AttributeDiff(params Graph[] graphs) }); return new AttributeDiff(floatAttributes, intAttributes, stringAttributes, toggleAttributes); } + + /// + /// Applies all to the given . + /// + /// graph modifiers to apply to the graph elements + /// the graph elements to modify + /// the type of the graph elements + /// the modified graph elements + public static IEnumerable ApplyAll(this IEnumerable modifiers, IEnumerable elements) + where T : GraphElement + { + return modifiers.Aggregate(elements, (current, modifier) => modifier.Apply(current)); + } } } diff --git a/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs index e6bd222427..2f14688e06 100644 --- a/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs +++ b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs @@ -7,7 +7,7 @@ namespace SEE.DataModel.GraphSearch /// /// A configurable filter for graph elements, mainly intended for use with . /// - public class GraphFilter + public class GraphFilter : IGraphModifier { // if empty, include all. otherwise, *at least one* must be present. /// @@ -64,5 +64,17 @@ public IEnumerable Apply(IEnumerable elements) where T : GraphElement { return elements.Where(Includes); } + + public bool IsActive() => !IncludeNodes || !IncludeEdges + || IncludeToggleAttributes.Count > 0 || ExcludeToggleAttributes.Count > 0 || ExcludeElements.Count > 0; + + public void Reset() + { + IncludeToggleAttributes.Clear(); + ExcludeToggleAttributes.Clear(); + ExcludeElements.Clear(); + IncludeNodes = true; + IncludeEdges = true; + } } } diff --git a/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs b/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs index 21a34f89fd..120935945f 100644 --- a/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs +++ b/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs @@ -27,20 +27,27 @@ public class GraphSearch : IObserver /// /// The graph to be searched. /// - private readonly Graph graph; + public readonly Graph Graph; /// - /// The filter that is applied to the graph elements before they are searched. + /// The filter that is applied to the graph elements when they are searched. /// public GraphFilter Filter { get; } = new(); + /// + /// The sorter that is applied to the graph elements when they are searched. + /// + public GraphSorter Sorter { get; } = new(); + + private IEnumerable Modifiers => new IGraphModifier[] { Filter, Sorter }; + /// /// Creates a new instance of for the given . /// /// The graph to be searched. public GraphSearch(Graph graph) { - this.graph = graph; + this.Graph = graph; elements = graph.Nodes().GroupBy(ElementToString).ToDictionary(g => g.Key, g => g.ToList()); graph.Subscribe(this); } @@ -54,10 +61,14 @@ public GraphSearch(Graph graph) /// A list of nodes which match the query. public IEnumerable Search(string query, int limit = 10, int cutoff = 40) { - return Process.ExtractTop(FilterString(query), elements.Keys, limit: limit, cutoff: cutoff) - .SelectMany(x => Filter.Apply(elements[x.Value]).Select(Element => (x.Score, Element))) - .OrderByDescending(x => x.Score) - .Select(x => x.Element); + IEnumerable<(int Score, Node Element)> results = Process.ExtractTop(FilterString(query), elements.Keys, limit: limit, cutoff: cutoff) + .SelectMany(x => Modifiers.ApplyAll(elements[x.Value]).Select(Element => (x.Score, Element))); + if (!Sorter.IsActive()) + { + // If we don't sort by any custom attribute, we sort by the fuzzy score. + results = results.OrderByDescending(x => x.Score); + } + return results.Select(x => x.Element); } /// diff --git a/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs b/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs new file mode 100644 index 0000000000..59d2d01f71 --- /dev/null +++ b/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using SEE.DataModel.DG; + +namespace SEE.DataModel.GraphSearch +{ + /// + /// A configurable sorter for graph elements, mainly intended for use with . + /// + public class GraphSorter: IGraphModifier + { + /// + /// The attributes to sort by along with whether to sort descending, in the order of precedence. + /// + public readonly List<(string attribute, bool descending)> SortAttributes = new(); + + public IEnumerable Apply(IEnumerable elements) where T : GraphElement + { + return SortAttributes.Count == 0 + ? elements + // The first `OrderBy` call is to get an IOrderedEnumerable that we can repeatedly pass to `ThenBy`. + // `OrderBy` is stable, so the order is preserved, as we are passing in a constant key. + : SortAttributes.Aggregate(elements.OrderBy(_ => 0), + (current, sortAttribute) => + { + (string attribute, bool descending) = sortAttribute; + return descending + ? current.ThenByDescending(e => GetElementKey(e, attribute)) + : current.ThenBy(e => GetElementKey(e, attribute)); + }); + + object GetElementKey(T element, string attribute) + { + return element.TryGetAny(attribute, out object value) ? value : null; + } + } + + /// + /// Whether the given attribute is sorted descending. + /// Note that this returns null if the attribute is not sorted at all. + /// + /// The attribute to check. + /// Whether the attribute is sorted descending, or null if it is not sorted at all. + public bool? IsAttributeDescending(string attribute) + { + (string attribute, bool descending) result = SortAttributes.FirstOrDefault(a => a.attribute == attribute); + if (result == default) + { + return null; + } + else + { + return result.descending; + } + } + + + public bool IsActive() => SortAttributes.Count > 0; + + public void Reset() => SortAttributes.Clear(); + } +} diff --git a/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs.meta b/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs.meta new file mode 100644 index 0000000000..92f54b7599 --- /dev/null +++ b/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 79b7ddb4545b4a6f9518f4771ce28166 +timeCreated: 1700743470 \ No newline at end of file diff --git a/Assets/SEE/DataModel/GraphSearch/IGraphModifier.cs b/Assets/SEE/DataModel/GraphSearch/IGraphModifier.cs new file mode 100644 index 0000000000..ecfd226496 --- /dev/null +++ b/Assets/SEE/DataModel/GraphSearch/IGraphModifier.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using SEE.DataModel.DG; + +namespace SEE.DataModel.GraphSearch +{ + /// + /// Modifies a collection of graph elements by filtering, sorting, or otherwise transforming it. + /// Intended for use with . + /// + public interface IGraphModifier + { + /// + /// Applies the modifier to the given collection of graph elements. + /// + /// The graph elements to modify. + /// The type of the graph elements. + /// The modified collection. + IEnumerable Apply(IEnumerable elements) where T : GraphElement; + + /// + /// Returns whether the modifier is active, that is, + /// whether it would modify the collection in its current configuration. + /// + bool IsActive(); + + /// + /// Resets the modifier to its default configuration. + /// + void Reset(); + } +} diff --git a/Assets/SEE/DataModel/GraphSearch/IGraphModifier.cs.meta b/Assets/SEE/DataModel/GraphSearch/IGraphModifier.cs.meta new file mode 100644 index 0000000000..ae7d2f6c7d --- /dev/null +++ b/Assets/SEE/DataModel/GraphSearch/IGraphModifier.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 61d4574687ca49b781ef2a1698ca6b5f +timeCreated: 1700745751 \ No newline at end of file diff --git a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs index 7c5996655c..6e302e5710 100644 --- a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs @@ -116,7 +116,7 @@ void OrderTreeRecursive(Node node, int level) OrderItemHere(id, level); if (expandedItems.Contains(id)) { - IList children = WithHiddenChildren(node.Children()).OrderBy(x => x.SourceName).ToList(); + IList children = Searcher.Sorter.Apply(WithHiddenChildren(node.Children())).ToList(); foreach (Node child in children) { OrderTreeRecursive(child, level + 1); @@ -651,100 +651,11 @@ protected override void StartDesktop() SearchField.onValueChanged.AddListener(SearchFor); FilterButton = root.Find("Search/Filter").gameObject.MustGetComponent(); - FilterButton.clickEvent.AddListener(ShowFilterMenu); + SortButton = root.Find("Search/Sort").gameObject.MustGetComponent(); + PopupMenu.PopupMenu popupMenu = gameObject.AddComponent(); + ContextMenu = new TreeWindowContextMenu(popupMenu, Searcher, Rebuild, FilterButton, SortButton); AddRoots().Forget(); - return; - - // Constructs the menu for the filter button. - void UpdateFilterMenuEntries() - { - ISet nodeToggles = Graph.AllToggleNodeAttributes(); - ISet edgeToggles = Graph.AllToggleEdgeAttributes(); - ISet commonToggles = nodeToggles.Intersect(edgeToggles).ToHashSet(); - // Don't include common toggles in node/edge toggles. - nodeToggles.ExceptWith(commonToggles); - edgeToggles.ExceptWith(commonToggles); - // TODO: Allow filtering by node type. - - List entries = new() - { - new PopupMenuAction("Edges", - () => - { - Searcher.Filter.IncludeEdges = !Searcher.Filter.IncludeEdges; - UpdateFilterMenuEntries(); - Rebuild(); - }, - Checkbox(Searcher.Filter.IncludeEdges), CloseAfterClick: false), - }; - - if (Searcher.Filter.ExcludeElements.Count > 0) - { - entries.Insert(0, new PopupMenuAction("Show hidden elements", - () => - { - Searcher.Filter.ExcludeElements.Clear(); - Rebuild(); - }, - Icons.Show)); - } - - if (commonToggles.Count > 0) - { - entries.Add(new PopupMenuHeading("Common properties")); - entries.AddRange(commonToggles.Select(FilterActionFor)); - } - if (nodeToggles.Count > 0) - { - entries.Add(new PopupMenuHeading("Node properties")); - entries.AddRange(nodeToggles.Select(FilterActionFor)); - } - if (edgeToggles.Count > 0) - { - entries.Add(new PopupMenuHeading("Edge properties")); - entries.AddRange(edgeToggles.Select(FilterActionFor)); - } - - ContextMenu.ClearEntries(); - ContextMenu.AddEntries(entries); - } - - void ShowFilterMenu() - { - UpdateFilterMenuEntries(); - ContextMenu.ShowWith(position: FilterButton.transform.position); - } - - PopupMenuAction FilterActionFor(string toggleAttribute) - { - return new PopupMenuAction(toggleAttribute, - () => - { - // Toggle from include->exclude->none->include. - if (Searcher.Filter.IncludeToggleAttributes.Contains(toggleAttribute)) - { - Searcher.Filter.IncludeToggleAttributes.Remove(toggleAttribute); - Searcher.Filter.ExcludeToggleAttributes.Add(toggleAttribute); - } - else if (Searcher.Filter.ExcludeToggleAttributes.Contains(toggleAttribute)) - { - Searcher.Filter.ExcludeToggleAttributes.Remove(toggleAttribute); - } - else - { - Searcher.Filter.IncludeToggleAttributes.Add(toggleAttribute); - } - UpdateFilterMenuEntries(); - Rebuild(); - }, - Searcher.Filter.ExcludeToggleAttributes.Contains(toggleAttribute) - ? Icons.MinusCheckbox - : Checkbox(Searcher.Filter.IncludeToggleAttributes.Contains(toggleAttribute)), - CloseAfterClick: false); - } - - char Checkbox(bool value) => value ? Icons.CheckedCheckbox : Icons.EmptyCheckbox; } /// diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs index 6b9ae81f5a..c534aedb45 100644 --- a/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs @@ -43,14 +43,15 @@ public partial class TreeWindow : BaseWindow, IObserver private GraphSearch Searcher; /// - /// The context menu that is displayed when the user right-clicks on an item. + /// Transform of the object containing the items of the tree window. /// - private PopupMenu.PopupMenu ContextMenu; + private RectTransform items; /// - /// Transform of the object containing the items of the tree window. + /// The context menu that is displayed when the user right-clicks on an item + /// or uses the filter or sort buttons. /// - private RectTransform items; + private TreeWindowContextMenu ContextMenu; /// /// The subscription to the graph observable. @@ -60,8 +61,6 @@ public partial class TreeWindow : BaseWindow, IObserver protected override void Start() { Searcher = new GraphSearch(Graph); - Searcher.Filter.IncludeToggleAttributes.UnionWith(Graph.AllToggleGraphElementAttributes()); - ContextMenu = gameObject.AddComponent(); subscription = Graph.Subscribe(this); base.Start(); } diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs new file mode 100644 index 0000000000..6027dcc16c --- /dev/null +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Michsky.UI.ModernUIPack; +using SEE.DataModel.DG; +using SEE.DataModel.GraphSearch; +using SEE.UI.PopupMenu; +using SEE.Utils; +using UnityEngine; + +namespace SEE.UI.Window.TreeWindow +{ + /// + /// Manages the context menu for the tree window. + /// + public class TreeWindowContextMenu + { + /// + /// The context menu that this class manages. + /// + private readonly PopupMenu.PopupMenu ContextMenu; + + /// + /// The graph search associated with the tree window. + /// We also retrieve the graph from this. + /// + private readonly GraphSearch Searcher; + + /// + /// The function to call to rebuild the tree window. + /// + private readonly Action Rebuild; + + /// + /// The button that opens the filter menu. + /// + private readonly ButtonManagerBasic FilterButton; + + /// + /// The button that opens the sort menu. + /// + private readonly ButtonManagerBasic SortButton; + + /// + /// Constructor. + /// + /// The context menu that this class manages. + /// The graph search associated with the tree window. + /// The function to call to rebuild the tree window. + /// The button that opens the filter menu. + /// The button that opens the sort menu. + public TreeWindowContextMenu(PopupMenu.PopupMenu contextMenu, GraphSearch searcher, Action rebuild, + ButtonManagerBasic filterButton, ButtonManagerBasic sortButton) + { + ContextMenu = contextMenu; + Searcher = searcher; + Rebuild = rebuild; + FilterButton = filterButton; + SortButton = sortButton; + + ResetFilter(); + ResetSort(); + FilterButton.clickEvent.AddListener(ShowFilterMenu); + SortButton.clickEvent.AddListener(ShowSortMenu); + } + + /// + /// Forwards to . + /// + public void ShowWith(IEnumerable entries, Vector2 position) => ContextMenu.ShowWith(entries, position); + + #region Filter menu + + /// + /// Displays the filter menu. + /// + public void ShowFilterMenu() + { + UpdateFilterMenuEntries(); + ContextMenu.ShowWith(position: FilterButton.transform.position); + } + + /// + /// Updates the filter menu entries. + /// + private void UpdateFilterMenuEntries() + { + ISet nodeToggles = Searcher.Graph.AllToggleNodeAttributes(); + ISet edgeToggles = Searcher.Graph.AllToggleEdgeAttributes(); + ISet commonToggles = nodeToggles.Intersect(edgeToggles).ToHashSet(); + // Don't include common toggles in node/edge toggles. + nodeToggles.ExceptWith(commonToggles); + edgeToggles.ExceptWith(commonToggles); + // TODO: Allow filtering by node type. + + List entries = new() + { + new PopupMenuAction("Reset", () => + { + ResetFilter(); + UpdateFilterMenuEntries(); + Rebuild(); + }, Icons.ArrowRotateLeft, CloseAfterClick: false), + new PopupMenuAction("Edges", + () => + { + Searcher.Filter.IncludeEdges = !Searcher.Filter.IncludeEdges; + UpdateFilterMenuEntries(); + Rebuild(); + }, + Checkbox(Searcher.Filter.IncludeEdges), CloseAfterClick: false), + }; + + if (Searcher.Filter.ExcludeElements.Count > 0) + { + entries.Insert(0, new PopupMenuAction("Show hidden elements", + () => + { + Searcher.Filter.ExcludeElements.Clear(); + Rebuild(); + }, + Icons.Show)); + } + + if (commonToggles.Count > 0) + { + entries.Add(new PopupMenuHeading("Common properties")); + entries.AddRange(commonToggles.Select(FilterActionFor)); + } + if (nodeToggles.Count > 0) + { + entries.Add(new PopupMenuHeading("Node properties")); + entries.AddRange(nodeToggles.Select(FilterActionFor)); + } + if (edgeToggles.Count > 0) + { + entries.Add(new PopupMenuHeading("Edge properties")); + entries.AddRange(edgeToggles.Select(FilterActionFor)); + } + + ContextMenu.ClearEntries(); + ContextMenu.AddEntries(entries); + } + + /// + /// Returns the filter action for the given . + /// + /// The toggle attribute to create a filter action for. + /// The filter action for the given . + private PopupMenuAction FilterActionFor(string toggleAttribute) + { + return new PopupMenuAction(toggleAttribute, ToggleFilterAction, + Searcher.Filter.ExcludeToggleAttributes.Contains(toggleAttribute) + ? Icons.MinusCheckbox + : Checkbox(Searcher.Filter.IncludeToggleAttributes.Contains(toggleAttribute)), + CloseAfterClick: false); + + void ToggleFilterAction() + { + // Toggle from include->exclude->none->include. + if (Searcher.Filter.IncludeToggleAttributes.Contains(toggleAttribute)) + { + Searcher.Filter.IncludeToggleAttributes.Remove(toggleAttribute); + Searcher.Filter.ExcludeToggleAttributes.Add(toggleAttribute); + } + else if (Searcher.Filter.ExcludeToggleAttributes.Contains(toggleAttribute)) + { + Searcher.Filter.ExcludeToggleAttributes.Remove(toggleAttribute); + } + else + { + Searcher.Filter.IncludeToggleAttributes.Add(toggleAttribute); + } + UpdateFilterMenuEntries(); + Rebuild(); + } + } + + /// + /// Resets the filter to its default state. + /// + private void ResetFilter() + { + Searcher.Filter.Reset(); + } + + /// + /// Returns the icon for a checkbox. + /// + /// Whether the checkbox is checked. + /// The icon for a checkbox. + private static char Checkbox(bool value) => value ? Icons.CheckedCheckbox : Icons.EmptyCheckbox; + + #endregion + + #region Sort menu + + /// + /// Displays the sort menu. + /// + public void ShowSortMenu() + { + UpdateSortMenuEntries(); + ContextMenu.ShowWith(position: SortButton.transform.position); + } + + /// + /// Updates the sort menu entries. + /// + private void UpdateSortMenuEntries() + { + List entries = new() + { + new PopupMenuAction("Reset", () => + { + ResetSort(); + UpdateSortMenuEntries(); + Rebuild(); + }, Icons.ArrowRotateLeft, CloseAfterClick: false) + }; + + // TODO: Add all attributes, or only pre-selected common ones? + entries.AddRange(Searcher.Graph.AllNumericAttributes().Select(attribute => SortActionFor(attribute, numeric: true))); + entries.AddRange(Searcher.Graph.AllStringAttributes().Select(attribute => SortActionFor(attribute, numeric: false))); + + ContextMenu.ClearEntries(); + ContextMenu.AddEntries(entries); + } + + /// + /// Returns the sort action for the given . + /// + /// The attribute to create a sort action for. + /// Whether the attribute name is for a numeric attribute. + /// The sort action for the given . + private PopupMenuAction SortActionFor(string attribute, bool numeric) + { + return new PopupMenuAction(attribute, ToggleSortAction, + SortIcon(numeric, Searcher.Sorter.IsAttributeDescending(attribute)), + CloseAfterClick: false); + + void ToggleSortAction() + { + // Switch from ascending->descending->none->ascending. + switch (Searcher.Sorter.IsAttributeDescending(attribute)) + { + case null: Searcher.Sorter.SortAttributes.Add((attribute, false)); + break; + case false: + Searcher.Sorter.SortAttributes.Remove((attribute, false)); + Searcher.Sorter.SortAttributes.Add((attribute, true)); + break; + default: Searcher.Sorter.SortAttributes.Remove((attribute, true)); + break; + } + UpdateSortMenuEntries(); + Rebuild(); + } + } + + /// + /// Returns the sort icon depending on whether the attribute is + /// and whether it is sorted in order. + /// + /// Whether the attribute is numeric. + /// Whether the attribute is sorted in descending order. + /// The sort icon depending on the given parameters. + private static char SortIcon(bool numeric, bool? descending) + { + return (numeric, descending) switch + { + (_, null) => ' ', + (true, true) => Icons.SortNumericDown, + (true, false) => Icons.SortNumericUp, + (false, true) => Icons.SortAlphabeticalDown, + (false, false) => Icons.SortAlphabeticalUp + }; + } + + /// + /// Resets the sort to its default state. + /// + private void ResetSort() + { + Searcher.Sorter.Reset(); + Searcher.Sorter.SortAttributes.Add((Node.SourceNameAttribute, false)); + } + + #endregion + } +} diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs.meta b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs.meta new file mode 100644 index 0000000000..ac3feb721e --- /dev/null +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 44c97c49b245449fa2f08e9c2b3e98a3 +timeCreated: 1700750122 \ No newline at end of file diff --git a/Assets/SEE/Utils/Icons.cs b/Assets/SEE/Utils/Icons.cs index 27c8602662..7fe8795450 100644 --- a/Assets/SEE/Utils/Icons.cs +++ b/Assets/SEE/Utils/Icons.cs @@ -30,5 +30,10 @@ public static class Icons public const char Hide = '\uF070'; public const char Show = '\uF06E'; public const char QuestionMark = '?'; + public const char ArrowRotateLeft = '\uF0E2'; + public const char SortAlphabeticalUp = '\uF15E'; + public const char SortAlphabeticalDown = '\uF15D'; + public const char SortNumericUp = '\uF163'; + public const char SortNumericDown = '\uF162'; } } From cbe62f4e681f2b82465f2f783fc8bb3bd9747cf7 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 9 Dec 2023 21:44:26 +0100 Subject: [PATCH 15/30] Implement GetOrAdd extension method --- Assets/SEE/Utils/CollectionExtensions.cs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Assets/SEE/Utils/CollectionExtensions.cs b/Assets/SEE/Utils/CollectionExtensions.cs index 88b3a99e19..8cdcbc0f26 100644 --- a/Assets/SEE/Utils/CollectionExtensions.cs +++ b/Assets/SEE/Utils/CollectionExtensions.cs @@ -42,13 +42,32 @@ public static ISet> Permutations(this IList inputList) /// The type of the elements in the set. public static void Toggle(this ISet set, T element) { - if (set.Contains(element)) + if (!set.Add(element)) { set.Remove(element); } + } + + /// + /// Gets the value for the given from the given . + /// If the key is not present in the dictionary, the given + /// will be added to the dictionary and returned. + /// + /// The dictionary from which the value shall be retrieved. + /// The key for which the value shall be retrieved. + /// The default value which shall be added to the dictionary if the key is not present. + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + /// The value for the given from the given . + public static V GetOrAdd(this IDictionary dict, K key, V defaultValue) + { + if (dict.TryGetValue(key, out V value)) + { + return value; + } else { - set.Add(element); + return dict[key] = defaultValue; } } } From 74e597f636b14374bb0cbe95d66410d983852b1d Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 9 Dec 2023 21:47:40 +0100 Subject: [PATCH 16/30] Implement DefaultDictionary --- Assets/SEE/Utils/DefaultDictionary.cs | 20 ++++++++++++++++++++ Assets/SEE/Utils/DefaultDictionary.cs.meta | 3 +++ 2 files changed, 23 insertions(+) create mode 100644 Assets/SEE/Utils/DefaultDictionary.cs create mode 100644 Assets/SEE/Utils/DefaultDictionary.cs.meta diff --git a/Assets/SEE/Utils/DefaultDictionary.cs b/Assets/SEE/Utils/DefaultDictionary.cs new file mode 100644 index 0000000000..129b29980a --- /dev/null +++ b/Assets/SEE/Utils/DefaultDictionary.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace SEE.Utils +{ + /// + /// A dictionary which adds and returns a default value if the key is not present. + /// Note that the dictionary must not be downcast to a normal dictionary, as this would + /// remove the default value functionality. + /// + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + public class DefaultDictionary : Dictionary where V : new() + { + public new V this[K key] + { + get => this.GetOrAdd(key, new V()); + set => base[key] = value; + } + } +} diff --git a/Assets/SEE/Utils/DefaultDictionary.cs.meta b/Assets/SEE/Utils/DefaultDictionary.cs.meta new file mode 100644 index 0000000000..70a06b8cca --- /dev/null +++ b/Assets/SEE/Utils/DefaultDictionary.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 438efdbfddf247a28fb4f9a2f261f5f9 +timeCreated: 1701353409 \ No newline at end of file From 09ac5cb5a56bf9a6c95de432c0f898950b242f43 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 9 Dec 2023 21:47:58 +0100 Subject: [PATCH 17/30] Simplify Graph.AllNodeTypes and add AllEdgeTypes --- Assets/SEE/DataModel/DG/Graph.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Assets/SEE/DataModel/DG/Graph.cs b/Assets/SEE/DataModel/DG/Graph.cs index 70493424cb..f21247f23f 100644 --- a/Assets/SEE/DataModel/DG/Graph.cs +++ b/Assets/SEE/DataModel/DG/Graph.cs @@ -486,15 +486,13 @@ public void RemoveElement(GraphElement element) /// Returns the names of all node types of this graph /// /// node types of this graph - internal HashSet AllNodeTypes() - { - HashSet result = new(); - foreach (Node node in Nodes()) - { - result.Add(node.Type); - } - return result; - } + internal HashSet AllNodeTypes() => Nodes().Select(n => n.Type).ToHashSet(); + + /// + /// Returns the names of all edge types of this graph + /// + /// edge types of this graph + internal HashSet AllEdgeTypes() => Edges().Select(e => e.Type).ToHashSet(); /// /// The number of nodes of the graph. From daa6e919b15c349986d32fd1f0d7d8c19034c453 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 9 Dec 2023 21:48:38 +0100 Subject: [PATCH 18/30] CI: Fix bad patterns not being detected --- .github/workflows/main.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e519a614d..437097a53d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,14 +71,17 @@ jobs: steps: - name: Collect bad patterns run: | - if [ -n "$GITHUB_BASE_REF" ] && ! PATTERNS=$(curl -H 'Accept: application/vnd.github.v3.diff' -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' -f 'https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }}' | ./GitScripts/check_for_bad_patterns.py); then - DELIMITER=7MApgggGyx6C0 - echo "PATTERNS<<$DELIMITER" >> $GITHUB_ENV - echo "$PATTERNS" >> $GITHUB_ENV - echo "$DELIMITER" >> $GITHUB_ENV - else - echo "PATTERNS=none" >> $GITHUB_ENV + if [ -n "$GITHUB_BASE_REF" ]; then + PATTERNS=$(curl -H 'Accept: application/vnd.github.v3.diff' -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' -f 'https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }}' | ./GitScripts/check_for_bad_patterns.py) + if [ "$PATTERNS" != "[]" ]; then + DELIMITER=7MApgggGyx6C0 + echo "PATTERNS<<$DELIMITER" >> $GITHUB_ENV + echo "$PATTERNS" >> $GITHUB_ENV + echo "$DELIMITER" >> $GITHUB_ENV + exit 0 + fi fi + echo "PATTERNS=none" >> $GITHUB_ENV - uses: actions/github-script@v6 name: Check for bad patterns with: From 2409a939fabced9207f1a2311e77fffb88da4960 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 9 Dec 2023 21:52:29 +0100 Subject: [PATCH 19/30] Update Unity to 2022.3.15f1 --- Axivion/axivion-jenkins.bat | 4 ++-- ProjectSettings/ProjectVersion.txt | 4 ++-- README.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Axivion/axivion-jenkins.bat b/Axivion/axivion-jenkins.bat index 14ea8be980..ee4d5db0ad 100644 --- a/Axivion/axivion-jenkins.bat +++ b/Axivion/axivion-jenkins.bat @@ -42,8 +42,8 @@ REM net (start|stop) "axivion_dashboard_service" REM or use the Windows Services Console (services.msc). REM The Visual Studio .csproj files need to be created before we can start the build. -"C:\Program Files\Unity\Hub\Editor\2022.3.13f1\Editor\Unity.exe" -batchmode -nographics -logFile - -executeMethod CITools.SolutionGenerator.Sync -projectPath . -quit -REM "C:\Program Files\Unity\Hub\Editor\2022.3.13f1\Editor\Unity.exe" -batchmode -nographics -logFile - -executeMethod UnityEditor.SyncVS.SyncSolution -projectPath . -quit +"C:\Program Files\Unity\Hub\Editor\2022.3.15f1\Editor\Unity.exe" -batchmode -nographics -logFile - -executeMethod CITools.SolutionGenerator.Sync -projectPath . -quit +REM "C:\Program Files\Unity\Hub\Editor\2022.3.15f1\Editor\Unity.exe" -batchmode -nographics -logFile - -executeMethod UnityEditor.SyncVS.SyncSolution -projectPath . -quit REM Count the number of command-line parameters setlocal enabledelayedexpansion diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index 91206fdc40..2e7bb8a620 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2022.3.13f1 -m_EditorVersionWithRevision: 2022.3.13f1 (5f90a5ebde0f) +m_EditorVersion: 2022.3.15f1 +m_EditorVersionWithRevision: 2022.3.15f1 (b58023a2b463) diff --git a/README.md b/README.md index 435b745028..4faa473b76 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Tests](https://github.com/uni-bremen-agst/SEE/actions/workflows/main.yml/badge.svg)](https://github.com/uni-bremen-agst/SEE/actions/workflows/main.yml) SEE visualizes hierarchical dependency graphs of software in 3D/VR based on the city metaphor. -The underlying game engine is Unity 3D (version 2022.3.13f1). +The underlying game engine is Unity 3D (version 2022.3.15f1). ![Screenshot of SEE](Screenshot.png) From f7ed25ae247ba4553619fc9f6f84d79acc847358 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 9 Dec 2023 21:59:17 +0100 Subject: [PATCH 20/30] Update packages to newest version --- .../Avatars/PersonalAssistantSpeechInput.cs | 4 +- Packages/manifest.json | 8 ++-- Packages/packages-lock.json | 40 ++++++++++++------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Assets/SEE/Game/Avatars/PersonalAssistantSpeechInput.cs b/Assets/SEE/Game/Avatars/PersonalAssistantSpeechInput.cs index 1bff50abcc..a40b0a90ad 100644 --- a/Assets/SEE/Game/Avatars/PersonalAssistantSpeechInput.cs +++ b/Assets/SEE/Game/Avatars/PersonalAssistantSpeechInput.cs @@ -228,7 +228,7 @@ async UniTaskVoid SendChatMessage(ChatRequest request, Notification notification { ChatResponse result = await openAiClient.ChatEndpoint.GetCompletionAsync(request); notification.Close(); - string message = result.FirstChoice.Message.Content; + string message = result.FirstChoice.Message.Content.ToString(); chatGptHistory.Add(new Message(Role.Assistant, message)); // We need to stop listening before we start speaking, else we will hear our own voice. StopListening(); @@ -319,4 +319,4 @@ private void Update() } } } -} \ No newline at end of file +} diff --git a/Packages/manifest.json b/Packages/manifest.json index 05b5e45618..9f9fa957ab 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -2,17 +2,17 @@ "dependencies": { "com.cakeslice.outline-effect": "https://github.com/cakeslice/Outline-Effect.git", "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#2.2.5", - "com.openai.unity": "5.0.11", + "com.openai.unity": "7.0.3", "com.unity.2d.sprite": "1.0.0", "com.unity.2d.tilemap": "1.0.0", "com.unity.ai.navigation": "1.1.5", - "com.unity.burst": "1.8.10", + "com.unity.burst": "1.8.11", "com.unity.formats.fbx": "4.2.1", - "com.unity.ide.rider": "3.0.26", + "com.unity.ide.rider": "3.0.27", "com.unity.ide.visualstudio": "2.0.22", "com.unity.ide.vscode": "1.2.5", "com.unity.inputsystem": "1.7.0", - "com.unity.netcode.gameobjects": "1.7.0", + "com.unity.netcode.gameobjects": "1.7.1", "com.unity.postprocessing": "3.2.2", "com.unity.shadergraph": "14.0.9", "com.unity.test-framework": "1.1.33", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index eb153d0dee..d57beeaf10 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -22,12 +22,13 @@ "hash": "72e620d169841f32bc6110ad0e12ee9eae6f1aaf" }, "com.openai.unity": { - "version": "5.0.11", + "version": "7.0.3", "depth": 0, "source": "registry", "dependencies": { - "com.utilities.rest": "2.1.8", - "com.utilities.encoder.wav": "1.0.6" + "com.utilities.rest": "2.3.1", + "com.utilities.encoder.wav": "1.0.8", + "com.utilities.encoder.ogg": "3.0.12" }, "url": "https://package.openupm.com" }, @@ -56,7 +57,7 @@ "url": "https://packages.unity.com" }, "com.unity.burst": { - "version": "1.8.10", + "version": "1.8.11", "depth": 0, "source": "registry", "dependencies": { @@ -99,7 +100,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.rider": { - "version": "3.0.26", + "version": "3.0.27", "depth": 0, "source": "registry", "dependencies": { @@ -140,7 +141,7 @@ "url": "https://packages.unity.com" }, "com.unity.netcode.gameobjects": { - "version": "1.7.0", + "version": "1.7.1", "depth": 0, "source": "registry", "dependencies": { @@ -354,7 +355,7 @@ "url": "https://packages.unity.com" }, "com.utilities.async": { - "version": "2.0.1", + "version": "2.1.1", "depth": 2, "source": "registry", "dependencies": { @@ -365,27 +366,36 @@ "url": "https://package.openupm.com" }, "com.utilities.audio": { - "version": "1.0.6", + "version": "1.0.11", "depth": 2, "source": "registry", "dependencies": { "com.unity.modules.audio": "1.0.0", "com.unity.modules.unitywebrequestaudio": "1.0.0", - "com.utilities.async": "2.0.1" + "com.utilities.async": "2.1.1" + }, + "url": "https://package.openupm.com" + }, + "com.utilities.encoder.ogg": { + "version": "3.0.12", + "depth": 1, + "source": "registry", + "dependencies": { + "com.utilities.audio": "1.0.11" }, "url": "https://package.openupm.com" }, "com.utilities.encoder.wav": { - "version": "1.0.6", + "version": "1.0.8", "depth": 1, "source": "registry", "dependencies": { - "com.utilities.audio": "1.0.6" + "com.utilities.audio": "1.0.11" }, "url": "https://package.openupm.com" }, "com.utilities.extensions": { - "version": "1.1.10", + "version": "1.1.13", "depth": 2, "source": "registry", "dependencies": { @@ -395,12 +405,12 @@ "url": "https://package.openupm.com" }, "com.utilities.rest": { - "version": "2.1.8", + "version": "2.3.1", "depth": 1, "source": "registry", "dependencies": { - "com.utilities.async": "2.0.1", - "com.utilities.extensions": "1.1.10", + "com.utilities.async": "2.1.1", + "com.utilities.extensions": "1.1.13", "com.unity.modules.unitywebrequest": "1.0.0", "com.unity.modules.unitywebrequestassetbundle": "1.0.0", "com.unity.modules.unitywebrequestaudio": "1.0.0", From 0a75910b01a64913132c4aa849924792fd1e08f7 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 9 Dec 2023 22:46:01 +0100 Subject: [PATCH 21/30] Implement grouping in TreeWindow #444 --- .../SEE/DataModel/GraphSearch/GraphFilter.cs | 2 +- .../SEE/DataModel/GraphSearch/GraphSorter.cs | 50 ++- .../SEE/Game/City/ReflexionVisualization.cs | 15 +- .../UI/Window/TreeWindow/DesktopTreeWindow.cs | 324 +++++++++++++----- Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs | 62 +++- .../TreeWindow/TreeWindowContextMenu.cs | 160 +++++++-- .../UI/Window/TreeWindow/TreeWindowGrouper.cs | 231 +++++++++++++ .../TreeWindow/TreeWindowGrouper.cs.meta | 3 + Assets/SEE/Utils/Icons.cs | 7 +- 9 files changed, 707 insertions(+), 147 deletions(-) create mode 100644 Assets/SEE/UI/Window/TreeWindow/TreeWindowGrouper.cs create mode 100644 Assets/SEE/UI/Window/TreeWindow/TreeWindowGrouper.cs.meta diff --git a/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs index 2f14688e06..2ce5b8e7d8 100644 --- a/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs +++ b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs @@ -7,7 +7,7 @@ namespace SEE.DataModel.GraphSearch /// /// A configurable filter for graph elements, mainly intended for use with . /// - public class GraphFilter : IGraphModifier + public record GraphFilter : IGraphModifier { // if empty, include all. otherwise, *at least one* must be present. /// diff --git a/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs b/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs index 59d2d01f71..c3dc73c95c 100644 --- a/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs +++ b/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using SEE.DataModel.DG; @@ -7,12 +8,33 @@ namespace SEE.DataModel.GraphSearch /// /// A configurable sorter for graph elements, mainly intended for use with . /// - public class GraphSorter: IGraphModifier + public class GraphSorter : IGraphModifier { /// /// The attributes to sort by along with whether to sort descending, in the order of precedence. /// - public readonly List<(string attribute, bool descending)> SortAttributes = new(); + private readonly List<(string Name, Func GetKey, bool Descending)> SortAttributes = new(); + + /// + /// Add an attribute to sort by. + /// + /// The name of the attribute to sort by. + /// A function that returns the value to sort by for the given element. + /// Whether to sort descending. + public void AddSortAttribute(string attributeName, Func getAttribute, bool descending) + { + SortAttributes.Add((attributeName, getAttribute, descending)); + } + + /// + /// Removes the sort attribute with the given name. + /// If there are multiple attributes with the given name, all of them are removed. + /// + /// The name of the attribute to remove. + public void RemoveSortAttribute(string attributeName) + { + SortAttributes.RemoveAll(a => a.Name == attributeName); + } public IEnumerable Apply(IEnumerable elements) where T : GraphElement { @@ -23,34 +45,32 @@ public IEnumerable Apply(IEnumerable elements) where T : GraphElement : SortAttributes.Aggregate(elements.OrderBy(_ => 0), (current, sortAttribute) => { - (string attribute, bool descending) = sortAttribute; - return descending - ? current.ThenByDescending(e => GetElementKey(e, attribute)) - : current.ThenBy(e => GetElementKey(e, attribute)); + (_, Func GetKey, bool Descending) = sortAttribute; + return Descending + ? current.ThenByDescending(x => GetKey(x)) + : current.ThenBy(x => GetKey(x)); }); - - object GetElementKey(T element, string attribute) - { - return element.TryGetAny(attribute, out object value) ? value : null; - } } /// /// Whether the given attribute is sorted descending. /// Note that this returns null if the attribute is not sorted at all. /// - /// The attribute to check. + /// The attribute to check. /// Whether the attribute is sorted descending, or null if it is not sorted at all. - public bool? IsAttributeDescending(string attribute) + /// + /// If there is more than one attribute with the given name, the first one is returned. + /// + public bool? IsAttributeDescending(string attributeName) { - (string attribute, bool descending) result = SortAttributes.FirstOrDefault(a => a.attribute == attribute); + (string, Func, bool Descending) result = SortAttributes.FirstOrDefault(a => a.Name == attributeName); if (result == default) { return null; } else { - return result.descending; + return result.Descending; } } diff --git a/Assets/SEE/Game/City/ReflexionVisualization.cs b/Assets/SEE/Game/City/ReflexionVisualization.cs index 215685bb35..291f341789 100644 --- a/Assets/SEE/Game/City/ReflexionVisualization.cs +++ b/Assets/SEE/Game/City/ReflexionVisualization.cs @@ -93,7 +93,7 @@ public void InitializeEdges() GameObject edgeObject = edge.GameObject(); if (edgeObject != null && edgeObject.TryGetComponent(out SEESpline spline)) { - spline.GradientColors = GetEdgeGradient(edge); + spline.GradientColors = GetEdgeGradient(edge.State()); if (edge.HasToggle(Edge.IsHiddenToggle)) { @@ -176,14 +176,13 @@ public void StartFromScratch(ReflexionGraph graph, SEEReflexionCity city) } /// - /// Returns a fitting color gradient from the first to the second color for the given edge by examining - /// its state. + /// Returns a fitting color gradient from the first to the second color for the given edge state. /// - /// edge for which to yield a color gradient + /// edge state for which to yield a color gradient /// color gradient - private static (Color, Color) GetEdgeGradient(Edge edge) + public static (Color, Color) GetEdgeGradient(State edgeState) { - (Color, Color) gradient = edge.State() switch + (Color, Color) gradient = edgeState switch { State.Undefined => (Color.black, Color.Lerp(Color.gray, Color.black, edgeGradientFactor)), State.Specified => (Color.gray, Color.Lerp(Color.gray, Color.black, edgeGradientFactor)), @@ -194,7 +193,7 @@ private static (Color, Color) GetEdgeGradient(Edge edge) State.Divergent => (Color.red, Color.Lerp(Color.red, Color.black, edgeGradientFactor)), State.Absent => (Color.yellow, Color.Lerp(Color.yellow, Color.black, edgeGradientFactor)), State.Convergent => (Color.green, Color.Lerp(Color.green, Color.black, edgeGradientFactor)), - _ => throw new ArgumentOutOfRangeException(nameof(edge), edge.State(), "Unknown state of given edge!") + _ => throw new ArgumentOutOfRangeException(nameof(edgeState), edgeState, "Unknown state of given edge!") }; return gradient; @@ -305,7 +304,7 @@ private async UniTaskVoid HandleEdgeChange(EdgeChange edgeChange) if (edge != null) { - (Color start, Color end) newColors = GetEdgeGradient(edgeChange.Edge); + (Color start, Color end) newColors = GetEdgeGradient(edgeChange.Edge.State()); EdgeOperator edgeOperator = edge.EdgeOperator(); edgeOperator.ShowOrHide(!edgeChange.Edge.HasToggle(Edge.IsHiddenToggle), city.EdgeLayoutSettings.AnimationKind); edgeOperator.ChangeColorsTo((newColors.start, newColors.end), useAlpha: false); diff --git a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs index 6e302e5710..c56ba70bee 100644 --- a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs @@ -16,6 +16,8 @@ using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; +using ArgumentException = System.ArgumentException; +using Edge = SEE.DataModel.DG.Edge; using Node = SEE.DataModel.DG.Node; namespace SEE.UI.Window.TreeWindow @@ -74,26 +76,56 @@ public partial class TreeWindow /// private ButtonManagerBasic SortButton; + /// + /// Orders the tree below the given group according to the graph hierarchy. + /// + /// The group below which the tree should be ordered. + private void OrderTree(TreeWindowGroup orderBelow) + { + Transform groupItem = items.Find(CleanupID(orderBelow.Text)); + if (groupItem == null) + { + return; + } + + int groupIndex = groupItem.GetSiblingIndex() + 1; + foreach (Node node in GetRoots(orderBelow)) + { + // The groups are always at the top level of the tree window. + // Thus, we put the root nodes (indented by one level) directly below the group. + items.Find(ElementId(node, orderBelow)).SetSiblingIndex(groupIndex); + // The next root should be added below the last one, which is why we use the current index. + groupIndex = OrderTree(node, 1, orderBelow) ?? groupIndex; + } + } + /// /// Orders the tree below the given node according to the graph hierarchy. /// This needs to be called whenever the tree is expanded. /// /// The node below which the tree should be ordered. - private void OrderTree(Node orderBelow) + /// The level of the given node. + /// The group in which is contained, if any. + /// The index in the hierarchy of the last node handled by this method. + /// If no nodes were ordered, null is returned. + private int? OrderTree(Node orderBelow, int? nodeLevel = null, TreeWindowGroup inGroup = null) { - Transform nodeItem = items.Find(CleanupID(orderBelow.ID)); + Transform nodeItem = items.Find(ElementId(orderBelow, inGroup)); if (nodeItem == null) { - return; + return null; } // We determine the node level based on the indent of the foreground. - int nodeLevel = Mathf.RoundToInt(((RectTransform)nodeItem.Find("Foreground")).offsetMin.x) / indentShift; + if (!nodeLevel.HasValue) + { + nodeLevel = Mathf.RoundToInt(((RectTransform)nodeItem.Find("Foreground")).offsetMin.x) / indentShift; + } int index = nodeItem.GetSiblingIndex(); - OrderTreeRecursive(orderBelow, nodeLevel); + OrderTreeRecursive(orderBelow, nodeLevel.Value); - return; + return index; // Orders the item with the given id to the current index and increments the index. void OrderItemHere(string id, int level) @@ -112,17 +144,22 @@ void OrderItemHere(string id, int level) // Recurses over the tree in pre-order and assigns indices to each node. void OrderTreeRecursive(Node node, int level) { - string id = CleanupID(node.ID); + string id = ElementId(node, inGroup); OrderItemHere(id, level); if (expandedItems.Contains(id)) { - IList children = Searcher.Sorter.Apply(WithHiddenChildren(node.Children())).ToList(); + IEnumerable children = Searcher.Sorter.Apply(WithHiddenChildren(node.Children(), inGroup)); + // When grouping is active, we sort by the count of group elements in this node. + if (Grouper.IsActive) + { + children = children.OrderByDescending(x => Grouper.DescendantsInGroup(x, inGroup)); + } foreach (Node child in children) { OrderTreeRecursive(child, level + 1); } - foreach ((List edges, string edgesType) in RelevantEdges(node)) + foreach ((List edges, string edgesType) in RelevantEdges(node, inGroup)) { HandleEdges($"{id}#{edgesType}", edges, level + 1); } @@ -146,25 +183,111 @@ void HandleEdges(string edgesId, ICollection edges, int level) } } + /// + /// Returns the TreeWindow ID of the given in the given . + /// + /// The element whose ID shall be returned. + /// The group in which the element is contained, if any. + /// The TreeWindow ID of the given in the given . + private static string ElementId(GraphElement element, TreeWindowGroup group) + { + string id = CleanupID(element.ID); + if (group != null) + { + // If it belongs to a group, we will need to append the group name, otherwise the ID will not be unique, + // as the element may be used repeatedly across multiple groups. + id = $"{id}#{CleanupID(group.Text)}"; + } + return id; + } + + /// + /// Adds the given to the bottom of the tree window. + /// + /// The group to be added. + private void AddGroup(TreeWindowGroup group) + { + AddItem(CleanupID(group.Text), true, $"{group.Text} [{Grouper.MembersInGroup(group)}]", + group.IconGlyph, gradient: group.Gradient, + collapseItem: CollapseGroup, expandItem: ExpandGroup); + if (expandedItems.Contains(CleanupID(group.Text))) + { + ExpandGroup(items.Find(CleanupID(group.Text))?.gameObject, order: false); + } + return; + + void CollapseGroup(GameObject item) + { + CollapseItem(item); + foreach (GraphElement element in GetRoots(group)) + { + RemoveItem(ElementId(element, group), element, + x => x is Node node ? GetChildItems(node, group) : Enumerable.Empty<(string, Node)>()); + } + } + + void ExpandGroup(GameObject item, bool order) + { + ExpandItem(item); + foreach (Node element in GetRoots(group)) + { + AddNode(element, group); + } + if (order) + { + OrderTree(group); + } + } + } + + /// + /// Whether the given shall be displayed in the tree window. + /// + /// The element to be checked. + /// The group in which the element is contained, if any. + /// Whether the given shall be displayed in the tree window. + private bool ShouldBeDisplayed(GraphElement element, TreeWindowGroup inGroup = null) + { + return (inGroup != null && Grouper.IsRelevantFor(element, inGroup)) || (inGroup == null && Searcher.Filter.Includes(element)); + } + /// /// Adds the given to the bottom of the tree window. /// /// The node to be added. - private void AddNode(Node node) + /// The group in which the node is contained, if any. + private void AddNode(Node node, TreeWindowGroup inGroup = null) { GameObject nodeGameObject = GraphElementIDMap.Find(node.ID); - int children = node.NumberOfChildren() + Mathf.Min(node.Outgoings.Count, 1) + Mathf.Min(node.Incomings.Count, 1); + int children = node.NumberOfChildren() + node.Edges.Count; - if (Searcher.Filter.Includes(node)) + if (ShouldBeDisplayed(node, inGroup)) { - AddItem(CleanupID(node.ID), children, node.ToShortString(), Icons.Node, nodeGameObject, node, - item => CollapseNode(node, item), (item, order) => ExpandNode(node, item, orderTree: order)); + string text = node.ToShortString(); + string id = ElementId(node, inGroup); + Color[] gradient = null; + if (inGroup != null) + { + // Not actually the number of direct children, but this doesn't matter, as we only + // need it for the text and to check whether there are any children at all. + children = Grouper.DescendantsInGroup(node, inGroup); + if (Grouper.GetGroupFor(node) != inGroup) + { + // This node is only included because it has relevant descendants. + // TODO: In this case, are italics fine, or should we color the item gray? + text = $"{text}"; + } + text = $"{text} [{children}]"; + } + AddItem(id, children > 0, text, Icons.Node, nodeGameObject, node, + gradient, collapseItem: item => CollapseNode(node, item, inGroup), + expandItem: (item, order) => ExpandNode(node, item, orderTree: order, inGroup)); } else { // The node itself may not be included, but its children (or edges) could be. // Thus, we assume this invisible node to be expanded by default and add its children. - ExpandNode(node, null); + ExpandNode(node, null, inGroup: inGroup); } } @@ -172,19 +295,21 @@ private void AddNode(Node node) /// Adds the given item to the tree window. /// /// The ID of the item to be added. - /// The number of children of the item to be added. + /// Whether the item has children. /// The text of the item to be added. /// The icon of the item to be added, given as a unicode character. /// The game object of the element represented by the item. May be null. /// The graph element represented by the item. May be null. + /// The gradient to be used for the item's background. May be null. /// A function that collapses the item. - /// It takes the item that was collapsed as an argument. + /// It takes the item that was collapsed as an argument. May be null. /// A function that expands the item. /// It takes the item that was expanded and a boolean indicating whether the - /// tree should be ordered after expanding the item as arguments. - private void AddItem(string id, int children, string text, char icon, - GameObject representedGameObject, GraphElement representedGraphElement, - Action collapseItem, Action expandItem) + /// tree should be ordered after expanding the item as arguments. May be null. + private void AddItem(string id, bool hasChildren, string text, char icon, + GameObject representedGameObject = null, GraphElement representedGraphElement = null, + Color[] gradient = null, + Action collapseItem = null, Action expandItem = null) { GameObject item = PrefabInstantiator.InstantiatePrefab(treeItemPrefab, items, false); Transform background = item.transform.Find("Background"); @@ -203,11 +328,11 @@ private void AddItem(string id, int children, string text, char icon, // has an identical name, except for all slashes being replaced by backslashes. // I hope this is unlikely enough to not be a problem for now. item.name = id; - if (children <= 0) + if (!hasChildren) { expandIcon.SetActive(false); } - else if (expandedItems.Contains(id)) + else if (expandedItems.Contains(id) && expandItem != null) { // If this item was previously expanded, we need to expand it again. // The tree should not be reordered after this – this should only happen at the end of the expansion, @@ -222,8 +347,7 @@ private void AddItem(string id, int children, string text, char icon, // Colors the item according to its game object. void ColorItem() { - Color[] gradient; - if (representedGameObject != null) + if (gradient == null && representedGameObject != null) { if (representedGameObject.IsNode()) { @@ -241,7 +365,7 @@ void ColorItem() throw new ArgumentException("Item must be either a node or an edge."); } } - else + else if (gradient == null) { gradient = new[] { Color.gray, Color.gray.Darker() }; } @@ -279,12 +403,12 @@ void RegisterClickHandler() } // We want all applicable actions for the element, except ones where the element - // element is shown in the TreeView, since we are already in the TreeView. + // element is shown in the TreeWindow, since we are already in the TreeWindow. IEnumerable actions = ContextMenuAction .GetApplicableOptions(representedGraphElement, representedGameObject) - .Where(x => !x.Name.Contains("TreeView")); - actions = actions.Append(new PopupMenuAction("Hide in TreeView", () => + .Where(x => !x.Name.Contains("TreeWindow")); + actions = actions.Append(new PopupMenuAction("Hide in TreeWindow", () => { Searcher.Filter.ExcludeElements.Add(representedGraphElement); Rebuild(); @@ -313,10 +437,13 @@ void RegisterClickHandler() /// which are not included in the current filter. /// /// The nodes to be filtered. + /// The group in which the nodes are contained, if any. /// The filtered nodes with any hidden transitive children. - private IEnumerable WithHiddenChildren(IList nodes) + private IEnumerable WithHiddenChildren(IList nodes, TreeWindowGroup inGroup) { - return nodes.Where(Searcher.Filter.Includes).Concat(nodes.Where(x => !Searcher.Filter.Includes(x)).SelectMany(x => WithHiddenChildren(x.Children()))); + return nodes.Where(x => ShouldBeDisplayed(x, inGroup)) + .Concat(nodes.Where(x => !ShouldBeDisplayed(x, inGroup)) + .SelectMany(x => WithHiddenChildren(x.Children(), inGroup))); } /// @@ -324,36 +451,43 @@ private IEnumerable WithHiddenChildren(IList nodes) /// transitively including all hidden children of those nodes. /// /// The nodes to be filtered. + /// The group in which the nodes are contained, if any. /// The transitive hidden children of the given nodes. - private IEnumerable HiddenChildren(IEnumerable nodes) + private IEnumerable HiddenChildren(IEnumerable nodes, TreeWindowGroup inGroup) { - return nodes.Where(x => !Searcher.Filter.Includes(x)).SelectMany(x => HiddenChildren(x.Children()).Append(x)); + return nodes.Where(x => !ShouldBeDisplayed(x, inGroup)).SelectMany(x => HiddenChildren(x.Children(), inGroup).Append(x)); } /// /// Removes the given 's children from the tree window. /// /// The node to be removed. - private void RemoveNodeChildren(Node node) + /// The group in which the node is contained, if any. + private void RemoveNodeChildren(Node node, TreeWindowGroup inGroup = null) { - foreach ((string childID, Node child) in GetChildItems(node)) + foreach ((string childID, Node child) in GetChildItems(node, inGroup)) { - RemoveItem(childID, child, GetChildItems); + RemoveItem(childID, child, x => GetChildItems(x, inGroup)); } - return; + } - IEnumerable<(string ID, Node child)> GetChildItems(Node n) + /// + /// Returns the child items of the given along with their ID. + /// + /// The node whose child items are requested. + /// The group in which the node is contained, if any. + /// The child items of the given along with their ID. + private IEnumerable<(string ID, Node child)> GetChildItems(Node node, TreeWindowGroup inGroup = null) + { + string cleanId = ElementId(node, inGroup); + IEnumerable<(string, Node)> children = WithHiddenChildren(node.Children(), inGroup).Select(x => (ElementId(x, inGroup), x)); + foreach ((List edges, string edgesType) in RelevantEdges(node, inGroup)) { - string cleanId = CleanupID(n.ID); - IEnumerable<(string, Node)> children = WithHiddenChildren(n.Children()).Select(x => (CleanupID(x.ID), x)); - foreach ((List edges, string edgesType) in RelevantEdges(n)) - { - // We need to remove the "Outgoing" and "Incoming" buttons if they exist, along with their children. - children = children.Append((cleanId + "#" + edgesType, null)) - .Concat(edges.Select(x => ($"{cleanId}#{edgesType}#{CleanupID(x.ID)}", null))); - } - return children; + // The "Outgoing" and "Incoming" buttons if they exist, along with their children, belong here too. + children = children.Append((cleanId + "#" + edgesType, null)) + .Concat(edges.Select(x => ($"{cleanId}#{edgesType}#{CleanupID(x.ID)}", null))); } + return children; } /// @@ -364,7 +498,8 @@ private void RemoveNodeChildren(Node node) /// The initial item whose children will be removed. /// A function that returns the children, along with their ID, of an item. /// The type of the item. - private void RemoveItem(string id, T initial, Func> getChildItems) + /// The type of the children of the item. + private void RemoveItem(string id, T initial, Func> getChildItems) where V : T { GameObject item = items.Find(id)?.gameObject; if (item == null) @@ -387,7 +522,7 @@ private void RemoveItem(string id, T initial, Func from the tree window. /// /// The ID of the item to be removed. - private void RemoveItem(string id) => RemoveItem(id, null, null); + private void RemoveItem(string id) => RemoveItem(id, null, null); /// /// Expands the given . @@ -424,23 +559,25 @@ private void CollapseItem(GameObject item) /// have at least one edge. /// /// The node whose edges are requested. + /// The group in which the node is contained, if any. /// The edges of the node, grouped by their type. - private IEnumerable<(List edges, string edgesType)> RelevantEdges(Node node) + private IEnumerable<(List edges, string edgesType)> RelevantEdges(Node node, TreeWindowGroup inGroup = null) { List outgoings = Searcher.Filter.Apply(node.Outgoings).ToList(); List incomings = Searcher.Filter.Apply(node.Incomings).ToList(); // We need to lift edges of any hidden children upwards to the first visible parent, which is // this node. We then need to filter them again, since they may have been hidden by the filter. - List hiddenChildren = HiddenChildren(node.Children()).ToList(); + List hiddenChildren = HiddenChildren(node.Children(), inGroup).ToList(); List liftedOutgoings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Outgoings)).ToList(); List liftedIncomings = Searcher.Filter.Apply(hiddenChildren.SelectMany(x => x.Incomings)).ToList(); return new[] - { - (outgoings, "Outgoing"), - (incomings, "Incoming"), - (liftedOutgoings, "Lifted Outgoing"), - (liftedIncomings, "Lifted Incoming") - }.Where(x => x.Item1.Count > 0); + { + (outgoings, "Outgoing"), + (incomings, "Incoming"), + (liftedOutgoings, "Lifted Outgoing"), + (liftedIncomings, "Lifted Incoming") + }.Select(x => inGroup == null ? x : (x.Item1.Where(y => Grouper.IsRelevantFor(y, inGroup)).ToList(), x.Item2)) + .Where(x => x.Item1.Count > 0); } /// @@ -452,25 +589,26 @@ private void CollapseItem(GameObject item) /// (i.e., no item actually exists in the TreeWindow) /// only the children of the node will be added, not its connected edges. /// Whether to order the tree after expanding the node. - private void ExpandNode(Node node, GameObject item, bool orderTree = false) + /// The group in which the node is contained, if any. + private void ExpandNode(Node node, GameObject item, bool orderTree = false, TreeWindowGroup inGroup = null) { foreach (Node child in node.Children()) { - AddNode(child); + AddNode(child, inGroup); } if (item != null) { ExpandItem(item); - foreach ((List edges, string edgesType) in RelevantEdges(node)) + foreach ((List edges, string edgesType) in RelevantEdges(node, inGroup)) { AddEdgeButton(edges, edgesType); } if (orderTree) { - OrderTree(node); + OrderTree(node, inGroup: inGroup); } } return; @@ -485,39 +623,44 @@ void AddEdgeButton(ICollection edges, string edgesType) "Lifted Outgoing" => Icons.LiftedOutgoingEdge, _ => Icons.QuestionMark }; - string cleanedId = CleanupID(node.ID); + string cleanedId = ElementId(node, inGroup); string id = $"{cleanedId}#{edgesType}"; // Note that an edge may appear multiple times in the tree view, // hence we make its ID dependent on the node it is connected to, // and whether it is an incoming or outgoing edge (to cover self-loops). - AddItem(id, edges.Count, $"{edgesType} Edges", icon, - representedGameObject: null, representedGraphElement: null, - collapsedItem => + AddItem(id, edges.Count > 0, $"{edgesType} Edges", icon, + collapseItem: collapsedItem => { CollapseItem(collapsedItem); foreach (Edge edge in edges) { RemoveItem($"{id}#{CleanupID(edge.ID)}"); } - }, (expandedItem, order) => + }, + expandItem: (expandedItem, order) => { ExpandItem(expandedItem); AddEdges(id, edges); if (order) { - OrderTree(node); + OrderTree(node, inGroup: inGroup); } }); } + } - void AddEdges(string id, IEnumerable edges) + /// + /// Adds the given to the tree window. + /// + /// The ID of the TreeWindow item to which the edges belong. + /// The edges to be added. + private void AddEdges(string id, IEnumerable edges) + { + foreach (Edge edge in edges) { - foreach (Edge edge in edges) - { - GameObject edgeObject = GraphElementIDMap.Find(edge.ID); - string title = edge.ToShortString(); - AddItem($"{id}#{CleanupID(edge.ID)}", 0, title, Icons.Edge, edgeObject, edge, null, null); - } + GameObject edgeObject = GraphElementIDMap.Find(edge.ID); + string title = edge.ToShortString(); + AddItem($"{id}#{CleanupID(edge.ID)}", false, title, Icons.Edge, edgeObject, edge); } } @@ -527,10 +670,11 @@ void AddEdges(string id, IEnumerable edges) /// /// The node represented by the item. /// The item to be collapsed. - private void CollapseNode(Node node, GameObject item) + /// The group in which the node is contained, if any. + private void CollapseNode(Node node, GameObject item, TreeWindowGroup inGroup = null) { CollapseItem(item); - RemoveNodeChildren(node); + RemoveNodeChildren(node, inGroup); } /// @@ -551,8 +695,8 @@ private void SearchFor(string searchTerm) { GameObject nodeGameObject = GraphElementIDMap.Find(node.ID, mustFindElement: true); AddItem(CleanupID(node.ID), - 0, node.ToShortString(), Icons.Node, nodeGameObject, node, - null, (_, _) => RevealElement(node).Forget()); + false, node.ToShortString(), Icons.Node, nodeGameObject, node, + expandItem: (_, _) => RevealElement(node).Forget()); } items.position = items.position.WithXYZ(y: 0); @@ -568,16 +712,17 @@ private void SearchFor(string searchTerm) /// Whether to make the source or target node of the edge visible. public async UniTaskVoid RevealElement(GraphElement element, bool viaSource = false) { + TreeWindowGroup group = Grouper?.GetGroupFor(element); if (SearchField == null) { // We need to wait until the window is initialized. // This case may occur when the method is called from the outside. await UniTask.WaitUntil(() => SearchField != null); } - if (!Searcher.Filter.Includes(element)) + if (!ShouldBeDisplayed(element) || (group == null && Grouper != null && Grouper.IsActive)) { ShowNotification.Warn("Element filtered out", - "Element is not included in the current filter and thus can't be shown."); + "Element is not included in the current filter or group and thus can't be shown."); return; } SearchField.onValueChanged.RemoveListener(SearchFor); @@ -592,13 +737,13 @@ public async UniTaskVoid RevealElement(GraphElement element, bool viaSource = fa Edge edge => viaSource ? edge.Source : edge.Target, _ => throw new ArgumentOutOfRangeException(nameof(element)) }; - string transformID = CleanupID(current.ID); + string transformID = ElementId(current, group); if (element is Edge) { expandedItems.Add(transformID); transformID += viaSource ? "#Outgoing" : "#Incoming"; expandedItems.Add(transformID); - transformID += $"#{CleanupID(element.ID)}"; + transformID += $"#{ElementId(element, group)}"; } // We need to find a path from the root to the node, which we do by working our way up the hierarchy. @@ -606,7 +751,13 @@ public async UniTaskVoid RevealElement(GraphElement element, bool viaSource = fa while (current.Parent != null) { current = current.Parent; - expandedItems.Add(CleanupID(current.ID)); + expandedItems.Add(ElementId(current, group)); + } + + // Finally, if the element is in a group, we need to expand the group. + if (group != null) + { + expandedItems.Add(CleanupID(group.Text)); } // We need to wait until the transform actually exists, hence the await. @@ -652,10 +803,12 @@ protected override void StartDesktop() FilterButton = root.Find("Search/Filter").gameObject.MustGetComponent(); SortButton = root.Find("Search/Sort").gameObject.MustGetComponent(); + GroupButton = root.Find("Search/Group").gameObject.MustGetComponent(); PopupMenu.PopupMenu popupMenu = gameObject.AddComponent(); - ContextMenu = new TreeWindowContextMenu(popupMenu, Searcher, Rebuild, FilterButton, SortButton); + ContextMenu = new TreeWindowContextMenu(popupMenu, Searcher, Grouper, Rebuild, + FilterButton, SortButton, GroupButton); - AddRoots().Forget(); + Rebuild(); } /// @@ -664,6 +817,7 @@ protected override void StartDesktop() private void Rebuild() { ClearTree(); + Grouper?.RebuildCounts(); AddRoots().Forget(); } } diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs index c534aedb45..3f03577137 100644 --- a/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindow.cs @@ -53,6 +53,11 @@ public partial class TreeWindow : BaseWindow, IObserver /// private TreeWindowContextMenu ContextMenu; + /// + /// The grouper that is used to group the elements in the tree window. + /// + private TreeWindowGrouper Grouper; + /// /// The subscription to the graph observable. /// @@ -61,6 +66,7 @@ public partial class TreeWindow : BaseWindow, IObserver protected override void Start() { Searcher = new GraphSearch(Graph); + Grouper = new TreeWindowGrouper(Searcher.Filter, Graph); subscription = Graph.Subscribe(this); base.Start(); } @@ -70,29 +76,54 @@ private void OnDestroy() subscription.Dispose(); } + /// + /// Returns the roots for the tree view. + /// + /// The group to which the roots should belong. + /// The roots for the tree view. + /// + /// The roots for the tree view may differ from the roots of the graph, such as when the graph is grouped. + /// + private IList GetRoots(TreeWindowGroup inGroup = null) + { + return WithHiddenChildren(Graph.GetRoots(), inGroup).ToList(); + } + /// /// Adds the roots of the graph to the tree view. /// It may take up to a frame to add and reorder all items, hence this method is asynchronous. /// private async UniTask AddRoots() { - // We will traverse the graph and add each node to the tree view. - IList roots = WithHiddenChildren(Graph.GetRoots()).ToList(); - - if (roots.Count == 0) + if (Grouper.IsActive) { - ShowNotification.Warn("Empty graph", "Graph has no roots. TreeView will be empty."); - return; - } - - foreach (Node root in roots) - { - AddNode(root); + // Instead of the roots, we should add the categories as the first level. + foreach (TreeWindowGroup group in Grouper.AllGroups) + { + if (Grouper.MembersInGroup(group) > 0) + { + AddGroup(group); + } + } } - await UniTask.Yield(); - foreach (Node root in roots) + else { - OrderTree(root); + IList roots = GetRoots(); + if (roots.Count == 0) + { + ShowNotification.Warn("Empty graph", "Graph has no roots. TreeView will be empty."); + return; + } + + foreach (Node root in roots) + { + AddNode(root); + } + await UniTask.Yield(); + foreach (Node root in roots) + { + OrderTree(root); + } } } @@ -152,8 +183,7 @@ public void OnNext(ChangeEvent value) case GraphElementTypeEvent: case HierarchyEvent: case NodeEvent: - ClearTree(); - AddRoots().Forget(); + Rebuild(); break; } } diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs index 6027dcc16c..5e5cdd66d7 100644 --- a/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs @@ -4,9 +4,13 @@ using Michsky.UI.ModernUIPack; using SEE.DataModel.DG; using SEE.DataModel.GraphSearch; +using SEE.Game.City; +using SEE.Tools.ReflexionAnalysis; using SEE.UI.PopupMenu; using SEE.Utils; using UnityEngine; +using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException; +using State = SEE.Tools.ReflexionAnalysis.State; namespace SEE.UI.Window.TreeWindow { @@ -26,6 +30,11 @@ public class TreeWindowContextMenu /// private readonly GraphSearch Searcher; + /// + /// The grouper that is used to group the elements in the tree window. + /// + private readonly TreeWindowGrouper Grouper; + /// /// The function to call to rebuild the tree window. /// @@ -41,27 +50,39 @@ public class TreeWindowContextMenu /// private readonly ButtonManagerBasic SortButton; + /// + /// The button that opens the group menu. + /// + private readonly ButtonManagerBasic GroupButton; + /// /// Constructor. /// /// The context menu that this class manages. /// The graph search associated with the tree window. + /// The grouper that is used to group the elements in the tree window. /// The function to call to rebuild the tree window. /// The button that opens the filter menu. /// The button that opens the sort menu. - public TreeWindowContextMenu(PopupMenu.PopupMenu contextMenu, GraphSearch searcher, Action rebuild, - ButtonManagerBasic filterButton, ButtonManagerBasic sortButton) + /// The button that opens the group menu. + public TreeWindowContextMenu(PopupMenu.PopupMenu contextMenu, GraphSearch searcher, TreeWindowGrouper grouper, + Action rebuild, ButtonManagerBasic filterButton, ButtonManagerBasic sortButton, + ButtonManagerBasic groupButton) { ContextMenu = contextMenu; Searcher = searcher; + Grouper = grouper; Rebuild = rebuild; FilterButton = filterButton; SortButton = sortButton; + GroupButton = groupButton; ResetFilter(); ResetSort(); + ResetGrouping(); FilterButton.clickEvent.AddListener(ShowFilterMenu); SortButton.clickEvent.AddListener(ShowSortMenu); + GroupButton.clickEvent.AddListener(ShowGroupMenu); } /// @@ -74,7 +95,7 @@ public TreeWindowContextMenu(PopupMenu.PopupMenu contextMenu, GraphSearch search /// /// Displays the filter menu. /// - public void ShowFilterMenu() + private void ShowFilterMenu() { UpdateFilterMenuEntries(); ContextMenu.ShowWith(position: FilterButton.transform.position); @@ -91,7 +112,6 @@ private void UpdateFilterMenuEntries() // Don't include common toggles in node/edge toggles. nodeToggles.ExceptWith(commonToggles); edgeToggles.ExceptWith(commonToggles); - // TODO: Allow filtering by node type. List entries = new() { @@ -198,7 +218,7 @@ private void ResetFilter() /// /// Displays the sort menu. /// - public void ShowSortMenu() + private void ShowSortMenu() { UpdateSortMenuEntries(); ContextMenu.ShowWith(position: SortButton.transform.position); @@ -219,38 +239,49 @@ private void UpdateSortMenuEntries() }, Icons.ArrowRotateLeft, CloseAfterClick: false) }; - // TODO: Add all attributes, or only pre-selected common ones? - entries.AddRange(Searcher.Graph.AllNumericAttributes().Select(attribute => SortActionFor(attribute, numeric: true))); - entries.AddRange(Searcher.Graph.AllStringAttributes().Select(attribute => SortActionFor(attribute, numeric: false))); + if (Grouper.IsActive) + { + entries.Add(new PopupMenuHeading("Grouping is active!")); + entries.Add(new PopupMenuHeading("Items ordered by group count.")); + } + + // TODO: Any other attributes we want to sort by? Or should we just include all attributes? + entries.Add(SortActionFor("Source Name", x => x is Node node ? node.SourceName : null, false)); + entries.Add(SortActionFor("Source Line", x => x.SourceLine(), true)); + entries.Add(SortActionFor("Filename", x => x.Filename(), false)); + entries.Add(SortActionFor("Type", x => x.Type, false)); ContextMenu.ClearEntries(); ContextMenu.AddEntries(entries); } /// - /// Returns the sort action for the given . + /// Returns the sort action for the given and . /// - /// The attribute to create a sort action for. - /// Whether the attribute name is for a numeric attribute. - /// The sort action for the given . - private PopupMenuAction SortActionFor(string attribute, bool numeric) + /// The name of the sort attribute. + /// The key to sort by. + /// Whether this is for a numeric attribute. + /// The sort action for the given . + private PopupMenuAction SortActionFor(string name, Func key, bool numeric) { - return new PopupMenuAction(attribute, ToggleSortAction, - SortIcon(numeric, Searcher.Sorter.IsAttributeDescending(attribute)), + return new PopupMenuAction(name, ToggleSortAction, + SortIcon(numeric, Searcher.Sorter.IsAttributeDescending(name)), CloseAfterClick: false); void ToggleSortAction() { // Switch from ascending->descending->none->ascending. - switch (Searcher.Sorter.IsAttributeDescending(attribute)) + switch (Searcher.Sorter.IsAttributeDescending(name)) { - case null: Searcher.Sorter.SortAttributes.Add((attribute, false)); + case null: + Searcher.Sorter.AddSortAttribute(name, key, false); break; case false: - Searcher.Sorter.SortAttributes.Remove((attribute, false)); - Searcher.Sorter.SortAttributes.Add((attribute, true)); + Searcher.Sorter.RemoveSortAttribute(name); + Searcher.Sorter.AddSortAttribute(name, key, true); break; - default: Searcher.Sorter.SortAttributes.Remove((attribute, true)); + default: + Searcher.Sorter.RemoveSortAttribute(name); break; } UpdateSortMenuEntries(); @@ -283,7 +314,94 @@ private static char SortIcon(bool numeric, bool? descending) private void ResetSort() { Searcher.Sorter.Reset(); - Searcher.Sorter.SortAttributes.Add((Node.SourceNameAttribute, false)); + Searcher.Sorter.AddSortAttribute("Source Name", x => x is Node node ? node.SourceName : null, false); + } + + #endregion + + #region Group menu + + /// + /// Displays the group menu. + /// + private void ShowGroupMenu() + { + UpdateGroupMenuEntries(); + ContextMenu.ShowWith(position: GroupButton.transform.position); + } + + /// + /// Updates the group menu entries. + /// + private void UpdateGroupMenuEntries() + { + ISet currentGroups = Grouper.AllGroups.ToHashSet(); + List entries = new() + { + new PopupMenuAction("None", () => + { + ResetGrouping(); + Rebuild(); + UpdateGroupMenuEntries(); + }, Radio(!Grouper.IsActive), CloseAfterClick: true), + GroupActionFor("Reflexion State", + new TreeWindowGroupAssigment(Enum.GetValues(typeof(State)).Cast() + .ToDictionary(keySelector: x => x, + elementSelector: ReflexionStateToGroup), + element => element is Edge edge ? edge.State() : null)), + // TODO: Other groups? Maybe grouping by type would be a good idea? + }; + ContextMenu.ClearEntries(); + ContextMenu.AddEntries(entries); + return; + + // Returns the group action for the given and . + PopupMenuAction GroupActionFor(string name, ITreeWindowGroupAssigment assignment) + { + return new PopupMenuAction(name, () => + { + Grouper.Assignment = assignment; + Rebuild(); + UpdateGroupMenuEntries(); + }, Radio(currentGroups.SetEquals(assignment.AllGroups)), + CloseAfterClick: true); + } + + // Returns the group for the given . + TreeWindowGroup ReflexionStateToGroup(State? state) + { + (string text, char icon) = state switch + { + State.Divergent => ("Divergent", Icons.CircleExclamationMark), + State.Absent => ("Absent", Icons.CircleMinus), + State.Allowed => ("Allowed", Icons.CircleCheckmark), + State.Convergent => ("Convergent", Icons.CircleCheckmark), + State.ImplicitlyAllowed => ("Implicitly allowed", Icons.CircleCheckmark), + State.AllowedAbsent => ("Allowed absent", Icons.CircleCheckmark), + State.Specified => ("Specified", Icons.CircleQuestionMark), + State.Unmapped => ("Unmapped", Icons.CircleQuestionMark), + State.Undefined => ("Undefined", Icons.QuestionMark), + null => ("Unknown", Icons.QuestionMark), + _ => throw new ArgumentOutOfRangeException(nameof(state), state, null) + }; + (Color start, Color end) = ReflexionVisualization.GetEdgeGradient(state ?? State.Undefined); + return new TreeWindowGroup(text, icon, start, end); + } + } + + /// + /// Returns a radio button icon for the given . + /// + /// Whether the radio button is checked. + /// A radio button icon for the given . + private static char Radio(bool value) => value ? Icons.CheckedRadio : Icons.EmptyRadio; + + /// + /// Resets the grouping to its default state. + /// + private void ResetGrouping() + { + Grouper.Reset(); } #endregion diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindowGrouper.cs b/Assets/SEE/UI/Window/TreeWindow/TreeWindowGrouper.cs new file mode 100644 index 0000000000..4207f66f4c --- /dev/null +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindowGrouper.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SEE.DataModel.DG; +using SEE.DataModel.GraphSearch; +using SEE.Utils; +using UnityEngine; + +namespace SEE.UI.Window.TreeWindow +{ + /// + /// A group of graph elements in the tree window. + /// + /// The text to display for this group. + /// The icon to display for this group. + /// The start color of the gradient to use for this group. + /// The end color of the gradient to use for this group. + public record TreeWindowGroup(string Text, char IconGlyph, Color StartColor, Color EndColor) + { + /// + /// Returns the color gradient to use for this group. + /// + public Color[] Gradient => new[] { StartColor, EndColor }; + } + + /// + /// An assignment of graph elements to s. + /// + public interface ITreeWindowGroupAssigment + { + /// + /// Returns the group the given element belongs to. + /// + /// The element whose group shall be returned. + /// The group the given element belongs to. + public TreeWindowGroup GroupFor(GraphElement element); + + /// + /// Returns all groups that are available, ordered by the number of members (descending). + /// + public IEnumerable AllGroups + { + get; + } + + /// + /// A dummy group assignment that always returns null. + /// + /// A dummy group assignment that always returns null. + public static ITreeWindowGroupAssigment Dummy() + { + return new TreeWindowGroupAssigment(new Dictionary(), _ => null); + } + } + + /// + /// A group assignment that is based on a mapping from group identifiers to the corresponding groups. + /// + /// A mapping from group identifiers to the corresponding groups. + /// This mapping does not need to be injective. + /// A function that returns the group identifier for a given element. + /// The type of the group identifiers. + public record TreeWindowGroupAssigment( + IDictionary Groups, + Func DetermineGroup) : ITreeWindowGroupAssigment + { + public IEnumerable AllGroups => Groups.Values.Distinct(); + + public TreeWindowGroup GroupFor(GraphElement element) + { + T itsGroup = DetermineGroup(element); + if (itsGroup == null) + { + return null; + } + return Groups.TryGetValue(DetermineGroup(element), out TreeWindowGroup group) ? group : null; + } + } + + /// + /// Manages the grouping of graph elements in the tree window. + /// + public class TreeWindowGrouper + { + /// + /// The assignment of graph elements to groups. + /// Note that you will need to invoke after changing this. + /// + public ITreeWindowGroupAssigment Assignment + { + private get; + set; + } + + /// + /// The filter to use for determining which elements are included in the tree window. + /// + private readonly GraphFilter Filter; + + /// + /// The graph on which the tree window is based. + /// + private readonly Graph Graph; + + /// + /// A mapping from node IDs and groups to the number of descendants of that node + /// that are included in the tree window and belong to that group. + /// + private readonly DefaultDictionary<(string nodeId, TreeWindowGroup group), int> DescendantCounts; + + /// + /// Creates a new with the given parameters. + /// + /// The filter to use for determining which elements are included in the tree window. + /// The graph on which the tree window is based. + public TreeWindowGrouper(GraphFilter filter, Graph graph) + { + Filter = filter; + Graph = graph; + DescendantCounts = new DefaultDictionary<(string, TreeWindowGroup), int>(); + Reset(); + } + + /// + /// Resets the grouping of graph elements in the tree window. + /// + public void Reset() + { + Assignment = ITreeWindowGroupAssigment.Dummy(); + DescendantCounts.Clear(); + } + + /// + /// Returns all groups that are available, ordered by the number of members (descending). + /// + public IOrderedEnumerable AllGroups => Assignment.AllGroups.OrderByDescending(MembersInGroup); + + /// + /// Whether there is at least one group. + /// + public bool IsActive => AllGroups.Any(); + + /// + /// Returns the group for the given element. + /// + /// The element whose group shall be returned. + /// The group for the given element. + public TreeWindowGroup GetGroupFor(GraphElement element) + { + return Assignment.GroupFor(element); + } + + /// + /// Returns the number of descendants of the given that are included in the tree window + /// and belong to the given . + /// + /// The node whose descendants shall be counted. + /// The group to which the descendants shall belong. + /// The number of descendants of the given that are included in the tree window + /// and belong to the given . + public int DescendantsInGroup(Node node, TreeWindowGroup group) => DescendantCounts[(node.ID, group)]; + + /// + /// Returns the number of members of the given . + /// + /// The group whose members shall be counted. + /// The number of members of the given . + public int MembersInGroup(TreeWindowGroup group) => Graph.GetRoots().Sum(x => DescendantsInGroup(x, group)); + + /// + /// Rebuilds the descendant counts for all nodes. + /// + /// + /// This method should be called whenever the graph or its filter changes. + /// + public void RebuildCounts() + { + DescendantCounts.Clear(); + foreach (Node root in Graph.GetRoots()) + { + BuildDescendantCounts(root); + } + } + + /// + /// Returns all direct children of the given that are included in the tree window + /// and belong to the given , including any connected edges. + /// + /// The node whose children and edges shall be returned. + /// The group to which the children and edges shall belong. + /// All direct children of the given that are included in the tree window + /// and belong to the given . + public IEnumerable ChildrenInGroup(Node node, TreeWindowGroup group) + { + return node.Children().Concat(node.Edges).Where(x => Filter.Includes(x) && GetGroupFor(x) == group); + } + + /// + /// Returns whether the given is relevant for the given . + /// Specifically, this is the case if the element is included in the tree window and belongs to the group, + /// or if it is an ascendant of such an element. + /// + /// The element whose relevance shall be checked. + /// The group for which the relevance shall be checked. + /// Whether the given is relevant for the given . + public bool IsRelevantFor(GraphElement element, TreeWindowGroup group) + { + return DescendantCounts[(element.ID, group)] > 0 || (Filter.Includes(element) && GetGroupFor(element) == group); + } + + /// + /// Computes the descendant counts for the given + /// and stores them in . + /// + /// The node whose descendant counts shall be computed. + private void BuildDescendantCounts(Node node) + { + foreach (Node descendant in node.PostOrderDescendants()) + { + // Due to post-order traversal, we can assume that the counts for all children have already been computed. + foreach (TreeWindowGroup group in AllGroups) + { + // We add the number of relevant children to the sum of the counts of *all* children. + // The latter uses all children, not just the relevant ones, because we want to include + // relevant descendants of irrelevant children as well. + DescendantCounts[(descendant.ID, group)] = descendant.Children().Sum(x => DescendantCounts[(x.ID, group)]) + ChildrenInGroup(descendant, group).Count(); + } + } + } + } +} diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindowGrouper.cs.meta b/Assets/SEE/UI/Window/TreeWindow/TreeWindowGrouper.cs.meta new file mode 100644 index 0000000000..0bd3621140 --- /dev/null +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindowGrouper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 05795f4b3d7b4b4e8511e1e25d5cc3ca +timeCreated: 1701276810 \ No newline at end of file diff --git a/Assets/SEE/Utils/Icons.cs b/Assets/SEE/Utils/Icons.cs index 7fe8795450..eed21942d0 100644 --- a/Assets/SEE/Utils/Icons.cs +++ b/Assets/SEE/Utils/Icons.cs @@ -19,7 +19,6 @@ public static class Icons public const char EmptyCheckbox = '\uF0C8'; public const char CheckedCheckbox = '\uF14A'; public const char MinusCheckbox = '\uF146'; - public const char DoubleCheckmark = '\uF560'; public const char Checkmark = '\uF00C'; public const char Trash = '\uF1F8'; public const char Info = '\uF05A'; @@ -35,5 +34,11 @@ public static class Icons public const char SortAlphabeticalDown = '\uF15D'; public const char SortNumericUp = '\uF163'; public const char SortNumericDown = '\uF162'; + public const char EmptyRadio = '\uF111'; + public const char CheckedRadio = '\uF192'; + public const char CircleMinus = '\uF056'; + public const char CircleCheckmark = '\uF058'; + public const char CircleQuestionMark = '\uF059'; + public const char CircleExclamationMark = '\uF06A'; } } From 851d07f9eb96ac9c40ae61c542cc44b2a6a207dc Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 9 Dec 2023 22:53:54 +0100 Subject: [PATCH 22/30] Implement grouping by Type --- Assets/SEE/DataModel/DG/Graph.cs | 6 ++++++ Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs | 8 +++++--- Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs | 9 ++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Assets/SEE/DataModel/DG/Graph.cs b/Assets/SEE/DataModel/DG/Graph.cs index f21247f23f..f6fe23884f 100644 --- a/Assets/SEE/DataModel/DG/Graph.cs +++ b/Assets/SEE/DataModel/DG/Graph.cs @@ -494,6 +494,12 @@ public void RemoveElement(GraphElement element) /// edge types of this graph internal HashSet AllEdgeTypes() => Edges().Select(e => e.Type).ToHashSet(); + /// + /// Returns the names of all element types of this graph + /// + /// element types of this graph + internal HashSet AllElementTypes() => Elements().Select(e => e.Type).ToHashSet(); + /// /// The number of nodes of the graph. /// diff --git a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs index c56ba70bee..1720a2c7cd 100644 --- a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs @@ -265,7 +265,6 @@ private void AddNode(Node node, TreeWindowGroup inGroup = null) { string text = node.ToShortString(); string id = ElementId(node, inGroup); - Color[] gradient = null; if (inGroup != null) { // Not actually the number of direct children, but this doesn't matter, as we only @@ -277,10 +276,13 @@ private void AddNode(Node node, TreeWindowGroup inGroup = null) // TODO: In this case, are italics fine, or should we color the item gray? text = $"{text}"; } - text = $"{text} [{children}]"; + if (children > 0) + { + text = $"{text} [{children}]"; + } } AddItem(id, children > 0, text, Icons.Node, nodeGameObject, node, - gradient, collapseItem: item => CollapseNode(node, item, inGroup), + collapseItem: item => CollapseNode(node, item, inGroup), expandItem: (item, order) => ExpandNode(node, item, orderTree: order, inGroup)); } else diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs index 5e5cdd66d7..1fde04e7d9 100644 --- a/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs @@ -349,7 +349,9 @@ private void UpdateGroupMenuEntries() .ToDictionary(keySelector: x => x, elementSelector: ReflexionStateToGroup), element => element is Edge edge ? edge.State() : null)), - // TODO: Other groups? Maybe grouping by type would be a good idea? + GroupActionFor("Type", + new TreeWindowGroupAssigment(Searcher.Graph.AllElementTypes().ToDictionary(x => x, TypeToGroup), element => element.Type)), + // TODO: Any other useful groups? }; ContextMenu.ClearEntries(); ContextMenu.AddEntries(entries); @@ -387,6 +389,11 @@ TreeWindowGroup ReflexionStateToGroup(State? state) (Color start, Color end) = ReflexionVisualization.GetEdgeGradient(state ?? State.Undefined); return new TreeWindowGroup(text, icon, start, end); } + + TreeWindowGroup TypeToGroup(string type) + { + return new TreeWindowGroup(type, Icons.Info, Color.white, Color.white.Darker()); + } } /// From 7f52901d5b82a32c72a20be36ab0cd06690dd490 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 9 Dec 2023 23:02:51 +0100 Subject: [PATCH 23/30] Fix TODO messages --- Assets/SEE/UI/PopupMenu/PopupMenu.cs | 2 +- Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Assets/SEE/UI/PopupMenu/PopupMenu.cs b/Assets/SEE/UI/PopupMenu/PopupMenu.cs index ac03affff2..b2e9a08b70 100644 --- a/Assets/SEE/UI/PopupMenu/PopupMenu.cs +++ b/Assets/SEE/UI/PopupMenu/PopupMenu.cs @@ -90,7 +90,7 @@ protected override void StartDesktop() // We hide the menu by default. Menu.gameObject.SetActive(false); - // TODO: Make this scrollable once it gets too big. + // TODO (#679): Make this scrollable once it gets too big. } /// diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs index 1fde04e7d9..0b7f59c344 100644 --- a/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs @@ -245,7 +245,7 @@ private void UpdateSortMenuEntries() entries.Add(new PopupMenuHeading("Items ordered by group count.")); } - // TODO: Any other attributes we want to sort by? Or should we just include all attributes? + // TODO: Any other attributes we want to sort by? Or should we just include all attributes in the future? entries.Add(SortActionFor("Source Name", x => x is Node node ? node.SourceName : null, false)); entries.Add(SortActionFor("Source Line", x => x.SourceLine(), true)); entries.Add(SortActionFor("Filename", x => x.Filename(), false)); From 253d823f374845c094c90d0f12d71e691555d77e Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Sat, 9 Dec 2023 23:36:08 +0100 Subject: [PATCH 24/30] CI: Fix non-zero exit code for bad pattern checker --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 437097a53d..e0f4039567 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -72,7 +72,7 @@ jobs: - name: Collect bad patterns run: | if [ -n "$GITHUB_BASE_REF" ]; then - PATTERNS=$(curl -H 'Accept: application/vnd.github.v3.diff' -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' -f 'https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }}' | ./GitScripts/check_for_bad_patterns.py) + PATTERNS=$(curl -H 'Accept: application/vnd.github.v3.diff' -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' -f 'https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }}' | ./GitScripts/check_for_bad_patterns.py || true) if [ "$PATTERNS" != "[]" ]; then DELIMITER=7MApgggGyx6C0 echo "PATTERNS<<$DELIMITER" >> $GITHUB_ENV From d8e5b22d9c4525f718897e60c250e1f690ae1241 Mon Sep 17 00:00:00 2001 From: Falko Galperin Date: Wed, 13 Dec 2023 21:45:24 +0100 Subject: [PATCH 25/30] Address review comments and implement suggestions by @koschke --- Assets/SEE/DataModel/DG/Graph.cs | 6 +++--- Assets/SEE/DataModel/GraphSearch/GraphFilter.cs | 1 - Assets/SEE/DataModel/GraphSearch/GraphSearch.cs | 3 +++ Assets/SEE/DataModel/GraphSearch/GraphSorter.cs | 1 - Assets/SEE/Utils/CollectionExtensions.cs | 3 +++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Assets/SEE/DataModel/DG/Graph.cs b/Assets/SEE/DataModel/DG/Graph.cs index f6fe23884f..d40a2cc406 100644 --- a/Assets/SEE/DataModel/DG/Graph.cs +++ b/Assets/SEE/DataModel/DG/Graph.cs @@ -483,19 +483,19 @@ public void RemoveElement(GraphElement element) } /// - /// Returns the names of all node types of this graph + /// Returns the names of all node types of this graph. /// /// node types of this graph internal HashSet AllNodeTypes() => Nodes().Select(n => n.Type).ToHashSet(); /// - /// Returns the names of all edge types of this graph + /// Returns the names of all edge types of this graph. /// /// edge types of this graph internal HashSet AllEdgeTypes() => Edges().Select(e => e.Type).ToHashSet(); /// - /// Returns the names of all element types of this graph + /// Returns the names of all element types of this graph. /// /// element types of this graph internal HashSet AllElementTypes() => Elements().Select(e => e.Type).ToHashSet(); diff --git a/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs index 2ce5b8e7d8..349f94739e 100644 --- a/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs +++ b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs @@ -9,7 +9,6 @@ namespace SEE.DataModel.GraphSearch /// public record GraphFilter : IGraphModifier { - // if empty, include all. otherwise, *at least one* must be present. /// /// A set of toggle attributes of which at least one must be present in a graph element for it to be included. /// diff --git a/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs b/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs index 120935945f..1c5c29ee96 100644 --- a/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs +++ b/Assets/SEE/DataModel/GraphSearch/GraphSearch.cs @@ -39,6 +39,9 @@ public class GraphSearch : IObserver /// public GraphSorter Sorter { get; } = new(); + /// + /// Returns all graph modifiers that shall be applied to the search results. + /// private IEnumerable Modifiers => new IGraphModifier[] { Filter, Sorter }; /// diff --git a/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs b/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs index c3dc73c95c..804320dc7f 100644 --- a/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs +++ b/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs @@ -74,7 +74,6 @@ public IEnumerable Apply(IEnumerable elements) where T : GraphElement } } - public bool IsActive() => SortAttributes.Count > 0; public void Reset() => SortAttributes.Clear(); diff --git a/Assets/SEE/Utils/CollectionExtensions.cs b/Assets/SEE/Utils/CollectionExtensions.cs index 8cdcbc0f26..9f00d38161 100644 --- a/Assets/SEE/Utils/CollectionExtensions.cs +++ b/Assets/SEE/Utils/CollectionExtensions.cs @@ -4,6 +4,9 @@ namespace SEE.Utils { + /// + /// Contains utility extension methods for collections and enumerables. + /// public static class CollectionExtensions { /// From a7f230d137a9468941a8d8d8549127eee12267dd Mon Sep 17 00:00:00 2001 From: Rainer Koschke Date: Thu, 14 Dec 2023 10:28:09 +0100 Subject: [PATCH 26/30] #444 Added see-cref references to the interface method implemented. --- Assets/SEE/DataModel/GraphSearch/GraphFilter.cs | 6 ++++++ Assets/SEE/DataModel/GraphSearch/GraphSorter.cs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs index 349f94739e..4879a206bb 100644 --- a/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs +++ b/Assets/SEE/DataModel/GraphSearch/GraphFilter.cs @@ -64,9 +64,15 @@ public IEnumerable Apply(IEnumerable elements) where T : GraphElement return elements.Where(Includes); } + /// + /// Implements . + /// public bool IsActive() => !IncludeNodes || !IncludeEdges || IncludeToggleAttributes.Count > 0 || ExcludeToggleAttributes.Count > 0 || ExcludeElements.Count > 0; + /// + /// Implements . + /// public void Reset() { IncludeToggleAttributes.Clear(); diff --git a/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs b/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs index 804320dc7f..830297dda9 100644 --- a/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs +++ b/Assets/SEE/DataModel/GraphSearch/GraphSorter.cs @@ -36,6 +36,9 @@ public void RemoveSortAttribute(string attributeName) SortAttributes.RemoveAll(a => a.Name == attributeName); } + /// + /// Implements . + /// public IEnumerable Apply(IEnumerable elements) where T : GraphElement { return SortAttributes.Count == 0 @@ -74,8 +77,14 @@ public IEnumerable Apply(IEnumerable elements) where T : GraphElement } } + /// + /// Implements . + /// public bool IsActive() => SortAttributes.Count > 0; + /// + /// Implements . + /// public void Reset() => SortAttributes.Clear(); } } From bc93f49a0b7d21a7362df03f59006b3a1255c6f0 Mon Sep 17 00:00:00 2001 From: Rainer Koschke Date: Thu, 14 Dec 2023 10:29:00 +0100 Subject: [PATCH 27/30] #444 Addressed the TODOs. --- Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs | 1 - Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs index 1720a2c7cd..27266a85d4 100644 --- a/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs +++ b/Assets/SEE/UI/Window/TreeWindow/DesktopTreeWindow.cs @@ -273,7 +273,6 @@ private void AddNode(Node node, TreeWindowGroup inGroup = null) if (Grouper.GetGroupFor(node) != inGroup) { // This node is only included because it has relevant descendants. - // TODO: In this case, are italics fine, or should we color the item gray? text = $"{text}"; } if (children > 0) diff --git a/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs index 0b7f59c344..27d8478b98 100644 --- a/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs +++ b/Assets/SEE/UI/Window/TreeWindow/TreeWindowContextMenu.cs @@ -245,7 +245,8 @@ private void UpdateSortMenuEntries() entries.Add(new PopupMenuHeading("Items ordered by group count.")); } - // TODO: Any other attributes we want to sort by? Or should we just include all attributes in the future? + // These are the attributes we want to sort by for the time being. We might want to include + // all other attributes in the future, in which case the following code needs to be adapted. entries.Add(SortActionFor("Source Name", x => x is Node node ? node.SourceName : null, false)); entries.Add(SortActionFor("Source Line", x => x.SourceLine(), true)); entries.Add(SortActionFor("Filename", x => x.Filename(), false)); @@ -344,6 +345,8 @@ private void UpdateGroupMenuEntries() Rebuild(); UpdateGroupMenuEntries(); }, Radio(!Grouper.IsActive), CloseAfterClick: true), + // Here we define the criteria by which a user can group. If new grouping criteria + // arise in the future, they need to be added here. GroupActionFor("Reflexion State", new TreeWindowGroupAssigment(Enum.GetValues(typeof(State)).Cast() .ToDictionary(keySelector: x => x, @@ -351,7 +354,6 @@ private void UpdateGroupMenuEntries() element => element is Edge edge ? edge.State() : null)), GroupActionFor("Type", new TreeWindowGroupAssigment(Searcher.Graph.AllElementTypes().ToDictionary(x => x, TypeToGroup), element => element.Type)), - // TODO: Any other useful groups? }; ContextMenu.ClearEntries(); ContextMenu.AddEntries(entries); From a637be20810a53975753486a47912261fd56fa9c Mon Sep 17 00:00:00 2001 From: Rainer Koschke Date: Thu, 14 Dec 2023 11:41:32 +0100 Subject: [PATCH 28/30] The dashboard's public key has changed. Updated here accordingly. --- .../SEE/Net/Dashboard/DashboardRetriever.cs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Assets/SEE/Net/Dashboard/DashboardRetriever.cs b/Assets/SEE/Net/Dashboard/DashboardRetriever.cs index 554adae3b9..3f0ad4d587 100644 --- a/Assets/SEE/Net/Dashboard/DashboardRetriever.cs +++ b/Assets/SEE/Net/Dashboard/DashboardRetriever.cs @@ -36,18 +36,15 @@ public partial class DashboardRetriever : MonoBehaviour [EnvironmentVariable("DASHBOARD_PUBLIC_KEY")] [TextArea] [Tooltip("The public key for the X.509 certificate authority from the dashboard's certificate.")] - public string PublicKey = "3082018A0282018100D5EB3F1B3AB0ABCE827FA70BA32FCF8C834B1206464B785B7AE1" - + "3D962F19887D6733B0A555651F130BCDB04D8BF6F199EF76DB7932C001B6916D0E3F0C" - + "84C9A7DDB7BC62C93590E49C5DD97109B01B2CFAFD183A45DD8E02F86EBAFB4C74DF24" - + "449D582DFC03D8E783DA035BB2985907EC00774D748AFC9A0195E05E2B992A7877F437" - + "DB6088E2490A53D83D8F729482A383142AE5FBAFA4F2C3112E5A5C16D520ADFCF5E0F0" - + "C9FF865126AFC8B97EAEFD77CE31A431F0E66C2200CDF70BC56B478FB7B56858CE605E" - + "3313A876E4B719529DB929D9D7A966B16A9656FF639AE7382B6B7591E19D05A0B35468" - + "03007DCE8354FDFDFC0DAB4E5103C0ED67A6BFD42E810C78A649DD419A0E4C1BB15267" - + "A85DB4336D101F3799B71A654C5A8422875EE4ADCF7FD7D684D5B71AC4C0E392A533DE" - + "143AACC68CCDD77F3FB47AFCF59F058E3873FCF454CED0EF1B5DF8A18A14C4D56A4C81" - + "E6F3D3D8246BDF2E402C78AB50DCA8CC603E2681B9E28A032BFE156DDC04C266986E31" - + "10112A86CC01C5150203010001"; + public string PublicKey = "3082018A028201810084BA2FF29AB0282BEE362EA659FE9C5A90CC7C6E6AED7743847C2" + + "CE12FCAE85963CE613C4DA2B1685EB8B355A95072FF7FBC4D08D5545573A3BB8C21667D7FEE766DA410B22681AC" + + "CD028DB46CE5EAE8E9D455B350BA3E6867480F2990799F3D3130F87EAAC7B8AD4226634A28C99922C43C8CC8984" + + "EA92FB25FDC7510AEFA7793AF0042DF30498BC1F0507613ABB1A30F3954FF21A6631EB83A40A4DD7B5EB89B5CA1" + + "B2982605453A0B1B2A8D4064917E8C6582A6DBE4A032E3EB84B9B2A4500C8ADEA236787CBC709E30D893D08DC8B" + + "824D309C0AA4CFE976A4645B8EBDA797E618A5A22A775078C84BC536486D6F45E0659A1FEB03FEEC6944393025E" + + "1EFF3948E42ADF05A803770D327F85B900B1D7ADFB9B7BBE4E01E23E7653578B28917D4683DA4EC538758EF6A02" + + "532CD74CAA6B644D0FFCE5AA096A6CFE76E5C0BD30DA19DF4187E1E358077CD771B0B470441A934B8F991FE78EA" + + "A90B35AD7CF600B573272D9E2888DB0BB34FA374F29FDA92E2D3E2D36A1E1A1E40164648D63F930203010001"; /// /// The URL to the Axivion Dashboard, up to the project name. @@ -373,7 +370,13 @@ protected override bool ValidateCertificate(byte[] certificateData) // https://docs.unity3d.com/ScriptReference/Networking.CertificateHandler.ValidateCertificate.html X509Certificate2 certificate = new(certificateData); string certPublicKey = certificate.GetPublicKeyString(); - return certPublicKey?.Equals(acceptKey) ?? false; + + bool result = certPublicKey?.Equals(acceptKey) ?? false; + if (!result) + { + Debug.LogError($"Public keys do not match:\nOurs: {acceptKey}\nServer's: {certPublicKey}\n"); + } + return result; } } } From 8580f1e42389d83b31c11dc0c0a4ebe908e76d25 Mon Sep 17 00:00:00 2001 From: Rainer Koschke Date: Thu, 14 Dec 2023 11:41:51 +0100 Subject: [PATCH 29/30] The dashboard's public key has changed. Updated here accordingly. --- Assets/Scenes/SEEWorld.unity | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Assets/Scenes/SEEWorld.unity b/Assets/Scenes/SEEWorld.unity index 8b020abe0f..f9037b8ee3 100644 --- a/Assets/Scenes/SEEWorld.unity +++ b/Assets/Scenes/SEEWorld.unity @@ -1372,6 +1372,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 175ce339391b42b1b31b4db72cbdb2ae, type: 3} m_Name: m_EditorClassIdentifier: + PKey: 3082018A028201810084BA2FF29AB0282BEE362EA659FE9C5A90CC7C6E6AED7743847C2CE12FCAE85963CE613C4DA2B1685EB8B355A95072FF7FBC4D08D5545573A3BB8C21667D7FEE766DA410B22681ACCD028DB46CE5EAE8E9D455B350BA3E6867480F2990799F3D3130F87EAAC7B8AD4226634A28C99922C43C8CC8984EA92FB25FDC7510AEFA7793AF0042DF30498BC1F0507613ABB1A30F3954FF21A6631EB83A40A4DD7B5EB89B5CA1B2982605453A0B1B2A8D4064917E8C6582A6DBE4A032E3EB84B9B2A4500C8ADEA236787CBC709E30D893D08DC8B824D309C0AA4CFE976A4645B8EBDA797E618A5A22A775078C84BC536486D6F45E0659A1FEB03FEEC6944393025E1EFF3948E42ADF05A803770D327F85B900B1D7ADFB9B7BBE4E01E23E7653578B28917D4683DA4EC538758EF6A02532CD74CAA6B644D0FFCE5AA096A6CFE76E5C0BD30DA19DF4187E1E358077CD771B0B470441A934B8F991FE78EAA90B35AD7CF600B573272D9E2888DB0BB34FA374F29FDA92E2D3E2D36A1E1A1E40164648D63F930203010001 PublicKey: 3082018A0282018100D5EB3F1B3AB0ABCE827FA70BA32FCF8C834B1206464B785B7AE13D962F19887D6733B0A555651F130BCDB04D8BF6F199EF76DB7932C001B6916D0E3F0C84C9A7DDB7BC62C93590E49C5DD97109B01B2CFAFD183A45DD8E02F86EBAFB4C74DF24449D582DFC03D8E783DA035BB2985907EC00774D748AFC9A0195E05E2B992A7877F437DB6088E2490A53D83D8F729482A383142AE5FBAFA4F2C3112E5A5C16D520ADFCF5E0F0C9FF865126AFC8B97EAEFD77CE31A431F0E66C2200CDF70BC56B478FB7B56858CE605E3313A876E4B719529DB929D9D7A966B16A9656FF639AE7382B6B7591E19D05A0B3546803007DCE8354FDFDFC0DAB4E5103C0ED67A6BFD42E810C78A649DD419A0E4C1BB15267A85DB4336D101F3799B71A654C5A8422875EE4ADCF7FD7D684D5B71AC4C0E392A533DE143AACC68CCDD77F3FB47AFCF59F058E3873FCF454CED0EF1B5DF8A18A14C4D56A4C81E6F3D3D8246BDF2E402C78AB50DCA8CC603E2681B9E28A032BFE156DDC04C266986E3110112A86CC01C5150203010001 BaseUrl: https://stvr2.informatik.uni-bremen.de:9443/axivion/projects/SEE/ Token: 0.000000000000Q.2Jb6PIgB1pk4g8ss-DtnfdtDp0xlcugYQHFRcRvBRH4 @@ -2723,8 +2724,12 @@ MonoBehaviour: Data: 8|System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Boolean, mscorlib]], mscorlib - Name: comparer - Entry: 9 - Data: 4 + Entry: 7 + Data: 9|System.Collections.Generic.GenericEqualityComparer`1[[System.String, + mscorlib]], mscorlib + - Name: + Entry: 8 + Data: - Name: Entry: 12 Data: 0 @@ -2736,11 +2741,11 @@ MonoBehaviour: Data: - Name: InnerNodeLayout Entry: 7 - Data: 9|System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[SEE.Game.City.NodeLayoutKind, + Data: 10|System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[SEE.Game.City.NodeLayoutKind, SEE]], mscorlib - Name: comparer Entry: 9 - Data: 4 + Data: 9 - Name: Entry: 12 Data: 0 @@ -2752,11 +2757,11 @@ MonoBehaviour: Data: - Name: InnerNodeShape Entry: 7 - Data: 10|System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[SEE.Game.City.NodeShapes, + Data: 11|System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[SEE.Game.City.NodeShapes, SEE]], mscorlib - Name: comparer Entry: 9 - Data: 4 + Data: 9 - Name: Entry: 12 Data: 0 @@ -2768,11 +2773,11 @@ MonoBehaviour: Data: - Name: LoadedForNodeTypes Entry: 7 - Data: 11|System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Boolean, + Data: 12|System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Boolean, mscorlib]], mscorlib - Name: comparer Entry: 9 - Data: 4 + Data: 9 - Name: Entry: 12 Data: 0 From 11dd077e56a548988d3306b5964abc8034fc3ed1 Mon Sep 17 00:00:00 2001 From: Rainer Koschke Date: Thu, 14 Dec 2023 11:42:09 +0100 Subject: [PATCH 30/30] The dashboard's version has changed. Updated here accordingly. --- Assets/SEE/Net/Dashboard/DashboardVersion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/SEE/Net/Dashboard/DashboardVersion.cs b/Assets/SEE/Net/Dashboard/DashboardVersion.cs index 2f5677b385..2a44c280d4 100644 --- a/Assets/SEE/Net/Dashboard/DashboardVersion.cs +++ b/Assets/SEE/Net/Dashboard/DashboardVersion.cs @@ -35,7 +35,7 @@ namespace SEE.Net.Dashboard /// Latest supported version of the Axivion Dashboard. /// Should be updated when new (supported and tested) versions come out. /// - public static readonly DashboardVersion SupportedVersion = new(7, 6, 3, 12797); + public static readonly DashboardVersion SupportedVersion = new(7, 7, 1, 13682); /// /// Represents the difference of another version in comparison to this one.