Skip to content

Commit d3d8d84

Browse files
committed
Add Comment headers and scrolling comment text.
1 parent 2e6236d commit d3d8d84

9 files changed

Lines changed: 334 additions & 19 deletions

File tree

src/XTMF2.GUI/Controls/ModelSystemCanvas.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,13 @@ public sealed partial class ModelSystemCanvas : Control
144144
private CommentBlockViewModel? _editingCommentBlock;
145145
/// <summary>Model-space position and size of the comment editor overlay.</summary>
146146
private double _editingCommentEditorX, _editingCommentEditorY, _editingCommentEditorW, _editingCommentEditorH;
147-
147+
// ── Inline comment header editor ───────────────────────────────
148+
/// <summary>Overlay single-line TextBox used for editing the comment block header.</summary>
149+
private readonly TextBox _commentHeaderEditor;
150+
/// <summary>The comment block whose header is being edited, or <c>null</c> when idle.</summary>
151+
private CommentBlockViewModel? _editingCommentHeaderBlock;
152+
/// <summary>Model-space position and size of the comment header editor overlay.</summary>
153+
private double _editingCommentHeaderEditorX, _editingCommentHeaderEditorY, _editingCommentHeaderEditorW, _editingCommentHeaderEditorH;
148154
// ── Inline name editor ───────────────────────────────────────────────────
149155
/// <summary>Overlay single-line TextBox used for renaming nodes and starts.</summary>
150156
private readonly TextBox _nameEditor;
@@ -167,6 +173,11 @@ public sealed partial class ModelSystemCanvas : Control
167173
private readonly Dictionary<(FunctionInstanceViewModel, FunctionParameterHook), NodeViewModel>
168174
_fiHookInlinedParam = new();
169175
/// <summary>
176+
/// Stores the maximum scroll offset (model-space px) for each comment block, computed during
177+
/// render. Used by the wheel handler to clamp scroll without recomputing the text layout.
178+
/// </summary>
179+
private readonly Dictionary<CommentBlockViewModel, double> _commentMaxScrollOffsets = new();
180+
/// <summary>
170181
/// BasicParameter nodes that are visible on the canvas AND connected via a Single (or
171182
/// FunctionParameterHook) hook, so they can offer a "minimize to inline" button.
172183
/// </summary>
@@ -266,6 +277,26 @@ public ModelSystemCanvas()
266277
LogicalChildren.Add(_commentEditor);
267278
VisualChildren.Add(_commentEditor);
268279

280+
// Build the single-line comment header editor; Enter commits, Escape cancels.
281+
_commentHeaderEditor = new TextBox
282+
{
283+
FontFamily = new Avalonia.Media.FontFamily("Segoe UI, Arial, sans-serif"),
284+
FontSize = CommentHeaderFontSize,
285+
FontWeight = Avalonia.Media.FontWeight.Bold,
286+
Foreground = CommentTextBrush,
287+
Background = new SolidColorBrush(Color.FromArgb(0xF2, 0xFF, 0xD5, 0x1A)),
288+
BorderThickness = new Thickness(1),
289+
BorderBrush = CommentBorderBrush,
290+
Padding = new Thickness(6, 2, 6, 2),
291+
VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center,
292+
IsVisible = false,
293+
};
294+
_commentHeaderEditor.LostFocus += OnCommentHeaderEditorLostFocus;
295+
_commentHeaderEditor.AddHandler(InputElement.KeyDownEvent, OnCommentHeaderEditorKeyDown,
296+
Avalonia.Interactivity.RoutingStrategies.Tunnel);
297+
LogicalChildren.Add(_commentHeaderEditor);
298+
VisualChildren.Add(_commentHeaderEditor);
299+
269300
// Build the single-line name editor; Enter commits, Escape cancels.
270301
_nameEditor = new TextBox
271302
{
@@ -634,6 +665,11 @@ protected override Size MeasureOverride(Size availableSize)
634665
{
635666
_commentEditor.Measure(new Size(_editingCommentEditorW * _scale, _editingCommentEditorH * _scale));
636667
}
668+
// Measure the comment header editor.
669+
if (_editingCommentHeaderBlock is not null)
670+
{
671+
_commentHeaderEditor.Measure(new Size(_editingCommentHeaderEditorW * _scale, _editingCommentHeaderEditorH * _scale));
672+
}
637673
// Measure the name editor.
638674
if (_editingNameElement is not null)
639675
{
@@ -704,6 +740,16 @@ protected override Size ArrangeOverride(Size finalSize)
704740
_editingCommentEditorW * _scale,
705741
_editingCommentEditorH * _scale));
706742
}
743+
// Position the comment header editor over the header band of the block being edited.
744+
if (_editingCommentHeaderBlock is not null)
745+
{
746+
_commentHeaderEditor.FontSize = CommentHeaderFontSize * _scale;
747+
_commentHeaderEditor.Arrange(new Rect(
748+
_editingCommentHeaderEditorX * _scale,
749+
_editingCommentHeaderEditorY * _scale,
750+
_editingCommentHeaderEditorW * _scale,
751+
_editingCommentHeaderEditorH * _scale));
752+
}
707753
// Position the name editor over the element header being renamed.
708754
if (_editingNameElement is not null)
709755
{

src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.Input.cs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,23 @@ protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
238238
e.Handled = true;
239239
return;
240240
}
241+
// If the pointer is over a comment block, scroll its comment body text.
242+
if (_vm is not null)
243+
{
244+
var mpos = ToCanvasPos(e.GetPosition(this));
245+
var commentHit = HitTest(mpos, testComments: true) as CommentBlockViewModel;
246+
if (commentHit is not null)
247+
{
248+
const double scrollStep = 24.0;
249+
double newOffset = commentHit.CommentScrollOffset - e.Delta.Y * scrollStep;
250+
// Clamp to [0, maxScroll] using the last-rendered max (0 if not yet rendered).
251+
_commentMaxScrollOffsets.TryGetValue(commentHit, out double maxScroll);
252+
commentHit.CommentScrollOffset = Math.Min(Math.Max(0.0, newOffset), maxScroll);
253+
InvalidateVisual();
254+
e.Handled = true;
255+
return;
256+
}
257+
}
241258
base.OnPointerWheelChanged(e);
242259
}
243260

