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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/XTMF2.GUI/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 30 additions & 2 deletions src/XTMF2.GUI/Controls/ModelSystemCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,7 @@ private void TryApplyZoomText()

/// <summary>
/// Scrolls the host <see cref="ScrollViewer"/> when the pointer is within
/// <see cref="AutoScrollZone"/> pixels of any viewport edge during an element drag.
/// <see cref="AutoScrollZone"/> pixels of any viewport edge during a drag.
/// The scroll delta is proportional to how far inside the zone the cursor sits,
/// reaching <see cref="AutoScrollSpeed"/> at the very edge.
/// Also starts/stops the continuous <see cref="_autoScrollTimer"/> based on whether
Expand Down Expand Up @@ -1035,6 +1035,8 @@ 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);
RefreshSelectionRectCurrentPos(svPos);
if (!_autoScrollTimer.IsEnabled)
_autoScrollTimer.Start();
}
Expand All @@ -1052,14 +1054,40 @@ private void TryAutoScrollForDrag(Point svPos)
/// </summary>
private void OnAutoScrollTick(object? sender, EventArgs e)
{
if (_dragging is null)
if (_dragging is null && _linkOrigin is null && _selRectStart is null)
{
_autoScrollTimer.Stop();
return;
}
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 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Link> GetDisableTargetsForClickedLink(LinkViewModel clickedLink)
{
if (_multiLinkSelection.Count <= 1 || !_multiLinkSelection.Contains(clickedLink.UnderlyingLink))
Expand Down Expand Up @@ -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…" };
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" };
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…" };
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…" };
var addFiItem = new MenuItem { Header = CreateShortcutMenuHeader("Add Function Instance…", "Ctrl+I") };
addFiItem.Click += (_, _) => _ = _vm.AddFunctionInstanceAtAsync(spawnPt.X, spawnPt.Y);
bgMenu.Items.Add(addFiItem);

Expand All @@ -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);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ private CanvasElementDto BuildNodeDto(NodeViewModel nvm)
TypeName: node.Type?.AssemblyQualifiedName,
ParameterValue: paramValue,
IsScriptedParam: isScriptedParam,
InlinedChildren: inlined);
InlinedChildren: inlined,
OriginalId: node.Id);
}

/// <summary>
Expand Down Expand Up @@ -159,14 +160,16 @@ 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,
(float)fi.X, (float)fi.Y,
(float)fi.Width, (float)fi.Height,
Name: fi.Name,
TemplateName: fi.TemplateName,
EmbeddedTemplateSnapshot: instanceTemplateSnapshot);
EmbeddedTemplateSnapshot: instanceTemplateSnapshot,
OriginalId: fi.UnderlyingInstance.Id);
break;

case GhostNodeViewModel ghost:
Expand Down Expand Up @@ -209,30 +212,30 @@ 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<CrossNodeLinkDto>();
if (origDto.CrossLinks is null)
{
origDto = origDto with { CrossLinks = crossLinks };
dtos[originIdx] = origDto;
}
crossLinks.Add(new CrossNodeLinkDto(hookName, destName));
crossLinks.Add(new CrossNodeLinkDto(hookName, destName, destNode.Id));
}
}

Expand Down Expand Up @@ -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;
}
}

}
Loading
Loading