Skip to content
30 changes: 6 additions & 24 deletions examples/ted/TedApp.EditCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,20 @@ internal View[] CreateEditMenuItems ()
new MenuItem ("_Find...", "Find text in the current document", Find),
new MenuItem ("_Replace...", "Find and replace text in the current document", Replace),
new Line (),
new MenuItem { Command = Command.Undo, Action = Undo, Key = KeyFor (Command.Undo) },
new MenuItem { Command = Command.Redo, Action = Redo, Key = KeyFor (Command.Redo) },
new MenuItem (Editor, Command.Undo) { Key = KeyFor (Command.Undo) },
new MenuItem (Editor, Command.Redo) { Key = KeyFor (Command.Redo) },
new Line (),
new MenuItem { Command = Command.Cut, Key = KeyFor (Command.Cut) },
new MenuItem { Command = Command.Copy, Key = KeyFor (Command.Copy) },
new MenuItem { Command = Command.Paste, Key = KeyFor (Command.Paste) },
new MenuItem { Command = Command.SelectAll, Action = SelectAll, Key = KeyFor (Command.SelectAll) }
new MenuItem (Editor, Command.Cut) { Key = KeyFor (Command.Cut) },
new MenuItem (Editor, Command.Copy) { Key = KeyFor (Command.Copy) },
new MenuItem (Editor, Command.Paste) { Key = KeyFor (Command.Paste) },
new MenuItem (Editor, Command.SelectAll) { Key = KeyFor (Command.SelectAll) }
];
}

private void Find () { ShowFindReplaceDialog (false); }

private void Replace () { ShowFindReplaceDialog (true); }

private void SelectAll () { Editor.SelectAll (); }

private void Undo ()
{
if (!Editor.ReadOnly)
{
Editor.Document?.UndoStack.Undo ();
}
}

private void Redo ()
{
if (!Editor.ReadOnly)
{
Editor.Document?.UndoStack.Redo ();
}
}

private void ShowFindReplaceDialog (bool selectReplaceTab)
{
if (App is null)
Expand Down
16 changes: 0 additions & 16 deletions examples/ted/TedApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,22 +148,6 @@ public TedApp (bool readOnly = false)
AlignmentModes = AlignmentModes.IgnoreFirstOrLast
};

PopoverMenu editorContextMenu = new (CreateEditMenuItems ())
{
Target = new WeakReference<View> (Editor)
};

Editor.MouseEvent += (_, mouse) =>
{
if (!mouse.Flags.HasFlag (MouseFlags.RightButtonClicked))
{
return;
}

editorContextMenu.MakeVisible (mouse.ScreenPosition);
mouse.Handled = true;
};

menu.Add (new MenuBarItem (Strings.menuFile,
[
new MenuItem { Command = Command.New, Action = New, Key = KeyFor (Command.New) },
Expand Down
18 changes: 14 additions & 4 deletions src/Terminal.Gui.Editor/Editor.Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,19 @@ private void CreateCommandsAndBindings ()
return true;
});

// Context menu — return false when suppressed so the command can bubble.
AddCommand (Command.Context, () =>
{
if (ContextMenu is null)
{
return false;
}

ShowContextMenu ();

return true;
});
Comment thread
tig marked this conversation as resolved.

ApplyKeyBindings (View.DefaultKeyBindings, DefaultKeyBindings);

// Reclaim Tab / Shift+Tab from the framework's default focus-cycling bindings so our
Expand Down Expand Up @@ -537,10 +550,7 @@ private void OverwriteAtOffset (int offset, string text)
}
}

if (fold is not null)
{
fold.IsFolded = !fold.IsFolded;
}
fold?.IsFolded = !fold.IsFolded;

return true;
}
Expand Down
123 changes: 123 additions & 0 deletions src/Terminal.Gui.Editor/Editor.ContextMenu.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.Drawing;
using Terminal.Gui.Input;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;

namespace Terminal.Gui.Editor;

