mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-04-06 10:58:00 +02:00

DefaultDockTreeClass: TDockTreeClass by DefaultDockManagerClass: TDockManagerClass as requested by DoDi git-svn-id: trunk@22395 -
3956 lines
121 KiB
ObjectPascal
3956 lines
121 KiB
ObjectPascal
{
|
|
/***************************************************************************
|
|
LDockCtrl.pas
|
|
-----------------
|
|
|
|
***************************************************************************/
|
|
|
|
*****************************************************************************
|
|
* *
|
|
* This file is part of the Lazarus Component Library (LCL) *
|
|
* *
|
|
* See the file COPYING.modifiedLGPL.txt, included in this distribution, *
|
|
* for details about the copyright. *
|
|
* *
|
|
* This program is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
|
|
* *
|
|
*****************************************************************************
|
|
|
|
Author: Mattias Gaertner
|
|
|
|
Abstract:
|
|
This unit contains visual components for docking and streaming.
|
|
|
|
ToDo:
|
|
- move the docking code to TCustomAnchoredDockManager
|
|
and keep only the resizing code here.
|
|
- restoring layout: pages
|
|
- restoring layout: move form after inserting a control
|
|
- restoring layout: spiral splitter
|
|
- save TLazDockConfigNode to stream (atm only xml implemented)
|
|
- load TLazDockConfigNode from stream (atm only xml implemented)
|
|
}
|
|
unit LDockCtrl;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, Math, SysUtils, TypInfo, LCLProc, Controls, Forms, Menus,
|
|
LCLStrConsts, AvgLvlTree, StringHashList, ExtCtrls, LazConfigStorage,
|
|
LDockCtrlEdit, LDockTree;
|
|
|
|
type
|
|
TNonDockConfigNames = (
|
|
ndcnControlName, // '-Control ' + AControl.Name
|
|
ndcnChildIndex, // '-ID ' + IntToStr(AControl index in Parent) +' '+ AControl.ClassName
|
|
ndcnParent // '-Parent' : AControl.Parent
|
|
);
|
|
|
|
const
|
|
NonDockConfigNamePrefixes: array[TNonDockConfigNames] of string = (
|
|
'-Name ',
|
|
'-ID ',
|
|
'-Parent');
|
|
|
|
type
|
|
TLDConfigNodeType = (
|
|
ldcntControl,
|
|
ldcntForm,
|
|
ldcntSplitterLeftRight,// vertical splitter, can be moved left/right
|
|
ldcntSplitterUpDown, // horizontal splitter, can be moved up/down
|
|
ldcntPages,
|
|
ldcntPage
|
|
);
|
|
|
|
const
|
|
LDConfigNodeTypeNames: array[TLDConfigNodeType] of string = (
|
|
'Control',
|
|
'Form',
|
|
'SplitterLeftRight',
|
|
'SplitterUpDown',
|
|
'Pages',
|
|
'Page'
|
|
);
|
|
|
|
type
|
|
|
|
{ TLazDockConfigNode }
|
|
|
|
TLazDockConfigNode = class(TPersistent)
|
|
private
|
|
FBounds: TRect;
|
|
FClientBounds: TRect;
|
|
FName: string;
|
|
FParent: TLazDockConfigNode;
|
|
FSides: array[TAnchorKind] of string;
|
|
FTheType: TLDConfigNodeType;
|
|
FChilds: TFPList;
|
|
FWindowState: TWindowState;
|
|
function GetChildCount: Integer;
|
|
function GetChilds(Index: integer): TLazDockConfigNode;
|
|
function GetSides(Side: TAnchorKind): string;
|
|
procedure SetBounds(const AValue: TRect);
|
|
procedure SetClientBounds(const AValue: TRect);
|
|
procedure SetName(const AValue: string);
|
|
procedure SetParent(const AValue: TLazDockConfigNode);
|
|
procedure SetSides(Side: TAnchorKind; const AValue: string);
|
|
procedure SetTheType(const AValue: TLDConfigNodeType);
|
|
procedure DoAdd(ChildNode: TLazDockConfigNode);
|
|
procedure DoRemove(ChildNode: TLazDockConfigNode);
|
|
public
|
|
constructor Create(ParentNode: TLazDockConfigNode);
|
|
constructor Create(ParentNode: TLazDockConfigNode; const AName: string);
|
|
destructor Destroy; override;
|
|
procedure Clear;
|
|
procedure Assign(Source: TPersistent); override;
|
|
function FindByName(const AName: string; Recursive: boolean = false;
|
|
WithRoot: boolean = true): TLazDockConfigNode;
|
|
function IndexOf(const AName: string): Integer;
|
|
function GetScreenBounds: TRect;
|
|
function FindNeighbour(SiblingSide: TAnchorKind;
|
|
NilIfAmbiguous: boolean;
|
|
IgnoreSplitters: boolean = true): TLazDockConfigNode;
|
|
function IsTheOnlyNeighbour(Node: TLazDockConfigNode;
|
|
SiblingSide: TAnchorKind): boolean;
|
|
procedure SaveToConfig(Config: TConfigStorage; const Path: string = '');
|
|
procedure LoadFromConfig(Config: TConfigStorage; const Path: string = '');
|
|
function GetPath: string;
|
|
procedure WriteDebugReport;
|
|
function DebugLayoutAsString: string;
|
|
public
|
|
property Bounds: TRect read FBounds write SetBounds;
|
|
property ClientBounds: TRect read FClientBounds write SetClientBounds;
|
|
property Parent: TLazDockConfigNode read FParent write SetParent;
|
|
property Sides[Side: TAnchorKind]: string read GetSides write SetSides;
|
|
property ChildCount: Integer read GetChildCount;
|
|
property Childs[Index: integer]: TLazDockConfigNode read GetChilds; default;
|
|
published
|
|
property TheType: TLDConfigNodeType read FTheType write SetTheType
|
|
default ldcntControl;
|
|
property Name: string read FName write SetName;
|
|
property WindowState: TWindowState read FWindowState write FWindowState;
|
|
end;
|
|
|
|
{ TLazDockerConfig }
|
|
|
|
TLazDockerConfig = class
|
|
private
|
|
FDockerName: string;
|
|
FRoot: TLazDockConfigNode;
|
|
public
|
|
constructor Create(const ADockerName: string; ANode: TLazDockConfigNode);
|
|
destructor Destroy; override;
|
|
procedure WriteDebugReport;
|
|
property DockerName: string read FDockerName;
|
|
property Root: TLazDockConfigNode read FRoot;
|
|
end;
|
|
|
|
TCustomLazControlDocker = class;
|
|
TCustomLazDockingManager = class;
|
|
|
|
{ TAnchoredDockManager }
|
|
|
|
TAnchoredDockManager = class(TCustomAnchoredDockManager)
|
|
private
|
|
FConfigs: TCustomLazDockingManager;
|
|
public
|
|
procedure DisableLayout(Control: TControl); override;
|
|
procedure EnableLayout(Control: TControl); override;
|
|
property Configs: TCustomLazDockingManager read FConfigs;
|
|
end;
|
|
|
|
{ TCustomLazDockingManager }
|
|
|
|
TCustomLazDockingManager = class(TComponent)
|
|
private
|
|
FDockers: TFPList;
|
|
FManager: TAnchoredDockManager;
|
|
FConfigs: TFPList;// list of TLazDockerConfig
|
|
function GetConfigCount: Integer;
|
|
function GetConfigs(Index: Integer): TLazDockerConfig;
|
|
function GetDockerCount: Integer;
|
|
function GetDockers(Index: Integer): TCustomLazControlDocker;
|
|
protected
|
|
procedure Remove(Docker: TCustomLazControlDocker);
|
|
function Add(Docker: TCustomLazControlDocker): Integer;
|
|
public
|
|
constructor Create(TheOwner: TComponent); override;
|
|
destructor Destroy; override;
|
|
function FindDockerByName(const ADockerName: string;
|
|
Ignore: TCustomLazControlDocker = nil): TCustomLazControlDocker;
|
|
function FindControlByDockerName(const ADockerName: string;
|
|
Ignore: TCustomLazControlDocker = nil): TControl;
|
|
function FindDockerByControl(AControl: TControl;
|
|
Ignore: TCustomLazControlDocker = nil): TCustomLazControlDocker;
|
|
function CreateUniqueName(const AName: string;
|
|
Ignore: TCustomLazControlDocker): string;
|
|
function GetControlConfigName(AControl: TControl): string;
|
|
procedure DisableLayout(Control: TControl);
|
|
procedure EnableLayout(Control: TControl);
|
|
procedure SaveToConfig(Config: TConfigStorage; const Path: string = '');
|
|
procedure LoadFromConfig(Config: TConfigStorage; const Path: string = '');
|
|
procedure AddOrReplaceConfig(const DockerName: string;
|
|
Config: TLazDockConfigNode);
|
|
procedure ClearConfigs;
|
|
function GetConfigWithDockerName(const DockerName: string
|
|
): TLazDockerConfig;
|
|
function CreateLayout(const DockerName: string; VisibleControl: TControl;
|
|
ExceptionOnError: boolean = false): TLazDockConfigNode;
|
|
function ConfigIsCompatible(RootNode: TLazDockConfigNode;
|
|
ExceptionOnError: boolean = false): boolean;
|
|
|
|
procedure WriteDebugReport;
|
|
public
|
|
property Manager: TAnchoredDockManager read FManager;
|
|
property DockerCount: Integer read GetDockerCount;
|
|
property Dockers[Index: Integer]: TCustomLazControlDocker read GetDockers; default;
|
|
property ConfigCount: Integer read GetConfigCount;
|
|
property Configs[Index: Integer]: TLazDockerConfig read GetConfigs;
|
|
end;
|
|
|
|
{ TLazDockingManager }
|
|
|
|
TLazDockingManager = class(TCustomLazDockingManager)
|
|
published
|
|
end;
|
|
|
|
{ TLCDMenuItem }
|
|
|
|
TLCDMenuItem = class
|
|
public
|
|
Menu: TPopupMenu;
|
|
Item: TMenuItem;
|
|
end;
|
|
|
|
{ TCustomLazControlDocker
|
|
A component to connect a form to the TLazDockingManager.
|
|
When the control gets visible TCustomLazControlDocker restores the layout.
|
|
Before the control gets invisible, TCustomLazControlDocker saves the layout.
|
|
}
|
|
TCustomLazControlDocker = class(TComponent)
|
|
private
|
|
FControl: TControl;
|
|
FDockerName: string;
|
|
FEnabled: boolean;
|
|
FExtendPopupMenu: boolean;
|
|
FLayoutLock: integer;
|
|
FLocalizedName: string;
|
|
FManager: TCustomLazDockingManager;
|
|
FMenus: TFPList;// list of TLCDMenuItem
|
|
FPopupMenuItem: TMenuItem;
|
|
procedure SetControl(const AValue: TControl);
|
|
procedure SetDockerName(const AValue: string);
|
|
procedure SetExtendPopupMenu(const AValue: boolean);
|
|
procedure SetLocalizedName(const AValue: string);
|
|
procedure SetManager(const AValue: TCustomLazDockingManager);
|
|
procedure PopupMenuItemClick(Sender: TObject);
|
|
protected
|
|
procedure UpdatePopupMenu; virtual;
|
|
procedure Loaded; override;
|
|
function GetLocalizedName: string;
|
|
procedure ControlVisibleChanging(Sender: TObject);
|
|
procedure ControlVisibleChanged(Sender: TObject);
|
|
function CreateFormAndDockWithSplitter(Layout: TLazDockConfigNode;
|
|
Side: TAnchorKind): boolean;
|
|
function DockAsPage(Layout: TLazDockConfigNode): boolean;
|
|
procedure FixControlBounds(Layout: TLazDockConfigNode;
|
|
ResizedControl: TControl);
|
|
procedure ShrinkNeighbourhood(Layout: TLazDockConfigNode;
|
|
AControl: TControl; Sides: TAnchors);
|
|
function FindPageNeighbours(Layout: TLazDockConfigNode;
|
|
StartControl: TControl;
|
|
out AnchorControls: TAnchorControlsRect
|
|
): TFPList; // list of TControls
|
|
procedure Notification(AComponent: TComponent;
|
|
Operation: TOperation); override;
|
|
function FindLCDMenuItem(AMenu: TMenu): TLCDMenuItem;
|
|
function FindLCDMenuItem(AMenuItem: TMenuItem): TLCDMenuItem;
|
|
public
|
|
constructor Create(TheOwner: TComponent); override;
|
|
destructor Destroy; override;
|
|
procedure ShowDockingEditor; virtual;
|
|
function GetLayoutFromControl: TLazDockConfigNode;
|
|
procedure SaveLayout;
|
|
procedure RestoreLayout;
|
|
procedure DisableLayout;
|
|
procedure EnableLayout;
|
|
function ControlIsDocked: boolean;
|
|
function GetControlName(AControl: TControl): string;
|
|
procedure AddPopupMenu(Menu: TPopupMenu);
|
|
procedure RemovePopupMenu(Menu: TPopupMenu);
|
|
property Control: TControl read FControl write SetControl;
|
|
property Manager: TCustomLazDockingManager read FManager write SetManager;
|
|
property ExtendPopupMenu: boolean read FExtendPopupMenu write SetExtendPopupMenu default true;
|
|
property PopupMenuItem: TMenuItem read FPopupMenuItem;
|
|
property LocalizedName: string read FLocalizedName write SetLocalizedName;
|
|
property DockerName: string read FDockerName write SetDockerName;
|
|
property Enabled: boolean read FEnabled write FEnabled;// true to auto restore layout on show
|
|
property LayoutLock: integer read FLayoutLock;
|
|
end;
|
|
|
|
{ TLazControlDocker }
|
|
|
|
TLazControlDocker = class(TCustomLazControlDocker)
|
|
published
|
|
property Control;
|
|
property Manager;
|
|
property ExtendPopupMenu;
|
|
property DockerName;
|
|
property Enabled;
|
|
end;
|
|
|
|
|
|
function LDConfigNodeTypeNameToType(const s: string): TLDConfigNodeType;
|
|
|
|
function FindExclusiveSplitter(ControlList: TFPList; Side: TAnchorKind
|
|
): TLazDockSplitter;
|
|
function FindNextControlAnchoredToBoundary(AControl: TControl;
|
|
Boundary, SearchDirection: TAnchorKind): TControl;
|
|
function FindSplitterRectangularNeighbourhood(Splitter: TLazDockSplitter;
|
|
SplitterSide: TAnchorKind; out Bounds: TAnchorControlsRect): TFPList;
|
|
|
|
function dbgs(Node: TLazDockConfigNode): string; overload;
|
|
|
|
procedure Register;
|
|
|
|
|
|
implementation
|
|
|
|
|
|
procedure Register;
|
|
begin
|
|
RegisterComponents('Misc',[TLazDockingManager,TLazControlDocker]);
|
|
end;
|
|
|
|
function LDConfigNodeTypeNameToType(const s: string): TLDConfigNodeType;
|
|
begin
|
|
for Result:=Low(TLDConfigNodeType) to High(TLDConfigNodeType) do
|
|
if CompareText(LDConfigNodeTypeNames[Result],s)=0 then exit;
|
|
Result:=ldcntControl;
|
|
end;
|
|
|
|
function FindExclusiveSplitter(ControlList: TFPList;
|
|
Side: TAnchorKind): TLazDockSplitter;
|
|
{ find a splitter, that is not part of ControlList and anchored on one side
|
|
only to the controls in ControlList
|
|
|
|
For example: A,B,C,S1,S2 (S1,S2 are the splitters between)
|
|
|
|
|+-----+
|
|
|| A |
|
|
|+-----+
|
|
|-------
|
|
|+-+|+-+
|
|
||B|||C|
|
|
|+-+|+-+
|
|
will return the splitter to the left and Side=akLeft.
|
|
}
|
|
var
|
|
AControl: TControl;
|
|
i: Integer;
|
|
AParent: TWinControl;
|
|
j: Integer;
|
|
AnchoredToControlList: Boolean;
|
|
AnchoredToOther: Boolean;
|
|
begin
|
|
Result:=nil;
|
|
if (ControlList=nil) or (ControlList.Count=0) then exit;
|
|
AControl:=TControl(ControlList[0]);
|
|
if AControl.Parent=nil then exit;
|
|
AParent:=AControl.Parent;
|
|
for i:=0 to AParent.ControlCount-1 do begin
|
|
Result:=TLazDockSplitter(AParent.Controls[i]);
|
|
if (Result is TLazDockSplitter)
|
|
and (ControlList.IndexOf(Result)<0)
|
|
then begin
|
|
// ASplitter is a splitter which is not in the ControlList
|
|
// => check if the splitter is exclusively anchored
|
|
AnchoredToControlList:=false;
|
|
AnchoredToOther:=false;
|
|
for j:=0 to AParent.ControlCount-1 do begin
|
|
AControl:=TControl(ControlList[j]);
|
|
if (AControl.AnchorSide[Side].Control=Result) then
|
|
begin
|
|
if ControlList.IndexOf(AControl)>=0 then
|
|
AnchoredToControlList:=true
|
|
else begin
|
|
AnchoredToOther:=true;
|
|
break;
|
|
end;
|
|
end;
|
|
if AnchoredToControlList and not AnchoredToOther then
|
|
exit;
|
|
end;
|
|
end;
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
|
|
function FindNextControlAnchoredToBoundary(
|
|
AControl: TControl; Boundary, SearchDirection: TAnchorKind): TControl;
|
|
{ Finds the next control anchored to the same as AControl
|
|
For example:
|
|
|
|
------------------------------------
|
|
+-+|+-+|+-+|
|
|
|A|||B|||C||
|
|
|
|
With Boundary=akTop and SearchDirection=akRight the next of A is the splitter
|
|
to the right, then the splitter right of B, then C, ...
|
|
}
|
|
var
|
|
AParent: TWinControl;
|
|
i: Integer;
|
|
BoundaryControl: TControl;
|
|
begin
|
|
Result:=AControl.AnchorSide[SearchDirection].Control;
|
|
if (Result<>nil) then begin
|
|
if Result.Parent=AControl.Parent then
|
|
exit
|
|
else
|
|
exit(nil);
|
|
end else begin
|
|
AParent:=AControl.Parent;
|
|
if AParent=nil then exit;
|
|
BoundaryControl:=AControl.AnchorSide[Boundary].Control;
|
|
if BoundaryControl=nil then exit;
|
|
for i:=0 to AParent.ControlCount-1 do begin
|
|
Result:=AParent.Controls[i];
|
|
if (Result.AnchorSide[Boundary].Control=BoundaryControl)
|
|
and (Result.AnchorSide[OppositeAnchor[SearchDirection]].Control=AControl)
|
|
then
|
|
exit;
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
end;
|
|
|
|
function FindSplitterRectangularNeighbourhood(
|
|
Splitter: TLazDockSplitter; SplitterSide: TAnchorKind;
|
|
out Bounds: TAnchorControlsRect): TFPList;
|
|
{ Find a list of controls, building a rectangular area (without holes) touching
|
|
the complete SplitterSide of Splitter.
|
|
RectBounds will be the four bounding controls (Parent or Siblings).
|
|
|
|
For example: akRight of
|
|
|
|
|+-----+
|
|
|| A |
|
|
|+-----+
|
|
|-------
|
|
|+-+|+-+
|
|
||B|||C|
|
|
|+-+|+-+
|
|
|
|
will find A,B,C and the two splitter controls between A,B,C.
|
|
}
|
|
|
|
function IsBoundary(AControl: TControl): boolean;
|
|
var
|
|
a: TAnchorKind;
|
|
begin
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do if Bounds[a]=AControl then
|
|
exit(true);
|
|
Result:=false;
|
|
end;
|
|
|
|
var
|
|
BoundSide1: TAnchorKind;
|
|
BoundSide2: TAnchorKind;
|
|
AControl: TControl;
|
|
a: TAnchorKind;
|
|
Candidate: TControl;
|
|
j: Integer;
|
|
i: Integer;
|
|
OppSide: TAnchorKind;
|
|
begin
|
|
Result:=nil;
|
|
BoundSide1:=ClockwiseAnchor[SplitterSide];
|
|
BoundSide2:=OppositeAnchor[BoundSide1];
|
|
OppSide:=OppositeAnchor[SplitterSide];
|
|
Bounds[OppSide]:=Splitter;
|
|
Bounds[BoundSide1]:=Splitter.AnchorSide[BoundSide1].Control;
|
|
Bounds[BoundSide2]:=Splitter.AnchorSide[BoundSide2].Control;
|
|
Bounds[SplitterSide]:=nil;
|
|
if (Bounds[BoundSide1]=nil) or (Bounds[BoundSide2]=nil) then exit;
|
|
|
|
{ search for a splitter, bounded the same as Splitter
|
|
--------
|
|
| |
|
|
| |
|
|
--------
|
|
}
|
|
AControl:=Splitter;
|
|
repeat
|
|
AControl:=FindNextControlAnchoredToBoundary(AControl,BoundSide1,SplitterSide);
|
|
if AControl=nil then break;
|
|
if (AControl is TLazDockSplitter)
|
|
and (AControl.AnchorSide[BoundSide1].Control=Bounds[BoundSide1])
|
|
and (AControl.AnchorSide[BoundSide2].Control=Bounds[BoundSide2]) then begin
|
|
// found
|
|
Bounds[SplitterSide]:=AControl;
|
|
break;
|
|
end;
|
|
until false;
|
|
|
|
if (Bounds[SplitterSide]=nil)
|
|
and (Bounds[BoundSide1]<>Splitter.Parent) then begin
|
|
{ check for example
|
|
------|
|
|
| | "Splitter" is the left one
|
|
| |
|
|
--------
|
|
}
|
|
AControl:=Bounds[BoundSide1].AnchorSide[SplitterSide].Control;
|
|
if (AControl is TLazDockSplitter)
|
|
and (AControl.AnchorSide[BoundSide2].Control=Bounds[BoundSide2]) then
|
|
Bounds[SplitterSide]:=AControl;
|
|
end;
|
|
|
|
if (Bounds[SplitterSide]=nil)
|
|
and (Bounds[BoundSide2]<>Splitter.Parent) then begin
|
|
{ check for example
|
|
--------
|
|
| | "Splitter" is the left one
|
|
| |
|
|
------|
|
|
}
|
|
AControl:=Bounds[BoundSide2].AnchorSide[SplitterSide].Control;
|
|
if (AControl is TLazDockSplitter)
|
|
and (AControl.AnchorSide[BoundSide1].Control=Bounds[BoundSide1]) then
|
|
Bounds[SplitterSide]:=AControl;
|
|
end;
|
|
|
|
if (Bounds[SplitterSide]=nil)
|
|
and (Bounds[BoundSide1]<>Splitter.Parent) then begin
|
|
{ check for example
|
|
------|
|
|
| | "Splitter" is the left one
|
|
| |
|
|
------|
|
|
}
|
|
AControl:=Bounds[BoundSide1].AnchorSide[SplitterSide].Control;
|
|
if (Acontrol<>nil)
|
|
and (Bounds[BoundSide2]<>nil)
|
|
and (AControl=Bounds[BoundSide2].AnchorSide[SplitterSide].Control) then
|
|
Bounds[SplitterSide]:=AControl;
|
|
end;
|
|
|
|
if Bounds[SplitterSide]=nil then exit;
|
|
|
|
// find all controls between the Bounds
|
|
|
|
// find a first control in the area
|
|
AControl:=FindNextControlAnchoredToBoundary(Splitter,BoundSide1,SplitterSide);
|
|
if (AControl=nil) or (AControl=Bounds[SplitterSide]) then exit;
|
|
Result:=TFPlist.Create;
|
|
Result.Add(AControl);
|
|
|
|
// add the others with flood fill
|
|
i:=0;
|
|
while i<Result.Count-1 do begin
|
|
AControl:=TControl(Result[i]);
|
|
// test all anchored to
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
Candidate:=AControl.AnchorSide[a].Control;
|
|
if (not IsBoundary(Candidate)) and (Result.IndexOf(Candidate)<0) then
|
|
Result.Add(Candidate);
|
|
end;
|
|
// test all anchored by
|
|
for j:=0 to Splitter.Parent.ControlCount-1 do begin
|
|
Candidate:=Splitter.Parent.Controls[j];
|
|
if IsBoundary(Candidate) then continue;
|
|
if Result.IndexOf(Candidate)>=0 then continue;
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
if Candidate.AnchorSide[a].Control=AControl then begin
|
|
Result.Add(Candidate);
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
inc(i);
|
|
end;
|
|
end;
|
|
|
|
function dbgs(Node: TLazDockConfigNode): string;
|
|
begin
|
|
if Node=nil then begin
|
|
Result:='nil';
|
|
end else begin
|
|
Result:=Node.Name+'{Type='+LDConfigNodeTypeNames[Node.TheType]
|
|
+',ChildCnt='+IntToStr(Node.ChildCount)+'}';
|
|
end;
|
|
end;
|
|
|
|
{ TCustomLazControlDocker }
|
|
|
|
procedure TCustomLazControlDocker.SetManager(
|
|
const AValue: TCustomLazDockingManager);
|
|
begin
|
|
if FManager=AValue then exit;
|
|
//DebugLn('TCustomLazControlDocker.SetManager Old=',DbgSName(Manager),' New=',DbgSName(AValue));
|
|
if FManager<>nil then FManager.Remove(Self);
|
|
FManager:=AValue;
|
|
if FManager<>nil then FManager.Add(Self);
|
|
UpdatePopupMenu;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.UpdatePopupMenu;
|
|
// creates or deletes the PopupMenuItem to the PopupMenu of Control
|
|
begin
|
|
if [csDesigning, csLoading, csDestroying] * ComponentState <> [] then Exit;
|
|
|
|
//DebugLn('TCustomLazControlDocker.UpdatePopupMenu ',DbgSName(Control),' Manager=',DbgSName(Manager),' PopupMenu=',dbgs((Control<>nil) and (Control.PopupMenu<>nil)),' ExtendPopupMenu=',dbgs(ExtendPopupMenu));
|
|
|
|
if ExtendPopupMenu and (Control<>nil) and (Control.PopupMenu<>nil)
|
|
and (Manager<>nil) then begin
|
|
//DebugLn('TCustomLazControlDocker.UpdatePopupMenu ADDING');
|
|
AddPopupMenu(Control.PopupMenu);
|
|
end else begin
|
|
// delete PopupMenuItem
|
|
if (Control<>nil) and (Control.PopupMenu<>nil) then
|
|
RemovePopupMenu(Control.PopupMenu);
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.Loaded;
|
|
begin
|
|
inherited Loaded;
|
|
UpdatePopupMenu;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.ShowDockingEditor;
|
|
var
|
|
Dlg: TLazDockControlEditorDlg;
|
|
i: Integer;
|
|
TargetDocker: TCustomLazControlDocker;
|
|
Side: TAlign;
|
|
CurDocker: TCustomLazControlDocker;
|
|
Anchor: TAnchorKind;
|
|
begin
|
|
if (Manager=nil) or (Control=nil) then
|
|
raise Exception.Create('TCustomLazControlDocker.ShowDockingEditor no docking available');
|
|
Dlg:=TLazDockControlEditorDlg.Create(nil);
|
|
try
|
|
// fill the list of controls this control can dock to
|
|
Dlg.DockControlComboBox.Text:='';
|
|
Dlg.DockControlComboBox.Items.BeginUpdate;
|
|
//DebugLn('TCustomLazControlDocker.ShowDockingEditor Self=',DockerName,' Manager.DockerCount=',dbgs(Manager.DockerCount));
|
|
try
|
|
Dlg.DockControlComboBox.Items.Clear;
|
|
for i:=0 to Manager.DockerCount-1 do begin
|
|
CurDocker:=Manager.Dockers[i];
|
|
//DebugLn('TCustomLazControlDocker.ShowDockingEditor Self=',DockerName,' CurDocker=',CurDocker.DockerName);
|
|
if CurDocker=Self then continue;
|
|
if CurDocker.Control=nil then continue;
|
|
Dlg.DockControlComboBox.Items.Add(CurDocker.GetLocalizedName);
|
|
end;
|
|
Dlg.DockControlComboBox.Enabled:=Dlg.DockControlComboBox.Items.Count>0;
|
|
finally
|
|
Dlg.DockControlComboBox.Items.EndUpdate;
|
|
end;
|
|
|
|
// enable Undock button, if Control is docked
|
|
Dlg.UndockGroupBox.Enabled:=ControlIsDocked;
|
|
|
|
// enable enlarge buttons
|
|
Dlg.EnlargeLeftSpeedButton.Visible:=
|
|
Manager.Manager.EnlargeControl(Control,akLeft,true);
|
|
Dlg.EnlargeTopSpeedButton.Visible:=
|
|
Manager.Manager.EnlargeControl(Control,akTop,true);
|
|
Dlg.EnlargeRightSpeedButton.Visible:=
|
|
Manager.Manager.EnlargeControl(Control,akRight,true);
|
|
Dlg.EnlargeBottomSpeedButton.Visible:=
|
|
Manager.Manager.EnlargeControl(Control,akBottom,true);
|
|
|
|
Dlg.EnlargeGroupBox.Visible := Dlg.EnlargeLeftSpeedButton.Visible or
|
|
Dlg.EnlargeTopSpeedButton.Visible or
|
|
Dlg.EnlargeRightSpeedButton.Visible or
|
|
Dlg.EnlargeBottomSpeedButton.Visible;
|
|
|
|
if Dlg.ShowModal=mrOk then begin
|
|
// dock or undock
|
|
case Dlg.DlgResult of
|
|
ldcedrUndock:
|
|
// undock
|
|
Manager.Manager.UndockControl(Control,true);
|
|
ldcedrDockLeft,ldcedrDockRight,ldcedrDockTop,
|
|
ldcedrDockBottom,ldcedrDockPage:
|
|
// dock
|
|
begin
|
|
TargetDocker:=nil;
|
|
for i:=0 to Manager.DockerCount-1 do begin
|
|
CurDocker:=Manager.Dockers[i];
|
|
if CurDocker=Self then continue;
|
|
if Dlg.DockControlComboBox.Text=CurDocker.GetLocalizedName then
|
|
TargetDocker:=CurDocker;
|
|
end;
|
|
if TargetDocker=nil then begin
|
|
RaiseGDBException('TCustomLazControlDocker.ShowDockingEditor TargetDocker=nil');
|
|
end;
|
|
case Dlg.DlgResult of
|
|
ldcedrDockLeft: Side:=alLeft;
|
|
ldcedrDockRight: Side:=alRight;
|
|
ldcedrDockTop: Side:=alTop;
|
|
ldcedrDockBottom: Side:=alBottom;
|
|
ldcedrDockPage: Side:=alClient;
|
|
else RaiseGDBException('TCustomLazControlDocker.ShowDockingEditor ?');
|
|
end;
|
|
Manager.Manager.DockControl(Control,Side,TargetDocker.Control);
|
|
end;
|
|
ldcedrEnlargeLeft,ldcedrEnlargeTop,ldcedrEnlargeRight,ldcedrEnlargeBottom:
|
|
begin
|
|
// enlarge
|
|
case Dlg.DlgResult of
|
|
ldcedrEnlargeLeft: Anchor:=akLeft;
|
|
ldcedrEnlargeRight: Anchor:=akRight;
|
|
ldcedrEnlargeTop: Anchor:=akTop;
|
|
ldcedrEnlargeBottom: Anchor:=akBottom;
|
|
else RaiseGDBException('TCustomLazControlDocker.ShowDockingEditor ?');
|
|
end;
|
|
Manager.Manager.EnlargeControl(Control,Anchor);
|
|
end;
|
|
end;
|
|
end;
|
|
finally
|
|
Dlg.Free;
|
|
end;
|
|
end;
|
|
|
|
function TCustomLazControlDocker.GetLocalizedName: string;
|
|
begin
|
|
Result:=LocalizedName;
|
|
if LocalizedName='' then begin
|
|
Result:=DockerName;
|
|
if (Result='') and (Control<>nil) then
|
|
Result:=Control.Name;
|
|
if Result='' then
|
|
Result:=Name;
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.ControlVisibleChanging(Sender: TObject);
|
|
begin
|
|
if Manager=nil then exit;
|
|
if Control<>Sender then begin
|
|
DebugLn('TCustomLazControlDocker.ControlVisibleChanging WARNING: ',
|
|
DbgSName(Control),'<>',DbgSName(Sender));
|
|
exit;
|
|
end;
|
|
{$IFDEF VerboseAnchorDocking}
|
|
DebugLn(['TCustomLazControlDocker.ControlVisibleChanging Sender=',DbgSName(Sender),' Control.Visible=',Control.Visible]);
|
|
DumpStack;
|
|
{$ENDIF}
|
|
if FLayoutLock>0 then begin
|
|
DebugLn(['TCustomLazControlDocker.ControlVisibleChanging ',DbgSName(Control),' ignore because FLayoutLock=',FLayoutLock]);
|
|
exit;
|
|
end;
|
|
|
|
if Control.Visible then begin
|
|
// control will be hidden -> the layout will change
|
|
// save the layout for later restore
|
|
SaveLayout;
|
|
{$IFDEF VerboseAnchorDocking}
|
|
DebugLn(['TCustomLazControlDocker.ControlVisibleChanging Parent=',DbgSName(Control.Parent)]);
|
|
{$ENDIF}
|
|
end else if ([csDestroying,csDesigning,csLoading]*ComponentState=[]) then begin
|
|
// the control will become visible -> dock it to restore the last layout
|
|
RestoreLayout;
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.ControlVisibleChanged(Sender: TObject);
|
|
begin
|
|
if Manager=nil then exit;
|
|
{$IFDEF VerboseAnchorDocking}
|
|
DebugLn(['TCustomLazControlDocker.ControlVisibleChanged Sender=',DbgSName(Sender),' Control.Visible=',Control.Visible]);
|
|
//DumpStack;
|
|
{$ENDIF}
|
|
if FLayoutLock>0 then begin
|
|
//DebugLn(['TCustomLazControlDocker.ControlVisibleChanged ',DbgSName(Control),' ignore because FLayoutLock=',FLayoutLock]);
|
|
exit;
|
|
end;
|
|
|
|
if Control.Visible then begin
|
|
// the control has become visible
|
|
end else if ([csDesigning,csLoading]*ComponentState=[]) then begin
|
|
// control was hidden (or destroyed)
|
|
if ControlIsDocked
|
|
and (Manager<>nil)
|
|
and (Manager.Manager<>nil) then begin
|
|
// auto undock
|
|
DebugLn(['TCustomLazControlDocker.ControlVisibleChanged auto undock ',DbgSName(Control)]);
|
|
Manager.Manager.UndockControl(Control,false);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TCustomLazControlDocker.CreateFormAndDockWithSplitter(
|
|
Layout: TLazDockConfigNode; Side: TAnchorKind): boolean;
|
|
{ Add a splitter to Side and dock to it. For example:
|
|
|
|
Side=akLeft
|
|
--------+ -------------+
|
|
---+| ---+#+------+|
|
|
A || A |#| ||
|
|
---+| ---+#| ||
|
|
====| -> ====#| Self ||
|
|
---+| ---+#| ||
|
|
B || B |#| ||
|
|
---+| ---+#+------+|
|
|
--------+ -------------+
|
|
If A has no parent, a TLazDockForm is created.
|
|
|
|
To get space for Self, either A,B are shrinked
|
|
and/or the parent of A,B is enlarged (including the grand parents of A,B).
|
|
}
|
|
|
|
function FindNextNeighbour(SplitterNode: TLazDockConfigNode;
|
|
Neighbours: TFPList; Append: boolean): boolean;
|
|
var
|
|
Neighbour: TControl;
|
|
i: Integer;
|
|
Sibling: TControl;
|
|
Search: TAnchorKind;
|
|
Splitter, CurSplitter: TLazDockSplitter;
|
|
OldAnchor, CurAnchor: TControl;
|
|
NewNeighbour: TControl;
|
|
NodeName: String;
|
|
Node: TLazDockConfigNode;
|
|
begin
|
|
Result:=false;
|
|
if Neighbours=nil then exit;
|
|
if Append then
|
|
Neighbour:=TControl(Neighbours[Neighbours.Count-1])
|
|
else
|
|
Neighbour:=TControl(Neighbours[0]);
|
|
if Neighbour.Parent=nil then exit;
|
|
if not GetLazDockSplitterOrParent(Neighbour,OppositeAnchor[Side],OldAnchor)
|
|
then exit;
|
|
// search direction
|
|
if (Side in [akLeft,akRight]) then begin
|
|
if Append then Search:=akBottom else Search:=akTop;
|
|
end else begin
|
|
if Append then Search:=akRight else Search:=akLeft;
|
|
end;
|
|
// find splitter
|
|
if not GetLazDockSplitter(Neighbour,Search,Splitter) then exit;
|
|
if (not GetLazDockSplitterOrParent(Splitter,OppositeAnchor[Side],CurAnchor))
|
|
or (CurAnchor<>OldAnchor) then exit;
|
|
// find neighbour (anchored to Splitter and OldAnchor)
|
|
NewNeighbour:=nil;
|
|
for i:=0 to Neighbour.Parent.ControlCount-1 do begin
|
|
Sibling:=Neighbour.Parent.Controls[i];
|
|
if Sibling=Neighbour then continue;
|
|
if (not GetLazDockSplitter(Sibling,OppositeAnchor[Search],CurSplitter))
|
|
or (CurSplitter<>Splitter) then continue;
|
|
if (not GetLazDockSplitterOrParent(Splitter,OppositeAnchor[Side],CurAnchor))
|
|
or (CurAnchor<>OldAnchor) then continue;
|
|
// Neighbour control found
|
|
NewNeighbour:=Sibling;
|
|
break;
|
|
end;
|
|
if NewNeighbour=nil then exit;
|
|
// check if this control is mentioned in Layout as Neighbour
|
|
NodeName:=Manager.GetControlConfigName(NewNeighbour);
|
|
if NodeName='' then exit;
|
|
Node:=Layout.FindByName(NodeName,true);
|
|
if Node=nil then exit;
|
|
if CompareText(Node.Sides[OppositeAnchor[Side]],SplitterNode.Name)<>0 then
|
|
exit;
|
|
// success: NewNeighbour is a neighbour on the current form and in the Layout
|
|
if Append then begin
|
|
Neighbours.Add(Splitter);
|
|
Neighbours.Add(NewNeighbour);
|
|
end else begin
|
|
Neighbours.Insert(0,Neighbour);
|
|
Neighbours.Insert(0,Splitter);
|
|
end;
|
|
Result:=true;
|
|
end;
|
|
|
|
var
|
|
SelfNode: TLazDockConfigNode;
|
|
SplitterNode: TLazDockConfigNode;
|
|
NeighbourNode: TLazDockConfigNode;
|
|
NeighbourControl: TControl;
|
|
NewParent: TWinControl;
|
|
Splitter: TLazDockSplitter;
|
|
a: TAnchorKind;
|
|
NewParentCreated: Boolean;
|
|
SplitterSize: LongInt;
|
|
i: Integer;
|
|
Side2: TAnchorKind;
|
|
Side3: TAnchorKind;
|
|
Neighbours: TFPList;
|
|
LeftTopNeighbour: TControl;
|
|
RightBottomNeighbour: TControl;
|
|
begin
|
|
Result:=false;
|
|
DebugLn(['TCustomLazControlDocker.CreateFormAndDockWithSplitter DockerName="',DockerName,'"']);
|
|
SelfNode:=Layout.FindByName(DockerName,true);
|
|
if SelfNode=nil then begin
|
|
DebugLn(['TCustomLazControlDocker.CreateFormAndDockWithSplitter SelfNode not found DockerName="',DockerName,'"']);
|
|
exit;
|
|
end;
|
|
SplitterNode:=Layout.FindByName(SelfNode.Sides[Side]);
|
|
if SplitterNode=nil then begin
|
|
DebugLn(['TCustomLazControlDocker.CreateFormAndDockWithSplitter SplitterNode not found "',SelfNode.Sides[Side],'"']);
|
|
exit;
|
|
end;
|
|
|
|
// search one Neighbour
|
|
NeighbourNode:=SplitterNode.FindNeighbour(OppositeAnchor[Side],false);
|
|
if NeighbourNode=nil then begin
|
|
DebugLn(['TCustomLazControlDocker.CreateFormAndDockWithSplitter NeighbourNode not found']);
|
|
exit;
|
|
end;
|
|
NeighbourControl:=Manager.FindControlByDockerName(NeighbourNode.Name);
|
|
if NeighbourControl=nil then begin
|
|
DebugLn(['TCustomLazControlDocker.CreateFormAndDockWithSplitter NeighbourControl not found "',NeighbourNode.Name,'"']);
|
|
exit;
|
|
end;
|
|
|
|
Neighbours:=nil;
|
|
NewParent:=nil;
|
|
try
|
|
if NeighbourControl.Parent=nil then begin
|
|
// NeighbourControl is a standalone control (e.g. an undocked form)
|
|
// => create a new TLazDockForm and put both controls into it
|
|
NewParent:=Manager.Manager.CreateForm;
|
|
NewParentCreated:=true;
|
|
end else begin
|
|
// NeighbourControl is docked
|
|
NewParent:=NeighbourControl.Parent;
|
|
NewParentCreated:=false;
|
|
end;
|
|
|
|
NewParent.DisableAlign;
|
|
|
|
// create a splitter
|
|
Splitter:=TLazDockSplitter.Create(nil);
|
|
Splitter.Align:=alNone;
|
|
Splitter.Beveled:=true;
|
|
Splitter.ResizeAnchor:=Side;
|
|
Splitter.Parent:=NewParent;
|
|
if Side in [akLeft,akRight] then
|
|
SplitterSize:=Manager.Manager.GetSplitterWidth(Splitter)
|
|
else
|
|
SplitterSize:=Manager.Manager.GetSplitterHeight(Splitter);
|
|
if Side in [akLeft,akRight] then
|
|
Splitter.Width:=SplitterSize
|
|
else
|
|
Splitter.Height:=SplitterSize;
|
|
DebugLn(['TCustomLazControlDocker.CreateFormAndDockWithSplitter Splitter=',DbgSName(Splitter),' ',dbgs(Splitter.BoundsRect)]);
|
|
|
|
if NewParentCreated then begin
|
|
// resize NewParent to bounds of NeighbourControl
|
|
if (NewParent is TCustomForm)
|
|
and (NeighbourControl is TCustomForm) then;
|
|
TCustomForm(NewParent).WindowState:=
|
|
TCustomForm(NeighbourControl).WindowState;
|
|
NewParent.BoundsRect:=NeighbourControl.BoundsRect;
|
|
NeighbourControl.Parent:=NewParent;
|
|
NeighbourControl.Align:=alNone;
|
|
end;
|
|
DebugLn(['TCustomLazControlDocker.CreateFormAndDockWithSplitter NewParent=',DbgSName(NewParent),' ',dbgs(NewParent.BoundsRect)]);
|
|
DebugLn(['TCustomLazControlDocker.CreateFormAndDockWithSplitter NeighbourControl=',DbgSName(NeighbourControl),' ',dbgs(NeighbourControl.BoundsRect)]);
|
|
|
|
// move Control to the new parent
|
|
Control.Parent:=NewParent;
|
|
Control.Align:=alNone;
|
|
Control.BoundsRect:=SelfNode.Bounds;
|
|
DebugLn(['TCustomLazControlDocker.CreateFormAndDockWithSplitter Control=',DbgSName(Control),' ',dbgs(Control.BoundsRect)]);
|
|
|
|
if NewParentCreated then begin
|
|
// one Neighbour, one splitter and the Control
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
// anchor Control
|
|
if a=Side then
|
|
Control.AnchorToNeighbour(a,0,Splitter)
|
|
else
|
|
Control.AnchorParallel(a,0,NewParent);
|
|
// anchor Splitter
|
|
if (Side in [akLeft,akRight]) <> (a in [akLeft,akRight]) then
|
|
Splitter.AnchorParallel(a,0,NewParent);
|
|
// anchor Neighbour
|
|
if a=OppositeAnchor[Side] then
|
|
NeighbourControl.AnchorToNeighbour(a,0,Splitter)
|
|
else
|
|
NeighbourControl.AnchorParallel(a,0,NewParent);
|
|
end;
|
|
end else begin
|
|
// several Neighbours
|
|
|
|
// find all Neighbours
|
|
Neighbours:=TFPList.Create;
|
|
Neighbours.Add(NeighbourControl);
|
|
while FindNextNeighbour(SplitterNode,Neighbours,false) do ;
|
|
while FindNextNeighbour(SplitterNode,Neighbours,true) do ;
|
|
// Neighbours now contains all controls, that need to be reanchored
|
|
// to the new Splitter
|
|
|
|
if Side in [akLeft,akRight] then
|
|
Side2:=akTop
|
|
else
|
|
Side2:=akLeft;
|
|
Side3:=OppositeAnchor[Side2];
|
|
LeftTopNeighbour:=TControl(Neighbours[0]);
|
|
RightBottomNeighbour:=TControl(Neighbours[Neighbours.Count-1]);
|
|
|
|
// anchor Control
|
|
Control.AnchorToNeighbour(Side,0,Splitter);
|
|
Control.AnchorSame(OppositeAnchor[Side],NeighbourControl);
|
|
Control.AnchorSame(Side2,LeftTopNeighbour);
|
|
Control.AnchorSame(Side3,RightBottomNeighbour);
|
|
|
|
// anchor Splitter
|
|
Splitter.AnchorSame(Side2,LeftTopNeighbour);
|
|
Splitter.AnchorSame(Side3,RightBottomNeighbour);
|
|
|
|
// anchor Neighbours
|
|
for i:=0 to Neighbours.Count-1 do begin
|
|
NeighbourControl:=TControl(Neighbours[i]);
|
|
DebugLn(['TCustomLazControlDocker.CreateFormAndDockWithSplitter NeighbourControl=',DbgSName(NeighbourControl),' i=',i]);
|
|
NeighbourControl.AnchorToNeighbour(OppositeAnchor[Side],0,Splitter);
|
|
end;
|
|
end;
|
|
|
|
if Side in [akLeft,akRight] then
|
|
ShrinkNeighbourhood(Layout,Control,[akLeft,akRight])
|
|
else
|
|
ShrinkNeighbourhood(Layout,Control,[akTop,akBottom]);
|
|
FixControlBounds(Layout,Control);
|
|
Manager.Manager.UpdateTitlePosition(Control);
|
|
|
|
finally
|
|
Neighbours.Free;
|
|
if NewParent<>nil then begin
|
|
NewParent.EnableAlign;
|
|
NewParent.Visible:=true;
|
|
end;
|
|
end;
|
|
|
|
Result:=true;
|
|
end;
|
|
|
|
function TCustomLazControlDocker.DockAsPage(Layout: TLazDockConfigNode
|
|
): boolean;
|
|
// dock as page like in Layout
|
|
// Requirements: Parent in Layout is a ldcntPage and a parent control exists.
|
|
var
|
|
SelfNode: TLazDockConfigNode;
|
|
PageNode: TLazDockConfigNode;
|
|
PageNodeIndex: LongInt;
|
|
PagesNode: TLazDockConfigNode;
|
|
NeighbourNode: TLazDockConfigNode;
|
|
NeighbourControl: TControl;
|
|
TopForm: TLazDockForm;
|
|
Pages: TLazDockPages;
|
|
NeighbourPage: TLazDockPage;
|
|
NeighbourControlPageIndex: LongInt;
|
|
Page: TLazDockPage;
|
|
PageIndex: LongInt;
|
|
NeighbourList: TFPList;
|
|
AnchorControls: TAnchorControlsRect;
|
|
TopFormBounds: TRect;
|
|
i: Integer;
|
|
a: TAnchorKind;
|
|
begin
|
|
Result:=false;
|
|
DebugLn(['TCustomLazControlDocker.DockAsPage DockerName="',DockerName,'"']);
|
|
SelfNode:=Layout.FindByName(DockerName,true);
|
|
if SelfNode=nil then begin
|
|
DebugLn(['TCustomLazControlDocker.DockAsPage SelfNode not found DockerName="',DockerName,'"']);
|
|
exit;
|
|
end;
|
|
PageNode:=SelfNode.Parent;
|
|
if PageNode=nil then begin
|
|
DebugLn(['TCustomLazControlDocker.DockAsPage SelfNode.Parent=nil DockerName="',DockerName,'"']);
|
|
exit;
|
|
end;
|
|
if PageNode.TheType<>ldcntPage then begin
|
|
DebugLn(['TCustomLazControlDocker.DockAsPage PageNode.TheType<>ldcntPage DockerName="',DockerName,'"']);
|
|
exit;
|
|
end;
|
|
if PageNode.ChildCount<>1 then begin
|
|
DebugLn(['TCustomLazControlDocker.DockAsPage PageNode.ChildCount<>1 DockerName="',DockerName,'"']);
|
|
exit;
|
|
end;
|
|
|
|
PagesNode:=PageNode.Parent;
|
|
PageNodeIndex:=PagesNode.IndexOf(PageNode.Name);
|
|
if PageNodeIndex>0 then
|
|
NeighbourNode:=PagesNode.Childs[PageNodeIndex-1].Childs[0]
|
|
else
|
|
NeighbourNode:=PagesNode.Childs[PageNodeIndex+1].Childs[0];
|
|
NeighbourControl:=Manager.FindControlByDockerName(NeighbourNode.Name);
|
|
if NeighbourControl=nil then begin
|
|
DebugLn(['TCustomLazControlDocker.DockAsPage NeighbourControl not found "',NeighbourNode.Name,'"']);
|
|
exit;
|
|
end;
|
|
|
|
if NeighbourControl.Parent=nil then begin
|
|
// NeighbourControl is a top level control (no parents, no neighbours)
|
|
// => create a TLazDockForm with a TLazDockPages and two TLazDockPage
|
|
TopForm:=Manager.Manager.CreateForm;
|
|
TopFormBounds:=PagesNode.Bounds;
|
|
// TODO: shrink TopFormBounds
|
|
TopForm.BoundsRect:=TopFormBounds;
|
|
|
|
Pages:=TLazDockPages.Create(nil);
|
|
Pages.DisableAlign;
|
|
try
|
|
Pages.Parent:=TopForm;
|
|
Pages.AnchorClient(0);
|
|
if PageNodeIndex>0 then begin
|
|
Pages.Pages.Add(NeighbourControl.Caption);
|
|
Pages.Pages.Add(Control.Caption);
|
|
NeighbourPage:=Pages.Page[0];
|
|
Page:=Pages.Page[1];
|
|
end else begin
|
|
Pages.Pages.Add(Control.Caption);
|
|
Pages.Pages.Add(NeighbourControl.Caption);
|
|
Page:=Pages.Page[0];
|
|
NeighbourPage:=Pages.Page[1];
|
|
end;
|
|
NeighbourControl.Parent:=NeighbourPage;
|
|
NeighbourControl.AnchorClient(0);
|
|
Control.Parent:=Page;
|
|
Control.AnchorClient(0);
|
|
finally
|
|
Pages.EnableAlign;
|
|
end;
|
|
end else if NeighbourControl.Parent is TLazDockPage then begin
|
|
// NeighbourControl is on a page
|
|
// => insert a new page
|
|
NeighbourPage:=TLazDockPage(NeighbourControl.Parent);
|
|
NeighbourControlPageIndex:=NeighbourPage.PageIndex;
|
|
if PageNodeIndex>0 then begin
|
|
// insert left
|
|
PageIndex:=NeighbourControlPageIndex;
|
|
end else begin
|
|
// insert right
|
|
PageIndex:=NeighbourControlPageIndex+1;
|
|
end;
|
|
Pages.Pages.Insert(PageIndex,Control.Caption);
|
|
Page:=Pages.Page[PageIndex];
|
|
Control.Parent:=Page;
|
|
Control.AnchorClient(0);
|
|
// TODO enlarge parents
|
|
end else begin
|
|
// NeighbourControl is a child control, but the parent is not yet a page
|
|
// => collect a rectangular area of neighbour controls to build a page
|
|
NeighbourList:=FindPageNeighbours(Layout,NeighbourControl,AnchorControls);
|
|
try
|
|
NeighbourControl.Parent.DisableAlign;
|
|
// TODO: create a PageControl and two pages. And move the neighbours onto
|
|
// one page and Control to the other page.
|
|
|
|
// create a TLazDockPages
|
|
Pages:=TLazDockPages.Create(nil);
|
|
// add it to the place where the neighbours are
|
|
Pages.Parent:=NeighbourControl.Parent;
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
Pages.AnchorSide[a].Control:=AnchorControls[a];
|
|
if (AnchorControls[a]=Pages.Parent)=(a in [akLeft,akTop]) then
|
|
Pages.AnchorSide[a].Side:=asrLeft
|
|
else
|
|
Pages.AnchorSide[a].Side:=asrRight;
|
|
end;
|
|
Pages.Anchors:=[akLeft,akTop,akRight,akBottom];
|
|
|
|
// create the two pages
|
|
Pages.Pages.Insert(0,NeighbourControl.Caption);
|
|
NeighbourPage:=Pages.Page[0];
|
|
|
|
// move the neighbours
|
|
for i:=0 to NeighbourList.Count-1 do begin
|
|
NeighbourControl:=TControl(NeighbourList[i]);
|
|
NeighbourControl.Parent:=NeighbourPage;
|
|
// fix anchors
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
if NeighbourControl.AnchorSide[a].Control=AnchorControls[a] then begin
|
|
NeighbourControl.AnchorSide[a].Control:=NeighbourPage;
|
|
if a in [akLeft,akTop] then
|
|
NeighbourControl.AnchorSide[a].Side:=asrLeft;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// add a second page
|
|
PageIndex:=1;
|
|
Pages.Pages.Insert(PageIndex,Control.Caption);
|
|
Page:=Pages.Page[PageIndex];
|
|
|
|
// add the control into the second page
|
|
Control.Parent:=Page;
|
|
Control.AnchorClient(0);
|
|
|
|
finally
|
|
NeighbourList.Free;
|
|
NeighbourControl.Parent.EnableAlign;
|
|
end;
|
|
end;
|
|
|
|
Result:=true;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.FixControlBounds(Layout: TLazDockConfigNode;
|
|
ResizedControl: TControl);
|
|
{ Fix bounds after inserting AddedControl }
|
|
type
|
|
TControlInfo = record
|
|
Control: TControl;
|
|
Docker: TLazDockerConfig;
|
|
Node: TLazDockConfigNode;
|
|
MinLeft: integer;
|
|
MinLeftValid: boolean;
|
|
MinLeftCalculating: boolean;
|
|
MinTop: integer;
|
|
MinTopValid: boolean;
|
|
MinTopCalculating: boolean;
|
|
MinClientSize: TPoint;
|
|
MinClientSizeValid: boolean;
|
|
end;
|
|
PControlInfo = ^TControlInfo;
|
|
var
|
|
ControlToInfo: TPointerToPointerTree;
|
|
NodeToInfo: TPointerToPointerTree;
|
|
|
|
procedure InitInfos;
|
|
begin
|
|
ControlToInfo:=TPointerToPointerTree.Create;
|
|
NodeToInfo:=TPointerToPointerTree.Create;
|
|
end;
|
|
|
|
procedure FreeInfos;
|
|
var
|
|
AControlPtr: Pointer;
|
|
AnInfo: Pointer;
|
|
Info: PControlInfo;
|
|
begin
|
|
if ControlToInfo.GetFirst(AControlPtr,AnInfo) then begin
|
|
repeat
|
|
Info:=PControlInfo(AnInfo);
|
|
Dispose(Info);
|
|
until not ControlToInfo.GetNext(AControlPtr,AControlPtr,AnInfo);
|
|
end;
|
|
ControlToInfo.Free;
|
|
NodeToInfo.Free;
|
|
end;
|
|
|
|
function GetInfo(AControl: TControl): PControlInfo;
|
|
begin
|
|
Result:=ControlToInfo[AControl];
|
|
if Result=nil then begin
|
|
New(Result);
|
|
FillChar(Result^,SizeOf(TControlInfo),0);
|
|
Result^.Control:=AControl;
|
|
Result^.Node:=
|
|
Layout.FindByName(Manager.GetControlConfigName(AControl),true);
|
|
ControlToInfo[AControl]:=Result;
|
|
if ControlToInfo[AControl]<>Result then
|
|
RaiseGDBException('');
|
|
end;
|
|
end;
|
|
|
|
function CalculateMinimumLeft(AControl: TControl): integer;
|
|
var
|
|
Info: PControlInfo;
|
|
|
|
procedure Improve(Neighbour: TControl);
|
|
begin
|
|
if Neighbour=nil then exit;
|
|
if Neighbour.Parent<>AControl.Parent then exit;
|
|
//DebugLn(['Left Improve AControl=',DbgSName(AControl),' Neighbour=',DbgSName(Neighbour)]);
|
|
Info^.MinLeft:=Max(Info^.MinLeft,
|
|
CalculateMinimumLeft(Neighbour)+Neighbour.Width);
|
|
end;
|
|
|
|
var
|
|
i: Integer;
|
|
Sibling: TControl;
|
|
begin
|
|
Info:=GetInfo(AControl);
|
|
if not Info^.MinLeftValid then begin
|
|
//DebugLn(['CalculateMinimumLeft ',DbgSName(AControl)]);
|
|
if Info^.MinLeftCalculating then
|
|
raise Exception.Create('anchor circle (left)');
|
|
Info^.MinLeftCalculating:=true;
|
|
|
|
Info^.MinLeft:=0;
|
|
if (akLeft in AControl.Anchors) then
|
|
Improve(AControl.AnchorSide[akLeft].Control);
|
|
if AControl.Parent<>nil then begin
|
|
for i:=0 to AControl.Parent.ControlCount-1 do begin
|
|
Sibling:=AControl.Parent.Controls[i];
|
|
if Sibling=AControl then continue;
|
|
if (akRight in Sibling.Anchors)
|
|
and (Sibling.AnchorSide[akRight].Control=AControl) then
|
|
Improve(Sibling);
|
|
end;
|
|
end;
|
|
|
|
Info^.MinLeftCalculating:=false;
|
|
Info^.MinLeftValid:=true;
|
|
//DebugLn(['CalculateMinimumLeft END ',DbgSName(AControl),' ',GetInfo(AControl)^.MinLeftValid]);
|
|
end;
|
|
Result:=Info^.MinLeft;
|
|
end;
|
|
|
|
function CalculateMinimumTop(AControl: TControl): integer;
|
|
var
|
|
Info: PControlInfo;
|
|
|
|
procedure Improve(Neighbour: TControl);
|
|
begin
|
|
if Neighbour=nil then exit;
|
|
if Neighbour.Parent<>AControl.Parent then exit;
|
|
Info^.MinTop:=Max(Info^.MinTop,
|
|
CalculateMinimumTop(Neighbour)+Neighbour.Height);
|
|
end;
|
|
|
|
var
|
|
i: Integer;
|
|
Sibling: TControl;
|
|
begin
|
|
Info:=GetInfo(AControl);
|
|
if not Info^.MinTopValid then begin
|
|
if Info^.MinTopCalculating then
|
|
raise Exception.Create('anchor circle (top)');
|
|
Info^.MinTopCalculating:=true;
|
|
|
|
Info^.MinTop:=0;
|
|
if (akTop in AControl.Anchors) then
|
|
Improve(AControl.AnchorSide[akTop].Control);
|
|
if AControl.Parent<>nil then begin
|
|
for i:=0 to AControl.Parent.ControlCount-1 do begin
|
|
Sibling:=AControl.Parent.Controls[i];
|
|
if Sibling=AControl then continue;
|
|
if (akBottom in Sibling.Anchors)
|
|
and (Sibling.AnchorSide[akBottom].Control=AControl) then
|
|
Improve(Sibling);
|
|
end;
|
|
end;
|
|
|
|
Info^.MinTopCalculating:=false;
|
|
Info^.MinTopValid:=true;
|
|
end;
|
|
Result:=Info^.MinTop;
|
|
end;
|
|
|
|
function CalculateClientSize(AControl: TControl): TPoint;
|
|
var
|
|
AWinControl: TWinControl;
|
|
i: Integer;
|
|
ChildControl: TControl;
|
|
begin
|
|
Result:=Point(0,0);
|
|
if AControl is TWinControl then begin
|
|
AWinControl:=TWinControl(AControl);
|
|
for i:=0 to AWinControl.ControlCount-1 do begin
|
|
ChildControl:=AWinControl.Controls[i];
|
|
Result.X:=Max(Result.X,CalculateMinimumLeft(ChildControl)
|
|
+ChildControl.Width);
|
|
Result.Y:=Max(Result.Y,CalculateMinimumTop(ChildControl)
|
|
+ChildControl.Height);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure ApplyBounds(ParentClientWidth, ParentClientHeight: Integer);
|
|
var
|
|
i: Integer;
|
|
Sibling: TControl;
|
|
Info: PControlInfo;
|
|
NewRect: TRect;
|
|
OldRect: TRect;
|
|
SideControl: TControl;
|
|
begin
|
|
for i:=0 to ResizedControl.Parent.ControlCount-1 do begin
|
|
Sibling:=ResizedControl.Parent.Controls[i];
|
|
Info:=GetInfo(Sibling);
|
|
NewRect.Left:=Info^.MinLeft;
|
|
NewRect.Right:=NewRect.Left+Sibling.Width;
|
|
SideControl:=Sibling.AnchorSide[akRight].Control;
|
|
if (akRight in Sibling.Anchors) and (SideControl<>nil) then begin
|
|
if SideControl=ResizedControl.Parent then
|
|
NewRect.Right:=ParentClientWidth
|
|
else if SideControl.Parent=ResizedControl.Parent then
|
|
NewRect.Right:=CalculateMinimumLeft(SideControl);
|
|
end;
|
|
NewRect.Top:=Info^.MinTop;
|
|
NewRect.Bottom:=NewRect.Top+Sibling.Height;
|
|
SideControl:=Sibling.AnchorSide[akBottom].Control;
|
|
if (akBottom in Sibling.Anchors) and (SideControl<>nil) then begin
|
|
if SideControl=ResizedControl.Parent then
|
|
NewRect.Bottom:=ParentClientHeight
|
|
else if SideControl.Parent=ResizedControl.Parent then
|
|
NewRect.Bottom:=CalculateMinimumTop(SideControl);
|
|
end;
|
|
OldRect:=Sibling.BoundsRect;
|
|
if not CompareRect(@OldRect,@NewRect) then begin
|
|
DebugLn(['ApplyBounds Sibling=',DbgSName(Sibling),' NewRect=',dbgs(NewRect)]);
|
|
Sibling.BoundsRect:=NewRect;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
var
|
|
ParentSize: TPoint;
|
|
CurParent: TWinControl;
|
|
DiffWidth: Integer;
|
|
DiffHeight: Integer;
|
|
AlignDisabledControl: TWinControl;
|
|
begin
|
|
DebugLn(['TCustomLazControlDocker.FixControlBounds ',DbgSName(ResizedControl)]);
|
|
CurParent:=ResizedControl.Parent;
|
|
if CurParent=nil then begin
|
|
DebugLn(['TCustomLazControlDocker.FixControlBounds WARNING: no parent']);
|
|
exit;
|
|
end;
|
|
CurParent.DisableAlign;
|
|
try
|
|
InitInfos;
|
|
// calculate minimum left, top, right, bottom of all siblings
|
|
ParentSize:=CalculateClientSize(CurParent);
|
|
DiffWidth:=ParentSize.X-CurParent.ClientWidth;
|
|
DiffHeight:=ParentSize.Y-CurParent.ClientHeight;
|
|
if (DiffWidth<>0) or (DiffHeight<>0) then begin
|
|
// parent needs resizing
|
|
DebugLn(['TCustomLazControlDocker.FixControlBounds Parent=',DbgSName(ResizedControl.Parent),' needs resizing to ',dbgs(ParentSize)]);
|
|
AlignDisabledControl:=CurParent.Parent;
|
|
if AlignDisabledControl<>nil then
|
|
AlignDisabledControl.DisableAlign;
|
|
try
|
|
CurParent.ClientWidth:=ParentSize.X;
|
|
CurParent.ClientHeight:=ParentSize.Y;
|
|
if CurParent.Parent<>nil then begin
|
|
// parent is a child
|
|
// => resize parent and fix the position recursively
|
|
FixControlBounds(Layout,CurParent);
|
|
end else begin
|
|
// parent is a free form
|
|
// => decide where to move the form on the screen using the Layout
|
|
|
|
// TODO
|
|
DebugLn(['TCustomLazControlDocker.FixControlBounds TODO move parent ',DbgSName(CurParent)]);
|
|
end;
|
|
finally
|
|
if AlignDisabledControl<>nil then
|
|
AlignDisabledControl.EnableAlign;
|
|
end;
|
|
end;
|
|
ApplyBounds(ParentSize.X,ParentSize.Y);
|
|
finally
|
|
FreeInfos;
|
|
CurParent.EnableAlign;
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.ShrinkNeighbourhood(
|
|
Layout: TLazDockConfigNode; AControl: TControl; Sides: TAnchors);
|
|
{ shrink neighbour controls according to Layout
|
|
A neighbour is the first control left or top of AControl, that can be shrinked
|
|
and is only anchored to AControl.
|
|
}
|
|
procedure ShrinkControl(CurControl: TControl; Side: TAnchorKind); forward;
|
|
|
|
procedure ShrinkNeighboursOnSide(CurControl: TControl; Side: TAnchorKind);
|
|
// shrink all controls, that are anchored on Side of CurControl
|
|
var
|
|
Neighbour: TControl;
|
|
i: Integer;
|
|
begin
|
|
DebugLn(['ShrinkNeighboursOnSide START ',DbgSName(CurControl),' ',AnchorNames[Side]]);
|
|
if Side in CurControl.Anchors then begin
|
|
Neighbour:=CurControl.AnchorSide[Side].Control;
|
|
DebugLn(['ShrinkNeighboursOnSide Neighbour=',DbgSName(Neighbour)]);
|
|
ShrinkControl(Neighbour,Side);
|
|
end;
|
|
for i:=0 to CurControl.Parent.ControlCount-1 do begin
|
|
Neighbour:=CurControl.Parent.Controls[i];
|
|
if (OppositeAnchor[Side] in Neighbour.Anchors)
|
|
and (Neighbour.AnchorSide[OppositeAnchor[Side]].Control=CurControl)
|
|
then
|
|
ShrinkControl(Neighbour,Side);
|
|
end;
|
|
end;
|
|
|
|
procedure ShrinkControl(CurControl: TControl; Side: TAnchorKind);
|
|
var
|
|
NodeName: String;
|
|
Node: TLazDockConfigNode;
|
|
CurBounds: TRect;
|
|
begin
|
|
DebugLn(['ShrinkControl START ',DbgSName(CurControl),' Side=',AnchorNames[Side]]);
|
|
if (CurControl=nil) or (CurControl=AControl)
|
|
or (CurControl.Parent<>AControl.Parent) then
|
|
exit;
|
|
if CurControl is TCustomSplitter then begin
|
|
// a splitter can not be shrinked
|
|
// => try to shrink the controls on the other side of the splitter
|
|
ShrinkNeighboursOnSide(CurControl,Side);
|
|
exit;
|
|
end;
|
|
// shrink according to Layout
|
|
NodeName:=Manager.GetControlConfigName(CurControl);
|
|
if NodeName='' then exit;
|
|
Node:=Layout.FindByName(NodeName,true);
|
|
if Node=nil then exit;
|
|
CurBounds:=Node.Bounds;
|
|
DebugLn(['ShrinkControl ',DbgSName(CurControl),' Side=',AnchorNames[Side],' LayoutBounds=',dbgs(CurBounds)]);
|
|
if Side in [akLeft,akRight] then
|
|
CurControl.Width:=Min(CurControl.Width,CurBounds.Right-CurBounds.Left)
|
|
else
|
|
CurControl.Height:=Min(CurControl.Height,CurBounds.Bottom-CurBounds.Top);
|
|
end;
|
|
|
|
var
|
|
a: TAnchorKind;
|
|
begin
|
|
DebugLn(['TCustomLazControlDocker.ShrinkNeighbourhood AControl=',DbgSName(AControl)]);
|
|
AControl.Parent.DisableAlign;
|
|
try
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do
|
|
if a in Sides then
|
|
ShrinkNeighboursOnSide(AControl,a);
|
|
finally
|
|
AControl.Parent.EnableAlign;
|
|
end;
|
|
end;
|
|
|
|
function TCustomLazControlDocker.FindPageNeighbours(Layout: TLazDockConfigNode;
|
|
StartControl: TControl; out AnchorControls: TAnchorControlsRect): TFPList;
|
|
{ Creates a list of TControl, containing StartControl and neighbours,
|
|
which are on the same page according to Layout and are a rectangular area.
|
|
AnchorControls are the four boundaries of the rectangular area and the list
|
|
contains all controls within these boundaries (and with the same Parent as
|
|
StartControl).
|
|
}
|
|
type
|
|
TPageCompatibility = (pcUnknown, pcNotOnSamePage, pcSamePage);
|
|
var
|
|
ControlList: TFPList;
|
|
PageNode: TLazDockConfigNode;
|
|
Parent: TWinControl;
|
|
Compatibility: array of TPageCompatibility;
|
|
|
|
procedure InitCompatibility;
|
|
var
|
|
i: Integer;
|
|
AControl: TControl;
|
|
NodeName: String;
|
|
Node: TLazDockConfigNode;
|
|
begin
|
|
// check all siblings if the Layout knows them
|
|
SetLength(Compatibility,Parent.ControlCount);
|
|
for i:=0 to Parent.ControlCount-1 do begin
|
|
Compatibility[i]:=pcUnknown;
|
|
AControl:=Parent.Controls[i];
|
|
if AControl is TLazDockSplitter then continue;
|
|
NodeName:=Manager.GetControlConfigName(AControl);
|
|
if NodeName='' then continue;
|
|
Node:=Layout.FindByName(NodeName,true);
|
|
if Node<>nil then begin
|
|
if Node.Parent=PageNode then
|
|
Compatibility[i]:=pcSamePage
|
|
else
|
|
Compatibility[i]:=pcNotOnSamePage;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function CheckSolution(Candidates: TFPList): boolean;
|
|
var
|
|
ARect: TAnchorControlsRect;
|
|
AllList: TFPList;
|
|
i: Integer;
|
|
Index: LongInt;
|
|
begin
|
|
Result:=false;
|
|
// find the minimum rectangle around the current selection
|
|
if not GetEnclosingControlRect(Candidates,ARect) then exit;
|
|
// get the controls in the rectangle
|
|
AllList:=GetEnclosedControls(ARect);
|
|
try
|
|
for i:=0 to AllList.Count-1 do begin
|
|
Index:=Parent.GetControlIndex(TControl(AllList[i]));
|
|
if Index<0 then exit(false);
|
|
if Compatibility[Index]=pcNotOnSamePage then exit(false);
|
|
end;
|
|
// AllList fits => use it as solution
|
|
ControlList.Assign(AllList);
|
|
AnchorControls:=ARect;
|
|
Result:=true;
|
|
finally
|
|
AllList.Free;
|
|
end;
|
|
end;
|
|
|
|
function TryLayoutSolution: boolean;
|
|
// check if a 1:1 of the layout is possible
|
|
var
|
|
i: Integer;
|
|
begin
|
|
ControlList.Clear;
|
|
for i:=0 to Parent.ControlCount-1 do begin
|
|
if Compatibility[i]=pcSamePage then
|
|
ControlList.Add(Parent.Controls[i]);
|
|
end;
|
|
Result:=CheckSolution(ControlList);
|
|
end;
|
|
|
|
procedure TrySubsets;
|
|
// add controls to the selection
|
|
var
|
|
List: TFPList;
|
|
i: Integer;
|
|
begin
|
|
List:=TFPList.Create;
|
|
List.Add(StartControl);
|
|
CheckSolution(List);
|
|
i:=0;
|
|
repeat
|
|
// add on more control to the selection
|
|
if Compatibility[i]=pcSamePage then begin
|
|
List.Add(Parent.Controls[i]);
|
|
if not CheckSolution(List) then
|
|
List.Remove(Parent.Controls[i]);
|
|
end;
|
|
inc(i);
|
|
until false;
|
|
List.Free;
|
|
end;
|
|
|
|
var
|
|
StartNodeName: String;
|
|
StartNode: TLazDockConfigNode;
|
|
a: TAnchorKind;
|
|
begin
|
|
// set defaults
|
|
ControlList:=TFPList.Create;
|
|
ControlList.Add(StartControl);
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do
|
|
AnchorControls[a]:=StartControl.AnchorSide[a].Control;
|
|
|
|
// check input
|
|
StartNodeName:=Manager.GetControlConfigName(StartControl);
|
|
if StartNodeName='' then exit;
|
|
StartNode:=Layout.FindByName(StartNodeName,true);
|
|
if StartNode=nil then exit;
|
|
PageNode:=StartNode.Parent;
|
|
if PageNode=nil then exit;
|
|
|
|
// init
|
|
Parent:=StartControl.Parent;
|
|
InitCompatibility;
|
|
|
|
// try some possibilities
|
|
if (not TryLayoutSolution) then
|
|
TrySubsets;
|
|
|
|
Result:=ControlList;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.Notification(AComponent: TComponent;
|
|
Operation: TOperation);
|
|
var
|
|
Item: TLCDMenuItem;
|
|
begin
|
|
inherited Notification(AComponent, Operation);
|
|
if Operation=opRemove then
|
|
begin
|
|
Item := nil;
|
|
if AComponent=FControl then
|
|
begin
|
|
if FControl.PopupMenu <> nil then
|
|
Item := FindLCDMenuItem(FControl.PopupMenu);
|
|
FControl.RemoveAllHandlersOfObject(Self);
|
|
FControl:=nil;
|
|
end;
|
|
|
|
if (AComponent is TMenu) then
|
|
Item := FindLCDMenuItem(TMenu(AComponent));
|
|
|
|
if (AComponent is TMenuItem) then
|
|
Item := FindLCDMenuItem(TMenu(AComponent));
|
|
|
|
if Item <> nil then
|
|
begin
|
|
FMenus.Remove(Item);
|
|
Item.Menu := nil;
|
|
if Item.Item <> AComponent then
|
|
FreeAndNil(Item.Item);
|
|
Item.Free;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TCustomLazControlDocker.FindLCDMenuItem(AMenu: TMenu): TLCDMenuItem;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if (FMenus<>nil) and (AMenu<>nil) then
|
|
for i:=0 to FMenus.Count-1 do begin
|
|
Result:=TLCDMenuItem(FMenus[i]);
|
|
if Result.Menu=AMenu then exit;
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
|
|
function TCustomLazControlDocker.FindLCDMenuItem(AMenuItem: TMenuItem
|
|
): TLCDMenuItem;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if (FMenus<>nil) and (AMenuItem<>nil) then
|
|
for i:=0 to FMenus.Count-1 do begin
|
|
Result:=TLCDMenuItem(FMenus[i]);
|
|
if Result.Item=AMenuItem then exit;
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
|
|
function TCustomLazControlDocker.GetControlName(AControl: TControl): string;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
Result:=Manager.GetControlConfigName(AControl);
|
|
if Result='' then begin
|
|
if AControl=Control.Parent then
|
|
Result:=NonDockConfigNamePrefixes[ndcnParent]
|
|
else if AControl.Name<>'' then
|
|
Result:=NonDockConfigNamePrefixes[ndcnControlName]+AControl.Name
|
|
else if AControl.Parent<>nil then begin
|
|
i:=AControl.Parent.ControlCount-1;
|
|
while (i>=0) and (AControl.Parent.Controls[i]<>AControl) do dec(i);
|
|
Result:=NonDockConfigNamePrefixes[ndcnChildIndex]+IntToStr(i)+' '
|
|
+AControl.ClassName;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.AddPopupMenu(Menu: TPopupMenu);
|
|
var
|
|
LCDItem: TLCDMenuItem;
|
|
begin
|
|
if FindLCDMenuItem(Menu)<>nil then exit;
|
|
if FMenus=nil then FMenus:=TFPList.Create;
|
|
LCDItem:=TLCDMenuItem.Create;
|
|
LCDItem.Menu:=Menu;
|
|
FMenus.Add(LCDItem);
|
|
Menu.FreeNotification(Self);
|
|
LCDItem.Item:=TMenuItem.Create(Self);
|
|
LCDItem.Item.Caption:=rsDocking;
|
|
LCDItem.Item.OnClick:=@PopupMenuItemClick;
|
|
Menu.Items.Add(LCDItem.Item);
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.RemovePopupMenu(Menu: TPopupMenu);
|
|
var
|
|
Item: TLCDMenuItem;
|
|
begin
|
|
Item:=FindLCDMenuItem(Menu);
|
|
if Item=nil then exit;
|
|
FMenus.Remove(Item);
|
|
FreeAndNil(Item.Item);
|
|
Item.Menu:=nil;
|
|
Item.Free;
|
|
end;
|
|
|
|
function TCustomLazControlDocker.GetLayoutFromControl: TLazDockConfigNode;
|
|
|
|
procedure CopyChildsLayout(ParentNode: TLazDockConfigNode;
|
|
ParentNodeControl: TWinControl);
|
|
// saves for each child node the names of the anchor side controls
|
|
var
|
|
i: Integer;
|
|
ChildNode: TLazDockConfigNode;
|
|
ChildControl: TControl;
|
|
a: TAnchorKind;
|
|
ChildNames: TStringHashList;// name to control mapping
|
|
ChildName: String;
|
|
CurAnchorControl: TControl;
|
|
CurAnchorCtrlName: String;
|
|
CurAnchorNode: TLazDockConfigNode;
|
|
begin
|
|
ChildNames:=TStringHashList.Create(false);
|
|
try
|
|
// build mapping of name to control
|
|
ChildNames.Data[ParentNode.Name]:=ParentNodeControl;
|
|
for i:=0 to ParentNodeControl.ControlCount-1 do begin
|
|
ChildControl:=ParentNodeControl.Controls[i];
|
|
ChildName:=GetControlName(ChildControl);
|
|
if ChildName<>'' then
|
|
ChildNames.Data[ChildName]:=ChildControl;
|
|
end;
|
|
// build mapping control to node
|
|
|
|
// set 'Sides'
|
|
for i:=0 to ParentNode.ChildCount-1 do begin
|
|
ChildNode:=ParentNode[i];
|
|
ChildControl:=TControl(ChildNames.Data[ChildNode.Name]);
|
|
if ChildControl=nil then continue;
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
CurAnchorControl:=ChildControl.AnchorSide[a].Control;
|
|
if CurAnchorControl=nil then continue;
|
|
if CurAnchorControl=ParentNodeControl then
|
|
CurAnchorNode:=ParentNode
|
|
else begin
|
|
CurAnchorCtrlName:=GetControlName(CurAnchorControl);
|
|
CurAnchorNode:=ParentNode.FindByName(CurAnchorCtrlName);
|
|
if CurAnchorNode=nil then
|
|
RaiseGDBException('inconsistency');
|
|
end;
|
|
//DebugLn('CopyChildsLayout ',DbgSName(CurAnchorControl),' CurAnchorCtrlName="',CurAnchorCtrlName,'"');
|
|
ChildNode.Sides[a]:=CurAnchorNode.Name;
|
|
end;
|
|
end;
|
|
finally
|
|
ChildNames.Free;
|
|
end;
|
|
end;
|
|
|
|
function AddNode(ParentNode: TLazDockConfigNode;
|
|
AControl: TControl): TLazDockConfigNode;
|
|
var
|
|
i: Integer;
|
|
CurChildControl: TControl;
|
|
NeedChildNodes: boolean;
|
|
begin
|
|
Result:=TLazDockConfigNode.Create(ParentNode,GetControlName(AControl));
|
|
|
|
// The Type
|
|
if AControl is TLazDockSplitter then begin
|
|
if TLazDockSplitter(AControl).ResizeAnchor in [akLeft,akRight] then
|
|
Result.FTheType:=ldcntSplitterLeftRight
|
|
else
|
|
Result.FTheType:=ldcntSplitterUpDown;
|
|
end else if AControl is TLazDockForm then
|
|
Result.FTheType:=ldcntForm
|
|
else if AControl is TLazDockPages then
|
|
Result.FTheType:=ldcntPages
|
|
else if AControl is TLazDockPage then
|
|
Result.FTheType:=ldcntPage
|
|
else
|
|
Result.FTheType:=ldcntControl;
|
|
|
|
// Bounds
|
|
Result.FBounds:=AControl.BoundsRect;
|
|
if AControl is TWinControl then
|
|
Result.FClientBounds:=TWinControl(AControl).GetChildsRect(false)
|
|
else
|
|
Result.FClientBounds:=Rect(0,0,Result.FBounds.Right-Result.FBounds.Left,
|
|
Result.FBounds.Bottom-Result.FBounds.Top);
|
|
|
|
// windowstate
|
|
if AControl is TCustomForm then
|
|
Result.WindowState:=TCustomForm(AControl).WindowState;
|
|
|
|
// Childs
|
|
if (AControl is TWinControl) then begin
|
|
// check if childs need nodes
|
|
NeedChildNodes:=(AControl is TLazDockPages)
|
|
or (AControl is TLazDockPage);
|
|
if not NeedChildNodes then begin
|
|
for i:=0 to TWinControl(AControl).ControlCount-1 do begin
|
|
CurChildControl:=TWinControl(AControl).Controls[i];
|
|
if Manager.FindDockerByControl(CurChildControl,nil)<>nil then begin
|
|
NeedChildNodes:=true;
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
// add child nodes
|
|
if NeedChildNodes then begin
|
|
for i:=0 to TWinControl(AControl).ControlCount-1 do begin
|
|
CurChildControl:=TWinControl(AControl).Controls[i];
|
|
AddNode(Result,CurChildControl);
|
|
end;
|
|
for i:=0 to Result.ChildCount-1 do begin
|
|
end;
|
|
end;
|
|
CopyChildsLayout(Result,TWinControl(AControl));
|
|
end;
|
|
end;
|
|
|
|
var
|
|
RootControl: TControl;
|
|
begin
|
|
if (Control=nil) or (Manager=nil) then exit(nil);
|
|
|
|
RootControl:=Control;
|
|
while RootControl.Parent<>nil do
|
|
RootControl:=RootControl.Parent;
|
|
Result:=AddNode(nil,RootControl);
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.SaveLayout;
|
|
var
|
|
Layout: TLazDockConfigNode;
|
|
begin
|
|
if Manager=nil then exit;
|
|
Layout:=GetLayoutFromControl;
|
|
if (Layout=nil) then exit;
|
|
Manager.AddOrReplaceConfig(DockerName,Layout);
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.RestoreLayout;
|
|
{ Goals of this algorithm:
|
|
- If a form is hidden and immediately shown again, the layout should be
|
|
restored 1:1.
|
|
That's why a TCustomLazControlDocker stores the complete layout on every
|
|
hide. And restores it on every show.
|
|
- If an application is closed and all dock forms are closed (in any order)
|
|
the layout should be restored on startup, when the forms
|
|
are created (in any order).
|
|
This is done by saving the layout before all forms are closed.
|
|
|
|
|
|
Example 1: Docking to a side.
|
|
|
|
Current:
|
|
+---+
|
|
| A |
|
|
+---+
|
|
|
|
Formerly:
|
|
+------------+
|
|
|+---+|+----+|
|
|
|| A |||Self||
|
|
|+---+|+----+|
|
|
+------------+
|
|
|
|
Then put A into a new TLazDockForm, add a splitter and Self.
|
|
|
|
|
|
Example 2: Docking in between
|
|
|
|
Current:
|
|
+-----------+
|
|
|+---+|+---+|
|
|
|| A ||| C ||
|
|
|+---+|+---+|
|
|
+-----------+
|
|
|
|
Formerly:
|
|
+------------------+
|
|
|+---+|+----+|+---+|
|
|
|| A |||Self||| C ||
|
|
|+---+|+----+|+---+|
|
|
+------------------+
|
|
|
|
Then enlarge the parent of A and C, add a splitter and Self.
|
|
|
|
Example:
|
|
|
|
Formerly:
|
|
+-------------------------+
|
|
|+-----------------------+|
|
|
|| A ||
|
|
|+-----------------------+|
|
|
|=========================|
|
|
|+---+#+-----------+#+---+|
|
|
|| D |#| |#| ||
|
|
|+---+#| |#| ||
|
|
|=====#| B |#| E ||
|
|
|+---+#| |#| ||
|
|
|| |#| |#| ||
|
|
|| |#+-----------+#+---+|
|
|
|| F |#===================|
|
|
|| |#+-----------------+|
|
|
|| |#| C ||
|
|
|+---+#+-----------------+|
|
|
+-------------------------+
|
|
|
|
|
|
1. Showing A:
|
|
There is no other form yet, so just show it at the old position.
|
|
+-----------------------+
|
|
| A |
|
|
+-----------------------+
|
|
|
|
|
|
2. Showing B:
|
|
B is the bottom sibling of A. Put A into a new TLazDockForm, add a splitter,
|
|
enlarge B horizontally.
|
|
|
|
+-------------------------+
|
|
|+-----------------------+|
|
|
|| A ||
|
|
|+-----------------------+|
|
|
|=========================|
|
|
|+-----------------------+|
|
|
|| ||
|
|
|| ||
|
|
|| B ||
|
|
|| ||
|
|
|| ||
|
|
|+-----------------------+|
|
|
+-------------------------+
|
|
|
|
|
|
3. Showing C:
|
|
C is the bottom sibling of B. Enlarge the parent vertically, add a splitter
|
|
and enlarge C horizontally.
|
|
|
|
+-------------------------+
|
|
|+-----------------------+|
|
|
|| A ||
|
|
|+-----------------------+|
|
|
|=========================|
|
|
|+-----------------------+|
|
|
|| ||
|
|
|| ||
|
|
|| B ||
|
|
|| ||
|
|
|| ||
|
|
|+-----------------------+|
|
|
|=========================|
|
|
|+-----------------------+|
|
|
|| C ||
|
|
|+-----------------------+|
|
|
+-------------------------+
|
|
|
|
|
|
4. Showing D:
|
|
D is below of A, and left of B and C. Shrink B and C, add a splitter.
|
|
|
|
+-------------------------+
|
|
|+-----------------------+|
|
|
|| A ||
|
|
|+-----------------------+|
|
|
|=========================|
|
|
|+---+#+-----------------+|
|
|
|| |#| ||
|
|
|| |#| ||
|
|
|| |#| B ||
|
|
|| |#| ||
|
|
|| D |#| ||
|
|
|| |#+-----------------+|
|
|
|| |#===================|
|
|
|| |#+-----------------+|
|
|
|| |#| C ||
|
|
|+---+#+-----------------+|
|
|
+-------------------------+
|
|
|
|
|
|
5. Showing E:
|
|
Shrink B, add a splitter.
|
|
|
|
+-------------------------+
|
|
|+-----------------------+|
|
|
|| A ||
|
|
|+-----------------------+|
|
|
|=========================|
|
|
|+---+#+-----------+#+---+|
|
|
|| |#| |#| ||
|
|
|| |#| |#| ||
|
|
|| |#| B |#| E ||
|
|
|| |#| |#| ||
|
|
|| D |#| |#| ||
|
|
|| |#+-----------+#+---+|
|
|
|| |#===================|
|
|
|| |#+-----------------+|
|
|
|| |#| C ||
|
|
|+---+#+-----------------+|
|
|
+-------------------------+
|
|
|
|
|
|
6. Showing F:
|
|
Shrink D and add a splitter.
|
|
|
|
+-------------------------+
|
|
|+-----------------------+|
|
|
|| A ||
|
|
|+-----------------------+|
|
|
|=========================|
|
|
|+---+#+-----------+#+---+|
|
|
|| D |#| |#| ||
|
|
|+---+#| |#| ||
|
|
|=====#| B |#| E ||
|
|
|+---+#| |#| ||
|
|
|| |#| |#| ||
|
|
|| |#+-----------+#+---+|
|
|
|| F |#===================|
|
|
|| |#+-----------------+|
|
|
|| |#| C ||
|
|
|+---+#+-----------------+|
|
|
+-------------------------+
|
|
}
|
|
var
|
|
Layout: TLazDockConfigNode;
|
|
SelfNode: TLazDockConfigNode;
|
|
|
|
function FindNode(const ANodeName: string): TLazDockConfigNode;
|
|
begin
|
|
if ANodeName='' then
|
|
Result:=nil
|
|
else
|
|
Result:=Layout.FindByName(ANodeName,true,true);
|
|
end;
|
|
|
|
function FindControl(const ADockerName: string): TControl;
|
|
begin
|
|
Result:=Manager.FindControlByDockerName(ADockerName);
|
|
end;
|
|
|
|
function DockWithSpiralSplitter: boolean;
|
|
begin
|
|
// TODO
|
|
Result:=false;
|
|
end;
|
|
|
|
function SplitterDocking: boolean;
|
|
var
|
|
a: TAnchorKind;
|
|
SplitterCount: Integer;
|
|
SideNode: TLazDockConfigNode;
|
|
begin
|
|
Result:=false;
|
|
SplitterCount:=0;
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
SideNode:=FindNode(SelfNode.Sides[a]);
|
|
if (SideNode<>nil)
|
|
and (SideNode.TheType in [ldcntSplitterLeftRight,ldcntSplitterUpDown])
|
|
then begin
|
|
if SideNode.IsTheOnlyNeighbour(SelfNode,a)
|
|
and CreateFormAndDockWithSplitter(Layout,a) then
|
|
exit(true);
|
|
inc(SplitterCount);
|
|
if (SplitterCount=4) and DockWithSpiralSplitter then
|
|
exit(true);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function PageDocking: boolean;
|
|
begin
|
|
Result:=false;
|
|
if (SelfNode.TheType<>ldcntPage) then exit;
|
|
if (SelfNode.Parent.ChildCount<>1) then exit;
|
|
Result:=DockAsPage(Layout);
|
|
end;
|
|
|
|
var
|
|
NewBounds: TRect;
|
|
begin
|
|
{$IFDEF VerboseAnchorDocking}
|
|
DebugLn(['TCustomLazControlDocker.RestoreLayout A ',DockerName,' Control=',DbgSName(Control)]);
|
|
{$ENDIF}
|
|
if (Manager=nil) or (Control=nil) then exit;
|
|
Layout:=nil;
|
|
try
|
|
Layout:=Manager.CreateLayout(DockerName,Control,false);
|
|
if (Layout=nil) then exit;
|
|
SelfNode:=Layout.FindByName(DockerName,true);
|
|
DebugLn(['TCustomLazControlDocker.RestoreLayout ',SelfNode<>nil,' DockerName=',DockerName]);
|
|
if (SelfNode=nil) or (SelfNode.TheType<>ldcntControl) then exit;
|
|
|
|
if SelfNode.Parent<>nil then begin
|
|
// this control was docked
|
|
if SplitterDocking then exit;
|
|
if PageDocking then exit;
|
|
end;
|
|
|
|
// default: do not dock, just move
|
|
DebugLn(['TCustomLazControlDocker.RestoreLayout ',DockerName,' not docking, just moving ...']);
|
|
NewBounds:=SelfNode.GetScreenBounds;
|
|
Control.SetBoundsKeepBase(NewBounds.Left,NewBounds.Top,
|
|
NewBounds.Right-NewBounds.Left,
|
|
NewBounds.Bottom-NewBounds.Top);
|
|
DebugLn(['TCustomLazControlDocker.RestoreLayout ',WindowStateToStr(Layout.WindowState),' Layout.Name=',Layout.Name]);
|
|
if (Control is TCustomForm) and (Control.Parent=nil) then
|
|
TCustomForm(Control).WindowState:=Layout.WindowState;
|
|
finally
|
|
DebugLn(['TCustomLazControlDocker.RestoreLayout END Control=',DbgSName(Control),' Control.BoundsRect=',dbgs(Control.BoundsRect)]);
|
|
Layout.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.DisableLayout;
|
|
begin
|
|
inc(fLayoutLock);
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.EnableLayout;
|
|
begin
|
|
dec(fLayoutLock);
|
|
end;
|
|
|
|
function TCustomLazControlDocker.ControlIsDocked: boolean;
|
|
begin
|
|
Result:=(Control<>nil)
|
|
and (Control.Parent<>nil)
|
|
and ((Control.Parent is TLazDockForm) or (Control.Parent is TLazDockPage));
|
|
end;
|
|
|
|
constructor TCustomLazControlDocker.Create(TheOwner: TComponent);
|
|
begin
|
|
inherited Create(TheOwner);
|
|
if (not (csLoading in ComponentState))
|
|
and (TheOwner is TControl) then
|
|
// use as default
|
|
Control:=TControl(TheOwner);
|
|
ExtendPopupMenu:=true;
|
|
end;
|
|
|
|
destructor TCustomLazControlDocker.Destroy;
|
|
var
|
|
i: integer;
|
|
Item: TLCDMenuItem;
|
|
OldMenus: TFPList;
|
|
begin
|
|
Control:=nil;
|
|
Manager:=nil;
|
|
inherited Destroy;
|
|
if FMenus <> nil then begin
|
|
OldMenus:=FMenus;
|
|
FMenus:=nil;
|
|
for i := OldMenus.Count - 1 downto 0 do
|
|
begin
|
|
Item:=TLCDMenuItem(OldMenus[i]);
|
|
FreeAndNil(Item.Item);
|
|
Item.Free;
|
|
end;
|
|
OldMenus.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.PopupMenuItemClick(Sender: TObject);
|
|
begin
|
|
ShowDockingEditor;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.SetControl(const AValue: TControl);
|
|
var
|
|
WinControl: TWinControl;
|
|
begin
|
|
if FControl=AValue then exit;
|
|
if FControl<>nil then begin
|
|
FControl.RemoveAllHandlersOfObject(Self);
|
|
FControl.RemoveFreeNotification(Self);
|
|
if (Manager<>nil) and (FControl is TWinControl) then
|
|
begin
|
|
WinControl:=TWinControl(FControl);
|
|
WinControl.UseDockManager:=false;
|
|
WinControl.DockManager:=nil;
|
|
end;
|
|
end;
|
|
FControl:=AValue;
|
|
if Control<>nil then begin
|
|
Control.AddHandlerOnVisibleChanging(@ControlVisibleChanging);
|
|
Control.AddHandlerOnVisibleChanged(@ControlVisibleChanged);
|
|
Control.FreeNotification(Self);
|
|
if (Manager<>nil) and (FControl is TWinControl) then
|
|
begin
|
|
WinControl:=TWinControl(FControl);
|
|
WinControl.DockManager:=Manager.Manager;
|
|
WinControl.UseDockManager:=true;
|
|
end;
|
|
end;
|
|
if (DockerName='') and (FControl<>nil) then
|
|
DockerName:=FControl.Name;
|
|
UpdatePopupMenu;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.SetDockerName(const AValue: string);
|
|
var
|
|
NewDockerName: String;
|
|
begin
|
|
if FDockerName=AValue then exit;
|
|
NewDockerName:=AValue;
|
|
if Manager<>nil then
|
|
NewDockerName:=Manager.CreateUniqueName(NewDockerName,Self);
|
|
FDockerName:=NewDockerName;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.SetExtendPopupMenu(const AValue: boolean);
|
|
begin
|
|
if FExtendPopupMenu=AValue then exit;
|
|
FExtendPopupMenu:=AValue;
|
|
UpdatePopupMenu;
|
|
end;
|
|
|
|
procedure TCustomLazControlDocker.SetLocalizedName(const AValue: string);
|
|
begin
|
|
if FLocalizedName=AValue then exit;
|
|
FLocalizedName:=AValue;
|
|
end;
|
|
|
|
{ TCustomLazDockingManager }
|
|
|
|
procedure TCustomLazDockingManager.Remove(Docker: TCustomLazControlDocker);
|
|
var
|
|
WinControl: TWinControl;
|
|
begin
|
|
if Docker.Control is TWinControl then
|
|
begin
|
|
WinControl:=TWinControl(Docker.Control);
|
|
WinControl.UseDockManager:=false;
|
|
WinControl.DockManager:=nil;
|
|
end;
|
|
FDockers.Remove(Docker);
|
|
end;
|
|
|
|
function TCustomLazDockingManager.Add(Docker: TCustomLazControlDocker): Integer;
|
|
var
|
|
WinControl: TWinControl;
|
|
begin
|
|
Docker.DockerName:=CreateUniqueName(Docker.DockerName,nil);
|
|
Result:=FDockers.Add(Docker);
|
|
if Docker.Control is TWinControl then
|
|
begin
|
|
WinControl:=TWinControl(Docker.Control);
|
|
WinControl.DockManager:=Manager;
|
|
WinControl.UseDockManager:=true;
|
|
end;
|
|
end;
|
|
|
|
function TCustomLazDockingManager.GetDockers(Index: Integer
|
|
): TCustomLazControlDocker;
|
|
begin
|
|
Result:=TCustomLazControlDocker(FDockers[Index]);
|
|
end;
|
|
|
|
function TCustomLazDockingManager.GetDockerCount: Integer;
|
|
begin
|
|
Result:=FDockers.Count;
|
|
end;
|
|
|
|
function TCustomLazDockingManager.GetConfigCount: Integer;
|
|
begin
|
|
if FConfigs<>nil then
|
|
Result:=FConfigs.Count
|
|
else
|
|
Result:=0;
|
|
end;
|
|
|
|
function TCustomLazDockingManager.GetConfigs(Index: Integer
|
|
): TLazDockerConfig;
|
|
begin
|
|
Result:=TLazDockerConfig(FConfigs[Index]);
|
|
end;
|
|
|
|
constructor TCustomLazDockingManager.Create(TheOwner: TComponent);
|
|
begin
|
|
inherited Create(TheOwner);
|
|
FDockers:=TFPList.Create;
|
|
FManager:=TAnchoredDockManager.Create(nil);
|
|
FManager.FConfigs:=Self;
|
|
end;
|
|
|
|
destructor TCustomLazDockingManager.Destroy;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
for i:=FDockers.Count-1 downto 0 do
|
|
Dockers[i].Manager:=nil;
|
|
FreeAndNil(FDockers);
|
|
FreeAndNil(FManager);
|
|
ClearConfigs;
|
|
FreeAndNil(FConfigs);
|
|
inherited Destroy;
|
|
end;
|
|
|
|
function TCustomLazDockingManager.FindDockerByName(const ADockerName: string;
|
|
Ignore: TCustomLazControlDocker): TCustomLazControlDocker;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
i:=DockerCount-1;
|
|
while (i>=0) do begin
|
|
Result:=Dockers[i];
|
|
if (CompareText(Result.DockerName,ADockerName)=0) and (Ignore<>Result) then
|
|
exit;
|
|
dec(i);
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
|
|
function TCustomLazDockingManager.FindControlByDockerName(
|
|
const ADockerName: string; Ignore: TCustomLazControlDocker): TControl;
|
|
var
|
|
Docker: TCustomLazControlDocker;
|
|
begin
|
|
Docker:=FindDockerByName(ADockerName);
|
|
if Docker=nil then
|
|
Result:=nil
|
|
else
|
|
Result:=Docker.Control;
|
|
end;
|
|
|
|
function TCustomLazDockingManager.FindDockerByControl(AControl: TControl;
|
|
Ignore: TCustomLazControlDocker): TCustomLazControlDocker;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
i:=DockerCount-1;
|
|
while (i>=0) do begin
|
|
Result:=Dockers[i];
|
|
if (Result.Control=AControl) and (Ignore<>Result) then
|
|
exit;
|
|
dec(i);
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
|
|
function TCustomLazDockingManager.CreateUniqueName(const AName: string;
|
|
Ignore: TCustomLazControlDocker): string;
|
|
begin
|
|
Result:=AName;
|
|
if FindDockerByName(Result,Ignore)=nil then exit;
|
|
Result:=CreateFirstIdentifier(Result);
|
|
while FindDockerByName(Result,Ignore)<>nil do
|
|
Result:=CreateNextIdentifier(Result);
|
|
end;
|
|
|
|
function TCustomLazDockingManager.GetControlConfigName(AControl: TControl
|
|
): string;
|
|
var
|
|
Docker: TCustomLazControlDocker;
|
|
begin
|
|
Docker:=FindDockerByControl(AControl,nil);
|
|
if Docker<>nil then
|
|
Result:=Docker.DockerName
|
|
else
|
|
Result:='';
|
|
end;
|
|
|
|
procedure TCustomLazDockingManager.DisableLayout(Control: TControl);
|
|
var
|
|
Docker: TCustomLazControlDocker;
|
|
begin
|
|
Docker:=FindDockerByControl(Control);
|
|
if Docker<>nil then
|
|
Docker.DisableLayout;
|
|
end;
|
|
|
|
procedure TCustomLazDockingManager.EnableLayout(Control: TControl);
|
|
var
|
|
Docker: TCustomLazControlDocker;
|
|
begin
|
|
Docker:=FindDockerByControl(Control);
|
|
if Docker<>nil then
|
|
Docker.EnableLayout;
|
|
end;
|
|
|
|
procedure TCustomLazDockingManager.SaveToConfig(Config: TConfigStorage;
|
|
const Path: string);
|
|
var
|
|
i: Integer;
|
|
ADocker: TCustomLazControlDocker;
|
|
CurDockConfig: TLazDockerConfig;
|
|
SubPath: String;
|
|
begin
|
|
// collect configs
|
|
for i:=0 to DockerCount-1 do begin
|
|
ADocker:=Dockers[i];
|
|
if ((ADocker.Control<>nil) and ADocker.Control.Visible) then begin
|
|
ADocker.SaveLayout;
|
|
end;
|
|
end;
|
|
|
|
// save configs
|
|
Config.SetDeleteValue(Path+'Configs/Count',ConfigCount,0);
|
|
for i:=0 to ConfigCount-1 do begin
|
|
SubPath:=Path+'Config'+IntToStr(i)+'/';
|
|
CurDockConfig:=Configs[i];
|
|
Config.SetDeleteValue(SubPath+'DockerName/Value',CurDockConfig.DockerName,'');
|
|
CurDockConfig.Root.SaveToConfig(Config,SubPath);
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomLazDockingManager.LoadFromConfig(Config: TConfigStorage;
|
|
const Path: string);
|
|
var
|
|
i: Integer;
|
|
NewConfigCount: LongInt;
|
|
SubPath: String;
|
|
NewRoot: TLazDockConfigNode;
|
|
NewDockerName: String;
|
|
NewRootName: String;
|
|
begin
|
|
// merge the configs
|
|
NewConfigCount:=Config.GetValue(Path+'Configs/Count',0);
|
|
//DebugLn(['TCustomLazDockingManager.LoadFromConfig NewConfigCount=',NewConfigCount]);
|
|
for i:=0 to NewConfigCount-1 do begin
|
|
SubPath:=Path+'Config'+IntToStr(i)+'/';
|
|
NewDockerName:=Config.GetValue(SubPath+'DockerName/Value','');
|
|
if NewDockerName='' then continue;
|
|
NewRootName:=Config.GetValue(SubPath+'Name/Value','');
|
|
if NewRootName='' then continue;
|
|
//DebugLn(['TCustomLazDockingManager.LoadFromConfig NewDockerName=',NewDockerName,' NewRootName=',NewRootName]);
|
|
NewRoot:=TLazDockConfigNode.Create(nil,NewRootName);
|
|
NewRoot.LoadFromConfig(Config,SubPath);
|
|
AddOrReplaceConfig(NewDockerName,NewRoot);
|
|
//NewRoot.WriteDebugReport;
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomLazDockingManager.AddOrReplaceConfig(
|
|
const DockerName: string; Config: TLazDockConfigNode);
|
|
var
|
|
i: Integer;
|
|
CurConfig: TLazDockerConfig;
|
|
begin
|
|
if FConfigs=nil then
|
|
FConfigs:=TFPList.Create;
|
|
for i:=FConfigs.Count-1 downto 0 do begin
|
|
CurConfig:=Configs[i];
|
|
if CompareText(CurConfig.DockerName,DockerName)=0 then begin
|
|
CurConfig.FRoot.Free;
|
|
CurConfig.FRoot:=Config;
|
|
exit;
|
|
end;
|
|
end;
|
|
FConfigs.Add(TLazDockerConfig.Create(DockerName,Config));
|
|
end;
|
|
|
|
procedure TCustomLazDockingManager.WriteDebugReport;
|
|
var
|
|
i: Integer;
|
|
ADocker: TCustomLazControlDocker;
|
|
begin
|
|
DebugLn('TCustomLazDockingManager.WriteDebugReport DockerCount=',dbgs(DockerCount));
|
|
for i:=0 to DockerCount-1 do begin
|
|
ADocker:=Dockers[i];
|
|
DebugLn(' ',dbgs(i),' Name="',ADocker.Name,'" DockerName="',ADocker.DockerName,'"');
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomLazDockingManager.ClearConfigs;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if FConfigs=nil then exit;
|
|
for i:=0 to FConfigs.Count-1 do TObject(FConfigs[i]).Free;
|
|
FConfigs.Clear;
|
|
end;
|
|
|
|
function TCustomLazDockingManager.GetConfigWithDockerName(
|
|
const DockerName: string): TLazDockerConfig;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
i:=ConfigCount-1;
|
|
while (i>=0) do begin
|
|
Result:=Configs[i];
|
|
if CompareText(Result.DockerName,DockerName)=0 then exit;
|
|
dec(i);
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
|
|
function TCustomLazDockingManager.CreateLayout(const DockerName: string;
|
|
VisibleControl: TControl; ExceptionOnError: boolean): TLazDockConfigNode;
|
|
// create a usable config
|
|
// This means: search a config, create a copy
|
|
// and remove all nodes without visible controls.
|
|
{$DEFINE VerboseAnchorDockCreateLayout}
|
|
var
|
|
Root: TLazDockConfigNode;
|
|
CurDockControl: TControl;
|
|
|
|
function ControlIsVisible(AControl: TControl): boolean;
|
|
begin
|
|
Result:=false;
|
|
if (AControl=nil) then exit;
|
|
if (not AControl.IsVisible) and (AControl<>VisibleControl) then exit;
|
|
if (CurDockControl<>nil) and (CurDockControl<>AControl.GetTopParent) then
|
|
exit;
|
|
Result:=true;
|
|
end;
|
|
|
|
function FindNode(const AName: string): TLazDockConfigNode;
|
|
begin
|
|
if AName='' then
|
|
Result:=nil
|
|
else
|
|
Result:=Root.FindByName(AName,true,true);
|
|
end;
|
|
|
|
procedure DeleteNode(var DeletingNode: TLazDockConfigNode);
|
|
|
|
function DeleteOwnSideSplitter(Side: TAnchorKind;
|
|
var SplitterNode: TLazDockConfigNode): boolean;
|
|
{ check if DeletingNode has a splitter to Side, and this node is the only
|
|
node anchored to the splitter at this side.
|
|
If yes, it removes the splitter and the DeletingNode and reconnects the
|
|
nodes using the splitter with the opposite side
|
|
For example:
|
|
---------+ --------+
|
|
--+#+---+| ---+|
|
|
B |#| || B ||
|
|
--+#| || ---+|
|
|
====| A || -> ====|
|
|
--+#| || ---+|
|
|
C |#| || C ||
|
|
--+#+---+| ---+|
|
|
---------+ --------+
|
|
}
|
|
var
|
|
i: Integer;
|
|
Sibling: TLazDockConfigNode;
|
|
OppositeSide: TAnchorKind;
|
|
begin
|
|
Result:=false;
|
|
// check if this is the only node using this Side of the splitter
|
|
if not SplitterNode.IsTheOnlyNeighbour(DeletingNode,Side) then
|
|
exit;
|
|
|
|
// All nodes, that uses the splitter from the other side will now be
|
|
// anchored to the other side of DeletingNode
|
|
OppositeSide:=OppositeAnchor[Side];
|
|
for i:=0 to DeletingNode.Parent.ChildCount-1 do begin
|
|
Sibling:=DeletingNode.Parent.Childs[i];
|
|
if CompareText(Sibling.Sides[OppositeSide],SplitterNode.Name)=0 then
|
|
Sibling.Sides[OppositeSide]:=DeletingNode.Sides[OppositeSide];
|
|
end;
|
|
|
|
// delete splitter
|
|
FreeAndNil(SplitterNode);
|
|
|
|
Result:=true;
|
|
end;
|
|
|
|
function UnbindSpiralNode: boolean;
|
|
{ DeletingNode has 4 splitters like a spiral.
|
|
In this case merge the two vertical splitters.
|
|
For example:
|
|
| |
|
|
-------| -----|
|
|
|+---+| |
|
|
|| A || -> |
|
|
|+---+| |
|
|
|-------- |------
|
|
| |
|
|
}
|
|
var
|
|
LeftSplitter: TLazDockConfigNode;
|
|
RightSplitter: TLazDockConfigNode;
|
|
i: Integer;
|
|
Sibling: TLazDockConfigNode;
|
|
begin
|
|
LeftSplitter:=FindNode(DeletingNode.Sides[akLeft]);
|
|
RightSplitter:=FindNode(DeletingNode.Sides[akRight]);
|
|
// remove LeftSplitter
|
|
|
|
// 1. enlarge RightSplitter
|
|
if CompareText(RightSplitter.Sides[akTop],DeletingNode.Sides[akTop])=0 then
|
|
RightSplitter.Sides[akTop]:=LeftSplitter.Sides[akTop];
|
|
if CompareText(RightSplitter.Sides[akBottom],DeletingNode.Sides[akBottom])=0 then
|
|
RightSplitter.Sides[akBottom]:=LeftSplitter.Sides[akBottom];
|
|
|
|
// 2. anchor all siblings using LeftSplitter to now use RightSplitter
|
|
for i:=0 to DeletingNode.Parent.ChildCount-1 do begin
|
|
Sibling:=DeletingNode.Parent.Childs[i];
|
|
if Sibling=DeletingNode then continue;
|
|
if CompareText(Sibling.Sides[akLeft],LeftSplitter.Name)=0 then
|
|
Sibling.Sides[akLeft]:=RightSplitter.Name;
|
|
if CompareText(Sibling.Sides[akRight],LeftSplitter.Name)=0 then
|
|
Sibling.Sides[akRight]:=RightSplitter.Name;
|
|
end;
|
|
|
|
// 3. delete LeftSplitter
|
|
FreeAndNil(LeftSplitter);
|
|
|
|
Result:=true;
|
|
end;
|
|
|
|
var
|
|
a: TAnchorKind;
|
|
SiblingNode: TLazDockConfigNode;
|
|
SplitterCount: Integer;// number of shared splitters
|
|
begin
|
|
DebugLn(['DeleteNode ',DeletingNode.Name]);
|
|
SplitterCount:=0;
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
SiblingNode:=FindNode(DeletingNode.Sides[a]);
|
|
if (SiblingNode<>nil)
|
|
and (SiblingNode.TheType in [ldcntSplitterLeftRight,ldcntSplitterUpDown])
|
|
then begin
|
|
// there is a splitter
|
|
if DeleteOwnSideSplitter(a,SiblingNode) then begin
|
|
// splitter deleted
|
|
break;
|
|
end else begin
|
|
inc(SplitterCount);// not own => shared
|
|
if SplitterCount=4 then begin
|
|
// this is a spiral splitter node -> merge two splitters
|
|
UnbindSpiralNode;
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
FreeAndNil(DeletingNode);
|
|
end;
|
|
|
|
procedure SimplifyOnePageNode(var PagesNode: TLazDockConfigNode);
|
|
{ PagesNode has only one page left.
|
|
Remove Page and Pages node and move the content to the parent
|
|
}
|
|
var
|
|
ParentNode: TLazDockConfigNode;
|
|
PageNode: TLazDockConfigNode;
|
|
i: Integer;
|
|
Child: TLazDockConfigNode;
|
|
ChildBounds: TRect;
|
|
PagesBounds: TRect;
|
|
OffsetX: Integer;
|
|
OffsetY: Integer;
|
|
a: TAnchorKind;
|
|
begin
|
|
DebugLn(['SimplifyOnePageNode ',dbgs(PagesNode)]);
|
|
ParentNode:=PagesNode.Parent;
|
|
if ParentNode=nil then RaiseGDBException('');
|
|
if (PagesNode.TheType<>ldcntPages) then RaiseGDBException('');
|
|
if PagesNode.ChildCount<>1 then RaiseGDBException('');
|
|
PageNode:=PagesNode.Childs[0];
|
|
PagesBounds:=PagesNode.Bounds;
|
|
OffsetX:=PagesBounds.Left;
|
|
OffsetY:=PagesBounds.Top;
|
|
for i:=0 to PageNode.ChildCount-1 do begin
|
|
Child:=PageNode.Childs[i];
|
|
// changes parent of child
|
|
Child.Parent:=ParentNode;
|
|
// move childs to place where PagesNode was
|
|
ChildBounds:=Child.Bounds;
|
|
OffsetRect(ChildBounds,OffsetX,OffsetY);
|
|
Child.Bounds:=ChildBounds;
|
|
// change anchors of child
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
if CompareText(Child.Sides[a],PageNode.Name)=0 then
|
|
Child.Sides[a]:=PagesNode.Sides[a];
|
|
end;
|
|
end;
|
|
FreeAndNil(PagesNode);
|
|
//debugln(Root.DebugLayoutAsString);
|
|
end;
|
|
|
|
procedure SimplifyOneChildForm(var FormNode: TLazDockConfigNode);
|
|
{ FormNode has only one child left.
|
|
Remove Form node and replace root with child
|
|
}
|
|
var
|
|
FormBounds: TRect;
|
|
OffsetX: LongInt;
|
|
OffsetY: LongInt;
|
|
Child: TLazDockConfigNode;
|
|
ChildBounds: TRect;
|
|
a: TAnchorKind;
|
|
OldFormNode: TLazDockConfigNode;
|
|
begin
|
|
//DebugLn(['SimplifyOneChildForm ',dbgs(FormNode)]);
|
|
if FormNode<>Root then RaiseGDBException('');
|
|
if FormNode.ChildCount<>1 then RaiseGDBException('');
|
|
FormBounds:=FormNode.Bounds;
|
|
OffsetX:=FormBounds.Left;
|
|
OffsetY:=FormBounds.Top;
|
|
Child:=FormNode.Childs[0];
|
|
// changes parent of child
|
|
Child.Parent:=FormNode.Parent;
|
|
Child.WindowState:=FormNode.WindowState;
|
|
// move child to place where FormNode was
|
|
ChildBounds:=Child.Bounds;
|
|
OffsetRect(ChildBounds,OffsetX,OffsetY);
|
|
Child.Bounds:=ChildBounds;
|
|
// change anchors of child
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
if CompareText(Child.Sides[a],FormNode.Name)=0 then
|
|
Child.Sides[a]:=FormNode.Sides[a];
|
|
end;
|
|
OldFormNode:=FormNode;
|
|
FormNode:=Child;
|
|
OldFormNode.Free;
|
|
//Root.WriteDebugReport;
|
|
end;
|
|
|
|
procedure RemoveEmptyNodes(var Node: TLazDockConfigNode);
|
|
// remove unneeded child nodes
|
|
// if no childs left and Node itself is unneeded, it s freed and set to nil
|
|
var
|
|
i: Integer;
|
|
Docker: TCustomLazControlDocker;
|
|
Child: TLazDockConfigNode;
|
|
begin
|
|
if Node=nil then exit;
|
|
{$IFDEF VerboseAnchorDockCreateLayout}
|
|
DebugLn(['RemoveEmptyNodes ',Node.Name,' Node.ChildCount=',Node.ChildCount]);
|
|
{$ENDIF}
|
|
|
|
// remove unneeded childs
|
|
i:=Node.ChildCount-1;
|
|
while i>=0 do begin
|
|
Child:=Node.Childs[i];
|
|
RemoveEmptyNodes(Child);// beware: this can delete more than one child
|
|
dec(i);
|
|
if i>=Node.ChildCount then i:=Node.ChildCount-1;
|
|
end;
|
|
|
|
case Node.TheType of
|
|
ldcntControl:
|
|
begin
|
|
Docker:=FindDockerByName(Node.Name);
|
|
// if the associated control does not exist or is not visible,
|
|
// then delete the node
|
|
if (Docker=nil) then begin
|
|
{$IFDEF VerboseAnchorDockCreateLayout}
|
|
DebugLn(['RemoveEmptyNodes delete unknown node: ',dbgs(Node)]);
|
|
{$ENDIF}
|
|
DeleteNode(Node);
|
|
end
|
|
else if not ControlIsVisible(Docker.Control) then begin
|
|
{$IFDEF VerboseAnchorDockCreateLayout}
|
|
DebugLn(['RemoveEmptyNodes delete invisible node: ',dbgs(Node)]);
|
|
{$ENDIF}
|
|
DeleteNode(Node);
|
|
end;
|
|
end;
|
|
ldcntPage:
|
|
// these are auto created parent node. If they have no childs: delete
|
|
if Node.ChildCount=0 then begin
|
|
{$IFDEF VerboseAnchorDockCreateLayout}
|
|
DebugLn(['RemoveEmptyNodes delete node without childs: ',dbgs(Node)]);
|
|
{$ENDIF}
|
|
DeleteNode(Node);
|
|
end;
|
|
ldcntForm:
|
|
// these are auto created parent node. If they have no childs: delete
|
|
// if they have only one child: delete node and move childs up
|
|
if Node.ChildCount=0 then begin
|
|
{$IFDEF VerboseAnchorDockCreateLayout}
|
|
DebugLn(['RemoveEmptyNodes delete node without childs: ',dbgs(Node)]);
|
|
{$ENDIF}
|
|
DeleteNode(Node);
|
|
end else if Node.ChildCount=1 then begin
|
|
// Only one child left
|
|
SimplifyOneChildForm(Node);
|
|
end;
|
|
ldcntPages:
|
|
// these are auto created parent node. If they have no childs: delete
|
|
// if they have only one child: delete node and move child up
|
|
if Node.ChildCount=0 then begin
|
|
{$IFDEF VerboseAnchorDockCreateLayout}
|
|
DebugLn(['RemoveEmptyNodes delete node without childs: ',dbgs(Node)]);
|
|
{$ENDIF}
|
|
DeleteNode(Node);
|
|
end else if Node.ChildCount=1 then begin
|
|
// Only one child left
|
|
SimplifyOnePageNode(Node);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function AllControlsAreOnSameForm: boolean;
|
|
var
|
|
RootForm: TControl;
|
|
|
|
function Check(Node: TLazDockConfigNode): boolean;
|
|
var
|
|
i: Integer;
|
|
CurForm: TControl;
|
|
begin
|
|
if Node.TheType=ldcntControl then begin
|
|
CurForm:=FindControlByDockerName(Node.Name);
|
|
if (CurForm<>nil) then begin
|
|
while CurForm.Parent<>nil do
|
|
CurForm:=CurForm.Parent;
|
|
if CurForm<>VisibleControl then begin
|
|
if RootForm=nil then
|
|
RootForm:=CurForm
|
|
else if RootForm<>CurForm then
|
|
exit(false);
|
|
end;
|
|
end;
|
|
end;
|
|
// check childs
|
|
for i:=0 to Node.ChildCount-1 do
|
|
if not Check(Node.Childs[i]) then exit(false);
|
|
Result:=true;
|
|
end;
|
|
|
|
begin
|
|
RootForm:=nil;
|
|
Result:=Check(Root);
|
|
end;
|
|
|
|
// FPC bug: when this function is internal of FindNearestControlNode then get win32 linker error
|
|
function FindOwnSplitterSiblingWithControl(Node: TLazDockConfigNode
|
|
): TLazDockConfigNode;
|
|
{ find a sibling, that is a direct neighbour behind a splitter, and the
|
|
splitter is only used by the node and the sibling
|
|
For example:
|
|
---------+
|
|
--+#+---+|
|
|
B |#| A ||
|
|
--+#+---+|
|
|
---------+
|
|
}
|
|
var
|
|
a: TAnchorKind;
|
|
SplitterNode: TLazDockConfigNode;
|
|
begin
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
if Node.Sides[a]='' then continue;
|
|
SplitterNode:=FindNode(Node.Sides[a]);
|
|
if (SplitterNode.TheType in [ldcntSplitterLeftRight,ldcntSplitterUpDown])
|
|
and SplitterNode.IsTheOnlyNeighbour(Node,a) then begin
|
|
Result:=SplitterNode.FindNeighbour(OppositeAnchor[a],true);
|
|
if Result<>nil then exit;
|
|
end;
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
|
|
function FindNearestControlNode: TLazDockConfigNode;
|
|
|
|
function FindSiblingWithControl(Node: TLazDockConfigNode
|
|
): TLazDockConfigNode;
|
|
var
|
|
ParentNode: TLazDockConfigNode;
|
|
i: Integer;
|
|
begin
|
|
ParentNode:=Node.Parent;
|
|
for i:=0 to ParentNode.ChildCount-1 do begin
|
|
Result:=ParentNode.Childs[i];
|
|
if CompareText(Result.Name,DockerName)=0 then continue;
|
|
if Result.TheType=ldcntControl then
|
|
exit;
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
|
|
function FindPageSiblingWithControl(Node: TLazDockConfigNode
|
|
): TLazDockConfigNode;
|
|
{ find direct page sibling
|
|
This means:
|
|
Node is the only child of a page
|
|
A sibling page has a single child with a control
|
|
}
|
|
var
|
|
PagesNode: TLazDockConfigNode;
|
|
PageNode: TLazDockConfigNode;
|
|
PageIndex: LongInt;
|
|
begin
|
|
// check if node is on a page without siblings
|
|
PageNode:=Node.Parent;
|
|
if (PageNode=nil) or (PageNode.TheType<>ldcntPage)
|
|
or (PageNode.ChildCount>1) then exit;
|
|
// check if left page has only one control
|
|
PagesNode:=PageNode.Parent;
|
|
PageIndex:=PagesNode.IndexOf(PageNode.Name);
|
|
if (PageIndex>0)
|
|
and (PagesNode[PageIndex-1].ChildCount=1) then begin
|
|
Result:=PagesNode[PageIndex-1].Childs[0];
|
|
if Result.TheType=ldcntControl then exit;
|
|
end;
|
|
// check if right page has only one control
|
|
if (PageIndex<PagesNode.ChildCount-1)
|
|
and (PagesNode[PageIndex+1].ChildCount=1) then begin
|
|
Result:=PagesNode[PageIndex+1].Childs[0];
|
|
if Result.TheType=ldcntControl then exit;
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
|
|
function FindOtherNodeWithControl(Node: TLazDockConfigNode
|
|
): TLazDockConfigNode;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
Result:=nil;
|
|
if (Node.TheType=ldcntControl)
|
|
and (CompareText(Node.Name,DockerName)<>0) then
|
|
exit(Node);
|
|
for i:=0 to Node.ChildCount-1 do begin
|
|
Result:=FindOtherNodeWithControl(Node.Childs[i]);
|
|
if Result<>nil then exit;
|
|
end;
|
|
end;
|
|
|
|
var
|
|
Node: TLazDockConfigNode;
|
|
begin
|
|
Node:=FindNode(DockerName);
|
|
Result:=FindOwnSplitterSiblingWithControl(Node);
|
|
if Result<>nil then exit;
|
|
Result:=FindSiblingWithControl(Node);
|
|
Node:=Root.FindByName(DockerName);
|
|
Result:=FindPageSiblingWithControl(Node);
|
|
if Result<>nil then exit;
|
|
Result:=FindOtherNodeWithControl(Root);
|
|
end;
|
|
|
|
var
|
|
Config: TLazDockerConfig;
|
|
CurControl: TControl;
|
|
NearestControlNode: TLazDockConfigNode;
|
|
begin
|
|
Result:=nil;
|
|
CurDockControl:=nil;
|
|
Root:=nil;
|
|
|
|
Config:=GetConfigWithDockerName(DockerName);
|
|
|
|
DebugLn(['TCustomLazDockingManager.CreateLayout DockerName="',DockerName,'"']);
|
|
if Config<>nil then
|
|
Config.WriteDebugReport;
|
|
|
|
if (Config=nil) or (Config.Root=nil) then begin
|
|
DebugLn(['TCustomLazDockingManager.CreateLayout DockerName="',DockerName,'" No control']);
|
|
exit;
|
|
end;
|
|
CurControl:=FindControlByDockerName(DockerName);
|
|
if not ControlIsVisible(CurControl) then begin
|
|
DebugLn(['TCustomLazDockingManager.CreateLayout DockerName="',DockerName,'" CurControl=',DbgSName(CurControl),' control not visible']);
|
|
exit;
|
|
end;
|
|
if (not ConfigIsCompatible(Config.Root,ExceptionOnError)) then begin
|
|
DebugLn(['TCustomLazDockingManager.CreateLayout DockerName="',DockerName,'" CurControl=',DbgSName(CurControl),' config is not compatible']);
|
|
exit;
|
|
end;
|
|
|
|
// create a copy of the config
|
|
Root:=TLazDockConfigNode.Create(nil);
|
|
try
|
|
Root.Assign(Config.Root);
|
|
|
|
// clean up by removing all invisible, unknown and empty nodes
|
|
RemoveEmptyNodes(Root);
|
|
|
|
// check if all used controls are on the same dock form
|
|
if not AllControlsAreOnSameForm then begin
|
|
DebugLn(['TCustomLazDockingManager.CreateLayout Not all Controls are on the same Form. Using only one form...']);
|
|
// the used controls are distributed on different dock forms
|
|
// => choose one dock form and remove the nodes of the others
|
|
NearestControlNode:=FindNearestControlNode;
|
|
if NearestControlNode=nil then RaiseGDBException('');
|
|
CurDockControl:=FindControlByDockerName(NearestControlNode.Name);
|
|
if CurDockControl=nil then RaiseGDBException('');
|
|
CurDockControl:=CurDockControl.GetTopParent;
|
|
// remove nodes of other dock forms
|
|
RemoveEmptyNodes(Root);
|
|
//DebugLn(['TCustomLazDockingManager.CreateLayout After removing nodes of other dock forms:']);
|
|
end;
|
|
|
|
DebugLn(['TCustomLazDockingManager.CreateLayout After removing unneeded nodes:']);
|
|
Root.WriteDebugReport;
|
|
|
|
Result:=Root;
|
|
Root:=nil;
|
|
finally
|
|
Root.Free;
|
|
end;
|
|
end;
|
|
|
|
function TCustomLazDockingManager.ConfigIsCompatible(
|
|
RootNode: TLazDockConfigNode; ExceptionOnError: boolean): boolean;
|
|
|
|
function CheckNode(Node: TLazDockConfigNode): boolean;
|
|
|
|
procedure Error(const Msg: string);
|
|
var
|
|
s: String;
|
|
begin
|
|
s:='Error: Node="'+Node.GetPath+'"';
|
|
s:=s+' NodeType='+LDConfigNodeTypeNames[Node.TheType];
|
|
s:=s+Msg;
|
|
DebugLn(s);
|
|
if ExceptionOnError then raise Exception.Create(s);
|
|
end;
|
|
|
|
function CheckSideAnchored(a: TAnchorKind): boolean;
|
|
var
|
|
SiblingName: string;
|
|
Sibling: TLazDockConfigNode;
|
|
|
|
procedure ErrorWrongSplitter;
|
|
begin
|
|
Error('invalid Node.Sides[a] Node="'+Node.Name+'"'
|
|
+' Node.Sides['+AnchorNames[a]+']="'+Node.Sides[a]+'"');
|
|
end;
|
|
|
|
begin
|
|
SiblingName:=Node.Sides[a];
|
|
if SiblingName='' then begin
|
|
Error('Node.Sides[a]=''''');
|
|
exit(false);
|
|
end;
|
|
Sibling:=RootNode.FindByName(SiblingName,true);
|
|
if Sibling=nil then begin
|
|
Error('Node.Sides[a] not found');
|
|
exit(false);
|
|
end;
|
|
if Sibling=Node.Parent then
|
|
exit(true); // anchored to parent: ok
|
|
if (a in [akLeft,akRight]) and (Sibling.TheType=ldcntSplitterLeftRight)
|
|
then
|
|
exit(true); // left/right side anchored to a left/right splitter: ok
|
|
if (a in [akTop,akBottom]) and (Sibling.TheType=ldcntSplitterUpDown)
|
|
then
|
|
exit(true); // top/bottom side anchored to a up/down splitter: ok
|
|
// otherwise: not ok
|
|
ErrorWrongSplitter;
|
|
Result:=false;
|
|
end;
|
|
|
|
function CheckAllSidesAnchored: boolean;
|
|
var
|
|
a: TAnchorKind;
|
|
begin
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do
|
|
if not CheckSideAnchored(a) then exit(false);
|
|
Result:=true;
|
|
end;
|
|
|
|
function CheckSideNotAnchored(a: TAnchorKind): boolean;
|
|
begin
|
|
if Node.Sides[a]<>'' then begin
|
|
Error('Sides[a]<>''''');
|
|
Result:=false;
|
|
end else
|
|
Result:=true;
|
|
end;
|
|
|
|
function CheckNoSideAnchored: boolean;
|
|
var
|
|
a: TAnchorKind;
|
|
begin
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do
|
|
if not CheckSideNotAnchored(a) then exit(false);
|
|
Result:=true;
|
|
end;
|
|
|
|
function CheckHasChilds: boolean;
|
|
begin
|
|
if Node.ChildCount=0 then begin
|
|
Error('ChildCount=0');
|
|
Result:=false;
|
|
end else
|
|
Result:=true;
|
|
end;
|
|
|
|
function CheckHasNoChilds: boolean;
|
|
begin
|
|
if Node.ChildCount>0 then begin
|
|
Error('ChildCount>0');
|
|
Result:=false;
|
|
end else
|
|
Result:=true;
|
|
end;
|
|
|
|
function CheckHasParent: boolean;
|
|
begin
|
|
if Node.Parent=nil then begin
|
|
Error('Parent=nil');
|
|
Result:=false;
|
|
end else
|
|
Result:=true;
|
|
end;
|
|
|
|
function CheckUniqueCorner(Side1, Side2: TAnchorKind): boolean;
|
|
var
|
|
i: Integer;
|
|
Child: TLazDockConfigNode;
|
|
begin
|
|
Result:=true;
|
|
if Node.Parent=nil then exit;
|
|
if Node.Sides[Side1]='' then exit;
|
|
if Node.Sides[Side2]='' then exit;
|
|
for i:=0 to Node.Parent.ChildCount-1 do begin
|
|
Child:=Node.Parent.Childs[i];
|
|
if Child=Node then continue;
|
|
if (CompareText(Node.Sides[Side1],Child.Sides[Side1])=0)
|
|
and (CompareText(Node.Sides[Side2],Child.Sides[Side2])=0) then begin
|
|
Error('overlapping nodes');
|
|
exit(false);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
var
|
|
a: TAnchorKind;
|
|
i: Integer;
|
|
begin
|
|
Result:=false;
|
|
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
if Node.Sides[a]<>'' then begin
|
|
if CompareText(Node.Sides[a],Node.Name)=0 then begin
|
|
Error('Node.Sides[a]=Node');
|
|
exit;
|
|
end;
|
|
if RootNode.FindByName(Node.Sides[a],true)=nil then begin
|
|
Error('unknown Node.Sides[a]');
|
|
exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
case Node.TheType of
|
|
ldcntControl:
|
|
begin
|
|
// a control node contains a TControl
|
|
if not CheckAllSidesAnchored then exit;
|
|
end;
|
|
ldcntForm:
|
|
begin
|
|
// a dock form is a dummy control, used as top level container
|
|
if Node.Parent<>nil then begin
|
|
Error('Parent<>nil');
|
|
exit;
|
|
end;
|
|
if not CheckHasChilds then exit;
|
|
if not CheckNoSideAnchored then exit;
|
|
end;
|
|
ldcntPages:
|
|
begin
|
|
// a pages node has only page nodes as childs
|
|
if not CheckHasParent then exit;
|
|
if not CheckHasChilds then exit;
|
|
for i:=0 to Node.ChildCount-1 do
|
|
if Node.Childs[i].TheType<>ldcntPage then begin
|
|
Error('Childs[i].TheType<>ldcntPage');
|
|
exit;
|
|
end;
|
|
if not CheckAllSidesAnchored then exit;
|
|
end;
|
|
ldcntPage:
|
|
begin
|
|
// a page is the child of a pages node, and a container
|
|
if not CheckHasParent then exit;
|
|
if not CheckHasChilds then exit;
|
|
if Node.Parent.TheType<>ldcntPages then begin
|
|
Error('Parent.TheType<>ldcntPages');
|
|
exit;
|
|
end;
|
|
if not CheckNoSideAnchored then exit;
|
|
end;
|
|
ldcntSplitterLeftRight:
|
|
begin
|
|
// a vertical splitter can be moved left/right
|
|
if not CheckHasParent then exit;
|
|
if not CheckHasNoChilds then exit;
|
|
if not CheckSideNotAnchored(akLeft) then exit;
|
|
if not CheckSideNotAnchored(akRight) then exit;
|
|
if not CheckSideAnchored(akTop) then exit;
|
|
if not CheckSideAnchored(akBottom) then exit;
|
|
end;
|
|
ldcntSplitterUpDown:
|
|
begin
|
|
// a horizontal splitter can be moved up/down
|
|
// it is anchored left and right, and not top/bottom
|
|
// it is not a root node
|
|
// it has no childs
|
|
if not CheckHasParent then exit;
|
|
if not CheckHasNoChilds then exit;
|
|
if not CheckSideNotAnchored(akTop) then exit;
|
|
if not CheckSideNotAnchored(akBottom) then exit;
|
|
if not CheckSideAnchored(akLeft) then exit;
|
|
if not CheckSideAnchored(akRight) then exit;
|
|
end;
|
|
else
|
|
Error('unknown type');
|
|
exit;
|
|
end;
|
|
|
|
if not CheckUniqueCorner(akLeft,akTop) then exit;
|
|
if not CheckUniqueCorner(akLeft,akBottom) then exit;
|
|
if not CheckUniqueCorner(akRight,akTop) then exit;
|
|
if not CheckUniqueCorner(akRight,akBottom) then exit;
|
|
|
|
// check childs
|
|
for i:=0 to Node.ChildCount-1 do
|
|
if not CheckNode(Node.Childs[i]) then exit;
|
|
|
|
Result:=true;
|
|
end;
|
|
|
|
begin
|
|
if RootNode=nil then exit(false);
|
|
Result:=CheckNode(RootNode);
|
|
end;
|
|
|
|
{ TLazDockConfigNode }
|
|
|
|
function TLazDockConfigNode.GetSides(Side: TAnchorKind): string;
|
|
begin
|
|
Result:=FSides[Side];
|
|
end;
|
|
|
|
function TLazDockConfigNode.GetChildCount: Integer;
|
|
begin
|
|
if FChilds<>nil then
|
|
Result:=FChilds.Count
|
|
else
|
|
Result:=0;
|
|
end;
|
|
|
|
function TLazDockConfigNode.GetChilds(Index: integer): TLazDockConfigNode;
|
|
begin
|
|
Result:=TLazDockConfigNode(FChilds[Index]);
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.SetBounds(const AValue: TRect);
|
|
begin
|
|
if CompareRect(@FBounds,@AValue) then exit;
|
|
FBounds:=AValue;
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.SetClientBounds(const AValue: TRect);
|
|
begin
|
|
if CompareRect(@FClientBounds,@AValue) then exit;
|
|
FClientBounds:=AValue;
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.SetName(const AValue: string);
|
|
begin
|
|
if FName=AValue then exit;
|
|
FName:=AValue;
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.SetParent(const AValue: TLazDockConfigNode);
|
|
begin
|
|
if FParent=AValue then exit;
|
|
if FParent<>nil then
|
|
FParent.DoRemove(Self);
|
|
FParent:=AValue;
|
|
if FParent<>nil then
|
|
FParent.DoAdd(Self);
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.SetSides(Side: TAnchorKind;
|
|
const AValue: string);
|
|
begin
|
|
FSides[Side]:=AValue;
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.SetTheType(const AValue: TLDConfigNodeType);
|
|
begin
|
|
if FTheType=AValue then exit;
|
|
FTheType:=AValue;
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.DoAdd(ChildNode: TLazDockConfigNode);
|
|
begin
|
|
if FChilds=nil then FChilds:=TFPList.Create;
|
|
FChilds.Add(ChildNode);
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.DoRemove(ChildNode: TLazDockConfigNode);
|
|
begin
|
|
if TObject(FChilds[FChilds.Count-1])=ChildNode then
|
|
FChilds.Delete(FChilds.Count-1)
|
|
else
|
|
FChilds.Remove(ChildNode);
|
|
end;
|
|
|
|
constructor TLazDockConfigNode.Create(ParentNode: TLazDockConfigNode);
|
|
begin
|
|
FTheType:=ldcntControl;
|
|
Parent:=ParentNode;
|
|
end;
|
|
|
|
constructor TLazDockConfigNode.Create(ParentNode: TLazDockConfigNode;
|
|
const AName: string);
|
|
begin
|
|
FName:=AName;
|
|
Create(ParentNode);
|
|
end;
|
|
|
|
destructor TLazDockConfigNode.Destroy;
|
|
begin
|
|
Clear;
|
|
Parent:=nil;
|
|
FChilds.Free;
|
|
FChilds:=nil;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.Clear;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if FChilds=nil then exit;
|
|
for i:=ChildCount-1 downto 0 do Childs[i].Free;
|
|
FChilds.Clear;
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.Assign(Source: TPersistent);
|
|
var
|
|
Src: TLazDockConfigNode;
|
|
i: Integer;
|
|
SrcChild: TLazDockConfigNode;
|
|
NewChild: TLazDockConfigNode;
|
|
a: TAnchorKind;
|
|
begin
|
|
if Source is TLazDockConfigNode then begin
|
|
Clear;
|
|
Src:=TLazDockConfigNode(Source);
|
|
FBounds:=Src.FBounds;
|
|
FClientBounds:=Src.FClientBounds;
|
|
FName:=Src.FName;
|
|
FWindowState:=Src.FWindowState;
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do
|
|
FSides[a]:=Src.FSides[a];
|
|
FTheType:=Src.FTheType;
|
|
for i:=0 to Src.ChildCount-1 do begin
|
|
SrcChild:=Src.Childs[i];
|
|
NewChild:=TLazDockConfigNode.Create(Self);
|
|
NewChild.Assign(SrcChild);
|
|
end;
|
|
end else
|
|
inherited Assign(Source);
|
|
end;
|
|
|
|
function TLazDockConfigNode.FindByName(const AName: string;
|
|
Recursive: boolean; WithRoot: boolean): TLazDockConfigNode;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if WithRoot and (CompareText(Name,AName)=0) then exit(Self);
|
|
if FChilds<>nil then
|
|
for i:=0 to FChilds.Count-1 do begin
|
|
Result:=Childs[i];
|
|
if CompareText(Result.Name,AName)=0 then exit;
|
|
if Recursive then begin
|
|
Result:=Result.FindByName(AName,true,false);
|
|
if Result<>nil then exit;
|
|
end;
|
|
end;
|
|
Result:=nil;
|
|
end;
|
|
|
|
function TLazDockConfigNode.IndexOf(const AName: string): Integer;
|
|
begin
|
|
if FChilds<>nil then begin
|
|
Result:=FChilds.Count-1;
|
|
while (Result>=0) and (CompareText(Childs[Result].Name,AName)<>0) do
|
|
dec(Result);
|
|
end else begin
|
|
Result:=-1;
|
|
end;
|
|
end;
|
|
|
|
function TLazDockConfigNode.GetScreenBounds: TRect;
|
|
var
|
|
NewWidth: Integer;
|
|
NewHeight: Integer;
|
|
NewLeft: LongInt;
|
|
NewTop: LongInt;
|
|
Node: TLazDockConfigNode;
|
|
begin
|
|
NewWidth:=FBounds.Right-FBounds.Left;
|
|
NewHeight:=FBounds.Bottom-FBounds.Top;
|
|
NewLeft:=FBounds.Left;
|
|
NewTop:=FBounds.Top;
|
|
Node:=Parent;
|
|
while Node<>nil do begin
|
|
inc(NewLeft,Node.FBounds.Left+Node.FClientBounds.Left);
|
|
inc(NewTop,Node.FBounds.Top+Node.FClientBounds.Top);
|
|
Node:=Node.Parent;
|
|
end;
|
|
Result:=Classes.Bounds(NewLeft,NewTop,NewWidth,NewHeight);
|
|
end;
|
|
|
|
function TLazDockConfigNode.FindNeighbour(SiblingSide: TAnchorKind;
|
|
NilIfAmbiguous: boolean; IgnoreSplitters: boolean): TLazDockConfigNode;
|
|
var
|
|
i: Integer;
|
|
ParentNode: TLazDockConfigNode;
|
|
Child: TLazDockConfigNode;
|
|
begin
|
|
Result:=nil;
|
|
ParentNode:=Parent;
|
|
for i:=0 to ParentNode.ChildCount-1 do begin
|
|
Child:=ParentNode.Childs[i];
|
|
if Child=Self then continue;
|
|
if IgnoreSplitters
|
|
and (Child.TheType in [ldcntSplitterLeftRight,ldcntSplitterUpDown]) then
|
|
continue;
|
|
if CompareText(Child.Sides[SiblingSide],Name)=0 then begin
|
|
if Result=nil then
|
|
Result:=Child
|
|
else if NilIfAmbiguous then
|
|
exit(nil);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TLazDockConfigNode.IsTheOnlyNeighbour(Node: TLazDockConfigNode;
|
|
SiblingSide: TAnchorKind): boolean;
|
|
{ check if one side is only used by Node.
|
|
For example: If only Node.Sides[SiblingSide]=Name
|
|
---------+
|
|
--+#+---+|
|
|
B |#| A ||
|
|
--+#+---+|
|
|
---------+}
|
|
begin
|
|
Result:=FindNeighbour(SiblingSide,true)<>nil;
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.SaveToConfig(Config: TConfigStorage;
|
|
const Path: string);
|
|
var
|
|
a: TAnchorKind;
|
|
i: Integer;
|
|
Child: TLazDockConfigNode;
|
|
SubPath: String;
|
|
begin
|
|
Config.SetDeleteValue(Path+'Name/Value',Name,'');
|
|
Config.SetDeleteValue(Path+'Type/Value',LDConfigNodeTypeNames[TheType],
|
|
LDConfigNodeTypeNames[ldcntControl]);
|
|
Config.SetDeleteValue(Path+'Bounds/',FBounds,Rect(0,0,0,0));
|
|
Config.SetDeleteValue(Path+'ClientBounds/',FClientBounds,
|
|
Rect(0,0,FBounds.Right-FBounds.Left,FBounds.Bottom-FBounds.Top));
|
|
Config.SetDeleteValue(Path+'WindowState/Value',WindowStateToStr(WindowState),
|
|
WindowStateToStr(wsNormal));
|
|
|
|
// Sides
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do
|
|
Config.SetDeleteValue(Path+'Sides/'+AnchorNames[a]+'/Name',Sides[a],'');
|
|
|
|
// childs
|
|
Config.SetDeleteValue(Path+'Childs/Count',ChildCount,0);
|
|
for i:=0 to ChildCount-1 do begin
|
|
Child:=Childs[i];
|
|
SubPath:=Path+'Child'+IntToStr(i+1)+'/';
|
|
Child.SaveToConfig(Config,SubPath);
|
|
end;
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.LoadFromConfig(Config: TConfigStorage;
|
|
const Path: string);
|
|
var
|
|
a: TAnchorKind;
|
|
i: Integer;
|
|
NewChildCount: LongInt;
|
|
NewChildName: String;
|
|
NewChild: TLazDockConfigNode;
|
|
SubPath: String;
|
|
begin
|
|
Clear;
|
|
// Note: 'Name' is stored only for information, but not restored on load
|
|
TheType:=LDConfigNodeTypeNameToType(Config.GetValue(Path+'Type/Value',
|
|
LDConfigNodeTypeNames[ldcntControl]));
|
|
Config.GetValue(Path+'Bounds/',FBounds,Rect(0,0,0,0));
|
|
Config.GetValue(Path+'ClientBounds/',FClientBounds,
|
|
Rect(0,0,FBounds.Right-FBounds.Left,FBounds.Bottom-FBounds.Top));
|
|
WindowState:=StrToWindowState(config.GetValue(Path+'WindowState/Value',''));
|
|
|
|
// Sides
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do
|
|
Sides[a]:=Config.GetValue(Path+'Sides/'+AnchorNames[a]+'/Name','');
|
|
|
|
// childs
|
|
NewChildCount:=Config.GetValue(Path+'Childs/Count',0);
|
|
for i:=0 to NewChildCount-1 do begin
|
|
SubPath:=Path+'Child'+IntToStr(i+1)+'/';
|
|
NewChildName:=Config.GetValue(SubPath+'Name/Value','');
|
|
NewChild:=TLazDockConfigNode.Create(Self,NewChildName);
|
|
NewChild.Parent:=Self;
|
|
NewChild.LoadFromConfig(Config,SubPath);
|
|
end;
|
|
end;
|
|
|
|
procedure TLazDockConfigNode.WriteDebugReport;
|
|
|
|
procedure WriteNode(const Prefix: string; ANode: TLazDockConfigNode);
|
|
var
|
|
a: TAnchorKind;
|
|
i: Integer;
|
|
s: string;
|
|
begin
|
|
if ANode=nil then exit;
|
|
DbgOut(Prefix,'Name="'+ANode.Name+'"');
|
|
DbgOut(' Type=',GetEnumName(TypeInfo(TLDConfigNodeType),ord(ANode.TheType)));
|
|
DbgOut(' Bounds='+dbgs(ANode.Bounds));
|
|
DbgOut(' ClientBounds='+dbgs(ANode.ClientBounds));
|
|
DbgOut(' Childs='+dbgs(ANode.ChildCount));
|
|
DbgOut(' WindowState='+WindowStateToStr(ANode.WindowState));
|
|
for a:=Low(TAnchorKind) to High(TAnchorKind) do begin
|
|
s:=ANode.Sides[a];
|
|
if s='' then
|
|
s:='?';
|
|
DbgOut(' '+AnchorNames[a]+'="'+s+'"');
|
|
end;
|
|
debugln;
|
|
for i:=0 to ANode.ChildCount-1 do begin
|
|
WriteNode(Prefix+' ',ANode[i]);
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
DebugLn('TLazDockConfigNode.WriteDebugReport Root=',dbgs(Self));
|
|
WriteNode(' ',Self);
|
|
DebugLn(DebugLayoutAsString);
|
|
DumpStack;
|
|
end;
|
|
|
|
function TLazDockConfigNode.DebugLayoutAsString: string;
|
|
type
|
|
TArrayOfRect = array of TRect;
|
|
TNodeInfo = record
|
|
MinSize: TPoint;
|
|
MinSizeValid, MinSizeCalculating: boolean;
|
|
MinLeft: integer;
|
|
MinLeftValid, MinLeftCalculating: boolean;
|
|
MinTop: Integer;
|
|
MinTopValid, MinTopCalculating: boolean;
|
|
end;
|
|
PNodeInfo = ^TNodeInfo;
|
|
var
|
|
Cols: LongInt;
|
|
Rows: LongInt;
|
|
LogCols: Integer;
|
|
NodeInfos: TPointerToPointerTree;// TLazDockConfigNode to PNodeInfo
|
|
|
|
procedure InitNodeInfos;
|
|
begin
|
|
NodeInfos:=TPointerToPointerTree.Create;
|
|
end;
|
|
|
|
procedure FreeNodeInfos;
|
|
var
|
|
Item: PNodeInfo;
|
|
NodePtr, InfoPtr: Pointer;
|
|
begin
|
|
NodeInfos.GetFirst(NodePtr,InfoPtr);
|
|
repeat
|
|
Item:=PNodeInfo(InfoPtr);
|
|
if Item=nil then break;
|
|
Dispose(Item);
|
|
until not NodeInfos.GetNext(NodePtr,NodePtr,InfoPtr);
|
|
NodeInfos.Free;
|
|
end;
|
|
|
|
function GetNodeInfo(Node: TLazDockConfigNode): PNodeInfo;
|
|
begin
|
|
Result:=PNodeInfo(NodeInfos[Node]);
|
|
if Result=nil then begin
|
|
New(Result);
|
|
FillChar(Result^,SizeOf(TNodeInfo),0);
|
|
NodeInfos[Node]:=Result;
|
|
end;
|
|
end;
|
|
|
|
procedure w(x,y: Integer; const s: string; MaxX: Integer = 0);
|
|
var
|
|
i: Integer;
|
|
begin
|
|
for i:=1 to length(s) do begin
|
|
if (MaxX>0) and (x+i>MaxX) then exit;
|
|
Result[LogCols*(y-1) + x + i-1]:=s[i];
|
|
end;
|
|
end;
|
|
|
|
procedure wfillrect(const ARect: TRect; c: char);
|
|
var
|
|
x: LongInt;
|
|
y: LongInt;
|
|
begin
|
|
for x:=ARect.Left to ARect.Right do
|
|
for y:=ARect.Top to ARect.Bottom do
|
|
w(x,y,c);
|
|
end;
|
|
|
|
procedure wrectangle(const ARect: TRect);
|
|
begin
|
|
w(ARect.Left,ARect.Top,'+');
|
|
w(ARect.Right,ARect.Top,'+');
|
|
w(ARect.Left,ARect.Bottom,'+');
|
|
w(ARect.Right,ARect.Bottom,'+');
|
|
if ARect.Left<ARect.Right then begin
|
|
if ARect.Top<ARect.Bottom then begin
|
|
wfillrect(Rect(ARect.Left+1,ARect.Top,ARect.Right-1,ARect.Top),'-');// top line
|
|
wfillrect(Rect(ARect.Left+1,ARect.Bottom,ARect.Right-1,ARect.Bottom),'-');// bottom line
|
|
wfillrect(Rect(ARect.Left,ARect.Top+1,ARect.Left,ARect.Bottom-1),'|');// left line
|
|
wfillrect(Rect(ARect.Right,ARect.Top+1,ARect.Right,ARect.Bottom-1),'|');// right line
|
|
end else begin
|
|
wfillrect(Rect(ARect.Left+1,ARect.Top,ARect.Right-1,ARect.Top),'=');// horizontal line
|
|
end;
|
|
end else begin
|
|
wfillrect(Rect(ARect.Left,ARect.Top+1,ARect.Left,ARect.Bottom-1),'#');// vertical line
|
|
end;
|
|
end;
|
|
|
|
function MapRect(const OriginalRect, OldBounds, NewBounds: TRect): TRect;
|
|
|
|
function MapX(i: Integer): Integer;
|
|
begin
|
|
Result:=NewBounds.Left+
|
|
(((i-OldBounds.Left)*(NewBounds.Right-NewBounds.Left))
|
|
div (OldBounds.Right-OldBounds.Left));
|
|
end;
|
|
|
|
function MapY(i: Integer): Integer;
|
|
begin
|
|
Result:=NewBounds.Top+
|
|
(((i-OldBounds.Top)*(NewBounds.Bottom-NewBounds.Top))
|
|
div (OldBounds.Bottom-OldBounds.Top));
|
|
end;
|
|
|
|
begin
|
|
Result.Left:=MapX(OriginalRect.Left);
|
|
Result.Top:=MapY(OriginalRect.Left);
|
|
Result.Right:=MapX(OriginalRect.Left);
|
|
Result.Bottom:=MapY(OriginalRect.Left);
|
|
end;
|
|
|
|
function GetMinSize(Node: TLazDockConfigNode): TPoint; forward;
|
|
|
|
function GetMinPos(Node: TLazDockConfigNode; Side: TAnchorKind): Integer;
|
|
// calculates left or top position of Node
|
|
|
|
function Compute(var MinPosValid, MinPosCalculating: boolean;
|
|
var MinPos: Integer): Integer;
|
|
|
|
procedure Improve(Neighbour: TLazDockConfigNode);
|
|
var
|
|
NeighbourPos: LongInt;
|
|
NeighbourSize: TPoint;
|
|
NeighbourLength: LongInt;
|
|
begin
|
|
if Neighbour=nil then exit;
|
|
if Neighbour.Parent<>Node.Parent then exit;
|
|
NeighbourPos:=GetMinPos(Neighbour,Side);
|
|
NeighbourSize:=GetMinSize(Neighbour);
|
|
if Side=akLeft then
|
|
NeighbourLength:=NeighbourSize.X
|
|
else
|
|
NeighbourLength:=NeighbourSize.Y;
|
|
MinPos:=Max(MinPos,NeighbourPos+NeighbourLength);
|
|
end;
|
|
|
|
var
|
|
Sibling: TLazDockConfigNode;
|
|
i: Integer;
|
|
begin
|
|
if MinPosCalculating then begin
|
|
DebugLn(['DebugLayoutAsString.GetMinPos.Compute WARNING: anchor circle detected']);
|
|
DumpStack;
|
|
exit(1);
|
|
end;
|
|
if (not MinPosValid) then begin
|
|
MinPosValid:=true;
|
|
MinPosCalculating:=true;
|
|
if Node.Sides[Side]<>'' then begin
|
|
Sibling:=FindByName(Node.Sides[Side],true,true);
|
|
Improve(Sibling);
|
|
end;
|
|
if Node.Parent<>nil then begin
|
|
for i:=0 to Node.Parent.ChildCount-1 do begin
|
|
Sibling:=Node.Parent.Childs[i];
|
|
if CompareText(Sibling.Sides[OppositeAnchor[Side]],Node.Name)=0 then
|
|
Improve(Sibling);
|
|
end;
|
|
end;
|
|
MinPosCalculating:=false;
|
|
end;
|
|
Result:=MinPos;
|
|
end;
|
|
|
|
var
|
|
Info: PNodeInfo;
|
|
begin
|
|
Info:=GetNodeInfo(Node);
|
|
//DebugLn(['GetMinPos ',Node.Name,' ',AnchorNames[Side],' ',Info^.MinLeftCalculating]);
|
|
if Side=akLeft then
|
|
Result:=Compute(Info^.MinLeftValid,Info^.MinLeftCalculating,Info^.MinLeft)
|
|
else
|
|
Result:=Compute(Info^.MinTopValid,Info^.MinTopCalculating,Info^.MinTop);
|
|
end;
|
|
|
|
function GetChildsMinSize(Node: TLazDockConfigNode): TPoint;
|
|
// calculate the minimum size needed to draw the content of the node
|
|
var
|
|
i: Integer;
|
|
ChildMinSize: TPoint;
|
|
Child: TLazDockConfigNode;
|
|
ChildSize: TPoint;
|
|
begin
|
|
//DebugLn(['GetChildsMinSize ',Node.name]);
|
|
Result:=Point(0,0);
|
|
if Node.TheType=ldcntPages then begin
|
|
// maximum size of all pages
|
|
for i:=0 to Node.ChildCount-1 do begin
|
|
ChildMinSize:=GetMinSize(Node.Childs[i]);
|
|
Result.X:=Max(Result.X,ChildMinSize.X);
|
|
Result.Y:=Max(Result.Y,ChildMinSize.Y);
|
|
end;
|
|
end else begin
|
|
for i:=0 to Node.ChildCount-1 do begin
|
|
Child:=Node.Childs[i];
|
|
ChildSize:=GetMinSize(Child);
|
|
Result.X:=Max(Result.X,GetMinPos(Child,akLeft)+ChildSize.X);
|
|
Result.Y:=Max(Result.Y,GetMinPos(Child,akTop)+ChildSize.Y);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function GetMinSize(Node: TLazDockConfigNode): TPoint;
|
|
// calculate the minimum size needed to draw the node
|
|
var
|
|
ChildMinSize: TPoint;
|
|
Info: PNodeInfo;
|
|
begin
|
|
//DebugLn(['GetMinSize ',Node.name]);
|
|
Info:=GetNodeInfo(Node);
|
|
if Info^.MinSizeValid then begin
|
|
Result:=Info^.MinSize;
|
|
exit;
|
|
end;
|
|
if Info^.MinSizeCalculating then begin
|
|
DebugLn(['DebugLayoutAsString.GetMinSize WARNING: anchor circle detected']);
|
|
DumpStack;
|
|
Result:=Point(1,1);
|
|
exit;
|
|
end;
|
|
Info^.MinSizeCalculating:=true;
|
|
Result.X:=2+length(Node.Name);// border plus caption
|
|
Result.Y:=2; // border
|
|
if (Node.ChildCount=0) then begin
|
|
case Node.TheType of
|
|
ldcntSplitterLeftRight,ldcntSplitterUpDown:
|
|
Result:=Point(1,1); // splitters don't need captions
|
|
end;
|
|
end else begin
|
|
ChildMinSize:=GetChildsMinSize(Node);
|
|
Result.X:=Max(Result.X,ChildMinSize.X+2);
|
|
Result.Y:=Max(Result.Y,ChildMinSize.Y+2);
|
|
end;
|
|
Info^.MinSize:=Result;
|
|
Info^.MinSizeValid:=true;
|
|
Info^.MinSizeCalculating:=false;
|
|
end;
|
|
|
|
procedure DrawNode(Node: TLazDockConfigNode; ARect: TRect);
|
|
var
|
|
i: Integer;
|
|
Child: TLazDockConfigNode;
|
|
ChildSize: TPoint;
|
|
ChildRect: TRect;
|
|
AnchorNode: TLazDockConfigNode;
|
|
begin
|
|
//DebugLn(['DrawNode Node=',Node.Name,' ARect=',dbgs(ARect)]);
|
|
wrectangle(ARect);
|
|
w(ARect.Left+1,ARect.Top,Node.Name,ARect.Right);
|
|
|
|
for i := 0 to Node.ChildCount-1 do begin
|
|
Child:=Node.Childs[i];
|
|
ChildRect.Left:=ARect.Left+1+GetMinPos(Child,akLeft);
|
|
ChildRect.Top:=ARect.Top+1+GetMinPos(Child,akTop);
|
|
ChildSize:=GetMinSize(Child);
|
|
ChildRect.Right:=ChildRect.Left+ChildSize.X-1;
|
|
ChildRect.Bottom:=ChildRect.Top+ChildSize.Y-1;
|
|
if Child.Sides[akRight]<>'' then begin
|
|
AnchorNode:=FindByName(Child.Sides[akRight]);
|
|
if AnchorNode=Node then
|
|
ChildRect.Right:=ARect.Right-1
|
|
else if AnchorNode.Parent=Node then
|
|
ChildRect.Right:=ARect.Left+1+GetMinPos(AnchorNode,akLeft)-1;
|
|
end;
|
|
if Child.Sides[akBottom]<>'' then begin
|
|
AnchorNode:=FindByName(Child.Sides[akBottom]);
|
|
if AnchorNode=Node then
|
|
ChildRect.Bottom:=ARect.Bottom-1
|
|
else if AnchorNode.Parent=Node then
|
|
ChildRect.Bottom:=ARect.Top+1+GetMinPos(AnchorNode,akTop)-1;
|
|
end;
|
|
DrawNode(Child,ChildRect);
|
|
if Node.TheType=ldcntPages then begin
|
|
// paint only one page
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
var
|
|
e: string;
|
|
y: Integer;
|
|
begin
|
|
Cols:=StrToIntDef(Application.GetOptionValue('ldcn-colunms'),79);
|
|
Rows:=StrToIntDef(Application.GetOptionValue('ldcn-rows'),20);
|
|
|
|
InitNodeInfos;
|
|
try
|
|
e:=LineEnding;
|
|
LogCols:=Cols+length(e);
|
|
SetLength(Result,LogCols*Rows);
|
|
// fill space
|
|
FillChar(Result[1],length(Result),' ');
|
|
// add line endings
|
|
for y:=1 to Rows do
|
|
w(Cols+1,y,e);
|
|
// draw node
|
|
DrawNode(Self,Rect(1,1,Cols,Rows));
|
|
finally
|
|
FreeNodeInfos;
|
|
end;
|
|
end;
|
|
|
|
function TLazDockConfigNode.GetPath: string;
|
|
var
|
|
Node: TLazDockConfigNode;
|
|
begin
|
|
Result:='';
|
|
Node:=Self;
|
|
while Node<>nil do begin
|
|
if Result<>'' then
|
|
Result:=Node.Name+'/'+Result
|
|
else
|
|
Result:=Node.Name;
|
|
Node:=Node.Parent;
|
|
end;
|
|
end;
|
|
|
|
{ TLazDockerConfig }
|
|
|
|
constructor TLazDockerConfig.Create(const ADockerName: string;
|
|
ANode: TLazDockConfigNode);
|
|
begin
|
|
FDockerName:=ADockerName;
|
|
FRoot:=ANode;
|
|
end;
|
|
|
|
destructor TLazDockerConfig.Destroy;
|
|
begin
|
|
FRoot.Free; // who will clear it else?
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TLazDockerConfig.WriteDebugReport;
|
|
begin
|
|
DebugLn(['TLazDockerConfig.WriteDebugReport DockerName="',DockerName,'"']);
|
|
if Root<>nil then begin
|
|
Root.WriteDebugReport;
|
|
end else begin
|
|
DebugLn([' Root=nil']);
|
|
end;
|
|
end;
|
|
|
|
{ TAnchoredDockManager }
|
|
|
|
procedure TAnchoredDockManager.DisableLayout(Control: TControl);
|
|
begin
|
|
FConfigs.DisableLayout(Control);
|
|
inherited DisableLayout(Control);
|
|
end;
|
|
|
|
procedure TAnchoredDockManager.EnableLayout(Control: TControl);
|
|
begin
|
|
inherited EnableLayout(Control);
|
|
FConfigs.EnableLayout(Control);
|
|
end;
|
|
|
|
end.
|