// SPDX-License-Identifier: LGPL-3.0-linking-exception
{******************************* CONTRIBUTOR(S) ******************************
- Edivando S. Santos Brasil | mailedivando@gmail.com
  (Compatibility with delphi VCL 11/2018)

***************************** END CONTRIBUTOR(S) *****************************}
unit BCMDButton;

{$I bgracontrols.inc}

// Set this to show number of repaint in each MDBUTTON
{ $DEFINE MDBUTTON_DEBUG}

// Set this to animate only a MDBUTTON at a time
{$DEFINE MDBUTTON_ANIMATEONLYONE}

interface

uses
  Classes, SysUtils, Types, {$IFDEF FPC}LResources,{$ELSE}BGRAGraphics, GraphType, FPImage,{$ENDIF}
  Forms, Controls, Graphics, Dialogs,
  BCBaseCtrls, BGRABitmap, BGRABitmapTypes, ExtCtrls, Math, BGRABlend;

type
  TBCMDButtonCheckMarkPosition = (cmpBottom,cmpTop,cmpLeft,cmpRight);

var
  // Default icons for Check Box
  {BCMDBUTTONBALLOTBOX: string = '☐'; // '✗'
  BCMDBUTTONBALLOTBOXWITHCHECK: string = '☑'; // '✓'

  // Default icons for Radio Button
  BCMDBUTTONRADIOBUTTON: string = '🔘';
  BCMDBUTTONRADIOBUTTONCIRCLE: string = '◯';}

  // Characters that can be used on systems that lack of the previous unicode symbols
  BCMDBUTTONBALLOTBOX: string = '[  ]';
  BCMDBUTTONBALLOTBOXWITHCHECK: string = '[X]';
  BCMDBUTTONRADIOBUTTON: string = '[O]';
  BCMDBUTTONRADIOBUTTONCIRCLE: string = '[  ]';

  // Animation speed
  // Possible values: between 0 and 1
  // 0 is an infinite animation that display nothing (only redraw itself)
  // 1 is the faster animation (like no animation, from 0 to 1 in 1 frame)
  // Recommended values: between 0.01 (slow) and 0.1 (fast), default 0.04
  // Hint: turn on debug to see how much frames are rendered
  BCMDBUTTONANIMATIONSPEED: double = 0.04;

  // Global enable/disable animations
  BCMDBUTTONANIMATION: boolean = True;

  // Global posiotn of checkmarks 0=bottom, 1=top, 2=left, 3=right
  BCMDBUTTONCHECKMARKPOSITION : TBCMDButtonCheckMarkPosition = cmpBottom;

  BCMDBUTTONCHECKMARKCOLOR : TColor = $00BB513F;

const
  // Timer speed: default 15 (a bit more than 60 fps)
  // Other values: 16 (60 fps) 20 (50 fps) 25 (40 fps) 33 (30 fps)
  // Hint: 15 is the smoothest -tested- value on Windows, even if 16 is closer to 60 fps
  //       * values below 15 are not noticeable
  //       * higher values are not smooth
  // Hint: changing this doesn't change the ammount of frames rendered,
  //       only changes the time between frames
  // Hint: if you decrease MDBUTTONTIMERSPEED, increase BCMDBUTTONANIMATIONSPEED
  //       to keep a smooth animation
  BCMDBUTTONTIMERSPEED: integer = 15;

