// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.GUI.Docking { /// /// Helper class used to handle docking windows dragging and docking. /// public class DockHintWindow { private FloatWindowDockPanel _toMove; private Float2 _dragOffset; private Float2 _defaultWindowSize; private Rectangle _rectDock; private Rectangle _rectWindow; private Float2 _mouse; private DockState _toSet; private DockPanel _toDock; private bool _lateDragOffsetUpdate; private float _lateDragStartTimer; private bool _moveWindow; private Window _dragSourceWindow; private Rectangle _rLeft, _rRight, _rBottom, _rUpper, _rCenter; private DockHintWindow(FloatWindowDockPanel toMove, Window dragSourceWindow) { _toMove = toMove; _toSet = DockState.Float; var window = toMove.Window.Window; // Remove focus from drag target //_toMove.Focus(); //_toMove.Defocus(); // Focus window //window.Focus(); // Check if window is maximized and restore window. if (window.IsMaximized) { // Restore window and set position to mouse. var mousePos = window.MousePosition; var previousSize = window.Size; window.Restore(); window.Position = Platform.MousePosition - mousePos * window.Size / previousSize; } // If the _toMove window was not focused when initializing this window, the result vector only contains zeros // and to prevent a failure, we need to perform an update for the drag offset at later time which will be done in the OnMouseMove event handler. //if (mouseScreenPosition != Float2.Zero) // CalculateDragOffset(mouseScreenPosition); //else // _lateDragOffsetUpdate = true; // Get initial size _defaultWindowSize = window.Size; // Init proxy window /*Proxy.Init(ref _defaultWindowSize); // Bind events */ //FlaxEngine.Input.MouseMove += OnMouseMove; FlaxEngine.Scripting.Update += OnUpdate; _toMove.Window.Window.MouseUp += OnMouseUp; /* Proxy.Window.MouseUp += OnMouseUp; Proxy.Window.MouseMove += OnMouseMove; Proxy.Window.LostFocus += OnLostFocus; _toMove.Window.Window.MouseUp += OnMouseUp; // Intercept the drag release mouse event from source window */ // Update window GUI //Proxy.Window.GUI.PerformLayout(); // Update rectangles UpdateRects(); // Enable hit window presentation /*Proxy.Window.RenderingEnabled = true; Proxy.Window.Show(); Proxy.Window.Focus();*/ // Hide base window //window.Hide(); // window.Show(); _dragSourceWindow = dragSourceWindow; _moveWindow = _dragSourceWindow != null; if (_dragSourceWindow != null) // Detaching a tab from existing window { _dragOffset = new Float2(window.Size.X / 2, 10.0f); // TODO: when detaching tab in floating window (not main window), the drag source window is still main window? var dragSourceWindowWayland = toMove.MasterPanel?.RootWindow.Window ?? Editor.Instance.Windows.MainWindow; window.DoDragDrop("", _dragOffset, dragSourceWindowWayland); _dragSourceWindow.MouseUp += OnMouseUp; // The mouse up event is sent to the source window on Windows } else { var mouseClientPosition = Platform.MousePosition; CalculateDragOffset(mouseClientPosition); window.DoDragDrop("", _dragOffset, window); } //window.Show(); //window.BringToFront(); //window.Focus(); //toMove.OnShow(); // Perform layout again //windowGUI.PerformLayout(); // Start tracking mouse //Proxy.Window.StartTrackingMouse(false); } /// /// Releases unmanaged and - optionally - managed resources. /// public void Dispose() { // End tracking mouse /*Proxy.Window.EndTrackingMouse(); // Disable rendering Proxy.Window.RenderingEnabled = false; // Unbind events Proxy.Window.MouseUp -= OnMouseUp; Proxy.Window.MouseMove -= OnMouseMove; Proxy.Window.LostFocus -= OnLostFocus; if (_toMove?.Window?.Window) _toMove.Window.Window.MouseUp -= OnMouseUp; // Hide the proxy Proxy.Hide();*/ RemoveDockHints(); //FlaxEngine.Input.MouseMove -= OnMouseMove; FlaxEngine.Scripting.Update -= OnUpdate; if (_toMove?.Window?.Window) _toMove.Window.Window.MouseUp -= OnMouseUp; if (_dragSourceWindow != null) _dragSourceWindow.MouseUp -= OnMouseUp; // The mouse up event is sent to the source window on Windows if (_toMove == null) return; _toMove.Window.Window.Opacity = 1.0f; // Check if window won't be docked if (_toSet == DockState.Float) { var window = _toMove.Window?.Window; if (window == null) return; var mouse = Platform.MousePosition; // Move base window //window.Position = mouse - _dragOffset; // Show base window window.Show(); } else { bool hasNoChildPanels = _toMove.ChildPanelsCount == 0; // Check if window has only single tab if (hasNoChildPanels && _toMove.TabsCount == 1) { // Dock window _toMove.GetTab(0).Show(_toSet, _toDock); } // Check if dock as tab and has no child panels else if (hasNoChildPanels && _toSet == DockState.DockFill) { // Dock all tabs while (_toMove.TabsCount > 0) { _toMove.GetTab(0).Show(DockState.DockFill, _toDock); } } else { var selectedTab = _toMove.SelectedTab; // Dock the first tab into the target location if (_toMove.TabsCount > 0) { var firstTab = _toMove.GetTab(0); firstTab.Show(_toSet, _toDock); // Dock rest of the tabs while (_toMove.TabsCount > 0) { _toMove.GetTab(0).Show(DockState.DockFill, firstTab); } } // Keep selected tab being selected selectedTab?.SelectTab(); } // Focus target window _toDock.Root.Focus(); } _toMove = null; } /// /// Creates the new dragging hit window. /// /// Floating dock panel to move. /// The dock hint window object. public static DockHintWindow Create(FloatWindowDockPanel toMove) { if (toMove == null) throw new ArgumentNullException(); return new DockHintWindow(toMove, null); } /// /// Creates the new dragging hit window. /// /// Dock window to move. /// The dock hint window object. public static DockHintWindow Create(DockWindow toMove, Window dragSourceWindow) { if (toMove == null) throw new ArgumentNullException(); // Show floating toMove.CreateFloating(); // Move window to the mouse position (with some offset for caption bar) var window = (WindowRootControl)toMove.Root; /*var mouse = Platform.MousePosition; window.Window.Position = mouse - new Float2(8, 8);*/ // Get floating panel var floatingPanelToMove = window.GetChild(0) as FloatWindowDockPanel; return new DockHintWindow(floatingPanelToMove, dragSourceWindow); } /// /// Calculates window rectangle in the dock window. /// /// Window dock state. /// Dock panel rectangle. /// Calculated window rectangle. public static Rectangle CalculateDockRect(DockState state, ref Rectangle rect) { Rectangle result = rect; switch (state) { case DockState.DockFill: result.Location.Y += DockPanel.DefaultHeaderHeight; result.Size.Y -= DockPanel.DefaultHeaderHeight; break; case DockState.DockTop: result.Size.Y *= DockPanel.DefaultSplitterValue; break; case DockState.DockLeft: result.Size.X *= DockPanel.DefaultSplitterValue; break; case DockState.DockBottom: result.Location.Y += result.Size.Y * (1 - DockPanel.DefaultSplitterValue); result.Size.Y *= DockPanel.DefaultSplitterValue; break; case DockState.DockRight: result.Location.X += result.Size.X * (1 - DockPanel.DefaultSplitterValue); result.Size.X *= DockPanel.DefaultSplitterValue; break; } return result; } private void CalculateDragOffset(Float2 mouseScreenPosition) { var baseWinPos = _toMove.Window.Window.Position; //_dragOffset = mouseScreenPosition - baseWinPos; //Editor.Log($"_dragOffset: {_dragOffset}, mouse: {mouseScreenPosition}, basewinpos: {baseWinPos}"); } DockHintControl _dockHintDown; DockHintControl _dockHintUp; DockHintControl _dockHintLeft; DockHintControl _dockHintRight; DockHintControl _dockHintCenter; private void AddDockHints() { if (_toDock == null) return; if (_toDock.RootWindow.Window != _dragSourceWindow) _toDock.RootWindow.Window.MouseUp += OnMouseUp; _dockHintDown = AddHintControl(new Float2(0.5f, 1)); _dockHintUp = AddHintControl(new Float2(0.5f, 0)); _dockHintLeft = AddHintControl(new Float2(0, 0.5f)); _dockHintRight = AddHintControl(new Float2(1, 0.5f)); _dockHintCenter = AddHintControl(new Float2(0.5f, 0.5f)); DockHintControl AddHintControl(Float2 pivot) { DockHintControl hintControl = _toDock.AddChild(); hintControl.Size = new Float2(Proxy.HintWindowsSize); hintControl.BackgroundColor = Style.Current.DragWindow.AlphaMultiplied(0.6f); hintControl.Pivot = pivot; hintControl.PivotRelative = true; //.SetAnchorPreset(AnchorPresets.StretchAll, true); return hintControl; } } private void RemoveDockHints() { if (_toDock == null) return; if (_toDock.RootWindow.Window != _dragSourceWindow) _toDock.RootWindow.Window.MouseUp -= OnMouseUp; _dockHintDown?.Parent.RemoveChild(_dockHintDown); _dockHintUp?.Parent.RemoveChild(_dockHintUp); _dockHintLeft?.Parent.RemoveChild(_dockHintLeft); _dockHintRight?.Parent.RemoveChild(_dockHintRight); _dockHintCenter?.Parent.RemoveChild(_dockHintCenter); _dockHintDown = _dockHintUp = _dockHintLeft = _dockHintRight = _dockHintCenter = null; } private void UpdateRects() { // Cache mouse position _mouse = Platform.MousePosition; // Check intersection with any dock panel var uiMouse = _mouse; DockPanel dockPanel = null; if (_toMove.MasterPanel.HitTest(ref uiMouse, _toMove, out var hitResults)) { dockPanel = hitResults[0]; // Prefer panel which currently has focus foreach (var hit in hitResults) { if (hit.RootWindow.Window.IsFocused) { dockPanel = hit; break; } } // Prefer panel in the same window we hit earlier if (dockPanel?.RootWindow != _toDock?.RootWindow) { foreach (var hit in hitResults) { if (hit.RootWindow == _toDock?.RootWindow) { dockPanel = _toDock; break; } } } } if (dockPanel != _toDock) { RemoveDockHints(); _toDock = dockPanel; AddDockHints(); // Make sure the all the dock hint areas are not under other windows _toDock?.RootWindow.Window.BringToFront(); _toMove.RootWindow.Window.BringToFront(); // Make the dragged window transparent when dock hints are visible _toMove.Window.Window.Opacity = _toDock == null ? 1.0f : 0.4f; } // Check dock state to use bool showProxyHints = _toDock != null; bool showBorderHints = showProxyHints; bool showCenterHint = showProxyHints; DockHintControl hoveredHintControl = null; Float2 hoveredSizeOverride = Float2.Zero; if (showProxyHints) { // If moved window has not only tabs but also child panels disable docking as tab if (_toMove.ChildPanelsCount > 0) showCenterHint = false; // Disable docking windows with one or more dock panels inside if (_toMove.ChildPanelsCount > 0) showBorderHints = false; // Get dock area _rectDock = _toDock.DockAreaBounds; // Cache dock rectangles var size = _rectDock.Size / Platform.DpiScale; var offset = _toDock.PointFromScreen(_rectDock.Location); var borderMargin = 4.0f; var hintWindowsSize = Proxy.HintWindowsSize; var hintWindowsSize2 = hintWindowsSize * 0.5f; var hintPreviewSize = new Float2(Math.Max(Proxy.HintWindowsSize * 2, size.X * 0.5f), Math.Max(Proxy.HintWindowsSize * 2, size.Y * 0.5f)); var centerX = size.X * 0.5f; var centerY = size.Y * 0.5f; _rUpper = new Rectangle(centerX - hintWindowsSize2, borderMargin, hintWindowsSize, hintWindowsSize) + offset; _rBottom = new Rectangle(centerX - hintWindowsSize2, size.Y - hintWindowsSize - borderMargin, hintWindowsSize, hintWindowsSize) + offset; _rLeft = new Rectangle(borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; _rRight = new Rectangle(size.X - hintWindowsSize - borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; _rCenter = new Rectangle(centerX - hintWindowsSize2, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; // Hit test DockState toSet = DockState.Float; if (showBorderHints) { if (_rUpper.Contains(_toDock.PointFromScreen(_mouse))) { toSet = DockState.DockTop; hoveredHintControl = _dockHintUp; hoveredSizeOverride = new Float2(size.X, hintPreviewSize.Y); } else if (_rBottom.Contains(_toDock.PointFromScreen(_mouse))) { toSet = DockState.DockBottom; hoveredHintControl = _dockHintDown; hoveredSizeOverride = new Float2(size.X, hintPreviewSize.Y); } else if (_rLeft.Contains(_toDock.PointFromScreen(_mouse))) { toSet = DockState.DockLeft; hoveredHintControl = _dockHintLeft; hoveredSizeOverride = new Float2(hintPreviewSize.X, size.Y); } else if (_rRight.Contains(_toDock.PointFromScreen(_mouse))) { toSet = DockState.DockRight; hoveredHintControl = _dockHintRight; hoveredSizeOverride = new Float2(hintPreviewSize.X, size.Y); } } if (showCenterHint && _rCenter.Contains(_toDock.PointFromScreen(_mouse))) { toSet = DockState.DockFill; hoveredHintControl = _dockHintCenter; hoveredSizeOverride = new Float2(size.X, size.Y); } //if (toSet != DockState.Float) _toSet = toSet; } else { _toSet = DockState.Float; } Editor.Log($"docking: {_toSet}, pos: {_mouse}"); // Calculate proxy/dock/window rectangles if (_toDock == null) { // Floating window over nothing //_rectWindow = new Rectangle(_mouse - _dragOffset, _defaultWindowSize); if (hoveredHintControl != null) hoveredHintControl.BackgroundColor = Color.Green; } else { if (hoveredHintControl != _dockHintDown) { _dockHintDown.Size = new Float2(Proxy.HintWindowsSize); _dockHintDown.BackgroundColor = Style.Current.DragWindow.AlphaMultiplied(0.6f); } if (hoveredHintControl != _dockHintLeft) { _dockHintLeft.Size = new Float2(Proxy.HintWindowsSize); _dockHintLeft.BackgroundColor = Style.Current.DragWindow.AlphaMultiplied(0.6f); } if (hoveredHintControl != _dockHintRight) { _dockHintRight.Size = new Float2(Proxy.HintWindowsSize); _dockHintRight.BackgroundColor = Style.Current.DragWindow.AlphaMultiplied(0.6f); } if (hoveredHintControl != _dockHintUp) { _dockHintUp.Size = new Float2(Proxy.HintWindowsSize); _dockHintUp.BackgroundColor = Style.Current.DragWindow.AlphaMultiplied(0.6f); } if (hoveredHintControl != _dockHintCenter) { _dockHintCenter.Size = new Float2(Proxy.HintWindowsSize); _dockHintCenter.BackgroundColor = Style.Current.DragWindow.AlphaMultiplied(0.6f); } if (_toSet == DockState.Float) { // Floating window over dock panel if (hoveredHintControl != null) hoveredHintControl.BackgroundColor = Color.Red; } else { // Use only part of the dock panel to show hint if (hoveredHintControl != null) { hoveredHintControl.BackgroundColor = Style.Current.DragWindow.AlphaMultiplied(1.0f); hoveredHintControl.Size = hoveredSizeOverride; } } } if (showProxyHints) { if (hoveredHintControl != _dockHintDown) _dockHintDown.Location = _rBottom.Location; if (hoveredHintControl != _dockHintLeft) _dockHintLeft.Location = _rLeft.Location; if (hoveredHintControl != _dockHintRight) _dockHintRight.Location = _rRight.Location; if (hoveredHintControl != _dockHintUp) _dockHintUp.Location = _rUpper.Location; if (hoveredHintControl != _dockHintCenter) _dockHintCenter.Location = _rCenter.Location; // Update proxy hint windows visibility _dockHintDown.Visible = showProxyHints & showBorderHints; _dockHintLeft.Visible = showProxyHints & showBorderHints; _dockHintRight.Visible = showProxyHints & showBorderHints; _dockHintUp.Visible = showProxyHints & showBorderHints; _dockHintCenter.Visible = showProxyHints & showCenterHint; } // Update proxy window //Proxy.Window.ClientBounds = _rectWindow; } private void OnMouseUp(ref Float2 location, MouseButton button, ref bool handled) { Editor.Log("DockHintWindow.OnMouseUp"); if (button == MouseButton.Left) { Dispose(); } } private void OnUpdate() { //Editor.Log("OnUpdate"); var mousePos = Platform.MousePosition; if (_mouse != mousePos) { //Editor.Log($"mouse pos {_mouse} -> {mousePos}"); OnMouseMove(mousePos); } } private void OnMouseMove(Float2 mousePos) { if (_moveWindow) { _toMove.Window.Window.Position = mousePos - _dragOffset; } //Editor.Log("OnMouseMove"); // Recalculate the drag offset because the current mouse screen position was invalid when we initialized the window if (_lateDragOffsetUpdate) { // Calculate dragging offset and move window to the destination position CalculateDragOffset(mousePos); // Reset state _lateDragOffsetUpdate = false; } UpdateRects(); } private void OnLostFocus() { Dispose(); } public class DockHintControl : Control { public DockHintControl() { AnchorPreset = AnchorPresets.StretchAll; Offsets = Margin.Zero; } } /// /// Contains helper proxy windows shared across docking panels. They are used to visualize docking window locations. /// public static class Proxy { /// /// The drag proxy window. /// public static Window Window; /// /// The left hint proxy window. /// public static Window Left; /// /// The right hint proxy window. /// public static Window Right; /// /// The up hint proxy window. /// public static Window Up; /// /// The down hint proxy window. /// public static Window Down; /// /// The center hint proxy window. /// public static Window Center; /// /// The hint windows size. /// public const float HintWindowsSize = 32.0f; /// /// Initializes the hit proxy windows. Those windows are used to indicate drag target areas (left, right, top, bottom, etc.). /// public static void InitHitProxy() { CreateProxy(ref Left, "DockHint.Left"); CreateProxy(ref Right, "DockHint.Right"); CreateProxy(ref Up, "DockHint.Up"); CreateProxy(ref Down, "DockHint.Down"); CreateProxy(ref Center, "DockHint.Center"); } /// /// Initializes the hint window. /// /// Initial size of the proxy window. public static void Init(ref Float2 initSize) { if (Window == null) { var settings = CreateWindowSettings.Default; settings.Title = "DockHint.Window"; settings.Size = initSize; settings.AllowInput = true; settings.AllowMaximize = false; settings.AllowMinimize = false; settings.HasBorder = false; settings.HasSizingFrame = false; settings.Type = WindowType.Utility; settings.SupportsTransparency = true; settings.ShowInTaskbar = false; settings.ShowAfterFirstPaint = false; settings.IsTopmost = true; Window = Platform.CreateWindow(ref settings); Window.Opacity = 0.6f; Window.GUI.BackgroundColor = Style.Current.DragWindow; } else { // Resize proxy Window.ClientSize = initSize; } InitHitProxy(); } private static void CreateProxy(ref Window win, string name) { if (win != null) return; var settings = CreateWindowSettings.Default; settings.Title = name; settings.Size = new Float2(HintWindowsSize * Platform.DpiScale); settings.AllowInput = false; settings.AllowMaximize = false; settings.AllowMinimize = false; settings.HasBorder = false; settings.HasSizingFrame = false; settings.Type = WindowType.Utility; settings.SupportsTransparency = true; settings.ShowInTaskbar = false; settings.ActivateWhenFirstShown = false; settings.IsTopmost = true; settings.ShowAfterFirstPaint = false; win = Platform.CreateWindow(ref settings); win.Opacity = 0.6f; win.GUI.BackgroundColor = Style.Current.DragWindow; } /// /// Hides proxy windows. /// public static void Hide() { HideProxy(ref Window); HideProxy(ref Left); HideProxy(ref Right); HideProxy(ref Up); HideProxy(ref Down); HideProxy(ref Center); } private static void HideProxy(ref Window win) { if (win) { win.Hide(); } } /// /// Releases proxy data and windows. /// public static void Dispose() { DisposeProxy(ref Window); DisposeProxy(ref Left); DisposeProxy(ref Right); DisposeProxy(ref Up); DisposeProxy(ref Down); DisposeProxy(ref Center); } private static void DisposeProxy(ref Window win) { if (win) { win.Close(ClosingReason.User); win = null; } } } } }