diff --git a/.editorconfig b/.editorconfig
index 040e7abd97..f2899c92af 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -20,4 +20,4 @@ csharp_style_var_when_type_is_apparent = true
csharp_prefer_braces = false
csharp_space_before_open_square_brackets = true
csharp_space_between_method_call_name_and_opening_parenthesis = true
-csharp_space_between_method_declaration_name_and_open_parenthesis = true
\ No newline at end of file
+csharp_space_between_method_declaration_name_and_open_parenthesis = true
diff --git a/Terminal.Gui/Application.cs b/Terminal.Gui/Application.cs
index 101b0587de..3f0baed243 100644
--- a/Terminal.Gui/Application.cs
+++ b/Terminal.Gui/Application.cs
@@ -18,10 +18,10 @@ namespace Terminal.Gui {
/// // 5 rows/columns of padding.
/// Application.Init();
/// var win = new Window ($"Example App ({Application.QuitKey} to quit)") {
- /// X = 5,
- /// Y = 5,
- /// Width = Dim.Fill (5),
- /// Height = Dim.Fill (5)
+ /// X = 5,
+ /// Y = 5,
+ /// Width = Dim.Fill (5),
+ /// Height = Dim.Fill (5)
/// };
/// Application.Top.Add(win);
/// Application.Run();
@@ -29,20 +29,19 @@ namespace Terminal.Gui {
///
///
///
- ///
- /// Creates a instance of to process input events, handle timers and
- /// other sources of data. It is accessible via the property.
- ///
- ///
- /// The event is invoked on each iteration of the .
- ///
- ///
- /// When invoked it sets the to one that is tied
- /// to the , allowing user code to use async/await.
- ///
+ ///
+ /// Creates a instance of to process input events, handle timers and
+ /// other sources of data. It is accessible via the property.
+ ///
+ ///
+ /// The event is invoked on each iteration of the .
+ ///
+ ///
+ /// When invoked it sets the to one that is tied
+ /// to the , allowing user code to use async/await.
+ ///
///
public static partial class Application {
- static readonly Stack _toplevels = new Stack ();
///
/// The current in use.
@@ -50,54 +49,15 @@ public static partial class Application {
public static ConsoleDriver Driver;
///
- /// Gets all the Mdi childes which represent all the not modal from the .
- ///
- public static List MdiChildren {
- get {
- if (MdiTop != null) {
- List _mdiChildren = new List ();
- foreach (var top in _toplevels) {
- if (top != MdiTop && !top.Modal) {
- _mdiChildren.Add (top);
- }
- }
- return _mdiChildren;
- }
- 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 ()
- ///
- /// The top.
- public static Toplevel Top { get; private set; }
-
- ///
- /// The current object. This is updated when enters and leaves to point to the current .
- ///
- /// The current.
- public static Toplevel Current { get; private set; }
-
- ///
- /// The current object that wants continuous mouse button pressed events.
+ /// If , forces the use of the System.Console-based (see ) driver. The default is .
///
- public static View WantContinuousButtonPressedView { get; private set; }
+ [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+ public static bool UseSystemConsole { get; set; } = false;
+ // For Unit testing - ignores UseSystemConsole
+ internal static bool _forceFakeConsole;
+
private static bool? _enableConsoleScrolling;
-
///
/// The current used in the terminal.
///
@@ -132,194 +92,35 @@ public static bool EnableConsoleScrolling {
}
}
- static Key _alternateForwardKey = Key.PageDown | Key.CtrlMask;
-
- ///
- /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.
- ///
- [SerializableConfigurationProperty (Scope = typeof (SettingsScope)), JsonConverter (typeof (KeyJsonConverter))]
- public static Key AlternateForwardKey {
- get => _alternateForwardKey;
- set {
- if (_alternateForwardKey != value) {
- var oldKey = _alternateForwardKey;
- _alternateForwardKey = value;
- OnAlternateForwardKeyChanged (new KeyChangedEventArgs (oldKey, value));
- }
- }
- }
-
- static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e)
- {
- foreach (var top in _toplevels.ToArray ()) {
- top.OnAlternateForwardKeyChanged (e);
- }
- }
-
- static Key _alternateBackwardKey = Key.PageUp | Key.CtrlMask;
-
- ///
- /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.
- ///
- [SerializableConfigurationProperty (Scope = typeof (SettingsScope)), JsonConverter (typeof (KeyJsonConverter))]
- public static Key AlternateBackwardKey {
- get => _alternateBackwardKey;
- set {
- if (_alternateBackwardKey != value) {
- var oldKey = _alternateBackwardKey;
- _alternateBackwardKey = value;
- OnAlternateBackwardKeyChanged (new KeyChangedEventArgs (oldKey, value));
- }
- }
- }
-
- static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey)
- {
- foreach (var top in _toplevels.ToArray ()) {
- top.OnAlternateBackwardKeyChanged (oldKey);
- }
- }
-
- static Key _quitKey = Key.Q | Key.CtrlMask;
-
- ///
- /// Gets or sets the key to quit the application.
- ///
- [SerializableConfigurationProperty (Scope = typeof (SettingsScope)), JsonConverter (typeof (KeyJsonConverter))]
- public static Key QuitKey {
- get => _quitKey;
- set {
- if (_quitKey != value) {
- var oldKey = _quitKey;
- _quitKey = value;
- OnQuitKeyChanged (new KeyChangedEventArgs (oldKey, value));
- }
- }
- }
-
- private static List _supportedCultures;
-
- ///
- /// Gets all supported cultures by the application without the invariant language.
- ///
- public static List SupportedCultures => _supportedCultures;
-
- static void OnQuitKeyChanged (KeyChangedEventArgs e)
- {
- // Duplicate the list so if it changes during enumeration we're safe
- foreach (var top in _toplevels.ToArray ()) {
- top.OnQuitKeyChanged (e);
- }
- }
-
- ///
- /// The driver for the application
- ///
- /// The main loop.
- public static MainLoop MainLoop { get; private set; }
-
- ///
- /// Disable or enable the mouse. The mouse is enabled by default.
- ///
- [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
- public static bool IsMouseDisabled { get; set; }
-
- ///
- /// Set to true to cause the RunLoop method to exit after the first iterations.
- /// Set to false (the default) to cause the RunLoop to continue running until Application.RequestStop() is called.
- ///
- public static bool ExitRunLoopAfterFirstIteration { get; set; } = false;
-
- ///
- /// Notify that a new was created ( was called). The token is created in
- /// and this event will be fired before that function exits.
- ///
- ///
- /// If is callers to
- /// must also subscribe to
- /// and manually dispose of the token when the application is done.
- ///
- public static event EventHandler NotifyNewRunState;
-
- ///
- /// Notify that a existent is stopping ( was called).
- ///
- ///
- /// If is callers to
- /// must also subscribe to
- /// and manually dispose of the token when the application is done.
- ///
- public static event EventHandler NotifyStopRunState;
+ private static List _cachedSupportedCultures;
///
- /// This event is raised on each iteration of the .
+ /// Gets all cultures supported by the application without the invariant language.
///
- ///
- /// See also
- ///
- public static Action Iteration;
+ public static List SupportedCultures => _cachedSupportedCultures;
- ///
- /// Returns a rectangle that is centered in the screen for the provided size.
- ///
- /// The centered rect.
- /// Size for the rectangle.
- public static Rect MakeCenteredRect (Size size)
+ private static List GetSupportedCultures ()
{
- return new Rect (new Point ((Driver.Cols - size.Width) / 2, (Driver.Rows - size.Height) / 2), size);
- }
-
- //
- // provides the sync context set while executing code in Terminal.Gui, to let
- // users use async/await on their code
- //
- class MainLoopSyncContext : SynchronizationContext {
- readonly MainLoop mainLoop;
+ CultureInfo [] culture = CultureInfo.GetCultures (CultureTypes.AllCultures);
- public MainLoopSyncContext (MainLoop mainLoop)
- {
- this.mainLoop = mainLoop;
- }
+ // Get the assembly
+ Assembly assembly = Assembly.GetExecutingAssembly ();
- public override SynchronizationContext CreateCopy ()
- {
- return new MainLoopSyncContext (MainLoop);
- }
+ //Find the location of the assembly
+ string assemblyLocation = AppDomain.CurrentDomain.BaseDirectory;
- public override void Post (SendOrPostCallback d, object state)
- {
- mainLoop.AddIdle (() => {
- d (state);
- return false;
- });
- //mainLoop.Driver.Wakeup ();
- }
+ // Find the resource file name of the assembly
+ string resourceFilename = $"{Path.GetFileNameWithoutExtension (assembly.Location)}.resources.dll";
- public override void Send (SendOrPostCallback d, object state)
- {
- if (Thread.CurrentThread.ManagedThreadId == _mainThreadId) {
- d (state);
- } else {
- var wasExecuted = false;
- mainLoop.Invoke (() => {
- d (state);
- wasExecuted = true;
- });
- while (!wasExecuted) {
- Thread.Sleep (15);
- }
- }
- }
+ // Return all culture for which satellite folder found with culture code.
+ return culture.Where (cultureInfo =>
+ assemblyLocation != null &&
+ Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name)) &&
+ File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename))
+ ).ToList ();
}
-
- ///
- /// If , forces the use of the System.Console-based (see ) driver. The default is .
- ///
- [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
- public static bool UseSystemConsole { get; set; } = false;
-
- // For Unit testing - ignores UseSystemConsole
- internal static bool _forceFakeConsole;
+
+ #region Initialization (Init/Shutdown)
///
/// Initializes a new instance of Application.
@@ -370,15 +171,6 @@ internal static void InternalInit (Func topLevelFactory, ConsoleDriver
throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown.");
}
- // Note in this case, we don't verify the type of the Toplevel created by new T().
- // Used only for start debugging on Unix.
- //#if DEBUG
- // while (!System.Diagnostics.Debugger.IsAttached) {
- // System.Threading.Thread.Sleep (100);
- // }
- // System.Diagnostics.Debugger.Break ();
- //#endif
-
if (!calledViaRunT) {
// Reset all class variables (Application is a singleton).
ResetState ();
@@ -386,12 +178,6 @@ internal static void InternalInit (Func topLevelFactory, ConsoleDriver
// For UnitTests
if (driver != null) {
- //if (mainLoopDriver == null) {
- // throw new ArgumentNullException ("InternalInit mainLoopDriver cannot be null if driver is provided.");
- //}
- //if (!(driver is FakeDriver)) {
- // throw new InvalidOperationException ("InternalInit can only be called with FakeDriver.");
- //}
Driver = driver;
}
@@ -440,7 +226,7 @@ internal static void InternalInit (Func topLevelFactory, ConsoleDriver
try {
Driver.EnableConsoleScrolling = EnableConsoleScrolling;
- Driver.Init (TerminalResized);
+ Driver.Init (OnTerminalResized);
} catch (InvalidOperationException ex) {
// This is a case where the driver is unable to initialize the console.
// This can happen if the console is already in use by another process or
@@ -453,47 +239,131 @@ internal static void InternalInit (Func topLevelFactory, ConsoleDriver
Top = topLevelFactory ();
Current = Top;
- _supportedCultures = GetSupportedCultures ();
+ _cachedSupportedCultures = GetSupportedCultures ();
_mainThreadId = Thread.CurrentThread.ManagedThreadId;
_initialized = true;
}
+
///
- /// Captures the execution state for the provided view.
+ /// Shutdown an application initialized with .
///
- public class RunState : IDisposable {
- ///
- /// Initializes a new class.
- ///
- ///
- public RunState (Toplevel view)
- {
- Toplevel = view;
- }
- ///
- /// The belong to this .
- ///
- public Toplevel Toplevel { get; internal set; }
+ ///
+ /// Shutdown must be called for every call to or
+ /// to ensure all resources are cleaned up (Disposed) and terminal settings are restored.
+ ///
+ public static void Shutdown ()
+ {
+ ResetState ();
+ ConfigurationManager.PrintJsonErrors ();
+ }
-#if DEBUG_IDISPOSABLE
- ///
- /// For debug purposes to verify objects are being disposed properly
- ///
- public bool WasDisposed = false;
- ///
- /// For debug purposes to verify objects are being disposed properly
- ///
- public int DisposedCount = 0;
- ///
- /// For debug purposes
- ///
- public static List Instances = new List ();
- ///
- /// For debug purposes
- ///
- public RunState ()
- {
- Instances.Add (this);
+ // Encapsulate all setting of initial state for Application; Having
+ // this in a function like this ensures we don't make mistakes in
+ // guaranteeing that the state of this singleton is deterministic when Init
+ // starts running and after Shutdown returns.
+ 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
+ foreach (var t in _toplevels) {
+ t.Running = false;
+ t.Dispose ();
+ }
+ _toplevels.Clear ();
+ Current = null;
+ Top?.Dispose ();
+ Top = null;
+
+ // BUGBUG: OverlappedTop is not cleared here, but it should be?
+
+ MainLoop = null;
+ Driver?.End ();
+ Driver = null;
+ Iteration = null;
+ RootMouseEvent = null;
+ RootKeyEvent = null;
+ TerminalResized = null;
+ _mainThreadId = -1;
+ NotifyNewRunState = null;
+ NotifyStopRunState = null;
+ _initialized = false;
+ _mouseGrabView = null;
+ _enableConsoleScrolling = false;
+ _lastMouseOwnerView = null;
+
+ // Reset synchronization context to allow the user to run async/await,
+ // as the main loop has been ended, the synchronization context from
+ // gui.cs does no longer process any callbacks. See #1084 for more details:
+ // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
+ SynchronizationContext.SetSynchronizationContext (syncContext: null);
+ }
+
+ #endregion Initialization (Init/Shutdown)
+
+ #region Run (Begin, Run, End)
+
+ ///
+ /// Notify that a new was created ( was called). The token is created in
+ /// and this event will be fired before that function exits.
+ ///
+ ///
+ /// If is callers to
+ /// must also subscribe to
+ /// and manually dispose of the token when the application is done.
+ ///
+ public static event EventHandler NotifyNewRunState;
+
+ ///
+ /// Notify that a existent is stopping ( was called).
+ ///
+ ///
+ /// If is callers to
+ /// must also subscribe to
+ /// and manually dispose of the token when the application is done.
+ ///
+ public static event EventHandler NotifyStopRunState;
+
+ ///
+ /// The execution state for a view.
+ ///
+ public class RunState : IDisposable {
+ ///
+ /// Initializes a new class.
+ ///
+ ///
+ public RunState (Toplevel view)
+ {
+ Toplevel = view;
+ }
+ ///
+ /// The belonging to this .
+ ///
+ public Toplevel Toplevel { get; internal set; }
+
+#if DEBUG_IDISPOSABLE
+ ///
+ /// For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly
+ ///
+ public bool WasDisposed = false;
+
+ ///
+ /// For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly
+ ///
+ public int DisposedCount = 0;
+
+ ///
+ /// For debug (see DEBUG_IDISPOSABLE define) purposes; the runstate instances that have been created
+ ///
+ public static List Instances = new List ();
+
+ ///
+ /// Creates a new RunState object.
+ ///
+ public RunState ()
+ {
+ Instances.Add (this);
}
#endif
@@ -530,485 +400,499 @@ protected virtual void Dispose (bool disposing)
}
}
- static void ProcessKeyEvent (KeyEvent ke)
- {
- if (RootKeyEvent?.Invoke (ke) ?? false) {
- return;
- }
-
- var chain = _toplevels.ToList ();
- foreach (var topLevel in chain) {
- if (topLevel.ProcessHotKey (ke))
- return;
- if (topLevel.Modal)
- break;
- }
-
- foreach (var topLevel in chain) {
- if (topLevel.ProcessKey (ke))
- return;
- if (topLevel.Modal)
- break;
- }
-
- foreach (var topLevel in chain) {
- // Process the key normally
- if (topLevel.ProcessColdKey (ke))
- return;
- if (topLevel.Modal)
- break;
- }
- }
-
- static void ProcessKeyDownEvent (KeyEvent ke)
+ ///
+ /// Building block API: Prepares the provided for execution.
+ ///
+ /// The handle that needs to be passed to the method upon completion.
+ /// The to prepare execution for.
+ ///
+ /// This method prepares the provided for running with the focus,
+ /// it adds this to the list of s, sets up the to process the
+ /// event, lays out the Subviews, focuses the first element, and draws the
+ /// in the screen. This is usually followed by executing
+ /// the method, and then the method upon termination which will
+ /// undo these changes.
+ ///
+ public static RunState Begin (Toplevel Toplevel)
{
- var chain = _toplevels.ToList ();
- foreach (var topLevel in chain) {
- if (topLevel.OnKeyDown (ke))
- return;
- if (topLevel.Modal)
- break;
+ if (Toplevel == null) {
+ throw new ArgumentNullException (nameof (Toplevel));
+ } else if (Toplevel.IsOverlappedContainer && OverlappedTop != Toplevel && OverlappedTop != null) {
+ throw new InvalidOperationException ("Only one Overlapped Container is allowed.");
}
- }
+ var rs = new RunState (Toplevel);
- static void ProcessKeyUpEvent (KeyEvent ke)
- {
- var chain = _toplevels.ToList ();
- foreach (var topLevel in chain) {
- if (topLevel.OnKeyUp (ke))
- return;
- if (topLevel.Modal)
- break;
+ // View implements ISupportInitializeNotification which is derived from ISupportInitialize
+ if (!Toplevel.IsInitialized) {
+ Toplevel.BeginInit ();
+ Toplevel.EndInit ();
}
- }
-
- 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;
- }
+ lock (_toplevels) {
+ // If Top was already initialized with Init, and Begin has never been called
+ // Top was not added to the Toplevels Stack. It will thus never get disposed.
+ // Clean it up here:
+ if (Top != null && Toplevel != Top && !_toplevels.Contains (Top)) {
+ Top.Dispose ();
+ Top = null;
+ } else if (Top != null && Toplevel != Top && _toplevels.Contains (Top)) {
+ Top.OnLeave (Toplevel);
+ }
+ 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 ();
- 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;
- }
- }
+ _toplevels.Push (Toplevel);
+ } else {
+ var dup = _toplevels.FirstOrDefault (x => x.Id.ToString () == Toplevel.Id);
+ if (dup == null) {
+ _toplevels.Push (Toplevel);
}
}
- }
- 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;
+ if (_toplevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0) {
+ throw new ArgumentException ("There are duplicates Toplevels Id's");
+ }
}
-
- var startFrame = start.Frame;
-
- if (!startFrame.Contains (x, y)) {
- resx = 0;
- resy = 0;
- return null;
+ if (Top == null || Toplevel.IsOverlappedContainer) {
+ Top = Toplevel;
}
- 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 = View.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;
- }
+ var refreshDriver = true;
+ if (OverlappedTop == null || Toplevel.IsOverlappedContainer || (Current?.Modal == false && Toplevel.Modal)
+ || (Current?.Modal == false && !Toplevel.Modal) || (Current?.Modal == true && Toplevel.Modal)) {
+
+ if (Toplevel.Visible) {
+ Current = Toplevel;
+ SetCurrentOverlappedAsTop ();
+ } else {
+ refreshDriver = false;
}
+ } else if ((OverlappedTop != null && Toplevel != OverlappedTop && Current?.Modal == true && !_toplevels.Peek ().Modal)
+ || (OverlappedTop != null && Toplevel != OverlappedTop && Current?.Running == false)) {
+ refreshDriver = false;
+ MoveCurrent (Toplevel);
+ } else {
+ refreshDriver = false;
+ MoveCurrent (Current);
}
- resx = x - startFrame.X;
- resy = y - startFrame.Y;
- return start;
- }
-
- static View FindTopFromView (View view)
- {
- View top = view?.SuperView != null && view?.SuperView != Top
- ? view.SuperView : view;
- while (top?.SuperView != null && top?.SuperView != Top) {
- top = top.SuperView;
+ Driver.PrepareToRun (MainLoop, ProcessKeyEvent, ProcessKeyDownEvent, ProcessKeyUpEvent, ProcessMouseEvent);
+ if (Toplevel.LayoutStyle == LayoutStyle.Computed) {
+ Toplevel.SetRelativeLayout (new Rect (0, 0, Driver.Cols, Driver.Rows));
+ }
+ Toplevel.LayoutSubviews ();
+ Toplevel.PositionToplevels ();
+ Toplevel.FocusFirst ();
+ if (refreshDriver) {
+ OverlappedTop?.OnChildLoaded (Toplevel);
+ Toplevel.OnLoaded ();
+ Toplevel.SetNeedsDisplay ();
+ Toplevel.Redraw (Toplevel.Bounds);
+ Toplevel.PositionCursor ();
+ Driver.Refresh ();
}
- return top;
- }
-
- static View _mouseGrabView;
-
- ///
- /// The view that grabbed the mouse, to where will be routed all the mouse events.
- ///
- public static View MouseGrabView => _mouseGrabView;
- ///
- /// Event to be invoked when a view want grab the mouse which can be canceled.
- ///
- public static event EventHandler GrabbingMouse;
+ NotifyNewRunState?.Invoke (Toplevel, new RunStateEventArgs (rs));
+ return rs;
+ }
///
- /// Event to be invoked when a view want ungrab the mouse which can be canceled.
+ /// Runs the application by calling with the value of .
///
- public static event EventHandler UnGrabbingMouse;
+ ///
+ /// See for more details.
+ ///
+ public static void Run (Func errorHandler = null)
+ {
+ Run (Top, errorHandler);
+ }
///
- /// Event to be invoked when a view grab the mouse.
+ /// Runs the application by calling
+ /// with a new instance of the specified -derived class.
+ ///
+ /// Calling first is not needed as this function will initialize the application.
+ ///
+ ///
+ /// must be called when the application is closing (typically after Run> has
+ /// returned) to ensure resources are cleaned up and terminal settings restored.
+ ///
///
- public static event EventHandler GrabbedMouse;
-
- ///
- /// Event to be invoked when a view ungrab the mouse.
- ///
- public static event EventHandler UnGrabbedMouse;
-
- ///
- /// Grabs the mouse, forcing all mouse events to be routed to the specified view until UngrabMouse is called.
- ///
- /// The grab.
- /// View that will receive all mouse events until UngrabMouse is invoked.
- public static void GrabMouse (View view)
+ ///
+ /// See for more details.
+ ///
+ ///
+ /// The to use. If not specified the default driver for the
+ /// platform will be used (, , or ).
+ /// Must be if has already been called.
+ ///
+ /// Specifies the to use.
+ public static void Run (Func errorHandler = null, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) where T : Toplevel, new()
{
- if (view == null)
- return;
- if (!OnGrabbingMouse (view)) {
- OnGrabbedMouse (view);
- _mouseGrabView = view;
- Driver.UncookMouse ();
+ if (_initialized) {
+ if (Driver != null) {
+ // Init() has been called and we have a driver, so just run the app.
+ var top = new T ();
+ var type = top.GetType ().BaseType;
+ while (type != typeof (Toplevel) && type != typeof (object)) {
+ type = type.BaseType;
+ }
+ if (type != typeof (Toplevel)) {
+ throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel");
+ }
+ Run (top, errorHandler);
+ } else {
+ // This codepath should be impossible because Init(null, null) will select the platform default driver
+ throw new InvalidOperationException ("Init() completed without a driver being set (this should be impossible); Run() cannot be called.");
+ }
+ } else {
+ // Init() has NOT been called.
+ InternalInit (() => new T (), driver, mainLoopDriver, calledViaRunT: true);
+ Run (Top, errorHandler);
}
}
///
- /// Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.
+ /// Runs the main loop on the given container.
///
- public static void UngrabMouse ()
+ ///
+ ///
+ /// This method is used to start processing events
+ /// for the main application, but it is also used to
+ /// run other modal s such as boxes.
+ ///
+ ///
+ /// To make a stop execution, call .
+ ///
+ ///
+ /// Calling is equivalent to calling , followed by ,
+ /// and then calling .
+ ///
+ ///
+ /// Alternatively, to have a program control the main loop and
+ /// process events manually, call to set things up manually and then
+ /// repeatedly call with the wait parameter set to false. By doing this
+ /// the method will only process any pending events, timers, idle handlers and
+ /// then return control immediately.
+ ///
+ ///
+ /// RELEASE builds only: When is any exeptions will be rethrown.
+ /// Otherwise, if will be called. If
+ /// returns the will resume; otherwise
+ /// this method will exit.
+ ///
+ ///
+ /// The to run modally.
+ /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, rethrows when null).
+ public static void Run (Toplevel view, Func errorHandler = null)
{
- if (_mouseGrabView == null)
- return;
- if (!OnUnGrabbingMouse (_mouseGrabView)) {
- OnUnGrabbedMouse (_mouseGrabView);
- _mouseGrabView = null;
- Driver.CookMouse ();
+ var resume = true;
+ while (resume) {
+#if !DEBUG
+ try {
+#endif
+ resume = false;
+ var runToken = Begin (view);
+ // If ExitRunLoopAfterFirstIteration is true then the user must dispose of the runToken
+ // by using NotifyStopRunState event.
+ RunLoop (runToken);
+ if (!ExitRunLoopAfterFirstIteration) {
+ End (runToken);
+ }
+#if !DEBUG
+ }
+ catch (Exception error)
+ {
+ if (errorHandler == null)
+ {
+ throw;
+ }
+ resume = errorHandler(error);
+ }
+#endif
}
- }
-
- static bool OnGrabbingMouse (View view)
- {
- if (view == null)
- return false;
- var evArgs = new GrabMouseEventArgs (view);
- GrabbingMouse?.Invoke (view, evArgs);
- return evArgs.Cancel;
- }
+ }
- static bool OnUnGrabbingMouse (View view)
- {
- if (view == null)
- return false;
- var evArgs = new GrabMouseEventArgs (view);
- UnGrabbingMouse?.Invoke (view, evArgs);
- return evArgs.Cancel;
- }
-
- static void OnGrabbedMouse (View view)
+ ///
+ /// Triggers a refresh of the entire display.
+ ///
+ public static void Refresh ()
{
- if (view == null)
- return;
- GrabbedMouse?.Invoke (view, new ViewEventArgs (view));
+ Driver.UpdateOffScreen ();
+ View last = null;
+ foreach (var v in _toplevels.Reverse ()) {
+ if (v.Visible) {
+ v.SetNeedsDisplay ();
+ v.Redraw (v.Bounds);
+ }
+ last = v;
+ }
+ last?.PositionCursor ();
+ Driver.Refresh ();
}
- static void OnUnGrabbedMouse (View view)
- {
- if (view == null)
- return;
- UnGrabbedMouse?.Invoke (view, new ViewEventArgs (view));
- }
+ ///
+ /// This event is raised on each iteration of the .
+ ///
+ ///
+ /// See also
+ ///
+ public static Action Iteration;
///
- /// Merely a debugging aid to see the raw mouse events
+ /// The driver for the application
///
- public static Action RootMouseEvent;
+ /// The main loop.
+ public static MainLoop MainLoop { get; private set; }
///
- ///
- /// Called for new KeyPress events before any processing is performed or
- /// views evaluate. Use for global key handling and/or debugging.
- ///
- /// Return true to suppress the KeyPress event
+ /// Set to true to cause the RunLoop method to exit after the first iterations.
+ /// Set to false (the default) to cause the RunLoop to continue running until Application.RequestStop() is called.
///
- public static Func RootKeyEvent;
+ public static bool ExitRunLoopAfterFirstIteration { get; set; } = false;
- static View lastMouseOwnerView;
+ //
+ // provides the sync context set while executing code in Terminal.Gui, to let
+ // users use async/await on their code
+ //
+ class MainLoopSyncContext : SynchronizationContext {
+ readonly MainLoop mainLoop;
- static void ProcessMouseEvent (MouseEvent me)
- {
- if (IsMouseDisabled) {
- return;
+ public MainLoopSyncContext (MainLoop mainLoop)
+ {
+ this.mainLoop = mainLoop;
}
- var view = View.FindDeepestView (Current, me.X, me.Y, out int rx, out int ry);
-
- if (view != null && view.WantContinuousButtonPressed) {
- WantContinuousButtonPressedView = view;
- } else {
- WantContinuousButtonPressedView = null;
- }
- if (view != null) {
- me.View = view;
+ public override SynchronizationContext CreateCopy ()
+ {
+ return new MainLoopSyncContext (MainLoop);
}
- RootMouseEvent?.Invoke (me);
- if (me.Handled) {
- return;
+ public override void Post (SendOrPostCallback d, object state)
+ {
+ mainLoop.AddIdle (() => {
+ d (state);
+ return false;
+ });
+ //mainLoop.Driver.Wakeup ();
}
- if (_mouseGrabView != null) {
- var newxy = _mouseGrabView.ScreenToView (me.X, me.Y);
- var nme = new MouseEvent () {
- X = newxy.X,
- Y = newxy.Y,
- Flags = me.Flags,
- OfX = me.X - newxy.X,
- OfY = me.Y - newxy.Y,
- View = view
- };
- if (OutsideFrame (new Point (nme.X, nme.Y), _mouseGrabView.Frame)) {
- lastMouseOwnerView?.OnMouseLeave (me);
- }
- //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
- if (_mouseGrabView?.OnMouseEvent (nme) == true) {
- return;
+ public override void Send (SendOrPostCallback d, object state)
+ {
+ if (Thread.CurrentThread.ManagedThreadId == _mainThreadId) {
+ d (state);
+ } else {
+ var wasExecuted = false;
+ mainLoop.Invoke (() => {
+ d (state);
+ wasExecuted = true;
+ });
+ while (!wasExecuted) {
+ Thread.Sleep (15);
+ }
}
}
+ }
- 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 = View.FindDeepestView (top, me.X, me.Y, out rx, out ry);
+ ///
+ /// Building block API: Runs the for the created .
+ ///
+ ///
+ /// Use the parameter to control whether this is a blocking or non-blocking call.
+ ///
+ /// The state returned by the method.
+ /// By default this is which will execute the loop waiting for events,
+ /// if set to , a single iteration will execute.
+ public static void RunLoop (RunState state, bool wait = true)
+ {
+ if (state == null)
+ throw new ArgumentNullException (nameof (state));
+ if (state.Toplevel == null)
+ throw new ObjectDisposedException ("state");
- if (view != null && view != MdiTop && top != Current) {
- MoveCurrent ((Toplevel)top);
+ bool firstIteration = true;
+ for (state.Toplevel.Running = true; state.Toplevel.Running;) {
+ if (ExitRunLoopAfterFirstIteration && !firstIteration) {
+ return;
}
+ RunMainLoopIteration (ref state, wait, ref firstIteration);
}
+ }
- if (view != null) {
- var nme = new MouseEvent () {
- X = rx,
- Y = ry,
- Flags = me.Flags,
- OfX = 0,
- OfY = 0,
- View = view
- };
-
- if (lastMouseOwnerView == null) {
- lastMouseOwnerView = view;
- view.OnMouseEnter (nme);
- } else if (lastMouseOwnerView != view) {
- lastMouseOwnerView.OnMouseLeave (nme);
- view.OnMouseEnter (nme);
- lastMouseOwnerView = view;
+ ///
+ /// Run one iteration of the .
+ ///
+ /// The state returned by .
+ /// If will execute the waiting for events. If
+ /// will return after a single iteration.
+ /// Set to if this is the first run loop iteration. Upon return,
+ /// it will be set to if at least one iteration happened.
+ public static void RunMainLoopIteration (ref RunState state, bool wait, ref bool firstIteration)
+ {
+ if (MainLoop.EventsPending (wait)) {
+ // Notify Toplevel it's ready
+ if (firstIteration) {
+ state.Toplevel.OnReady ();
}
- if (!view.WantMousePositionReports && me.Flags == MouseFlags.ReportMousePosition)
- return;
-
- if (view.WantContinuousButtonPressed)
- WantContinuousButtonPressedView = view;
- else
- WantContinuousButtonPressedView = null;
-
- // Should we bubbled up the event, if it is not handled?
- view.OnMouseEvent (nme);
-
- EnsuresTopOnFront ();
- }
- }
+ MainLoop.RunIteration ();
+ Iteration?.Invoke ();
- // 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 ());
+ EnsureModalOrVisibleAlwaysOnTop (state.Toplevel);
+ if ((state.Toplevel != Current && Current?.Modal == true)
+ || (state.Toplevel != Current && Current?.Modal == false)) {
+ OverlappedTop?.OnDeactivate (state.Toplevel);
+ state.Toplevel = Current;
+ OverlappedTop?.OnActivate (state.Toplevel);
+ Top.SetSubViewNeedsDisplay ();
+ Refresh ();
}
- 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++;
+ if (Driver.EnsureCursorVisibility ()) {
+ state.Toplevel.SetNeedsDisplay ();
}
- return false;
+ } else if (!wait) {
+ return;
}
- // 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 ());
- }
+ firstIteration = false;
+
+ if (state.Toplevel != Top
+ && (!Top._needsDisplay.IsEmpty || Top._childNeedsDisplay || Top.LayoutNeeded)) {
+ state.Toplevel.SetNeedsDisplay (state.Toplevel.Bounds);
+ Top.Redraw (Top.Bounds);
+ foreach (var top in _toplevels.Reverse ()) {
+ if (top != Top && top != state.Toplevel) {
+ top.SetNeedsDisplay ();
+ top.Redraw (top.Bounds);
}
- 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;
- }
+ if (_toplevels.Count == 1 && state.Toplevel == Top
+ && (Driver.Cols != state.Toplevel.Frame.Width || Driver.Rows != state.Toplevel.Frame.Height)
+ && (!state.Toplevel._needsDisplay.IsEmpty || state.Toplevel._childNeedsDisplay || state.Toplevel.LayoutNeeded)) {
+
+ Driver.SetAttribute (Colors.TopLevel.Normal);
+ state.Toplevel.Clear (new Rect (0, 0, Driver.Cols, Driver.Rows));
+
+ }
+
+ if (!state.Toplevel._needsDisplay.IsEmpty || state.Toplevel._childNeedsDisplay || state.Toplevel.LayoutNeeded
+ || OverlappedChildNeedsDisplay ()) {
+ state.Toplevel.Redraw (state.Toplevel.Bounds);
+ //if (state.Toplevel.SuperView != null) {
+ // state.Toplevel.SuperView?.OnRenderLineCanvas ();
+ //} else {
+ // state.Toplevel.OnRenderLineCanvas ();
+ //}
+ state.Toplevel.PositionCursor ();
+ Driver.Refresh ();
+ } else {
+ Driver.UpdateCursor ();
+ }
+ if (state.Toplevel != Top && !state.Toplevel.Modal
+ && (!Top._needsDisplay.IsEmpty || Top._childNeedsDisplay || Top.LayoutNeeded)) {
+ Top.Redraw (Top.Bounds);
}
- return true;
}
- static bool OutsideFrame (Point p, Rect r)
+ ///
+ /// Wakes up the that might be waiting on input; must be thread safe.
+ ///
+ public static void DoEvents ()
{
- return p.X < 0 || p.X > r.Width - 1 || p.Y < 0 || p.Y > r.Height - 1;
+ MainLoop.Driver.Wakeup ();
}
///
- /// Building block API: Prepares the provided for execution.
+ /// Stops running the most recent or the if provided.
///
- /// The handle that needs to be passed to the method upon completion.
- /// The to prepare execution for.
+ /// The to stop.
///
- /// This method prepares the provided toplevel for running with the focus,
- /// it adds this to the list of toplevels, sets up the mainloop to process the
- /// event, lays out the subviews, focuses the first element, and draws the
- /// toplevel in the screen. This is usually followed by executing
- /// the method, and then the method upon termination which will
- /// undo these changes.
+ ///
+ /// This will cause to return.
+ ///
+ ///
+ /// Calling is equivalent to setting the property
+ /// on the currently running to false.
+ ///
///
- public static RunState Begin (Toplevel toplevel)
+ public static void RequestStop (Toplevel top = null)
{
- if (toplevel == null) {
- throw new ArgumentNullException (nameof (toplevel));
- } else if (toplevel.IsMdiContainer && MdiTop != toplevel && MdiTop != null) {
- throw new InvalidOperationException ("Only one Mdi Container is allowed.");
+ if (OverlappedTop == null || top == null || (OverlappedTop == null && top != null)) {
+ top = Current;
}
- var rs = new RunState (toplevel);
+ if (OverlappedTop != null && top.IsOverlappedContainer && top?.Running == true
+ && (Current?.Modal == false || (Current?.Modal == true && Current?.Running == false))) {
- // View implements ISupportInitializeNotification which is derived from ISupportInitialize
- if (!toplevel.IsInitialized) {
- toplevel.BeginInit ();
- toplevel.EndInit ();
- }
+ OverlappedTop.RequestStop ();
+ } else if (OverlappedTop != null && top != Current && Current?.Running == true && Current?.Modal == true
+ && top.Modal && top.Running) {
- lock (_toplevels) {
- // If Top was already initialized with Init, and Begin has never been called
- // Top was not added to the toplevels Stack. It will thus never get disposed.
- // Clean it up here:
- if (Top != null && toplevel != Top && !_toplevels.Contains (Top)) {
- Top.Dispose ();
- Top = null;
- } else if (Top != null && toplevel != Top && _toplevels.Contains (Top)) {
- Top.OnLeave (toplevel);
+ var ev = new ToplevelClosingEventArgs (Current);
+ Current.OnClosing (ev);
+ if (ev.Cancel) {
+ return;
}
- 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 ();
+ ev = new ToplevelClosingEventArgs (top);
+ top.OnClosing (ev);
+ if (ev.Cancel) {
+ return;
+ }
+ Current.Running = false;
+ OnNotifyStopRunState (Current);
+ top.Running = false;
+ OnNotifyStopRunState (top);
+ } else if ((OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false
+ && Current?.Running == true && !top.Running)
+ || (OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false
+ && Current?.Running == false && !top.Running && _toplevels.ToArray () [1].Running)) {
- _toplevels.Push (toplevel);
+ MoveCurrent (top);
+ } else if (OverlappedTop != 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;
+ OnNotifyStopRunState (Current);
+ } else if (OverlappedTop != null && Current == top && OverlappedTop?.Running == true && Current?.Running == true && top.Running
+ && Current?.Modal == true && top.Modal) {
+ // The OverlappedTop 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;
+ OnNotifyStopRunState (Current);
+ } else {
+ Toplevel currentTop;
+ if (top == Current || (Current?.Modal == true && !top.Modal)) {
+ currentTop = Current;
} else {
- var dup = _toplevels.FirstOrDefault (x => x.Id.ToString () == toplevel.Id);
- if (dup == null) {
- _toplevels.Push (toplevel);
- }
+ currentTop = top;
}
-
- if (_toplevels.FindDuplicates (new ToplevelEqualityComparer ()).Count > 0) {
- throw new ArgumentException ("There are duplicates toplevels Id's");
+ if (!currentTop.Running) {
+ return;
}
- }
- if (Top == null || 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;
+ var ev = new ToplevelClosingEventArgs (currentTop);
+ currentTop.OnClosing (ev);
+ if (ev.Cancel) {
+ return;
}
- } 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);
+ currentTop.Running = false;
+ OnNotifyStopRunState (currentTop);
}
+ }
- Driver.PrepareToRun (MainLoop, ProcessKeyEvent, ProcessKeyDownEvent, ProcessKeyUpEvent, ProcessMouseEvent);
- if (toplevel.LayoutStyle == LayoutStyle.Computed) {
- toplevel.SetRelativeLayout (new Rect (0, 0, Driver.Cols, Driver.Rows));
- }
- toplevel.LayoutSubviews ();
- toplevel.PositionToplevels ();
- toplevel.WillPresent ();
- if (refreshDriver) {
- MdiTop?.OnChildLoaded (toplevel);
- toplevel.OnLoaded ();
- toplevel.SetNeedsDisplay ();
- toplevel.Redraw (toplevel.Bounds);
- toplevel.PositionCursor ();
- Driver.Refresh ();
+ static void OnNotifyStopRunState (Toplevel top)
+ {
+ if (ExitRunLoopAfterFirstIteration) {
+ NotifyStopRunState?.Invoke (top, new ToplevelEventArgs (top));
}
-
- NotifyNewRunState?.Invoke (toplevel, new RunStateEventArgs (rs));
- return rs;
}
-
+
///
/// Building block API: completes the execution of a that was started with .
///
@@ -1018,14 +902,14 @@ public static void End (RunState runState)
if (runState == null)
throw new ArgumentNullException (nameof (runState));
- if (MdiTop != null) {
- MdiTop.OnChildUnloaded (runState.Toplevel);
+ if (OverlappedTop != null) {
+ OverlappedTop.OnChildUnloaded (runState.Toplevel);
} else {
runState.Toplevel.OnUnloaded ();
}
// End the RunState.Toplevel
- // First, take it off the toplevel Stack
+ // First, take it off the Toplevel Stack
if (_toplevels.Count > 0) {
if (_toplevels.Peek () != runState.Toplevel) {
// If there the top of the stack is not the RunState.Toplevel then
@@ -1038,10 +922,10 @@ public static void End (RunState runState)
// Notify that it is closing
runState.Toplevel?.OnClosed (runState.Toplevel);
- // If there is a MdiTop that is not the RunState.Toplevel then runstate.TopLevel
- // is a child of MidTop and we should notify the MdiTop that it is closing
- if (MdiTop != null && !(runState.Toplevel).Modal && runState.Toplevel != MdiTop) {
- MdiTop.OnChildClosed (runState.Toplevel);
+ // If there is a OverlappedTop that is not the RunState.Toplevel then runstate.TopLevel
+ // is a child of MidTop and we should notify the OverlappedTop that it is closing
+ if (OverlappedTop != null && !(runState.Toplevel).Modal && runState.Toplevel != OverlappedTop) {
+ OverlappedTop.OnChildClosed (runState.Toplevel);
}
// Set Current and Top to the next TopLevel on the stack
@@ -1049,10 +933,10 @@ public static void End (RunState runState)
Current = null;
} else {
Current = _toplevels.Peek ();
- if (_toplevels.Count == 1 && Current == MdiTop) {
- MdiTop.OnAllChildClosed ();
+ if (_toplevels.Count == 1 && Current == OverlappedTop) {
+ OverlappedTop.OnAllChildClosed ();
} else {
- SetCurrentAsTop ();
+ SetCurrentOverlappedAsTop ();
Current.OnEnter (Current);
}
Refresh ();
@@ -1063,544 +947,488 @@ public static void End (RunState runState)
runState.Dispose ();
}
+ #endregion Run (Begin, Run, End)
+
+ #region Toplevel handling
+ static readonly Stack _toplevels = new Stack ();
+
///
- /// Shutdown an application initialized with .
+ /// The object used for the application on startup ()
///
- ///
- /// Shutdown must be called for every call to or
- /// to ensure all resources are cleaned up (Disposed) and terminal settings are restored.
- ///
- public static void Shutdown ()
+ /// The top.
+ public static Toplevel Top { get; private set; }
+
+ ///
+ /// The current object. This is updated when
+ /// enters and leaves to point to the current .
+ ///
+ /// The current.
+ public static Toplevel Current { get; private set; }
+
+ static void EnsureModalOrVisibleAlwaysOnTop (Toplevel Toplevel)
{
- ResetState ();
+ if (!Toplevel.Running || (Toplevel == Current && Toplevel.Visible) || OverlappedTop == null || _toplevels.Peek ().Modal) {
+ return;
+ }
- ConfigurationManager.PrintJsonErrors ();
+ foreach (var top in _toplevels.Reverse ()) {
+ if (top.Modal && top != Current) {
+ MoveCurrent (top);
+ return;
+ }
+ }
+ if (!Toplevel.Visible && Toplevel == Current) {
+ OverlappedMoveNext ();
+ }
}
- // Encapsulate all setting of initial state for Application; Having
- // this in a function like this ensures we don't make mistakes in
- // guaranteeing that the state of this singleton is deterministic when Init
- // starts running and after Shutdown returns.
- static void ResetState ()
+ static View FindDeepestTop (Toplevel start, int x, int y, out int resx, out int resy)
{
- // 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
- foreach (var t in _toplevels) {
- t.Running = false;
- t.Dispose ();
+ var startFrame = start.Frame;
+
+ if (!startFrame.Contains (x, y)) {
+ resx = 0;
+ resy = 0;
+ return null;
}
- _toplevels.Clear ();
- Current = null;
- Top?.Dispose ();
- Top = null;
- // BUGBUG: MdiTop is not cleared here, but it should be?
+ 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 FindTopFromView (View view)
+ {
+ View top = view?.SuperView != null && view?.SuperView != Top
+ ? view.SuperView : view;
- MainLoop = null;
- Driver?.End ();
- Driver = null;
- Iteration = null;
- RootMouseEvent = null;
- RootKeyEvent = null;
- Resized = null;
- _mainThreadId = -1;
- NotifyNewRunState = null;
- NotifyStopRunState = null;
- _initialized = false;
- _mouseGrabView = null;
- _enableConsoleScrolling = false;
- lastMouseOwnerView = null;
+ while (top?.SuperView != null && top?.SuperView != Top) {
+ top = top.SuperView;
+ }
+ return top;
+ }
- // Reset synchronization context to allow the user to run async/await,
- // as the main loop has been ended, the synchronization context from
- // gui.cs does no longer process any callbacks. See #1084 for more details:
- // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
- SynchronizationContext.SetSynchronizationContext (syncContext: null);
+ // 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 (OverlappedTop != null && top != OverlappedTop && 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 (OverlappedTop != null && top != OverlappedTop && 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 ((OverlappedTop != null && top?.Modal == true && _toplevels.Peek () != top)
+ || (OverlappedTop != null && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop)
+ || (OverlappedTop != null && Current?.Modal == false && top != Current)
+ || (OverlappedTop != null && Current?.Modal == true && top == OverlappedTop)) {
+ lock (_toplevels) {
+ _toplevels.MoveTo (top, 0, new ToplevelEqualityComparer ());
+ Current = top;
+ }
+ }
+ return true;
}
///
- /// Triggers a refresh of the entire display.
+ /// Invoked when the terminal was resized. The new size of the terminal is provided.
///
- public static void Refresh ()
+ public static Action TerminalResized;
+
+ static void OnTerminalResized ()
{
- Driver.UpdateOffScreen ();
- View last = null;
- foreach (var v in _toplevels.Reverse ()) {
- if (v.Visible) {
- v.SetNeedsDisplay ();
- v.Redraw (v.Bounds);
- }
- last = v;
+ var full = new Rect (0, 0, Driver.Cols, Driver.Rows);
+ TerminalResized?.Invoke (new ResizedEventArgs () { Cols = full.Width, Rows = full.Height });
+ Driver.Clip = full;
+ foreach (var t in _toplevels) {
+ t.SetRelativeLayout (full);
+ t.LayoutSubviews ();
+ t.PositionToplevels ();
+ t.OnTerminalResized (new SizeChangedEventArgs (full.Size));
}
- last?.PositionCursor ();
- Driver.Refresh ();
+ Refresh ();
}
+ #endregion Toplevel handling
+ #region Mouse handling
+ ///
+ /// Disable or enable the mouse. The mouse is enabled by default.
+ ///
+ [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+ public static bool IsMouseDisabled { get; set; }
///
- /// Building block API: Runs the for the created .
+ /// The current object that wants continuous mouse button pressed events.
///
- ///
- /// Use the parameter to control whether this is a blocking or non-blocking call.
- ///
- /// The state returned by the method.
- /// By default this is which will execute the runloop waiting for events,
- /// if set to , a single iteration will execute.
- public static void RunLoop (RunState state, bool wait = true)
- {
- if (state == null)
- throw new ArgumentNullException (nameof (state));
- if (state.Toplevel == null)
- throw new ObjectDisposedException ("state");
+ public static View WantContinuousButtonPressedView { get; private set; }
- bool firstIteration = true;
- for (state.Toplevel.Running = true; state.Toplevel.Running;) {
- if (ExitRunLoopAfterFirstIteration && !firstIteration) {
- return;
- }
- RunMainLoopIteration (ref state, wait, ref firstIteration);
- }
- }
+ static View _mouseGrabView;
///
- /// Run one iteration of the .
+ /// The view that grabbed the mouse, to where mouse events will be routed to.
///
- /// The state returned by .
- /// If will execute the runloop waiting for events. If
- /// will return after a single iteration.
- /// Set to if this is the first run loop iteration. Upon return,
- /// it will be set to if at least one iteration happened.
- public static void RunMainLoopIteration (ref RunState state, bool wait, ref bool firstIteration)
- {
- if (MainLoop.EventsPending (wait)) {
- // Notify Toplevel it's ready
- if (firstIteration) {
- state.Toplevel.OnReady ();
- }
-
- MainLoop.MainIteration ();
- Iteration?.Invoke ();
+ public static View MouseGrabView => _mouseGrabView;
- EnsureModalOrVisibleAlwaysOnTop (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.SetSubViewNeedsDisplay ();
- Refresh ();
- }
- if (Driver.EnsureCursorVisibility ()) {
- state.Toplevel.SetNeedsDisplay ();
- }
- } else if (!wait) {
- return;
- }
- firstIteration = false;
+ ///
+ /// Invoked when a view wants to grab the mouse; can be canceled.
+ ///
+ public static event EventHandler GrabbingMouse;
- if (state.Toplevel != Top
- && (!Top._needsDisplay.IsEmpty || Top._childNeedsDisplay || Top.LayoutNeeded)) {
- state.Toplevel.SetNeedsDisplay (state.Toplevel.Bounds);
- Top.Redraw (Top.Bounds);
- foreach (var top in _toplevels.Reverse ()) {
- if (top != Top && top != state.Toplevel) {
- top.SetNeedsDisplay ();
- top.Redraw (top.Bounds);
- }
- }
- }
- if (_toplevels.Count == 1 && state.Toplevel == Top
- && (Driver.Cols != state.Toplevel.Frame.Width || Driver.Rows != state.Toplevel.Frame.Height)
- && (!state.Toplevel._needsDisplay.IsEmpty || state.Toplevel._childNeedsDisplay || state.Toplevel.LayoutNeeded)) {
+ ///
+ /// Invoked when a view wants ungrab the mouse; can be canceled.
+ ///
+ public static event EventHandler UnGrabbingMouse;
- Driver.SetAttribute (Colors.TopLevel.Normal);
- state.Toplevel.Clear (new Rect (0, 0, Driver.Cols, Driver.Rows));
+ ///
+ /// Invoked after a view has grabbed the mouse.
+ ///
+ public static event EventHandler GrabbedMouse;
- }
+ ///
+ /// Invoked after a view has ungrabbed the mouse.
+ ///
+ public static event EventHandler UnGrabbedMouse;
- if (!state.Toplevel._needsDisplay.IsEmpty || state.Toplevel._childNeedsDisplay || state.Toplevel.LayoutNeeded
- || MdiChildNeedsDisplay ()) {
- state.Toplevel.Redraw (state.Toplevel.Bounds);
- //if (state.Toplevel.SuperView != null) {
- // state.Toplevel.SuperView?.OnRenderLineCanvas ();
- //} else {
- // state.Toplevel.OnRenderLineCanvas ();
- //}
- state.Toplevel.PositionCursor ();
- Driver.Refresh ();
- } else {
- Driver.UpdateCursor ();
- }
- if (state.Toplevel != Top && !state.Toplevel.Modal
- && (!Top._needsDisplay.IsEmpty || Top._childNeedsDisplay || Top.LayoutNeeded)) {
- Top.Redraw (Top.Bounds);
+ ///
+ /// Grabs the mouse, forcing all mouse events to be routed to the specified view until is called.
+ ///
+ /// View that will receive all mouse events until is invoked.
+ public static void GrabMouse (View view)
+ {
+ if (view == null)
+ return;
+ if (!OnGrabbingMouse (view)) {
+ OnGrabbedMouse (view);
+ _mouseGrabView = view;
+ Driver.UncookMouse ();
}
}
- static void EnsureModalOrVisibleAlwaysOnTop (Toplevel toplevel)
+ ///
+ /// Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.
+ ///
+ public static void UngrabMouse ()
{
- if (!toplevel.Running || (toplevel == Current && toplevel.Visible) || MdiTop == null || _toplevels.Peek ().Modal) {
+ if (_mouseGrabView == null)
return;
- }
-
- foreach (var top in _toplevels.Reverse ()) {
- if (top.Modal && top != Current) {
- MoveCurrent (top);
- return;
- }
- }
- if (!toplevel.Visible && toplevel == Current) {
- MoveNext ();
+ if (!OnUnGrabbingMouse (_mouseGrabView)) {
+ OnUnGrabbedMouse (_mouseGrabView);
+ _mouseGrabView = null;
+ Driver.CookMouse ();
}
}
- static bool MdiChildNeedsDisplay ()
+ static bool OnGrabbingMouse (View view)
{
- if (MdiTop == null) {
+ if (view == null)
return false;
- }
+ var evArgs = new GrabMouseEventArgs (view);
+ GrabbingMouse?.Invoke (view, evArgs);
+ return evArgs.Cancel;
+ }
- foreach (var top in _toplevels) {
- if (top != Current && top.Visible && (!top._needsDisplay.IsEmpty || top._childNeedsDisplay || top.LayoutNeeded)) {
- MdiTop.SetSubViewNeedsDisplay ();
- return true;
- }
- }
- return false;
+ static bool OnUnGrabbingMouse (View view)
+ {
+ if (view == null)
+ return false;
+ var evArgs = new GrabMouseEventArgs (view);
+ UnGrabbingMouse?.Invoke (view, evArgs);
+ return evArgs.Cancel;
}
- ///
- /// Runs the application by calling with the value of .
- ///
- ///
- /// See for more details.
- ///
- public static void Run (Func errorHandler = null)
+ static void OnGrabbedMouse (View view)
{
- Run (Top, errorHandler);
+ if (view == null)
+ return;
+ GrabbedMouse?.Invoke (view, new ViewEventArgs (view));
}
- ///
- /// Runs the application by calling
- /// with a new instance of the specified -derived class.
- ///
- /// Calling first is not needed as this function will initialze the application.
- ///
- ///
- /// must be called when the application is closing (typically after Run> has
- /// returned) to ensure resources are cleaned up and terminal settings restored.
- ///
- ///
- ///
- /// See for more details.
- ///
- ///
- /// The to use. If not specified the default driver for the
- /// platform will be used (, , or ).
- /// This parameteter must be if has already been called.
- ///
- /// Specifies the to use.
- public static void Run (Func errorHandler = null, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) where T : Toplevel, new()
+ static void OnUnGrabbedMouse (View view)
{
- if (_initialized) {
- if (Driver != null) {
- // Init() has been called and we have a driver, so just run the app.
- var top = new T ();
- var type = top.GetType ().BaseType;
- while (type != typeof (Toplevel) && type != typeof (object)) {
- type = type.BaseType;
- }
- if (type != typeof (Toplevel)) {
- throw new ArgumentException ($"{top.GetType ().Name} must be derived from TopLevel");
- }
- Run (top, errorHandler);
- } else {
- // This codepath should be impossible because Init(null, null) will select the platform default driver
- throw new InvalidOperationException ("Init() completed without a driver being set (this should be impossible); Run() cannot be called.");
- }
- } else {
- // Init() has NOT been called.
- InternalInit (() => new T (), driver, mainLoopDriver, calledViaRunT: true);
- Run (Top, errorHandler);
- }
+ if (view == null)
+ return;
+ UnGrabbedMouse?.Invoke (view, new ViewEventArgs (view));
}
///
- /// Runs the main loop on the given container.
+ /// Merely a debugging aid to see the raw mouse events
///
- ///
- ///
- /// This method is used to start processing events
- /// for the main application, but it is also used to
- /// run other modal s such as boxes.
- ///
- ///
- /// To make a stop execution, call .
- ///
- ///
- /// Calling is equivalent to calling , followed by ,
- /// and then calling .
- ///
- ///
- /// Alternatively, to have a program control the main loop and
- /// process events manually, call to set things up manually and then
- /// repeatedly call with the wait parameter set to false. By doing this
- /// the method will only process any pending events, timers, idle handlers and
- /// then return control immediately.
- ///
- ///
- /// RELEASE builds only: When is any exeptions will be rethrown.
- /// Otheriwse, if will be called. If
- /// returns the will resume; otherwise
- /// this method will exit.
- ///
- ///
- /// The to run modally.
- /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, rethrows when null).
- public static void Run (Toplevel view, Func errorHandler = null)
+ public static Action RootMouseEvent;
+
+ static View _lastMouseOwnerView;
+
+ static void ProcessMouseEvent (MouseEvent me)
{
- var resume = true;
- while (resume) {
-#if !DEBUG
- try {
-#endif
- resume = false;
- var runToken = Begin (view);
- // If ExitRunLoopAfterFirstIteration is true then the user must dispose of the runToken
- // by using NotifyStopRunState event.
- RunLoop (runToken);
- if (!ExitRunLoopAfterFirstIteration) {
- End (runToken);
- }
-#if !DEBUG
- }
- catch (Exception error)
- {
- if (errorHandler == null)
- {
- throw;
- }
- resume = errorHandler(error);
- }
-#endif
+ bool OutsideFrame (Point p, Rect r)
+ {
+ return p.X < 0 || p.X > r.Width - 1 || p.Y < 0 || p.Y > r.Height - 1;
}
- }
- ///
- /// 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 currently running to false.
- ///
- ///
- public static void RequestStop (Toplevel top = null)
- {
- if (MdiTop == null || top == null || (MdiTop == null && top != null)) {
- top = Current;
+ if (IsMouseDisabled) {
+ return;
}
- if (MdiTop != null && top.IsMdiContainer && top?.Running == true
- && (Current?.Modal == false || (Current?.Modal == true && Current?.Running == false))) {
+ var view = View.FindDeepestView (Current, me.X, me.Y, out int rx, out int ry);
- MdiTop.RequestStop ();
- } else if (MdiTop != null && top != Current && Current?.Running == true && Current?.Modal == true
- && top.Modal && top.Running) {
+ if (view != null && view.WantContinuousButtonPressed) {
+ WantContinuousButtonPressedView = view;
+ } else {
+ WantContinuousButtonPressedView = null;
+ }
+ if (view != null) {
+ me.View = view;
+ }
+ RootMouseEvent?.Invoke (me);
- var ev = new ToplevelClosingEventArgs (Current);
- Current.OnClosing (ev);
- if (ev.Cancel) {
- return;
+ if (me.Handled) {
+ return;
+ }
+
+ if (_mouseGrabView != null) {
+ var newxy = _mouseGrabView.ScreenToView (me.X, me.Y);
+ var nme = new MouseEvent () {
+ X = newxy.X,
+ Y = newxy.Y,
+ Flags = me.Flags,
+ OfX = me.X - newxy.X,
+ OfY = me.Y - newxy.Y,
+ View = view
+ };
+ if (OutsideFrame (new Point (nme.X, nme.Y), _mouseGrabView.Frame)) {
+ _lastMouseOwnerView?.OnMouseLeave (me);
}
- ev = new ToplevelClosingEventArgs (top);
- top.OnClosing (ev);
- if (ev.Cancel) {
+ //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
+ if (_mouseGrabView?.OnMouseEvent (nme) == true) {
return;
}
- Current.Running = false;
- OnNotifyStopRunState (Current);
- top.Running = false;
- OnNotifyStopRunState (top);
- } 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;
- OnNotifyStopRunState (Current);
- } 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;
- OnNotifyStopRunState (Current);
- } else {
- Toplevel currentTop;
- if (top == Current || (Current?.Modal == true && !top.Modal)) {
- currentTop = Current;
- } else {
- currentTop = top;
+ if ((view == null || view == OverlappedTop) && !Current.Modal && OverlappedTop != null
+ && me.Flags != MouseFlags.ReportMousePosition && me.Flags != 0) {
+
+ var top = FindDeepestTop (Top, me.X, me.Y, out _, out _);
+ view = View.FindDeepestView (top, me.X, me.Y, out rx, out ry);
+
+ if (view != null && view != OverlappedTop && top != Current) {
+ MoveCurrent ((Toplevel)top);
}
- if (!currentTop.Running) {
- return;
+ }
+
+ if (view != null) {
+ var nme = new MouseEvent () {
+ X = rx,
+ Y = ry,
+ Flags = me.Flags,
+ OfX = 0,
+ OfY = 0,
+ View = view
+ };
+
+ if (_lastMouseOwnerView == null) {
+ _lastMouseOwnerView = view;
+ view.OnMouseEnter (nme);
+ } else if (_lastMouseOwnerView != view) {
+ _lastMouseOwnerView.OnMouseLeave (nme);
+ view.OnMouseEnter (nme);
+ _lastMouseOwnerView = view;
}
- var ev = new ToplevelClosingEventArgs (currentTop);
- currentTop.OnClosing (ev);
- if (ev.Cancel) {
+
+ if (!view.WantMousePositionReports && me.Flags == MouseFlags.ReportMousePosition)
return;
- }
- currentTop.Running = false;
- OnNotifyStopRunState (currentTop);
- }
- }
- static void OnNotifyStopRunState (Toplevel top)
- {
- if (ExitRunLoopAfterFirstIteration) {
- NotifyStopRunState?.Invoke (top, new ToplevelEventArgs (top));
- }
- }
+ if (view.WantContinuousButtonPressed)
+ WantContinuousButtonPressedView = view;
+ else
+ WantContinuousButtonPressedView = null;
- ///
- /// Invoked when the terminal was resized. The new size of the terminal is provided.
- ///
- public static Action Resized;
+ // Should we bubbled up the event, if it is not handled?
+ view.OnMouseEvent (nme);
- static void TerminalResized ()
- {
- var full = new Rect (0, 0, Driver.Cols, Driver.Rows);
- Resized?.Invoke (new ResizedEventArgs () { Cols = full.Width, Rows = full.Height });
- Driver.Clip = full;
- foreach (var t in _toplevels) {
- t.SetRelativeLayout (full);
- t.LayoutSubviews ();
- t.PositionToplevels ();
- t.OnResized (new SizeChangedEventArgs (full.Size));
+ BringOverlappedTopToFront ();
}
- Refresh ();
}
+ #endregion Mouse handling
- static bool SetCurrentAsTop ()
- {
- if (MdiTop == null && Current != Top && Current?.SuperView == null && Current?.Modal == false) {
- if (Current.Frame != new Rect (0, 0, Driver.Cols, Driver.Rows)) {
- Current.Frame = new Rect (0, 0, Driver.Cols, Driver.Rows);
+ #region Keyboard handling
+
+
+ static Key _alternateForwardKey = Key.PageDown | Key.CtrlMask;
+
+ ///
+ /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.
+ ///
+ [SerializableConfigurationProperty (Scope = typeof (SettingsScope)), JsonConverter (typeof (KeyJsonConverter))]
+ public static Key AlternateForwardKey {
+ get => _alternateForwardKey;
+ set {
+ if (_alternateForwardKey != value) {
+ var oldKey = _alternateForwardKey;
+ _alternateForwardKey = value;
+ OnAlternateForwardKeyChanged (new KeyChangedEventArgs (oldKey, value));
}
- Top = Current;
- return true;
}
- return false;
}
- ///
- /// Move to the next Mdi child from the .
- ///
- public static void MoveNext ()
+ static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e)
{
- if (MdiTop != null && !Current.Modal) {
- lock (_toplevels) {
- _toplevels.MoveNext ();
- var isMdi = false;
- while (_toplevels.Peek () == MdiTop || !_toplevels.Peek ().Visible) {
- if (!isMdi && _toplevels.Peek () == MdiTop) {
- isMdi = true;
- } else if (isMdi && _toplevels.Peek () == MdiTop) {
- MoveCurrent (Top);
- break;
- }
- _toplevels.MoveNext ();
- }
- Current = _toplevels.Peek ();
- }
+ foreach (var top in _toplevels.ToArray ()) {
+ top.OnAlternateForwardKeyChanged (e);
}
}
+ static Key _alternateBackwardKey = Key.PageUp | Key.CtrlMask;
+
///
- /// Move to the previous Mdi child from the .
+ /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.
///
- public static void MovePrevious ()
- {
- if (MdiTop != null && !Current.Modal) {
- lock (_toplevels) {
- _toplevels.MovePrevious ();
- var isMdi = false;
- while (_toplevels.Peek () == MdiTop || !_toplevels.Peek ().Visible) {
- if (!isMdi && _toplevels.Peek () == MdiTop) {
- isMdi = true;
- } else if (isMdi && _toplevels.Peek () == MdiTop) {
- MoveCurrent (Top);
- break;
- }
- _toplevels.MovePrevious ();
- }
- Current = _toplevels.Peek ();
+ [SerializableConfigurationProperty (Scope = typeof (SettingsScope)), JsonConverter (typeof (KeyJsonConverter))]
+ public static Key AlternateBackwardKey {
+ get => _alternateBackwardKey;
+ set {
+ if (_alternateBackwardKey != value) {
+ var oldKey = _alternateBackwardKey;
+ _alternateBackwardKey = value;
+ OnAlternateBackwardKeyChanged (new KeyChangedEventArgs (oldKey, value));
}
}
}
- internal static bool ShowChild (Toplevel top)
+ static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey)
{
- if (top.Visible && MdiTop != null && Current?.Modal == false) {
- lock (_toplevels) {
- _toplevels.MoveTo (top, 0, new ToplevelEqualityComparer ());
- Current = top;
- }
- return true;
+ foreach (var top in _toplevels.ToArray ()) {
+ top.OnAlternateBackwardKeyChanged (oldKey);
}
- return false;
}
+ static Key _quitKey = Key.Q | Key.CtrlMask;
+
///
- /// Wakes up the mainloop that might be waiting on input, must be thread safe.
+ /// Gets or sets the key to quit the application.
///
- public static void DoEvents ()
+ [SerializableConfigurationProperty (Scope = typeof (SettingsScope)), JsonConverter (typeof (KeyJsonConverter))]
+ public static Key QuitKey {
+ get => _quitKey;
+ set {
+ if (_quitKey != value) {
+ var oldKey = _quitKey;
+ _quitKey = value;
+ OnQuitKeyChanged (new KeyChangedEventArgs (oldKey, value));
+ }
+ }
+ }
+ static void OnQuitKeyChanged (KeyChangedEventArgs e)
{
- MainLoop.Driver.Wakeup ();
+ // Duplicate the list so if it changes during enumeration we're safe
+ foreach (var top in _toplevels.ToArray ()) {
+ top.OnQuitKeyChanged (e);
+ }
}
- ///
- /// Ensures that the superview of the most focused view is on front.
- ///
- public static void EnsuresTopOnFront ()
+ static void ProcessKeyEvent (KeyEvent ke)
{
- if (MdiTop != null) {
+ if (RootKeyEvent?.Invoke (ke) ?? false) {
return;
}
- var top = FindTopFromView (Top?.MostFocused);
- if (top != null && Top.Subviews.Count > 1 && Top.Subviews [Top.Subviews.Count - 1] != top) {
- Top.BringSubviewToFront (top);
+
+ var chain = _toplevels.ToList ();
+ foreach (var topLevel in chain) {
+ if (topLevel.ProcessHotKey (ke))
+ return;
+ if (topLevel.Modal)
+ break;
+ }
+
+ foreach (var topLevel in chain) {
+ if (topLevel.ProcessKey (ke))
+ return;
+ if (topLevel.Modal)
+ break;
+ }
+
+ foreach (var topLevel in chain) {
+ // Process the key normally
+ if (topLevel.ProcessColdKey (ke))
+ return;
+ if (topLevel.Modal)
+ break;
}
}
- internal static List GetSupportedCultures ()
+ static void ProcessKeyDownEvent (KeyEvent ke)
{
- CultureInfo [] culture = CultureInfo.GetCultures (CultureTypes.AllCultures);
-
- // Get the assembly
- Assembly assembly = Assembly.GetExecutingAssembly ();
+ var chain = _toplevels.ToList ();
+ foreach (var topLevel in chain) {
+ if (topLevel.OnKeyDown (ke))
+ return;
+ if (topLevel.Modal)
+ break;
+ }
+ }
- //Find the location of the assembly
- string assemblyLocation = AppDomain.CurrentDomain.BaseDirectory;
+ static void ProcessKeyUpEvent (KeyEvent ke)
+ {
+ var chain = _toplevels.ToList ();
+ foreach (var topLevel in chain) {
+ if (topLevel.OnKeyUp (ke))
+ return;
+ if (topLevel.Modal)
+ break;
+ }
+ }
- // Find the resource file name of the assembly
- string resourceFilename = $"{Path.GetFileNameWithoutExtension (assembly.Location)}.resources.dll";
+ ///
+ ///
+ /// Called for new KeyPress events before any processing is performed or
+ /// views evaluate. Use for global key handling and/or debugging.
+ ///
+ /// Return true to suppress the KeyPress event
+ ///
+ public static Func RootKeyEvent;
- // Return all culture for which satellite folder found with culture code.
- return culture.Where (cultureInfo =>
- assemblyLocation != null &&
- Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name)) &&
- File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename))
- ).ToList ();
- }
+ #endregion Keyboard handling
}
}
diff --git a/Terminal.Gui/Configuration/DictionaryJsonConverter.cs b/Terminal.Gui/Configuration/DictionaryJsonConverter.cs
index e8218e5cfa..5027e22cb6 100644
--- a/Terminal.Gui/Configuration/DictionaryJsonConverter.cs
+++ b/Terminal.Gui/Configuration/DictionaryJsonConverter.cs
@@ -24,7 +24,6 @@ public override Dictionary Read (ref Utf8JsonReader reader, Type type
return dictionary;
}
-
public override void Write (Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options)
{
writer.WriteStartArray ();
diff --git a/Terminal.Gui/Configuration/Scope.cs b/Terminal.Gui/Configuration/Scope.cs
index 078ff17e6e..c69aae46e1 100644
--- a/Terminal.Gui/Configuration/Scope.cs
+++ b/Terminal.Gui/Configuration/Scope.cs
@@ -7,7 +7,6 @@
using System.Text.Json.Serialization;
using static Terminal.Gui.ConfigurationManager;
-
#nullable enable
namespace Terminal.Gui {
diff --git a/Terminal.Gui/Configuration/SettingsScope.cs b/Terminal.Gui/Configuration/SettingsScope.cs
index 49d6f18394..cc91eb0e2a 100644
--- a/Terminal.Gui/Configuration/SettingsScope.cs
+++ b/Terminal.Gui/Configuration/SettingsScope.cs
@@ -6,7 +6,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-
#nullable enable
namespace Terminal.Gui {
diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
index a8eac2754a..53f04004ca 100644
--- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
@@ -534,7 +534,7 @@ public static Dictionary Create ()
}
///
- /// The application toplevel color scheme, for the default toplevel views.
+ /// The application Toplevel color scheme, for the default Toplevel views.
///
///
///
@@ -544,7 +544,7 @@ public static Dictionary Create ()
public static ColorScheme TopLevel { get => GetColorScheme (); set => SetColorScheme (value); }
///
- /// The base color scheme, for the default toplevel views.
+ /// The base color scheme, for the default Toplevel views.
///
///
///
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
index df53595428..d3ab5d1d7c 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
@@ -191,7 +191,7 @@ bool CheckTimers (bool wait, out int pollTimeout)
return ic > 0;
}
- void IMainLoopDriver.MainIteration ()
+ void IMainLoopDriver.Iteration ()
{
if (winChanged) {
winChanged = false;
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs
index 1fb3580fba..8a0ed3da78 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs
@@ -73,7 +73,6 @@ public struct MouseEvent {
static UnmanagedLibrary curses_library;
static NativeMethods methods;
-
[DllImport ("libc")]
public extern static int setlocale (int cate, [MarshalAs (UnmanagedType.LPStr)] string locale);
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/handles.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/handles.cs
index de923e9685..8a46149d50 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/handles.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/handles.cs
@@ -59,7 +59,6 @@ static public Window Current {
}
}
-
public int wtimeout (int delay)
{
return Curses.wtimeout (Handle, delay);
diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs
index 592b974fcc..06a08a0698 100644
--- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs
+++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs
@@ -87,7 +87,7 @@ bool CheckTimers (bool wait, out int waitTimeout)
return ic > 0;
}
- void IMainLoopDriver.MainIteration ()
+ void IMainLoopDriver.Iteration ()
{
if (keyResult.HasValue) {
KeyPressed?.Invoke (keyResult.Value);
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs
index 08e8276834..1617cd1889 100644
--- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs
@@ -1115,7 +1115,6 @@ public override void Suspend ()
{
}
-
public override void SetAttribute (Attribute c)
{
base.SetAttribute (c);
@@ -1172,7 +1171,6 @@ Key MapKey (ConsoleKeyInfo keyInfo)
case ConsoleKey.Insert:
return MapKeyModifiers (keyInfo, Key.InsertChar);
-
case ConsoleKey.Oem1:
case ConsoleKey.Oem2:
case ConsoleKey.Oem3:
@@ -1441,7 +1439,6 @@ public override bool GetCursorVisibility (out CursorVisibility visibility)
return visibility == CursorVisibility.Default;
}
-
///
public override bool SetCursorVisibility (CursorVisibility visibility)
{
@@ -1634,7 +1631,7 @@ bool CheckTimers (bool wait, out int waitTimeout)
return ic > 0;
}
- void IMainLoopDriver.MainIteration ()
+ void IMainLoopDriver.Iteration ()
{
while (inputResult.Count > 0) {
ProcessInput?.Invoke (inputResult.Dequeue ().Value);
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
index 094b79be65..71b837abff 100644
--- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
@@ -577,7 +577,6 @@ public struct ConsoleCursorInfo {
[DllImport ("kernel32.dll")]
static extern bool GetConsoleMode (IntPtr hConsoleHandle, out uint lpMode);
-
[DllImport ("kernel32.dll")]
static extern bool SetConsoleMode (IntPtr hConsoleHandle, uint dwMode);
@@ -1980,7 +1979,7 @@ bool CheckTimers (bool wait, out int waitTimeout)
return ic > 0;
}
- void IMainLoopDriver.MainIteration ()
+ void IMainLoopDriver.Iteration ()
{
while (resultQueue.Count > 0) {
var inputRecords = resultQueue.Dequeue ();
diff --git a/Terminal.Gui/Drawing/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas.cs
index eeda2b8e57..039bb170a7 100644
--- a/Terminal.Gui/Drawing/LineCanvas.cs
+++ b/Terminal.Gui/Drawing/LineCanvas.cs
@@ -52,7 +52,6 @@ public class LineCanvas {
{IntersectionRuneType.RightTee,new RightTeeIntersectionRuneResolver()},
{IntersectionRuneType.BottomTee,new BottomTeeIntersectionRuneResolver()},
-
{IntersectionRuneType.Crosshair,new CrosshairIntersectionRuneResolver()},
// TODO: Add other resolvers
};
@@ -132,6 +131,8 @@ public Rect Bounds {
}
}
+ // TODO: Unless there's an obvious use case for this API we should delete it in favor of the
+ // simpler version that doensn't take an area.
///
/// Evaluates the lines that have been added to the canvas and returns a map containing
/// the glyphs and their locations. The glyphs are the characters that should be rendered
@@ -168,8 +169,7 @@ public Dictionary GetMap (Rect inArea)
/// the glyphs and their locations. The glyphs are the characters that should be rendered
/// so that all lines connect up with the appropriate intersection symbols.
///
- /// A rectangle to constrain the search by.
- /// A map of the points within the canvas that intersect with .
+ /// A map of all the points within the canvas.
public Dictionary GetCellMap ()
{
var map = new Dictionary ();
@@ -243,7 +243,6 @@ public override string ToString ()
return sb.ToString ();
}
-
private abstract class IntersectionRuneResolver {
readonly Rune round;
readonly Rune doubleH;
@@ -267,7 +266,6 @@ public IntersectionRuneResolver (Rune round, Rune doubleH, Rune doubleV, Rune do
bool doubleHorizontal = intersects.Any (l => l.Line.Orientation == Orientation.Horizontal && l.Line.Style == LineStyle.Double);
bool doubleVertical = intersects.Any (l => l.Line.Orientation == Orientation.Vertical && l.Line.Style == LineStyle.Double);
-
if (doubleHorizontal) {
return doubleVertical ? doubleBoth : doubleH;
}
@@ -392,19 +390,24 @@ public CrosshairIntersectionRuneResolver () :
}
+ ///
+ /// Represents a single row/column within the . Includes the glyph and the foreground/background colors.
+ ///
public class Cell
{
- public Cell ()
- {
-
- }
-
+ ///
+ /// The glyph to draw.
+ ///
public Rune? Rune { get; set; }
+
+ ///
+ /// The foreground color to draw the glyph with.
+ ///
public Attribute? Attribute { get; set; }
}
- private Cell? GetCellForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects)
+ private Cell GetCellForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects)
{
if (!intersects.Any ()) {
return null;
@@ -416,7 +419,6 @@ public Cell ()
return cell;
}
-
private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition [] intersects)
{
var set = new HashSet (intersects.Select (i => i.Type));
@@ -445,7 +447,6 @@ private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition []
return IntersectionRuneType.Crosshair;
}
-
if (Has (set,
IntersectionType.StartLeft,
IntersectionType.StartRight,
@@ -455,7 +456,6 @@ private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition []
}
#endregion
-
#region Corner Conditions
if (Exactly (set,
IntersectionType.StartRight,
@@ -507,7 +507,6 @@ private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition []
return IntersectionRuneType.BottomTee;
}
-
if (Has (set,
IntersectionType.PassOverVertical,
IntersectionType.StartRight)) {
@@ -520,7 +519,6 @@ private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition []
return IntersectionRuneType.LeftTee;
}
-
if (Has (set,
IntersectionType.PassOverVertical,
IntersectionType.StartLeft)) {
diff --git a/Terminal.Gui/Drawing/Ruler.cs b/Terminal.Gui/Drawing/Ruler.cs
index 78bdcb3d20..cb59040c11 100644
--- a/Terminal.Gui/Drawing/Ruler.cs
+++ b/Terminal.Gui/Drawing/Ruler.cs
@@ -33,7 +33,6 @@ public class Ruler {
string _hTemplate { get; } = "|123456789";
string _vTemplate { get; } = "-123456789";
-
///
/// Draws the .
///
diff --git a/Terminal.Gui/FileServices/AllowedType.cs b/Terminal.Gui/FileServices/AllowedType.cs
index 7072c95e9a..bcf44490c6 100644
--- a/Terminal.Gui/FileServices/AllowedType.cs
+++ b/Terminal.Gui/FileServices/AllowedType.cs
@@ -22,7 +22,6 @@ public interface IAllowedType
bool IsAllowed (string path);
}
-
///
/// that allows selection of any types (*.*).
///
@@ -74,7 +73,6 @@ public AllowedType (string description, params string [] extensions)
///
public string [] Extensions { get; set; }
-
///
/// Returns plus all separated by semicolons.
///
@@ -100,7 +98,6 @@ public bool IsAllowed(string path)
return false;
}
-
return this.Extensions.Any (e => e.Equals (extension));
}
}
diff --git a/Terminal.Gui/FileServices/FileDialogHistory.cs b/Terminal.Gui/FileServices/FileDialogHistory.cs
index 02c181313e..d490a73a78 100644
--- a/Terminal.Gui/FileServices/FileDialogHistory.cs
+++ b/Terminal.Gui/FileServices/FileDialogHistory.cs
@@ -78,7 +78,6 @@ internal bool CanUp ()
return this.dlg.State?.Directory.Parent != null;
}
-
internal void Push (FileDialogState state, bool clearForward)
{
if (state == null) {
diff --git a/Terminal.Gui/FileServices/FileDialogState.cs b/Terminal.Gui/FileServices/FileDialogState.cs
index 6db932e656..ab3ed1033f 100644
--- a/Terminal.Gui/FileServices/FileDialogState.cs
+++ b/Terminal.Gui/FileServices/FileDialogState.cs
@@ -55,7 +55,6 @@ protected virtual IEnumerable GetChildren (IDirectoryInfo d
children = children.Where (MatchesApiFilter).ToList ();
}
-
// allow navigating up as '..'
if (dir.Parent != null) {
children.Add (new FileSystemInfoStats (dir.Parent) { IsParent = true });
diff --git a/Terminal.Gui/FileServices/FileDialogStyle.cs b/Terminal.Gui/FileServices/FileDialogStyle.cs
index 7c4f61d5d9..981733de67 100644
--- a/Terminal.Gui/FileServices/FileDialogStyle.cs
+++ b/Terminal.Gui/FileServices/FileDialogStyle.cs
@@ -23,7 +23,6 @@ public class FileDialogStyle {
[SerializableConfigurationProperty(Scope = typeof (SettingsScope))]
public static bool DefaultUseColors { get; set; }
-
///
/// Gets or sets the default value to use for .
/// This can be populated from .tui config files via
@@ -158,14 +157,12 @@ public class FileDialogStyle {
///
public bool UseUnicodeCharacters { get; set; } = DefaultUseUnicodeCharacters;
-
///
/// User defined delegate for picking which character(s)/unicode
/// symbol(s) to use as an 'icon' for files/folders.
///
public Func IconGetter { get; set; }
-
///
/// Gets or sets the format to use for date/times in the Modified column.
/// Defaults to
@@ -173,7 +170,6 @@ public class FileDialogStyle {
///
public string DateFormat { get; set; }
-
///
/// Creates a new instance of the class.
///
@@ -233,7 +229,6 @@ private static IEnumerable DefaultTreeRootGetter ()
// Cannot get the system disks thats fine
}
-
try {
foreach (var special in Enum.GetValues (typeof (Environment.SpecialFolder)).Cast ()) {
try {
diff --git a/Terminal.Gui/FileServices/FileDialogTreeBuilder.cs b/Terminal.Gui/FileServices/FileDialogTreeBuilder.cs
index 408a0d762a..462a9284d8 100644
--- a/Terminal.Gui/FileServices/FileDialogTreeBuilder.cs
+++ b/Terminal.Gui/FileServices/FileDialogTreeBuilder.cs
@@ -3,7 +3,6 @@
using System.IO;
using System.Linq;
-
namespace Terminal.Gui {
class FileDialogTreeBuilder : ITreeBuilder
internal class FileSystemInfoStats {
-
/* ---- Colors used by the ls command line tool ----
*
* Blue: Directory
@@ -127,7 +126,6 @@ private static string GetHumanReadableFileSize (long value)
int mag = (int)Math.Log (value, ByteConversion);
double adjustedSize = value / Math.Pow (1000, mag);
-
return string.Format ("{0:n2} {1}", adjustedSize, SizeSuffixes [mag]);
}
}
diff --git a/Terminal.Gui/FileServices/IFileOperations.cs b/Terminal.Gui/FileServices/IFileOperations.cs
index e586b62d46..d308e11a90 100644
--- a/Terminal.Gui/FileServices/IFileOperations.cs
+++ b/Terminal.Gui/FileServices/IFileOperations.cs
@@ -19,7 +19,6 @@ public interface IFileOperations {
/// error handling (e.g. showing a
bool Delete (IEnumerable toDelete);
-
///
/// Specifies how to handle file/directory rename attempts
/// in .
@@ -31,7 +30,6 @@ public interface IFileOperations {
/// error handling (e.g. showing a
IFileSystemInfo Rename (IFileSystem fileSystem, IFileSystemInfo toRename);
-
///
/// Specifies how to handle 'new directory' operation
/// in .
diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs
index 9d106f6643..29de444eea 100644
--- a/Terminal.Gui/Input/Command.cs
+++ b/Terminal.Gui/Input/Command.cs
@@ -127,7 +127,6 @@ public enum Command {
///
ToggleOverwrite,
-
///
/// Enables overwrite mode such that newly typed text overwrites the text that is
/// already there (typically associated with the Insert key).
@@ -356,12 +355,12 @@ public enum Command {
PreviousView,
///
- /// Moves focus to the next view or toplevel (case of MDI).
+ /// Moves focus to the next view or Toplevel (case of Overlapped).
///
NextViewOrTop,
///
- /// Moves focus to the next previous or toplevel (case of MDI).
+ /// Moves focus to the next previous or Toplevel (case of Overlapped).
///
PreviousViewOrTop,
diff --git a/Terminal.Gui/MainLoop.cs b/Terminal.Gui/MainLoop.cs
index b48fe2a77b..777b70e9b8 100644
--- a/Terminal.Gui/MainLoop.cs
+++ b/Terminal.Gui/MainLoop.cs
@@ -10,17 +10,17 @@
namespace Terminal.Gui {
///
- /// Public interface to create your own platform specific main loop driver.
+ /// Public interface to create a platform specific driver.
///
public interface IMainLoopDriver {
///
- /// Initializes the main loop driver, gets the calling main loop for the initialization.
+ /// Initializes the , gets the calling main loop for the initialization.
///
/// Main loop.
void Setup (MainLoop mainLoop);
///
- /// Wakes up the mainloop that might be waiting on input, must be thread safe.
+ /// Wakes up the that might be waiting on input, must be thread safe.
///
void Wakeup ();
@@ -34,7 +34,7 @@ public interface IMainLoopDriver {
///
/// The iteration function.
///
- void MainIteration ();
+ void Iteration ();
}
///
@@ -61,12 +61,12 @@ public sealed class Timeout {
}
internal SortedList timeouts = new SortedList ();
- object timeoutsLockToken = new object ();
+ object _timeoutsLockToken = new object ();
///
/// The idle handlers and lock that must be held while manipulating them
///
- object idleHandlersLock = new object ();
+ object _idleHandlersLock = new object ();
internal List> idleHandlers = new List> ();
///
@@ -81,14 +81,14 @@ public sealed class Timeout {
///
public ReadOnlyCollection> IdleHandlers {
get {
- lock (idleHandlersLock) {
+ lock (_idleHandlersLock) {
return new List> (idleHandlers).AsReadOnly ();
}
}
}
///
- /// The current IMainLoopDriver in use.
+ /// The current in use.
///
/// The driver.
public IMainLoopDriver Driver { get; }
@@ -123,7 +123,8 @@ public void Invoke (Action action)
}
///
- /// Adds specified idle handler function to mainloop processing. The handler function will be called once per iteration of the main loop after other events have been handled.
+ /// Adds specified idle handler function to processing.
+ /// The handler function will be called once per iteration of the main loop after other events have been handled.
///
///
///
@@ -136,7 +137,7 @@ public void Invoke (Action action)
/// Token that can be used to remove the idle handler with .
public Func AddIdle (Func idleHandler)
{
- lock (idleHandlersLock) {
+ lock (_idleHandlersLock) {
idleHandlers.Add (idleHandler);
}
@@ -152,13 +153,13 @@ public Func AddIdle (Func idleHandler)
/// This method also returns false if the idle handler is not found.
public bool RemoveIdle (Func token)
{
- lock (idleHandlersLock)
+ lock (_idleHandlersLock)
return idleHandlers.Remove (token);
}
void AddTimeout (TimeSpan time, Timeout timeout)
{
- lock (timeoutsLockToken) {
+ lock (_timeoutsLockToken) {
var k = (DateTime.UtcNow + time).Ticks;
timeouts.Add (NudgeToUniqueKey (k), timeout);
TimeoutAdded?.Invoke (this, new TimeoutEventArgs(timeout, k));
@@ -166,7 +167,7 @@ void AddTimeout (TimeSpan time, Timeout timeout)
}
///
- /// Adds a timeout to the mainloop.
+ /// Adds a timeout to the .
///
///
/// When time specified passes, the callback will be invoked.
@@ -198,7 +199,7 @@ public object AddTimeout (TimeSpan time, Func callback)
/// This method also returns false if the timeout is not found.
public bool RemoveTimeout (object token)
{
- lock (timeoutsLockToken) {
+ lock (_timeoutsLockToken) {
var idx = timeouts.IndexOfValue (token as Timeout);
if (idx == -1)
return false;
@@ -216,7 +217,7 @@ void RunTimers ()
// after we have taken the copy but before
// we have allocated a new list (which would
// result in lost timeouts or errors during enumeration)
- lock (timeoutsLockToken) {
+ lock (_timeoutsLockToken) {
copy = timeouts;
timeouts = new SortedList ();
}
@@ -228,7 +229,7 @@ void RunTimers ()
if (timeout.Callback (this))
AddTimeout (timeout.Span, timeout);
} else {
- lock (timeoutsLockToken) {
+ lock (_timeoutsLockToken) {
timeouts.Add (NudgeToUniqueKey (k), timeout);
}
}
@@ -243,7 +244,7 @@ void RunTimers ()
///
private long NudgeToUniqueKey (long k)
{
- lock (timeoutsLockToken) {
+ lock (_timeoutsLockToken) {
while (timeouts.ContainsKey (k)) {
k++;
}
@@ -255,26 +256,26 @@ private long NudgeToUniqueKey (long k)
void RunIdle ()
{
List> iterate;
- lock (idleHandlersLock) {
+ lock (_idleHandlersLock) {
iterate = idleHandlers;
idleHandlers = new List> ();
}
foreach (var idle in iterate) {
if (idle ())
- lock (idleHandlersLock)
+ lock (_idleHandlersLock)
idleHandlers.Add (idle);
}
}
- bool running;
+ bool _running;
///
/// Stops the mainloop.
///
public void Stop ()
{
- running = false;
+ _running = false;
Driver.Wakeup ();
}
@@ -295,20 +296,21 @@ public bool EventsPending (bool wait = false)
/// Runs one iteration of timers and file watches
///
///
- /// You use this to process all pending events (timers, idle handlers and file watches).
+ /// Use this to process all pending events (timers, idle handlers and file watches).
///
- /// You can use it like this:
- /// while (main.EvensPending ()) MainIteration ();
+ ///
+ /// while (main.EvenstPending ()) RunIteration ();
+ ///
///
- public void MainIteration ()
+ public void RunIteration ()
{
if (timeouts.Count > 0)
RunTimers ();
- Driver.MainIteration ();
+ Driver.Iteration ();
bool runIdle = false;
- lock (idleHandlersLock) {
+ lock (_idleHandlersLock) {
runIdle = idleHandlers.Count > 0;
}
if (runIdle) {
@@ -317,17 +319,17 @@ public void MainIteration ()
}
///
- /// Runs the mainloop.
+ /// Runs the .
///
public void Run ()
{
- bool prev = running;
- running = true;
- while (running) {
+ bool prev = _running;
+ _running = true;
+ while (_running) {
EventsPending (true);
- MainIteration ();
+ RunIteration ();
}
- running = prev;
+ _running = prev;
}
}
}
diff --git a/Terminal.Gui/README.md b/Terminal.Gui/README.md
index a8b431e91a..fb380d4c0f 100644
--- a/Terminal.Gui/README.md
+++ b/Terminal.Gui/README.md
@@ -34,13 +34,12 @@ All files required to build the **Terminal.Gui** library (and NuGet package).
- `Views\` - Sub-classes of `View`
- `Toplevel` - Derived from `View`, the base class for modal visual elements such as top-level windows and dialogs. Supports the concept of `MenuBar` and `StatusBar`.
- - `Window` - Derived from `TopLevel`; implements toplevel views with a visible frame and Title.
+ - `Window` - Derived from `TopLevel`; implements Toplevel views with a visible frame and Title.
- `Dialog` -
- etc...
- `Types/` - A folder (not namespace) containing implementations of `Point`, `Rect`, and `Size` which are ancient versions of the modern `System.Drawing.Point`, `System.Drawing.Size`, and `System.Drawning.Rectangle`.
-
## Version numbers
Version info for Terminal.Gui is managed by [gitversion](https://gitversion.net).
diff --git a/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs
index 5f6caaa9ea..4ca620fa73 100644
--- a/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs
+++ b/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs
@@ -30,7 +30,6 @@ public AppendAutocomplete (TextField textField)
this.textField = textField;
SelectionKey = Key.Tab;
-
ColorScheme = new ColorScheme{
Normal = new Attribute(Color.DarkGray,0),
Focus = new Attribute(Color.DarkGray,0),
@@ -147,7 +146,6 @@ internal bool AcceptSelectionIfAny ()
return false;
}
-
internal void SetTextTo (FileSystemInfo fileSystemInfo)
{
var newText = fileSystemInfo.FullName;
@@ -158,7 +156,6 @@ internal void SetTextTo (FileSystemInfo fileSystemInfo)
textField.MoveEnd ();
}
-
///
/// Returns true if there is a suggestion that can be made and the control
/// is in a state where user would expect to see auto-complete (i.e. focused and
diff --git a/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs b/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs
index 3c27995f4b..a168eb0db6 100644
--- a/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs
+++ b/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs
@@ -26,7 +26,6 @@ public abstract class AutocompleteBase : IAutocomplete {
///
-
///
public virtual bool Visible { get; set; }
@@ -34,11 +33,9 @@ public abstract class AutocompleteBase : IAutocomplete {
public virtual ReadOnlyCollection Suggestions { get; set; } = new ReadOnlyCollection (new Suggestion [0]);
-
///
public virtual int SelectedIdx { get; set; }
-
///
public abstract ColorScheme ColorScheme { get; set; }
@@ -65,7 +62,6 @@ public virtual void ClearSuggestions ()
Suggestions = Enumerable.Empty ().ToList ().AsReadOnly ();
}
-
///
public virtual void GenerateSuggestions (AutocompleteContext context)
{
diff --git a/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs
index 8ce2ad3abb..a12c931613 100644
--- a/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs
+++ b/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs
@@ -75,7 +75,6 @@ public interface IAutocomplete {
///
void RenderOverlay (Point renderAt);
-
///
/// Handle key events before e.g. to make key events like
/// up/down apply to the autocomplete control instead of changing the cursor position in
@@ -100,14 +99,12 @@ public interface IAutocomplete {
///
void ClearSuggestions ();
-
///
/// Gets or Sets the class responsible for generating
/// based on a given of the .
///
ISuggestionGenerator SuggestionGenerator { get; set; }
-
///
/// Populates with all
/// proposed by at the given
diff --git a/Terminal.Gui/Text/Autocomplete/ISuggestionGenerator.cs b/Terminal.Gui/Text/Autocomplete/ISuggestionGenerator.cs
index f4e2d08a52..7ad64569f2 100644
--- a/Terminal.Gui/Text/Autocomplete/ISuggestionGenerator.cs
+++ b/Terminal.Gui/Text/Autocomplete/ISuggestionGenerator.cs
@@ -12,7 +12,6 @@ public interface ISuggestionGenerator {
///
IEnumerable GenerateSuggestions (AutocompleteContext context);
-
///
/// Returns if is a character that
/// would continue autocomplete suggesting. Returns if it
diff --git a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs
index 5ac399d20b..e97f9611fe 100644
--- a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs
+++ b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs
@@ -120,7 +120,6 @@ private void ManipulatePopup ()
}
}
-
///
/// When more suggestions are available than can be rendered the user
/// can scroll down the dropdown list. This indicates how far down they
@@ -256,7 +255,6 @@ public override void EnsureSelectedIdxIsValid ()
{
base.EnsureSelectedIdxIsValid ();
-
// if user moved selection up off top of current scroll window
if (SelectedIdx < ScrollOffset) {
ScrollOffset = SelectedIdx;
@@ -410,7 +408,6 @@ protected void RenderSelectedIdxByMouse (MouseEvent me)
}
-
///
/// Completes the autocomplete selection process. Called when user hits the .
///
@@ -447,7 +444,6 @@ protected virtual bool InsertSelection (Suggestion accepted)
return true;
}
-
///
/// Deletes the text backwards before insert the selected text in the .
///
diff --git a/Terminal.Gui/Text/Autocomplete/SingleWordSuggestionGenerator.cs b/Terminal.Gui/Text/Autocomplete/SingleWordSuggestionGenerator.cs
index e222b9794c..3af30b8c1e 100644
--- a/Terminal.Gui/Text/Autocomplete/SingleWordSuggestionGenerator.cs
+++ b/Terminal.Gui/Text/Autocomplete/SingleWordSuggestionGenerator.cs
@@ -53,7 +53,6 @@ public virtual bool IsWordChar (Rune rune)
return Char.IsLetterOrDigit ((char)rune);
}
-
///
///
/// Given a of characters, returns the word which ends at
diff --git a/Terminal.Gui/Text/Autocomplete/Suggestion.cs b/Terminal.Gui/Text/Autocomplete/Suggestion.cs
index 88f8b23750..8e0ee2f077 100644
--- a/Terminal.Gui/Text/Autocomplete/Suggestion.cs
+++ b/Terminal.Gui/Text/Autocomplete/Suggestion.cs
@@ -21,7 +21,6 @@ public class Suggestion {
///
public string Replacement { get; }
-
///
/// Creates a new instance of the class.
///
diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs
index d3cd9fa30c..97b02b587a 100644
--- a/Terminal.Gui/Text/TextFormatter.cs
+++ b/Terminal.Gui/Text/TextFormatter.cs
@@ -877,7 +877,6 @@ public static bool FindHotKey (ustring text, Rune hotKeySpecifier, bool firstUpp
i++;
}
-
// Legacy support - use first upper case char if the specifier was not found
if (hot_pos == -1 && firstUpperCase) {
i = 0;
@@ -1106,7 +1105,6 @@ public static bool IsTopToBottom (TextDirection textDirection)
///
public bool WordWrap { get; set; } = false;
-
///
/// Gets or sets the size of the area the text will be constrained to when formatted.
///
diff --git a/Terminal.Gui/TimeoutEventArgs.cs b/Terminal.Gui/TimeoutEventArgs.cs
index c488bdc114..876dde6782 100644
--- a/Terminal.Gui/TimeoutEventArgs.cs
+++ b/Terminal.Gui/TimeoutEventArgs.cs
@@ -17,7 +17,6 @@ public class TimeoutEventArgs : EventArgs {
///
public long Ticks { get; }
-
///
/// Creates a new instance of the class.
///
diff --git a/Terminal.Gui/Types/PointF.cs b/Terminal.Gui/Types/PointF.cs
index be874231b5..c1927df125 100644
--- a/Terminal.Gui/Types/PointF.cs
+++ b/Terminal.Gui/Types/PointF.cs
@@ -104,7 +104,6 @@ public float Y {
///
public static PointF Subtract (PointF pt, SizeF sz) => new PointF (pt.X - sz.Width, pt.Y - sz.Height);
-
///
/// Compares two objects. The result specifies whether the values of the
/// and properties of the two
@@ -112,7 +111,6 @@ public float Y {
///
public override bool Equals (object obj) => obj is PointF && Equals ((PointF)obj);
-
///
/// Compares two objects. The result specifies whether the values of the
/// and properties of the two
diff --git a/Terminal.Gui/Types/Rect.cs b/Terminal.Gui/Types/Rect.cs
index 8d3ed16f61..9a1e62fc96 100644
--- a/Terminal.Gui/Types/Rect.cs
+++ b/Terminal.Gui/Types/Rect.cs
@@ -261,7 +261,6 @@ public Rect (int x, int y, int width, int height)
}
-
///
/// Bottom Property
///
diff --git a/Terminal.Gui/Types/SizeF.cs b/Terminal.Gui/Types/SizeF.cs
index 226dadc0dc..155f6793bc 100644
--- a/Terminal.Gui/Types/SizeF.cs
+++ b/Terminal.Gui/Types/SizeF.cs
@@ -136,7 +136,6 @@ public float Height {
///
public override bool Equals (object obj) => obj is SizeF && Equals ((SizeF)obj);
-
///
/// Tests whether two objects are identical.
///
diff --git a/Terminal.Gui/View/Border.cs b/Terminal.Gui/View/Border.cs
deleted file mode 100644
index a68a48e271..0000000000
--- a/Terminal.Gui/View/Border.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using NStack;
-using System;
-using System.Text.Json.Serialization;
-using System.Data;
-using System.Text;
-using System.Collections.Generic;
-
-namespace Terminal.Gui {
-
-}
\ No newline at end of file
diff --git a/Terminal.Gui/View/Frame.cs b/Terminal.Gui/View/Frame.cs
index 508160eaf2..c6e7d43cc1 100644
--- a/Terminal.Gui/View/Frame.cs
+++ b/Terminal.Gui/View/Frame.cs
@@ -12,7 +12,6 @@ namespace Terminal.Gui {
// TODO: v2 - If a Frame has focus, navigation keys (e.g Command.NextView) should cycle through SubViews of the Frame
// QUESTION: How does a user navigate out of a Frame to another Frame, or back into the Parent's SubViews?
-
///
/// Frames are a special form of that act as adornments; they appear outside of the
/// enabling borders, menus, etc...
@@ -184,7 +183,6 @@ public override void Redraw (Rect bounds)
sideLineLength++;
}
-
if (Id == "Border" && Thickness.Top > 0 && maxTitleWidth > 0 && !ustring.IsNullOrEmpty (Parent?.Title)) {
var prevAttr = Driver.GetAttribute ();
Driver.SetAttribute (Parent.HasFocus ? Parent.GetHotNormalColor () : Parent.GetNormalColor ());
@@ -278,7 +276,6 @@ public override void Redraw (Rect bounds)
}
}
-
Driver.Clip = prevClip;
}
diff --git a/Terminal.Gui/View/Layout/ResizedEventArgs.cs b/Terminal.Gui/View/Layout/ResizedEventArgs.cs
index a4cc81a0ba..cf3505840f 100644
--- a/Terminal.Gui/View/Layout/ResizedEventArgs.cs
+++ b/Terminal.Gui/View/Layout/ResizedEventArgs.cs
@@ -17,7 +17,7 @@
namespace Terminal.Gui {
///
- /// Event arguments for the event.
+ /// Event arguments for the event.
///
public class ResizedEventArgs : EventArgs {
///
diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs
index 4ee7e0298e..7e662a72fe 100644
--- a/Terminal.Gui/View/View.cs
+++ b/Terminal.Gui/View/View.cs
@@ -1,32 +1,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
-using System.Diagnostics;
using System.Linq;
-using System.Reflection;
using NStack;
-
namespace Terminal.Gui {
- ///
- /// Determines the LayoutStyle for a , if Absolute, during , the
- /// value from the will be used, if the value is Computed, then
- /// will be updated from the X, Y objects and the Width and Height objects.
- ///
- public enum LayoutStyle {
- ///
- /// The position and size of the view are based .
- ///
- Absolute,
-
- ///
- /// The position and size of the view will be computed based on
- /// , , , and . will
- /// provide the absolute computed values.
- ///
- Computed
- }
-
+ #region API Docs
///
/// View is the base class for all views on the screen and represents a visible element that can render itself and
/// contains zero or more nested views, called SubViews.
@@ -37,6 +16,28 @@ public enum LayoutStyle {
/// can contain one or more subviews, can respond to user input and render themselves on the screen.
///
///
+ /// SubView - A View that is contained in another view and will be rendered as part of the containing view's ContentArea.
+ /// SubViews are added to another view via the ` method. A View may only be a SubView of a single View.
+ ///
+ ///
+ /// SuperView - The View that is a container for SubViews.
+ ///
+ ///
+ /// Focus is a concept that is used to describe which Responder is currently receiving user input. Only views that are
+ /// , , and will receive focus.
+ ///
+ ///
+ /// Views that are focusable should implement the to make sure that
+ /// the cursor is placed in a location that makes sense. Unix terminals do not have
+ /// a way of hiding the cursor, so it can be distracting to have the cursor left at
+ /// the last focused view. So views should make sure that they place the cursor
+ /// in a visually sensible place.
+ ///
+ ///
+ /// The View defines the base functionality for user interface elements in Terminal.Gui. Views
+ /// can contain one or more subviews, can respond to user input and render themselves on the screen.
+ ///
+ ///
/// Views supports two layout styles: or .
/// The choice as to which layout style is used by the View
/// is determined when the View is initialized. To create a View using Absolute layout, call a constructor that takes a
@@ -73,10 +74,16 @@ public enum LayoutStyle {
/// To flag the entire view for redraw call .
///
///
+ /// The method is invoked when the size or layout of a view has
+ /// changed. The default processing system will keep the size and dimensions
+ /// for views that use the , and will recompute the
+ /// frames for the vies that use .
+ ///
+ ///
/// Views have a property that defines the default colors that subviews
/// should use for rendering. This ensures that the views fit in the context where
/// they are being used, and allows for themes to be plugged in. For example, the
- /// default colors for windows and toplevels uses a blue background, while it uses
+ /// default colors for windows and Toplevels uses a blue background, while it uses
/// a white background for dialog boxes and a red background for errors.
///
///
@@ -90,19 +97,6 @@ public enum LayoutStyle {
/// in color as well as black and white displays.
///
///
- /// Views that are focusable should implement the to make sure that
- /// the cursor is placed in a location that makes sense. Unix terminals do not have
- /// a way of hiding the cursor, so it can be distracting to have the cursor left at
- /// the last focused view. So views should make sure that they place the cursor
- /// in a visually sensible place.
- ///
- ///
- /// The method is invoked when the size or layout of a view has
- /// changed. The default processing system will keep the size and dimensions
- /// for views that use the , and will recompute the
- /// frames for the vies that use .
- ///
- ///
/// Views can also opt-in to more sophisticated initialization
/// by implementing overrides to and
/// which will be called
@@ -117,135 +111,215 @@ public enum LayoutStyle {
/// instead of on every run.
///
///
+ #endregion API Docs
public partial class View : Responder, ISupportInitializeNotification {
- internal enum Direction {
- Forward,
- Backward
- }
- View _superView = null;
- View _focused = null;
- Direction _focusDirection;
- bool _autoSize;
- ShortcutHelper _shortcutHelper;
+ #region Constructors and Initialization
///
- /// Event fired when this view is added to another.
+ /// Initializes a new instance of a class with the absolute
+ /// dimensions specified in the parameter.
///
- public event EventHandler Added;
+ /// The region covered by this view.
+ ///
+ /// This constructor initialize a View with a of .
+ /// Use to initialize a View with of
+ ///
+ public View (Rect frame) : this (frame, null) { }
///
- /// Event fired when this view is removed from another.
+ /// Initializes a new instance of using layout.
///
- public event EventHandler Removed;
+ ///
+ ///
+ /// Use , , , and properties to dynamically control the size and location of the view.
+ /// The will be created using
+ /// coordinates. The initial size () will be
+ /// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
+ ///
+ ///
+ /// If is greater than one, word wrapping is provided.
+ ///
+ ///
+ /// This constructor initialize a View with a of .
+ /// Use , , , and properties to dynamically control the size and location of the view.
+ ///
+ ///
+ public View () : this (text: string.Empty, direction: TextDirection.LeftRight_TopBottom) { }
///
- /// Event fired when the view gets focus.
+ /// Initializes a new instance of using layout.
///
- public event EventHandler Enter;
+ ///
+ ///
+ /// The will be created at the given
+ /// coordinates with the given string. The size () will be
+ /// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
+ ///
+ ///
+ /// No line wrapping is provided.
+ ///
+ ///
+ /// column to locate the View.
+ /// row to locate the View.
+ /// text to initialize the property with.
+ public View (int x, int y, ustring text) : this (TextFormatter.CalcRect (x, y, text), text) { }
///
- /// Event fired when the view looses focus.
+ /// Initializes a new instance of using layout.
///
- public event EventHandler Leave;
+ ///
+ ///
+ /// The will be created at the given
+ /// coordinates with the given string. The initial size () will be
+ /// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
+ ///
+ ///
+ /// If rect.Height is greater than one, word wrapping is provided.
+ ///
+ ///
+ /// Location.
+ /// text to initialize the property with.
+ public View (Rect rect, ustring text)
+ {
+ SetInitialProperties (text, rect, LayoutStyle.Absolute, TextDirection.LeftRight_TopBottom);
+ }
///
- /// Event fired when the view receives the mouse event for the first time.
+ /// Initializes a new instance of using layout.
///
- public event EventHandler MouseEnter;
+ ///
+ ///
+ /// The will be created using
+ /// coordinates with the given string. The initial size () will be
+ /// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
+ ///
+ ///
+ /// If is greater than one, word wrapping is provided.
+ ///
+ ///
+ /// text to initialize the property with.
+ /// The text direction.
+ public View (ustring text, TextDirection direction = TextDirection.LeftRight_TopBottom)
+ {
+ SetInitialProperties (text, Rect.Empty, LayoutStyle.Computed, direction);
+ }
+ // TODO: v2 - Remove constructors with parameters
///
- /// Event fired when the view receives a mouse event for the last time.
+ /// Private helper to set the initial properties of the View that were provided via constructors.
///
- public event EventHandler MouseLeave;
+ ///
+ ///
+ ///
+ ///
+ void SetInitialProperties (ustring text, Rect rect, LayoutStyle layoutStyle = LayoutStyle.Computed,
+ TextDirection direction = TextDirection.LeftRight_TopBottom)
+ {
+ TextFormatter = new TextFormatter ();
+ TextFormatter.HotKeyChanged += TextFormatter_HotKeyChanged;
+ TextDirection = direction;
- ///
- /// Event fired when a mouse event is generated.
- ///
- public event EventHandler MouseClick;
+ _shortcutHelper = new ShortcutHelper ();
+ CanFocus = false;
+ TabIndex = -1;
+ TabStop = false;
+ LayoutStyle = layoutStyle;
- ///
- /// Event fired when the value is being changed.
- ///
- public event EventHandler CanFocusChanged;
+ Text = text == null ? ustring.Empty : text;
+ LayoutStyle = layoutStyle;
+ Frame = rect.IsEmpty ? TextFormatter.CalcRect (0, 0, text, direction) : rect;
+ OnResizeNeeded ();
- ///
- /// Event fired when the value is being changed.
- ///
- public event EventHandler EnabledChanged;
+ CreateFrames ();
+
+ LayoutFrames ();
+ }
///
- /// Event fired when the value is being changed.
+ /// Get or sets if the has been initialized (via
+ /// and ).
///
- public event EventHandler VisibleChanged;
+ ///
+ /// If first-run-only initialization is preferred, overrides to
+ /// can be implemented, in which case the
+ /// methods will only be called if
+ /// is . This allows proper inheritance hierarchies
+ /// to override base class layout code optimally by doing so only on first run,
+ /// instead of on every run.
+ ///
+ public virtual bool IsInitialized { get; set; }
///
- /// Event invoked when the is changed.
+ /// Signals the View that initialization is starting. See .
///
- public event EventHandler HotKeyChanged;
+ ///
+ ///
+ /// Views can opt-in to more sophisticated initialization
+ /// by implementing overrides to and
+ /// which will be called
+ /// when the view is added to a .
+ ///
+ ///
+ /// If first-run-only initialization is preferred, overrides to
+ /// can be implemented too, in which case the
+ /// methods will only be called if
+ /// is . This allows proper inheritance hierarchies
+ /// to override base class layout code optimally by doing so only on first run,
+ /// instead of on every run.
+ ///
+ ///
+ public virtual void BeginInit ()
+ {
+ if (!IsInitialized) {
+ _oldCanFocus = CanFocus;
+ _oldTabIndex = _tabIndex;
+
+ UpdateTextDirection (TextDirection);
+ UpdateTextFormatterText ();
+ SetHotKey ();
- Key _hotKey = Key.Null;
+ // TODO: Figure out why ScrollView and other tests fail if this call is put here
+ // instead of the constructor.
+ //InitializeFrames ();
+
+ } else {
+ //throw new InvalidOperationException ("The view is already initialized.");
- ///
- /// Gets or sets the HotKey defined for this view. A user pressing HotKey on the keyboard while this view has focus will cause the Clicked event to fire.
- ///
- public virtual Key HotKey {
- get => _hotKey;
- set {
- if (_hotKey != value) {
- var v = value == Key.Unknown ? Key.Null : value;
- if (_hotKey != Key.Null && ContainsKeyBinding (Key.Space | _hotKey)) {
- if (v == Key.Null) {
- ClearKeybinding (Key.Space | _hotKey);
- } else {
- ReplaceKeyBinding (Key.Space | _hotKey, Key.Space | v);
- }
- } else if (v != Key.Null) {
- AddKeyBinding (Key.Space | v, Command.Accept);
- }
- _hotKey = TextFormatter.HotKey = v;
- }
}
- }
- ///
- /// Gets or sets the specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'.
- ///
- public virtual Rune HotKeySpecifier {
- get {
- if (TextFormatter != null) {
- return TextFormatter.HotKeySpecifier;
- } else {
- return new Rune ('\xFFFF');
+ if (_subviews?.Count > 0) {
+ foreach (var view in _subviews) {
+ if (!view.IsInitialized) {
+ view.BeginInit ();
+ }
}
}
- set {
- TextFormatter.HotKeySpecifier = value;
- SetHotKey ();
- }
}
///
- /// This is the global setting that can be used as a global shortcut to invoke an action if provided.
+ /// Signals the View that initialization is ending. See .
///
- public Key Shortcut {
- get => _shortcutHelper.Shortcut;
- set {
- if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == Key.Null)) {
- _shortcutHelper.Shortcut = value;
+ public void EndInit ()
+ {
+ IsInitialized = true;
+ OnResizeNeeded ();
+ if (_subviews != null) {
+ foreach (var view in _subviews) {
+ if (!view.IsInitialized) {
+ view.EndInit ();
+ }
}
}
+ Initialized?.Invoke (this, EventArgs.Empty);
}
+ #endregion Constructors and Initialization
///
- /// The keystroke combination used in the as string.
- ///
- public ustring ShortcutTag => ShortcutHelper.GetShortcutTag (_shortcutHelper.Shortcut);
-
- ///
- /// The action to run if the is defined.
+ /// Points to the current driver in use by the view, it is a convenience property
+ /// for simplifying the development of new views.
///
- public virtual Action ShortcutAction { get; set; }
+ public static ConsoleDriver Driver => Application.Driver;
///
/// Gets or sets arbitrary data for the view.
@@ -253,327 +327,17 @@ public Key Shortcut {
/// This property is not used internally.
public object Data { get; set; }
- internal Direction FocusDirection {
- get => SuperView?.FocusDirection ?? _focusDirection;
- set {
- if (SuperView != null)
- SuperView.FocusDirection = value;
- else
- _focusDirection = value;
- }
- }
-
- ///
- /// Points to the current driver in use by the view, it is a convenience property
- /// for simplifying the development of new views.
- ///
- public static ConsoleDriver Driver => Application.Driver;
-
- static readonly IList _empty = new List (0).AsReadOnly ();
-
- // This is null, and allocated on demand.
- List _subviews;
-
///
- /// This returns a list of the subviews contained by this view.
+ /// Gets or sets an identifier for the view;
///
- /// The subviews.
- public IList Subviews => _subviews?.AsReadOnly () ?? _empty;
-
- // Internally, we use InternalSubviews rather than subviews, as we do not expect us
- // to make the same mistakes our users make when they poke at the Subviews.
- internal IList InternalSubviews => _subviews ?? _empty;
-
- // This is null, and allocated on demand.
- List _tabIndexes;
+ /// The identifier.
+ /// The id should be unique across all Views that share a SuperView.
+ public string Id { get; set; } = "";
+ ustring _title = ustring.Empty;
///
- /// Configurable keybindings supported by the control
- ///
- private Dictionary KeyBindings { get; set; } = new Dictionary ();
- private Dictionary> CommandImplementations { get; set; } = new Dictionary> ();
-
- ///
- /// This returns a tab index list of the subviews contained by this view.
- ///
- /// The tabIndexes.
- public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty;
-
- int _tabIndex = -1;
-
- ///
- /// Indicates the index of the current from the list.
- ///
- public int TabIndex {
- get { return _tabIndex; }
- set {
- if (!CanFocus) {
- _tabIndex = -1;
- return;
- } else if (SuperView?._tabIndexes == null || SuperView?._tabIndexes.Count == 1) {
- _tabIndex = 0;
- return;
- } else if (_tabIndex == value) {
- return;
- }
- _tabIndex = value > SuperView._tabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 : value < 0 ? 0 : value;
- _tabIndex = GetTabIndex (_tabIndex);
- if (SuperView._tabIndexes.IndexOf (this) != _tabIndex) {
- SuperView._tabIndexes.Remove (this);
- SuperView._tabIndexes.Insert (_tabIndex, this);
- SetTabIndex ();
- }
- }
- }
-
- int GetTabIndex (int idx)
- {
- var i = 0;
- foreach (var v in SuperView._tabIndexes) {
- if (v._tabIndex == -1 || v == this) {
- continue;
- }
- i++;
- }
- return Math.Min (i, idx);
- }
-
- void SetTabIndex ()
- {
- var i = 0;
- foreach (var v in SuperView._tabIndexes) {
- if (v._tabIndex == -1) {
- continue;
- }
- v._tabIndex = i;
- i++;
- }
- }
-
- bool _tabStop = true;
-
- ///
- /// This only be if the is also
- /// and the focus can be avoided by setting this to
- ///
- public bool TabStop {
- get => _tabStop;
- set {
- if (_tabStop == value) {
- return;
- }
- _tabStop = CanFocus && value;
- }
- }
-
- bool _oldCanFocus;
- int _oldTabIndex;
-
- ///
- public override bool CanFocus {
- get => base.CanFocus;
- set {
- if (!_addingView && IsInitialized && SuperView?.CanFocus == false && value) {
- throw new InvalidOperationException ("Cannot set CanFocus to true if the SuperView CanFocus is false!");
- }
- if (base.CanFocus != value) {
- base.CanFocus = value;
-
- switch (value) {
- case false when _tabIndex > -1:
- TabIndex = -1;
- break;
- case true when SuperView?.CanFocus == false && _addingView:
- SuperView.CanFocus = true;
- break;
- }
-
- if (value && _tabIndex == -1) {
- TabIndex = SuperView != null ? SuperView._tabIndexes.IndexOf (this) : -1;
- }
- TabStop = value;
-
- if (!value && SuperView?.Focused == this) {
- SuperView._focused = null;
- }
- if (!value && HasFocus) {
- SetHasFocus (false, this);
- SuperView?.EnsureFocus ();
- if (SuperView != null && SuperView.Focused == null) {
- SuperView.FocusNext ();
- if (SuperView.Focused == null) {
- Application.Current.FocusNext ();
- }
- Application.EnsuresTopOnFront ();
- }
- }
- if (_subviews != null && IsInitialized) {
- foreach (var view in _subviews) {
- if (view.CanFocus != value) {
- if (!value) {
- view._oldCanFocus = view.CanFocus;
- view._oldTabIndex = view._tabIndex;
- view.CanFocus = false;
- view._tabIndex = -1;
- } else {
- if (_addingView) {
- view._addingView = true;
- }
- view.CanFocus = view._oldCanFocus;
- view._tabIndex = view._oldTabIndex;
- view._addingView = false;
- }
- }
- }
- }
- OnCanFocusChanged ();
- SetNeedsDisplay ();
- }
- }
- }
-
- // The frame for the object. Superview relative.
- Rect _frame;
-
- ///
- /// Gets or sets an identifier for the view;
- ///
- /// The identifier.
- /// The id should be unique across all Views that share a SuperView.
- public string Id { get; set; } = "";
-
- ///
- /// Returns a value indicating if this View is currently on Top (Active)
- ///
- public bool IsCurrentTop => Application.Current == this;
-
- ///
- /// Gets or sets a value indicating whether this wants mouse position reports.
- ///
- /// if want mouse position reports; otherwise, .
- public virtual bool WantMousePositionReports { get; set; }
-
- ///
- /// Gets or sets a value indicating whether this want continuous button pressed event.
- ///
- public virtual bool WantContinuousButtonPressed { get; set; }
-
- ///
- /// Gets or sets the frame for the view. The frame is relative to the view's container ().
- ///
- /// The frame.
- ///
- ///
- /// Change the Frame when using the layout style to move or resize views.
- ///
- ///
- /// Altering the Frame of a view will trigger the redrawing of the
- /// view as well as the redrawing of the affected regions of the .
- ///
- ///
- public virtual Rect Frame {
- get => _frame;
- set {
- _frame = new Rect (value.X, value.Y, Math.Max (value.Width, 0), Math.Max (value.Height, 0));
- if (IsInitialized || LayoutStyle == LayoutStyle.Absolute) {
- LayoutFrames ();
- TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
- SetNeedsLayout ();
- SetNeedsDisplay ();
- }
- }
- }
-
- ///
- /// The Thickness that separates a View from other SubViews of the same SuperView.
- /// The Margin is not part of the View's content and is not clipped by the View's Clip Area.
- ///
- public Frame Margin { get; private set; }
-
- ///
- /// Thickness where a visual border (drawn using line-drawing glyphs) and the Title are drawn.
- /// The Border expands inward; in other words if `Border.Thickness.Top == 2` the border and
- /// title will take up the first row and the second row will be filled with spaces.
- /// The Border is not part of the View's content and is not clipped by the View's `ClipArea`.
- ///
- ///
- /// provides a simple helper for turning a simple border frame on or off.
- ///
- public Frame Border { get; private set; }
-
- ///
- /// Means the Thickness inside of an element that offsets the `Content` from the Border.
- /// Padding is `{0, 0, 0, 0}` by default. Padding is not part of the View's content and is not clipped by the View's `ClipArea`.
- ///
- ///
- /// (NOTE: in v1 `Padding` is OUTSIDE of the `Border`).
- ///
- public Frame Padding { get; private set; }
-
- ///
- /// Helper to get the total thickness of the , , and .
- ///
- /// A thickness that describes the sum of the Frames' thicknesses.
- public Thickness GetFramesThickness ()
- {
- var left = Margin.Thickness.Left + Border.Thickness.Left + Padding.Thickness.Left;
- var top = Margin.Thickness.Top + Border.Thickness.Top + Padding.Thickness.Top;
- var right = Margin.Thickness.Right + Border.Thickness.Right + Padding.Thickness.Right;
- var bottom = Margin.Thickness.Bottom + Border.Thickness.Bottom + Padding.Thickness.Bottom;
- return new Thickness (left, top, right, bottom);
- }
-
- ///
- /// Helper to get the X and Y offset of the Bounds from the Frame. This is the sum of the Left and Top properties of
- /// , and .
- ///
- public Point GetBoundsOffset () => new Point (Padding?.Thickness.GetInside (Padding.Frame).X ?? 0, Padding?.Thickness.GetInside (Padding.Frame).Y ?? 0);
-
- ///
- /// Creates the view's objects. This internal method is overridden by Frame to do nothing
- /// to prevent recursion during View construction.
- ///
- internal virtual void CreateFrames ()
- {
- void ThicknessChangedHandler (object sender, EventArgs e)
- {
- LayoutFrames ();
- SetNeedsLayout ();
- SetNeedsDisplay ();
- }
-
- if (Margin != null) {
- Margin.ThicknessChanged -= ThicknessChangedHandler;
- Margin.Dispose ();
- }
- Margin = new Frame () { Id = "Margin", Thickness = new Thickness (0) };
- Margin.ThicknessChanged += ThicknessChangedHandler;
- Margin.Parent = this;
-
- if (Border != null) {
- Border.ThicknessChanged -= ThicknessChangedHandler;
- Border.Dispose ();
- }
- Border = new Frame () { Id = "Border", Thickness = new Thickness (0) };
- Border.ThicknessChanged += ThicknessChangedHandler;
- Border.Parent = this;
-
- // TODO: Create View.AddAdornment
-
- if (Padding != null) {
- Padding.ThicknessChanged -= ThicknessChangedHandler;
- Padding.Dispose ();
- }
- Padding = new Frame () { Id = "Padding", Thickness = new Thickness (0) };
- Padding.ThicknessChanged += ThicknessChangedHandler;
- Padding.Parent = this;
- }
-
- ustring _title = ustring.Empty;
-
- ///
- /// The title to be displayed for this . The title will be displayed if .
- /// is greater than 0.
+ /// The title to be displayed for this . The title will be displayed if .
+ /// is greater than 0.
///
/// The title.
public ustring Title {
@@ -628,2710 +392,105 @@ public virtual void OnTitleChanged (ustring oldTitle, ustring newTitle)
///
public event EventHandler TitleChanged;
-
- LayoutStyle _layoutStyle;
-
- ///
- /// Controls how the View's is computed during the LayoutSubviews method, if the style is set to
- /// ,
- /// LayoutSubviews does not change the . If the style is
- /// the is updated using
- /// the , , , and properties.
- ///
- /// The layout style.
- public LayoutStyle LayoutStyle {
- get => _layoutStyle;
- set {
- _layoutStyle = value;
- SetNeedsLayout ();
- }
- }
-
///
- /// The View-relative rectangle where View content is displayed. SubViews are positioned relative to
- /// Bounds.Location (which is always (0, 0)) and clips drawing to
- /// Bounds.Size.
+ /// Event fired when the value is being changed.
///
- /// The bounds.
- ///
- ///
- /// The of Bounds is always (0, 0). To obtain the offset of the Bounds from the Frame use
- /// .
- ///
- ///
- public virtual Rect Bounds {
- get {
-#if DEBUG
- if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) {
- Debug.WriteLine ($"WARNING: Bounds is being accessed before the View has been initialized. This is likely a bug. View: {this}");
- }
-#endif // DEBUG
- var frameRelativeBounds = Padding?.Thickness.GetInside (Padding.Frame) ?? new Rect (default, Frame.Size);
- return new Rect (default, frameRelativeBounds.Size);
- }
- set {
- // BUGBUG: Margin etc.. can be null (if typeof(Frame))
- Frame = new Rect (Frame.Location,
- new Size (
- value.Size.Width + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal,
- value.Size.Height + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical
- )
- );
- }
- }
-
- // Diagnostics to highlight when X or Y is read before the view has been initialized
- private Pos VerifyIsIntialized (Pos pos)
- {
-#if DEBUG
- if (LayoutStyle == LayoutStyle.Computed && (!IsInitialized)) {
- Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; position is indeterminate {pos}. This is likely a bug.");
- }
-#endif // DEBUG
- return pos;
- }
+ public event EventHandler EnabledChanged;
- // Diagnostics to highlight when Width or Height is read before the view has been initialized
- private Dim VerifyIsIntialized (Dim dim)
- {
-#if DEBUG
- if (LayoutStyle == LayoutStyle.Computed && (!IsInitialized)) {
- Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; dimension is indeterminate: {dim}. This is likely a bug.");
- }
-#endif // DEBUG
- return dim;
- }
+ ///
+ public override void OnEnabledChanged () => EnabledChanged?.Invoke (this, EventArgs.Empty);
- Pos _x, _y;
+ bool _oldEnabled;
- ///
- /// Gets or sets the X position for the view (the column). Only used if the is .
- ///
- /// The X Position.
- ///
- /// If is changing this property has no effect and its value is indeterminate.
- ///
- public Pos X {
- get => VerifyIsIntialized (_x);
+ ///
+ public override bool Enabled {
+ get => base.Enabled;
set {
- if (ForceValidatePosDim && !ValidatePosDim (_x, value)) {
- throw new ArgumentException ();
- }
-
- _x = value;
-
- OnResizeNeeded ();
- }
- }
+ if (base.Enabled != value) {
+ if (value) {
+ if (SuperView == null || SuperView?.Enabled == true) {
+ base.Enabled = value;
+ }
+ } else {
+ base.Enabled = value;
+ }
+ if (!value && HasFocus) {
+ SetHasFocus (false, this);
+ }
+ OnEnabledChanged ();
+ SetNeedsDisplay ();
- ///
- /// Gets or sets the Y position for the view (the row). Only used if the is .
- ///
- /// The y position (line).
- ///
- /// If is changing this property has no effect and its value is indeterminate.
- ///
- public Pos Y {
- get => VerifyIsIntialized (_y);
- set {
- if (ForceValidatePosDim && !ValidatePosDim (_y, value)) {
- throw new ArgumentException ();
+ if (_subviews != null) {
+ foreach (var view in _subviews) {
+ if (!value) {
+ view._oldEnabled = view.Enabled;
+ view.Enabled = false;
+ } else {
+ view.Enabled = view._oldEnabled;
+ view._addingView = false;
+ }
+ }
+ }
}
-
- _y = value;
-
- OnResizeNeeded ();
}
}
- Dim _width, _height;
-
+
///
- /// Gets or sets the width of the view. Only used the is .
+ /// Event fired when the value is being changed.
///
- /// The width.
- ///
- /// If is changing this property has no effect and its value is indeterminate.
- ///
- public Dim Width {
- get => VerifyIsIntialized (_width);
- set {
- if (ForceValidatePosDim && !ValidatePosDim (_width, value)) {
- throw new ArgumentException ("ForceValidatePosDim is enabled", nameof (Width));
- }
-
- _width = value;
-
- if (ForceValidatePosDim) {
- var isValidNewAutSize = AutoSize && IsValidAutoSizeWidth (_width);
+ public event EventHandler VisibleChanged;
- if (IsAdded && AutoSize && !isValidNewAutSize) {
- throw new InvalidOperationException ("Must set AutoSize to false before set the Width.");
- }
- }
- OnResizeNeeded ();
- }
- }
+ ///
+ public override void OnVisibleChanged () => VisibleChanged?.Invoke (this, EventArgs.Empty);
///
- /// Gets or sets the height of the view. Only used the is .
+ /// Gets or sets whether a view is cleared if the property is .
///
- /// The height.
- /// If is changing this property has no effect and its value is indeterminate.
- public Dim Height {
- get => VerifyIsIntialized (_height);
- set {
- if (ForceValidatePosDim && !ValidatePosDim (_height, value)) {
- throw new ArgumentException ("ForceValidatePosDim is enabled", nameof (Height));
- }
-
- _height = value;
-
- if (ForceValidatePosDim) {
- var isValidNewAutSize = AutoSize && IsValidAutoSizeHeight (_height);
+ public bool ClearOnVisibleFalse { get; set; } = true;
- if (IsAdded && AutoSize && !isValidNewAutSize) {
- throw new InvalidOperationException ("Must set AutoSize to false before set the Height.");
+ /// >
+ public override bool Visible {
+ get => base.Visible;
+ set {
+ if (base.Visible != value) {
+ base.Visible = value;
+ if (!value) {
+ if (HasFocus) {
+ SetHasFocus (false, this);
+ }
+ if (ClearOnVisibleFalse) {
+ Clear ();
+ }
}
+ OnVisibleChanged ();
+ SetNeedsDisplay ();
}
- OnResizeNeeded ();
}
}
- ///
- /// Forces validation with layout
- /// to avoid breaking the and settings.
- ///
- public bool ForceValidatePosDim { get; set; }
-
- bool ValidatePosDim (object oldValue, object newValue)
+ bool CanBeVisible (View view)
{
- if (!IsInitialized || _layoutStyle == LayoutStyle.Absolute || oldValue == null || oldValue.GetType () == newValue.GetType () || this is Toplevel) {
- return true;
+ if (!view.Visible) {
+ return false;
}
- if (_layoutStyle == LayoutStyle.Computed) {
- if (oldValue.GetType () != newValue.GetType () && !(newValue is Pos.PosAbsolute || newValue is Dim.DimAbsolute)) {
- return true;
+ for (var c = view.SuperView; c != null; c = c.SuperView) {
+ if (!c.Visible) {
+ return false;
}
}
- return false;
- }
- // BUGBUG: This API is broken - It should be renamed to `GetMinimumBoundsForFrame and
- // should not assume Frame.Height == Bounds.Height
+ return true;
+ }
+
///
- /// Gets the minimum dimensions required to fit the View's , factoring in .
+ /// Pretty prints the View
///
- /// The minimum dimensions required.
- /// if the dimensions fit within the View's , otherwise.
- ///
- /// Always returns if is or
- /// if (Horizontal) or (Vertical) are not not set or zero.
- /// Does not take into account word wrapping.
- ///
- public bool GetMinimumBounds (out Size size)
- {
- size = Bounds.Size;
-
- if (!AutoSize && !ustring.IsNullOrEmpty (TextFormatter.Text)) {
- switch (TextFormatter.IsVerticalDirection (TextDirection)) {
- case true:
- var colWidth = TextFormatter.GetSumMaxCharWidth (new List { TextFormatter.Text }, 0, 1);
- // TODO: v2 - This uses frame.Width; it should only use Bounds
- if (_frame.Width < colWidth &&
- (Width == null ||
- (Bounds.Width >= 0 &&
- Width is Dim.DimAbsolute &&
- Width.Anchor (0) >= 0 &&
- Width.Anchor (0) < colWidth))) {
- size = new Size (colWidth, Bounds.Height);
- return true;
- }
- break;
- default:
- if (_frame.Height < 1 &&
- (Height == null ||
- (Height is Dim.DimAbsolute &&
- Height.Anchor (0) == 0))) {
- size = new Size (Bounds.Width, 1);
- return true;
- }
- break;
- }
- }
- return false;
- }
-
- // BUGBUG - v2 - Should be renamed "SetBoundsToFitFrame"
- ///
- /// Sets the size of the View to the minimum width or height required to fit (see .
- ///
- /// if the size was changed, if
- /// will not fit.
- public bool SetMinWidthHeight ()
- {
- if (GetMinimumBounds (out Size size)) {
- _frame = new Rect (_frame.Location, size);
- return true;
- }
- return false;
- }
-
- ///
- /// Gets or sets the used to format .
- ///
- public TextFormatter TextFormatter { get; set; }
-
- ///
- /// Returns the container for this view, or null if this view has not been added to a container.
- ///
- /// The super view.
- public virtual View SuperView {
- get {
- return _superView;
- }
- set {
- throw new NotImplementedException ();
- }
- }
-
- ///
- /// Initializes a new instance of a class with the absolute
- /// dimensions specified in the parameter.
- ///
- /// The region covered by this view.
- ///
- /// This constructor initialize a View with a of .
- /// Use to initialize a View with of
- ///
- public View (Rect frame) : this (frame, null) { }
-
- ///
- /// Initializes a new instance of using layout.
- ///
- ///
- ///
- /// Use , , , and properties to dynamically control the size and location of the view.
- /// The will be created using
- /// coordinates. The initial size () will be
- /// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
- ///
- ///
- /// If is greater than one, word wrapping is provided.
- ///
- ///
- /// This constructor initialize a View with a of .
- /// Use , , , and properties to dynamically control the size and location of the view.
- ///
- ///
- public View () : this (text: string.Empty, direction: TextDirection.LeftRight_TopBottom) { }
-
- ///
- /// Initializes a new instance of using layout.
- ///
- ///
- ///
- /// The will be created at the given
- /// coordinates with the given string. The size () will be
- /// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
- ///
- ///
- /// No line wrapping is provided.
- ///
- ///
- /// column to locate the View.
- /// row to locate the View.
- /// text to initialize the property with.
- public View (int x, int y, ustring text) : this (TextFormatter.CalcRect (x, y, text), text) { }
-
- ///
- /// Initializes a new instance of using layout.
- ///
- ///
- ///
- /// The will be created at the given
- /// coordinates with the given string. The initial size () will be
- /// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
- ///
- ///
- /// If rect.Height is greater than one, word wrapping is provided.
- ///
- ///
- /// Location.
- /// text to initialize the property with.
- public View (Rect rect, ustring text)
- {
- SetInitialProperties (text, rect, LayoutStyle.Absolute, TextDirection.LeftRight_TopBottom);
- }
-
- ///
- /// Initializes a new instance of using layout.
- ///
- ///
- ///
- /// The will be created using
- /// coordinates with the given string. The initial size () will be
- /// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
- ///
- ///
- /// If is greater than one, word wrapping is provided.
- ///
- ///
- /// text to initialize the property with.
- /// The text direction.
- public View (ustring text, TextDirection direction = TextDirection.LeftRight_TopBottom)
- {
- SetInitialProperties (text, Rect.Empty, LayoutStyle.Computed, direction);
- }
-
- // TODO: v2 - Remove constructors with parameters
- ///
- /// Private helper to set the initial properties of the View that were provided via constructors.
- ///
- ///
- ///
- ///
- ///
- void SetInitialProperties (ustring text, Rect rect, LayoutStyle layoutStyle = LayoutStyle.Computed,
- TextDirection direction = TextDirection.LeftRight_TopBottom)
- {
- TextFormatter = new TextFormatter ();
- TextFormatter.HotKeyChanged += TextFormatter_HotKeyChanged;
- TextDirection = direction;
-
- _shortcutHelper = new ShortcutHelper ();
- CanFocus = false;
- TabIndex = -1;
- TabStop = false;
- LayoutStyle = layoutStyle;
-
- Text = text == null ? ustring.Empty : text;
- LayoutStyle = layoutStyle;
- Frame = rect.IsEmpty ? TextFormatter.CalcRect (0, 0, text, direction) : rect;
- OnResizeNeeded ();
-
- CreateFrames ();
-
- LayoutFrames ();
- }
-
- ///
- /// Can be overridden if the has
- /// different format than the default.
- ///
- protected virtual void UpdateTextFormatterText ()
- {
- if (TextFormatter != null) {
- TextFormatter.Text = _text;
- }
- }
-
- ///
- /// Called whenever the view needs to be resized. Sets and
- /// triggers a call. ///
- ///
- ///
- /// Can be overridden if the view resize behavior is different than the default.
- ///
- protected virtual void OnResizeNeeded ()
- {
- var actX = _x is Pos.PosAbsolute ? _x.Anchor (0) : _frame.X;
- var actY = _y is Pos.PosAbsolute ? _y.Anchor (0) : _frame.Y;
-
- if (AutoSize) {
- //if (TextAlignment == TextAlignment.Justified) {
- // throw new InvalidOperationException ("TextAlignment.Justified cannot be used with AutoSize");
- //}
- var s = GetAutoSize ();
- var w = _width is Dim.DimAbsolute && _width.Anchor (0) > s.Width ? _width.Anchor (0) : s.Width;
- var h = _height is Dim.DimAbsolute && _height.Anchor (0) > s.Height ? _height.Anchor (0) : s.Height;
- _frame = new Rect (new Point (actX, actY), new Size (w, h)); // Set frame, not Frame!
- } else {
- var w = _width is Dim.DimAbsolute ? _width.Anchor (0) : _frame.Width;
- var h = _height is Dim.DimAbsolute ? _height.Anchor (0) : _frame.Height;
- // BUGBUG: v2 - ? - If layoutstyle is absolute, this overwrites the current frame h/w with 0. Hmmm...
- // This is needed for DimAbsolute values by setting the frame before LayoutSubViews.
- _frame = new Rect (new Point (actX, actY), new Size (w, h)); // Set frame, not Frame!
- }
- //// BUGBUG: I think these calls are redundant or should be moved into just the AutoSize case
- if (IsInitialized || LayoutStyle == LayoutStyle.Absolute) {
- SetMinWidthHeight ();
- LayoutFrames ();
- TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
- SetNeedsLayout ();
- SetNeedsDisplay ();
- }
- }
-
- void TextFormatter_HotKeyChanged (object sender, KeyChangedEventArgs e)
- {
- HotKeyChanged?.Invoke (this, e);
- }
-
- internal bool LayoutNeeded { get; private set; } = true;
-
- internal void SetNeedsLayout ()
- {
- if (LayoutNeeded)
- return;
- LayoutNeeded = true;
- if (SuperView == null)
- return;
- SuperView.SetNeedsLayout ();
- foreach (var view in Subviews) {
- view.SetNeedsLayout ();
- }
- TextFormatter.NeedsFormat = true;
- }
-
- ///
- /// Removes the setting on this view.
- ///
- protected void ClearLayoutNeeded ()
- {
- LayoutNeeded = false;
- }
-
- // The view-relative region that needs to be redrawn
- internal Rect _needsDisplay { get; private set; } = Rect.Empty;
-
- ///
- /// Sets a flag indicating this view needs to be redisplayed because its state has changed.
- ///
- public void SetNeedsDisplay ()
- {
- if (!IsInitialized) return;
- SetNeedsDisplay (Bounds);
- }
-
- ///
- /// Flags the view-relative region on this View as needing to be redrawn.
- ///
- /// The view-relative region that needs to be redrawn.
- public void SetNeedsDisplay (Rect region)
- {
- if (_needsDisplay.IsEmpty)
- _needsDisplay = region;
- else {
- var x = Math.Min (_needsDisplay.X, region.X);
- var y = Math.Min (_needsDisplay.Y, region.Y);
- var w = Math.Max (_needsDisplay.Width, region.Width);
- var h = Math.Max (_needsDisplay.Height, region.Height);
- _needsDisplay = new Rect (x, y, w, h);
- }
- _superView?.SetSubViewNeedsDisplay ();
-
- if (_subviews == null)
- return;
-
- foreach (var view in _subviews)
- if (view.Frame.IntersectsWith (region)) {
- var childRegion = Rect.Intersect (view.Frame, region);
- childRegion.X -= view.Frame.X;
- childRegion.Y -= view.Frame.Y;
- view.SetNeedsDisplay (childRegion);
- }
- }
-
- private Rect GetNeedsDisplayRectScreen (Rect containerBounds)
- {
- Rect rect = ViewToScreen (_needsDisplay);
- if (!containerBounds.IsEmpty) {
- rect.Width = Math.Min (_needsDisplay.Width, containerBounds.Width);
- rect.Height = Math.Min (_needsDisplay.Height, containerBounds.Height);
- }
-
- return rect;
- }
-
-
- internal bool _childNeedsDisplay { get; private set; }
-
- ///
- /// Indicates that any Subviews (in the list) need to be repainted.
- ///
- public void SetSubViewNeedsDisplay ()
- {
- if (_childNeedsDisplay) {
- return;
- }
- _childNeedsDisplay = true;
- if (_superView != null && !_superView._childNeedsDisplay)
- _superView.SetSubViewNeedsDisplay ();
- }
-
- internal bool _addingView;
-
- ///
- /// Adds a subview (child) to this view.
- ///
- ///
- /// The Views that have been added to this view can be retrieved via the property.
- /// See also
- ///
- public virtual void Add (View view)
- {
- if (view == null) {
- return;
- }
- if (_subviews == null) {
- _subviews = new List ();
- }
- if (_tabIndexes == null) {
- _tabIndexes = new List ();
- }
- _subviews.Add (view);
- _tabIndexes.Add (view);
- view._superView = this;
- if (view.CanFocus) {
- _addingView = true;
- if (SuperView?.CanFocus == false) {
- SuperView._addingView = true;
- SuperView.CanFocus = true;
- SuperView._addingView = false;
- }
- CanFocus = true;
- view._tabIndex = _tabIndexes.IndexOf (view);
- _addingView = false;
- }
- if (view.Enabled && !Enabled) {
- view._oldEnabled = true;
- view.Enabled = false;
- }
- SetNeedsLayout ();
- SetNeedsDisplay ();
-
-
- OnAdded (new SuperViewChangedEventArgs (this, view));
- if (IsInitialized && !view.IsInitialized) {
- view.BeginInit ();
- view.EndInit ();
- }
- }
-
- ///
- /// Adds the specified views (children) to the view.
- ///
- /// Array of one or more views (can be optional parameter).
- ///
- /// The Views that have been added to this view can be retrieved via the property.
- /// See also
- ///
- public void Add (params View [] views)
- {
- if (views == null) {
- return;
- }
- foreach (var view in views) {
- Add (view);
- }
- }
-
- ///
- /// Removes all subviews (children) added via or from this View.
- ///
- public virtual void RemoveAll ()
- {
- if (_subviews == null) {
- return;
- }
-
- while (_subviews.Count > 0) {
- Remove (_subviews [0]);
- }
- }
-
- ///
- /// Removes a subview added via or from this View.
- ///
- ///
- ///
- public virtual void Remove (View view)
- {
- if (view == null || _subviews == null) return;
-
- var touched = view.Frame;
- _subviews.Remove (view);
- _tabIndexes.Remove (view);
- view._superView = null;
- view._tabIndex = -1;
- SetNeedsLayout ();
- SetNeedsDisplay ();
-
- foreach (var v in _subviews) {
- if (v.Frame.IntersectsWith (touched))
- view.SetNeedsDisplay ();
- }
- OnRemoved (new SuperViewChangedEventArgs (this, view));
- if (_focused == view) {
- _focused = null;
- }
- }
-
- void PerformActionForSubview (View subview, Action action)
- {
- if (_subviews.Contains (subview)) {
- action (subview);
- }
-
- SetNeedsDisplay ();
- subview.SetNeedsDisplay ();
- }
-
- ///
- /// Brings the specified subview to the front so it is drawn on top of any other views.
- ///
- /// The subview to send to the front
- ///
- /// .
- ///
- public void BringSubviewToFront (View subview)
- {
- PerformActionForSubview (subview, x => {
- _subviews.Remove (x);
- _subviews.Add (x);
- });
- }
-
- ///
- /// Sends the specified subview to the front so it is the first view drawn
- ///
- /// The subview to send to the front
- ///
- /// .
- ///
- public void SendSubviewToBack (View subview)
- {
- PerformActionForSubview (subview, x => {
- _subviews.Remove (x);
- _subviews.Insert (0, subview);
- });
- }
-
- ///
- /// Moves the subview backwards in the hierarchy, only one step
- ///
- /// The subview to send backwards
- ///
- /// If you want to send the view all the way to the back use SendSubviewToBack.
- ///
- public void SendSubviewBackwards (View subview)
- {
- PerformActionForSubview (subview, x => {
- var idx = _subviews.IndexOf (x);
- if (idx > 0) {
- _subviews.Remove (x);
- _subviews.Insert (idx - 1, x);
- }
- });
- }
-
- ///
- /// Moves the subview backwards in the hierarchy, only one step
- ///
- /// The subview to send backwards
- ///
- /// If you want to send the view all the way to the back use SendSubviewToBack.
- ///
- public void BringSubviewForward (View subview)
- {
- PerformActionForSubview (subview, x => {
- var idx = _subviews.IndexOf (x);
- if (idx + 1 < _subviews.Count) {
- _subviews.Remove (x);
- _subviews.Insert (idx + 1, x);
- }
- });
- }
-
- ///
- /// Clears the view region with the current color.
- ///
- ///
- ///
- /// This clears the entire region used by this view.
- ///
- ///
- public void Clear ()
- {
- var h = Frame.Height;
- var w = Frame.Width;
- for (var line = 0; line < h; line++) {
- Move (0, line);
- for (var col = 0; col < w; col++)
- Driver.AddRune (' ');
- }
- }
-
-
- // BUGBUG: Stupid that this takes screen-relative. We should have a tenet that says
- // "View APIs only deal with View-relative coords".
- ///
- /// Clears the specified region with the current color.
- ///
- ///
- ///
- /// The screen-relative region to clear.
- public void Clear (Rect regionScreen)
- {
- var h = regionScreen.Height;
- var w = regionScreen.Width;
- for (var line = regionScreen.Y; line < regionScreen.Y + h; line++) {
- Driver.Move (regionScreen.X, line);
- for (var col = 0; col < w; col++)
- Driver.AddRune (' ');
- }
- }
-
- ///
- /// Converts a point from screen-relative coordinates to view-relative coordinates.
- ///
- /// The mapped point.
- /// X screen-coordinate point.
- /// Y screen-coordinate point.
- public Point ScreenToView (int x, int y)
- {
- if (SuperView == null) {
- return new Point (x - Frame.X, y - _frame.Y);
- } else {
- var parent = SuperView.ScreenToView (x, y);
- return new Point (parent.X - _frame.X, parent.Y - _frame.Y);
- }
- }
-
- ///
- /// Converts a point from screen-relative coordinates to bounds-relative coordinates.
- ///
- /// The mapped point.
- /// X screen-coordinate point.
- /// Y screen-coordinate point.
- public Point ScreenToBounds (int x, int y)
- {
- if (SuperView == null) {
- var boundsOffset = GetBoundsOffset ();
- return new Point (x - Frame.X + boundsOffset.X, y - Frame.Y + boundsOffset.Y);
- } else {
- var parent = SuperView.ScreenToView (x, y);
- return new Point (parent.X - _frame.X, parent.Y - _frame.Y);
- }
- }
-
- ///
- /// Converts a view-relative location to a screen-relative location (col,row). The output is optionally clamped to the screen dimensions.
- ///
- /// View-relative column.
- /// View-relative row.
- /// Absolute column; screen-relative.
- /// Absolute row; screen-relative.
- /// If , and will be clamped to the
- /// screen dimensions (they never be negative and will always be less than to and
- /// , respectively.
- public virtual void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clamped = true)
- {
- var boundsOffset = GetBoundsOffset ();
- rcol = col + Frame.X + boundsOffset.X;
- rrow = row + Frame.Y + boundsOffset.Y;
-
- var super = SuperView;
- while (super != null) {
- boundsOffset = super.GetBoundsOffset ();
- rcol += super.Frame.X + boundsOffset.X;
- rrow += super.Frame.Y + boundsOffset.Y;
- super = super.SuperView;
- }
-
- // The following ensures that the cursor is always in the screen boundaries.
- if (clamped) {
- rrow = Math.Min (rrow, Driver.Rows - 1);
- rcol = Math.Min (rcol, Driver.Cols - 1);
- }
- }
-
- ///
- /// Converts a region in view-relative coordinates to screen-relative coordinates.
- ///
- internal Rect ViewToScreen (Rect region)
- {
- ViewToScreen (region.X, region.Y, out var x, out var y, clamped: false);
- return new Rect (x, y, region.Width, region.Height);
- }
-
- // Clips a rectangle in screen coordinates to the dimensions currently available on the screen
- internal Rect ScreenClip (Rect regionScreen)
- {
- var x = regionScreen.X < 0 ? 0 : regionScreen.X;
- var y = regionScreen.Y < 0 ? 0 : regionScreen.Y;
- var w = regionScreen.X + regionScreen.Width >= Driver.Cols ? Driver.Cols - regionScreen.X : regionScreen.Width;
- var h = regionScreen.Y + regionScreen.Height >= Driver.Rows ? Driver.Rows - regionScreen.Y : regionScreen.Height;
-
- return new Rect (x, y, w, h);
- }
-
- ///
- /// Sets the 's clip region to .
- ///
- /// The current screen-relative clip region, which can be then re-applied by setting .
- ///
- ///
- /// is View-relative.
- ///
- ///
- /// If and do not intersect, the clip region will be set to .
- ///
- ///
- public Rect ClipToBounds ()
- {
- var clip = Bounds;
-
-
- return SetClip (clip);
- }
-
- // BUGBUG: v2 - SetClip should return VIEW-relative so that it can be used to reset it; using Driver.Clip directly should not be necessary.
- ///
- /// Sets the clip region to the specified view-relative region.
- ///
- /// The current screen-relative clip region, which can be then re-applied by setting .
- /// View-relative clip region.
- ///
- /// If and do not intersect, the clip region will be set to .
- ///
- public Rect SetClip (Rect region)
- {
- var previous = Driver.Clip;
- Driver.Clip = Rect.Intersect (previous, ViewToScreen (region));
- return previous;
- }
-
- ///
- /// Utility function to draw strings that contain a hotkey.
- ///
- /// String to display, the hotkey specifier before a letter flags the next letter as the hotkey.
- /// Hot color.
- /// Normal color.
- ///
- /// The hotkey is any character following the hotkey specifier, which is the underscore ('_') character by default.
- /// The hotkey specifier can be changed via
- ///
- public void DrawHotString (ustring text, Attribute hotColor, Attribute normalColor)
- {
- var hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier;
- Application.Driver.SetAttribute (normalColor);
- foreach (var rune in text) {
- if (rune == hotkeySpec) {
- Application.Driver.SetAttribute (hotColor);
- continue;
- }
- Application.Driver.AddRune (rune);
- Application.Driver.SetAttribute (normalColor);
- }
- }
-
- ///
- /// Utility function to draw strings that contains a hotkey using a and the "focused" state.
- ///
- /// String to display, the underscore before a letter flags the next letter as the hotkey.
- /// If set to this uses the focused colors from the color scheme, otherwise the regular ones.
- /// The color scheme to use.
- public void DrawHotString (ustring text, bool focused, ColorScheme scheme)
- {
- if (focused)
- DrawHotString (text, scheme.HotFocus, scheme.Focus);
- else
- DrawHotString (text, Enabled ? scheme.HotNormal : scheme.Disabled, Enabled ? scheme.Normal : scheme.Disabled);
- }
-
- ///
- /// This moves the cursor to the specified column and row in the view.
- ///
- /// The move.
- /// The column to move to, in view-relative coordinates.
- /// the row to move to, in view-relative coordinates.
- /// Whether to clip the result of the ViewToScreen method,
- /// If , the and values are clamped to the screen (terminal) dimensions (0..TerminalDim-1).
- public void Move (int col, int row, bool clipped = true)
- {
- if (Driver.Rows == 0) {
- return;
- }
-
- ViewToScreen (col, row, out var rCol, out var rRow, clipped);
- Driver.Move (rCol, rRow);
- }
-
- ///
- /// Positions the cursor in the right position based on the currently focused view in the chain.
- ///
- /// Views that are focusable should override to ensure
- /// the cursor is placed in a location that makes sense. Unix terminals do not have
- /// a way of hiding the cursor, so it can be distracting to have the cursor left at
- /// the last focused view. Views should make sure that they place the cursor
- /// in a visually sensible place.
- public virtual void PositionCursor ()
- {
- if (!CanBeVisible (this) || !Enabled) {
- return;
- }
-
- // BUGBUG: v2 - This needs to support children of Frames too
-
- if (_focused == null && SuperView != null) {
- SuperView.EnsureFocus ();
- } else if (_focused?.Visible == true && _focused?.Enabled == true && _focused?.Frame.Width > 0 && _focused.Frame.Height > 0) {
- _focused.PositionCursor ();
- } else if (_focused?.Visible == true && _focused?.Enabled == false) {
- _focused = null;
- } else if (CanFocus && HasFocus && Visible && Frame.Width > 0 && Frame.Height > 0) {
- Move (TextFormatter.HotKeyPos == -1 ? 0 : TextFormatter.CursorPosition, 0);
- } else {
- Move (_frame.X, _frame.Y);
- }
- }
-
- // BUGBUG: v2 - Seems weird that this is in View and not Responder.
- bool _hasFocus;
-
- ///
- public override bool HasFocus => _hasFocus;
-
- void SetHasFocus (bool value, View view, bool force = false)
- {
- if (_hasFocus != value || force) {
- _hasFocus = value;
- if (value) {
- OnEnter (view);
- } else {
- OnLeave (view);
- }
- SetNeedsDisplay ();
- }
-
- // Remove focus down the chain of subviews if focus is removed
- if (!value && _focused != null) {
- var f = _focused;
- f.OnLeave (view);
- f.SetHasFocus (false, view);
- _focused = null;
- }
- }
-
- ///
- /// Method invoked when a subview is being added to this view.
- ///
- /// Event where is the subview being added.
- public virtual void OnAdded (SuperViewChangedEventArgs e)
- {
- var view = e.Child;
- view.IsAdded = true;
- view.OnResizeNeeded ();
- view._x ??= view._frame.X;
- view._y ??= view._frame.Y;
- view._width ??= view._frame.Width;
- view._height ??= view._frame.Height;
-
- view.Added?.Invoke (this, e);
- }
-
- ///
- /// Method invoked when a subview is being removed from this view.
- ///
- /// Event args describing the subview being removed.
- public virtual void OnRemoved (SuperViewChangedEventArgs e)
- {
- var view = e.Child;
- view.IsAdded = false;
- view.Removed?.Invoke (this, e);
- }
-
- ///
- public override bool OnEnter (View view)
- {
- var args = new FocusEventArgs (view);
- Enter?.Invoke (this, args);
- if (args.Handled)
- return true;
- if (base.OnEnter (view))
- return true;
-
- return false;
- }
-
- ///
- public override bool OnLeave (View view)
- {
- var args = new FocusEventArgs (view);
- Leave?.Invoke (this, args);
- if (args.Handled)
- return true;
- if (base.OnLeave (view))
- return true;
-
- return false;
- }
-
- ///
- /// Returns the currently focused view inside this view, or null if nothing is focused.
- ///
- /// The focused.
- public View Focused => _focused;
-
- ///
- /// Returns the most focused view in the chain of subviews (the leaf view that has the focus).
- ///
- /// The most focused View.
- public View MostFocused {
- get {
- if (Focused == null)
- return null;
- var most = Focused.MostFocused;
- if (most != null)
- return most;
- return Focused;
- }
- }
-
- ColorScheme _colorScheme;
-
- ///
- /// The color scheme for this view, if it is not defined, it returns the 's
- /// color scheme.
- ///
- public virtual ColorScheme ColorScheme {
- get {
- if (_colorScheme == null) {
- return SuperView?.ColorScheme;
- }
- return _colorScheme;
- }
- set {
- if (_colorScheme != value) {
- _colorScheme = value;
- SetNeedsDisplay ();
- }
- }
- }
-
- ///
- /// Displays the specified character in the specified column and row of the View.
- ///
- /// Column (view-relative).
- /// Row (view-relative).
- /// Ch.
- public void AddRune (int col, int row, Rune ch)
- {
- if (row < 0 || col < 0)
- return;
- if (row > _frame.Height - 1 || col > _frame.Width - 1)
- return;
- Move (col, row);
- Driver.AddRune (ch);
- }
-
- ///
- /// Removes the and the setting on this view.
- ///
- protected void ClearNeedsDisplay ()
- {
- _needsDisplay = Rect.Empty;
- _childNeedsDisplay = false;
- }
-
- ///
- /// The canvas that any line drawing that is to be shared by subviews of this view should add lines to.
- ///
- /// adds border lines to this LineCanvas.
- public virtual LineCanvas LineCanvas { get; set; } = new LineCanvas ();
-
- ///
- /// Gets or sets whether this View will use it's SuperView's for
- /// rendering any border lines. If the rendering of any borders drawn
- /// by this Frame will be done by it's parent's SuperView. If (the default)
- /// this View's method will be called to render the borders.
- ///
- public virtual bool SuperViewRendersLineCanvas { get; set; } = false;
-
- // TODO: Make this cancelable
- ///
- ///
- ///
- ///
- public virtual bool OnDrawFrames ()
- {
- if (!IsInitialized) {
- return false;
- }
-
- var prevClip = Driver.Clip;
- Driver.Clip = ViewToScreen (Frame);
-
- // TODO: Figure out what we should do if we have no superview
- //if (SuperView != null) {
- // TODO: Clipping is disabled for now to ensure we see errors
- Driver.Clip = new Rect (0, 0, Driver.Cols, Driver.Rows);// screenBounds;// SuperView.ClipToBounds ();
- //}
-
- // Each of these renders lines to either this View's LineCanvas
- // Those lines will be finally rendered in OnRenderLineCanvas
- Margin?.Redraw (Margin.Frame);
- Border?.Redraw (Border.Frame);
- Padding?.Redraw (Padding.Frame);
-
- Driver.Clip = prevClip;
-
- return true;
- }
-
- ///
- /// Redraws this view and its subviews; only redraws the views that have been flagged for a re-display.
- ///
- /// The bounds (view-relative region) to redraw.
- ///
- ///
- /// Always use (view-relative) when calling , NOT (superview-relative).
- ///
- ///
- /// Views should set the color that they want to use on entry, as otherwise this will inherit
- /// the last color that was set globally on the driver.
- ///
- ///
- /// Overrides of must ensure they do not set Driver.Clip to a clip region
- /// larger than the parameter, as this will cause the driver to clip the entire region.
- ///
- ///
- public virtual void Redraw (Rect bounds)
- {
- if (!CanBeVisible (this)) {
- return;
- }
-
- OnDrawFrames ();
-
- var prevClip = ClipToBounds ();
-
- if (ColorScheme != null) {
- //Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
- Driver.SetAttribute (GetNormalColor ());
- }
-
- if (SuperView != null) {
- Clear (ViewToScreen (bounds));
- }
-
- // Invoke DrawContentEvent
- OnDrawContent (bounds);
-
- // Draw subviews
- // TODO: Implement OnDrawSubviews (cancelable);
- if (_subviews != null) {
- foreach (var view in _subviews) {
- if (view.Visible) { //!view._needsDisplay.IsEmpty || view._childNeedsDisplay || view.LayoutNeeded) {
- if (true) { //view.Frame.IntersectsWith (bounds)) { // && (view.Frame.IntersectsWith (bounds) || bounds.X < 0 || bounds.Y < 0)) {
- if (view.LayoutNeeded) {
- view.LayoutSubviews ();
- }
-
- // Draw the subview
- // Use the view's bounds (view-relative; Location will always be (0,0)
- //if (view.Visible && view.Frame.Width > 0 && view.Frame.Height > 0) {
- view.Redraw (view.Bounds);
- //}
- }
- view.ClearNeedsDisplay ();
- }
- }
- }
-
- // Invoke DrawContentCompleteEvent
- OnDrawContentComplete (bounds);
- Driver.Clip = prevClip;
-
- OnRenderLineCanvas ();
-
- // BUGBUG: v2 - We should be able to use View.SetClip here and not have to resort to knowing Driver details.
- ClearLayoutNeeded ();
- ClearNeedsDisplay ();
- }
-
- internal void OnRenderLineCanvas ()
- {
- //Driver.SetAttribute (new Attribute(Color.White, Color.Black));
-
- // If we have a SuperView, it'll render our frames.
- if (!SuperViewRendersLineCanvas && LineCanvas.Bounds != Rect.Empty) {
- foreach (var p in LineCanvas.GetCellMap ()) { // Get the entire map
- Driver.SetAttribute (p.Value.Attribute?.Value ?? ColorScheme.Normal);
- Driver.Move (p.Key.X, p.Key.Y);
- Driver.AddRune (p.Value.Rune.Value);
- }
- LineCanvas.Clear ();
- }
-
- if (Subviews.Select (s => s.SuperViewRendersLineCanvas).Count () > 0) {
- foreach (var subview in Subviews.Where (s => s.SuperViewRendersLineCanvas)) {
- // Combine the LineCavas'
- LineCanvas.Merge (subview.LineCanvas);
- subview.LineCanvas.Clear ();
- }
-
- foreach (var p in LineCanvas.GetCellMap ()) { // Get the entire map
- Driver.SetAttribute (p.Value.Attribute?.Value ?? ColorScheme.Normal);
- Driver.Move (p.Key.X, p.Key.Y);
- Driver.AddRune (p.Value.Rune.Value);
- }
- LineCanvas.Clear ();
- }
- }
-
- ///
- /// Event invoked when the content area of the View is to be drawn.
- ///
- ///
- ///
- /// Will be invoked before any subviews added with have been drawn.
- ///
- ///
- /// Rect provides the view-relative rectangle describing the currently visible viewport into the .
- ///
- ///
- public event EventHandler DrawContent;
-
- ///
- /// Enables overrides to draw infinitely scrolled content and/or a background behind added controls.
- ///
- /// The view-relative rectangle describing the currently visible viewport into the
- ///
- /// This method will be called before any subviews added with have been drawn.
- ///
- public virtual void OnDrawContent (Rect contentArea)
- {
- // TODO: Make DrawContent a cancelable event
- // if (!DrawContent?.Invoke(this, new DrawEventArgs (viewport)) {
- DrawContent?.Invoke (this, new DrawEventArgs (contentArea));
-
- if (!ustring.IsNullOrEmpty (TextFormatter.Text)) {
- if (TextFormatter != null) {
- TextFormatter.NeedsFormat = true;
- }
- // This should NOT clear
- TextFormatter?.Draw (ViewToScreen (contentArea), HasFocus ? GetFocusColor () : GetNormalColor (),
- HasFocus ? ColorScheme.HotFocus : GetHotNormalColor (),
- Rect.Empty, false);
- SetSubViewNeedsDisplay ();
- }
- }
-
- ///
- /// Event invoked when the content area of the View is completed drawing.
- ///
- ///
- ///
- /// Will be invoked after any subviews removed with have been completed drawing.
- ///
- ///
- /// Rect provides the view-relative rectangle describing the currently visible viewport into the .
- ///
- ///
- public event EventHandler DrawContentComplete;
-
- ///
- /// Enables overrides after completed drawing infinitely scrolled content and/or a background behind removed controls.
- ///
- /// The view-relative rectangle describing the currently visible viewport into the
- ///
- /// This method will be called after any subviews removed with have been completed drawing.
- ///
- public virtual void OnDrawContentComplete (Rect viewport)
- {
- DrawContentComplete?.Invoke (this, new DrawEventArgs (viewport));
- }
-
- ///
- /// Causes the specified subview to have focus.
- ///
- /// View.
- void SetFocus (View view)
- {
- if (view == null) {
- return;
- }
- //Console.WriteLine ($"Request to focus {view}");
- if (!view.CanFocus || !view.Visible || !view.Enabled) {
- return;
- }
- if (_focused?._hasFocus == true && _focused == view) {
- return;
- }
- if ((_focused?._hasFocus == true && _focused?.SuperView == view) || view == this) {
-
- if (!view._hasFocus) {
- view._hasFocus = true;
- }
- return;
- }
- // Make sure that this view is a subview
- View c;
- for (c = view._superView; c != null; c = c._superView)
- if (c == this)
- break;
- if (c == null)
- throw new ArgumentException ("the specified view is not part of the hierarchy of this view");
-
- if (_focused != null)
- _focused.SetHasFocus (false, view);
-
- var f = _focused;
- _focused = view;
- _focused.SetHasFocus (true, f);
- _focused.EnsureFocus ();
-
- // Send focus upwards
- if (SuperView != null) {
- SuperView.SetFocus (this);
- } else {
- SetFocus (this);
- }
- }
-
- ///
- /// Causes the specified view and the entire parent hierarchy to have the focused order updated.
- ///
- public void SetFocus ()
- {
- if (!CanBeVisible (this) || !Enabled) {
- if (HasFocus) {
- SetHasFocus (false, this);
- }
- return;
- }
-
- if (SuperView != null) {
- SuperView.SetFocus (this);
- } else {
- SetFocus (this);
- }
- }
-
- ///
- /// Invoked when a character key is pressed and occurs after the key up event.
- ///
- public event EventHandler KeyPress;
-
- ///
- public override bool ProcessKey (KeyEvent keyEvent)
- {
- if (!Enabled) {
- return false;
- }
-
- var args = new KeyEventEventArgs (keyEvent);
- KeyPress?.Invoke (this, args);
- if (args.Handled)
- return true;
- if (Focused?.Enabled == true) {
- Focused?.KeyPress?.Invoke (this, args);
- if (args.Handled)
- return true;
- }
-
- return Focused?.Enabled == true && Focused?.ProcessKey (keyEvent) == true;
- }
-
- ///
- /// Invokes any binding that is registered on this
- /// and matches the
- ///
- /// The key event passed.
- protected bool? InvokeKeybindings (KeyEvent keyEvent)
- {
- bool? toReturn = null;
-
- if (KeyBindings.ContainsKey (keyEvent.Key)) {
-
- foreach (var command in KeyBindings [keyEvent.Key]) {
-
- if (!CommandImplementations.ContainsKey (command)) {
- throw new NotSupportedException ($"A KeyBinding was set up for the command {command} ({keyEvent.Key}) but that command is not supported by this View ({GetType ().Name})");
- }
-
- // each command has its own return value
- var thisReturn = CommandImplementations [command] ();
-
- // if we haven't got anything yet, the current command result should be used
- if (toReturn == null) {
- toReturn = thisReturn;
- }
-
- // if ever see a true then that's what we will return
- if (thisReturn ?? false) {
- toReturn = true;
- }
- }
- }
-
- return toReturn;
- }
-
-
- ///
- /// Adds a new key combination that will trigger the given
- /// (if supported by the View - see )
- ///
- /// If the key is already bound to a different it will be
- /// rebound to this one
- /// Commands are only ever applied to the current (i.e. this feature
- /// cannot be used to switch focus to another view and perform multiple commands there)
- ///
- ///
- /// The command(s) to run on the when is pressed.
- /// When specifying multiple commands, all commands will be applied in sequence. The bound strike
- /// will be consumed if any took effect.
- public void AddKeyBinding (Key key, params Command [] command)
- {
- if (command.Length == 0) {
- throw new ArgumentException ("At least one command must be specified", nameof (command));
- }
-
- if (KeyBindings.ContainsKey (key)) {
- KeyBindings [key] = command;
- } else {
- KeyBindings.Add (key, command);
- }
- }
-
- ///
- /// Replaces a key combination already bound to .
- ///
- /// The key to be replaced.
- /// The new key to be used.
- protected void ReplaceKeyBinding (Key fromKey, Key toKey)
- {
- if (KeyBindings.ContainsKey (fromKey)) {
- var value = KeyBindings [fromKey];
- KeyBindings.Remove (fromKey);
- KeyBindings [toKey] = value;
- }
- }
-
- ///
- /// Checks if the key binding already exists.
- ///
- /// The key to check.
- /// If the key already exist, otherwise.
- public bool ContainsKeyBinding (Key key)
- {
- return KeyBindings.ContainsKey (key);
- }
-
- ///
- /// Removes all bound keys from the View and resets the default bindings.
- ///
- public void ClearKeybindings ()
- {
- KeyBindings.Clear ();
- }
-
- ///
- /// Clears the existing keybinding (if any) for the given .
- ///
- ///
- public void ClearKeybinding (Key key)
- {
- KeyBindings.Remove (key);
- }
-
- ///
- /// Removes all key bindings that trigger the given command. Views can have multiple different
- /// keys bound to the same command and this method will clear all of them.
- ///
- ///
- public void ClearKeybinding (params Command [] command)
- {
- foreach (var kvp in KeyBindings.Where (kvp => kvp.Value.SequenceEqual (command)).ToArray ()) {
- KeyBindings.Remove (kvp.Key);
- }
- }
-
- ///
- /// States that the given supports a given
- /// and what to perform to make that command happen
- ///
- /// If the already has an implementation the
- /// will replace the old one
- ///
- /// The command.
- /// The function.
- protected void AddCommand (Command command, Func f)
- {
- // if there is already an implementation of this command
- if (CommandImplementations.ContainsKey (command)) {
- // replace that implementation
- CommandImplementations [command] = f;
- } else {
- // else record how to perform the action (this should be the normal case)
- CommandImplementations.Add (command, f);
- }
- }
-
- ///
- /// Returns all commands that are supported by this .
- ///
- ///
- public IEnumerable GetSupportedCommands ()
- {
- return CommandImplementations.Keys;
- }
-
- ///
- /// Gets the key used by a command.
- ///
- /// The command to search.
- /// The used by a
- public Key GetKeyFromCommand (params Command [] command)
- {
- return KeyBindings.First (kb => kb.Value.SequenceEqual (command)).Key;
- }
-
- ///
- public override bool ProcessHotKey (KeyEvent keyEvent)
- {
- if (!Enabled) {
- return false;
- }
-
- var args = new KeyEventEventArgs (keyEvent);
- if (MostFocused?.Enabled == true) {
- MostFocused?.KeyPress?.Invoke (this, args);
- if (args.Handled)
- return true;
- }
- if (MostFocused?.Enabled == true && MostFocused?.ProcessKey (keyEvent) == true)
- return true;
- if (_subviews == null || _subviews.Count == 0)
- return false;
-
- foreach (var view in _subviews)
- if (view.Enabled && view.ProcessHotKey (keyEvent))
- return true;
- return false;
- }
-
- ///
- public override bool ProcessColdKey (KeyEvent keyEvent)
- {
- if (!Enabled) {
- return false;
- }
-
- var args = new KeyEventEventArgs (keyEvent);
- KeyPress?.Invoke (this, args);
- if (args.Handled)
- return true;
- if (MostFocused?.Enabled == true) {
- MostFocused?.KeyPress?.Invoke (this, args);
- if (args.Handled)
- return true;
- }
- if (MostFocused?.Enabled == true && MostFocused?.ProcessKey (keyEvent) == true)
- return true;
- if (_subviews == null || _subviews.Count == 0)
- return false;
-
- foreach (var view in _subviews)
- if (view.Enabled && view.ProcessColdKey (keyEvent))
- return true;
- return false;
- }
-
- ///
- /// Invoked when a key is pressed.
- ///
- public event EventHandler KeyDown;
-
- ///
- public override bool OnKeyDown (KeyEvent keyEvent)
- {
- if (!Enabled) {
- return false;
- }
-
- var args = new KeyEventEventArgs (keyEvent);
- KeyDown?.Invoke (this, args);
- if (args.Handled) {
- return true;
- }
- if (Focused?.Enabled == true) {
- Focused.KeyDown?.Invoke (this, args);
- if (args.Handled) {
- return true;
- }
- if (Focused?.OnKeyDown (keyEvent) == true) {
- return true;
- }
- }
-
- return false;
- }
-
- ///
- /// Invoked when a key is released.
- ///
- public event EventHandler KeyUp;
-
- ///
- public override bool OnKeyUp (KeyEvent keyEvent)
- {
- if (!Enabled) {
- return false;
- }
-
- var args = new KeyEventEventArgs (keyEvent);
- KeyUp?.Invoke (this, args);
- if (args.Handled) {
- return true;
- }
- if (Focused?.Enabled == true) {
- Focused.KeyUp?.Invoke (this, args);
- if (args.Handled) {
- return true;
- }
- if (Focused?.OnKeyUp (keyEvent) == true) {
- return true;
- }
- }
-
- return false;
- }
-
- ///
- /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, does nothing.
- ///
- public void EnsureFocus ()
- {
- if (_focused == null && _subviews?.Count > 0) {
- if (FocusDirection == Direction.Forward) {
- FocusFirst ();
- } else {
- FocusLast ();
- }
- }
- }
-
- ///
- /// Focuses the first focusable subview if one exists.
- ///
- public void FocusFirst ()
- {
- if (!CanBeVisible (this)) {
- return;
- }
-
- if (_tabIndexes == null) {
- SuperView?.SetFocus (this);
- return;
- }
-
- foreach (var view in _tabIndexes) {
- if (view.CanFocus && view._tabStop && view.Visible && view.Enabled) {
- SetFocus (view);
- return;
- }
- }
- }
-
- ///
- /// Focuses the last focusable subview if one exists.
- ///
- public void FocusLast ()
- {
- if (!CanBeVisible (this)) {
- return;
- }
-
- if (_tabIndexes == null) {
- SuperView?.SetFocus (this);
- return;
- }
-
- for (var i = _tabIndexes.Count; i > 0;) {
- i--;
-
- var v = _tabIndexes [i];
- if (v.CanFocus && v._tabStop && v.Visible && v.Enabled) {
- SetFocus (v);
- return;
- }
- }
- }
-
- ///
- /// Focuses the previous view.
- ///
- /// if previous was focused, otherwise.
- public bool FocusPrev ()
- {
- if (!CanBeVisible (this)) {
- return false;
- }
-
- FocusDirection = Direction.Backward;
- if (_tabIndexes == null || _tabIndexes.Count == 0)
- return false;
-
- if (_focused == null) {
- FocusLast ();
- return _focused != null;
- }
-
- var focusedIdx = -1;
- for (var i = _tabIndexes.Count; i > 0;) {
- i--;
- var w = _tabIndexes [i];
-
- if (w.HasFocus) {
- if (w.FocusPrev ())
- return true;
- focusedIdx = i;
- continue;
- }
- if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled) {
- _focused.SetHasFocus (false, w);
-
- if (w.CanFocus && w._tabStop && w.Visible && w.Enabled)
- w.FocusLast ();
-
- SetFocus (w);
- return true;
- }
- }
- if (_focused != null) {
- _focused.SetHasFocus (false, this);
- _focused = null;
- }
- return false;
- }
-
- ///
- /// Focuses the next view.
- ///
- /// if next was focused, otherwise.
- public bool FocusNext ()
- {
- if (!CanBeVisible (this)) {
- return false;
- }
-
- FocusDirection = Direction.Forward;
- if (_tabIndexes == null || _tabIndexes.Count == 0)
- return false;
-
- if (_focused == null) {
- FocusFirst ();
- return _focused != null;
- }
- var focusedIdx = -1;
- for (var i = 0; i < _tabIndexes.Count; i++) {
- var w = _tabIndexes [i];
-
- if (w.HasFocus) {
- if (w.FocusNext ())
- return true;
- focusedIdx = i;
- continue;
- }
- if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled) {
- _focused.SetHasFocus (false, w);
-
- if (w.CanFocus && w._tabStop && w.Visible && w.Enabled)
- w.FocusFirst ();
-
- SetFocus (w);
- return true;
- }
- }
- if (_focused != null) {
- _focused.SetHasFocus (false, this);
- _focused = null;
- }
- return false;
- }
-
- View GetMostFocused (View view)
- {
- if (view == null) {
- return null;
- }
-
- return view._focused != null ? GetMostFocused (view._focused) : view;
- }
-
- ///
- /// Sets the View's to the frame-relative coordinates if its container. The
- /// container size and location are specified by and are relative to the
- /// View's superview.
- ///
- /// The supserview-relative rectangle describing View's container (nominally the
- /// same as this.SuperView.Frame).
- internal void SetRelativeLayout (Rect superviewFrame)
- {
- int newX, newW, newY, newH;
- var autosize = Size.Empty;
-
- if (AutoSize) {
- // Note this is global to this function and used as such within the local functions defined
- // below. In v2 AutoSize will be re-factored to not need to be dealt with in this function.
- autosize = GetAutoSize ();
- }
-
- // Returns the new dimension (width or height) and location (x or y) for the View given
- // the superview's Frame.X or Frame.Y
- // the superview's width or height
- // the current Pos (View.X or View.Y)
- // the current Dim (View.Width or View.Height)
- (int newLocation, int newDimension) GetNewLocationAndDimension (int superviewLocation, int superviewDimension, Pos pos, Dim dim, int autosizeDimension)
- {
- int newDimension, newLocation;
-
- switch (pos) {
- case Pos.PosCenter:
- if (dim == null) {
- newDimension = AutoSize ? autosizeDimension : superviewDimension;
- } else {
- newDimension = dim.Anchor (superviewDimension);
- newDimension = AutoSize && autosizeDimension > newDimension ? autosizeDimension : newDimension;
- }
- newLocation = pos.Anchor (superviewDimension - newDimension);
- break;
-
- case Pos.PosCombine combine:
- int left, right;
- (left, newDimension) = GetNewLocationAndDimension (superviewLocation, superviewDimension, combine.left, dim, autosizeDimension);
- (right, newDimension) = GetNewLocationAndDimension (superviewLocation, superviewDimension, combine.right, dim, autosizeDimension);
- if (combine.add) {
- newLocation = left + right;
- } else {
- newLocation = left - right;
- }
- newDimension = Math.Max (CalculateNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0);
- break;
-
- case Pos.PosAbsolute:
- case Pos.PosAnchorEnd:
- case Pos.PosFactor:
- case Pos.PosFunc:
- case Pos.PosView:
- default:
- newLocation = pos?.Anchor (superviewDimension) ?? 0;
- newDimension = Math.Max (CalculateNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0);
- break;
- }
- return (newLocation, newDimension);
- }
-
- // Recursively calculates the new dimension (width or height) of the given Dim given:
- // the current location (x or y)
- // the current dimension (width or height)
- int CalculateNewDimension (Dim d, int location, int dimension, int autosize)
- {
- int newDimension;
- switch (d) {
- case null:
- newDimension = AutoSize ? autosize : dimension;
- break;
- case Dim.DimCombine combine:
- int leftNewDim = CalculateNewDimension (combine.left, location, dimension, autosize);
- int rightNewDim = CalculateNewDimension (combine.right, location, dimension, autosize);
- if (combine.add) {
- newDimension = leftNewDim + rightNewDim;
- } else {
- newDimension = leftNewDim - rightNewDim;
- }
- newDimension = AutoSize && autosize > newDimension ? autosize : newDimension;
- break;
-
- case Dim.DimFactor factor when !factor.IsFromRemaining ():
- newDimension = d.Anchor (dimension);
- newDimension = AutoSize && autosize > newDimension ? autosize : newDimension;
- break;
-
- case Dim.DimFill:
- default:
- newDimension = Math.Max (d.Anchor (dimension - location), 0);
- newDimension = AutoSize && autosize > newDimension ? autosize : newDimension;
- break;
- }
-
- return newDimension;
- }
-
-
- // horizontal
- (newX, newW) = GetNewLocationAndDimension (superviewFrame.X, superviewFrame.Width, _x, _width, autosize.Width);
-
- // vertical
- (newY, newH) = GetNewLocationAndDimension (superviewFrame.Y, superviewFrame.Height, _y, _height, autosize.Height);
-
- var r = new Rect (newX, newY, newW, newH);
- if (Frame != r) {
- Frame = r;
- // BUGBUG: Why is this AFTER setting Frame? Seems duplicative.
- if (!SetMinWidthHeight ()) {
- TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
- }
- }
- }
-
- ///
- /// Fired after the View's method has completed.
- ///
- ///
- /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed.
- ///
- public event EventHandler LayoutStarted;
-
- ///
- /// Raises the event. Called from before any subviews have been laid out.
- ///
- internal virtual void OnLayoutStarted (LayoutEventArgs args)
- {
- LayoutStarted?.Invoke (this, args);
- }
-
- ///
- /// Fired after the View's method has completed.
- ///
- ///
- /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed.
- ///
- public event EventHandler LayoutComplete;
-
- ///
- /// Event called only once when the is being initialized for the first time.
- /// Allows configurations and assignments to be performed before the being shown.
- /// This derived from to allow notify all the views that are being initialized.
- ///
- public event EventHandler Initialized;
-
- ///
- /// Raises the event. Called from before all sub-views have been laid out.
- ///
- internal virtual void OnLayoutComplete (LayoutEventArgs args)
- {
- LayoutComplete?.Invoke (this, args);
- }
-
- internal void CollectPos (Pos pos, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges)
- {
- switch (pos) {
- case Pos.PosView pv:
- // See #2461
- //if (!from.InternalSubviews.Contains (pv.Target)) {
- // throw new InvalidOperationException ($"View {pv.Target} is not a subview of {from}");
- //}
- if (pv.Target != this) {
- nEdges.Add ((pv.Target, from));
- }
- return;
- case Pos.PosCombine pc:
- CollectPos (pc.left, from, ref nNodes, ref nEdges);
- CollectPos (pc.right, from, ref nNodes, ref nEdges);
- break;
- }
- }
-
- internal void CollectDim (Dim dim, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges)
- {
- switch (dim) {
- case Dim.DimView dv:
- // See #2461
- //if (!from.InternalSubviews.Contains (dv.Target)) {
- // throw new InvalidOperationException ($"View {dv.Target} is not a subview of {from}");
- //}
- if (dv.Target != this) {
- nEdges.Add ((dv.Target, from));
- }
- return;
- case Dim.DimCombine dc:
- CollectDim (dc.left, from, ref nNodes, ref nEdges);
- CollectDim (dc.right, from, ref nNodes, ref nEdges);
- break;
- }
- }
-
- internal void CollectAll (View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges)
- {
- foreach (var v in from.InternalSubviews) {
- nNodes.Add (v);
- if (v._layoutStyle != LayoutStyle.Computed) {
- continue;
- }
- CollectPos (v.X, v, ref nNodes, ref nEdges);
- CollectPos (v.Y, v, ref nNodes, ref nEdges);
- CollectDim (v.Width, v, ref nNodes, ref nEdges);
- CollectDim (v.Height, v, ref nNodes, ref nEdges);
- }
- }
-
- // https://en.wikipedia.org/wiki/Topological_sorting
- internal static List TopologicalSort (View superView, IEnumerable nodes, ICollection<(View From, View To)> edges)
- {
- var result = new List ();
-
- // Set of all nodes with no incoming edges
- var noEdgeNodes = new HashSet (nodes.Where (n => edges.All (e => !e.To.Equals (n))));
-
-
- while (noEdgeNodes.Any ()) {
- // remove a node n from S
- var n = noEdgeNodes.First ();
- noEdgeNodes.Remove (n);
-
- // add n to tail of L
- if (n != superView)
- result.Add (n);
-
- // for each node m with an edge e from n to m do
- foreach (var e in edges.Where (e => e.From.Equals (n)).ToArray ()) {
- var m = e.To;
-
- // remove edge e from the graph
- edges.Remove (e);
-
- // if m has no other incoming edges then
- if (edges.All (me => !me.To.Equals (m)) && m != superView) {
- // insert m into S
- noEdgeNodes.Add (m);
- }
- }
- }
-
- if (edges.Any ()) {
- foreach ((var from, var to) in edges) {
- if (from == to) {
- // if not yet added to the result, add it and remove from edge
- if (result.Find (v => v == from) == null) {
- result.Add (from);
- }
- edges.Remove ((from, to));
- } else if (from.SuperView == to.SuperView) {
- // if 'from' is not yet added to the result, add it
- if (result.Find (v => v == from) == null) {
- result.Add (from);
- }
- // if 'to' is not yet added to the result, add it
- if (result.Find (v => v == to) == null) {
- result.Add (to);
- }
- // remove from edge
- edges.Remove ((from, to));
- } else if (from != superView?.GetTopSuperView (to, from) && !ReferenceEquals (from, to)) {
- if (ReferenceEquals (from.SuperView, to)) {
- throw new InvalidOperationException ($"ComputedLayout for \"{superView}\": \"{to}\" references a SubView (\"{from}\").");
- } else {
- throw new InvalidOperationException ($"ComputedLayout for \"{superView}\": \"{from}\" linked with \"{to}\" was not found. Did you forget to add it to {superView}?");
- }
- }
- }
- }
- // return L (a topologically sorted order)
- return result;
- } // TopologicalSort
-
- ///
- /// Overriden by to do nothing, as the does not have frames.
- ///
- internal virtual void LayoutFrames ()
- {
- if (Margin == null) return; // CreateFrames() has not been called yet
-
- if (Margin.Frame.Size != Frame.Size) {
- Margin._frame = new Rect (Point.Empty, Frame.Size);
- Margin.X = 0;
- Margin.Y = 0;
- Margin.Width = Frame.Size.Width;
- Margin.Height = Frame.Size.Height;
- Margin.SetNeedsLayout ();
- Margin.LayoutSubviews ();
- Margin.SetNeedsDisplay ();
- }
-
- var border = Margin.Thickness.GetInside (Margin.Frame);
- if (border != Border.Frame) {
- Border._frame = new Rect (new Point (border.Location.X, border.Location.Y), border.Size);
- Border.X = border.Location.X;
- Border.Y = border.Location.Y;
- Border.Width = border.Size.Width;
- Border.Height = border.Size.Height;
- Border.SetNeedsLayout ();
- Border.LayoutSubviews ();
- Border.SetNeedsDisplay ();
- }
-
- var padding = Border.Thickness.GetInside (Border.Frame);
- if (padding != Padding.Frame) {
- Padding._frame = new Rect (new Point (padding.Location.X, padding.Location.Y), padding.Size);
- Padding.X = padding.Location.X;
- Padding.Y = padding.Location.Y;
- Padding.Width = padding.Size.Width;
- Padding.Height = padding.Size.Height;
- Padding.SetNeedsLayout ();
- Padding.LayoutSubviews ();
- Padding.SetNeedsDisplay ();
- }
- }
-
- ///
- /// Invoked when a view starts executing or when the dimensions of the view have changed, for example in
- /// response to the container view or terminal resizing.
- ///
- ///
- /// Calls (which raises the event) before it returns.
- ///
- public virtual void LayoutSubviews ()
- {
- if (!LayoutNeeded) {
- return;
- }
-
- LayoutFrames ();
-
- var oldBounds = Bounds;
- OnLayoutStarted (new LayoutEventArgs () { OldBounds = oldBounds });
-
- TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
-
- // Sort out the dependencies of the X, Y, Width, Height properties
- var nodes = new HashSet ();
- var edges = new HashSet<(View, View)> ();
- CollectAll (this, ref nodes, ref edges);
- var ordered = View.TopologicalSort (SuperView, nodes, edges);
- foreach (var v in ordered) {
- LayoutSubview (v, new Rect (GetBoundsOffset (), Bounds.Size));
- }
-
- // If the 'to' is rooted to 'from' and the layoutstyle is Computed it's a special-case.
- // Use LayoutSubview with the Frame of the 'from'
- if (SuperView != null && GetTopSuperView () != null && LayoutNeeded && edges.Count > 0) {
- foreach ((var from, var to) in edges) {
- LayoutSubview (to, from.Frame);
- }
- }
-
- LayoutNeeded = false;
-
- OnLayoutComplete (new LayoutEventArgs () { OldBounds = oldBounds });
- }
-
- private void LayoutSubview (View v, Rect contentArea)
- {
- if (v.LayoutStyle == LayoutStyle.Computed) {
- v.SetRelativeLayout (contentArea);
- }
-
- v.LayoutSubviews ();
- v.LayoutNeeded = false;
- }
-
- ustring _text;
-
- ///
- /// The text displayed by the .
- ///
- ///
- ///
- /// The text will be drawn before any subviews are drawn.
- ///
- ///
- /// The text will be drawn starting at the view origin (0, 0) and will be formatted according
- /// to and .
- ///
- ///
- /// The text will word-wrap to additional lines if it does not fit horizontally. If 's height
- /// is 1, the text will be clipped.
- ///
- ///
- /// Set the to enable hotkey support. To disable hotkey support set to
- /// (Rune)0xffff.
- ///
- ///
- public virtual ustring Text {
- get => _text;
- set {
- _text = value;
- SetHotKey ();
- UpdateTextFormatterText ();
- //TextFormatter.Format ();
- OnResizeNeeded ();
-
-#if DEBUG
- if (_text != null && string.IsNullOrEmpty (Id)) {
- Id = _text.ToString ();
- }
-#endif
- }
- }
-
- ///
- /// Gets or sets a flag that determines whether the View will be automatically resized to fit the
- /// within
- ///
- /// The default is . Set to to turn on AutoSize. If then
- /// and will be used if can fit;
- /// if won't fit the view will be resized as needed.
- ///
- ///
- /// In addition, if is the new values of and
- /// must be of the same types of the existing one to avoid breaking the settings.
- ///
- ///
- public virtual bool AutoSize {
- get => _autoSize;
- set {
- var v = ResizeView (value);
- TextFormatter.AutoSize = v;
- if (_autoSize != v) {
- _autoSize = v;
- TextFormatter.NeedsFormat = true;
- UpdateTextFormatterText ();
- OnResizeNeeded ();
- }
- }
- }
-
- ///
- /// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved
- /// or not when is enabled.
- /// If trailing spaces at the end of wrapped lines will be removed when
- /// is formatted for display. The default is .
- ///
- public virtual bool PreserveTrailingSpaces {
- get => TextFormatter.PreserveTrailingSpaces;
- set {
- if (TextFormatter.PreserveTrailingSpaces != value) {
- TextFormatter.PreserveTrailingSpaces = value;
- TextFormatter.NeedsFormat = true;
- }
- }
- }
-
- ///
- /// Gets or sets how the View's is aligned horizontally when drawn. Changing this property will redisplay the .
- ///
- /// The text alignment.
- public virtual TextAlignment TextAlignment {
- get => TextFormatter.Alignment;
- set {
- TextFormatter.Alignment = value;
- UpdateTextFormatterText ();
- OnResizeNeeded ();
- }
- }
-
- ///
- /// Gets or sets how the View's is aligned vertically when drawn. Changing this property will redisplay the .
- ///
- /// The text alignment.
- public virtual VerticalTextAlignment VerticalTextAlignment {
- get => TextFormatter.VerticalAlignment;
- set {
- TextFormatter.VerticalAlignment = value;
- SetNeedsDisplay ();
- }
- }
-
- ///
- /// Gets or sets the direction of the View's . Changing this property will redisplay the .
- ///
- /// The text alignment.
- public virtual TextDirection TextDirection {
- get => TextFormatter.Direction;
- set {
- UpdateTextDirection (value);
- TextFormatter.Direction = value;
- }
- }
-
- private void UpdateTextDirection (TextDirection newDirection)
- {
- var directionChanged = TextFormatter.IsHorizontalDirection (TextFormatter.Direction)
- != TextFormatter.IsHorizontalDirection (newDirection);
- TextFormatter.Direction = newDirection;
-
- var isValidOldAutoSize = AutoSize && IsValidAutoSize (out var _);
-
- UpdateTextFormatterText ();
-
- if ((!ForceValidatePosDim && directionChanged && AutoSize)
- || (ForceValidatePosDim && directionChanged && AutoSize && isValidOldAutoSize)) {
- OnResizeNeeded ();
- } else if (directionChanged && IsAdded) {
- SetWidthHeight (Bounds.Size);
- SetMinWidthHeight ();
- } else {
- SetMinWidthHeight ();
- }
- TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
- SetNeedsDisplay ();
- }
-
- ///
- /// Get or sets if the has been initialized (via
- /// and ).
- ///
- ///
- /// If first-run-only initialization is preferred, overrides to
- /// can be implemented, in which case the
- /// methods will only be called if
- /// is . This allows proper inheritance hierarchies
- /// to override base class layout code optimally by doing so only on first run,
- /// instead of on every run.
- ///
- public virtual bool IsInitialized { get; set; }
-
- ///
- /// Gets information if the view was already added to the .
- ///
- public bool IsAdded { get; private set; }
-
- bool _oldEnabled;
-
- ///
- public override bool Enabled {
- get => base.Enabled;
- set {
- if (base.Enabled != value) {
- if (value) {
- if (SuperView == null || SuperView?.Enabled == true) {
- base.Enabled = value;
- }
- } else {
- base.Enabled = value;
- }
- if (!value && HasFocus) {
- SetHasFocus (false, this);
- }
- OnEnabledChanged ();
- SetNeedsDisplay ();
-
- if (_subviews != null) {
- foreach (var view in _subviews) {
- if (!value) {
- view._oldEnabled = view.Enabled;
- view.Enabled = false;
- } else {
- view.Enabled = view._oldEnabled;
- view._addingView = false;
- }
- }
- }
- }
- }
- }
-
- ///
- /// Gets or sets whether a view is cleared if the property is .
- ///
- public bool ClearOnVisibleFalse { get; set; } = true;
-
- /// >
- public override bool Visible {
- get => base.Visible;
- set {
- if (base.Visible != value) {
- base.Visible = value;
- if (!value) {
- if (HasFocus) {
- SetHasFocus (false, this);
- }
- if (ClearOnVisibleFalse) {
- Clear ();
- }
- }
- OnVisibleChanged ();
- SetNeedsDisplay ();
- }
- }
- }
-
- ///
- /// Gets or sets whether the view has a one row/col thick border.
- ///
- ///
- ///
- /// This is a helper for manipulating the view's . Setting this property to any value other than
- /// is equivalent to setting 's
- /// to `1` and to the value.
- ///
- ///
- /// Setting this property to is equivalent to setting 's
- /// to `0` and to .
- ///
- ///
- /// For more advanced customization of the view's border, manipulate see directly.
- ///
- ///
- public LineStyle BorderStyle {
- get {
- return Border?.BorderStyle ?? LineStyle.None;
- }
- set {
- if (Border == null) {
- throw new InvalidOperationException ("Border is null; this is likely a bug.");
- }
- if (value != LineStyle.None) {
- Border.Thickness = new Thickness (1);
- } else {
- Border.Thickness = new Thickness (0);
- }
- Border.BorderStyle = value;
- LayoutFrames ();
- SetNeedsLayout ();
- }
- }
-
- ///
- /// Pretty prints the View
- ///
- ///
- public override string ToString ()
+ ///
+ public override string ToString ()
{
return $"{GetType ().Name}({Id})({Frame})";
}
- void SetHotKey ()
- {
- if (TextFormatter == null) {
- return; // throw new InvalidOperationException ("Can't set HotKey unless a TextFormatter has been created");
- }
- TextFormatter.FindHotKey (_text, HotKeySpecifier, true, out _, out var hk);
- if (_hotKey != hk) {
- HotKey = hk;
- }
- }
-
- bool ResizeView (bool autoSize)
- {
- if (!autoSize) {
- return false;
- }
-
- var aSize = true;
- var nBoundsSize = GetAutoSize ();
- if (nBoundsSize != Bounds.Size) {
- if (ForceValidatePosDim) {
- aSize = SetWidthHeight (nBoundsSize);
- } else {
- Height = nBoundsSize.Height;
- Width = nBoundsSize.Width; // = new Rect (Bounds.X, Bounds.Y, nBoundsSize.Width, nBoundsSize.Height);
- }
- }
- // BUGBUG: This call may be redundant
- TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
- return aSize;
- }
-
- ///
- /// Resizes the View to fit the specified size.
- ///
- ///
- ///
- bool SetWidthHeight (Size nBounds)
- {
- var aSize = false;
- var canSizeW = TrySetWidth (nBounds.Width - GetHotKeySpecifierLength (), out var rW);
- var canSizeH = TrySetHeight (nBounds.Height - GetHotKeySpecifierLength (false), out var rH);
- if (canSizeW) {
- aSize = true;
- _width = rW;
- }
- if (canSizeH) {
- aSize = true;
- _height = rH;
- }
- if (aSize) {
- Bounds = new Rect (Bounds.X, Bounds.Y, canSizeW ? rW : Bounds.Width, canSizeH ? rH : Bounds.Height);
- }
-
- return aSize;
- }
-
- ///
- /// Gets the Frame dimensions required to fit using the text specified by the
- /// property and accounting for any characters.
- ///
- /// The required to fit the text.
- public Size GetAutoSize ()
- {
- var rect = TextFormatter.CalcRect (Bounds.X, Bounds.Y, TextFormatter.Text, TextFormatter.Direction);
- var newWidth = rect.Size.Width - GetHotKeySpecifierLength () + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal;
- var newHeight = rect.Size.Height - GetHotKeySpecifierLength (false) + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical;
- return new Size (newWidth, newHeight);
- }
-
- bool IsValidAutoSize (out Size autoSize)
- {
- var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
- autoSize = new Size (rect.Size.Width - GetHotKeySpecifierLength (),
- rect.Size.Height - GetHotKeySpecifierLength (false));
- return !(ForceValidatePosDim && (!(Width is Dim.DimAbsolute) || !(Height is Dim.DimAbsolute))
- || _frame.Size.Width != rect.Size.Width - GetHotKeySpecifierLength ()
- || _frame.Size.Height != rect.Size.Height - GetHotKeySpecifierLength (false));
- }
-
- bool IsValidAutoSizeWidth (Dim width)
- {
- var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
- var dimValue = width.Anchor (0);
- return !(ForceValidatePosDim && (!(width is Dim.DimAbsolute)) || dimValue != rect.Size.Width
- - GetHotKeySpecifierLength ());
- }
-
- bool IsValidAutoSizeHeight (Dim height)
- {
- var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
- var dimValue = height.Anchor (0);
- return !(ForceValidatePosDim && (!(height is Dim.DimAbsolute)) || dimValue != rect.Size.Height
- - GetHotKeySpecifierLength (false));
- }
-
- ///
- /// Gets the width or height of the characters
- /// in the property.
- ///
- ///
- /// Only the first hotkey specifier found in is supported.
- ///
- /// If (the default) the width required for the hotkey specifier is returned. Otherwise the height is returned.
- /// The number of characters required for the . If the text direction specified
- /// by does not match the parameter, 0 is returned.
- public int GetHotKeySpecifierLength (bool isWidth = true)
- {
- if (isWidth) {
- return TextFormatter.IsHorizontalDirection (TextDirection) &&
- TextFormatter.Text?.Contains (HotKeySpecifier) == true
- ? Math.Max (Rune.ColumnWidth (HotKeySpecifier), 0) : 0;
- } else {
- return TextFormatter.IsVerticalDirection (TextDirection) &&
- TextFormatter.Text?.Contains (HotKeySpecifier) == true
- ? Math.Max (Rune.ColumnWidth (HotKeySpecifier), 0) : 0;
- }
- }
-
- ///
- /// Gets the dimensions required for ignoring a .
- ///
- ///
- public Size GetSizeNeededForTextWithoutHotKey ()
- {
- return new Size (TextFormatter.Size.Width - GetHotKeySpecifierLength (),
- TextFormatter.Size.Height - GetHotKeySpecifierLength (false));
- }
-
- ///
- /// Gets the dimensions required for accounting for a .
- ///
- ///
- public Size GetSizeNeededForTextAndHotKey ()
- {
- if (ustring.IsNullOrEmpty (TextFormatter.Text)) {
-
- if (!IsInitialized) return Size.Empty;
-
- return Bounds.Size;
- }
-
- // BUGBUG: This IGNORES what Text is set to, using on only the current View size. This doesn't seem to make sense.
- // BUGBUG: This uses Frame; in v2 it should be Bounds
- return new Size (_frame.Size.Width + GetHotKeySpecifierLength (),
- _frame.Size.Height + GetHotKeySpecifierLength (false));
- }
-
- ///
- public override bool OnMouseEnter (MouseEvent mouseEvent)
- {
- if (!Enabled) {
- return true;
- }
-
- if (!CanBeVisible (this)) {
- return false;
- }
-
- var args = new MouseEventEventArgs (mouseEvent);
- MouseEnter?.Invoke (this, args);
-
- return args.Handled || base.OnMouseEnter (mouseEvent);
- }
-
- ///
- public override bool OnMouseLeave (MouseEvent mouseEvent)
- {
- if (!Enabled) {
- return true;
- }
-
- if (!CanBeVisible (this)) {
- return false;
- }
-
- var args = new MouseEventEventArgs (mouseEvent);
- MouseLeave?.Invoke (this, args);
-
- return args.Handled || base.OnMouseLeave (mouseEvent);
- }
-
- ///
- /// Method invoked when a mouse event is generated
- ///
- ///
- /// , if the event was handled, otherwise.
- public virtual bool OnMouseEvent (MouseEvent mouseEvent)
- {
- if (!Enabled) {
- return true;
- }
-
- if (!CanBeVisible (this)) {
- return false;
- }
-
- var args = new MouseEventEventArgs (mouseEvent);
- if (OnMouseClick (args))
- return true;
- if (MouseEvent (mouseEvent))
- return true;
-
- if (mouseEvent.Flags == MouseFlags.Button1Clicked) {
- if (CanFocus && !HasFocus && SuperView != null) {
- SuperView.SetFocus (this);
- SetNeedsDisplay ();
- }
-
- return true;
- }
- return false;
- }
-
- ///
- /// Invokes the MouseClick event.
- ///
- protected bool OnMouseClick (MouseEventEventArgs args)
- {
- if (!Enabled) {
- return true;
- }
-
- MouseClick?.Invoke (this, args);
- return args.Handled;
- }
-
- ///
- public override void OnCanFocusChanged () => CanFocusChanged?.Invoke (this, EventArgs.Empty);
-
- ///
- public override void OnEnabledChanged () => EnabledChanged?.Invoke (this, EventArgs.Empty);
-
- ///
- public override void OnVisibleChanged () => VisibleChanged?.Invoke (this, EventArgs.Empty);
-
///
protected override void Dispose (bool disposing)
{
@@ -3349,248 +508,5 @@ protected override void Dispose (bool disposing)
}
base.Dispose (disposing);
}
-
- ///
- /// Signals the View that initialization is starting. See .
- ///
- ///
- ///
- /// Views can opt-in to more sophisticated initialization
- /// by implementing overrides to and
- /// which will be called
- /// when the view is added to a .
- ///
- ///
- /// If first-run-only initialization is preferred, overrides to
- /// can be implemented too, in which case the
- /// methods will only be called if
- /// is . This allows proper inheritance hierarchies
- /// to override base class layout code optimally by doing so only on first run,
- /// instead of on every run.
- ///
- ///
- public virtual void BeginInit ()
- {
- if (!IsInitialized) {
- _oldCanFocus = CanFocus;
- _oldTabIndex = _tabIndex;
-
- UpdateTextDirection (TextDirection);
- UpdateTextFormatterText ();
- SetHotKey ();
-
- // TODO: Figure out why ScrollView and other tests fail if this call is put here
- // instead of the constructor.
- //InitializeFrames ();
-
- } else {
- //throw new InvalidOperationException ("The view is already initialized.");
-
- }
-
- if (_subviews?.Count > 0) {
- foreach (var view in _subviews) {
- if (!view.IsInitialized) {
- view.BeginInit ();
- }
- }
- }
- }
-
- ///
- /// Signals the View that initialization is ending. See .
- ///
- public void EndInit ()
- {
- IsInitialized = true;
- OnResizeNeeded ();
- if (_subviews != null) {
- foreach (var view in _subviews) {
- if (!view.IsInitialized) {
- view.EndInit ();
- }
- }
- }
- Initialized?.Invoke (this, EventArgs.Empty);
- }
-
- bool CanBeVisible (View view)
- {
- if (!view.Visible) {
- return false;
- }
- for (var c = view.SuperView; c != null; c = c.SuperView) {
- if (!c.Visible) {
- return false;
- }
- }
-
- return true;
- }
-
- ///
- /// Determines if the View's can be set to a new value.
- ///
- ///
- /// Contains the width that would result if were set to "/>
- /// if the View's can be changed to the specified value. False otherwise.
- internal bool TrySetWidth (int desiredWidth, out int resultWidth)
- {
- var w = desiredWidth;
- bool canSetWidth;
- switch (Width) {
- case Dim.DimCombine _:
- case Dim.DimView _:
- case Dim.DimFill _:
- // It's a Dim.DimCombine and so can't be assigned. Let it have it's Width anchored.
- w = Width.Anchor (w);
- canSetWidth = !ForceValidatePosDim;
- break;
- case Dim.DimFactor factor:
- // Tries to get the SuperView Width otherwise the view Width.
- var sw = SuperView != null ? SuperView.Frame.Width : w;
- if (factor.IsFromRemaining ()) {
- sw -= Frame.X;
- }
- w = Width.Anchor (sw);
- canSetWidth = !ForceValidatePosDim;
- break;
- default:
- canSetWidth = true;
- break;
- }
- resultWidth = w;
-
- return canSetWidth;
- }
-
- ///
- /// Determines if the View's can be set to a new value.
- ///
- ///
- /// Contains the width that would result if were set to "/>
- /// if the View's can be changed to the specified value. False otherwise.
- internal bool TrySetHeight (int desiredHeight, out int resultHeight)
- {
- var h = desiredHeight;
- bool canSetHeight;
- switch (Height) {
- case Dim.DimCombine _:
- case Dim.DimView _:
- case Dim.DimFill _:
- // It's a Dim.DimCombine and so can't be assigned. Let it have it's height anchored.
- h = Height.Anchor (h);
- canSetHeight = !ForceValidatePosDim;
- break;
- case Dim.DimFactor factor:
- // Tries to get the SuperView height otherwise the view height.
- var sh = SuperView != null ? SuperView.Frame.Height : h;
- if (factor.IsFromRemaining ()) {
- sh -= Frame.Y;
- }
- h = Height.Anchor (sh);
- canSetHeight = !ForceValidatePosDim;
- break;
- default:
- canSetHeight = true;
- break;
- }
- resultHeight = h;
-
- return canSetHeight;
- }
-
- ///
- /// Determines the current based on the value.
- ///
- /// if is
- /// or if is .
- /// If it's overridden can return other values.
- public virtual Attribute GetNormalColor ()
- {
- return Enabled ? ColorScheme.Normal : ColorScheme.Disabled;
- }
-
- ///
- /// Determines the current based on the value.
- ///
- /// if is
- /// or if is .
- /// If it's overridden can return other values.
- public virtual Attribute GetFocusColor ()
- {
- return Enabled ? ColorScheme.Focus : ColorScheme.Disabled;
- }
-
- ///
- /// Determines the current based on the value.
- ///
- /// if is
- /// or if is .
- /// If it's overridden can return other values.
- public virtual Attribute GetHotNormalColor ()
- {
- return Enabled ? ColorScheme.HotNormal : ColorScheme.Disabled;
- }
-
- ///
- /// Get the top superview of a given .
- ///
- /// The superview view.
- public View GetTopSuperView (View view = null, View superview = null)
- {
- View top = superview ?? Application.Top;
- for (var v = view?.SuperView ?? (this?.SuperView); v != null; v = v.SuperView) {
- top = v;
- if (top == superview) {
- break;
- }
- }
-
- return top;
- }
-
- ///
- /// Finds which view that belong to the superview at the provided location.
- ///
- /// The superview where to look for.
- /// The column location in the superview.
- /// The row location in the superview.
- /// The found view screen relative column location.
- /// The found view screen relative row location.
- ///
- /// The view that was found at the and coordinates.
- /// if no view was found.
- ///
- public static View FindDeepestView (View 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 (start.InternalSubviews != null) {
- int count = start.InternalSubviews.Count;
- if (count > 0) {
- var boundsOffset = start.GetBoundsOffset ();
- var rx = x - (startFrame.X + boundsOffset.X);
- var ry = y - (startFrame.Y + boundsOffset.Y);
- for (int i = count - 1; i >= 0; i--) {
- View v = start.InternalSubviews [i];
- if (v.Visible && v.Frame.Contains (rx, ry)) {
- var deep = FindDeepestView (v, rx, ry, out resx, out resy);
- if (deep == null)
- return v;
- return deep;
- }
- }
- }
- }
- resx = x - startFrame.X;
- resy = y - startFrame.Y;
- return start;
- }
}
}
diff --git a/Terminal.Gui/View/ViewDrawing.cs b/Terminal.Gui/View/ViewDrawing.cs
new file mode 100644
index 0000000000..502a6db07b
--- /dev/null
+++ b/Terminal.Gui/View/ViewDrawing.cs
@@ -0,0 +1,492 @@
+using System;
+using System.Linq;
+using NStack;
+
+namespace Terminal.Gui {
+ public partial class View {
+
+ ColorScheme _colorScheme;
+
+ ///
+ /// The color scheme for this view, if it is not defined, it returns the 's
+ /// color scheme.
+ ///
+ public virtual ColorScheme ColorScheme {
+ get {
+ if (_colorScheme == null) {
+ return SuperView?.ColorScheme;
+ }
+ return _colorScheme;
+ }
+ set {
+ if (_colorScheme != value) {
+ _colorScheme = value;
+ SetNeedsDisplay ();
+ }
+ }
+ }
+
+ ///
+ /// Determines the current based on the value.
+ ///
+ /// if is
+ /// or if is .
+ /// If it's overridden can return other values.
+ public virtual Attribute GetNormalColor ()
+ {
+ return Enabled ? ColorScheme.Normal : ColorScheme.Disabled;
+ }
+
+ ///
+ /// Determines the current based on the value.
+ ///
+ /// if is
+ /// or if is .
+ /// If it's overridden can return other values.
+ public virtual Attribute GetFocusColor ()
+ {
+ return Enabled ? ColorScheme.Focus : ColorScheme.Disabled;
+ }
+
+ ///
+ /// Determines the current based on the value.
+ ///
+ /// if is
+ /// or if is .
+ /// If it's overridden can return other values.
+ public virtual Attribute GetHotNormalColor ()
+ {
+ return Enabled ? ColorScheme.HotNormal : ColorScheme.Disabled;
+ }
+
+ ///
+ /// Displays the specified character in the specified column and row of the View.
+ ///
+ /// Column (view-relative).
+ /// Row (view-relative).
+ /// Ch.
+ public void AddRune (int col, int row, Rune ch)
+ {
+ if (row < 0 || col < 0)
+ return;
+ if (row > _frame.Height - 1 || col > _frame.Width - 1)
+ return;
+ Move (col, row);
+ Driver.AddRune (ch);
+ }
+
+ ///
+ /// Removes the and the setting on this view.
+ ///
+ protected void ClearNeedsDisplay ()
+ {
+ _needsDisplay = Rect.Empty;
+ _childNeedsDisplay = false;
+ }
+
+ // The view-relative region that needs to be redrawn
+ internal Rect _needsDisplay { get; private set; } = Rect.Empty;
+
+ ///
+ /// Sets a flag indicating this view needs to be redisplayed because its state has changed.
+ ///
+ public void SetNeedsDisplay ()
+ {
+ if (!IsInitialized) return;
+ SetNeedsDisplay (Bounds);
+ }
+
+ ///
+ /// Flags the view-relative region on this View as needing to be redrawn.
+ ///
+ /// The view-relative region that needs to be redrawn.
+ public void SetNeedsDisplay (Rect region)
+ {
+ if (_needsDisplay.IsEmpty)
+ _needsDisplay = region;
+ else {
+ var x = Math.Min (_needsDisplay.X, region.X);
+ var y = Math.Min (_needsDisplay.Y, region.Y);
+ var w = Math.Max (_needsDisplay.Width, region.Width);
+ var h = Math.Max (_needsDisplay.Height, region.Height);
+ _needsDisplay = new Rect (x, y, w, h);
+ }
+ _superView?.SetSubViewNeedsDisplay ();
+
+ if (_subviews == null)
+ return;
+
+ foreach (var view in _subviews)
+ if (view.Frame.IntersectsWith (region)) {
+ var childRegion = Rect.Intersect (view.Frame, region);
+ childRegion.X -= view.Frame.X;
+ childRegion.Y -= view.Frame.Y;
+ view.SetNeedsDisplay (childRegion);
+ }
+ }
+
+ internal bool _childNeedsDisplay { get; private set; }
+
+ ///
+ /// Indicates that any Subviews (in the list) need to be repainted.
+ ///
+ public void SetSubViewNeedsDisplay ()
+ {
+ if (_childNeedsDisplay) {
+ return;
+ }
+ _childNeedsDisplay = true;
+ if (_superView != null && !_superView._childNeedsDisplay)
+ _superView.SetSubViewNeedsDisplay ();
+ }
+
+ ///
+ /// Clears the view region with the current color.
+ ///
+ ///
+ ///
+ /// This clears the entire region used by this view.
+ ///
+ ///
+ public void Clear ()
+ {
+ var h = Frame.Height;
+ var w = Frame.Width;
+ for (var line = 0; line < h; line++) {
+ Move (0, line);
+ for (var col = 0; col < w; col++)
+ Driver.AddRune (' ');
+ }
+ }
+
+ // BUGBUG: Stupid that this takes screen-relative. We should have a tenet that says
+ // "View APIs only deal with View-relative coords".
+ ///
+ /// Clears the specified region with the current color.
+ ///
+ ///
+ ///
+ /// The screen-relative region to clear.
+ public void Clear (Rect regionScreen)
+ {
+ var h = regionScreen.Height;
+ var w = regionScreen.Width;
+ for (var line = regionScreen.Y; line < regionScreen.Y + h; line++) {
+ Driver.Move (regionScreen.X, line);
+ for (var col = 0; col < w; col++)
+ Driver.AddRune (' ');
+ }
+ }
+
+ // Clips a rectangle in screen coordinates to the dimensions currently available on the screen
+ internal Rect ScreenClip (Rect regionScreen)
+ {
+ var x = regionScreen.X < 0 ? 0 : regionScreen.X;
+ var y = regionScreen.Y < 0 ? 0 : regionScreen.Y;
+ var w = regionScreen.X + regionScreen.Width >= Driver.Cols ? Driver.Cols - regionScreen.X : regionScreen.Width;
+ var h = regionScreen.Y + regionScreen.Height >= Driver.Rows ? Driver.Rows - regionScreen.Y : regionScreen.Height;
+
+ return new Rect (x, y, w, h);
+ }
+
+ ///
+ /// Sets the 's clip region to .
+ ///
+ /// The current screen-relative clip region, which can be then re-applied by setting .
+ ///
+ ///
+ /// is View-relative.
+ ///
+ ///
+ /// If and do not intersect, the clip region will be set to .
+ ///
+ ///
+ public Rect ClipToBounds ()
+ {
+ var clip = Bounds;
+
+ return SetClip (clip);
+ }
+
+ // BUGBUG: v2 - SetClip should return VIEW-relative so that it can be used to reset it; using Driver.Clip directly should not be necessary.
+ ///
+ /// Sets the clip region to the specified view-relative region.
+ ///
+ /// The current screen-relative clip region, which can be then re-applied by setting .
+ /// View-relative clip region.
+ ///
+ /// If and do not intersect, the clip region will be set to .
+ ///
+ public Rect SetClip (Rect region)
+ {
+ var previous = Driver.Clip;
+ Driver.Clip = Rect.Intersect (previous, ViewToScreen (region));
+ return previous;
+ }
+
+ ///
+ /// Utility function to draw strings that contain a hotkey.
+ ///
+ /// String to display, the hotkey specifier before a letter flags the next letter as the hotkey.
+ /// Hot color.
+ /// Normal color.
+ ///
+ /// The hotkey is any character following the hotkey specifier, which is the underscore ('_') character by default.
+ /// The hotkey specifier can be changed via
+ ///
+ public void DrawHotString (ustring text, Attribute hotColor, Attribute normalColor)
+ {
+ var hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier;
+ Application.Driver.SetAttribute (normalColor);
+ foreach (var rune in text) {
+ if (rune == hotkeySpec) {
+ Application.Driver.SetAttribute (hotColor);
+ continue;
+ }
+ Application.Driver.AddRune (rune);
+ Application.Driver.SetAttribute (normalColor);
+ }
+ }
+
+ ///
+ /// Utility function to draw strings that contains a hotkey using a and the "focused" state.
+ ///
+ /// String to display, the underscore before a letter flags the next letter as the hotkey.
+ /// If set to this uses the focused colors from the color scheme, otherwise the regular ones.
+ /// The color scheme to use.
+ public void DrawHotString (ustring text, bool focused, ColorScheme scheme)
+ {
+ if (focused)
+ DrawHotString (text, scheme.HotFocus, scheme.Focus);
+ else
+ DrawHotString (text, Enabled ? scheme.HotNormal : scheme.Disabled, Enabled ? scheme.Normal : scheme.Disabled);
+ }
+
+ ///
+ /// This moves the cursor to the specified column and row in the view.
+ ///
+ /// The move.
+ /// The column to move to, in view-relative coordinates.
+ /// the row to move to, in view-relative coordinates.
+ /// Whether to clip the result of the ViewToScreen method,
+ /// If , the and values are clamped to the screen (terminal) dimensions (0..TerminalDim-1).
+ public void Move (int col, int row, bool clipped = true)
+ {
+ if (Driver.Rows == 0) {
+ return;
+ }
+
+ ViewToScreen (col, row, out var rCol, out var rRow, clipped);
+ Driver.Move (rCol, rRow);
+ }
+ ///
+ /// The canvas that any line drawing that is to be shared by subviews of this view should add lines to.
+ ///
+ /// adds border lines to this LineCanvas.
+ public virtual LineCanvas LineCanvas { get; set; } = new LineCanvas ();
+
+ ///
+ /// Gets or sets whether this View will use it's SuperView's for
+ /// rendering any border lines. If the rendering of any borders drawn
+ /// by this Frame will be done by it's parent's SuperView. If (the default)
+ /// this View's method will be called to render the borders.
+ ///
+ public virtual bool SuperViewRendersLineCanvas { get; set; } = false;
+
+ // TODO: Make this cancelable
+ ///
+ ///
+ ///
+ ///
+ public virtual bool OnDrawFrames ()
+ {
+ if (!IsInitialized) {
+ return false;
+ }
+
+ var prevClip = Driver.Clip;
+ Driver.Clip = ViewToScreen (Frame);
+
+ // TODO: Figure out what we should do if we have no superview
+ //if (SuperView != null) {
+ // TODO: Clipping is disabled for now to ensure we see errors
+ Driver.Clip = new Rect (0, 0, Driver.Cols, Driver.Rows);// screenBounds;// SuperView.ClipToBounds ();
+ //}
+
+ // Each of these renders lines to either this View's LineCanvas
+ // Those lines will be finally rendered in OnRenderLineCanvas
+ Margin?.Redraw (Margin.Frame);
+ Border?.Redraw (Border.Frame);
+ Padding?.Redraw (Padding.Frame);
+
+ Driver.Clip = prevClip;
+
+ return true;
+ }
+
+ ///
+ /// Redraws this view and its subviews; only redraws the views that have been flagged for a re-display.
+ ///
+ /// The bounds (view-relative region) to redraw.
+ ///
+ ///
+ /// Always use (view-relative) when calling , NOT (superview-relative).
+ ///
+ ///
+ /// Views should set the color that they want to use on entry, as otherwise this will inherit
+ /// the last color that was set globally on the driver.
+ ///
+ ///
+ /// Overrides of must ensure they do not set Driver.Clip to a clip region
+ /// larger than the parameter, as this will cause the driver to clip the entire region.
+ ///
+ ///
+ public virtual void Redraw (Rect bounds)
+ {
+ if (!CanBeVisible (this)) {
+ return;
+ }
+
+ OnDrawFrames ();
+
+ var prevClip = ClipToBounds ();
+
+ if (ColorScheme != null) {
+ //Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
+ Driver.SetAttribute (GetNormalColor ());
+ }
+
+ if (SuperView != null) {
+ Clear (ViewToScreen (bounds));
+ }
+
+ // Invoke DrawContentEvent
+ OnDrawContent (bounds);
+
+ // Draw subviews
+ // TODO: Implement OnDrawSubviews (cancelable);
+ if (_subviews != null) {
+ foreach (var view in _subviews) {
+ if (view.Visible) { //!view._needsDisplay.IsEmpty || view._childNeedsDisplay || view.LayoutNeeded) {
+ if (true) { //view.Frame.IntersectsWith (bounds)) { // && (view.Frame.IntersectsWith (bounds) || bounds.X < 0 || bounds.Y < 0)) {
+ if (view.LayoutNeeded) {
+ view.LayoutSubviews ();
+ }
+
+ // Draw the subview
+ // Use the view's bounds (view-relative; Location will always be (0,0)
+ //if (view.Visible && view.Frame.Width > 0 && view.Frame.Height > 0) {
+ view.Redraw (view.Bounds);
+ //}
+ }
+ view.ClearNeedsDisplay ();
+ }
+ }
+ }
+
+ // Invoke DrawContentCompleteEvent
+ OnDrawContentComplete (bounds);
+ Driver.Clip = prevClip;
+
+ OnRenderLineCanvas ();
+
+ // BUGBUG: v2 - We should be able to use View.SetClip here and not have to resort to knowing Driver details.
+ ClearLayoutNeeded ();
+ ClearNeedsDisplay ();
+ }
+
+ internal void OnRenderLineCanvas ()
+ {
+ //Driver.SetAttribute (new Attribute(Color.White, Color.Black));
+
+ // If we have a SuperView, it'll render our frames.
+ if (!SuperViewRendersLineCanvas && LineCanvas.Bounds != Rect.Empty) {
+ foreach (var p in LineCanvas.GetCellMap ()) { // Get the entire map
+ Driver.SetAttribute (p.Value.Attribute?.Value ?? ColorScheme.Normal);
+ Driver.Move (p.Key.X, p.Key.Y);
+ Driver.AddRune (p.Value.Rune.Value);
+ }
+ LineCanvas.Clear ();
+ }
+
+ if (Subviews.Select (s => s.SuperViewRendersLineCanvas).Count () > 0) {
+ foreach (var subview in Subviews.Where (s => s.SuperViewRendersLineCanvas)) {
+ // Combine the LineCavas'
+ LineCanvas.Merge (subview.LineCanvas);
+ subview.LineCanvas.Clear ();
+ }
+
+ foreach (var p in LineCanvas.GetCellMap ()) { // Get the entire map
+ Driver.SetAttribute (p.Value.Attribute?.Value ?? ColorScheme.Normal);
+ Driver.Move (p.Key.X, p.Key.Y);
+ Driver.AddRune (p.Value.Rune.Value);
+ }
+ LineCanvas.Clear ();
+ }
+ }
+
+ ///
+ /// Event invoked when the content area of the View is to be drawn.
+ ///
+ ///
+ ///
+ /// Will be invoked before any subviews added with have been drawn.
+ ///
+ ///
+ /// Rect provides the view-relative rectangle describing the currently visible viewport into the .
+ ///
+ ///
+ public event EventHandler DrawContent;
+
+ ///
+ /// Enables overrides to draw infinitely scrolled content and/or a background behind added controls.
+ ///
+ /// The view-relative rectangle describing the currently visible viewport into the
+ ///
+ /// This method will be called before any subviews added with have been drawn.
+ ///
+ public virtual void OnDrawContent (Rect contentArea)
+ {
+ // TODO: Make DrawContent a cancelable event
+ // if (!DrawContent?.Invoke(this, new DrawEventArgs (viewport)) {
+ DrawContent?.Invoke (this, new DrawEventArgs (contentArea));
+
+ if (!ustring.IsNullOrEmpty (TextFormatter.Text)) {
+ if (TextFormatter != null) {
+ TextFormatter.NeedsFormat = true;
+ }
+ // This should NOT clear
+ TextFormatter?.Draw (ViewToScreen (contentArea), HasFocus ? GetFocusColor () : GetNormalColor (),
+ HasFocus ? ColorScheme.HotFocus : GetHotNormalColor (),
+ Rect.Empty, false);
+ SetSubViewNeedsDisplay ();
+ }
+ }
+
+ ///
+ /// Event invoked when the content area of the View is completed drawing.
+ ///
+ ///
+ ///
+ /// Will be invoked after any subviews removed with have been completed drawing.
+ ///
+ ///
+ /// Rect provides the view-relative rectangle describing the currently visible viewport into the .
+ ///
+ ///
+ public event EventHandler DrawContentComplete;
+
+ ///
+ /// Enables overrides after completed drawing infinitely scrolled content and/or a background behind removed controls.
+ ///
+ /// The view-relative rectangle describing the currently visible viewport into the
+ ///
+ /// This method will be called after any subviews removed with have been completed drawing.
+ ///
+ public virtual void OnDrawContentComplete (Rect viewport)
+ {
+ DrawContentComplete?.Invoke (this, new DrawEventArgs (viewport));
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Terminal.Gui/View/ViewKeyboard.cs b/Terminal.Gui/View/ViewKeyboard.cs
new file mode 100644
index 0000000000..4f23603d94
--- /dev/null
+++ b/Terminal.Gui/View/ViewKeyboard.cs
@@ -0,0 +1,464 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using NStack;
+
+namespace Terminal.Gui {
+ public partial class View {
+ ShortcutHelper _shortcutHelper;
+
+ ///
+ /// Event invoked when the is changed.
+ ///
+ public event EventHandler HotKeyChanged;
+
+ Key _hotKey = Key.Null;
+
+ ///
+ /// Gets or sets the HotKey defined for this view. A user pressing HotKey on the keyboard while this view has focus will cause the Clicked event to fire.
+ ///
+ public virtual Key HotKey {
+ get => _hotKey;
+ set {
+ if (_hotKey != value) {
+ var v = value == Key.Unknown ? Key.Null : value;
+ if (_hotKey != Key.Null && ContainsKeyBinding (Key.Space | _hotKey)) {
+ if (v == Key.Null) {
+ ClearKeybinding (Key.Space | _hotKey);
+ } else {
+ ReplaceKeyBinding (Key.Space | _hotKey, Key.Space | v);
+ }
+ } else if (v != Key.Null) {
+ AddKeyBinding (Key.Space | v, Command.Accept);
+ }
+ _hotKey = TextFormatter.HotKey = v;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'.
+ ///
+ public virtual Rune HotKeySpecifier {
+ get {
+ if (TextFormatter != null) {
+ return TextFormatter.HotKeySpecifier;
+ } else {
+ return new Rune ('\xFFFF');
+ }
+ }
+ set {
+ TextFormatter.HotKeySpecifier = value;
+ SetHotKey ();
+ }
+ }
+
+ ///
+ /// This is the global setting that can be used as a global shortcut to invoke an action if provided.
+ ///
+ public Key Shortcut {
+ get => _shortcutHelper.Shortcut;
+ set {
+ if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == Key.Null)) {
+ _shortcutHelper.Shortcut = value;
+ }
+ }
+ }
+
+ ///
+ /// The keystroke combination used in the as string.
+ ///
+ public ustring ShortcutTag => ShortcutHelper.GetShortcutTag (_shortcutHelper.Shortcut);
+
+ ///
+ /// The action to run if the is defined.
+ ///
+ public virtual Action ShortcutAction { get; set; }
+
+ // This is null, and allocated on demand.
+ List _tabIndexes;
+
+ ///
+ /// Configurable keybindings supported by the control
+ ///
+ private Dictionary KeyBindings { get; set; } = new Dictionary ();
+ private Dictionary> CommandImplementations { get; set; } = new Dictionary> ();
+
+ ///
+ /// This returns a tab index list of the subviews contained by this view.
+ ///
+ /// The tabIndexes.
+ public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty;
+
+ int _tabIndex = -1;
+
+ ///
+ /// Indicates the index of the current from the list.
+ ///
+ public int TabIndex {
+ get { return _tabIndex; }
+ set {
+ if (!CanFocus) {
+ _tabIndex = -1;
+ return;
+ } else if (SuperView?._tabIndexes == null || SuperView?._tabIndexes.Count == 1) {
+ _tabIndex = 0;
+ return;
+ } else if (_tabIndex == value) {
+ return;
+ }
+ _tabIndex = value > SuperView._tabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 : value < 0 ? 0 : value;
+ _tabIndex = GetTabIndex (_tabIndex);
+ if (SuperView._tabIndexes.IndexOf (this) != _tabIndex) {
+ SuperView._tabIndexes.Remove (this);
+ SuperView._tabIndexes.Insert (_tabIndex, this);
+ SetTabIndex ();
+ }
+ }
+ }
+
+ int GetTabIndex (int idx)
+ {
+ var i = 0;
+ foreach (var v in SuperView._tabIndexes) {
+ if (v._tabIndex == -1 || v == this) {
+ continue;
+ }
+ i++;
+ }
+ return Math.Min (i, idx);
+ }
+
+ void SetTabIndex ()
+ {
+ var i = 0;
+ foreach (var v in SuperView._tabIndexes) {
+ if (v._tabIndex == -1) {
+ continue;
+ }
+ v._tabIndex = i;
+ i++;
+ }
+ }
+
+ bool _tabStop = true;
+
+ ///
+ /// This only be if the is also
+ /// and the focus can be avoided by setting this to
+ ///
+ public bool TabStop {
+ get => _tabStop;
+ set {
+ if (_tabStop == value) {
+ return;
+ }
+ _tabStop = CanFocus && value;
+ }
+ }
+
+ int _oldTabIndex;
+
+ ///
+ /// Invoked when a character key is pressed and occurs after the key up event.
+ ///
+ public event EventHandler KeyPress;
+
+ ///
+ public override bool ProcessKey (KeyEvent keyEvent)
+ {
+ if (!Enabled) {
+ return false;
+ }
+
+ var args = new KeyEventEventArgs (keyEvent);
+ KeyPress?.Invoke (this, args);
+ if (args.Handled)
+ return true;
+ if (Focused?.Enabled == true) {
+ Focused?.KeyPress?.Invoke (this, args);
+ if (args.Handled)
+ return true;
+ }
+
+ return Focused?.Enabled == true && Focused?.ProcessKey (keyEvent) == true;
+ }
+
+ ///
+ /// Invokes any binding that is registered on this
+ /// and matches the
+ ///
+ /// The key event passed.
+ protected bool? InvokeKeybindings (KeyEvent keyEvent)
+ {
+ bool? toReturn = null;
+
+ if (KeyBindings.ContainsKey (keyEvent.Key)) {
+
+ foreach (var command in KeyBindings [keyEvent.Key]) {
+
+ if (!CommandImplementations.ContainsKey (command)) {
+ throw new NotSupportedException ($"A KeyBinding was set up for the command {command} ({keyEvent.Key}) but that command is not supported by this View ({GetType ().Name})");
+ }
+
+ // each command has its own return value
+ var thisReturn = CommandImplementations [command] ();
+
+ // if we haven't got anything yet, the current command result should be used
+ if (toReturn == null) {
+ toReturn = thisReturn;
+ }
+
+ // if ever see a true then that's what we will return
+ if (thisReturn ?? false) {
+ toReturn = true;
+ }
+ }
+ }
+
+ return toReturn;
+ }
+
+ ///
+ /// Adds a new key combination that will trigger the given
+ /// (if supported by the View - see )
+ ///
+ /// If the key is already bound to a different it will be
+ /// rebound to this one
+ /// Commands are only ever applied to the current (i.e. this feature
+ /// cannot be used to switch focus to another view and perform multiple commands there)
+ ///
+ ///
+ /// The command(s) to run on the when is pressed.
+ /// When specifying multiple commands, all commands will be applied in sequence. The bound strike
+ /// will be consumed if any took effect.
+ public void AddKeyBinding (Key key, params Command [] command)
+ {
+ if (command.Length == 0) {
+ throw new ArgumentException ("At least one command must be specified", nameof (command));
+ }
+
+ if (KeyBindings.ContainsKey (key)) {
+ KeyBindings [key] = command;
+ } else {
+ KeyBindings.Add (key, command);
+ }
+ }
+
+ ///
+ /// Replaces a key combination already bound to .
+ ///
+ /// The key to be replaced.
+ /// The new key to be used.
+ protected void ReplaceKeyBinding (Key fromKey, Key toKey)
+ {
+ if (KeyBindings.ContainsKey (fromKey)) {
+ var value = KeyBindings [fromKey];
+ KeyBindings.Remove (fromKey);
+ KeyBindings [toKey] = value;
+ }
+ }
+
+ ///
+ /// Checks if the key binding already exists.
+ ///
+ /// The key to check.
+ /// If the key already exist, otherwise.
+ public bool ContainsKeyBinding (Key key)
+ {
+ return KeyBindings.ContainsKey (key);
+ }
+
+ ///
+ /// Removes all bound keys from the View and resets the default bindings.
+ ///
+ public void ClearKeybindings ()
+ {
+ KeyBindings.Clear ();
+ }
+
+ ///
+ /// Clears the existing keybinding (if any) for the given .
+ ///
+ ///
+ public void ClearKeybinding (Key key)
+ {
+ KeyBindings.Remove (key);
+ }
+
+ ///
+ /// Removes all key bindings that trigger the given command. Views can have multiple different
+ /// keys bound to the same command and this method will clear all of them.
+ ///
+ ///
+ public void ClearKeybinding (params Command [] command)
+ {
+ foreach (var kvp in KeyBindings.Where (kvp => kvp.Value.SequenceEqual (command)).ToArray ()) {
+ KeyBindings.Remove (kvp.Key);
+ }
+ }
+
+ ///
+ /// States that the given supports a given
+ /// and what to perform to make that command happen
+ ///
+ /// If the already has an implementation the
+ /// will replace the old one
+ ///
+ /// The command.
+ /// The function.
+ protected void AddCommand (Command command, Func f)
+ {
+ // if there is already an implementation of this command
+ if (CommandImplementations.ContainsKey (command)) {
+ // replace that implementation
+ CommandImplementations [command] = f;
+ } else {
+ // else record how to perform the action (this should be the normal case)
+ CommandImplementations.Add (command, f);
+ }
+ }
+
+ ///
+ /// Returns all commands that are supported by this .
+ ///
+ ///
+ public IEnumerable GetSupportedCommands ()
+ {
+ return CommandImplementations.Keys;
+ }
+
+ ///
+ /// Gets the key used by a command.
+ ///
+ /// The command to search.
+ /// The used by a
+ public Key GetKeyFromCommand (params Command [] command)
+ {
+ return KeyBindings.First (kb => kb.Value.SequenceEqual (command)).Key;
+ }
+
+ ///
+ public override bool ProcessHotKey (KeyEvent keyEvent)
+ {
+ if (!Enabled) {
+ return false;
+ }
+
+ var args = new KeyEventEventArgs (keyEvent);
+ if (MostFocused?.Enabled == true) {
+ MostFocused?.KeyPress?.Invoke (this, args);
+ if (args.Handled)
+ return true;
+ }
+ if (MostFocused?.Enabled == true && MostFocused?.ProcessKey (keyEvent) == true)
+ return true;
+ if (_subviews == null || _subviews.Count == 0)
+ return false;
+
+ foreach (var view in _subviews)
+ if (view.Enabled && view.ProcessHotKey (keyEvent))
+ return true;
+ return false;
+ }
+
+ ///
+ public override bool ProcessColdKey (KeyEvent keyEvent)
+ {
+ if (!Enabled) {
+ return false;
+ }
+
+ var args = new KeyEventEventArgs (keyEvent);
+ KeyPress?.Invoke (this, args);
+ if (args.Handled)
+ return true;
+ if (MostFocused?.Enabled == true) {
+ MostFocused?.KeyPress?.Invoke (this, args);
+ if (args.Handled)
+ return true;
+ }
+ if (MostFocused?.Enabled == true && MostFocused?.ProcessKey (keyEvent) == true)
+ return true;
+ if (_subviews == null || _subviews.Count == 0)
+ return false;
+
+ foreach (var view in _subviews)
+ if (view.Enabled && view.ProcessColdKey (keyEvent))
+ return true;
+ return false;
+ }
+
+ ///
+ /// Invoked when a key is pressed.
+ ///
+ public event EventHandler KeyDown;
+
+ ///
+ public override bool OnKeyDown (KeyEvent keyEvent)
+ {
+ if (!Enabled) {
+ return false;
+ }
+
+ var args = new KeyEventEventArgs (keyEvent);
+ KeyDown?.Invoke (this, args);
+ if (args.Handled) {
+ return true;
+ }
+ if (Focused?.Enabled == true) {
+ Focused.KeyDown?.Invoke (this, args);
+ if (args.Handled) {
+ return true;
+ }
+ if (Focused?.OnKeyDown (keyEvent) == true) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Invoked when a key is released.
+ ///
+ public event EventHandler KeyUp;
+
+ ///
+ public override bool OnKeyUp (KeyEvent keyEvent)
+ {
+ if (!Enabled) {
+ return false;
+ }
+
+ var args = new KeyEventEventArgs (keyEvent);
+ KeyUp?.Invoke (this, args);
+ if (args.Handled) {
+ return true;
+ }
+ if (Focused?.Enabled == true) {
+ Focused.KeyUp?.Invoke (this, args);
+ if (args.Handled) {
+ return true;
+ }
+ if (Focused?.OnKeyUp (keyEvent) == true) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ void SetHotKey ()
+ {
+ if (TextFormatter == null) {
+ return; // throw new InvalidOperationException ("Can't set HotKey unless a TextFormatter has been created");
+ }
+ TextFormatter.FindHotKey (_text, HotKeySpecifier, true, out _, out var hk);
+ if (_hotKey != hk) {
+ HotKey = hk;
+ }
+ }
+ }
+}
diff --git a/Terminal.Gui/View/ViewLayout.cs b/Terminal.Gui/View/ViewLayout.cs
new file mode 100644
index 0000000000..187d3cb6c5
--- /dev/null
+++ b/Terminal.Gui/View/ViewLayout.cs
@@ -0,0 +1,1157 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using NStack;
+
+namespace Terminal.Gui {
+ ///
+ /// Determines the LayoutStyle for a , if Absolute, during , the
+ /// value from the will be used, if the value is Computed, then
+ /// will be updated from the X, Y objects and the Width and Height objects.
+ ///
+ public enum LayoutStyle {
+ ///
+ /// The position and size of the view are based .
+ ///
+ Absolute,
+
+ ///
+ /// The position and size of the view will be computed based on
+ /// , , , and . will
+ /// provide the absolute computed values.
+ ///
+ Computed
+ }
+
+ public partial class View {
+
+ // The frame for the object. Superview relative.
+ Rect _frame;
+
+ ///
+ /// Gets or sets the frame for the view. The frame is relative to the view's container ().
+ ///
+ /// The frame.
+ ///
+ ///
+ /// Change the Frame when using the layout style to move or resize views.
+ ///
+ ///
+ /// Altering the Frame of a view will trigger the redrawing of the
+ /// view as well as the redrawing of the affected regions of the .
+ ///
+ ///
+ public virtual Rect Frame {
+ get => _frame;
+ set {
+ _frame = new Rect (value.X, value.Y, Math.Max (value.Width, 0), Math.Max (value.Height, 0));
+ if (IsInitialized || LayoutStyle == LayoutStyle.Absolute) {
+ LayoutFrames ();
+ TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
+ SetNeedsLayout ();
+ SetNeedsDisplay ();
+ }
+ }
+ }
+
+ ///
+ /// The Thickness that separates a View from other SubViews of the same SuperView.
+ /// The Margin is not part of the View's content and is not clipped by the View's Clip Area.
+ ///
+ public Frame Margin { get; private set; }
+
+ ///
+ /// Thickness where a visual border (drawn using line-drawing glyphs) and the Title are drawn.
+ /// The Border expands inward; in other words if `Border.Thickness.Top == 2` the border and
+ /// title will take up the first row and the second row will be filled with spaces.
+ /// The Border is not part of the View's content and is not clipped by the View's `ClipArea`.
+ ///
+ ///
+ /// provides a simple helper for turning a simple border frame on or off.
+ ///
+ public Frame Border { get; private set; }
+
+ ///
+ /// Gets or sets whether the view has a one row/col thick border.
+ ///
+ ///
+ ///
+ /// This is a helper for manipulating the view's . Setting this property to any value other than
+ /// is equivalent to setting 's
+ /// to `1` and to the value.
+ ///
+ ///
+ /// Setting this property to is equivalent to setting 's
+ /// to `0` and to .
+ ///
+ ///
+ /// For more advanced customization of the view's border, manipulate see directly.
+ ///
+ ///
+ public LineStyle BorderStyle {
+ get {
+ return Border?.BorderStyle ?? LineStyle.None;
+ }
+ set {
+ if (Border == null) {
+ throw new InvalidOperationException ("Border is null; this is likely a bug.");
+ }
+ if (value != LineStyle.None) {
+ Border.Thickness = new Thickness (1);
+ } else {
+ Border.Thickness = new Thickness (0);
+ }
+ Border.BorderStyle = value;
+ LayoutFrames ();
+ SetNeedsLayout ();
+ }
+ }
+
+ ///
+ /// Means the Thickness inside of an element that offsets the `Content` from the Border.
+ /// Padding is `{0, 0, 0, 0}` by default. Padding is not part of the View's content and is not clipped by the View's `ClipArea`.
+ ///
+ ///
+ /// (NOTE: in v1 `Padding` is OUTSIDE of the `Border`).
+ ///
+ public Frame Padding { get; private set; }
+
+ ///
+ /// Helper to get the total thickness of the , , and .
+ ///
+ /// A thickness that describes the sum of the Frames' thicknesses.
+ public Thickness GetFramesThickness ()
+ {
+ var left = Margin.Thickness.Left + Border.Thickness.Left + Padding.Thickness.Left;
+ var top = Margin.Thickness.Top + Border.Thickness.Top + Padding.Thickness.Top;
+ var right = Margin.Thickness.Right + Border.Thickness.Right + Padding.Thickness.Right;
+ var bottom = Margin.Thickness.Bottom + Border.Thickness.Bottom + Padding.Thickness.Bottom;
+ return new Thickness (left, top, right, bottom);
+ }
+
+ ///
+ /// Helper to get the X and Y offset of the Bounds from the Frame. This is the sum of the Left and Top properties of
+ /// , and .
+ ///
+ public Point GetBoundsOffset () => new Point (Padding?.Thickness.GetInside (Padding.Frame).X ?? 0, Padding?.Thickness.GetInside (Padding.Frame).Y ?? 0);
+
+ ///
+ /// Creates the view's objects. This internal method is overridden by Frame to do nothing
+ /// to prevent recursion during View construction.
+ ///
+ internal virtual void CreateFrames ()
+ {
+ void ThicknessChangedHandler (object sender, EventArgs e)
+ {
+ LayoutFrames ();
+ SetNeedsLayout ();
+ SetNeedsDisplay ();
+ }
+
+ if (Margin != null) {
+ Margin.ThicknessChanged -= ThicknessChangedHandler;
+ Margin.Dispose ();
+ }
+ Margin = new Frame () { Id = "Margin", Thickness = new Thickness (0) };
+ Margin.ThicknessChanged += ThicknessChangedHandler;
+ Margin.Parent = this;
+
+ if (Border != null) {
+ Border.ThicknessChanged -= ThicknessChangedHandler;
+ Border.Dispose ();
+ }
+ Border = new Frame () { Id = "Border", Thickness = new Thickness (0) };
+ Border.ThicknessChanged += ThicknessChangedHandler;
+ Border.Parent = this;
+
+ // TODO: Create View.AddAdornment
+
+ if (Padding != null) {
+ Padding.ThicknessChanged -= ThicknessChangedHandler;
+ Padding.Dispose ();
+ }
+ Padding = new Frame () { Id = "Padding", Thickness = new Thickness (0) };
+ Padding.ThicknessChanged += ThicknessChangedHandler;
+ Padding.Parent = this;
+ }
+
+ LayoutStyle _layoutStyle;
+
+ ///
+ /// Controls how the View's is computed during the LayoutSubviews method, if the style is set to
+ /// ,
+ /// LayoutSubviews does not change the . If the style is
+ /// the is updated using
+ /// the , , , and properties.
+ ///
+ /// The layout style.
+ public LayoutStyle LayoutStyle {
+ get => _layoutStyle;
+ set {
+ _layoutStyle = value;
+ SetNeedsLayout ();
+ }
+ }
+
+ ///
+ /// The View-relative rectangle where View content is displayed. SubViews are positioned relative to
+ /// Bounds.Location (which is always (0, 0)) and clips drawing to
+ /// Bounds.Size.
+ ///
+ /// The bounds.
+ ///
+ ///
+ /// The of Bounds is always (0, 0). To obtain the offset of the Bounds from the Frame use
+ /// .
+ ///
+ ///
+ public virtual Rect Bounds {
+ get {
+#if DEBUG
+ if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) {
+ Debug.WriteLine ($"WARNING: Bounds is being accessed before the View has been initialized. This is likely a bug. View: {this}");
+ }
+#endif // DEBUG
+ var frameRelativeBounds = Padding?.Thickness.GetInside (Padding.Frame) ?? new Rect (default, Frame.Size);
+ return new Rect (default, frameRelativeBounds.Size);
+ }
+ set {
+ // BUGBUG: Margin etc.. can be null (if typeof(Frame))
+ Frame = new Rect (Frame.Location,
+ new Size (
+ value.Size.Width + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal,
+ value.Size.Height + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical
+ )
+ );
+ }
+ }
+
+ // Diagnostics to highlight when X or Y is read before the view has been initialized
+ private Pos VerifyIsIntialized (Pos pos)
+ {
+#if DEBUG
+ if (LayoutStyle == LayoutStyle.Computed && (!IsInitialized)) {
+ Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; position is indeterminate {pos}. This is likely a bug.");
+ }
+#endif // DEBUG
+ return pos;
+ }
+
+ // Diagnostics to highlight when Width or Height is read before the view has been initialized
+ private Dim VerifyIsIntialized (Dim dim)
+ {
+#if DEBUG
+ if (LayoutStyle == LayoutStyle.Computed && (!IsInitialized)) {
+ Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; dimension is indeterminate: {dim}. This is likely a bug.");
+ }
+#endif // DEBUG
+ return dim;
+ }
+
+ Pos _x, _y;
+
+ ///
+ /// Gets or sets the X position for the view (the column). Only used if the is .
+ ///
+ /// The X Position.
+ ///
+ /// If is changing this property has no effect and its value is indeterminate.
+ ///
+ public Pos X {
+ get => VerifyIsIntialized (_x);
+ set {
+ if (ForceValidatePosDim && !ValidatePosDim (_x, value)) {
+ throw new ArgumentException ();
+ }
+
+ _x = value;
+
+ OnResizeNeeded ();
+ }
+ }
+
+ ///
+ /// Gets or sets the Y position for the view (the row). Only used if the is .
+ ///
+ /// The y position (line).
+ ///
+ /// If is changing this property has no effect and its value is indeterminate.
+ ///
+ public Pos Y {
+ get => VerifyIsIntialized (_y);
+ set {
+ if (ForceValidatePosDim && !ValidatePosDim (_y, value)) {
+ throw new ArgumentException ();
+ }
+
+ _y = value;
+
+ OnResizeNeeded ();
+ }
+ }
+ Dim _width, _height;
+
+ ///
+ /// Gets or sets the width of the view. Only used the is .
+ ///
+ /// The width.
+ ///
+ /// If is changing this property has no effect and its value is indeterminate.
+ ///
+ public Dim Width {
+ get => VerifyIsIntialized (_width);
+ set {
+ if (ForceValidatePosDim && !ValidatePosDim (_width, value)) {
+ throw new ArgumentException ("ForceValidatePosDim is enabled", nameof (Width));
+ }
+
+ _width = value;
+
+ if (ForceValidatePosDim) {
+ var isValidNewAutSize = AutoSize && IsValidAutoSizeWidth (_width);
+
+ if (IsAdded && AutoSize && !isValidNewAutSize) {
+ throw new InvalidOperationException ("Must set AutoSize to false before set the Width.");
+ }
+ }
+ OnResizeNeeded ();
+ }
+ }
+
+ ///
+ /// Gets or sets the height of the view. Only used the is .
+ ///
+ /// The height.
+ /// If is changing this property has no effect and its value is indeterminate.
+ public Dim Height {
+ get => VerifyIsIntialized (_height);
+ set {
+ if (ForceValidatePosDim && !ValidatePosDim (_height, value)) {
+ throw new ArgumentException ("ForceValidatePosDim is enabled", nameof (Height));
+ }
+
+ _height = value;
+
+ if (ForceValidatePosDim) {
+ var isValidNewAutSize = AutoSize && IsValidAutoSizeHeight (_height);
+
+ if (IsAdded && AutoSize && !isValidNewAutSize) {
+ throw new InvalidOperationException ("Must set AutoSize to false before set the Height.");
+ }
+ }
+ OnResizeNeeded ();
+ }
+ }
+
+ ///
+ /// Forces validation with layout
+ /// to avoid breaking the and settings.
+ ///
+ public bool ForceValidatePosDim { get; set; }
+
+ bool ValidatePosDim (object oldValue, object newValue)
+ {
+ if (!IsInitialized || _layoutStyle == LayoutStyle.Absolute || oldValue == null || oldValue.GetType () == newValue.GetType () || this is Toplevel) {
+ return true;
+ }
+ if (_layoutStyle == LayoutStyle.Computed) {
+ if (oldValue.GetType () != newValue.GetType () && !(newValue is Pos.PosAbsolute || newValue is Dim.DimAbsolute)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // BUGBUG: This API is broken - It should be renamed to `GetMinimumBoundsForFrame and
+ // should not assume Frame.Height == Bounds.Height
+ ///
+ /// Gets the minimum dimensions required to fit the View's , factoring in .
+ ///
+ /// The minimum dimensions required.
+ /// if the dimensions fit within the View's , otherwise.
+ ///
+ /// Always returns if is or
+ /// if (Horizontal) or (Vertical) are not not set or zero.
+ /// Does not take into account word wrapping.
+ ///
+ public bool GetMinimumBounds (out Size size)
+ {
+ size = Bounds.Size;
+
+ if (!AutoSize && !ustring.IsNullOrEmpty (TextFormatter.Text)) {
+ switch (TextFormatter.IsVerticalDirection (TextDirection)) {
+ case true:
+ var colWidth = TextFormatter.GetSumMaxCharWidth (new List { TextFormatter.Text }, 0, 1);
+ // TODO: v2 - This uses frame.Width; it should only use Bounds
+ if (_frame.Width < colWidth &&
+ (Width == null ||
+ (Bounds.Width >= 0 &&
+ Width is Dim.DimAbsolute &&
+ Width.Anchor (0) >= 0 &&
+ Width.Anchor (0) < colWidth))) {
+ size = new Size (colWidth, Bounds.Height);
+ return true;
+ }
+ break;
+ default:
+ if (_frame.Height < 1 &&
+ (Height == null ||
+ (Height is Dim.DimAbsolute &&
+ Height.Anchor (0) == 0))) {
+ size = new Size (Bounds.Width, 1);
+ return true;
+ }
+ break;
+ }
+ }
+ return false;
+ }
+
+ // BUGBUG - v2 - Should be renamed "SetBoundsToFitFrame"
+ ///
+ /// Sets the size of the View to the minimum width or height required to fit (see .
+ ///
+ /// if the size was changed, if
+ /// will not fit.
+ public bool SetMinWidthHeight ()
+ {
+ if (GetMinimumBounds (out Size size)) {
+ _frame = new Rect (_frame.Location, size);
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Called whenever the view needs to be resized. Sets and
+ /// triggers a call. ///
+ ///
+ ///
+ /// Can be overridden if the view resize behavior is different than the default.
+ ///
+ protected virtual void OnResizeNeeded ()
+ {
+ var actX = _x is Pos.PosAbsolute ? _x.Anchor (0) : _frame.X;
+ var actY = _y is Pos.PosAbsolute ? _y.Anchor (0) : _frame.Y;
+
+ if (AutoSize) {
+ //if (TextAlignment == TextAlignment.Justified) {
+ // throw new InvalidOperationException ("TextAlignment.Justified cannot be used with AutoSize");
+ //}
+ var s = GetAutoSize ();
+ var w = _width is Dim.DimAbsolute && _width.Anchor (0) > s.Width ? _width.Anchor (0) : s.Width;
+ var h = _height is Dim.DimAbsolute && _height.Anchor (0) > s.Height ? _height.Anchor (0) : s.Height;
+ _frame = new Rect (new Point (actX, actY), new Size (w, h)); // Set frame, not Frame!
+ } else {
+ var w = _width is Dim.DimAbsolute ? _width.Anchor (0) : _frame.Width;
+ var h = _height is Dim.DimAbsolute ? _height.Anchor (0) : _frame.Height;
+ // BUGBUG: v2 - ? - If layoutstyle is absolute, this overwrites the current frame h/w with 0. Hmmm...
+ // This is needed for DimAbsolute values by setting the frame before LayoutSubViews.
+ _frame = new Rect (new Point (actX, actY), new Size (w, h)); // Set frame, not Frame!
+ }
+ //// BUGBUG: I think these calls are redundant or should be moved into just the AutoSize case
+ if (IsInitialized || LayoutStyle == LayoutStyle.Absolute) {
+ SetMinWidthHeight ();
+ LayoutFrames ();
+ TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
+ SetNeedsLayout ();
+ SetNeedsDisplay ();
+ }
+ }
+
+ internal bool LayoutNeeded { get; private set; } = true;
+
+ internal void SetNeedsLayout ()
+ {
+ if (LayoutNeeded)
+ return;
+ LayoutNeeded = true;
+ if (SuperView == null)
+ return;
+ SuperView.SetNeedsLayout ();
+ foreach (var view in Subviews) {
+ view.SetNeedsLayout ();
+ }
+ TextFormatter.NeedsFormat = true;
+ }
+
+ ///
+ /// Removes the setting on this view.
+ ///
+ protected void ClearLayoutNeeded ()
+ {
+ LayoutNeeded = false;
+ }
+
+ ///
+ /// Converts a point from screen-relative coordinates to view-relative coordinates.
+ ///
+ /// The mapped point.
+ /// X screen-coordinate point.
+ /// Y screen-coordinate point.
+ public Point ScreenToView (int x, int y)
+ {
+ if (SuperView == null) {
+ return new Point (x - Frame.X, y - _frame.Y);
+ } else {
+ var parent = SuperView.ScreenToView (x, y);
+ return new Point (parent.X - _frame.X, parent.Y - _frame.Y);
+ }
+ }
+
+ ///
+ /// Converts a point from screen-relative coordinates to bounds-relative coordinates.
+ ///
+ /// The mapped point.
+ /// X screen-coordinate point.
+ /// Y screen-coordinate point.
+ public Point ScreenToBounds (int x, int y)
+ {
+ if (SuperView == null) {
+ var boundsOffset = GetBoundsOffset ();
+ return new Point (x - Frame.X + boundsOffset.X, y - Frame.Y + boundsOffset.Y);
+ } else {
+ var parent = SuperView.ScreenToView (x, y);
+ return new Point (parent.X - _frame.X, parent.Y - _frame.Y);
+ }
+ }
+
+ ///
+ /// Converts a view-relative location to a screen-relative location (col,row). The output is optionally clamped to the screen dimensions.
+ ///
+ /// View-relative column.
+ /// View-relative row.
+ /// Absolute column; screen-relative.
+ /// Absolute row; screen-relative.
+ /// If , and will be clamped to the
+ /// screen dimensions (they never be negative and will always be less than to and
+ /// , respectively.
+ public virtual void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clamped = true)
+ {
+ var boundsOffset = GetBoundsOffset ();
+ rcol = col + Frame.X + boundsOffset.X;
+ rrow = row + Frame.Y + boundsOffset.Y;
+
+ var super = SuperView;
+ while (super != null) {
+ boundsOffset = super.GetBoundsOffset ();
+ rcol += super.Frame.X + boundsOffset.X;
+ rrow += super.Frame.Y + boundsOffset.Y;
+ super = super.SuperView;
+ }
+
+ // The following ensures that the cursor is always in the screen boundaries.
+ if (clamped) {
+ rrow = Math.Min (rrow, Driver.Rows - 1);
+ rcol = Math.Min (rcol, Driver.Cols - 1);
+ }
+ }
+
+ ///
+ /// Converts a region in view-relative coordinates to screen-relative coordinates.
+ ///
+ internal Rect ViewToScreen (Rect region)
+ {
+ ViewToScreen (region.X, region.Y, out var x, out var y, clamped: false);
+ return new Rect (x, y, region.Width, region.Height);
+ }
+
+
+ ///
+ /// Sets the View's to the frame-relative coordinates if its container. The
+ /// container size and location are specified by and are relative to the
+ /// View's superview.
+ ///
+ /// The supserview-relative rectangle describing View's container (nominally the
+ /// same as this.SuperView.Frame).
+ internal void SetRelativeLayout (Rect superviewFrame)
+ {
+ int newX, newW, newY, newH;
+ var autosize = Size.Empty;
+
+ if (AutoSize) {
+ // Note this is global to this function and used as such within the local functions defined
+ // below. In v2 AutoSize will be re-factored to not need to be dealt with in this function.
+ autosize = GetAutoSize ();
+ }
+
+ // Returns the new dimension (width or height) and location (x or y) for the View given
+ // the superview's Frame.X or Frame.Y
+ // the superview's width or height
+ // the current Pos (View.X or View.Y)
+ // the current Dim (View.Width or View.Height)
+ (int newLocation, int newDimension) GetNewLocationAndDimension (int superviewLocation, int superviewDimension, Pos pos, Dim dim, int autosizeDimension)
+ {
+ int newDimension, newLocation;
+
+ switch (pos) {
+ case Pos.PosCenter:
+ if (dim == null) {
+ newDimension = AutoSize ? autosizeDimension : superviewDimension;
+ } else {
+ newDimension = dim.Anchor (superviewDimension);
+ newDimension = AutoSize && autosizeDimension > newDimension ? autosizeDimension : newDimension;
+ }
+ newLocation = pos.Anchor (superviewDimension - newDimension);
+ break;
+
+ case Pos.PosCombine combine:
+ int left, right;
+ (left, newDimension) = GetNewLocationAndDimension (superviewLocation, superviewDimension, combine.left, dim, autosizeDimension);
+ (right, newDimension) = GetNewLocationAndDimension (superviewLocation, superviewDimension, combine.right, dim, autosizeDimension);
+ if (combine.add) {
+ newLocation = left + right;
+ } else {
+ newLocation = left - right;
+ }
+ newDimension = Math.Max (CalculateNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0);
+ break;
+
+ case Pos.PosAbsolute:
+ case Pos.PosAnchorEnd:
+ case Pos.PosFactor:
+ case Pos.PosFunc:
+ case Pos.PosView:
+ default:
+ newLocation = pos?.Anchor (superviewDimension) ?? 0;
+ newDimension = Math.Max (CalculateNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0);
+ break;
+ }
+ return (newLocation, newDimension);
+ }
+
+ // Recursively calculates the new dimension (width or height) of the given Dim given:
+ // the current location (x or y)
+ // the current dimension (width or height)
+ int CalculateNewDimension (Dim d, int location, int dimension, int autosize)
+ {
+ int newDimension;
+ switch (d) {
+ case null:
+ newDimension = AutoSize ? autosize : dimension;
+ break;
+ case Dim.DimCombine combine:
+ int leftNewDim = CalculateNewDimension (combine.left, location, dimension, autosize);
+ int rightNewDim = CalculateNewDimension (combine.right, location, dimension, autosize);
+ if (combine.add) {
+ newDimension = leftNewDim + rightNewDim;
+ } else {
+ newDimension = leftNewDim - rightNewDim;
+ }
+ newDimension = AutoSize && autosize > newDimension ? autosize : newDimension;
+ break;
+
+ case Dim.DimFactor factor when !factor.IsFromRemaining ():
+ newDimension = d.Anchor (dimension);
+ newDimension = AutoSize && autosize > newDimension ? autosize : newDimension;
+ break;
+
+ case Dim.DimFill:
+ default:
+ newDimension = Math.Max (d.Anchor (dimension - location), 0);
+ newDimension = AutoSize && autosize > newDimension ? autosize : newDimension;
+ break;
+ }
+
+ return newDimension;
+ }
+
+ // horizontal
+ (newX, newW) = GetNewLocationAndDimension (superviewFrame.X, superviewFrame.Width, _x, _width, autosize.Width);
+
+ // vertical
+ (newY, newH) = GetNewLocationAndDimension (superviewFrame.Y, superviewFrame.Height, _y, _height, autosize.Height);
+
+ var r = new Rect (newX, newY, newW, newH);
+ if (Frame != r) {
+ Frame = r;
+ // BUGBUG: Why is this AFTER setting Frame? Seems duplicative.
+ if (!SetMinWidthHeight ()) {
+ TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
+ }
+ }
+ }
+
+ ///
+ /// Fired after the View's method has completed.
+ ///
+ ///
+ /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed.
+ ///
+ public event EventHandler LayoutStarted;
+
+ ///
+ /// Raises the event. Called from before any subviews have been laid out.
+ ///
+ internal virtual void OnLayoutStarted (LayoutEventArgs args)
+ {
+ LayoutStarted?.Invoke (this, args);
+ }
+
+ ///
+ /// Fired after the View's method has completed.
+ ///
+ ///
+ /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed.
+ ///
+ public event EventHandler LayoutComplete;
+
+ ///
+ /// Event called only once when the is being initialized for the first time.
+ /// Allows configurations and assignments to be performed before the being shown.
+ /// This derived from to allow notify all the views that are being initialized.
+ ///
+ public event EventHandler Initialized;
+
+ ///
+ /// Raises the event. Called from before all sub-views have been laid out.
+ ///
+ internal virtual void OnLayoutComplete (LayoutEventArgs args)
+ {
+ LayoutComplete?.Invoke (this, args);
+ }
+
+ internal void CollectPos (Pos pos, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges)
+ {
+ switch (pos) {
+ case Pos.PosView pv:
+ // See #2461
+ //if (!from.InternalSubviews.Contains (pv.Target)) {
+ // throw new InvalidOperationException ($"View {pv.Target} is not a subview of {from}");
+ //}
+ if (pv.Target != this) {
+ nEdges.Add ((pv.Target, from));
+ }
+ return;
+ case Pos.PosCombine pc:
+ CollectPos (pc.left, from, ref nNodes, ref nEdges);
+ CollectPos (pc.right, from, ref nNodes, ref nEdges);
+ break;
+ }
+ }
+
+ internal void CollectDim (Dim dim, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges)
+ {
+ switch (dim) {
+ case Dim.DimView dv:
+ // See #2461
+ //if (!from.InternalSubviews.Contains (dv.Target)) {
+ // throw new InvalidOperationException ($"View {dv.Target} is not a subview of {from}");
+ //}
+ if (dv.Target != this) {
+ nEdges.Add ((dv.Target, from));
+ }
+ return;
+ case Dim.DimCombine dc:
+ CollectDim (dc.left, from, ref nNodes, ref nEdges);
+ CollectDim (dc.right, from, ref nNodes, ref nEdges);
+ break;
+ }
+ }
+
+ internal void CollectAll (View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges)
+ {
+ foreach (var v in from.InternalSubviews) {
+ nNodes.Add (v);
+ if (v._layoutStyle != LayoutStyle.Computed) {
+ continue;
+ }
+ CollectPos (v.X, v, ref nNodes, ref nEdges);
+ CollectPos (v.Y, v, ref nNodes, ref nEdges);
+ CollectDim (v.Width, v, ref nNodes, ref nEdges);
+ CollectDim (v.Height, v, ref nNodes, ref nEdges);
+ }
+ }
+
+ // https://en.wikipedia.org/wiki/Topological_sorting
+ internal static List TopologicalSort (View superView, IEnumerable nodes, ICollection<(View From, View To)> edges)
+ {
+ var result = new List ();
+
+ // Set of all nodes with no incoming edges
+ var noEdgeNodes = new HashSet (nodes.Where (n => edges.All (e => !e.To.Equals (n))));
+
+ while (noEdgeNodes.Any ()) {
+ // remove a node n from S
+ var n = noEdgeNodes.First ();
+ noEdgeNodes.Remove (n);
+
+ // add n to tail of L
+ if (n != superView)
+ result.Add (n);
+
+ // for each node m with an edge e from n to m do
+ foreach (var e in edges.Where (e => e.From.Equals (n)).ToArray ()) {
+ var m = e.To;
+
+ // remove edge e from the graph
+ edges.Remove (e);
+
+ // if m has no other incoming edges then
+ if (edges.All (me => !me.To.Equals (m)) && m != superView) {
+ // insert m into S
+ noEdgeNodes.Add (m);
+ }
+ }
+ }
+
+ if (edges.Any ()) {
+ foreach ((var from, var to) in edges) {
+ if (from == to) {
+ // if not yet added to the result, add it and remove from edge
+ if (result.Find (v => v == from) == null) {
+ result.Add (from);
+ }
+ edges.Remove ((from, to));
+ } else if (from.SuperView == to.SuperView) {
+ // if 'from' is not yet added to the result, add it
+ if (result.Find (v => v == from) == null) {
+ result.Add (from);
+ }
+ // if 'to' is not yet added to the result, add it
+ if (result.Find (v => v == to) == null) {
+ result.Add (to);
+ }
+ // remove from edge
+ edges.Remove ((from, to));
+ } else if (from != superView?.GetTopSuperView (to, from) && !ReferenceEquals (from, to)) {
+ if (ReferenceEquals (from.SuperView, to)) {
+ throw new InvalidOperationException ($"ComputedLayout for \"{superView}\": \"{to}\" references a SubView (\"{from}\").");
+ } else {
+ throw new InvalidOperationException ($"ComputedLayout for \"{superView}\": \"{from}\" linked with \"{to}\" was not found. Did you forget to add it to {superView}?");
+ }
+ }
+ }
+ }
+ // return L (a topologically sorted order)
+ return result;
+ } // TopologicalSort
+
+ ///
+ /// Overriden by to do nothing, as the does not have frames.
+ ///
+ internal virtual void LayoutFrames ()
+ {
+ if (Margin == null) return; // CreateFrames() has not been called yet
+
+ if (Margin.Frame.Size != Frame.Size) {
+ Margin._frame = new Rect (Point.Empty, Frame.Size);
+ Margin.X = 0;
+ Margin.Y = 0;
+ Margin.Width = Frame.Size.Width;
+ Margin.Height = Frame.Size.Height;
+ Margin.SetNeedsLayout ();
+ Margin.LayoutSubviews ();
+ Margin.SetNeedsDisplay ();
+ }
+
+ var border = Margin.Thickness.GetInside (Margin.Frame);
+ if (border != Border.Frame) {
+ Border._frame = new Rect (new Point (border.Location.X, border.Location.Y), border.Size);
+ Border.X = border.Location.X;
+ Border.Y = border.Location.Y;
+ Border.Width = border.Size.Width;
+ Border.Height = border.Size.Height;
+ Border.SetNeedsLayout ();
+ Border.LayoutSubviews ();
+ Border.SetNeedsDisplay ();
+ }
+
+ var padding = Border.Thickness.GetInside (Border.Frame);
+ if (padding != Padding.Frame) {
+ Padding._frame = new Rect (new Point (padding.Location.X, padding.Location.Y), padding.Size);
+ Padding.X = padding.Location.X;
+ Padding.Y = padding.Location.Y;
+ Padding.Width = padding.Size.Width;
+ Padding.Height = padding.Size.Height;
+ Padding.SetNeedsLayout ();
+ Padding.LayoutSubviews ();
+ Padding.SetNeedsDisplay ();
+ }
+ }
+
+ ///
+ /// Invoked when a view starts executing or when the dimensions of the view have changed, for example in
+ /// response to the container view or terminal resizing.
+ ///
+ ///
+ /// Calls (which raises the event) before it returns.
+ ///
+ public virtual void LayoutSubviews ()
+ {
+ if (!LayoutNeeded) {
+ return;
+ }
+
+ LayoutFrames ();
+
+ var oldBounds = Bounds;
+ OnLayoutStarted (new LayoutEventArgs () { OldBounds = oldBounds });
+
+ TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
+
+ // Sort out the dependencies of the X, Y, Width, Height properties
+ var nodes = new HashSet ();
+ var edges = new HashSet<(View, View)> ();
+ CollectAll (this, ref nodes, ref edges);
+ var ordered = View.TopologicalSort (SuperView, nodes, edges);
+ foreach (var v in ordered) {
+ LayoutSubview (v, new Rect (GetBoundsOffset (), Bounds.Size));
+ }
+
+ // If the 'to' is rooted to 'from' and the layoutstyle is Computed it's a special-case.
+ // Use LayoutSubview with the Frame of the 'from'
+ if (SuperView != null && GetTopSuperView () != null && LayoutNeeded && edges.Count > 0) {
+ foreach ((var from, var to) in edges) {
+ LayoutSubview (to, from.Frame);
+ }
+ }
+
+ LayoutNeeded = false;
+
+ OnLayoutComplete (new LayoutEventArgs () { OldBounds = oldBounds });
+ }
+
+ private void LayoutSubview (View v, Rect contentArea)
+ {
+ if (v.LayoutStyle == LayoutStyle.Computed) {
+ v.SetRelativeLayout (contentArea);
+ }
+
+ v.LayoutSubviews ();
+ v.LayoutNeeded = false;
+ }
+
+ bool _autoSize;
+
+ ///
+ /// Gets or sets a flag that determines whether the View will be automatically resized to fit the
+ /// within
+ ///
+ /// The default is . Set to to turn on AutoSize. If then
+ /// and will be used if can fit;
+ /// if won't fit the view will be resized as needed.
+ ///
+ ///
+ /// In addition, if is the new values of and
+ /// must be of the same types of the existing one to avoid breaking the settings.
+ ///
+ ///
+ public virtual bool AutoSize {
+ get => _autoSize;
+ set {
+ var v = ResizeView (value);
+ TextFormatter.AutoSize = v;
+ if (_autoSize != v) {
+ _autoSize = v;
+ TextFormatter.NeedsFormat = true;
+ UpdateTextFormatterText ();
+ OnResizeNeeded ();
+ }
+ }
+ }
+
+ bool ResizeView (bool autoSize)
+ {
+ if (!autoSize) {
+ return false;
+ }
+
+ var aSize = true;
+ var nBoundsSize = GetAutoSize ();
+ if (nBoundsSize != Bounds.Size) {
+ if (ForceValidatePosDim) {
+ aSize = SetWidthHeight (nBoundsSize);
+ } else {
+ Height = nBoundsSize.Height;
+ Width = nBoundsSize.Width; // = new Rect (Bounds.X, Bounds.Y, nBoundsSize.Width, nBoundsSize.Height);
+ }
+ }
+ // BUGBUG: This call may be redundant
+ TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
+ return aSize;
+ }
+
+ ///
+ /// Resizes the View to fit the specified size.
+ ///
+ ///
+ ///
+ bool SetWidthHeight (Size nBounds)
+ {
+ var aSize = false;
+ var canSizeW = TrySetWidth (nBounds.Width - GetHotKeySpecifierLength (), out var rW);
+ var canSizeH = TrySetHeight (nBounds.Height - GetHotKeySpecifierLength (false), out var rH);
+ if (canSizeW) {
+ aSize = true;
+ _width = rW;
+ }
+ if (canSizeH) {
+ aSize = true;
+ _height = rH;
+ }
+ if (aSize) {
+ Bounds = new Rect (Bounds.X, Bounds.Y, canSizeW ? rW : Bounds.Width, canSizeH ? rH : Bounds.Height);
+ }
+
+ return aSize;
+ }
+
+ ///
+ /// Gets the Frame dimensions required to fit using the text specified by the
+ /// property and accounting for any characters.
+ ///
+ /// The required to fit the text.
+ public Size GetAutoSize ()
+ {
+ var rect = TextFormatter.CalcRect (Bounds.X, Bounds.Y, TextFormatter.Text, TextFormatter.Direction);
+ var newWidth = rect.Size.Width - GetHotKeySpecifierLength () + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal;
+ var newHeight = rect.Size.Height - GetHotKeySpecifierLength (false) + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical;
+ return new Size (newWidth, newHeight);
+ }
+
+ bool IsValidAutoSize (out Size autoSize)
+ {
+ var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
+ autoSize = new Size (rect.Size.Width - GetHotKeySpecifierLength (),
+ rect.Size.Height - GetHotKeySpecifierLength (false));
+ return !(ForceValidatePosDim && (!(Width is Dim.DimAbsolute) || !(Height is Dim.DimAbsolute))
+ || _frame.Size.Width != rect.Size.Width - GetHotKeySpecifierLength ()
+ || _frame.Size.Height != rect.Size.Height - GetHotKeySpecifierLength (false));
+ }
+
+ bool IsValidAutoSizeWidth (Dim width)
+ {
+ var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
+ var dimValue = width.Anchor (0);
+ return !(ForceValidatePosDim && (!(width is Dim.DimAbsolute)) || dimValue != rect.Size.Width
+ - GetHotKeySpecifierLength ());
+ }
+
+ bool IsValidAutoSizeHeight (Dim height)
+ {
+ var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
+ var dimValue = height.Anchor (0);
+ return !(ForceValidatePosDim && (!(height is Dim.DimAbsolute)) || dimValue != rect.Size.Height
+ - GetHotKeySpecifierLength (false));
+ }
+
+ ///
+ /// Determines if the View's can be set to a new value.
+ ///
+ ///
+ /// Contains the width that would result if were set to "/>
+ /// if the View's can be changed to the specified value. False otherwise.
+ internal bool TrySetWidth (int desiredWidth, out int resultWidth)
+ {
+ var w = desiredWidth;
+ bool canSetWidth;
+ switch (Width) {
+ case Dim.DimCombine _:
+ case Dim.DimView _:
+ case Dim.DimFill _:
+ // It's a Dim.DimCombine and so can't be assigned. Let it have it's Width anchored.
+ w = Width.Anchor (w);
+ canSetWidth = !ForceValidatePosDim;
+ break;
+ case Dim.DimFactor factor:
+ // Tries to get the SuperView Width otherwise the view Width.
+ var sw = SuperView != null ? SuperView.Frame.Width : w;
+ if (factor.IsFromRemaining ()) {
+ sw -= Frame.X;
+ }
+ w = Width.Anchor (sw);
+ canSetWidth = !ForceValidatePosDim;
+ break;
+ default:
+ canSetWidth = true;
+ break;
+ }
+ resultWidth = w;
+
+ return canSetWidth;
+ }
+
+ ///
+ /// Determines if the View's can be set to a new value.
+ ///
+ ///
+ /// Contains the width that would result if were set to "/>
+ /// if the View's can be changed to the specified value. False otherwise.
+ internal bool TrySetHeight (int desiredHeight, out int resultHeight)
+ {
+ var h = desiredHeight;
+ bool canSetHeight;
+ switch (Height) {
+ case Dim.DimCombine _:
+ case Dim.DimView _:
+ case Dim.DimFill _:
+ // It's a Dim.DimCombine and so can't be assigned. Let it have it's height anchored.
+ h = Height.Anchor (h);
+ canSetHeight = !ForceValidatePosDim;
+ break;
+ case Dim.DimFactor factor:
+ // Tries to get the SuperView height otherwise the view height.
+ var sh = SuperView != null ? SuperView.Frame.Height : h;
+ if (factor.IsFromRemaining ()) {
+ sh -= Frame.Y;
+ }
+ h = Height.Anchor (sh);
+ canSetHeight = !ForceValidatePosDim;
+ break;
+ default:
+ canSetHeight = true;
+ break;
+ }
+ resultHeight = h;
+
+ return canSetHeight;
+ }
+
+ ///
+ /// Finds which view that belong to the superview at the provided location.
+ ///
+ /// The superview where to look for.
+ /// The column location in the superview.
+ /// The row location in the superview.
+ /// The found view screen relative column location.
+ /// The found view screen relative row location.
+ ///
+ /// The view that was found at the and coordinates.
+ /// if no view was found.
+ ///
+ public static View FindDeepestView (View 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 (start.InternalSubviews != null) {
+ int count = start.InternalSubviews.Count;
+ if (count > 0) {
+ var boundsOffset = start.GetBoundsOffset ();
+ var rx = x - (startFrame.X + boundsOffset.X);
+ var ry = y - (startFrame.Y + boundsOffset.Y);
+ for (int i = count - 1; i >= 0; i--) {
+ View v = start.InternalSubviews [i];
+ if (v.Visible && v.Frame.Contains (rx, ry)) {
+ var deep = FindDeepestView (v, rx, ry, out resx, out resy);
+ if (deep == null)
+ return v;
+ return deep;
+ }
+ }
+ }
+ }
+ resx = x - startFrame.X;
+ resy = y - startFrame.Y;
+ return start;
+ }
+ }
+}
diff --git a/Terminal.Gui/View/ViewMouse.cs b/Terminal.Gui/View/ViewMouse.cs
new file mode 100644
index 0000000000..f5e1e8c376
--- /dev/null
+++ b/Terminal.Gui/View/ViewMouse.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using NStack;
+
+namespace Terminal.Gui {
+ public partial class View {
+ ///
+ /// Event fired when the view receives the mouse event for the first time.
+ ///
+ public event EventHandler MouseEnter;
+
+ ///
+ /// Event fired when the view receives a mouse event for the last time.
+ ///
+ public event EventHandler MouseLeave;
+
+ ///
+ /// Event fired when a mouse event is generated.
+ ///
+ public event EventHandler MouseClick;
+
+ ///
+ public override bool OnMouseEnter (MouseEvent mouseEvent)
+ {
+ if (!Enabled) {
+ return true;
+ }
+
+ if (!CanBeVisible (this)) {
+ return false;
+ }
+
+ var args = new MouseEventEventArgs (mouseEvent);
+ MouseEnter?.Invoke (this, args);
+
+ return args.Handled || base.OnMouseEnter (mouseEvent);
+ }
+
+ ///
+ public override bool OnMouseLeave (MouseEvent mouseEvent)
+ {
+ if (!Enabled) {
+ return true;
+ }
+
+ if (!CanBeVisible (this)) {
+ return false;
+ }
+
+ var args = new MouseEventEventArgs (mouseEvent);
+ MouseLeave?.Invoke (this, args);
+
+ return args.Handled || base.OnMouseLeave (mouseEvent);
+ }
+
+ ///
+ /// Method invoked when a mouse event is generated
+ ///
+ ///
+ /// , if the event was handled, otherwise.
+ public virtual bool OnMouseEvent (MouseEvent mouseEvent)
+ {
+ if (!Enabled) {
+ return true;
+ }
+
+ if (!CanBeVisible (this)) {
+ return false;
+ }
+
+ var args = new MouseEventEventArgs (mouseEvent);
+ if (OnMouseClick (args))
+ return true;
+ if (MouseEvent (mouseEvent))
+ return true;
+
+ if (mouseEvent.Flags == MouseFlags.Button1Clicked) {
+ if (CanFocus && !HasFocus && SuperView != null) {
+ SuperView.SetFocus (this);
+ SetNeedsDisplay ();
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Invokes the MouseClick event.
+ ///
+ protected bool OnMouseClick (MouseEventEventArgs args)
+ {
+ if (!Enabled) {
+ return true;
+ }
+
+ MouseClick?.Invoke (this, args);
+ return args.Handled;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this wants mouse position reports.
+ ///
+ /// if want mouse position reports; otherwise, .
+ public virtual bool WantMousePositionReports { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this want continuous button pressed event.
+ ///
+ public virtual bool WantContinuousButtonPressed { get; set; }
+ }
+}
diff --git a/Terminal.Gui/View/ViewSubViews.cs b/Terminal.Gui/View/ViewSubViews.cs
new file mode 100644
index 0000000000..fd9a0f5f2d
--- /dev/null
+++ b/Terminal.Gui/View/ViewSubViews.cs
@@ -0,0 +1,723 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using NStack;
+
+namespace Terminal.Gui {
+ public partial class View {
+ static readonly IList _empty = new List (0).AsReadOnly ();
+
+ View _superView = null;
+
+ ///
+ /// Returns the container for this view, or null if this view has not been added to a container.
+ ///
+ /// The super view.
+ public virtual View SuperView {
+ get {
+ return _superView;
+ }
+ set {
+ throw new NotImplementedException ();
+ }
+ }
+
+ List _subviews; // This is null, and allocated on demand.
+ ///
+ /// This returns a list of the subviews contained by this view.
+ ///
+ /// The subviews.
+ public IList Subviews => _subviews?.AsReadOnly () ?? _empty;
+
+ // Internally, we use InternalSubviews rather than subviews, as we do not expect us
+ // to make the same mistakes our users make when they poke at the Subviews.
+ internal IList InternalSubviews => _subviews ?? _empty;
+
+ ///
+ /// Returns a value indicating if this View is currently on Top (Active)
+ ///
+ public bool IsCurrentTop => Application.Current == this;
+
+ ///
+ /// Event fired when this view is added to another.
+ ///
+ public event EventHandler Added;
+
+ internal bool _addingView;
+
+ ///
+ /// Adds a subview (child) to this view.
+ ///
+ ///
+ /// The Views that have been added to this view can be retrieved via the property.
+ /// See also
+ ///
+ public virtual void Add (View view)
+ {
+ if (view == null) {
+ return;
+ }
+ if (_subviews == null) {
+ _subviews = new List ();
+ }
+ if (_tabIndexes == null) {
+ _tabIndexes = new List ();
+ }
+ _subviews.Add (view);
+ _tabIndexes.Add (view);
+ view._superView = this;
+ if (view.CanFocus) {
+ _addingView = true;
+ if (SuperView?.CanFocus == false) {
+ SuperView._addingView = true;
+ SuperView.CanFocus = true;
+ SuperView._addingView = false;
+ }
+ CanFocus = true;
+ view._tabIndex = _tabIndexes.IndexOf (view);
+ _addingView = false;
+ }
+ if (view.Enabled && !Enabled) {
+ view._oldEnabled = true;
+ view.Enabled = false;
+ }
+ SetNeedsLayout ();
+ SetNeedsDisplay ();
+
+ OnAdded (new SuperViewChangedEventArgs (this, view));
+ if (IsInitialized && !view.IsInitialized) {
+ view.BeginInit ();
+ view.EndInit ();
+ }
+ }
+
+ ///
+ /// Adds the specified views (children) to the view.
+ ///
+ /// Array of one or more views (can be optional parameter).
+ ///
+ /// The Views that have been added to this view can be retrieved via the property.
+ /// See also
+ ///
+ public void Add (params View [] views)
+ {
+ if (views == null) {
+ return;
+ }
+ foreach (var view in views) {
+ Add (view);
+ }
+ }
+
+ ///
+ /// Method invoked when a subview is being added to this view.
+ ///
+ /// Event where is the subview being added.
+ public virtual void OnAdded (SuperViewChangedEventArgs e)
+ {
+ var view = e.Child;
+ view.IsAdded = true;
+ view.OnResizeNeeded ();
+ view._x ??= view._frame.X;
+ view._y ??= view._frame.Y;
+ view._width ??= view._frame.Width;
+ view._height ??= view._frame.Height;
+
+ view.Added?.Invoke (this, e);
+ }
+
+ ///
+ /// Gets information if the view was already added to the .
+ ///
+ public bool IsAdded { get; private set; }
+
+ ///
+ /// Event fired when this view is removed from another.
+ ///
+ public event EventHandler Removed;
+
+ ///
+ /// Removes all subviews (children) added via or from this View.
+ ///
+ public virtual void RemoveAll ()
+ {
+ if (_subviews == null) {
+ return;
+ }
+
+ while (_subviews.Count > 0) {
+ Remove (_subviews [0]);
+ }
+ }
+
+ ///
+ /// Removes a subview added via or from this View.
+ ///
+ ///
+ ///
+ public virtual void Remove (View view)
+ {
+ if (view == null || _subviews == null) return;
+
+ var touched = view.Frame;
+ _subviews.Remove (view);
+ _tabIndexes.Remove (view);
+ view._superView = null;
+ view._tabIndex = -1;
+ SetNeedsLayout ();
+ SetNeedsDisplay ();
+
+ foreach (var v in _subviews) {
+ if (v.Frame.IntersectsWith (touched))
+ view.SetNeedsDisplay ();
+ }
+ OnRemoved (new SuperViewChangedEventArgs (this, view));
+ if (_focused == view) {
+ _focused = null;
+ }
+ }
+
+ ///
+ /// Method invoked when a subview is being removed from this view.
+ ///
+ /// Event args describing the subview being removed.
+ public virtual void OnRemoved (SuperViewChangedEventArgs e)
+ {
+ var view = e.Child;
+ view.IsAdded = false;
+ view.Removed?.Invoke (this, e);
+ }
+
+
+ void PerformActionForSubview (View subview, Action action)
+ {
+ if (_subviews.Contains (subview)) {
+ action (subview);
+ }
+
+ SetNeedsDisplay ();
+ subview.SetNeedsDisplay ();
+ }
+
+ ///
+ /// Brings the specified subview to the front so it is drawn on top of any other views.
+ ///
+ /// The subview to send to the front
+ ///
+ /// .
+ ///
+ public void BringSubviewToFront (View subview)
+ {
+ PerformActionForSubview (subview, x => {
+ _subviews.Remove (x);
+ _subviews.Add (x);
+ });
+ }
+
+ ///
+ /// Sends the specified subview to the front so it is the first view drawn
+ ///
+ /// The subview to send to the front
+ ///
+ /// .
+ ///
+ public void SendSubviewToBack (View subview)
+ {
+ PerformActionForSubview (subview, x => {
+ _subviews.Remove (x);
+ _subviews.Insert (0, subview);
+ });
+ }
+
+ ///
+ /// Moves the subview backwards in the hierarchy, only one step
+ ///
+ /// The subview to send backwards
+ ///
+ /// If you want to send the view all the way to the back use SendSubviewToBack.
+ ///
+ public void SendSubviewBackwards (View subview)
+ {
+ PerformActionForSubview (subview, x => {
+ var idx = _subviews.IndexOf (x);
+ if (idx > 0) {
+ _subviews.Remove (x);
+ _subviews.Insert (idx - 1, x);
+ }
+ });
+ }
+
+ ///
+ /// Moves the subview backwards in the hierarchy, only one step
+ ///
+ /// The subview to send backwards
+ ///
+ /// If you want to send the view all the way to the back use SendSubviewToBack.
+ ///
+ public void BringSubviewForward (View subview)
+ {
+ PerformActionForSubview (subview, x => {
+ var idx = _subviews.IndexOf (x);
+ if (idx + 1 < _subviews.Count) {
+ _subviews.Remove (x);
+ _subviews.Insert (idx + 1, x);
+ }
+ });
+ }
+
+ ///
+ /// Get the top superview of a given .
+ ///
+ /// The superview view.
+ public View GetTopSuperView (View view = null, View superview = null)
+ {
+ View top = superview ?? Application.Top;
+ for (var v = view?.SuperView ?? (this?.SuperView); v != null; v = v.SuperView) {
+ top = v;
+ if (top == superview) {
+ break;
+ }
+ }
+
+ return top;
+ }
+
+
+
+ #region Focus
+ View _focused = null;
+
+ internal enum Direction {
+ Forward,
+ Backward
+ }
+
+ ///
+ /// Event fired when the view gets focus.
+ ///
+ public event EventHandler Enter;
+
+ ///
+ /// Event fired when the view looses focus.
+ ///
+ public event EventHandler Leave;
+
+ Direction _focusDirection;
+ internal Direction FocusDirection {
+ get => SuperView?.FocusDirection ?? _focusDirection;
+ set {
+ if (SuperView != null)
+ SuperView.FocusDirection = value;
+ else
+ _focusDirection = value;
+ }
+ }
+
+
+ // BUGBUG: v2 - Seems weird that this is in View and not Responder.
+ bool _hasFocus;
+
+ ///
+ public override bool HasFocus => _hasFocus;
+
+ void SetHasFocus (bool value, View view, bool force = false)
+ {
+ if (_hasFocus != value || force) {
+ _hasFocus = value;
+ if (value) {
+ OnEnter (view);
+ } else {
+ OnLeave (view);
+ }
+ SetNeedsDisplay ();
+ }
+
+ // Remove focus down the chain of subviews if focus is removed
+ if (!value && _focused != null) {
+ var f = _focused;
+ f.OnLeave (view);
+ f.SetHasFocus (false, view);
+ _focused = null;
+ }
+ }
+
+ ///
+ /// Event fired when the value is being changed.
+ ///
+ public event EventHandler CanFocusChanged;
+
+ ///
+ public override void OnCanFocusChanged () => CanFocusChanged?.Invoke (this, EventArgs.Empty);
+
+ bool _oldCanFocus;
+ ///
+ public override bool CanFocus {
+ get => base.CanFocus;
+ set {
+ if (!_addingView && IsInitialized && SuperView?.CanFocus == false && value) {
+ throw new InvalidOperationException ("Cannot set CanFocus to true if the SuperView CanFocus is false!");
+ }
+ if (base.CanFocus != value) {
+ base.CanFocus = value;
+
+ switch (value) {
+ case false when _tabIndex > -1:
+ TabIndex = -1;
+ break;
+ case true when SuperView?.CanFocus == false && _addingView:
+ SuperView.CanFocus = true;
+ break;
+ }
+
+ if (value && _tabIndex == -1) {
+ TabIndex = SuperView != null ? SuperView._tabIndexes.IndexOf (this) : -1;
+ }
+ TabStop = value;
+
+ if (!value && SuperView?.Focused == this) {
+ SuperView._focused = null;
+ }
+ if (!value && HasFocus) {
+ SetHasFocus (false, this);
+ SuperView?.EnsureFocus ();
+ if (SuperView != null && SuperView.Focused == null) {
+ SuperView.FocusNext ();
+ if (SuperView.Focused == null) {
+ Application.Current.FocusNext ();
+ }
+ Application.BringOverlappedTopToFront ();
+ }
+ }
+ if (_subviews != null && IsInitialized) {
+ foreach (var view in _subviews) {
+ if (view.CanFocus != value) {
+ if (!value) {
+ view._oldCanFocus = view.CanFocus;
+ view._oldTabIndex = view._tabIndex;
+ view.CanFocus = false;
+ view._tabIndex = -1;
+ } else {
+ if (_addingView) {
+ view._addingView = true;
+ }
+ view.CanFocus = view._oldCanFocus;
+ view._tabIndex = view._oldTabIndex;
+ view._addingView = false;
+ }
+ }
+ }
+ }
+ OnCanFocusChanged ();
+ SetNeedsDisplay ();
+ }
+ }
+ }
+
+
+ ///
+ public override bool OnEnter (View view)
+ {
+ var args = new FocusEventArgs (view);
+ Enter?.Invoke (this, args);
+ if (args.Handled)
+ return true;
+ if (base.OnEnter (view))
+ return true;
+
+ return false;
+ }
+
+ ///
+ public override bool OnLeave (View view)
+ {
+ var args = new FocusEventArgs (view);
+ Leave?.Invoke (this, args);
+ if (args.Handled)
+ return true;
+ if (base.OnLeave (view))
+ return true;
+
+ return false;
+ }
+
+ ///
+ /// Returns the currently focused view inside this view, or null if nothing is focused.
+ ///
+ /// The focused.
+ public View Focused => _focused;
+
+ ///
+ /// Returns the most focused view in the chain of subviews (the leaf view that has the focus).
+ ///
+ /// The most focused View.
+ public View MostFocused {
+ get {
+ if (Focused == null)
+ return null;
+ var most = Focused.MostFocused;
+ if (most != null)
+ return most;
+ return Focused;
+ }
+ }
+
+ ///
+ /// Causes the specified subview to have focus.
+ ///
+ /// View.
+ void SetFocus (View view)
+ {
+ if (view == null) {
+ return;
+ }
+ //Console.WriteLine ($"Request to focus {view}");
+ if (!view.CanFocus || !view.Visible || !view.Enabled) {
+ return;
+ }
+ if (_focused?._hasFocus == true && _focused == view) {
+ return;
+ }
+ if ((_focused?._hasFocus == true && _focused?.SuperView == view) || view == this) {
+
+ if (!view._hasFocus) {
+ view._hasFocus = true;
+ }
+ return;
+ }
+ // Make sure that this view is a subview
+ View c;
+ for (c = view._superView; c != null; c = c._superView)
+ if (c == this)
+ break;
+ if (c == null)
+ throw new ArgumentException ("the specified view is not part of the hierarchy of this view");
+
+ if (_focused != null)
+ _focused.SetHasFocus (false, view);
+
+ var f = _focused;
+ _focused = view;
+ _focused.SetHasFocus (true, f);
+ _focused.EnsureFocus ();
+
+ // Send focus upwards
+ if (SuperView != null) {
+ SuperView.SetFocus (this);
+ } else {
+ SetFocus (this);
+ }
+ }
+
+ ///
+ /// Causes the specified view and the entire parent hierarchy to have the focused order updated.
+ ///
+ public void SetFocus ()
+ {
+ if (!CanBeVisible (this) || !Enabled) {
+ if (HasFocus) {
+ SetHasFocus (false, this);
+ }
+ return;
+ }
+
+ if (SuperView != null) {
+ SuperView.SetFocus (this);
+ } else {
+ SetFocus (this);
+ }
+ }
+
+ ///
+ /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, does nothing.
+ ///
+ public void EnsureFocus ()
+ {
+ if (_focused == null && _subviews?.Count > 0) {
+ if (FocusDirection == Direction.Forward) {
+ FocusFirst ();
+ } else {
+ FocusLast ();
+ }
+ }
+ }
+
+ ///
+ /// Focuses the first focusable subview if one exists.
+ ///
+ public void FocusFirst ()
+ {
+ if (!CanBeVisible (this)) {
+ return;
+ }
+
+ if (_tabIndexes == null) {
+ SuperView?.SetFocus (this);
+ return;
+ }
+
+ foreach (var view in _tabIndexes) {
+ if (view.CanFocus && view._tabStop && view.Visible && view.Enabled) {
+ SetFocus (view);
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Focuses the last focusable subview if one exists.
+ ///
+ public void FocusLast ()
+ {
+ if (!CanBeVisible (this)) {
+ return;
+ }
+
+ if (_tabIndexes == null) {
+ SuperView?.SetFocus (this);
+ return;
+ }
+
+ for (var i = _tabIndexes.Count; i > 0;) {
+ i--;
+
+ var v = _tabIndexes [i];
+ if (v.CanFocus && v._tabStop && v.Visible && v.Enabled) {
+ SetFocus (v);
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Focuses the previous view.
+ ///
+ /// if previous was focused, otherwise.
+ public bool FocusPrev ()
+ {
+ if (!CanBeVisible (this)) {
+ return false;
+ }
+
+ FocusDirection = Direction.Backward;
+ if (_tabIndexes == null || _tabIndexes.Count == 0)
+ return false;
+
+ if (_focused == null) {
+ FocusLast ();
+ return _focused != null;
+ }
+
+ var focusedIdx = -1;
+ for (var i = _tabIndexes.Count; i > 0;) {
+ i--;
+ var w = _tabIndexes [i];
+
+ if (w.HasFocus) {
+ if (w.FocusPrev ())
+ return true;
+ focusedIdx = i;
+ continue;
+ }
+ if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled) {
+ _focused.SetHasFocus (false, w);
+
+ if (w.CanFocus && w._tabStop && w.Visible && w.Enabled)
+ w.FocusLast ();
+
+ SetFocus (w);
+ return true;
+ }
+ }
+ if (_focused != null) {
+ _focused.SetHasFocus (false, this);
+ _focused = null;
+ }
+ return false;
+ }
+
+ ///
+ /// Focuses the next view.
+ ///
+ /// if next was focused, otherwise.
+ public bool FocusNext ()
+ {
+ if (!CanBeVisible (this)) {
+ return false;
+ }
+
+ FocusDirection = Direction.Forward;
+ if (_tabIndexes == null || _tabIndexes.Count == 0)
+ return false;
+
+ if (_focused == null) {
+ FocusFirst ();
+ return _focused != null;
+ }
+ var focusedIdx = -1;
+ for (var i = 0; i < _tabIndexes.Count; i++) {
+ var w = _tabIndexes [i];
+
+ if (w.HasFocus) {
+ if (w.FocusNext ())
+ return true;
+ focusedIdx = i;
+ continue;
+ }
+ if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled) {
+ _focused.SetHasFocus (false, w);
+
+ if (w.CanFocus && w._tabStop && w.Visible && w.Enabled)
+ w.FocusFirst ();
+
+ SetFocus (w);
+ return true;
+ }
+ }
+ if (_focused != null) {
+ _focused.SetHasFocus (false, this);
+ _focused = null;
+ }
+ return false;
+ }
+
+ View GetMostFocused (View view)
+ {
+ if (view == null) {
+ return null;
+ }
+
+ return view._focused != null ? GetMostFocused (view._focused) : view;
+ }
+
+ ///
+ /// Positions the cursor in the right position based on the currently focused view in the chain.
+ ///
+ /// Views that are focusable should override to ensure
+ /// the cursor is placed in a location that makes sense. Unix terminals do not have
+ /// a way of hiding the cursor, so it can be distracting to have the cursor left at
+ /// the last focused view. Views should make sure that they place the cursor
+ /// in a visually sensible place.
+ public virtual void PositionCursor ()
+ {
+ if (!CanBeVisible (this) || !Enabled) {
+ return;
+ }
+
+ // BUGBUG: v2 - This needs to support children of Frames too
+
+ if (_focused == null && SuperView != null) {
+ SuperView.EnsureFocus ();
+ } else if (_focused?.Visible == true && _focused?.Enabled == true && _focused?.Frame.Width > 0 && _focused.Frame.Height > 0) {
+ _focused.PositionCursor ();
+ } else if (_focused?.Visible == true && _focused?.Enabled == false) {
+ _focused = null;
+ } else if (CanFocus && HasFocus && Visible && Frame.Width > 0 && Frame.Height > 0) {
+ Move (TextFormatter.HotKeyPos == -1 ? 0 : TextFormatter.CursorPosition, 0);
+ } else {
+ Move (_frame.X, _frame.Y);
+ }
+ }
+ #endregion Focus
+ }
+}
diff --git a/Terminal.Gui/View/ViewText.cs b/Terminal.Gui/View/ViewText.cs
new file mode 100644
index 0000000000..a8dcd1b7f0
--- /dev/null
+++ b/Terminal.Gui/View/ViewText.cs
@@ -0,0 +1,196 @@
+using NStack;
+using System;
+
+namespace Terminal.Gui {
+
+ public partial class View {
+ ustring _text;
+
+ ///
+ /// The text displayed by the .
+ ///
+ ///
+ ///
+ /// The text will be drawn before any subviews are drawn.
+ ///
+ ///
+ /// The text will be drawn starting at the view origin (0, 0) and will be formatted according
+ /// to and .
+ ///
+ ///
+ /// The text will word-wrap to additional lines if it does not fit horizontally. If 's height
+ /// is 1, the text will be clipped.
+ ///
+ ///
+ /// Set the to enable hotkey support. To disable hotkey support set to
+ /// (Rune)0xffff.
+ ///
+ ///
+ public virtual ustring Text {
+ get => _text;
+ set {
+ _text = value;
+ SetHotKey ();
+ UpdateTextFormatterText ();
+ //TextFormatter.Format ();
+ OnResizeNeeded ();
+
+#if DEBUG
+ if (_text != null && string.IsNullOrEmpty (Id)) {
+ Id = _text.ToString ();
+ }
+#endif
+ }
+ }
+
+ ///
+ /// Gets or sets the used to format .
+ ///
+ public TextFormatter TextFormatter { get; set; }
+
+ void TextFormatter_HotKeyChanged (object sender, KeyChangedEventArgs e)
+ {
+ HotKeyChanged?.Invoke (this, e);
+ }
+
+ ///
+ /// Can be overridden if the has
+ /// different format than the default.
+ ///
+ protected virtual void UpdateTextFormatterText ()
+ {
+ if (TextFormatter != null) {
+ TextFormatter.Text = _text;
+ }
+ }
+
+ ///
+ /// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved
+ /// or not when is enabled.
+ /// If trailing spaces at the end of wrapped lines will be removed when
+ /// is formatted for display. The default is .
+ ///
+ public virtual bool PreserveTrailingSpaces {
+ get => TextFormatter.PreserveTrailingSpaces;
+ set {
+ if (TextFormatter.PreserveTrailingSpaces != value) {
+ TextFormatter.PreserveTrailingSpaces = value;
+ TextFormatter.NeedsFormat = true;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets how the View's is aligned horizontally when drawn. Changing this property will redisplay the .
+ ///
+ /// The text alignment.
+ public virtual TextAlignment TextAlignment {
+ get => TextFormatter.Alignment;
+ set {
+ TextFormatter.Alignment = value;
+ UpdateTextFormatterText ();
+ OnResizeNeeded ();
+ }
+ }
+
+ ///
+ /// Gets or sets how the View's is aligned vertically when drawn. Changing this property will redisplay the .
+ ///
+ /// The text alignment.
+ public virtual VerticalTextAlignment VerticalTextAlignment {
+ get => TextFormatter.VerticalAlignment;
+ set {
+ TextFormatter.VerticalAlignment = value;
+ SetNeedsDisplay ();
+ }
+ }
+
+ ///
+ /// Gets or sets the direction of the View's . Changing this property will redisplay the .
+ ///
+ /// The text alignment.
+ public virtual TextDirection TextDirection {
+ get => TextFormatter.Direction;
+ set {
+ UpdateTextDirection (value);
+ TextFormatter.Direction = value;
+ }
+ }
+
+ private void UpdateTextDirection (TextDirection newDirection)
+ {
+ var directionChanged = TextFormatter.IsHorizontalDirection (TextFormatter.Direction)
+ != TextFormatter.IsHorizontalDirection (newDirection);
+ TextFormatter.Direction = newDirection;
+
+ var isValidOldAutoSize = AutoSize && IsValidAutoSize (out var _);
+
+ UpdateTextFormatterText ();
+
+ if ((!ForceValidatePosDim && directionChanged && AutoSize)
+ || (ForceValidatePosDim && directionChanged && AutoSize && isValidOldAutoSize)) {
+ OnResizeNeeded ();
+ } else if (directionChanged && IsAdded) {
+ SetWidthHeight (Bounds.Size);
+ SetMinWidthHeight ();
+ } else {
+ SetMinWidthHeight ();
+ }
+ TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
+ SetNeedsDisplay ();
+ }
+
+ ///
+ /// Gets the width or height of the characters
+ /// in the property.
+ ///
+ ///
+ /// Only the first hotkey specifier found in is supported.
+ ///
+ /// If (the default) the width required for the hotkey specifier is returned. Otherwise the height is returned.
+ /// The number of characters required for the . If the text direction specified
+ /// by does not match the parameter, 0 is returned.
+ public int GetHotKeySpecifierLength (bool isWidth = true)
+ {
+ if (isWidth) {
+ return TextFormatter.IsHorizontalDirection (TextDirection) &&
+ TextFormatter.Text?.Contains (HotKeySpecifier) == true
+ ? Math.Max (Rune.ColumnWidth (HotKeySpecifier), 0) : 0;
+ } else {
+ return TextFormatter.IsVerticalDirection (TextDirection) &&
+ TextFormatter.Text?.Contains (HotKeySpecifier) == true
+ ? Math.Max (Rune.ColumnWidth (HotKeySpecifier), 0) : 0;
+ }
+ }
+
+ ///
+ /// Gets the dimensions required for ignoring a .
+ ///
+ ///
+ public Size GetSizeNeededForTextWithoutHotKey ()
+ {
+ return new Size (TextFormatter.Size.Width - GetHotKeySpecifierLength (),
+ TextFormatter.Size.Height - GetHotKeySpecifierLength (false));
+ }
+
+ ///
+ /// Gets the dimensions required for accounting for a .
+ ///
+ ///
+ public Size GetSizeNeededForTextAndHotKey ()
+ {
+ if (ustring.IsNullOrEmpty (TextFormatter.Text)) {
+
+ if (!IsInitialized) return Size.Empty;
+
+ return Bounds.Size;
+ }
+
+ // BUGBUG: This IGNORES what Text is set to, using on only the current View size. This doesn't seem to make sense.
+ // BUGBUG: This uses Frame; in v2 it should be Bounds
+ return new Size (_frame.Size.Width + GetHotKeySpecifierLength (),
+ _frame.Size.Height + GetHotKeySpecifierLength (false));
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Views/AutocompleteFilepathContext.cs b/Terminal.Gui/Views/AutocompleteFilepathContext.cs
index eb8af28890..0a7bb090c5 100644
--- a/Terminal.Gui/Views/AutocompleteFilepathContext.cs
+++ b/Terminal.Gui/Views/AutocompleteFilepathContext.cs
@@ -17,7 +17,6 @@ public AutocompleteFilepathContext (ustring currentLine, int cursorPosition, Fil
}
}
-
internal class FilepathSuggestionGenerator : ISuggestionGenerator {
FileDialogState state;
@@ -66,7 +65,6 @@ public IEnumerable GenerateSuggestions (AutocompleteContext context)
.OrderBy (m => m.Length)
.ToArray ();
-
// nothing to suggest
if (validSuggestions.Length == 0 || validSuggestions [0].Length == term.Length) {
return Enumerable.Empty ();
@@ -82,10 +80,8 @@ public bool IsWordChar (Rune rune)
return false;
}
-
return true;
}
-
}
}
\ No newline at end of file
diff --git a/Terminal.Gui/Views/ContextMenu.cs b/Terminal.Gui/Views/ContextMenu.cs
index 4a0ec5a5f0..13f0b66f2b 100644
--- a/Terminal.Gui/Views/ContextMenu.cs
+++ b/Terminal.Gui/Views/ContextMenu.cs
@@ -79,7 +79,7 @@ public void Dispose ()
}
if (container != null) {
container.Closing -= Container_Closing;
- container.Resized -= Container_Resized;
+ container.TerminalResized -= Container_Resized;
}
}
@@ -93,7 +93,7 @@ public void Show ()
}
container = Application.Top;
container.Closing += Container_Closing;
- container.Resized += Container_Resized;
+ container.TerminalResized += Container_Resized;
var frame = container.Frame;
var position = Position;
if (Host != null) {
diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs
index df882f608c..2522fc9ccb 100644
--- a/Terminal.Gui/Views/Dialog.cs
+++ b/Terminal.Gui/Views/Dialog.cs
@@ -90,7 +90,6 @@ private void SetInitialProperties (Button [] buttons)
LayoutButtons ();
};
-
}
///
@@ -146,7 +145,6 @@ public enum ButtonAlignments {
Right
}
-
///
/// Determines how the s are aligned along the
/// bottom of the dialog.
diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs
index eb43d1ec09..4bf419a1b2 100644
--- a/Terminal.Gui/Views/FileDialog.cs
+++ b/Terminal.Gui/Views/FileDialog.cs
@@ -63,7 +63,6 @@ public partial class FileDialog : Dialog {
'"','<','>','|','*','?',
};
-
///
/// The UI selected from combo box. May be null.
///
@@ -125,7 +124,6 @@ public partial class FileDialog : Dialog {
/// error handling (e.g. showing a
public IFileOperations FileOperationsHandler { get; set; } = new DefaultFileOperations ();
-
///
/// Initializes a new instance of the class.
///
@@ -247,7 +245,6 @@ public FileDialog (IFileSystem fileSystem)
CycleToNextTableEntryBeginningWith (k);
}
-
};
this.treeView = new TreeView