Răsfoiți Sursa

Bug 1524: Option to open downloaded file on operation completion confirmation when transferring file from command-line/URL + Logging destination file + Bug fix: When opening local folder in explorer fails, error message ID was shown instead of actual message. + Refactored ShowExtendedExceptionEx

https://winscp.net/tracker/1524

Source commit: ba2f26e59a60311bf483a9456324cbdd5494e3dc
Martin Prikryl 8 ani în urmă
părinte
comite
eafabd97e5

+ 1 - 0
source/core/CoreMain.cpp

@@ -25,6 +25,7 @@ TQueryButtonAlias::TQueryButtonAlias()
   OnClick = NULL;
   GroupWith = -1;
   ElevationRequired = false;
+  MenuButton = false;
 }
 //---------------------------------------------------------------------------
 TQueryParams::TQueryParams(unsigned int AParams, UnicodeString AHelpKeyword)

+ 15 - 1
source/core/Exceptions.cpp

@@ -391,6 +391,11 @@ ExtException * __fastcall ExtException::Clone()
   return CloneFrom(this);
 }
 //---------------------------------------------------------------------------
+void __fastcall ExtException::Rethrow()
+{
+  throw ExtException(this, L"");
+}
+//---------------------------------------------------------------------------
 UnicodeString __fastcall SysErrorMessageForError(int LastError)
 {
   UnicodeString Result;
@@ -439,7 +444,12 @@ ExtException * __fastcall EFatal::Clone()
 //---------------------------------------------------------------------------
 ExtException * __fastcall ESshTerminate::Clone()
 {
-  return new ESshTerminate(this, L"", Operation);
+  return new ESshTerminate(this, L"", Operation, TargetLocalPath, DestLocalFileName);
+}
+//---------------------------------------------------------------------------
+void __fastcall ESshTerminate::Rethrow()
+{
+  throw ESshTerminate(this, L"", Operation, TargetLocalPath, DestLocalFileName);
 }
 //---------------------------------------------------------------------------
 __fastcall ECallbackGuardAbort::ECallbackGuardAbort() : EAbort(L"callback abort")
@@ -502,6 +512,10 @@ void __fastcall RethrowException(Exception * E)
   {
     // noop, should never get here
   }
+  else if (dynamic_cast<ExtException *>(E) != NULL)
+  {
+    dynamic_cast<ExtException *>(E)->Rethrow();
+  }
   else
   {
     throw ExtException(E, L"");

+ 10 - 2
source/core/Exceptions.h

@@ -59,6 +59,7 @@ public:
   static ExtException * __fastcall CloneFrom(Exception * E);
 
   virtual ExtException * __fastcall Clone();
+  virtual void __fastcall Rethrow();
 
 protected:
   void __fastcall AddMoreMessages(Exception* E);
@@ -170,15 +171,22 @@ DERIVE_FATAL_EXCEPTION(ESshFatal, EFatal);
 class ESshTerminate : public EFatal
 {
 public:
-  inline __fastcall ESshTerminate(Exception* E, UnicodeString Msg, TOnceDoneOperation AOperation) :
+  inline __fastcall ESshTerminate(
+      Exception* E, UnicodeString Msg, TOnceDoneOperation AOperation,
+      const UnicodeString & ATargetLocalPath, const UnicodeString & ADestLocalFileName) :
     EFatal(E, Msg),
-    Operation(AOperation)
+    Operation(AOperation),
+    TargetLocalPath(ATargetLocalPath),
+    DestLocalFileName(ADestLocalFileName)
   {
   }
 
   virtual ExtException * __fastcall Clone();
+  virtual void __fastcall Rethrow();
 
   TOnceDoneOperation Operation;
+  UnicodeString TargetLocalPath;
+  UnicodeString DestLocalFileName;
 };
 //---------------------------------------------------------------------------
 class ECallbackGuardAbort : public EAbort

+ 4 - 3
source/core/FtpFileSystem.cpp

@@ -1744,7 +1744,8 @@ void __fastcall TFTPFileSystem::Sink(const UnicodeString FileName,
       Attrs = FileGetAttrFix(ApiPath(DestFullName));
     }
 
-    Action.Destination(ExpandUNCFileName(DestFullName));
+    UnicodeString ExpandedDestFullName = ExpandUNCFileName(DestFullName);
+    Action.Destination(ExpandedDestFullName);
 
     if (Attrs == -1)
     {
@@ -1760,7 +1761,7 @@ void __fastcall TFTPFileSystem::Sink(const UnicodeString FileName,
       FILE_OPERATION_LOOP_END(FMTLOAD(CANT_SET_ATTRS, (DestFullName)));
     }
 
-    FTerminal->LogFileDone(OperationProgress);
+    FTerminal->LogFileDone(OperationProgress, ExpandedDestFullName);
   }
 
   if (FLAGSET(Params, cpDelete))
@@ -2031,7 +2032,7 @@ void __fastcall TFTPFileSystem::Source(const UnicodeString FileName,
       }
     }
 
-    FTerminal->LogFileDone(OperationProgress);
+    FTerminal->LogFileDone(OperationProgress, DestFullName);
   }
 
   /* TODO : Delete also read-only files. */