public partial class Editor
{
/// <summary>
/// Gets or sets the built-in context menu shown on right-click or <see cref="Command.Context" />.
/// Defaults to a standard editing menu (Undo, Redo, Cut, Copy, Paste, Select All) whose items
/// are state-aware — mutating items are disabled when <see cref="ReadOnly" /> is <see langword="true" />,
/// Copy / Cut are disabled when there is no selection, and Undo / Redo reflect the undo stack.
/// Set to <see langword="null" /> to suppress the context menu entirely; set to a custom
/// <see cref="PopoverMenu" /> to replace it.
/// </summary>
public PopoverMenu? ContextMenu
{
get;
set
{
field = value;

field?.Target = new WeakReference<View> (this);
}
}

/// <summary>Creates the default editing context menu items using declarative command binding.</summary>
/// <remarks>
/// Each <see cref="MenuItem" /> is constructed with <c>this</c> as the target view and a
/// <see cref="Command" />. The framework resolves title, help text, and key from
/// <c>GlobalResources</c> and routes the command to this <see cref="Editor" /> via command
/// bubbling — no explicit <see cref="MenuItem.Action" /> delegates are needed.
/// </remarks>
private View[] CreateDefaultContextMenuItems ()
{
return
[
new MenuItem (this, Command.Undo),
new MenuItem (this, Command.Redo),
new Line (),
new MenuItem (this, Command.Cut),
new MenuItem (this, Command.Copy),
new MenuItem (this, Command.Paste),
new Line (),
new MenuItem (this, Command.SelectAll)
];
}

/// <summary>
/// Updates the <see cref="View.Enabled" /> state of the context menu items to reflect the current
/// editor state (ReadOnly, selection, clipboard, undo/redo).
/// </summary>
private void UpdateContextMenuState ()
{
if (ContextMenu?.Root is null)
{
return;
}

var hasSelection = HasSelection;
var canUndo = !ReadOnly && _document is { UndoStack.CanUndo: true };
var canRedo = !ReadOnly && _document is { UndoStack.CanRedo: true };
var canPaste = !ReadOnly;
var canCut = !ReadOnly && hasSelection;

foreach (View child in ContextMenu.Root.SubViews)
{
if (child is not MenuItem menuItem)
{
continue;
}

switch (menuItem.Command)
{
case Command.Undo:
menuItem.Enabled = canUndo;

break;
case Command.Redo:
menuItem.Enabled = canRedo;

break;
case Command.Cut:
menuItem.Enabled = canCut;

break;
case Command.Copy:
menuItem.Enabled = hasSelection;

break;
case Command.Paste:
menuItem.Enabled = canPaste;

break;

// Unknown commands (e.g. from a custom ContextMenu) are left untouched
// so the caller's intentional Enabled state is preserved.
}
}
}

/// <summary>
/// Shows the context menu at the given screen position, after updating item state.
/// </summary>
private void ShowContextMenu (Point? screenPosition = null)
{
if (ContextMenu is null)
{
return;
}

UpdateContextMenuState ();
ContextMenu.MakeVisible (screenPosition);
}

/// <summary>Builds and assigns the default context menu.</summary>
private void InitializeDefaultContextMenu ()
{
ContextMenu = new PopoverMenu (CreateDefaultContextMenuItems ());
}
}
61 changes: 38 additions & 23 deletions src/Terminal.Gui.Editor/Editor.Mouse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,9 @@ namespace Terminal.Gui.Editor;

public partial class Editor
{
/// <summary>
/// Which gesture the in-progress left-button drag belongs to. One state instead of a set
/// of fighting "suppress…UntilRelease" booleans: the press classifies the gesture, every
/// subsequent drag/release event dispatches on it. Reset to <see cref="DragMode.Select" />
/// (the neutral default) on release.
/// </summary>
private enum DragMode
{
/// <summary>Plain or Shift drag: extend the primary selection to the drag point.</summary>
Select,

/// <summary>Ctrl+Click add-caret: swallow drag events so they don't move the primary.</summary>
AddCaret,

/// <summary>
/// Alt drag: build a vertical column of carets from press row to drag row. Alt (not
/// VS Code's Shift+Alt) because Windows Terminal reserves Shift+drag for its own
/// forced/block text selection while an app has mouse mode on — see
/// specs/decisions.md DEC-006 and gui-cs/Terminal.Gui#4888.
/// </summary>
ColumnCarets
}
private Point _columnDragAnchor;

private DragMode _dragMode;
private Point _columnDragAnchor;

/// <inheritdoc />
protected override bool OnMouseEvent (Mouse mouse)
Expand All @@ -48,6 +26,20 @@ protected override bool OnMouseEvent (Mouse mouse)
return false;
}

