unit ExCheckCombo;

{$mode objfpc}{$H+}

interface

uses
  LCLIntf, LCLType, Classes, SysUtils, Controls, StdCtrls, ImgList, 
  GroupedEdit, EditBtn, CheckLst, Forms;

type
  { TCheckComboBoxEx }
  TCheckComboBoxEx = class;

  TCheckItemChange = procedure(Sender: TObject; AIndex: Integer) of object;
  TCheckComboBoxHintMode = (cbhmDefault, cbhmItems);
                
  TCheckComboBoxExStrings = class(TStringList)
  private
    function GetChecked(AIndex: Integer): Boolean;
    function GetEnabled(AIndex: Integer): Boolean;
    function GetObj(AIndex: Integer): TObject;
    function GetState(AIndex: Integer): TCheckboxState;
    procedure SetChecked(AIndex: Integer; AValue: Boolean);
    procedure SetEnabled(AIndex: Integer; AValue: Boolean);
    procedure SetObj(AIndex: Integer; AValue: TObject);
    procedure SetState(AIndex: Integer; AValue: TCheckboxState);
  protected
    procedure InsertItem(AIndex: Integer; const AText: string); override;
    procedure InsertItem(AIndex: Integer; const AText: string; Obj: TObject); override;
    
  public
    destructor Destroy; override;
    procedure Assign(ASource: TPersistent); override;
    procedure Clear; override;
    procedure Delete(AIndex: Integer); override;
    property Checked[AIndex: Integer]: Boolean read GetChecked write SetChecked;
    property Enabled[AIndex: Integer]: boolean read GetEnabled write SetEnabled;
    property Objects[AIndex: Integer]: TObject read GetObj write SetObj;
    property State[AIndex: Integer]: TCheckboxState read GetState write SetState;
  end;
  
  TCheckComboBoxExImageIndex = class(TPersistent)
  private
    FOwner: TCheckComboBoxEx;
    FImgIndex: array[0..7] of TImageIndex;
    function GetImgIndex(AIndex: Integer): TImageIndex;
    procedure SetImgIndex(AIndex: Integer; AValue: TImageIndex);
  public
    constructor Create(AOwner: TCheckComboBoxEx);
    property Owner: TCheckComboBoxEx read FOwner;
  published
    property NormalDown: TImageIndex index 0 read GetImgIndex write SetImgIndex default -1;
    property HotDown: TImageIndex index 1 read GetImgIndex write SetImgIndex default -1;
    property PressedDown: TImageIndex index 2 read GetImgIndex write SetImgIndex default -1;
    property DisabledDown: TImageIndex index 3 read GetImgIndex write SetImgIndex default -1;
    property NormalUp: TImageIndex index 4 read GetImgIndex write SetImgIndex default -1;
    property HotUp: TImageIndex index 5 read GetImgIndex write SetImgIndex default -1;
    property PressedUp: TImageIndex index 6 read GetImgIndex write SetImgIndex default -1;
    property DisabledUp: TImageIndex index 7 read GetImgIndex write SetImgIndex default -1;
  end;
    
  TCheckComboBoxEx = class(TCustomEditButton)
  private
    FAllowGrayed: Boolean;
    FAutoDropDown: Boolean;
    FButtonWidth: Integer;
    FCheckListBox: TCheckListBox;
    FCloseUpTime: TDateTime;
    FDelimiter: Char;
    FDropDownCount: Integer;
    FDropDownImageIndex: TCheckComboBoxExImageIndex;
    FDroppedDown: Boolean;
    FEscCancels: Boolean;
    FItemIndex: Integer;
    FItems: TCheckComboBoxExStrings;
    FItemWidth: Integer;
    FHintMode: TCheckComboBoxHintMode;
    FSavedChecks: array of Integer;
    FSorted: Boolean;
    FOnChange: TNotifyEvent;
    FOnCloseUp: TNotifyEvent;
    FOnDropDown: TNotifyEvent;
    FOnItemChange: TCheckItemChange;
    FOnItemClick: TCheckListClicked;
    procedure ButtonMouseDownHandler(Sender: TObject;
      AButton: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure CheckComboBoxFormCloseHandler(Sender: TObject; var CloseAction: TCloseAction);
    function GetButtonWidth: Integer;
    function GetChecked(AIndex: Integer): Boolean;
    function GetImages: TCustomImagelist;
    function GetImageWidth: Integer;
    function GetItemEnabled(AIndex: Integer): Boolean;
    function GetItems: TStrings;
    function GetState(AIndex: Integer): TCheckBoxState;
    procedure ItemClickHandler(Sender: TObject; AIndex: Integer);
    procedure ItemsToCheckListbox(ACheckListbox: TCheckListbox);
    procedure SetAllowGrayed(AValue: Boolean);
    procedure SetButtonWidth(AValue: Integer);
    procedure SetChecked(AIndex: Integer; const AValue: Boolean);
    procedure SetDelimiter(AValue: Char);
    procedure SetDropDownCount(AValue: Integer);
    procedure SetImages(AValue: TCustomImageList);
    procedure SetImageWidth(AValue: Integer);
    procedure SetItemEnabled(AIndex: Integer; const AValue: Boolean);
    procedure SetItemIndex(AValue: Integer);
    procedure SetItems(const AValue: TStrings);
    procedure SetItemWidth(AValue: Integer);
    procedure SetSorted(AValue: Boolean);
    procedure SetState(AIndex: Integer; const AValue: TCheckboxState);
  protected
    procedure ButtonClick; override;
    procedure CloseUp;
    function CreateBuddy: TControl; override;
    procedure CreateHandle; override;
    procedure DoAutoAdjustLayout(const AMode: TLayoutAdjustmentPolicy;
      const AXProportion, AYProportion: Double); override;
    procedure DoEnter; override;
    procedure DoChange; virtual;
    procedure DoItemChange(AIndex: Integer); virtual;
    procedure DoCloseUp; virtual;
    procedure DoDropDown; virtual;
    procedure DoItemClick(AIndex: Integer); virtual;
    procedure DoOnShowHint(HintInfo: PHintInfo); override;
    procedure EditKeyDown(var Key: Word; Shift: TShiftState); override;
    function GetBuddyClassType: TControlClass; override;
    function GetEditorClassType: TGEEditClass; override;
    procedure InvalidateButton; virtual;
    procedure RestoreInitialChecks;
    procedure SaveInitialChecks;
    procedure SetBiDiMode(AValue: TBiDiMode); override;
    procedure ShowPopup;
    procedure UpdateCaption;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function AddItem(AText: String; AChecked: Boolean = false; AEnabled: Boolean = true): Integer;
    procedure CheckAll(AState: TCheckBoxState; AAllowGrayed: Boolean = true; AAllowDisabled: Boolean = true);
    procedure Clear;
    procedure Toggle(AIndex: Integer);
    // public properties
    property Checked[AIndex: Integer]: Boolean read GetChecked write SetChecked;
    property DroppedDown: Boolean read FDroppedDown;
    property ItemEnabled[AIndex: Integer]: Boolean read GetItemEnabled write SetItemEnabled;
    property Text;
  published
    property AllowGrayed: Boolean read FAllowGrayed write SetAllowGrayed default false;
    property AutoDropDown: Boolean read FAutoDropDown write FAutoDropDown default false;
    property ButtonWidth: Integer read FButtonWidth write SetButtonWidth default -1;
    property Delimiter: char read FDelimiter write SetDelimiter default ';';
    property DropDownCount: Integer read FDropDownCount write SetDropDownCount default 8;
    property DropDownImageIndex: TCheckComboBoxExImageIndex read FDropDownImageIndex write FDropDownImageIndex;
    property EscCancels: Boolean read FEscCancels write FEscCancels default true;
    property HintMode: TCheckComboBoxHintMode read FHintMode write FHintMode default cbhmDefault;
    property Images: TCustomImageList read GetImages write SetImages;
    property ImageWidth: Integer read GetImageWidth write SetImageWidth default 0;
    property ItemIndex: Integer read FItemIndex write SetItemIndex default -1;
    property Items: TStrings read GetItems write SetItems;
    property ItemWidth: Integer read FItemWidth write FItemWidth default 0;
    property Sorted: Boolean read FSorted write SetSorted default false;
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
    property OnCloseUp: TNotifyEvent read FOnCloseUp write FOnCloseUp;
    property OnDropDown: TNotifyEvent read FOnDropDown write FOnDropDown;
    property OnItemChange: TCheckItemChange read FOnItemChange write FOnItemChange;
    property OnItemClick: TCheckListClicked read FOnItemClick write FOnItemClick;
    // inherited properties
    property Align;
    property Anchors;
    property AutoSize;
    property BiDiMode;
    property BorderSpacing;
    property Color;
    property Constraints;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property Font;
    property Hint;
    property ParentBidiMode;
    property ParentColor;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowHint;
    property Spacing default 0;
    property TabOrder;
    property TabStop;
    property TextHint;
    property Visible;  
    // inherited events
    property OnChangeBounds;
    property OnClick;
    property OnContextPopup;
    property OnDblClick;
    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 OnStartDrag;
  end;

  
implementation

uses
  Buttons, Themes, WSForms;

{ TCCBItem }     // CCB = CheckComboBox
type
  TCCBItem = class
    Caption: String;
    State: TCheckboxState;
    Enabled: Boolean;
    Obj: TObject;
    constructor Create;
  end;

constructor TCCBItem.Create;
begin
  State := cbUnchecked;
  Enabled := true;
end;


{ TCheckComboBoxExStrings }

destructor TCheckComboBoxExStrings.Destroy;
begin
  Clear;
  inherited;
end;

procedure TCheckComboBoxExStrings.Assign(ASource: TPersistent);
var
  i: Integer;
begin
  inherited Assign(ASource);
  if (ASource is TCheckComboBoxExStrings) then
    for i := 0 to Count-1 do
    begin
      State[i] := TCheckComboBoxExStrings(ASource).State[i];
      Enabled[i] := TCheckComboBoxExStrings(ASource).Enabled[i];
    end;
end;
    
procedure TCheckComboBoxExStrings.Clear;
var
  i: Integer;
begin
  for i := 0 to Count-1 do
    TCCBItem(inherited Objects[i]).Free;
  inherited;
end;

procedure TCheckComboBoxExStrings.Delete(AIndex: Integer);
begin
  TCCBItem(inherited Objects[AIndex]).Free;
  inherited;
end;

function TCheckComboBoxExStrings.GetChecked(AIndex: Integer): Boolean;
begin
  Result := GetState(AIndex) = cbChecked;
end;

function TCheckComboBoxExStrings.GetEnabled(AIndex: Integer): Boolean;
begin
  Result := TCCBItem(inherited Objects[AIndex]).Enabled;
end;

function TCheckComboBoxExStrings.GetObj(AIndex: Integer): TObject;
begin
  Result := TCCBItem(inherited Objects[AIndex]).Obj;
end;

function TCheckComboBoxExStrings.GetState(AIndex: Integer): TCheckboxState;
begin
  Result := TCCBItem(inherited Objects[AIndex]).State;
end;

procedure TCheckComboBoxExStrings.InsertItem(AIndex: Integer; const AText: String);
begin
  inherited InsertItem(AIndex, AText, TCCBItem.Create);
end;
  
procedure TCheckComboBoxExStrings.InsertItem(AIndex: Integer; const AText: String;
  Obj: TObject);
begin
  InsertItem(AIndex, AText);
  TCCBItem(inherited Objects[AIndex]).Obj := Obj;
end;
  
procedure TCheckComboBoxExStrings.SetChecked(AIndex: Integer; AValue: Boolean);
begin
  if AValue then
    SetState(AIndex, cbChecked)
  else
    SetState(AIndex, cbUnchecked);
end;

procedure TCheckComboBoxExStrings.SetEnabled(AIndex: Integer; AValue: Boolean);
var
  item: TCCBItem;
begin
  item := TCCBItem(inherited Objects[AIndex]);
  if item.Enabled = AValue then exit;
  item.Enabled := AValue;
end;

procedure TCheckComboBoxExStrings.SetObj(AIndex: Integer; AValue: TObject);
begin
  TCCBItem(inherited Objects[AIndex]).Obj := AValue;
end;

procedure TCheckComboBoxExStrings.SetState(AIndex: Integer; AValue: TCheckBoxState);
var
  item: TCCBItem;
begin
  item := TCCBItem(inherited Objects[AIndex]);
  if item.State = AValue then exit;
  item.State := AValue;
end;


{ TCCBEdit }     
type
  TCCBEdit = class(TEbEdit)
  protected
    procedure DoOnShowHint(HintInfo: PHintInfo); override;
  end;

procedure TCCBEdit.DoOnShowHint(HintInfo: PHintInfo); 
begin
  (Parent as TCheckComboBoxEx).DoOnShowHint(HintInfo);
end;

  
{ TCCBButton }
type
  TCCBButton = class(TSpeedButton)
  private
    procedure SetHotImageIndex;
    procedure SetImageIndex(AEnabledIndex, ADisabledIndex: Integer; Up: Boolean);
    procedure SetNormalImageIndex;
    procedure SetPressedImageIndex;
  protected
    procedure DoOnShowHint(HintInfo: PHintInfo); override;
    procedure MouseEnter; override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;     
    procedure MouseLeave; override;
    procedure Paint; override;
  public
    procedure UpdateImageIndex;
  end;

procedure TCCBButton.DoOnShowHint(HintInfo: PHintInfo); 
begin
  (Parent as TCheckComboBoxEx).DoOnShowHint(HintInfo);
end;
  
procedure TCCBButton.MouseEnter;
begin
  inherited;
  SetHotImageIndex;
end;

procedure TCCBButton.MouseDown(Button: TMouseButton; Shift: TShiftState;
  X,Y: Integer);
begin
  inherited;
  if (Button = mbLeft) and (Shift = []) then
    SetPressedImageIndex;
end;

procedure TCCBButton.MouseLeave;
begin
  inherited;
  SetNormalImageIndex;
end;

procedure TCCBButton.Paint;
const
  DETAILS: array[TButtonState] of TThemedComboBox = (
    tcDropDownButtonNormal,   // bsUp = button is up
    tcDropDownButtonDisabled, // bsDisabled = button is disabled
    tcDropDownButtonPressed,  // bsPressed = button is down
    tcDropDownButtonPressed,  // (not used)
    tcDropDownButtonHot       // bsHot = mouse is under mouse
  );
var
  detail: TThemedElementDetails;
begin
  inherited Paint;
  if Images = nil then
  begin
    detail := ThemeServices.GetElementDetails(DETAILS[FState]);
    ThemeServices.DrawElement(Canvas.Handle, detail, ClientRect);
  end;
end;
  
procedure TCCBButton.SetHotImageIndex;
begin
  with Parent as TCheckComboBoxEx do
    if DroppedDown then
      Self.SetImageIndex(DropDownImageIndex.HotUp, DropDownImageIndex.DisabledUp, true)
    else
      Self.SetImageIndex(DropDownImageIndex.HotDown, DropDownImageIndex.DisabledDown, false);
end;

procedure TCCBButton.SetImageIndex(AEnabledIndex, ADisabledIndex: Integer;
  Up: Boolean);
var
  idx: Integer;
begin
  if Parent.Enabled then
  begin
    if AEnabledIndex > -1 then
      ImageIndex := AEnabledIndex
    else
      ImageIndex := (Parent as TCheckComboBoxEx).DropDownImageIndex.NormalDown;
  end else
  begin
    if ADisabledIndex > -1 then
      ImageIndex := ADisabledIndex
    else
    begin
      idx := -1;
      if Up then
        idx := (Parent as TCheckComboBoxEx).DropDownImageIndex.NormalUp;
      if idx = -1 then 
        idx := (Parent as TCheckComboBoxEx).DropDownImageIndex.NormalDown;
      ImageIndex := idx;
    end;
  end;
end;

procedure TCCBButton.SetNormalImageIndex;
begin
  with Parent as TCheckComboBoxEx do
    if DroppedDown then
      Self.SetImageIndex(DropDownImageIndex.NormalUp, DropDownImageIndex.DisabledUp, true)
    else
      Self.SetImageIndex(DropDownImageIndex.NormalDown, DropDownImageIndex.DisabledDown, false);
end;
  
procedure TCCBButton.SetPressedImageIndex;
begin
  with Parent as TCheckComboBoxEx do
    if DroppedDown then
      Self.SetImageIndex(DropDownImageIndex.PressedUp, DropDownImageIndex.DisabledUp, true)
    else
      Self.SetImageIndex(DropDownImageIndex.PressedDown, DropDownImageIndex.DisabledDown, false);
end;

procedure TCCBButton.UpdateImageIndex;
var
  P: TPoint;
begin
  P := ScreenToClient(Mouse.CursorPos);
  if PtInRect(BoundsRect, P) then
    SetHotImageIndex
  else
    SetNormalImageIndex;
end;


{ TCheckComboBoxForm }
type
  TCheckComboBoxForm = class(TForm)
  private
    FCaller: TControl;
    FCheckListBox: TCheckListBox;
    FDropDownCount: Integer;
  protected
    procedure ActivateDoubleBuffered;
    procedure DblClickHandler(Sender: TObject);
    procedure Deactivate; override;
    procedure KeepInView(APopupOrigin: TPoint);    
    procedure KeyDownHandler(Sender: TObject; var Key: Word; Shift: TShiftState); 
    procedure MeasureHeight(out AHeight: Integer);
  public
    constructor CreateNew(AOwner: TComponent; Num: Integer = 0); override;
    procedure Initialize(ADropdownCount: Integer; APosition: TPoint; AWidth: Integer);
  end;

constructor TCheckComboBoxForm.CreateNew(AOwner: TComponent; Num: Integer = 0);
begin
  inherited CreateNew(AOwner, Num);
  BorderStyle := bsNone;
  FCheckListbox := TCheckListbox.Create(self);
  FCheckListbox.Align := alClient;
  FCheckListbox.Parent := self;
  FCheckListbox.OnDblClick := @DblClickHandler;
  FCheckListbox.OnKeyDown := @KeyDownHandler;
  FDropDownCount := 8;
end;
  
procedure TCheckComboBoxForm.ActivateDoubleBuffered;
begin
  DoubleBuffered := TWSCustomFormClass(WidgetSetClass).GetDefaultDoubleBuffered;
end;

procedure TCheckComboBoxForm.Deactivate;
begin
  Close;
end;

procedure TCheckComboboxForm.DblClickHandler(Sender: TObject);
begin
  {$IFDEF LCLCocoa}
  Free;
  {$ELSE}
  Close;
  {$ENDIF}
end;

procedure TCheckComboBoxForm.Initialize(ADropDownCount: Integer;
  APosition: TPoint; AWidth: Integer);
var
  h: Integer;
begin
  FDropDownCount := ADropDownCount;
  MeasureHeight(h);
  SetBounds(APosition.X, APosition.Y, AWidth, h);
  KeepInView(APosition);
  AutoScroll := FCheckListBox.Items.Count > FDropDownCount;
end;
  
procedure TCheckComboBoxForm.KeepInView(APopupOrigin: TPoint);
var
  R: TRect;
begin
  R := Screen.MonitorFromPoint(APopupOrigin).WorkAreaRect; // take care of taskbar
  
  if APopupOrigin.X + Width > R.Right then
    Left := R.Right - Width
  else if APopupOrigin.X < R.Left then
    Left := R.Left
  else
    Left := APopupOrigin.X;
  
  if APopupOrigin.Y + Height > R.Bottom then
  begin
    if Assigned(FCaller) then
      Top := APopupOrigin.Y - FCaller.Height - Height
    else
      Top := R.Bottom - Height;
  end else if APopupOrigin.Y < R.Top then
    Top := R.Top
  else
    Top := APopupOrigin.Y;
  
  if Left < R.Left then Left := 0;
  if Top < R.Top then Top := 0;
end;
  
procedure TCheckComboBoxForm.KeyDownHandler(Sender: TObject; var Key: Word; 
  Shift: TShiftState); 
begin
  inherited;
  if (Key = VK_ESCAPE) and TCheckComboBoxEx(FCaller).EscCancels then
    TCheckComboBoxEx(FCaller).RestoreInitialChecks;
  if (Key = VK_ESCAPE) or (Key = VK_TAB) or (Key = VK_RETURN) or
     (((Key = VK_UP) or (Key = VK_DOWN)) and (Shift = [ssAlt])) 
  then
    Close;
end;

procedure TCheckComboBoxForm.MeasureHeight(out AHeight: Integer);
var
  i: Integer;
  h: Integer = 0;
begin
  AHeight := 0;
  for i := 0 to FCheckListbox.Items.Count-1 do
    if i < FDropDownCount then
    begin
      FCheckListbox.MeasureItem(i, h);
      inc(AHeight,h);
    end;
  inc(AHeight, 6);
end;
    

{ TCheckComboBoxExImageIndex }

constructor TCheckComboBoxExImageIndex.Create(AOwner: TCheckComboBoxEx);
var
  i: Integer;
begin
  inherited Create;
  FOwner := AOwner;
  for i := 0 to High(FImgIndex) do
    FImgIndex[i] := -1;
end;

function TCheckComboBoxExImageIndex.GetImgIndex(AIndex: Integer): TImageIndex;
begin
  Result := FImgIndex[AIndex];
end;

procedure TCheckComboBoxExImageIndex.SetImgIndex(AIndex: Integer; AValue: TImageIndex);
begin
  if FImgIndex[AIndex] = AValue then
    exit;
  FImgIndex[AIndex] := AValue;
  TCheckComboBoxEx(FOwner).InvalidateButton;
end;


{ TCheckComboBoxEx }

constructor TCheckComboBoxEx.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Spacing := 0;
  Button.Flat := true;
  FButtonWidth := -1;
  FDelimiter := ';';
  FDropDownCount := 8;
  FDropDownImageIndex := TCheckComboBoxExImageIndex.Create(self);
  ImageIndex := FDropDownImageIndex.NormalDown;
  FItems := TCheckComboBoxExStrings.Create;
  FEscCancels := true;
end;

destructor TCheckComboBoxEx.Destroy;
begin
  FItems.Free;
  FDropdownImageIndex.Free;
  inherited Destroy;
end;

function TCheckComboBoxEx.AddItem(AText: String; AChecked: Boolean = false; 
  AEnabled: Boolean = true): Integer;
var
  j: Integer;
begin
  j := FItems.Add(AText);
  if AChecked then
    FItems.State[j] := cbChecked
  else
    FItems.State[j] := cbUnchecked;
  FItems.Enabled[j] := AEnabled;

  if FDroppedDown then
    with FCheckListBox do
    begin
      Result := Items.Add(AText);
      Checked[Result] := AChecked;
      ItemEnabled[Result] := AEnabled;
    end;
  
  UpdateCaption;
end;

procedure TCheckComboBoxEx.ButtonClick;
begin
  {
  if DroppedDown then
    CloseUp
  else
    ShowPopup;
}
end;

procedure TCheckComboBoxEx.ButtonMouseDownHandler(Sender: TObject;
  AButton: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if DroppedDown then
    CloseUp
  else
    ShowPopup;
end;

procedure TCheckComboBoxEx.CheckAll(AState: TCheckBoxState;
  AAllowGrayed: Boolean = true; AAllowDisabled: Boolean = true);
var
  i: Integer;
begin
  for i := 0 to FItems.Count-1 do
  begin
    if not FItems.Enabled[i] and not AAllowDisabled then
      Continue;
    if AState in [cbChecked, cbUnchecked] then
      FItems.State[i] := AState
    else if (AState = cbGrayed) then
    begin
      if AAllowGrayed then
        FItems.State[i] := cbGrayed
      else
        FItems.State[i] := cbUnChecked;
    end;
  end;

  if FDroppedDown then
    with FCheckListBox do
      CheckAll(AState, AAllowGrayed, AAllowDisabled);
  
  UpdateCaption;
  DoChange;
end;

procedure TCheckComboBoxEx.CheckComboBoxFormCloseHandler(Sender: TObject;
  var CloseAction: TCloseAction);
begin
  CloseUp;
  FCheckListBox := nil;
  CloseAction := caFree;
end;

procedure TCheckComboBoxEx.Clear;
begin
  FItems.Clear;
  
  if FDroppedDown then
    FCheckListBox.Clear;
  UpdateCaption;
  DoChange;
end;

procedure TCheckComboBoxEx.CloseUp;
begin
  FDroppedDown := false;
  InvalidateButton;
  UpdateCaption;
  DoCloseUp;
  FCloseUpTime := Now();
end;

function TCheckComboBoxEx.CreateBuddy: TControl;
begin
  Result := inherited;
  TSpeedButton(Result).OnMouseDown := @ButtonMouseDownHandler;
end;

procedure TCheckComboBoxEx.CreateHandle;
begin
  inherited;
  if FButtonWidth < 0 then
    Buddy.Width := GetSystemMetrics(SM_CXVSCROLL);
end;

procedure TCheckComboBoxEx.DoAutoAdjustLayout(const AMode: TLayoutAdjustmentPolicy;
  const AXProportion, AYProportion: Double);
begin
  inherited;
  if AMode in [lapAutoAdjustWithoutHorizontalScrolling, lapAutoAdjustForDPI] then
  begin
    if FButtonWidth < 0 then
      Buddy.Width := GetSystemMetrics(SM_CXVSCROLL)
    else
    begin
      FButtonWidth := round(AXProportion * FButtonWidth);
      Buddy.Width := FButtonWidth;
    end;
  end;
end;

procedure TCheckComboBoxEx.DoChange;
begin
  if Assigned(FOnChange) then FOnChange(self);
end;

procedure TCheckComboBoxEx.DoCloseUp;
begin
  if Assigned(FOnCloseUp) then FOnCloseUp(self);
end;
  
procedure TCheckComboBoxEx.DoDropDown;
begin
  if Assigned(FOnDropDown) then FOnDropDown(self);
end;

procedure TCheckComboBoxEx.DoEnter;
begin
  inherited;
  if FAutoDropDown then ShowPopup;
end;

procedure TCheckComboBoxEx.DoItemChange(AIndex: Integer);
begin
  if Assigned(FOnItemChange) then
    FOnItemChange(self, AIndex);
end;

procedure TCheckComboBoxEx.DoItemClick(AIndex: Integer);
begin
  if Assigned(FOnItemClick) then
    FOnItemClick(self, AIndex);
end;

procedure TCheckComboBoxEx.DoOnShowHint(HintInfo: PHintInfo); 
begin
  if FHintMode = cbhmItems then
    HintInfo^.HintStr := StringReplace(Text, FDelimiter+' ', LineEnding, [rfReplaceAll]);
  inherited;
end;

procedure TCheckComboBoxEx.EditKeyDown(var Key: Word; Shift: TShiftState); 
begin
  if (Key = VK_TAB) then
  begin
    inherited;
    exit;
  end;
  
  if (Key = VK_RETURN) or (Key = VK_SPACE) or 
     (((Key = VK_DOWN) or (Key = VK_UP)) and (Shift = [ssAlt])) 
  then
    ShowPopup;
  
  // Kill all other keys
  Key := 0;
end;

function TCheckComboBoxEx.GetBuddyClassType: TControlClass;
begin
  Result := TCCBButton;
end;

function TCheckComboBoxEx.GetButtonWidth: Integer;
begin
  Result := Buddy.Width;
end;

function TCheckComboBoxEx.GetChecked(AIndex: Integer): Boolean;
begin
  Result := FItems.Checked[AIndex];
end;

function TCheckComboBoxEx.GetEditorClassType: TGEEditClass; 
begin
  Result := TCCBEdit;
end;

function TCheckComboBoxEx.GetImages: TCustomImageList;
begin
  Result := (Buddy as TSpeedButton).Images;
end;

function TCheckComboBoxEx.GetImageWidth: Integer;
begin
  Result := (Buddy as TSpeedButton).ImageWidth;
end;

function TCheckComboBoxEx.GetItemEnabled(AIndex: Integer): Boolean;
begin
  Result := FItems.Enabled[AIndex];
end;

function TCheckComboBoxEx.GetItems: TStrings;
begin
  Result := FItems;
end;

function TCheckComboBoxEx.GetState(AIndex: Integer): TCheckboxState;
begin
  Result := FItems.State[AIndex];
end;

procedure TCheckComboBoxEx.InvalidateButton;
begin
  with (Buddy as TCCBButton) do
    UpdateImageIndex;
end;

procedure TCheckComboBoxEx.ItemClickHandler(Sender: TObject; AIndex: Integer);
begin
  UpdateCaption;
  DoChange;
  DoItemChange(AIndex);
  DoItemClick(AIndex);
end;

procedure TCheckComboBoxEx.ItemsToCheckListbox(ACheckListbox: TCheckListbox);
var
  i: Integer;
begin
  ACheckListbox.Items.BeginUpdate;
  try
    ACheckListbox.Items.Clear;
    for i := 0 to FItems.Count-1 do
    begin
      ACheckListbox.Items.Add(FItems[i]);
      ACheckListbox.State[i] := FItems.State[i];
      ACheckListbox.ItemEnabled[i] := FItems.Enabled[i];
    end;
    if (FItemIndex > -1) and (FItemIndex < ACheckListbox.Count) then
      ACheckListbox.ItemIndex := FItemIndex;
  finally
    ACheckListbox.Items.EndUpdate;
  end;
end;
    
procedure TCheckComboBoxEx.RestoreInitialChecks;
var
  i: Integer;
begin
  for i := 0 to FItems.Count-1 do
    FItems.Checked[i] := false;
  for i := 0 to High(FSavedChecks) do
    FItems.Checked[FSavedChecks[i]] := true;
end;
  
procedure TCheckComboBoxEx.SaveInitialChecks;
var
  i, n: Integer;
begin
  SetLength(FSavedChecks, FItems.Count);
  n := 0;
  for i := 0 to FItems.Count-1 do
    if FItems.Checked[i] then 
    begin
      FSavedChecks[n] := i;
      inc(n);
    end;
  SetLength(FSavedChecks, n);
end;

procedure TCheckComboBoxEx.SetAllowGrayed(AValue: Boolean);
begin
  if FAllowGrayed = AValue then exit;
  FAllowGrayed := AValue;
  
  if FDroppedDown then
    FCheckListBox.AllowGrayed := AValue;
end;

procedure TCheckComboBoxEx.SetBiDiMode(AValue: TBiDiMode);
begin
  if AValue = BiDiMode then exit;
  
  inherited SetBiDiMode(AValue);
  
  if FDroppedDown then
    FCheckListBox.BiDiMode := AValue;
  if IsRightToLeft then
    Buddy.Align := alLeft 
  else
    Buddy.Align := alRight;
  
  UpdateCaption;
end;

procedure TCheckComboBoxEx.SetButtonWidth(AValue: Integer);
begin
  if FButtonWidth = AValue then
    exit;
  FButtonWidth := AValue;
  if FButtonWidth < 0 then
    Buddy.Width := GetSystemMetrics(SM_CXVSCROLL)
  else
    Buddy.Width := FButtonWidth;
end;

procedure TCheckComboBoxEx.SetChecked(AIndex: Integer; const AValue: Boolean);
begin
  if FItems.Checked[AIndex] = AValue then
    exit;
  if AValue then
    FItems.State[AIndex] := cbChecked
  else
    FItems.State[AIndex] := cbUnchecked;
  
  if FDroppedDown then
    FCheckListBox.Checked[AIndex] := AValue;
  
  UpdateCaption;
  DoChange;
  DoItemChange(AIndex);
end;

procedure TCheckComboBoxEx.SetDelimiter(AValue: Char);
begin
  if FDelimiter = AValue then exit;
  FDelimiter := AValue;
  UpdateCaption;
end;

procedure TCheckComboBoxEx.SetDropDownCount(AValue: Integer);
begin
  if (FDropDownCount = AValue) or (AValue <= 0) then exit;
  FDropDownCount := AValue;
end;

procedure TCheckComboBoxEx.SetImages(AValue: TCustomImageList);
begin
  (Buddy as TSpeedButton).Images := AValue;
end;

procedure TCheckComboBoxEx.SetImageWidth(AValue: Integer);
begin
  (Buddy as TSpeedButton).ImageWidth := AValue;
end;

procedure TCheckComboBoxEx.SetItemEnabled(AIndex: Integer; const AValue: Boolean);
begin
  if FItems.Enabled[AIndex] = AValue then exit;
  FItems.Enabled[AIndex] := AValue;
  
  if FDroppedDown then
    FCheckListbox.ItemEnabled[AIndex] := AValue;
end;

procedure TCheckComboBoxEx.SetItemIndex(AValue: Integer);
begin
  if FItemIndex = AValue then exit;
  FItemIndex := AValue;
  if FDroppedDown then
    FCheckListBox.ItemIndex := AValue;
end;

procedure TCheckComboBoxEx.SetItems(const AValue: TStrings);
begin
  FItems.Assign(AValue);
  if FDroppedDown then
    FCheckListBox.Items.Assign(AValue);
end;

procedure TCheckComboBoxEx.SetItemWidth(AValue: Integer);
begin
  if FItemWidth = AValue then exit;
  if AValue < 0 then
    FItemWidth := 0
  else
    FItemWidth := AValue;
end;

procedure TCheckComboBoxEx.SetSorted(AValue: Boolean);
begin
  if AValue = FSorted then
    exit;
  
  FSorted := AValue;
  if FSorted then
    FItems.Sort;

  UpdateCaption;
end;

procedure TCheckComboBoxEx.SetState(AIndex: Integer; const AValue: TCheckBoxState);
begin
  if FItems.State[AIndex] = AValue then
    exit;
  
  FItems.State[AIndex] := AValue;
  if FDroppedDown then
    FCheckListbox.State[AIndex] := AValue;
  
  DoChange;
  DoItemChange(AIndex);
end;

procedure TCheckComboBoxEx.ShowPopup;
const
  MILLISECOND = 1.0/(24*60*60*100);
  DELAY = 10*MILLISECOND;
var
  PopupOrigin: TPoint;
  PopupWidth: Integer;
  F: TCheckComboBoxForm;
begin
  if FItems.Count = 0 then
    exit;

  if Now() - FCloseUpTime < DELAY then
    exit;
    
  F := TCheckComboBoxForm.CreateNew(Application);
  F.FCaller := Self;
  F.BiDiMode := BiDiMode;
  if DoubleBuffered then
    F.ActivateDoubleBuffered;
  F.FCheckListBox.AllowGrayed := FAllowGrayed;
  PopupOrigin := ControlToScreen(Point(0, Height));
  if FItemWidth > 0 then
    PopupWidth := FItemWidth
  else
    PopupWidth := Self.Width;
  ItemsToCheckListbox(F.FCheckListbox);
  F.Initialize(FDropDownCount, PopupOrigin, PopupWidth);
  SaveInitialChecks;
  F.FCheckListbox.OnItemClick := @ItemClickHandler;
  F.OnClose := @CheckComboBoxFormCloseHandler;
  F.Show;
  FDroppedDown := true;
  FCheckListbox := F.FCheckListbox;
  InvalidateButton;
  DoDropDown;
end;

procedure TCheckComboBoxEx.Toggle(AIndex: Integer);
begin
  SetChecked(AIndex, not GetChecked(AIndex));
end;

procedure TCheckComboBoxEx.UpdateCaption;
var
  i: Integer;
  s: String;
begin
  if FDroppedDown then
    for i := 0 to FCheckListBox.Items.Count - 1 do
      FItems.State[i] := FCheckListbox.State[i];
  
  s := '';
  if IsRightToLeft then
  begin
    for i := FItems.Count-1 downto 0 do
      if FItems.Checked[i] then
        s := s + FItems[i] + FDelimiter + ' '
  end else  
  begin
    for i := 0 to FItems.Count-1 do
      if FItems.Checked[i] then
        s := s + FItems[i] + FDelimiter + ' ';
  end;
  if s <> '' then
    SetLength(s, Length(s)-2);
  Text := s;
end;
  

end.