type
  TBCMDButtonState = (mdbsNormal, mdbsHover, mdbsActive);
  TBCMDButtonKind = (mdbkNormal, mdbkToggle, mdbkToggleGroup, mdbkCheckBox,
    mdbkRadioButton, mdbkTab);

  { TBCMDButtonStyle }

  TBCMDButtonStyle = class(TPersistent)
  private
    FColor: TColor;
    FOnChange: TNotifyEvent;
    FTextColor: TColor;
    procedure SetFColor(AValue: TColor);
    procedure SetFTextColor(AValue: TColor);
  public
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
  public
    constructor Create;
  published
    property Color: TColor read FColor write SetFColor;
    property TextColor: TColor read FTextColor write SetFTextColor;
  end;

  { TCustomBCMDButton }

  TCustomBCMDButton = class(TBGRAGraphicCtrl)
  private
    FChecked: boolean;
    FKind: TBCMDButtonKind;
    {$IFDEF INDEBUG}
    FCount: integer;
    {$ENDIF}
    FRounding: integer;
    FTextAutoSize: boolean;
    FTextProportional: boolean;
    FTextProportionalRatio: single;
    FTimer: TTimer;
    FPercent: double;
    FCircleSize: double;
    FCX, FCY: integer;
    FAlphaPercent: double;
    FAlignment: TAlignment;
    FAnimation: boolean;
    FState: TBCMDButtonState;
    FStyleActive: TBCMDButtonStyle;
    FStyleDisabled: TBCMDButtonStyle;
    FStyleHover: TBCMDButtonStyle;
    FStyleNormal: TBCMDButtonStyle;
    FTextLayout: TTextLayout;
    procedure OnChangeStyle(Sender: TObject);
    procedure SetFAlignment(AValue: TAlignment);
    procedure SetFAnimation(AValue: boolean);
    procedure SetFChecked(AValue: boolean);
    procedure SetFKind(AValue: TBCMDButtonKind);
    procedure SetFStyleActive(AValue: TBCMDButtonStyle);
    procedure SetFStyleDisabled(AValue: TBCMDButtonStyle);
    procedure SetFStyleHover(AValue: TBCMDButtonStyle);
    procedure SetFStyleNormal(AValue: TBCMDButtonStyle);
    procedure SetFTextAutoSize(AValue: boolean);
    procedure SetFTextLayout(AValue: TTextLayout);
    procedure SetFTextProportional(AValue: boolean);
    procedure SetFTextProportionalRatio(AValue: single);
  protected
    procedure CalculatePreferredSize(var PreferredWidth, PreferredHeight: integer;
    {%H-}WithThemeSpace: boolean); override;
    procedure Paint; override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
      X, Y: integer); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
    procedure MouseEnter; override;
    procedure MouseLeave; override;
    procedure RealSetText(const Value: TCaption); override;
    procedure OnTimer(Sender: TObject);
    procedure OnStartTimer(Sender: TObject);
    procedure OnStopTimer(Sender: TObject);
    function easeInOutQuad(t: double): double;
    function easeOutQuad(t: double): double;
    procedure UncheckOthers;
    class function GetControlClassDefaultSize: TSize; override;
    function GetRealCaption: string;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure SelectAll;
    procedure UnselectAll;
    procedure InvertSelection;
    function GetSelected: TStringList;
  published
    property Animation: boolean read FAnimation write SetFAnimation default False;
    property Alignment: TAlignment read FAlignment write SetFAlignment default taCenter;
    property TextLayout: TTextLayout
      read FTextLayout write SetFTextLayout default tlCenter;
    property StyleNormal: TBCMDButtonStyle read FStyleNormal write SetFStyleNormal;
    property StyleHover: TBCMDButtonStyle read FStyleHover write SetFStyleHover;
    property StyleActive: TBCMDButtonStyle read FStyleActive write SetFStyleActive;
    property StyleDisabled: TBCMDButtonStyle read FStyleDisabled write SetFStyleDisabled;
    property Checked: boolean read FChecked write SetFChecked default False;
    property Kind: TBCMDButtonKind read FKind write SetFKind default mdbkNormal;
    // If text size is used to measure buttons
    // Disable it if you use the buttons in a grid, for example
    property TextAutoSize: boolean read FTextAutoSize write SetFTextAutoSize;
    // Enable it if you want that text size grows with height
    property TextProportional: boolean read FTextProportional write SetFTextProportional;
    // Each character font height proportional to height of control
    // Set it in conjunction with TextProportional, values recommended between 0...1
    property TextProportionalRatio: single read FTextProportionalRatio
      write SetFTextProportionalRatio;
  end;

  TBCMDButton = class(TCustomBCMDButton)
    property Action;
    property Align;
    property Anchors;
    property AutoSize;
    property BidiMode;
    property BorderSpacing;
    {$IFDEF FPC} //#
    property OnChangeBounds;
    {$ENDIF}
    //property Cancel;
    property Caption;
    property Color;
    property Constraints;
    //property Default;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property Font;
    property ParentBidiMode;
    //property ModalResult;
    property OnClick;
    property OnContextPopup;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDrag;
    //property OnEnter;
    //property OnExit;
    //property OnKeyDown;
    //property OnKeyPress;
    //property OnKeyUp;
    property OnMouseDown;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnMouseMove;
    property OnMouseUp;
    property OnMouseWheel;
    property OnMouseWheelDown;
    property OnMouseWheelUp;
    property OnResize;
    property OnStartDrag;
    //property OnUTF8KeyPress;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowHint;
    //property TabOrder;
    //property TabStop;
    property Visible;
  end;

