{

  Copyright (c) 1997-2003 by CoolDev.Com.. Part of CoolControls package.

}

{$I versions.inc}
unit CoolWinDock;

interface

uses Classes, Forms, Controls, Messages, Windows, CoolCtrls;

const WMFindMagnet = WM_USER + 634;
      C_Version = 'V 1.12';

type
  // TCoolWinDock is non visual component that can be used to make forms
  // dock to each other when moving. Similar behaviour as the well-known
  // WinAmp's forms demonstrate.
  TCoolWinDock = class(TComponent)
  private
    fAboutBox: TCCAboutBox;
    fForm : TForm;
    fOrigWndProc : TWndMethod;
    fMagRange : integer;
    fOffsetX, fOffsetY : integer;
    SourceFormWidth, SourceFormHeight : integer;
    fSticky: boolean;
    fGroupIndex: integer;
    fMoving : boolean;
    fSizingEdge : Word;
    fStartPoint : TRect;
    procedure WndProc(var Message: TMessage);
    function CheckVertBorder( const RealPos, OtherBorderXPos,
                                    OtherBorderTop, OtherBorderBottom : integer;
                                    const CheckLeft : boolean;
                                    PWinRect : PRect) : boolean;
    function CheckHorzBorder( const RealPos, OtherBorderYPos,
                                    OtherBorderLeft, OtherBorderRight : integer;
                                    const CheckTop : boolean;
                                    PWinRect : PRect) : boolean;

    procedure ProcFindMagnet (var Msg : TMessage);
    procedure ProcMoveSize(var Message: TMessage);
    procedure ProcBeginMove;
    procedure SetFollow(MasterMagList: TList);
    function FormsTouch(RemForm: TForm): boolean;
    function FormToRect(frm: TForm): TRect;
    procedure MoveFollowersBy(MovX, MovY : integer);
    function GetVersion: string;
    procedure SetVersion(const Value: string);
  protected
    fMagList : TList;
    fFollowMove : boolean;      // True if Form shall follow move of master-form
  public
{$IFDEF DebugLog}
    LogStrings : TStrings;
    procedure Log(s : string);
{$ENDIF}
    constructor Create(AOwner:TComponent); override;
    destructor Destroy; override;
    property Version : string read GetVersion write SetVersion;
  published
    // Displays information about the control and package version.
    property About: TCCAboutBox read FAboutBox write FAboutBox;
    // The distance upon getting to the window will be docked to the other
    // with the TCoolWinDock component with similar GroupIndex in it.
    property MagRange : integer read fMagRange write fMagRange;
    // Whether the others windows docked to the one with the current
    // TCoolWinDock should be moved with that one when it is moved.
    property Sticky : boolean read fSticky write fSticky;
    // Use GroupIndex to determine which windows can be docked together.
    // Only those with the matched GroupIndex will dock to each other.
    property GroupIndex : integer read fGroupIndex write fGroupIndex;
  end;

implementation

{$IFDEF DebugLog}
uses Sysutils;
{$Endif}

// ######################################
// ###  Create, Free...
// ######################################

constructor TCoolWinDock.Create(AOwner: TComponent);
begin
     inherited;
     fForm:=Owner as TForm;

     // Steel WndProc from parent form
     fOrigWndProc:=fForm.WindowProc;
     fForm.WindowProc:=WndProc;

     fMagRange:=10;
     fOffsetX:=0;
     fOffsetY:=0;
     fSticky:=False;
     fFollowMove:=False;

     fMagList := TList.Create;

end;

destructor TCoolWinDock.Destroy;
begin
     // restore original WndProc
     fForm.WindowProc:=fOrigWndProc;
     fMagList.Free;

     inherited;
end;

// ######################################
// ###  Check if form should be attracted to a border
// ######################################

// check if attracted by a vertical border
function TCoolWinDock.CheckVertBorder( const RealPos, OtherBorderXPos,
                                    OtherBorderTop, OtherBorderBottom : integer;
                                    const CheckLeft : boolean;
                                    PWinRect : PRect) : boolean;