@@ -266,12 +283,14 @@ private void OnPointerPressedTunnel(object? sender, PointerPressedEventArgs e)
266283
bool isResizingEditedElement =
267284
ReferenceEquals(resizeHit, _editingParamNode) ||
268285
ReferenceEquals(resizeHit, _editingNameElement) ||
269-
ReferenceEquals(resizeHit, _editingCommentBlock);
286+
ReferenceEquals(resizeHit, _editingCommentBlock) ||
287+
ReferenceEquals(resizeHit, _editingCommentHeaderBlock);
270288

271289
if (!isResizingEditedElement)
272290
{
273291
if (_editingParamNode is not null) CommitParamEdit();
274292
if (_editingCommentBlock is not null) CommitCommentEdit();
293+
if (_editingCommentHeaderBlock is not null) CommitCommentHeaderEdit();
275294
if (_editingNameElement is not null) CommitNameEdit();
276295
}
277296

@@ -320,12 +339,14 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
320339
bool isResizingEditedElement =
321340
ReferenceEquals(resizeHit, _editingParamNode) ||
322341
ReferenceEquals(resizeHit, _editingNameElement) ||
323-
ReferenceEquals(resizeHit, _editingCommentBlock);
342+
ReferenceEquals(resizeHit, _editingCommentBlock) ||
343+
ReferenceEquals(resizeHit, _editingCommentHeaderBlock);
324344

