Files
FlaxEngine/Source/Editor/GUI/Docking/DockPanel.cs
2020-12-07 23:40:54 +01:00

644 lines
19 KiB
C#

// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEngine;
using FlaxEngine.Assertions;
using FlaxEngine.GUI;
namespace FlaxEditor.GUI.Docking
{
/// <summary>
/// Dockable window mode.
/// </summary>
[HideInEditor]
public enum DockState
{
/// <summary>
/// The unknown state.
/// </summary>
Unknown = 0,
/// <summary>
/// The floating window.
/// </summary>
Float = 1,
//DockTopAutoHide = 2,
//DockLeftAutoHide = 3,
//DockBottomAutoHide = 4,
//DockRightAutoHide = 5,
/// <summary>
/// The dock fill as a tab.
/// </summary>
DockFill = 6,
/// <summary>
/// The dock top.
/// </summary>
DockTop = 7,
/// <summary>
/// The dock left.
/// </summary>
DockLeft = 8,
/// <summary>
/// The dock bottom.
/// </summary>
DockBottom = 9,
/// <summary>
/// The dock right.
/// </summary>
DockRight = 10,
/// <summary>
/// The hidden state.
/// </summary>
Hidden = 11
}
/// <summary>
/// Dockable panel control.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
public class DockPanel : ContainerControl
{
/// <summary>
/// The default dock tabs header height.
/// </summary>
public const float DefaultHeaderHeight = 20;
/// <summary>
/// The default tabs header text left margin.
/// </summary>
public const float DefaultLeftTextMargin = 4;
/// <summary>
/// The default tabs header text right margin.
/// </summary>
public const float DefaultRightTextMargin = 8;
/// <summary>
/// The default tabs header buttons size.
/// </summary>
public const float DefaultButtonsSize = 12;
/// <summary>
/// The default tabs header buttons margin.
/// </summary>
public const float DefaultButtonsMargin = 2;
/// <summary>
/// The default splitters value.
/// </summary>
public const float DefaultSplitterValue = 0.25f;
private readonly DockPanel _parentPanel;
private readonly List<DockPanel> _childPanels = new List<DockPanel>();
private readonly List<DockWindow> _tabs = new List<DockWindow>();
private DockWindow _selectedTab;
private DockPanelProxy _tabsProxy;
/// <summary>
/// Returns true if this panel is a master panel.
/// </summary>
public virtual bool IsMaster => false;
/// <summary>
/// Returns true if this panel is a floating window panel.
/// </summary>
public virtual bool IsFloating => false;
/// <summary>
/// Gets docking area bounds (tabs rectangle) in a screen space.
/// </summary>
public Rectangle DockAreaBounds
{
get
{
var parentWin = Root;
if (parentWin == null)
throw new InvalidOperationException("Missing parent window.");
var control = _tabsProxy != null ? (Control)_tabsProxy : this;
var dpiScale = Platform.DpiScale;
var clientPos = control.PointToWindow(Vector2.Zero);
return new Rectangle(parentWin.ClientToScreen(clientPos * dpiScale), control.Size * dpiScale);
}
}
/// <summary>
/// Gets the child panels.
/// </summary>
public List<DockPanel> ChildPanels => _childPanels;
/// <summary>
/// Gets the child panels count.
/// </summary>
public int ChildPanelsCount => _childPanels.Count;
/// <summary>
/// Gets the tabs.
/// </summary>
public List<DockWindow> Tabs => _tabs;
/// <summary>
/// Gets amount of the tabs in a dock panel.
/// </summary>
public int TabsCount => _tabs.Count;
/// <summary>
/// Gets or sets the index of the selected tab.
/// </summary>
public int SelectedTabIndex
{
get => _tabs.IndexOf(_selectedTab);
set => SelectTab(value);
}
/// <summary>
/// Gets the selected tab.
/// </summary>
public DockWindow SelectedTab => _selectedTab;
/// <summary>
/// Gets the first tab.
/// </summary>
public DockWindow FirstTab => _tabs.Count > 0 ? _tabs[0] : null;
/// <summary>
/// Gets the last tab.
/// </summary>
public DockWindow LastTab => _tabs.Count > 0 ? _tabs[_tabs.Count - 1] : null;
/// <summary>
/// Gets the parent panel.
/// </summary>
public DockPanel ParentDockPanel => _parentPanel;
/// <summary>
/// Gets the tabs header proxy.
/// </summary>
public DockPanelProxy TabsProxy => _tabsProxy;
/// <summary>
/// Initializes a new instance of the <see cref="DockPanel"/> class.
/// </summary>
/// <param name="parentPanel">The parent panel.</param>
public DockPanel(DockPanel parentPanel)
{
AutoFocus = false;
_parentPanel = parentPanel;
_parentPanel?._childPanels.Add(this);
AnchorPreset = AnchorPresets.StretchAll;
Offsets = Margin.Zero;
}
/// <summary>
/// Closes all the windows.
/// </summary>
/// <param name="reason">Window closing reason.</param>
/// <returns>True if action has been cancelled (due to window internal logic).</returns>
public bool CloseAll(ClosingReason reason = ClosingReason.CloseEvent)
{
while (_tabs.Count > 0)
{
if (_tabs[0].Close(reason))
return true;
}
return false;
}
/// <summary>
/// Gets tab at the given index.
/// </summary>
/// <param name="tabIndex">The index of the tab page.</param>
/// <returns>The tab.</returns>
public DockWindow GetTab(int tabIndex)
{
return _tabs[tabIndex];
}
/// <summary>
/// Gets tab at the given index.
/// </summary>
/// <param name="tab">The tab page.</param>
/// <returns>The index of the given tab.</returns>
public int GetTabIndex(DockWindow tab)
{
return _tabs.IndexOf(tab);
}
/// <summary>
/// Determines whether panel contains the specified tab.
/// </summary>
/// <param name="tab">The tab.</param>
/// <returns>
/// <c>true</c> if panel contains the specified tab; otherwise, <c>false</c>.
/// </returns>
public bool ContainsTab(DockWindow tab)
{
return _tabs.Contains(tab);
}
/// <summary>
/// Selects the tab page.
/// </summary>
/// <param name="tabIndex">The index of the tab page to select.</param>
public void SelectTab(int tabIndex)
{
DockWindow tab = null;
if (tabIndex >= 0 && _tabs.Count > tabIndex && _tabs.Count > 0)
tab = _tabs[tabIndex];
SelectTab(tab);
}
/// <summary>
/// Selects the tab page.
/// </summary>
/// <param name="tab">The tab page to select.</param>
/// <param name="autoFocus">True if focus tab after selection change.</param>
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();
}
}
/// <summary>
/// Called when selected tab gets changed.
/// </summary>
protected virtual void OnSelectedTabChanged()
{
}
/// <summary>
/// Performs hit test over dock panel
/// </summary>
/// <param name="position">Screen space position to test</param>
/// <returns>Dock panel that has been hit or null if nothing found</returns>
public DockPanel HitTest(ref Vector2 position)
{
// Get parent window and transform point position into local coordinates system
var parentWin = Root;
if (parentWin == null)
return null;
Vector2 clientPos = parentWin.ScreenToClient(position);
Vector2 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;
}
/// <summary>
/// Try get panel dock state
/// </summary>
/// <param name="splitterValue">Splitter value</param>
/// <returns>Dock State</returns>
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;
}
/// <summary>
/// Create child dock panel
/// </summary>
/// <param name="state">Dock panel state</param>
/// <param name="splitterValue">Initial splitter value</param>
/// <returns>Child panel</returns>
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();
}
/// <summary>
/// Called when last tab gets removed.
/// </summary>
protected virtual void OnLastTabRemoved()
{
// Check if dock panel is linked to the split panel
if (HasParent)
{
if (Parent.Parent is SplitPanel splitter)
{
// Check if has any child panels
var childPanel = new List<DockPanel>(_childPanels);
for (int i = 0; i < childPanel.Count; i++)
{
// Undock all tabs
var panel = childPanel[i];
int count = panel.TabsCount;
while (count-- > 0)
{
panel.GetTab(0).Close();
}
}
// 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)
{
DockWindow(state, window);
}
/// <summary>
/// Docks the window.
/// </summary>
/// <param name="state">The state.</param>
/// <param name="window">The window.</param>
protected virtual void DockWindow(DockState state, DockWindow window)
{
CreateTabsProxy();
// Check if dock like a tab or not
if (state == DockState.DockFill)
{
// Add tab
AddTab(window);
}
else
{
// Create child panel
var dockPanel = CreateChildPanel(state, DefaultSplitterValue);
// Dock window as a tab in a child panel
dockPanel.DockWindow(DockState.DockFill, window);
}
}
internal void UndockWindowInternal(DockWindow window)
{
UndockWindow(window);
}
/// <summary>
/// Undocks the window.
/// </summary>
/// <param name="window">The window.</param>
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();
}
}
/// <summary>
/// Adds the tab.
/// </summary>
/// <param name="window">The window to insert as a tab.</param>
protected virtual void AddTab(DockWindow window)
{
// Dock
_tabs.Add(window);
window.ParentDockPanel = this;
// Select tab
SelectTab(window);
}
private void CreateTabsProxy()
{
// Check if has no tabs proxy created
if (_tabsProxy == null)
{
// Create proxy and make set simple full dock
_tabsProxy = new DockPanelProxy(this);
_tabsProxy.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 - 2)
{
var tab = _tabs[index];
_tabs.RemoveAt(index);
_tabs.Insert(index + 1, tab);
}
}
/// <inheritdoc />
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();
}
}
}
}
}