kascdedit.pas 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. {
  2. Double Commander
  3. -------------------------------------------------------------------------
  4. Custom edit control with the look and feel like TLabel
  5. Copyright (C) 2017-2024 Alexander Koblov ([email protected])
  6. This program is free software; you can redistribute it and/or
  7. modify it under the terms of the GNU General Public License
  8. as published by the Free Software Foundation; either version 2
  9. of the License, or (at your option) any later version.
  10. This program is distributed in the hope that it will be useful,
  11. but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. GNU General Public License for more details.
  14. You should have received a copy of the GNU General Public License
  15. along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. }
  17. unit KASCDEdit;
  18. {$mode delphi}
  19. interface
  20. uses
  21. Classes, SysUtils, LResources, Controls, Graphics, Dialogs, Types,
  22. Menus, CustomDrawnControls, CustomDrawnDrawers, CustomDrawn_Common;
  23. type
  24. { TKASCDEdit }
  25. TKASCDEdit = class(TCDEdit)
  26. private
  27. FDragDropStarted: Boolean;
  28. FEditMenu: TPopupMenu; static;
  29. private
  30. procedure CreatePopupMenu;
  31. procedure ShowMenu(Data: PtrInt);
  32. procedure MenuCopy(Sender: TObject);
  33. procedure MenuSelectAll(Sender: TObject);
  34. function MousePosToCaretPos(X, Y: Integer): TPoint;
  35. protected
  36. procedure RealSetText(const Value: TCaption); override;
  37. procedure CalculatePreferredSize(var PreferredWidth, PreferredHeight: Integer;
  38. WithThemeSpace: Boolean); override;
  39. procedure CalculateSize(MaxWidth: Integer; var NeededWidth, NeededHeight: Integer);
  40. procedure KeyDown(var Key: word; Shift: TShiftState); override;
  41. public
  42. constructor Create(AOwner: TComponent); override;
  43. procedure MouseMove(Shift: TShiftState; X, Y: integer); override;
  44. procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
  45. procedure MouseUp(Button: TMouseButton; Shift:TShiftState; X,Y:Integer); override;
  46. public
  47. procedure SelectAll;
  48. procedure CopyToClipboard;
  49. published
  50. property Color default clDefault;
  51. property Cursor default crIBeam;
  52. property ReadOnly default True;
  53. property OnMouseDown;
  54. property OnMouseMove;
  55. property OnMouseUp;
  56. end;
  57. { TKASCDDrawer }
  58. TKASCDDrawer = class(TCDDrawerCommon)
  59. public
  60. function GetMeasures(AMeasureID: Integer): Integer; override;
  61. procedure DrawEditBackground(ADest: TCanvas; ADestPos: TPoint; ASize: TSize;
  62. AState: TCDControlState; AStateEx: TCDEditStateEx); override;
  63. procedure DrawEdit(ADest: TCanvas; ASize: TSize;
  64. AState: TCDControlState; AStateEx: TCDEditStateEx); override;
  65. end;
  66. procedure Register;
  67. implementation
  68. uses
  69. Math, Forms, Clipbrd, LCLType, LCLIntf, LazUTF8;
  70. resourcestring
  71. rsMnuCopyToClipboard = 'Copy';
  72. rsMnuSelectAll = 'Select &All';
  73. procedure Register;
  74. begin
  75. RegisterComponents('KASComponents', [TKASCDEdit]);
  76. end;
  77. { TKASCDDrawer }
  78. function TKASCDDrawer.GetMeasures(AMeasureID: Integer): Integer;
  79. begin
  80. case AMeasureID of
  81. TCDEDIT_LEFT_TEXT_SPACING: Result := 0;
  82. TCDEDIT_RIGHT_TEXT_SPACING: Result := 0;
  83. else Result:= inherited GetMeasures(AMeasureID);
  84. end;
  85. end;
  86. procedure TKASCDDrawer.DrawEditBackground(ADest: TCanvas; ADestPos: TPoint;
  87. ASize: TSize; AState: TCDControlState; AStateEx: TCDEditStateEx);
  88. begin
  89. // None
  90. end;
  91. procedure TKASCDDrawer.DrawEdit(ADest: TCanvas; ASize: TSize;
  92. AState: TCDControlState; AStateEx: TCDEditStateEx);
  93. var
  94. lVisibleText, lControlText: TCaption;
  95. lSelLeftPos, lSelLength, lSelRightPos: Integer;
  96. lLineHeight, lLineTop: Integer;
  97. lControlTextLen: PtrInt;
  98. lTextLeftSpacing, lTextTopSpacing, lTextBottomSpacing: Integer;
  99. lTextColor: TColor;
  100. i, lVisibleLinesCount: Integer;
  101. AClipRect: TRect;
  102. begin
  103. // General text configurations which apply to all lines
  104. // Configure the text color
  105. if csfEnabled in AState then
  106. lTextColor := AStateEx.Font.Color
  107. else
  108. lTextColor := clGrayText;
  109. ADest.Brush.Style := bsClear;
  110. ADest.Font.Assign(AStateEx.Font);
  111. ADest.Font.Color := lTextColor;
  112. lTextLeftSpacing := GetMeasures(TCDEDIT_LEFT_TEXT_SPACING);
  113. //lTextRightSpacing := GetMeasures(TCDEDIT_RIGHT_TEXT_SPACING);
  114. lTextTopSpacing := GetMeasures(TCDEDIT_TOP_TEXT_SPACING);
  115. lTextBottomSpacing := GetMeasures(TCDEDIT_BOTTOM_TEXT_SPACING);
  116. lLineHeight := ADest.TextHeight(cddTestStr)+2;
  117. lLineHeight := Min(ASize.cy-lTextBottomSpacing, lLineHeight);
  118. // Fill this to be used in other parts
  119. AStateEx.LineHeight := lLineHeight;
  120. AStateEx.FullyVisibleLinesCount := ASize.cy - lTextTopSpacing - lTextBottomSpacing;
  121. AStateEx.FullyVisibleLinesCount := AStateEx.FullyVisibleLinesCount div lLineHeight;
  122. AStateEx.FullyVisibleLinesCount := Min(AStateEx.FullyVisibleLinesCount, AStateEx.Lines.Count);
  123. // Calculate how many lines to draw
  124. if AStateEx.Multiline then
  125. lVisibleLinesCount := AStateEx.FullyVisibleLinesCount + 1
  126. else
  127. lVisibleLinesCount := 1;
  128. lVisibleLinesCount := Min(lVisibleLinesCount, AStateEx.Lines.Count);
  129. // Now draw each line
  130. for i := 0 to lVisibleLinesCount - 1 do
  131. begin
  132. lControlText := AStateEx.Lines.Strings[AStateEx.VisibleTextStart.Y+i];
  133. lControlText := VisibleText(lControlText, AStateEx.PasswordChar);
  134. lControlTextLen := UTF8Length(lControlText);
  135. lLineTop := lTextTopSpacing + i * lLineHeight;
  136. // The text
  137. ADest.Pen.Style := psClear;
  138. ADest.Brush.Style := bsClear;
  139. lVisibleText := UTF8Copy(lControlText, AStateEx.VisibleTextStart.X, lControlTextLen);
  140. // ToDo: Implement multi-line selection
  141. if (AStateEx.SelLength = 0) or (AStateEx.SelStart.Y <> AStateEx.VisibleTextStart.Y+i) then
  142. begin
  143. ADest.TextOut(lTextLeftSpacing, lLineTop, lVisibleText);
  144. end
  145. // Text and Selection
  146. else
  147. begin
  148. lSelLeftPos := AStateEx.SelStart.X;
  149. if AStateEx.SelLength < 0 then lSelLeftPos := lSelLeftPos + AStateEx.SelLength;
  150. lSelRightPos := AStateEx.SelStart.X;
  151. if AStateEx.SelLength > 0 then lSelRightPos := lSelRightPos + AStateEx.SelLength;
  152. lSelLength := AStateEx.SelLength;
  153. if lSelLength < 0 then lSelLength := lSelLength * -1;
  154. // Draw a normal text
  155. ADest.Font.Color := lTextColor;
  156. ADest.TextOut(lTextLeftSpacing, lLineTop, lVisibleText);
  157. // Draw a selected text
  158. ADest.Brush.Color := clHighlight;
  159. ADest.Font.Color := clHighlightText;
  160. // Calculate a clip rect
  161. AClipRect := ADest.ClipRect;
  162. AClipRect.Left := ADest.TextWidth(UTF8Copy(lVisibleText, 1, lSelLeftPos));
  163. AClipRect.Right := ADest.TextWidth(UTF8Copy(lVisibleText, 1, lSelLeftPos + lSelLength));
  164. IntersectClipRect(ADest.Handle, AClipRect.Left, AClipRect.Top, AClipRect.Right, AClipRect.Bottom);
  165. ADest.TextOut(lTextLeftSpacing, lLineTop, lVisibleText);
  166. end;
  167. end;
  168. // And the caret
  169. DrawCaret(ADest, Point(0, 0), ASize, AState, AStateEx);
  170. end;
  171. { TKASCDEdit }
  172. procedure TKASCDEdit.CreatePopupMenu;
  173. var
  174. MenuItem: TMenuItem;
  175. begin
  176. if not Assigned(FEditMenu) then
  177. begin
  178. FEditMenu:= TPopupMenu.Create(Application);
  179. MenuItem:= TMenuItem.Create(FEditMenu);
  180. MenuItem.Caption:= rsMnuCopyToClipboard;
  181. MenuItem.OnClick:= MenuCopy;
  182. FEditMenu.Items.Add(MenuItem);
  183. MenuItem:= TMenuItem.Create(FEditMenu);
  184. MenuItem.Caption:= '-';
  185. FEditMenu.Items.Add(MenuItem);
  186. MenuItem:= TMenuItem.Create(FEditMenu);
  187. MenuItem.Caption:= rsMnuSelectAll;
  188. MenuItem.OnClick:= MenuSelectAll;
  189. FEditMenu.Items.Add(MenuItem);
  190. end;
  191. end;
  192. procedure TKASCDEdit.ShowMenu(Data: PtrInt);
  193. begin
  194. FEditMenu.Tag:= Data;
  195. FEditMenu.PopUp;
  196. end;
  197. procedure TKASCDEdit.MenuCopy(Sender: TObject);
  198. begin
  199. TKASCDEdit(TMenuItem(Sender).Owner.Tag).CopyToClipboard;
  200. end;
  201. procedure TKASCDEdit.MenuSelectAll(Sender: TObject);
  202. begin
  203. TKASCDEdit(TMenuItem(Sender).Owner.Tag).SelectAll;
  204. end;
  205. function TKASCDEdit.MousePosToCaretPos(X, Y: Integer): TPoint;
  206. var
  207. lStrLen, i: PtrInt;
  208. lBeforeStr: String;
  209. lTextLeftSpacing: Integer;
  210. lVisibleStr, lCurChar: String;
  211. lPos: Integer;
  212. lBestDiff: Cardinal = $FFFFFFFF;
  213. lLastDiff: Cardinal = $FFFFFFFF;
  214. lCurDiff, lBestMatch: Integer;
  215. begin
  216. // Find the best Y position
  217. lPos := Y - FDrawer.GetMeasures(TCDEDIT_TOP_TEXT_SPACING);
  218. Result.Y := lPos div FEditState.LineHeight;
  219. Result.Y := Min(Result.Y, FEditState.FullyVisibleLinesCount);
  220. Result.Y := Min(Result.Y, FEditState.Lines.Count-1);
  221. if Result.Y < 0 then
  222. begin
  223. Result.X := 1;
  224. Result.Y := 0;
  225. Exit;
  226. end;
  227. // Find the best X position
  228. Canvas.Font := Font;
  229. lVisibleStr := Lines.Strings[Result.Y];
  230. lVisibleStr := LazUTF8.UTF8Copy(lVisibleStr, FEditState.VisibleTextStart.X, Length(lVisibleStr));
  231. lVisibleStr := TCDDrawer.VisibleText(lVisibleStr, FEditState.PasswordChar);
  232. lStrLen := LazUTF8.UTF8Length(lVisibleStr);
  233. lTextLeftSpacing := FDrawer.GetMeasures(TCDEDIT_LEFT_TEXT_SPACING);
  234. lBestMatch := 0;
  235. lBeforeStr := EmptyStr;
  236. lPos := lTextLeftSpacing;
  237. for i := 0 to lStrLen do
  238. begin
  239. lCurDiff := X - lPos;
  240. if lCurDiff < 0 then lCurDiff := lCurDiff * -1;
  241. if lCurDiff < lBestDiff then
  242. begin
  243. lBestDiff := lCurDiff;
  244. lBestMatch := i;
  245. end;
  246. // When the diff starts to grow we already found the caret pos, so exit
  247. if lCurDiff > lLastDiff then Break
  248. else lLastDiff := lCurDiff;
  249. if i <> lStrLen then
  250. begin
  251. lCurChar := LazUTF8.UTF8Copy(lVisibleStr, i + 1, 1);
  252. lBeforeStr := lBeforeStr + lCurChar;
  253. lPos := lTextLeftSpacing + Canvas.TextWidth(lBeforeStr);
  254. end;
  255. end;
  256. Result.X := lBestMatch+(FEditState.VisibleTextStart.X-1);
  257. Result.X := Min(Result.X, FEditState.VisibleTextStart.X+lStrLen-1);
  258. end;
  259. procedure TKASCDEdit.RealSetText(const Value: TCaption);
  260. begin
  261. Lines.Text := Value;
  262. inherited RealSetText(Value);
  263. end;
  264. procedure TKASCDEdit.CalculatePreferredSize(var PreferredWidth,
  265. PreferredHeight: Integer; WithThemeSpace: Boolean);
  266. var
  267. AWidth: Integer;
  268. begin
  269. if (Parent = nil) or (not Parent.HandleAllocated) then Exit;
  270. AWidth := Constraints.MinMaxWidth(10000);
  271. CalculateSize(AWidth, PreferredWidth, PreferredHeight);
  272. end;
  273. procedure TKASCDEdit.CalculateSize(MaxWidth: Integer; var NeededWidth,
  274. NeededHeight: Integer);
  275. var
  276. DC: HDC;
  277. R: TRect;
  278. Flags: Cardinal;
  279. OldFont: HGDIOBJ;
  280. LabelText: String;
  281. lTextLeftSpacing, lTextTopSpacing,
  282. lTextBottomSpacing, lTextRightSpacing: Integer;
  283. begin
  284. LabelText := Text;
  285. if LabelText = '' then
  286. begin
  287. NeededWidth:= 1;
  288. NeededHeight:= 1;
  289. Exit;
  290. end;
  291. lTextLeftSpacing := FDrawer.GetMeasures(TCDEDIT_LEFT_TEXT_SPACING);
  292. lTextTopSpacing := FDrawer.GetMeasures(TCDEDIT_TOP_TEXT_SPACING);
  293. lTextRightSpacing := FDrawer.GetMeasures(TCDEDIT_RIGHT_TEXT_SPACING);
  294. lTextBottomSpacing := FDrawer.GetMeasures(TCDEDIT_BOTTOM_TEXT_SPACING);
  295. DC := GetDC(Parent.Handle);
  296. try
  297. R := Rect(0, 0, MaxWidth, 10000);
  298. OldFont := SelectObject(DC, HGDIOBJ(Font.Reference.Handle));
  299. Flags := DT_CALCRECT or DT_EXPANDTABS;
  300. if not MultiLine then Flags := Flags or DT_SINGLELINE;
  301. DrawText(DC, PAnsiChar(LabelText), Length(LabelText), R, Flags);
  302. SelectObject(DC, OldFont);
  303. NeededWidth := R.Right - R.Left + lTextLeftSpacing + lTextRightSpacing;
  304. NeededHeight := R.Bottom - R.Top + lTextTopSpacing + lTextBottomSpacing;
  305. finally
  306. ReleaseDC(Parent.Handle, DC);
  307. end;
  308. end;
  309. procedure TKASCDEdit.KeyDown(var Key: word; Shift: TShiftState);
  310. begin
  311. if (ssModifier in Shift) then
  312. begin
  313. case Key of
  314. VK_A:
  315. begin
  316. SelectAll;
  317. Key:= 0;
  318. end;
  319. VK_C:
  320. begin
  321. CopyToClipboard;
  322. Key:= 0;
  323. end;
  324. end;
  325. end;
  326. if ReadOnly and (Key in [VK_BACK, VK_DELETE]) then
  327. begin
  328. Key:= 0;
  329. end;
  330. inherited KeyDown(Key, Shift);
  331. end;
  332. constructor TKASCDEdit.Create(AOwner: TComponent);
  333. begin
  334. CreatePopupMenu;
  335. inherited Create(AOwner);
  336. ReadOnly:= True;
  337. Cursor:= crIBeam;
  338. Color:= clDefault;
  339. DrawStyle:= dsExtra1;
  340. ControlStyle:= ControlStyle + [csParentBackground] - [csOpaque];
  341. end;
  342. procedure TKASCDEdit.MouseMove(Shift: TShiftState; X, Y: integer);
  343. begin
  344. inherited MouseMove(Shift, X, Y);
  345. // Mouse dragging selection
  346. if FDragDropStarted then
  347. begin
  348. FEditState.CaretPos := MousePosToCaretPos(X, Y);
  349. FEditState.SelLength := FEditState.CaretPos.X - FEditState.SelStart.X;
  350. Invalidate;
  351. end;
  352. end;
  353. procedure TKASCDEdit.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  354. Y: integer);
  355. begin
  356. if (Button = mbLeft) or (GetSelLength = 0) then
  357. begin
  358. inherited MouseDown(Button, Shift, X, Y);
  359. FDragDropStarted := True;
  360. // Caret positioning
  361. FEditState.CaretPos := MousePosToCaretPos(X, Y);
  362. FEditState.SelStart.X := FEditState.CaretPos.X;
  363. FEditState.SelStart.Y := FEditState.CaretPos.Y;
  364. Invalidate;
  365. end
  366. else if Assigned(OnMouseDown) then begin
  367. OnMouseDown(Self, Button, Shift, X, Y);
  368. end;
  369. end;
  370. procedure TKASCDEdit.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
  371. begin
  372. inherited MouseUp(Button, Shift, X, Y);
  373. FDragDropStarted := False;
  374. if Button = mbRight then begin
  375. Application.QueueAsyncCall(ShowMenu, PtrInt(Self));
  376. end;
  377. end;
  378. procedure TKASCDEdit.SelectAll;
  379. begin
  380. FEditState.SelStart.X:= 0;
  381. FEditState.SelLength:= UTF8Length(Text);
  382. Invalidate;
  383. end;
  384. procedure TKASCDEdit.CopyToClipboard;
  385. begin
  386. if (FEditState.SelLength >= 0) then
  387. Clipboard.AsText:= UTF8Copy(Text, FEditState.SelStart.X + 1, FEditState.SelLength)
  388. else begin
  389. Clipboard.AsText:= UTF8Copy(Text, FEditState.SelStart.X + FEditState.SelLength + 1, -FEditState.SelLength);
  390. end;
  391. end;
  392. initialization
  393. RegisterDrawer(TKASCDDrawer.Create, dsExtra1);
  394. end.