{ Double Commander Components ------------------------------------------------------------------------- Path edit class with auto complete feature Copyright (C) 2012-2022 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 . } unit KASPathEdit; {$mode delphi} {$IF DEFINED(LCLCOCOA)} {$modeswitch objectivec1} {$ENDIF} interface uses Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, StdCtrls, ShellCtrls, LCLType, LCLVersion {$IF DEFINED(LCLCOCOA)} , CocoaAll, CocoaWindows {$ENDIF} ; type { TKASPathEdit } TKASPathEdit = class(TEdit) private FKeyDown: Word; FBasePath: String; FListBox: TListBox; FPanel: THintWindow; FAutoComplete: Boolean; FStringList: TStringList; FObjectTypes: TObjectTypes; FFileSortType: TFileSortType; private procedure setTextAndSelect( newText:String ); procedure handleSpecialKeys( var Key: Word ); procedure handleUpKey; procedure handleDownKey; procedure AutoComplete(const Path: String); procedure SetObjectTypes(const AValue: TObjectTypes); procedure FormChangeBoundsEvent(Sender: TObject); procedure ListBoxClick(Sender: TObject); procedure ListBoxMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); private function isShowingListBox(): Boolean; inline; procedure ShowListBox; procedure HideListBox; protected {$IF DEFINED(LCLWIN32)} procedure CreateWnd; override; {$ENDIF} {$IF DEFINED(LCLCOCOA)} procedure TextChanged; override; {$ENDIF} procedure DoExit; override; procedure VisibleChanged; override; procedure KeyDown(var Key: Word; Shift: TShiftState); override; procedure KeyUpAfterInterface(var Key: Word; Shift: TShiftState); override; public onKeyESCAPE: TNotifyEvent; onKeyRETURN: TNotifyEvent; constructor Create(AOwner: TComponent); override; destructor Destroy; override; published property ObjectTypes: TObjectTypes read FObjectTypes write SetObjectTypes; property FileSortType: TFileSortType read FFileSortType write FFileSortType; end; procedure Register; implementation uses LazUTF8, Math, LazFileUtils, Masks {$IF DEFINED(LCLWIN32)} , ComObj {$ENDIF} {$IF DEFINED(MSWINDOWS)} , Windows {$ENDIF} ; {$IF DEFINED(LCLWIN32)} const SHACF_AUTOAPPEND_FORCE_ON = $40000000; SHACF_AUTOSUGGEST_FORCE_ON = $10000000; SHACF_FILESYS_ONLY = $00000010; SHACF_FILESYS_DIRS = $00000020; function SHAutoComplete(hwndEdit: HWND; dwFlags: DWORD): HRESULT; stdcall; external 'shlwapi.dll'; function SHAutoCompleteX(hwndEdit: HWND; ObjectTypes: TObjectTypes): Boolean; var dwFlags: DWORD; begin if (ObjectTypes = []) then Exit(False); dwFlags := SHACF_AUTOAPPEND_FORCE_ON or SHACF_AUTOSUGGEST_FORCE_ON; if (otNonFolders in ObjectTypes) then dwFlags := dwFlags or SHACF_FILESYS_ONLY else if (otFolders in ObjectTypes) then dwFlags := dwFlags or SHACF_FILESYS_DIRS; Result:= (SHAutoComplete(hwndEdit, dwFlags) = 0); end; {$ENDIF} procedure Register; begin RegisterComponents('KASComponents', [TKASPathEdit]); end; function FilesSortAlphabet(List: TStringList; Index1, Index2: Integer): Integer; begin Result:= CompareFilenames(List[Index1], List[Index2]); end; function FilesSortFoldersFirst(List: TStringList; Index1, Index2: Integer): Integer; var Attr1, Attr2: IntPtr; begin Attr1:= IntPtr(List.Objects[Index1]); Attr2:= IntPtr(List.Objects[Index2]); if (Attr1 and faDirectory <> 0) and (Attr2 and faDirectory <> 0) then Result:= CompareFilenames(List[Index1], List[Index2]) else begin if (Attr1 and faDirectory <> 0) then Result:= -1 else begin Result:= 1; end; end; end; procedure GetFilesInDir(const ABaseDir: String; AMask: String; AObjectTypes: TObjectTypes; AResult: TStringList; AFileSortType: TFileSortType); var ExcludeAttr: Integer; SearchRec: TSearchRec; {$IF DEFINED(MSWINDOWS)} ErrMode : LongWord; {$ENDIF} begin {$IF DEFINED(MSWINDOWS)} ErrMode:= SetErrorMode(SEM_FAILCRITICALERRORS or SEM_NOALIGNMENTFAULTEXCEPT or SEM_NOGPFAULTERRORBOX or SEM_NOOPENFILEERRORBOX); try {$ENDIF} if FindFirst(ABaseDir + AMask, faAnyFile, SearchRec) = 0 then begin ExcludeAttr:= 0; if not (otHidden in AObjectTypes) then ExcludeAttr:= ExcludeAttr or faHidden; if not (otFolders in AObjectTypes) then ExcludeAttr:= ExcludeAttr or faDirectory; repeat if (SearchRec.Attr and ExcludeAttr <> 0) then Continue; if (SearchRec.Name = '.') or (SearchRec.Name = '..')then Continue; if (SearchRec.Attr and faDirectory = 0) and not (otNonFolders in AObjectTypes) then Continue; AResult.AddObject(SearchRec.Name, TObject(IntPtr(SearchRec.Attr))); until FindNext(SearchRec) <> 0; if AResult.Count > 0 then begin case AFileSortType of fstAlphabet: AResult.CustomSort(@FilesSortAlphabet); fstFoldersFirst: AResult.CustomSort(@FilesSortFoldersFirst); end; end; end; SysUtils.FindClose(SearchRec); {$IF DEFINED(MSWINDOWS)} finally SetErrorMode(ErrMode); end; {$ENDIF} end; { TKASPathEdit } function TKASPathEdit.isShowingListBox(): Boolean; begin Result:= FPanel<>nil; end; procedure TKASPathEdit.AutoComplete(const Path: String); {$IF LCL_FULLVERSION < 4990000} const AFlags: array[Boolean] of TMaskOptions = ( [moDisableSets], [moDisableSets, moCaseSensitive] ); {$ENDIF} var I: Integer; AMask: TMask; BasePath: String; begin FListBox.Clear; if Pos(PathDelim, Path) = 0 then HideListBox else begin BasePath:= ExtractFilePath(Path); if CompareFilenames(FBasePath, BasePath) <> 0 then begin FStringList.Clear; FBasePath:= BasePath; GetFilesInDir(BasePath, AllFilesMask, FObjectTypes, FStringList, FFileSortType); end; if (FStringList.Count > 0) then begin FListBox.Items.BeginUpdate; try // Check mask and make absolute file name AMask:= TMask.Create(ExtractFileName(Path) + '*', {$IF LCL_FULLVERSION < 4990000} AFlags[FileNameCaseSensitive] {$ELSE} FileNameCaseSensitive {$ENDIF} ); for I:= 0 to FStringList.Count - 1 do begin if AMask.Matches(FStringList[I]) then FListBox.Items.Add(BasePath + FStringList[I]); end; AMask.Free; finally FListBox.Items.EndUpdate; end; if FListBox.Items.Count = 0 then HideListBox; if FListBox.Items.Count > 0 then begin ShowListBox; // Calculate ListBox height with FListBox.ItemRect(0) do I:= Bottom - Top; // TListBox.ItemHeight sometimes don't work under GTK2 with FListBox do begin {$IF NOT DEFINED(LCLCOCOA)} if Items.Count = 1 then FPanel.ClientHeight:= Self.Height else FPanel.ClientHeight:= I * IfThen(Items.Count > 10, 11, Items.Count + 1); {$ELSE} FPanel.ClientHeight:= I * IfThen(Items.Count > 10, 11, Items.Count + 1) + trunc(i/2); {$ENDIF} end; end; end; end; end; procedure TKASPathEdit.SetObjectTypes(const AValue: TObjectTypes); begin if FObjectTypes = AValue then Exit; FObjectTypes:= AValue; {$IF DEFINED(LCLWIN32)} if HandleAllocated then RecreateWnd(Self); if FAutoComplete then {$ENDIF} FAutoComplete:= (FObjectTypes <> []); end; procedure TKASPathEdit.FormChangeBoundsEvent(Sender: TObject); begin HideListBox; end; procedure TKASPathEdit.ListBoxClick(Sender: TObject); begin if FListBox.ItemIndex >= 0 then begin setTextAndSelect( FListBox.Items[FListBox.ItemIndex] ); HideListBox; SetFocus; end; end; procedure TKASPathEdit.ListBoxMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin FListBox.ItemIndex:= FListBox.ItemAtPos(Classes.Point(X, Y), True); end; {$IF DEFINED(LCLCOCOA)} procedure cocoaNeedMouseEvent( hintWindow: THintWindow ); var cnt: TCocoaWindowContent; begin cnt:= TCocoaWindowContent( hintWindow.Handle ); cnt.window.setIgnoresMouseEvents( false ); end; {$ENDIF} procedure TKASPathEdit.ShowListBox; begin if not isShowingListBox() then begin FPanel:= THintWindow.Create(Self); {$IF DEFINED(LCLCOCOA)} cocoaNeedMouseEvent(FPanel); {$ENDIF} FPanel.Color:= clDefault; FListBox.Parent:= FPanel; with Parent.ClientToScreen(CLasses.Point(Left, Top)) do begin FPanel.Left:= X; FPanel.Top:= Y + Height; end; FPanel.Width:= Width; FPanel.Visible:= True; Application.AddOnDeactivateHandler(FormChangeBoundsEvent, True); GetParentForm(Self).AddHandlerOnChangeBounds(FormChangeBoundsEvent, True); end; end; procedure TKASPathEdit.HideListBox; begin if isShowingListBox() then begin FPanel.Visible:= False; FListBox.Parent:= nil; FreeAndNil(FPanel); Application.RemoveOnDeactivateHandler(FormChangeBoundsEvent); GetParentForm(Self).RemoveHandlerOnChangeBounds(FormChangeBoundsEvent); end; end; {$IF DEFINED(LCLWIN32)} procedure TKASPathEdit.CreateWnd; begin inherited CreateWnd; FAutoComplete:= not SHAutoCompleteX(Handle, FObjectTypes); end; {$ENDIF} {$IF DEFINED(LCLCOCOA)} procedure TKASPathEdit.TextChanged; begin Inherited; if not Modified then Exit; if FAutoComplete then AutoComplete(Text); end; {$ENDIF} procedure TKASPathEdit.setTextAndSelect( newText:String ); var start: Integer; begin if Pos(Text,newText) > 0 then start:= UTF8Length(Text) else start:= UTF8Length(ExtractFilePath(Text)); Text:= newText; SelStart:= start; SelLength:= UTF8Length(Text)-SelStart; end; procedure TKASPathEdit.DoExit; begin HideListBox; inherited DoExit; end; procedure TKASPathEdit.VisibleChanged; begin FBasePath:= EmptyStr; inherited VisibleChanged; end; procedure TKASPathEdit.handleSpecialKeys( var Key: Word ); begin if isShowingListBox() then begin HideListBox; Key:= 0; end else begin if Key=VK_ESCAPE then begin if Assigned(onKeyESCAPE) then begin onKeyESCAPE( self ); Key:= 0; end; end else begin if Assigned(onKeyRETURN) then begin onKeyRETURN( self ); Key:= 0; end; end; end; end; procedure TKASPathEdit.handleUpKey; begin if FListBox.ItemIndex = -1 then FListBox.ItemIndex:= FListBox.Items.Count - 1 else if FListBox.ItemIndex - 1 < 0 then FListBox.ItemIndex:= - 1 else FListBox.ItemIndex:= FListBox.ItemIndex - 1; if FListBox.ItemIndex >= 0 then setTextAndSelect( FListBox.Items[FListBox.ItemIndex] ) else setTextAndSelect( ExtractFilePath(Text) ); end; procedure TKASPathEdit.handleDownKey; begin if FListBox.ItemIndex + 1 >= FListBox.Items.Count then FListBox.ItemIndex:= -1 else if FListBox.ItemIndex = -1 then FListBox.ItemIndex:= IfThen(FListBox.Items.Count > 0, 0, -1) else FListBox.ItemIndex:= FListBox.ItemIndex + 1; if FListBox.ItemIndex >= 0 then setTextAndSelect( FListBox.Items[FListBox.ItemIndex] ) else setTextAndSelect( ExtractFilePath(Text) ); end; procedure TKASPathEdit.KeyDown(var Key: Word; Shift: TShiftState); begin FKeyDown:= Key; case Key of VK_ESCAPE, VK_RETURN, VK_SELECT: handleSpecialKeys( Key ); VK_UP: if isShowingListBox() then begin Key:= 0; handleUpKey(); end; VK_DOWN: if isShowingListBox() then begin Key:= 0; handleDownKey(); end; end; inherited KeyDown(Key, Shift); {$IFDEF LCLGTK2} // Workaround for GTK2 - up and down arrows moving through controls. if Key in [VK_UP, VK_DOWN] then Key:= 0; {$ENDIF} end; procedure TKASPathEdit.KeyUpAfterInterface(var Key: Word; Shift: TShiftState); begin {$IF not DEFINED(LCLCOCOA)} if (FKeyDown = Key) and FAutoComplete and not (Key in [VK_ESCAPE, VK_RETURN, VK_SELECT, VK_UP, VK_DOWN]) then begin if Modified then begin Modified:= False; AutoComplete(Text); end; end; {$ENDIF} inherited KeyUpAfterInterface(Key, Shift); {$IF DEFINED(LCLWIN32)} // Windows auto-completer eats the TAB so LCL doesn't get it and doesn't move to next control. if not FAutoComplete and (Key = VK_TAB) then GetParentForm(Self).SelectNext(Self, True, True); {$ENDIF} end; constructor TKASPathEdit.Create(AOwner: TComponent); begin inherited Create(AOwner); FStringList:= TStringList.Create; FListBox:= TListBox.Create(Self); FListBox.TabStop:= False; FListBox.Align:= alClient; FListBox.ParentFont:= False; FListBox.ClickOnSelChange:= False; FListBox.OnClick:= ListBoxClick; FListBox.OnMouseMove:= ListBoxMouseMove; FAutoComplete:= True; FFileSortType:= fstFoldersFirst; FObjectTypes:= [otNonFolders, otFolders]; end; destructor TKASPathEdit.Destroy; begin inherited Destroy; FStringList.Free; end; end.