Files
Terminal.Gui/UICatalog/Scenarios/BackgroundWorkerCollection.cs
Tig dcb3b359ad Fixes #2926 - Refactor KeyEvent and KeyEventEventArgs to simplify (#2927)
* Adds basic MainLoop unit tests

* Remove WinChange action from Curses

* Remove WinChange action from Curses

* Remove ProcessInput action from Windows MainLoop

* Simplified MainLoop/ConsoleDriver by making MainLoop internal and moving impt fns to Application

* Modernized Terminal resize events

* Modernized Terminal resize events

* Removed un used property

* for _isWindowsTerminal devenv->wininit; not sure what changed

* Modernized mouse/keyboard events (Action->EventHandler)

* Updated OnMouseEvent API docs

* Using WT_SESSION to detect WT

* removes hacky GetParentProcess

* Updates to fix #2634 (clear last line)

* removes hacky GetParentProcess2

* Addressed mac resize issue

* Addressed mac resize issue

* Removes ConsoleDriver.PrepareToRun, has Init return MainLoop

* Removes unneeded Attribute methods

* Removed GetProcesssName

* Removed GetProcesssName

* Refactored KeyEvent and KeyEventEventArgs into a single class

* Revert "Refactored KeyEvent and KeyEventEventArgs into a single class"

This reverts commit 88a00658db.

* Fixed key repeat issue; reverted stupidity on 1049/1047 confusion

* Updated CSI API Docs

* merge

* Rearranged Event.cs to Keyboard.cs and Mouse.cs

* Renamed KeyEventEventArgs KeyEventArgs

* temp renamed KeyEvent OldKeyEvent

* Merged KeyEvent into KeyEventArgs

* Renamed Application.ProcessKey members

* Renamed Application.ProcessKey members

* Renamed Application.ProcessKey members

* Added Responder.KeyPressed

* Removed unused references

* Fixed arg naming

* InvokeKeybindings->InvokeKeyBindings

* InvokeKeybindings->InvokeKeyBindings

* Fixed unit tests fail

* More progress on refactoring key input; still broken and probably wrong

* Moved OnKeyPressed out of Responder and made ProcessKeyPrssed non-virtual

* Updated API docs

* Moved key handling from Responder to View

* Updated API docs

* Updated HotKey API docs

* Updated shortcut API docs

* Fixed responder unit tests

* Removed Shortcut from View as it is not used

* Removed unneeded OnHotKey override from Button

* Fixed BackTab logic

* Button now uses Key Bindings exclusively

* Button now uses Key Bindings exclusively

* Updated keyboard.md docs

* Fixed unit tests to account for Toplevel handling default button

* Added View.InvokeCommand API

* Modernized RadioGroup

* Removed ColdKey

* Modernized (partially) StatusBar

* Worked around FileDialog issue with Ctrl-F

* Fixed driver unit test; view must be focused to reciev key pressed

* Application code cleanup

* Start on updaing menu

* Menu now mostly works

* Menu Select refinement

* Fixed known menu bugs!

* Enabled HotKey to cause focus- experimental

* Fixes #3022 & adds unit test to prove it

* Actually Fixes #3022 & adds unit test to prove it

* Working through hotkey issues

* Misc fixes

* removed hot/cold key stuff from Keys scenario

* Fixed scenarios

* Simplified shortcut string handling

* Modernized Checkbox

* Modernized TileView

* Updated API docs

* Updated API docs

* attempting to publish v2 docs

* Revert "attempting to publish v2 docs"

This reverts commit 59dcec111b.

* Playing with api docs

* Removed Key.BackTab

* Removed Caps/Scroll/Numlock

* Partial removal of keymodifiers - unit tests pass

* Partial removal of keymodifiers - broke netdriver somewhere

* WindowsDriver & added KeyEventArgsTests

* Fixing menu shortcut/hotkeys - broke Menu.cs into separate files

* Fixed MenuBar!

* Finished modernizing Menu/MenuBar

* Removed Key.a-z. Broke lots of stuff

* checkout@v4

* progress on key mapping and formatting

