{ Extended checked controls (radiobutton, checkbox, radiogroup, checkgroup)

  Copyright (C) 2020 Lazarus team

  This library is free software; you can redistribute it and/or modify it
  under the same terms as the Lazarus Component Library (LCL)

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

}

unit ExCheckCtrls;

{$mode objfpc}{$H+}

interface

uses
  LCLType, LCLIntf, LCLProc, LMessages,
  Graphics, Classes, SysUtils, Types, Themes, Controls,
  StdCtrls, ExtCtrls, ImgList;

type
  TGetImageIndexEvent = procedure (Sender: TObject; AHover, APressed, AEnabled: Boolean;
    AState: TCheckboxState; var AImgIndex: Integer) of object;

  { TCustomCheckControlEx }

  TCustomCheckControlEx = class(TCustomControl)
  private
    type
      TCheckControlKind = (cckCheckbox, cckRadioButton);
  private
    FAlignment: TLeftRight;
    FAllowGrayed: Boolean;
    FThemedBtnSize: TSize;
    FBtnLayout: TTextLayout;
    FDistance: Integer;        // between button and caption
    FDrawFocusRect: Boolean;
    FFocusBorder: Integer;
    FGroupLock: Integer;
    FHover: Boolean;
    FImages: TCustomImageList;
    FImagesWidth: Integer;
    FKind: TCheckControlKind;
    FPressed: Boolean;
    FReadOnly: Boolean;
    FState: TCheckBoxState;
    FTextLayout: TTextLayout;
    FThemedCaption: Boolean;
//    FTransparent: Boolean;
    FWordWrap: Boolean;
    FOnChange: TNotifyEvent;
    FOnGetImageIndex: TGetImageIndexEvent;
    function GetCaption: TCaption;
    function GetChecked: Boolean;
    procedure SetAlignment(const AValue: TLeftRight);
    procedure SetBtnLayout(const AValue: TTextLayout);
    procedure SetCaption(const AValue: TCaption);
    procedure SetChecked(const AValue: Boolean);
    procedure SetDrawFocusRect(const AValue: Boolean);
    procedure SetImages(const AValue: TCustomImageList);
    procedure SetImagesWidth(const AValue: Integer);
    procedure SetState(const AValue: TCheckBoxState);
    procedure SetTextLayout(const AValue: TTextLayout);
    procedure SetThemedCaption(const AValue: Boolean);
    //procedure SetTransparent(const AValue: Boolean);
    procedure SetWordWrap(const AValue: Boolean);

  protected
    procedure AfterSetState; virtual;
    procedure CalculatePreferredSize(var PreferredWidth, PreferredHeight: Integer;
      {%H-}WithThemeSpace: Boolean); override;
    function CanExecUserChange: Boolean; virtual;
    procedure Click; override;
    procedure CMBiDiModeChanged(var {%H-}Message: TLMessage); message CM_BIDIMODECHANGED;
    procedure CreateHandle; override;
    procedure DoAutoAdjustLayout(const AMode: TLayoutAdjustmentPolicy;
      const AXProportion, AYProportion: Double); override;
    procedure DoEnter; override;
    procedure DoExit; override;
    procedure DrawBackground;
    procedure DrawButton(AHovered, APressed, AEnabled: Boolean; AState: TCheckboxState);
    procedure DrawButtonText(AHovered, APressed, AEnabled: Boolean;
      AState: TCheckboxState);
    function GetBtnSize: TSize; virtual;
    function GetDrawTextFlags: Cardinal;
    function GetTextExtent(const ACaption: String): TSize;
    function GetThemedButtonDetails(AHovered, APressed, AEnabled: Boolean;
      AState: TCheckboxState): TThemedElementDetails; virtual; abstract;
//    procedure InitBtnSize(Scaled: Boolean);
    procedure LockGroup;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    procedure KeyUp(var Key: Word; Shift: TShiftState); override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
    procedure MouseEnter; override;
    procedure MouseLeave; override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
    procedure Paint; override;
    procedure TextChanged; override;
    procedure UnlockGroup;
    procedure UserChange; virtual;
    procedure WMSize(var Message: TLMSize); message LM_SIZE;

    property Alignment: TLeftRight read FAlignment write SetAlignment default taRightJustify;
    property AllowGrayed: Boolean read FAllowGrayed write FAllowGrayed default False;
    property ButtonLayout: TTextLayout read FBtnLayout write SetBtnLayout default tlCenter;
    property Caption: TCaption read GetCaption write SetCaption;
    property Checked: Boolean read GetChecked write SetChecked default false;
    property DrawFocusRect: Boolean read FDrawFocusRect write SetDrawFocusRect default true;
    property Images: TCustomImageList read FImages write SetImages;
    property ImagesWidth: Integer read FImagesWidth write SetImagesWidth default 0;
    property ReadOnly: Boolean read FReadOnly write FReadOnly default false;
    property State: TCheckBoxState read FState write SetState default cbUnchecked;
    property TextLayout: TTextLayout read FTextLayout write SetTextLayout default tlCenter;
    property ThemedCaption: Boolean read FThemedCaption write SetThemedCaption default true;
    //property Transparent: Boolean read FTransparent write SetTransparent default true;
    property WordWrap: Boolean read FWordWrap write SetWordWrap default false;
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
    property OnGetImageIndex: TGetImageIndexEvent read FOnGetImageIndex write FOnGetImageIndex;

  public
    constructor Create(AOwner: TComponent); override;

  end;

  { TCustomCheckboxEx }

  TCustomCheckboxEx = class(TCustomCheckControlEx)
  private
  protected
    function GetThemedButtonDetails(AHovered, APressed, AEnabled: Boolean;
      AState: TCheckboxState): TThemedElementDetails; override;
  public
    constructor Create(AOwner: TComponent); override;
  end;


{ TCheckBoxEx }

  TCheckBoxEx = class(TCustomCheckBoxEx)
  published
    //property Action;
    property Align;
    property Alignment;
    property AllowGrayed;
    property Anchors;
    property AutoSize default true;
    property BiDiMode;
    property BorderSpacing;
    property ButtonLayout;
    property Caption;
    property Checked;
    property Color;
    property Constraints;
    property Cursor;
    property DoubleBuffered;
    property DragCursor;
    property DragKind;
    property DragMode;
    property DrawFocusRect;
    property Enabled;
    property Font;
    property Height;
    property HelpContext;
    property HelpKeyword;
    property HelpType;
    property Hint;
    property Images;
    property ImagesWidth;
    property Left;
    property Name;
    property ParentBiDiMode;
    property ParentColor;
    property ParentDoubleBuffered;
    property ParentFont;
    property ParentShowHint;
    property ReadOnly;
    property ShowHint;
    property State;
    property TabOrder;
    property TabStop;
    property Tag;
    property TextLayout;
    property ThemedCaption;
    property Top;
    //property Transparent;
    property Visible;
    property Width;
    property WordWrap;
    property OnChange;
    property OnChangeBounds;
    property OnClick;
    property OnContextPopup;
    property OnDragDrop;
    property OnDragOver;
    property OnEditingDone;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnGetImageIndex;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMouseDown;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnMouseUp;
    property OnMouseWheel;
    property OnMouseWheelDown;
    property OnMouseWheelUp;
    property OnResize;
    property OnStartDrag;
    property OnUTF8KeyPress;
  end;

  { TCustomRadioButtonEx }

  TCustomRadioButtonEx = class(TCustomCheckControlEx)
  protected
    procedure AfterSetState; override;
    function CanExecUserChange: Boolean; override;
    function GetThemedButtonDetails(AHovered, APressed, AEnabled: Boolean;
      AState: TCheckboxState): TThemedElementDetails; override;
  public
    constructor Create(AOwner: TComponent); override;
  published
  end;

  { TRadioButtonEx }

  TRadioButtonEx = class(TCustomRadioButtonEx)
  published
    property Align;
    property Alignment;
    property Anchors;
    property AutoSize default true;
    property BiDiMode;
    property BorderSpacing;
    property ButtonLayout;
    property Caption;
    property Checked;
    property Color;
    property Constraints;
    property Cursor;
    property DoubleBuffered;
    property DragCursor;
    property DragKind;
    property DragMode;
    property DrawFocusRect;
    property Enabled;
    property Font;
    property Height;
    property HelpContext;
    property HelpKeyword;
    property HelpType;
    property Hint;
    property Images;
    property ImagesWidth;
    property Left;
    property Name;
    property ParentBiDiMode;
    property ParentColor;
    property ParentDoubleBuffered;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ReadOnly;
    property ShowHint;
    property State;
    property TabOrder;
    property TabStop;
    property Tag;
    property TextLayout;
    property ThemedCaption;
    //property Transparent;
    property Visible;
    property WordWrap;
    property Width;

    property OnChange;
    property OnChangeBounds;
    property OnClick;
    property OnContextPopup;
    property OnDragDrop;
    property OnDragOver;
    property OnEnter;
    property OnExit;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMouseDown;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnMouseUp;
    property OnMouseWheel;
    property OnMouseWheelDown;
    property OnMouseWheelUp;
    property OnResize;
    property OnStartDrag;
    property OnGetImageIndex;
  end;


