unit mbColorTree;

{$MODE DELPHI}

interface

uses
  LCLIntf, LCLType, SysUtils, Classes, Controls, ComCtrls, Graphics, Themes,
  GraphUtil, ImgList, Forms,
  HTMLColors;

type
  TmbColor = record
    Name: string;
    Value: TColor;
  end;

  TDrawCaptionEvent = procedure (Sender: TObject; AIndex: integer; AFont: TFont; var AText: string; Selected: boolean) of object;
  TDrawLabelEvent = procedure (Sender: TObject; AIndex: integer; AFont: TFont; var AText: string) of object;
  TGetHintEvent = procedure (AIndex: integer; var AHint: string; var Handled: boolean) of object;

  TmbColorTree = class(TCustomTreeView)
  private
    FInfo1, FInfo2: string;
    FInfoLabel: string;
    FDraw: TDrawCaptionEvent;
    FDraw1, FDraw2, FDraw3: TDrawLabelEvent;
    mx, my: integer;
    FGetHint: TGetHintEvent;
    FOnStartDrag: TStartDragEvent;
    FOnEndDrag: TEndDragEvent;
    procedure SetInfo1(Value: string);
    procedure SetInfo2(Value: string);
    procedure SetInfoLabel(Value: string);
  protected
    procedure CMHintShow(var Message: TCMHintShow); message CM_HINTSHOW;
    function CustomDrawItem(Node: TTreeNode; State: TCustomDrawState;
      {%H-}Stage: TCustomDrawStage; var {%H-}PaintImages: Boolean): Boolean; override;
    procedure DoArrow(c: TCanvas; dir: TScrollDirection; p: TPoint; sel: boolean);
    procedure DrawColorItem(R: TRect; Selected: boolean; AIndex: Integer;
      AItemText: String; Expanded: boolean); dynamic;
    procedure DrawInfoItem(R: TRect; Index: integer); dynamic;
    function IsCustomDrawn({%H-}Target: TCustomDrawTarget; {%H-}Stage: TCustomDrawStage): Boolean; override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
  public
    Colors: array of TmbColor;
    constructor Create(AOwner: TComponent); override;
    procedure AddColor(AName: string; AValue: TColor; ARefresh: boolean = true);
    procedure ClearColors;
    function ColorCount: integer;
    procedure DeleteColor(AIndex: integer; ARefresh: boolean = true);
    procedure DeleteColorByName(AName: string; All: boolean);
    procedure DeleteColorByValue(AValue: TColor; All: boolean);
    procedure InsertColor(AIndex: integer; AName: string; AValue: TColor);
    procedure UpdateColors;
  published
    property InfoLabelText: string read FInfoLabel write SetInfoLabel;
    property InfoDisplay1: string read FInfo1 write SetInfo1;
    property InfoDisplay2: string read FInfo2 write SetInfo2;
    property Align;
    property Anchors;
    property AutoExpand;
    property BorderStyle;
    property BorderWidth;
    property Constraints;
    property Color;
    property DragKind;
    property DragCursor;
    property DragMode;
    property Enabled;
    property Font;
    property Indent;
    property ParentColor default False;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property RightClickSelect;
    property ShowHint;
    property SortType;
    property TabOrder;
    property TabStop default True;
    property ToolTips;
    property Visible;
    property OnGetHint: TGetHintEvent read FGetHint write FGetHint;
    property OnDrawCaption: TDrawCaptionEvent read FDraw write FDraw;
    property OnDrawInfoLabel: TDrawLabelEvent read FDraw1 write FDraw1;
    property OnDrawInfoDisplay1: TDrawLabelEvent read FDraw2 write FDraw2;
    property OnDrawInfoDisplay2: TDrawLabelEvent read FDraw3 write FDraw3;
    property OnAdvancedCustomDraw;
    property OnAdvancedCustomDrawItem;
    property OnChange;
    property OnChanging;
    property OnClick;
    property OnCollapsed;
    property OnCollapsing;
    property OnCompare;
    property OnContextPopup;
    property OnCustomDraw;
    property OnCustomDrawItem;
    property OnDblClick;
    property OnDeletion;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDock;
    property OnEndDrag: TEndDragEvent read FOnEndDrag write FOnEndDrag;
    property OnEnter;
    property OnExit;
    property OnExpanding;
    property OnExpanded;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnStartDock;
    property OnStartDrag: TStartDragEvent read FOnStartDrag write FOnStartDrag;
    property Items;
  end;

implementation

uses
  PalUtils, mbUtils;

{  TmbColorTree  }

