{
    Double Commander
    -------------------------------------------------------------------------
    Execute internal or external viewer, editor or differ

    Copyright (C) 2006-2023 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 uShowForm;

{$mode objfpc}{$H+}

interface

uses
  Classes, Forms, DCBasicTypes, uFileSource, uFileSourceOperation, uFile,
  uFileSourceCopyOperation;

type

  { TWaitData }

  TWaitData = class
  private
    procedure ShowOnTopAsync(Data: PtrInt);
  public
    procedure ShowOnTop(AForm: TCustomForm);
    procedure ShowWaitForm; virtual; abstract;
    procedure Done; virtual; abstract;
  end;

  { TViewerWaitData }

  TViewerWaitData = class(TWaitData)
  private
    FFileSource: IFileSource;
  public
    constructor Create(aFileSource: IFileSource);
    destructor Destroy; override;
    procedure ShowWaitForm; override;
    procedure Done; override;
  end;

  { TEditorWaitData }

  TEditorWaitData = class(TWaitData)
  public
    Files: TFiles;
    function GetFileList: TStringList;
  protected
    FileTimes: array of TFileTime;
    TargetPath: String;
    SourceFileSource: IFileSource;
    TargetFileSource: IFileSource;
    FModal: Boolean;
    function GetRelativeFileName(const FullPath: string): string;
    function GetRelativeFileNames: string;
    function GetFromPath: string;
  public
    constructor Create(aCopyOutOperation: TFileSourceCopyOperation; Modal: Boolean = False);
    destructor Destroy; override;
    procedure ShowWaitForm; override;
    procedure Done; override;
  protected
    procedure OnCopyInStateChanged(Operation: TFileSourceOperation;
                                   State: TFileSourceOperationState);
  end;

  { TViewerModeData }

  TViewerModeData = class
  private
    FMode: Integer;
  public
    constructor Create(AMode: Integer);
    procedure OnCopyOutStateChanged(Operation: TFileSourceOperation;
                                    State: TFileSourceOperationState);
  end;

  TToolDataPreparedProc = procedure(const FileList: TStringList; WaitData: TWaitData; Modal: Boolean = False);

  // Callback may be called either asynchoronously or synchronously (for modal operations)
  // pdrInCallback is returned when FunctionToCall either will be called or was already called
  TPrepareDataResult = (pdrFailed, pdrSynchronous, pdrInCallback);

function PrepareData(FileSource: IFileSource; var SelectedFiles: TFiles;
                     FunctionToCall: TFileSourceOperationStateChangedNotify;
                     Modal: Boolean = False): TPrepareDataResult;

procedure PrepareToolData(FileSource: IFileSource; var SelectedFiles: TFiles;
                          FunctionToCall: TToolDataPreparedProc); overload;

procedure PrepareToolData(FileSource1: IFileSource; var SelectedFiles1: TFiles;
                          FileSource2: IFileSource; var SelectedFiles2: TFiles;
                          FunctionToCall: TToolDataPreparedProc); overload;

procedure PrepareToolData(FileSource1: IFileSource; File1: TFile;
                          FileSource2: IFileSource; File2: TFile;
                          FunctionToCall: TToolDataPreparedProc;
                          Modal: Boolean = False); overload;

procedure RunExtDiffer(CompareList: TStringList);

procedure ShowEditorByGlob(const sFileName: String);
procedure ShowEditorByGlob(WaitData: TEditorWaitData); overload;

procedure ShowDifferByGlob(const LeftName, RightName: String);
procedure ShowDifferByGlobList(const CompareList: TStringList; WaitData: TWaitData; Modal: Boolean = False);

procedure ShowViewerByGlob(const sFileName: String);
procedure ShowViewerByGlobList(const FilesToView: TStringList;
                               const aFileSource: IFileSource);

implementation

uses
  SysUtils, Process, DCProcessUtf8, Dialogs, LCLIntf, DCDateTimeUtils,
  uShellExecute, uGlobs, uOSUtils, fEditor, fViewer, uDCUtils,
  uTempFileSystemFileSource, uLng, fDiffer, uDebug, DCOSUtils, uShowMsg,
  DCStrUtils, uFileSourceProperty, uWfxPluginCopyOutOperation,
  uFileSourceOperationOptions, uOperationsManager, uFileSourceOperationTypes,
  uMultiArchiveFileSource, fFileExecuteYourSelf, uFileProcs, uFileSystemFileSource;

type

  { TWaitDataDouble }

  TWaitDataDouble = class(TWaitData)
  private
    FWaitData1, FWaitData2: TEditorWaitData;
  public
    constructor Create(WaitData1: TEditorWaitData; WaitData2: TEditorWaitData);
    procedure ShowWaitForm; override;
    procedure Done; override;
    destructor Destroy; override;
  end;

  { TViewerWaitThread }

  TViewerWaitThread = class(TThread)
  private
    FFileList : TStringList;
    FFileSource: IFileSource;
  protected
    procedure Execute; override;
  public
    constructor Create(const FilesToView: TStringList; const aFileSource: IFileSource);
    destructor Destroy; override;
  end;

  { TExtToolWaitThread }

  TExtToolWaitThread = class(TThread)
  private
    FExternalTool: TExternalTool;
    FFileList: TStringList;
    FWaitData: TWaitData;
  private
    procedure RunEditDone;
    procedure ShowWaitForm;
  protected
    procedure Execute; override;
  public
    constructor Create(ExternalTool: TExternalTool;
                       const FileList: TStringList;
                       WaitData: TWaitData);
    destructor Destroy; override;
  end;

procedure RunExtTool(const ExtTool: TExternalToolOptions; sFileName: String);
var
  sCmd: String;
  sParams: String = '';
begin
  sCmd := ExtTool.Path;
  sParams := ExtTool.Parameters;
  // If there is %p already configured in the parameter, we assume user configured it the way he wants.
  // This might be in non-common case where there are parameters AFTER the filename to open.
  // If there is not %p, let's do thing like legacy was and let's add the filename received as parameter.
  if (Pos('%p', sParams) = 0) and (Pos('%f', sParams) = 0) then
  begin
    sParams := ConcatenateStrWithSpace(sParams, '%' + ASCII_DLE);
    sParams := ConcatenateStrWithSpace(sParams, QuoteStr(sFileName));
  end;

  ProcessExtCommandFork(sCmd, sParams, '', nil, ExtTool.RunInTerminal, ExtTool.KeepTerminalOpen);
end;

procedure RunExtDiffer(CompareList: TStringList);
var
  i : Integer;
  sCmd: String;
  sParams:string='';
begin
  with gExternalTools[etDiffer] do
  begin
    sCmd := QuoteStr(ReplaceEnvVars(Path));
    if Parameters <> EmptyStr then begin
      sParams := sParams + ' ' + Parameters;
    end;
    sParams := ConcatenateStrWithSpace(sParams, '%' + ASCII_DLE);
    for i := 0 to CompareList.Count - 1 do
      sParams := sParams + ' ' + QuoteStr(CompareList.Strings[i]);
    try
      ProcessExtCommandFork(sCmd, sParams, '', nil, RunInTerminal, KeepTerminalOpen);
    except
      on e: EInvalidCommandLine do
        MessageDlg(rsToolErrorOpeningDiffer,
          rsMsgInvalidCommandLine + ' (' + rsToolDiffer + '):' + LineEnding + e.Message,
          mtError, [mbOK], 0);
    end;
  end;
end;

procedure ShowEditorByGlob(const sFileName: String);
begin
  if gExternalTools[etEditor].Enabled then
  begin
    try
      RunExtTool(gExternalTools[etEditor], sFileName);
    except
      on e: EInvalidCommandLine do
        MessageDlg(rsToolErrorOpeningEditor,
          rsMsgInvalidCommandLine + ' (' + rsToolEditor + '):' + LineEnding + e.Message,
          mtError, [mbOK], 0);
    end;
  end
  else
    ShowEditor(sFileName);
end;

procedure ShowEditorByGlob(WaitData: TEditorWaitData);
var
  FileList: TStringList;
begin
  if gExternalTools[etEditor].Enabled then
  begin
    FileList := TStringList.Create;
    try
      FileList.Add(WaitData.Files[0].FullPath);
      with TExtToolWaitThread.Create(etEditor, FileList, WaitData) do Start;
    finally
      FileList.Free
    end;
  end
  else begin
    ShowEditor(WaitData.Files[0].FullPath, WaitData);
  end;
end;

procedure ShowViewerByGlob(const sFileName: String);
var
  sl:TStringList;
begin
  if gExternalTools[etViewer].Enabled then
  begin
    try
      RunExtTool(gExternalTools[etViewer], sFileName);
    except
      on e: EInvalidCommandLine do
        MessageDlg(rsToolErrorOpeningViewer,
          rsMsgInvalidCommandLine + ' (' + rsToolViewer + '):' + LineEnding + e.Message,
          mtError, [mbOK], 0);
    end;
  end
  else
  begin
    sl:=TStringList.Create;
    try
      sl.Add(sFileName);
      ShowViewer(sl);
    finally
      FreeAndNil(sl);
    end;
  end;
end;

procedure ShowDifferByGlob(const LeftName, RightName: String);
var
  sl: TStringList;
begin
  if gExternalTools[etDiffer].Enabled then
    begin
      sl:= TStringList.Create;
      try
        sl.add(LeftName);
        sl.add(RightName);
        RunExtDiffer(sl);
      finally
        sl.free;
      end;
    end
  else
    ShowDiffer(LeftName, RightName);
end;

procedure ShowDifferByGlobList(const CompareList: TStringList; WaitData: TWaitData; Modal: Boolean = False);
begin
  if gExternalTools[etDiffer].Enabled then
  begin
    if Assigned(WaitData) then
      with TExtToolWaitThread.Create(etDiffer, CompareList, WaitData) do Start
    else
      RunExtDiffer(CompareList);
  end
  else
    ShowDiffer(CompareList[0], CompareList[1], WaitData, Modal);
end;

procedure ShowViewerByGlobList(const FilesToView : TStringList;
                               const aFileSource: IFileSource);
var
  I : Integer;
  WaitThread : TViewerWaitThread;
begin
  if gExternalTools[etViewer].Enabled then
  begin
    if aFileSource.IsClass(TTempFileSystemFileSource) then
      begin
        WaitThread := TViewerWaitThread.Create(FilesToView, aFileSource);
        WaitThread.Start;
      end
    else
    begin
      // TODO: If possible should run one instance of external viewer
      // with multiple file names as parameters.
      for i:=0 to FilesToView.Count-1 do
        RunExtTool(gExternalTools[etViewer], FilesToView.Strings[i]);
    end;
  end // gUseExtView
  else begin
    if aFileSource.IsClass(TTempFileSystemFileSource) then
      ShowViewer(FilesToView, TViewerWaitData.Create(aFileSource))
    else
      ShowViewer(FilesToView);
  end;
end;

{ TWaitData }

procedure TWaitData.ShowOnTopAsync(Data: PtrInt);
var
  Form: TCustomForm absolute Data;
begin
  Form.ShowOnTop;
end;

procedure TWaitData.ShowOnTop(AForm: TCustomForm);
var
  Data: PtrInt absolute AForm;
begin
  Application.QueueAsyncCall(@ShowOnTopAsync, Data);
end;

{ TViewerWaitData }

constructor TViewerWaitData.Create(aFileSource: IFileSource);
begin
  FFileSource:= aFileSource;
end;

destructor TViewerWaitData.Destroy;
begin
  inherited Destroy;
  FFileSource:= nil;
end;

procedure TViewerWaitData.ShowWaitForm;
begin

end;

procedure TViewerWaitData.Done;
begin

end;

{ TWaitDataDouble }

constructor TWaitDataDouble.Create(WaitData1: TEditorWaitData; WaitData2: TEditorWaitData);
begin
  FWaitData1 := WaitData1;
  FWaitData2 := WaitData2;
end;

procedure TWaitDataDouble.ShowWaitForm;
begin
  try
    if Assigned(FWaitData1) then
      FWaitData1.ShowWaitForm;
  finally
    if Assigned(FWaitData2) then
      FWaitData2.ShowWaitForm;
  end;
end;

procedure TWaitDataDouble.Done;
begin
  try
    if Assigned(FWaitData1) then
      FWaitData1.Done;
  finally
    FWaitData1 := nil;
    try
      if Assigned(FWaitData2) then
        FWaitData2.Done;
    finally
      FWaitData2 := nil;
      Free;
    end;
  end;
end;

destructor TWaitDataDouble.Destroy;
begin
  inherited Destroy;
  if Assigned(FWaitData1) then
    FWaitData1.Free;
  if Assigned(FWaitData2) then
    FWaitData2.Free;
end;

{ TEditorWaitData }

constructor TEditorWaitData.Create(aCopyOutOperation: TFileSourceCopyOperation; Modal: Boolean = False);
var
  I: Integer;
  aFileSource: ITempFileSystemFileSource;
begin
  aFileSource := aCopyOutOperation.TargetFileSource as ITempFileSystemFileSource;
  TargetPath := aCopyOutOperation.SourceFiles.Path;
  Files := aCopyOutOperation.SourceFiles.Clone;
  ChangeFileListRoot(aFileSource.FileSystemRoot, Files);
  SetLength(FileTimes, Files.Count);
  for I := 0 to Files.Count - 1 do
    FileTimes[I] := mbFileAge(Files[I].FullPath);
  // Special case for bzip2 like archivers which don't store file size
  if Files.Count = 1 then
    Files[0].Size  := mbFileSize(Files[0].FullPath);
  SourceFileSource := aFileSource;
  TargetFileSource := aCopyOutOperation.FileSource as IFileSource;
  FModal := Modal;
end;

destructor TEditorWaitData.Destroy;
begin
  inherited Destroy;
  Files.Free;
  SourceFileSource:= nil;
  TargetFileSource:= nil;
end;

function TEditorWaitData.GetRelativeFileName(const FullPath: string): string;
begin
  Result := ExtractDirLevel(IncludeTrailingPathDelimiter(Files.Path), FullPath);
end;

function TEditorWaitData.GetRelativeFileNames: string;
var
  I: Integer;
begin
  Result := GetRelativeFileName(Files[0].FullPath);
  for I := 1 to Files.Count - 1 do
    Result := Result + ', ' + GetRelativeFileName(Files[I].FullPath);
end;

function TEditorWaitData.GetFromPath: string;
begin
  if StrBegins(TargetPath, TargetFileSource.CurrentAddress) then
    Result := TargetPath // Workaround for TGioFileSource
  else
    Result := TargetFileSource.CurrentAddress + TargetPath;
end;

procedure TEditorWaitData.ShowWaitForm;
begin
  ShowFileEditExternal(GetRelativeFileNames, GetFromPath, Self, FModal);
end;

procedure TEditorWaitData.Done;
var
  I: Integer;
  Msg: String;
  FileTime: TFileTime;
  DoNotFreeYet: Boolean = False;
  Operation: TFileSourceCopyOperation;
begin
  try
    for I := Files.Count - 1 downto 0 do
    begin
      FileTime:= mbFileAge(Files[I].FullPath);
      if (FileTime = FileTimes[I]) or
         (not msgYesNo(Format(rsMsgCopyBackward, [GetRelativeFileName(Files[I].FullPath)]) + LineEnding + LineEnding + GetFromPath)) then
      begin
        Files.Delete(I);
      end
      else begin
        Files[I].ModificationTime:= FileTimeToDateTime(FileTime);
      end;
    end;

    // Files were modified
    if Files.Count > 0 then
    begin
      if (fsoCopyIn in TargetFileSource.GetOperationsTypes) and
         (not (TargetFileSource is TMultiArchiveFileSource)) then
      begin
        Operation:= TargetFileSource.CreateCopyInOperation(SourceFileSource, Files, TargetPath) as TFileSourceCopyOperation;
        // Copy files back
        if Assigned(Operation) then
        begin
          Operation.AddStateChangedListener([fsosStopped], @OnCopyInStateChanged);
          Operation.FileExistsOption:= fsoofeOverwrite;
          if FModal then
            OperationsManager.AddOperationModal(Operation)
          else
            OperationsManager.AddOperation(Operation);
          DoNotFreeYet:= True; // Will be free in operation
        end;
      end
      else
      begin
        Msg := rsMsgCouldNotCopyBackward + LineEnding;
        for I := 0 to Files.Count-1 do
          Msg := Msg + LineEnding + Files[I].FullPath;
        if msgYesNo(Msg) then
          (SourceFileSource as ITempFileSystemFileSource).DeleteOnDestroy:= False;
      end;
    end;
  finally
    if not DoNotFreeYet then
      Free;
  end;
end;

procedure TEditorWaitData.OnCopyInStateChanged(Operation: TFileSourceOperation;
                                               State: TFileSourceOperationState);
var
  I: Integer;
  Msg: string;
  aFileSource: ITempFileSystemFileSource;
  aCopyOperation: TFileSourceCopyOperation;
begin
  if (State = fsosStopped) then
  begin
    aCopyOperation := Operation as TFileSourceCopyOperation;
    aFileSource := aCopyOperation.SourceFileSource as ITempFileSystemFileSource;
    with aCopyOperation.RetrieveStatistics do
    begin
      if DoneFiles <> TotalFiles then
      begin
        Msg := rsMsgCouldNotCopyBackward + LineEnding;
        for I := 0 to aCopyOperation.SourceFiles.Count-1 do
          Msg := Msg + LineEnding + aCopyOperation.SourceFiles[I].FullPath;
        if msgYesNo(Operation.Thread, Msg) then
        begin
          aFileSource.DeleteOnDestroy:= False;
        end;
      end;
    end;
    Free;
  end;
end;

function TEditorWaitData.GetFileList: TStringList;
var
  I: Integer;
begin
  Result := TStringList.Create;
  for I := 0 to Files.Count - 1 do
    Result.Add(Files[I].FullPath);
end;

{ TViewerModeData }

constructor TViewerModeData.Create(AMode: Integer);
begin
  FMode:= AMode;
end;

procedure TViewerModeData.OnCopyOutStateChanged(
  Operation: TFileSourceOperation; State: TFileSourceOperationState);
var
  aFileList: TStringList;
  aFileSource: ITempFileSystemFileSource;
  aCopyOutOperation: TFileSourceCopyOperation;
begin
  try
    if (State = fsosStopped) and (Operation.Result = fsorFinished) then
    begin
      aFileList := TStringList.Create;
      try
        aCopyOutOperation := Operation as TFileSourceCopyOperation;
        aFileSource := aCopyOutOperation.TargetFileSource as ITempFileSystemFileSource;
        ChangeFileListRoot(aFileSource.FileSystemRoot, aCopyOutOperation.SourceFiles);

        aFileList.Add(aCopyOutOperation.SourceFiles[0].FullPath);

        ShowViewer(aFileList, FMode, TViewerWaitData.Create(aFileSource));
      finally
        aFileList.Free;
      end;
    end;
  finally
    Free;
  end;
end;

{ TExtToolWaitThread }

procedure TExtToolWaitThread.RunEditDone;
begin
  FWaitData.Done;
end;

procedure TExtToolWaitThread.ShowWaitForm;
begin
  FWaitData.ShowWaitForm;
end;

procedure TExtToolWaitThread.Execute;
var
  I: Integer;
  StartTime: QWord;
  Process : TProcessUTF8;
  sCmd, sSecureEmptyStr: String;
begin
  try
    Process := TProcessUTF8.Create(nil);
    try
      with gExternalTools[FExternalTool] do
      begin
        sCmd := ReplaceEnvVars(Path);
        // TProcess arguments must be enclosed with double quotes and not escaped.
        if RunInTerminal then
        begin
          sCmd := QuoteStr(sCmd);
          if Parameters <> EmptyStr then
            sCmd := sCmd + ' ' + Parameters;
          for I := 0 to FFileList.Count - 1 do
            sCmd := sCmd + ' ' + QuoteStr(FFileList[I]);
          sSecureEmptyStr := EmptyStr; // Let's play safe and don't let EmptyStr being passed as "VAR" parameter of "FormatTerminal"
          FormatTerminal(sCmd, sSecureEmptyStr, False);
        end
        else
        begin
          sCmd := '"' + sCmd + '"';
          if Parameters <> EmptyStr then
            sCmd := sCmd + ' ' + Parameters;
          for I := 0 to FFileList.Count - 1 do
            sCmd := sCmd + ' "' + FFileList[I] + '"';
        end;
      end;

      Process.CommandLine := sCmd;
      Process.Options := [poWaitOnExit];
      StartTime:= GetTickCount64;
      Process.Execute;

      // If an editor closes within gEditWaitTime amount of milliseconds,
      // assume that it's a multiple document editor and show dialog where
      // user can confirm when editing has ended.
      if GetTickCount64 - StartTime < gEditWaitTime then
      begin
        Synchronize(@ShowWaitForm);
      end
      else begin
        Synchronize(@RunEditDone);
      end;

    finally
      Process.Free;
    end;
  except
    FWaitData.Free;
  end;
end;

constructor TExtToolWaitThread.Create(ExternalTool: TExternalTool;
                                      const FileList: TStringList;
                                      WaitData: TWaitData);
begin
  inherited Create(True);

  FreeOnTerminate := True;

  FExternalTool := ExternalTool;

  FFileList := TStringList.Create;
  // Make a copy of list elements.
  FFileList.Assign(FileList);

  FWaitData := WaitData;
end;

destructor TExtToolWaitThread.Destroy;
begin
  FFileList.Free;
  inherited Destroy;
end;

{ TViewerWaitThread }

constructor TViewerWaitThread.Create(const FilesToView: TStringList; const aFileSource: IFileSource);
begin
  inherited Create(True);

  FreeOnTerminate := True;

  FFileList := TStringList.Create;
  // Make a copy of list elements.
  FFileList.Assign(FilesToView);
  FFileSource := aFileSource;
end;

destructor TViewerWaitThread.Destroy;
begin
  if Assigned(FFileList) then
    FreeAndNil(FFileList);

  // Delete the temporary file source and all files inside.
  FFileSource := nil;

  inherited Destroy;
end;

procedure TViewerWaitThread.Execute;
var
  Process : TProcessUTF8;
  sCmd, sSecureEmptyStr: String;
begin
  Process := TProcessUTF8.Create(nil);

  with gExternalTools[etViewer] do
  begin
    sCmd := ReplaceEnvVars(Path);
    // TProcess arguments must be enclosed with double quotes and not escaped.
    if RunInTerminal then
    begin
      sCmd := QuoteStr(sCmd);
      if Parameters <> EmptyStr then
        sCmd := sCmd + ' ' + Parameters;
      sCmd := sCmd + ' ' + QuoteStr(FFileList.Strings[0]);
      sSecureEmptyStr := EmptyStr; //Let's play safe and don't let EmptyStr being passed as "VAR" parameter of "FormatTerminal"
      FormatTerminal(sCmd, sSecureEmptyStr, False);
    end
    else
    begin
      sCmd := '"' + sCmd + '"';
      if Parameters <> EmptyStr then
        sCmd := sCmd + ' ' + Parameters;
      sCmd := sCmd + ' "' + FFileList.Strings[0] + '"';
    end;
  end;

  Process.CommandLine := sCmd;
  Process.Options := [poWaitOnExit];
  Process.Execute;
  Process.Free;
end;

{ PrepareData }

function PrepareData(FileSource: IFileSource; var SelectedFiles: TFiles;
                     FunctionToCall: TFileSourceOperationStateChangedNotify;
                     Modal: Boolean = False): TPrepareDataResult;
var
  I: Integer;
  aFile: TFile;
  Directory: String;
  TempFiles: TFiles = nil;
  TempFileSource: ITempFileSystemFileSource = nil;
  Operation: TFileSourceOperation;
begin
  // If files are links to local files
  if (fspLinksToLocalFiles in FileSource.Properties) then
    begin
      for I := 0 to SelectedFiles.Count - 1 do
        begin
          aFile := SelectedFiles[I];
          FileSource.GetLocalName(aFile);
        end;
    end
  // If files not directly accessible copy them to temp file source.
  else if not (fspDirectAccess in FileSource.Properties) then
  begin
    if not (fsoCopyOut in FileSource.GetOperationsTypes) then
    begin
      msgWarning(rsMsgErrNotSupported);
      Exit(pdrFailed);
    end;

    Directory := GetTempName(GetTempFolderDeletableAtTheEnd, EmptyStr);
    if not mbForceDirectory(Directory) then
    begin
      MessageDlg(mbSysErrorMessage(GetLastOSError), mtError, [mbOK], 0);
      Exit(pdrFailed);
    end;

    TempFileSource := TTempFileSystemFileSource.Create(Directory);

    TempFiles := SelectedFiles.Clone;
    try
      Operation := FileSource.CreateCopyOutOperation(
                       TempFileSource,
                       TempFiles,
                       TempFileSource.FileSystemRoot);
      if Operation is TWfxPluginCopyOutOperation then
        (Operation as TWfxPluginCopyOutOperation).NeedsConnection := False; // use separate connection
    finally
      TempFiles.Free;
    end;

    if not Assigned(Operation) then
    begin
      msgWarning(rsMsgErrNotSupported);
      Exit(pdrFailed);
    end;

    Operation.AddStateChangedListener([fsosStopped], FunctionToCall);

    if Modal then
      OperationsManager.AddOperationModal(Operation)
    else
      OperationsManager.AddOperation(Operation);

    Exit(pdrInCallback);
  end;
  Exit(pdrSynchronous);
end;

{ TToolDataPreparator }

type
  TToolDataPreparator = class
  protected
    FFunc: TToolDataPreparedProc;
    FCallOnFail: Boolean;
    procedure OnCopyOutStateChanged(Operation: TFileSourceOperation;
                                    State: TFileSourceOperationState);
  public
    constructor Create(FunctionToCall: TToolDataPreparedProc; CallOnFail: Boolean = False);
    procedure Prepare(FileSource: IFileSource; var SelectedFiles: TFiles);
  end;

constructor TToolDataPreparator.Create(FunctionToCall: TToolDataPreparedProc; CallOnFail: Boolean = False);
begin
  FFunc := FunctionToCall;
  FCallOnFail := CallOnFail;
end;

procedure TToolDataPreparator.Prepare(FileSource: IFileSource; var SelectedFiles: TFiles);
var
  I: Integer;
  FileList: TStringList;
begin
  case PrepareData(FileSource, SelectedFiles, @OnCopyOutStateChanged) of
  pdrSynchronous:
    try
      FileList := TStringList.Create;
      for I := 0 to SelectedFiles.Count - 1 do
        FileList.Add(SelectedFiles[i].FullPath);
      FFunc(FileList, nil);
    finally
      Free;
    end;
  pdrFailed:
    try
      if FCallOnFail then
        FFunc(nil, nil);
    finally
      Free;
    end;
  end;
end;

procedure TToolDataPreparator.OnCopyOutStateChanged(
  Operation: TFileSourceOperation; State: TFileSourceOperationState);
var WaitData: TEditorWaitData;
begin
  if (State <> fsosStopped) then
    Exit;
  try
    if Operation.Result = fsorFinished then
    begin
      WaitData := TEditorWaitData.Create(Operation as TFileSourceCopyOperation);
      FFunc(WaitData.GetFileList, WaitData);
    end
    else
    begin
      if FCallOnFail then
        FFunc(nil, nil);
    end;
  finally
    Free;
  end;
end;

{ TToolDataPreparator2 }

type
  TToolDataPreparator2 = class
  protected
    FFunc: TToolDataPreparedProc;
    FCallOnFail: Boolean;
    FModal: Boolean;
    FFailed: Boolean;
    FFileList1: TStringList;
    FFileList2: TStringList;
    FPrepared1: Boolean;
    FPrepared2: Boolean;
    FWaitData1: TEditorWaitData;
    FWaitData2: TEditorWaitData;
    procedure OnCopyOutStateChanged1(Operation: TFileSourceOperation;
                                     State: TFileSourceOperationState);
    procedure OnCopyOutStateChanged2(Operation: TFileSourceOperation;
                                     State: TFileSourceOperationState);
    procedure TryFinish;
  public
    constructor Create(FunctionToCall: TToolDataPreparedProc; CallOnFail: Boolean = False);
    procedure Prepare(FileSource1: IFileSource; var SelectedFiles1: TFiles;
                      FileSource2: IFileSource; var SelectedFiles2: TFiles;
                      Modal: Boolean = False);
    destructor Destroy; override;
  end;

constructor TToolDataPreparator2.Create(FunctionToCall: TToolDataPreparedProc; CallOnFail: Boolean = False);
begin
  FFunc := FunctionToCall;
  FCallOnFail := CallOnFail;
end;

procedure TToolDataPreparator2.Prepare(FileSource1: IFileSource; var SelectedFiles1: TFiles;
                                       FileSource2: IFileSource; var SelectedFiles2: TFiles;
                                       Modal: Boolean = False);
var
  I: Integer;
begin
  FModal := Modal;

  case PrepareData(FileSource1, SelectedFiles1, @OnCopyOutStateChanged1, Modal) of
  pdrSynchronous:
    begin
      FFileList1 := TStringList.Create;
      for I := 0 to SelectedFiles1.Count - 1 do
        FFileList1.Add(SelectedFiles1[I].FullPath);
      FPrepared1 := True;
    end;
  pdrFailed:
    begin
      try
        if FCallOnFail then
          FFunc(nil, nil, FModal);
      finally
        Free;
      end;
      Exit;
    end;
  end;

  case PrepareData(FileSource2, SelectedFiles2, @OnCopyOutStateChanged2, Modal) of
  pdrSynchronous:
    begin
      FFileList2 := TStringList.Create;
      for I := 0 to SelectedFiles2.Count - 1 do
        FFileList2.Add(SelectedFiles2[I].FullPath);
      FPrepared2 := True;
      TryFinish;
    end;
  pdrFailed:
    begin
      FPrepared2 := True;
      FFailed := True;
      TryFinish;
    end;
  end;
end;

procedure TToolDataPreparator2.OnCopyOutStateChanged1(
  Operation: TFileSourceOperation; State: TFileSourceOperationState);
begin
  if (State <> fsosStopped) then
    Exit;
  FPrepared1 := True;
  if not FFailed then
  begin
    if Operation.Result = fsorFinished then
    begin
      FWaitData1 := TEditorWaitData.Create(Operation as TFileSourceCopyOperation, FModal);
      FFileList1 := FWaitData1.GetFileList;
    end
    else
    begin
      FFailed := True;
//      if not FPrepared2 and Assigned(FOperation2) then
//        FOperation2.Stop();
    end;
  end;
  TryFinish;
end;

procedure TToolDataPreparator2.OnCopyOutStateChanged2(
  Operation: TFileSourceOperation; State: TFileSourceOperationState);
begin
  if (State <> fsosStopped) then
    Exit;
  FPrepared2 := True;
  if not FFailed then
  begin
    if Operation.Result = fsorFinished then
    begin
      FWaitData2 := TEditorWaitData.Create(Operation as TFileSourceCopyOperation, FModal);
      FFileList2 := FWaitData2.GetFileList;
    end
    else
    begin
      FFailed := True;
//      if not FPrepared1 and Assigned(FOperation1) then
//        FOperation1.Stop();
    end;
  end;
  TryFinish;
end;

procedure TToolDataPreparator2.TryFinish;
var
  s: string;
  WaitData: TWaitDataDouble;
begin
  if FPrepared1 and FPrepared2 then
  try
    if FFailed then
    begin
      if FCallOnFail then
        FFunc(nil, nil, FModal);
      Exit;
    end;
    if Assigned(FFileList2) then
      for s in FFileList2 do
        FFileList1.Append(s);
    if Assigned(FWaitData1) or Assigned(FWaitData2) then
    begin
      WaitData := TWaitDataDouble.Create(FWaitData1, FWaitData2);
      FWaitData1 := nil;
      FWaitData2 := nil;
      FFunc(FFileList1, WaitData, FModal);
    end
    else
      FFunc(FFileList1, nil, FModal);
  finally
    Free;
  end;
end;

destructor TToolDataPreparator2.Destroy;
begin
  inherited Destroy;
  if Assigned(FFileList1) then
     FFileList1.Free;
  if Assigned(FFileList2) then
     FFileList2.Free;
  if Assigned(FWaitData1) then
     FWaitData1.Free;
  if Assigned(FWaitData2) then
     FWaitData2.Free;
end;

procedure PrepareToolData(FileSource: IFileSource; var SelectedFiles: TFiles;
                          FunctionToCall: TToolDataPreparedProc);
begin
  with TToolDataPreparator.Create(FunctionToCall) do
    Prepare(FileSource, SelectedFiles);
end;

procedure PrepareToolData(FileSource1: IFileSource; var SelectedFiles1: TFiles;
                          FileSource2: IFileSource; var SelectedFiles2: TFiles;
                          FunctionToCall: TToolDataPreparedProc);
begin
  with TToolDataPreparator2.Create(FunctionToCall) do
    Prepare(FileSource1, SelectedFiles1, FileSource2, SelectedFiles2);
end;

procedure PrepareToolData(FileSource1: IFileSource; File1: TFile;
                          FileSource2: IFileSource; File2: TFile;
                          FunctionToCall: TToolDataPreparedProc;
                          Modal: Boolean = False);
var Files1, Files2: TFiles;
begin
  Files1 := TFiles.Create(File1.Path);
  try
    Files1.Add(File1.Clone);
    Files2 := TFiles.Create(File2.Path);
    try
      Files2.Add(File2.Clone);
      with TToolDataPreparator2.Create(FunctionToCall) do
        Prepare(FileSource1, Files1, FileSource2, Files2, Modal);
    finally
      Files2.Free;
    end;
  finally
    Files1.Free;
  end;
end;

end.