var CorrDist : integer;
begin
{$IFDEF DebugLog}
     Log('CheckVertBorder');
{$ENDIF}

     CorrDist:=RealPos - OtherBorderXPos;       // left - offsetX - OtherborderXPos
     if (abs(CorrDist) < fMagRange) and
        (OtherBorderTop <= PWinRect^.Bottom) and
        (OtherBorderBottom >= PWinRect^.Top)
     then
     begin
          result:=true;

          if fMoving then
          begin
               fOffsetX:=-CorrDist;
               PWinRect^.Left:=OtherBorderXPos;
               PWinRect^.Right:=OtherBorderXPos + SourceFormWidth;
          end
          else
          begin   // sizing
               if CheckLeft and
                  ( (fSizingEdge = WMSZ_LEFT) or
                    (fSizingEdge = WMSZ_TOPLEFT) or
                    (fSizingEdge = WMSZ_BOTTOMLEFT) )
               then
                   PWinRect^.Left:=OtherBorderXPos;

               if not CheckLeft and
                  ( (fSizingEdge = WMSZ_RIGHT) or
                    (fSizingEdge = WMSZ_TOPRIGHT) or
                    (fSizingEdge = WMSZ_BOTTOMRIGHT) )
               then
                   PWinRect^.Right:=OtherBorderXPos + SourceFormWidth;
          end;
     end
     else
         result:=False;
end;

// check if attracted by a horizontal border
function TCoolWinDock.CheckHorzBorder( const RealPos, OtherBorderYPos,
                                    OtherBorderLeft, OtherBorderRight : integer;
                                    const CheckTop : boolean;
                                    PWinRect : PRect) : boolean;
var CorrDist : integer;
begin
     CorrDist:=RealPos - OtherBorderYPos;
     if (abs(CorrDist) < fMagRange) and
        (OtherBorderLeft <= PWinRect^.Right) and
        (OtherBorderRight >= PWinRect^.Left)
     then
     begin
          result:=true;

          if fMoving then
          begin
               fOffsetY:=-CorrDist;
               PWinRect^.Top:=OtherBorderYPos;
               PWinRect^.Bottom:=OtherBorderYPos + SourceFormHeight;
          end
          else
          begin   // sizing
               if CheckTop and
                  ( (fSizingEdge = WMSZ_TOP) or
                    (fSizingEdge = WMSZ_TOPLEFT) or
                    (fSizingEdge = WMSZ_TOPRIGHT) )
               then
                   PWinRect^.Top:=OtherBorderYPos;

               if (fSizingEdge = WMSZ_BOTTOM) or
                  (fSizingEdge = WMSZ_BOTTOMLEFT) or
                  (fSizingEdge = WMSZ_BOTTOMRIGHT)
               then
                   PWinRect^.Bottom:=OtherBorderYPos + SourceFormHeight;
          end;
     end
     else
         result:=False;
end;

// ######################################
// ######################################
// ###  functions associated to certain windows-messages
// ######################################
// ######################################

// ######################################
// ###  initialize everything for the move...
procedure TCoolWinDock.ProcBeginMove;
var counter : integer;
    RemoteMagnet : TCoolWinDock;
begin
     fFollowMove:=False;
     fMagList.Clear;

     // GetAsyncKeyState und GetKeyState
     // if CTRL is NOT held down ->
     //    get list of all other forms
     if GetAsyncKeyState(VK_CONTROL) and $8000=0 then       
     for counter := Screen.FormCount - 1 downto 0 do
     begin
          RemoteMagnet:=TCoolWinDock(Screen.Forms[counter].Perform(WMFindMagnet,
                                  integer(self), 0));
          if RemoteMagnet<>nil then
          begin
               fMagList.Add(RemoteMagnet);
               RemoteMagnet.fFollowMove:=False;
          end;
     end;

     SetFollow(fMagList);
end;

// ######################################
// ###  determine forms with a magnet on them
procedure TCoolWinDock.ProcFindMagnet(var Msg: TMessage);
var RemoteMagnet : TCoolWinDock;
begin
     RemoteMagnet := TCoolWinDock(Msg.WParam);

     fStartPoint:=Rect(fForm.Left, fForm.Top,0,0);

     if (self <> RemoteMagnet) and
        (fGroupIndex = RemoteMagnet.fGroupIndex) and
        fForm.Visible then
     begin
          Msg.Result:=integer(self);
     end;
end;

// called when a window is moved
procedure TCoolWinDock.ProcMoveSize(var Message: TMessage);
var
  PNewPos : PRect;
  RealPosX, RealPosY : integer;
  counter : integer;
  RemoteMagnet : TCoolWinDock;
  FoundSnapX1, FoundSnapX2,
  FoundSnapY1, FoundSnapY2 : Boolean;
  TargetRect : TRect;        // Border of attracting frame
  MovX, MovY : integer;
  BoolCount : integer;
  R: TRect;
const
  BoolTo01 : array [False..True] of integer = (0,1);