constructor TmbColorTree.Create(AOwner: TComponent);
begin
  inherited;
  ControlStyle := ControlStyle + [csDisplayDragImage];
  ReadOnly := true;
  ShowButtons := false;
  ShowLines := false;
  ShowRoot := true;
  RowSelect := true;
  HotTrack := false;
  SetLength(Colors, 0);
  Images := TImageList.Create(Self);
  Images.Width := 48;
  Images.Height := 48;
  FInfoLabel := 'Color Values:';
  FInfo1 := 'RGB: %r.%g.%b';
  FInfo2 := 'HEX: #%hex';
end;

procedure TmbColorTree.AddColor(AName: string; AValue: TColor;
  ARefresh: boolean = true);
var
  L: integer;
begin
  L := Length(Colors);
  SetLength(Colors, L + 1);
  Colors[L].Name := AName;
  Colors[L].Value := AValue;
  if ARefresh then
    UpdateColors;
end;

procedure TmbColorTree.ClearColors;
begin
  SetLength(Colors, 0);
  UpdateColors;
end;

procedure TmbColorTree.CMHintShow(var Message: TCMHintShow);
var
  Handled: boolean;
  i: integer;
  n: TTreeNode;
begin
  if PtInRect(ClientRect, Point(mx, my)) and ShowHint and not Dragging then
  begin
    n := GetNodeAt(mx, my);
    if n <> nil then
    begin
      if not n.HasChildren then
        i := n.Parent.Index
      else
        i := n.Index;
      with TCMHintShow(Message) do
        if not ShowHint then
          Message.Result := 1
        else
          with HintInfo^ do
          begin
            Result := 0;
            ReshowTimeout := 2000;
            HideTimeout := 1000;
            Handled := false;
            if Assigned(FGetHint) then
              FGetHint(i, HintStr, Handled);
            if Handled then
              HintStr := FormatHint(HintStr, Colors[i].Value)
            else
              HintStr := Colors[i].Name;
          end;
    end;
  end;
  inherited;
end;

function TmbColorTree.ColorCount: integer;
begin
  Result := Length(Colors);
end;

function TmbColorTree.CustomDrawItem(Node: TTreeNode; State: TCustomDrawState;
  Stage: TCustomDrawStage; var PaintImages: Boolean): Boolean;
begin
  Result := true;
  if Length(Colors) = 0 then Exit;
  if Node.HasChildren then
    DrawColorItem(Node.DisplayRect(false), cdsSelected in State, node.Index, node.Text, node.Expanded)
  else
    DrawInfoItem(Node.DisplayRect(false), node.Parent.Index);
end;

procedure TmbColorTree.DeleteColorByValue(AValue: TColor; All: boolean);
var
  i: integer;
begin
  for i := Length(Colors) - 1 downto 0 do
    if Colors[i].Value = AValue then
    begin
      DeleteColor(i, false);
      if not All then
      begin
        UpdateColors;
        Exit;
      end;
    end;
  UpdateColors;
end;

procedure TmbColorTree.DoArrow(c: TCanvas; dir: TScrollDirection; p: TPoint;
  sel: boolean);
var
  b: TBitmap;
begin
  b := TBitmap.Create;
  try
    b.Height := 12;
    b.Width := 12;
    if Sel then
    begin
      b.Canvas.Brush.Color := clHighlight;
      b.Canvas.Pen.Color := clHighlightText;
    end
    else
    begin
      b.Canvas.Brush.Color := clFuchsia;
      b.Canvas.Pen.Color := clWindowText;
      b.Transparent := true;
      b.TransparentColor := clFuchsia;
    end;
    b.Canvas.FillRect(B.Canvas.ClipRect);
    case dir of
      sdDown  : DrawArrow(b.Canvas, dir, Point(2, 3), 3);
      sdRight : DrawArrow(b.Canvas, dir, Point(1, 2), 3);
    end;
    c.Draw(p.x, p.y, b);
  finally
    b.Free;
  end;
end;

procedure TmbColorTree.DrawColorItem(R: TRect; Selected: boolean; AIndex: integer;
  AItemText: string; Expanded: boolean);
const
  FLAGS = DT_LEFT or DT_NOCLIP or DT_END_ELLIPSIS;
var
  SR, TR: TRect;
