{%MainUnit ../stdctrls.pp}

{******************************************************************************
                               TCustomLabel
 ******************************************************************************

 *****************************************************************************
  This file is part of the Lazarus Component Library (LCL)

  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.
 *****************************************************************************

  TODO:

    - Enable Tabbing/Focusing to focus FocusControl
    - Enable Escaped '&' Shortcut to focus FocusControl
    - Compare/Match AutoSize to Delphi/Kylix's
    - ?? Check For Full Delphi/Kylix Compatibility
    - Support of rotated multiline/wordwrapped text.

}

const
  cMaxLabelSize = 10000;

procedure TCustomLabel.CalculatePreferredSize(
  var PreferredWidth, PreferredHeight: integer; WithThemeSpace: Boolean);
// assumes: (Parent <> nil) and Parent.HandleAllocated
var
  AWidth: Integer;
  R: TRect;
  angle: Double;
begin
  if (Parent = nil) or (not Parent.HandleAllocated) then Exit;
  if WidthIsAnchored and WordWrap then
    AWidth:=Width
  else
    AWidth:=cMaxLabelSize;
  AWidth:=Constraints.MinMaxWidth(AWidth);
  CalculateSize(AWidth,PreferredWidth,PreferredHeight);

  if Font.Orientation <> 0 then
  begin
    angle := Font.Orientation / 10 * pi / 180;
    R := RotateRect(PreferredWidth, PreferredHeight, angle);
    PreferredWidth := R.Right - R.Left;
    PreferredHeight := R.Bottom - R.Top;
  end;
end;

procedure TCustomLabel.CalculateSize(MaxWidth: integer; var NeededWidth,
  NeededHeight: integer);
var
  DC, OldHandle: HDC;
  R: TRect;
  OldFont: HGDIOBJ;
  Flags: cardinal;
  LabelText: String;
begin
  LabelText := GetLabelText;
  if LabelText='' then begin
    NeededWidth:=1;
    NeededHeight:=1;
    exit;
  end;

  DC := GetDC(0);
  try
    OldHandle := Canvas.Handle;
    Canvas.Handle := DC;
    R := Rect(0, 0, MaxWidth, cMaxLabelSize);
    OldFont := SelectObject(DC, HGDIOBJ(Font.Reference.Handle));
    Flags := DT_CALCRECT or DT_EXPANDTABS;
    if WordWrap then
      Flags := Flags or DT_WORDBREAK
    else
    if not HasMultiLine then
      Flags := Flags or DT_SINGLELINE;
    if not ShowAccelChar then
      Flags := Flags or DT_NOPREFIX;
    if UseRightToLeftReading then
      Flags := Flags or DT_RTLREADING;

    DoDrawText(R, Flags);
    SelectObject(DC, OldFont);
    NeededWidth := R.Right - R.Left;
    NeededHeight := R.Bottom - R.Top;
    Canvas.Handle := OldHandle;
    //DebugLn(['TCustomLabel.CalculatePreferredSize ',DbgSName(Self),' R=',dbgs(R),' MaxWidth=',MaxWidth,' DT_WORDBREAK=',(DT_WORDBREAK and Flags)>0,' LabelText="',LabelText,'"']);
  finally
    ReleaseDC(0, DC);
  end;
end;

procedure TCustomLabel.FontChanged(Sender: TObject);
begin
  inherited FontChanged(Sender);
  UpdateSize;
end;

class function TCustomLabel.GetControlClassDefaultSize: TSize;
begin
  Result.CX := 65;
  Result.CY := 17;
end;

function TCustomLabel.HasMultiLine: boolean;
var
  s: String;
