#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, positioning, and /// drawing. In addition, View provides keyboard and mouse event handling. /// /// /// /// /// 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. /// /// /// /// Focus is a concept that is used to describe which View is currently receiving user input. Only Views that are /// , , and will receive focus. /// /// /// 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. /// /// /// 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. /// /// /// To create a View using Absolute layout, call a constructor that takes a Rect parameter to specify the /// absolute position and size or simply set ). To create a View using Computed layout use /// a constructor that does not take a Rect parameter and set the X, Y, Width and Height properties on the view to /// non-absolute values. Both approaches use coordinates that are relative to the of the /// the View is added to. /// /// /// Computed layout is more flexible and supports dynamic console apps where controls adjust layout as the /// terminal resizes or other Views change size or position. 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. /// /// /// Absolute layout requires specifying coordinates and sizes of Views explicitly, and the View will typically /// stay in a fixed position and size. To change the position and size use the property. /// /// /// Subviews (child views) can be added to a View by calling the method. The container of /// a View can be accessed with the property. /// /// /// To flag a region of the View's to be redrawn call /// /// . /// To flag the entire view for redraw call . /// /// /// The method is called when the size or layout of a view has changed. The will /// cause to be called on the next so there is normally no reason to direclty call /// see . /// /// /// 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 /// a white background for dialog boxes and a red background for errors. /// /// /// Subclasses should not rely on being set at construction time. If a /// is not set on a view, the view will inherit the value from its /// and the value might only be valid once a view has been added to a SuperView. /// /// By using applications will work both in color as well as black and white displays. /// /// Views can also 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 : Responder, 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 ConsoleDriver? Driver => Application.Driver; /// Initializes a new instance of . /// /// /// Use , , , and properties to dynamically /// control the size and location of the view. /// /// public View () { SetupAdornments (); SetupCommands (); SetupKeyboard (); //SetupMouse (); SetupText (); } /// /// 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 (); SetLayoutNeeded (); 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 IsLayoutNeeded in Mainloop Layout (); Initialized?.Invoke (this, EventArgs.Empty); } #endregion Constructors and Initialization #region Visibility private bool _enabled = true; // This is a cache of the Enabled property so that we can restore it when the superview is re-enabled. private bool _oldEnabled; /// 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 (); SetNeedsDisplay (); if (_subviews is null) { return; } foreach (View view in _subviews) { if (!_enabled) { view._oldEnabled = view.Enabled; view.Enabled = _enabled; } else { view.Enabled = view._oldEnabled; #if AUTO_CANFOCUS view._addingViewSoCanFocusAlsoUpdatesSuperView = _enabled; #endif } } } } /// 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); SetLayoutNeeded (); SuperView?.SetLayoutNeeded(); SetNeedsDisplay (); SuperView?.SetNeedsDisplay(); } } /// 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 (); SetNeedsDisplay (); #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}"; } /// protected override void Dispose (bool disposing) { LineCanvas.Dispose (); DisposeKeyboard (); DisposeAdornments (); for (int i = InternalSubviews.Count - 1; i >= 0; i--) { View subview = InternalSubviews [i]; Remove (subview); subview.Dispose (); } base.Dispose (disposing); Debug.Assert (InternalSubviews.Count == 0); } }