kaspathedit.pas 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. {
  2. Double Commander Components
  3. -------------------------------------------------------------------------
  4. Path edit class with auto complete feature
  5. Copyright (C) 2012-2022 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 as
  8. published by the Free Software Foundation; either version 2 of the
  9. License, or (at your option) any later version.
  10. This program is distributed in the hope that it will be useful, but
  11. WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. 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 KASPathEdit;
  18. {$mode delphi}
  19. {$IF DEFINED(LCLCOCOA)}
  20. {$modeswitch objectivec1}
  21. {$ENDIF}
  22. interface
  23. uses
  24. Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, StdCtrls,
  25. ShellCtrls, LCLType, LCLVersion
  26. {$IF DEFINED(LCLCOCOA)}
  27. , CocoaAll, CocoaWindows
  28. {$ENDIF}
  29. ;
  30. type
  31. { TKASPathEdit }
  32. TKASPathEdit = class(TEdit)
  33. private
  34. FKeyDown: Word;
  35. FBasePath: String;
  36. FListBox: TListBox;
  37. FPanel: THintWindow;
  38. FAutoComplete: Boolean;
  39. FStringList: TStringList;
  40. FObjectTypes: TObjectTypes;
  41. FFileSortType: TFileSortType;
  42. private
  43. procedure setTextAndSelect( newText:String );
  44. procedure handleSpecialKeys( var Key: Word );
  45. procedure handleUpKey;
  46. procedure handleDownKey;
  47. procedure AutoComplete(const Path: String);
  48. procedure SetObjectTypes(const AValue: TObjectTypes);
  49. procedure FormChangeBoundsEvent(Sender: TObject);
  50. procedure ListBoxClick(Sender: TObject);
  51. procedure ListBoxMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
  52. private
  53. function isShowingListBox(): Boolean; inline;
  54. procedure ShowListBox;
  55. procedure HideListBox;
  56. protected
  57. {$IF DEFINED(LCLWIN32)}
  58. procedure CreateWnd; override;
  59. {$ENDIF}
  60. {$IF DEFINED(LCLCOCOA)}
  61. procedure TextChanged; override;
  62. {$ENDIF}
  63. procedure DoExit; override;
  64. procedure VisibleChanged; override;
  65. procedure KeyDown(var Key: Word; Shift: TShiftState); override;
  66. procedure KeyUpAfterInterface(var Key: Word; Shift: TShiftState); override;
  67. public
  68. onKeyESCAPE: TNotifyEvent;
  69. onKeyRETURN: TNotifyEvent;
  70. constructor Create(AOwner: TComponent); override;
  71. destructor Destroy; override;
  72. published
  73. property ObjectTypes: TObjectTypes read FObjectTypes write SetObjectTypes;
  74. property FileSortType: TFileSortType read FFileSortType write FFileSortType;
  75. end;
  76. procedure Register;
  77. implementation
  78. uses
  79. LazUTF8, Math, LazFileUtils, Masks
  80. {$IF DEFINED(LCLWIN32)}
  81. , ComObj
  82. {$ENDIF}
  83. {$IF DEFINED(MSWINDOWS)}
  84. , Windows
  85. {$ENDIF}
  86. ;
  87. {$IF DEFINED(LCLWIN32)}
  88. const
  89. SHACF_AUTOAPPEND_FORCE_ON = $40000000;
  90. SHACF_AUTOSUGGEST_FORCE_ON = $10000000;
  91. SHACF_FILESYS_ONLY = $00000010;
  92. SHACF_FILESYS_DIRS = $00000020;
  93. function SHAutoComplete(hwndEdit: HWND; dwFlags: DWORD): HRESULT; stdcall; external 'shlwapi.dll';
  94. function SHAutoCompleteX(hwndEdit: HWND; ObjectTypes: TObjectTypes): Boolean;
  95. var
  96. dwFlags: DWORD;
  97. begin
  98. if (ObjectTypes = []) then Exit(False);
  99. dwFlags := SHACF_AUTOAPPEND_FORCE_ON or SHACF_AUTOSUGGEST_FORCE_ON;
  100. if (otNonFolders in ObjectTypes) then
  101. dwFlags := dwFlags or SHACF_FILESYS_ONLY
  102. else if (otFolders in ObjectTypes) then
  103. dwFlags := dwFlags or SHACF_FILESYS_DIRS;
  104. Result:= (SHAutoComplete(hwndEdit, dwFlags) = 0);
  105. end;
  106. {$ENDIF}
  107. procedure Register;
  108. begin
  109. RegisterComponents('KASComponents', [TKASPathEdit]);
  110. end;
  111. function FilesSortAlphabet(List: TStringList; Index1, Index2: Integer): Integer;
  112. begin
  113. Result:= CompareFilenames(List[Index1], List[Index2]);
  114. end;
  115. function FilesSortFoldersFirst(List: TStringList; Index1, Index2: Integer): Integer;
  116. var
  117. Attr1, Attr2: IntPtr;
  118. begin
  119. Attr1:= IntPtr(List.Objects[Index1]);
  120. Attr2:= IntPtr(List.Objects[Index2]);
  121. if (Attr1 and faDirectory <> 0) and (Attr2 and faDirectory <> 0) then
  122. Result:= CompareFilenames(List[Index1], List[Index2])
  123. else begin
  124. if (Attr1 and faDirectory <> 0) then
  125. Result:= -1
  126. else begin
  127. Result:= 1;
  128. end;
  129. end;
  130. end;
  131. procedure GetFilesInDir(const ABaseDir: String; AMask: String; AObjectTypes: TObjectTypes;
  132. AResult: TStringList; AFileSortType: TFileSortType);
  133. var
  134. ExcludeAttr: Integer;
  135. SearchRec: TSearchRec;
  136. {$IF DEFINED(MSWINDOWS)}
  137. ErrMode : LongWord;
  138. {$ENDIF}
  139. begin
  140. {$IF DEFINED(MSWINDOWS)}
  141. ErrMode:= SetErrorMode(SEM_FAILCRITICALERRORS or SEM_NOALIGNMENTFAULTEXCEPT or SEM_NOGPFAULTERRORBOX or SEM_NOOPENFILEERRORBOX);
  142. try
  143. {$ENDIF}
  144. if FindFirst(ABaseDir + AMask, faAnyFile, SearchRec) = 0 then
  145. begin
  146. ExcludeAttr:= 0;
  147. if not (otHidden in AObjectTypes) then
  148. ExcludeAttr:= ExcludeAttr or faHidden;
  149. if not (otFolders in AObjectTypes) then
  150. ExcludeAttr:= ExcludeAttr or faDirectory;
  151. repeat
  152. if (SearchRec.Attr and ExcludeAttr <> 0) then
  153. Continue;
  154. if (SearchRec.Name = '.') or (SearchRec.Name = '..')then
  155. Continue;
  156. if (SearchRec.Attr and faDirectory = 0) and not (otNonFolders in AObjectTypes) then
  157. Continue;
  158. AResult.AddObject(SearchRec.Name, TObject(IntPtr(SearchRec.Attr)));
  159. until FindNext(SearchRec) <> 0;
  160. if AResult.Count > 0 then
  161. begin
  162. case AFileSortType of
  163. fstAlphabet: AResult.CustomSort(@FilesSortAlphabet);
  164. fstFoldersFirst: AResult.CustomSort(@FilesSortFoldersFirst);
  165. end;
  166. end;
  167. end;
  168. SysUtils.FindClose(SearchRec);
  169. {$IF DEFINED(MSWINDOWS)}
  170. finally
  171. SetErrorMode(ErrMode);
  172. end;
  173. {$ENDIF}
  174. end;
  175. { TKASPathEdit }
  176. function TKASPathEdit.isShowingListBox(): Boolean;
  177. begin
  178. Result:= FPanel<>nil;
  179. end;
  180. procedure TKASPathEdit.AutoComplete(const Path: String);
  181. {$IF LCL_FULLVERSION < 4990000}
  182. const
  183. AFlags: array[Boolean] of TMaskOptions = (
  184. [moDisableSets], [moDisableSets, moCaseSensitive]
  185. );
  186. {$ENDIF}
  187. var
  188. I: Integer;
  189. AMask: TMask;
  190. BasePath: String;
  191. begin
  192. FListBox.Clear;
  193. if Pos(PathDelim, Path) = 0 then
  194. HideListBox
  195. else begin
  196. BasePath:= ExtractFilePath(Path);
  197. if CompareFilenames(FBasePath, BasePath) <> 0 then
  198. begin
  199. FStringList.Clear;
  200. FBasePath:= BasePath;
  201. GetFilesInDir(BasePath, AllFilesMask, FObjectTypes, FStringList, FFileSortType);
  202. end;
  203. if (FStringList.Count > 0) then
  204. begin
  205. FListBox.Items.BeginUpdate;
  206. try
  207. // Check mask and make absolute file name
  208. AMask:= TMask.Create(ExtractFileName(Path) + '*',
  209. {$IF LCL_FULLVERSION < 4990000}
  210. AFlags[FileNameCaseSensitive]
  211. {$ELSE}
  212. FileNameCaseSensitive
  213. {$ENDIF}
  214. );
  215. for I:= 0 to FStringList.Count - 1 do
  216. begin
  217. if AMask.Matches(FStringList[I]) then
  218. FListBox.Items.Add(BasePath + FStringList[I]);
  219. end;
  220. AMask.Free;
  221. finally
  222. FListBox.Items.EndUpdate;
  223. end;
  224. if FListBox.Items.Count = 0 then HideListBox;
  225. if FListBox.Items.Count > 0 then
  226. begin
  227. ShowListBox;
  228. // Calculate ListBox height
  229. with FListBox.ItemRect(0) do
  230. I:= Bottom - Top; // TListBox.ItemHeight sometimes don't work under GTK2
  231. with FListBox do
  232. begin
  233. {$IF NOT DEFINED(LCLCOCOA)}
  234. if Items.Count = 1 then
  235. FPanel.ClientHeight:= Self.Height
  236. else
  237. FPanel.ClientHeight:= I * IfThen(Items.Count > 10, 11, Items.Count + 1);
  238. {$ELSE}
  239. FPanel.ClientHeight:= I * IfThen(Items.Count > 10, 11, Items.Count + 1) + trunc(i/2);
  240. {$ENDIF}
  241. end;
  242. end;
  243. end;
  244. end;
  245. end;
  246. procedure TKASPathEdit.SetObjectTypes(const AValue: TObjectTypes);
  247. begin
  248. if FObjectTypes = AValue then Exit;
  249. FObjectTypes:= AValue;
  250. {$IF DEFINED(LCLWIN32)}
  251. if HandleAllocated then RecreateWnd(Self);
  252. if FAutoComplete then
  253. {$ENDIF}
  254. FAutoComplete:= (FObjectTypes <> []);
  255. end;
  256. procedure TKASPathEdit.FormChangeBoundsEvent(Sender: TObject);
  257. begin
  258. HideListBox;
  259. end;
  260. procedure TKASPathEdit.ListBoxClick(Sender: TObject);
  261. begin
  262. if FListBox.ItemIndex >= 0 then
  263. begin
  264. setTextAndSelect( FListBox.Items[FListBox.ItemIndex] );
  265. HideListBox;
  266. SetFocus;
  267. end;
  268. end;
  269. procedure TKASPathEdit.ListBoxMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
  270. begin
  271. FListBox.ItemIndex:= FListBox.ItemAtPos(Classes.Point(X, Y), True);
  272. end;
  273. {$IF DEFINED(LCLCOCOA)}
  274. procedure cocoaNeedMouseEvent( hintWindow: THintWindow );
  275. var
  276. cnt: TCocoaWindowContent;
  277. begin
  278. cnt:= TCocoaWindowContent( hintWindow.Handle );
  279. cnt.window.setIgnoresMouseEvents( false );
  280. end;
  281. {$ENDIF}
  282. procedure TKASPathEdit.ShowListBox;
  283. begin
  284. if not isShowingListBox() then
  285. begin
  286. FPanel:= THintWindow.Create(Self);
  287. {$IF DEFINED(LCLCOCOA)}
  288. cocoaNeedMouseEvent(FPanel);
  289. {$ENDIF}
  290. FPanel.Color:= clDefault;
  291. FListBox.Parent:= FPanel;
  292. with Parent.ClientToScreen(CLasses.Point(Left, Top)) do
  293. begin
  294. FPanel.Left:= X;
  295. FPanel.Top:= Y + Height;
  296. end;
  297. FPanel.Width:= Width;
  298. FPanel.Visible:= True;
  299. Application.AddOnDeactivateHandler(FormChangeBoundsEvent, True);
  300. GetParentForm(Self).AddHandlerOnChangeBounds(FormChangeBoundsEvent, True);
  301. end;
  302. end;
  303. procedure TKASPathEdit.HideListBox;
  304. begin
  305. if isShowingListBox() then
  306. begin
  307. FPanel.Visible:= False;
  308. FListBox.Parent:= nil;
  309. FreeAndNil(FPanel);
  310. Application.RemoveOnDeactivateHandler(FormChangeBoundsEvent);
  311. GetParentForm(Self).RemoveHandlerOnChangeBounds(FormChangeBoundsEvent);
  312. end;
  313. end;
  314. {$IF DEFINED(LCLWIN32)}
  315. procedure TKASPathEdit.CreateWnd;
  316. begin
  317. inherited CreateWnd;
  318. FAutoComplete:= not SHAutoCompleteX(Handle, FObjectTypes);
  319. end;
  320. {$ENDIF}
  321. {$IF DEFINED(LCLCOCOA)}
  322. procedure TKASPathEdit.TextChanged;
  323. begin
  324. Inherited;
  325. if not Modified then
  326. Exit;
  327. if FAutoComplete then
  328. AutoComplete(Text);
  329. end;
  330. {$ENDIF}
  331. procedure TKASPathEdit.setTextAndSelect( newText:String );
  332. var
  333. start: Integer;
  334. begin
  335. if Pos(Text,newText) > 0 then
  336. start:= UTF8Length(Text)
  337. else
  338. start:= UTF8Length(ExtractFilePath(Text));
  339. Text:= newText;
  340. SelStart:= start;
  341. SelLength:= UTF8Length(Text)-SelStart;
  342. end;
  343. procedure TKASPathEdit.DoExit;
  344. begin
  345. HideListBox;
  346. inherited DoExit;
  347. end;
  348. procedure TKASPathEdit.VisibleChanged;
  349. begin
  350. FBasePath:= EmptyStr;
  351. inherited VisibleChanged;
  352. end;
  353. procedure TKASPathEdit.handleSpecialKeys( var Key: Word );
  354. begin
  355. if isShowingListBox() then begin
  356. HideListBox;
  357. Key:= 0;
  358. end else begin
  359. if Key=VK_ESCAPE then begin
  360. if Assigned(onKeyESCAPE) then begin
  361. onKeyESCAPE( self );
  362. Key:= 0;
  363. end;
  364. end else begin
  365. if Assigned(onKeyRETURN) then begin
  366. onKeyRETURN( self );
  367. Key:= 0;
  368. end;
  369. end;
  370. end;
  371. end;
  372. procedure TKASPathEdit.handleUpKey;
  373. begin
  374. if FListBox.ItemIndex = -1 then
  375. FListBox.ItemIndex:= FListBox.Items.Count - 1
  376. else if FListBox.ItemIndex - 1 < 0 then
  377. FListBox.ItemIndex:= - 1
  378. else
  379. FListBox.ItemIndex:= FListBox.ItemIndex - 1;
  380. if FListBox.ItemIndex >= 0 then
  381. setTextAndSelect( FListBox.Items[FListBox.ItemIndex] )
  382. else
  383. setTextAndSelect( ExtractFilePath(Text) );
  384. end;
  385. procedure TKASPathEdit.handleDownKey;
  386. begin
  387. if FListBox.ItemIndex + 1 >= FListBox.Items.Count then
  388. FListBox.ItemIndex:= -1
  389. else if FListBox.ItemIndex = -1 then
  390. FListBox.ItemIndex:= IfThen(FListBox.Items.Count > 0, 0, -1)
  391. else
  392. FListBox.ItemIndex:= FListBox.ItemIndex + 1;
  393. if FListBox.ItemIndex >= 0 then
  394. setTextAndSelect( FListBox.Items[FListBox.ItemIndex] )
  395. else
  396. setTextAndSelect( ExtractFilePath(Text) );
  397. end;
  398. procedure TKASPathEdit.KeyDown(var Key: Word; Shift: TShiftState);
  399. begin
  400. FKeyDown:= Key;
  401. case Key of
  402. VK_ESCAPE,
  403. VK_RETURN,
  404. VK_SELECT:
  405. handleSpecialKeys( Key );
  406. VK_UP:
  407. if isShowingListBox() then
  408. begin
  409. Key:= 0;
  410. handleUpKey();
  411. end;
  412. VK_DOWN:
  413. if isShowingListBox() then
  414. begin
  415. Key:= 0;
  416. handleDownKey();
  417. end;
  418. end;
  419. inherited KeyDown(Key, Shift);
  420. {$IFDEF LCLGTK2}
  421. // Workaround for GTK2 - up and down arrows moving through controls.
  422. if Key in [VK_UP, VK_DOWN] then Key:= 0;
  423. {$ENDIF}
  424. end;
  425. procedure TKASPathEdit.KeyUpAfterInterface(var Key: Word; Shift: TShiftState);
  426. begin
  427. {$IF not DEFINED(LCLCOCOA)}
  428. if (FKeyDown = Key) and FAutoComplete and not (Key in [VK_ESCAPE, VK_RETURN, VK_SELECT, VK_UP, VK_DOWN]) then
  429. begin
  430. if Modified then
  431. begin
  432. Modified:= False;
  433. AutoComplete(Text);
  434. end;
  435. end;
  436. {$ENDIF}
  437. inherited KeyUpAfterInterface(Key, Shift);
  438. {$IF DEFINED(LCLWIN32)}
  439. // Windows auto-completer eats the TAB so LCL doesn't get it and doesn't move to next control.
  440. if not FAutoComplete and (Key = VK_TAB) then
  441. GetParentForm(Self).SelectNext(Self, True, True);
  442. {$ENDIF}
  443. end;
  444. constructor TKASPathEdit.Create(AOwner: TComponent);
  445. begin
  446. inherited Create(AOwner);
  447. FStringList:= TStringList.Create;
  448. FListBox:= TListBox.Create(Self);
  449. FListBox.TabStop:= False;
  450. FListBox.Align:= alClient;
  451. FListBox.ParentFont:= False;
  452. FListBox.ClickOnSelChange:= False;
  453. FListBox.OnClick:= ListBoxClick;
  454. FListBox.OnMouseMove:= ListBoxMouseMove;
  455. FAutoComplete:= True;
  456. FFileSortType:= fstFoldersFirst;
  457. FObjectTypes:= [otNonFolders, otFolders];
  458. end;
  459. destructor TKASPathEdit.Destroy;
  460. begin
  461. inherited Destroy;
  462. FStringList.Free;
  463. end;
  464. end.