diff --git a/COMPILATION_FIXES.md b/COMPILATION_FIXES.md deleted file mode 100644 index ce0d081..0000000 --- a/COMPILATION_FIXES.md +++ /dev/null @@ -1,169 +0,0 @@ -# XTMF2 GUI Compilation Issues - Solutions - -## 1. Rect.Empty and Rect.IsEmpty in Avalonia - -**The Issue**: Errors like "Rect does not contain a definition for Empty" or "IsEmpty" - -**The Solution**: In Avalonia 11.3.14, use the correct Rect-related APIs: - -```csharp -// ✅ CORRECT: Initialize empty Rect -private Rect _sidePanelBounds = Rect.Empty; - -// ✅ CORRECT: Check if Rect is empty -if (!_sidePanelLinkBounds.IsEmpty && _sidePanelLinkBounds.Contains(pos)) -{ - // Rect has bounds and contains the point -} - -// ✅ CORRECT: Create rect with specific bounds -var bounds = new Rect(x, y, width, height); - -// ✅ CORRECT: Zero-size rect -var emptyRect = new Rect(0, 0, 0, 0); // Alternative to Rect.Empty -``` - -**Key Points**: -- `Rect.Empty` is a static property that returns a rect with zero width/height -- `IsEmpty` is a read-only property that returns `true` if Width or Height is 0 or negative -- Requires: `using Avalonia;` in your imports - -**Live Example from Codebase**: -- [ModelSystemCanvas.cs](src/XTMF2.GUI/Controls/ModelSystemCanvas.cs#L192): `private Rect _sidePanelBounds = Rect.Empty;` -- [ModelSystemCanvas.Input.cs](src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs#L661): `if (!_sidePanelLinkBounds.IsEmpty && _sidePanelLinkBounds.Contains(pos) && ...)` - ---- - -## 2. Accessing ModuleRepository from ModelSystemSession in GUI Code - -**The Issue**: `GetModuleRepository()` is marked as `internal`, so it's not directly accessible from external assemblies. - -**The Solution**: There are multiple correct approaches depending on your context: - -### Option A: Use Public Properties (Recommended for GUI Code) -Instead of accessing `ModuleRepository` directly, use the public properties that expose what you need: - -```csharp -// ❌ WRONG: GetModuleRepository() is internal -var repo = modelSystemSession.GetModuleRepository(); - -// ✅ CORRECT: Use public properties -var loadedModuleTypes = modelSystemSession.LoadedModuleTypes; // IObservable -var openGenericTypes = modelSystemSession.OpenGenericModuleTypes; // IReadOnlyList -var allExportedTypes = modelSystemSession.AllExportedTypes; // IReadOnlyList - -// ✅ CORRECT: For module compatibility checking -var compatibleTypes = modelSystemSession.GetCompatibleModuleTypes(hookType); -``` - -**Example from ModelSystemCanvas.Rendering.cs**: -```csharp -// Getting module metadata without direct repo access -private void GetModuleMetadata(Node node) -{ - // _vm.Session is the ModelSystemSession - // Access LoadedModuleTypes through public property - if (node.Type != null && _vm.Session.LoadedModuleTypes.Contains(node.Type)) - { - // Type is valid and loaded - } -} -``` - -### Option B: In Same Assembly (XTMF2.Editing Layer) -If you're within the XTMF2 core assembly, you can call the `internal` method: - -```csharp -// ✅ CORRECT (only in XTMF2.Editing namespace or XTMF2 assembly) -internal ModuleRepository GetModuleRepository() -{ - return _session.GetModuleRepository(); -} -``` - -### Option C: Via Hook Metadata -If you have a Node, you can query hooks through the node itself: - -```csharp -// ✅ CORRECT: Access hooks from a Node -if (node.Type != null && node.Hooks != null) // Hooks populated lazily -{ - foreach (var hook in node.Hooks) - { - // hook.Type, hook.Name, hook.Cardinality, etc. - } -} -``` - ---- - -## 3. Common Patterns in GUI Code - -### Pattern: Get Module Metadata in ModelSystemCanvas -```csharp -// DO NOT: var moduleRepo = _vm.Session.GetModuleRepository(); // ❌ Internal - -// DO: Use public session properties -var module = node?.Type; -if (module != null && _vm.Session.LoadedModuleTypes.Contains(module)) -{ - var hooks = node.Hooks; // Populated lazily by the node - foreach (var hook in hooks) - { - // Render hook information - } -} -``` - -### Pattern: Get Compatible Types for a Hook -```csharp -// ✅ CORRECT: Use public method -var compatibleTypes = _vm.Session.GetCompatibleModuleTypes(hookType); -foreach (var type in compatibleTypes) -{ - // Add to type picker -} -``` - -### Pattern: Access All Exported Types -```csharp -// ✅ CORRECT: Use public property for context type picking -var allTypes = _vm.Session.AllExportedTypes; -// Use for IAction or IFunction context selection -``` - ---- - -## 4. Visibility Rules Summary - -| Member | Scope | Accessibility | GUI Usable? | -|--------|-------|----------------|------------| -| `GetModuleRepository()` | ModelSystemSession | `internal` | ❌ Only in XTMF2 assembly | -| `LoadedModuleTypes` | ModelSystemSession | `public` | ✅ Yes, directly | -| `OpenGenericModuleTypes` | ModelSystemSession | `public` | ✅ Yes, directly | -| `AllExportedTypes` | ModelSystemSession | `public` | ✅ Yes, directly | -| `GetCompatibleModuleTypes()` | ModelSystemSession | `public` | ✅ Yes, directly | -| `Hooks` | Node | `public` | ✅ Yes, lazily populated | - ---- - -## 5. Quick Fix Checklist - -When you get compilation errors, check: - -- [ ] Is `using Avalonia;` present for `Rect` types? -- [ ] Are you using `Rect.Empty` (not `Rect.default` or similar)? -- [ ] Are you accessing ModuleRepository through public session properties? -- [ ] If you need repo data, is it available through `LoadedModuleTypes`, `OpenGenericModuleTypes`, or `GetCompatibleModuleTypes()`? -- [ ] For hook metadata, are you accessing `node.Hooks` directly instead of repo? -- [ ] Are you in the GUI (XTMF2.GUI) or core (XTMF2) assembly? If GUI, don't use `internal` methods. - ---- - -## Files Referenced - -- **Avalonia Version**: 11.3.14 (from XTMF2.GUI.csproj) -- **Working Examples**: - - [ModelSystemCanvas.cs](src/XTMF2.GUI/Controls/ModelSystemCanvas.cs) - - [ModelSystemCanvas.Rendering.cs](src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Rendering.cs) - Line 1370 shows session usage - - [ModelSystemSession.cs](src/XTMF2/Editing/ModelSystemSession.cs) - Lines 77-110 show repository access patterns diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs index 861b6f3..d9e5e21 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas.cs @@ -398,6 +398,8 @@ public ModelSystemCanvas() // ── Resize drag state ───────────────────────────────────────────────── /// The canvas element being resized, or null when not resizing. private ICanvasElement? _resizing; + /// true while a drag or resize operation is in progress (used to prevent inline editor close on focus loss). + private bool _inDragOrResize; /// Pointer position at the start of the resize drag. private Point _resizeStartPos; /// Node rendered width at the start of the resize drag. diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs index 857b331..b021e77 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs @@ -260,11 +260,20 @@ private void OnPointerPressedTunnel(object? sender, PointerPressedEventArgs e) var resizeHit = HitTestResizeHandle(mpos); if (resizeHit is null) return; - // Commit any open editor so its LostFocus handler doesn't fire after - // we capture the pointer, which would interfere with the resize drag. - if (_editingParamNode is not null) CommitParamEdit(); - if (_editingCommentBlock is not null) CommitCommentEdit(); - if (_editingNameElement is not null) CommitNameEdit(); + // Don't commit open editors if we're resizing the element being edited. + // The _inDragOrResize flag will prevent LostFocus handlers from closing them, + // and SyncEditingElementPositions() will keep them synchronized during the resize. + bool isResizingEditedElement = + ReferenceEquals(resizeHit, _editingParamNode) || + ReferenceEquals(resizeHit, _editingNameElement) || + ReferenceEquals(resizeHit, _editingCommentBlock); + + if (!isResizingEditedElement) + { + if (_editingParamNode is not null) CommitParamEdit(); + if (_editingCommentBlock is not null) CommitCommentEdit(); + if (_editingNameElement is not null) CommitNameEdit(); + } ClearMultiSelection(); _resizing = resizeHit; @@ -307,9 +316,18 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) var resizeHit = HitTestResizeHandle(mpos); if (resizeHit is not null) { - if (_editingParamNode is not null) CommitParamEdit(); - if (_editingCommentBlock is not null) CommitCommentEdit(); - if (_editingNameElement is not null) CommitNameEdit(); + // Don't commit editors if we're resizing the element being edited. + bool isResizingEditedElement = + ReferenceEquals(resizeHit, _editingParamNode) || + ReferenceEquals(resizeHit, _editingNameElement) || + ReferenceEquals(resizeHit, _editingCommentBlock); + + if (!isResizingEditedElement) + { + if (_editingParamNode is not null) CommitParamEdit(); + if (_editingCommentBlock is not null) CommitCommentEdit(); + if (_editingNameElement is not null) CommitNameEdit(); + } ClearMultiSelection(); _resizing = resizeHit; _resizeStartPos = mpos; @@ -365,11 +383,26 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) // Clicking elsewhere commits any open edit. // Guard: if the click was already handled by a child (e.g. the variable // autocomplete dropdown's TextBlock items), do not commit the edit. + // Also: if we're about to drag one of the edited elements, keep the editor open. if (!e.Handled) { - if (_editingParamNode is not null) CommitParamEdit(); - if (_editingCommentBlock is not null) CommitCommentEdit(); - if (_editingNameElement is not null) CommitNameEdit(); + var clickedElement = HitTest(mpos, testComments: false); + bool isDraggingEditedElement = + ReferenceEquals(clickedElement, _editingParamNode) || + ReferenceEquals(clickedElement, _editingNameElement) || + ReferenceEquals(clickedElement, _editingCommentBlock) || + (clickedElement is not null && _multiSelection.Contains(clickedElement) && + ((_editingParamNode is not null && _multiSelection.Contains(_editingParamNode)) || + (_editingNameElement is not null && _multiSelection.Contains(_editingNameElement)) || + (_editingCommentBlock is not null && _multiSelection.Contains(_editingCommentBlock)))); + + // Only commit if we're not about to drag one of the edited elements. + if (!isDraggingEditedElement) + { + if (_editingParamNode is not null) CommitParamEdit(); + if (_editingCommentBlock is not null) CommitCommentEdit(); + if (_editingNameElement is not null) CommitNameEdit(); + } } } @@ -632,9 +665,17 @@ protected override void OnPointerMoved(PointerEventArgs e) // ── Resize drag ─────────────────────────────────────────────────── if (_resizing is not null) { + _inDragOrResize = true; var dw = mpos.X - _resizeStartPos.X; var dh = mpos.Y - _resizeStartPos.Y; _resizing.ResizeToPreview(_resizeStartW + dw, _resizeStartH + dh); + // Only sync inline editor if it's the element being resized + if (ReferenceEquals(_resizing, _editingParamNode) || + ReferenceEquals(_resizing, _editingNameElement) || + ReferenceEquals(_resizing, _editingCommentBlock)) + { + SyncEditingElementPositions(); + } InvalidateAndMeasure(); e.Handled = true; return; @@ -675,6 +716,7 @@ protected override void OnPointerMoved(PointerEventArgs e) if (_dragging is null) return; // ── Element drag (single or group) ──────────────────────────────── + _inDragOrResize = true; if (_multiSelection.Count > 1 && _multiSelection.Contains(_dragging)) { // Group drag: preview every element in the multi-selection by the per-frame delta. @@ -696,6 +738,22 @@ protected override void OnPointerMoved(PointerEventArgs e) _dragging.MoveToPreview(newX, newY); } + // Only sync inline editor if it's being dragged as part of the current drag operation + if (_multiSelection.Count > 1) + { + if ((_editingParamNode is not null && _multiSelection.Contains(_editingParamNode)) || + (_editingNameElement is not null && _multiSelection.Contains(_editingNameElement)) || + (_editingCommentBlock is not null && _multiSelection.Contains(_editingCommentBlock))) + { + SyncEditingElementPositions(); + } + } + else if (ReferenceEquals(_dragging, _editingParamNode) || + ReferenceEquals(_dragging, _editingNameElement) || + ReferenceEquals(_dragging, _editingCommentBlock)) + { + SyncEditingElementPositions(); + } InvalidateAndMeasure(); TryAutoScrollForDrag(svPos); e.Handled = true; @@ -733,6 +791,9 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) } } + // ── Drag/resize release ────────────────────────────────────────────── + _inDragOrResize = false; + // ── Right-drag release: complete link creation ──────────────────── if (_linkOrigin is not null) { diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.NameCommentEditing.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.NameCommentEditing.cs index f63730a..27e44cf 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.NameCommentEditing.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.NameCommentEditing.cs @@ -146,6 +146,9 @@ private void OnNameEditorKeyDown(object? sender, KeyEventArgs e) private void OnNameEditorLostFocus(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { + // Don't commit if we're in the middle of a drag/resize operation. + if (_inDragOrResize) return; + if (_editingNameElement is not null) { CommitNameEdit(); @@ -240,6 +243,9 @@ private void OnCommentEditorKeyDown(object? sender, KeyEventArgs e) private void OnCommentEditorLostFocus(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { + // Don't commit if we're in the middle of a drag/resize operation. + if (_inDragOrResize) return; + if (_editingCommentBlock is not null) { CommitCommentEdit(); diff --git a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ParamEditing.cs b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ParamEditing.cs index ac41708..a897132 100644 --- a/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ParamEditing.cs +++ b/src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ParamEditing.cs @@ -684,9 +684,77 @@ private void OnInlineEditorLostFocus(object? sender, Avalonia.Interactivity.Rout // and will immediately return focus to the inline editor. if (_varDropdownVisible) return; + // Don't commit if we're in the middle of a drag/resize operation (focus naturally shifts to canvas during pointer operations). + if (_inDragOrResize) return; + // Commit on focus loss (e.g. user clicks away to another element). if (_editingParamNode is not null) CommitParamEdit(); } + /// + /// Updates the stored inline editor position and size fields to match the current + /// position and size of the element being edited. This is called during element + /// move/resize operations to keep the inline editor textbox synchronized. + /// + private void SyncEditingElementPositions() + { + // Sync parameter editor position/size if editing + if (_editingParamNode is not null) + { + _editingParamEditorX = _editingParamNode.X; + _editingParamEditorY = _editingParamNode.Y + NodeHeaderHeight; + _editingParamEditorW = NodeRenderWidth(_editingParamNode); + } + + // Sync name editor position/size if editing + if (_editingNameElement is not null) + { + if (_editingNameElement is NodeViewModel nvm) + { + _nameEditorX = nvm.X; + _nameEditorY = nvm.Y; + _nameEditorW = NodeRenderWidth(nvm); + _nameEditorH = NodeHeaderHeight; + } + else if (_editingNameElement is StartViewModel svm) + { + _nameEditorX = svm.X - StartViewModel.Radius; + _nameEditorY = svm.Y + StartViewModel.Radius + 2; + _nameEditorW = svm.Diameter + 20; + _nameEditorH = NodeHeaderHeight; + } + else if (_editingNameElement is FunctionTemplateViewModel ftvm) + { + _nameEditorX = ftvm.X; + _nameEditorY = ftvm.Y; + _nameEditorW = ftvm.Width; + _nameEditorH = FtHeaderHeight; + } + else if (_editingNameElement is FunctionInstanceViewModel fivm) + { + _nameEditorX = fivm.X; + _nameEditorY = fivm.Y; + _nameEditorW = fivm.Width; + _nameEditorH = FtHeaderHeight; + } + else if (_editingNameElement is FunctionParameterViewModel fpvm) + { + _nameEditorX = fpvm.X; + _nameEditorY = fpvm.Y; + _nameEditorW = fpvm.Width; + _nameEditorH = FtHeaderHeight; + } + } + + // Sync comment editor position/size if editing + if (_editingCommentBlock is not null) + { + _editingCommentEditorX = _editingCommentBlock.X; + _editingCommentEditorY = _editingCommentBlock.Y; + _editingCommentEditorW = _editingCommentBlock.Width; + _editingCommentEditorH = _editingCommentBlock.Height; + } + } + }