* VK tests are still failing

* Fixed some unit tests

* Added Hotkey and Keybinding unit tests

* fixed unit test

* All unit tests pass again...

* Fixed broken unit tests

* KeyEventArgs.KeyValue -> AsRune

* Fixed bugs. Still some broken

* Added KeyEventArgs.IsAlpha. Added KeyEventArgs.cast ops. Fixed bugs. Unit tests pass

* Fixed WindowsDriver

* Oops.

* Refactoring based on bdisp's help. Not complete!

* removed calling into subviews from OnKeyBindings

* removed calling into subviews from OnKeyBindings

* Improved View KeyEvent unit tests

* More hotkey unit tests

* BIg change - Got rid of KeyPress w/in Application/Drivers

* Unit tests now pass again

* Refreshed API docs

* Better HotKey logic. More progress. Getting close.

* Fixed handling of shifted chars like ö

* Minor code cleanup

* Minor code cleanup2

* Why is build Action failing?

* Why is build Action failing??

* upgraded to .net8 to try to fix weird CI/CD build errors

* upgraded to .net8 to try to fix weird CI/CD build errors2

* Disabling TextViewTests to diagnose build errors

* reenable TextViewTests to diagnose build errors

* Arrrrrrg

* Merged v2_develop

* Fixed uppercase accented keys in WindowsDriver

* Fixed key binding api docs

* Experimental impl of CommandScope.SubViews for MenuBar

* Removed dead code from application.cs

* Removed dead code from application.cs

* Removed dead code from ConsoleDriver.cs

* Cleaned up some key binding stuff

* Disabled Alt to activate menu for now

* Updated label commands

* Fixed menu bugs. Upgraded menu unit tests

* Fixed unit tests

* Working on NetDriver

* fixed netdriver

* Fixed issues called out by @bdisp CR

* fixed CursesDriver

* added todo to netdriver

* Cherry picked treeview test fix 1b415e5

* Fix NetDriver.

* CommandScope->KeyBindingScope

* Address some tznind feedback

* Refactored KeyBindings big time!

* Added key consts to KeyEventArgs and renamed Key to ConsoleDriverKey

* Fixed some API docs

* Moved ConsoleDriverKey to ConsoleDriver.cs

* Renamed Key->ConsoleDriverKey

* Renamed Key->ConsoleDriverKey

* Renamed Key->ConsoleDriverKey

* renamed file I forgot to rename before

* Updated name and API docs of KeyEventArgs.isAlpha

* Fixed issues with OnKeyUp not doing the right thing.

* Fixed MainLoop.Running never being used

* Fixed MainLoop.Running never being used - unit tests

* Claned up BUGBUG comments

* Disabled a unit test to see why ci/cd tests are failing

* Removed defunct commented code

* Removed more defunct commented code

* Re-eanbled unit test; jsut removing one test case...

* Disabled more...

* Renambed Global->Applicaton and updated scope API docs

* Disabled more unit tests...

* Removed dead code

* Disabled more unit tests...2

* Disabled more unit tests...3

* Renambed Global->Applicaton and updated scope API docs 2

* Added more KeyBinding scope tests

* Added more KeyBinding scope tests2

* ConsoleDriverKey too long. Key too ambiguous. Settled on KeyCode. (Partialy because eventually I want to intro a class named Key).

* KeyEventArgs improvements. cast to Rune must be explicit as it's lossy

* Fixed warnings

* Renamed KeyEventArgs to Key... progress on fixing broken stuff that resulted

* Fix ConsoleKeyMapping bugs.

* Fix NetDriver issue from converting a lower case to a upper case.

* Started migration to Key from KeyCode - e.g. made HotKeys all consistent.

* Fixed build warnings

* Added key defns to Key

* KeyBindings now uses Key vs. KeyCode

* Verified by tweaking UICatalog

* Fixed treeview test ... again

* Renamed ProcessKeyDown/Up to NewKeyDown/Up and OnKeyPressed to OnProcessKeyDown to make things more clear

