doublecmd/components/KASToolBar/kascdedit.pas

464 lines
14 KiB
ObjectPascal

{
Double Commander
-------------------------------------------------------------------------
Custom edit control with the look and feel like TLabel
Copyright (C) 2017-2024 Alexander Koblov (alexx2000@mail.ru)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
}
unit KASCDEdit;
{$mode delphi}
interface
uses
Classes, SysUtils, LazVersion, LResources, Controls, Graphics, Dialogs, Types,
Menus, CustomDrawnControls, CustomDrawnDrawers, CustomDrawn_Common;
type
{ TKASCDEdit }
TKASCDEdit = class(TCDEdit)
private
FDragDropStarted: Boolean;
FEditMenu: TPopupMenu; static;
private
procedure CreatePopupMenu;
procedure ShowMenu(Data: PtrInt);
procedure MenuCopy(Sender: TObject);
procedure MenuSelectAll(Sender: TObject);
function MousePosToCaretPos(X, Y: Integer): TPoint;
protected
procedure RealSetText(const Value: TCaption); override;
procedure CalculatePreferredSize(var PreferredWidth, PreferredHeight: Integer;
WithThemeSpace: Boolean); override;
procedure CalculateSize(MaxWidth: Integer; var NeededWidth, NeededHeight: Integer);
procedure KeyDown(var Key: word; Shift: TShiftState); override;
public
constructor Create(AOwner: TComponent); override;
procedure MouseMove(Shift: TShiftState; X, Y: integer); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
procedure MouseUp(Button: TMouseButton; Shift:TShiftState; X,Y:Integer); override;
public
procedure SelectAll;
procedure CopyToClipboard;
published
property Color default clDefault;
property Cursor default crIBeam;
property ReadOnly default True;
property OnMouseDown;
property OnMouseMove;
property OnMouseUp;
end;
{ TKASCDDrawer }
TKASCDDrawer = class(TCDDrawerCommon)
public
function GetMeasures(AMeasureID: Integer): Integer; override;
procedure DrawEditBackground(ADest: TCanvas; ADestPos: TPoint; ASize: TSize;
AState: TCDControlState; AStateEx: TCDEditStateEx); override;
procedure DrawEdit(ADest: TCanvas; ASize: TSize;
AState: TCDControlState; AStateEx: TCDEditStateEx); override;
end;
procedure Register;
implementation
uses
Math, Forms, Clipbrd, LCLType, LCLIntf, LazUTF8;
resourcestring
rsMnuCopyToClipboard = 'Copy';
rsMnuSelectAll = 'Select &All';
procedure Register;
begin
RegisterComponents('KASComponents', [TKASCDEdit]);
end;
{ TKASCDDrawer }
function TKASCDDrawer.GetMeasures(AMeasureID: Integer): Integer;
begin
case AMeasureID of
TCDEDIT_LEFT_TEXT_SPACING: Result := 0;
TCDEDIT_RIGHT_TEXT_SPACING: Result := 0;
else Result:= inherited GetMeasures(AMeasureID);
end;
end;
procedure TKASCDDrawer.DrawEditBackground(ADest: TCanvas; ADestPos: TPoint;
ASize: TSize; AState: TCDControlState; AStateEx: TCDEditStateEx);
begin
// None
end;
procedure TKASCDDrawer.DrawEdit(ADest: TCanvas; ASize: TSize;
AState: TCDControlState; AStateEx: TCDEditStateEx);
var
lVisibleText, lControlText: TCaption;
lSelLeftPos, lSelLength, lSelRightPos: Integer;
lLineHeight, lLineTop: Integer;
lControlTextLen: PtrInt;
lTextLeftSpacing, lTextTopSpacing, lTextBottomSpacing: Integer;
lTextColor: TColor;
i, lVisibleLinesCount: Integer;
AClipRect: TRect;
begin
// General text configurations which apply to all lines
// Configure the text color
if csfEnabled in AState then
lTextColor := AStateEx.Font.Color
else
lTextColor := clGrayText;
ADest.Brush.Style := bsClear;
ADest.Font.Assign(AStateEx.Font);
ADest.Font.Color := lTextColor;
lTextLeftSpacing := GetMeasures(TCDEDIT_LEFT_TEXT_SPACING);
//lTextRightSpacing := GetMeasures(TCDEDIT_RIGHT_TEXT_SPACING);
lTextTopSpacing := GetMeasures(TCDEDIT_TOP_TEXT_SPACING);
lTextBottomSpacing := GetMeasures(TCDEDIT_BOTTOM_TEXT_SPACING);
lLineHeight := ADest.TextHeight(cddTestStr)+2;
lLineHeight := Min(ASize.cy-lTextBottomSpacing, lLineHeight);
// Fill this to be used in other parts
AStateEx.LineHeight := lLineHeight;
AStateEx.FullyVisibleLinesCount := ASize.cy - lTextTopSpacing - lTextBottomSpacing;
AStateEx.FullyVisibleLinesCount := AStateEx.FullyVisibleLinesCount div lLineHeight;
AStateEx.FullyVisibleLinesCount := Min(AStateEx.FullyVisibleLinesCount, AStateEx.Lines.Count);
// Calculate how many lines to draw
if AStateEx.Multiline then
lVisibleLinesCount := AStateEx.FullyVisibleLinesCount + 1
else
lVisibleLinesCount := 1;
lVisibleLinesCount := Min(lVisibleLinesCount, AStateEx.Lines.Count);
// Now draw each line
for i := 0 to lVisibleLinesCount - 1 do
begin
lControlText := AStateEx.Lines.Strings[AStateEx.VisibleTextStart.Y+i];
lControlText := VisibleText(lControlText, AStateEx.PasswordChar);
lControlTextLen := UTF8Length(lControlText);
lLineTop := lTextTopSpacing + i * lLineHeight;
// The text
ADest.Pen.Style := psClear;
ADest.Brush.Style := bsClear;
lVisibleText := UTF8Copy(lControlText, AStateEx.VisibleTextStart.X, lControlTextLen);
// ToDo: Implement multi-line selection
if (AStateEx.SelLength = 0) or (AStateEx.SelStart.Y <> AStateEx.VisibleTextStart.Y+i) then
begin
ADest.TextOut(lTextLeftSpacing, lLineTop, lVisibleText);
end
// Text and Selection
else
begin
lSelLeftPos := AStateEx.SelStart.X;
if AStateEx.SelLength < 0 then lSelLeftPos := lSelLeftPos + AStateEx.SelLength;
lSelRightPos := AStateEx.SelStart.X;
if AStateEx.SelLength > 0 then lSelRightPos := lSelRightPos + AStateEx.SelLength;
lSelLength := AStateEx.SelLength;
if lSelLength < 0 then lSelLength := lSelLength * -1;
// Draw a normal text
ADest.Font.Color := lTextColor;
ADest.TextOut(lTextLeftSpacing, lLineTop, lVisibleText);
// Draw a selected text
ADest.Brush.Color := clHighlight;
ADest.Font.Color := clHighlightText;
// Calculate a clip rect
AClipRect := ADest.ClipRect;
AClipRect.Left := ADest.TextWidth(UTF8Copy(lVisibleText, 1, lSelLeftPos));
AClipRect.Right := ADest.TextWidth(UTF8Copy(lVisibleText, 1, lSelLeftPos + lSelLength));
IntersectClipRect(ADest.Handle, AClipRect.Left, AClipRect.Top, AClipRect.Right, AClipRect.Bottom);
ADest.TextOut(lTextLeftSpacing, lLineTop, lVisibleText);
end;
end;
// And the caret
DrawCaret(ADest, Point(0, 0), ASize, AState, AStateEx);
end;
{ TKASCDEdit }
procedure TKASCDEdit.CreatePopupMenu;
var
MenuItem: TMenuItem;
begin
if not Assigned(FEditMenu) then
begin
FEditMenu:= TPopupMenu.Create(Application);
MenuItem:= TMenuItem.Create(FEditMenu);
MenuItem.Caption:= rsMnuCopyToClipboard;
MenuItem.OnClick:= MenuCopy;
FEditMenu.Items.Add(MenuItem);
MenuItem:= TMenuItem.Create(FEditMenu);
MenuItem.Caption:= '-';
FEditMenu.Items.Add(MenuItem);
MenuItem:= TMenuItem.Create(FEditMenu);
MenuItem.Caption:= rsMnuSelectAll;
MenuItem.OnClick:= MenuSelectAll;
FEditMenu.Items.Add(MenuItem);
end;
end;
procedure TKASCDEdit.ShowMenu(Data: PtrInt);
begin
FEditMenu.Tag:= Data;
FEditMenu.PopUp;
end;
procedure TKASCDEdit.MenuCopy(Sender: TObject);
begin
TKASCDEdit(TMenuItem(Sender).Owner.Tag).CopyToClipboard;
end;
procedure TKASCDEdit.MenuSelectAll(Sender: TObject);
begin
TKASCDEdit(TMenuItem(Sender).Owner.Tag).SelectAll;
end;
function TKASCDEdit.MousePosToCaretPos(X, Y: Integer): TPoint;
var
lStrLen, i: PtrInt;
lBeforeStr: String;
lTextLeftSpacing: Integer;
lVisibleStr, lCurChar: String;
lPos: Integer;
lBestDiff: Cardinal = $FFFFFFFF;
lLastDiff: Cardinal = $FFFFFFFF;
lCurDiff, lBestMatch: Integer;
begin
// Find the best Y position
lPos := Y - FDrawer.GetMeasures(TCDEDIT_TOP_TEXT_SPACING);
Result.Y := lPos div FEditState.LineHeight;
Result.Y := Min(Result.Y, FEditState.FullyVisibleLinesCount);
Result.Y := Min(Result.Y, FEditState.Lines.Count-1);
if Result.Y < 0 then
begin
Result.X := 1;
Result.Y := 0;
Exit;
end;
// Find the best X position
Canvas.Font := Font;
lVisibleStr := Lines.Strings[Result.Y];
lVisibleStr := LazUTF8.UTF8Copy(lVisibleStr, FEditState.VisibleTextStart.X, Length(lVisibleStr));
lVisibleStr := TCDDrawer.VisibleText(lVisibleStr, FEditState.PasswordChar);
lStrLen := LazUTF8.UTF8Length(lVisibleStr);
lTextLeftSpacing := FDrawer.GetMeasures(TCDEDIT_LEFT_TEXT_SPACING);
lBestMatch := 0;
lBeforeStr := EmptyStr;
lPos := lTextLeftSpacing;
for i := 0 to lStrLen do
begin
lCurDiff := X - lPos;
if lCurDiff < 0 then lCurDiff := lCurDiff * -1;
if lCurDiff < lBestDiff then
begin
lBestDiff := lCurDiff;
lBestMatch := i;
end;
// When the diff starts to grow we already found the caret pos, so exit
if lCurDiff > lLastDiff then Break
else lLastDiff := lCurDiff;
if i <> lStrLen then
begin
lCurChar := LazUTF8.UTF8Copy(lVisibleStr, i + 1, 1);
lBeforeStr := lBeforeStr + lCurChar;
lPos := lTextLeftSpacing + Canvas.TextWidth(lBeforeStr);
end;
end;
Result.X := lBestMatch+(FEditState.VisibleTextStart.X-1);
Result.X := Min(Result.X, FEditState.VisibleTextStart.X+lStrLen-1);
end;
procedure TKASCDEdit.RealSetText(const Value: TCaption);
begin
Lines.Text := Value;
inherited RealSetText(Value);
end;
procedure TKASCDEdit.CalculatePreferredSize(var PreferredWidth,
PreferredHeight: Integer; WithThemeSpace: Boolean);
var
AWidth: Integer;
begin
if (Parent = nil) or (not Parent.HandleAllocated) then Exit;
AWidth := Constraints.MinMaxWidth(10000);
CalculateSize(AWidth, PreferredWidth, PreferredHeight);
end;
procedure TKASCDEdit.CalculateSize(MaxWidth: Integer; var NeededWidth,
NeededHeight: Integer);
var
DC: HDC;
R: TRect;
Flags: Cardinal;
OldFont: HGDIOBJ;
LabelText: String;
lTextLeftSpacing, lTextTopSpacing,
lTextBottomSpacing, lTextRightSpacing: Integer;
begin
LabelText := Text;
if LabelText = '' then
begin
NeededWidth:= 1;
NeededHeight:= 1;
Exit;
end;
lTextLeftSpacing := FDrawer.GetMeasures(TCDEDIT_LEFT_TEXT_SPACING);
lTextTopSpacing := FDrawer.GetMeasures(TCDEDIT_TOP_TEXT_SPACING);
lTextRightSpacing := FDrawer.GetMeasures(TCDEDIT_RIGHT_TEXT_SPACING);
lTextBottomSpacing := FDrawer.GetMeasures(TCDEDIT_BOTTOM_TEXT_SPACING);
DC := GetDC(Parent.Handle);
try
R := Rect(0, 0, MaxWidth, 10000);
OldFont := SelectObject(DC, HGDIOBJ(Font.Reference.Handle));
Flags := DT_CALCRECT or DT_EXPANDTABS;
if not MultiLine then Flags := Flags or DT_SINGLELINE;
DrawText(DC, PAnsiChar(LabelText), Length(LabelText), R, Flags);
SelectObject(DC, OldFont);
NeededWidth := R.Right - R.Left + lTextLeftSpacing + lTextRightSpacing;
NeededHeight := R.Bottom - R.Top + lTextTopSpacing + lTextBottomSpacing;
finally
ReleaseDC(Parent.Handle, DC);
end;
end;
procedure TKASCDEdit.KeyDown(var Key: word; Shift: TShiftState);
begin
if (ssModifier in Shift) then
begin
case Key of
VK_A:
begin
SelectAll;
Key:= 0;
end;
VK_C:
begin
CopyToClipboard;
Key:= 0;
end;
end;
end;
if ReadOnly and (Key in [VK_BACK, VK_DELETE]) then
begin
Key:= 0;
end;
inherited KeyDown(Key, Shift);
end;
constructor TKASCDEdit.Create(AOwner: TComponent);
begin
CreatePopupMenu;
inherited Create(AOwner);
ReadOnly:= True;
Cursor:= crIBeam;
Color:= clDefault;
DrawStyle:= dsExtra1;
ControlStyle:= ControlStyle + [csParentBackground] - [csOpaque];
end;
procedure TKASCDEdit.MouseMove(Shift: TShiftState; X, Y: integer);
begin
inherited MouseMove(Shift, X, Y);
// Mouse dragging selection
if FDragDropStarted then
begin
FEditState.CaretPos := MousePosToCaretPos(X, Y);
FEditState.SelLength := FEditState.CaretPos.X - FEditState.SelStart.X;
Invalidate;
end;
end;
procedure TKASCDEdit.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: integer);
begin
if (Button = mbLeft) or (GetSelLength = 0) then
begin
inherited MouseDown(Button, Shift, X, Y);
{$IF Laz_FullVersion >= 4990000}
// see also LCL 4b41b7a:
// customdrawn: remove redundant SetFocus from TCDControl.MouseDown
SetFocus();
{$ENDIF}
FDragDropStarted := True;
// Caret positioning
FEditState.CaretPos := MousePosToCaretPos(X, Y);
FEditState.SelStart.X := FEditState.CaretPos.X;
FEditState.SelStart.Y := FEditState.CaretPos.Y;
Invalidate;
end
else if Assigned(OnMouseDown) then begin
OnMouseDown(Self, Button, Shift, X, Y);
end;
end;
procedure TKASCDEdit.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
inherited MouseUp(Button, Shift, X, Y);
FDragDropStarted := False;
if Button = mbRight then begin
Application.QueueAsyncCall(ShowMenu, PtrInt(Self));
end;
end;
procedure TKASCDEdit.SelectAll;
begin
FEditState.SelStart.X:= 0;
FEditState.SelLength:= UTF8Length(Text);
Invalidate;
end;
procedure TKASCDEdit.CopyToClipboard;
begin
if (FEditState.SelLength >= 0) then
Clipboard.AsText:= UTF8Copy(Text, FEditState.SelStart.X + 1, FEditState.SelLength)
else begin
Clipboard.AsText:= UTF8Copy(Text, FEditState.SelStart.X + FEditState.SelLength + 1, -FEditState.SelLength);
end;
end;
initialization
RegisterDrawer(TKASCDDrawer.Create, dsExtra1);
end.