mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
* Pulled from v2_release * Refactor migration guide for Terminal.Gui v2 Restructured and expanded the migration guide to provide a comprehensive resource for transitioning from Terminal.Gui v1 to v2. Key updates include: - Added a Table of Contents for easier navigation. - Summarized major architectural changes in v2, including the instance-based application model, IRunnable architecture, and 24-bit TrueColor support. - Updated examples to reflect new patterns, such as initializers replacing constructors and explicit disposal using `IDisposable`. - Documented changes to the layout system, including the removal of `Absolute`/`Computed` styles and the introduction of `Viewport`. - Standardized event patterns to use `object sender, EventArgs args`. - Detailed updates to the Keyboard, Mouse, and Navigation APIs, including configurable key bindings and viewport-relative mouse coordinates. - Replaced legacy components like `ScrollView` and `ContextMenu` with built-in scrolling and `PopoverMenu`. - Clarified disposal rules and introduced best practices for resource management. - Provided a complete migration example and a summary of breaking changes. This update aims to simplify the migration process by addressing breaking changes, introducing new features, and aligning with modern .NET conventions. * Refactor to use Application.Instance for lifecycle management Replaced all occurrences of `ApplicationImpl.Instance` with the new `Application.Instance` property across the codebase to align with the updated application lifecycle model. Encapsulated the `ApplicationImpl` class by making it `internal`, ensuring it is no longer directly accessible outside its assembly. Introduced the `[Obsolete]` `Application.Instance` property as a backward-compatible singleton for the legacy static `Application` model, while encouraging the use of `Application.Create()` for new code. Updated `MessageBox` methods to use `Application.Instance` for consistent modal dialog management. Improved documentation to reflect these changes and emphasize the transition to the instance-based application model. Performed code cleanup in multiple classes to ensure consistency and maintainability. These changes maintain backward compatibility while preparing the codebase for the eventual removal of the legacy `ApplicationImpl` class. * Fix doc bug * - Removed obsolete `.cd` class diagram files. - Introduced `IRunnable` interface for decoupling component execution. - Added fluent API for running dialogs and retrieving results. - Enhanced `View` with `App` and `Driver` properties for better decoupling. - Improved testability with support for mock and real applications. - Implemented `IDisposable` for proper resource cleanup. - Replaced `RunnableSessionStack` with `SessionStack` for session management. - Updated driver architecture to align with the new model. - Scoped `IKeyboard` to application contexts for modularity. - Updated documentation with migration strategies and best practices. These changes modernize the library, improve maintainability, and align with current development practices.
438 lines
13 KiB
C#
438 lines
13 KiB
C#
#nullable enable
|
|
|
|
namespace UICatalog.Scenarios;
|
|
|
|
[ScenarioMetadata ("Notepad", "Multi-tab text editor using the TabView control.")]
|
|
[ScenarioCategory ("Controls")]
|
|
[ScenarioCategory ("TabView")]
|
|
[ScenarioCategory ("TextView")]
|
|
public class Notepad : Scenario
|
|
{
|
|
private TabView? _focusedTabView;
|
|
private int _numNewTabs = 1;
|
|
private TabView? _tabView;
|
|
public Shortcut? LenShortcut { get; private set; }
|
|
|
|
public override void Main ()
|
|
{
|
|
Application.Init ();
|
|
|
|
Window top = new ()
|
|
{
|
|
BorderStyle = LineStyle.None,
|
|
};
|
|
|
|
// MenuBar
|
|
MenuBar menu = new ();
|
|
|
|
menu.Add (
|
|
new MenuBarItem (
|
|
"_File",
|
|
[
|
|
new MenuItem
|
|
{
|
|
Title = "_New",
|
|
Key = Key.N.WithCtrl.WithAlt,
|
|
Action = New
|
|
},
|
|
new MenuItem
|
|
{
|
|
Title = "_Open",
|
|
Action = Open
|
|
},
|
|
new MenuItem
|
|
{
|
|
Title = "_Save",
|
|
Action = Save
|
|
},
|
|
new MenuItem
|
|
{
|
|
Title = "Save _As",
|
|
Action = () => SaveAs ()
|
|
},
|
|
new MenuItem
|
|
{
|
|
Title = "_Close",
|
|
Action = Close
|
|
},
|
|
new MenuItem
|
|
{
|
|
Title = "_Quit",
|
|
Action = Quit
|
|
}
|
|
]
|
|
)
|
|
);
|
|
|
|
menu.Add (
|
|
new MenuBarItem (
|
|
"_About",
|
|
[
|
|
new MenuItem
|
|
{
|
|
Title = "_About",
|
|
Action = () => MessageBox.Query (Application.Instance, "Notepad", "About Notepad...", "Ok")
|
|
}
|
|
]
|
|
)
|
|
);
|
|
|
|
_tabView = CreateNewTabView ();
|
|
|
|
_tabView.Style.ShowBorder = true;
|
|
_tabView.ApplyStyleChanges ();
|
|
|
|
_tabView.X = 0;
|
|
_tabView.Y = Pos.Bottom (menu);
|
|
_tabView.Width = Dim.Fill ();
|
|
_tabView.Height = Dim.Fill (1);
|
|
|
|
LenShortcut = new (Key.Empty, "Len: ", null);
|
|
|
|
// StatusBar
|
|
StatusBar statusBar = new (
|
|
[
|
|
new (Application.QuitKey, "Quit", Quit),
|
|
new (Key.F2, "Open", Open),
|
|
new (Key.F1, "New", New),
|
|
new (Key.F3, "Save", Save),
|
|
new (Key.F6, "Close", Close),
|
|
LenShortcut
|
|
]
|
|
)
|
|
{
|
|
AlignmentModes = AlignmentModes.IgnoreFirstOrLast
|
|
};
|
|
|
|
top.Add (menu, _tabView, statusBar);
|
|
|
|
_focusedTabView = _tabView;
|
|
_tabView.SelectedTabChanged += TabView_SelectedTabChanged;
|
|
_tabView.HasFocusChanging += (s, e) => _focusedTabView = _tabView;
|
|
|
|
top.IsModalChanged += (s, e) =>
|
|
{
|
|
if (e.Value)
|
|
{
|
|
New ();
|
|
LenShortcut.Title = $"Len:{_focusedTabView?.Text?.Length ?? 0}";
|
|
}
|
|
};
|
|
|
|
Application.Run (top);
|
|
top.Dispose ();
|
|
Application.Shutdown ();
|
|
}
|
|
|
|
public void Save ()
|
|
{
|
|
if (_focusedTabView?.SelectedTab is { })
|
|
{
|
|
Save (_focusedTabView, _focusedTabView.SelectedTab);
|
|
}
|
|
}
|
|
|
|
public void Save (TabView tabViewToSave, Tab tabToSave)
|
|
{
|
|
if (tabToSave is not OpenedFile tab)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (tab.File is null)
|
|
{
|
|
SaveAs ();
|
|
}
|
|
else
|
|
{
|
|
tab.Save ();
|
|
}
|
|
|
|
tabViewToSave.SetNeedsDraw ();
|
|
}
|
|
|
|
public bool SaveAs ()
|
|
{
|
|
if (_focusedTabView?.SelectedTab is not OpenedFile tab)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SaveDialog fd = new ();
|
|
Application.Run (fd);
|
|
|
|
if (string.IsNullOrWhiteSpace (fd.Path) || fd.Canceled)
|
|
{
|
|
fd.Dispose ();
|
|
|
|
return false;
|
|
}
|
|
|
|
tab.File = new (fd.Path);
|
|
tab.Text = fd.FileName;
|
|
tab.Save ();
|
|
|
|
fd.Dispose ();
|
|
|
|
return true;
|
|
}
|
|
|
|
private void Close ()
|
|
{
|
|
if (_focusedTabView?.SelectedTab is { })
|
|
{
|
|
Close (_focusedTabView, _focusedTabView.SelectedTab);
|
|
}
|
|
}
|
|
|
|
private void Close (TabView tv, Tab tabToClose)
|
|
{
|
|
if (tabToClose is not OpenedFile tab)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_focusedTabView = tv;
|
|
|
|
if (tab.UnsavedChanges)
|
|
{
|
|
int? result = MessageBox.Query (Application.Instance,
|
|
"Save Changes",
|
|
$"Save changes to {tab.Text.TrimEnd ('*')}",
|
|
"Yes",
|
|
"No",
|
|
"Cancel"
|
|
);
|
|
|
|
if (result is null || result == 2)
|
|
{
|
|
// user cancelled
|
|
return;
|
|
}
|
|
|
|
if (result == 0)
|
|
{
|
|
if (tab.File is null)
|
|
{
|
|
SaveAs ();
|
|
}
|
|
else
|
|
{
|
|
tab.Save ();
|
|
}
|
|
}
|
|
}
|
|
|
|
// close and dispose the tab
|
|
tv.RemoveTab (tab);
|
|
tab.View?.Dispose ();
|
|
_focusedTabView = tv;
|
|
|
|
// If last tab is closed, open a new one
|
|
if (tv.Tabs.Count == 0)
|
|
{
|
|
New ();
|
|
}
|
|
}
|
|
|
|
private TabView CreateNewTabView ()
|
|
{
|
|
TabView tv = new () { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () };
|
|
|
|
tv.TabClicked += TabView_TabClicked;
|
|
tv.SelectedTabChanged += TabView_SelectedTabChanged;
|
|
tv.HasFocusChanging += (s, e) => _focusedTabView = tv;
|
|
|
|
return tv;
|
|
}
|
|
|
|
private void New () { Open (null!, $"new {_numNewTabs++}"); }
|
|
|
|
private void Open ()
|
|
{
|
|
OpenDialog open = new () { Title = "Open", AllowsMultipleSelection = true };
|
|
|
|
Application.Run (open);
|
|
|
|
bool canceled = open.Canceled;
|
|
|
|
if (!canceled)
|
|
{
|
|
foreach (string path in open.FilePaths)
|
|
{
|
|
if (string.IsNullOrEmpty (path) || !File.Exists (path))
|
|
{
|
|
break;
|
|
}
|
|
|
|
// TODO should open in focused TabView
|
|
Open (new (path), Path.GetFileName (path));
|
|
}
|
|
}
|
|
|
|
open.Dispose ();
|
|
}
|
|
|
|
/// <summary>Creates a new tab with initial text</summary>
|
|
/// <param name="fileInfo">File that was read or null if a new blank document</param>
|
|
/// <param name="tabName"></param>
|
|
private void Open (FileInfo? fileInfo, string tabName)
|
|
{
|
|
if (_focusedTabView is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OpenedFile tab = new (this) { DisplayText = tabName, File = fileInfo };
|
|
tab.View = tab.CreateTextView (fileInfo);
|
|
tab.SavedText = tab.View.Text;
|
|
tab.RegisterTextViewEvents (_focusedTabView);
|
|
|
|
_focusedTabView.AddTab (tab, true);
|
|
}
|
|
|
|
private void Quit () { Application.RequestStop (); }
|
|
|
|
private void TabView_SelectedTabChanged (object? sender, TabChangedEventArgs e)
|
|
{
|
|
if (LenShortcut is { })
|
|
{
|
|
LenShortcut.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}";
|
|
}
|
|
|
|
e.NewTab?.View?.SetFocus ();
|
|
}
|
|
|
|
private void TabView_TabClicked (object? sender, TabMouseEventArgs e)
|
|
{
|
|
// we are only interested in right clicks
|
|
if (!e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked))
|
|
{
|
|
return;
|
|
}
|
|
|
|
View [] items;
|
|
|
|
if (e.Tab is null)
|
|
{
|
|
items = [new MenuItem { Title = "Open", Action = Open }];
|
|
}
|
|
else
|
|
{
|
|
var tv = (TabView)sender!;
|
|
|
|
items =
|
|
[
|
|
new MenuItem { Title = "Save", Action = () => Save (_focusedTabView!, e.Tab) },
|
|
new MenuItem { Title = "Close", Action = () => Close (tv, e.Tab) }
|
|
];
|
|
}
|
|
|
|
PopoverMenu contextMenu = new (items);
|
|
|
|
// Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused
|
|
// and the context menu is disposed when it is closed.
|
|
if (sender is TabView tabView && tabView.App?.Popover is { })
|
|
{
|
|
tabView.App.Popover.Register (contextMenu);
|
|
}
|
|
|
|
contextMenu.MakeVisible (e.MouseEvent.ScreenPosition);
|
|
|
|
e.MouseEvent.Handled = true;
|
|
}
|
|
|
|
private class OpenedFile (Notepad notepad) : Tab
|
|
{
|
|
private readonly Notepad _notepad = notepad;
|
|
|
|
public OpenedFile CloneTo (TabView other)
|
|
{
|
|
OpenedFile newTab = new (_notepad) { DisplayText = Text, File = File };
|
|
newTab.View = newTab.CreateTextView (newTab.File);
|
|
newTab.SavedText = newTab.View.Text;
|
|
newTab.RegisterTextViewEvents (other);
|
|
other.AddTab (newTab, true);
|
|
|
|
return newTab;
|
|
}
|
|
|
|
public View CreateTextView (FileInfo? file)
|
|
{
|
|
var initialText = string.Empty;
|
|
|
|
if (file is { Exists: true })
|
|
{
|
|
initialText = System.IO.File.ReadAllText (file.FullName);
|
|
}
|
|
|
|
return new TextView
|
|
{
|
|
X = 0,
|
|
Y = 0,
|
|
Width = Dim.Fill (),
|
|
Height = Dim.Fill (),
|
|
Text = initialText,
|
|
AllowsTab = false
|
|
};
|
|
}
|
|
|
|
public FileInfo? File { get; set; }
|
|
|
|
public void RegisterTextViewEvents (TabView parent)
|
|
{
|
|
if (View is not TextView textView)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// when user makes changes rename tab to indicate unsaved
|
|
textView.ContentsChanged += (s, k) =>
|
|
{
|
|
// if current text doesn't match saved text
|
|
bool areDiff = UnsavedChanges;
|
|
|
|
if (areDiff)
|
|
{
|
|
if (!DisplayText.EndsWith ('*'))
|
|
{
|
|
DisplayText = Text + '*';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (DisplayText.EndsWith ('*'))
|
|
{
|
|
DisplayText = Text.TrimEnd ('*');
|
|
}
|
|
}
|
|
|
|
if (_notepad.LenShortcut is { })
|
|
{
|
|
_notepad.LenShortcut.Title = $"Len:{textView.Text.Length}";
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>The text of the tab the last time it was saved</summary>
|
|
public string? SavedText { get; set; }
|
|
|
|
public bool UnsavedChanges => View is { } && !string.Equals (SavedText, View.Text);
|
|
|
|
internal void Save ()
|
|
{
|
|
if (View is null || File is null || string.IsNullOrWhiteSpace (File.FullName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string newText = View.Text;
|
|
|
|
System.IO.File.WriteAllText (File.FullName, newText);
|
|
SavedText = newText;
|
|
|
|
DisplayText = DisplayText.TrimEnd ('*');
|
|
}
|
|
}
|
|
}
|