From 8de7fdeff6ce1348d04c6101f6d420f25b53f800 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 01:05:33 -0400 Subject: [PATCH 01/12] TypePicker Enter Now selects top --- src/XTMF2.GUI/Views/TypePickerDialog.axaml | 1 + src/XTMF2.GUI/Views/TypePickerDialog.axaml.cs | 10 +++ .../Headless/TypePickerDialogTests.cs | 61 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/XTMF2.GUI.Tests/Headless/TypePickerDialogTests.cs diff --git a/src/XTMF2.GUI/Views/TypePickerDialog.axaml b/src/XTMF2.GUI/Views/TypePickerDialog.axaml index cdf1751..652a7ab 100644 --- a/src/XTMF2.GUI/Views/TypePickerDialog.axaml +++ b/src/XTMF2.GUI/Views/TypePickerDialog.axaml @@ -31,6 +31,7 @@ diff --git a/src/XTMF2.GUI/Views/TypePickerDialog.axaml.cs b/src/XTMF2.GUI/Views/TypePickerDialog.axaml.cs index b6191dc..3ede997 100644 --- a/src/XTMF2.GUI/Views/TypePickerDialog.axaml.cs +++ b/src/XTMF2.GUI/Views/TypePickerDialog.axaml.cs @@ -385,6 +385,16 @@ private void UpdateFilter() /// Pick button for the second type-argument slot (the ReturnType for IFunction). private async void PickTypeArg1_Click(object? sender, RoutedEventArgs e) => await PickTypeArgAsync(1); + private void FilterBox_EnterPressed(object? sender, RoutedEventArgs e) + { + if (FilteredTypes.Count > 0) + { + TypeListBox.SelectedIndex = 0; + } + + OK_Click(null, e); + } + private void OK_Click(object? sender, RoutedEventArgs e) { if (!CanOK) return; diff --git a/tests/XTMF2.GUI.Tests/Headless/TypePickerDialogTests.cs b/tests/XTMF2.GUI.Tests/Headless/TypePickerDialogTests.cs new file mode 100644 index 0000000..7fd581a --- /dev/null +++ b/tests/XTMF2.GUI.Tests/Headless/TypePickerDialogTests.cs @@ -0,0 +1,61 @@ +/* + Copyright 2026 University of Toronto + + This file is part of XTMF2. + + XTMF2 is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + XTMF2 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with XTMF2. If not, see . +*/ + +using System; +using System.Collections.ObjectModel; +using Avalonia.Controls; +using Avalonia.Headless; +using Avalonia.Interactivity; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using XTMF2.GUI.Controls; +using XTMF2.GUI.Views; + +namespace XTMF2.GUI.Tests.Headless; + +[TestClass] +public class TypePickerDialogTests +{ + private HeadlessUnitTestSession Session => HeadlessAppLifetime.HeadlessSession!; + + [TestMethod] + public void FilterBoxEnter_SelectsFirstFilteredTypeAndConfirmsDialog() + { + Session.Dispatch(() => + { + var types = new ReadOnlyObservableCollection(new ObservableCollection + { + typeof(TypePickerDialogTests), + typeof(SearchBox) + }); + + var dialog = new TypePickerDialog(types) + { + FilterText = nameof(SearchBox) + }; + + var filterBox = dialog.FindControl("FilterBox"); + Assert.IsNotNull(filterBox); + + filterBox.RaiseEvent(new RoutedEventArgs(SearchBox.EnterPressedEvent)); + + Assert.IsFalse(dialog.WasCancelled); + Assert.AreEqual(typeof(SearchBox), dialog.SelectedType); + }, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); + } +} \ No newline at end of file From ff4750473d61d2d6f64b37437f5764e01f5b3127 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 01:10:35 -0400 Subject: [PATCH 02/12] Added keyboard shortcuts for adding model system canvas elements --- .../ModelSystemCanvas.ContextMenu.cs | 8 ++-- .../ModelSystemCanvas.Input.cs | 46 +++++++++++++++++++ .../ModelSystemCanvasHeadlessTests.cs | 41 +++++++++++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ContextMenu.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ContextMenu.cs index 94a8a8a..e5963fd 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ContextMenu.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ContextMenu.cs @@ -304,19 +304,19 @@ private void ShowContextMenu(ICanvasElement? element, LinkViewModel? link) addStartItem.Click += (_, _) => _vm.AddStartAt(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(addStartItem); - var addModuleItem = new MenuItem { Header = "Add Module…" }; + var addModuleItem = new MenuItem { Header = "Add Module…\tCtrl+M" }; addModuleItem.Click += (_, _) => _ = _vm.AddModuleAtAsync(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(addModuleItem); - var addCommentItem = new MenuItem { Header = "Add Comment" }; + var addCommentItem = new MenuItem { Header = "Add Comment\tCtrl+N" }; addCommentItem.Click += (_, _) => _vm.AddCommentBlockAt(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(addCommentItem); - var addFtItem = new MenuItem { Header = "Add Function Template…" }; + var addFtItem = new MenuItem { Header = "Add Function Template…\tCtrl+T" }; addFtItem.Click += (_, _) => _ = _vm.AddFunctionTemplateAtAsync(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(addFtItem); - var addFiItem = new MenuItem { Header = "Add Function Instance…" }; + var addFiItem = new MenuItem { Header = "Add Function Instance…\tCtrl+I" }; addFiItem.Click += (_, _) => _ = _vm.AddFunctionInstanceAtAsync(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(addFiItem); diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs index 7ff1941..cbee1d6 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs @@ -77,6 +77,10 @@ protected override void OnKeyDown(KeyEventArgs e) ClearMultiSelection(); e.Handled = true; } + else if (TryHandleAddShortcut(e)) + { + e.Handled = true; + } else if ((e.Key is Key.Return or Key.Enter) && (e.KeyModifiers & KeyModifiers.Control) != 0) { if (_vm.SelectedElement is FunctionTemplateViewModel ftvm) @@ -236,6 +240,48 @@ or FunctionTemplateViewModel or FunctionInstanceViewModel } base.OnKeyDown(e); } + + private bool TryHandleAddShortcut(KeyEventArgs e) + { + if (_vm is null + || _editingParamNode is not null + || _editingNameElement is not null + || _editingCommentBlock is not null + || _editingCommentHeaderBlock is not null + || (e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) != KeyModifiers.Control) + { + return false; + } + + var spawnPt = GetKeyboardSpawnPoint(); + switch (e.Key) + { + case Key.M: + _ = _vm.AddModuleAtAsync(spawnPt.X, spawnPt.Y); + return true; + case Key.I: + _ = _vm.AddFunctionInstanceAtAsync(spawnPt.X, spawnPt.Y); + return true; + case Key.T: + _ = _vm.AddFunctionTemplateAtAsync(spawnPt.X, spawnPt.Y); + return true; + case Key.N: + _vm.AddCommentBlockAt(spawnPt.X, spawnPt.Y); + return true; + default: + return false; + } + } + + private Point GetKeyboardSpawnPoint() + { + var sv = GetScrollViewer(); + double viewportWidth = sv?.Viewport.Width ?? Bounds.Width; + double viewportHeight = sv?.Viewport.Height ?? Bounds.Height; + double x = ((sv?.Offset.X ?? 0) + viewportWidth / 2.0) / _scale; + double y = ((sv?.Offset.Y ?? 0) + viewportHeight / 2.0) / _scale; + return new Point(x, y); + } protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { diff --git a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs index 5b919d3..9336de5 100644 --- a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs +++ b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs @@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License using Avalonia.Headless; using Avalonia.Controls; +using Avalonia.Input; using Microsoft.VisualStudio.TestTools.UnitTesting; using XTMF2.GUI.Controls; using XTMF2.GUI.Tests.Modules; @@ -71,6 +72,46 @@ public void ModelSystemCanvas_DataContext_DefaultsToNull() }, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); } + [TestMethod] + public void ModelSystemCanvas_AddShortcuts_CreateCommentAndFunctionTemplate() + { + TestGuiHelper.RunInModelSystemContext( + nameof(ModelSystemCanvas_AddShortcuts_CreateCommentAndFunctionTemplate), + (user, projectSession, msSession) => + { + using var vm = new ModelSystemEditorViewModel(msSession, user, runController: null); + + Session.Dispatch(() => + { + var canvas = new ModelSystemCanvas { DataContext = vm }; + canvas.Measure(new Avalonia.Size(800, 600)); + canvas.Arrange(new Avalonia.Rect(0, 0, 800, 600)); + + var handleAddShortcut = typeof(ModelSystemCanvas).GetMethod( + "TryHandleAddShortcut", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(handleAddShortcut); + + var commentShortcut = new KeyEventArgs + { + Key = Key.N, + KeyModifiers = KeyModifiers.Control + }; + var templateShortcut = new KeyEventArgs + { + Key = Key.T, + KeyModifiers = KeyModifiers.Control + }; + + Assert.IsTrue((bool)handleAddShortcut!.Invoke(canvas, new object[] { commentShortcut })!); + Assert.IsTrue((bool)handleAddShortcut.Invoke(canvas, new object[] { templateShortcut })!); + + Assert.HasCount(1, vm.CommentBlocks); + Assert.HasCount(1, vm.FunctionTemplates); + }, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); + }); + } + [TestMethod] public void ModelSystemCanvas_ResolveFilePathTargetNode_TargetsParameterNodeFromHook() { From 429208c01dd4457bbfa81fe63883c571dc006c66 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 01:15:05 -0400 Subject: [PATCH 03/12] Update context menu to have the keyboard shortcuts aligned on the right --- .../ModelSystemCanvas.ContextMenu.cs | 52 +++++++++++++++---- .../ModelSystemCanvasHeadlessTests.cs | 32 ++++++++++++ 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ContextMenu.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ContextMenu.cs index e5963fd..792dbe9 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ContextMenu.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ContextMenu.cs @@ -19,7 +19,9 @@ You should have received a copy of the GNU General Public License using System; using System.Collections.Generic; using System.Linq; +using Avalonia; using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.Styling; using XTMF2.GUI.ViewModels; using XTMF2.ModelSystemConstruct; @@ -101,6 +103,34 @@ private void ClearElementMultiSelectionOnly() _ => null, }; + private static Grid CreateShortcutMenuHeader(string description, string shortcut) + { + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,Auto"), + MinWidth = 280 + }; + + var descriptionText = new TextBlock + { + Text = description + }; + + var shortcutText = new TextBlock + { + Text = shortcut, + Margin = new Thickness(24, 0, 0, 0), + HorizontalAlignment = HorizontalAlignment.Right, + Opacity = 0.65 + }; + + Grid.SetColumn(descriptionText, 0); + Grid.SetColumn(shortcutText, 1); + grid.Children.Add(descriptionText); + grid.Children.Add(shortcutText); + return grid; + } + private List GetDisableTargetsForClickedLink(LinkViewModel clickedLink) { if (_multiLinkSelection.Count <= 1 || !_multiLinkSelection.Contains(clickedLink.UnderlyingLink)) @@ -304,19 +334,19 @@ private void ShowContextMenu(ICanvasElement? element, LinkViewModel? link) addStartItem.Click += (_, _) => _vm.AddStartAt(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(addStartItem); - var addModuleItem = new MenuItem { Header = "Add Module…\tCtrl+M" }; + var addModuleItem = new MenuItem { Header = CreateShortcutMenuHeader("Add Module…", "Ctrl+M") }; addModuleItem.Click += (_, _) => _ = _vm.AddModuleAtAsync(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(addModuleItem); - var addCommentItem = new MenuItem { Header = "Add Comment\tCtrl+N" }; + var addCommentItem = new MenuItem { Header = CreateShortcutMenuHeader("Add Comment", "Ctrl+N") }; addCommentItem.Click += (_, _) => _vm.AddCommentBlockAt(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(addCommentItem); - var addFtItem = new MenuItem { Header = "Add Function Template…\tCtrl+T" }; + var addFtItem = new MenuItem { Header = CreateShortcutMenuHeader("Add Function Template…", "Ctrl+T") }; addFtItem.Click += (_, _) => _ = _vm.AddFunctionTemplateAtAsync(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(addFtItem); - var addFiItem = new MenuItem { Header = "Add Function Instance…\tCtrl+I" }; + var addFiItem = new MenuItem { Header = CreateShortcutMenuHeader("Add Function Instance…", "Ctrl+I") }; addFiItem.Click += (_, _) => _ = _vm.AddFunctionInstanceAtAsync(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(addFiItem); @@ -330,7 +360,7 @@ private void ShowContextMenu(ICanvasElement? element, LinkViewModel? link) // ── Paste (always available; reads system clipboard at click-time) ──── bgMenu.Items.Add(new Separator()); - var pasteItem = new MenuItem { Header = "Paste\tCtrl+V" }; + var pasteItem = new MenuItem { Header = CreateShortcutMenuHeader("Paste", "Ctrl+V") }; pasteItem.Click += (_, _) => _ = PasteElementsAsync(spawnPt.X, spawnPt.Y); bgMenu.Items.Add(pasteItem); @@ -855,12 +885,12 @@ private void ShowContextMenu(ICanvasElement? element, LinkViewModel? link) el is NodeViewModel envm && !envm.IsParameterNode || el is FunctionInstanceViewModel); - string extractHeader = extractCount > 1 - ? $"Extract {extractCount} Elements to Function Template…\tCtrl+Shift+M" - : "Extract to Function Template…\tCtrl+Shift+M"; + string extractDescription = extractCount > 1 + ? $"Extract {extractCount} Elements to Function Template…" + : "Extract to Function Template…"; var capturedExtractElements = selElements; - var extractItem = new MenuItem { Header = extractHeader }; + var extractItem = new MenuItem { Header = CreateShortcutMenuHeader(extractDescription, "Ctrl+Shift+M") }; extractItem.Click += (_, _) => _ = vm.ExtractSelectionToFunctionTemplateAsync(capturedExtractElements); menu.Items.Add(new Separator()); menu.Items.Add(extractItem); @@ -1071,9 +1101,9 @@ el is NodeViewModel envm && !envm.IsParameterNode _ => false, }) : 1; - string copyHeader = copyCount > 1 ? $"Copy {copyCount} Elements\tCtrl+C" : "Copy\tCtrl+C"; + string copyDescription = copyCount > 1 ? $"Copy {copyCount} Elements" : "Copy"; - var copyItem = new MenuItem { Header = copyHeader }; + var copyItem = new MenuItem { Header = CreateShortcutMenuHeader(copyDescription, "Ctrl+C") }; copyItem.Click += (_, _) => { // Narrow selection to just this element when it isn't already multi-selected. diff --git a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs index 9336de5..af10193 100644 --- a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs +++ b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs @@ -112,6 +112,38 @@ public void ModelSystemCanvas_AddShortcuts_CreateCommentAndFunctionTemplate() }); } + [TestMethod] + public void ModelSystemCanvas_ContextMenu_ShortcutHeadersUseTwoColumnGrid() + { + TestGuiHelper.RunInModelSystemContext( + nameof(ModelSystemCanvas_ContextMenu_ShortcutHeadersUseTwoColumnGrid), + (user, projectSession, msSession) => + { + using var vm = new ModelSystemEditorViewModel(msSession, user, runController: null); + + Session.Dispatch(() => + { + var canvas = new ModelSystemCanvas { DataContext = vm }; + var showMenu = typeof(ModelSystemCanvas).GetMethod( + "ShowContextMenu", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(showMenu); + + showMenu!.Invoke(canvas, new object?[] { null, null }); + + var menu = canvas.ContextMenu; + Assert.IsNotNull(menu); + + var addModuleItem = menu!.Items.OfType().FirstOrDefault(item => + item.Header is Grid grid + && grid.Children.OfType().Any(text => text.Text == "Add Module…") + && grid.Children.OfType().Any(text => text.Text == "Ctrl+M")); + + Assert.IsNotNull(addModuleItem); + }, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); + }); + } + [TestMethod] public void ModelSystemCanvas_ResolveFilePathTargetNode_TargetsParameterNodeFromHook() { From d075bab82d08b55c5e9be90ccd2c8a5258c5cc69 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 01:58:32 -0400 Subject: [PATCH 04/12] Dragging a link to the edge now scrolls --- src/XTMF2.GUI/Controls/ModelSystemCanvas.cs | 18 ++++++++- .../ModelSystemCanvas.Input.cs | 1 + .../ModelSystemCanvasHeadlessTests.cs | 40 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs index 09bf991..8eaefb8 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs @@ -997,7 +997,7 @@ private void TryApplyZoomText() /// /// Scrolls the host when the pointer is within - /// pixels of any viewport edge during an element drag. + /// pixels of any viewport edge during a drag. /// The scroll delta is proportional to how far inside the zone the cursor sits, /// reaching at the very edge. /// Also starts/stops the continuous based on whether @@ -1035,6 +1035,7 @@ private void TryAutoScrollForDrag(Point svPos) sv.Offset = new Vector( Math.Max(0, sv.Offset.X + dx), Math.Max(0, sv.Offset.Y + dy)); + RefreshPendingLinkCurrentPos(svPos); if (!_autoScrollTimer.IsEnabled) _autoScrollTimer.Start(); } @@ -1052,7 +1053,7 @@ private void TryAutoScrollForDrag(Point svPos) /// private void OnAutoScrollTick(object? sender, EventArgs e) { - if (_dragging is null) + if (_dragging is null && _linkOrigin is null) { _autoScrollTimer.Stop(); return; @@ -1060,6 +1061,19 @@ private void OnAutoScrollTick(object? sender, EventArgs e) TryAutoScrollForDrag(_lastSvPos); } + private void RefreshPendingLinkCurrentPos(Point svPos) + { + if (_linkOrigin is null) return; + + var sv = GetScrollViewer(); + if (sv is null) return; + + _linkCurrentPos = new Point( + (sv.Offset.X + svPos.X) / _scale, + (sv.Offset.Y + svPos.Y) / _scale); + InvalidateVisual(); + } + private void OnZoomTextBoxKeyDown(object? sender, KeyEventArgs e) { if (e.Key is Key.Enter or Key.Return) diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs index cbee1d6..a344c77 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs @@ -772,6 +772,7 @@ protected override void OnPointerMoved(PointerEventArgs e) if (_linkOrigin is not null) { _linkCurrentPos = mpos; + TryAutoScrollForDrag(svPos); InvalidateVisual(); e.Handled = true; return; diff --git a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs index af10193..2957973 100644 --- a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs +++ b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs @@ -20,6 +20,7 @@ You should have received a copy of the GNU General Public License using Avalonia.Headless; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; using XTMF2.GUI.Controls; using XTMF2.GUI.Tests.Modules; @@ -144,6 +145,45 @@ item.Header is Grid grid }); } + [TestMethod] + public void ModelSystemCanvas_AutoScrollTimer_ContinuesForPendingLinkDrag() + { + TestGuiHelper.RunInModelSystemContext( + nameof(ModelSystemCanvas_AutoScrollTimer_ContinuesForPendingLinkDrag), + (user, projectSession, msSession) => + { + using var vm = new ModelSystemEditorViewModel(msSession, user, runController: null); + vm.AddStartAt(20, 20); + + Session.Dispatch(() => + { + var canvas = new ModelSystemCanvas { DataContext = vm }; + var linkOriginField = typeof(ModelSystemCanvas).GetField( + "_linkOrigin", + BindingFlags.Instance | BindingFlags.NonPublic); + var timerField = typeof(ModelSystemCanvas).GetField( + "_autoScrollTimer", + BindingFlags.Instance | BindingFlags.NonPublic); + var autoScrollTick = typeof(ModelSystemCanvas).GetMethod( + "OnAutoScrollTick", + BindingFlags.Instance | BindingFlags.NonPublic); + + Assert.IsNotNull(linkOriginField); + Assert.IsNotNull(timerField); + Assert.IsNotNull(autoScrollTick); + + linkOriginField!.SetValue(canvas, vm.Starts[0]); + var timer = (DispatcherTimer)timerField!.GetValue(canvas)!; + timer.Start(); + + autoScrollTick!.Invoke(canvas, new object?[] { null, EventArgs.Empty }); + + Assert.IsTrue(timer.IsEnabled); + timer.Stop(); + }, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); + }); + } + [TestMethod] public void ModelSystemCanvas_ResolveFilePathTargetNode_TargetsParameterNodeFromHook() { From 2985b97514d4d29ccf37f7b2a9c4f6733d05f00c Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 02:02:35 -0400 Subject: [PATCH 05/12] Stop Canvas Navigation while editing --- .../ModelSystemCanvas.Input.cs | 17 +++-- .../ModelSystemCanvasHeadlessTests.cs | 65 +++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs index a344c77..6c75cb7 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs @@ -186,7 +186,7 @@ or FunctionTemplateViewModel or FunctionInstanceViewModel } } else if (e.Key == Key.Up - && _editingParamNode is null + && !IsParameterOrCommentEditing && (e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) == 0) { // Arrow Up: navigate to nearest element above current selection. @@ -194,20 +194,24 @@ or FunctionTemplateViewModel or FunctionInstanceViewModel e.Handled = true; } else if (e.Key == Key.Down - && _editingParamNode is null + && !IsParameterOrCommentEditing && (e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) == 0) { // Arrow Down: navigate to nearest element below current selection. NavigateToNextElement(NavigationDirection.Down); e.Handled = true; } - else if (e.Key == Key.Left && (e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) == 0) + else if (e.Key == Key.Left + && !IsParameterOrCommentEditing + && (e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) == 0) { // Arrow Left: navigate to nearest element to the left of current selection. NavigateToNextElement(NavigationDirection.Left); e.Handled = true; } - else if (e.Key == Key.Right && (e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) == 0) + else if (e.Key == Key.Right + && !IsParameterOrCommentEditing + && (e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift)) == 0) { // Arrow Right: navigate to nearest element to the right of current selection. NavigateToNextElement(NavigationDirection.Right); @@ -241,6 +245,11 @@ or FunctionTemplateViewModel or FunctionInstanceViewModel base.OnKeyDown(e); } + private bool IsParameterOrCommentEditing => + _editingParamNode is not null + || _editingCommentBlock is not null + || _editingCommentHeaderBlock is not null; + private bool TryHandleAddShortcut(KeyEventArgs e) { if (_vm is null diff --git a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs index 2957973..c0514a3 100644 --- a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs +++ b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs @@ -145,6 +145,71 @@ item.Header is Grid grid }); } + [TestMethod] + public void ModelSystemCanvas_ArrowKeys_DoNotNavigateWhileEditingParameterOrComment() + { + TestGuiHelper.RunInModelSystemContext( + nameof(ModelSystemCanvas_ArrowKeys_DoNotNavigateWhileEditingParameterOrComment), + (user, projectSession, msSession) => + { + var boundary = msSession.ModelSystem.GlobalBoundary; + Assert.IsTrue(msSession.AddNode( + user, + boundary, + "ParameterNode", + typeof(XTMF2.RuntimeModules.BasicParameter), + new Rectangle(20, 20, 160, 60), + out var parameterNode, + out var parameterError), + parameterError?.Message); + Assert.IsNotNull(parameterNode); + Assert.IsTrue(msSession.AddNode( + user, + boundary, + "RightNode", + typeof(SimpleGuiTestModule), + new Rectangle(260, 20, 160, 60), + out _, + out var rightNodeError), + rightNodeError?.Message); + + using var vm = new ModelSystemEditorViewModel(msSession, user, runController: null); + vm.AddCommentBlockAt(20, 160); + + Session.Dispatch(() => + { + var canvas = new ModelSystemCanvas { DataContext = vm }; + var beginParamEdit = typeof(ModelSystemCanvas).GetMethod( + "BeginParamEdit", + BindingFlags.Instance | BindingFlags.NonPublic); + var beginCommentEdit = typeof(ModelSystemCanvas).GetMethod( + "BeginCommentEdit", + BindingFlags.Instance | BindingFlags.NonPublic); + var onKeyDown = typeof(ModelSystemCanvas).GetMethod( + "OnKeyDown", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(beginParamEdit); + Assert.IsNotNull(beginCommentEdit); + Assert.IsNotNull(onKeyDown); + + var parameterVm = vm.Nodes.FirstOrDefault(n => ReferenceEquals(n.UnderlyingNode, parameterNode)); + var commentVm = vm.CommentBlocks.FirstOrDefault(); + Assert.IsNotNull(parameterVm); + Assert.IsNotNull(commentVm); + + vm.SelectElementCommand.Execute(commentVm); + beginCommentEdit!.Invoke(canvas, new object[] { commentVm! }); + onKeyDown!.Invoke(canvas, new object[] { new KeyEventArgs { Key = Key.Right } }); + Assert.AreSame(commentVm, vm.SelectedElement); + + vm.SelectElementCommand.Execute(parameterVm); + beginParamEdit!.Invoke(canvas, new object?[] { parameterVm!, -1.0, -1.0, -1.0, null, null }); + onKeyDown.Invoke(canvas, new object[] { new KeyEventArgs { Key = Key.Right } }); + Assert.AreSame(parameterVm, vm.SelectedElement); + }, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); + }); + } + [TestMethod] public void ModelSystemCanvas_AutoScrollTimer_ContinuesForPendingLinkDrag() { From c5f8a5056d8a3752b6a1ef6182b3d6427e98b373 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 02:17:27 -0400 Subject: [PATCH 06/12] Copy/Paste now links to external elements if pasted within the same model system --- .../ModelSystemCanvas.CopyPaste.cs | 64 ++++-- .../ViewModels/CanvasClipboardPayload.cs | 22 +- .../ViewModels/ModelSystemEditorViewModel.cs | 90 +++++++- .../Modules/SimpleGuiTestModule.cs | 11 + ...odelSystemEditorViewModelClipboardTests.cs | 201 ++++++++++++++++++ 5 files changed, 361 insertions(+), 27 deletions(-) diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.CopyPaste.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.CopyPaste.cs index ea8c451..bd78a57 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.CopyPaste.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.CopyPaste.cs @@ -72,7 +72,8 @@ private CanvasElementDto BuildNodeDto(NodeViewModel nvm) TypeName: node.Type?.AssemblyQualifiedName, ParameterValue: paramValue, IsScriptedParam: isScriptedParam, - InlinedChildren: inlined); + InlinedChildren: inlined, + OriginalId: node.Id); } /// @@ -159,6 +160,7 @@ private async Task CopySelectedElementsAsync() break; case FunctionInstanceViewModel fi: + nodeToDtoIndex[fi.UnderlyingInstance] = dtos.Count; _vm.TryExportFunctionTemplateSnapshot(fi.UnderlyingInstance.Template, out var instanceTemplateSnapshot); dto = new CanvasElementDto( CanvasElementKind.FunctionInstance, @@ -166,7 +168,8 @@ private async Task CopySelectedElementsAsync() (float)fi.Width, (float)fi.Height, Name: fi.Name, TemplateName: fi.TemplateName, - EmbeddedTemplateSnapshot: instanceTemplateSnapshot); + EmbeddedTemplateSnapshot: instanceTemplateSnapshot, + OriginalId: fi.UnderlyingInstance.Id); break; case GhostNodeViewModel ghost: @@ -209,22 +212,22 @@ private async Task CopySelectedElementsAsync() } } - // Second pass: detect links where both origin and destination are in the copied set - // and record them as cross-node links on the origin's DTO. - if (nodeToDtoIndex.Count > 1 && _vm is not null) + // Second pass: record outgoing links from copied node-like origins. On paste, + // destinations are resolved first to pasted nodes by GUID, then to existing nodes + // in the target model system by the same original GUID. + if (nodeToDtoIndex.Count > 0 && _vm is not null) { foreach (var lvm in _vm.Links) { - if (lvm.Origin is not NodeViewModel originNvm) continue; - if (!nodeToDtoIndex.TryGetValue(originNvm.UnderlyingNode, out var originIdx)) continue; + if (!TryGetCopyableNode(lvm.Origin, out var originNode, out _)) continue; + if (!nodeToDtoIndex.TryGetValue(originNode, out var originIdx)) continue; - if (lvm.Destination is not NodeViewModel destNvm) continue; - if (destNvm.IsInlined) continue; - if (!nodeToDtoIndex.ContainsKey(destNvm.UnderlyingNode)) continue; + if (!TryGetLinkDestinationForRenderedBranch(lvm, out var destNode)) continue; + bool destIsInlined = lvm.Destination is NodeViewModel { IsInlined: true }; + if (destIsInlined) continue; - // Both ends are in the selection — record a cross-link. var hookName = lvm.UnderlyingLink.OriginHook.Name; - var destName = destNvm.UnderlyingNode.Name; + var destName = destNode.Name; var origDto = dtos[originIdx]; var crossLinks = origDto.CrossLinks ?? new System.Collections.Generic.List(); if (origDto.CrossLinks is null) @@ -232,7 +235,7 @@ private async Task CopySelectedElementsAsync() origDto = origDto with { CrossLinks = crossLinks }; dtos[originIdx] = origDto; } - crossLinks.Add(new CrossNodeLinkDto(hookName, destName)); + crossLinks.Add(new CrossNodeLinkDto(hookName, destName, destNode.Id)); } } @@ -261,4 +264,39 @@ private async Task PasteElementsAsync(double anchorX, double anchorY) await _vm.PasteElementsAsync(payload, anchorX, anchorY); } + private static bool TryGetCopyableNode(ICanvasElement? element, out Node node, out bool isInlined) + { + switch (element) + { + case NodeViewModel nvm: + node = nvm.UnderlyingNode; + isInlined = nvm.IsInlined; + return true; + case FunctionInstanceViewModel fivm: + node = fivm.UnderlyingInstance; + isInlined = false; + return true; + default: + node = null!; + isInlined = false; + return false; + } + } + + private static bool TryGetLinkDestinationForRenderedBranch(LinkViewModel link, out Node destination) + { + switch (link.UnderlyingLink) + { + case SingleLink singleLink: + destination = singleLink.Destination; + return true; + case MultiLink multiLink when link.DestinationIndex >= 0 && link.DestinationIndex < multiLink.Destinations.Count: + destination = multiLink.Destinations[link.DestinationIndex]; + return true; + default: + destination = null!; + return false; + } + } + } diff --git a/src/XTMF2.GUI/ViewModels/CanvasClipboardPayload.cs b/src/XTMF2.GUI/ViewModels/CanvasClipboardPayload.cs index 5099425..1dd20dd 100644 --- a/src/XTMF2.GUI/ViewModels/CanvasClipboardPayload.cs +++ b/src/XTMF2.GUI/ViewModels/CanvasClipboardPayload.cs @@ -54,6 +54,10 @@ internal sealed record CanvasClipboardPayload( /// All element types share this record; unused fields are omitted from JSON. /// /// Discriminator — one of the constants. +/// +/// Stable ID of the copied model node, when the element represents a node-like object. +/// Used to reconnect pasted origins to pasted or already-existing link destinations. +/// /// /// Display name of the element. For , /// this is treated as a legacy fallback for older payloads. @@ -122,22 +126,28 @@ internal sealed record CanvasElementDto( [property: JsonPropertyName("crossLinks")] List? CrossLinks = null, [property: JsonPropertyName("isTemplateCompanion")] bool IsTemplateCompanion = false, [property: JsonPropertyName("body")] string? CommentBody = null, - [property: JsonPropertyName("commentHeader")] string? CommentHeader = null + [property: JsonPropertyName("commentHeader")] string? CommentHeader = null, + [property: JsonPropertyName("originalId")] Guid? OriginalId = null ); /// -/// A link from one copied to another node that -/// was in the same copy selection. Stored per-origin so it can be recreated on paste -/// once all nodes have been constructed. +/// A link from one copied node-like element to another node-like element. Stored per-origin +/// so it can be recreated on paste once all pasted elements have been constructed, or +/// reconnected to an existing destination in the target model system by destination ID. /// /// The name of the hook on the origin node. /// /// The of the destination node within the same -/// . +/// . Used as a legacy fallback for older payloads. +/// +/// +/// Stable ID of the original destination node. Used to find either the pasted destination +/// from the same payload, or an already-existing destination in the target model system. /// internal sealed record CrossNodeLinkDto( [property: JsonPropertyName("hookName")] string HookName, - [property: JsonPropertyName("destName")] string DestName); + [property: JsonPropertyName("destName")] string? DestName = null, + [property: JsonPropertyName("destId")] Guid? DestId = null); /// /// A single function-parameter slot captured from a . diff --git a/src/XTMF2.GUI/ViewModels/ModelSystemEditorViewModel.cs b/src/XTMF2.GUI/ViewModels/ModelSystemEditorViewModel.cs index b42da35..5b00e0f 100644 --- a/src/XTMF2.GUI/ViewModels/ModelSystemEditorViewModel.cs +++ b/src/XTMF2.GUI/ViewModels/ModelSystemEditorViewModel.cs @@ -4069,7 +4069,7 @@ internal async Task PasteElementsAsync(CanvasClipboardPayload payload, double an pastedTemplatesBySnapshot[element.EmbeddedTemplateSnapshot] = pastedTemplate; } - // Pass 1: create all nodes (without linking inlined children yet). + // Pass 1: create all node-like elements (without linking inlined children yet). var createdNodes = new List<(CanvasElementDto Element, Node Node)>(); Node? firstNode = null; foreach (var element in payload.Elements) @@ -4090,7 +4090,9 @@ internal async Task PasteElementsAsync(CanvasClipboardPayload payload, double an break; case CanvasElementKind.FunctionInstance: - PasteFunctionInstance(element, dx, dy, pastedTemplatesBySnapshot); + var pastedInstance = PasteFunctionInstance(element, dx, dy, pastedTemplatesBySnapshot); + if (pastedInstance is not null) + createdNodes.Add((element, pastedInstance)); break; case CanvasElementKind.GhostNode: @@ -4105,17 +4107,21 @@ internal async Task PasteElementsAsync(CanvasClipboardPayload payload, double an foreach (var (element, node) in createdNodes) PasteNodeInlinedChildren(element, node); - // Pass 3: restore links between pasted nodes (cross-node links). - if (createdNodes.Count > 1) + // Pass 3: restore links from pasted origins to pasted destinations, or to + // existing destination nodes in the target model system matched by GUID. + if (createdNodes.Count > 0) { var nameToNode = createdNodes .Where(e => !string.IsNullOrWhiteSpace(e.Element.Name)) .ToDictionary(e => e.Element.Name!, e => e.Node); + var originalIdToPastedNode = createdNodes + .Where(e => e.Element.OriginalId.HasValue) + .ToDictionary(e => e.Element.OriginalId!.Value, e => e.Node); foreach (var (element, originNode) in createdNodes) { foreach (var crossLink in element.CrossLinks ?? []) { - if (!nameToNode.TryGetValue(crossLink.DestName, out var destNode)) continue; + if (!TryResolvePastedLinkDestination(crossLink, originalIdToPastedNode, nameToNode, out var destNode)) continue; var hook = originNode.Hooks?.FirstOrDefault(h => h.Name == crossLink.HookName); if (hook is null) continue; Session.AddLink(User, originNode, hook, destNode, out _, out _); @@ -4135,6 +4141,72 @@ internal async Task PasteElementsAsync(CanvasClipboardPayload payload, double an } } + private bool TryResolvePastedLinkDestination( + CrossNodeLinkDto crossLink, + IReadOnlyDictionary originalIdToPastedNode, + IReadOnlyDictionary nameToPastedNode, + [NotNullWhen(true)] out Node? destination) + { + if (crossLink.DestId is Guid destId) + { + if (originalIdToPastedNode.TryGetValue(destId, out destination)) + return true; + + if (TryFindNodeById(destId, Session.ModelSystem.GlobalBoundary, out destination)) + return true; + } + + if (!string.IsNullOrWhiteSpace(crossLink.DestName) + && nameToPastedNode.TryGetValue(crossLink.DestName, out destination)) + { + return true; + } + + destination = null; + return false; + } + + private static bool TryFindNodeById(Guid id, Boundary root, [NotNullWhen(true)] out Node? node) + { + var queue = new Queue(); + queue.Enqueue(root); + while (queue.Count > 0) + { + var boundary = queue.Dequeue(); + foreach (var module in boundary.Modules) + { + if (module.Id == id) + { + node = module; + return true; + } + } + foreach (var start in boundary.Starts) + { + if (start.Id == id) + { + node = start; + return true; + } + } + foreach (var instance in boundary.FunctionInstances) + { + if (instance.Id == id) + { + node = instance; + return true; + } + } + foreach (var template in boundary.FunctionTemplates) + queue.Enqueue(template.InternalModules); + foreach (var child in boundary.Boundaries) + queue.Enqueue(child); + } + + node = null; + return false; + } + internal bool TryExportFunctionTemplateSnapshot(FunctionTemplate template, out string? snapshot) { snapshot = null; @@ -4293,14 +4365,14 @@ private void PasteCommentBlockEntry(CanvasElementDto element, float dx, float dy return ft; } - private void PasteFunctionInstance( + private FunctionInstance? PasteFunctionInstance( CanvasElementDto element, float dx, float dy, IReadOnlyDictionary pastedTemplatesBySnapshot) { var template = ResolveTemplateForPastedFunctionInstance(element, dx, dy, pastedTemplatesBySnapshot); - if (template is null) return; + if (template is null) return null; // Generate a unique name if needed. string baseName = string.IsNullOrWhiteSpace(element.Name) ? "Function Instance" : element.Name; @@ -4313,7 +4385,9 @@ private void PasteFunctionInstance( float h = element.H > 0 ? element.H : 70f; var loc = new Rectangle(element.X + dx, element.Y + dy, w, h); - Session.AddFunctionInstance(User, _currentBoundary, template, name, loc, out _, out _); + return Session.AddFunctionInstance(User, _currentBoundary, template, name, loc, out var instance, out _) + ? instance + : null; } private FunctionTemplate? ResolveTemplateForPastedFunctionInstance( diff --git a/tests/XTMF2.GUI.Tests/Modules/SimpleGuiTestModule.cs b/tests/XTMF2.GUI.Tests/Modules/SimpleGuiTestModule.cs index bdcdac9..2c9aa70 100644 --- a/tests/XTMF2.GUI.Tests/Modules/SimpleGuiTestModule.cs +++ b/tests/XTMF2.GUI.Tests/Modules/SimpleGuiTestModule.cs @@ -26,3 +26,14 @@ public sealed class SimpleGuiTestModule : BaseFunction { public override string Invoke() => "GUI Test"; } + +[Module(Name = "Linked GUI Test Module", + DocumentationLink = "http://example.com", + Description = "A minimal module with an outgoing hook used in GUI unit tests.")] +public sealed class LinkedGuiTestModule : BaseFunction +{ + [SubModule(Name = "Child", Description = "A linked child module", Required = false, Index = 0)] + public SimpleGuiTestModule? Child { get; set; } + + public override string Invoke() => Child?.Invoke() ?? string.Empty; +} diff --git a/tests/XTMF2.GUI.Tests/ViewModels/ModelSystemEditorViewModelClipboardTests.cs b/tests/XTMF2.GUI.Tests/ViewModels/ModelSystemEditorViewModelClipboardTests.cs index 531a6d9..080194f 100644 --- a/tests/XTMF2.GUI.Tests/ViewModels/ModelSystemEditorViewModelClipboardTests.cs +++ b/tests/XTMF2.GUI.Tests/ViewModels/ModelSystemEditorViewModelClipboardTests.cs @@ -18,7 +18,10 @@ You should have received a copy of the GNU General Public License */ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Linq; +using System.Reflection; using XTMF2.Editing; +using XTMF2.GUI.Controls; +using XTMF2.GUI.Tests.Modules; using XTMF2.GUI.ViewModels; using XTMF2.ModelSystemConstruct; using XTMF2.RuntimeModules; @@ -194,6 +197,204 @@ public void PasteElementsAsync_CommentBlock_LegacyNameFallbackStillWorks() }); } + [TestMethod] + public void PasteElementsAsync_CopiedOriginLinksToExistingDestinationById() + { + TestGuiHelper.RunInModelSystemContext( + nameof(PasteElementsAsync_CopiedOriginLinksToExistingDestinationById), + (user, _, msSession) => + { + var boundary = msSession.ModelSystem.GlobalBoundary; + Assert.IsTrue(msSession.AddNode( + user, + boundary, + "Origin", + typeof(LinkedGuiTestModule), + new Rectangle(20, 20, 160, 60), + out var origin, + out var originError), + originError?.Message); + Assert.IsNotNull(origin); + + Assert.IsTrue(msSession.AddNode( + user, + boundary, + "Destination", + typeof(SimpleGuiTestModule), + new Rectangle(260, 20, 160, 60), + out var destination, + out var destinationError), + destinationError?.Message); + Assert.IsNotNull(destination); + + var hook = origin!.Hooks.First(h => h.Name == "Child"); + Assert.IsTrue(msSession.AddLink(user, origin, hook, destination!, out var addedLink, out var linkError), + linkError?.Message); + Assert.IsNotNull(addedLink); + + using var vmEditor = new ModelSystemEditorViewModel(msSession, user, runController: null); + var payload = new CanvasClipboardPayload( + Source: "XTMF2Canvas", + Version: 1, + Elements: + [ + new CanvasElementDto( + Kind: CanvasElementKind.Node, + X: 20f, + Y: 20f, + W: 160f, + H: 60f, + Name: "Origin", + TypeName: typeof(LinkedGuiTestModule).AssemblyQualifiedName, + OriginalId: origin.Id, + CrossLinks: + [ + new CrossNodeLinkDto("Child", "Destination", destination!.Id) + ]) + ]); + + vmEditor.PasteElementsAsync(payload, anchorX: 400, anchorY: 20) + .GetAwaiter().GetResult(); + + var pastedOrigin = boundary.Modules.Single(n => n.Name == "Origin" && n.Id != origin.Id); + var pastedLink = boundary.Links.OfType().SingleOrDefault(l => + ReferenceEquals(l.Origin, pastedOrigin) + && ReferenceEquals(l.Destination, destination)); + + Assert.IsNotNull(pastedLink); + }); + } + + [TestMethod] + public void PasteElementsAsync_CopiedOriginLinksToExistingDestinationByIdInAnotherBoundary() + { + TestGuiHelper.RunInModelSystemContext( + nameof(PasteElementsAsync_CopiedOriginLinksToExistingDestinationByIdInAnotherBoundary), + (user, _, msSession) => + { + var boundary = msSession.ModelSystem.GlobalBoundary; + Assert.IsTrue(msSession.AddBoundary(user, boundary, "Other", out var otherBoundary, out var boundaryError), + boundaryError?.Message); + Assert.IsNotNull(otherBoundary); + + Assert.IsTrue(msSession.AddNode( + user, + boundary, + "Origin", + typeof(LinkedGuiTestModule), + new Rectangle(20, 20, 160, 60), + out var origin, + out var originError), + originError?.Message); + Assert.IsNotNull(origin); + + Assert.IsTrue(msSession.AddNode( + user, + otherBoundary!, + "Destination", + typeof(SimpleGuiTestModule), + new Rectangle(260, 20, 160, 60), + out var destination, + out var destinationError), + destinationError?.Message); + Assert.IsNotNull(destination); + + var hook = origin!.Hooks.First(h => h.Name == "Child"); + Assert.IsTrue(msSession.AddLink(user, origin, hook, destination!, out var addedLink, out var linkError), + linkError?.Message); + Assert.IsNotNull(addedLink); + + using var vmEditor = new ModelSystemEditorViewModel(msSession, user, runController: null); + var payload = new CanvasClipboardPayload( + Source: "XTMF2Canvas", + Version: 1, + Elements: + [ + new CanvasElementDto( + Kind: CanvasElementKind.Node, + X: 20f, + Y: 20f, + W: 160f, + H: 60f, + Name: "Origin", + TypeName: typeof(LinkedGuiTestModule).AssemblyQualifiedName, + OriginalId: origin.Id, + CrossLinks: + [ + new CrossNodeLinkDto("Child", "Destination", destination!.Id) + ]) + ]); + + vmEditor.PasteElementsAsync(payload, anchorX: 400, anchorY: 20) + .GetAwaiter().GetResult(); + + var pastedOrigin = boundary.Modules.Single(n => n.Name == "Origin" && n.Id != origin.Id); + var pastedLink = boundary.Links.OfType().SingleOrDefault(l => + ReferenceEquals(l.Origin, pastedOrigin) + && ReferenceEquals(l.Destination, destination)); + + Assert.IsNotNull(pastedLink); + Assert.IsTrue(vmEditor.Links.Any(lvm => ReferenceEquals(lvm.UnderlyingLink, pastedLink)), + "The pasted cross-boundary link should be represented in the current canvas view."); + }); + } + + [TestMethod] + public void CopyMetadata_CrossBoundaryLinkNeedsUnderlyingDestinationWhenCanvasDestinationIsNotVisible() + { + TestGuiHelper.RunInModelSystemContext( + nameof(CopyMetadata_CrossBoundaryLinkNeedsUnderlyingDestinationWhenCanvasDestinationIsNotVisible), + (user, _, msSession) => + { + var boundary = msSession.ModelSystem.GlobalBoundary; + Assert.IsTrue(msSession.AddBoundary(user, boundary, "Other", out var otherBoundary, out var boundaryError), + boundaryError?.Message); + Assert.IsNotNull(otherBoundary); + + Assert.IsTrue(msSession.AddNode( + user, + boundary, + "Origin", + typeof(LinkedGuiTestModule), + new Rectangle(20, 20, 160, 60), + out var origin, + out var originError), + originError?.Message); + Assert.IsNotNull(origin); + + Assert.IsTrue(msSession.AddNode( + user, + otherBoundary!, + "Destination", + typeof(SimpleGuiTestModule), + new Rectangle(260, 20, 160, 60), + out var destination, + out var destinationError), + destinationError?.Message); + Assert.IsNotNull(destination); + + var hook = origin!.Hooks.First(h => h.Name == "Child"); + Assert.IsTrue(msSession.AddLink(user, origin, hook, destination!, out var addedLink, out var linkError), + linkError?.Message); + Assert.IsNotNull(addedLink); + + using var vmEditor = new ModelSystemEditorViewModel(msSession, user, runController: null); + var renderedLink = vmEditor.Links.Single(lvm => ReferenceEquals(lvm.UnderlyingLink, addedLink)); + + Assert.IsNull(renderedLink.Destination, + "The cross-boundary destination is not represented by a visible canvas element in the current boundary."); + + var destinationResolver = typeof(ModelSystemCanvas).GetMethod( + "TryGetLinkDestinationForRenderedBranch", + BindingFlags.Static | BindingFlags.NonPublic); + Assert.IsNotNull(destinationResolver); + + object?[] args = [renderedLink, null]; + Assert.IsTrue((bool)destinationResolver!.Invoke(null, args)!); + Assert.AreSame(destination, args[1]); + }); + } + [TestMethod] public void PasteElementsAsync_CommentBlock_WorksInInnerBoundary() { From ec21c608be52255fd05fe898c658f629b3372183 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 02:25:07 -0400 Subject: [PATCH 07/12] Scroll Canvas when dragging a selection --- src/XTMF2.GUI/Controls/ModelSystemCanvas.cs | 16 +++++++++- .../ModelSystemCanvas.Input.cs | 1 + .../ModelSystemCanvasHeadlessTests.cs | 31 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs index 8eaefb8..325b4be 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs @@ -1036,6 +1036,7 @@ private void TryAutoScrollForDrag(Point svPos) Math.Max(0, sv.Offset.X + dx), Math.Max(0, sv.Offset.Y + dy)); RefreshPendingLinkCurrentPos(svPos); + RefreshSelectionRectCurrentPos(svPos); if (!_autoScrollTimer.IsEnabled) _autoScrollTimer.Start(); } @@ -1053,7 +1054,7 @@ private void TryAutoScrollForDrag(Point svPos) /// private void OnAutoScrollTick(object? sender, EventArgs e) { - if (_dragging is null && _linkOrigin is null) + if (_dragging is null && _linkOrigin is null && _selRectStart is null) { _autoScrollTimer.Stop(); return; @@ -1074,6 +1075,19 @@ private void RefreshPendingLinkCurrentPos(Point svPos) InvalidateVisual(); } + private void RefreshSelectionRectCurrentPos(Point svPos) + { + if (_selRectStart is null) return; + + var sv = GetScrollViewer(); + if (sv is null) return; + + _selRectCurrent = new Point( + (sv.Offset.X + svPos.X) / _scale, + (sv.Offset.Y + svPos.Y) / _scale); + InvalidateVisual(); + } + private void OnZoomTextBoxKeyDown(object? sender, KeyEventArgs e) { if (e.Key is Key.Enter or Key.Return) diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs index 6c75cb7..8c43b48 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs @@ -828,6 +828,7 @@ protected override void OnPointerMoved(PointerEventArgs e) if (_selRectStart is not null) { _selRectCurrent = mpos; + TryAutoScrollForDrag(svPos); InvalidateVisual(); e.Handled = true; return; diff --git a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs index c0514a3..d691705 100644 --- a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs +++ b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs @@ -249,6 +249,37 @@ public void ModelSystemCanvas_AutoScrollTimer_ContinuesForPendingLinkDrag() }); } + [TestMethod] + public void ModelSystemCanvas_AutoScrollTimer_ContinuesForRubberBandSelection() + { + Session.Dispatch(() => + { + var canvas = new ModelSystemCanvas(); + var selectionStartField = typeof(ModelSystemCanvas).GetField( + "_selRectStart", + BindingFlags.Instance | BindingFlags.NonPublic); + var timerField = typeof(ModelSystemCanvas).GetField( + "_autoScrollTimer", + BindingFlags.Instance | BindingFlags.NonPublic); + var autoScrollTick = typeof(ModelSystemCanvas).GetMethod( + "OnAutoScrollTick", + BindingFlags.Instance | BindingFlags.NonPublic); + + Assert.IsNotNull(selectionStartField); + Assert.IsNotNull(timerField); + Assert.IsNotNull(autoScrollTick); + + selectionStartField!.SetValue(canvas, new Avalonia.Point(20, 20)); + var timer = (DispatcherTimer)timerField!.GetValue(canvas)!; + timer.Start(); + + autoScrollTick!.Invoke(canvas, new object?[] { null, EventArgs.Empty }); + + Assert.IsTrue(timer.IsEnabled); + timer.Stop(); + }, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); + } + [TestMethod] public void ModelSystemCanvas_ResolveFilePathTargetNode_TargetsParameterNodeFromHook() { From 7b7d461cf9617670bc38621b6a32a0f446316b04 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 02:30:09 -0400 Subject: [PATCH 08/12] Fix parameter edit textbox position when option parameter is not visable. --- .../ModelSystemCanvas.Input.cs | 34 +++++++++- .../ModelSystemCanvasHeadlessTests.cs | 64 +++++++++++++++++++ .../Modules/SimpleGuiTestModule.cs | 14 ++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs index 8c43b48..21c9878 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs @@ -1542,7 +1542,7 @@ private bool NavigateToNextParameter(bool backward) return (nodeVm.X, nodeVm.Y + NodeHeaderHeight, NodeRenderWidth(nodeVm)); } - var nodeHooks = nodeVm.UnderlyingNode.Hooks; + var nodeHooks = GetVisibleHooksForNode(nodeVm); if (nodeHooks is null) return null; int hookIdx = -1; @@ -1627,6 +1627,38 @@ private struct ParameterHookInfo public NodeViewModel? InlinedParam { get; set; } } + private IReadOnlyList? GetVisibleHooksForNode(NodeViewModel nodeVm) + { + if (_nodeVisibleHooks.TryGetValue(nodeVm, out var visibleHooks)) + { + return visibleHooks; + } + + var nodeHooks = nodeVm.UnderlyingNode.Hooks; + if (nodeHooks is null) return null; + if (_vm?.ShowAllHooks == true || nodeVm.ShowHooks) + { + return nodeHooks; + } + + var connected = new HashSet(); + if (_vm is not null) + { + foreach (var link in _vm.Links) + { + if (ReferenceEquals(link.Origin, nodeVm)) + { + connected.Add(link.UnderlyingLink.OriginHook); + } + } + } + + return [.. nodeHooks.Where(h => + h.Cardinality == HookCardinality.Single || + h.Cardinality == HookCardinality.AtLeastOne || + connected.Contains(h))]; + } + /// /// Gets all editable parameter targets on the given element, in order. /// Includes direct value-row editing for parameter nodes and inlined parameters for hooks. diff --git a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs index d691705..74bae87 100644 --- a/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs +++ b/tests/XTMF2.GUI.Tests/Headless/ModelSystemCanvasHeadlessTests.cs @@ -210,6 +210,70 @@ public void ModelSystemCanvas_ArrowKeys_DoNotNavigateWhileEditingParameterOrComm }); } + [TestMethod] + public void ModelSystemCanvas_ParameterTabPosition_IgnoresHiddenOptionalHooks() + { + TestGuiHelper.RunInModelSystemContext( + nameof(ModelSystemCanvas_ParameterTabPosition_IgnoresHiddenOptionalHooks), + (user, projectSession, msSession) => + { + var boundary = msSession.ModelSystem.GlobalBoundary; + Assert.IsTrue(msSession.AddNode( + user, + boundary, + "Origin", + typeof(OptionalThenParameterGuiTestModule), + new Rectangle(20, 40, 160, 60), + out var origin, + out var originError), + originError?.Message); + Assert.IsNotNull(origin); + + Assert.IsTrue(msSession.AddNode( + user, + boundary, + "Editable Value", + typeof(XTMF2.RuntimeModules.BasicParameter), + Rectangle.Hidden, + out var parameter, + out var parameterError), + parameterError?.Message); + Assert.IsNotNull(parameter); + + var editableHook = origin!.Hooks.First(h => h.Name == "Editable Value"); + Assert.IsTrue(msSession.AddLink(user, origin, editableHook, parameter!, out var link, out var linkError), + linkError?.Message); + Assert.IsNotNull(link); + + using var vm = new ModelSystemEditorViewModel(msSession, user, runController: null); + + Session.Dispatch(() => + { + var canvas = new ModelSystemCanvas { DataContext = vm }; + var buildCache = typeof(ModelSystemCanvas).GetMethod( + "BuildHookAnchorCache", + BindingFlags.Instance | BindingFlags.NonPublic); + var calculateRow = typeof(ModelSystemCanvas).GetMethod( + "CalculateParameterRowPosition", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(buildCache); + Assert.IsNotNull(calculateRow); + + buildCache!.Invoke(canvas, []); + + var originVm = vm.Nodes.FirstOrDefault(n => ReferenceEquals(n.UnderlyingNode, origin)); + Assert.IsNotNull(originVm); + + var result = calculateRow!.Invoke(canvas, [originVm!, editableHook]); + Assert.IsNotNull(result); + + var row = ((double X, double Y, double W))result!; + Assert.AreEqual(originVm!.Y + 28.0, row.Y, 0.0001, + "The editor should align to the first visible hook row, not the raw hook index after a hidden optional hook."); + }, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); + }); + } + [TestMethod] public void ModelSystemCanvas_AutoScrollTimer_ContinuesForPendingLinkDrag() { diff --git a/tests/XTMF2.GUI.Tests/Modules/SimpleGuiTestModule.cs b/tests/XTMF2.GUI.Tests/Modules/SimpleGuiTestModule.cs index 2c9aa70..76d99f3 100644 --- a/tests/XTMF2.GUI.Tests/Modules/SimpleGuiTestModule.cs +++ b/tests/XTMF2.GUI.Tests/Modules/SimpleGuiTestModule.cs @@ -37,3 +37,17 @@ public sealed class LinkedGuiTestModule : BaseFunction public override string Invoke() => Child?.Invoke() ?? string.Empty; } + +[Module(Name = "Optional Then Parameter GUI Test Module", + DocumentationLink = "http://example.com", + Description = "A module whose hidden optional hook precedes an inlined parameter hook.")] +public sealed class OptionalThenParameterGuiTestModule : BaseFunction +{ + [SubModule(Name = "Optional Child", Description = "An unselected optional hook", Required = false, Index = 0)] + public SimpleGuiTestModule? OptionalChild { get; set; } + + [Parameter(Name = "Editable Value", Description = "An editable parameter hook", Required = false, Index = 1, DefaultValue = "")] + public IFunction? EditableValue { get; set; } + + public override string Invoke() => EditableValue?.Invoke() ?? string.Empty; +} From 65a775751e624a21a81cbda1939bf2050b8288a2 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 16:36:19 -0400 Subject: [PATCH 09/12] Added search icon for navagating nodes in the model system canvas --- .../Views/ModelSystemEditorView.axaml | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/XTMF2.GUI/Views/ModelSystemEditorView.axaml b/src/XTMF2.GUI/Views/ModelSystemEditorView.axaml index 6c116d5..4e3f789 100644 --- a/src/XTMF2.GUI/Views/ModelSystemEditorView.axaml +++ b/src/XTMF2.GUI/Views/ModelSystemEditorView.axaml @@ -98,15 +98,33 @@ - + + + + + + + + From aba9b2b0d8270c0d3b1749a110723f0e9fe44120 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 18:55:44 -0400 Subject: [PATCH 10/12] Updated namespace syntax for RuntimeModules --- src/XTMF2/RuntimeModules/BasicEvent.cs | 87 ++++---- src/XTMF2/RuntimeModules/BasicParameter.cs | 19 +- src/XTMF2/RuntimeModules/Cache.cs | 99 +++++---- src/XTMF2/RuntimeModules/CombineContext.cs | 147 +++++++------ src/XTMF2/RuntimeModules/DirectoryPath.cs | 29 ++- src/XTMF2/RuntimeModules/Execute.cs | 119 ++++++----- .../ExecuteActionsThenFunction.cs | 5 +- src/XTMF2/RuntimeModules/Fail.cs | 119 ++++++----- src/XTMF2/RuntimeModules/If.cs | 4 +- src/XTMF2/RuntimeModules/Ignore.cs | 4 +- src/XTMF2/RuntimeModules/Log.cs | 103 +++++----- .../RuntimeModules/OpenReadStreamFromFile.cs | 71 ++++--- .../OpenReadStreamFromMemoryPipe.cs | 23 +-- .../RuntimeModules/OpenWriteStreamFromFile.cs | 55 +++-- .../OpenWriteStreamFromMemoryPipe.cs | 23 +-- src/XTMF2/RuntimeModules/ReportInvocation.cs | 4 +- src/XTMF2/RuntimeModules/ScriptedParameter.cs | 91 ++++----- src/XTMF2/RuntimeModules/SetableParameter.cs | 23 +-- src/XTMF2/RuntimeModules/StartModule.cs | 4 +- src/XTMF2/RuntimeModules/StepUp.cs | 63 +++--- src/XTMF2/RuntimeModules/WriteToLog.cs | 193 +++++++++--------- 21 files changed, 629 insertions(+), 656 deletions(-) diff --git a/src/XTMF2/RuntimeModules/BasicEvent.cs b/src/XTMF2/RuntimeModules/BasicEvent.cs index ce9abab..8e92cff 100644 --- a/src/XTMF2/RuntimeModules/BasicEvent.cs +++ b/src/XTMF2/RuntimeModules/BasicEvent.cs @@ -21,63 +21,62 @@ You should have received a copy of the GNU General Public License using System.Linq; using System.Text; -namespace XTMF2.RuntimeModules +namespace XTMF2.RuntimeModules; + +[Module(Name = "Basic Event", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/BasicEvent.html", +Description = "Provides the ability for modules to invoke a set of other modules that are waiting for something to occur.")] +public sealed class BasicEvent : BaseEvent { - [Module(Name = "Basic Event", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/BasicEvent.html", - Description = "Provides the ability for modules to invoke a set of other modules that are waiting for something to occur.")] - public sealed class BasicEvent : BaseEvent - { - private readonly List _toInvoke = new List(); + private readonly List _toInvoke = new List(); - public override void Invoke() + public override void Invoke() + { + // make a copy in case the invocation causes an additional registration + List copy; + lock(_toInvoke) { - // make a copy in case the invocation causes an additional registration - List copy; - lock(_toInvoke) - { - copy = _toInvoke.ToList(); - } - foreach(var registered in copy) - { - registered.Invoke(); - } + copy = _toInvoke.ToList(); } - - public override void Register(Action module) + foreach(var registered in copy) { - lock(_toInvoke) - { - _toInvoke.Add(module); - } + registered.Invoke(); } } - [Module(Name = "Basic Event", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/BasicEvent.html", - Description = "Provides the ability for modules to invoke a set of other modules that are waiting for something to occur.")] - public sealed class BasicEvent : BaseEvent + public override void Register(Action module) { - private readonly List> _toInvoke = new List>(); + lock(_toInvoke) + { + _toInvoke.Add(module); + } + } +} - public override void Invoke(Context context) +[Module(Name = "Basic Event", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/BasicEvent.html", +Description = "Provides the ability for modules to invoke a set of other modules that are waiting for something to occur.")] +public sealed class BasicEvent : BaseEvent +{ + private readonly List> _toInvoke = new List>(); + + public override void Invoke(Context context) + { + // make a copy in case the invocation causes an additional registration + List> copy; + lock (_toInvoke) + { + copy = _toInvoke.ToList(); + } + foreach (var registered in copy) { - // make a copy in case the invocation causes an additional registration - List> copy; - lock (_toInvoke) - { - copy = _toInvoke.ToList(); - } - foreach (var registered in copy) - { - registered.Invoke(context); - } + registered.Invoke(context); } + } - public override void Register(Action module) + public override void Register(Action module) + { + lock (_toInvoke) { - lock (_toInvoke) - { - _toInvoke.Add(module); - } + _toInvoke.Add(module); } } -} +} \ No newline at end of file diff --git a/src/XTMF2/RuntimeModules/BasicParameter.cs b/src/XTMF2/RuntimeModules/BasicParameter.cs index 47f617e..1c42c2b 100644 --- a/src/XTMF2/RuntimeModules/BasicParameter.cs +++ b/src/XTMF2/RuntimeModules/BasicParameter.cs @@ -20,19 +20,18 @@ You should have received a copy of the GNU General Public License using System.Collections.Generic; using System.Text; -namespace XTMF2.RuntimeModules +namespace XTMF2.RuntimeModules; + +[Module(Name = "Basic Parameter", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/BasicParameter.html", +Description = "Provides the ability to have a value in a model system.")] +public class BasicParameter : BaseFunction { - [Module(Name = "Basic Parameter", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/BasicParameter.html", - Description = "Provides the ability to have a value in a model system.")] - public class BasicParameter : BaseFunction - { #pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public T Value; + public T Value; #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public override T Invoke() - { - return Value; - } + public override T Invoke() + { + return Value; } } diff --git a/src/XTMF2/RuntimeModules/Cache.cs b/src/XTMF2/RuntimeModules/Cache.cs index 5fb9b2f..e1a5366 100644 --- a/src/XTMF2/RuntimeModules/Cache.cs +++ b/src/XTMF2/RuntimeModules/Cache.cs @@ -21,78 +21,77 @@ You should have received a copy of the GNU General Public License using System.Text; using System.Threading; -namespace XTMF2.RuntimeModules +namespace XTMF2.RuntimeModules; + +[Module(Name = "Cache", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Cache.html", +Description = "Provides a way to keep the result of a function unless unloaded by an event.")] +public sealed class Cache : BaseFunction, IDisposable { - [Module(Name = "Cache", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Cache.html", - Description = "Provides a way to keep the result of a function unless unloaded by an event.")] - public sealed class Cache : BaseFunction, IDisposable - { - private readonly Lock _lock = new Lock(); + private readonly Lock _lock = new Lock(); #pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - private T _cachedValue; + private T _cachedValue; #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - private bool _initialized = false; + private bool _initialized = false; - [SubModule(Required = true, Name = "Source", Description = "Get the cached data", Index = 0)] - public IFunction? Source; + [SubModule(Required = true, Name = "Source", Description = "Get the cached data", Index = 0)] + public IFunction? Source; - [SubModule(Required = false, Name = "Force Update", Description = "Invoke to force an update", Index = 1)] - public IEvent? ForceUpdate; + [SubModule(Required = false, Name = "Force Update", Description = "Invoke to force an update", Index = 1)] + public IEvent? ForceUpdate; - public override T Invoke() + public override T Invoke() + { + lock (_lock) { - lock (_lock) + if (!_initialized) { - if (!_initialized) - { - _cachedValue = Source!.Invoke(); - _initialized = true; - GC.ReRegisterForFinalize(this); - } - return _cachedValue!; + _cachedValue = Source!.Invoke(); + _initialized = true; + GC.ReRegisterForFinalize(this); } + return _cachedValue!; } + } - public override bool RuntimeValidation(ref string? error) + public override bool RuntimeValidation(ref string? error) + { + ForceUpdate?.Register(() => { - ForceUpdate?.Register(() => + lock (_lock) { - lock (_lock) - { - _initialized = false; - Dispose(); - } - }); - return true; - } + _initialized = false; + Dispose(); + } + }); + return true; + } + + ~Cache() + { + Dispose(false); + } - ~Cache() + private void Dispose(bool managed) + { + if(managed) { - Dispose(false); + GC.SuppressFinalize(this); } - - private void Dispose(bool managed) + lock (_lock) { - if(managed) - { - GC.SuppressFinalize(this); - } - lock (_lock) + if (_initialized && _cachedValue is IDisposable disposable) { - if (_initialized && _cachedValue is IDisposable disposable) - { - disposable.Dispose(); - } - _cachedValue = default!; - _initialized = false; + disposable.Dispose(); } + _cachedValue = default!; + _initialized = false; } + } - public void Dispose() - { - Dispose(true); - } + public void Dispose() + { + Dispose(true); } } diff --git a/src/XTMF2/RuntimeModules/CombineContext.cs b/src/XTMF2/RuntimeModules/CombineContext.cs index d0a88bb..451a312 100644 --- a/src/XTMF2/RuntimeModules/CombineContext.cs +++ b/src/XTMF2/RuntimeModules/CombineContext.cs @@ -20,103 +20,102 @@ You should have received a copy of the GNU General Public License using System.Collections.Generic; using System.Text; -namespace XTMF2.RuntimeModules -{ - [Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", +namespace XTMF2.RuntimeModules; + +[Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", Description = "Combines the contexts as derived from First and Second and invokes To Invoke with the combined context.")] - public sealed class CombineContextAFromNoContext : BaseAction +public sealed class CombineContextAFromNoContext : BaseAction +{ + [SubModule(Name = "First", Required = true, Index = 0, Description = "The first context to use.")] + public IFunction? First; + + [SubModule(Name = "Second", Required = true, Index = 1, Description = "The second context to use.")] + public IFunction? Second; + + [SubModule(Name = "To Invoke", Required = true, Index = 2, Description = "The module to invoke with the combined context.")] + public IAction<(Context1, Context2)>? ToInvoke; + + public override void Invoke() { - [SubModule(Name = "First", Required = true, Index = 0, Description = "The first context to use.")] - public IFunction? First; - - [SubModule(Name = "Second", Required = true, Index = 1, Description = "The second context to use.")] - public IFunction? Second; - - [SubModule(Name = "To Invoke", Required = true, Index = 2, Description = "The module to invoke with the combined context.")] - public IAction<(Context1, Context2)> ?ToInvoke; - - public override void Invoke() - { - ToInvoke!.Invoke((First!.Invoke(), Second!.Invoke())); - } + ToInvoke!.Invoke((First!.Invoke(), Second!.Invoke())); } +} - [Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", +[Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", Description = "Combines the contexts as derived from First and Second and invokes To Invoke with the combined context.")] - public sealed class CombineContextA : BaseAction +public sealed class CombineContextA : BaseAction +{ + [SubModule(Name = "Second", Required = true, Index = 0, Description = "The second context to use.")] + public IFunction? Second; + [SubModule(Name = "To Invoke", Required = true, Index = 1, Description = "The module to invoke with the combined context.")] + public IAction<(Context1, Context2)>? ToInvoke; + + public override void Invoke(Context1 context) { - [SubModule(Name = "Second", Required = true, Index = 0, Description = "The second context to use.")] - public IFunction? Second; - [SubModule(Name = "To Invoke", Required = true, Index = 1, Description = "The module to invoke with the combined context.")] - public IAction<(Context1, Context2)>? ToInvoke; - - public override void Invoke(Context1 context) - { - ToInvoke!.Invoke((context, Second!.Invoke())); - } + ToInvoke!.Invoke((context, Second!.Invoke())); } +} - [Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", +[Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", Description = "Combines the contexts as derived from First and Second and invokes To Invoke with the combined context.")] - public sealed class CombineContextAFromContext : BaseAction +public sealed class CombineContextAFromContext : BaseAction +{ + [SubModule(Name = "Second", Required = true, Index = 0, Description = "The second context to use.")] + public IFunction? Second; + [SubModule(Name = "To Invoke", Required = true, Index = 1, Description = "The module to invoke with the combined context.")] + public IAction<(Context1, Context2)>? ToInvoke; + + public override void Invoke(Context1 context) { - [SubModule(Name = "Second", Required = true, Index = 0, Description = "The second context to use.")] - public IFunction? Second; - [SubModule(Name = "To Invoke", Required = true, Index = 1, Description = "The module to invoke with the combined context.")] - public IAction<(Context1, Context2)>? ToInvoke; - - public override void Invoke(Context1 context) - { - ToInvoke!.Invoke((context, Second!.Invoke(context))); - } + ToInvoke!.Invoke((context, Second!.Invoke(context))); } +} - [Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", +[Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", Description = "Combines the contexts as derived from First and Second and invokes To Invoke with the combined context.")] - public sealed class CombineContextFFromNoContext : BaseFunction - { - [SubModule(Name = "First", Required = true, Index = 0, Description = "The first context to use.")] - public IFunction? First; +public sealed class CombineContextFFromNoContext : BaseFunction +{ + [SubModule(Name = "First", Required = true, Index = 0, Description = "The first context to use.")] + public IFunction? First; - [SubModule(Name = "Second", Required = true, Index = 1, Description = "The second context to use.")] - public IFunction? Second; + [SubModule(Name = "Second", Required = true, Index = 1, Description = "The second context to use.")] + public IFunction? Second; - [SubModule(Name = "To Invoke", Required = true, Index = 2, Description = "The module to invoke with the combined context.")] - public IFunction<(Context1, Context2), Return>? ToInvoke; + [SubModule(Name = "To Invoke", Required = true, Index = 2, Description = "The module to invoke with the combined context.")] + public IFunction<(Context1, Context2), Return>? ToInvoke; - public override Return Invoke() - { - return ToInvoke!.Invoke((First!.Invoke(), Second!.Invoke())); - } + public override Return Invoke() + { + return ToInvoke!.Invoke((First!.Invoke(), Second!.Invoke())); } +} - [Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", +[Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", Description = "Combines the contexts as derived from First and Second and invokes To Invoke with the combined context.")] - public sealed class CombineContexF : BaseFunction +public sealed class CombineContexF : BaseFunction +{ + [SubModule(Name = "Second", Required = true, Index = 0, Description = "The second context to use.")] + public IFunction? Second; + [SubModule(Name = "To Invoke", Required = true, Index = 1, Description = "The module to invoke with the combined context.")] + public IFunction<(Context1, Context2), Return>? ToInvoke; + + public override Return Invoke(Context1 context) { - [SubModule(Name = "Second", Required = true, Index = 0, Description = "The second context to use.")] - public IFunction? Second; - [SubModule(Name = "To Invoke", Required = true, Index = 1, Description = "The module to invoke with the combined context.")] - public IFunction<(Context1, Context2), Return>? ToInvoke; - - public override Return Invoke(Context1 context) - { - return ToInvoke!.Invoke((context, Second!.Invoke())); - } + return ToInvoke!.Invoke((context, Second!.Invoke())); } +} - [Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", +[Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", Description = "Combines the contexts as derived from First and Second and invokes To Invoke with the combined context.")] - public sealed class CombineContextFFromContext : BaseFunction +public sealed class CombineContextFFromContext : BaseFunction +{ + [SubModule(Name = "Second", Required = true, Index = 0, Description = "The second context to use.")] + public IFunction? Second; + [SubModule(Name = "To Invoke", Required = true, Index = 1, Description = "The module to invoke with the combined context.")] + public IFunction<(Context1, Context2), Return>? ToInvoke; + + public override Return Invoke(Context1 context) { - [SubModule(Name = "Second", Required = true, Index = 0, Description = "The second context to use.")] - public IFunction? Second; - [SubModule(Name = "To Invoke", Required = true, Index = 1, Description = "The module to invoke with the combined context.")] - public IFunction<(Context1, Context2), Return>? ToInvoke; - - public override Return Invoke(Context1 context) - { - return ToInvoke!.Invoke((context, Second!.Invoke(context))); - } + return ToInvoke!.Invoke((context, Second!.Invoke(context))); } } diff --git a/src/XTMF2/RuntimeModules/DirectoryPath.cs b/src/XTMF2/RuntimeModules/DirectoryPath.cs index ace38f9..02914ff 100644 --- a/src/XTMF2/RuntimeModules/DirectoryPath.cs +++ b/src/XTMF2/RuntimeModules/DirectoryPath.cs @@ -20,25 +20,24 @@ You should have received a copy of the GNU General Public License using System.Collections.Generic; using System.Text; -namespace XTMF2.RuntimeModules -{ - [Module(Name = "Directory Path", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/DirectoryPath.html", +namespace XTMF2.RuntimeModules; + +[Module(Name = "Directory Path", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/DirectoryPath.html", Description = "Provides the ability to specify a directory path recursively.")] - public sealed class DirectoryPath : BaseFunction - { - [SubModule(Required = false, Name = "Parent", Description = "Optional parent directory", Index = 0)] - public DirectoryPath? Parent; +public sealed class DirectoryPath : BaseFunction +{ + [SubModule(Required = false, Name = "Parent", Description = "Optional parent directory", Index = 0)] + public DirectoryPath? Parent; - [Parameter(Name = "Name", DefaultValue = "directoryName", Description = "The path to add to the Parent path", Index = 1)] - public IFunction? Path; + [Parameter(Name = "Name", DefaultValue = "directoryName", Description = "The path to add to the Parent path", Index = 1)] + public IFunction? Path; - public override string Invoke() + public override string Invoke() + { + if(Parent != null) { - if(Parent != null) - { - return System.IO.Path.Combine(Parent.Invoke(), Path!.Invoke()); - } - return Path!.Invoke(); + return System.IO.Path.Combine(Parent.Invoke(), Path!.Invoke()); } + return Path!.Invoke(); } } diff --git a/src/XTMF2/RuntimeModules/Execute.cs b/src/XTMF2/RuntimeModules/Execute.cs index f5c4f65..61a21e1 100644 --- a/src/XTMF2/RuntimeModules/Execute.cs +++ b/src/XTMF2/RuntimeModules/Execute.cs @@ -21,93 +21,92 @@ You should have received a copy of the GNU General Public License using System.Text; using System.Threading.Tasks; -namespace XTMF2.RuntimeModules -{ - [Module(Name = "Execute", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Execute.html", +namespace XTMF2.RuntimeModules; + +[Module(Name = "Execute", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Execute.html", Description = "Provides a way to execute a series of actions in order, optionally in parallel or with multiple iterations.")] - public class Execute : BaseAction - { - [Parameter(DefaultValue = "false", Name = "Parallel Execution", Required = false, Index = 0)] - public IFunction? ParallelExecution; +public class Execute : BaseAction +{ + [Parameter(DefaultValue = "false", Name = "Parallel Execution", Required = false, Index = 0)] + public IFunction? ParallelExecution; - [Parameter(DefaultValue = "1", Name = "Iterations", Required = false, Index = 1)] - public IFunction? Iterations; + [Parameter(DefaultValue = "1", Name = "Iterations", Required = false, Index = 1)] + public IFunction? Iterations; - [SubModule(Name = "Current Iteration", Required = false, Description = "Place to store the current iteration", Index = 2, PassesExecution = true)] - public ISetableValue? CurrentIteration; + [SubModule(Name = "Current Iteration", Required = false, Description = "Place to store the current iteration", Index = 2, PassesExecution = true)] + public ISetableValue? CurrentIteration; - [SubModule(Name = "To Execute", Description = "The modules in order to execute", Index = 3, PassesExecution = true)] - public IAction[]? ToInvoke; + [SubModule(Name = "To Execute", Description = "The modules in order to execute", Index = 3, PassesExecution = true)] + public IAction[]? ToInvoke; - public override void Invoke() + public override void Invoke() + { + var iterations = Iterations?.Invoke() ?? 1; + var parallel = ParallelExecution?.Invoke() ?? false; + if (parallel) { - var iterations = Iterations?.Invoke() ?? 1; - var parallel = ParallelExecution?.Invoke() ?? false; - if (parallel) + for (int iteration = 0; iteration < iterations; iteration++) { - for (int iteration = 0; iteration < iterations; iteration++) + CurrentIteration?.Set(iteration); + Parallel.ForEach(ToInvoke!, (action) => { - CurrentIteration?.Set(iteration); - Parallel.ForEach(ToInvoke!, (action) => - { - action?.Invoke(); - }); - } + action?.Invoke(); + }); } - else + } + else + { + for (int iteration = 0; iteration < iterations; iteration++) { - for (int iteration = 0; iteration < iterations; iteration++) + CurrentIteration?.Set(iteration); + foreach (var module in ToInvoke!) { - CurrentIteration?.Set(iteration); - foreach (var module in ToInvoke!) - { - module?.Invoke(); - } + module?.Invoke(); } } } } +} - [Module(Name = "Execute", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Execute.html", +[Module(Name = "Execute", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Execute.html", Description = "Provides a way to execute a series of actions in order, optionally in parallel or with multiple iterations.")] - public class Execute : BaseAction - { - [Parameter(DefaultValue = "false", Name = "Parallel Execution", Required = false, Index = 0)] - public IFunction? ParallelExecution; +public class Execute : BaseAction +{ + [Parameter(DefaultValue = "false", Name = "Parallel Execution", Required = false, Index = 0)] + public IFunction? ParallelExecution; - [Parameter(DefaultValue = "1", Name = "Iterations", Required = false, Index = 1)] - public IFunction? Iterations; + [Parameter(DefaultValue = "1", Name = "Iterations", Required = false, Index = 1)] + public IFunction? Iterations; - [SubModule(Name = "Current Iteration", Required = false, Description = "Place to store the current iteration", Index = 2, PassesExecution = true)] - public ISetableValue? CurrentIteration; + [SubModule(Name = "Current Iteration", Required = false, Description = "Place to store the current iteration", Index = 2, PassesExecution = true)] + public ISetableValue? CurrentIteration; - [SubModule(Name = "To Execute", Description = "The modules in order to execute", Index = 3, PassesExecution = true)] - public IAction[]? ToInvoke; + [SubModule(Name = "To Execute", Description = "The modules in order to execute", Index = 3, PassesExecution = true)] + public IAction[]? ToInvoke; - public override void Invoke(Context context) + public override void Invoke(Context context) + { + var iterations = Iterations?.Invoke() ?? 1; + var parallel = ParallelExecution?.Invoke() ?? false; + if (parallel) { - var iterations = Iterations?.Invoke() ?? 1; - var parallel = ParallelExecution?.Invoke() ?? false; - if (parallel) + for (int iteration = 0; iteration < iterations; iteration++) { - for (int iteration = 0; iteration < iterations; iteration++) + CurrentIteration?.Set(iteration); + Parallel.ForEach(ToInvoke!, (action) => { - CurrentIteration?.Set(iteration); - Parallel.ForEach(ToInvoke!, (action) => - { - action?.Invoke(context); - }); - } + action?.Invoke(context); + }); } - else + } + else + { + for (int iteration = 0; iteration < iterations; iteration++) { - for (int iteration = 0; iteration < iterations; iteration++) + CurrentIteration?.Set(iteration); + foreach (var module in ToInvoke!) { - CurrentIteration?.Set(iteration); - foreach (var module in ToInvoke!) - { - module?.Invoke(context); - } + module?.Invoke(context); } } } diff --git a/src/XTMF2/RuntimeModules/ExecuteActionsThenFunction.cs b/src/XTMF2/RuntimeModules/ExecuteActionsThenFunction.cs index 25f9e85..6fa6d19 100644 --- a/src/XTMF2/RuntimeModules/ExecuteActionsThenFunction.cs +++ b/src/XTMF2/RuntimeModules/ExecuteActionsThenFunction.cs @@ -21,8 +21,7 @@ You should have received a copy of the GNU General Public License using System.Text; using System.Threading.Tasks; -namespace XTMF2.RuntimeModules -{ +namespace XTMF2.RuntimeModules; [Module(Name = "Execute Actions Then Function", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/ExecuteActionsThenFunction.html", Description = "Allows you to execute actions before calling a function. This allows you to ")] public class ExecuteActionsThenFunction : BaseFunction @@ -55,4 +54,4 @@ public override Return Invoke() return EndWith!.Invoke(); } } -} + diff --git a/src/XTMF2/RuntimeModules/Fail.cs b/src/XTMF2/RuntimeModules/Fail.cs index 7a4c12a..84a3965 100644 --- a/src/XTMF2/RuntimeModules/Fail.cs +++ b/src/XTMF2/RuntimeModules/Fail.cs @@ -20,83 +20,82 @@ You should have received a copy of the GNU General Public License using System.Collections.Generic; using System.Text; -namespace XTMF2.RuntimeModules -{ - [Module(Name = "Fail", Description = "Crash the model run with a message.", - DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Fail.html")] - public sealed class FailA : BaseAction - { - [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] - public IFunction? Message; +namespace XTMF2.RuntimeModules; - public override void Invoke() - { - throw new XTMFRuntimeException(this, Message?.Invoke()); - } - } +[Module(Name = "Fail", Description = "Crash the model run with a message.", + DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Fail.html")] +public sealed class FailA : BaseAction +{ + [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] + public IFunction? Message; - [Module(Name = "Fail", Description = "Crash the model run with a message.", - DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Fail.html")] - public sealed class FailA : BaseAction + public override void Invoke() { - [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] - public IFunction? Message; - - public override void Invoke(Context context) - { - throw new XTMFRuntimeException(this, Message?.Invoke()); - } + throw new XTMFRuntimeException(this, Message?.Invoke()); } +} - [Module(Name = "Fail", Description = "Crash the model run with a message.", +[Module(Name = "Fail", Description = "Crash the model run with a message.", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Fail.html")] - public sealed class FailWithContextA : BaseAction - { - [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] - public IFunction? Message; +public sealed class FailA : BaseAction +{ + [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] + public IFunction? Message; - public override void Invoke(Context context) - { - throw new XTMFRuntimeException(this, Message?.Invoke(context)); - } + public override void Invoke(Context context) + { + throw new XTMFRuntimeException(this, Message?.Invoke()); } +} - [Module(Name = "Fail", Description = "Crash the model run with a message.", - DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Fail.html")] - public sealed class FailF : BaseFunction - { - [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] - public IFunction? Message; +[Module(Name = "Fail", Description = "Crash the model run with a message.", +DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Fail.html")] +public sealed class FailWithContextA : BaseAction +{ + [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] + public IFunction? Message; - public override Return Invoke() - { - throw new XTMFRuntimeException(this, Message?.Invoke()); - } + public override void Invoke(Context context) + { + throw new XTMFRuntimeException(this, Message?.Invoke(context)); } +} - [Module(Name = "Fail", Description = "Crash the model run with a message.", - DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Fail.html")] - public sealed class FailF : BaseFunction - { - [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] - public IFunction? Message; +[Module(Name = "Fail", Description = "Crash the model run with a message.", +DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Fail.html")] +public sealed class FailF : BaseFunction +{ + [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] + public IFunction? Message; - public override Return Invoke(Context context) - { - throw new XTMFRuntimeException(this, Message?.Invoke()); - } + public override Return Invoke() + { + throw new XTMFRuntimeException(this, Message?.Invoke()); } +} - [Module(Name = "Fail", Description = "Crash the model run with a message.", +[Module(Name = "Fail", Description = "Crash the model run with a message.", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Fail.html")] - public sealed class FailWithContextF : BaseFunction +public sealed class FailF : BaseFunction +{ + [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] + public IFunction? Message; + + public override Return Invoke(Context context) { - [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] - public IFunction? Message; + throw new XTMFRuntimeException(this, Message?.Invoke()); + } +} - public override Return Invoke(Context context) - { - throw new XTMFRuntimeException(this, Message?.Invoke(context)); - } +[Module(Name = "Fail", Description = "Crash the model run with a message.", +DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Fail.html")] +public sealed class FailWithContextF : BaseFunction +{ + [Parameter(Name = "Message", Index = 0, Description = "The message to fail with.", DefaultValue = "Invalid state!")] + public IFunction? Message; + + public override Return Invoke(Context context) + { + throw new XTMFRuntimeException(this, Message?.Invoke(context)); } } diff --git a/src/XTMF2/RuntimeModules/If.cs b/src/XTMF2/RuntimeModules/If.cs index 9f81a06..3afc5dd 100644 --- a/src/XTMF2/RuntimeModules/If.cs +++ b/src/XTMF2/RuntimeModules/If.cs @@ -20,8 +20,7 @@ You should have received a copy of the GNU General Public License using System.Collections.Generic; using System.Text; -namespace XTMF2.RuntimeModules -{ +namespace XTMF2.RuntimeModules; [Module(Name = "If", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/If.html", Description = "Provides a way to conditionally execute. If the condition is true or false different functions will be invoked.")] public sealed class IfF : BaseFunction @@ -183,4 +182,3 @@ public override void Invoke(Context context) } } } -} diff --git a/src/XTMF2/RuntimeModules/Ignore.cs b/src/XTMF2/RuntimeModules/Ignore.cs index b487a67..591a3fe 100644 --- a/src/XTMF2/RuntimeModules/Ignore.cs +++ b/src/XTMF2/RuntimeModules/Ignore.cs @@ -21,8 +21,7 @@ You should have received a copy of the GNU General Public License using System.Text; using XTMF2; -namespace XTMF2.RuntimeModules -{ +namespace XTMF2.RuntimeModules; [Module(Name = "Ignore Result", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/IgnoreResult.html", Description = "Ignore the result of a function call. This allows you to call functions from an action.")] public class IgnoreResult : BaseAction @@ -74,4 +73,3 @@ public override Return Invoke(Context context) return ToInvoke!.Invoke(); } } -} diff --git a/src/XTMF2/RuntimeModules/Log.cs b/src/XTMF2/RuntimeModules/Log.cs index f998cae..ac9f210 100644 --- a/src/XTMF2/RuntimeModules/Log.cs +++ b/src/XTMF2/RuntimeModules/Log.cs @@ -23,77 +23,76 @@ You should have received a copy of the GNU General Public License using System.Text.Unicode; using System.Threading; -namespace XTMF2.RuntimeModules -{ - [Module(Name = "Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Log.html", +namespace XTMF2.RuntimeModules; + +[Module(Name = "Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/Log.html", Description = "Provides functionality for synchronizing the writing of events to a log and providing time stamps.")] - public sealed class Log : BaseAction, IFunction, IDisposable - { - [SubModule(Required = true, Name = "LogStream", Description = "The stream to save the log to.", Index = 0)] - public IFunction? LogStream; +public sealed class Log : BaseAction, IFunction, IDisposable +{ + [SubModule(Required = true, Name = "LogStream", Description = "The stream to save the log to.", Index = 0)] + public IFunction? LogStream; - private readonly Lock _writeLock = new(); + private readonly Lock _writeLock = new(); - private StreamWriter? _writer; + private StreamWriter? _writer; - private bool _alwaysFlush = false; + private bool _alwaysFlush = false; - public override void Invoke(string message) + public override void Invoke(string message) + { + lock (_writeLock) { - lock (_writeLock) + if(_writer is null) { - if(_writer is null) + if (LogStream?.Invoke() is WriteStream writeStream) { - if (LogStream?.Invoke() is WriteStream writeStream) - { - // Check to see if we need to always flush the stream. - _alwaysFlush = writeStream is RunStatusStream; - var encoding = _alwaysFlush ? new UTF8Encoding(false, true) : Encoding.UTF8; - _writer = new StreamWriter(writeStream, encoding, 0x4000, false); - } - else - { - throw new XTMFRuntimeException(this, "Unable to create a write stream to store the log into!"); - } + // Check to see if we need to always flush the stream. + _alwaysFlush = writeStream is RunStatusStream; + var encoding = _alwaysFlush ? new UTF8Encoding(false, true) : Encoding.UTF8; + _writer = new StreamWriter(writeStream, encoding, 0x4000, false); } - // don't block while writing - _writer.Write(TimeStampMessage(message)); - if (_alwaysFlush) + else { - _writer.Flush(); + throw new XTMFRuntimeException(this, "Unable to create a write stream to store the log into!"); } } + // don't block while writing + _writer.Write(TimeStampMessage(message)); + if (_alwaysFlush) + { + _writer.Flush(); + } } + } - Log IFunction.Invoke() - { - return this; - } + Log IFunction.Invoke() + { + return this; + } - private static string TimeStampMessage(string message) - { - var now = DateTime.Now; - return $"[{now.Hour:D2}:{now.Minute:D2}:{now.Second:D2}] {message}"; - } + private static string TimeStampMessage(string message) + { + var now = DateTime.Now; + return $"[{now.Hour:D2}:{now.Minute:D2}:{now.Second:D2}] {message}"; + } - private void Dispose(bool managed) + private void Dispose(bool managed) + { + if(managed) { - if(managed) - { - GC.SuppressFinalize(this); - } - _writer?.Dispose(); - _writer = null; + GC.SuppressFinalize(this); } + _writer?.Dispose(); + _writer = null; + } - public void Dispose() - { - Dispose(true); - } + public void Dispose() + { + Dispose(true); + } - ~Log() - { - Dispose(false); - } + ~Log() + { + Dispose(false); } } diff --git a/src/XTMF2/RuntimeModules/OpenReadStreamFromFile.cs b/src/XTMF2/RuntimeModules/OpenReadStreamFromFile.cs index aab73e2..0012534 100644 --- a/src/XTMF2/RuntimeModules/OpenReadStreamFromFile.cs +++ b/src/XTMF2/RuntimeModules/OpenReadStreamFromFile.cs @@ -20,58 +20,57 @@ You should have received a copy of the GNU General Public License using System.IO; using System.Text; -namespace XTMF2.RuntimeModules -{ - [Module(Name = "Open Read Stream From File", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/OpenReadStreamFromFile.html", +namespace XTMF2.RuntimeModules; + +[Module(Name = "Open Read Stream From File", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/OpenReadStreamFromFile.html", Description = "Provides the ability to read a file from the path given to it via the context.")] - public class OpenReadStreamFromFile : BaseFunction - { - [Parameter(DefaultValue = "true", Description = "True if the file should be checked at runtime to ensure that it exists.", Index=1, - Name="Check File Exists At Run Start", Required = true)] - public IFunction? CheckFileExistsAtRunStart; +public class OpenReadStreamFromFile : BaseFunction +{ + [Parameter(DefaultValue = "true", Description = "True if the file should be checked at runtime to ensure that it exists.", Index=1, + Name="Check File Exists At Run Start", Required = true)] + public IFunction? CheckFileExistsAtRunStart; - [Parameter(DefaultValue = "", Description = "The path to the file to load.", Index = 0, - Name = "File Path", Required = true)] - public IFunction? FilePath; + [Parameter(DefaultValue = "", Description = "The path to the file to load.", Index = 0, + Name = "File Path", Required = true)] + public IFunction? FilePath; - public override ReadStream Invoke() + public override ReadStream Invoke() + { + try { - try + if (FilePath?.Invoke() is string path) { - if (FilePath?.Invoke() is string path) - { - return new ReadStream(File.OpenRead(path)); - } - else - { - throw new XTMFRuntimeException(this, "No path was given to open a ReadStream from!"); - } + return new ReadStream(File.OpenRead(path)); } - catch(IOException e) + else { - throw new XTMFRuntimeException(this, e.Message, e); + throw new XTMFRuntimeException(this, "No path was given to open a ReadStream from!"); } } + catch(IOException e) + { + throw new XTMFRuntimeException(this, e.Message, e); + } + } - public override bool RuntimeValidation(ref string? error) + public override bool RuntimeValidation(ref string? error) + { + if(CheckFileExistsAtRunStart?.Invoke() == true) { - if(CheckFileExistsAtRunStart?.Invoke() == true) + if (FilePath?.Invoke() is string filePath) { - if (FilePath?.Invoke() is string filePath) - { - if (!File.Exists(filePath)) - { - error = $"The file '{filePath}' does not exist!"; - return false; - } - } - else + if (!File.Exists(filePath)) { - + error = $"The file '{filePath}' does not exist!"; return false; } } - return true; + else + { + + return false; + } } + return true; } } diff --git a/src/XTMF2/RuntimeModules/OpenReadStreamFromMemoryPipe.cs b/src/XTMF2/RuntimeModules/OpenReadStreamFromMemoryPipe.cs index 9bd5ee4..7e1a30f 100644 --- a/src/XTMF2/RuntimeModules/OpenReadStreamFromMemoryPipe.cs +++ b/src/XTMF2/RuntimeModules/OpenReadStreamFromMemoryPipe.cs @@ -17,19 +17,18 @@ You should have received a copy of the GNU General Public License along with XTMF2. If not, see . */ -namespace XTMF2.RuntimeModules +namespace XTMF2.RuntimeModules; + +[Module(Name = "Open Read Stream From Memory Pipe", + Description = "Gets a ReadStream that is backed by memory.", + DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/OpenReadStreamFromMemoryPipe.html")] +public sealed class OpenReadStreamFromMemoryPipe : BaseFunction { - [Module(Name = "Open Read Stream From Memory Pipe", - Description = "Gets a ReadStream that is backed by memory.", - DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/OpenReadStreamFromMemoryPipe.html")] - public sealed class OpenReadStreamFromMemoryPipe : BaseFunction - { - [SubModule(Index=0,Name = "Pipe", Description = "The pipe to read from", Required = true)] - public IFunction? Pipe; + [SubModule(Index=0,Name = "Pipe", Description = "The pipe to read from", Required = true)] + public IFunction? Pipe; - public override ReadStream Invoke() - { - return Pipe!.Invoke().GetReadStream(this); - } + public override ReadStream Invoke() + { + return Pipe!.Invoke().GetReadStream(this); } } diff --git a/src/XTMF2/RuntimeModules/OpenWriteStreamFromFile.cs b/src/XTMF2/RuntimeModules/OpenWriteStreamFromFile.cs index 3351269..ffe15fe 100644 --- a/src/XTMF2/RuntimeModules/OpenWriteStreamFromFile.cs +++ b/src/XTMF2/RuntimeModules/OpenWriteStreamFromFile.cs @@ -21,42 +21,41 @@ You should have received a copy of the GNU General Public License using System.IO; using System.Text; -namespace XTMF2.RuntimeModules -{ - [Module(Name = "Open Write Stream From File", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/OpenWriteStreamFromFile.html", +namespace XTMF2.RuntimeModules; + +[Module(Name = "Open Write Stream From File", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/OpenWriteStreamFromFile.html", Description = "Provides a WriteStream to the given file name from context.")] - public class OpenWriteStreamFromFile : BaseFunction - { +public class OpenWriteStreamFromFile : BaseFunction +{ - [Parameter(DefaultValue = "", Description = "The path to the file to load.", Index = 0, - Name = "File Path", Required = true)] - public IFunction? FilePath; + [Parameter(DefaultValue = "", Description = "The path to the file to load.", Index = 0, + Name = "File Path", Required = true)] + public IFunction? FilePath; - public override WriteStream Invoke() + public override WriteStream Invoke() + { + var context = FilePath!.Invoke(); + if(String.IsNullOrWhiteSpace(context)) { - var context = FilePath!.Invoke(); - if(String.IsNullOrWhiteSpace(context)) - { - throw new XTMFRuntimeException(this, "The provided file path was empty!"); - } - try + throw new XTMFRuntimeException(this, "The provided file path was empty!"); + } + try + { + FileInfo f = new FileInfo(context); + var dir = f.Directory; + if(dir is null) { - FileInfo f = new FileInfo(context); - var dir = f.Directory; - if(dir is null) - { - throw new XTMFRuntimeException(this, $"The provided file path is not a valid file path!"); - } - if (!dir.Exists) - { - dir.Create(); - } - return new WriteStream(File.Open(context, FileMode.Create, FileAccess.Write)); + throw new XTMFRuntimeException(this, $"The provided file path is not a valid file path!"); } - catch(IOException e) + if (!dir.Exists) { - throw new XTMFRuntimeException(this, e.Message, e); + dir.Create(); } + return new WriteStream(File.Open(context, FileMode.Create, FileAccess.Write)); + } + catch(IOException e) + { + throw new XTMFRuntimeException(this, e.Message, e); } } } diff --git a/src/XTMF2/RuntimeModules/OpenWriteStreamFromMemoryPipe.cs b/src/XTMF2/RuntimeModules/OpenWriteStreamFromMemoryPipe.cs index 4f9361a..dbea1b0 100644 --- a/src/XTMF2/RuntimeModules/OpenWriteStreamFromMemoryPipe.cs +++ b/src/XTMF2/RuntimeModules/OpenWriteStreamFromMemoryPipe.cs @@ -18,19 +18,18 @@ You should have received a copy of the GNU General Public License */ -namespace XTMF2.RuntimeModules +namespace XTMF2.RuntimeModules; + +[Module(Name = "Open Write Stream From Memory Pipe", +Description = "Gets a WriteStream that is backed by memory.", +DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/OpenWriteStreamFromMemoryPipe.html")] +public sealed class OpenWriteStreamFromMemoryPipe : BaseFunction { - [Module(Name = "Open Write Stream From Memory Pipe", - Description = "Gets a WriteStream that is backed by memory.", - DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/OpenWriteStreamFromMemoryPipe.html")] - public sealed class OpenWriteStreamFromMemoryPipe : BaseFunction - { - [SubModule(Index = 0, Name = "Pipe", Description = "The pipe to write to", Required = true)] - public IFunction? Pipe; + [SubModule(Index = 0, Name = "Pipe", Description = "The pipe to write to", Required = true)] + public IFunction? Pipe; - public override WriteStream Invoke() - { - return Pipe!.Invoke().GetWriteStream(this); - } + public override WriteStream Invoke() + { + return Pipe!.Invoke().GetWriteStream(this); } } diff --git a/src/XTMF2/RuntimeModules/ReportInvocation.cs b/src/XTMF2/RuntimeModules/ReportInvocation.cs index 6fcba20..a19fb25 100644 --- a/src/XTMF2/RuntimeModules/ReportInvocation.cs +++ b/src/XTMF2/RuntimeModules/ReportInvocation.cs @@ -21,8 +21,7 @@ You should have received a copy of the GNU General Public License using System.Text; using XTMF2.Configuration; -namespace XTMF2.RuntimeModules -{ +namespace XTMF2.RuntimeModules; [Module(Name = "Report Invocation", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/ReportInvocation.html", Description = "Reports to XTMF that the model system has run through this point.")] public sealed class ReportFunctionInvocation : BaseFunction @@ -143,4 +142,3 @@ public override void Invoke(Context context) ToInvoke!.Invoke(context); } } -} diff --git a/src/XTMF2/RuntimeModules/ScriptedParameter.cs b/src/XTMF2/RuntimeModules/ScriptedParameter.cs index f5d0407..a36d85d 100644 --- a/src/XTMF2/RuntimeModules/ScriptedParameter.cs +++ b/src/XTMF2/RuntimeModules/ScriptedParameter.cs @@ -20,64 +20,63 @@ You should have received a copy of the GNU General Public License using System.Diagnostics.CodeAnalysis; using XTMF2.ModelSystemConstruct; -namespace XTMF2.RuntimeModules +namespace XTMF2.RuntimeModules; + +[Module(Name = "Scripted Parameter", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/ScriptedParameter.html", +Description = "Provides the ability to have a value that is calculated in an expression.")] +public sealed class ScriptedParameter : BaseFunction { - [Module(Name = "Scripted Parameter", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/ScriptedParameter.html", - Description = "Provides the ability to have a value that is calculated in an expression.")] - public sealed class ScriptedParameter : BaseFunction - { #pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public ParameterExpression Expression; + public ParameterExpression Expression; #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public override T Invoke() + public override T Invoke() + { + string? error = null; + if (!Expression.IsCompatible(typeof(T), ref error)) { - string? error = null; - if (!Expression.IsCompatible(typeof(T), ref error)) - { - Throw(error); - } - var ret = Expression.GetValue(this, typeof(T), ref error); - if (ret is null) - { - ThrowGotNull(); - } - return (T)ret; + Throw(error); } - - /// - /// - /// - /// - /// The requested error message. - [DoesNotReturn] - private void Throw(string error) + var ret = Expression.GetValue(this, typeof(T), ref error); + if (ret is null) { - throw new XTMFRuntimeException(this, error); + ThrowGotNull(); } + return (T)ret; + } - /// - /// - /// - /// - [DoesNotReturn] - private void ThrowGotNull() + /// + /// + /// + /// + /// The requested error message. + [DoesNotReturn] + private void Throw(string error) + { + throw new XTMFRuntimeException(this, error); + } + + /// + /// + /// + /// + [DoesNotReturn] + private void ThrowGotNull() + { + throw new XTMFRuntimeException(this, $"Unable to get a {typeof(T).FullName} value from expression '{Expression.Representation}'!"); + } + + public override bool RuntimeValidation(ref string? error) + { + if(!base.RuntimeValidation(ref error)) { - throw new XTMFRuntimeException(this, $"Unable to get a {typeof(T).FullName} value from expression '{Expression.Representation}'!"); + return false; } - - public override bool RuntimeValidation(ref string? error) + if (Expression is null) { - if(!base.RuntimeValidation(ref error)) - { - return false; - } - if (Expression is null) - { - error = "Expression is not set!"; - return false; - } - return true; + error = "Expression is not set!"; + return false; } + return true; } } diff --git a/src/XTMF2/RuntimeModules/SetableParameter.cs b/src/XTMF2/RuntimeModules/SetableParameter.cs index 647c171..82b2c73 100644 --- a/src/XTMF2/RuntimeModules/SetableParameter.cs +++ b/src/XTMF2/RuntimeModules/SetableParameter.cs @@ -20,20 +20,19 @@ You should have received a copy of the GNU General Public License using System.Collections.Generic; using System.Text; -namespace XTMF2.RuntimeModules +namespace XTMF2.RuntimeModules; + +[Module(Name = "Setable Parameter", Description = "A basic data store of variable type that can be set.", + DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/SetableParameter.html")] +public sealed class SetableParameter : BasicParameter, ISetableValue { - [Module(Name = "Setable Parameter", Description = "A basic data store of variable type that can be set.", - DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/SetableParameter.html")] - public sealed class SetableParameter : BasicParameter, ISetableValue + public T Get() { - public T Get() - { - return Value; - } + return Value; + } - public void Set(T value) - { - Value = value; - } + public void Set(T value) + { + Value = value; } } diff --git a/src/XTMF2/RuntimeModules/StartModule.cs b/src/XTMF2/RuntimeModules/StartModule.cs index 30be9d9..f8eadcd 100644 --- a/src/XTMF2/RuntimeModules/StartModule.cs +++ b/src/XTMF2/RuntimeModules/StartModule.cs @@ -20,8 +20,7 @@ You should have received a copy of the GNU General Public License using System.Collections.Generic; using System.Text; -namespace XTMF2.RuntimeModules -{ +namespace XTMF2.RuntimeModules; /// /// The type used for a start node /// @@ -37,4 +36,3 @@ public override void Invoke() ToExecute?.Invoke(); } } -} diff --git a/src/XTMF2/RuntimeModules/StepUp.cs b/src/XTMF2/RuntimeModules/StepUp.cs index 3567729..3fc7d11 100644 --- a/src/XTMF2/RuntimeModules/StepUp.cs +++ b/src/XTMF2/RuntimeModules/StepUp.cs @@ -20,47 +20,46 @@ You should have received a copy of the GNU General Public License using System.Collections.Generic; using System.Text; -namespace XTMF2.RuntimeModules -{ - [Module(Name = "Step Return Up", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/StepReturnUp.html", +namespace XTMF2.RuntimeModules; + +[Module(Name = "Step Return Up", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/StepReturnUp.html", Description = "Converts the result of a function to the expected type from the calling module.")] - public sealed class StepReturnUp : BaseFunction - where Original : ConvertTo - { - [SubModule(Required = true, Name = "ToInvoke", Description = "Invoke with converted context", Index = 0)] - public IFunction? ToInvoke; +public sealed class StepReturnUp : BaseFunction + where Original : ConvertTo +{ + [SubModule(Required = true, Name = "ToInvoke", Description = "Invoke with converted context", Index = 0)] + public IFunction? ToInvoke; - public override ConvertTo Invoke() - { - return ToInvoke!.Invoke(); - } + public override ConvertTo Invoke() + { + return ToInvoke!.Invoke(); } +} - [Module(Name = "Step Return Up", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/StepReturnUp.html", +[Module(Name = "Step Return Up", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/StepReturnUp.html", Description = "Converts the result of a function to the expected type from the calling module.")] - public sealed class StepReturnUp : BaseFunction - where Original : ConvertTo - { - [SubModule(Required = true, Name = "ToInvoke", Description = "Invoke with converted context", Index = 0)] - public IFunction? ToInvoke; +public sealed class StepReturnUp : BaseFunction + where Original : ConvertTo +{ + [SubModule(Required = true, Name = "ToInvoke", Description = "Invoke with converted context", Index = 0)] + public IFunction? ToInvoke; - public override ConvertTo Invoke(Context context) - { - return ToInvoke!.Invoke(context); - } + public override ConvertTo Invoke(Context context) + { + return ToInvoke!.Invoke(context); } +} - [Module(Name = "Step Return Up", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/StepReturnUp.html", +[Module(Name = "Step Return Up", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/StepReturnUp.html", Description = "Converts the result of a function to the expected type from the calling module.")] - public sealed class StepActionUp : BaseAction - where Original : ConvertTo - { - [SubModule(Required = true, Name = "ToInvoke", Description = "Invoke with converted context", Index = 0)] - public IAction? ToInvoke; +public sealed class StepActionUp : BaseAction + where Original : ConvertTo +{ + [SubModule(Required = true, Name = "ToInvoke", Description = "Invoke with converted context", Index = 0)] + public IAction? ToInvoke; - public override void Invoke(Original context) - { - ToInvoke!.Invoke(context); - } + public override void Invoke(Original context) + { + ToInvoke!.Invoke(context); } } diff --git a/src/XTMF2/RuntimeModules/WriteToLog.cs b/src/XTMF2/RuntimeModules/WriteToLog.cs index 8c16ef1..5e438ff 100644 --- a/src/XTMF2/RuntimeModules/WriteToLog.cs +++ b/src/XTMF2/RuntimeModules/WriteToLog.cs @@ -20,132 +20,129 @@ You should have received a copy of the GNU General Public License using System.Collections.Generic; using System.Text; -namespace XTMF2.RuntimeModules +namespace XTMF2.RuntimeModules; +[Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", + Description = "Writes the provided mess to the log and then invokes the next step.")] +public sealed class WriteToLogF : BaseFunction { - [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] - public sealed class WriteToLogF : BaseFunction - { - [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] - public IFunction Log = null!; + [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] + public IFunction Log = null!; - [SubModule(Required = true, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] - public IFunction ToInvoke = null!; + [SubModule(Required = true, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] + public IFunction ToInvoke = null!; - [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] - public IFunction Message = null!; + [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] + public IFunction Message = null!; - public override Return Invoke() - { - var log = Log.Invoke(); - log.Invoke(Message.Invoke()); - return ToInvoke.Invoke(); - } + public override Return Invoke() + { + var log = Log.Invoke(); + log.Invoke(Message.Invoke()); + return ToInvoke.Invoke(); } +} - [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] - public sealed class WriteToLogF : BaseFunction - { - [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] - public IFunction Log = null!; +[Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", + Description = "Writes the provided mess to the log and then invokes the next step.")] +public sealed class WriteToLogF : BaseFunction +{ + [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] + public IFunction Log = null!; - [SubModule(Required = true, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] - public IFunction ToInvoke = null!; + [SubModule(Required = true, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] + public IFunction ToInvoke = null!; - [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] - public IFunction Message = null!; + [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] + public IFunction Message = null!; - public override Return Invoke(Context context) - { - var log = Log.Invoke(); - log.Invoke(Message.Invoke()); - return ToInvoke.Invoke(context); - } + public override Return Invoke(Context context) + { + var log = Log.Invoke(); + log.Invoke(Message.Invoke()); + return ToInvoke.Invoke(context); } +} - [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] - public sealed class WriteToLogBasedOnContextF : BaseFunction - { - [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] - public IFunction Log = null!; +[Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", + Description = "Writes the provided mess to the log and then invokes the next step.")] +public sealed class WriteToLogBasedOnContextF : BaseFunction +{ + [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] + public IFunction Log = null!; - [SubModule(Required = true, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] - public IFunction ToInvoke = null!; + [SubModule(Required = true, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] + public IFunction ToInvoke = null!; - [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] - public IFunction Message = null!; + [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] + public IFunction Message = null!; - public override Return Invoke(Context context) - { - var log = Log.Invoke(); - log.Invoke(Message.Invoke(context)); - return ToInvoke.Invoke(context); - } + public override Return Invoke(Context context) + { + var log = Log.Invoke(); + log.Invoke(Message.Invoke(context)); + return ToInvoke.Invoke(context); } +} - [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] - public sealed class WriteToLogA : BaseAction - { - [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] - public IFunction Log = null!; +[Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", + Description = "Writes the provided mess to the log and then invokes the next step.")] +public sealed class WriteToLogA : BaseAction +{ + [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] + public IFunction Log = null!; - [SubModule(Required = false, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] - public IAction? ToInvoke; + [SubModule(Required = false, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] + public IAction? ToInvoke; - [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] - public IFunction Message = null!; + [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] + public IFunction Message = null!; - public override void Invoke() - { - var log = Log.Invoke(); - log.Invoke(Message.Invoke()); - ToInvoke?.Invoke(); - } + public override void Invoke() + { + var log = Log.Invoke(); + log.Invoke(Message.Invoke()); + ToInvoke?.Invoke(); } +} - [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] - public sealed class WriteToLogA : BaseAction - { - [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] - public IFunction Log = null!; +[Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", + Description = "Writes the provided mess to the log and then invokes the next step.")] +public sealed class WriteToLogA : BaseAction +{ + [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] + public IFunction Log = null!; - [SubModule(Required = false, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] - public IAction? ToInvoke; + [SubModule(Required = false, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] + public IAction? ToInvoke; - [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] - public IFunction Message = null!; + [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] + public IFunction Message = null!; - public override void Invoke(Context context) - { - var log = Log.Invoke(); - log.Invoke(Message.Invoke()); - ToInvoke?.Invoke(context); - } + public override void Invoke(Context context) + { + var log = Log.Invoke(); + log.Invoke(Message.Invoke()); + ToInvoke?.Invoke(context); } +} - [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] - public sealed class WriteToLogBasedOnContextA : BaseAction - { - [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] - public IFunction Log = null!; +[Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", + Description = "Writes the provided mess to the log and then invokes the next step.")] +public sealed class WriteToLogBasedOnContextA : BaseAction +{ + [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] + public IFunction Log = null!; - [SubModule(Required = true, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] - public IAction ToInvoke = null!; + [SubModule(Required = true, Name = "To Invoke", Description = "The function to execute after writing to the log.", Index = 1, PassesExecution = true)] + public IAction ToInvoke = null!; - [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] - public IFunction Message = null!; + [Parameter(Required = true, Name = "Message", Description = "The message to write to the log.", DefaultValue = "", Index = 2)] + public IFunction Message = null!; - public override void Invoke(Context context) - { - var log = Log.Invoke(); - log.Invoke(Message.Invoke(context)); - ToInvoke.Invoke(context); - } + public override void Invoke(Context context) + { + var log = Log.Invoke(); + log.Invoke(Message.Invoke(context)); + ToInvoke.Invoke(context); } - } From 74f74c5c4b1b347fb52258cdc99599dd9fd2c425 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Thu, 27 Aug 2026 19:08:43 -0400 Subject: [PATCH 11/12] Fixed typos --- src/XTMF2/RuntimeModules/CombineContext.cs | 2 +- src/XTMF2/RuntimeModules/OpenReadStreamFromFile.cs | 2 +- src/XTMF2/RuntimeModules/WriteToLog.cs | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/XTMF2/RuntimeModules/CombineContext.cs b/src/XTMF2/RuntimeModules/CombineContext.cs index 451a312..5145ed5 100644 --- a/src/XTMF2/RuntimeModules/CombineContext.cs +++ b/src/XTMF2/RuntimeModules/CombineContext.cs @@ -92,7 +92,7 @@ public override Return Invoke() [Module(Name = "Combine Context From No Context", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/CombineContextFromNoContext.html", Description = "Combines the contexts as derived from First and Second and invokes To Invoke with the combined context.")] -public sealed class CombineContexF : BaseFunction +public sealed class CombineContextF : BaseFunction { [SubModule(Name = "Second", Required = true, Index = 0, Description = "The second context to use.")] public IFunction? Second; diff --git a/src/XTMF2/RuntimeModules/OpenReadStreamFromFile.cs b/src/XTMF2/RuntimeModules/OpenReadStreamFromFile.cs index 0012534..6379352 100644 --- a/src/XTMF2/RuntimeModules/OpenReadStreamFromFile.cs +++ b/src/XTMF2/RuntimeModules/OpenReadStreamFromFile.cs @@ -67,7 +67,7 @@ public override bool RuntimeValidation(ref string? error) } else { - + error = "No path was given to open a ReadStream from!"; return false; } } diff --git a/src/XTMF2/RuntimeModules/WriteToLog.cs b/src/XTMF2/RuntimeModules/WriteToLog.cs index 5e438ff..dddd0ce 100644 --- a/src/XTMF2/RuntimeModules/WriteToLog.cs +++ b/src/XTMF2/RuntimeModules/WriteToLog.cs @@ -22,7 +22,7 @@ You should have received a copy of the GNU General Public License namespace XTMF2.RuntimeModules; [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] + Description = "Writes the provided message to the log and then invokes the next step.")] public sealed class WriteToLogF : BaseFunction { [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] @@ -43,7 +43,7 @@ public override Return Invoke() } [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] + Description = "Writes the provided message to the log and then invokes the next step.")] public sealed class WriteToLogF : BaseFunction { [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] @@ -64,7 +64,7 @@ public override Return Invoke(Context context) } [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] + Description = "Writes the provided message to the log and then invokes the next step.")] public sealed class WriteToLogBasedOnContextF : BaseFunction { [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] @@ -85,7 +85,7 @@ public override Return Invoke(Context context) } [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] + Description = "Writes the provided message to the log and then invokes the next step.")] public sealed class WriteToLogA : BaseAction { [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] @@ -106,7 +106,7 @@ public override void Invoke() } [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] + Description = "Writes the provided message to the log and then invokes the next step.")] public sealed class WriteToLogA : BaseAction { [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] @@ -127,7 +127,7 @@ public override void Invoke(Context context) } [Module(Name = "Write to Log", DocumentationLink = "https://tmg.utoronto.ca/doc/2.0/xtmf2/modules/XTMF2/RuntimeModules/WriteToLog.html", - Description = "Writes the provided mess to the log and then invokes the next step.")] + Description = "Writes the provided message to the log and then invokes the next step.")] public sealed class WriteToLogBasedOnContextA : BaseAction { [SubModule(Required = true, Name = "Log", Description = "The log that will be written to.", Index = 0)] From 49d234bf6e4ff18e5b3a0c9eac0036d1c1e75a18 Mon Sep 17 00:00:00 2001 From: James Vaughan Date: Mon, 31 Aug 2026 15:53:45 -0400 Subject: [PATCH 12/12] Removed debugging delay --- src/XTMF2.GUI/App.axaml.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/XTMF2.GUI/App.axaml.cs b/src/XTMF2.GUI/App.axaml.cs index ac32b4e..28b9ed9 100644 --- a/src/XTMF2.GUI/App.axaml.cs +++ b/src/XTMF2.GUI/App.axaml.cs @@ -47,8 +47,6 @@ public override void OnFrameworkInitializationCompleted() // Load the XTMF Runtime asynchronously _ = Task.Run(async () => { - // Artificial delay for testing (0.5 seconds) - await Task.Delay(500); try { // Create the XTMF Runtime