// Right-click → show built-in context menu at the click position.
// When ContextMenu is null the click is left unhandled so it can bubble.
if (mouse.Flags.HasFlag (MouseFlags.RightButtonClicked))
{
if (ContextMenu is null)
{
return false;
}

ShowContextMenu (mouse.ScreenPosition);

return true;
}
Comment thread
tig marked this conversation as resolved.

var shift = mouse.Flags.HasFlag (MouseFlags.Shift);

if (mouse.Flags.HasFlag (MouseFlags.LeftButtonTripleClicked))
Expand Down Expand Up @@ -261,4 +253,27 @@ private CellVisualLine BuildVisualLineForSegment (DocumentLine documentLine, int

return visualLine;
}

/// <summary>
/// Which gesture the in-progress left-button drag belongs to. One state instead of a set
/// of fighting "suppress…UntilRelease" booleans: the press classifies the gesture, every
/// subsequent drag/release event dispatches on it. Reset to <see cref="DragMode.Select" />
/// (the neutral default) on release.
/// </summary>
private enum DragMode
{
/// <summary>Plain or Shift drag: extend the primary selection to the drag point.</summary>
Select,

/// <summary>Ctrl+Click add-caret: swallow drag events so they don't move the primary.</summary>
AddCaret,

/// <summary>
/// Alt drag: build a vertical column of carets from press row to drag row. Alt (not
/// VS Code's Shift+Alt) because Windows Terminal reserves Shift+drag for its own
/// forced/block text selection while an app has mouse mode on — see
/// specs/decisions.md DEC-006 and gui-cs/Terminal.Gui#4888.
/// </summary>
ColumnCarets
}
}
7 changes: 1 addition & 6 deletions src/Terminal.Gui.Editor/Editor.Selection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,12 +290,7 @@ internal int ViewRowToLineNumber (int row)
return 1;
}

if (idx >= visibleLines.Count)
{
return visibleLines[^1];
}

return visibleLines[idx];
return idx >= visibleLines.Count ? visibleLines[^1] : visibleLines[idx];
}

/// <summary>
Expand Down
5 changes: 3 additions & 2 deletions src/Terminal.Gui.Editor/Editor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public Editor ()
CreateCommandsAndBindings ();
OverlayRenderers.Add (new MultiCaretRenderer (this));
Document = new TextDocument ();
InitializeDefaultContextMenu ();
ThemeManager.ThemeChanged += OnThemeChanged;
}

Expand Down Expand Up @@ -680,7 +681,7 @@ private void InvalidateVisualLineCaches (DocumentChangeEventArgs e)
// Net character shift. Cached visual lines store *absolute* element offsets, so a
// same-line-count edit upstream (no newline added/removed) still leaves every
// downstream cached line stale even though its line *number* is unchanged.
var offsetDelta = (insertedText.Length - removedText.Length);
var offsetDelta = insertedText.Length - removedText.Length;

RekeyCache (_defaultVisualLineCache, threshold, lineDelta, removedNewlines, offsetDelta);
RekeyCache (_drawVisualLineCache, threshold, lineDelta, removedNewlines, offsetDelta);
Expand Down Expand Up @@ -972,7 +973,7 @@ private bool TryGetVerticalOffset (int startOffset, int delta, int targetVisualC
return true;
}

var targetLineIndex = (_document.GetLineByOffset (startOffset).LineNumber - 1) + delta;
var targetLineIndex = _document.GetLineByOffset (startOffset).LineNumber - 1 + delta;

if (targetLineIndex < 0 || targetLineIndex > _document.LineCount - 1)
{
Expand Down
Loading
Loading