begin
  s := GetLabelText;
  Result := (pos(#10, s) > 0) or (pos(#13, s) > 0);
end;

procedure TCustomLabel.DoAutoSize;
begin
  inherited DoAutoSize;
  //debugln('TCustomLabel.DoAutoSize ',DbgSName(Self),' AutoSizing=',dbgs(AutoSizing),' AutoSize=',dbgs(AutoSize),' Parent=',DbgSName(Parent),' csLoading=',dbgs(csLoading in ComponentState),' Parnet.HandleAllocated=',dbgs((Parent<>nil) and (Parent.HandleAllocated)));
end;

procedure TCustomLabel.DoDrawText(var Rect: TRect; Flags: Longint);
var
  LabelText: string;
  OldFontColor: TColor;
  Rect2: TRect;
begin
  LabelText := GetLabelText;
  OldFontColor := Canvas.Font.Color;
  if not IsEnabled and (Flags and DT_CALCRECT = 0) then
    if ThemeServices.ThemesEnabled then
      Canvas.Font.Color := clGrayText
    else
    begin
      Canvas.Font.Color := clBtnHighlight;
      Rect2 := Rect;
      Types.OffsetRect(Rect2, 1, 1);
      DrawText(Canvas.Handle, PChar(LabelText), Length(LabelText), Rect2, Flags);
      Canvas.Font.Color := clBtnShadow;
    end;
  DrawText(Canvas.Handle, PChar(LabelText), Length(LabelText), Rect, Flags or DT_NOCLIP);
  Canvas.Font.Color := OldFontColor;
end;

procedure TCustomLabel.SetAlignment(Value : TAlignment);
begin
  //debugln('TCustomLabel.SetAlignment Old=',dbgs(ord(Alignment)),' New=',dbgs(ord(Value)),' csLoading=',dbgs(csLoading in ComponentState));
  if FAlignment <> Value then
  begin
    FAlignment := Value;
    Invalidate;
  end;
end;

procedure TCustomLabel.Notification(AComponent : TComponent; Operation : TOperation);
begin
  inherited Notification(AComponent, Operation);
  if (AComponent = FFocusControl) and (Operation = opRemove) then
    FFocusControl := nil;
end;

procedure TCustomLabel.SetFocusControl(Value : TWinControl);
begin
  if Value <> FFocusControl then
  begin
    if FFocusControl <> nil then
      FFocusControl.RemoveFreeNotification(Self);
    FFocusControl:= Value;
    if Value <> nil then
      Value.FreeNotification(Self);
  end;
end;

procedure TCustomLabel.WMActivate(var Message: TLMActivate);
begin
  if (FFocusControl <> nil) and (FFocusControl.CanFocus) then
    FFocusControl.SetFocus;
end;

function TCustomLabel.GetLabelText: string;
begin
  Result := Caption;
end;

procedure TCustomLabel.SetShowAccelChar(Value : Boolean);
begin
  if FShowAccelChar <> Value then
  begin
    FShowAccelChar := Value;
    Invalidate;
    UpdateSize;
  end;
end;

procedure TCustomLabel.TextChanged;
begin
  Invalidate;
  UpdateSize;
  AccessibleValue := Caption;
end;

procedure TCustomLabel.DoSetBounds(ALeft, ATop, AWidth, AHeight: integer);
var
  WidthChanged: Boolean;
begin
  WidthChanged:=AWidth<>Width;
  inherited DoSetBounds(ALeft, ATop, AWidth, AHeight);
  if OptimalFill and (not AutoSize) then
    AdjustFontForOptimalFill;
  if WidthChanged and WordWrap then begin
    InvalidatePreferredSize;
    AdjustSize;
  end;
end;

function TCustomLabel.CanTab: boolean;
begin
  Result := False;
end;

constructor TCustomLabel.Create(TheOwner: TComponent);
begin
  inherited Create(TheOwner);
  ControlStyle := [csCaptureMouse, csSetCaption, csClickEvents, csDoubleClicks, csReplicatable];
  with GetControlClassDefaultSize do
    SetInitialBounds(0, 0, CX, CY);
  FShowAccelChar := True;
  FInternalSetBounds := False;
  AutoSize := True;
  // Accessibility
  AccessibleRole := larLabel;
end;

function TCustomLabel.GetTransparent: boolean;
begin
  Result := not(csOpaque in ControlStyle);
end;

{------------------------------------------------------------------------------
  Method: TCustomLabel.SetLayout
  Params: None
  Returns: Nothing

 ------------------------------------------------------------------------------}
procedure TCustomLabel.SetLayout(Value: TTextLayout);
begin
  if FLayout <> Value then
  begin
    FLayout:= Value;
    Invalidate;
  end;
end;

procedure TCustomLabel.SetTransparent(NewTransparent: boolean);
begin
  if Transparent = NewTransparent then
    exit;
  if NewTransparent then
    ControlStyle := ControlStyle - [csOpaque]
  else
    ControlStyle := ControlStyle + [csOpaque];
  Invalidate;
end;

{------------------------------------------------------------------------------
  Method: TCustomLabel.SetWordWrap
  Params: None
  Returns: Nothing

 ------------------------------------------------------------------------------}
procedure TCustomLabel.SetWordWrap(Value: Boolean);
begin
  if FWordWrap <> Value then
  begin
    FWordWrap := Value;
    Invalidate;
    UpdateSize;
  end;
end;

function TCustomLabel.DialogChar(var Message: TLMKey): boolean;
begin
  Result := False;
  if not FShowAccelChar then exit;
  if FFocusControl = nil then exit;
  if KeyDataToShiftState(Message.KeyData) * [ssCtrl, ssAlt, ssShift] <> [ssAlt] then exit;

  if IsAccel(Message.CharCode, GetLabelText) and FFocusControl.CanFocus then
  begin
    Result := True;
    FFocusControl.SetFocus;
  end else
    Result := inherited DialogChar(Message);
end;

procedure TCustomLabel.Loaded;
begin
  inherited Loaded;
  AdjustSize;
end;

procedure TCustomLabel.UpdateSize;
begin
  InvalidatePreferredSize;
  if OptimalFill and (not AutoSize) then
    AdjustFontForOptimalFill;
  AdjustSize;
end;

{-------------------------------------------------------------------------------
  function TCustomLabel.CalcFittingFontHeight(const TheText: string;
    MaxWidth, MaxHeight: Integer;
    var FontSize, NeededWidth, NeededHeight: integer): Boolean;

  Calculates the maximum font size for TheText to fit into MaxWidth and
  MaxHeight.
-------------------------------------------------------------------------------}
function TCustomLabel.CalcFittingFontHeight(const TheText: string;
  MaxWidth, MaxHeight: Integer;
  out FontHeight, NeededWidth, NeededHeight: Integer): Boolean;
var
  R: TRect;
  DC: HDC;
  DrawFlags: UINT;
  OldFont: HGDIOBJ;
  MinFontHeight: Integer;
  MaxFontHeight: Integer;
  TestFont: TFont;
  CurFontHeight: Integer;
begin
  Result := False;
  FontHeight := 0;
  if AutoSizeDelayed or (TheText = '') or (MaxWidth < 1) or (MaxHeight < 1) then
    Exit;
  TestFont := TFont.Create;
  try
    TestFont.Assign(Font);
    MinFontHeight := 4;
    MaxFontHeight := MaxHeight * 2;
    CurFontHeight := (MinFontHeight + MaxFontHeight) div 2;
    DrawFlags := DT_CALCRECT or DT_NOPREFIX or DT_EXPANDTABS;
    if WordWrap then
      DrawFlags := DrawFlags or DT_WORDBREAK;
    R.Left := 0;
    R.Top := 0;
    DC := GetDC(Parent.Handle);
    try
      while (MinFontHeight <= MaxFontHeight) and
        (CurFontHeight >= MinFontHeight) and
        (CurFontHeight <= MaxFontHeight) do
      begin
        TestFont.Height := CurFontHeight; // NOTE: some TFont do not allow any integer
        //debugln('TCustomLabel.CalcFittingFontHeight A ',dbgs(MinFontHeight),'<=',dbgs(AFont.Height),'<=',dbgs(MaxFontHeight));
        OldFont := SelectObject(DC, HGDIOBJ(TestFont.Reference.Handle));
        R.Right := MaxWidth;
        R.Bottom := MaxHeight;
        DrawText(DC, PChar(TheText), Length(TheText), R, DrawFlags);
        SelectObject(DC, OldFont);
        NeededWidth := R.Right - R.Left;
        NeededHeight := R.Bottom - R.Top;
        //debugln('TCustomLabel.CalcFittingFontHeight B NeededWidth=',dbgs(NeededWidth),' NeededHeight=',dbgs(NeededHeight),' MaxWidth=',dbgs(MaxWidth),' MaxHeight=',dbgs(MaxHeight));
        if (NeededWidth > 0) and (NeededWidth <= MaxWidth) and (NeededHeight > 0) and (NeededHeight <= MaxHeight) then
        begin
          // TheText fits into the bounds
          if (not Result) or (FontHeight < TestFont.Height) then
            FontHeight := TestFont.Height;
          Result := True;
          MinFontHeight := CurFontHeight;
          // -> try bigger (binary search)
          CurFontHeight := (MaxFontHeight + CurFontHeight +1 ) div 2; // +1 to round up
          if CurFontHeight = MinFontHeight then
            Break;
        end
        else
        begin
          // TheText does not fit into the bounds
          MaxFontHeight := CurFontHeight - 1;
          // -> try smaller (binary search)
          CurFontHeight := (MinFontHeight + CurFontHeight) div 2;
        end;
      end;
    finally
      ReleaseDC(Parent.Handle, DC);
    end;
  finally
    TestFont.Free;
  end;
end;

{-------------------------------------------------------------------------------
  function TCustomLabel.AdjustFontForOptimalFill: Boolean;
  
  Maximizes Font.Height
  Return true if Font.Height changed.
-------------------------------------------------------------------------------}
function TCustomLabel.AdjustFontForOptimalFill: Boolean;
var
  NeededWidth: Integer;
  NeededHeight: Integer;
  NewFontHeight: Integer;
  OldFontHeight: LongInt;
begin
  Result := False;
  if not CalcFittingFontHeight(GetLabelText, Width, Height, NewFontHeight,
                               NeededWidth, NeededHeight) then Exit;
  if Font.Height = NewFontHeight then Exit;
  //debugln('TCustomLabel.AdjustFontForOptimalFill OldFontHeight=',dbgs(Font.Height),' NewFontHeight=',dbgs(NewFontHeight));
  OldFontHeight := Font.Height;
  Font.Height := NewFontHeight;
  Result := OldFontHeight <> Font.Height;
end;

procedure TCustomLabel.Paint;
var
  R, CalcRect: TRect;
  TextLeft, TextTop: integer;
  Flags: Longint;
  angle: Double;
const
  cAlignment: array[TAlignment] of Longint = (DT_LEFT, DT_RIGHT, DT_CENTER);
begin
  R := Rect(0,0,Width,Height);
  Canvas.Brush.Color := Color;
  if not Transparent then
  begin
    Canvas.Brush.Style:=bsSolid;
    Canvas.FillRect(R);
  end;
  Canvas.Brush.Style:=bsClear;
  Canvas.Font := Font;

  Flags := DT_EXPANDTABS;
  if WordWrap then
    Flags := Flags or DT_WORDBREAK
  else
  if not HasMultiLine then
    Flags := Flags or DT_SINGLELINE;
  if not ShowAccelChar then
    Flags := Flags or DT_NOPREFIX;
  if UseRightToLeftReading then
    Flags := Flags or DT_RTLREADING;

  CalcRect := R;
  if Font.Orientation = 0 then
  begin
    Flags := Flags or cAlignment[BidiFlipAlignment(Self.Alignment, UseRightToLeftAlignment)];
    DoDrawText(CalcRect, Flags or DT_CALCRECT);
    if FLayout<>tlTop then
    begin
      case FLayout of
        tlTop: ; // nothing
        tlCenter: Types.OffsetRect(R, 0, (R.Height-CalcRect.Height) div 2);
        tlBottom: Types.OffsetRect(R, 0, R.Height-CalcRect.Height)
      end;
      R.Height := CalcRect.Height;
    end;
  end else
  begin
    // Rotated text. Must be drawn as taLeftJustify/tlTop. No wordbreak and multiline ATM.
    Flags := Flags or DT_SINGLELINE and not DT_WORDBREAK;
    DoDrawText(CalcRect, Flags or DT_CALCRECT);
    angle := Font.Orientation * 0.1 * pi/180;
    CalcRect := RotateRect(CalcRect.Width, CalcRect.Height, angle);
    R := CalcRect;
    case FAlignment of
      taLeftJustify: Types.OffsetRect(R, -CalcRect.Left, 0);
      taCenter: Types.OffsetRect(R, (Width - CalcRect.Width) div 2 - CalcRect.Left, 0);
      taRightJustify: Types.OffsetRect(r, Width - CalcRect.Right, 0);
    end;
    case FLayout of
      tlTop: Types.OffsetRect(R, 0, -CalcRect.Top);
      tlCenter: Types.OffsetRect(R, 0, (Height - CalcRect.Height) div 2 - CalcRect.Top);
      tlBottom: Types.OffsetRect(R, 0, Height - CalcRect.Bottom);
    end;
    Types.OffsetRect(R, -CalcRect.Left-1, -CalcRect.Top-1);
  end;
  //debugln('TCustomLabel.Paint ',dbgs(Alignment=tacenter),' ',dbgs(Layout=tlCenter),' ',dbgs(TextLeft),' TextTop=',dbgs(TextTop),' ',dbgs(R));
  DoDrawText(R, Flags);
end;

procedure TCustomLabel.SetBounds(aLeft, aTop, aWidth, aHeight: integer);
begin
  if (Left=aLeft) and (Top=aTop) and (Width=aWidth) and (Height=aHeight) then exit;
  if not FInternalSetBounds and AutoSize and WordWrap then
    InvalidatePreferredSize;
  inherited SetBounds(aLeft, aTop, aWidth, aHeight);
end;

procedure TCustomLabel.SetOptimalFill(const AValue: Boolean);
begin
  if FOptimalFill = AValue then Exit;
  FOptimalFill := AValue;
  if OptimalFill and AutoSize then
    AutoSize := False;
  if OptimalFill then
    AdjustFontForOptimalFill;
  Invalidate;
end;

class procedure TCustomLabel.WSRegisterClass;
begin
  inherited WSRegisterClass;
  RegisterCustomLabel;
  RegisterPropertyToSkip(TCustomLabel, 'EllipsisPosition', 'VCL compatibility property', '');
end;

 // included by stdctrls.pp


