{ TColumnCombo is a text-only combobox that displays its dropdown items list in
  single phrase columns, which are parsed according to the (Char) Delimiter
  property.
  Column width in the dropdown is adjusted automatically to accomomodate the
  longest word/phrase in each column.
  The number of columns shown depends entirely on the number of delimiters found
  in each listed item, hence is an unpublished read-only property.
  The default delimiter is the comma.
  There is a display property -- commented out -- ShowColSeparators (False by
  default) which displays vertical lines between the listed columns. It seems ugly to me,
  so I disabled it, but you can re-enable it if you want.
  The ColumnMargin property allows for adjustment of all column widths by a fixed amount.

  H Page-Clark 2013


  License:

  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 ExCombo;

{$mode objfpc}{$H+}

interface

uses
  LCLIntf, LCLType, LazLoggerBase,
  Classes, SysUtils, Graphics, Types,
  StdCtrls, Controls, Forms;

type

  TColumnComboBoxEx = class(TCustomComboBox)
  private
    FColumnCount: Integer;
    FColumnMargin: Integer;
    FDelimiter: AnsiChar;
    FOffsets: TIntegerDynArray;
    FParser: TStringList;
    FColSeparatorColor: TColor;
    FShowColSeparators: Boolean;
    FSelectedColor: TColor;
    FSelectedTextColor: TColor;
    FTextHeight: Integer;
    FNeedMeasure: Boolean;
    function ColumnMarginStored: Boolean;
    function GetColumnCount: Integer;
    function GetDelimiteds(const aLine: String): TStringArray;
    procedure SetColSeparatorColor(AValue: TColor);
    procedure SetColumnMargin(aValue: Integer);
    procedure SetDelimiter(aValue: AnsiChar);
    procedure SetOffsets;
    procedure SetShowColSeparators(aValue: Boolean);
    procedure SetSelectedColor(AValue: TColor);
    procedure SetSelectedTextColor(AValue: TColor);
  protected
    procedure CreateHandle; override;
    procedure DoAutoAdjustLayout(const AMode: TLayoutAdjustmentPolicy;
      const AXProportion, AYProportion: Double); override;
    procedure DrawItem(Index: Integer; ARect: TRect; State: TOwnerDrawState); override;
    class function GetControlClassDefaultSize: TSize; override;
    procedure GetItems; override;
    procedure InitializeWnd; override;
    procedure FontChanged(Sender: TObject); override;
    procedure SetItems(const Value: TStrings); override;
    procedure SetStyle(AValue: TComboBoxStyle); override;
  public
    constructor Create(TheOwner: TComponent); override;
    destructor Destroy; override;
    property ColumnCount: Integer read FColumnCount;
  published
    // new properties
    property ColumnMargin: Integer read FColumnMargin write SetColumnMargin stored ColumnMarginStored;
    property ColSeparatorColor: TColor read FColSeparatorColor write SetColSeparatorColor default clSilver;
    property Delimiter: AnsiChar read FDelimiter write SetDelimiter default ',';
    property SelectedColor: TColor read FSelectedColor write SetSelectedColor default clHighlight;
    property SelectedTextColor: TColor read FSelectedTextColor write SetSelectedTextColor default clHighlightText;
    property ShowColSeparators: Boolean read FShowColSeparators write SetShowColSeparators default False;
    // inherited comboBox properties
    property Align;
    property Anchors;
    property ArrowKeysTraverseList;
    property AutoComplete;
    property AutoCompleteText;
    property AutoDropDown;
    property AutoSelect;
    property AutoSize;
    property BidiMode;
    property BorderSpacing;
    property BorderStyle;
    property CharCase;
    property Color default clWindow;
    property Constraints;
    property DragCursor;
    property DragKind;
    property DragMode;
    property DropDownCount;
    property Enabled;
    property Font;
    property ItemHeight;
    property ItemIndex;
    property Items;
    property ItemWidth;
    property MaxLength;
    property OnChange;
    property OnChangeBounds;
    property OnClick;
    property OnCloseUp;
    property OnContextPopup;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnDrawItem;
    property OnEndDrag;
    property OnDropDown;
    property OnEditingDone;
    property OnEnter;
    property OnExit;
    property OnGetItems;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMeasureItem;
    property OnMouseDown;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnMouseMove;
    property OnMouseUp;
    property OnStartDrag;
    property OnSelect;
    property OnUTF8KeyPress;
    property ParentBidiMode;
    property ParentColor;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowHint;
    property Sorted;
    property Style default csOwnerDrawFixed;
    property TabOrder;
    property TabStop;
    property Text;
    property Visible;
  end;


implementation

uses
  Themes;

const
  DEFAULT_COLUMN_MARGIN = 4;
  TEXT_SAMPLE = 'TgjÜ';

{ TColumnCombo }

constructor TColumnComboBoxEx.Create(TheOwner: TComponent);
begin
  inherited Create(TheOwner);
  Color := clWindow;

  FParser := TStringList.Create;
  FColumnMargin := DEFAULT_COLUMN_MARGIN;
  FColumnCount := 0;
  FColSeparatorColor := clSilver;
  FDelimiter := ',';
  FShowColSeparators := False;
  FSelectedColor := clHighlight;
  FSelectedTextColor := clHighlightText;
  SetStyle(csOwnerDrawFixed);
  FOffsets := nil;
  FColumnCount := 0;
  FNeedMeasure := true;
end;

destructor TColumnComboBoxEx.Destroy;
begin
  FParser.Free;
  inherited Destroy;
end;

function TColumnComboBoxEx.ColumnMarginStored: Boolean;
begin
  Result := FColumnMargin <> DEFAULT_COLUMN_MARGIN;
end;

procedure TColumnComboBoxEx.CreateHandle;
begin
  inherited;
  FNeedMeasure := true;
end;

procedure TColumnComboBoxEx.DoAutoAdjustLayout(
  const AMode: TLayoutAdjustmentPolicy; const AXProportion, AYProportion: Double
  );
begin
  inherited DoAutoAdjustLayout(AMode, AXProportion, AYProportion);

  if AMode in [lapAutoAdjustWithoutHorizontalScrolling, lapAutoAdjustForDPI] then
  begin
    if ColumnMarginStored then
      FColumnMargin := Round(FColumnMargin * AXProportion);
    Invalidate;
  end;
end;

procedure TColumnComboBoxEx.DrawItem(Index: Integer; ARect: TRect; State: TOwnerDrawState);
var
  i, y, xl: Integer;
  txt: String;
  savedColor, savedFontColor: TColor;
begin
  if Assigned(OnDrawItem) then
  begin
    OnDrawItem(Self, Index, ARect, State);
    exit;
  end;

  savedColor := Canvas.Brush.Color;
  savedFontColor := Canvas.Font.Color;

  Canvas.Font.Assign(Font);
  if FNeedMeasure then begin
    FTextHeight := Canvas.TextHeight(TEXT_SAMPLE);
    SetOffsets;
    FNeedMeasure := false;
  end;

  if DroppedDown then
  begin
    if (odSelected in State) then
    begin
      Canvas.Brush.Color := FSelectedColor;
      Canvas.Font.Color := FSelectedTextColor;
    end else
    if (Canvas.Brush.Color <> Color) then
      Canvas.Brush.Color := Color;
    Canvas.Brush.Style := bsSolid;
    Canvas.FillRect(ARect);
  end else
    Canvas.Font.Color := clWindowText;

  if Index < 0 then
    txt := ''
  else
    txt := Items[Index];

  FParser.StrictDelimiter := FDelimiter <> ' ';
  FParser.Delimiter := FDelimiter;
  FParser.DelimitedText := txt;

  y := (ARect.Top + ARect.Bottom - FTextHeight) div 2;
  Canvas.Brush.Style := bsClear;  // transparent text background
  if Assigned(FOffsets) then
  begin
    for i := 0 to FParser.Count-1 do
    begin
      xl := ARect.Left + FOffsets[i];
      Canvas.TextOut(xl, y, FParser[i]);
    end;
    if FShowColSeparators then
    begin
      Canvas.Pen.Color := FColSeparatorColor;
      for i := 1 to High(FOffsets) do
      begin
        xl := FOffsets[i];
        Dec(xl, FColumnMargin);
        Canvas.Line(xl, ARect.Top, xl, ARect.Bottom);
      end;
    end;
  end
  else
    Canvas.TextOut(xl, y, txt);

  Canvas.Brush.Color := savedColor;
  Canvas.Font.Color := savedFontColor;
end;


procedure TColumnComboBoxEx.FontChanged(Sender: TObject);
begin
  inherited FontChanged(Sender);
  FNeedMeasure := true;
end;

function TColumnComboBoxEx.GetColumnCount: Integer;
var
  i, tmp: Integer;
  s: String;

  function GetDelimCount: Integer;
  var
    p: Integer;
  begin
    Result := 0;
    for p := 1 to Length(s) do
      if s[p] = FDelimiter then
        Inc(Result);
  end;

begin
  Result := 0;
  for i := 0 to Items.Count-1 do
    begin
      s := Items[i];
      tmp := GetDelimCount;
      if Result < tmp then
        Result := tmp;
    end;
  Inc(Result);
end;

class function TColumnComboBoxEx.GetControlClassDefaultSize: TSize;
begin
  Result := inherited GetControlClassDefaultSize;
  Result.cx := 200;
end;

function TColumnComboBoxEx.GetDelimiteds(const aLine: String): TStringArray;
var
  p, start, resultIdx: Integer;
begin
  Result := Nil;
  SetLength(Result, FColumnCount);
  start := 1;
  resultIdx := 0;
  for p := 1 to Length(aLine) do
    begin
      case (aLine[p] = FDelimiter) of
        True: begin
                Result[resultIdx] := Copy(aLine, start, p - start);
                start := Succ(p);
                Inc(resultIdx);
              end;
        False: ;
      end;
    end;
  Result[resultIdx] := Copy(aLine, start, Length(aLine));
end;

procedure TColumnComboBoxEx.GetItems;
begin
  inherited GetItems;
end;

procedure TColumnComboBoxEx.InitializeWnd;
begin
  inherited;
  FNeedMeasure := true;
end;

procedure TColumnComboBoxEx.SetColSeparatorColor(AValue: TColor);
begin
  if FColSeparatorColor <> AValue then
  begin
    FColSeparatorColor := AValue;
    Invalidate;
  end;
end;

procedure TColumnComboBoxEx.SetColumnMargin(aValue: Integer);
begin
  if FColumnMargin <> aValue then
  begin
    FColumnMargin := aValue;
    Invalidate;
  end;
end;

procedure TColumnComboBoxEx.SetDelimiter(aValue: AnsiChar);
begin
  if FDelimiter <> aValue then
  begin
    FDelimiter := aValue;
    FColumnCount := GetColumnCount;
  end;
end;

procedure TColumnComboBoxEx.SetItems(const Value: TStrings);
begin
  inherited SetItems(Value);
  FNeedMeasure := true;
end;

procedure TColumnComboBoxEx.SetOffsets;
var
  widths: TIntegerDynArray = nil;
  i, j: Integer;
  sa: TStringArray;
  w: Integer;
begin
  if not Assigned(Parent) or (Items.Count = 0) then
    Exit;

  FColumnCount := GetColumnCount;

  SetLength(widths, FColumnCount);
  for i := 0 to Items.Count-1 do
    begin
      sa := GetDelimiteds(Items[i]);
      for j := 0 to High(sa) do
        begin
          w := Canvas.TextWidth(sa[j]);
          if widths[j] < w then
            widths[j] := w;
        end;
    end;
  SetLength(FOffsets, FColumnCount);
  for j := 0 to High(FOffsets) do
    case j of
      0: FOffsets[j] := FColumnMargin;
      else
        FOffsets[j] := FOffsets[Pred(j)] + widths[Pred(j)] + FColumnMargin shl 1;
    end;
end;

procedure TColumnComboBoxEx.SetSelectedColor(AValue: TColor);
begin
  if FSelectedColor <> AValue then
  begin
    FSelectedColor := AValue;
    Invalidate;
  end;
end;

procedure TColumnComboBoxEx.SetSelectedTextColor(AValue: TColor);
begin
  if FSelectedTextColor <> AValue then
  begin
    FSelectedTextColor := AValue;
    Invalidate;
  end;
end;

procedure TColumnComboBoxEx.SetShowColSeparators(aValue: Boolean);
begin
  if FShowColSeparators <> aValue then
  begin
    FShowColSeparators := aValue;
    Invalidate;
  end;
end;

procedure TColumnComboBoxEx.SetStyle(AValue: TComboBoxStyle);
begin
  if (AValue in [csSimple, csDropDown, csDropDownList]) then
    raise Exception.Create('Only owner-draw styles allowed.');

  inherited SetStyle(AValue);
end;

end.