325345
if (!isResizingEditedElement)
326346
{
327347
if (_editingParamNode is not null) CommitParamEdit();
328348
if (_editingCommentBlock is not null) CommitCommentEdit();
349+
if (_editingCommentHeaderBlock is not null) CommitCommentHeaderEdit();
329350
if (_editingNameElement is not null) CommitNameEdit();
330351
}
331352
ClearMultiSelection();
@@ -349,6 +370,7 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
349370
{
350371
if (_editingParamNode is not null) CommitParamEdit();
351372
if (_editingCommentBlock is not null) CommitCommentEdit();
373+
if (_editingCommentHeaderBlock is not null) CommitCommentHeaderEdit();
352374
if (_editingNameElement is not null) CommitNameEdit();
353375
minimizeHit.InlineBasicParameter();
354376
InvalidateAndMeasure();
@@ -391,6 +413,7 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
391413
ReferenceEquals(clickedElement, _editingParamNode) ||
392414
ReferenceEquals(clickedElement, _editingNameElement) ||
393415
ReferenceEquals(clickedElement, _editingCommentBlock) ||
416+
ReferenceEquals(clickedElement, _editingCommentHeaderBlock) ||
394417
(clickedElement is not null && _multiSelection.Contains(clickedElement) &&
395418
((_editingParamNode is not null && _multiSelection.Contains(_editingParamNode)) ||
396419
(_editingNameElement is not null && _multiSelection.Contains(_editingNameElement)) ||
@@ -401,6 +424,7 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
401424
{
402425
if (_editingParamNode is not null) CommitParamEdit();
403426
if (_editingCommentBlock is not null) CommitCommentEdit();
427+
if (_editingCommentHeaderBlock is not null) CommitCommentHeaderEdit();
404428
if (_editingNameElement is not null) CommitNameEdit();
405429
}
406430
}
@@ -466,12 +490,16 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
466490
return;
467491
}
468492

469-
// ── Double-click on a comment block: open the inline comment editor ──
493+
// ── Double-click on a comment block: open the inline comment or header editor ──
470494
var commentHit = HitTest(mpos, testComments: true) as CommentBlockViewModel;
471495
if (commentHit is not null)
472496
{
473497
_vm.SelectElementCommand.Execute(commentHit);
474-
BeginCommentEdit(commentHit);
498+
// Open the header editor when the click lands in the header band; otherwise the comment editor.
499+
if (mpos.Y < commentHit.Y + CommentHeaderHeight)
500+
BeginCommentHeaderEdit(commentHit);
501+
else
502+
BeginCommentEdit(commentHit);
475503
e.Handled = true;
476504
return;
477505
}
@@ -554,6 +582,7 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
554582
// Always commit any open inline edit first.
555583
if (_editingParamNode is not null) CommitParamEdit();
556584
if (_editingCommentBlock is not null) CommitCommentEdit();
585+
if (_editingCommentHeaderBlock is not null) CommitCommentHeaderEdit();
557586
if (_editingNameElement is not null) CommitNameEdit();
558587

559588
if (hit is not null)
@@ -672,7 +701,8 @@ protected override void OnPointerMoved(PointerEventArgs e)
672701
// Only sync inline editor if it's the element being resized
673702
if (ReferenceEquals(_resizing, _editingParamNode) ||
674703
ReferenceEquals(_resizing, _editingNameElement) ||
675-
ReferenceEquals(_resizing, _editingCommentBlock))
704+
ReferenceEquals(_resizing, _editingCommentBlock) ||
705+
ReferenceEquals(_resizing, _editingCommentHeaderBlock))
676706
{
677707
SyncEditingElementPositions();
678708
}
@@ -743,14 +773,16 @@ protected override void OnPointerMoved(PointerEventArgs e)
743773
{
744774
if ((_editingParamNode is not null && _multiSelection.Contains(_editingParamNode)) ||
745775
(_editingNameElement is not null && _multiSelection.Contains(_editingNameElement)) ||
746-
(_editingCommentBlock is not null && _multiSelection.Contains(_editingCommentBlock)))
776+
(_editingCommentBlock is not null && _multiSelection.Contains(_editingCommentBlock)) ||
777+
(_editingCommentHeaderBlock is not null && _multiSelection.Contains(_editingCommentHeaderBlock)))
747778
{
748779
SyncEditingElementPositions();
749780
}
750781
}
751782
else if (ReferenceEquals(_dragging, _editingParamNode) ||
752783
ReferenceEquals(_dragging, _editingNameElement) ||
753-
ReferenceEquals(_dragging, _editingCommentBlock))
784+
ReferenceEquals(_dragging, _editingCommentBlock) ||
785+
ReferenceEquals(_dragging, _editingCommentHeaderBlock))
754786
{
755787
SyncEditingElementPositions();
756788
}

src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.NameCommentEditing.cs

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ private void BeginNameEdit(ICanvasElement element)
3434
{
3535
CommitParamEdit();
3636
CommitCommentEdit();
37+
CommitCommentHeaderEdit();
3738

3839
bool isLight = Application.Current?.ActualThemeVariant == ThemeVariant.Light;
3940
_nameEditor.Foreground = isLight ? Brushes.Black : Brushes.White;
@@ -179,14 +180,15 @@ public void BeginCommentEditForSelected()
179180
}
180181
}
181182

182-
/// <summary>Shows the multi-line comment editor over <paramref name="comment"/>.</summary>
183+
/// <summary>Shows the multi-line comment editor over <paramref name="comment"/>, positioned below the header band.</summary>
183184
private void BeginCommentEdit(CommentBlockViewModel comment)
184185
{
186+
CommitCommentHeaderEdit();
185187
_editingCommentBlock = comment;
186188
_editingCommentEditorX = comment.X;
187-
_editingCommentEditorY = comment.Y;
189+
_editingCommentEditorY = comment.Y + CommentHeaderHeight;
188190
_editingCommentEditorW = comment.Width;
189-
_editingCommentEditorH = comment.Height;
191+
_editingCommentEditorH = comment.Height - CommentHeaderHeight;
190192
_commentEditor.Text = comment.Name; // Name returns the underlying Comment text.
191193
// Pick colours based on the active theme.
192194
bool isLight = Application.Current?.ActualThemeVariant == ThemeVariant.Light;
@@ -252,4 +254,75 @@ private void OnCommentEditorLostFocus(object? sender, Avalonia.Interactivity.Rou
252254
}
253255
}
254256

