{ ***************************************************************************** See the file COPYING.modifiedLGPL.txt, included in this distribution, for details about the license. ***************************************************************************** Author: Michael W. Vogel } unit DockedAnchorControl; {$mode objfpc}{$H+} {$modeswitch advancedrecords} {$modeswitch typehelpers} interface uses // RTL, FCL Classes, SysUtils, fgl, FPCanvas, // LCL Controls, ExtCtrls, Graphics, LCLProc, Dialogs, PairSplitter, ComCtrls, LCLType, IDEDialogs, // DockedFormEditor DockedOptionsIDE, DockedStrConsts, DockedTools; type { TAnchorsHelper } TAnchorsHelper = type helper for TAnchors function ToString: String; end; const AnchorSideReferenceStr: array[TAnchorSideReference] of String = ('Top', 'Bottom', 'Center'); type TAttachDetach = (adAttachControl, adDetachControl, adAttachPoint, adDetachPoint, adAttachSide, adDetachSide); const AttachDetachStr: array[TAttachDetach] of String = (SAttachControl, SDetachControl, SAttachPoint, SDetachPoint, SAttachSide, SDetachSide); type TAnchorControlState = (acsNone, acsInvalid, acsSelected, acsUpdating); TAnchorControlStates = set of TAnchorControlState; TAnchorState = (asMouseDown, asMoving, asAnchorLeft, asAnchorTop, asAnchorRight, asAnchorBottom, asBordering); TAnchorStates = set of TAnchorState; { TAnchorStatesHelper } TAnchorStatesHelper = type helper for TAnchorStates function IsAnchoring: Boolean; function IsAnchoringBottom: Boolean; function IsAnchoringHorz: Boolean; function IsAnchoringLeft: Boolean; function IsAnchoringRight: Boolean; function IsAnchoringTop: Boolean; function IsAnchoringVert: Boolean; function IsBordering: Boolean; function IsMouseDown: Boolean; function IsMoving: Boolean; end; { TTargetAnchorSide } TTargetAnchorSide = record Kind: TAnchorKind; Side: TAnchorSideReference; class operator = (Item1, Item2: TTargetAnchorSide): Boolean; end; { TTargetAnchorSides } TTargetAnchorSides = class(specialize TFPGList) public procedure Add(AKind: TAnchorKind; ASide: TAnchorSideReference); overload; end; { TAnchorControl } TAnchorControl = class(TPanel) private FIsChildControl: Boolean; FRootControl: TControl; FState: TAnchorControlStates; FTargetAnchorSides: TTargetAnchorSides; procedure AnchorControlShowHint(Sender: TObject; HintInfo: PHintInfo); function BorderSpacingStr: String; function BoundsString: String; procedure CheckIsChildControl; procedure CreateCaption; public constructor Create(AParent: TWinControl; ARootControl: TControl); reintroduce; destructor Destroy; override; function AnchorSideStr(AKind: TAnchorKind): String; function AnchorsString: String; function AnchorValid(AKind: TAnchorKind; AControl: TControl; ASide: TAnchorSideReference): Boolean; function AnchorsValid: Boolean; procedure AssignAnchor(ASource: TAnchorControl; AKind: TAnchorKind); procedure AssignAnchors(ASource: TAnchorControl); procedure AssignBounds(ASource: TAnchorControl); procedure AssignFull(ASource: TAnchorControl); procedure AssignToRoot_Anchor(AKind: TAnchorKind); procedure AssignToRoot_Anchors; procedure AssignToRoot_Bounds; procedure AssignToRoot_ControlsBounds(SelfBoundsToRoot: Boolean); procedure AssignToRoot_Full; procedure Invalid; function IsInvalid: Boolean; procedure Paint; override; procedure RemoveAnchorSide(AAnchorKind: TAnchorKind); procedure RemoveAnchorSides; procedure SetNewParent(AParent: TWinControl); procedure TargetAnchorSidesGet(AAnchorControl: TAnchorControl); procedure Validate; public property IsChildControl: Boolean read FIsChildControl; property RootControl: TControl read FRootControl; property State: TAnchorControlStates read FState write FState; end; { TAnchorControls } TAnchorControls = class(specialize TFPGObjectList) // on Index[0], there is FBackGround public destructor Destroy; override; procedure BeginUpdate; procedure EndUpdate; procedure CheckAnchors; procedure CheckParents; procedure CheckProperties; procedure DeleteIndexAndAbove(AIndex: Integer); function IndexOf(AControl: TControl): Integer; overload; procedure Invalid; function RemoveInvalid: Boolean; end; { TBorderControl } TBorderControl = class(TPanel) FBitmap: TBitmap; public constructor Create(TheOwner: TComponent); override; destructor Destroy; override; procedure Paint; override; end; implementation { TAnchorsHelper } function TAnchorsHelper.ToString: String; begin Result := EmptyStr; if akLeft in Self then Result := EnumerationString(Result, STabPositionLeft); if akTop in Self then Result := EnumerationString(Result, STabPositionTop); if akRight in Self then Result := EnumerationString(Result, STabPositionRight); if akBottom in Self then Result := EnumerationString(Result, STabPositionBottom); Result := '[' + Result + ']'; end; { TAnchorStatesHelper } function TAnchorStatesHelper.IsAnchoring: Boolean; begin Result := Self * [asAnchorLeft, asAnchorRight, asAnchorTop, asAnchorBottom] <> []; end; function TAnchorStatesHelper.IsAnchoringBottom: Boolean; begin Result := asAnchorBottom in Self; end; function TAnchorStatesHelper.IsAnchoringHorz: Boolean; begin Result := Self * [asAnchorLeft, asAnchorRight] <> []; end; function TAnchorStatesHelper.IsAnchoringLeft: Boolean; begin Result := asAnchorLeft in Self; end; function TAnchorStatesHelper.IsAnchoringRight: Boolean; begin Result := asAnchorRight in Self; end; function TAnchorStatesHelper.IsAnchoringTop: Boolean; begin Result := asAnchorTop in Self; end; function TAnchorStatesHelper.IsAnchoringVert: Boolean; begin Result := Self * [asAnchorTop, asAnchorBottom] <> []; end; function TAnchorStatesHelper.IsBordering: Boolean; begin Result := asBordering in Self; end; function TAnchorStatesHelper.IsMouseDown: Boolean; begin Result := asMouseDown in Self; end; function TAnchorStatesHelper.IsMoving: Boolean; begin Result := asMoving in Self; end; { TTargetAnchorSide } class operator TTargetAnchorSide. = (Item1, Item2: TTargetAnchorSide): Boolean; begin Result := (Item1.Kind = Item2.Kind) and (Item1.Side = Item2.Side); end; { TTargetAnchorSides } procedure TTargetAnchorSides.Add(AKind: TAnchorKind; ASide: TAnchorSideReference); var LTargetAnchorSide: TTargetAnchorSide; begin LTargetAnchorSide.Kind := AKind; LTargetAnchorSide.Side := ASide; Add(LTargetAnchorSide); end; { TAnchorControl } procedure TAnchorControl.AnchorControlShowHint(Sender: TObject; HintInfo: PHintInfo); begin HintInfo^.HintStr := 'Name [' + DbgSName(RootControl) + ']' + LineEnding + 'Bounds [' + BoundsString + ']' + LineEnding + 'Align [' + DbgS(Align) + ']' + LineEnding + AnchorsString + 'Borderspacing [' + BorderSpacingStr + ']'; HintInfo^.HideTimeout := 5000; end; function TAnchorControl.BorderSpacingStr: String; begin Result := 'Around: ' + BorderSpacing.Around.ToString + ', Left: ' + BorderSpacing.Left.ToString + ', Top: ' + BorderSpacing.Top.ToString + ', Right: ' + BorderSpacing.Right.ToString + ', Bottom: ' + BorderSpacing.Bottom.ToString; end; function TAnchorControl.BoundsString: String; begin Result := 'Left: ' + Left.ToString + ', Top: ' + Top.ToString + ', Width: ' + Width.ToString + ', Height: ' + Height.ToString; end; procedure TAnchorControl.CheckIsChildControl; begin FIsChildControl := (FRootControl is TPairSplitterSide) or ((FRootControl is TSplitter) and (FRootControl.Parent is TPairSplitter)) or (FRootControl is TToolButton) or (FRootControl is TPage) or (FRootControl is TTabSheet); end; procedure TAnchorControl.CreateCaption; begin if not FRootControl.Name.IsEmpty then Caption := FRootControl.Name else Caption := FRootControl.ClassName; end; constructor TAnchorControl.Create(AParent: TWinControl; ARootControl: TControl); begin inherited Create(nil); FTargetAnchorSides := TTargetAnchorSides.Create; FRootControl := ARootControl; CreateCaption; CheckIsChildControl; SetBounds(ARootControl.Left, ARootControl.Top, ARootControl.Width, ARootControl.Height); BorderSpacing := ARootControl.BorderSpacing; Align := ARootControl.Align; Parent := AParent; Color := DockedOptions.AnchorControlColor; ParentColor := False; FState := [acsNone]; ParentShowHint := False; ShowHint := True; OnShowHint := @AnchorControlShowHint; end; destructor TAnchorControl.Destroy; begin FTargetAnchorSides.Free; inherited Destroy; end; function TAnchorControl.AnchorSideStr(AKind: TAnchorKind): String; begin case AKind of akLeft: Result := 'Anchor Left ['; akTop: Result := 'Anchor Top ['; akRight: Result := 'Anchor Right ['; akBottom: Result := 'Anchor Bottom ['; end; if not Assigned(AnchorSide[AKind].Control) or not (AnchorSide[AKind].Control is TAnchorControl) then Result := Result + 'Control: nil]' else Result := Result + 'Control: ' + TAnchorControl(AnchorSide[AKind].Control).Caption + ', Side: ' + AnchorSideReferenceStr[AnchorSide[AKind].Side] + ']'; end; function TAnchorControl.AnchorsString: String; var LKind: TAnchorKind; begin Result := ''; for LKind := akTop to akBottom do if LKind in Anchors then Result := LinedString(Result, AnchorSideStr(LKind)); if not Result.IsEmpty then Result := Result + LineEnding; end; function TAnchorControl.AnchorValid(AKind: TAnchorKind; AControl: TControl; ASide: TAnchorSideReference): Boolean; var LReferenceControl: TControl; LReferenceSide: TAnchorSideReference; LPosition: Integer; begin Result := AnchorSide[AKind].CheckSidePosition(AControl, ASide, LReferenceControl, LReferenceSide, LPosition); end; function TAnchorControl.AnchorsValid: Boolean; var LKind: TAnchorKind; begin for LKind := akTop to akBottom do begin if not (LKind in Anchors) then Continue; if not AnchorValid(LKind, AnchorSide[LKind].Control, AnchorSide[LKind].Side) then if IDEMessageDialog( SWarningCaption, SCircularDependency, mtWarning, [mbIgnore, mbCancel]) = mrIgnore then Exit(True) else Exit(False); end; Result := True; end; procedure TAnchorControl.AssignAnchor(ASource: TAnchorControl; AKind: TAnchorKind); begin AnchorSide[AKind].Control := ASource.AnchorSide[AKind].Control; AnchorSide[AKind].Side := ASource.AnchorSide[AKind].Side; if AKind in ASource.Anchors then Anchors := Anchors + [AKind] else Anchors := Anchors - [AKind]; if not DockedOptions.TreatAlign then Exit; case ASource.Align of alNone, alCustom: Exit; alTop: if AKind = akBottom then Exit; alLeft: if AKind = akRight then Exit; alRight: if AKind = akLeft then Exit; alBottom: if AKind = akTop then Exit; end; AnchorSide[AKind].Control := ASource.Parent; case AKind of akTop: AnchorSide[AKind].Side := asrTop; akLeft: AnchorSide[AKind].Side := asrTop; akRight: AnchorSide[AKind].Side := asrBottom; akBottom: AnchorSide[AKind].Side := asrBottom; end; end; procedure TAnchorControl.AssignAnchors(ASource: TAnchorControl); var LKind: TAnchorKind; begin for LKind := akTop to akBottom do AssignAnchor(ASource, LKind); Anchors := ASource.Anchors; end; procedure TAnchorControl.AssignBounds(ASource: TAnchorControl); var LRect: TRect; begin LRect := ASource.BoundsRect; SetBounds(LRect.Left, LRect.Top, LRect.Width, LRect.Height); BorderSpacing := ASource.BorderSpacing; end; procedure TAnchorControl.AssignFull(ASource: TAnchorControl); begin Align := ASource.Align; AssignAnchors(ASource); AssignBounds(ASource); end; procedure TAnchorControl.AssignToRoot_Anchor(AKind: TAnchorKind); var LAnchorSideControl: TControl; begin LAnchorSideControl := AnchorSide[AKind].Control; if LAnchorSideControl is TAnchorControl then RootControl.AnchorSide[AKind].Control := TAnchorControl(LAnchorSideControl).RootControl else RootControl.AnchorSide[AKind].Control := nil; RootControl.AnchorSide[AKind].Side := AnchorSide[AKind].Side; end; procedure TAnchorControl.AssignToRoot_Anchors; var LKind: TAnchorKind; begin if DockedOptions.TreatAlign then RootControl.Align := alNone; for LKind := akTop to akBottom do AssignToRoot_Anchor(LKind); RootControl.Anchors := Anchors; end; procedure TAnchorControl.AssignToRoot_Bounds; var LRect: TRect; begin LRect := BoundsRect; RootControl.SetBounds(LRect.Left, LRect.Top, LRect.Width, LRect.Height); RootControl.BorderSpacing := BorderSpacing; end; procedure TAnchorControl.AssignToRoot_ControlsBounds(SelfBoundsToRoot: Boolean); var i: Integer; begin if SelfBoundsToRoot then AssignToRoot_Bounds; for i := 0 to ControlCount - 1 do if Controls[i] is TAnchorControl then TAnchorControl(Controls[i]).AssignToRoot_ControlsBounds(True); end; procedure TAnchorControl.AssignToRoot_Full; begin AssignToRoot_Anchors; AssignToRoot_Bounds; end; procedure TAnchorControl.Invalid; begin FState := [acsInvalid]; end; function TAnchorControl.IsInvalid: Boolean; begin Result := acsInvalid in State; end; procedure TAnchorControl.Paint; var LKind: TAnchorKind; LColor: TColor; LTargetAnchorSide: TTargetAnchorSide; begin inherited Paint; if acsInvalid in State then Exit; if acsUpdating in State then Exit; for LTargetAnchorSide in FTargetAnchorSides do begin case LTargetAnchorSide.Kind of akBottom: Canvas.Pen.Color := DockedOptions.AnchorBottomColor; akLeft: Canvas.Pen.Color := DockedOptions.AnchorLeftColor; akRight: Canvas.Pen.Color := DockedOptions.AnchorRightColor; akTop: Canvas.Pen.Color := DockedOptions.AnchorTopColor; end; if LTargetAnchorSide.Kind in [akLeft, akRight] then case LTargetAnchorSide.Side of asrLeft: Canvas.Line(0, 0, 0, Height - 1); asrCenter: Canvas.Line((Width - 1) div 2, 0, (Width - 1) div 2, Height - 1); asrRight: Canvas.Line(Width - 1, 0, Width - 1, Height - 1); end else case LTargetAnchorSide.Side of asrTop: Canvas.Line(0, 0, Width - 1, 0); asrCenter: Canvas.Line(0, (Height - 1) div 2, Width - 1, (Height - 1) div 2); asrBottom: Canvas.Line(0, Height - 1, Width - 1, Height - 1); end; end; if acsSelected in State then for LKind := akTop to akBottom do begin LColor := clBlack; if (LKind in Anchors) and Assigned(AnchorSide[LKind].Control) then case LKind of akBottom: LColor := DockedOptions.AnchorBottomColor; akLeft: LColor := DockedOptions.AnchorLeftColor; akRight: LColor := DockedOptions.AnchorRightColor; akTop: LColor := DockedOptions.AnchorTopColor; end; Canvas.Pen.Color := LColor; case LKind of akBottom: Canvas.Line(0, Height - 1, Width - 1, Height - 1); akLeft: Canvas.Line(0, 0, 0, Height - 1); akRight: Canvas.Line(Width - 1, 0, Width - 1, Height - 1); akTop: Canvas.Line(0, 0, Width - 1, 0); end; end; end; procedure TAnchorControl.RemoveAnchorSide(AAnchorKind: TAnchorKind); begin AnchorSide[AAnchorKind].Control := nil; AnchorSide[AAnchorKind].Side := asrTop; Anchors := Anchors - [AAnchorKind]; end; procedure TAnchorControl.RemoveAnchorSides; var LKind: TAnchorKind; begin if DockedOptions.TreatAlign then Align := alNone; for LKind := akTop to akBottom do RemoveAnchorSide(LKind); end; procedure TAnchorControl.SetNewParent(AParent: TWinControl); begin Parent := AParent; end; procedure TAnchorControl.TargetAnchorSidesGet(AAnchorControl: TAnchorControl); var LKind: TAnchorKind; begin FTargetAnchorSides.Clear; if not Assigned(AAnchorControl) then Exit; for LKind := akTop to akBottom do if (LKind in AAnchorControl.Anchors) and (AAnchorControl.AnchorSide[LKind].Control = Self) then FTargetAnchorSides.Add(LKind, AAnchorControl.AnchorSide[LKind].Side); end; procedure TAnchorControl.Validate; begin FState := [acsNone]; end; { TAnchorControls } destructor TAnchorControls.Destroy; begin while Count > 0 do Delete(Count - 1); inherited Destroy; end; procedure TAnchorControls.BeginUpdate; var i: Integer; begin for i := 1 to Count - 1 do if Self[i].IsInvalid then Continue else Self[i].State := Self[i].State + [acsUpdating]; end; procedure TAnchorControls.EndUpdate; var i: Integer; begin for i := 1 to Count - 1 do if Self[i].IsInvalid then Continue else Self[i].State := Self[i].State - [acsUpdating]; end; procedure TAnchorControls.CheckAnchors; var i: Integer; LKind: TAnchorKind; LIndex: Integer; begin for i := 1 to Count - 1 do if Self[i].IsInvalid then Continue else begin for LKind := akTop to akBottom do begin Self[i].AnchorSide[LKind].Side := Self[i].RootControl.AnchorSide[LKind].Side; if Self[i].RootControl.AnchorSide[LKind].Control = nil then begin Self[i].AnchorSide[LKind].Control := nil; Continue; end; LIndex := IndexOf(Self[i].RootControl.AnchorSide[LKind].Control); if LIndex >= 0 then Self[i].AnchorSide[LKind].Control := Self[LIndex] else Self[i].AnchorSide[LKind].Control := nil; end; Self[i].Anchors := Self[i].RootControl.Anchors; end; end; procedure TAnchorControls.CheckParents; var LIndex, i: Integer; begin for i := 1 to Count - 1 do if Self[i].IsInvalid then Continue else begin LIndex := IndexOf(Self[i].RootControl.Parent); if LIndex >= 0 then Self[i].SetNewParent(Self[LIndex]) else // use BackGround Self[i].SetNewParent(Self[0]); end; end; procedure TAnchorControls.CheckProperties; var i, LLeft, LTop, LWidth, LHeight: Integer; begin for i := 1 to Count - 1 do if Self[i].IsInvalid or Self[i].IsChildControl then Continue else begin LLeft := Self[i].RootControl.Left; LTop := Self[i].RootControl.Top; LWidth := Self[i].RootControl.Width; LHeight := Self[i].RootControl.Height; if (LLeft <> Self[i].Left) or (LTop <> Self[i].Top) or (LWidth <> Self[i].Width) or (LHeight <> Self[i].Height) then Self[i].SetBounds(LLeft, LTop, LWidth, LHeight); Self[i].Align := Self[i].RootControl.Align; Self[i].BorderSpacing := Self[i].RootControl.BorderSpacing; Self[i].CreateCaption; end; end; procedure TAnchorControls.DeleteIndexAndAbove(AIndex: Integer); begin while Count > AIndex do Delete(Count - 1); end; function TAnchorControls.IndexOf(AControl: TControl): Integer; var i: Integer; begin Result := - 1; for i := 0 to Count - 1 do if Self[i].RootControl = AControl then Exit(i); end; procedure TAnchorControls.Invalid; var i: Integer; begin for i := 1 to Count - 1 do Self[i].Invalid; end; function TAnchorControls.RemoveInvalid: Boolean; var i: Integer; begin Result := False; for i := Count - 1 downto 1 do if Self[i].IsInvalid then begin Delete(i); Result := True; end; end; { TBorderControl } constructor TBorderControl.Create(TheOwner: TComponent); begin inherited Create(TheOwner); FBitmap := TBitmap.Create; FBitmap.SetSize(2, 2); FBitmap.Canvas.Pixels[0, 0] := DockedOptions.AnchorBorderColor; FBitmap.Canvas.Pixels[0, 1] := DockedOptions.AnchorControlColor; FBitmap.Canvas.Pixels[1, 0] := DockedOptions.AnchorControlColor; FBitmap.Canvas.Pixels[1, 1] := DockedOptions.AnchorBorderColor; BevelOuter := bvNone; end; destructor TBorderControl.Destroy; begin FBitmap.Free; inherited Destroy; end; procedure TBorderControl.Paint; begin Canvas.Brush.Style := bsImage; Canvas.Brush.Bitmap := FBitmap; Canvas.FillRect(0, 0, ClientWidth, ClientHeight); end; end.