From 4cd694ac78c36874c542e22b802cf5afa6e6dec8 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 11 Jul 2021 00:12:51 +0100 Subject: [PATCH] Added a Mdi Container feature. --- Terminal.Gui/Core/Application.cs | 455 ++++++++++++++++++++++-- Terminal.Gui/Core/StackExtensions.cs | 196 ++++++++++ Terminal.Gui/Core/Toplevel.cs | 510 +++++++++++++++++++++++++-- Terminal.Gui/Core/View.cs | 55 ++- Terminal.Gui/Core/Window.cs | 71 ---- 5 files changed, 1137 insertions(+), 150 deletions(-) create mode 100644 Terminal.Gui/Core/StackExtensions.cs diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index 47324606a..0ba5ffd51 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 @@ using System.ComponentModel; namespace Terminal.Gui { /// - /// A static, singelton class provding the main application driver for Terminal.Gui apps. + /// A static, singleton class providing the main application driver for Terminal.Gui apps. /// /// /// @@ -55,11 +55,36 @@ namespace Terminal.Gui { /// /// public static class Application { + static Stack toplevels = new Stack (); + /// /// The current in use. /// public static ConsoleDriver Driver; + /// + /// Gets all the Mdi childes which represent all the not modal from the . + /// + public static List MdiChildes { + get { + if (MdiTop != null) { + List mdiChildes = new List (); + foreach (var top in toplevels) { + if (top != MdiTop && !top.Modal) { + mdiChildes.Add (top); + } + } + return mdiChildes; + } + return null; + } + } + + /// + /// The object used for the application on startup which is true. + /// + public static Toplevel MdiTop { get; private set; } + /// /// The object used for the application on startup () /// @@ -125,8 +150,6 @@ namespace Terminal.Gui { /// The main loop. public static MainLoop MainLoop { get; private set; } - static Stack toplevels = new Stack (); - /// /// This event is raised on each iteration of the /// @@ -252,6 +275,9 @@ namespace Terminal.Gui { SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); } Top = topLevelFactory (); + if (Top.IsMdiContainer) { + MdiTop = Top; + } Current = Top; _initialized = true; } @@ -349,6 +375,72 @@ namespace Terminal.Gui { } } + static View FindDeepestTop (Toplevel start, int x, int y, out int resx, out int resy) + { + var startFrame = start.Frame; + + if (!startFrame.Contains (x, y)) { + resx = 0; + resy = 0; + return null; + } + + if (toplevels != null) { + int count = toplevels.Count; + if (count > 0) { + var rx = x - startFrame.X; + var ry = y - startFrame.Y; + foreach (var t in toplevels) { + if (t != Current) { + if (t != start && t.Visible && t.Frame.Contains (rx, ry)) { + start = t; + break; + } + } + } + } + } + resx = x - startFrame.X; + resy = y - startFrame.Y; + return start; + } + + static View FindDeepestMdiView (View start, int x, int y, out int resx, out int resy) + { + if (start.GetType ().BaseType != typeof (Toplevel) + && !((Toplevel)start).IsMdiContainer) { + resx = 0; + resy = 0; + return null; + } + + var startFrame = start.Frame; + + if (!startFrame.Contains (x, y)) { + resx = 0; + resy = 0; + return null; + } + + int count = toplevels.Count; + for (int i = count - 1; i >= 0; i--) { + foreach (var top in toplevels) { + var rx = x - startFrame.X; + var ry = y - startFrame.Y; + if (top.Visible && top.Frame.Contains (rx, ry)) { + var deep = FindDeepestView (top, rx, ry, out resx, out resy); + if (deep == null) + return FindDeepestMdiView (top, rx, ry, out resx, out resy); + if (deep != MdiTop) + return deep; + } + } + } + resx = x - startFrame.X; + resy = y - startFrame.Y; + return start; + } + static View FindDeepestView (View start, int x, int y, out int resx, out int resy) { var startFrame = start.Frame; @@ -380,6 +472,18 @@ namespace Terminal.Gui { return start; } + static View FindTopFromView (View view) + { + View top = view?.SuperView != null ? view.SuperView : view; + + while (top?.SuperView != null) { + if (top?.SuperView != null) { + top = top.SuperView; + } + } + return top; + } + internal static View mouseGrabView; /// @@ -441,6 +545,17 @@ namespace Terminal.Gui { } } + if ((view == null || view == MdiTop) && !Current.Modal && MdiTop != null + && me.Flags != MouseFlags.ReportMousePosition && me.Flags != 0) { + + var top = FindDeepestTop (Top, me.X, me.Y, out _, out _); + view = FindDeepestView (top, me.X, me.Y, out rx, out ry); + + if (view != null && view != MdiTop && top != Current) { + MoveCurrent ((Toplevel)top); + } + } + if (view != null) { var nme = new MouseEvent () { X = rx, @@ -473,6 +588,56 @@ namespace Terminal.Gui { } } + // Only return true if the Current has changed. + static bool MoveCurrent (Toplevel top) + { + // The Current is modal and the top is not modal toplevel then + // the Current must be moved above the first not modal toplevel. + if (MdiTop != null && top != MdiTop && top != Current && Current?.Modal == true && !toplevels.Peek ().Modal) { + lock (toplevels) { + toplevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); + } + var index = 0; + var savedToplevels = toplevels.ToArray (); + foreach (var t in savedToplevels) { + if (!t.Modal && t != Current && t != top && t != savedToplevels [index]) { + lock (toplevels) { + toplevels.MoveTo (top, index, new ToplevelEqualityComparer ()); + } + } + index++; + } + return false; + } + // The Current and the top are both not running toplevel then + // the top must be moved above the first not running toplevel. + if (MdiTop != null && top != MdiTop && top != Current && Current?.Running == false && !top.Running) { + lock (toplevels) { + toplevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); + } + var index = 0; + foreach (var t in toplevels.ToArray ()) { + if (!t.Running && t != Current && index > 0) { + lock (toplevels) { + toplevels.MoveTo (top, index - 1, new ToplevelEqualityComparer ()); + } + } + index++; + } + return false; + } + if ((MdiTop != null && top?.Modal == true && toplevels.Peek () != top) + || (MdiTop != null && Current != MdiTop && Current?.Modal == false && top == MdiTop) + || (MdiTop != null && Current?.Modal == false && top != Current) + || (MdiTop != null && Current?.Modal == true && top == MdiTop)) { + lock (toplevels) { + toplevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); + Current = top; + } + } + return true; + } + static bool OutsideFrame (Point p, Rect r) { return p.X < 0 || p.X > r.Width - 1 || p.Y < 0 || p.Y > r.Height - 1; @@ -493,8 +658,12 @@ namespace Terminal.Gui { /// public static RunState Begin (Toplevel toplevel) { - if (toplevel == null) + if (toplevel == null) { throw new ArgumentNullException (nameof (toplevel)); + } else if (toplevel.IsMdiContainer && MdiTop != null) { + throw new InvalidOperationException ("Only one Mdi Container is allowed."); + } + var rs = new RunState (toplevel); Init (); @@ -506,18 +675,68 @@ namespace Terminal.Gui { initializable.BeginInit (); initializable.EndInit (); } - toplevels.Push (toplevel); - Current = toplevel; - SetCurrentAsTop (); + + lock (toplevels) { + if (string.IsNullOrEmpty (toplevel.Id.ToString ())) { + var count = 1; + var id = (toplevels.Count + count).ToString (); + while (toplevels.Count > 0 && toplevels.FirstOrDefault (x => x.Id.ToString () == id) != null) { + count++; + id = (toplevels.Count + count).ToString (); + } + toplevel.Id = (toplevels.Count + count).ToString (); + + toplevels.Push (toplevel); + } else { + var dup = toplevels.FirstOrDefault (x => x.Id.ToString () == toplevel.Id); + if (dup == null) { + toplevels.Push (toplevel); + } + } + + if (toplevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0) { + throw new ArgumentException ("There are duplicates toplevels Id's"); + } + } + if (toplevel.IsMdiContainer) { + MdiTop = toplevel; + Top = MdiTop; + } + + var refreshDriver = true; + if (MdiTop == null || toplevel.IsMdiContainer || (Current?.Modal == false && toplevel.Modal) + || (Current?.Modal == false && !toplevel.Modal) || (Current?.Modal == true && toplevel.Modal)) { + + if (toplevel.Visible) { + Current = toplevel; + SetCurrentAsTop (); + } else { + refreshDriver = false; + } + } else if ((MdiTop != null && toplevel != MdiTop && Current?.Modal == true && !toplevels.Peek ().Modal) + || (MdiTop != null && toplevel != MdiTop && Current?.Running == false)) { + refreshDriver = false; + MoveCurrent (toplevel); + } else { + refreshDriver = false; + MoveCurrent (Current); + } + Driver.PrepareToRun (MainLoop, ProcessKeyEvent, ProcessKeyDownEvent, ProcessKeyUpEvent, ProcessMouseEvent); if (toplevel.LayoutStyle == LayoutStyle.Computed) toplevel.SetRelativeLayout (new Rect (0, 0, Driver.Cols, Driver.Rows)); + toplevel.PositionToplevels (); toplevel.LayoutSubviews (); toplevel.WillPresent (); - toplevel.OnLoaded (); - Redraw (toplevel); - toplevel.PositionCursor (); - Driver.Refresh (); + if (refreshDriver) { + if (MdiTop != null) { + MdiTop.OnChildLoaded (toplevel); + } + toplevel.OnLoaded (); + Redraw (toplevel); + toplevel.PositionCursor (); + Driver.Refresh (); + } return rs; } @@ -531,7 +750,11 @@ namespace Terminal.Gui { if (runState == null) throw new ArgumentNullException (nameof (runState)); - runState.Toplevel.OnUnloaded (); + if (MdiTop != null) { + MdiTop.OnChildUnloaded (runState.Toplevel); + } else { + runState.Toplevel.OnUnloaded (); + } runState.Dispose (); } @@ -560,6 +783,7 @@ namespace Terminal.Gui { toplevels.Clear (); Current = null; Top = null; + MdiTop = null; MainLoop = null; Driver?.End (); @@ -597,8 +821,10 @@ namespace Terminal.Gui { Driver.UpdateScreen (); View last = null; foreach (var v in toplevels.Reverse ()) { - v.SetNeedsDisplay (); - v.Redraw (v.Bounds); + if (v.Visible) { + v.SetNeedsDisplay (); + v.Redraw (v.Bounds); + } last = v; } last?.PositionCursor (); @@ -611,6 +837,19 @@ namespace Terminal.Gui { throw new ArgumentException ("The view that you end with must be balanced"); toplevels.Pop (); + (view as Toplevel)?.OnClosed ((Toplevel)view); + + if (MdiTop != null && !((Toplevel)view).Modal && view != MdiTop) { + MdiTop.OnChildClosed (view as Toplevel); + } + + if (toplevels.Count == 1 && Current == MdiTop) { + MdiTop.OnAllChildClosed (); + if (!MdiTop.IsMdiContainer) { + MdiTop = null; + } + } + if (toplevels.Count == 0) { Current = null; } else { @@ -648,17 +887,28 @@ namespace Terminal.Gui { MainLoop.MainIteration (); Iteration?.Invoke (); + EnsureModalAlwaysOnTop (state.Toplevel); + if ((state.Toplevel != Current && Current?.Modal == true) + || (state.Toplevel != Current && Current?.Modal == false)) { + MdiTop?.OnDeactivate (state.Toplevel); + state.Toplevel = Current; + MdiTop?.OnActivate (state.Toplevel); + Top.SetChildNeedsDisplay (); + Refresh (); + } if (Driver.EnsureCursorVisibility ()) { state.Toplevel.SetNeedsDisplay (); } } else if (!wait) { return; } - if (state.Toplevel != Top && (!Top.NeedDisplay.IsEmpty || Top.ChildNeedsDisplay || Top.LayoutNeeded)) { + if (state.Toplevel != Top + && (!Top.NeedDisplay.IsEmpty || Top.ChildNeedsDisplay || Top.LayoutNeeded)) { Top.Redraw (Top.Bounds); state.Toplevel.SetNeedsDisplay (state.Toplevel.Bounds); } - if (!state.Toplevel.NeedDisplay.IsEmpty || state.Toplevel.ChildNeedsDisplay || state.Toplevel.LayoutNeeded) { + if (!state.Toplevel.NeedDisplay.IsEmpty || state.Toplevel.ChildNeedsDisplay || state.Toplevel.LayoutNeeded + || MdiChildNeedsDisplay ()) { state.Toplevel.Redraw (state.Toplevel.Bounds); if (DebugDrawBounds) { DrawBounds (state.Toplevel); @@ -668,9 +918,42 @@ namespace Terminal.Gui { } 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; // Need to look into why this does not work properly. @@ -698,14 +981,17 @@ namespace Terminal.Gui { if (_initialized && Driver != null) { var top = new T (); if (top.GetType ().BaseType == typeof (Toplevel)) { - Top = top; + if (MdiTop == null) { + Top = top; + } } else { throw new ArgumentException (top.GetType ().BaseType.Name); } + Run (top, errorHandler); } else { Init (() => new T ()); + Run (Top, errorHandler); } - Run (Top, errorHandler); } /// @@ -735,7 +1021,7 @@ namespace Terminal.Gui { /// When is null the exception is rethrown, when it returns true the application is resumed and when false method exits gracefully. /// /// - /// The tu run modally. + /// The to run modally. /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). public static void Run (Toplevel view, Func errorHandler = null) { @@ -763,19 +1049,74 @@ namespace Terminal.Gui { } /// - /// Stops running the most recent . + /// Stops running the most recent or the if provided. /// + /// The toplevel to request stop. /// /// /// This will cause to return. /// /// - /// Calling is equivalent to setting the property on the curently running to false. + /// Calling is equivalent to setting the property on the currently running to false. /// /// - public static void RequestStop () + public static void RequestStop (Toplevel top = null) { - Current.Running = false; + if (MdiTop == null || top == null || (MdiTop == null && top != null)) { + top = Current; + } + + if (MdiTop != null && top.IsMdiContainer && top?.Running == true + && (Current?.Modal == false || (Current?.Modal == true && Current?.Running == false))) { + + MdiTop.RequestStop (); + } else if (MdiTop != null && top != Current && Current?.Running == true && Current?.Modal == true + && top.Modal && top.Running) { + + var ev = new ToplevelClosingEventArgs (Current); + Current.OnClosing (ev); + if (ev.Cancel) { + return; + } + ev = new ToplevelClosingEventArgs (top); + top.OnClosing (ev); + if (ev.Cancel) { + return; + } + Current.Running = false; + top.Running = false; + } else if ((MdiTop != null && top != MdiTop && top != Current && Current?.Modal == false + && Current?.Running == true && !top.Running) + || (MdiTop != null && top != MdiTop && top != Current && Current?.Modal == false + && Current?.Running == false && !top.Running && toplevels.ToArray () [1].Running)) { + + MoveCurrent (top); + } else if (MdiTop != null && Current != top && Current?.Running == true && !top.Running + && Current?.Modal == true && top.Modal) { + // The Current and the top are both modal so needed to set the Current.Running to false too. + Current.Running = false; + } else if (MdiTop != null && Current == top && MdiTop?.Running == true && Current?.Running == true && top.Running + && Current?.Modal == true && top.Modal) { + // The MdiTop was requested to stop inside a modal toplevel which is the Current and top, + // both are the same, so needed to set the Current.Running to false too. + Current.Running = false; + } else { + Toplevel currentTop; + if (top == Current || (Current?.Modal == true && !top.Modal)) { + currentTop = Current; + } else { + currentTop = top; + } + if (!currentTop.Running) { + return; + } + var ev = new ToplevelClosingEventArgs (currentTop); + currentTop.OnClosing (ev); + if (ev.Cancel) { + return; + } + currentTop.Running = false; + } } /// @@ -804,8 +1145,8 @@ namespace Terminal.Gui { Resized?.Invoke (new ResizedEventArgs () { Cols = full.Width, Rows = full.Height }); Driver.Clip = full; foreach (var t in toplevels) { - t.PositionToplevels (); t.SetRelativeLayout (full); + t.PositionToplevels (); t.LayoutSubviews (); } Refresh (); @@ -813,22 +1154,74 @@ namespace Terminal.Gui { static void SetToplevelsSize (Rect full) { - foreach (var t in toplevels) { - if (t?.SuperView == null && !t.Modal) { - t.Frame = full; - t.Width = full.Width; - t.Height = full.Height; + if (MdiTop == null) { + foreach (var t in toplevels) { + if (t?.SuperView == null && !t.Modal) { + t.Frame = full; + t.Width = full.Width; + t.Height = full.Height; + } } + } else { + Top.Frame = full; + Top.Width = full.Width; + Top.Height = full.Height; } } static bool SetCurrentAsTop () { - if (Current != Top && Current?.SuperView == null && !Current.Modal) { + if (MdiTop == null && Current != Top && Current?.SuperView == null && Current?.Modal == false) { Top = Current; return true; } return false; } + + /// + /// Move to the next Mdi child from the . + /// + public static void MoveNext () + { + if (MdiTop != null && !Current.Modal) { + lock (toplevels) { + toplevels.MoveNext (); + while (toplevels.Peek () == MdiTop || !toplevels.Peek ().Visible) { + toplevels.MoveNext (); + } + Current = toplevels.Peek (); + } + } + } + + /// + /// Move to the previous Mdi child from the . + /// + public static void MovePrevious () + { + if (MdiTop != null && !Current.Modal) { + lock (toplevels) { + toplevels.MovePrevious (); + while (toplevels.Peek () == MdiTop || !toplevels.Peek ().Visible) { + lock (toplevels) { + toplevels.MovePrevious (); + } + } + Current = toplevels.Peek (); + } + } + } + + internal static bool ShowChild (Toplevel top) + { + if (top.Visible && MdiTop != null && Current?.Modal == false) { + lock (toplevels) { + toplevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); + Current = top; + } + return true; + } + return false; + } } } diff --git a/Terminal.Gui/Core/StackExtensions.cs b/Terminal.Gui/Core/StackExtensions.cs new file mode 100644 index 000000000..1d433e33f --- /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 a6bc51955..fe525db55 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 @@ namespace Terminal.Gui { /// public event Action Unloaded; + /// + /// Invoked once the Toplevel's becomes the . + /// + public event Action Activate; + + /// + /// Invoked once the Toplevel's ceases to be the . + /// + public event Action Deactivate; + + /// + /// Invoked once the child Toplevel's is closed from the + /// + public event Action ChildClosed; + + /// + /// Invoked once the last child Toplevel's is closed from the + /// + public event Action AllChildClosed; + + /// + /// Invoked once the Toplevel's is being closing from the + /// + public event Action Closing; + + /// + /// Invoked once the Toplevel's is closed from the + /// + public event Action Closed; + + /// + /// Invoked once the child Toplevel's has begin loaded. + /// + public event Action ChildLoaded; + + /// + /// Invoked once the child Toplevel's has begin unloaded. + /// + public event Action ChildUnloaded; + + internal virtual void OnChildUnloaded (Toplevel top) + { + ChildUnloaded?.Invoke (top); + } + + internal virtual void OnChildLoaded (Toplevel top) + { + ChildLoaded?.Invoke (top); + } + + internal virtual void OnClosed (Toplevel top) + { + Closed?.Invoke (top); + } + + internal virtual bool OnClosing (ToplevelClosingEventArgs ev) + { + Closing?.Invoke (ev); + return ev.Cancel; + } + + internal virtual void OnAllChildClosed () + { + AllChildClosed?.Invoke (); + } + + internal virtual void OnChildClosed (Toplevel top) + { + if (IsMdiContainer) { + SetChildNeedsDisplay (); + } + ChildClosed?.Invoke (top); + } + + internal virtual void OnDeactivate (Toplevel activated) + { + Deactivate?.Invoke (activated); + } + + internal virtual void OnActivate (Toplevel deactivated) + { + Activate?.Invoke (deactivated); + } + /// /// Called from before the is redraws for the first time. /// @@ -149,6 +233,20 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { switch (ShortcutHelper.GetModifiersKey (keyEvent)) { case Key.Q | Key.CtrlMask: // FIXED: stop current execution of this container - Application.RequestStop (); + if (Application.MdiTop != null) { + Application.MdiTop.RequestStop (); + } else { + Application.RequestStop (); + } break; case Key.Z | Key.CtrlMask: Driver.Suspend (); @@ -239,16 +341,26 @@ namespace Terminal.Gui { return true; case Key.Tab | Key.CtrlMask: case Key key when key == Application.AlternateForwardKey: // Needed on Unix - Application.Top.FocusNext (); - if (Application.Top.Focused == null) { + if (Application.MdiTop == null) { Application.Top.FocusNext (); + if (Application.Top.Focused == null) { + Application.Top.FocusNext (); + } + Application.Top.SetNeedsDisplay (); + } else { + MoveNext (); } return true; case Key.Tab | Key.ShiftMask | Key.CtrlMask: case Key key when key == Application.AlternateBackwardKey: // Needed on Unix - Application.Top.FocusPrev (); - if (Application.Top.Focused == null) { + if (Application.MdiTop == null) { Application.Top.FocusPrev (); + if (Application.Top.Focused == null) { + Application.Top.FocusPrev (); + } + Application.Top.SetNeedsDisplay (); + } else { + MovePrevious (); } return true; case Key.L | Key.CtrlMask: @@ -383,7 +495,7 @@ namespace Terminal.Gui { //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}"); bool m, s; if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) { - m = Application.Top.MenuBar != null; + m = Application.MdiTop?.MenuBar != null || Application.Top.MenuBar != null; } else { m = ((Toplevel)SuperView).MenuBar != null; } @@ -394,7 +506,9 @@ namespace Terminal.Gui { } ny = Math.Max (y, l); if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) { - s = Application.Top.StatusBar != null && Application.Top.StatusBar.Visible; + s = (Application.MdiTop != null && Application.MdiTop.StatusBar != null + && Application.MdiTop.StatusBar.Visible) + || (Application.Top.StatusBar != null && Application.Top.StatusBar.Visible); } else { s = ((Toplevel)SuperView).StatusBar != null && ((Toplevel)SuperView).StatusBar.Visible; } @@ -429,7 +543,8 @@ namespace Terminal.Gui { public virtual void PositionToplevel (Toplevel top) { EnsureVisibleBounds (top, top.Frame.X, top.Frame.Y, out int nx, out int ny); - if (top?.SuperView != null && (nx != top.Frame.X || ny != top.Frame.Y) && top.LayoutStyle == LayoutStyle.Computed) { + if ((top?.SuperView != null || Application.MdiTop != null && top != Application.MdiTop) + && (nx > top.Frame.X || ny > top.Frame.Y) && top.LayoutStyle == LayoutStyle.Computed) { if ((top.X == null || top.X is Pos.PosAbsolute) && top.Bounds.X != nx) { top.X = nx; } @@ -437,16 +552,28 @@ namespace Terminal.Gui { top.Y = ny; } } - var statusBar = top?.SuperView != null && top.SuperView is Toplevel toplevel - ? toplevel.StatusBar : null; + View superView = null; + StatusBar statusBar = null; + + if (top != Application.MdiTop && Application.MdiTop != null && Application.MdiTop.StatusBar != null) { + superView = Application.MdiTop; + statusBar = Application.MdiTop.StatusBar; + } else if (top?.SuperView != null && top.SuperView is Toplevel toplevel) { + superView = top.SuperView; + statusBar = toplevel.StatusBar; + } if (statusBar != null) { - if (ny + top.Frame.Height != top.SuperView.Frame.Height - (statusBar.Visible ? 1 : 0)) { + if (ny + top.Frame.Height >= superView.Frame.Height - (statusBar.Visible ? 1 : 0)) { if (top.Height is Dim.DimFill) { top.Height = Dim.Fill (statusBar.Visible ? 1 : 0); } } - top.SuperView.LayoutSubviews (); + if (superView == Application.MdiTop) { + top.SetRelativeLayout (superView.Frame); + } else { + superView.LayoutSubviews (); + } } if (top.StatusBar != null) { if (top.StatusBar.Frame.Y != top.Frame.Height - (top.StatusBar.Visible ? 1 : 0)) { @@ -460,30 +587,151 @@ namespace Terminal.Gui { /// public override void Redraw (Rect bounds) { - if (IsCurrentTop || this == Application.Top || Application.Current.GetType ().BaseType == typeof (Toplevel)) { - if (!NeedDisplay.IsEmpty || LayoutNeeded) { - Driver.SetAttribute (Colors.TopLevel.Normal); + if (this == Application.MdiTop) { + RedrawMdi (bounds); + } else { + if (!CanBeVisible (this)) { + return; + } + + if (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded) { + Driver.SetAttribute (ColorScheme.Normal); // This is the Application.Top. Clear just the region we're being asked to redraw // (the bounds passed to us). // Must be the screen-relative region to clear, not the bounds. Clear (Frame); Driver.SetAttribute (Colors.Base.Normal); - PositionToplevels (); - - foreach (var view in Subviews) { - if (view.Frame.IntersectsWith (bounds)) { - view.SetNeedsLayout (); - view.SetNeedsDisplay (view.Bounds); - } - } - - ClearLayoutNeeded (); - ClearNeedsDisplay (); } } - base.Redraw (base.Bounds); + if (LayoutStyle == LayoutStyle.Computed) + SetRelativeLayout (Bounds); + PositionToplevels (); + LayoutSubviews (); + + foreach (var view in Subviews) { + if (view.Frame.IntersectsWith (bounds) && !OutsideTopFrame (this)) { + view.SetNeedsLayout (); + view.SetNeedsDisplay (view.Bounds); + view.Redraw (view.Bounds); + } + } + + ClearLayoutNeeded (); + ClearNeedsDisplay (); + } + + void RedrawMdi (Rect bounds) + { + if (!IsMdiContainer) { + return; + } + + if (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded) { + Driver.SetAttribute (ColorScheme.Normal); + + Clear (Frame); + + Driver.SetAttribute (Colors.Base.Normal); + + if (LayoutStyle == LayoutStyle.Computed) + SetRelativeLayout (Bounds); + PositionToplevels (); + LayoutSubviews (); + + foreach (var top in Application.MdiChildes.AsEnumerable ().Reverse ()) { + if (top.Frame.IntersectsWith (bounds)) { + if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible) { + top.SetNeedsLayout (); + top.SetNeedsDisplay (top.Bounds); + top.Redraw (top.Bounds); + } + } + } + + ClearLayoutNeeded (); + ClearNeedsDisplay (); + } + } + + bool OutsideTopFrame (Toplevel top) + { + if (top.Frame.X > Driver.Cols || top.Frame.Y > Driver.Rows) { + return true; + } + return false; + } + + // + // FIXED:It does not look like the event is raised on clicked-drag + // need to figure that out. + // + internal static Point? dragPosition; + Point start; + + /// + public override bool MouseEvent (MouseEvent mouseEvent) + { + // FIXED:The code is currently disabled, because the + // Driver.UncookMouse does not seem to have an effect if there is + // a pending mouse event activated. + + int nx, ny; + if (!dragPosition.HasValue && mouseEvent.Flags == (MouseFlags.Button1Pressed)) { + // Only start grabbing if the user clicks on the title bar. + if (mouseEvent.Y == 0) { + start = new Point (mouseEvent.X, mouseEvent.Y); + dragPosition = new Point (); + nx = mouseEvent.X - mouseEvent.OfX; + ny = mouseEvent.Y - mouseEvent.OfY; + dragPosition = new Point (nx, ny); + Application.GrabMouse (this); + } + + //System.Diagnostics.Debug.WriteLine ($"Starting at {dragPosition}"); + return true; + } else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) || + mouseEvent.Flags == MouseFlags.Button3Pressed) { + if (dragPosition.HasValue) { + if (SuperView == null) { + // Redraw the entire app window using just our Frame. Since we are + // Application.Top, and our Frame always == our Bounds (Location is always (0,0)) + // our Frame is actually view-relative (which is what Redraw takes). + // We need to pass all the view bounds because since the windows was + // moved around, we don't know exactly what was the affected region. + Application.Top.SetNeedsDisplay (); + } else { + SuperView.SetNeedsDisplay (); + } + EnsureVisibleBounds (this, mouseEvent.X + (SuperView == null ? mouseEvent.OfX - start.X : Frame.X - start.X), + mouseEvent.Y + (SuperView == null ? mouseEvent.OfY : Frame.Y), out nx, out ny); + + dragPosition = new Point (nx, ny); + LayoutSubviews (); + Frame = new Rect (nx, ny, Frame.Width, Frame.Height); + if (X == null || X is Pos.PosAbsolute) { + X = nx; + } + if (Y == null || Y is Pos.PosAbsolute) { + Y = ny; + } + //System.Diagnostics.Debug.WriteLine ($"nx:{nx},ny:{ny}"); + + // 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; } /// @@ -494,5 +742,209 @@ namespace Terminal.Gui { { 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 f721feedf..4d6db10ec 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -268,7 +268,7 @@ namespace Terminal.Gui { } } - private int GetTabIndex (int idx) + int GetTabIndex (int idx) { int i = 0; foreach (var v in SuperView.tabIndexes) { @@ -280,7 +280,7 @@ namespace Terminal.Gui { return Math.Min (i, idx); } - private void SetTabIndex () + void SetTabIndex () { int i = 0; foreach (var v in SuperView.tabIndexes) { @@ -989,8 +989,8 @@ namespace Terminal.Gui { internal void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true) { // Computes the real row, col relative to the screen. - rrow = row + frame.Y; - rcol = col + frame.X; + rrow = Math.Max (row + frame.Y, 0); + rcol = Math.Max (col + frame.X, 0); var ccontainer = container; while (ccontainer != null) { rrow += ccontainer.frame.Y; @@ -1338,18 +1338,7 @@ namespace Terminal.Gui { var clipRect = new Rect (Point.Empty, frame.Size); - if (ColorScheme != null) { - Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); - } - - if (!ustring.IsNullOrEmpty (Text)) { - Clear (); - // Draw any Text - if (textFormatter != null) { - textFormatter.NeedsFormat = true; - } - textFormatter?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); - } + DrawText (); // Invoke DrawContentEvent OnDrawContent (bounds); @@ -1377,6 +1366,33 @@ namespace Terminal.Gui { ClearNeedsDisplay (); } + void DrawText () + { + if (ColorScheme != null) { + Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); + } + + if (!ustring.IsNullOrEmpty (Text)) { + var savedClip = ClipToBounds (); + Rect viewBounds = Bounds; + if (SuperView != null && viewBounds.Width > SuperView.Bounds.Width) { + viewBounds.Width = SuperView.Bounds.Width; + } + if (SuperView != null && viewBounds.Height > SuperView.Bounds.Height) { + viewBounds.Height = SuperView.Bounds.Height; + } + var viewFrame = ViewToScreen (viewBounds); + Clear (viewFrame); + // Draw any Text + if (textFormatter != null) { + textFormatter.NeedsFormat = true; + } + textFormatter?.Draw (viewFrame, HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); + + Driver.Clip = savedClip; + } + } + /// /// Event invoked when the content area of the View is to be drawn. /// @@ -1957,8 +1973,9 @@ namespace Terminal.Gui { v.LayoutNeeded = false; } - if (SuperView == Application.Top && LayoutNeeded && ordered.Count == 0 && LayoutStyle == LayoutStyle.Computed) { - SetRelativeLayout (Frame); + if (SuperView != null && SuperView == Application.Top && LayoutNeeded + && ordered.Count == 0 && LayoutStyle == LayoutStyle.Computed) { + SetRelativeLayout (SuperView.Frame); } LayoutNeeded = false; @@ -2259,7 +2276,7 @@ namespace Terminal.Gui { /// public bool Visible { get; set; } = true; - bool CanBeVisible (View view) + internal bool CanBeVisible (View view) { if (!view.Visible) { return false; diff --git a/Terminal.Gui/Core/Window.cs b/Terminal.Gui/Core/Window.cs index 3d91da879..99734a657 100644 --- a/Terminal.Gui/Core/Window.cs +++ b/Terminal.Gui/Core/Window.cs @@ -209,77 +209,6 @@ namespace Terminal.Gui { } } - // - // 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 . ///