From 480a63d2223eae66f2f85a32ebb487174afe5b12 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 5 Jun 2021 00:47:03 +0100 Subject: [PATCH 01/15] Toplevel improvement as a subviews container without frame borders. --- Terminal.Gui/Core/Application.cs | 46 +++- Terminal.Gui/Core/Toplevel.cs | 42 ++-- UICatalog/Scenarios/AllViewsTester.cs | 6 +- UICatalog/Scenarios/BackgroundWorkerSample.cs | 208 ++++++++++++++++++ 4 files changed, 276 insertions(+), 26 deletions(-) create mode 100644 UICatalog/Scenarios/BackgroundWorkerSample.cs diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index 95d3dadada..47324606ab 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -59,7 +59,7 @@ public static class Application { /// The current in use. /// public static ConsoleDriver Driver; - + /// /// The object used for the application on startup () /// @@ -508,6 +508,7 @@ public static RunState Begin (Toplevel toplevel) } toplevels.Push (toplevel); Current = toplevel; + SetCurrentAsTop (); Driver.PrepareToRun (MainLoop, ProcessKeyEvent, ProcessKeyDownEvent, ProcessKeyUpEvent, ProcessMouseEvent); if (toplevel.LayoutStyle == LayoutStyle.Computed) toplevel.SetRelativeLayout (new Rect (0, 0, Driver.Cols, Driver.Rows)); @@ -544,9 +545,10 @@ public static void Shutdown () // Encapsulate all setting of initial state for Application; Having // this in a function like this ensures we don't make mistakes in - // guranteeing that the state of this singleton is deterministic when Init + // guaranteeing that the state of this singleton is deterministic when Init // starts running and after Shutdown returns. - static void ResetState () { + static void ResetState () + { // Shutdown is the bookend for Init. As such it needs to clean up all resources // Init created. Apps that do any threading will need to code defensively for this. // e.g. see Issue #537 @@ -613,6 +615,7 @@ internal static void End (View view) Current = null; } else { Current = toplevels.Peek (); + SetCurrentAsTop (); Refresh (); } } @@ -644,7 +647,7 @@ public static void RunLoop (RunState state, bool wait = true) MainLoop.MainIteration (); Iteration?.Invoke (); - + if (Driver.EnsureCursorVisibility ()) { state.Toplevel.SetNeedsDisplay (); } @@ -692,7 +695,16 @@ public static void Run (Func errorHandler = null) /// public static void Run (Func errorHandler = null) where T : Toplevel, new() { - Init (() => new T ()); + if (_initialized && Driver != null) { + var top = new T (); + if (top.GetType ().BaseType == typeof (Toplevel)) { + Top = top; + } else { + throw new ArgumentException (top.GetType ().BaseType.Name); + } + } else { + Init (() => new T ()); + } Run (Top, errorHandler); } @@ -788,9 +800,7 @@ public class ResizedEventArgs : EventArgs { static void TerminalResized () { var full = new Rect (0, 0, Driver.Cols, Driver.Rows); - Top.Frame = full; - Top.Width = full.Width; - Top.Height = full.Height; + SetToplevelsSize (full); Resized?.Invoke (new ResizedEventArgs () { Cols = full.Width, Rows = full.Height }); Driver.Clip = full; foreach (var t in toplevels) { @@ -800,5 +810,25 @@ static void TerminalResized () } Refresh (); } + + static void SetToplevelsSize (Rect full) + { + foreach (var t in toplevels) { + if (t?.SuperView == null && !t.Modal) { + t.Frame = full; + t.Width = full.Width; + t.Height = full.Height; + } + } + } + + static bool SetCurrentAsTop () + { + if (Current != Top && Current?.SuperView == null && !Current.Modal) { + Top = Current; + return true; + } + return false; + } } } diff --git a/Terminal.Gui/Core/Toplevel.cs b/Terminal.Gui/Core/Toplevel.cs index a933f9a059..a6bc519550 100644 --- a/Terminal.Gui/Core/Toplevel.cs +++ b/Terminal.Gui/Core/Toplevel.cs @@ -112,7 +112,7 @@ public Toplevel () : base () void Initialize () { - ColorScheme = Colors.Base; + ColorScheme = Colors.TopLevel; } /// @@ -142,12 +142,12 @@ public override bool CanFocus { /// /// Gets or sets the menu for this Toplevel /// - public MenuBar MenuBar { get; set; } + public virtual MenuBar MenuBar { get; set; } /// /// Gets or sets the status bar for this Toplevel /// - public StatusBar StatusBar { get; set; } + public virtual StatusBar StatusBar { get; set; } /// public override bool OnKeyDown (KeyEvent keyEvent) @@ -234,7 +234,7 @@ public override bool ProcessKey (KeyEvent keyEvent) old?.SetNeedsDisplay (); Focused?.SetNeedsDisplay (); } else { - FocusNearestView (SuperView?.TabIndexes?.Reverse(), Direction.Backward); + FocusNearestView (SuperView?.TabIndexes?.Reverse (), Direction.Backward); } return true; case Key.Tab | Key.CtrlMask: @@ -265,7 +265,7 @@ public override bool ProcessColdKey (KeyEvent keyEvent) return true; } - if (ShortcutHelper.FindAndOpenByShortcut(keyEvent, this)) { + if (ShortcutHelper.FindAndOpenByShortcut (keyEvent, this)) { return true; } return false; @@ -319,9 +319,7 @@ void FocusNearestView (IEnumerable views, Direction direction) /// public override void Add (View view) { - if (this == Application.Top) { - AddMenuStatusBar (view); - } + AddMenuStatusBar (view); base.Add (view); } @@ -424,10 +422,14 @@ internal void PositionToplevels () } } - private void PositionToplevel (Toplevel top) + /// + /// Virtual method which allow to be overridden to implement specific positions for inherited . + /// + /// The toplevel. + public virtual void PositionToplevel (Toplevel top) { EnsureVisibleBounds (top, top.Frame.X, top.Frame.Y, out int nx, out int ny); - if ((nx != top.Frame.X || ny != top.Frame.Y) && top.LayoutStyle == LayoutStyle.Computed) { + if (top?.SuperView != null && (nx != top.Frame.X || ny != top.Frame.Y) && top.LayoutStyle == LayoutStyle.Computed) { if ((top.X == null || top.X is Pos.PosAbsolute) && top.Bounds.X != nx) { top.X = nx; } @@ -435,11 +437,18 @@ private void PositionToplevel (Toplevel top) top.Y = ny; } } - if (top.StatusBar != null) { - if (ny + top.Frame.Height > top.Frame.Height - (top.StatusBar.Visible ? 1 : 0)) { - if (top.Height is Dim.DimFill) - top.Height = Dim.Fill () - (top.StatusBar.Visible ? 1 : 0); + var statusBar = top?.SuperView != null && top.SuperView is Toplevel toplevel + ? toplevel.StatusBar : null; + + if (statusBar != null) { + if (ny + top.Frame.Height != top.SuperView.Frame.Height - (statusBar.Visible ? 1 : 0)) { + if (top.Height is Dim.DimFill) { + top.Height = Dim.Fill (statusBar.Visible ? 1 : 0); + } } + top.SuperView.LayoutSubviews (); + } + if (top.StatusBar != null) { if (top.StatusBar.Frame.Y != top.Frame.Height - (top.StatusBar.Visible ? 1 : 0)) { top.StatusBar.Y = top.Frame.Height - (top.StatusBar.Visible ? 1 : 0); top.LayoutSubviews (); @@ -451,13 +460,14 @@ private void PositionToplevel (Toplevel top) /// public override void Redraw (Rect bounds) { - if (IsCurrentTop || this == Application.Top) { + if (IsCurrentTop || this == Application.Top || Application.Current.GetType ().BaseType == typeof (Toplevel)) { if (!NeedDisplay.IsEmpty || LayoutNeeded) { Driver.SetAttribute (Colors.TopLevel.Normal); // This is the Application.Top. Clear just the region we're being asked to redraw // (the bounds passed to us). - Clear (bounds); + // Must be the screen-relative region to clear, not the bounds. + Clear (Frame); Driver.SetAttribute (Colors.Base.Normal); PositionToplevels (); diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs index 4cea78d2ff..ee790f0880 100644 --- a/UICatalog/Scenarios/AllViewsTester.cs +++ b/UICatalog/Scenarios/AllViewsTester.cs @@ -366,8 +366,10 @@ View CreateClass (Type type) view.Width = Dim.Percent(75); view.Height = Dim.Percent (75); - // Set the colorscheme to make it stand out - view.ColorScheme = Colors.Base; + // Set the colorscheme to make it stand out if is null by default + if (view.ColorScheme == null) { + view.ColorScheme = Colors.Base; + } // If the view supports a Text property, set it so we have something to look at if (view.GetType ().GetProperty ("Text") != null) { diff --git a/UICatalog/Scenarios/BackgroundWorkerSample.cs b/UICatalog/Scenarios/BackgroundWorkerSample.cs new file mode 100644 index 0000000000..330a36e8b6 --- /dev/null +++ b/UICatalog/Scenarios/BackgroundWorkerSample.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using Terminal.Gui; + +namespace UICatalog { + [ScenarioMetadata (Name: "BackgroundWorker", Description: "A persisting multi Toplevel BackgroundWorker threading")] + [ScenarioCategory ("Threading")] + [ScenarioCategory ("TopLevel")] + [ScenarioCategory ("Dialogs")] + [ScenarioCategory ("Controls")] + class BackgroundWorkerSample : Scenario { + public override void Run () + { + Top.Dispose (); + + Application.Run (); + + Top.Dispose (); + } + } + + public class MainApp : Toplevel { + private List log = new List (); + private ListView listLog; + private Dictionary stagingWorkers; + + public MainApp () + { + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_Options", new MenuItem [] { + new MenuItem ("_Run Worker", "", () => RunWorker(), null, null, Key.CtrlMask | Key.R), + new MenuItem ("_Cancel Worker", "", () => CancelWorker(), null, null, Key.CtrlMask | Key.C), + null, + new MenuItem ("_Quit", "", () => Application.RequestStop (), null, null, Key.CtrlMask | Key.Q) + }) + }); + Add (menu); + + var statusBar = new StatusBar (new [] { + new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Exit", () => Application.RequestStop()), + new StatusItem(Key.CtrlMask | Key.P, "~^R~ Run Worker", () => RunWorker()), + new StatusItem(Key.CtrlMask | Key.P, "~^C~ Cancel Worker", () => CancelWorker()) + }); + Add (statusBar); + + var top = new Toplevel (); + + top.Add (new Label ("Worker Log") { + X = Pos.Center (), + Y = 0 + }); + + listLog = new ListView (log) { + X = 0, + Y = 2, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + top.Add (listLog); + Add (top); + } + + private void RunWorker () + { + var stagingUI = new StagingUIController (); + + var worker = new BackgroundWorker () { WorkerSupportsCancellation = true }; + + worker.DoWork += (s, e) => { + var stageResult = new List (); + for (int i = 0; i < 500; i++) { + stageResult.Add ( + $"Worker {i} started at {DateTime.UtcNow}"); + e.Result = stageResult; + Thread.Sleep (1); + if (worker.CancellationPending) { + e.Cancel = true; + return; + } + } + }; + + worker.RunWorkerCompleted += (s, e) => { + if (e.Error != null) { + // Failed + log.Add ($"Exception occurred {e.Error.Message} on Worker {stagingUI.StartStaging}.{stagingUI.StartStaging:fff} at {DateTime.UtcNow}"); + listLog.SetNeedsDisplay (); + } else if (e.Cancelled) { + // Canceled + log.Add ($"Worker {stagingUI.StartStaging}.{stagingUI.StartStaging:fff} was canceled at {DateTime.UtcNow}!"); + listLog.SetNeedsDisplay (); + } else { + // Passed + log.Add ($"Worker {stagingUI.StartStaging}.{stagingUI.StartStaging:fff} was completed at {DateTime.UtcNow}."); + listLog.SetNeedsDisplay (); + Application.Refresh (); + stagingUI.Load (e.Result as List); + } + stagingWorkers.Remove (stagingUI); + }; + + Application.Run (stagingUI); + + if (stagingUI.StartStaging != null) { + log.Add ($"Worker is started at {stagingUI.StartStaging}.{stagingUI.StartStaging:fff}"); + listLog.SetNeedsDisplay (); + if (stagingWorkers == null) { + stagingWorkers = new Dictionary (); + } + stagingWorkers.Add (stagingUI, worker); + worker.RunWorkerAsync (); + } + } + + private void CancelWorker () + { + if (stagingWorkers.Count == 0) { + log.Add ($"Worker is not running at {DateTime.UtcNow}!"); + listLog.SetNeedsDisplay (); + return; + } + + var eStaging = stagingWorkers.GetEnumerator (); + eStaging.MoveNext (); + var fStaging = eStaging.Current; + var stagingUI = fStaging.Key; + var worker = fStaging.Value; + worker.CancelAsync (); + log.Add ($"Worker {stagingUI.StartStaging}.{stagingUI.StartStaging:fff} is canceling at {DateTime.UtcNow}!"); + listLog.SetNeedsDisplay (); + } + } + + public class StagingUIController : Window { + private Label label; + private ListView listView; + private Button start; + private Button close; + + public DateTime? StartStaging { get; private set; } + + public StagingUIController () + { + X = Pos.Center (); + Y = Pos.Center (); + Width = Dim.Percent (85); + Height = Dim.Percent (85); + + ColorScheme = Colors.Dialog; + Modal = true; + + Title = "Run Worker"; + + label = new Label ("Press start to do the work or close to exit.") { + 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 }; + start.Clicked += () => { + StartStaging = DateTime.UtcNow; + Application.RequestStop (); + }; + Add (start); + + close = new Button ("Close"); + close.Clicked += () => Application.RequestStop (); + Add (close); + + LayoutStarted += (_) => { + var btnsWidth = start.Bounds.Width + close.Bounds.Width + 2 - 1; + var shiftLeft = Math.Max ((Bounds.Width - btnsWidth) / 2 - 2, 0); + + shiftLeft += close.Bounds.Width + 1; + close.X = Pos.AnchorEnd (shiftLeft); + close.Y = Pos.AnchorEnd (1); + + shiftLeft += start.Bounds.Width + 1; + start.X = Pos.AnchorEnd (shiftLeft); + start.Y = Pos.AnchorEnd (1); + }; + + } + + public void Load (List list) + { + var stagingUI = new StagingUIController (); + stagingUI.Title = $"Worker started at {StartStaging}.{StartStaging:fff}"; + stagingUI.label.Text = "Work list:"; + stagingUI.listView.SetSource (list); + stagingUI.start.Visible = false; + + Application.Run (stagingUI); + } + } +} From 4cd694ac78c36874c542e22b802cf5afa6e6dec8 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 11 Jul 2021 00:12:51 +0100 Subject: [PATCH 02/15] Added a Mdi Container feature. --- Terminal.Gui/Core/Application.cs | 455 ++++++++++++++++++++++-- Terminal.Gui/Core/StackExtensions.cs | 196 +++++++++++ Terminal.Gui/Core/Toplevel.cs | 502 +++++++++++++++++++++++++-- Terminal.Gui/Core/View.cs | 55 ++- Terminal.Gui/Core/Window.cs | 71 ---- 5 files changed, 1133 insertions(+), 146 deletions(-) create mode 100644 Terminal.Gui/Core/StackExtensions.cs diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index 47324606ab..0ba5ffd51e 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -10,7 +10,7 @@ // - "Colors" type or "Attributes" type? // - What to surface as "BackgroundCOlor" when clearing a window, an attribute or colors? // -// Optimziations +// Optimizations // - Add rendering limitation to the exposed area using System; using System.Collections; @@ -23,7 +23,7 @@ namespace Terminal.Gui { /// - /// A static, singelton class provding the main application driver for Terminal.Gui apps. + /// A static, singleton class providing the main application driver for Terminal.Gui apps. /// /// /// @@ -55,11 +55,36 @@ namespace Terminal.Gui { /// /// public static class Application { + static Stack toplevels = new Stack (); + /// /// The current in use. /// public static ConsoleDriver Driver; + /// + /// Gets all the Mdi childes which represent all the not modal from the . + /// + public static List MdiChildes { + get { + if (MdiTop != null) { + List mdiChildes = new List (); + foreach (var top in toplevels) { + if (top != MdiTop && !top.Modal) { + mdiChildes.Add (top); + } + } + return mdiChildes; + } + return null; + } + } + + /// + /// The object used for the application on startup which is true. + /// + public static Toplevel MdiTop { get; private set; } + /// /// The object used for the application on startup () /// @@ -125,8 +150,6 @@ public static bool AlwaysSetPosition { /// The main loop. public static MainLoop MainLoop { get; private set; } - static Stack toplevels = new Stack (); - /// /// This event is raised on each iteration of the /// @@ -252,6 +275,9 @@ static void Init (Func topLevelFactory, ConsoleDriver driver = null, I SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); } Top = topLevelFactory (); + if (Top.IsMdiContainer) { + MdiTop = Top; + } Current = Top; _initialized = true; } @@ -349,6 +375,72 @@ static void ProcessKeyUpEvent (KeyEvent ke) } } + static View FindDeepestTop (Toplevel start, int x, int y, out int resx, out int resy) + { + var startFrame = start.Frame; + + if (!startFrame.Contains (x, y)) { + resx = 0; + resy = 0; + return null; + } + + if (toplevels != null) { + int count = toplevels.Count; + if (count > 0) { + var rx = x - startFrame.X; + var ry = y - startFrame.Y; + foreach (var t in toplevels) { + if (t != Current) { + if (t != start && t.Visible && t.Frame.Contains (rx, ry)) { + start = t; + break; + } + } + } + } + } + resx = x - startFrame.X; + resy = y - startFrame.Y; + return start; + } + + static View FindDeepestMdiView (View start, int x, int y, out int resx, out int resy) + { + if (start.GetType ().BaseType != typeof (Toplevel) + && !((Toplevel)start).IsMdiContainer) { + resx = 0; + resy = 0; + return null; + } + + var startFrame = start.Frame; + + if (!startFrame.Contains (x, y)) { + resx = 0; + resy = 0; + return null; + } + + int count = toplevels.Count; + for (int i = count - 1; i >= 0; i--) { + foreach (var top in toplevels) { + var rx = x - startFrame.X; + var ry = y - startFrame.Y; + if (top.Visible && top.Frame.Contains (rx, ry)) { + var deep = FindDeepestView (top, rx, ry, out resx, out resy); + if (deep == null) + return FindDeepestMdiView (top, rx, ry, out resx, out resy); + if (deep != MdiTop) + return deep; + } + } + } + resx = x - startFrame.X; + resy = y - startFrame.Y; + return start; + } + static View FindDeepestView (View start, int x, int y, out int resx, out int resy) { var startFrame = start.Frame; @@ -380,6 +472,18 @@ static View FindDeepestView (View start, int x, int y, out int resx, out int res return start; } + static View FindTopFromView (View view) + { + View top = view?.SuperView != null ? view.SuperView : view; + + while (top?.SuperView != null) { + if (top?.SuperView != null) { + top = top.SuperView; + } + } + return top; + } + internal static View mouseGrabView; /// @@ -441,6 +545,17 @@ static void ProcessMouseEvent (MouseEvent me) } } + if ((view == null || view == MdiTop) && !Current.Modal && MdiTop != null + && me.Flags != MouseFlags.ReportMousePosition && me.Flags != 0) { + + var top = FindDeepestTop (Top, me.X, me.Y, out _, out _); + view = FindDeepestView (top, me.X, me.Y, out rx, out ry); + + if (view != null && view != MdiTop && top != Current) { + MoveCurrent ((Toplevel)top); + } + } + if (view != null) { var nme = new MouseEvent () { X = rx, @@ -473,6 +588,56 @@ static void ProcessMouseEvent (MouseEvent me) } } + // Only return true if the Current has changed. + static bool MoveCurrent (Toplevel top) + { + // The Current is modal and the top is not modal toplevel then + // the Current must be moved above the first not modal toplevel. + if (MdiTop != null && top != MdiTop && top != Current && Current?.Modal == true && !toplevels.Peek ().Modal) { + lock (toplevels) { + toplevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); + } + var index = 0; + var savedToplevels = toplevels.ToArray (); + foreach (var t in savedToplevels) { + if (!t.Modal && t != Current && t != top && t != savedToplevels [index]) { + lock (toplevels) { + toplevels.MoveTo (top, index, new ToplevelEqualityComparer ()); + } + } + index++; + } + return false; + } + // The Current and the top are both not running toplevel then + // the top must be moved above the first not running toplevel. + if (MdiTop != null && top != MdiTop && top != Current && Current?.Running == false && !top.Running) { + lock (toplevels) { + toplevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); + } + var index = 0; + foreach (var t in toplevels.ToArray ()) { + if (!t.Running && t != Current && index > 0) { + lock (toplevels) { + toplevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ()); + } + } + index++; + } + return false; + } + if ((MdiTop != null && top?.Modal == true && toplevels.Peek () != top) + || (MdiTop != null && Current != MdiTop && Current?.Modal == false && top == MdiTop) + || (MdiTop != null && Current?.Modal == false && top != Current) + || (MdiTop != null && Current?.Modal == true && top == MdiTop)) { + lock (toplevels) { + toplevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); + Current = top; + } + } + return true; + } + static bool OutsideFrame (Point p, Rect r) { return p.X < 0 || p.X > r.Width - 1 || p.Y < 0 || p.Y > r.Height - 1; @@ -493,8 +658,12 @@ static bool OutsideFrame (Point p, Rect r) /// public static RunState Begin (Toplevel toplevel) { - if (toplevel == null) + if (toplevel == null) { throw new ArgumentNullException (nameof (toplevel)); + } else if (toplevel.IsMdiContainer && MdiTop != null) { + throw new InvalidOperationException ("Only one Mdi Container is allowed."); + } + var rs = new RunState (toplevel); Init (); @@ -506,18 +675,68 @@ public static RunState Begin (Toplevel toplevel) initializable.BeginInit (); initializable.EndInit (); } - toplevels.Push (toplevel); - Current = toplevel; - SetCurrentAsTop (); + + lock (toplevels) { + if (string.IsNullOrEmpty (toplevel.Id.ToString ())) { + var count = 1; + var id = (toplevels.Count + count).ToString (); + while (toplevels.Count > 0 && toplevels.FirstOrDefault (x => x.Id.ToString () == id) != null) { + count++; + id = (toplevels.Count + count).ToString (); + } + toplevel.Id = (toplevels.Count + count).ToString (); + + toplevels.Push (toplevel); + } else { + var dup = toplevels.FirstOrDefault (x => x.Id.ToString () == toplevel.Id); + if (dup == null) { + toplevels.Push (toplevel); + } + } + + if (toplevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0) { + throw new ArgumentException ("There are duplicates toplevels Id's"); + } + } + if (toplevel.IsMdiContainer) { + MdiTop = toplevel; + Top = MdiTop; + } + + var refreshDriver = true; + if (MdiTop == null || toplevel.IsMdiContainer || (Current?.Modal == false && toplevel.Modal) + || (Current?.Modal == false && !toplevel.Modal) || (Current?.Modal == true && toplevel.Modal)) { + + if (toplevel.Visible) { + Current = toplevel; + SetCurrentAsTop (); + } else { + refreshDriver = false; + } + } else if ((MdiTop != null && toplevel != MdiTop && Current?.Modal == true && !toplevels.Peek ().Modal) + || (MdiTop != null && toplevel != MdiTop && Current?.Running == false)) { + refreshDriver = false; + MoveCurrent (toplevel); + } else { + refreshDriver = false; + MoveCurrent (Current); + } + Driver.PrepareToRun (MainLoop, ProcessKeyEvent, ProcessKeyDownEvent, ProcessKeyUpEvent, ProcessMouseEvent); if (toplevel.LayoutStyle == LayoutStyle.Computed) toplevel.SetRelativeLayout (new Rect (0, 0, Driver.Cols, Driver.Rows)); + toplevel.PositionToplevels (); toplevel.LayoutSubviews (); toplevel.WillPresent (); - toplevel.OnLoaded (); - Redraw (toplevel); - toplevel.PositionCursor (); - Driver.Refresh (); + if (refreshDriver) { + if (MdiTop != null) { + MdiTop.OnChildLoaded (toplevel); + } + toplevel.OnLoaded (); + Redraw (toplevel); + toplevel.PositionCursor (); + Driver.Refresh (); + } return rs; } @@ -531,7 +750,11 @@ public static void End (RunState runState) if (runState == null) throw new ArgumentNullException (nameof (runState)); - runState.Toplevel.OnUnloaded (); + if (MdiTop != null) { + MdiTop.OnChildUnloaded (runState.Toplevel); + } else { + runState.Toplevel.OnUnloaded (); + } runState.Dispose (); } @@ -560,6 +783,7 @@ static void ResetState () toplevels.Clear (); Current = null; Top = null; + MdiTop = null; MainLoop = null; Driver?.End (); @@ -597,8 +821,10 @@ public static void Refresh () Driver.UpdateScreen (); View last = null; foreach (var v in toplevels.Reverse ()) { - v.SetNeedsDisplay (); - v.Redraw (v.Bounds); + if (v.Visible) { + v.SetNeedsDisplay (); + v.Redraw (v.Bounds); + } last = v; } last?.PositionCursor (); @@ -611,6 +837,19 @@ internal static void End (View view) throw new ArgumentException ("The view that you end with must be balanced"); toplevels.Pop (); + (view as Toplevel)?.OnClosed ((Toplevel)view); + + if (MdiTop != null && !((Toplevel)view).Modal && view != MdiTop) { + MdiTop.OnChildClosed (view as Toplevel); + } + + if (toplevels.Count == 1 && Current == MdiTop) { + MdiTop.OnAllChildClosed (); + if (!MdiTop.IsMdiContainer) { + MdiTop = null; + } + } + if (toplevels.Count == 0) { Current = null; } else { @@ -648,17 +887,28 @@ public static void RunLoop (RunState state, bool wait = true) MainLoop.MainIteration (); Iteration?.Invoke (); + EnsureModalAlwaysOnTop (state.Toplevel); + if ((state.Toplevel != Current && Current?.Modal == true) + || (state.Toplevel != Current && Current?.Modal == false)) { + MdiTop?.OnDeactivate (state.Toplevel); + state.Toplevel = Current; + MdiTop?.OnActivate (state.Toplevel); + Top.SetChildNeedsDisplay (); + Refresh (); + } if (Driver.EnsureCursorVisibility ()) { state.Toplevel.SetNeedsDisplay (); } } else if (!wait) { return; } - if (state.Toplevel != Top && (!Top.NeedDisplay.IsEmpty || Top.ChildNeedsDisplay || Top.LayoutNeeded)) { + if (state.Toplevel != Top + && (!Top.NeedDisplay.IsEmpty || Top.ChildNeedsDisplay || Top.LayoutNeeded)) { Top.Redraw (Top.Bounds); state.Toplevel.SetNeedsDisplay (state.Toplevel.Bounds); } - if (!state.Toplevel.NeedDisplay.IsEmpty || state.Toplevel.ChildNeedsDisplay || state.Toplevel.LayoutNeeded) { + if (!state.Toplevel.NeedDisplay.IsEmpty || state.Toplevel.ChildNeedsDisplay || state.Toplevel.LayoutNeeded + || MdiChildNeedsDisplay ()) { state.Toplevel.Redraw (state.Toplevel.Bounds); if (DebugDrawBounds) { DrawBounds (state.Toplevel); @@ -668,7 +918,40 @@ public static void RunLoop (RunState state, bool wait = true) } else { Driver.UpdateCursor (); } + if (state.Toplevel != Top && !state.Toplevel.Modal + && (!Top.NeedDisplay.IsEmpty || Top.ChildNeedsDisplay || Top.LayoutNeeded)) { + Top.Redraw (Top.Bounds); + } + } + } + + static void EnsureModalAlwaysOnTop (Toplevel toplevel) + { + if (!toplevel.Running || toplevel == Current || MdiTop == null || toplevels.Peek ().Modal) { + return; + } + + foreach (var top in toplevels.Reverse ()) { + if (top.Modal && top != Current) { + MoveCurrent (top); + return; + } + } + } + + static bool MdiChildNeedsDisplay () + { + if (MdiTop == null) { + return false; + } + + foreach (var top in toplevels) { + if (top != Current && top.Visible && (!top.NeedDisplay.IsEmpty || top.ChildNeedsDisplay || top.LayoutNeeded)) { + MdiTop.SetChildNeedsDisplay (); + return true; + } } + return false; } internal static bool DebugDrawBounds = false; @@ -698,14 +981,17 @@ public static void Run (Func errorHandler = null) if (_initialized && Driver != null) { var top = new T (); if (top.GetType ().BaseType == typeof (Toplevel)) { - Top = top; + if (MdiTop == null) { + Top = top; + } } else { throw new ArgumentException (top.GetType ().BaseType.Name); } + Run (top, errorHandler); } else { Init (() => new T ()); + Run (Top, errorHandler); } - Run (Top, errorHandler); } /// @@ -735,7 +1021,7 @@ public static void Run (Func errorHandler = null) /// When is null the exception is rethrown, when it returns true the application is resumed and when false method exits gracefully. /// /// - /// The tu run modally. + /// The to run modally. /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). public static void Run (Toplevel view, Func errorHandler = null) { @@ -763,19 +1049,74 @@ public static void Run (Toplevel view, Func errorHandler = null } /// - /// Stops running the most recent . + /// Stops running the most recent or the if provided. /// + /// The toplevel to request stop. /// /// /// This will cause to return. /// /// - /// Calling is equivalent to setting the property on the curently running to false. + /// Calling is equivalent to setting the property on the currently running to false. /// /// - public static void RequestStop () + public static void RequestStop (Toplevel top = null) { - Current.Running = false; + if (MdiTop == null || top == null || (MdiTop == null && top != null)) { + top = Current; + } + + if (MdiTop != null && top.IsMdiContainer && top?.Running == true + && (Current?.Modal == false || (Current?.Modal == true && Current?.Running == false))) { + + MdiTop.RequestStop (); + } else if (MdiTop != null && top != Current && Current?.Running == true && Current?.Modal == true + && top.Modal && top.Running) { + + var ev = new ToplevelClosingEventArgs (Current); + Current.OnClosing (ev); + if (ev.Cancel) { + return; + } + ev = new ToplevelClosingEventArgs (top); + top.OnClosing (ev); + if (ev.Cancel) { + return; + } + Current.Running = false; + top.Running = false; + } else if ((MdiTop != null && top != MdiTop && top != Current && Current?.Modal == false + && Current?.Running == true && !top.Running) + || (MdiTop != null && top != MdiTop && top != Current && Current?.Modal == false + && Current?.Running == false && !top.Running && toplevels.ToArray () [1].Running)) { + + MoveCurrent (top); + } else if (MdiTop != null && Current != top && Current?.Running == true && !top.Running + && Current?.Modal == true && top.Modal) { + // The Current and the top are both modal so needed to set the Current.Running to false too. + Current.Running = false; + } else if (MdiTop != null && Current == top && MdiTop?.Running == true && Current?.Running == true && top.Running + && Current?.Modal == true && top.Modal) { + // The MdiTop was requested to stop inside a modal toplevel which is the Current and top, + // both are the same, so needed to set the Current.Running to false too. + Current.Running = false; + } else { + Toplevel currentTop; + if (top == Current || (Current?.Modal == true && !top.Modal)) { + currentTop = Current; + } else { + currentTop = top; + } + if (!currentTop.Running) { + return; + } + var ev = new ToplevelClosingEventArgs (currentTop); + currentTop.OnClosing (ev); + if (ev.Cancel) { + return; + } + currentTop.Running = false; + } } /// @@ -804,8 +1145,8 @@ static void TerminalResized () Resized?.Invoke (new ResizedEventArgs () { Cols = full.Width, Rows = full.Height }); Driver.Clip = full; foreach (var t in toplevels) { - t.PositionToplevels (); t.SetRelativeLayout (full); + t.PositionToplevels (); t.LayoutSubviews (); } Refresh (); @@ -813,22 +1154,74 @@ static void TerminalResized () static void SetToplevelsSize (Rect full) { - foreach (var t in toplevels) { - if (t?.SuperView == null && !t.Modal) { - t.Frame = full; - t.Width = full.Width; - t.Height = full.Height; + if (MdiTop == null) { + foreach (var t in toplevels) { + if (t?.SuperView == null && !t.Modal) { + t.Frame = full; + t.Width = full.Width; + t.Height = full.Height; + } } + } else { + Top.Frame = full; + Top.Width = full.Width; + Top.Height = full.Height; } } static bool SetCurrentAsTop () { - if (Current != Top && Current?.SuperView == null && !Current.Modal) { + if (MdiTop == null && Current != Top && Current?.SuperView == null && Current?.Modal == false) { Top = Current; return true; } return false; } + + /// + /// Move to the next Mdi child from the . + /// + public static void MoveNext () + { + if (MdiTop != null && !Current.Modal) { + lock (toplevels) { + toplevels.MoveNext (); + while (toplevels.Peek () == MdiTop || !toplevels.Peek ().Visible) { + toplevels.MoveNext (); + } + Current = toplevels.Peek (); + } + } + } + + /// + /// Move to the previous Mdi child from the . + /// + public static void MovePrevious () + { + if (MdiTop != null && !Current.Modal) { + lock (toplevels) { + toplevels.MovePrevious (); + while (toplevels.Peek () == MdiTop || !toplevels.Peek ().Visible) { + lock (toplevels) { + toplevels.MovePrevious (); + } + } + Current = toplevels.Peek (); + } + } + } + + internal static bool ShowChild (Toplevel top) + { + if (top.Visible && MdiTop != null && Current?.Modal == false) { + lock (toplevels) { + toplevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); + Current = top; + } + return true; + } + return false; + } } } diff --git a/Terminal.Gui/Core/StackExtensions.cs b/Terminal.Gui/Core/StackExtensions.cs new file mode 100644 index 0000000000..1d433e33f3 --- /dev/null +++ b/Terminal.Gui/Core/StackExtensions.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; + +namespace Terminal.Gui { + /// + /// Extension of helper to work with specific + /// + public static class StackExtensions { + /// + /// Replaces an stack object values that match with the value to replace. + /// + /// The stack object type. + /// The stack object. + /// Value to replace. + /// Value to replace with to what matches the value to replace. + /// The comparison object. + public static void Replace (this Stack stack, T valueToReplace, + T valueToReplaceWith, IEqualityComparer comparer = null) + { + comparer = comparer ?? EqualityComparer.Default; + + var temp = new Stack (); + while (stack.Count > 0) { + var value = stack.Pop (); + if (comparer.Equals (value, valueToReplace)) { + stack.Push (valueToReplaceWith); + break; + } + temp.Push (value); + } + + while (temp.Count > 0) + stack.Push (temp.Pop ()); + } + + /// + /// Swap two stack objects values that matches with the both values. + /// + /// The stack object type. + /// The stack object. + /// Value to swap from. + /// Value to swap to. + /// The comparison object. + public static void Swap (this Stack stack, T valueToSwapFrom, + T valueToSwapTo, IEqualityComparer comparer = null) + { + comparer = comparer ?? EqualityComparer.Default; + + int index = stack.Count - 1; + T [] stackArr = new T [stack.Count]; + while (stack.Count > 0) { + var value = stack.Pop (); + if (comparer.Equals (value, valueToSwapFrom)) { + stackArr [index] = valueToSwapTo; + } else if (comparer.Equals (value, valueToSwapTo)) { + stackArr [index] = valueToSwapFrom; + } else { + stackArr [index] = value; + } + index--; + } + + for (int i = 0; i < stackArr.Length; i++) + stack.Push (stackArr [i]); + } + + /// + /// Move the first stack object value to the end. + /// + /// The stack object type. + /// The stack object. + public static void MoveNext (this Stack stack) + { + var temp = new Stack (); + var last = stack.Pop (); + while (stack.Count > 0) { + var value = stack.Pop (); + temp.Push (value); + } + temp.Push (last); + + while (temp.Count > 0) + stack.Push (temp.Pop ()); + } + + /// + /// Move the last stack object value to the top. + /// + /// The stack object type. + /// The stack object. + public static void MovePrevious (this Stack stack) + { + var temp = new Stack (); + T first = default; + while (stack.Count > 0) { + var value = stack.Pop (); + temp.Push (value); + if (stack.Count == 1) { + first = stack.Pop (); + } + } + + while (temp.Count > 0) + stack.Push (temp.Pop ()); + stack.Push (first); + } + + /// + /// Find all duplicates stack objects values. + /// + /// The stack object type. + /// The stack object. + /// The comparison object. + /// The duplicates stack object. + public static Stack FindDuplicates (this Stack stack, IEqualityComparer comparer = null) + { + comparer = comparer ?? EqualityComparer.Default; + + var dup = new Stack (); + T [] stackArr = stack.ToArray (); + for (int i = 0; i < stackArr.Length; i++) { + var value = stackArr [i]; + for (int j = i + 1; j < stackArr.Length; j++) { + var valueToFind = stackArr [j]; + if (comparer.Equals (value, valueToFind) && !Contains (dup, valueToFind)) { + dup.Push (value); + } + } + } + + return dup; + } + + /// + /// Check if the stack object contains the value to find. + /// + /// The stack object type. + /// The stack object. + /// Value to find. + /// The comparison object. + /// true If the value was found.false otherwise. + public static bool Contains (this Stack stack, T valueToFind, IEqualityComparer comparer = null) + { + comparer = comparer ?? EqualityComparer.Default; + + foreach (T obj in stack) { + if (comparer.Equals (obj, valueToFind)) { + return true; + } + } + return false; + } + + /// + /// Move the stack object value to the index. + /// + /// The stack object type. + /// The stack object. + /// Value to move. + /// The index where to move. + /// The comparison object. + public static void MoveTo (this Stack stack, T valueToMove, int index = 0, + IEqualityComparer comparer = null) + { + if (index < 0) { + return; + } + + comparer = comparer ?? EqualityComparer.Default; + + var temp = new Stack (); + var toMove = default (T); + var stackCount = stack.Count; + var count = 0; + while (stack.Count > 0) { + var value = stack.Pop (); + if (comparer.Equals (value, valueToMove)) { + toMove = value; + break; + } + temp.Push (value); + count++; + } + + int idx = 0; + while (stack.Count < stackCount) { + if (count - idx == index) { + stack.Push (toMove); + } else { + stack.Push (temp.Pop ()); + } + idx++; + } + } + } +} diff --git a/Terminal.Gui/Core/Toplevel.cs b/Terminal.Gui/Core/Toplevel.cs index a6bc519550..fe525db557 100644 --- a/Terminal.Gui/Core/Toplevel.cs +++ b/Terminal.Gui/Core/Toplevel.cs @@ -16,11 +16,11 @@ namespace Terminal.Gui { /// /// /// Toplevels can be modally executing views, started by calling . - /// They return control to the caller when has + /// They return control to the caller when has /// been called (which sets the property to false). /// /// - /// A Toplevel is created when an application initialzies Terminal.Gui by callling . + /// A Toplevel is created when an application initializes Terminal.Gui by calling . /// The application Toplevel can be accessed via . Additional Toplevels can be created /// and run (e.g. s. To run a Toplevel, create the and /// call . @@ -67,6 +67,90 @@ public class Toplevel : View { /// public event Action Unloaded; + /// + /// Invoked once the Toplevel's becomes the . + /// + public event Action Activate; + + /// + /// Invoked once the Toplevel's ceases to be the . + /// + public event Action Deactivate; + + /// + /// Invoked once the child Toplevel's is closed from the + /// + public event Action ChildClosed; + + /// + /// Invoked once the last child Toplevel's is closed from the + /// + public event Action AllChildClosed; + + /// + /// Invoked once the Toplevel's is being closing from the + /// + public event Action Closing; + + /// + /// Invoked once the Toplevel's is closed from the + /// + public event Action Closed; + + /// + /// Invoked once the child Toplevel's has begin loaded. + /// + public event Action ChildLoaded; + + /// + /// Invoked once the child Toplevel's has begin unloaded. + /// + public event Action ChildUnloaded; + + internal virtual void OnChildUnloaded (Toplevel top) + { + ChildUnloaded?.Invoke (top); + } + + internal virtual void OnChildLoaded (Toplevel top) + { + ChildLoaded?.Invoke (top); + } + + internal virtual void OnClosed (Toplevel top) + { + Closed?.Invoke (top); + } + + internal virtual bool OnClosing (ToplevelClosingEventArgs ev) + { + Closing?.Invoke (ev); + return ev.Cancel; + } + + internal virtual void OnAllChildClosed () + { + AllChildClosed?.Invoke (); + } + + internal virtual void OnChildClosed (Toplevel top) + { + if (IsMdiContainer) { + SetChildNeedsDisplay (); + } + ChildClosed?.Invoke (top); + } + + internal virtual void OnDeactivate (Toplevel activated) + { + Deactivate?.Invoke (activated); + } + + internal virtual void OnActivate (Toplevel deactivated) + { + Activate?.Invoke (deactivated); + } + /// /// Called from before the is redraws for the first time. /// @@ -149,6 +233,20 @@ public override bool CanFocus { /// public virtual StatusBar StatusBar { get; set; } + /// + /// Gets or sets if this Toplevel is a Mdi container. + /// + public bool IsMdiContainer { get; set; } + + /// + /// Gets or sets if this Toplevel is a Mdi child. + /// + public bool IsMdiChild { + get { + return Application.MdiTop != null && Application.MdiTop != this && !Modal; + } + } + /// public override bool OnKeyDown (KeyEvent keyEvent) { @@ -198,7 +296,11 @@ public override bool ProcessKey (KeyEvent keyEvent) switch (ShortcutHelper.GetModifiersKey (keyEvent)) { case Key.Q | Key.CtrlMask: // FIXED: stop current execution of this container - Application.RequestStop (); + if (Application.MdiTop != null) { + Application.MdiTop.RequestStop (); + } else { + Application.RequestStop (); + } break; case Key.Z | Key.CtrlMask: Driver.Suspend (); @@ -239,16 +341,26 @@ public override bool ProcessKey (KeyEvent keyEvent) return true; case Key.Tab | Key.CtrlMask: case Key key when key == Application.AlternateForwardKey: // Needed on Unix - Application.Top.FocusNext (); - if (Application.Top.Focused == null) { + if (Application.MdiTop == null) { Application.Top.FocusNext (); + if (Application.Top.Focused == null) { + Application.Top.FocusNext (); + } + Application.Top.SetNeedsDisplay (); + } else { + MoveNext (); } return true; case Key.Tab | Key.ShiftMask | Key.CtrlMask: case Key key when key == Application.AlternateBackwardKey: // Needed on Unix - Application.Top.FocusPrev (); - if (Application.Top.Focused == null) { + if (Application.MdiTop == null) { Application.Top.FocusPrev (); + if (Application.Top.Focused == null) { + Application.Top.FocusPrev (); + } + Application.Top.SetNeedsDisplay (); + } else { + MovePrevious (); } return true; case Key.L | Key.CtrlMask: @@ -383,7 +495,7 @@ internal void EnsureVisibleBounds (Toplevel top, int x, int y, out int nx, out i //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}"); bool m, s; if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) { - m = Application.Top.MenuBar != null; + m = Application.MdiTop?.MenuBar != null || Application.Top.MenuBar != null; } else { m = ((Toplevel)SuperView).MenuBar != null; } @@ -394,7 +506,9 @@ internal void EnsureVisibleBounds (Toplevel top, int x, int y, out int nx, out i } ny = Math.Max (y, l); if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) { - s = Application.Top.StatusBar != null && Application.Top.StatusBar.Visible; + s = (Application.MdiTop != null && Application.MdiTop.StatusBar != null + && Application.MdiTop.StatusBar.Visible) + || (Application.Top.StatusBar != null && Application.Top.StatusBar.Visible); } else { s = ((Toplevel)SuperView).StatusBar != null && ((Toplevel)SuperView).StatusBar.Visible; } @@ -429,7 +543,8 @@ internal void PositionToplevels () public virtual void PositionToplevel (Toplevel top) { EnsureVisibleBounds (top, top.Frame.X, top.Frame.Y, out int nx, out int ny); - if (top?.SuperView != null && (nx != top.Frame.X || ny != top.Frame.Y) && top.LayoutStyle == LayoutStyle.Computed) { + if ((top?.SuperView != null || Application.MdiTop != null && top != Application.MdiTop) + && (nx > top.Frame.X || ny > top.Frame.Y) && top.LayoutStyle == LayoutStyle.Computed) { if ((top.X == null || top.X is Pos.PosAbsolute) && top.Bounds.X != nx) { top.X = nx; } @@ -437,16 +552,28 @@ public virtual void PositionToplevel (Toplevel top) top.Y = ny; } } - var statusBar = top?.SuperView != null && top.SuperView is Toplevel toplevel - ? toplevel.StatusBar : null; + View superView = null; + StatusBar statusBar = null; + + if (top != Application.MdiTop && Application.MdiTop != null && Application.MdiTop.StatusBar != null) { + superView = Application.MdiTop; + statusBar = Application.MdiTop.StatusBar; + } else if (top?.SuperView != null && top.SuperView is Toplevel toplevel) { + superView = top.SuperView; + statusBar = toplevel.StatusBar; + } if (statusBar != null) { - if (ny + top.Frame.Height != top.SuperView.Frame.Height - (statusBar.Visible ? 1 : 0)) { + if (ny + top.Frame.Height >= superView.Frame.Height - (statusBar.Visible ? 1 : 0)) { if (top.Height is Dim.DimFill) { top.Height = Dim.Fill (statusBar.Visible ? 1 : 0); } } - top.SuperView.LayoutSubviews (); + if (superView == Application.MdiTop) { + top.SetRelativeLayout (superView.Frame); + } else { + superView.LayoutSubviews (); + } } if (top.StatusBar != null) { if (top.StatusBar.Frame.Y != top.Frame.Height - (top.StatusBar.Visible ? 1 : 0)) { @@ -460,30 +587,151 @@ public virtual void PositionToplevel (Toplevel top) /// public override void Redraw (Rect bounds) { - if (IsCurrentTop || this == Application.Top || Application.Current.GetType ().BaseType == typeof (Toplevel)) { - if (!NeedDisplay.IsEmpty || LayoutNeeded) { - Driver.SetAttribute (Colors.TopLevel.Normal); + if (this == Application.MdiTop) { + RedrawMdi (bounds); + } else { + if (!CanBeVisible (this)) { + return; + } + + if (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded) { + Driver.SetAttribute (ColorScheme.Normal); // This is the Application.Top. Clear just the region we're being asked to redraw // (the bounds passed to us). // Must be the screen-relative region to clear, not the bounds. Clear (Frame); Driver.SetAttribute (Colors.Base.Normal); - PositionToplevels (); + } + } - foreach (var view in Subviews) { - if (view.Frame.IntersectsWith (bounds)) { - view.SetNeedsLayout (); - view.SetNeedsDisplay (view.Bounds); + if (LayoutStyle == LayoutStyle.Computed) + SetRelativeLayout (Bounds); + PositionToplevels (); + LayoutSubviews (); + + foreach (var view in Subviews) { + if (view.Frame.IntersectsWith (bounds) && !OutsideTopFrame (this)) { + view.SetNeedsLayout (); + view.SetNeedsDisplay (view.Bounds); + view.Redraw (view.Bounds); + } + } + + ClearLayoutNeeded (); + ClearNeedsDisplay (); + } + + void RedrawMdi (Rect bounds) + { + if (!IsMdiContainer) { + return; + } + + if (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded) { + Driver.SetAttribute (ColorScheme.Normal); + + Clear (Frame); + + Driver.SetAttribute (Colors.Base.Normal); + + if (LayoutStyle == LayoutStyle.Computed) + SetRelativeLayout (Bounds); + PositionToplevels (); + LayoutSubviews (); + + foreach (var top in Application.MdiChildes.AsEnumerable ().Reverse ()) { + if (top.Frame.IntersectsWith (bounds)) { + if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible) { + top.SetNeedsLayout (); + top.SetNeedsDisplay (top.Bounds); + top.Redraw (top.Bounds); } } + } + + ClearLayoutNeeded (); + ClearNeedsDisplay (); + } + } + + bool OutsideTopFrame (Toplevel top) + { + if (top.Frame.X > Driver.Cols || top.Frame.Y > Driver.Rows) { + return true; + } + return false; + } + + // + // FIXED:It does not look like the event is raised on clicked-drag + // need to figure that out. + // + internal static Point? dragPosition; + Point start; + + /// + public override bool MouseEvent (MouseEvent mouseEvent) + { + // FIXED:The code is currently disabled, because the + // Driver.UncookMouse does not seem to have an effect if there is + // a pending mouse event activated. + + int nx, ny; + if (!dragPosition.HasValue && mouseEvent.Flags == (MouseFlags.Button1Pressed)) { + // Only start grabbing if the user clicks on the title bar. + if (mouseEvent.Y == 0) { + start = new Point (mouseEvent.X, mouseEvent.Y); + dragPosition = new Point (); + nx = mouseEvent.X - mouseEvent.OfX; + ny = mouseEvent.Y - mouseEvent.OfY; + dragPosition = new Point (nx, ny); + Application.GrabMouse (this); + } + + //System.Diagnostics.Debug.WriteLine ($"Starting at {dragPosition}"); + return true; + } else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) || + mouseEvent.Flags == MouseFlags.Button3Pressed) { + if (dragPosition.HasValue) { + if (SuperView == null) { + // Redraw the entire app window using just our Frame. Since we are + // Application.Top, and our Frame always == our Bounds (Location is always (0,0)) + // our Frame is actually view-relative (which is what Redraw takes). + // We need to pass all the view bounds because since the windows was + // moved around, we don't know exactly what was the affected region. + Application.Top.SetNeedsDisplay (); + } else { + SuperView.SetNeedsDisplay (); + } + EnsureVisibleBounds (this, mouseEvent.X + (SuperView == null ? mouseEvent.OfX - start.X : Frame.X - start.X), + mouseEvent.Y + (SuperView == null ? mouseEvent.OfY : Frame.Y), out nx, out ny); + + dragPosition = new Point (nx, ny); + LayoutSubviews (); + Frame = new Rect (nx, ny, Frame.Width, Frame.Height); + if (X == null || X is Pos.PosAbsolute) { + X = nx; + } + if (Y == null || Y is Pos.PosAbsolute) { + Y = ny; + } + //System.Diagnostics.Debug.WriteLine ($"nx:{nx},ny:{ny}"); - ClearLayoutNeeded (); - ClearNeedsDisplay (); + // FIXED: optimize, only SetNeedsDisplay on the before/after regions. + SetNeedsDisplay (); + return true; } } - base.Redraw (base.Bounds); + if (mouseEvent.Flags == MouseFlags.Button1Released && dragPosition.HasValue) { + Application.UngrabMouse (); + Driver.UncookMouse (); + dragPosition = null; + } + + //System.Diagnostics.Debug.WriteLine (mouseEvent.ToString ()); + return false; } /// @@ -494,5 +742,209 @@ public virtual void WillPresent () { FocusFirst (); } + + /// + /// Move to the next Mdi child from the . + /// + public virtual void MoveNext () + { + Application.MoveNext (); + } + + /// + /// Move to the previous Mdi child from the . + /// + public virtual void MovePrevious () + { + Application.MovePrevious (); + } + + /// + /// Stops running this . + /// + public virtual void RequestStop () + { + if (IsMdiContainer && Running + && (Application.Current == this + || Application.Current?.Modal == false + || Application.Current?.Modal == true && Application.Current?.Running == false)) { + + foreach (var child in Application.MdiChildes) { + var ev = new ToplevelClosingEventArgs (this); + if (child.OnClosing (ev)) { + return; + } + child.Running = false; + Application.RequestStop (child); + } + Running = false; + Application.RequestStop (this); + } else if (IsMdiContainer && Running && Application.Current?.Modal == true && Application.Current?.Running == true) { + var ev = new ToplevelClosingEventArgs (Application.Current); + if (OnClosing (ev)) { + return; + } + Application.RequestStop (Application.Current); + } else if (!IsMdiContainer && Running && (!Modal || (Modal && Application.Current != this))) { + var ev = new ToplevelClosingEventArgs (this); + if (OnClosing (ev)) { + return; + } + Running = false; + Application.RequestStop (this); + } else { + Application.RequestStop (Application.Current); + } + } + + /// + /// Stops running the . + /// + /// The toplevel to request stop. + public virtual void RequestStop (Toplevel top) + { + top.RequestStop (); + } + + /// + public override void PositionCursor () + { + if (!IsMdiContainer) { + base.PositionCursor (); + return; + } + + if (Focused == null) { + foreach (var top in Application.MdiChildes) { + if (top != this && top.Visible) { + top.SetFocus (); + return; + } + } + } + base.PositionCursor (); + } + + /// + /// Gets the current visible toplevel Mdi child that match the arguments pattern. + /// + /// The type. + /// The strings to exclude. + /// The matched view. + public View GetTopMdiChild (Type type = null, string [] exclude = null) + { + if (Application.MdiTop == null) { + return null; + } + + foreach (var top in Application.MdiChildes) { + if (type != null && top.GetType () == type + && exclude?.Contains (top.Data.ToString ()) == false) { + return top; + } else if ((type != null && top.GetType () != type) + || (exclude?.Contains (top.Data.ToString ()) == true)) { + continue; + } + return top; + } + return null; + } + + /// + /// Shows the Mdi child indicated by the setting as . + /// + /// The toplevel. + /// if the toplevel can be showed. otherwise. + public virtual bool ShowChild (Toplevel top = null) + { + if (Application.MdiTop != null) { + return Application.ShowChild (top == null ? this : top); + } + return false; + } + } + + /// + /// Implements the to comparing two used by . + /// + public class ToplevelEqualityComparer : IEqualityComparer { + /// Determines whether the specified objects are equal. + /// The first object of type to compare. + /// The second object of type to compare. + /// + /// if the specified objects are equal; otherwise, . + public bool Equals (Toplevel x, Toplevel y) + { + if (y == null && x == null) + return true; + else if (x == null || y == null) + return false; + else if (x.Id == y.Id) + return true; + else + return false; + } + + /// Returns a hash code for the specified object. + /// The for which a hash code is to be returned. + /// A hash code for the specified object. + /// The type of is a reference type and is . + public int GetHashCode (Toplevel obj) + { + if (obj == null) + throw new ArgumentNullException (); + + int hCode = 0; + if (int.TryParse (obj.Id.ToString (), out int result)) { + hCode = result; + } + return hCode.GetHashCode (); + } + } + + /// + /// Implements the to sort the from the if needed. + /// + public sealed class ToplevelComparer : IComparer { + /// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other. + /// The first object to compare. + /// The second object to compare. + /// A signed integer that indicates the relative values of and , as shown in the following table.Value Meaning Less than zero + /// is less than .Zero + /// equals .Greater than zero + /// is greater than . + public int Compare (Toplevel x, Toplevel y) + { + if (ReferenceEquals (x, y)) + return 0; + else if (x == null) + return -1; + else if (y == null) + return 1; + else + return string.Compare (x.Id.ToString (), y.Id.ToString ()); + } + } + /// + /// implementation for the event. + /// + public class ToplevelClosingEventArgs : EventArgs { + /// + /// The toplevel requesting stop. + /// + public View RequestingTop { get; } + /// + /// Provides an event cancellation option. + /// + public bool Cancel { get; set; } + + /// + /// Initializes the event arguments with the requesting toplevel. + /// + /// The . + public ToplevelClosingEventArgs (Toplevel requestingTop) + { + RequestingTop = requestingTop; + } } } diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index f721feedf5..4d6db10ec7 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -268,7 +268,7 @@ public int TabIndex { } } - private int GetTabIndex (int idx) + int GetTabIndex (int idx) { int i = 0; foreach (var v in SuperView.tabIndexes) { @@ -280,7 +280,7 @@ private int GetTabIndex (int idx) return Math.Min (i, idx); } - private void SetTabIndex () + void SetTabIndex () { int i = 0; foreach (var v in SuperView.tabIndexes) { @@ -989,8 +989,8 @@ public void Clear (Rect regionScreen) internal void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true) { // Computes the real row, col relative to the screen. - rrow = row + frame.Y; - rcol = col + frame.X; + rrow = Math.Max (row + frame.Y, 0); + rcol = Math.Max (col + frame.X, 0); var ccontainer = container; while (ccontainer != null) { rrow += ccontainer.frame.Y; @@ -1338,18 +1338,7 @@ public virtual void Redraw (Rect bounds) var clipRect = new Rect (Point.Empty, frame.Size); - if (ColorScheme != null) { - Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); - } - - if (!ustring.IsNullOrEmpty (Text)) { - Clear (); - // Draw any Text - if (textFormatter != null) { - textFormatter.NeedsFormat = true; - } - textFormatter?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); - } + DrawText (); // Invoke DrawContentEvent OnDrawContent (bounds); @@ -1377,6 +1366,33 @@ public virtual void Redraw (Rect bounds) ClearNeedsDisplay (); } + void DrawText () + { + if (ColorScheme != null) { + Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); + } + + if (!ustring.IsNullOrEmpty (Text)) { + var savedClip = ClipToBounds (); + Rect viewBounds = Bounds; + if (SuperView != null && viewBounds.Width > SuperView.Bounds.Width) { + viewBounds.Width = SuperView.Bounds.Width; + } + if (SuperView != null && viewBounds.Height > SuperView.Bounds.Height) { + viewBounds.Height = SuperView.Bounds.Height; + } + var viewFrame = ViewToScreen (viewBounds); + Clear (viewFrame); + // Draw any Text + if (textFormatter != null) { + textFormatter.NeedsFormat = true; + } + textFormatter?.Draw (viewFrame, HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); + + Driver.Clip = savedClip; + } + } + /// /// Event invoked when the content area of the View is to be drawn. /// @@ -1957,8 +1973,9 @@ void CollectAll (View from, ref HashSet nNodes, ref HashSet<(View, View)> v.LayoutNeeded = false; } - if (SuperView == Application.Top && LayoutNeeded && ordered.Count == 0 && LayoutStyle == LayoutStyle.Computed) { - SetRelativeLayout (Frame); + if (SuperView != null && SuperView == Application.Top && LayoutNeeded + && ordered.Count == 0 && LayoutStyle == LayoutStyle.Computed) { + SetRelativeLayout (SuperView.Frame); } LayoutNeeded = false; @@ -2259,7 +2276,7 @@ public void EndInit () /// public bool Visible { get; set; } = true; - bool CanBeVisible (View view) + internal bool CanBeVisible (View view) { if (!view.Visible) { return false; diff --git a/Terminal.Gui/Core/Window.cs b/Terminal.Gui/Core/Window.cs index 3d91da8792..99734a6577 100644 --- a/Terminal.Gui/Core/Window.cs +++ b/Terminal.Gui/Core/Window.cs @@ -209,77 +209,6 @@ public override void Redraw (Rect bounds) } } - // - // FIXED:It does not look like the event is raised on clicked-drag - // need to figure that out. - // - internal static Point? dragPosition; - Point start; - - /// - public override bool MouseEvent (MouseEvent mouseEvent) - { - // FIXED:The code is currently disabled, because the - // Driver.UncookMouse does not seem to have an effect if there is - // a pending mouse event activated. - - int nx, ny; - if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed - || mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) { - // Only start grabbing if the user clicks on the title bar. - if (mouseEvent.Y == 0) { - start = new Point (mouseEvent.X, mouseEvent.Y); - dragPosition = new Point (); - nx = mouseEvent.X - mouseEvent.OfX; - ny = mouseEvent.Y - mouseEvent.OfY; - dragPosition = new Point (nx, ny); - Application.GrabMouse (this); - } - - //System.Diagnostics.Debug.WriteLine ($"Starting at {dragPosition}"); - return true; - } else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) || - mouseEvent.Flags == MouseFlags.Button3Pressed) { - if (dragPosition.HasValue) { - if (SuperView == null) { - Application.Top.SetNeedsDisplay (Frame); - // Redraw the entire app window using just our Frame. Since we are - // Application.Top, and our Frame always == our Bounds (Location is always (0,0)) - // our Frame is actually view-relative (which is what Redraw takes). - Application.Top.Redraw (Frame); - } else { - SuperView.SetNeedsDisplay (Frame); - } - EnsureVisibleBounds (this, mouseEvent.X + (SuperView == null ? mouseEvent.OfX - start.X : Frame.X - start.X), - mouseEvent.Y + (SuperView == null ? mouseEvent.OfY : Frame.Y), out nx, out ny); - - dragPosition = new Point (nx, ny); - LayoutSubviews (); - Frame = new Rect (nx, ny, Frame.Width, Frame.Height); - if (X == null || X is Pos.PosAbsolute) { - X = nx; - } - if (Y == null || Y is Pos.PosAbsolute) { - Y = ny; - } - //System.Diagnostics.Debug.WriteLine ($"nx:{nx},ny:{ny}"); - - // FIXED: optimize, only SetNeedsDisplay on the before/after regions. - SetNeedsDisplay (); - return true; - } - } - - if (mouseEvent.Flags == MouseFlags.Button1Released && dragPosition.HasValue) { - Application.UngrabMouse (); - Driver.UncookMouse (); - dragPosition = null; - } - - //System.Diagnostics.Debug.WriteLine (mouseEvent.ToString ()); - return false; - } - /// /// The text displayed by the . /// From b2a8a7cb6d3ba4e167fee8352016a76f64eb15d5 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 11 Jul 2021 00:14:28 +0100 Subject: [PATCH 03/15] Improves the MenuOpening event. --- Terminal.Gui/Views/Menu.cs | 60 +++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index a947d0836e..3a4d5099e6 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -166,7 +166,7 @@ public bool IsEnabled () /// /// Gets if this is from a sub-menu. /// - internal bool IsFromSubMenu { get {return Parent != null; } } + internal bool IsFromSubMenu { get { return Parent != null; } } /// /// Merely a debugging aid to see the interaction with main @@ -274,7 +274,7 @@ public MenuBarItem (MenuItem [] children) : this ("", children) { } /// /// Initializes a new . /// - public MenuBarItem () : this (children: new MenuItem [] { }) { } + public MenuBarItem () : this (children: new MenuItem [] { }) { } //static int GetMaxTitleLength (MenuItem [] children) //{ @@ -447,7 +447,7 @@ public override void Redraw (Rect bounds) for (int p = 0; p < Frame.Width - 2; p++) if (item == null) Driver.AddRune (Driver.HLine); - else if (p == Frame.Width - 3 && barItems.SubMenu(barItems.Children [i]) != null) + else if (p == Frame.Width - 3 && barItems.SubMenu (barItems.Children [i]) != null) Driver.AddRune (Driver.RightArrow); else Driver.AddRune (' '); @@ -916,7 +916,7 @@ public override void Redraw (Rect bounds) var menu = Menus [i]; Move (pos, 0); Attribute hotColor, normalColor; - if (i == selected) { + if (i == selected && IsMenuOpen) { hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal; normalColor = i == selected ? ColorScheme.Focus : ColorScheme.Normal; } else if (openedByAltKey) { @@ -966,7 +966,7 @@ void Selected (MenuItem item) /// /// Raised as a menu is opening. /// - public event Action MenuOpening; + public event Action MenuOpening; /// /// Raised when a menu is closing. @@ -986,11 +986,15 @@ void Selected (MenuItem item) public bool IsMenuOpen { get; protected set; } /// - /// Virtual method that will invoke the + /// Virtual method that will invoke the event if it's defined. /// - public virtual void OnMenuOpening () + /// The current menu to be replaced. + /// /// Returns the + public virtual MenuOpeningEventArgs OnMenuOpening (MenuBarItem currentMenu) { - MenuOpening?.Invoke (); + var ev = new MenuOpeningEventArgs (currentMenu); + MenuOpening?.Invoke (ev); + return ev; } /// @@ -1011,11 +1015,17 @@ public virtual void OnMenuClosing () internal void OpenMenu (int index, int sIndex = -1, MenuBarItem subMenu = null) { isMenuOpening = true; - OnMenuOpening (); + var newMenu = OnMenuOpening (Menus [index]); + if (newMenu.Cancel) { + return; + } + if (newMenu.NewMenuBarItem != null && Menus [index].Title == newMenu.NewMenuBarItem.Title) { + Menus [index] = newMenu.NewMenuBarItem; + } int pos = 0; switch (subMenu) { case null: - lastFocused = lastFocused ?? SuperView.MostFocused; + lastFocused = lastFocused ?? SuperView?.MostFocused; if (openSubMenu != null) CloseMenu (false, true); if (openMenu != null) { @@ -1460,7 +1470,7 @@ public override bool ProcessKey (KeyEvent kb) case Key.CursorDown: case Key.Enter: if (selected > -1) { - ProcessMenu (selected, Menus [selected]); + ProcessMenu (selected, Menus [selected]); } break; @@ -1638,4 +1648,32 @@ public override bool OnEnter (View view) return base.OnEnter (view); } } + + /// + /// An which allows passing a cancelable menu opening event or replacing with a new . + /// + public class MenuOpeningEventArgs : EventArgs { + /// + /// The current parent. + /// + public MenuBarItem CurrentMenu { get; } + + /// + /// The new to be replaced. + /// + public MenuBarItem NewMenuBarItem { get; set; } + /// + /// Flag that allows you to cancel the opening of the menu. + /// + public bool Cancel { get; set; } + + /// + /// Initializes a new instance of + /// + /// The current parent. + public MenuOpeningEventArgs (MenuBarItem currentMenu) + { + CurrentMenu = currentMenu; + } + } } From 8d6c2e0aa8cb7440c216ad3e6386516021d6781a Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 11 Jul 2021 00:16:40 +0100 Subject: [PATCH 04/15] Some changes in the UICatalog and Scenarios. --- UICatalog/Scenarios/AllViewsTester.cs | 7 + .../Scenarios/BackgroundWorkerCollection.cs | 389 ++++++++++++++++++ UICatalog/Scenarios/BackgroundWorkerSample.cs | 208 ---------- UICatalog/Scenarios/Buttons.cs | 9 +- UICatalog/Scenarios/SingleBackgroundWorker.cs | 187 +++++++++ UICatalog/Scenarios/Threading.cs | 2 +- UICatalog/UICatalog.cs | 25 +- 7 files changed, 610 insertions(+), 217 deletions(-) create mode 100644 UICatalog/Scenarios/BackgroundWorkerCollection.cs delete mode 100644 UICatalog/Scenarios/BackgroundWorkerSample.cs create mode 100644 UICatalog/Scenarios/SingleBackgroundWorker.cs diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs index ee790f0880..7b8e3bba09 100644 --- a/UICatalog/Scenarios/AllViewsTester.cs +++ b/UICatalog/Scenarios/AllViewsTester.cs @@ -236,7 +236,12 @@ void DimPosChanged (View view) if (view == null) { return; } + + var layout = view.LayoutStyle; + try { + view.LayoutStyle = LayoutStyle.Absolute; + switch (_xRadioGroup.SelectedItem) { case 0: view.X = Pos.Percent (_xVal); @@ -292,6 +297,8 @@ void DimPosChanged (View view) } } catch (Exception e) { MessageBox.ErrorQuery ("Exception", e.Message, "Ok"); + } finally { + view.LayoutStyle = layout; } UpdateTitle (view); } diff --git a/UICatalog/Scenarios/BackgroundWorkerCollection.cs b/UICatalog/Scenarios/BackgroundWorkerCollection.cs new file mode 100644 index 0000000000..9c0b746f05 --- /dev/null +++ b/UICatalog/Scenarios/BackgroundWorkerCollection.cs @@ -0,0 +1,389 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Terminal.Gui; + +namespace UICatalog { + [ScenarioMetadata (Name: "BackgroundWorker Collection", Description: "A persisting multi Toplevel BackgroundWorker threading")] + [ScenarioCategory ("Threading")] + [ScenarioCategory ("TopLevel")] + [ScenarioCategory ("Dialogs")] + [ScenarioCategory ("Controls")] + class BackgroundWorkerCollection : Scenario { + public override void Init (Toplevel top, ColorScheme colorScheme) + { + Application.Top.Dispose (); + + Application.Run (); + + Application.Top.Dispose (); + } + + public override void Run () + { + } + + class MdiMain : Toplevel { + private WorkerApp workerApp; + private bool canOpenWorkerApp; + MenuBar menu; + + public MdiMain () + { + Data = "MdiMain"; + + IsMdiContainer = true; + + workerApp = new WorkerApp () { Visible = false }; + + menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_Options", new MenuItem [] { + new MenuItem ("_Run Worker", "", () => workerApp.RunWorker(), null, null, Key.CtrlMask | Key.R), + new MenuItem ("_Cancel Worker", "", () => workerApp.CancelWorker(), null, null, Key.CtrlMask | Key.C), + null, + new MenuItem ("_Quit", "", () => Quit(), null, null, Key.CtrlMask | Key.Q) + }), + new MenuBarItem ("_View", new MenuItem [] { }), + new MenuBarItem ("_Window", new MenuItem [] { }) + }); + menu.MenuOpening += Menu_MenuOpening; + Add (menu); + + var statusBar = new StatusBar (new [] { + new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Exit", () => Quit()), + new StatusItem(Key.CtrlMask | Key.R, "~^R~ Run Worker", () => workerApp.RunWorker()), + new StatusItem(Key.CtrlMask | Key.C, "~^C~ Cancel Worker", () => workerApp.CancelWorker()) + }); + Add (statusBar); + + Activate += MdiMain_Activate; + Deactivate += MdiMain_Deactivate; + + Closed += MdiMain_Closed; + + Application.Iteration += () => { + if (canOpenWorkerApp && !workerApp.Running && Application.MdiTop.Running) { + Application.Run (workerApp); + } + }; + } + + private void MdiMain_Closed (Toplevel obj) + { + workerApp.Dispose (); + Dispose (); + } + + private void Menu_MenuOpening (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 MdiMain_Deactivate (Toplevel top) + { + workerApp.WriteLog ($"{top.Data} deactivate."); + } + + private void MdiMain_Activate (Toplevel top) + { + workerApp.WriteLog ($"{top.Data} activate."); + } + + private MenuBarItem View () + { + List menuItems = new List (); + var item = new MenuItem () { + Title = "WorkerApp", + CheckType = MenuItemCheckStyle.Checked + }; + var top = Application.MdiChildes?.Find ((x) => x.Data.ToString () == "WorkerApp"); + if (top != null) { + item.Checked = top.Visible; + } + item.Action += () => { + var top = Application.MdiChildes.Find ((x) => x.Data.ToString () == "WorkerApp"); + item.Checked = top.Visible = !item.Checked; + Application.MdiTop.SetNeedsDisplay (); + }; + menuItems.Add (item); + return new MenuBarItem ("_View", + new List () { menuItems.Count == 0 ? new MenuItem [] { } : menuItems.ToArray () }); + } + + private MenuBarItem OpenedWindows () + { + var index = 1; + List menuItems = new List (); + var sortedChildes = Application.MdiChildes; + sortedChildes.Sort (new ToplevelComparer ()); + foreach (var top in sortedChildes) { + 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 == top.GetTopMdiChild () && topTitle == itemTitle) { + item.Checked = true; + } else { + item.Checked = false; + } + item.Action += () => { + top.ShowChild (); + }; + menuItems.Add (item); + } + if (menuItems.Count == 0) { + return new MenuBarItem ("_Window", "", null); + } else { + return new MenuBarItem ("_Window", new List () { menuItems.ToArray () }); + } + } + + private void Quit () + { + RequestStop (); + } + } + + class WorkerApp : Toplevel { + private List log = new List (); + private ListView listLog; + private Dictionary stagingWorkers; + private List 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 (); + 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) { + 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 (); + } + 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 (); + } + 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.MoveEnd (); + } + } + + class StagingUIController : Window { + private Label label; + private ListView listView; + private Button start; + private Button close; + public Staging Staging { get; private set; } + + public event Action ReportClosed; + + public StagingUIController (Staging staging, List 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 exit.") { + 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 }; + start.Clicked += () => { + Staging = new Staging (DateTime.Now); + RequestStop (); + }; + Add (start); + + close = new Button ("Close"); + close.Clicked += OnReportClosed; + Add (close); + + KeyPress += (e) => { + if (e.KeyEvent.Key == Key.Esc) { + OnReportClosed (); + } + }; + + LayoutStarted += (_) => { + var btnsWidth = start.Bounds.Width + close.Bounds.Width + 2 - 1; + var shiftLeft = Math.Max ((Bounds.Width - btnsWidth) / 2 - 2, 0); + + shiftLeft += close.Bounds.Width + 1; + close.X = Pos.AnchorEnd (shiftLeft); + close.Y = Pos.AnchorEnd (1); + + shiftLeft += start.Bounds.Width + 1; + start.X = Pos.AnchorEnd (shiftLeft); + start.Y = Pos.AnchorEnd (1); + }; + } + + private void OnReportClosed () + { + 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; + } + } + } +} diff --git a/UICatalog/Scenarios/BackgroundWorkerSample.cs b/UICatalog/Scenarios/BackgroundWorkerSample.cs deleted file mode 100644 index 330a36e8b6..0000000000 --- a/UICatalog/Scenarios/BackgroundWorkerSample.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Threading; -using Terminal.Gui; - -namespace UICatalog { - [ScenarioMetadata (Name: "BackgroundWorker", Description: "A persisting multi Toplevel BackgroundWorker threading")] - [ScenarioCategory ("Threading")] - [ScenarioCategory ("TopLevel")] - [ScenarioCategory ("Dialogs")] - [ScenarioCategory ("Controls")] - class BackgroundWorkerSample : Scenario { - public override void Run () - { - Top.Dispose (); - - Application.Run (); - - Top.Dispose (); - } - } - - public class MainApp : Toplevel { - private List log = new List (); - private ListView listLog; - private Dictionary stagingWorkers; - - public MainApp () - { - var menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_Options", new MenuItem [] { - new MenuItem ("_Run Worker", "", () => RunWorker(), null, null, Key.CtrlMask | Key.R), - new MenuItem ("_Cancel Worker", "", () => CancelWorker(), null, null, Key.CtrlMask | Key.C), - null, - new MenuItem ("_Quit", "", () => Application.RequestStop (), null, null, Key.CtrlMask | Key.Q) - }) - }); - Add (menu); - - var statusBar = new StatusBar (new [] { - new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Exit", () => Application.RequestStop()), - new StatusItem(Key.CtrlMask | Key.P, "~^R~ Run Worker", () => RunWorker()), - new StatusItem(Key.CtrlMask | Key.P, "~^C~ Cancel Worker", () => CancelWorker()) - }); - Add (statusBar); - - var top = new Toplevel (); - - top.Add (new Label ("Worker Log") { - X = Pos.Center (), - Y = 0 - }); - - listLog = new ListView (log) { - X = 0, - Y = 2, - Width = Dim.Fill (), - Height = Dim.Fill () - }; - top.Add (listLog); - Add (top); - } - - private void RunWorker () - { - var stagingUI = new StagingUIController (); - - var worker = new BackgroundWorker () { WorkerSupportsCancellation = true }; - - worker.DoWork += (s, e) => { - var stageResult = new List (); - for (int i = 0; i < 500; i++) { - stageResult.Add ( - $"Worker {i} started at {DateTime.UtcNow}"); - e.Result = stageResult; - Thread.Sleep (1); - if (worker.CancellationPending) { - e.Cancel = true; - return; - } - } - }; - - worker.RunWorkerCompleted += (s, e) => { - if (e.Error != null) { - // Failed - log.Add ($"Exception occurred {e.Error.Message} on Worker {stagingUI.StartStaging}.{stagingUI.StartStaging:fff} at {DateTime.UtcNow}"); - listLog.SetNeedsDisplay (); - } else if (e.Cancelled) { - // Canceled - log.Add ($"Worker {stagingUI.StartStaging}.{stagingUI.StartStaging:fff} was canceled at {DateTime.UtcNow}!"); - listLog.SetNeedsDisplay (); - } else { - // Passed - log.Add ($"Worker {stagingUI.StartStaging}.{stagingUI.StartStaging:fff} was completed at {DateTime.UtcNow}."); - listLog.SetNeedsDisplay (); - Application.Refresh (); - stagingUI.Load (e.Result as List); - } - stagingWorkers.Remove (stagingUI); - }; - - Application.Run (stagingUI); - - if (stagingUI.StartStaging != null) { - log.Add ($"Worker is started at {stagingUI.StartStaging}.{stagingUI.StartStaging:fff}"); - listLog.SetNeedsDisplay (); - if (stagingWorkers == null) { - stagingWorkers = new Dictionary (); - } - stagingWorkers.Add (stagingUI, worker); - worker.RunWorkerAsync (); - } - } - - private void CancelWorker () - { - if (stagingWorkers.Count == 0) { - log.Add ($"Worker is not running at {DateTime.UtcNow}!"); - listLog.SetNeedsDisplay (); - return; - } - - var eStaging = stagingWorkers.GetEnumerator (); - eStaging.MoveNext (); - var fStaging = eStaging.Current; - var stagingUI = fStaging.Key; - var worker = fStaging.Value; - worker.CancelAsync (); - log.Add ($"Worker {stagingUI.StartStaging}.{stagingUI.StartStaging:fff} is canceling at {DateTime.UtcNow}!"); - listLog.SetNeedsDisplay (); - } - } - - public class StagingUIController : Window { - private Label label; - private ListView listView; - private Button start; - private Button close; - - public DateTime? StartStaging { get; private set; } - - public StagingUIController () - { - X = Pos.Center (); - Y = Pos.Center (); - Width = Dim.Percent (85); - Height = Dim.Percent (85); - - ColorScheme = Colors.Dialog; - Modal = true; - - Title = "Run Worker"; - - label = new Label ("Press start to do the work or close to exit.") { - 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 }; - start.Clicked += () => { - StartStaging = DateTime.UtcNow; - Application.RequestStop (); - }; - Add (start); - - close = new Button ("Close"); - close.Clicked += () => Application.RequestStop (); - Add (close); - - LayoutStarted += (_) => { - var btnsWidth = start.Bounds.Width + close.Bounds.Width + 2 - 1; - var shiftLeft = Math.Max ((Bounds.Width - btnsWidth) / 2 - 2, 0); - - shiftLeft += close.Bounds.Width + 1; - close.X = Pos.AnchorEnd (shiftLeft); - close.Y = Pos.AnchorEnd (1); - - shiftLeft += start.Bounds.Width + 1; - start.X = Pos.AnchorEnd (shiftLeft); - start.Y = Pos.AnchorEnd (1); - }; - - } - - public void Load (List list) - { - var stagingUI = new StagingUIController (); - stagingUI.Title = $"Worker started at {StartStaging}.{StartStaging:fff}"; - stagingUI.label.Text = "Work list:"; - stagingUI.listView.SetSource (list); - stagingUI.start.Visible = false; - - Application.Run (stagingUI); - } - } -} diff --git a/UICatalog/Scenarios/Buttons.cs b/UICatalog/Scenarios/Buttons.cs index e0a5d120d6..16062bc3ce 100644 --- a/UICatalog/Scenarios/Buttons.cs +++ b/UICatalog/Scenarios/Buttons.cs @@ -103,8 +103,13 @@ static void DoMessage (Button button, ustring txt) ColorScheme = Colors.Error }; Win.Add (removeButton); - // This in intresting test case because `moveBtn` and below are laid out relative to this one! - removeButton.Clicked += () => Win.Remove (removeButton); + // This in interesting test case because `moveBtn` and below are laid out relative to this one! + removeButton.Clicked += () => { + // Now this throw a InvalidOperationException on the TopologicalSort method as is expected. + //Win.Remove (removeButton); + + removeButton.Visible = false; + }; var computedFrame = new FrameView ("Computed Layout") { X = 0, diff --git a/UICatalog/Scenarios/SingleBackgroundWorker.cs b/UICatalog/Scenarios/SingleBackgroundWorker.cs new file mode 100644 index 0000000000..2cf11e6efc --- /dev/null +++ b/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using Terminal.Gui; + +namespace UICatalog { + [ScenarioMetadata (Name: "Single BackgroundWorker", Description: "A single BackgroundWorker threading opening another Toplevel")] + [ScenarioCategory ("Threading")] + [ScenarioCategory ("TopLevel")] + [ScenarioCategory ("Dialogs")] + [ScenarioCategory ("Controls")] + class SingleBackgroundWorker : Scenario { + public override void Run () + { + Top.Dispose (); + + Application.Run (); + + Top.Dispose (); + } + + public class MainApp : Toplevel { + private BackgroundWorker worker; + private List log = new List (); + private DateTime? startStaging; + private ListView listLog; + + public MainApp () + { + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_Options", new MenuItem [] { + new MenuItem ("_Run Worker", "", () => RunWorker(), null, null, Key.CtrlMask | Key.R), + null, + new MenuItem ("_Quit", "", () => Application.RequestStop(), null, null, Key.CtrlMask | Key.Q) + }) + }); + Add (menu); + + var statusBar = new StatusBar (new [] { + new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Exit", () => Application.RequestStop()), + new StatusItem(Key.CtrlMask | Key.P, "~^R~ Run Worker", () => RunWorker()) + }); + Add (statusBar); + + var top = new Toplevel (); + + top.Add (new Label ("Worker Log") { + X = Pos.Center (), + Y = 0 + }); + + listLog = new ListView (log) { + X = 0, + Y = 2, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + top.Add (listLog); + Add (top); + } + + public void Load () + { + Application.Run (this); + } + + private void RunWorker () + { + worker = new BackgroundWorker () { WorkerSupportsCancellation = true }; + + var cancel = new Button ("Cancel Worker"); + cancel.Clicked += () => { + if (worker == null) { + log.Add ($"Worker is not running at {DateTime.Now}!"); + listLog.SetNeedsDisplay (); + return; + } + + log.Add ($"Worker {startStaging}.{startStaging:fff} is canceling at {DateTime.Now}!"); + listLog.SetNeedsDisplay (); + worker.CancelAsync (); + }; + + startStaging = DateTime.Now; + log.Add ($"Worker is started at {startStaging}.{startStaging:fff}"); + listLog.SetNeedsDisplay (); + + var md = new Dialog ($"Running Worker started at {startStaging}.{startStaging:fff}", cancel); + + worker.DoWork += (s, e) => { + var stageResult = new List (); + 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 (md.IsCurrentTop) { + //Close the dialog + Application.RequestStop (); + } + + if (e.Error != null) { + // Failed + log.Add ($"Exception occurred {e.Error.Message} on Worker {startStaging}.{startStaging:fff} at {DateTime.Now}"); + listLog.SetNeedsDisplay (); + } else if (e.Cancelled) { + // Canceled + log.Add ($"Worker {startStaging}.{startStaging:fff} was canceled at {DateTime.Now}!"); + listLog.SetNeedsDisplay (); + } else { + // Passed + log.Add ($"Worker {startStaging}.{startStaging:fff} was completed at {DateTime.Now}."); + listLog.SetNeedsDisplay (); + Application.Refresh (); + var builderUI = new StagingUIController (startStaging, e.Result as List); + builderUI.Load (); + } + worker = null; + }; + worker.RunWorkerAsync (); + Application.Run (md); + } + } + + public class StagingUIController : Window { + Toplevel top; + + public StagingUIController (DateTime? start, List list) + { + top = new Toplevel (Application.Top.Frame); + top.KeyPress += (e) => { + // Prevents Ctrl+Q from closing this. + // Only Ctrl+C is allowed. + if (e.KeyEvent.Key == (Key.Q | Key.CtrlMask)) { + e.Handled = true; + } + }; + + bool Close () + { + var n = MessageBox.Query (50, 7, "Close Window.", "Are you sure you want to close this window?", "Yes", "No"); + return n == 0; + } + + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_Stage", new MenuItem [] { + new MenuItem ("_Close", "", () => { if (Close()) { Application.RequestStop(); } }, null, null, Key.CtrlMask | Key.C) + }) + }); + top.Add (menu); + + var statusBar = new StatusBar (new [] { + new StatusItem(Key.CtrlMask | Key.C, "~^C~ Close", () => { if (Close()) { Application.RequestStop(); } }), + }); + top.Add (statusBar); + + Title = $"Worker started at {start}.{start:fff}"; + Y = 1; + Height = Dim.Fill (1); + + ColorScheme = Colors.Base; + + Add (new ListView (list) { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill () + }); + + top.Add (this); + } + + public void Load () + { + Application.Run (top); + } + } + } +} diff --git a/UICatalog/Scenarios/Threading.cs b/UICatalog/Scenarios/Threading.cs index 14ab8d4d4e..933d3a0cff 100644 --- a/UICatalog/Scenarios/Threading.cs +++ b/UICatalog/Scenarios/Threading.cs @@ -89,7 +89,7 @@ public override void Setup () var _btnClearData = new Button (80, 20, "Clear Data"); _btnClearData.Clicked += () => { _itemsList.Source = null; LogJob ("Cleaning Data"); }; var _btnQuit = new Button (80, 22, "Quit"); - _btnQuit.Clicked += Application.RequestStop; + _btnQuit.Clicked += () => Application.RequestStop (); Win.Add (_itemsList, _btnActionCancel, _logJob, text, _btnAction, _btnLambda, _btnHandler, _btnSync, _btnMethod, _btnClearData, _btnQuit); diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 66f20e6874..883ab7d005 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -65,6 +65,7 @@ public class UICatalogApp { private static ConsoleDriver.DiagnosticFlags _diagnosticFlags; private static bool _heightAsBuffer = false; private static bool _alwaysSetPosition; + private static bool _isFirstRunning = true; static void Main (string [] args) { @@ -108,13 +109,15 @@ static void Main (string [] args) scenario.Setup (); scenario.Run (); - static void LoadedHandler () - { - _rightPane.SetFocus (); - _top.Loaded -= LoadedHandler; - } + //static void LoadedHandler () + //{ + // _rightPane.SetFocus (); + // _top.Loaded -= LoadedHandler; + //} - _top.Loaded += LoadedHandler; + //_top.Loaded += LoadedHandler; + + Application.Shutdown (); #if DEBUG_IDISPOSABLE // After the scenario runs, validate all Responder-based instances @@ -269,11 +272,21 @@ private static Scenario GetScenarioToRun () _top.Add (_leftPane); _top.Add (_rightPane); _top.Add (_statusBar); + _top.Loaded += () => { if (_runningScenario != null) { _runningScenario = null; + _isFirstRunning = false; } }; + void ReadyHandler () + { + if (!_isFirstRunning) { + _rightPane.SetFocus (); + } + _top.Ready -= ReadyHandler; + } + _top.Ready += ReadyHandler; Application.Run (_top); return _runningScenario; From c707d1351dddbe143b45ce6dc73c77861d716d37 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 11 Jul 2021 00:19:11 +0100 Subject: [PATCH 05/15] Some changes and adding much more unit tests. --- UnitTests/ApplicationTests.cs | 604 ++++++++++++++++++++++++++++++ UnitTests/AssemblyInfo.cs | 2 +- UnitTests/PosTests.cs | 18 +- UnitTests/ScenarioTests.cs | 14 +- UnitTests/StackExtensionsTests.cs | 192 ++++++++++ UnitTests/ViewTests.cs | 38 ++ 6 files changed, 856 insertions(+), 12 deletions(-) create mode 100644 UnitTests/StackExtensionsTests.cs diff --git a/UnitTests/ApplicationTests.cs b/UnitTests/ApplicationTests.cs index dc0168c4e0..bd9cac432b 100644 --- a/UnitTests/ApplicationTests.cs +++ b/UnitTests/ApplicationTests.cs @@ -372,5 +372,609 @@ public void AlternateForwardKey_AlternateBackwardKey_Tests () // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); } + + [Fact] + public void Application_RequestStop_With_Params_On_A_Not_MdiContainer_Always_Use_The_Application_Current () + { + Init (); + + var top1 = new Toplevel (); + var top2 = new Toplevel (); + var top3 = new Window (); + var top4 = new Window (); + var d = new Dialog (); + + // top1, top2, top3, d1 = 4 + var iterations = 4; + + top1.Ready += () => { + Assert.Null (Application.MdiChildes); + Application.Run (top2); + }; + top2.Ready += () => { + Assert.Null (Application.MdiChildes); + Application.Run (top3); + }; + top3.Ready += () => { + Assert.Null (Application.MdiChildes); + Application.Run (top4); + }; + top4.Ready += () => { + Assert.Null (Application.MdiChildes); + Application.Run (d); + }; + + d.Ready += () => { + Assert.Null (Application.MdiChildes); + // This will close the d because on a not MdiContainer the Application.Current it always used. + Application.RequestStop (top1); + Assert.True (Application.Current == d); + }; + + d.Closed += (e) => Application.RequestStop (top1); + + Application.Iteration += () => { + Assert.Null (Application.MdiChildes); + if (iterations == 4) { + Assert.True (Application.Current == d); + } else if (iterations == 3) { + Assert.True (Application.Current == top4); + } else if (iterations == 2) { + Assert.True (Application.Current == top3); + } else if (iterations == 1) { + Assert.True (Application.Current == top2); + } else { + Assert.True (Application.Current == top1); + } + Application.RequestStop (top1); + iterations--; + }; + + Application.Run (top1); + + Assert.Null (Application.MdiChildes); + + Application.Shutdown (); + } + + class Mdi : Toplevel { + public Mdi () + { + IsMdiContainer = true; + } + } + + [Fact] + public void MdiContainer_With_Toplevel_RequestStop_Balanced () + { + Init (); + + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d = new Dialog (); + + // MdiChild = c1, c2, c3 + // d1 = 1 + var iterations = 4; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d); + }; + + // More easy because the Mdi Container handles all at once + d.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + // This will not close the MdiContainer because d is a modal toplevel and will be closed. + mdi.RequestStop (); + }; + + // Now this will close the MdiContainer propagating through the MdiChildes. + d.Closed += (e) => { + mdi.RequestStop (); + }; + + Application.Iteration += () => { + if (iterations == 4) { + // The Dialog was not closed before and will be closed now. + Assert.True (Application.Current == d); + Assert.False (d.Running); + } else { + Assert.Equal (iterations, Application.MdiChildes.Count); + for (int i = 0; i < iterations; i++) { + Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); + } + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + + Application.Shutdown (); + } + + [Fact] + public void MdiContainer_With_Application_RequestStop_MdiTop_With_Params () + { + Init (); + + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d = new Dialog (); + + // MdiChild = c1, c2, c3 + // d1 = 1 + var iterations = 4; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d); + }; + + // Also easy because the Mdi Container handles all at once + d.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + // This will not close the MdiContainer because d is a modal toplevel + Application.RequestStop (mdi); + }; + + // Now this will close the MdiContainer propagating through the MdiChildes. + d.Closed += (e) => Application.RequestStop (mdi); + + Application.Iteration += () => { + if (iterations == 4) { + // The Dialog was not closed before and will be closed now. + Assert.True (Application.Current == d); + Assert.False (d.Running); + } else { + Assert.Equal (iterations, Application.MdiChildes.Count); + for (int i = 0; i < iterations; i++) { + Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); + } + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + + Application.Shutdown (); + } + + [Fact] + public void MdiContainer_With_Application_RequestStop_MdiTop_Without_Params () + { + Init (); + + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d = new Dialog (); + + // MdiChild = c1, c2, c3 = 3 + // d1 = 1 + var iterations = 4; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d); + }; + + //More harder because it's sequential. + d.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + // Close the Dialog + Application.RequestStop (); + }; + + // Now this will close the MdiContainer propagating through the MdiChildes. + d.Closed += (e) => Application.RequestStop (mdi); + + Application.Iteration += () => { + if (iterations == 4) { + // The Dialog still is the current top and we can't request stop to MdiContainer + // because we are not using parameter calls. + Assert.True (Application.Current == d); + Assert.False (d.Running); + } else { + Assert.Equal (iterations, Application.MdiChildes.Count); + for (int i = 0; i < iterations; i++) { + Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); + } + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + + Application.Shutdown (); + } + + [Fact] + public void IsMdiChild_Testing () + { + Init (); + + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d = new Dialog (); + + Application.Iteration += () => { + Assert.False (mdi.IsMdiChild); + Assert.True (c1.IsMdiChild); + Assert.True (c2.IsMdiChild); + Assert.True (c3.IsMdiChild); + Assert.False (d.IsMdiChild); + + mdi.RequestStop (); + }; + + Application.Run (mdi); + + Application.Shutdown (); + } + + [Fact] + public void Modal_Toplevel_Can_Open_Another_Modal_Toplevel_But_RequestStop_To_The_Caller_Also_Sets_Current_Running_To_False_Too () + { + Init (); + + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d1 = new Dialog (); + var d2 = new Dialog (); + + // MdiChild = c1, c2, c3 = 3 + // d1, d2 = 2 + var iterations = 5; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d1); + }; + d1.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d2); + }; + + d2.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Assert.True (Application.Current == d2); + Assert.True (Application.Current.Running); + // Trying to close the Dialog1 + d1.RequestStop (); + }; + + // Now this will close the MdiContainer propagating through the MdiChildes. + d1.Closed += (e) => { + Assert.True (Application.Current == d1); + Assert.False (Application.Current.Running); + mdi.RequestStop (); + }; + + Application.Iteration += () => { + if (iterations == 5) { + // The Dialog2 still is the current top and we can't request stop to MdiContainer + // because Dialog2 and Dialog1 must be closed first. + // Dialog2 will be closed in this iteration. + Assert.True (Application.Current == d2); + Assert.False (Application.Current.Running); + Assert.False (d1.Running); + } else if (iterations == 4) { + // Dialog1 will be closed in this iteration. + Assert.True (Application.Current == d1); + Assert.False (Application.Current.Running); + } else { + Assert.Equal (iterations, Application.MdiChildes.Count); + for (int i = 0; i < iterations; i++) { + Assert.Equal ((iterations - i + 1).ToString (), Application.MdiChildes [i].Id); + } + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + + Application.Shutdown (); + } + + [Fact] + public void Modal_Toplevel_Can_Open_Another_Not_Modal_Toplevel_But_RequestStop_To_The_Caller_Also_Sets_Current_Running_To_False_Too () + { + Init (); + + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + var d1 = new Dialog (); + var c4 = new Toplevel (); + + // MdiChild = c1, c2, c3, c4 = 4 + // d1 = 1 + var iterations = 5; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (d1); + }; + d1.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + Application.Run (c4); + }; + + c4.Ready += () => { + Assert.Equal (4, Application.MdiChildes.Count); + // Trying to close the Dialog1 + d1.RequestStop (); + }; + + // Now this will close the MdiContainer propagating through the MdiChildes. + d1.Closed += (e) => { + mdi.RequestStop (); + }; + + Application.Iteration += () => { + if (iterations == 5) { + // The Dialog2 still is the current top and we can't request stop to MdiContainer + // because Dialog2 and Dialog1 must be closed first. + // Using request stop here will call the Dialog again without need + Assert.True (Application.Current == d1); + Assert.False (Application.Current.Running); + Assert.True (c4.Running); + } else { + Assert.Equal (iterations, Application.MdiChildes.Count); + for (int i = 0; i < iterations; i++) { + Assert.Equal ((iterations - i + (iterations == 4 && i == 0 ? 2 : 1)).ToString (), + Application.MdiChildes [i].Id); + } + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + + Application.Shutdown (); + } + + [Fact] + public void MoveCurrent_Returns_False_If_The_Current_And_Top_Parameter_Are_Both_With_Running_Set_To_False () + { + Init (); + + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + + // MdiChild = c1, c2, c3 + var iterations = 3; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + c3.RequestStop (); + c1.RequestStop (); + }; + // Now this will close the MdiContainer propagating through the MdiChildes. + c1.Closed += (e) => { + mdi.RequestStop (); + }; + Application.Iteration += () => { + if (iterations == 3) { + // The Current still is c3 because Current.Running is false. + Assert.True (Application.Current == c3); + Assert.False (Application.Current.Running); + // But the childes order were reorder by Running = false + Assert.True (Application.MdiChildes [0] == c3); + Assert.True (Application.MdiChildes [1] == c1); + Assert.True (Application.MdiChildes [^1] == c2); + } else if (iterations == 2) { + // The Current is c1 and Current.Running is false. + Assert.True (Application.Current == c1); + Assert.False (Application.Current.Running); + Assert.True (Application.MdiChildes [0] == c1); + Assert.True (Application.MdiChildes [^1] == c2); + } else if (iterations == 1) { + // The Current is c2 and Current.Running is false. + Assert.True (Application.Current == c2); + Assert.False (Application.Current.Running); + Assert.True (Application.MdiChildes [^1] == c2); + } else { + // The Current is mdi. + Assert.True (Application.Current == mdi); + Assert.Empty (Application.MdiChildes); + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + + Application.Shutdown (); + } + + [Fact] + public void MdiContainer_Throws_If_More_Than_One () + { + Init (); + + var mdi = new Mdi (); + var mdi2 = new Mdi (); + + mdi.Ready += () => { + Assert.Throws (() => Application.Run (mdi2)); + mdi.RequestStop (); + }; + + Application.Run (mdi); + + Application.Shutdown (); + } + + [Fact] + public void MdiContainer_Open_And_Close_Modal_And_Open_Not_Modal_Toplevels_Randomly () + { + Init (); + + var mdi = new Mdi (); + var logger = new Toplevel (); + + var iterations = 1; // The logger + var running = true; + var stageCompleted = true; + var allStageClosed = false; + var mdiRequestStop = false; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (logger); + }; + + logger.Ready += () => Assert.Single (Application.MdiChildes); + + Application.Iteration += () => { + if (stageCompleted && running) { + stageCompleted = false; + var stage = new Window () { Modal = true }; + + stage.Ready += () => { + Assert.Equal (iterations, Application.MdiChildes.Count); + stage.RequestStop (); + }; + + stage.Closed += (_) => { + if (iterations == 11) { + allStageClosed = true; + } + Assert.Equal (iterations, Application.MdiChildes.Count); + if (running) { + stageCompleted = true; + + var rpt = new Window (); + + rpt.Ready += () => { + iterations++; + Assert.Equal (iterations, Application.MdiChildes.Count); + }; + + Application.Run (rpt); + } + }; + + Application.Run (stage); + + } else if (iterations == 11 && running) { + running = false; + Assert.Equal (iterations, Application.MdiChildes.Count); + + } else if (!mdiRequestStop && running && !allStageClosed) { + Assert.Equal (iterations, Application.MdiChildes.Count); + + } else if (!mdiRequestStop && !running && allStageClosed) { + Assert.Equal (iterations, Application.MdiChildes.Count); + mdiRequestStop = true; + mdi.RequestStop (); + } else { + Assert.Empty (Application.MdiChildes); + } + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + + Application.Shutdown (); + } } } diff --git a/UnitTests/AssemblyInfo.cs b/UnitTests/AssemblyInfo.cs index f4b6575808..1278f5deab 100644 --- a/UnitTests/AssemblyInfo.cs +++ b/UnitTests/AssemblyInfo.cs @@ -7,7 +7,7 @@ // Since Application is a singleton we can't run tests in parallel [assembly: CollectionBehavior (DisableTestParallelization = true)] -// This class enables test functions annotaed with the [AutoInitShutdown] attribute to +// This class enables test functions annotated with the [AutoInitShutdown] attribute to // automatically call Application.Init before called and Application.Shutdown after // // This is necessary because a) Application is a singleton and Init/Shutdown must be called diff --git a/UnitTests/PosTests.cs b/UnitTests/PosTests.cs index 73404bde05..77e1b382ef 100644 --- a/UnitTests/PosTests.cs +++ b/UnitTests/PosTests.cs @@ -296,37 +296,43 @@ void cleanup (Application.RunState rs) var app = setup (); app.button.Y = Pos.Left (app.win); rs = Application.Begin (Application.Top); - Application.Run (); + // If Application.RunState is used then we must use Application.RunLoop with the rs parameter + Application.RunLoop (rs); cleanup (rs); app = setup (); app.button.Y = Pos.X (app.win); rs = Application.Begin (Application.Top); - Application.Run (); + // If Application.RunState is used then we must use Application.RunLoop with the rs parameter + Application.RunLoop (rs); cleanup (rs); app = setup (); app.button.Y = Pos.Top (app.win); rs = Application.Begin (Application.Top); - Application.Run (); + // If Application.RunState is used then we must use Application.RunLoop with the rs parameter + Application.RunLoop (rs); cleanup (rs); app = setup (); app.button.Y = Pos.Y (app.win); rs = Application.Begin (Application.Top); - Application.Run (); + // If Application.RunState is used then we must use Application.RunLoop with the rs parameter + Application.RunLoop (rs); cleanup (rs); app = setup (); app.button.Y = Pos.Bottom (app.win); rs = Application.Begin (Application.Top); - Application.Run (); + // If Application.RunState is used then we must use Application.RunLoop with the rs parameter + Application.RunLoop (rs); cleanup (rs); app = setup (); app.button.Y = Pos.Right (app.win); rs = Application.Begin (Application.Top); - Application.Run (); + // If Application.RunState is used then we must use Application.RunLoop with the rs parameter + Application.RunLoop (rs); cleanup (rs); } diff --git a/UnitTests/ScenarioTests.cs b/UnitTests/ScenarioTests.cs index 689e4863c7..03b0372c87 100644 --- a/UnitTests/ScenarioTests.cs +++ b/UnitTests/ScenarioTests.cs @@ -72,10 +72,12 @@ public void Run_All_Scenarios () var scenario = (Scenario)Activator.CreateInstance (scenarioClass); scenario.Init (Application.Top, Colors.Base); scenario.Setup (); - var rs = Application.Begin (Application.Top); + // There is no need to call Application.Begin because Init already creates the Application.Top + // If Application.RunState is used then the Application.RunLoop must also be used instead Application.Run. + //var rs = Application.Begin (Application.Top); scenario.Run (); - Application.End (rs); + //Application.End (rs); // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); @@ -100,7 +102,7 @@ public void Run_Generic () Assert.NotEmpty (scenarioClasses); var item = scenarioClasses.FindIndex (t => Scenario.ScenarioMetadata.GetName (t).Equals ("Generic", StringComparison.OrdinalIgnoreCase)); - var scenarioClass = scenarioClasses[item]; + var scenarioClass = scenarioClasses [item]; // Setup some fake keypresses // Passing empty string will cause just a ctrl-q to be fired int stackSize = CreateInput (""); @@ -132,10 +134,12 @@ public void Run_Generic () var scenario = (Scenario)Activator.CreateInstance (scenarioClass); scenario.Init (Application.Top, Colors.Base); scenario.Setup (); - var rs = Application.Begin (Application.Top); + // There is no need to call Application.Begin because Init already creates the Application.Top + // If Application.RunState is used then the Application.RunLoop must also be used instead Application.Run. + //var rs = Application.Begin (Application.Top); scenario.Run (); - Application.End (rs); + //Application.End (rs); Assert.Equal (0, abortCount); // # of key up events should match # of iterations diff --git a/UnitTests/StackExtensionsTests.cs b/UnitTests/StackExtensionsTests.cs new file mode 100644 index 0000000000..fb61b1334d --- /dev/null +++ b/UnitTests/StackExtensionsTests.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Terminal.Gui.Core { + public class StackExtensionsTests { + [Fact] + public void Stack_Toplevels_CreateToplevels () + { + Stack toplevels = CreateToplevels (); + + int index = toplevels.Count - 1; + foreach (var top in toplevels) { + if (top.GetType () == typeof (Toplevel)) { + Assert.Equal ("Top", top.Id); + } else { + Assert.Equal ($"w{index}", top.Id); + } + index--; + } + + var tops = toplevels.ToArray (); + + Assert.Equal ("w4", tops [0].Id); + Assert.Equal ("w3", tops [1].Id); + Assert.Equal ("w2", tops [2].Id); + Assert.Equal ("w1", tops [3].Id); + Assert.Equal ("Top", tops [^1].Id); + } + + [Fact] + public void Stack_Toplevels_Replace () + { + Stack toplevels = CreateToplevels (); + + var valueToReplace = new Window () { Id = "w1" }; + var valueToReplaceWith = new Window () { Id = "new" }; + ToplevelEqualityComparer comparer = new ToplevelEqualityComparer (); + + toplevels.Replace (valueToReplace, valueToReplaceWith, comparer); + + var tops = toplevels.ToArray (); + + Assert.Equal ("w4", tops [0].Id); + Assert.Equal ("w3", tops [1].Id); + Assert.Equal ("w2", tops [2].Id); + Assert.Equal ("new", tops [3].Id); + Assert.Equal ("Top", tops [^1].Id); + } + + [Fact] + public void Stack_Toplevels_Swap () + { + Stack toplevels = CreateToplevels (); + + var valueToSwapFrom = new Window () { Id = "w3" }; + var valueToSwapTo = new Window () { Id = "w1" }; + ToplevelEqualityComparer comparer = new ToplevelEqualityComparer (); + toplevels.Swap (valueToSwapFrom, valueToSwapTo, comparer); + + var tops = toplevels.ToArray (); + + Assert.Equal ("w4", tops [0].Id); + Assert.Equal ("w1", tops [1].Id); + Assert.Equal ("w2", tops [2].Id); + Assert.Equal ("w3", tops [3].Id); + Assert.Equal ("Top", tops [^1].Id); + } + + [Fact] + public void Stack_Toplevels_MoveNext () + { + Stack toplevels = CreateToplevels (); + + toplevels.MoveNext (); + + var tops = toplevels.ToArray (); + + Assert.Equal ("w3", tops [0].Id); + Assert.Equal ("w2", tops [1].Id); + Assert.Equal ("w1", tops [2].Id); + Assert.Equal ("Top", tops [3].Id); + Assert.Equal ("w4", tops [^1].Id); + } + + [Fact] + public void Stack_Toplevels_MovePrevious () + { + Stack toplevels = CreateToplevels (); + + toplevels.MovePrevious (); + + var tops = toplevels.ToArray (); + + Assert.Equal ("Top", tops [0].Id); + Assert.Equal ("w4", tops [1].Id); + Assert.Equal ("w3", tops [2].Id); + Assert.Equal ("w2", tops [3].Id); + Assert.Equal ("w1", tops [^1].Id); + } + + [Fact] + public void ToplevelEqualityComparer_GetHashCode () + { + Stack toplevels = CreateToplevels (); + + // Only allows unique keys + HashSet hCodes = new HashSet (); + + foreach (var top in toplevels) { + Assert.True (hCodes.Add (top.GetHashCode ())); + } + } + + [Fact] + public void Stack_Toplevels_FindDuplicates () + { + Stack toplevels = CreateToplevels (); + ToplevelEqualityComparer comparer = new ToplevelEqualityComparer (); + + toplevels.Push (new Toplevel () { Id = "w4" }); + toplevels.Push (new Toplevel () { Id = "w1" }); + + var dup = toplevels.FindDuplicates (comparer).ToArray (); + + Assert.Equal ("w4", dup [0].Id); + Assert.Equal ("w1", dup [^1].Id); + } + + [Fact] + public void Stack_Toplevels_Contains () + { + Stack toplevels = CreateToplevels (); + ToplevelEqualityComparer comparer = new ToplevelEqualityComparer (); + + Assert.True (toplevels.Contains (new Window () { Id = "w2" }, comparer)); + Assert.False (toplevels.Contains (new Toplevel () { Id = "top2" }, comparer)); + } + + [Fact] + public void Stack_Toplevels_MoveTo () + { + Stack toplevels = CreateToplevels (); + + var valueToMove = new Window () { Id = "w1" }; + ToplevelEqualityComparer comparer = new ToplevelEqualityComparer (); + + toplevels.MoveTo (valueToMove, 1, comparer); + + var tops = toplevels.ToArray (); + + Assert.Equal ("w4", tops [0].Id); + Assert.Equal ("w1", tops [1].Id); + Assert.Equal ("w3", tops [2].Id); + Assert.Equal ("w2", tops [3].Id); + Assert.Equal ("Top", tops [^1].Id); + } + + [Fact] + public void Stack_Toplevels_MoveTo_From_Last_To_Top () + { + Stack toplevels = CreateToplevels (); + + var valueToMove = new Window () { Id = "Top" }; + ToplevelEqualityComparer comparer = new ToplevelEqualityComparer (); + + toplevels.MoveTo (valueToMove, 0, comparer); + + var tops = toplevels.ToArray (); + + Assert.Equal ("Top", tops [0].Id); + Assert.Equal ("w4", tops [1].Id); + Assert.Equal ("w3", tops [2].Id); + Assert.Equal ("w2", tops [3].Id); + Assert.Equal ("w1", tops [^1].Id); + } + + + private Stack CreateToplevels () + { + Stack toplevels = new Stack (); + + toplevels.Push (new Toplevel () { Id = "Top" }); + toplevels.Push (new Window () { Id = "w1" }); + toplevels.Push (new Window () { Id = "w2" }); + toplevels.Push (new Window () { Id = "w3" }); + toplevels.Push (new Window () { Id = "w4" }); + + return toplevels; + } + } +} diff --git a/UnitTests/ViewTests.cs b/UnitTests/ViewTests.cs index d6abee6d2e..cd4b6a52f3 100644 --- a/UnitTests/ViewTests.cs +++ b/UnitTests/ViewTests.cs @@ -1332,5 +1332,43 @@ public void AutoSize_True_SetWidthHeight_With_Dim_Fill_And_Dim_Absolute () Assert.True (label.AutoSize); Assert.Equal ("{X=0,Y=0,Width=28,Height=2}", label.Bounds.ToString ()); } + + [Theory] + [InlineData (1)] + [InlineData (2)] + [InlineData (3)] + public void LabelChangeText_RendersCorrectly_Constructors (int choice) + { + var driver = new FakeDriver (); + Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + try { + // Create a label with a short text + Label lbl; + var text = "test"; + + if (choice == 1) { + // An object initializer should call the default constructor. + lbl = new Label { Text = text }; + } else if (choice == 2) { + // Calling the default constructor followed by the object initializer. + lbl = new Label () { Text = text }; + } else { + // Calling the Text constructor. + lbl = new Label (text); + } + lbl.ColorScheme = new ColorScheme (); + lbl.Redraw (lbl.Bounds); + + // should have the initial text + Assert.Equal ('t', driver.Contents [0, 0, 0]); + Assert.Equal ('e', driver.Contents [0, 1, 0]); + Assert.Equal ('s', driver.Contents [0, 2, 0]); + Assert.Equal ('t', driver.Contents [0, 3, 0]); + Assert.Equal (' ', driver.Contents [0, 4, 0]); + } finally { + Application.Shutdown (); + } + } } } From 5a35bcae264d380eb1fa8e7d5ec154914b0a12f9 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 11 Jul 2021 12:49:26 +0100 Subject: [PATCH 06/15] Fixes #1317. Forces wakeup the mainloop in a thread. --- Terminal.Gui/Core/Application.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index 0ba5ffd51e..7ef15a2b36 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -1223,5 +1223,13 @@ internal static bool ShowChild (Toplevel top) } return false; } + + /// + /// Wakes up the mainloop that might be waiting on input, must be thread safe. + /// + public static void DoEvents () + { + MainLoop.Driver.Wakeup (); + } } } From 0f77914e69df3760333f73bfa7888ff2d5a76295 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 15 Jul 2021 17:56:19 +0100 Subject: [PATCH 07/15] Turning MdiTop as read only. --- Terminal.Gui/Core/Application.cs | 35 ++++++++++++++------------------ 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index 7ef15a2b36..a880e6f2c8 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -83,7 +83,14 @@ public static List MdiChildes { /// /// The object used for the application on startup which is true. /// - public static Toplevel MdiTop { get; private set; } + public static Toplevel MdiTop { + get { + if (Top.IsMdiContainer) { + return Top; + } + return null; + } + } /// /// The object used for the application on startup () @@ -275,9 +282,6 @@ static void Init (Func topLevelFactory, ConsoleDriver driver = null, I SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); } Top = topLevelFactory (); - if (Top.IsMdiContainer) { - MdiTop = Top; - } Current = Top; _initialized = true; } @@ -699,8 +703,7 @@ public static RunState Begin (Toplevel toplevel) } } if (toplevel.IsMdiContainer) { - MdiTop = toplevel; - Top = MdiTop; + Top = toplevel; } var refreshDriver = true; @@ -783,7 +786,6 @@ static void ResetState () toplevels.Clear (); Current = null; Top = null; - MdiTop = null; MainLoop = null; Driver?.End (); @@ -843,18 +845,15 @@ internal static void End (View view) MdiTop.OnChildClosed (view as Toplevel); } - if (toplevels.Count == 1 && Current == MdiTop) { - MdiTop.OnAllChildClosed (); - if (!MdiTop.IsMdiContainer) { - MdiTop = null; - } - } - if (toplevels.Count == 0) { Current = null; } else { Current = toplevels.Peek (); - SetCurrentAsTop (); + if (toplevels.Count == 1 && Current == MdiTop) { + MdiTop.OnAllChildClosed (); + } else { + SetCurrentAsTop (); + } Refresh (); } } @@ -980,11 +979,7 @@ public static void Run (Func errorHandler = null) { if (_initialized && Driver != null) { var top = new T (); - if (top.GetType ().BaseType == typeof (Toplevel)) { - if (MdiTop == null) { - Top = top; - } - } else { + if (top.GetType ().BaseType != typeof (Toplevel)) { throw new ArgumentException (top.GetType ().BaseType.Name); } Run (top, errorHandler); From 9a73657cba6d754a215b969b447a85c0ce832f33 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 15 Jul 2021 18:27:44 +0100 Subject: [PATCH 08/15] Simplifying the Redraw method. --- Terminal.Gui/Core/Toplevel.cs | 83 ++++++++++++----------------------- 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/Terminal.Gui/Core/Toplevel.cs b/Terminal.Gui/Core/Toplevel.cs index fe525db557..95453b709b 100644 --- a/Terminal.Gui/Core/Toplevel.cs +++ b/Terminal.Gui/Core/Toplevel.cs @@ -495,7 +495,7 @@ internal void EnsureVisibleBounds (Toplevel top, int x, int y, out int nx, out i //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}"); bool m, s; if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) { - m = Application.MdiTop?.MenuBar != null || Application.Top.MenuBar != null; + m = Application.Top.MenuBar != null; } else { m = ((Toplevel)SuperView).MenuBar != null; } @@ -506,9 +506,7 @@ internal void EnsureVisibleBounds (Toplevel top, int x, int y, out int nx, out i } ny = Math.Max (y, l); if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) { - s = (Application.MdiTop != null && Application.MdiTop.StatusBar != null - && Application.MdiTop.StatusBar.Visible) - || (Application.Top.StatusBar != null && Application.Top.StatusBar.Visible); + s = Application.Top.StatusBar != null && Application.Top.StatusBar.Visible; } else { s = ((Toplevel)SuperView).StatusBar != null && ((Toplevel)SuperView).StatusBar.Visible; } @@ -543,7 +541,7 @@ internal void PositionToplevels () public virtual void PositionToplevel (Toplevel top) { EnsureVisibleBounds (top, top.Frame.X, top.Frame.Y, out int nx, out int ny); - if ((top?.SuperView != null || Application.MdiTop != null && top != Application.MdiTop) + if ((top?.SuperView != null || top != Application.Top) && (nx > top.Frame.X || ny > top.Frame.Y) && top.LayoutStyle == LayoutStyle.Computed) { if ((top.X == null || top.X is Pos.PosAbsolute) && top.Bounds.X != nx) { top.X = nx; @@ -556,9 +554,9 @@ public virtual void PositionToplevel (Toplevel top) View superView = null; StatusBar statusBar = null; - if (top != Application.MdiTop && Application.MdiTop != null && Application.MdiTop.StatusBar != null) { - superView = Application.MdiTop; - statusBar = Application.MdiTop.StatusBar; + if (top != Application.Top && Application.Top.StatusBar != null) { + superView = Application.Top; + statusBar = Application.Top.StatusBar; } else if (top?.SuperView != null && top.SuperView is Toplevel toplevel) { superView = top.SuperView; statusBar = toplevel.StatusBar; @@ -569,7 +567,7 @@ public virtual void PositionToplevel (Toplevel top) top.Height = Dim.Fill (statusBar.Visible ? 1 : 0); } } - if (superView == Application.MdiTop) { + if (superView == Application.Top) { top.SetRelativeLayout (superView.Frame); } else { superView.LayoutSubviews (); @@ -587,52 +585,17 @@ public virtual void PositionToplevel (Toplevel top) /// public override void Redraw (Rect bounds) { - if (this == Application.MdiTop) { - RedrawMdi (bounds); - } else { - if (!CanBeVisible (this)) { - return; - } - - if (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded) { - Driver.SetAttribute (ColorScheme.Normal); - - // This is the Application.Top. Clear just the region we're being asked to redraw - // (the bounds passed to us). - // Must be the screen-relative region to clear, not the bounds. - Clear (Frame); - Driver.SetAttribute (Colors.Base.Normal); - } - } - - if (LayoutStyle == LayoutStyle.Computed) - SetRelativeLayout (Bounds); - PositionToplevels (); - LayoutSubviews (); - - foreach (var view in Subviews) { - if (view.Frame.IntersectsWith (bounds) && !OutsideTopFrame (this)) { - view.SetNeedsLayout (); - view.SetNeedsDisplay (view.Bounds); - view.Redraw (view.Bounds); - } - } - - ClearLayoutNeeded (); - ClearNeedsDisplay (); - } - - void RedrawMdi (Rect bounds) - { - if (!IsMdiContainer) { + if (!Visible) { return; } if (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded) { Driver.SetAttribute (ColorScheme.Normal); + // This is the Application.Top. Clear just the region we're being asked to redraw + // (the bounds passed to us). + // Must be the screen-relative region to clear, not the bounds. Clear (Frame); - Driver.SetAttribute (Colors.Base.Normal); if (LayoutStyle == LayoutStyle.Computed) @@ -640,19 +603,31 @@ void RedrawMdi (Rect bounds) PositionToplevels (); LayoutSubviews (); - foreach (var top in Application.MdiChildes.AsEnumerable ().Reverse ()) { - if (top.Frame.IntersectsWith (bounds)) { - if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible) { - top.SetNeedsLayout (); - top.SetNeedsDisplay (top.Bounds); - top.Redraw (top.Bounds); + if (this == Application.MdiTop) { + foreach (var top in Application.MdiChildes.AsEnumerable ().Reverse ()) { + if (top.Frame.IntersectsWith (bounds)) { + if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible) { + top.SetNeedsLayout (); + top.SetNeedsDisplay (top.Bounds); + top.Redraw (top.Bounds); + } } } } + foreach (var view in Subviews) { + if (view.Frame.IntersectsWith (bounds) && !OutsideTopFrame (this)) { + view.SetNeedsLayout (); + view.SetNeedsDisplay (view.Bounds); + //view.Redraw (view.Bounds); + } + } + ClearLayoutNeeded (); ClearNeedsDisplay (); } + + base.Redraw (Bounds); } bool OutsideTopFrame (Toplevel top) From 69200a2f85cb77b2d20ad083c265aa0fed23e146 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 15 Jul 2021 18:37:44 +0100 Subject: [PATCH 09/15] Reverting the draw text. --- Terminal.Gui/Core/View.cs | 42 ++++++++++++--------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 4d6db10ec7..7d575c851e 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -1338,7 +1338,18 @@ public virtual void Redraw (Rect bounds) var clipRect = new Rect (Point.Empty, frame.Size); - DrawText (); + if (ColorScheme != null) { + Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); + } + + if (!ustring.IsNullOrEmpty (Text) || (this is Label && !AutoSize)) { + Clear (); + // Draw any Text + if (textFormatter != null) { + textFormatter.NeedsFormat = true; + } + textFormatter?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); + } // Invoke DrawContentEvent OnDrawContent (bounds); @@ -1366,33 +1377,6 @@ public virtual void Redraw (Rect bounds) ClearNeedsDisplay (); } - void DrawText () - { - if (ColorScheme != null) { - Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); - } - - if (!ustring.IsNullOrEmpty (Text)) { - var savedClip = ClipToBounds (); - Rect viewBounds = Bounds; - if (SuperView != null && viewBounds.Width > SuperView.Bounds.Width) { - viewBounds.Width = SuperView.Bounds.Width; - } - if (SuperView != null && viewBounds.Height > SuperView.Bounds.Height) { - viewBounds.Height = SuperView.Bounds.Height; - } - var viewFrame = ViewToScreen (viewBounds); - Clear (viewFrame); - // Draw any Text - if (textFormatter != null) { - textFormatter.NeedsFormat = true; - } - textFormatter?.Draw (viewFrame, HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); - - Driver.Clip = savedClip; - } - } - /// /// Event invoked when the content area of the View is to be drawn. /// @@ -2276,7 +2260,7 @@ public void EndInit () /// public bool Visible { get; set; } = true; - internal bool CanBeVisible (View view) + bool CanBeVisible (View view) { if (!view.Visible) { return false; From f32a76b4a93243ecfc9a5b8ed7077c41039f4193 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 15 Jul 2021 19:24:10 +0100 Subject: [PATCH 10/15] Fixes the View Menu to set current child if is visible. --- UICatalog/Scenarios/BackgroundWorkerCollection.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/UICatalog/Scenarios/BackgroundWorkerCollection.cs b/UICatalog/Scenarios/BackgroundWorkerCollection.cs index 9c0b746f05..0cc30f211e 100644 --- a/UICatalog/Scenarios/BackgroundWorkerCollection.cs +++ b/UICatalog/Scenarios/BackgroundWorkerCollection.cs @@ -113,7 +113,11 @@ private MenuBarItem View () item.Action += () => { var top = Application.MdiChildes.Find ((x) => x.Data.ToString () == "WorkerApp"); item.Checked = top.Visible = !item.Checked; - Application.MdiTop.SetNeedsDisplay (); + if (top.Visible) { + top.ShowChild (); + } else { + Application.MdiTop.SetNeedsDisplay (); + } }; menuItems.Add (item); return new MenuBarItem ("_View", From 623b6bf24bad0570ccfe335bc910d4b3628fee8c Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 15 Jul 2021 19:27:13 +0100 Subject: [PATCH 11/15] Fixes the height from hiding the first character. --- UICatalog/Scenarios/AllViewsTester.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs index 7b8e3bba09..a91cb289c3 100644 --- a/UICatalog/Scenarios/AllViewsTester.cs +++ b/UICatalog/Scenarios/AllViewsTester.cs @@ -228,7 +228,8 @@ public override void Setup () Top.Add (_leftPane, _settingsPane, _hostPane); - _curView = CreateClass (_viewClasses.First ().Value); + // This is not needed because ListView always run the SelectedItemChanged event at first time + //_curView = CreateClass (_viewClasses.First ().Value); } void DimPosChanged (View view) From 5851d0e6f1dace6e8b56524933a9b905d79ed4dd Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 15 Jul 2021 19:29:35 +0100 Subject: [PATCH 12/15] Adding more unit tests. --- UnitTests/ApplicationTests.cs | 151 ++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/UnitTests/ApplicationTests.cs b/UnitTests/ApplicationTests.cs index bd9cac432b..59406f0f19 100644 --- a/UnitTests/ApplicationTests.cs +++ b/UnitTests/ApplicationTests.cs @@ -976,5 +976,156 @@ public void MdiContainer_Open_And_Close_Modal_And_Open_Not_Modal_Toplevels_Rando Application.Shutdown (); } + + [Fact] + public void AllChildClosed_Event_Test () + { + Init (); + + var mdi = new Mdi (); + var c1 = new Toplevel (); + var c2 = new Window (); + var c3 = new Window (); + + // MdiChild = c1, c2, c3 + var iterations = 3; + + mdi.Ready += () => { + Assert.Empty (Application.MdiChildes); + Application.Run (c1); + }; + c1.Ready += () => { + Assert.Single (Application.MdiChildes); + Application.Run (c2); + }; + c2.Ready += () => { + Assert.Equal (2, Application.MdiChildes.Count); + Application.Run (c3); + }; + c3.Ready += () => { + Assert.Equal (3, Application.MdiChildes.Count); + c3.RequestStop (); + c2.RequestStop (); + c1.RequestStop (); + }; + // Now this will close the MdiContainer when all MdiChildes was closed + mdi.AllChildClosed += () => { + mdi.RequestStop (); + }; + Application.Iteration += () => { + if (iterations == 3) { + // The Current still is c3 because Current.Running is false. + Assert.True (Application.Current == c3); + Assert.False (Application.Current.Running); + // But the childes order were reorder by Running = false + Assert.True (Application.MdiChildes [0] == c3); + Assert.True (Application.MdiChildes [1] == c2); + Assert.True (Application.MdiChildes [^1] == c1); + } else if (iterations == 2) { + // The Current is c2 and Current.Running is false. + Assert.True (Application.Current == c2); + Assert.False (Application.Current.Running); + Assert.True (Application.MdiChildes [0] == c2); + Assert.True (Application.MdiChildes [^1] == c1); + } else if (iterations == 1) { + // The Current is c1 and Current.Running is false. + Assert.True (Application.Current == c1); + Assert.False (Application.Current.Running); + Assert.True (Application.MdiChildes [^1] == c1); + } else { + // The Current is mdi. + Assert.True (Application.Current == mdi); + Assert.False (Application.Current.Running); + Assert.Empty (Application.MdiChildes); + } + iterations--; + }; + + Application.Run (mdi); + + Assert.Empty (Application.MdiChildes); + + Application.Shutdown (); + } + + [Fact] + public void SetCurrentAsTop_Run_A_Not_Modal_Toplevel_Make_It_The_Current_Application_Top () + { + Init (); + + var t1 = new Toplevel (); + var t2 = new Toplevel (); + var t3 = new Toplevel (); + var d = new Dialog (); + var t4 = new Toplevel (); + + // t1, t2, t3, d, t4 + var iterations = 5; + + t1.Ready += () => { + Assert.Equal (t1, Application.Top); + Application.Run (t2); + }; + t2.Ready += () => { + Assert.Equal (t2, Application.Top); + Application.Run (t3); + }; + t3.Ready += () => { + Assert.Equal (t3, Application.Top); + Application.Run (d); + }; + d.Ready += () => { + Assert.Equal (t3, Application.Top); + Application.Run (t4); + }; + t4.Ready += () => { + Assert.Equal (t4, Application.Top); + t4.RequestStop (); + d.RequestStop (); + t3.RequestStop (); + t2.RequestStop (); + }; + // Now this will close the MdiContainer when all MdiChildes was closed + t2.Closed += (_) => { + t1.RequestStop (); + }; + Application.Iteration += () => { + if (iterations == 5) { + // The Current still is t4 because Current.Running is false. + Assert.Equal (t4, Application.Current); + Assert.False (Application.Current.Running); + Assert.Equal (t4, Application.Top); + } else if (iterations == 4) { + // The Current is d and Current.Running is false. + Assert.Equal (d, Application.Current); + Assert.False (Application.Current.Running); + Assert.Equal (t4, Application.Top); + } else if (iterations == 3) { + // The Current is t3 and Current.Running is false. + Assert.Equal (t3, Application.Current); + Assert.False (Application.Current.Running); + Assert.Equal (t3, Application.Top); + } else if (iterations == 2) { + // The Current is t2 and Current.Running is false. + Assert.Equal (t2, Application.Current); + Assert.False (Application.Current.Running); + Assert.Equal (t2, Application.Top); + } else { + // The Current is t1. + Assert.Equal (t1, Application.Current); + Assert.False (Application.Current.Running); + Assert.Equal (t1, Application.Top); + } + iterations--; + }; + + Application.Run (t1); + + Assert.Equal (t1, Application.Top); + + Application.Shutdown (); + + Assert.Null (Application.Top); + } } } From 57a3a5d9f1d604b90ab56f266dc15a36da45d596 Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 16 Jul 2021 11:46:50 +0100 Subject: [PATCH 13/15] Fixes #1260. Added unit test to emulate the "All Views Tester" scenario. --- UnitTests/ScenarioTests.cs | 415 +++++++++++++++++++++++++++++++++++++ 1 file changed, 415 insertions(+) diff --git a/UnitTests/ScenarioTests.cs b/UnitTests/ScenarioTests.cs index 03b0372c87..8c31c4ba17 100644 --- a/UnitTests/ScenarioTests.cs +++ b/UnitTests/ScenarioTests.cs @@ -1,6 +1,8 @@ +using NStack; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Terminal.Gui; using UICatalog; using Xunit; @@ -157,5 +159,418 @@ public void Run_Generic () Responder.Instances.Clear (); #endif } + + [Fact] + public void Run_All_Views_Tester_Scenario () + { + Window _leftPane; + ListView _classListView; + FrameView _hostPane; + + Dictionary _viewClasses; + View _curView = null; + + // Settings + FrameView _settingsPane; + CheckBox _computedCheckBox; + FrameView _locationFrame; + RadioGroup _xRadioGroup; + TextField _xText; + int _xVal = 0; + RadioGroup _yRadioGroup; + TextField _yText; + int _yVal = 0; + + FrameView _sizeFrame; + RadioGroup _wRadioGroup; + TextField _wText; + int _wVal = 0; + RadioGroup _hRadioGroup; + TextField _hText; + int _hVal = 0; + List posNames = new List { "Factor", "AnchorEnd", "Center", "Absolute" }; + List dimNames = new List { "Factor", "Fill", "Absolute" }; + + + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + + var Top = Application.Top; + + _viewClasses = GetAllViewClassesCollection () + .OrderBy (t => t.Name) + .Select (t => new KeyValuePair (t.Name, t)) + .ToDictionary (t => t.Key, t => t.Value); + + _leftPane = new Window ("Classes") { + X = 0, + Y = 0, + Width = 15, + Height = Dim.Fill (1), // for status bar + CanFocus = false, + ColorScheme = Colors.TopLevel, + }; + + _classListView = new ListView (_viewClasses.Keys.ToList ()) { + X = 0, + Y = 0, + Width = Dim.Fill (0), + Height = Dim.Fill (0), + AllowsMarking = false, + ColorScheme = Colors.TopLevel, + }; + _leftPane.Add (_classListView); + + _settingsPane = new FrameView ("Settings") { + X = Pos.Right (_leftPane), + Y = 0, // for menu + Width = Dim.Fill (), + Height = 10, + CanFocus = false, + ColorScheme = Colors.TopLevel, + }; + _computedCheckBox = new CheckBox ("Computed Layout", true) { X = 0, Y = 0 }; + _settingsPane.Add (_computedCheckBox); + + var radioItems = new ustring [] { "Percent(x)", "AnchorEnd(x)", "Center", "At(x)" }; + _locationFrame = new FrameView ("Location (Pos)") { + X = Pos.Left (_computedCheckBox), + Y = Pos.Bottom (_computedCheckBox), + Height = 3 + radioItems.Length, + Width = 36, + }; + _settingsPane.Add (_locationFrame); + + var label = new Label ("x:") { X = 0, Y = 0 }; + _locationFrame.Add (label); + _xRadioGroup = new RadioGroup (radioItems) { + X = 0, + Y = Pos.Bottom (label), + }; + _xText = new TextField ($"{_xVal}") { X = Pos.Right (label) + 1, Y = 0, Width = 4 }; + _locationFrame.Add (_xText); + + _locationFrame.Add (_xRadioGroup); + + radioItems = new ustring [] { "Percent(y)", "AnchorEnd(y)", "Center", "At(y)" }; + label = new Label ("y:") { X = Pos.Right (_xRadioGroup) + 1, Y = 0 }; + _locationFrame.Add (label); + _yText = new TextField ($"{_yVal}") { X = Pos.Right (label) + 1, Y = 0, Width = 4 }; + _locationFrame.Add (_yText); + _yRadioGroup = new RadioGroup (radioItems) { + X = Pos.X (label), + Y = Pos.Bottom (label), + }; + _locationFrame.Add (_yRadioGroup); + + _sizeFrame = new FrameView ("Size (Dim)") { + X = Pos.Right (_locationFrame), + Y = Pos.Y (_locationFrame), + Height = 3 + radioItems.Length, + Width = 40, + }; + + radioItems = new ustring [] { "Percent(width)", "Fill(width)", "Sized(width)" }; + label = new Label ("width:") { X = 0, Y = 0 }; + _sizeFrame.Add (label); + _wRadioGroup = new RadioGroup (radioItems) { + X = 0, + Y = Pos.Bottom (label), + }; + _wText = new TextField ($"{_wVal}") { X = Pos.Right (label) + 1, Y = 0, Width = 4 }; + _sizeFrame.Add (_wText); + _sizeFrame.Add (_wRadioGroup); + + radioItems = new ustring [] { "Percent(height)", "Fill(height)", "Sized(height)" }; + label = new Label ("height:") { X = Pos.Right (_wRadioGroup) + 1, Y = 0 }; + _sizeFrame.Add (label); + _hText = new TextField ($"{_hVal}") { X = Pos.Right (label) + 1, Y = 0, Width = 4 }; + _sizeFrame.Add (_hText); + + _hRadioGroup = new RadioGroup (radioItems) { + X = Pos.X (label), + Y = Pos.Bottom (label), + }; + _sizeFrame.Add (_hRadioGroup); + + _settingsPane.Add (_sizeFrame); + + _hostPane = new FrameView ("") { + X = Pos.Right (_leftPane), + Y = Pos.Bottom (_settingsPane), + Width = Dim.Fill (), + Height = Dim.Fill (1), // + 1 for status bar + ColorScheme = Colors.Dialog, + }; + + _classListView.OpenSelectedItem += (a) => { + _settingsPane.SetFocus (); + }; + _classListView.SelectedItemChanged += (args) => { + ClearClass (_curView); + _curView = CreateClass (_viewClasses.Values.ToArray () [_classListView.SelectedItem]); + }; + + _computedCheckBox.Toggled += (previousState) => { + if (_curView != null) { + _curView.LayoutStyle = previousState ? LayoutStyle.Absolute : LayoutStyle.Computed; + _hostPane.LayoutSubviews (); + } + }; + + _xRadioGroup.SelectedItemChanged += (selected) => DimPosChanged (_curView); + + _xText.TextChanged += (args) => { + try { + _xVal = int.Parse (_xText.Text.ToString ()); + DimPosChanged (_curView); + } catch { + + } + }; + + _yText.TextChanged += (args) => { + try { + _yVal = int.Parse (_yText.Text.ToString ()); + DimPosChanged (_curView); + } catch { + + } + }; + + _yRadioGroup.SelectedItemChanged += (selected) => DimPosChanged (_curView); + + _wRadioGroup.SelectedItemChanged += (selected) => DimPosChanged (_curView); + + _wText.TextChanged += (args) => { + try { + _wVal = int.Parse (_wText.Text.ToString ()); + DimPosChanged (_curView); + } catch { + + } + }; + + _hText.TextChanged += (args) => { + try { + _hVal = int.Parse (_hText.Text.ToString ()); + DimPosChanged (_curView); + } catch { + + } + }; + + _hRadioGroup.SelectedItemChanged += (selected) => DimPosChanged (_curView); + + Top.Add (_leftPane, _settingsPane, _hostPane); + + // This is not needed because ListView always run the SelectedItemChanged event at first time + //_curView = CreateClass (_viewClasses.First ().Value); + + int iterations = 0; + + Application.Iteration += () => { + iterations++; + + if (iterations < _viewClasses.Count) { + _classListView.MoveDown (); + } else { + Application.RequestStop (); + } + }; + + Application.Run (); + + Assert.Equal (_viewClasses.Count, iterations); + + Application.Shutdown (); + + + void DimPosChanged (View view) + { + if (view == null) { + return; + } + + var layout = view.LayoutStyle; + + try { + view.LayoutStyle = LayoutStyle.Absolute; + + switch (_xRadioGroup.SelectedItem) { + case 0: + view.X = Pos.Percent (_xVal); + break; + case 1: + view.X = Pos.AnchorEnd (_xVal); + break; + case 2: + view.X = Pos.Center (); + break; + case 3: + view.X = Pos.At (_xVal); + break; + } + + switch (_yRadioGroup.SelectedItem) { + case 0: + view.Y = Pos.Percent (_yVal); + break; + case 1: + view.Y = Pos.AnchorEnd (_yVal); + break; + case 2: + view.Y = Pos.Center (); + break; + case 3: + view.Y = Pos.At (_yVal); + break; + } + + switch (_wRadioGroup.SelectedItem) { + case 0: + view.Width = Dim.Percent (_wVal); + break; + case 1: + view.Width = Dim.Fill (_wVal); + break; + case 2: + view.Width = Dim.Sized (_wVal); + break; + } + + switch (_hRadioGroup.SelectedItem) { + case 0: + view.Height = Dim.Percent (_hVal); + break; + case 1: + view.Height = Dim.Fill (_hVal); + break; + case 2: + view.Height = Dim.Sized (_hVal); + break; + } + } catch (Exception e) { + MessageBox.ErrorQuery ("Exception", e.Message, "Ok"); + } finally { + view.LayoutStyle = layout; + } + UpdateTitle (view); + } + + void UpdateSettings (View view) + { + var x = view.X.ToString (); + var y = view.Y.ToString (); + _xRadioGroup.SelectedItem = posNames.IndexOf (posNames.Where (s => x.Contains (s)).First ()); + _yRadioGroup.SelectedItem = posNames.IndexOf (posNames.Where (s => y.Contains (s)).First ()); + _xText.Text = $"{view.Frame.X}"; + _yText.Text = $"{view.Frame.Y}"; + + var w = view.Width.ToString (); + var h = view.Height.ToString (); + _wRadioGroup.SelectedItem = dimNames.IndexOf (dimNames.Where (s => w.Contains (s)).First ()); + _hRadioGroup.SelectedItem = dimNames.IndexOf (dimNames.Where (s => h.Contains (s)).First ()); + _wText.Text = $"{view.Frame.Width}"; + _hText.Text = $"{view.Frame.Height}"; + } + + void UpdateTitle (View view) + { + _hostPane.Title = $"{view.GetType ().Name} - {view.X.ToString ()}, {view.Y.ToString ()}, {view.Width.ToString ()}, {view.Height.ToString ()}"; + } + + List GetAllViewClassesCollection () + { + List types = new List (); + foreach (Type type in typeof (View).Assembly.GetTypes () + .Where (myType => myType.IsClass && !myType.IsAbstract && myType.IsPublic && myType.IsSubclassOf (typeof (View)))) { + types.Add (type); + } + return types; + } + + void ClearClass (View view) + { + // Remove existing class, if any + if (view != null) { + view.LayoutComplete -= LayoutCompleteHandler; + _hostPane.Remove (view); + view.Dispose (); + _hostPane.Clear (); + } + } + + View CreateClass (Type type) + { + // If we are to create a generic Type + if (type.IsGenericType) { + + // For each of the arguments + List typeArguments = new List (); + + // use + foreach (var arg in type.GetGenericArguments ()) { + typeArguments.Add (typeof (object)); + } + + // And change what type we are instantiating from MyClass to MyClass + type = type.MakeGenericType (typeArguments.ToArray ()); + } + // Instantiate view + var view = (View)Activator.CreateInstance (type); + + //_curView.X = Pos.Center (); + //_curView.Y = Pos.Center (); + view.Width = Dim.Percent (75); + view.Height = Dim.Percent (75); + + // Set the colorscheme to make it stand out if is null by default + if (view.ColorScheme == null) { + view.ColorScheme = Colors.Base; + } + + // If the view supports a Text property, set it so we have something to look at + if (view.GetType ().GetProperty ("Text") != null) { + try { + view.GetType ().GetProperty ("Text")?.GetSetMethod ()?.Invoke (view, new [] { ustring.Make ("Test Text") }); + } catch (TargetInvocationException e) { + MessageBox.ErrorQuery ("Exception", e.InnerException.Message, "Ok"); + view = null; + } + } + + // If the view supports a Title property, set it so we have something to look at + if (view != null && view.GetType ().GetProperty ("Title") != null) { + view?.GetType ().GetProperty ("Title")?.GetSetMethod ()?.Invoke (view, new [] { ustring.Make ("Test Title") }); + } + + // If the view supports a Source property, set it so we have something to look at + if (view != null && view.GetType ().GetProperty ("Source") != null && view.GetType ().GetProperty ("Source").PropertyType == typeof (Terminal.Gui.IListDataSource)) { + var source = new ListWrapper (new List () { ustring.Make ("Test Text #1"), ustring.Make ("Test Text #2"), ustring.Make ("Test Text #3") }); + view?.GetType ().GetProperty ("Source")?.GetSetMethod ()?.Invoke (view, new [] { source }); + } + + // Set Settings + _computedCheckBox.Checked = view.LayoutStyle == LayoutStyle.Computed; + + // Add + _hostPane.Add (view); + //DimPosChanged (); + _hostPane.LayoutSubviews (); + _hostPane.Clear (); + _hostPane.SetNeedsDisplay (); + UpdateSettings (view); + UpdateTitle (view); + + view.LayoutComplete += LayoutCompleteHandler; + + return view; + } + + void LayoutCompleteHandler (View.LayoutEventArgs args) + { + UpdateTitle (_curView); + } + } } } From 1aa1161163ef75102e618221e35319f15c4a4cde Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 16 Jul 2021 13:04:25 +0100 Subject: [PATCH 14/15] Asserting type names. --- UnitTests/ScenarioTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UnitTests/ScenarioTests.cs b/UnitTests/ScenarioTests.cs index 8c31c4ba17..d95be2f0c5 100644 --- a/UnitTests/ScenarioTests.cs +++ b/UnitTests/ScenarioTests.cs @@ -373,6 +373,8 @@ public void Run_All_Views_Tester_Scenario () if (iterations < _viewClasses.Count) { _classListView.MoveDown (); + Assert.Equal (_curView.GetType ().Name, + _viewClasses.Values.ToArray () [_classListView.SelectedItem].Name); } else { Application.RequestStop (); } From 138a14746d6c5582082c4e4a0fdf06e87d275629 Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 16 Jul 2021 15:17:20 +0100 Subject: [PATCH 15/15] Using LayoutSubviews to fix the size height hiding first character. --- UICatalog/Scenarios/AllViewsTester.cs | 5 +++-- UnitTests/ScenarioTests.cs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs index a91cb289c3..0cc2bf3d32 100644 --- a/UICatalog/Scenarios/AllViewsTester.cs +++ b/UICatalog/Scenarios/AllViewsTester.cs @@ -228,8 +228,9 @@ public override void Setup () Top.Add (_leftPane, _settingsPane, _hostPane); - // This is not needed because ListView always run the SelectedItemChanged event at first time - //_curView = CreateClass (_viewClasses.First ().Value); + Top.LayoutSubviews (); + + _curView = CreateClass (_viewClasses.First ().Value); } void DimPosChanged (View view) diff --git a/UnitTests/ScenarioTests.cs b/UnitTests/ScenarioTests.cs index d95be2f0c5..e3a807e3ff 100644 --- a/UnitTests/ScenarioTests.cs +++ b/UnitTests/ScenarioTests.cs @@ -363,8 +363,9 @@ public void Run_All_Views_Tester_Scenario () Top.Add (_leftPane, _settingsPane, _hostPane); - // This is not needed because ListView always run the SelectedItemChanged event at first time - //_curView = CreateClass (_viewClasses.First ().Value); + Top.LayoutSubviews (); + + _curView = CreateClass (_viewClasses.First ().Value); int iterations = 0;