{ TCustomCheckControlGroupEx }

  TCustomCheckControlGroupEx = class(TCustomGroupBox)
  private
    FAutoFill: Boolean;
    FButtonList: TFPList;
    FColumnLayout: TColumnLayout;
    FColumns: integer;
    FImages: TCustomImageList;
    FImagesWidth: Integer;
    FItems: TStrings;
    FIgnoreClicks: boolean;
    FReadOnly: Boolean;
    FUpdatingItems: Boolean;
    FOnClick: TNotifyEvent;
    FOnGetImageIndex: TGetImageIndexEvent;
    FOnItemEnter: TNotifyEvent;
    FOnItemExit: TNotifyEvent;
    FOnSelectionChanged: TNotifyEvent;
    procedure ItemEnter(Sender: TObject); virtual;
    procedure ItemExit(Sender: TObject); virtual;
    procedure ItemKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); virtual;
    procedure ItemKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); virtual;
    procedure ItemKeyPress(Sender: TObject; var Key: Char); virtual;
    procedure ItemUTF8KeyPress(Sender: TObject; var UTF8Key: TUTF8Char); virtual;
    procedure SetAutoFill(const AValue: Boolean);
    procedure SetColumnLayout(const AValue: TColumnLayout);
    procedure SetColumns(const AValue: integer);
    procedure SetImages(const AValue: TCustomImageList);
    procedure SetImagesWidth(const AValue: Integer);
    procedure SetItems(const AValue: TStrings);
    procedure SetOnGetImageIndex(const AValue: TGetImageIndexEvent);
    procedure SetReadOnly(const AValue: Boolean);
  protected
    procedure UpdateAll;
    procedure UpdateControlsPerLine;
    procedure UpdateInternalObjectList;
    procedure UpdateItems; virtual; abstract;
    procedure UpdateTabStops;
    property AutoFill: Boolean read FAutoFill write SetAutoFill default true;
    property ColumnLayout: TColumnLayout read FColumnLayout write SetColumnLayout default clHorizontalThenVertical;
    property Columns: Integer read FColumns write SetColumns default 1;
    property Images: TCustomImageList read FImages write SetImages;
    property ImagesWidth: Integer read FImagesWidth write SetImagesWidth default 0;
    property Items: TStrings read FItems write SetItems;
    property ReadOnly: Boolean read FReadOnly write SetReadOnly default false;
    property OnClick: TNotifyEvent read FOnClick write FOnClick;
    property OnGetImageIndex: TGetImageIndexEvent read FOnGetImageIndex write SetOnGetImageIndex;
    property OnItemEnter: TNotifyEvent read FOnItemEnter write FOnItemEnter;
    property OnItemExit: TNotifyEvent read FOnItemExit write FOnItemExit;
    property OnSelectionChanged: TNotifyEvent read FOnSelectionChanged write FOnSelectionChanged;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function CanModify: boolean; virtual;
    procedure FlipChildren(AllLevels: Boolean); override;
    function Rows: integer;
  end;

  { TCustomRadioGroupEx }
  TCustomRadioGroupEx = class(TCustomCheckControlGroupEx)
  private
    FCreatingWnd: Boolean;
    FHiddenButton: TRadioButtonEx;
    FItemIndex: integer;
    FLastClickedItemIndex: Integer;
    FReading: Boolean;
    procedure Changed(Sender: TObject);
    procedure Clicked(Sender: TObject);
    function GetButtonCount: Integer;
    function GetButtons(AIndex: Integer): TRadioButtonEx;
    procedure SetItemIndex(const AValue: Integer);
  protected
    procedure CheckItemIndexChanged; virtual;
    procedure InitializeWnd; override;
    procedure ItemKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); override;
    procedure ReadState(AReader: TReader); override;
    procedure UpdateItems; override;
    procedure UpdateRadioButtonStates; virtual;
    property ItemIndex: Integer read FItemIndex write SetItemIndex default -1;
  public
    property ButtonCount: Integer read GetButtonCount;
    property Buttons[AIndex: Integer]: TRadioButtonEx read GetButtons;
  published
    constructor Create(AOwner: TComponent); override;
  end;

  { TRadioGroupEx }
  TRadioGroupEx = class(TCustomRadioGroupEx)
  published
    property Align;
    property Anchors;
    property AutoFill;
    property AutoSize;
    property BiDiMode;
    property BorderSpacing;
    property Caption;
    property ChildSizing;
    property Color;
    property ColumnLayout;
    property Columns;
    property Constraints;
    property Cursor;
    property DoubleBuffered;
    property DragCursor;
    property DragMode;
    property Enabled;
    property Font;
    property Height;
    property HelpContext;
    property HelpKeyword;
    property HelpType;
    property Hint;
    property Images;
    property ImagesWidth;
    property ItemIndex;
    property Items;
    property Left;
    property Name;
    property ParentBiDiMode;
    property ParentColor;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ReadOnly;
    property ShowHint;
    property TabOrder;
    property TabStop;
    property Tag;
    property Top;
    property Visible;
    property Width;
    property OnChangeBounds;
    property OnClick;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnGetImageIndex;
    property OnItemEnter;
    property OnItemExit;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMouseDown;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnMouseUp;
    property OnMouseWheel;
    property OnMouseWheelDown;
    property OnMouseWheelUp;
    property OnResize;
    property OnSelectionChanged;
    property OnStartDrag;
    property OnUTF8KeyPress;
  end;

  TCustomCheckGroupEx = class(TCustomCheckControlGroupEx)
  private
    FOnItemClick: TCheckGroupClicked;
    procedure Clicked(Sender: TObject);
    procedure DoClick(AIndex: integer);
    function GetButtonCount: Integer;
    function GetButtons(AIndex: Integer): TCheckBoxEx;
    function GetChecked(AIndex: integer): boolean;
    function GetCheckEnabled(AIndex: integer): boolean;
    procedure RaiseIndexOutOfBounds(AIndex: integer);
    procedure SetChecked(AIndex: integer; const AValue: boolean);
    procedure SetCheckEnabled(AIndex: integer; const AValue: boolean);
  protected
    procedure DefineProperties(Filer: TFiler); override;
    procedure Loaded; override;
    procedure ReadData(Stream: TStream);
    procedure UpdateItems; override;
    procedure WriteData(Stream: TStream);
//    procedure DoOnResize; override;
  public
    constructor Create(AOwner: TComponent); override;
    property ButtonCount: Integer read GetButtonCount;
    property Buttons[AIndex: Integer]: TCheckBoxEx read GetButtons;
  public
    property Checked[Index: integer]: boolean read GetChecked write SetChecked;
    property CheckEnabled[Index: integer]: boolean read GetCheckEnabled write SetCheckEnabled;
    property OnItemClick: TCheckGroupClicked read FOnItemClick write FOnItemClick;
  end;

  { TCheckGroupEx }

  TCheckGroupEx = class(TCustomCheckGroupEx)
  published
    property Align;
    property Anchors;
    property AutoFill;
    property AutoSize;
    property BiDiMode;
    property BorderSpacing;
    property Caption;
    property ChildSizing;
    property ClientHeight;
    property ClientWidth;
    property Color;
    property ColumnLayout;
    property Columns;
    property Constraints;
    property DoubleBuffered;
    property DragCursor;
    property DragMode;
    property Enabled;
    property Font;
    property Images;
    property ImagesWidth;
    property Items;
    property ParentBiDiMode;
    property ParentFont;
    property ParentColor;
    property ParentDoubleBuffered;
    property ParentShowHint;
    property PopupMenu;
    property ReadOnly;
    property ShowHint;
    property TabOrder;
    property TabStop;
    property Visible;

    property OnChangeBounds;
    property OnClick;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnGetImageIndex;
    property OnItemClick;
    property OnItemEnter;
    property OnItemExit;
    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;
  end;

implementation

uses
  Math, LCLStrConsts, LResources;

const
  cIndent = 5;

  FIRST_RADIOBUTTON_DETAIL = tbRadioButtonUncheckedNormal;
  FIRST_CHECKBOX_DETAIL = tbCheckBoxUncheckedNormal;
  HOT_OFFSET = 1;
  PRESSED_OFFSET = 2;
  DISABLED_OFFSET = 3;
  CHECKED_OFFSET = 4;
  MIXED_OFFSET = 8;

