#nullable enable using System.ComponentModel; using System.Diagnostics; namespace Terminal.Gui; #region API Docs /// /// View is the base class all visible elements. View can render itself and /// contains zero or more nested views, called SubViews. View provides basic functionality for layout, arrangement, and /// drawing. In addition, View provides keyboard and mouse event handling. See the /// /// View /// Deep Dive /// /// for more. /// /// /// /// /// TermDefinition /// /// /// 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. /// /// /// /// SuperViewThe View that is a container for SubViews. /// /// /// Input /// /// /// Key Bindings is the preferred way of handling keyboard input in View implementations. /// The View calls /// to declare /// it supports a particular command and then uses /// to indicate which key presses will invoke the command. /// /// /// Mouse Bindings is the preferred way of handling mouse input in View implementations. The View calls /// to declare /// it supports a /// particular command and then uses to indicate which mouse events will /// invoke the command. /// /// /// See the /// /// Mouse /// Deep Dive /// /// and /// /// Keyboard /// Deep Dive /// /// for more information. /// /// /// /// /// Layout /// /// /// Terminal.Gui provides a rich system for how View objects are laid out relative to each other. The /// layout system also defines how coordinates are specified. /// /// /// The , , , and properties are /// and objects that dynamically update the position of a view. The /// X and Y properties are of type and you can use either absolute positions, /// percentages, or anchor points. The Width and Height properties are of type and /// can use absolute position, percentages, and anchors. These are useful as they will take care of /// repositioning views when view's adornments are resized or if the terminal size changes. /// /// /// See the /// /// Layout /// Deep Dive /// /// for more information. /// /// /// /// /// Arrangement /// /// /// Complimenting the Layout system, controls how the user can use the mouse /// and keyboard to arrange views and enables either Tiled or Overlapped layouts. /// /// /// See the /// /// Arrangement /// Deep Dive /// /// for more information. /// /// /// /// /// Drawing /// /// /// Apps draw using the and APIs. Move selects the /// column and row of the Cell and AddRune places /// the specified glyph in that cell using the that was most recently set via /// . /// The ConsoleDriver caches all changed Cells and efficiently outputs them to the terminal each /// iteration of the Application. In other words, Terminal.Gui uses deferred rendering. /// /// /// The View draw APIs all take coordinates specified in Viewport-Relative coordinates. That is, /// (0,0) is the top-left cell visible to the user. /// /// /// If a View need to redraw because something changed within it's Content Area it can call /// . /// /// /// Terminal.Gui supports the full range of Unicode/wide characters. /// This includes emoji, CJK characters, and other wide characters. For Unicode characters that require /// more than one cell, /// AddRune and the ConsoleDriver automatically manage the cells. Extension methods to Rune are /// provided to determine if a Rune is a wide character and to get the width of a Rune. /// /// /// The provides consistent colors across all views. The /// is inherited from the . The /// is used to set the for drawing. /// /// The class represents a color. It provides automatic mapping between the legacy /// 4-bit (16-color) system and 24-bit colors. It contains properties for the red, green, and blue /// components of the color. /// The Color class also contains a static property for each of the 16 ANSI colors. Use /// to change the colors used when drawing. /// /// /// /// Clipping enables better performance by ensuring on regions of the terminal that need to be drawn /// actually get drawn by the ConsoleDriver. Terminal.Gui supports non-rectangular clip regions with /// . /// There is an -managed clip region. Developers cannot change this directly, /// but can use , , and to /// modify the clip region. /// /// /// provides auto join, a smart TUI drawing system that automatically selects /// the correct line/box drawing glyphs for intersections making drawing complex shapes easy. /// /// /// A set of static properties are provided for the common glyphs used in TUI apps. See /// . /// /// /// See the /// /// Drawing /// Deep Dive /// /// for more information. /// /// /// /// /// Text /// /// /// A rich text formatting engine is provided in . TextFormatter provides /// methods for formatting text with horizontal and vertical alignment, word wrapping, and hotkeys. /// /// /// See the /// /// Navigation /// Deep Dive /// /// for more information. /// /// /// /// /// Navigation /// /// /// Navigation refers to the user experience for moving focus between views in the application /// view-hierarchy. Focus is a concept that is used to describe which View is currently receiving user /// input. Only /// Views that are /// , , and will receive focus. NOTE: /// is by default. /// /// /// Views that are focusable should override to make sure that the cursor /// is /// placed in a location that makes sense. Some 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 default implementation of /// will place the /// cursor at either the hotkey (if defined) or 0,0. /// /// /// See the /// /// Navigation /// Deep Dive /// /// for more information. /// /// /// /// /// Scrolling /// /// /// The ability to scroll content is built into View. The represents the /// scrollable "viewport" into the View's Content Area (which is defined by the return value of /// ). /// /// /// Terminal.Gui also provides the ability show a visual scroll bar that responds to mouse input. This /// ability is not enabled by default given how precious TUI screen real estate is. /// Use and to enable this feature. /// /// /// Use to adjust the behavior of scrolling. /// /// /// See the /// /// Scrolling /// Deep Dive /// /// for more information. /// /// /// /// /// /// 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, 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. /// /// See for an overview of View keyboard handling. /// #endregion API Docs public partial class View : IDisposable, ISupportInitializeNotification { #region Constructors and Initialization /// Gets or sets arbitrary data for the view. /// This property is not used internally. public object? Data { get; set; } /// 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; } = ""; /// /// Points to the current driver in use by the view, it is a convenience property for simplifying the development /// of new views. /// public static IConsoleDriver? Driver => Application.Driver; /// Initializes a new instance of . /// /// /// Use , , , and properties to dynamically /// control the size and location of the view. /// /// public View () { #if DEBUG_IDISPOSABLE Instances.Add (this); #endif SetupAdornments (); SetupCommands (); SetupKeyboard (); SetupMouse (); SetupText (); SetupScrollBars (); } /// /// Raised once when the is being initialized for the first time. Allows /// configurations and assignments to be performed before the being shown. /// View implements to allow for more sophisticated initialization. /// public event EventHandler? Initialized; /// /// 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; } /// 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 is initialized. /// /// /// 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) { throw new InvalidOperationException ("The view is already initialized."); } #if AUTO_CANFOCUS _oldCanFocus = CanFocus; _oldTabIndex = _tabIndex; #endif BeginInitAdornments (); if (_subviews?.Count > 0) { foreach (View view in _subviews) { if (!view.IsInitialized) { view.BeginInit (); } } } } // TODO: Implement logic that allows EndInit to throw if BeginInit has not been called // TODO: See EndInit_Called_Without_BeginInit_Throws test. /// Signals the View that initialization is ending. See . /// /// Initializes all Subviews and Invokes the event. /// public virtual void EndInit () { if (IsInitialized) { throw new InvalidOperationException ("The view is already initialized."); } IsInitialized = true; EndInitAdornments (); // TODO: Move these into ViewText.cs as EndInit_Text() to consolidate. // TODO: Verify UpdateTextDirection really needs to be called here. // These calls were moved from BeginInit as they access Viewport which is indeterminate until EndInit is called. UpdateTextDirection (TextDirection); UpdateTextFormatterText (); if (_subviews is { }) { foreach (View view in _subviews) { if (!view.IsInitialized) { view.EndInit (); } } } // TODO: Figure out how to move this out of here and just depend on LayoutNeeded in Mainloop Layout (); // the EventLog in AllViewsTester fails to layout correctly if this is not here (convoluted Dim.Fill(Func)). SetNeedsLayout (); Initialized?.Invoke (this, EventArgs.Empty); } #endregion Constructors and Initialization #region Visibility private bool _enabled = true; /// Gets or sets a value indicating whether this can respond to user interaction. public bool Enabled { get => _enabled; set { if (_enabled == value) { return; } _enabled = value; if (!_enabled && HasFocus) { HasFocus = false; } if (_enabled && CanFocus && Visible && !HasFocus && SuperView is null or { HasFocus: true, Visible: true, Enabled: true, Focused: null }) { SetFocus (); } OnEnabledChanged (); SetNeedsDraw (); if (Border is { }) { Border.Enabled = _enabled; } if (_subviews is null) { return; } foreach (View view in _subviews) { view.Enabled = Enabled; } } } /// Raised when the value is being changed. public event EventHandler? EnabledChanged; // TODO: Change this event to match the standard TG event model. /// Invoked when the property from a view is changed. public virtual void OnEnabledChanged () { EnabledChanged?.Invoke (this, EventArgs.Empty); } private bool _visible = true; // TODO: Remove virtual once Menu/MenuBar are removed. MenuBar is the only override. /// Gets or sets a value indicating whether this is visible. public virtual bool Visible { get => _visible; set { if (_visible == value) { return; } if (OnVisibleChanging ()) { return; } CancelEventArgs args = new (in _visible, ref value); VisibleChanging?.Invoke (this, args); if (args.Cancel) { return; } _visible = value; if (!_visible) { if (HasFocus) { HasFocus = false; } } if (_visible && CanFocus && Enabled && !HasFocus && SuperView is null or { HasFocus: true, Visible: true, Enabled: true, Focused: null }) { SetFocus (); } OnVisibleChanged (); VisibleChanged?.Invoke (this, EventArgs.Empty); SetNeedsLayout (); SuperView?.SetNeedsLayout (); SetNeedsDraw (); if (SuperView is { }) { SuperView?.SetNeedsDraw (); } else { Application.ClearScreenNextIteration = true; } } } /// Called when is changing. Can be cancelled by returning . protected virtual bool OnVisibleChanging () { return false; } /// /// Raised when the value is being changed. Can be cancelled by setting Cancel to /// . /// public event EventHandler>? VisibleChanging; /// Called when has changed. protected virtual void OnVisibleChanged () { } /// Raised when has changed. public event EventHandler? VisibleChanged; /// /// INTERNAL Indicates whether all views up the Superview hierarchy are visible. /// /// The view to test. /// /// if `view.Visible` is or any Superview is not visible, /// otherwise. /// internal static bool CanBeVisible (View view) { if (!view.Visible) { return false; } for (View? c = view.SuperView; c != null; c = c.SuperView) { if (!c.Visible) { return false; } } return true; } #endregion Visibility #region Title private string _title = string.Empty; /// Gets the used to format . internal TextFormatter TitleTextFormatter { get; init; } = new (); /// /// The title to be displayed for this . The title will be displayed if . /// is greater than 0. The title can be used to set the /// for the view by prefixing character with (e.g. "T_itle"). /// /// /// /// Set the to enable hotkey support. To disable Title-based hotkey support set /// to (Rune)0xffff. /// /// /// Only the first HotKey specifier found in is supported. /// /// /// To cause the hotkey to be rendered with , /// set View. to the desired character. /// /// /// The title. public string Title { get { #if DEBUG_IDISPOSABLE if (WasDisposed) { throw new ObjectDisposedException (GetType ().FullName); } #endif return _title; } set { #if DEBUG_IDISPOSABLE if (WasDisposed) { throw new ObjectDisposedException (GetType ().FullName); } #endif if (value == _title) { return; } if (!OnTitleChanging (ref value)) { string old = _title; _title = value; TitleTextFormatter.Text = _title; SetTitleTextFormatterSize (); SetHotKeyFromTitle (); SetNeedsDraw (); #if DEBUG if (string.IsNullOrEmpty (Id)) { Id = _title; } #endif // DEBUG OnTitleChanged (); } } } private void SetTitleTextFormatterSize () { TitleTextFormatter.ConstrainToSize = new ( TextFormatter.GetWidestLineLength (TitleTextFormatter.Text) - (TitleTextFormatter.Text?.Contains ((char)HotKeySpecifier.Value) == true ? Math.Max (HotKeySpecifier.GetColumns (), 0) : 0), 1); } // TODO: Change this event to match the standard TG event model. /// Called when the has been changed. Invokes the event. protected void OnTitleChanged () { TitleChanged?.Invoke (this, new (in _title)); } /// /// Called before the changes. Invokes the event, which can /// be cancelled. /// /// The new to be replaced. /// `true` if an event handler canceled the Title change. protected bool OnTitleChanging (ref string newTitle) { CancelEventArgs args = new (ref _title, ref newTitle); TitleChanging?.Invoke (this, args); return args.Cancel; } /// Raised after the has been changed. public event EventHandler>? TitleChanged; /// /// Raised when the is changing. Set to `true` /// to cancel the Title change. /// public event EventHandler>? TitleChanging; #endregion /// Pretty prints the View /// public override string ToString () { return $"{GetType ().Name}({Id}){Frame}"; } private bool _disposedValue; /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// /// If disposing equals true, the method has been called directly or indirectly by a user's code. Managed and /// unmanaged resources can be disposed. If disposing equals false, the method has been called by the runtime from /// inside the finalizer and you should not reference other objects. Only unmanaged resources can be disposed. /// /// protected virtual void Dispose (bool disposing) { LineCanvas.Dispose (); DisposeMouse (); DisposeKeyboard (); DisposeAdornments (); DisposeScrollBars (); for (int i = InternalSubviews.Count - 1; i >= 0; i--) { View subview = InternalSubviews [i]; Remove (subview); subview.Dispose (); } if (!_disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects) } _disposedValue = true; } Debug.Assert (InternalSubviews.Count == 0); } /// /// Riased when the is being disposed. /// public event EventHandler? Disposing; /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resource. public void Dispose () { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Disposing?.Invoke (this, EventArgs.Empty); Dispose (true); GC.SuppressFinalize (this); #if DEBUG_IDISPOSABLE WasDisposed = true; foreach (View instance in Instances.Where (x => x.WasDisposed).ToList ()) { Instances.Remove (instance); } #endif } #if DEBUG_IDISPOSABLE /// For debug purposes to verify objects are being disposed properly public bool WasDisposed { get; set; } /// For debug purposes to verify objects are being disposed properly public int DisposedCount { get; set; } = 0; /// For debug purposes public static List Instances { get; set; } = []; #endif }