* Added test AllViews_KeyDown_All_EventsFire unit tests and fixed a few Views that were wrong

* fixed stupid KeyUp event bug

* If key not handled, return false for datefield

* dotnet test --no-restore --verbosity diag

* dotnet test --blame

* run tests on windows

* Fix TestVKPacket unit test and move it to ConsoleKeyMappingTests.cs file.

* Remove unnecessary commented code.

* Tweaked unit tests and removed Key.BareKey

* Fixed little details and updated api docs

* updated api docs

* AddKeyBindingsForHotKey: KeyCode->Key

* Cleaned up more old KeyCode usages. Added TODOs

---------

Co-authored-by: BDisp <bd.bdisp@gmail.com>
2023-12-16 12:04:23 -07:00

387 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Terminal.Gui;
namespace UICatalog.Scenarios {
[ScenarioMetadata (Name: "BackgroundWorker Collection", Description: "A persisting multi Toplevel BackgroundWorker threading")]
[ScenarioCategory ("Threading")]
[ScenarioCategory ("Top Level Windows")]
[ScenarioCategory ("Dialogs")]
[ScenarioCategory ("Controls")]
public class BackgroundWorkerCollection : Scenario {
public override void Run ()
{
Application.Run<OverlappedMain> ();
}
class OverlappedMain : Toplevel {
private WorkerApp workerApp;
private bool canOpenWorkerApp;
MenuBar menu;
public OverlappedMain ()
{
Data = "OverlappedMain";
IsOverlappedContainer = true;
workerApp = new WorkerApp () { Visible = false };
menu = new MenuBar (new MenuBarItem [] {
new MenuBarItem ("_Options", new MenuItem [] {
new MenuItem ("_Run Worker", "", () => workerApp.RunWorker(), null, null, KeyCode.CtrlMask | KeyCode.R),
new MenuItem ("_Cancel Worker", "", () => workerApp.CancelWorker(), null, null, KeyCode.CtrlMask | KeyCode.C),
null,
new MenuItem ("_Quit", "", () => Quit(), null, null, (KeyCode)Application.QuitKey)
}),
new MenuBarItem ("_View", new MenuItem [] { }),
new MenuBarItem ("_Window", new MenuItem [] { })
}); ;
menu.MenuOpening += Menu_MenuOpening;
Add (menu);
var statusBar = new StatusBar (new [] {
new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()),
new StatusItem(KeyCode.CtrlMask | KeyCode.R, "~^R~ Run Worker", () => workerApp.RunWorker()),
new StatusItem(KeyCode.CtrlMask | KeyCode.C, "~^C~ Cancel Worker", () => workerApp.CancelWorker())
});
Add (statusBar);
Activate += OverlappedMain_Activate;
Deactivate += OverlappedMain_Deactivate;
Closed += OverlappedMain_Closed;
Application.Iteration += (s, a) => {
if (canOpenWorkerApp && !workerApp.Running && Application.OverlappedTop.Running) {
Application.Run (workerApp);
}
};
}
private void OverlappedMain_Closed (object sender, ToplevelEventArgs e)
{
workerApp.Dispose ();
Dispose ();
}
private void Menu_MenuOpening (object sender, MenuOpeningEventArgs menu)
{
if (!canOpenWorkerApp) {
canOpenWorkerApp = true;
return;
}
if (menu.CurrentMenu.Title == "_Window") {
menu.NewMenuBarItem = OpenedWindows ();
} else if (menu.CurrentMenu.Title == "_View") {
menu.NewMenuBarItem = View ();
}
}
private void OverlappedMain_Deactivate (object sender, ToplevelEventArgs top)
{
workerApp.WriteLog ($"{top.Toplevel.Data} deactivate.");
}
private void OverlappedMain_Activate (object sender, ToplevelEventArgs top)
{
workerApp.WriteLog ($"{top.Toplevel.Data} activate.");
}
private MenuBarItem View ()
{
List<MenuItem> menuItems = new List<MenuItem> ();
var item = new MenuItem () {
Title = "WorkerApp",
CheckType = MenuItemCheckStyle.Checked
};
var top = Application.OverlappedChildren?.Find ((x) => x.Data.ToString () == "WorkerApp");
if (top != null) {
item.Checked = top.Visible;
}
item.Action += () => {
var top = Application.OverlappedChildren.Find ((x) => x.Data.ToString () == "WorkerApp");
item.Checked = top.Visible = (bool)!item.Checked;
if (top.Visible) {
Application.MoveToOverlappedChild (top);
} else {
Application.OverlappedTop.SetNeedsDisplay ();
}
};
menuItems.Add (item);
return new MenuBarItem ("_View",
new List<MenuItem []> () { menuItems.Count == 0 ? new MenuItem [] { } : menuItems.ToArray () });
}
private MenuBarItem OpenedWindows ()
{
var index = 1;
List<MenuItem> menuItems = new List<MenuItem> ();
var sortedChildren = Application.OverlappedChildren;
sortedChildren.Sort (new ToplevelComparer ());
foreach (var top in sortedChildren) {
if (top.Data.ToString () == "WorkerApp" && !top.Visible) {
continue;
}
var item = new MenuItem ();
item.Title = top is Window ? $"{index} {((Window)top).Title}" : $"{index} {top.Data}";
index++;
item.CheckType |= MenuItemCheckStyle.Checked;
var topTitle = top is Window ? ((Window)top).Title : top.Data.ToString ();
var itemTitle = item.Title.Substring (index.ToString ().Length + 1);
if (top == Application.GetTopOverlappedChild () && topTitle == itemTitle) {
item.Checked = true;
} else {
item.Checked = false;
}
item.Action += () => {
Application.MoveToOverlappedChild (top);
};
menuItems.Add (item);
}
if (menuItems.Count == 0) {
return new MenuBarItem ("_Window", "", null);
} else {
return new MenuBarItem ("_Window", new List<MenuItem []> () { menuItems.ToArray () });
}
}
private void Quit ()
{
RequestStop ();
}
}
class WorkerApp : Toplevel {
private List<string> log = new List<string> ();
private ListView listLog;
private Dictionary<Staging, BackgroundWorker> stagingWorkers;
private List<StagingUIController> stagingsUI;
public WorkerApp ()
{
Data = "WorkerApp";
Width = Dim.Percent (80);
Height = Dim.Percent (50);
ColorScheme = Colors.Base;
var label = new Label ("Worker collection Log") {
X = Pos.Center (),
Y = 0
};
Add (label);
listLog = new ListView (log) {
X = 0,
Y = Pos.Bottom (label),
Width = Dim.Fill (),
Height = Dim.Fill ()
};
Add (listLog);
}
public void RunWorker ()
{
var stagingUI = new StagingUIController () { Modal = true };
Staging staging = null;
var worker = new BackgroundWorker () { WorkerSupportsCancellation = true };
worker.DoWork += (s, e) => {
var stageResult = new List<string> ();
for (int i = 0; i < 500; i++) {
stageResult.Add (
$"Worker {i} started at {DateTime.Now}");
e.Result = stageResult;
Thread.Sleep (1);
if (worker.CancellationPending) {
e.Cancel = true;
return;
}
}
};
worker.RunWorkerCompleted += (s, e) => {
if (e.Error != null) {
// Failed
WriteLog ($"Exception occurred {e.Error.Message} on Worker {staging.StartStaging}.{staging.StartStaging:fff} at {DateTime.Now}");
} else if (e.Cancelled) {
// Canceled
WriteLog ($"Worker {staging.StartStaging}.{staging.StartStaging:fff} was canceled at {DateTime.Now}!");
} else {
// Passed
WriteLog ($"Worker {staging.StartStaging}.{staging.StartStaging:fff} was completed at {DateTime.Now}.");
Application.Refresh ();
var stagingUI = new StagingUIController (staging, e.Result as List<string>) {
Modal = false,
Title = $"Worker started at {staging.StartStaging}.{staging.StartStaging:fff}",
Data = $"{staging.StartStaging}.{staging.StartStaging:fff}"
};
stagingUI.ReportClosed += StagingUI_ReportClosed;
if (stagingsUI == null) {
stagingsUI = new List<StagingUIController> ();
}
stagingsUI.Add (stagingUI);
stagingWorkers.Remove (staging);
stagingUI.Run ();
}
};
Application.Run (stagingUI);
if (stagingUI.Staging != null && stagingUI.Staging.StartStaging != null) {
staging = new Staging (stagingUI.Staging.StartStaging);
WriteLog ($"Worker is started at {staging.StartStaging}.{staging.StartStaging:fff}");
if (stagingWorkers == null) {
stagingWorkers = new Dictionary<Staging, BackgroundWorker> ();
}
stagingWorkers.Add (staging, worker);
worker.RunWorkerAsync ();
stagingUI.Dispose ();
}
}
private void StagingUI_ReportClosed (StagingUIController obj)
{
WriteLog ($"Report {obj.Staging.StartStaging}.{obj.Staging.StartStaging:fff} closed.");
stagingsUI.Remove (obj);
}
public void CancelWorker ()
{
if (stagingWorkers == null || stagingWorkers.Count == 0) {
WriteLog ($"Worker is not running at {DateTime.Now}!");
return;
}
foreach (var sw in stagingWorkers) {
var key = sw.Key;
var value = sw.Value;
if (!key.Completed) {
value.CancelAsync ();
}
WriteLog ($"Worker {key.StartStaging}.{key.StartStaging:fff} is canceling at {DateTime.Now}!");
stagingWorkers.Remove (sw.Key);
}
}
public void WriteLog (string msg)
{
log.Add (msg);
listLog.MoveDown ();
}
}
class StagingUIController : Window {
private Label label;
private ListView listView;
private Button start;
private Button close;
public Staging Staging { get; private set; }
public event Action<StagingUIController> ReportClosed;
public StagingUIController (Staging staging, List<string> list) : this ()
{
Staging = staging;
label.Text = "Work list:";
listView.SetSource (list);
start.Visible = false;
Id = "";
}
public StagingUIController ()
{
X = Pos.Center ();
Y = Pos.Center ();
Width = Dim.Percent (85);
Height = Dim.Percent (85);
ColorScheme = Colors.Dialog;
Title = "Run Worker";
label = new Label ("Press start to do the work or close to quit.") {
X = Pos.Center (),
Y = 1,
ColorScheme = Colors.Dialog
};
Add (label);
listView = new ListView () {
X = 0,
Y = 2,
Width = Dim.Fill (),
Height = Dim.Fill (2)
};
Add (listView);
start = new Button ("Start") { IsDefault = true, ClearOnVisibleFalse = false };
start.Clicked += (s, e) => {
Staging = new Staging (DateTime.Now);
RequestStop ();
};
Add (start);
close = new Button ("Close");
close.Clicked += OnReportClosed;
Add (close);
KeyDown += (s, e) => {
if (e.KeyCode == KeyCode.Esc) {
OnReportClosed (this, EventArgs.Empty);
}
};
LayoutStarted += (s,e) => {
var btnsWidth = start.Frame.Width + close.Frame.Width + 2 - 1;
var shiftLeft = Math.Max ((Bounds.Width - btnsWidth) / 2 - 2, 0);
shiftLeft += close.Frame.Width + 1;
close.X = Pos.AnchorEnd (shiftLeft);
close.Y = Pos.AnchorEnd (1);
shiftLeft += start.Frame.Width + 1;
start.X = Pos.AnchorEnd (shiftLeft);
start.Y = Pos.AnchorEnd (1);
};
}
private void OnReportClosed (object sender, EventArgs e)
{
if (Staging?.StartStaging != null) {
ReportClosed?.Invoke (this);
}
RequestStop ();
}
public void Run ()
{
Application.Run (this);
}
}
class Staging {
public DateTime? StartStaging { get; private set; }
public bool Completed { get; }
public Staging (DateTime? startStaging, bool completed = false)
{
StartStaging = startStaging;
Completed = completed;
}
}
}
}