{$IFDEF FPC}procedure Register;{$ENDIF}

implementation

{$IFDEF MDBUTTON_ANIMATEONLYONE}
var
  MDAnimating: TCustomBCMDButton;

{$ENDIF}

{$IFDEF FPC}
procedure Register;
begin
  RegisterComponents('BGRA Button Controls', [TBCMDButton]);
end;
{$ENDIF}

{ TBCMDButtonStyle }

procedure TBCMDButtonStyle.SetFColor(AValue: TColor);
begin
  if FColor = AValue then
    Exit;
  FColor := AValue;
  if Assigned(FOnChange) then
    OnChange(Self);
end;

procedure TBCMDButtonStyle.SetFTextColor(AValue: TColor);
begin
  if FTextColor = AValue then
    Exit;
  FTextColor := AValue;
  if Assigned(FOnChange) then
    OnChange(Self);
end;

constructor TBCMDButtonStyle.Create;
begin
  inherited Create;
  FColor := clWhite;
  FTextColor := clBlack;
end;

{ TCustomBCMDButton }

procedure TCustomBCMDButton.SetFStyleActive(AValue: TBCMDButtonStyle);
begin
  if FStyleActive = AValue then
    Exit;
  FStyleActive := AValue;
end;

procedure TCustomBCMDButton.SetFAlignment(AValue: TAlignment);
begin
  if FAlignment = AValue then
    Exit;
  FAlignment := AValue;
  Invalidate;
end;

procedure TCustomBCMDButton.SetFAnimation(AValue: boolean);
begin
  if FAnimation = AValue then
    Exit;
  FAnimation := AValue;
  Invalidate;
end;

procedure TCustomBCMDButton.SetFChecked(AValue: boolean);
begin
  if FChecked = AValue then
    Exit;
  FChecked := AValue;
  if FChecked and (FKind in [mdbkToggleGroup, mdbkRadioButton, mdbkTab]) then
    UncheckOthers;
  Invalidate;
end;

procedure TCustomBCMDButton.SetFKind(AValue: TBCMDButtonKind);
begin
  if FKind = AValue then
    Exit;
  FKind := AValue;
  Invalidate;
end;

procedure TCustomBCMDButton.OnChangeStyle(Sender: TObject);
begin
  Invalidate;
end;

procedure TCustomBCMDButton.SetFStyleDisabled(AValue: TBCMDButtonStyle);
begin
  if FStyleDisabled = AValue then
    Exit;
  FStyleDisabled := AValue;
end;

procedure TCustomBCMDButton.SetFStyleHover(AValue: TBCMDButtonStyle);
begin
  if FStyleHover = AValue then
    Exit;
  FStyleHover := AValue;
end;

procedure TCustomBCMDButton.SetFStyleNormal(AValue: TBCMDButtonStyle);
begin
  if FStyleNormal = AValue then
    Exit;
  FStyleNormal := AValue;
end;

procedure TCustomBCMDButton.SetFTextAutoSize(AValue: boolean);
begin
  if FTextAutoSize = AValue then
    Exit;
  FTextAutoSize := AValue;
end;

procedure TCustomBCMDButton.SetFTextLayout(AValue: TTextLayout);
begin
  if FTextLayout = AValue then
    Exit;
  FTextLayout := AValue;
  Invalidate;
end;

procedure TCustomBCMDButton.SetFTextProportional(AValue: boolean);
begin
  if FTextProportional=AValue then Exit;
  FTextProportional:=AValue;
  Invalidate;
