// 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();
}
}
}
}
}