begin
     // planned new position of window...
     PNewPos:=PRect(Message.LParam);

     if fMoving then
     begin
          // calc positions window would have without offset
          RealPosX:=PNewPos^.Left - fOffsetX;
          RealPosY:=PNewPos^.Top - fOffsetY;
     end
     else
     begin
          RealPosX:=PNewPos^.Left;
          RealPosY:=PNewPos^.Top;

          fSizingEdge:=Message.WParam;
     end;

     // calc Forms width and height
     SourceFormWidth:=PNewPos^.Right - PNewPos^.Left;
     SourceFormHeight:=PNewPos^.Bottom - PNewPos^.Top;

     SystemParametersInfo(SPI_GETWORKAREA, 0, @R, 0);
     // check if window attaches to vert. screenborder
     // RealPos, OtherBorderXPos, OtherBorderTop, OtherBorderBottom : integer; PWinRect : PRect
     FoundSnapX1 := CheckVertBorder(RealPosX, 0, R.Top, R.Bottom, True, PNewPos);
     FoundSnapX2 := (FoundSnapX1 and fMoving) or
       CheckVertBorder(RealPosX, R.Right - R.Left - SourceFormWidth, R.Top, R.Bottom, False, PNewPos);

     // check if window attaches to horiz. screenborder
     // RealPos, OtherBorderYPos, OtherBorderLeft, OtherBorderRight : integer; PWinRect : PRect
     FoundSnapY1 := CheckHorzBorder(RealPosY, 0, R.Left, R.Right, True, PNewPos);
     FoundSnapY2 := (FoundSnapY1 and fMoving) or
       CheckHorzBorder(RealPosY, R.Bottom - R.Top - SourceFormHeight, R.Left, R.Right, False, PNewPos);

     counter:=0;

     while (counter<fMagList.count) and not (FoundSnapX1 and FoundSnapX2 and FoundSnapY1 and FoundSnapY2) do
     begin
          RemoteMagnet:=fMagList.Items[counter];

          if not RemoteMagnet.fFollowMove or not fMoving then
          begin
               TargetRect:=FormToRect(RemoteMagnet.fForm);

               // Windowposition + Offset = Pos on screen

               BoolCount:=BoolTo01[FoundSnapX1] + BoolTo01[FoundSnapX2];
               if not (FoundSnapX1 and FoundSnapX2) then   // BoolCount<>2
               begin
                    FoundSnapX1:= FoundSnapX1 or CheckVertBorder(RealPosX, TargetRect.Right, TargetRect.Top, TargetRect.Bottom, True, PNewPos);
                    FoundSnapX2:= (FoundSnapX1 and fMoving) or
                                  FoundSnapX2 or CheckVertBorder(RealPosX, TargetRect.Left - SourceFormWidth, TargetRect.Top, TargetRect.Bottom, False, PNewPos);

                    // if anything new appeared....
                    if BoolTo01[FoundSnapX1] + BoolTo01[FoundSnapX2] <> BoolCount
                    then
                    begin
                         // Try to align Top to Top and Bottom to Bottom
                         FoundSnapY1:= FoundSnapY1 or CheckHorzBorder(RealPosY, TargetRect.Top, TargetRect.Left, TargetRect.Right, True, PNewPos);
                         FoundSnapY2:= (FoundSnapY1 and fMoving) or
                                       FoundSnapY2 or CheckHorzBorder(RealPosY, TargetRect.bottom - SourceFormHeight, TargetRect.Left, TargetRect.Right, False, PNewPos);
                    end;
               end;

               BoolCount:=BoolTo01[FoundSnapY1] + BoolTo01[FoundSnapY2];
               if BoolCount<2 then
               begin
                    FoundSnapY1:= FoundSnapY1 or CheckHorzBorder(RealPosY, TargetRect.Bottom, TargetRect.Left, TargetRect.Right, True, PNewPos);
                    FoundSnapY2:= (FoundSnapY1 and fMoving) or
                                  FoundSnapY2 or CheckHorzBorder(RealPosY, TargetRect.Top - SourceFormHeight, TargetRect.Left, TargetRect.Right, False, PNewPos);

                    // if anything new appeared....
                    if BoolTo01[FoundSnapY1] + BoolTo01[FoundSnapY2] <> BoolCount
                    then
                    begin
                         // Try to align Left to Left and Right to Right
                         FoundSnapX1:= FoundSnapX1 or CheckVertBorder(RealPosX, TargetRect.Left, TargetRect.Top, TargetRect.Bottom, True, PNewPos);
                         FoundSnapX2:= (FoundSnapX1 and fMoving) or
                                       FoundSnapX2 or CheckVertBorder(RealPosX, TargetRect.Right - SourceFormWidth, TargetRect.Top, TargetRect.Bottom, False, PNewPos);
                    end;
               end;
          end;

          Inc(counter);
     end;

     if fMoving then
     begin
          // no attaching Border found - restore position without offset
          if not FoundSnapX2 then
          begin
               PNewPos^.Left:=RealPosX;
               PNewPos^.Right:=RealPosX + SourceFormWidth;
               fOffsetX:=0;
          end;

          if not FoundSnapY2 then
          begin
               PNewPos^.Top:=RealPosY;
               PNewPos^.Bottom:=RealPosY + SourceFormHeight;
               fOffsetY:=0;
          end;

          MovX:=PNewPos^.Left - fStartPoint.Left;
          MovY:=PNewPos^.Top - fStartPoint.Top;
          MoveFollowersBy(MovX, MovY);
     end;