end;

procedure TCustomBCMDButton.SetFTextProportionalRatio(AValue: single);
begin
  if FTextProportionalRatio=AValue then Exit;
  FTextProportionalRatio:=AValue;
  Invalidate;
end;

procedure TCustomBCMDButton.CalculatePreferredSize(
  var PreferredWidth, PreferredHeight: integer; WithThemeSpace: boolean);
var
  bmp: TBGRABitmap;
  s: TSize;
begin
  bmp := TBGRABitmap.Create;
  bmp.FontName := Font.Name;
  if FTextProportional then
    bmp.FontHeight := Round(Height * FTextProportionalRatio)
  else
    bmp.FontHeight := 0;
  bmp.FontAntialias := True;
  bmp.FontQuality := fqSystemClearType;
  bmp.FontStyle := Font.Style;
  s := bmp.TextSize(GetRealCaption);
  if FTextAutoSize then
  begin
    PreferredWidth := s.Width + 26 {$IFDEF FPC}+ BorderSpacing.InnerBorder{$ENDIF};
    PreferredHeight := s.Height + 10 {$IFDEF FPC}+ BorderSpacing.InnerBorder{$ENDIF};
  end
  else
  begin
    {$IFDEF FPC}//#
    PreferredWidth := BorderSpacing.InnerBorder;
    PreferredHeight := BorderSpacing.InnerBorder;
    {$ENDIF}
  end;
  bmp.Free;
end;

procedure TCustomBCMDButton.Paint;
var
  bmp: TBGRABitmap;
  iTemp: integer;
  alpha: byte;
  tempState: TBCMDButtonState;
  tempText: string;
  tempRounding: integer;
  tempColor, hoverColor: TBGRAPixel;