+ 1 - 0
source/core/Interface.h

@@ -90,6 +90,7 @@ struct TQueryButtonAlias
   int GroupWith;
   TShiftState GrouppedShiftState;
   bool ElevationRequired;
+  bool MenuButton;
 };
 
 typedef void __fastcall (__closure *TQueryParamsTimerEvent)(unsigned int & Result);

+ 2 - 2
source/core/ScpFileSystem.cpp

@@ -1969,7 +1969,7 @@ void __fastcall TSCPFileSystem::SCPSource(const UnicodeString FileName,
           Rights);
       }
 
-      FTerminal->LogFileDone(OperationProgress);
+      FTerminal->LogFileDone(OperationProgress, AbsoluteFileName);
     }
   }
   __finally
@@ -2667,7 +2667,7 @@ void __fastcall TSCPFileSystem::SCPSink(const UnicodeString TargetDir,
             FILE_OPERATION_LOOP_END(FMTLOAD(CANT_SET_ATTRS, (DestFileName)));
           }
 
-          FTerminal->LogFileDone(OperationProgress);
+          FTerminal->LogFileDone(OperationProgress, DestFileName);
         }
       }
     }

+ 5 - 3
source/core/SftpFileSystem.cpp

@@ -5000,7 +5000,7 @@ void __fastcall TSFTPFileSystem::SFTPSource(const UnicodeString FileName,
         }
       }
 
-      FTerminal->LogFileDone(OperationProgress);
+      FTerminal->LogFileDone(OperationProgress, DestFullName);
     }
   }
   __finally
@@ -5673,6 +5673,7 @@ void __fastcall TSFTPFileSystem::SFTPSink(const UnicodeString FileName,
     RawByteString RemoteHandle;
     UnicodeString LocalFileName = DestFullName;
     TSFTPOverwriteMode OverwriteMode = omOverwrite;
+    UnicodeString ExpandedDestFullName;
 
     try
     {
@@ -5844,7 +5845,8 @@ void __fastcall TSFTPFileSystem::SFTPSink(const UnicodeString FileName,
         }
       }
 
-      Action.Destination(ExpandUNCFileName(DestFullName));
+      ExpandedDestFullName = ExpandUNCFileName(DestFullName);
+      Action.Destination(ExpandedDestFullName);
 
       // if not already opened (resume, append...), create new empty file
       if (!LocalHandle)
@@ -6084,7 +6086,7 @@ void __fastcall TSFTPFileSystem::SFTPSink(const UnicodeString FileName,
       }
     }
 
-    FTerminal->LogFileDone(OperationProgress);
+    FTerminal->LogFileDone(OperationProgress, ExpandedDestFullName);
   }
 
   if (Params & cpDelete)

+ 24 - 5
source/core/Terminal.cpp

@@ -2750,14 +2750,16 @@ bool __fastcall TTerminal::HandleException(Exception * E)
   }
 }
 //---------------------------------------------------------------------------
-void __fastcall TTerminal::CloseOnCompletion(TOnceDoneOperation Operation, const UnicodeString Message)
+void __fastcall TTerminal::CloseOnCompletion(
+  TOnceDoneOperation Operation, const UnicodeString & Message,
+  const UnicodeString & TargetLocalPath, const UnicodeString & DestLocalFileName)
 {
   Configuration->Usage->Inc(L"ClosesOnCompletion");
   LogEvent(L"Closing session after completed operation (as requested by user)");
   Close();
   throw ESshTerminate(NULL,
     Message.IsEmpty() ? UnicodeString(LoadStr(CLOSED_ON_COMPLETION)) : Message,
-    Operation);
+    Operation, TargetLocalPath, DestLocalFileName);
 }
 //---------------------------------------------------------------------------
 TBatchOverwrite __fastcall TTerminal::EffectiveBatchOverwrite(
@@ -3310,12 +3312,21 @@ void __fastcall TTerminal::LogFileDetails(const UnicodeString & FileName, TDateT
   }
 }
 //---------------------------------------------------------------------------
