// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; namespace FlaxEditor.GUI.Docking { /// /// Dockable window mode. /// [HideInEditor] public enum DockState { /// /// The unknown state. /// Unknown = 0, /// /// The floating window. /// Float = 1, //DockTopAutoHide = 2, //DockLeftAutoHide = 3, //DockBottomAutoHide = 4, //DockRightAutoHide = 5, /// /// The dock fill as a tab. /// DockFill = 6, /// /// The dock top. /// DockTop = 7, /// /// The dock left. /// DockLeft = 8, /// /// The dock bottom. /// DockBottom = 9, /// /// The dock right. /// DockRight = 10, /// /// The hidden state. /// Hidden = 11 } /// /// Dockable panel control. /// /// public class DockPanel : ContainerControl { /// /// The default dock tabs header height. /// public const float DefaultHeaderHeight = 20; /// /// The default tabs header text left margin. /// public const float DefaultLeftTextMargin = 4; /// /// The default tabs header text right margin. /// public const float DefaultRightTextMargin = 8; /// /// The default tabs header buttons size. /// public const float DefaultButtonsSize = 15; /// /// The default tabs header buttons margin. /// public const float DefaultButtonsMargin = 2; /// /// The default splitters value. /// public const float DefaultSplitterValue = 0.25f; private readonly DockPanel _parentPanel; private readonly List _childPanels = new List(); private readonly List _tabs = new List(); private DockWindow _selectedTab; private DockPanelProxy _tabsProxy; /// /// Returns true if this panel is a master panel. /// public virtual bool IsMaster => false; /// /// Returns true if this panel is a floating window panel. /// public virtual bool IsFloating => false; /// /// Gets docking area bounds (tabs rectangle) in a screen space. /// public Rectangle DockAreaBounds { get { var parentWin = Root; if (parentWin == null) throw new InvalidOperationException("Missing parent window."); var control = _tabsProxy != null ? (Control)_tabsProxy : this; var clientPos = control.PointToWindow(Float2.Zero); return new Rectangle(parentWin.PointToScreen(clientPos), control.Size * DpiScale); } } /// /// Gets the child panels. /// public List ChildPanels => _childPanels; /// /// Gets the child panels count. /// public int ChildPanelsCount => _childPanels.Count; /// /// Gets the tabs. /// public List Tabs => _tabs; /// /// Gets amount of the tabs in a dock panel. /// public int TabsCount => _tabs.Count; /// /// Gets or sets the index of the selected tab. /// public int SelectedTabIndex { get => _tabs.IndexOf(_selectedTab); set => SelectTab(value); } /// /// Gets the selected tab. /// public DockWindow SelectedTab => _selectedTab; /// /// Gets the first tab. /// public DockWindow FirstTab => _tabs.Count > 0 ? _tabs[0] : null; /// /// Gets the last tab. /// public DockWindow LastTab => _tabs.Count > 0 ? _tabs[_tabs.Count - 1] : null; /// /// Gets the parent panel. /// public DockPanel ParentDockPanel => _parentPanel; /// /// Gets the tabs header proxy. /// public DockPanelProxy TabsProxy => _tabsProxy; /// /// Initializes a new instance of the class. /// /// The parent panel. public DockPanel(DockPanel parentPanel) { AutoFocus = false; _parentPanel = parentPanel; _parentPanel?._childPanels.Add(this); AnchorPreset = AnchorPresets.StretchAll; Offsets = Margin.Zero; } /// /// Closes all the windows. /// /// Window closing reason. /// True if action has been cancelled (due to window internal logic). public bool CloseAll(ClosingReason reason = ClosingReason.CloseEvent) { while (_tabs.Count > 0) { if (_tabs[0].Close(reason)) return true; } return false; } /// /// Gets tab at the given index. /// /// The index of the tab page. /// The tab. public DockWindow GetTab(int tabIndex) { return _tabs[tabIndex]; } /// /// Gets tab at the given index. /// /// The tab page. /// The index of the given tab. public int GetTabIndex(DockWindow tab) { return _tabs.IndexOf(tab); } /// /// Determines whether panel contains the specified tab. /// /// The tab. /// true if panel contains the specified tab; otherwise, false. public bool ContainsTab(DockWindow tab) { return _tabs.Contains(tab); } /// /// Selects the tab page. /// /// The index of the tab page to select. public void SelectTab(int tabIndex) { DockWindow tab = null; if (tabIndex >= 0 && _tabs.Count > tabIndex && _tabs.Count > 0) tab = _tabs[tabIndex]; SelectTab(tab); } /// /// Selects the tab page. /// /// The tab page to select. /// True if focus tab after selection change. public void SelectTab(DockWindow tab, bool autoFocus = true) { // Check if tab will change if (_selectedTab != tab) { // Change ContainerControl proxy; if (_selectedTab != null) { proxy = _selectedTab.Parent; _selectedTab.Parent = null; } else { CreateTabsProxy(); proxy = _tabsProxy; } _selectedTab = tab; if (_selectedTab != null) { _selectedTab.UnlockChildrenRecursive(); _selectedTab.Parent = proxy; if (autoFocus) _selectedTab.Focus(); } OnSelectedTabChanged(); } else if (autoFocus && _selectedTab != null && !_selectedTab.ContainsFocus) { _selectedTab.Focus(); } } /// /// Called when selected tab gets changed. /// protected virtual void OnSelectedTabChanged() { } /// /// Performs hit test over dock panel /// /// Screen space position to test /// Dock panel that has been hit or null if nothing found public DockPanel HitTest(ref Float2 position) { // Get parent window and transform point position into local coordinates system var parentWin = Root; if (parentWin == null) return null; var clientPos = parentWin.PointFromScreen(position); var localPos = PointFromWindow(clientPos); // Early out if (localPos.X < 0 || localPos.Y < 0) return null; if (localPos.X > Width || localPos.Y > Height) return null; // Test all docked controls (find the smallest one) float sizeLengthSquared = float.MaxValue; DockPanel result = null; for (int i = 0; i < _childPanels.Count; i++) { var panel = _childPanels[i].HitTest(ref position); if (panel != null) { var sizeLen = panel.Size.LengthSquared; if (sizeLen < sizeLengthSquared) { sizeLengthSquared = sizeLen; result = panel; } } } if (result != null) return result; // Itself return this; } /// /// Try get panel dock state /// /// Splitter value /// Dock State public virtual DockState TryGetDockState(out float splitterValue) { DockState result = DockState.Unknown; splitterValue = DefaultSplitterValue; if (HasParent) { if (Parent.Parent is SplitPanel splitter) { splitterValue = splitter.SplitterValue; if (Parent == splitter.Panel1) { if (splitter.Orientation == Orientation.Horizontal) result = DockState.DockLeft; else result = DockState.DockTop; } else { if (splitter.Orientation == Orientation.Horizontal) result = DockState.DockRight; else result = DockState.DockBottom; splitterValue = 1 - splitterValue; } } } return result; } /// /// Create child dock panel /// /// Dock panel state /// Initial splitter value /// Child panel public DockPanel CreateChildPanel(DockState state, float splitterValue) { CreateTabsProxy(); // Create child dock panel var dockPanel = new DockPanel(this); // Switch dock mode Control c1; Control c2; Orientation o; switch (state) { case DockState.DockTop: { o = Orientation.Vertical; c1 = dockPanel; c2 = _tabsProxy; break; } case DockState.DockBottom: { splitterValue = 1 - splitterValue; o = Orientation.Vertical; c1 = _tabsProxy; c2 = dockPanel; break; } case DockState.DockLeft: { o = Orientation.Horizontal; c1 = dockPanel; c2 = _tabsProxy; break; } case DockState.DockRight: { splitterValue = 1 - splitterValue; o = Orientation.Horizontal; c1 = _tabsProxy; c2 = dockPanel; break; } default: throw new ArgumentOutOfRangeException(); } // Create splitter and link controls var parent = _tabsProxy.Parent; var splitter = new SplitPanel(o, ScrollBars.None, ScrollBars.None) { AnchorPreset = AnchorPresets.StretchAll, Offsets = Margin.Zero, SplitterValue = splitterValue, }; splitter.Panel1.AddChild(c1); splitter.Panel2.AddChild(c2); parent.AddChild(splitter); // Update splitter.UnlockChildrenRecursive(); splitter.PerformLayout(); return dockPanel; } internal void RemoveIt() { OnLastTabRemoved(); } /// /// Called when last tab gets removed. /// protected virtual void OnLastTabRemoved() { // Check if dock panel is linked to the split panel if (HasParent) { if (Parent.Parent is SplitPanel splitter) { // Check if there is another nested dock panel inside this dock panel and extract it here var childPanels = _childPanels.ToArray(); if (childPanels.Length != 0) { // Move tabs from child panels into this one DockWindow selectedTab = null; foreach (var childPanel in childPanels) { var childPanelTabs = childPanel.Tabs.ToArray(); for (var i = 0; i < childPanelTabs.Length; i++) { var childPanelTab = childPanelTabs[i]; if (selectedTab == null && childPanelTab.IsSelected) selectedTab = childPanelTab; childPanel.UndockWindow(childPanelTab); AddTab(childPanelTab, false); } } if (selectedTab != null) SelectTab(selectedTab); } else { // Unlink splitter var splitterParent = splitter.Parent; Assert.IsNotNull(splitterParent); splitter.Parent = null; // Move controls from second split panel to the split panel parent var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2; var srcPanelChildrenCount = scrPanel.ChildrenCount; for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) { scrPanel.GetChild(i).Parent = splitterParent; } Assert.IsTrue(scrPanel.ChildrenCount == 0); Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); // Delete splitter.Dispose(); } } else if (!IsMaster) { throw new InvalidOperationException(); } } else if (!IsMaster) { throw new InvalidOperationException(); } } internal virtual void DockWindowInternal(DockState state, DockWindow window, bool autoSelect = true, float? splitterValue = null) { DockWindow(state, window, autoSelect, splitterValue); } /// /// Docks the window. /// /// The state. /// The window. /// Whether or not to automatically select the window after docking it. /// The splitter value to use when docking to window. protected virtual void DockWindow(DockState state, DockWindow window, bool autoSelect = true, float? splitterValue = null) { CreateTabsProxy(); // Check if dock like a tab or not if (state == DockState.DockFill) { // Add tab AddTab(window, autoSelect); } else { // Create child panel var dockPanel = CreateChildPanel(state, splitterValue ?? DefaultSplitterValue); // Dock window as a tab in a child panel dockPanel.DockWindow(DockState.DockFill, window); } } internal void UndockWindowInternal(DockWindow window) { UndockWindow(window); } /// /// Undocks the window. /// /// The window. protected virtual void UndockWindow(DockWindow window) { var index = GetTabIndex(window); if (index == -1) throw new IndexOutOfRangeException(); // Check if tab was selected if (window == SelectedTab) { // Change selection if (index == 0 && TabsCount > 1) SelectTab(1); else SelectTab(index - 1); } // Undock _tabs.RemoveAt(index); window.ParentDockPanel = null; // Check if has no more tabs if (_tabs.Count == 0) { OnLastTabRemoved(); } else { // Update PerformLayout(); } } /// /// Adds the tab. /// /// The window to insert as a tab. /// True if auto-select newly added tab. protected virtual void AddTab(DockWindow window, bool autoSelect = true) { _tabs.Add(window); window.ParentDockPanel = this; if (autoSelect) SelectTab(window); } private void CreateTabsProxy() { if (_tabsProxy == null) { // Create proxy and make set simple full dock _tabsProxy = new DockPanelProxy(this) { Parent = this }; _tabsProxy.UnlockChildrenRecursive(); } } internal void MoveTabLeft(int index) { if (index > 0) { var tab = _tabs[index]; _tabs.RemoveAt(index); _tabs.Insert(index - 1, tab); } } internal void MoveTabRight(int index) { if (index < _tabs.Count - 1) { var tab = _tabs[index]; _tabs.RemoveAt(index); _tabs.Insert(index + 1, tab); } } /// public override void OnDestroy() { _parentPanel?._childPanels.Remove(this); base.OnDestroy(); // Clear other tabs (not in the view) for (int i = 0; i < _tabs.Count; i++) { if (!_tabs[i].IsDisposing) { _tabs[i].Dispose(); } } } } }