procedure DrawParentImage(Control: TControl; Dest: TCanvas);
var
  SaveIndex: integer;
  DC: HDC;
  Position: TPoint;
begin
  with Control do
  begin
    if Parent = nil then Exit;
    DC := Dest.Handle;
    SaveIndex := SaveDC(DC);
    GetViewportOrgEx(DC, @Position);
    SetViewportOrgEx(DC, Position.X - Left, Position.Y - Top, nil);
    IntersectClipRect(DC, 0, 0, Parent.ClientWidth, Parent.ClientHeight);
    Parent.Perform(LM_ERASEBKGND, DC, 0);
    Parent.Perform(LM_PAINT, DC, 0);
    RestoreDC(DC, SaveIndex);
  end;
end;

function ProcessLineBreaks(const AString: string; ToC: Boolean): String;
var
  idx: Integer;

  procedure AddChar(ch: Char);
  begin
    Result[idx] := ch;
    inc(idx);
    if idx > Length(Result) then
      SetLength(Result, Length(Result) + 100);
  end;

var
  P, PEnd: PChar;
begin
  if AString = '' then
  begin
    Result := '';
    exit;
  end;

  SetLength(Result, Length(AString));
  idx := 1;
  P := @AString[1];
  PEnd := P + Length(AString);

  if ToC then
    // Replace line breaks by '\n'
    while P < PEnd do begin
      if (P^ = #13) then begin
        AddChar('\');
        AddChar('n');
        inc(P);
        if P^ <> #10 then dec(P);
      end else
      if P^ = #10 then
      begin
        AddChar('\');
        AddChar('n');
      end else
      if P^ = '\' then
      begin
        AddChar('\');
        AddChar('\');
      end else
        AddChar(P^);
      inc(P);
    end
  else
    // Replace '\n' by LineEnding
    while (P < PEnd) do
    begin
      if (P^ = '\') and (P < PEnd-1) then
      begin
        inc(P);
        if (P^ = 'n') or (P^ = 'N') then
          AddChar(#10)
        else
          AddChar(P^);
      end else
        AddChar(P^);
      inc(P);
    end;
  SetLength(Result, idx-1);
end;

{ TCheckboxControlEx }

constructor TCustomCheckControlEx.Create(AOwner: TComponent);
begin
  inherited;
  ControlStyle := ControlStyle  + [csParentBackground, csReplicatable] - [csOpaque]
    - csMultiClicks - [csClickEvents, csNoStdEvents];  { inherited Click not used }

  FAlignment := taRightJustify;
  FBtnLayout := tlCenter;
  FDrawFocusRect := true;
  FKind := cckCheckbox;
  FDistance := cIndent;
  FFocusBorder := FDistance div 2;
  FTextLayout := tlCenter;
  FThemedCaption := true;
//  FTransparent := true;

  AutoSize := true;
  TabStop := true;
end;

// Is called after the State has changed in SetState. Will be overridden by
// TCustomRadioButtonEx to uncheck all other iteme.s
procedure TCustomCheckControlEx.AfterSetState;
begin
end;

procedure TCustomCheckControlEx.CalculatePreferredSize(var PreferredWidth,
  PreferredHeight: Integer; WithThemeSpace: Boolean);
var
  flags: Cardinal;
  textSize: TSize;
  R: TRect;
  captn: String;
  details: TThemedElementDetails;
  btnSize: TSize;
begin
  captn := inherited Caption;
  if (captn = '') then
  begin
    btnSize := GetBtnSize;
    PreferredWidth := btnSize.CX;
    PreferredHeight := btnSize.CY;
    exit;
  end;

  Canvas.Font.Assign(Font);

  R := ClientRect;
  btnSize := GetBtnSize;
  dec(R.Right, btnSize.CX + FDistance);
  R.Bottom := MaxInt;   // Max height possible

  flags := GetDrawTextFlags + DT_CALCRECT;

  // rectangle available for text
  if FThemedCaption then
  begin
    details := GetThemedButtonDetails(false, false, true, cbChecked);
    if FWordWrap then
    begin
      with ThemeServices.GetTextExtent(Canvas.Handle, details, captn, flags, @R) do begin
        textSize.CX := Right;
        textSize.CY := Bottom;
      end;
    end else
      with ThemeServices.GetTextExtent(Canvas.Handle, details, captn, flags, nil) do begin
        textSize.CX := Right;
        textSize.CY := Bottom;
      end;
  end else
  begin
    DrawText(Canvas.Handle, PChar(captn), Length(captn), R, flags);
    textSize.CX := R.Right - R.Left;
    textSize.CY := R.Bottom - R.Top;
  end;

  PreferredWidth := btnSize.CX + FDistance + textSize.CX + FFocusBorder;
  PreferredHeight := Max(btnSize.CY, textSize.CY + 2*FFocusBorder);
end;

// Will be overridden by the radio button to prevent unchecking a checked btn.
function TCustomCheckControlEx.CanExecUserChange: Boolean;
begin
  Result := true;
end;

procedure TCustomCheckControlEx.CMBiDiModeChanged(var Message: TLMessage);
begin
  Invalidate;
end;

procedure TCustomCheckControlEx.CreateHandle;
var
  w, h: Integer;
begin
  inherited;
  if (Width = 0) or (Height = 0) then begin
    CalculatePreferredSize(w{%H-}, h{%H-}, false);
    if Width <> 0 then w := Width;
    if Height <> 0 then h := Height;
    SetBounds(Left, Top, w, h);
  end;
end;

procedure TCustomCheckControlEx.DoAutoAdjustLayout(
  const AMode: TLayoutAdjustmentPolicy;
  const AXProportion, AYProportion: Double);
begin
  inherited;
  if AMode in [lapAutoAdjustWithoutHorizontalScrolling, lapAutoAdjustForDPI] then
  begin
    FDistance := Round(cIndent * AXProportion);
    FFocusBorder := FDistance div 2;
  end;
end;

procedure TCustomCheckControlEx.Click;
begin
  if Assigned(OnClick) then
    OnClick(Self);

  UserChange;
end;

procedure TCustomCheckControlEx.DoEnter;
begin
  inherited DoEnter;
  Invalidate;
end;

procedure TCustomCheckControlEx.DoExit;
begin
  inherited DoExit;
  Invalidate;
end;

procedure TCustomCheckControlEx.DrawBackground;
var
  R: TRect;
begin
  R := Rect(0, 0, Width, Height);
  Canvas.Brush.Style := bsSolid;
  Canvas.Brush.Color := Color;
  Canvas.FillRect(R);
end;

procedure TCustomCheckControlEx.DrawButton(AHovered, APressed, AEnabled: Boolean; AState: TCheckboxState);
var
  btnRect: TRect;
  btnPoint: TPoint = (X:0; Y:0);
  details: TThemedElementDetails;
  imgIndex: Integer;
  imgRes: TScaledImageListResolution;
  btnSize: TSize;
begin
  // Checkbox/Radiobutton size and position
  btnSize := GetBtnSize;
  case FAlignment of
    taLeftJustify:
      if not IsRightToLeft then btnPoint.X := ClientWidth - btnSize.CX;
    taRightJustify:
      if IsRightToLeft then btnPoint.X := ClientWidth - btnSize.CX;
  end;
  case FBtnLayout of
    tlTop: btnPoint.Y := FFocusBorder;
    tlCenter: btnPoint.Y := (ClientHeight - btnSize.CY) div 2;
    tlBottom: btnPoint.Y := ClientHeight - btnSize.CY - FFocusBorder;
  end;
  btnRect := Rect(0, 0, btnSize.CX, btnSize.CY);
  OffsetRect(btnRect, btnPoint.X, btnPoint.Y);

  imgIndex := -1;
  if (FImages <> nil) and Assigned(FOnGetImageIndex) then
    FOnGetImageIndex(Self, AHovered, APressed, AEnabled, AState, imgIndex);

  if imgIndex > -1 then
  begin
    ImgRes := FImages.ResolutionForPPI[FImagesWidth, Font.PixelsPerInch, GetCanvasScaleFactor];
    ImgRes.Draw(Canvas, btnRect.Left, btnRect.Top, imgIndex, AEnabled);
  end else
  begin
    // Drawing style of button
    details := GetThemedButtonDetails(AHovered, APressed, AEnabled, AState);
    // Draw button
    ThemeServices.DrawElement(Canvas.Handle, details, btnRect);
  end;
end;

procedure TCustomCheckControlEx.DrawButtonText(AHovered, APressed, AEnabled: Boolean;
  AState: TCheckboxState);
var
  R: TRect;
//  textStyle: TTextStyle;
  delta: Integer;
  details: TThemedElementDetails;
  flags: Cardinal;
  textSize: TSize;
  captn: TCaption;
  btnSize: TSize;
begin
  captn := inherited Caption;   // internal string with line breaks

  if captn = '' then
    exit;

  // Determine text drawing parameters
  flags := GetDrawTextFlags;

  btnSize := GetBtnSize;
  delta := btnSize.CX + FDistance;
  R := ClientRect;
  dec(R.Right, delta);
  Canvas.Font.Assign(Font);
  if FThemedCaption then
  begin
    R.Bottom := MaxInt;   // max height for word-wrap
    details := GetThemedButtonDetails(AHovered, APressed, AEnabled, AState);
    with ThemeServices.GetTextExtent(Canvas.Handle, details, captn, flags, @R) do begin
      textSize.CX := Right;
      textSize.CY := Bottom;
    end;
  end else
  begin
    if not AEnabled then Canvas.Font.Color := clGrayText;
    DrawText(Canvas.Handle, PChar(captn), Length(captn), R, flags + DT_CALCRECT);
    textSize.CX := R.Right - R.Left;
    textSize.CY := R.Bottom - R.Top;
  end;

  R := ClientRect;

  case FTextLayout of
    tlTop:
      R.Top := 0;
    tlCenter:
      R.Top := (R.Top + R.Bottom - textSize.CY) div 2;
    tlBottom:
      R.Top := R.Bottom - textSize.CY;
  end;
  R.Bottom := R.Top + textSize.CY;

  if (FAlignment = taRightJustify) and IsRightToLeft then
  begin
    dec(R.Right, delta);
    R.Left := R.Right - textSize.CX;
  end else
  begin
    inc(R.Left, delta);
    R.Right := R.Left + textSize.CX;
  end;

  // Draw text
  if FThemedCaption then
  begin
    ThemeServices.DrawText(Canvas, details, captn, R, flags, 0);
  end else
  begin
    Canvas.Brush.Style := bsClear;
    DrawText(Canvas.Handle, PChar(captn), Length(captn), R, flags);
  end;

  // Draw focus rect
  if Focused and FDrawFocusRect then begin
    InflateRect(R, FFocusBorder, 0);
    if R.Left + R.Width > ClientWidth then R.Width := ClientWidth - R.Left;
    if R.Left < 0 then R.Left := 0;
    //LCLIntf.SetBkColor(Canvas.Handle, ColorToRGB(clBtnFace));
    Canvas.Font.Color := clBlack;
    LCLIntf.DrawFocusRect(Canvas.Handle, R);
  end;
end;

function TCustomCheckControlEx.GetBtnSize: TSize;
var
  ImgRes: TScaledImageListResolution;
begin
  if (FImages <> nil) then begin
    ImgRes := FImages.ResolutionForPPI[FImagesWidth, Font.PixelsPerInch, GetCanvasScaleFactor];
    Result.CX := ImgRes.Width;
    Result.CY := ImgRes.Height;
  end else
  begin
    with ThemeServices do
      if FKind = cckCheckbox then
        Result := GetDetailSize(GetElementDetails(tbCheckBoxCheckedNormal))
      else
      if FKind = cckRadioButton then
        Result := GetDetailSize(GetElementDetails(tbRadioButtonCheckedNormal));
      //Result.CX := Scale96ToFont(Result.CX);
      //Result.CY := Scale96ToFont(Result.CY);
  end;
end;

// Replaces linebreaks in the inherited Caption by '\n'  (and '\' by '\\') so
// that line breaks can be entered at designtime.
function TCustomCheckControlEx.GetCaption: TCaption;
const
  TO_C = true;
begin
  Result := ProcessLineBreaks(inherited Caption, TO_C);
end;

function TCustomCheckControlEx.GetChecked: Boolean;
begin
  Result := (FState = cbChecked);
end;

// Determine text drawing parameters for the DrawText command
function TCustomCheckControlEx.GetDrawTextFlags: Cardinal;
begin
  Result := 0;
  case FTextLayout of
    tlTop: inc(Result, DT_TOP);
    tlCenter: inc(Result, DT_VCENTER);
    tlBottom: inc(Result, DT_BOTTOM);
  end;

  if (FAlignment = taRightJustify) and IsRightToLeft then
    inc(Result, DT_RIGHT)
  else
    inc(Result, DT_LEFT);

  if IsRightToLeft then inc(Result, DT_RTLREADING);
  if FWordWrap then inc(Result, DT_WORDBREAK);
end;

function TCustomCheckControlEx.GetTextExtent(const ACaption: String): TSize;
var
  L: TStrings;
  s: String;
begin
  Result := Size(0, 0);
  L := TStringList.Create;
  try
    L.Text := ACaption;
    for s in L do
    begin
      Result.CY := Result.CY + Canvas.TextHeight(s);
      Result.CX := Max(Result.CX, Canvas.TextWidth(s));
    end;
  finally
    L.Free;
  end;
end;
              (*
procedure TCustomCheckControlEx.InitBtnSize(Scaled: Boolean);
var
  ImgRes: TScaledImageListResolution;
begin
  if (FImages <> nil) then begin
    if Scaled then begin
      ImgRes := FImages.ResolutionForPPI[FImagesWidth, Font.PixelsPerInch, GetCanvasScaleFactor];
      FBtnSize.CX := ImgRes.Width;
      FBtnSize.CY := ImgRes.Height;
    end else
    begin
      FBtnSize.CX := FImages.Width;
      FBtnSize.CY := FImages.Height;
    end;
  end else
  begin
    with ThemeServices do
      if FKind = cckCheckbox then
        FBtnSize := GetDetailSize(GetElementDetails(tbCheckBoxCheckedNormal))
      else if FKind = cckRadioButton then
        FBtnSize := GetDetailSize(GetElementDetails(tbRadioButtonCheckedNormal));
    if Scaled then
    begin
      FBtnSize.CX := Scale96ToFont(FBtnSize.CX);
      FBtnSize.CY := Scale96ToFont(FBtnSize.CY);
    end;
  end;
end;
*)

procedure TCustomCheckControlEx.KeyDown(var Key: Word; Shift: TShiftState);
begin
  inherited KeyDown(Key, Shift);
  if (Key in [VK_RETURN, VK_SPACE]) and not (ssCtrl in Shift) and (not FReadOnly) then
  begin
    FPressed := True;
    Invalidate;
  end;
end;

procedure TCustomCheckControlEx.KeyUp(var Key: Word; Shift: TShiftState);
begin
  inherited KeyUp(Key, Shift);
  if (Key in [VK_RETURN, VK_SPACE]) and not (ssCtrl in Shift) then
  begin
    FPressed :=  False;
    UserChange;
  end;
end;

procedure TCustomCheckControlEx.LockGroup;
begin
  inc(FGroupLock);
end;

procedure TCustomCheckControlEx.MouseDown(Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  inherited MouseDown(Button, Shift, X, Y);
  if (Button = mbLeft) and FHover and not FReadOnly then
  begin
    FPressed := True;
    Invalidate;
  end;
  SetFocus;
end;

procedure TCustomCheckControlEx.MouseEnter;
begin
  FHover := true;
  Invalidate;
  inherited;
end;

procedure TCustomCheckControlEx.MouseLeave;
begin
  FHover := false;
  Invalidate;
  inherited;
end;

procedure TCustomCheckControlEx.MouseUp(Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  inherited MouseUp(Button, Shift, X, Y);
  if Button = mbLeft then begin
    if PtInRect(ClientRect, Point(X, Y)) then Click;
    FPressed := False;
    Invalidate;
  end;
end;

procedure TCustomCheckControlEx.Paint;
begin
  {
  if FTransparent then
    DrawParentImage(Self, Self.Canvas)
  else
    DrawBackground;
  }
  DrawButton(FHover, FPressed, IsEnabled, FState);
  DrawButtonText(FHover, FPressed, IsEnabled, FState);
end;

procedure TCustomCheckControlEx.SetAlignment(const AValue: TLeftRight);
begin
  if AValue = FAlignment then exit;
  FAlignment := AValue;
  Invalidate;
end;

procedure TCustomCheckControlEx.SetBtnLayout(const AValue: TTextLayout);
begin
  if AValue = FBtnLayout then exit;
  FBtnLayout := AValue;
  Invalidate;
end;

procedure TCustomCheckControlEx.SetCaption(const AValue: TCaption);
const
  FROM_C = false;
begin
  if AValue = GetCaption then exit;
  inherited Caption := ProcessLineBreaks(AValue, FROM_C);
end;

procedure TCustomCheckControlEx.SetChecked(const AValue: Boolean);
begin
  if AValue then
    State := cbChecked
  else
    State := cbUnChecked;
end;

procedure TCustomCheckControlEx.SetDrawFocusRect(const AValue: Boolean);
begin
  if AValue = FDrawFocusRect then exit;
  FDrawFocusRect := AValue;
  Invalidate;
end;

procedure TCustomCheckControlEx.SetImages(const AValue: TCustomImageList);
begin
  if AValue = FImages then exit;
  FImages := AValue;
//  InitBtnSize(true);
  InvalidatePreferredSize;
  AdjustSize;
end;

procedure TCustomCheckControlEx.SetImagesWidth(const AValue: Integer);
begin
  if AValue = FImagesWidth then exit;
  FImagesWidth := AValue;
//  InitBtnSize(true);
  InvalidatePreferredSize;
  AdjustSize;
end;

procedure TCustomCheckControlEx.SetTextLayout(const AValue: TTextLayout);
begin
  if AValue = FTextLayout then exit;
  FTextLayout := AValue;
  Invalidate;
end;

procedure TCustomCheckControlEx.SetThemedCaption(const AValue: Boolean);
begin
  if AValue = FThemedCaption then exit;
  FThemedCaption := AValue;
  Invalidate;
end;

procedure TCustomCheckControlEx.SetState(const AValue: TCheckboxState);
begin
  if (FState = AValue) then exit;
  FState := AValue;
  if [csLoading, csDestroying, csDesigning] * ComponentState = [] then begin
    if Assigned(OnEditingDone) then OnEditingDone(self);
    if Assigned(OnChange) then OnChange(self);
    {
    // Execute only when Action.Checked is changed
    if not CheckFromAction then begin
      if Assigned(OnClick) then
        if not (Assigned(Action) and
          CompareMethods(TMethod(Action.OnExecute), TMethod(OnClick)))
          then OnClick(self);
      if (Action is TCustomAction) and
        (TCustomAction(Action).Checked <> (AValue = cbChecked))
        then ActionLink.Execute(self);
    end;
    }
    AfterSetState;
  end;
  Invalidate;
end;
{
procedure TCustomCheckControlEx.SetTransparent(const AValue: Boolean);
begin
  if AValue = FTransparent then exit;
  FTransparent := AValue;
  Invalidate;
end;
}

procedure TCustomCheckControlEx.SetWordWrap(const AValue: Boolean);
begin
  if AValue = FWordWrap then exit;
  FWordWrap := AValue;
  Invalidate;
end;

procedure TCustomCheckControlEx.TextChanged;
begin
  inherited TextChanged;
  Invalidate;
end;

procedure TCustomCheckControlEx.UnlockGroup;
begin
  dec(FGroupLock);
end;

// Executes a change triggered by user interaction (not by code)
procedure TCustomCheckControlEx.UserChange;
begin
  if FReadOnly then
    exit;

  if not CanExecUserChange then
    exit;

  if AllowGrayed then begin
    case FState of
      cbUnchecked: SetState(cbGrayed);
      cbGrayed: SetState(cbChecked);
      cbChecked: SetState(cbUnchecked);
    end;
  end else
    Checked := not Checked;
end;

procedure TCustomCheckControlEx.WMSize(var Message: TLMSize);
begin
  inherited WMSize(Message);
  Invalidate;
end;


{ TCustomRadioButtonEx }

constructor TCustomRadioButtonEx.Create(AOwner: TComponent);
begin
  inherited;
  FKind := cckRadioButton;
//  InitBtnSize(false);
end;

{ Is called by SetState and is supposed to uncheck all other radiobuttons in the
  same group, i.e. having the same parent. Provides a locking mechanism because
  uncheding another radiobutton would trigger AfterSetState again. }
procedure TCustomRadioButtonEx.AfterSetState;
var
  i: Integer;
  C: TControl;
begin
  if (FGroupLock > 0) or (Parent = nil) then
    exit;
  for i := 0 to Parent.ControlCount-1 do
  begin
    C := Parent.Controls[i];
    if (C is TCustomRadioButtonEx) and (C <> self) then
      with TCustomRadioButtonEx(C) do
      begin
        LockGroup;
        try
          State := cbUnChecked;
        finally
          UnlockGroup;
        end;
      end;
  end;
//  Parent.Invalidate;
end;

// Prevents the user from unchecking the btn when the btn is already checked.
function TCustomRadioButtonEx.CanExecUserChange: Boolean;
begin
  Result := FState <> cbChecked;
end;

function TCustomRadioButtonEx.GetThemedButtonDetails(
  AHovered, APressed, AEnabled: Boolean; AState: TCheckboxState): TThemedElementDetails;
var
  offset: Integer = 0;
  tb: TThemedButton;
begin
  offset := ord(FIRST_RADIOBUTTON_DETAIL);
  if APressed then
    inc(offset, PRESSED_OFFSET)
  else if AHovered then
    inc(offset, HOT_OFFSET);
  if not AEnabled then inc(offset, DISABLED_OFFSET);
  if AState = cbChecked then inc(offset, CHECKED_OFFSET);
  tb := TThemedButton(offset);
  Result := ThemeServices.GetElementDetails(tb);
end;
(*
  offset := 0
const                     // hovered      pressed      state
  caEnabledDetails: array [False..True, False..True, cbUnChecked..cbChecked] of TThemedElementDetails =
    (
     (
      (tbRadioButtonUncheckedNormal, tbRadioButtonCheckedNormal),
      (tbRadioButtonUncheckedPressed, tbRadioButtonCheckedPressed)
     ),
     (
      (tbRadioButtonUncheckedHot, tbRadioButtonCheckedHot),
      (tbRadioButtonUncheckedPressed, tbRadioButtonCheckedPressed)
     )
    );

  caDisabledDetails: array [cbUnchecked..cbChecked] of TThemedButton =
    (tbRadioButtonUncheckedDisabled, tbRadioButtonCheckedDisabled);
begin
  if Enabled then
    Result := caEnabledDetails[AHovered, APressed, AState]
  else
    Result := caDisabledDetails[AState];
end;
            *)


{==============================================================================}
{                               TCustomCheckboxEx                              }
{==============================================================================}

constructor TCustomCheckboxEx.Create(AOwner: TComponent);
begin
  inherited;
  FKind := cckCheckbox;
//  InitBtnSize(false);
end;

function TCustomCheckBoxEx.GetThemedButtonDetails(
  AHovered, APressed, AEnabled: Boolean; AState: TCheckboxState): TThemedElementDetails;
var
  offset: Integer = 0;
  tb: TThemedButton;
begin
  offset := ord(FIRST_CHECKBOX_DETAIL);
  if APressed then
    inc(offset, PRESSED_OFFSET)
  else if AHovered then
    inc(offset, HOT_OFFSET);
  if not AEnabled then inc(offset, DISABLED_OFFSET);
  case AState of
    cbChecked: inc(offset, CHECKED_OFFSET);
    cbGrayed: inc(offset, MIXED_OFFSET);
  end;
  tb := TThemedButton(offset);
  Result := ThemeServices.GetElementDetails(tb);
end;
      (*

const                     // hovered    pressed      state
  caEnabledDetails: array [False..True, False..True, cbUnchecked..cbGrayed] of TThemedButton =
    (
     (
      (tbCheckBoxUncheckedNormal, tbCheckBoxCheckedNormal, tbCheckBoxMixedNormal),
      (tbCheckBoxUncheckedPressed, tbCheckBoxCheckedPressed, tbCheckBoxMixedPressed)
     ),
     (
      (tbCheckBoxUncheckedHot, tbCheckBoxCheckedHot, tbCheckBoxMixedHot),
      (tbCheckBoxUncheckedPressed, tbCheckBoxCheckedPressed, tbCheckBoxMixedPressed)
     )
    );

  caDisabledDetails: array [cbUnchecked..cbGrayed] of TThemedButton =
    (tbCheckBoxUncheckedDisabled, tbCheckBoxCheckedDisabled, tbCheckBoxMixedDisabled);
var
  tb: TThemedButton;
begin
  if Enabled then
    tb := caEnabledDetails[AHovered, APressed, AState]
  else
    tb := caDisabledDetails[AState];
  Result := ThemeServices.GetElementDetails(tb);
end;    *)


{==============================================================================}
{                       TCustomCheckControlGroupEx                             }
{==============================================================================}
constructor TCustomCheckControlGroupEx.Create(AOwner: TComponent);
begin
  inherited;
  FAutoFill := true;
  FButtonList := TFPList.Create;
  FColumns := 1;
  FColumnLayout := clHorizontalThenVertical;
  ChildSizing.Layout := cclLeftToRightThenTopToBottom;
  ChildSizing.ControlsPerLine := FColumns;
  ChildSizing.ShrinkHorizontal := crsScaleChilds;
  ChildSizing.ShrinkVertical := crsScaleChilds;
  ChildSizing.EnlargeHorizontal := crsHomogenousChildResize;
  ChildSizing.EnlargeVertical := crsHomogenousChildResize;
  ChildSizing.LeftRightSpacing := 6;
  ChildSizing.TopBottomSpacing := 0;
end;

destructor TCustomCheckControlGroupEx.Destroy;
var
  i: Integer;
begin
  for i:=0 to FButtonList.Count-1 do
    TCustomCheckControlEx(FButtonList[i]).Free;
  FButtonList.Free;
  FItems.Free;
  inherited;
end;

function TCustomCheckControlGroupEx.CanModify: Boolean;
begin
  Result := not FReadOnly;
end;

procedure TCustomCheckControlgroupEx.FlipChildren(AllLevels: Boolean);
begin
  // no flipping
end;

procedure TCustomCheckControlGroupEx.ItemEnter(Sender: TObject);
begin
  if Assigned(FOnItemEnter) then FOnItemEnter(Sender);
end;

procedure TCustomCheckControlGroupEx.ItemExit(Sender: TObject);
begin
  if Assigned(FOnItemExit) then FOnItemExit(Sender);
end;

procedure TCustomCheckControlGroupEx.ItemKeyDown(Sender: TObject;
  var Key: Word; Shift: TShiftState);
begin
  if Key <> 0 then
    KeyDown(Key, Shift);
end;

procedure TCustomCheckControlGroupEx.ItemKeyUp(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Key <> 0 then
    KeyUp(Key, Shift);
end;

procedure TCustomCheckControlGroupEx.ItemKeyPress(Sender: TObject; var Key: Char);
begin
  if Key <> #0 then
    KeyPress(Key);
end;

procedure TCustomCheckControlGroupEx.ItemUTF8KeyPress(Sender: TObject;
  var UTF8Key: TUTF8Char);
begin
  UTF8KeyPress(UTF8Key);
end;

function TCustomCheckControlGroupEx.Rows: integer;
begin
  if FItems.Count > 0 then
    Result := ((FItems.Count-1) div Columns) + 1
  else
    Result := 0;
end;

procedure TCustomCheckControlGroupEx.SetAutoFill(const AValue: Boolean);
begin
  if FAutoFill = AValue then exit;
  FAutoFill := AValue;
  DisableAlign;
  try
    if FAutoFill then begin
      ChildSizing.EnlargeHorizontal := crsHomogenousChildResize;
      ChildSizing.EnlargeVertical := crsHomogenousChildResize;
    end else begin
      ChildSizing.EnlargeHorizontal := crsAnchorAligning;
      ChildSizing.EnlargeVertical := crsAnchorAligning;
    end;
  finally
    EnableAlign;
  end;
end;

procedure TCustomCheckControlGroupEx.SetColumnLayout(const AValue: TColumnLayout);
begin
  if FColumnLayout = AValue then exit;
  FColumnLayout := AValue;
  if FColumnLayout = clHorizontalThenVertical then
    ChildSizing.Layout := cclLeftToRightThenTopToBottom
  else
    ChildSizing.Layout := cclTopToBottomThenLeftToRight;
  UpdateControlsPerLine;
end;

procedure TCustomCheckControlGroupEx.SetColumns(const AValue: integer);
begin
  if AValue <> FColumns then begin
    if (AValue < 1) then
       raise Exception.Create('TCustomRadioGroup: Columns must be >= 1');
    FColumns := AValue;
    UpdateControlsPerLine;
  end;
end;

procedure TCustomCheckControlGroupEx.SetOnGetImageIndex(const AValue: TGetImageIndexEvent);
var
  i: Integer;
begin
  FOnGetImageIndex := AValue;
  for i := 0 to FButtonList.Count - 1 do
    TCustomCheckControlEx(FButtonList[i]).OnGetImageIndex := AValue;
end;

procedure TCustomCheckControlGroupEx.SetImages(const AValue: TCustomImagelist);
var
  i: Integer;
begin
  if AValue = FImages then exit;
  FImages := AValue;
  for i:=0 to FButtonList.Count-1 do
    TCustomCheckControlEx(FButtonList[i]).Images := FImages;
end;

procedure TCustomCheckControlGroupEx.SetImagesWidth(const AValue: Integer);
var
  i: Integer;
begin
  if AValue = FImagesWidth then exit;
  FImagesWidth := AValue;
  for i := 0 to FButtonList.Count - 1 do
    TCustomCheckControlEx(FButtonList[i]).ImagesWidth := FImagesWidth;
end;

procedure TCustomCheckControlGroupEx.SetItems(const AValue: TStrings);
begin
  if (AValue <> FItems) then
  begin
    FItems.Assign(AValue);
    UpdateItems;
    UpdateControlsPerLine;
  end;
end;

procedure TCustomCheckControlGroupEx.SetReadOnly(const AValue: Boolean);
var
  i: Integer;
begin
  if AValue = FReadOnly then exit;
  FReadOnly := AValue;
  for i := 0 to FButtonList.Count -1 do
    TCustomCheckControlEx(FButtonList[i]).ReadOnly := FReadOnly;
end;

procedure TCustomCheckControlGroupEx.UpdateAll;
begin
  UpdateItems;
  UpdateControlsPerLine;
  OwnerFormDesignerModified(Self);
end;

procedure TCustomCheckControlGroupEx.UpdateControlsPerLine;
var
  newControlsPerLine: LongInt;
begin
  if ChildSizing.Layout = cclLeftToRightThenTopToBottom then
    newControlsPerLine := Max(1, FColumns)
  else
    newControlsPerLine := Max(1, Rows);
  ChildSizing.ControlsPerLine := NewControlsPerLine;
end;

procedure TCustomCheckControlGroupEx.UpdateInternalObjectList;
begin
  UpdateItems;
end;

procedure TCustomCheckControlGroupEx.UpdateTabStops;
var
  i: Integer;
  btn: TCustomCheckControlEx;
begin
  for i := 0 to FButtonList.Count - 1 do
  begin
    btn := TCustomCheckControlEx(FButtonList[i]);
    btn.TabStop := btn.Checked;
  end;
end;

{==============================================================================}
{                           TRadioGroupExStringList                            }
{==============================================================================}

type
  TRadioGroupExStringList = class(TStringList)
  private
    FRadioGroup: TCustomRadioGroupEx;
  protected
    procedure Changed; override;
  public
    constructor Create(ARadioGroup: TCustomRadioGroupEx);
    procedure Assign(Source: TPersistent); override;
  end;

constructor TRadioGroupExStringList.Create(ARadioGroup: TCustomRadioGroupEx);
begin
  inherited Create;
  FRadioGroup := ARadioGroup;
end;

procedure TRadioGroupExStringList.Assign(Source: TPersistent);
var
  savedIndex: Integer;
begin
  savedIndex := FRadioGroup.ItemIndex;
  inherited Assign(Source);
  if savedIndex < Count then FRadioGroup.ItemIndex := savedIndex;
end;

procedure TRadioGroupExStringList.Changed;
begin
  inherited Changed;
  if (UpdateCount = 0) then
    FRadioGroup.UpdateAll
  else
    FRadioGroup.UpdateInternalObjectList;
  FRadioGroup.FLastClickedItemIndex := FRadioGroup.FItemIndex;
end;


{==============================================================================}
{                           TCustomRadioGroupEx                                }
{==============================================================================}

constructor TCustomRadioGroupEx.Create(AOwner: TComponent);
begin
  inherited;
  FItems := TRadioGroupExStringList.Create(Self);
  FItemIndex := -1;
  FLastClickedItemIndex := -1;
end;

procedure TCustomRadioGroupEx.Changed(Sender: TObject);
begin
  CheckItemIndexChanged;
end;

procedure TCustomRadioGroupEx.CheckItemIndexChanged;
begin
  if FCreatingWnd or FUpdatingItems then
    exit;
  if [csLoading,csDestroying]*ComponentState<>[] then exit;
  UpdateRadioButtonStates;
  if [csDesigning]*ComponentState<>[] then exit;
  if FLastClickedItemIndex=FItemIndex then exit;
  FLastClickedItemIndex:=FItemIndex;
  EditingDone;
  // for Delphi compatibility: OnClick should be invoked, whenever ItemIndex
  // has changed
  if Assigned (FOnClick) then FOnClick(Self);
  // And a better named LCL equivalent
  if Assigned (FOnSelectionChanged) then FOnSelectionChanged(Self);
end;

procedure TCustomRadioGroupEx.Clicked(Sender: TObject);
begin
  if FIgnoreClicks then exit;
  CheckItemIndexChanged;
end;

function TCustomRadioGroupEx.GetButtonCount: Integer;
var
  i: Integer;
begin
  Result := 0;
  for i := 0 to ControlCount-1 do
    if (Controls[i] is TCustomRadioButtonEx) and (Controls[i] <> FHiddenButton) then
      inc(Result);
end;

function TCustomRadioGroupEx.GetButtons(AIndex: Integer): TRadioButtonEx;
begin
  Result := Controls[AIndex] as TRadioButtonEx;
end;

procedure TCustomRadioGroupEx.InitializeWnd;

  procedure RealizeItemIndex;
  var
    i: Integer;
  begin
    if (FItemIndex <> -1) and (FItemIndex<FButtonList.Count) then
      TRadioButtonEx(FButtonList[FItemIndex]).Checked := true
    else if FHiddenButton<>nil then
      FHiddenButton.Checked := true;
    for i:=0 to FItems.Count-1 do begin
      TRadioButtonEx(FButtonList[i]).Checked := (FItemIndex = i);
    end;
  end;

begin
  if FCreatingWnd then RaiseGDBException('TCustomRadioGroup.InitializeWnd');
  FCreatingWnd := true;
  UpdateItems;
  inherited InitializeWnd;
  RealizeItemIndex;
  //debugln(['TCustomRadioGroup.InitializeWnd END']);
  FCreatingWnd := false;
end;

procedure TCustomRadioGroupEx.ItemKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);

  procedure MoveSelection(HorzDiff, VertDiff: integer);
  var
    Count: integer;
    StepSize: integer;
    BlockSize : integer;
    NewIndex : integer;
    WrapOffset: integer;
  begin
    if FReadOnly then
      exit;

    Count := FButtonList.Count;
    if FColumnLayout = clHorizontalThenVertical then begin
      //add a row for ease wrapping
      BlockSize := Columns * (Rows+1);
      StepSize := HorzDiff + VertDiff * Columns;
      WrapOffSet := VertDiff;
    end
    else begin
      //add a column for ease wrapping
      BlockSize := (Columns+1) * Rows;
      StepSize := HorzDiff * Rows + VertDiff;
      WrapOffSet := HorzDiff;
    end;
    NewIndex := ItemIndex;
    repeat
      Inc(NewIndex, StepSize);
      if (NewIndex >= Count) or (NewIndex < 0) then begin
        NewIndex := (NewIndex + WrapOffSet + BlockSize) mod BlockSize;
        // Keep moving in the same direction until in valid range
        while NewIndex >= Count do
           NewIndex := (NewIndex + StepSize) mod BlockSize;
      end;
    until (NewIndex = ItemIndex) or TCustomCheckControlEx(FButtonList[NewIndex]).Enabled;
    ItemIndex := NewIndex;
    TCustomCheckControlEx(FButtonList[ItemIndex]).SetFocus;
    Key := 0;
  end;

begin
  if Shift=[] then begin
    case Key of
      VK_LEFT: MoveSelection(-1,0);
      VK_RIGHT: MoveSelection(1,0);
      VK_UP: MoveSelection(0,-1);
      VK_DOWN: MoveSelection(0,1);
    end;
  end;
  if Key <> 0 then
    KeyDown(Key, Shift);
end;

procedure TCustomRadioGroupEx.ReadState(AReader: TReader);
begin
  FReading := True;
  inherited ReadState(AReader);
  FReading := False;
  if (FItemIndex < -1) or (FItemIndex >= FItems.Count) then
    FItemIndex := -1;
  FLastClickedItemIndex := FItemIndex;
end;

procedure TCustomRadioGroupEx.SetItemIndex(const AValue: integer);
var
  oldItemIndex: LongInt;
  oldIgnoreClicks: Boolean;
begin
  if (AValue = FItemIndex) or FReadOnly then exit;

  // needed later if handle isn't allocated
  oldItemIndex := FItemIndex;

  if FReading then
    FItemIndex := AValue
  else begin
    if (AValue < -1) or (AValue >= FItems.Count) then
      raise Exception.CreateFmt(rsIndexOutOfBounds, [ClassName, AValue, FItems.Count-1]);

    if HandleAllocated then
    begin
      // the radiobuttons are grouped by the widget interface
      // and some does not allow to uncheck all buttons in a group
      // Therefore there is a hidden button
      FItemIndex := AValue;
      oldIgnoreClicks := FIgnoreClicks;
      FIgnoreClicks := true;
      try
        if (FItemIndex <> -1) then
          TCustomCheckControlEx(FButtonList[FItemIndex]).Checked := true
        else
          FHiddenButton.Checked := true;
        // uncheck old radiobutton
        if (OldItemIndex <> -1) then begin
          if (OldItemIndex >= 0) and (OldItemIndex < FButtonList.Count) then
            TCustomCheckControlEx(FButtonList[OldItemIndex]).Checked := false
        end else
          FHiddenButton.Checked := false;
      finally
        FIgnoreClicks := OldIgnoreClicks;
      end;
      // this has automatically unset the old button. But they do not recognize
      // it. Update the states.
      CheckItemIndexChanged;
      UpdateTabStops;
      OwnerFormDesignerModified(Self);
    end else
    begin
      FItemIndex := AValue;

      // maybe handle was recreated. issue #26714
      FLastClickedItemIndex := -1;

      // trigger event to be delphi compat, even if handle isn't allocated.
      // issue #15989
      if (AValue <> oldItemIndex) and not FCreatingWnd then
      begin
        if Assigned(FOnClick) then FOnClick(Self);
        if Assigned(FOnSelectionChanged) then FOnSelectionChanged(Self);
        FLastClickedItemIndex := FItemIndex;
      end;
    end;
  end;
end;

procedure TCustomRadioGroupEx.UpdateItems;
var
  i: integer;
  button: TCustomCheckControlEx;
begin
  if FUpdatingItems then exit;
  FUpdatingItems := true;
  try
    // destroy radiobuttons, if there are too many
    while FButtonList.Count > FItems.Count do
    begin
      TObject(FButtonList[FButtonList.Count-1]).Free;
      FButtonList.Delete(FButtonList.Count-1);
    end;

    // create as many TRadioButtons as needed
    while (FButtonList.Count < FItems.Count) do
    begin
      button := TRadioButtonEx.Create(Self);
      with TCustomCheckControlEx(button) do
      begin
        Name := 'RadioButtonEx' + IntToStr(FButtonList.Count);
        OnClick := @Self.Clicked;
        OnChange := @Self.Changed;
        OnEnter := @Self.ItemEnter;
        OnExit := @Self.ItemExit;
        OnKeyDown := @Self.ItemKeyDown;
        OnKeyUp := @Self.ItemKeyUp;
        OnKeyPress := @Self.ItemKeyPress;
        OnUTF8KeyPress := @Self.ItemUTF8KeyPress;
        ParentFont := True;
        ReadOnly := Self.ReadOnly;
        BorderSpacing.CellAlignHorizontal := ccaLeftTop;
        BorderSpacing.CellAlignVertical := ccaCenter;
        ControlStyle := ControlStyle + [csNoDesignSelectable];
      end;
      FButtonList.Add(button);
    end;
    if FHiddenButton = nil then begin
      FHiddenButton := TRadioButtonEx.Create(nil);
      with FHiddenButton do
      begin
        Name := 'HiddenRadioButton';
        Visible := False;
        ControlStyle := ControlStyle + [csNoDesignSelectable, csNoDesignVisible];
      end;
    end;

    if (FItemIndex >= FItems.Count) and not (csLoading in ComponentState) then
      FItemIndex := FItems.Count-1;

    if FItems.Count > 0 then
    begin
      // to reduce overhead do it in several steps

      // assign Caption and then Parent
      for i:=0 to FItems.Count-1 do
      begin
        button := TCustomCheckControlEx(FButtonList[i]);
        button.Caption := FItems[i];
        button.Parent := Self;
      end;
      FHiddenButton.Parent := Self;

      // the checked and unchecked states can be applied only after all other
      for i := 0 to FItems.Count-1 do
      begin
        button := TCustomCheckControlEx(FButtonList[i]);
        button.Checked := (i = FItemIndex);
        button.Visible := true;
      end;

      //FHiddenButton must remain the last item in Controls[], so that Controls[] is in sync with Items[]
      Self.RemoveControl(FHiddenButton);
      Self.InsertControl(FHiddenButton);
      if HandleAllocated then
        FHiddenButton.HandleNeeded;
      FHiddenButton.Checked := (FItemIndex = -1);
      UpdateTabStops;
    end;
  finally
    FUpdatingItems := false;
  end;
end;

procedure TCustomRadioGroupEx.UpdateRadioButtonStates;
var
  i: Integer;
begin
  if FReadOnly then
    exit;

  FItemIndex := -1;
  FHiddenButton.Checked;
  for i:=0 to FButtonList.Count-1 do
    if TCustomRadioButtonEx(FButtonList[i]).Checked then FItemIndex := i;
  UpdateTabStops;
end;


{==============================================================================}
{                       TCheckGroupExStringList                                }
{==============================================================================}

type
  TCheckGroupExStringList = class(TStringList)
  private
    FCheckGroup: TCustomCheckGroupEx;
    procedure RestoreCheckStates(const AStates: TByteDynArray);
    procedure SaveCheckStates(out AStates: TByteDynArray);
  protected
    procedure Changed; override;
  public
    constructor Create(ACheckGroup: TCustomCheckGroupEx);
    procedure Delete(AIndex: Integer); override;
  end;


constructor TCheckGroupExStringList.Create(ACheckGroup: TCustomCheckGroupEx);
begin
  inherited Create;
  FCheckGroup := ACheckGroup;
end;

procedure TCheckGroupExStringList.Changed;
begin
  inherited Changed;
  if UpdateCount = 0 then
    FCheckGroup.UpdateAll
  else
    FCheckGroup.UpdateInternalObjectList;
end;

procedure TCheckGroupExStringList.Delete(AIndex: Integer);
// Deleting destroys the checked state of the items -> we must save and restore it
// Issue https://bugs.freepascal.org/view.php?id=34327.
var
  b: TByteDynArray;
  i: Integer;
begin
  SaveCheckStates(b);

  inherited Delete(AIndex);

  for i:= AIndex to High(b)-1 do b[i] := b[i+1];
  SetLength(b, Length(b)-1);
  RestoreCheckStates(b);
end;

procedure TCheckGroupExStringList.RestoreCheckStates(const AStates: TByteDynArray);
var
  i: Integer;
begin
  Assert(Length(AStates) = FCheckGroup.Items.Count);
  for i:=0 to FCheckgroup.Items.Count-1 do begin
    FCheckGroup.Checked[i] := AStates[i] and 1 <> 0;
    FCheckGroup.CheckEnabled[i] := AStates[i] and 2 <> 0;
  end;
end;

procedure TCheckGroupExStringList.SaveCheckStates(out AStates: TByteDynArray);
var
  i: Integer;
begin
  SetLength(AStates, FCheckgroup.Items.Count);
  for i:=0 to FCheckgroup.Items.Count-1 do begin
    AStates[i] := 0;
    if FCheckGroup.Checked[i] then inc(AStates[i]);
    if FCheckGroup.CheckEnabled[i] then inc(AStates[i], 2);
  end;
end;


{==============================================================================}
{                         TCustomCheckGroupEx                                  }
{==============================================================================}

constructor TCustomCheckGroupEx.Create(AOwner: TComponent);
begin
  inherited;
  FItems := TCheckGroupExStringList.Create(Self);
end;

procedure TCustomCheckGroupEx.Clicked(Sender: TObject);
var
  index: Integer;
begin
  index := FButtonList.IndexOf(Sender);
  if index < 0 then exit;
  DoClick(index);
end;

procedure TCustomCheckGroupEx.DefineProperties(Filer: TFiler);
begin
  inherited DefineProperties(Filer);
  Filer.DefineBinaryProperty('Data', @ReadData, @WriteData, FItems.Count > 0);
end;

procedure TCustomCheckGroupEx.DoClick(AIndex: integer);
begin
  if [csLoading,csDestroying, csDesigning] * ComponentState <> [] then exit;
  EditingDone;
  if Assigned(FOnItemClick) then FOnItemClick(Self, AIndex);
end;

function TCustomCheckGroupEx.GetButtonCount: Integer;
var
  i: Integer;
begin
  Result := 0;
  for i := 0 to ControlCount-1 do
    if (Controls[i] is TCustomCheckBoxEx) then
      inc(Result);
end;

function TCustomCheckGroupEx.GetButtons(AIndex: Integer): TCheckBoxEx;
begin
  Result := Controls[AIndex] as TCheckBoxEx;
end;

function TCustomCheckGroupEx.GetChecked(AIndex: Integer): Boolean;
begin
  if (AIndex < -1) or (AIndex >= FItems.Count) then
    RaiseIndexOutOfBounds(AIndex);
  Result := TCustomCheckControlEx(FButtonList[AIndex]).Checked;
end;

function TCustomCheckGroupEx.GetCheckEnabled(AIndex: integer): boolean;
begin
  if (AIndex < -1) or (AIndex >= FItems.Count) then
    RaiseIndexOutOfBounds(AIndex);
  Result := TCustomCheckControlEx(FButtonList[AIndex]).Enabled;
end;

procedure TCustomCheckGroupEx.Loaded;
begin
  inherited Loaded;
  UpdateItems;
end;

procedure TCustomCheckGroupEx.RaiseIndexOutOfBounds(AIndex: integer);
begin
  raise Exception.CreateFmt(rsIndexOutOfBounds, [ClassName, AIndex, FItems.Count - 1]);
end;

procedure TCustomCheckGroupEx.ReadData(Stream: TStream);
var
  ChecksCount: integer;
  Checks: string;
  i: Integer;
  v: Integer;
begin
  ChecksCount := ReadLRSInteger(Stream);
  if ChecksCount > 0 then begin
    SetLength(Checks, ChecksCount);
    Stream.ReadBuffer(Checks[1], ChecksCount);
    for i:=0 to ChecksCount-1 do begin
      v := ord(Checks[i+1]);
      Checked[i] := ((v and 1) > 0);
      CheckEnabled[i] := ((v and 2) > 0);
    end;
  end;
end;

procedure TCustomCheckGroupEx.SetChecked(AIndex: integer; const AValue: boolean);
begin
  if (AIndex < -1) or (AIndex >= FItems.Count) then
    RaiseIndexOutOfBounds(AIndex);
  // disable OnClick
  TCheckBox(FButtonList[AIndex]).OnClick := nil;
  // set value
  TCheckBox(FButtonList[AIndex]).Checked := AValue;
  // enable OnClick
  TCheckBox(FButtonList[AIndex]).OnClick := @Clicked;
end;

procedure TCustomCheckGroupEx.SetCheckEnabled(AIndex: integer;
  const AValue: boolean);
begin
  if (AIndex < -1) or (AIndex >= FItems.Count) then
    RaiseIndexOutOfBounds(AIndex);
  TCustomCheckControlEx(FButtonList[AIndex]).Enabled := AValue;
end;

procedure TCustomCheckGroupEx.UpdateItems;
var
  i: integer;
  btn: TCustomCheckControlEx;
begin
  if FUpdatingItems then exit;
  FUpdatingItems := true;
  try
    // destroy checkboxes, if there are too many
    while FButtonList.Count > FItems.Count do begin
      TObject(FButtonList[FButtonList.Count-1]).Free;
      FButtonList.Delete(FButtonList.Count-1);
    end;

    // create as many TCheckBoxes as needed
    while (FButtonList.Count < FItems.Count) do begin
      btn := TCheckBoxEx.Create(Self);
      with TCheckBoxEx(btn) do begin
        Name := 'CheckBoxEx' + IntToStr(FButtonList.Count);
        OnClick := @Self.Clicked;
        OnKeyDown := @Self.ItemKeyDown;
        OnKeyUp := @Self.ItemKeyUp;
        OnKeyPress := @Self.ItemKeyPress;
        OnUTF8KeyPress := @Self.ItemUTF8KeyPress;
        AutoSize := False;
        Parent := Self;
        ParentFont := true;
        ReadOnly := Self.ReadOnly;
        BorderSpacing.CellAlignHorizontal := ccaLeftTop;
        BorderSpacing.CellAlignVertical := ccaCenter;
        ControlStyle := ControlStyle + [csNoDesignSelectable];
      end;
      FButtonList.Add(btn);
    end;

    for i:=0 to FItems.Count-1 do begin
      btn := TCustomCheckControlEx(FButtonList[i]);
      btn.Caption := FItems[i];
    end;
  finally
    FUpdatingItems := false;
  end;
end;

procedure TCustomCheckGroupEx.WriteData(Stream: TStream);
var
  ChecksCount: integer;
  Checks: string;
  i: Integer;
  v: Integer;
begin
  ChecksCount := FItems.Count;
  WriteLRSInteger(Stream, ChecksCount);
  if ChecksCount > 0 then begin
    SetLength(Checks, ChecksCount);
    for i := 0 to ChecksCount-1 do begin
      v := 0;
      if Checked[i] then inc(v, 1);
      if CheckEnabled[i] then inc(v, 2);
      Checks[i+1] := chr(v);
    end;
    Stream.WriteBuffer(Checks[1], ChecksCount);
  end;
end;

end.