-void __fastcall TTerminal::LogFileDone(TFileOperationProgressType * OperationProgress)
+void __fastcall TTerminal::LogFileDone(TFileOperationProgressType * OperationProgress, const UnicodeString & DestFileName)
 {
+  if (FDestFileName.IsEmpty())
+  {
+    FDestFileName = DestFileName;
+  }
+  else
+  {
+    FMultipleDestinationFiles = true;
+  }
+
   // optimization
   if (Log->Logging)
   {
-    LogEvent(FORMAT("Transfer done: '%s' [%s]", (OperationProgress->FullFileName, IntToStr(OperationProgress->TransferredSize))));
+    LogEvent(FORMAT("Transfer done: '%s' => '%s' [%s]", (OperationProgress->FullFileName, DestFileName, IntToStr(OperationProgress->TransferredSize))));
   }
 }
 //---------------------------------------------------------------------------
@@ -6503,6 +6514,9 @@ bool __fastcall TTerminal::CopyToLocal(TStrings * FilesToCopy,
   DebugAssert(FilesToCopy != NULL);
   TOnceDoneOperation OnceDoneOperation = odoIdle;
 
+  FDestFileName = L"";
+  FMultipleDestinationFiles = false;
+
   BeginTransaction();
   try
   {
@@ -6619,7 +6633,12 @@ bool __fastcall TTerminal::CopyToLocal(TStrings * FilesToCopy,
 
   if (OnceDoneOperation != odoIdle)
   {
-    CloseOnCompletion(OnceDoneOperation);
+    UnicodeString DestFileName;
+    if (!FMultipleDestinationFiles)
+    {
+      DestFileName = FDestFileName;
+    }
+    CloseOnCompletion(OnceDoneOperation, UnicodeString(), TargetDir, DestFileName);
   }
 
   return Result;

+ 6 - 2
source/core/Terminal.h

@@ -214,6 +214,8 @@ private:
   int FNesting;
   UnicodeString FFingerprintScanned;
   DWORD FLastProgressLogged;
+  UnicodeString FDestFileName;
+  bool FMultipleDestinationFiles;
 
   void __fastcall CommandError(Exception * E, const UnicodeString Msg);
   unsigned int __fastcall CommandError(Exception * E, const UnicodeString Msg,
@@ -385,7 +387,7 @@ protected:
   void __fastcall LogRemoteFile(TRemoteFile * File);
   UnicodeString __fastcall FormatFileDetailsForLog(const UnicodeString & FileName, TDateTime Modification, __int64 Size);
   void __fastcall LogFileDetails(const UnicodeString & FileName, TDateTime Modification, __int64 Size);
-  void __fastcall LogFileDone(TFileOperationProgressType * OperationProgress);
+  void __fastcall LogFileDone(TFileOperationProgressType * OperationProgress, const UnicodeString & DestFileName);
   void __fastcall LogTotalTransferDetails(
     const UnicodeString TargetDir, const TCopyParamType * CopyParam,
     TFileOperationProgressType * OperationProgress, bool Parallel, TStrings * Files);
@@ -425,7 +427,9 @@ public:
   void __fastcall RecryptPasswords();
   bool __fastcall AllowedAnyCommand(const UnicodeString Command);
   void __fastcall AnyCommand(const UnicodeString Command, TCaptureOutputEvent OutputEvent);
-  void __fastcall CloseOnCompletion(TOnceDoneOperation Operation = odoDisconnect, const UnicodeString Message = L"");
+  void __fastcall CloseOnCompletion(
+    TOnceDoneOperation Operation = odoDisconnect, const UnicodeString & Message = L"",
+    const UnicodeString & TargetLocalPath = L"", const UnicodeString & DestLocalFileName = L"");
   UnicodeString __fastcall AbsolutePath(UnicodeString Path, bool Local);
   void __fastcall BeginTransaction();
   void __fastcall ReadCurrentDirectory();

+ 4 - 3
source/core/WebDAVFileSystem.cpp

@@ -1608,7 +1608,7 @@ void __fastcall TWebDAVFileSystem::Source(const UnicodeString FileName,
         }
       }
 
-      FTerminal->LogFileDone(OperationProgress);
+      FTerminal->LogFileDone(OperationProgress, DestFullName);
     }
   }
   __finally
@@ -2219,7 +2219,8 @@ void __fastcall TWebDAVFileSystem::Sink(const UnicodeString FileName,
       FilePath = L"/";
     }
 
-    Action.Destination(ExpandUNCFileName(DestFullName));
+    UnicodeString ExpandedDestFullName = ExpandUNCFileName(DestFullName);
+    Action.Destination(ExpandedDestFullName);
 
     FILE_OPERATION_LOOP_BEGIN
     {
@@ -2295,7 +2296,7 @@ void __fastcall TWebDAVFileSystem::Sink(const UnicodeString FileName,
       FILE_OPERATION_LOOP_END(FMTLOAD(CANT_SET_ATTRS, (DestFullName)));
     }
 
-    FTerminal->LogFileDone(OperationProgress);
+    FTerminal->LogFileDone(OperationProgress, ExpandedDestFullName);
   }
 
   if (FLAGSET(Params, cpDelete))

+ 4 - 8
source/forms/CustomScpExplorer.cpp

@@ -6679,9 +6679,9 @@ void __fastcall TCustomScpExplorerForm::RemoteFileControlDDEnd(TObject * Sender)
     }
   }
 