begin
  bmp := TBGRABitmap.Create(Width, Height);
  bmp.FontName := Font.Name;
  if FTextProportional then
    bmp.FontHeight := Round(Height * FTextProportionalRatio)
  else
    bmp.FontHeight := 0;
  bmp.FontAntialias := True;
  bmp.FontQuality := fqSystemClearType;
  bmp.FontStyle := Font.Style;
  tempState := FState;

  if Kind = mdbkTab then
    tempRounding := 0
  else
    tempRounding := FRounding;

  if FChecked then
    tempState := mdbsActive
  else
    tempState := FState;

  tempText := GetRealCaption;

  // Enabled
  if Enabled then
  begin
    if not FTimer.Enabled then
    begin
      case tempState of
        mdbsNormal:
        begin
          bmp.RoundRect(0, 0, Width, Height, tempRounding, tempRounding,
            FStyleNormal.Color,
            FStyleNormal.Color);
          {$IFDEF FPC}
          bmp.TextRect(Rect(BorderSpacing.InnerBorder, BorderSpacing.InnerBorder,
            Width - BorderSpacing.InnerBorder, Height - BorderSpacing.InnerBorder),
            tempText, Alignment,
            TextLayout, FStyleNormal.TextColor);
          {$ELSE}
          bmp.TextRect(Rect(0, 0,Width, Height),tempText, Alignment, TextLayout, FStyleNormal.TextColor);
          {$ENDIF}
        end;
        mdbsHover:
        begin
          bmp.RoundRect(0, 0, Width, Height, tempRounding, tempRounding,
            FStyleHover.Color, FStyleHover.Color);
          {$IFDEF FPC}
          bmp.TextRect(Rect(BorderSpacing.InnerBorder, BorderSpacing.InnerBorder,
            Width - BorderSpacing.InnerBorder, Height - BorderSpacing.InnerBorder),
            tempText, Alignment,
            TextLayout, FStyleHover.TextColor);
          {$ELSE}
          bmp.TextRect(Rect(0, 0,Width, Height),tempText, Alignment, TextLayout, FStyleHover.TextColor);
          {$ENDIF}
        end;
        mdbsActive:
        begin
          if not FAnimation then
          begin
            if FKind in [mdbkNormal] then
              bmp.RoundRect(0, 0, Width, Height, tempRounding,
                tempRounding, FStyleActive.Color,
                FStyleActive.Color)
            else
              bmp.RoundRect(0, 0, Width, Height, tempRounding,
                tempRounding, FStyleHover.Color,
                FStyleHover.Color);
          end
          else
            bmp.RoundRect(0, 0, Width, Height, tempRounding, tempRounding,
              FStyleHover.Color,
              FStyleHover.Color);
          {$IFDEF FPC}
          bmp.TextRect(Rect(BorderSpacing.InnerBorder, BorderSpacing.InnerBorder,
            Width - BorderSpacing.InnerBorder, Height - BorderSpacing.InnerBorder),
            tempText, Alignment,
            TextLayout, FStyleActive.TextColor);
          {$ELSE}
          bmp.TextRect(Rect(0, 0,Width, Height),tempText, Alignment, TextLayout, FStyleActive.TextColor);
          {$ENDIF}
        end;
      end;
    end
    else
    begin
      iTemp := round(FCircleSize * easeOutQuad(FPercent));
      alpha := round(easeInOutQuad(FAlphaPercent) * 255);
      case tempState of
        mdbsNormal:
        begin
          bmp.RoundRect(0, 0, Width, Height, tempRounding, tempRounding,
            FStyleNormal.Color,
            FStyleNormal.Color);
          if FPercent < 1 then
            tempColor := FStyleHover.Color
          else
          begin
            tempColor := FStyleNormal.Color;
            hoverColor := ColorToBGRA(FStyleHover.Color, alpha);
            PutPixels(@tempColor, @hoverColor, 1, dmDrawWithTransparency, 255);
          end;
          bmp.FillEllipseAntialias(FCX, FCY, iTemp,
            iTemp, tempColor);
          {$IFDEF FPC}
          bmp.TextRect(Rect(BorderSpacing.InnerBorder, BorderSpacing.InnerBorder,
            Width - BorderSpacing.InnerBorder, Height - BorderSpacing.InnerBorder),
            tempText, Alignment,
            TextLayout, FStyleNormal.TextColor);
          {$ELSE}
          bmp.TextRect(Rect(0, 0,Width, Height),tempText, Alignment, TextLayout, FStyleNormal.TextColor);
          {$ENDIF}
        end;
        mdbsHover, mdbsActive:
        begin
          bmp.RoundRect(0, 0, Width, Height, tempRounding, tempRounding,
            FStyleHover.Color, FStyleHover.Color);
          if FPercent < 1 then
            tempColor := FStyleActive.Color
          else
          begin
            tempColor := FStyleHover.Color;
            hoverColor := ColorToBGRA(FStyleActive.Color, alpha);
            PutPixels(@tempColor, @hoverColor, 1, dmDrawWithTransparency, 255);
          end;
          bmp.FillEllipseAntialias(FCX, FCY, iTemp,
            iTemp, tempColor);
          {$IFDEF FPC}
          bmp.TextRect(Rect(BorderSpacing.InnerBorder, BorderSpacing.InnerBorder,
            Width - BorderSpacing.InnerBorder, Height - BorderSpacing.InnerBorder),
            tempText, Alignment,
            TextLayout, FStyleHover.TextColor);
          {$ELSE}
          bmp.TextRect(Rect(0, 0,Width, Height),tempText, Alignment, TextLayout, FStyleHover.TextColor);
          {$ENDIF}
        end;
      end;
    end;
  end
  // Disabled
  else
  begin
    if FChecked then
    begin
      bmp.RoundRect(0, 0, Width, Height, tempRounding, tempRounding,
        FStyleHover.Color, FStyleHover.Color);
    end
    else
      bmp.RoundRect(0, 0, Width, Height, tempRounding, tempRounding,
        FStyleDisabled.Color, FStyleDisabled.Color);
    {$IFDEF FPC}
    bmp.TextRect(Rect(BorderSpacing.InnerBorder, BorderSpacing.InnerBorder,
      Width - BorderSpacing.InnerBorder, Height - BorderSpacing.InnerBorder),
      tempText, Alignment,
      TextLayout, FStyleDisabled.TextColor);
    {$ELSE}
    bmp.TextRect(Rect(0, 0,Width, Height),tempText, Alignment, TextLayout, FStyleDisabled.TextColor);
    {$ENDIF}
  end;

  // Tab
  if Kind = mdbkTab then
  begin
    if FTimer.Enabled then
    begin
      iTemp := round((bmp.Width div 2) * easeInOutQuad(FPercent));
      case BCMDBUTTONCHECKMARKPOSITION of
      cmpBottom : begin
             iTemp := round((bmp.Width div 2) * easeInOutQuad(FPercent));
             bmp.Rectangle((bmp.Width div 2) - iTemp, bmp.Height - 2,(bmp.Width div 2) + iTemp, bmp.Height, BCMDBUTTONCHECKMARKCOLOR, dmSet);
          end;
      cmpTop : begin
            iTemp := round((bmp.Width div 2) * easeInOutQuad(FPercent));
            bmp.Rectangle((bmp.Width div 2) - iTemp, 0,(bmp.Width div 2) + iTemp, 2, BCMDBUTTONCHECKMARKCOLOR, dmSet);
          end;
      cmpLeft : begin
            iTemp := round((bmp.Height div 2) * easeInOutQuad(FPercent));
            bmp.Rectangle(0, (bmp.Height div 2) - iTemp, 2, (bmp.Height div 2) + iTemp, BCMDBUTTONCHECKMARKCOLOR, dmSet);
          end;
      cmpRight : begin
            iTemp := round((bmp.Height div 2) * easeInOutQuad(FPercent));
            bmp.Rectangle(bmp.width-2, (bmp.Height div 2) - iTemp, bmp.width, (bmp.Height div 2) + iTemp, BCMDBUTTONCHECKMARKCOLOR, dmSet);
          end;
      end; // case
    end
    else
    begin
      if FChecked then
        case BCMDBUTTONCHECKMARKPOSITION of
        cmpBottom : bmp.Rectangle(0, bmp.Height - 2, bmp.Width, bmp.Height, BCMDBUTTONCHECKMARKCOLOR, dmSet);
        cmpTop : bmp.Rectangle(0, 0, bmp.Width, 2, BCMDBUTTONCHECKMARKCOLOR, dmSet);
        cmpLeft : bmp.Rectangle(0, 0, 2, bmp.Height, BCMDBUTTONCHECKMARKCOLOR, dmSet);
        cmpRight : bmp.Rectangle(bmp.Width - 2, 0, bmp.Width, bmp.Height, BCMDBUTTONCHECKMARKCOLOR, dmSet);
        end; // case
    end;
  end;

  {$IFDEF MDBUTTON_DEBUG}
  bmp.FontHeight := 10;
  bmp.TextOut(0, 0, FCount.ToString, BGRA(255, 0, 0, 255));
  FCount += 1;
  {$ENDIF}
  bmp.Draw(Canvas, 0, 0, False);
  bmp.Free;
  inherited Paint;