begin
  with Canvas do
  begin
    //background
    Pen.Color := clWindow;
    if Selected then
      Brush.Color := clHighlight
    else
      Brush.Color := Color;
    FillRect(R);
    MoveTo(R.Left, R.Bottom - 1);
    LineTo(R.Right, R.Bottom - 1);

    //swatches
    SR := Rect(R.Left + 6, R.Top + 6, R.Left + 42, R.Top + 42);
    Brush.Color := Self.Colors[AIndex].value;
    if Selected then
    begin
      if ThemeServices.ThemesEnabled then
      begin
        ThemeServices.DrawElement(Canvas.Handle,
          ThemeServices.GetElementDetails(teEditTextNormal), SR);
        InflateRect(SR, -2, -2);
        Brush.Color := Blend(Self.Colors[AIndex].value, clBlack, 80);
        FillRect(SR);
        InflateRect(SR, -1, -1);
        Brush.Color := Blend(Self.Colors[AIndex].value, clBlack, 90);
        FillRect(SR);
        InflateRect(SR, -1, -1);
        Brush.Color := Self.Colors[AIndex].value;
        FillRect(SR);
      end
      else
      //windows 9x
      begin
        Pen.Color := clBackground;
        Brush.Color := clWindow;
        Rectangle(SR);
        InflateRect(SR, -1, -1);
        FillRect(SR);
        InflateRect(SR, 1, 1);
        InflateRect(SR, -2, -2);
        Brush.Color := Blend(Self.Colors[AIndex].value, clBlack, 75);
        FillRect(SR);
        InflateRect(SR, -1, -1);
        Brush.Color := Blend(Self.Colors[AIndex].value, clBlack, 87);
        FillRect(SR);
        InflateRect(SR, -1, -1);
        Brush.Color := Self.Colors[AIndex].value;
        FillRect(SR);
      end;
    end
    else
    //not selected
    begin
      //windows XP
      if ThemeServices.ThemesEnabled then
      begin
        ThemeServices.DrawElement(Canvas.Handle, ThemeServices.GetElementDetails(teEditTextNormal), SR);
        InflateRect(SR, -2, -2);
        Brush.Color := Self.Colors[AIndex].value;
        FillRect(SR);
      end
      else
      //windows 9x
      begin
        DrawEdge(Canvas.Handle, SR, BDR_SUNKENOUTER, BF_RECT);
        InflateRect(SR, -2, -2);
        Brush.Color := Self.Colors[AIndex].value;
        Pen.Color := clBlack;
        Rectangle(SR);
        InflateRect(SR, -1, -1);
        FillRect(SR);
        InflateRect(SR, 1, 1);
      end;
    end;
    //names
    Font.Style := [fsBold];
    if Selected then
    begin
      //Brush.Color := clHighlightText;
      Pen.Color := clHighlightText;
      Font.Color := clHighlightText;
    end
    else
    begin
      //Brush.Color := clWindowText;
      Pen.Color := clWindowText;
      Font.Color := clWindowText;
    end;
    TR := Rect(R.Left + 48, R.Top + (48 - TextHeight(AItemText)) div 2, R.Right - 15, R.Bottom);
    if Assigned(FDraw) then FDraw(Self, AIndex, Canvas.Font, AItemText, Selected);
    SetBkMode(Canvas.Handle, TRANSPARENT);
    DrawText(Canvas.Handle, PChar(AItemText), Length(AItemText), TR, FLAGS);
    SetBkMode(Canvas.Handle, OPAQUE);
    if R.Right > 60 then
    begin
      if Expanded then
        DoArrow(Canvas, sdDown, Point(R.Right - 13, R.Top + 20), selected)
      else
        DoArrow(Canvas, sdRight, Point(R.Right - 10, R.Top + 18), selected);
    end;
  end;
end;

procedure TmbColorTree.DrawInfoItem(R: TRect; Index: integer);
const
  FLAGS = DT_LEFT or DT_END_ELLIPSIS or DT_NOCLIP;
  DELTA = 2;
var
  b: TBitmap;
  BR, TR: TRect;
  s: string;
  h: Integer;
begin
  b := TBitmap.Create;
  try
    b.Width := R.Right - R.Left;
    b.Height := R.Bottom - R.Top;
    BR := b.Canvas.ClipRect;
    with b.Canvas do
    begin
      Canvas.Font.Assign(Self.Font);
      Brush.Color := Blend(clBtnFace, clWindow, 30);
      FillRect(BR);
      BR := Rect(BR.Left + 42, BR.Top, BR.Right, BR.Bottom);
      FillRect(BR);
      Inc(BR.Left, 6);
      Font.Style := [];
      Font.Size := 7;

      s := FInfoLabel;
      h := TextHeight(s);
      TR := Rect(BR.Left, BR.Top{ + 2}, BR.Right, BR.Top + {2 + }h + DELTA);
      if Assigned(FDraw1) then FDraw1(Self, Index, Canvas.Font, s);
      DrawText(b.Canvas.Handle, PChar(s), Length(s), TR, FLAGS);

      DrawHorDottedLine(b.Canvas, BR.Left, BR.Right, TR.Bottom + DELTA, clGray);

      s := FormatHint(FInfo1, Self.Colors[Index].value);
      TR.Top := TR.Bottom + 2 * DELTA;
      TR.Bottom := TR.Top + h + DELTA;
      if Assigned(FDraw2) then FDraw2(Self, Index, Canvas.Font, s);
      DrawText(b.Canvas.Handle, PChar(s), Length(s), TR, FLAGS);

      DrawHorDottedLine(b.Canvas, BR.LEft, BR.Right, TR.Bottom + DELTA, clGray);

      s := FormatHint(FInfo2, Self.Colors[Index].value);
      TR.Top := TR.Bottom + 2 * DELTA;
      TR.Bottom := TR.Top + h + DELTA;
      if Assigned(FDraw3) then FDraw3(Self, Index, Canvas.Font, s);
        DrawText(b.Canvas.Handle, PChar(s), Length(s), TR, FLAGS);
    end;

    Canvas.Draw(R.Left, R.Top, b);
  finally
    b.Free;
  end;
