lazarus/examples/dockmanager/package/umakesite.pas
2009-11-24 12:59:34 +00:00

696 lines
19 KiB
ObjectPascal

unit uMakeSite;
(* Create elastic dock sites within a form, make forms dockable.
Owners:
The DockMaster can own all floating forms, for easy enumeration.
The DockMaster can own all dock grips, for easy detection of the dockable forms.
Handle destruction how?
The owner of the dockable forms is responsible for creating or finding dockable forms?
The auto-created forms are/shall be owned by DockMaster.Owner?
Problems:
Forms are not (easily) dockable on all platforms,
we add a grabber icon to each dockable form,
and wrap them in a managed floating form.
Default floating sites are owned by Application,
we have to create the floating sites in the form.OnEndDock event.
Owning panels is dangerous, they are not destroyed with their parent form!
*)
{$mode objfpc}{$H+}
{$DEFINE ownSites} //floating sites owned by DockMaster?
{$DEFINE ownForms} //dockable forms owned by owner of DockMaster
{$DEFINE ownGrips} //docking grips owned by DockMaster?
interface
uses
Classes, SysUtils, Controls, Forms, ExtCtrls, EasyDockSite,
fFloatingSite;
type
sDockSides = TAlignSet;
TDockPanel = class(TPanel)
protected
AutoExpand: boolean; //do autoshrink?
Splitter: TSplitter; //associated
procedure pnlDockDrop(Sender: TObject; Source: TDragDockObject;
X, Y: Integer);
procedure pnlDockOver(Sender: TObject; Source: TDragDockObject;
X, Y: Integer; State: TDragState; var Accept: Boolean);
procedure pnlGetSiteInfo(Sender: TObject; DockClient: TControl;
var InfluenceRect: TRect; MousePos: TPoint; var CanDock: Boolean);
procedure pnlUnDock(Sender: TObject; Client: TControl;
NewTarget: TWinControl; var Allow: Boolean);
public
end;
(* The owner of all docksites (if ownSites is defined),
and of all dockable window grips (if ownGrips is defined)
*)
TDockMaster = class(TComponent)
protected //event handlers
procedure DockHandleMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
procedure FormEndDock(Sender, Target: TObject; X, Y: Integer);
protected //utilities
function ReloadForm(const AName: string): TCustomForm; virtual;
function WrapDockable(Client: TControl): TFloatingSite;
private
LastSite: TFloatingSite;
public
Factory: TWinControl; //generic owner
procedure AddElasticSites(AForm: TCustomForm; Sides: sDockSides);
function CreateDockable(const AName: string; fMultiInst: boolean; fWrap: boolean = True): TCustomForm;
procedure DumpSites;
procedure LoadFromStream(Stream: TStream);
procedure SaveToStream(Stream: TStream);
end;
implementation
uses
LCLIntf, LCLProc;
//fMasterSite,
type
TWinControlAccess = class(TWinControl)
end;
const
PanelNames: array[TAlign] of string = (
'', '', //alNone, alTop,
'pnlBottom', 'pnlLeft', 'pnlRight',
'', '' //alClient, alCustom
);
{ TDockMaster }
procedure TDockMaster.AddElasticSites(AForm: TCustomForm; Sides: sDockSides);
var
side: TAlign;
pnl: TDockPanel;
spl: TSplitter;
dm: TEasyTree;
const
AllowedSides: sDockSides = [alLeft, alRight, alBottom];
begin
for side := low(side) to high(side) do begin
if (side in AllowedSides) and (side in Sides) then begin
//TWinControlAccess(AForm).ReloadDockedControl(PanelNames[side], pnl);
TComponent(pnl) := AForm.FindComponent(PanelNames[side]);
if pnl = nil then begin
//create the components
{$IFDEF ownSites}
pnl := TDockPanel.Create(self); //owned by?
{$ELSE}
pnl := TDockPanel.Create(AForm); //owned by?
{$ENDIF}
pnl.Name := PanelNames[side];
pnl.Parent := AForm;
pnl.Align := side;
pnl.BorderWidth := 1;
//pnl.BorderStyle := bsSingle; // does not properly handle the size
dm := TEasyTree.Create(pnl);
dm.SetStyle(hsForm);
pnl.DockSite := True;
pnl.UseDockManager := True;
pnl.Visible := True;
spl := TSplitter.Create(AForm);
spl.Parent := AForm;
spl.Align := side;
//spl.BorderStyle := bsSingle;
spl.Beveled := True;
//size components
pnl.Splitter := spl;
if side in [alLeft,alRight] then
pnl.Width := 0
else
pnl.Height := 0;
//handlers required for elastic sites
pnl.OnDockDrop := @pnl.pnlDockDrop;
pnl.OnDockOver := @pnl.pnlDockOver;
pnl.OnUnDock := @pnl.pnlUnDock;
pnl.OnGetSiteInfo := @pnl.pnlGetSiteInfo;
end;
end;
end;
end;
function TDockMaster.CreateDockable(const AName: string;
fMultiInst: boolean; fWrap: boolean): TCustomForm;
var
img: TImage;
r: TRect;
Site: TFloatingSite;
Res: TWinControlAccess absolute Result;
begin
(* Create a dockable form, based on its name.
Used also to restore a layout.
Options (to come or to be removed)
fMultiInst allows to auto-create new versions (if True),
otherwise an already existing instance is returned. (really returned?)
*)
//get the form
Result := ReloadForm(AName);
if Result = nil then
exit;
//check make dockable
if Res.DragKind <> dkDock then begin
//make it dockable
Res.DragKind := dkDock;
Res.OnEndDock := @FormEndDock; //float into default host site
end;
//wrap into floating site, if requested (not on restore Layout)
if fWrap then begin
//wrap into dock site
Site := WrapDockable(Result);
end;
//create a docking handle - should become a component?
if LastSite <> nil then begin //problem: find grabber picture!?
img := TImage.Create(Result); //we could own the img, and be notified when its parent becomes nil
img.Parent := Result;
img.Align := alNone;
img.Anchors := [akTop, akRight];
r := Result.ClientRect;
r.bottom := 16;
r.Left := r.Right - 16;
img.BoundsRect := r;
img.Picture := LastSite.Image1.Picture;
img.OnMouseMove := @DockHandleMouseMove;
img.Visible := True;
end;
Result.Visible := True;
end;
procedure TDockMaster.FormEndDock(Sender, Target: TObject; X, Y: Integer);
var
ctl: TControl;
Site: TFloatingSite;
begin
(* Handler for Form.OnEndDock.
When a form becomes floating, dock immediately into a new floating host docksite.
*)
if Target <> nil then
exit; //docked, not floating
ctl := Sender as TControl;
if ctl.HostDockSite = nil then begin
//DebugLn('--- floating');
WrapDockable(ctl);
end else begin
//DebugLn('--- in ' + HostDockSite.Name);
end;
end;
procedure TDockMaster.LoadFromStream(Stream: TStream);
var
ctl, pre: TControl;
site: TFloatingSite;
nb: TEasyBook;
procedure MakeForm;
begin
pre := ctl;
ctl := CreateDockable('', True, False);
end;
begin
(* Restore a layout.
- Create all ElasticSites (to come)
- Create all floating sites
- Reload all docked controls
Notebooks?
In the simple case a notebook is created automatically, by docking a control
with align=alCustom.
In order to maintain proper docking we'll have to create and name the notebooks
before, then create and dock all their clients.
Ownership?
When notebooks are dockable, they cannot be owned by the DockSite!
*)
//Test0;
site := TFloatingSite.Create(self);
MakeForm; ctl.ManualDock(site, nil, alClient);
MakeForm; ctl.ManualDock(site, pre, alRight);
if False then begin //simple case
MakeForm; ctl.ManualDock(site, pre, alBottom);
MakeForm; ctl.ManualDock(site, pre, alCustom);
end else begin
nb := NoteBookCreate(site); //name it...
nb.ManualDock(site, ctl, alBottom);
//MakeForm; NoteBookAdd(nb, ctl);
MakeForm; ctl.ManualDock(site, nb, alCustom);
MakeForm; ctl.ManualDock(site, nb, alCustom);
end;
end;
procedure TDockMaster.SaveToStream(Stream: TStream);
begin
end;
function TDockMaster.ReloadForm(const AName: string): TCustomForm;
var
basename, instname: string;
i, l, instno: integer;
fc: TFormClass;
fo: TComponent; //form owner
ctl: TControl;
const
digits = ['0'..'9'];
begin
(* Get a form from the Factory, or search/create it.
The name is split into basename and instance number.
A component of T<basename> is created (and named AName - automatic!).
*)
Result := nil;
//check if Factory can provide the form
if assigned(Factory) then begin
TWinControlAccess(Factory).ReloadDockedControl(AName, ctl);
if ctl is TCustomForm then begin
Result := TCustomForm(ctl);
exit;
end; //else assume that we should do everything?
FreeAndNil(ctl);
end;
//search/create ourselves
{$IFDEF ownForms}
fo := Owner; //our owner also owns the forms
{$ELSE}
fo := Self; //we own the forms
{$ENDIF}
if AName = '' then begin
//test!
Result := TForm.Create(fo); //named Form1, Form2...
end else begin
//find the instance number, if present
instno := 0;
l := Length(AName);
i := l;
while AName[i] in digits do begin
dec(i);
end;
//i now is the position of the last non-digit in the name
//extract the instance number
basename := Copy(AName, 1, i);
while i < l do begin
inc(i);
instno := instno * 10 + ord(AName[i])-ord('0');
end;
if instno = 0 then
instno := 1; //default instance number for forms
//lookup existing instance
instname := basename + IntToStr(instno);
if fo.FindComponent(instname) <> nil then
exit;
//create new instance
basename := 'T' + basename;
fc := TFormClass(GetClass(basename)); //must be registered class name!
if not assigned(fc) then
exit;
Result := fc.Create(fo);
if Result.Name <> AName then
Result.Name := AName; //???
end;
end;
function TDockMaster.WrapDockable(Client: TControl): TFloatingSite;
var
Site: TFloatingSite absolute Result;
begin
{$IFDEF ownSites}
Site := TFloatingSite.Create(Self); //the new site
{$ELSE}
Site := TFloatingSite.Create(Application); //the new site
{$ENDIF}
LastSite := Site;
Site.BoundsRect := Client.BoundsRect; //the new position and extension
Client.Align := alClient;
Client.Visible := True; //otherwise docking may be rejected
Client.ManualDock(Site);
//Site.DockManager.ResetBounds(True); //does not work on first attempt?
end;
procedure TDockMaster.DockHandleMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
var
ctl: TControl; // absolute Sender;
begin
(* Handler for DockHandle.OnMouseMove.
When the left button is pressed, start dragging (for docking).
*)
if ssLeft in Shift then begin
ctl := Sender as TControl;
ctl.Parent.BeginDrag(False);
end;
end;
procedure TDockMaster.DumpSites;
const
OrientString: array[TDockOrientation] of char = (
'N','H','V' {$IFDEF FPC} ,'P' {$ENDIF}
);
AlignString: array[TAlign] of char = (
//(alNone, alTop, alBottom, alLeft, alRight, alClient, alCustom);
'n', 't', 'B', 'L', 'R', 'C', 'c'
);
function SiteName(ph: TControl): string;
begin
if ph = nil then
exit('<nil>');
Result := ph.Name;
if Result = '' then
Result := '<' + ph.ClassName + '>';
end;
procedure DumpSite(ASite: TWinControl);
var
ctl: TControl;
wc: TWinControl absolute ctl;
n, s: string;
hds: boolean;
Site: TWinControl;
j: integer;
begin
ctl := ASite;
s := Format('Site=%s (%d,%d)[%d,%d]', [SiteName(ctl),
ctl.Left, ctl.Top, ctl.Width, ctl.Height]);
while ctl <> nil do begin
hds := ctl.HostDockSite <> nil;
if hds then begin
Site := ctl.HostDockSite;
if Site <> nil then
n := ' in ' + SiteName(Site) + '@' + OrientString[ctl.DockOrientation];
end else begin
Site := ctl.Parent;
if Site <> nil then
n := ' at ' + SiteName(Site) + '@' + AlignString[ctl.Align];
end;
if Site = nil then
break;
s := s + n;
ctl := Site;
end;
DebugLn(s);
//clients
Site := ASite;
for j := 0 to Site.DockClientCount - 1 do begin
ctl := site.DockClients[j];
s := OrientString[ctl.DockOrientation];
DebugLn(' %s.Client=%s@%s (%d,%d)[%d,%d]', [SiteName(ASite), SiteName(ctl), s,
ctl.Left, ctl.Top, ctl.Width, ctl.Height]);
//if ctl is TFloatingSite then
if (ctl is TWinControl) and wc.DockSite then
DumpSite(wc);
end;
end;
var
i, j: integer;
//Site: TWinControl;
cmp: TComponent;
wc: TWinControl absolute cmp;
ctl: TControl absolute cmp;
n, s: string;
hds: boolean;
begin
(* Dump registered docking sites.
Elastic panels have no name.
Dump of docked clients by DockManager (structural info!)
Notebooks are docked, i.e. HostDockSite<>nil.
Pages are DockSites???
EditPages contain Files -> include (full?) filename
--> dump-levels
dock sites[] and clients[]
contents[]
*)
DebugLn('--- dump sites ---');
for i := 0 to ComponentCount - 1 do begin
cmp := Components[i];
{$IFnDEF old}
if (cmp is TWinControl) and wc.DockSite then
DumpSite(wc)
else if ctl is TControl then begin
DebugLn('Client=%s in %s (%d,%d)[%d,%d]', [SiteName(ctl), SiteName(ctl.HostDockSite),
ctl.Left, ctl.Top, ctl.Width, ctl.Height]);
end;
{$ELSE}
if cmp is TWinControl then begin
//path
Site := TWinControl(cmp);
if Site.DockSite then begin
//reached only when ownSites is defined!
ctl := Site;
s := Format('Site=%s (%d,%d)[%d,%d]', [SiteName(ctl),
ctl.Left, ctl.Top, ctl.Width, ctl.Height]);
while ctl <> nil do begin
hds := ctl.HostDockSite <> nil;
if hds then begin
Site := ctl.HostDockSite;
if Site <> nil then
n := ' in ' + SiteName(Site) + '@' + OrientString[ctl.DockOrientation];
end else begin
Site := ctl.Parent;
if Site <> nil then
n := ' at ' + SiteName(Site) + '@' + AlignString[ctl.Align];
end;
if Site = nil then
break;
s := s + n;
ctl := Site;
end;
DebugLn(s);
//clients
Site := TWinControl(cmp);
for j := 0 to site.DockClientCount - 1 do begin
ctl := site.DockClients[j];
s := OrientString[ctl.DockOrientation];
DebugLn(' Client=%s@%s (%d,%d)[%d,%d]', [SiteName(ctl), s,
ctl.Left, ctl.Top, ctl.Width, ctl.Height]);
//if ctl is TFloatingSite then
end;
end else begin
ctl := Site;
DebugLn('Client=%s in %s (%d,%d)[%d,%d]', [SiteName(ctl), SiteName(ctl.HostDockSite),
ctl.Left, ctl.Top, ctl.Width, ctl.Height]);
end;
end;
{$ENDIF}
end;
DebugLn('--- end dump ---');
end;
{ TDockPanel }
procedure TDockPanel.pnlDockDrop(Sender: TObject; Source: TDragDockObject;
X, Y: Integer);
var
w: integer;
r: TRect;
begin
(* Adjust docksite extent, if required.
H/V depending on align LR/TB.
Take 1/3 of the form's extent for the dock site.
When changed, ensure that the form layout is updated.
*)
if (DockClientCount > 1)
or ((Width > 1) and (Height > 1)) //NoteBook!
then
exit; //no adjustments of the dock site required
//this is the first drop - handle AutoExpand
with Source do begin
if Align in [alLeft, alRight] then begin
w := Parent.Width div 3;
if Width < w then begin
//enlarge docksite
Parent.DisableAlign; //form(?)
Width := w;
if Align = alRight then begin
if AutoExpand then begin
r := Parent.BoundsRect;
inc(r.Right, w);
Parent.BoundsRect := r;
end else begin
Left := Left-w;
Splitter.Left := Splitter.Left-w;
end;
end else if AutoExpand then begin
//enlarge left
r := Parent.BoundsRect;
dec(r.Left, w);
Parent.BoundsRect := r;
end;
Parent.EnableAlign;
end;
end else begin //alBottom
w := Parent.Height div 3;
if Height < w then begin
//enlarge docksite
Parent.DisableAlign; //form(?)
Height := w;
if Align = alBottom then begin
if AutoExpand then begin
r := Parent.BoundsRect;
inc(r.Bottom, w);
Parent.BoundsRect := r;
end else begin
Splitter.Top := Splitter.Top-w;
Top := Top-w;
end;
end;
Parent.EnableAlign;
end;
end;
end;
end;
procedure TDockPanel.pnlDockOver(Sender: TObject; Source: TDragDockObject;
X, Y: Integer; State: TDragState; var Accept: Boolean);
var
r: TRect;
procedure Adjust(dw, dh: integer);
begin
(* r.TopLeft in screen coords, r.BottomRight is W/H(?)
negative values mean expansion towards screen origin
*)
if dw <> 0 then begin
r.Right := r.Left;
inc(r.Bottom, r.Top);
if dw > 0 then
inc(r.Right, dw)
else
inc(r.Left, dw);
end else begin
r.Bottom := r.Top;
inc(r.Right, r.Left);
if dh > 0 then
inc(r.Bottom, dh)
else
inc(r.Top, dh);
end;
end;
var
dw, dh: integer;
const
d = 10; //shift mousepos with InfluenceRect
begin
(* This handler has to determine the intended DockRect,
and the alignment within this rectangle.
*)
if Source.DragTarget = nil then begin
//DragManager signals deny!
exit;
end;
if State = dsDragMove then begin
if DockClientCount > 0 then
exit; //everything should be okay
//make DockRect reflect the docking area
r := BoundsRect; //XYWH
r.TopLeft := Parent.ClientToScreen(r.TopLeft);
dw := Parent.Width div 3; //r.Right := r.Left + dw;
dh := Parent.Height div 3; //r.Bottom := r.Top + dh;
//dock inside/outside depending on mouse position
case Align of
alLeft:
begin
AutoExpand := Source.DragPos.x + d < r.Left;
if AutoExpand then Adjust(-dw, 0) else Adjust(dw, 0);
end;
alRight:
begin
AutoExpand := Source.DragPos.x + d >= r.Left;
if AutoExpand then Adjust(dw, 0) else Adjust(-dw, 0);
end;
alBottom:
begin
AutoExpand := Source.DragPos.y + d > r.Top;
if AutoExpand then Adjust(0, dh) else Adjust(0, -dh);
end
else
exit;
end;
Source.DockRect := r;
Accept := True;
end;
end;
procedure TDockPanel.pnlGetSiteInfo(Sender: TObject; DockClient: TControl;
var InfluenceRect: TRect; MousePos: TPoint; var CanDock: Boolean);
begin
(* Signal acceptance.
Inflate InfluenceRect, for easier docking into a shrinked site.
*)
CanDock := True;
InflateRect(InfluenceRect, 20, 20);
end;
procedure TDockPanel.pnlUnDock(Sender: TObject; Client: TControl;
NewTarget: TWinControl; var Allow: Boolean);
var
wh: integer;
r: TRect;
begin
(* When the last client is undocked, shrink the dock site to zero extent.
Called *before* the dock client is removed.
*)
if DockClientCount <= 1 then begin
//become empty, hide the dock site
Parent.DisableAlign;
case Align of
alLeft:
begin
wh := Width;
Width := 0; //behaves as expected
if AutoExpand then begin
r := Parent.BoundsRect;
inc(r.Left, wh);
Parent.BoundsRect := r;
end;
end;
alRight:
begin
wh := Width;
Width := 0;
if AutoExpand then begin
r := Parent.BoundsRect;
dec(r.Right, wh);
Parent.BoundsRect := r;
end else begin
Left := Left+wh;
Splitter.Left := Splitter.Left+wh;
end;
end;
alBottom:
begin
wh := Height;
Height := 0;
if AutoExpand then begin
r := Parent.BoundsRect;
dec(r.Bottom, wh);
Parent.BoundsRect := r;
Splitter.Top := Splitter.Top-wh;
end else begin
Top := Top+wh;
Splitter.Top := Top - Splitter.Height - 10;
end;
end;
end;
Parent.EnableAlign;
end;
end;
end.