end;

// ######################################
// ###  WndProc - called whenever form is moved...
// ###     (called much more ofter, but move is OUR event)
// ######################################

procedure TCoolWinDock.WndProc(var Message: TMessage);
begin
     if not (CsDesigning in ComponentState) then
     begin
          if Message.Msg=WM_MOVING
          then
          begin
               fMoving:=True;
               ProcMoveSize(Message);
          end
          else
          if Message.Msg=WM_SIZING
          then
          begin
               fMoving:=False;
               ProcMoveSize(Message);
          end
          else
          if Message.Msg=WMFindMagnet
          then
              ProcFindMagnet(Message);
          if Message.Msg=WM_ENTERSIZEMOVE then
             ProcBeginMove;
     end;

     if Message.Msg<>WMFindMagnet then // avoid overwriting of result
        fOrigWndProc(Message);
end;

// ######################################
// ###
// ######################################

// ######################################
// ###   Determine which forms have to follow the current movement
procedure TCoolWinDock.SetFollow(MasterMagList : TList);
var counter : integer;
    RemoteMagnet : TCoolWinDock;
begin
     if fFollowMove then Exit;   // already set to true

     fFollowMove:=True;

     if fSticky then
     for counter:=0 to MasterMagList.count-1 do
     begin
          RemoteMagnet:=TCoolWinDock(MasterMagList.Items[counter]);

          if not RemoteMagnet.fFollowMove and
             FormsTouch(RemoteMagnet.fForm)
          then
              RemoteMagnet.SetFollow(MasterMagList);
     end;
end;

// returns true if 2 Forms touch
function TCoolWinDock.FormsTouch(RemForm : TForm) : boolean;
var SourceRect, DestRect : TRect;
begin
     SourceRect:=FormToRect(fForm);
     DestRect:=  FormToRect(RemForm);

     result:=False;

     if (DestRect.Left <= SourceRect.Right) and
        (DestRect.Right >= SourceRect.Left) and   // next to each other (horizontally)

        ( (SourceRect.Top = DestRect.Bottom) or  // touch bottom-top
          (SourceRect.Bottom = DestRect.Top) )
     then
         result:=True;

     if (DestRect.Top <= SourceRect.Bottom) and
        (DestRect.Bottom >= SourceRect.Top) and   // above each other

        ( (SourceRect.Left = DestRect.Right) or  // touch right-left
          (SourceRect.Right = DestRect.Left) )
     then
         result:=True;
end;

// move all following windows by a given value
procedure TCoolWinDock.MoveFollowersBy(MovX, MovY : integer);
var counter : integer;
    RemoteMagnet : TCoolWinDock;
begin
     for counter:=0 to fMagList.count-1 do
     begin
          RemoteMagnet := TCoolWinDock(fMagList.Items[counter]);
          if RemoteMagnet.fFollowMove then
          begin
               RemoteMagnet.fForm.Left:=RemoteMagnet.fStartPoint.Left + MovX;
               RemoteMagnet.fForm.Top:=RemoteMagnet.fStartPoint.Top + MovY;
          end;
     end;
end;

function TCoolWinDock.FormToRect(frm : TForm) : TRect;
begin
     result:=Rect( frm.Left, frm.Top,
                       frm.Left + frm.Width, frm.Top + frm.Height);
end;


// ###################################
// ######   Version
// ###################################

procedure TCoolWinDock.SetVersion(const Value: string);
begin
end;

function TCoolWinDock.GetVersion: string;
begin
     result:=C_Version;
end;

{$IFDEF DebugLog}
procedure TCoolWinDock.Log(s: string);
begin
     if assigned(LogStrings) then
        LogStrings.Add(s);
end;
{$ENDIF}

end.
