diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index 95d3dadada..a880e6f2c8 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,43 @@ 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 { + if (Top.IsMdiContainer) { + return Top; + } + return null; + } + } + /// /// The object used for the application on startup () /// @@ -125,8 +157,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 /// @@ -349,6 +379,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 +476,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 +549,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 +592,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 +662,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,17 +679,67 @@ public static RunState Begin (Toplevel toplevel) initializable.BeginInit (); initializable.EndInit (); } - toplevels.Push (toplevel); - Current = toplevel; + + 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) { + Top = toplevel; + } + + 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; } @@ -530,7 +753,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 (); } @@ -544,9 +771,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 @@ -595,8 +823,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 (); @@ -609,10 +839,21 @@ 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 == 0) { Current = null; } else { Current = toplevels.Peek (); + if (toplevels.Count == 1 && Current == MdiTop) { + MdiTop.OnAllChildClosed (); + } else { + SetCurrentAsTop (); + } Refresh (); } } @@ -644,18 +885,29 @@ 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); @@ -665,7 +917,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; @@ -692,8 +977,16 @@ public static void Run (Func errorHandler = null) /// public static void Run (Func errorHandler = null) where T : Toplevel, new() { - Init (() => new T ()); - Run (Top, errorHandler); + if (_initialized && Driver != null) { + var top = new T (); + if (top.GetType ().BaseType != typeof (Toplevel)) { + throw new ArgumentException (top.GetType ().BaseType.Name); + } + Run (top, errorHandler); + } else { + Init (() => new T ()); + Run (Top, errorHandler); + } } /// @@ -723,7 +1016,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) { @@ -751,19 +1044,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; + } } /// @@ -788,17 +1136,95 @@ 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) { - t.PositionToplevels (); t.SetRelativeLayout (full); + t.PositionToplevels (); t.LayoutSubviews (); } Refresh (); } + + static void SetToplevelsSize (Rect full) + { + 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 (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; + } + + /// + /// Wakes up the mainloop that might be waiting on input, must be thread safe. + /// + public static void DoEvents () + { + MainLoop.Driver.Wakeup (); + } } } 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 a933f9a059..95453b709b 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. /// @@ -112,7 +196,7 @@ public Toplevel () : base () void Initialize () { - ColorScheme = Colors.Base; + ColorScheme = Colors.TopLevel; } /// @@ -142,12 +226,26 @@ 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; } + + /// + /// 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 (); @@ -234,21 +336,31 @@ 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: 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: @@ -265,7 +377,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 +431,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 +534,15 @@ 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 || 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; } @@ -435,11 +550,30 @@ 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); + + View superView = null; + StatusBar statusBar = null; + + 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; + } + if (statusBar != null) { + 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); + } + } + if (superView == Application.Top) { + top.SetRelativeLayout (superView.Frame); + } else { + 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,29 +585,128 @@ private void PositionToplevel (Toplevel top) /// public override void Redraw (Rect bounds) { - if (IsCurrentTop || this == Application.Top) { - 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); - Driver.SetAttribute (Colors.Base.Normal); - PositionToplevels (); + if (!Visible) { + return; + } - foreach (var view in Subviews) { - if (view.Frame.IntersectsWith (bounds)) { - view.SetNeedsLayout (); - view.SetNeedsDisplay (view.Bounds); + 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 (); + + 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) + { + 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; } /// @@ -484,5 +717,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..7d575c851e 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; @@ -1342,7 +1342,7 @@ public virtual void Redraw (Rect bounds) Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); } - if (!ustring.IsNullOrEmpty (Text)) { + if (!ustring.IsNullOrEmpty (Text) || (this is Label && !AutoSize)) { Clear (); // Draw any Text if (textFormatter != null) { @@ -1957,8 +1957,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; 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 . /// 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; + } + } } diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs index 4cea78d2ff..0cc2bf3d32 100644 --- a/UICatalog/Scenarios/AllViewsTester.cs +++ b/UICatalog/Scenarios/AllViewsTester.cs @@ -228,6 +228,8 @@ public override void Setup () Top.Add (_leftPane, _settingsPane, _hostPane); + Top.LayoutSubviews (); + _curView = CreateClass (_viewClasses.First ().Value); } @@ -236,7 +238,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 +299,8 @@ void DimPosChanged (View view) } } catch (Exception e) { MessageBox.ErrorQuery ("Exception", e.Message, "Ok"); + } finally { + view.LayoutStyle = layout; } UpdateTitle (view); } @@ -366,8 +375,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/BackgroundWorkerCollection.cs b/UICatalog/Scenarios/BackgroundWorkerCollection.cs new file mode 100644 index 0000000000..0cc30f211e --- /dev/null +++ b/UICatalog/Scenarios/BackgroundWorkerCollection.cs @@ -0,0 +1,393 @@ +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; + if (top.Visible) { + top.ShowChild (); + } else { + 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/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; diff --git a/UnitTests/ApplicationTests.cs b/UnitTests/ApplicationTests.cs index dc0168c4e0..59406f0f19 100644 --- a/UnitTests/ApplicationTests.cs +++ b/UnitTests/ApplicationTests.cs @@ -372,5 +372,760 @@ 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 (); + } + + [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); + } } } 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..e3a807e3ff 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; @@ -72,10 +74,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 +104,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 +136,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 @@ -153,5 +159,421 @@ 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); + + Top.LayoutSubviews (); + + _curView = CreateClass (_viewClasses.First ().Value); + + int iterations = 0; + + Application.Iteration += () => { + iterations++; + + if (iterations < _viewClasses.Count) { + _classListView.MoveDown (); + Assert.Equal (_curView.GetType ().Name, + _viewClasses.Values.ToArray () [_classListView.SelectedItem].Name); + } 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); + } + } } } 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 (); + } + } } }