#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 ();
}
/// Creates a new tab with initial text
/// File that was read or null if a new blank document
///
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}";
}
};
}
/// The text of the tab the last time it was saved
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 ('*');
}
}
}