end;

procedure TmbColorTree.InsertColor(AIndex: integer; AName: string; AValue: TColor);
var
  i: integer;
begin
  if AIndex > Length(Colors) - 1 then
    raise Exception.Create(Format('List index out of bounds (%d)', [AIndex]));

  SetLength(Colors, Length(Colors) + 1);
  for i := Length(Colors) - 1 downto AIndex do
    Colors[i] := Colors[i-1];

  Colors[AIndex].Name := AName;
  Colors[AIndex].Value := AValue;

  UpdateColors;
end;

function TmbColorTree.IsCustomDrawn(Target: TCustomDrawTarget;
  Stage: TCustomDrawStage): Boolean;
begin
  Result := true;
end;

procedure TmbColorTree.MouseDown(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
var
  r: TRect;
begin
  inherited;
  if (ssShift in Shift) or (ssCtrl in Shift) then
    Exit;
  if Selected <> nil then
    r := Selected.DisplayRect(false)
  else
    exit;
  if (x > r.Right - 15) and (x < r.Right - 3) and (y > r.Top + 13) and (y < r.Top + 30) then
    if (Selected.HasChildren) and PtInRect(r, Point(x, y)) then
    begin
      if selected.Expanded then
        Selected.Collapse(false)
      else
        Selected.Expand(false);
      Invalidate;
    end;
end;

procedure TmbColorTree.MouseMove(Shift: TShiftState; X, Y: Integer);
var
  r: TRect;
begin
  inherited;
  mx := x;
  my := y;
  if GetNodeAt(x, y) <> nil then
    r := GetNodeAt(x, y).DisplayRect(false)
  else
  begin
    Cursor := crDefault;
    exit;
  end;

  if (x > r.Right - 15) and (x < r.Right - 3) and (y > r.Top + 13) and (y < r.Top + 30) then
  begin
    if (GetNodeAt(x, y).HasChildren) and PtInRect(r, Point(x, y)) then
      Cursor := crHandPoint
    else
      Cursor := crDefault;
  end
  else
    Cursor := crDefault;
end;

procedure TmbColorTree.SetInfoLabel(Value: string);
begin
  if FInfoLabel <> Value then
  begin
    FInfoLabel := Value;
    Invalidate;
  end;
end;

procedure TmbColorTree.SetInfo1(Value: string);
begin
  if FInfo1 <> Value then
  begin
    FInfo1 := Value;
    Invalidate;
  end;
end;

procedure TmbColorTree.SetInfo2(Value: string);
begin
  if FInfo2 <> Value then
  begin
    FInfo2 := Value;
    Invalidate;
  end;
end;

procedure TmbColorTree.DeleteColor(AIndex: integer; ARefresh: boolean = true);
var
  i: integer;
begin
  if Length(Colors) = 0 then
   raise Exception.Create('There''s nothing to delete! The length of the array is 0.');

  if AIndex > Length(Colors) - 1 then
   raise Exception.Create(Format('List index out of bounds (%d)', [AIndex]));

  for i := AIndex to Length(Colors) - 2 do
    Colors[i] := Colors[i+1];
  SetLength(Colors, Length(Colors) - 1);
  if ARefresh then
    UpdateColors;
end;

procedure TmbColorTree.DeleteColorByName(AName: string; All: boolean);
var
  i: integer;
begin
  for i := Length(Colors) - 1 downto 0 do
  if SameText(Colors[i].Name, AName) then
  begin
    DeleteColor(i, false);
    if not All then
    begin
      UpdateColors;
      Exit;
     end;
  end;
  UpdateColors;
end;

procedure TmbColorTree.UpdateColors;
var
  i: integer;
  n: TTreeNode;
begin
  Items.Clear;
  for i := 0 to Length(Colors) - 1 do
  begin
    n := Items.Add(TopItem, Colors[i].name);
    Items.AddChild(n, '');
  end;
end;

end.