lazarus/components/chatcontrol/chatcontrol.pas
2025-01-04 15:37:54 +01:00

781 lines
21 KiB
ObjectPascal

{
/***************************************************************************
chatcontrol.pp
-------------------
basic 2-user-chat displaying control
***************************************************************************/
*****************************************************************************
This file is part of the Lazarus Component Library (LCL)
See the file COPYING.modifiedLGPL.txt, included in this distribution,
for details about the license.
*****************************************************************************
}
unit chatcontrol;
{$mode ObjFPC}{$H+}
interface
uses
Classes, SysUtils, Contnrs, Graphics, Controls, ExtCtrls, StdCtrls, forms, typingindicator;
Const
DefaultBackground : TColor = $00CDEBD0;
DefaultRightBackground : TColor = $00B6F5A2;
DefaultLeftBackground : TColor = $00F0F0F0;
DefaultItemSpacing = 8;
DefaultItemPadding = 8;
DefaultItemMargin = 4;
Type
TTextSide = (tsLeft,tsRight);
{ TChatItem }
TChatItem = Class(TObject)
private
FText: String;
FSide: TTextSide;
Public
constructor Create(aText : String; aSide : TTextSide);
Property Text : String Read FText Write FText;
Property Side : TTextSide Read FSide Write FSide;
end;
TChatItemArray = Array of TChatItem;
{ TChatControl }
TItemClickEvent = procedure(Sender : TObject; aItem : TChatItem) of object;
TChatControl = class(TScrollingWinControl)
Protected
Type
TDisplayChatItem = Class;
Private
FChatList : TFPObjectList;
FCtrlSelects: Boolean;
FItemMargin: Integer;
FItemPadding: Integer;
FItemSpacing: Integer;
FLeftBackground: TColor;
FLeftTextColor: TColor;
FOnItemClick: TItemClickEvent;
FRightBackground: TColor;
FRightTextColor: TColor;
FTyping : Array[TTextSide] of TTypingIndicator;
function GetChatCount: Integer;
function GetDisplayItem(aIndex: Integer): TDisplayChatItem;
function GetIndicatorSettings(AIndex: Integer): TTypingDotIndicatorSettings;
function GetIsTyping(aSide : TTextSide): Boolean;
function GetItem(aIndex : integer): TChatItem;
function GetLeftTyping: Boolean;
function GetRightTyping: Boolean;
function GetTyping(aIndex : TTextSide): TTypingIndicator;
function LayoutTypingItem(aSide: TTextSide; aTop: Integer): Integer;
procedure SetIndicatorSettings(AIndex: Integer; AValue: TTypingDotIndicatorSettings);
procedure SetIsTyping(aSide : TTextSide; AValue: Boolean);
procedure SetItemMargin(AValue: Integer);
procedure SetItemPadding(AValue: Integer);
procedure SetItemSpacing(AValue: Integer);
procedure SetLeftBackground(AValue: TColor);
procedure SetLeftTextColor(AValue: TColor);
procedure SetLeftTyping(AValue: Boolean);
procedure SetRightBackground(AValue: TColor);
procedure SetRightTextColor(AValue: TColor);
procedure SetRightTyping(AValue: Boolean);
Protected
Type
{ TDisplayChatItem }
TDisplayChatItem = class(TChatItem)
Private
Type
TColorKind = (ckBackground,ckBackgroundPen,ckBackgroundBrush,ckLabelFont,ckLabelBack);
Private
FBackground: TShape;
FLabel: TLabel;
FOnClick: TNotifyEvent;
FDownShift: TShiftState;
FSelected: Boolean;
FSavedColors: Array[TColorKind] of TColor;
procedure DoMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
function GetBottom: Integer;
procedure SetSelected(AValue: Boolean);
procedure SetColors;
protected
procedure SetBackground(AValue: TShape); virtual;
procedure SetLabel(AValue: TLabel); virtual;
Procedure LayoutAt(aTop,aPadding,aMargin : Integer); virtual;
procedure DoClick(Sender : TObject); virtual;
procedure Invalidate; virtual;
Public
Destructor Destroy; override;
Property OnClick : TNotifyEvent Read FOnClick Write FOnClick;
Property Bottom : Integer Read GetBottom;
Property TextLabel : TLabel Read FLabel Write SetLabel;
Property TextBackGround : TShape Read FBackground Write SetBackground;
Property Selected : Boolean read FSelected Write SetSelected;
end;
procedure DoItemClick(Sender: TObject);
Procedure SelectItem(aItem : TDisplayChatItem; IsSelect,aAddToSelection : Boolean);
function CreateChatLabel(aParent: TWinControl): TLabel; virtual;
function CreateTypingIndicator(aSide: TTextSide): TTypingIndicator; virtual;
function DoCreateItem(aText: String; aSide: TTextSide): TDisplayChatItem; virtual;
function CreateItem(aText: String; aSide: TTextSide; aParent: TWinControl): TDisplayChatItem;
procedure DoOnResize; override;
function LayoutItem(aIndex: Integer; aTop: Integer): Integer;
procedure LayoutItems;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
function LayoutTyping(aTop: Integer): Integer;
Property Typing [aIndex : TTextSide] : TTypingIndicator Read GetTyping;
Property DisplayItems[aIndex: Integer] : TDisplayChatItem Read GetDisplayItem;
Public
constructor create(aOwner : TComponent); override;
destructor destroy; override;
procedure clear;
procedure CopySelectionToClipBoard;
Procedure AddText(const aText : String; aSide : TTextSide);
Procedure SelectItem(aIndex : Integer; aAddToSelection : Boolean);
Procedure DeleteItem(aItem : TChatItem);
Procedure DeleteItem(aIndex : Integer);
Function GetItemAt(X,Y : Integer) : TChatItem;
Function GetSelectedItems : TChatItemArray;
Property ChatItems[aIndex : integer] : TChatItem Read GetItem;
Property ChatCount : Integer Read GetChatCount;
Property IsTyping[aSide : TTextSide] : Boolean Read GetIsTyping Write SetIsTyping;
Published
Property CtrlSelects : Boolean Read FCtrlSelects Write FCtrlSelects;
Property OnItemClick : TItemClickEvent Read FOnItemClick Write FOnItemClick;
Property LeftTyping : Boolean Read GetLeftTyping Write SetLeftTyping;
Property RightTyping : Boolean Read GetRightTyping Write SetRightTyping;
Property LeftBackground : TColor Read FLeftBackground Write SetLeftBackground;
Property RightBackground : TColor Read FRightBackground Write SetRightBackground;
Property LeftTextColor : TColor Read FLeftTextColor Write SetLeftTextColor;
Property RightTextColor : TColor Read FRightTextColor Write SetRightTextColor;
Property ItemSpacing : Integer Read FItemSpacing Write SetItemSpacing;
Property ItemPadding : Integer Read FItemPadding Write SetItemPadding;
Property ItemMargin : Integer Read FItemMargin Write SetItemMargin;
Property LeftTypingIndicator : TTypingDotIndicatorSettings Index Ord(tsLeft) Read GetIndicatorSettings Write SetIndicatorSettings;
Property RightTypingIndicator : TTypingDotIndicatorSettings Index Ord(tsRight) Read GetIndicatorSettings Write SetIndicatorSettings;
Published
property Align;
property Anchors;
property AutoSize;
property AutoScroll default True;
property BorderSpacing;
property BiDiMode;
property BorderStyle default bsSingle;
property ClientHeight;
property ClientWidth;
property Constraints;
property DockSite;
property DragCursor;
property DragKind;
property DragMode;
property Enabled;
property Color nodefault;
property Font;
property ParentBackground default True;
property ParentBiDiMode;
property ParentColor;
property ParentFont;
property ParentShowHint;
property PopupMenu;
property ShowHint;
property TabOrder;
property TabStop;
property Visible;
//property OnCanResize;
property OnClick;
property OnConstrainedResize;
property OnDblClick;
property OnDockDrop;
property OnDockOver;
property OnDragDrop;
property OnDragOver;
property OnEndDock;
property OnEndDrag;
property OnEnter;
property OnExit;
property OnGetSiteInfo;
property OnMouseDown;
property OnMouseEnter;
property OnMouseLeave;
property OnMouseMove;
property OnMouseUp;
property OnMouseWheel;
property OnMouseWheelDown;
property OnMouseWheelUp;
property OnMouseWheelHorz;
property OnMouseWheelLeft;
property OnMouseWheelRight;
property OnResize;
property OnStartDock;
property OnStartDrag;
property OnUnDock;
property OnPaint;
end;
implementation
uses lcltype, clipbrd;
{ TChatItem }
constructor TChatItem.Create(aText: String; aSide: TTextSide);
begin
FText:=aText;
FSide:=aSide;
end;
{ TChatControl }
procedure TChatControl.SetItemSpacing(AValue: Integer);
begin
if FItemSpacing=AValue then Exit;
FItemSpacing:=AValue;
LayoutItems;
end;
function TChatControl.LayoutItem(aIndex: Integer; aTop: Integer): Integer;
var
NewMax : Integer;
Recalc : Boolean;
W,H : Integer;
begin
With TDisplayChatItem(FChatList[aIndex]) do
begin
NewMax:=Round(Width*0.75);
// Work around a strange behaviour in Laz:
// When resizing and we have a bigger size, the label does not resize to take more space.
// After much experimenting the following workaround was found:
// By switching off wordwrap and switching back on wordwrap, it works.
Recalc:=NewMax>TextLabel.Constraints.MaxWidth;
TextLabel.Constraints.MaxWidth:=NewMax;
if Recalc then
begin
TextLabel.WordWrap:=False;
TextLabel.WordWrap:=True;
end;
LayoutAt(aTop,FItemPadding,FItemMargin);
Result:=Bottom+ItemSpacing;
end;
end;
procedure TChatControl.LayoutItems;
var
I : Integer;
lTop : Integer;
begin
lTop:=ItemSpacing;
For I:=0 to FChatList.Count-1 do
lTop:=LayoutItem(I,lTop)+ItemSpacing;
LayoutTyping(lTop);
VertScrollbar.Position:=VertScrollbar.Range-1;
end;
procedure TChatControl.KeyDown(var Key: Word; Shift: TShiftState);
begin
inherited KeyDown(Key, Shift);
if (Key=Ord('c')) or (Key=ord('C')) and (Shift=[ssCtrl]) then
CopySelectionToClipBoard;
end;
function TChatControl.LayoutTypingItem(aSide : TTextSide; aTop : Integer) : Integer;
var
Ind : TTypingIndicator;
lPos : TPoint;
begin
Ind:=FTyping[aSide];
lPos.Y:=aTop;
if aSide=tsLeft then
lPos.X:=ItemMargin
else
lPos.X:=Width-Ind.Width-ItemMargin;
Ind.SetBounds(lPos.X,lPos.Y,80,40);
Result:=aTop+Ind.Height;
end;
procedure TChatControl.SetIndicatorSettings(AIndex: Integer; AValue: TTypingDotIndicatorSettings);
begin
FTyping[TTextSide(aIndex)].DotSettings:=aValue;
end;
procedure TChatControl.SetIsTyping(aSide : TTextSide; AValue: Boolean);
begin
FTyping[aSide].Visible:=aValue;
FTyping[aSide].Active:=aValue;
end;
function TChatControl.LayoutTyping(aTop: Integer): Integer;
var
aSide : TTextSide;
begin
if ChatCount=0 then
aSide:=tsLeft
else
aSide:=ChatItems[ChatCount-1].Side;
aTop:=LayoutTypingItem(aSide,aTop)+ItemSpacing;
if aSide=tsLeft then
aSide:=tsRight
else
aSide:=tsLeft;
Result:=LayoutTypingItem(aSide,aTop);
end;
procedure TChatControl.SetItemMargin(AValue: Integer);
begin
if FItemMargin=AValue then Exit;
FItemMargin:=AValue;
LayoutItems;
end;
procedure TChatControl.SetItemPadding(AValue: Integer);
begin
if FItemPadding=AValue then Exit;
FItemPadding:=AValue;
LayoutItems;
end;
function TChatControl.GetItem(aIndex : integer): TChatItem;
begin
Result:=TChatItem(FChatList[aIndex])
end;
function TChatControl.GetLeftTyping: Boolean;
begin
Result:=FTyping[tsLeft].Visible;
end;
function TChatControl.GetRightTyping: Boolean;
begin
Result:=FTyping[tsRight].Visible;
end;
function TChatControl.GetTyping(aIndex : TTextSide): TTypingIndicator;
begin
Result:=FTyping[aIndex]
end;
function TChatControl.GetChatCount: Integer;
begin
Result:=FChatList.Count;
end;
function TChatControl.GetDisplayItem(aIndex: Integer): TDisplayChatItem;
begin
Result:=TDisplayChatItem(FChatList[aIndex]);
end;
procedure TChatControl.DoItemClick(Sender: TObject);
var
lItm : TDisplayChatItem absolute sender;
doSelect : Boolean;
doAdd : Boolean;
begin
if not (Sender is TDisplayChatItem) then
exit;
doSelect:=(Not CtrlSelects) or (ssCtrl in lItm.FDownShift);
if DoSelect then
begin
if CtrlSelects then
doAdd:=ssShift in lItm.FDownShift
else
doAdd:=ssCtrl in lItm.FDownShift;
SelectItem(lItm,not lItm.Selected,doAdd);
end;
If Assigned(FOnItemClick) then
FOnItemClick(Self,lItm);
end;
function TChatControl.GetIndicatorSettings(AIndex: Integer): TTypingDotIndicatorSettings;
begin
Result:=FTyping[TTextSide(aIndex)].DotSettings;
end;
function TChatControl.GetIsTyping(aSide : TTextSide): Boolean;
begin
Result:=FTyping[aSide].Visible;
end;
procedure TChatControl.SetLeftBackground(AValue: TColor);
begin
if FLeftBackground=AValue then Exit;
FLeftBackground:=AValue;
end;
procedure TChatControl.SetLeftTextColor(AValue: TColor);
begin
if FLeftTextColor=AValue then Exit;
FLeftTextColor:=AValue;
end;
procedure TChatControl.SetLeftTyping(AValue: Boolean);
begin
FTyping[tsLeft].Visible:=aValue;
end;
procedure TChatControl.SetRightBackground(AValue: TColor);
begin
if FRightBackground=AValue then Exit;
FRightBackground:=AValue;
end;
procedure TChatControl.SetRightTextColor(AValue: TColor);
begin
if FRightTextColor=AValue then Exit;
FRightTextColor:=AValue;
end;
procedure TChatControl.SetRightTyping(AValue: Boolean);
begin
FTyping[tsRight].Visible:=aValue;
end;
procedure TChatControl.SelectItem(aItem: TDisplayChatItem; IsSelect, aAddToSelection: Boolean);
var
I : Integer;
lItem : TDisplayChatItem;
begin
if IsSelect and not aAddToSelection then
For I:=0 to ChatCount-1 do
begin
lItem:=DisplayItems[i];
if (lItem<>aItem) and lItem.Selected then
lItem.Selected:=False;
end;
aItem.Selected:=IsSelect;
end;
function TChatControl.CreateChatLabel(aParent : TWinControl): TLabel;
begin
Result:=Tlabel.Create(Self);
Result.Parent:=aParent;
end;
function TChatControl.DoCreateItem(aText: String; aSide: TTextSide): TDisplayChatItem;
begin
Result:=TDisplayChatItem.Create(aText,aSide);
end;
function TChatControl.CreateItem(aText: String; aSide: TTextSide; aParent: TWinControl): TDisplayChatItem;
var
L : TLabel;
S : TShape;
begin
Result:=DoCreateItem(aText,aSide);
L:=CreateChatLabel(Self);
L.AutoSize:=True;
L.Constraints.MaxWidth:=Round(ClientWidth*0.75);
L.WordWrap:=True;
L.Caption:=Result.Text;
S:=TShape.Create(Self);
S.Parent:=Self;
S.Shape:=stRoundRect;
if aSide=tsLeft then
begin
L.Font.Color:=LeftTextColor;
S.Brush.Color:=LeftBackground;
S.Pen.Color:=LeftBackground;
end
else
begin
L.Font.Color:=RightTextColor;
S.Brush.Color:=RightBackground;
S.Pen.Color:=RightBackground;
end;
Result.TextLabel:=L;
Result.TextBackground:=S;
Result.OnClick:=@DoItemClick;
end;
procedure TChatControl.DoOnResize;
begin
inherited DoOnResize;
LayoutItems;
end;
function TChatControl.CreateTypingIndicator(aSide: TTextSide): TTypingIndicator;
begin
Result:=TTypingIndicator.Create(Self);
Result.Parent:=Self;
Result.Visible:=False;
Result.Active:=False;
end;
constructor TChatControl.create(aOwner: TComponent);
begin
Inherited;
fCompStyle:= csScrollBox;
ControlStyle := ControlStyle + [csCaptureMouse]
- [csOpaque] + [csParentBackground]; // we need the default background
AutoScroll := True;
BorderStyle := bsSingle;
FTyping[tsLeft]:=CreateTypingIndicator(tsLeft);
FTyping[tsRight]:=CreateTypingIndicator(tsRight);
FChatList:=TFPObjectList.Create(True);
Color:=DefaultBackground;
RightTextColor:=clBlack;
LeftTextColor:=clBlack;
RightBackground:=DefaultRightBackGround;
LeftBackground:=DefaultLeftBackGround;
ItemSpacing:=DefaultItemSpacing;
ItemPadding:=DefaultItemPadding;
ItemMargin:=DefaultItemMargin;
end;
destructor TChatControl.destroy;
begin
FreeAndNil(FTyping[tsLeft]);
FreeAndNil(FTyping[tsRight]);
FreeAndNil(FChatList);
inherited destroy;
end;
procedure TChatControl.clear;
begin
FChatList.Clear;
Invalidate;
end;
procedure TChatControl.CopySelectionToClipBoard;
var
I,lCount : Integer;
S : String;
begin
lCount:=0;
For I:=0 to ChatCount-1 do
if DisplayItems[i].selected then
inc(lCount);
if lCount=0 then
exit;
S:='';
For I:=0 to ChatCount-1 do
if DisplayItems[i].selected then
begin
if S<>'' then
S:=S+sLineBreak+sLineBreak;
S:=S+DisplayItems[i].Text;
end;
ClipBoard.AsText:=S;
end;
procedure TChatControl.AddText(const aText: String; aSide: TTextSide);
var
aItem : TDisplayChatItem;
Idx,ltop : Integer;
begin
aItem:=CreateItem(aText,aSide,Self);
Idx:=FChatList.Add(aItem);
lTop:=ItemSpacing;
if Idx>0 then
lTop:=lTop+TDisplayChatItem(FChatList[Idx-1]).Bottom;
lTop:=LayoutItem(Idx,lTop)+ItemSpacing;
LayoutTyping(lTop);
VertScrollbar.Position:=VertScrollbar.Range-1;
end;
procedure TChatControl.SelectItem(aIndex: Integer; aAddToSelection: Boolean);
begin
SelectItem(ChatItems[aIndex] as TDisplayChatItem,True,aAddToSelection);
end;
procedure TChatControl.DeleteItem(aItem: TChatItem);
begin
FChatList.Remove(aItem);
Invalidate;
end;
procedure TChatControl.DeleteItem(aIndex: Integer);
begin
FChatList.Delete(aIndex);
Invalidate;
end;
function TChatControl.GetItemAt(X, Y: Integer): TChatItem;
var
lPt : TPoint;
Itm : TDisplayChatItem;
I : Integer;
begin
Result:=Nil;
lpt:=GetClientScrollOffset;
lpt.Offset(X,y);
for I:=0 to FChatList.Count-1 do
begin
Itm:=TDisplayChatItem(GetItem(I));
if Itm.FBackground.BoundsRect.Contains(lpt) then
Exit(Itm);
end;
end;
function TChatControl.GetSelectedItems: TChatItemArray;
var
i,lCount : Integer;
lItem : TDisplayChatItem;
begin
lCount:=0;
For I:=0 to FChatList.Count-1 do
if TDisplayChatItem(FChatList[i]).Selected then
inc(lCount);
SetLength(Result,lCount);
if lCount=0 then exit;
lCount:=0;
For I:=0 to FChatList.Count-1 do
begin
lItem:=TDisplayChatItem(FChatList[i]);
if lItem.Selected then
begin
Result[lCount]:=lItem;
inc(lCount);
end;
end;
end;
{ TChatControl.TChatItem }
function TChatControl.TDisplayChatItem.GetBottom: Integer;
begin
Result:=FBackground.Top+FBackground.Height;
end;
procedure TChatControl.TDisplayChatItem.SetColors;
begin
if Not Selected then
begin
if Assigned(FBackground) then
begin
FBackground.Pen.Color:=FSavedColors[ckBackgroundPen];
FBackground.Brush.Color:=FSavedColors[ckBackgroundBrush];
FBackground.Color:=FSavedColors[ckBackground];
end;
if Assigned(FLabel) then
begin
FLabel.Font.Color:=FSavedColors[ckLabelFont];
FLabel.Color:=FSavedColors[ckLabelBack];
end;
end
else
begin
if Assigned(FBackground) then
begin
FBackground.Pen.Color:=clHighlight;
FBackground.Brush.Color:=clHighlight;
FBackground.Color:=clHighlight;
end;
if Assigned(FLabel) then
begin
FLabel.Font.Color:=clHighlightText;
FLabel.Color:=clHighlight;
end;
end;
end;
procedure TChatControl.TDisplayChatItem.SetSelected(AValue: Boolean);
begin
if FSelected=AValue then Exit;
FSelected:=AValue;
SetColors;
end;
procedure TChatControl.TDisplayChatItem.Invalidate;
begin
If assigned(FBackground) then
FBackground.Invalidate;
If assigned(FLabel) then
FLabel.Invalidate;
end;
procedure TChatControl.TDisplayChatItem.DoMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
FDownShift:=Shift;
end;
procedure TChatControl.TDisplayChatItem.SetBackground(AValue: TShape);
begin
if FBackground=AValue then Exit;
FBackground:=AValue;
if Assigned(FBackground) then
begin
FSavedColors[ckBackground]:=FBackGround.Color;
FSavedColors[ckBackgroundPen]:=FBackGround.Pen.Color;
FSavedColors[ckBackgroundBrush]:=FBackGround.Brush.Color;
FBackGround.OnClick:=@DoClick;
FBackGround.OnMouseDown:=@DoMouseDown;
end;
end;
procedure TChatControl.TDisplayChatItem.SetLabel(AValue: TLabel);
begin
if FLabel=AValue then Exit;
FLabel:=AValue;
if Assigned(FLabel) then
begin
FSavedColors[ckLabelFont]:=FLabel.Font.Color;
FSavedColors[ckLabelBack]:=FLabel.Color;
FLabel.OnClick:=@DoClick;
FLabel.OnMouseDown:=@DoMouseDown;
end;
end;
destructor TChatControl.TDisplayChatItem.Destroy;
begin
FreeAndNil(FLabel);
FreeAndNil(FBackground);
inherited Destroy;
end;
procedure TChatControl.TDisplayChatItem.LayoutAt(aTop, aPadding, aMargin: Integer);
var
lPos : TPoint;
lSize : TPoint;
begin
lPos.Y:=aTop;
lSize.X:=TextLabel.Width;
lSize.Y:=TextLabel.Height;
Case Side of
tsLeft : lPos.X:=4;
tsRight : lPos.X:=TextLabel.Parent.Width-lSize.X-2*aPadding-aMargin;
end;
TextBackground.SetBounds(lPos.X,lpos.Y,lSize.X+2*aPadding,lSize.Y+2*aPadding);
TextLabel.Left:=lPos.X+aPadding;
TextLabel.Top:=lPos.Y+aPadding;
TextLabel.BringToFront;
end;
procedure TChatControl.TDisplayChatItem.DoClick(Sender: TObject);
begin
If Assigned(FOnClick) then
FOnClick(Self);
end;
end.