end;

procedure TCustomBCMDButton.MouseDown(Button: TMouseButton; Shift: TShiftState;
  X, Y: integer);
begin
  inherited MouseDown(Button, Shift, X, Y);
  FState := mdbsActive;
  if FAnimation and BCMDBUTTONANIMATION then
  begin
    FCircleSize := max(round(Width / 1.5) + abs((Width div 2) - X),
      round(Height / 1.5) + abs((Height div 2) - Y));
    FCX := X;
    FCY := Y;
    FTimer.Enabled := False;
    FTimer.Enabled := True;
    {$IFDEF MDBUTTON_ANIMATEONLYONE}
    MDAnimating := Self;
    {$ENDIF}
  end;
  if FKind in [mdbkToggle, mdbkToggleGroup, mdbkCheckBox, mdbkRadioButton, mdbkTab] then
  begin
    FChecked := not FChecked;
    if FKind in [mdbkToggleGroup, mdbkRadioButton, mdbkTab] then
    begin
      FChecked := True;
      UncheckOthers;
    end;
  end;
  Invalidate;
end;

procedure TCustomBCMDButton.MouseUp(Button: TMouseButton; Shift: TShiftState;
  X, Y: integer);
begin
  inherited MouseUp(Button, Shift, X, Y);
  if (x > 0) and (x < Width) and (y > 0) and (y < Height) and (FState = mdbsActive) then
    FState := mdbsHover
  else
    FState := mdbsNormal;
  Invalidate;