-  if (!FDragDropSshTerminate.IsEmpty())
+  if (FDragDropSshTerminate.get() != NULL)
   {
-    throw ESshTerminate(NULL, FDragDropSshTerminate, FDragDropOnceDoneOperation);
+    FDragDropSshTerminate->Rethrow();
   }
 }
 //---------------------------------------------------------------------------
@@ -7420,8 +7420,7 @@ void __fastcall TCustomScpExplorerForm::RemoteFileControlDDDragDetect(
   FDDFileList = new TStringList();
   FDragTempDir = WinConfiguration->TemporaryDir();
   FDDTotalSize = 0;
-  FDragDropSshTerminate = L"";
-  FDragDropOnceDoneOperation = odoIdle;
+  FDragDropSshTerminate.reset(NULL);
 }
 //---------------------------------------------------------------------------
 void __fastcall TCustomScpExplorerForm::RemoteFileControlDDQueryContinueDrag(
@@ -7439,10 +7438,7 @@ void __fastcall TCustomScpExplorerForm::RemoteFileControlDDQueryContinueDrag(
       }
       catch(ESshTerminate & E)
       {
-        DebugAssert(E.MoreMessages == NULL); // not supported
-        DebugAssert(!E.Message.IsEmpty());
-        FDragDropSshTerminate = E.Message;
-        FDragDropOnceDoneOperation = E.Operation;
+        FDragDropSshTerminate.reset(E.Clone());
       }
     }
     catch (Exception &E)

+ 1 - 2
source/forms/CustomScpExplorer.h

@@ -208,8 +208,7 @@ private:
   TTimer * FDelayedDeletionTimer;
   TStrings * FDDFileList;
   __int64 FDDTotalSize;
-  UnicodeString FDragDropSshTerminate;
-  TOnceDoneOperation FDragDropOnceDoneOperation;
+  std::unique_ptr<ExtException> FDragDropSshTerminate;
   HINSTANCE FOle32Library;
   HCURSOR FDragMoveCursor;
   UnicodeString FDragTempDir;

+ 13 - 2
source/forms/MessageDlg.cpp

