#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. /// /// #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 (); } } } if (ApplicationImpl.Instance.IsLegacy) { // 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 }