end;

procedure TCustomBCMDButton.MouseEnter;
begin
  inherited MouseEnter;
  FState := mdbsHover;
  Invalidate;
end;

procedure TCustomBCMDButton.MouseLeave;
begin
  inherited MouseLeave;
  FState := mdbsNormal;
  Invalidate;
end;

procedure TCustomBCMDButton.RealSetText(const Value: TCaption);
begin
  inherited RealSetText(Value);
  InvalidatePreferredSize;
  Invalidate;
end;

procedure TCustomBCMDButton.OnTimer(Sender: TObject);
begin
  {$IFDEF MDBUTTON_ANIMATEONLYONE}
  if MDAnimating = Self then
  begin
  {$ENDIF}
    FPercent := FPercent + BCMDBUTTONANIMATIONSPEED;
    if FPercent < 0 then
      FPercent := 0
    else if FPercent > 1 then
      FPercent := 1;

    if FPercent = 1 then
    begin
      FAlphaPercent := FAlphaPercent -BCMDBUTTONANIMATIONSPEED;
      if FAlphaPercent < 0 then
        FAlphaPercent := 0
      else if FAlphaPercent > 1 then
        FAlphaPercent := 1;
    end;
  {$IFDEF MDBUTTON_ANIMATEONLYONE}
  end
  else
    FTimer.Enabled := False;
  {$ENDIF}

  Invalidate;
  if (FPercent >= 1) and (FAlphaPercent <= 0) then
    FTimer.Enabled := False;
end;

procedure TCustomBCMDButton.OnStartTimer(Sender: TObject);
begin
  FPercent := 0;
  FAlphaPercent := 1;
end;

procedure TCustomBCMDButton.OnStopTimer(Sender: TObject);
begin

end;

function TCustomBCMDButton.easeInOutQuad(t: double): double;
begin
  if t < 0.5 then
    Result := 2 * t * t
  else
    Result := -1 + (4 - 2 * t) * t;
end;

function TCustomBCMDButton.easeOutQuad(t: double): double;
begin
  Result := t * (2 - t);
end;

procedure TCustomBCMDButton.UncheckOthers;
var
  i: integer;
  control: TWinControl;
begin
  if Parent is TWinControl then
  begin
    control := TWinControl(Parent);
    for i := 0 to control.ControlCount - 1 do
      if (control.Controls[i] <> Self) and (control.Controls[i] is TCustomBCMDButton) then
        if (TCustomBCMDButton(control.Controls[i]).Kind in
          [mdbkToggleGroup, mdbkRadioButton, mdbkTab]) then
          TCustomBCMDButton(control.Controls[i]).Checked := False;
  end;
end;

class function TCustomBCMDButton.GetControlClassDefaultSize: TSize;
begin
  Result.CX := 75;
  Result.CY := 25;
end;

function TCustomBCMDButton.GetRealCaption: string;
var
  tempText: string;
begin
  tempText := Caption;

  case FKind of
    mdbkCheckBox:
    begin
      if Length(Caption) > 0 then
        tempText := ' ' + Caption;
      if FChecked then
        tempText := BCMDBUTTONBALLOTBOXWITHCHECK + tempText
      else
        tempText := BCMDBUTTONBALLOTBOX + tempText;
    end;
    mdbkRadioButton:
    begin
      if Length(Caption) > 0 then
        tempText := ' ' + Caption;
      if FChecked then
        tempText := BCMDBUTTONRADIOBUTTON + tempText
      else
        tempText := BCMDBUTTONRADIOBUTTONCIRCLE + tempText;
    end;
  end;
  result := tempText;
end;