@@ -477,7 +477,7 @@ static UnicodeString __fastcall GetKeyNameStr(int Key)
 TButton * __fastcall TMessageForm::CreateButton(
   UnicodeString Name, UnicodeString Caption, unsigned int Answer,
   TNotifyEvent OnClick, bool IsTimeoutButton,
-  int GroupWith, TShiftState GrouppedShiftState, bool ElevationRequired,
+  int GroupWith, TShiftState GrouppedShiftState, bool ElevationRequired, bool MenuButton,
   TAnswerButtons & AnswerButtons, bool HasMoreMessages, int & ButtonWidths)
 {
   UnicodeString MeasureCaption = Caption;
@@ -497,6 +497,10 @@ TButton * __fastcall TMessageForm::CreateButton(
     // Elevation icon
     CurButtonWidth += ScaleByTextHeightRunTime(this, 16);
   }
+  if (MenuButton)
+  {
+    CurButtonWidth += ScaleByTextHeightRunTime(this, 16);
+  }
 
   TButton * Button = NULL;
 
@@ -610,6 +614,11 @@ TButton * __fastcall TMessageForm::CreateButton(
 
     Button->ElevationRequired = ElevationRequired;
     ButtonWidths += Button->Width;
+
+    if (MenuButton)
+    {
+      ::MenuButton(Button);
+    }
   }
 
   return Button;
@@ -846,6 +855,7 @@ TForm * __fastcall TMessageForm::Create(const UnicodeString & Msg,
       int GroupWith = -1;
       TShiftState GrouppedShiftState;
       bool ElevationRequired = false;
+      bool MenuButton = false;
       if (Aliases != NULL)
       {
         for (unsigned int i = 0; i < AliasesCount; i++)
@@ -860,6 +870,7 @@ TForm * __fastcall TMessageForm::Create(const UnicodeString & Msg,
             GroupWith = Aliases[i].GroupWith;
             GrouppedShiftState = Aliases[i].GrouppedShiftState;
             ElevationRequired = Aliases[i].ElevationRequired;
+            MenuButton = Aliases[i].MenuButton;
             DebugAssert((OnClick == NULL) || (GrouppedShiftState == TShiftState()));
             break;
           }
@@ -895,7 +906,7 @@ TForm * __fastcall TMessageForm::Create(const UnicodeString & Msg,
 
       TButton * Button = Result->CreateButton(
         Name, Caption, Answer,
-        OnClick, IsTimeoutButton, GroupWith, GrouppedShiftState, ElevationRequired,
+        OnClick, IsTimeoutButton, GroupWith, GrouppedShiftState, ElevationRequired, MenuButton,
         AnswerButtons, HasMoreMessages, ButtonWidths);
 
       if (Button != NULL)

+ 1 - 1
source/forms/MessageDlg.h

@@ -63,7 +63,7 @@ private:
   TButton * __fastcall CreateButton(
     UnicodeString Name, UnicodeString Caption, unsigned int Answer,
     TNotifyEvent OnClick, bool IsTimeoutButton,
-    int GroupWith, TShiftState GrouppedShiftState, bool ElevationRequired,
+    int GroupWith, TShiftState GrouppedShiftState, bool ElevationRequired, bool MenuButton,
     TAnswerButtons & AnswerButtons, bool HasMoreMessages, int & ButtonWidths);
 };
 //----------------------------------------------------------------------------

+ 1 - 1
source/forms/Preferences.cpp

@@ -1337,7 +1337,7 @@ void __fastcall TPreferencesDialog::EditorFontColorButtonClick(TObject * /*Sende
   // WORKAROUND: Compiler keeps crashing randomly (but frequently) with
   // "internal error" when passing menu directly to unique_ptr.
   // Splitting it to two statements seems to help.
-  // The same hack exists in TSiteAdvancedDialog::ColorButtonClick
+  // The same hack exists in TSiteAdvancedDialog::ColorButtonClick and TOpenLocalPathHandler::Open
   TPopupMenu * Menu = CreateColorPopupMenu(FEditorFont->Color, EditorFontColorChange);
   // Popup menu has to survive the popup as TBX calls click handler asynchronously (post).
   FColorPopupMenu.reset(Menu);

+ 1 - 5
source/forms/ScpCommander.cpp

@@ -946,11 +946,7 @@ void __fastcall TScpCommanderForm::FullSynchronizeDirectories()
 //---------------------------------------------------------------------------
 void __fastcall TScpCommanderForm::ExploreLocalDirectory()
 {
-  if ((int)ShellExecute(Application->Handle, L"explore",
-      (wchar_t*)LocalDirView->Path.data(), NULL, NULL, SW_SHOWNORMAL) <= 32)
-  {
-    throw Exception(FORMAT(EXPLORE_LOCAL_DIR_ERROR, (LocalDirView->Path)));
-  }
+  OpenFolderInExplorer(LocalDirView->Path);
 }
 //---------------------------------------------------------------------------
 void __fastcall TScpCommanderForm::LocalDirViewExecFile(TObject *Sender,

+ 2 - 0
source/resource/TextsWin.h

@@ -285,6 +285,8 @@
 #define USAGE_REFRESH           1574
 #define USAGE_LOGSIZE           1575
 #define PASSWORD_CHANGED        1576
+#define OPEN_TARGET_FOLDER      1577
+#define OPEN_DOWNLOADED_FILE    1578
 
 #define WIN_FORMS_STRINGS       1600
 #define COPY_FILE               1605

+ 2 - 0
source/resource/TextsWin1.rc

@@ -288,6 +288,8 @@ BEGIN
         USAGE_REFRESH, "Reloads remote panel of all running instances of WinSCP. If session (and optionally a path) is specified, refreshes only the instances with that session (and path)."
         USAGE_LOGSIZE, "Enables log rotation and optionally deleting of old logs."
         PASSWORD_CHANGED, "Password has been changed."
+        OPEN_TARGET_FOLDER, "Open &Target Folder"
+        OPEN_DOWNLOADED_FILE, "Open &Downloaded File"
 
         WIN_FORMS_STRINGS, "WIN_FORMS_STRINGS"
         COPY_FILE, "%s file '%s' to %s:"

+ 15 - 0
source/windows/Tools.cpp

@@ -650,6 +650,21 @@ void __fastcall OpenBrowser(UnicodeString URL)
   ShellExecute(Application->Handle, L"open", URL.c_str(), NULL, NULL, SW_SHOWNORMAL);
 }
 //---------------------------------------------------------------------------
+void __fastcall OpenFolderInExplorer(const UnicodeString & Path)
+{
+  if ((int)ShellExecute(Application->Handle, L"explore",
+      (wchar_t*)Path.data(), NULL, NULL, SW_SHOWNORMAL) <= 32)
+  {
+    throw Exception(FMTLOAD(EXPLORE_LOCAL_DIR_ERROR, (Path)));
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall OpenFileInExplorer(const UnicodeString & Path)
+{
+  PCIDLIST_ABSOLUTE Folder = ILCreateFromPathW(ApiPath(Path).c_str());
+  SHOpenFolderAndSelectItems(Folder, 0, NULL, 0);
+}
+//---------------------------------------------------------------------------
 void __fastcall ShowHelp(const UnicodeString & AHelpKeyword)
 {
   // see also AppendUrlParams

+ 2 - 0
source/windows/Tools.h

@@ -43,6 +43,8 @@ void __fastcall ValidateMaskEdit(TMemo * Edit, bool Directory);
 bool __fastcall IsWinSCPUrl(const UnicodeString & Url);
 UnicodeString __fastcall SecureUrl(const UnicodeString & Url);
 void __fastcall OpenBrowser(UnicodeString URL);
+void __fastcall OpenFileInExplorer(const UnicodeString & Path);
+void __fastcall OpenFolderInExplorer(const UnicodeString & Path);
 void __fastcall ShowHelp(const UnicodeString & HelpKeyword);
 bool __fastcall IsFormatInClipboard(unsigned int Format);
 bool __fastcall TextFromClipboard(UnicodeString & Text, bool Trim);

+ 148 - 81
source/windows/UserInterface.cpp

@@ -130,6 +130,57 @@ void __fastcall TerminateApplication()
   Application->Terminate();
 }
 //---------------------------------------------------------------------------
+struct TOpenLocalPathHandler
+{
+  UnicodeString LocalPath;
+  UnicodeString LocalFileName;
+
+  void __fastcall Open(TObject * Sender)
+  {
+    TButton * Button = DebugNotNull(dynamic_cast<TButton *>(Sender));
+    // Reason for separate AMenu variable is given in TPreferencesDialog::EditorFontColorButtonClick
+    TPopupMenu * AMenu = new TPopupMenu(Application);
+    // Popup menu has to survive the popup as TBX calls click handler asynchronously (post).
+    Menu.reset(AMenu);
+    TMenuItem * Item;
+
+    Item = new TMenuItem(Menu.get());
+    Menu->Items->Add(Item);
+    Item->Caption = LoadStr(OPEN_TARGET_FOLDER);
+    Item->OnClick = OpenFolderClick;
+
+    if (!LocalFileName.IsEmpty())
+    {
+      Item = new TMenuItem(Menu.get());
+      Menu->Items->Add(Item);
+      Item->Caption = LoadStr(OPEN_DOWNLOADED_FILE);
+      Item->OnClick = OpenFileClick;
+    }
+
+    MenuPopup(Menu.get(), Button);
+  }
+
+private:
+  std::unique_ptr<TPopupMenu> Menu;
+
+  void __fastcall OpenFolderClick(TObject * /*Sender*/)
+  {
+    if (LocalFileName.IsEmpty())
+    {
+      OpenFolderInExplorer(LocalPath);
+    }
+    else
+    {
+      OpenFileInExplorer(LocalFileName);
+    }
+  }
+
+  void __fastcall OpenFileClick(TObject * /*Sender*/)
+  {
+    ExecuteShellChecked(LocalFileName, L"");
+  }
+};
+//---------------------------------------------------------------------------
 void __fastcall ShowExtendedExceptionEx(TTerminal * Terminal,
   Exception * E)
 {
@@ -165,130 +216,146 @@ void __fastcall ShowExtendedExceptionEx(TTerminal * Terminal,
   {
     TTerminalManager * Manager = TTerminalManager::Instance(false);
 
-    TQueryType Type;
     ESshTerminate * Terminate = dynamic_cast<ESshTerminate*>(E);
     bool CloseOnCompletion = (Terminate != NULL);
-    Type = CloseOnCompletion ? qtInformation : qtError;
-    bool ConfirmExitOnCompletion =
-      CloseOnCompletion &&
-      ((Terminate->Operation == odoDisconnect) || (Terminate->Operation == odoSuspend)) &&
-      WinConfiguration->ConfirmExitOnCompletion;
-
-    if (E->InheritsFrom(__classid(EFatal)) && (Terminal != NULL) &&
-        (Manager != NULL) && (Manager->ActiveTerminal == Terminal))
+
+    bool ForActiveTerminal =
+      E->InheritsFrom(__classid(EFatal)) && (Terminal != NULL) &&
+      (Manager != NULL) && (Manager->ActiveTerminal == Terminal);
+
+    unsigned int Result;
+    if (CloseOnCompletion)
     {
-      int SessionReopenTimeout = 0;
-      TManagedTerminal * ManagedTerminal = dynamic_cast<TManagedTerminal *>(Manager->ActiveTerminal);
-      if ((ManagedTerminal != NULL) &&
-          ((Configuration->SessionReopenTimeout == 0) ||
-           ((double)ManagedTerminal->ReopenStart == 0) ||
-           (int(double(Now() - ManagedTerminal->ReopenStart) * MSecsPerDay) < Configuration->SessionReopenTimeout)))
+      if (ForActiveTerminal)
       {
-        SessionReopenTimeout = GUIConfiguration->SessionReopenAutoIdle;
+        Manager->DisconnectActiveTerminal();
       }
 
-      unsigned int Result;
-      if (CloseOnCompletion)
+      if (Terminate->Operation == odoSuspend)
       {
-        Manager->DisconnectActiveTerminal();
+        // suspend, so that exit prompt is shown only after windows resume
+        SuspendWindows();
+      }
+
+      DebugAssert(Show);
+      bool ConfirmExitOnCompletion =
+        CloseOnCompletion &&
+        ((Terminate->Operation == odoDisconnect) || (Terminate->Operation == odoSuspend)) &&
+        WinConfiguration->ConfirmExitOnCompletion;
 
-        if (Terminate->Operation == odoSuspend)
+      if (ConfirmExitOnCompletion)
+      {
+        TMessageParams Params(mpNeverAskAgainCheck);
+        unsigned int Answers = 0;
+        TQueryButtonAlias Aliases[1];
+        TOpenLocalPathHandler OpenLocalPathHandler;
+        if (!Terminate->TargetLocalPath.IsEmpty() && !ForActiveTerminal)
         {
-          // suspend, so that exit prompt is shown only after windows resume
-          SuspendWindows();
+          OpenLocalPathHandler.LocalPath = Terminate->TargetLocalPath;
+          OpenLocalPathHandler.LocalFileName = Terminate->DestLocalFileName;
+
+          Aliases[0].Button = qaIgnore;
+          Aliases[0].Alias = LoadStr(OPEN_BUTTON);
+          Aliases[0].OnClick = OpenLocalPathHandler.Open;
+          Aliases[0].MenuButton = true;
+          Answers |= Aliases[0].Button;
+          Params.Aliases = Aliases;
+          Params.AliasesCount = LENOF(Aliases);
         }
 
-        DebugAssert(Show);
-        if (ConfirmExitOnCompletion)
+        if (ForActiveTerminal)
         {
-          TMessageParams Params(mpNeverAskAgainCheck);
           UnicodeString MessageFormat =
             MainInstructions((Manager->Count > 1) ?
               FMTLOAD(DISCONNECT_ON_COMPLETION, (Manager->Count - 1)) :
               LoadStr(EXIT_ON_COMPLETION));
-          Result = FatalExceptionMessageDialog(E, Type, 0,
+          Result = FatalExceptionMessageDialog(E, qtInformation, 0,
             MessageFormat,
-            qaYes | qaNo, HELP_NONE, &Params);
-
-          if (Result == qaNeverAskAgain)
-          {
-            Result = qaYes;
-            WinConfiguration->ConfirmExitOnCompletion = false;
-          }
+            Answers | qaYes | qaNo, HELP_NONE, &Params);
         }
         else
         {
-          Result = qaYes;
+          Result =
+            ExceptionMessageDialog(E, qtInformation, L"", Answers | qaOK, HELP_NONE, &Params);
         }
       }
       else
       {
-        if (Show)
+        Result = qaYes;
+      }
+    }
+    else
+    {
+      if (Show)
+      {
+        if (ForActiveTerminal)
         {
-          Result = FatalExceptionMessageDialog(E, Type, SessionReopenTimeout);
+          int SessionReopenTimeout = 0;
+          TManagedTerminal * ManagedTerminal = dynamic_cast<TManagedTerminal *>(Manager->ActiveTerminal);
+          if ((ManagedTerminal != NULL) &&
+              ((Configuration->SessionReopenTimeout == 0) ||
+               ((double)ManagedTerminal->ReopenStart == 0) ||
+               (int(double(Now() - ManagedTerminal->ReopenStart) * MSecsPerDay) < Configuration->SessionReopenTimeout)))
+          {
+            SessionReopenTimeout = GUIConfiguration->SessionReopenAutoIdle;
+          }
+          Result = FatalExceptionMessageDialog(E, qtError, SessionReopenTimeout);
         }
         else
         {
-          Result = qaOK;
+          Result = ExceptionMessageDialog(E, qtError);
         }
       }
-
-      if (Result == qaYes)
+      else
       {
-        DebugAssert(CloseOnCompletion);
-        DebugAssert(Terminate != NULL);
-        DebugAssert(Terminate->Operation != odoIdle);
-        TerminateApplication();
+        Result = qaOK;
+      }
+    }
 
-        switch (Terminate->Operation)
-        {
-          case odoDisconnect:
-            break;
+    if (Result == qaNeverAskAgain)
+    {
+      DebugAssert(CloseOnCompletion);
+      Result = qaYes;
+      WinConfiguration->ConfirmExitOnCompletion = false;
+    }
+
+    if (Result == qaYes)
+    {
+      DebugAssert(CloseOnCompletion);
+      DebugAssert(Terminate != NULL);
+      DebugAssert(Terminate->Operation != odoIdle);
+      TerminateApplication();
+
+      switch (Terminate->Operation)
+      {
+        case odoDisconnect:
+          break;
 
-          case odoSuspend:
-            // suspended before already
-            break;
+        case odoSuspend:
+          // suspended before already
+          break;
 
-          case odoShutDown:
-            ShutDownWindows();
-            break;
+        case odoShutDown:
+          ShutDownWindows();
+          break;
 
-          default:
-            DebugFail();
-        }
+        default:
+          DebugFail();
       }
-      else if (Result == qaRetry)
+    }
+    else if (Result == qaRetry)
+    {
+      // qaRetry is used by FatalExceptionMessageDialog
+      if (DebugAlwaysTrue(ForActiveTerminal))
       {
         Manager->ReconnectActiveTerminal();
       }
-      else
-      {
-        Manager->FreeActiveTerminal();
-      }
     }
     else
     {
-      // this should not happen as we never use Terminal->CloseOnCompletion
-      // on inactive terminal
-      if (CloseOnCompletion)
-      {
-        DebugAssert(Show);
-        if (ConfirmExitOnCompletion)
-        {
-          TMessageParams Params(mpNeverAskAgainCheck);
-          if (ExceptionMessageDialog(E, Type, L"", qaOK, HELP_NONE, &Params) ==
-                qaNeverAskAgain)
-          {
-            WinConfiguration->ConfirmExitOnCompletion = false;
-          }
-        }
-      }
-      else
+      if (ForActiveTerminal)
       {
-        if (Show)
-        {
-          ExceptionMessageDialog(E, Type);
-        }
+        Manager->FreeActiveTerminal();
       }
     }
   }