257+
// ── Inline comment header editor helpers ────────────────────────────
258+
259+
/// <summary>Shows the single-line header editor over the header band of <paramref name="comment"/>.</summary>
260+
internal void BeginCommentHeaderEdit(CommentBlockViewModel comment)
261+
{
262+
CommitCommentEdit();
263+
_editingCommentHeaderBlock = comment;
264+
_editingCommentHeaderEditorX = comment.X;
265+
_editingCommentHeaderEditorY = comment.Y;
266+
// Leave room for the fold corner at the top-right.
267+
_editingCommentHeaderEditorW = comment.Width - CommentFoldSize;
268+
_editingCommentHeaderEditorH = CommentHeaderHeight;
269+
_commentHeaderEditor.Text = comment.Header;
270+
bool isLight = Application.Current?.ActualThemeVariant == ThemeVariant.Light;
271+
_commentHeaderEditor.Foreground = isLight ? CommentTextBrush : Brushes.White;
272+
_commentHeaderEditor.Background = isLight
273+
? new SolidColorBrush(Color.FromArgb(0xF2, 0xFF, 0xD5, 0x1A))
274+
: new SolidColorBrush(Color.FromRgb(0x2A, 0x20, 0x00));
275+
_commentHeaderEditor.IsVisible = true;
276+
InvalidateMeasure();
277+
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
278+
{
279+
_commentHeaderEditor.Focus();
280+
_commentHeaderEditor.SelectAll();
281+
}, Avalonia.Threading.DispatcherPriority.Render);
282+
}
283+
284+
/// <summary>Saves the header editor text and closes the editor.</summary>
285+
internal void CommitCommentHeaderEdit()
286+
{
287+
if (_editingCommentHeaderBlock is null) return;
288+
var comment = _editingCommentHeaderBlock;
289+
var text = (_commentHeaderEditor.Text ?? string.Empty).Trim();
290+
_editingCommentHeaderBlock = null;
291+
_commentHeaderEditor.IsVisible = false;
292+
comment.SetHeader(text);
293+
InvalidateAndMeasure();
294+
}
295+
296+
/// <summary>Discards the header edit without saving.</summary>
297+
private void CancelCommentHeaderEdit()
298+
{
299+
_editingCommentHeaderBlock = null;
300+
_commentHeaderEditor.IsVisible = false;
301+
InvalidateAndMeasure();
302+
Focus();
303+
}
304+
305+
private void OnCommentHeaderEditorKeyDown(object? sender, KeyEventArgs e)
306+
{
307+
if (e.Key is Key.Return or Key.Enter)
308+
{
309+
CommitCommentHeaderEdit();
310+
Focus();
311+
e.Handled = true;
312+
}
313+
else if (e.Key == Key.Escape)
314+
{
315+
CancelCommentHeaderEdit();
316+
e.Handled = true;
317+
}
318+
}
319+
320+
private void OnCommentHeaderEditorLostFocus(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
321+
{
322+
if (_inDragOrResize) return;
323+
if (_editingCommentHeaderBlock is not null)
324+
{
325+
CommitCommentHeaderEdit();
326+
}
327+
}
255328
}

src/XTMF2.GUI/Controls/ModelSystemCanvas/ModelSystemCanvas.ParamEditing.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,9 +751,18 @@ private void SyncEditingElementPositions()
751751
if (_editingCommentBlock is not null)
752752
{
753753
_editingCommentEditorX = _editingCommentBlock.X;
754-
_editingCommentEditorY = _editingCommentBlock.Y;
754+
_editingCommentEditorY = _editingCommentBlock.Y + CommentHeaderHeight;
755755
_editingCommentEditorW = _editingCommentBlock.Width;
756-
_editingCommentEditorH = _editingCommentBlock.Height;
756+
_editingCommentEditorH = _editingCommentBlock.Height - CommentHeaderHeight;
757+
}
758+
759+
// Sync comment header editor position/size if editing
760+
if (_editingCommentHeaderBlock is not null)
761+
{
762+
_editingCommentHeaderEditorX = _editingCommentHeaderBlock.X;
763+
_editingCommentHeaderEditorY = _editingCommentHeaderBlock.Y;
764+
_editingCommentHeaderEditorW = _editingCommentHeaderBlock.Width - CommentFoldSize;
765+
_editingCommentHeaderEditorH = CommentHeaderHeight;
757766
}
758767
}
759768

0 commit comments

Comments
 (0)