constructor TCustomBCMDButton.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  {$IFDEF INDEBUG}
  FCount := 0;
  {$ENDIF}
  // State
  FState := mdbsNormal;
  FChecked := False;
  FKind := mdbkNormal;
  // Text
  FTextAutoSize := True;
  FAlignment := taCenter;
  FTextLayout := tlCenter;
  FTextProportional := False;
  FTextProportionalRatio := 0.5;
  // Style
  FRounding := 6;
  FStyleNormal := TBCMDButtonStyle.Create;
  FStyleNormal.OnChange := OnChangeStyle;
  FStyleHover := TBCMDButtonStyle.Create;
  FStyleHover.OnChange := OnChangeStyle;
  FStyleActive := TBCMDButtonStyle.Create;
  FStyleActive.OnChange := OnChangeStyle;
  FStyleDisabled := TBCMDButtonStyle.Create;
  FStyleDisabled.OnChange := OnChangeStyle;
  // Default Style
  FStyleHover.Color := RGBToColor(220, 220, 220);
  FStyleActive.Color := RGBToColor(198, 198, 198);
  FStyleDisabled.TextColor := RGBToColor(163, 163, 163);
  // Animation
  FAnimation := False;
  FTimer := TTimer.Create(Self);
  FTimer.Enabled := False;
  FTimer.Interval := BCMDBUTTONTIMERSPEED;
  FTimer.OnTimer := OnTimer;
  {$IFDEF FPC}//#
  FTimer.OnStartTimer := OnStartTimer;
  FTimer.OnStopTimer := OnStopTimer;
  {$ENDIF}
  // Setup default sizes
  with GetControlClassDefaultSize do
    SetInitialBounds(0, 0, CX, CY);
end;

destructor TCustomBCMDButton.Destroy;
begin
  FTimer.OnTimer := nil;
  {$IFDEF FPC}//#
  FTimer.OnStartTimer := nil;
  FTimer.OnStopTimer := nil;
  {$ENDIF}
  FTimer.Enabled := False;
  FStyleNormal.Free;
  FStyleHover.Free;
  FStyleActive.Free;
  FStyleDisabled.Free;
  inherited Destroy;
end;

procedure TCustomBCMDButton.SelectAll;
var
  i: integer;
  control: TWinControl;
begin
  if (Parent <> nil) and (Parent is TWinControl) then
  begin
    control := TWinControl(Parent);
    for i := 0 to control.ControlCount - 1 do
      if (control.Controls[i] is TCustomBCMDButton) then
        if (TCustomBCMDButton(control.Controls[i]).Kind in
          [mdbkToggle, mdbkCheckBox]) then
          TCustomBCMDButton(control.Controls[i]).Checked := True;
  end;
end;

procedure TCustomBCMDButton.UnselectAll;
var
  i: integer;
  control: TWinControl;
begin
  if (Parent <> nil) and (Parent is TWinControl) then
  begin
    control := TWinControl(Parent);
    for i := 0 to control.ControlCount - 1 do
      if (control.Controls[i] is TCustomBCMDButton) then
        if (TCustomBCMDButton(control.Controls[i]).Kind in
          [mdbkToggle, mdbkCheckBox]) then
          TCustomBCMDButton(control.Controls[i]).Checked := False;
  end;
end;

procedure TCustomBCMDButton.InvertSelection;
var
  i: integer;
  control: TWinControl;
begin
  if (Parent <> nil) and (Parent is TWinControl) then
  begin
    control := TWinControl(Parent);
    for i := 0 to control.ControlCount - 1 do
      if (control.Controls[i] is TCustomBCMDButton) then
        if (TCustomBCMDButton(control.Controls[i]).Kind in
          [mdbkToggle, mdbkCheckBox]) then
          TCustomBCMDButton(control.Controls[i]).Checked :=
            not TCustomBCMDButton(control.Controls[i]).Checked;
  end;
end;

function TCustomBCMDButton.GetSelected: TStringList;
var
  i: integer;
  control: TWinControl;
begin
  Result := TStringList.Create;
  if (Parent <> nil) and (Parent is TWinControl) then
  begin
    control := TWinControl(Parent);
    for i := 0 to control.ControlCount - 1 do
      if (control.Controls[i] is TCustomBCMDButton) then
        if TCustomBCMDButton(control.Controls[i]).Checked then
          Result.AddObject(TCustomBCMDButton(control.Controls[i]).Caption,
            TCustomBCMDButton(control.Controls[i]));
  end;
end;

end.
