Selaa lähdekoodia

Extensions

Source commit: 33e7793a0f0ef19eec38b3a55e36ecb11c4595b6
Martin Prikryl 9 vuotta sitten
vanhempi
sitoutus
f852f86f1a

+ 5 - 0
source/core/Common.cpp

@@ -3390,3 +3390,8 @@ UnicodeString __fastcall AssemblyNewClassInstanceEnd(TAssemblyLanguage Language,
   }
   return Result;
 }
+//---------------------------------------------------------------------------
+void __fastcall LoadScriptFromFile(UnicodeString FileName, TStrings * Lines)
+{
+  Lines->LoadFromFile(FileName, TEncoding::UTF8);
+}

+ 1 - 0
source/core/Common.h

@@ -141,6 +141,7 @@ void __fastcall ParseCertificate(const UnicodeString & Path,
   const UnicodeString & Passphrase, X509 *& Certificate, EVP_PKEY *& PrivateKey,
   bool & WrongPassphrase);
 bool __fastcall IsHttpUrl(const UnicodeString & S);
+void __fastcall LoadScriptFromFile(UnicodeString FileName, TStrings * Lines);
 //---------------------------------------------------------------------------
 typedef void __fastcall (__closure* TProcessLocalFileEvent)
   (const UnicodeString FileName, const TSearchRec Rec, void * Param);

+ 9 - 0
source/core/FileInfo.cpp

@@ -267,3 +267,12 @@ int __fastcall CalculateCompoundVersion(int MajorVer,
     100 * MajorVer));
   return CompoundVer;
 }
+//---------------------------------------------------------------------------
+int __fastcall StrToCompoundVersion(UnicodeString S)
+{
+  int MajorVer = StrToInt(CutToChar(S, L'.', false));
+  int MinorVer = StrToInt(CutToChar(S, L'.', false));
+  int Release = S.IsEmpty() ? 0 : StrToInt(CutToChar(S, L'.', false));
+  int Build = S.IsEmpty() ? 0 : StrToInt(CutToChar(S, L'.', false));
+  return CalculateCompoundVersion(MajorVer, MinorVer, Release, Build);
+}

+ 2 - 0
source/core/FileInfo.h

@@ -31,4 +31,6 @@ UnicodeString __fastcall GetFileInfoString(void * FileInfo,
 int __fastcall CalculateCompoundVersion(int MajorVer,
   int MinorVer, int Release, int Build);
 
+int __fastcall StrToCompoundVersion(UnicodeString S);
+
 #endif // FileInfoH

+ 17 - 10
source/forms/CustomScpExplorer.cpp

@@ -3958,6 +3958,21 @@ void __fastcall TCustomScpExplorerForm::KeyProcessed(Word & Key, TShiftState Shi
   Key = 0;
 }
 //---------------------------------------------------------------------------
+void __fastcall TCustomScpExplorerForm::CheckCustomCommandShortCut(
+  TCustomCommandList * List, Word & Key, Classes::TShiftState Shift, TShortCut KeyShortCut)
+{
+  const TCustomCommandType * Command = List->Find(KeyShortCut);
+  if (Command != NULL)
+  {
+    KeyProcessed(Key, Shift);
+    if (CustomCommandState(*Command, false) > 0)
+    {
+      ExecuteFileOperationCommand(foCustomCommand, osRemote,
+        false, false, const_cast<TCustomCommandType *>(Command));
+    }
+  }
+}
+//---------------------------------------------------------------------------
 void __fastcall TCustomScpExplorerForm::KeyDown(Word & Key, Classes::TShiftState Shift)
 {
   if (QueueView3->Focused() && (QueueView3->OnKeyDown != NULL))
@@ -4004,16 +4019,8 @@ void __fastcall TCustomScpExplorerForm::KeyDown(Word & Key, Classes::TShiftState
 
     if (IsCustomShortCut(KeyShortCut))
     {
-      const TCustomCommandType * Command = WinConfiguration->CustomCommandList->Find(KeyShortCut);
-      if (Command != NULL)
-      {
-        KeyProcessed(Key, Shift);
-        if (CustomCommandState(*Command, false) > 0)
-        {
-          ExecuteFileOperationCommand(foCustomCommand, osRemote,
-            false, false, const_cast<TCustomCommandType *>(Command));
-        }
-      }
+      CheckCustomCommandShortCut(WinConfiguration->CustomCommandList, Key, Shift, KeyShortCut);
+      CheckCustomCommandShortCut(WinConfiguration->ExtensionList, Key, Shift, KeyShortCut);
 
       if (WinConfiguration->SharedBookmarks != NULL)
       {

+ 1 - 0
source/forms/CustomScpExplorer.h

@@ -294,6 +294,7 @@ private:
   void __fastcall UpdateSessionsPageControlHeight();
   TDragDropFilesEx * __fastcall CreateDragDropFilesEx();
   void __fastcall KeyProcessed(Word & Key, TShiftState Shift);
+  void __fastcall CheckCustomCommandShortCut(TCustomCommandList * List, Word & Key, Classes::TShiftState Shift, TShortCut KeyShortCut);
   bool __fastcall CanPasteToDirViewFromClipBoard();
   void __fastcall CMShowingChanged(TMessage & Message);
   void __fastcall WMClose(TMessage & Message);

+ 1 - 1
source/forms/LocationProfiles.cpp

@@ -1083,7 +1083,7 @@ void __fastcall TLocationProfilesDialog::ShortCutBookmarkButtonClick(
   TBookmark * Bookmark = static_cast<TBookmark *>(Node->Data);
 
   TShortCuts ShortCuts;
-  WinConfiguration->CustomCommandList->ShortCuts(ShortCuts);
+  WinConfiguration->CustomCommandShortCuts(ShortCuts);
   BookmarkList->ShortCuts(ShortCuts);
   TShortCut ShortCut = Bookmark->ShortCut;
   if (DoShortCutDialog(ShortCut, ShortCuts, HelpKeyword))

+ 81 - 31
source/forms/NonVisual.cpp

@@ -1034,12 +1034,17 @@ UnicodeString __fastcall TNonVisualDataModule::CustomCommandHint(const TCustomCo
   return Result;
 }
 //---------------------------------------------------------------------------
-void __fastcall TNonVisualDataModule::CreateCustomCommandsMenu(
-  TTBCustomItem * Menu, bool OnFocused, bool Toolbar, bool Both)
+const int CustomCommandOnFocused = 0x0100;
+const int CustomCommandBoth = 0x0200;
+const int CustomCommandExtension = 0x0400;
+const int CustomCommandIndexMask = 0x00FF;
+//---------------------------------------------------------------------------
+void __fastcall TNonVisualDataModule::CreateCustomCommandsListMenu(
+  TCustomCommandList * List, TTBCustomItem * Menu, bool OnFocused, bool Toolbar, bool Both, int Tag)
 {
-  for (int Index = 0; Index < WinConfiguration->CustomCommandList->Count; Index++)
+  for (int Index = 0; Index < List->Count; Index++)
   {
-    const TCustomCommandType * Command = WinConfiguration->CustomCommandList->Commands[Index];
+    const TCustomCommandType * Command = List->Commands[Index];
     int State;
 
     if (!Both)
@@ -1055,15 +1060,15 @@ void __fastcall TNonVisualDataModule::CreateCustomCommandsMenu(
     {
       TTBCustomItem * Item = new TTBXItem(Owner);
       Item->Caption = CustomCommandCaption(Command, Toolbar);
-      Item->Tag = Index;
+      Item->Tag = Index | Tag;
       Item->Enabled = (State > 0);
       if (OnFocused)
       {
-        Item->Tag = Item->Tag | 0x0100;
+        Item->Tag = Item->Tag | CustomCommandOnFocused;
       }
       if (Both)
       {
-        Item->Tag = Item->Tag | 0x0200;
+        Item->Tag = Item->Tag | CustomCommandBoth;
       }
       UnicodeString Name = StripHotkey(Command->Name);
       UnicodeString ShortHint = FMTLOAD(CUSTOM_COMMAND_HINT, (Name));
@@ -1078,6 +1083,12 @@ void __fastcall TNonVisualDataModule::CreateCustomCommandsMenu(
       Menu->Add(Item);
     }
   }
+}
+//---------------------------------------------------------------------------
+void __fastcall TNonVisualDataModule::CreateCustomCommandsMenu(
+  TTBCustomItem * Menu, bool OnFocused, bool Toolbar, bool Both)
+{
+  CreateCustomCommandsListMenu(WinConfiguration->CustomCommandList, Menu, OnFocused, Toolbar, Both, 0);
 
   TTBCustomItem * Item;
 
@@ -1096,6 +1107,11 @@ void __fastcall TNonVisualDataModule::CreateCustomCommandsMenu(
     Menu->Add(Item);
   }
 
+  TTBXSeparatorItem * Separator = AddMenuSeparator(Menu);
+  Separator->Visible = (WinConfiguration->ExtensionList->Count > 0);
+
+  CreateCustomCommandsListMenu(WinConfiguration->ExtensionList, Menu, OnFocused, Toolbar, Both, CustomCommandExtension);
+
   AddMenuSeparator(Menu);
 
   if (!Toolbar && !Both)
@@ -1138,6 +1154,39 @@ void __fastcall TNonVisualDataModule::CreateCustomCommandsMenu(TAction * Action)
   }
 }
 //---------------------------------------------------------------------------
+bool __fastcall TNonVisualDataModule::CheckCustomCommandsToolbarList(TTBXToolbar * Toolbar, TCustomCommandList * List, int & Index)
+{
+  bool Changed = false;
+  int CommandIndex = 0;
+  while (!Changed && (CommandIndex < List->Count))
+  {
+    TTBCustomItem * Item = Toolbar->Items->Items[Index];
+    const TCustomCommandType * Command = List->Commands[CommandIndex];
+
+    Changed =
+      (Item->Caption != CustomCommandCaption(Command, true)) ||
+      (Item->Hint != CustomCommandHint(Command));
+
+    Index++;
+    CommandIndex++;
+  }
+  return Changed;
+}
+//---------------------------------------------------------------------------
+void __fastcall TNonVisualDataModule::UpdateCustomCommandsToolbarList(TTBXToolbar * Toolbar, TCustomCommandList * List, int & Index)
+{
+  int CommandIndex = 0;
+  for (int CommandIndex = 0; CommandIndex < List->Count; CommandIndex++, Index++)
+  {
+    TTBCustomItem * Item = Toolbar->Items->Items[Index];
+    int CommandIndex2 = (Item->Tag & CustomCommandIndexMask);
+    DebugAssert(CommandIndex2 == CommandIndex);
+    int State = ScpExplorer->CustomCommandState(*List->Commands[CommandIndex], false);
+    DebugAssert(State >= 0);
+    Item->Enabled = (State > 0);
+  }
+}
+//---------------------------------------------------------------------------
 void __fastcall TNonVisualDataModule::UpdateCustomCommandsToolbar(TTBXToolbar * Toolbar)
 {
   // can be called while explorer is being created
@@ -1146,22 +1195,28 @@ void __fastcall TNonVisualDataModule::UpdateCustomCommandsToolbar(TTBXToolbar *
     return;
   }
 
-  int AdditionalCommands = 4;
   TCustomCommandList * CustomCommandList = WinConfiguration->CustomCommandList;
-  bool Changed = (CustomCommandList->Count != (Toolbar->Items->Count - AdditionalCommands));
+  TCustomCommandList * ExtensionList = WinConfiguration->ExtensionList;
+  int ExtensionSeparatorCount = (ExtensionList->Count > 0) ? 1 : 0;
+  int AfterCustomCommandsCommandCount = 2; // ad hoc, last
+  int AdditionalCommands = AfterCustomCommandsCommandCount + 3; // custom/ext separator + separator, customize
+  int CommandCount = CustomCommandList->Count + ExtensionList->Count;
+  bool Changed = (CommandCount + AdditionalCommands != Toolbar->Items->Count);
   if (!Changed)
   {
     int Index = 0;
-    while (!Changed && (Index < CustomCommandList->Count))
-    {
-      TTBCustomItem * Item = Toolbar->Items->Items[Index];
-      const TCustomCommandType * Command = CustomCommandList->Commands[Index];
-
-      Changed =
-        (Item->Caption != CustomCommandCaption(Command, true)) ||
-        (Item->Hint != CustomCommandHint(Command));
+    Changed = CheckCustomCommandsToolbarList(Toolbar, CustomCommandList, Index);
 
+    if (!Changed)
+    {
+      Index += AfterCustomCommandsCommandCount;
+      Changed = (dynamic_cast<TTBXSeparatorItem *>(Toolbar->Items->Items[Index]) == NULL);
       Index++;
+
+      if (!Changed)
+      {
+        Changed = CheckCustomCommandsToolbarList(Toolbar, ExtensionList, Index);
+      }
     }
   }
 
@@ -1172,7 +1227,7 @@ void __fastcall TNonVisualDataModule::UpdateCustomCommandsToolbar(TTBXToolbar *
     {
       Toolbar->Items->Clear();
       CreateCustomCommandsMenu(Toolbar->Items, false, true, false);
-      DebugAssert(CustomCommandList->Count == (Toolbar->Items->Count - AdditionalCommands));
+      DebugAssert(CommandCount + AdditionalCommands == Toolbar->Items->Count);
     }
     __finally
     {
@@ -1181,16 +1236,10 @@ void __fastcall TNonVisualDataModule::UpdateCustomCommandsToolbar(TTBXToolbar *
   }
   else
   {
-    for (int Index = 0; Index < Toolbar->Items->Count - AdditionalCommands; Index++)
-    {
-      TTBCustomItem * Item = Toolbar->Items->Items[Index];
-      int CommandIndex = (Item->Tag & 0x00FF);
-      DebugAssert(CommandIndex == Index);
-      int State = ScpExplorer->CustomCommandState(
-        *CustomCommandList->Commands[CommandIndex], false);
-      DebugAssert(State >= 0);
-      Item->Enabled = (State > 0);
-    }
+    int Index = 0;
+    UpdateCustomCommandsToolbarList(Toolbar, CustomCommandList, Index);
+    Index += AfterCustomCommandsCommandCount + 1;
+    UpdateCustomCommandsToolbarList(Toolbar, ExtensionList, Index);
   }
 }
 //---------------------------------------------------------------------------
@@ -1198,11 +1247,12 @@ void __fastcall TNonVisualDataModule::CustomCommandClick(TObject * Sender)
 {
   TTBCustomItem * Item = dynamic_cast<TTBCustomItem *>(Sender);
   DebugAssert(Item);
-  const TCustomCommandType * Command = WinConfiguration->CustomCommandList->Commands[Item->Tag & 0x00FF];
-  if (FLAGCLEAR(Item->Tag, 0x0200))
+  const TCustomCommandList * List = FLAGSET(Item->Tag, CustomCommandExtension) ? WinConfiguration->ExtensionList : WinConfiguration->CustomCommandList;
+  const TCustomCommandType * Command = List->Commands[Item->Tag & CustomCommandIndexMask];
+  if (FLAGCLEAR(Item->Tag, CustomCommandBoth))
   {
     ScpExplorer->ExecuteFileOperationCommand(foCustomCommand, osRemote,
-      FLAGSET(Item->Tag, 0x0100), false, const_cast<TCustomCommandType *>(Command));
+      FLAGSET(Item->Tag, CustomCommandOnFocused), false, const_cast<TCustomCommandType *>(Command));
   }
   else
   {

+ 3 - 0
source/forms/NonVisual.h

@@ -644,7 +644,10 @@ protected:
   UnicodeString __fastcall GetSessionFolderRoot(TSessionData * Data, int Level);
   void __fastcall CreateWorkspacesMenu(TAction * Action);
   void __fastcall WorkspaceItemClick(TObject * Sender);
+  void __fastcall CreateCustomCommandsListMenu(TCustomCommandList * List, TTBCustomItem * Menu, bool OnFocused, bool Toolbar, bool Both, int Tag);
   void __fastcall CreateCustomCommandsMenu(TAction * Action);
+  bool __fastcall CheckCustomCommandsToolbarList(TTBXToolbar * Toolbar, TCustomCommandList * List, int & Index);
+  void __fastcall UpdateCustomCommandsToolbarList(TTBXToolbar * Toolbar, TCustomCommandList * List, int & Index);
   void __fastcall CreateSessionColorMenu(TAction * Action);
   void __fastcall SessionColorChange(TColor Color);
   void __fastcall CreateOpenedSessionListMenu(TAction * Action);

+ 1 - 1
source/forms/OpenDirectory.cpp

@@ -591,7 +591,7 @@ void __fastcall TOpenDirectoryDialog::ShortCutBookmarkButtonClick(
   DebugAssert(Bookmark != NULL);
 
   TShortCuts ShortCuts;
-  WinConfiguration->CustomCommandList->ShortCuts(ShortCuts);
+  WinConfiguration->CustomCommandShortCuts(ShortCuts);
   BookmarkList->ShortCuts(ShortCuts);
   TShortCut ShortCut = Bookmark->ShortCut;
   if (DoShortCutDialog(ShortCut, ShortCuts, HelpKeyword))

+ 128 - 29
source/forms/Preferences.cpp

@@ -67,6 +67,7 @@ __fastcall TPreferencesDialog::TPreferencesDialog(
   FAfterFilenameEditDialog = false;
   FCustomCommandList = new TCustomCommandList();
   FCustomCommandChanging = false;
+  FExtensionList = new TCustomCommandList();
   FListViewDragDest = -1;
   FCopyParamList = new TCopyParamList();
   FEditorList = new TEditorList();
@@ -81,6 +82,9 @@ __fastcall TPreferencesDialog::TPreferencesDialog(
   FEditorScrollOnDragOver = new TListViewScrollOnDragOver(EditorListView3, true);
   FixListColumnWidth(EditorListView3, -1);
 
+  FOrigCustomCommandsViewWindowProc = CustomCommandsView->WindowProc;
+  CustomCommandsView->WindowProc = CustomCommandsViewWindowProc;
+
   ComboAutoSwitchInitialize(UpdatesBetaVersionsCombo);
   EnableControl(UpdatesBetaVersionsCombo, !WinConfiguration->IsBeta);
   EnableControl(UpdatesBetaVersionsLabel, UpdatesBetaVersionsCombo->Enabled);
@@ -119,6 +123,8 @@ __fastcall TPreferencesDialog::~TPreferencesDialog()
   SAFE_DESTROY(FCustomCommandsScrollOnDragOver);
   delete FCustomCommandList;
   FCustomCommandList = NULL;
+  delete FExtensionList;
+  FExtensionList = NULL;
   delete FCopyParamList;
   FCopyParamList = NULL;
   delete FEditorList;
@@ -349,6 +355,7 @@ void __fastcall TPreferencesDialog::LoadConfiguration()
     RandomSeedFileEdit->Visible = WinConfiguration->ExpertMode;
 
     FCustomCommandList->Assign(WinConfiguration->CustomCommandList);
+    FExtensionList->Assign(WinConfiguration->ExtensionList);
     UpdateCustomCommandsView();
 
     PuttyPathEdit->Text = GUIConfiguration->PuttyPath;
@@ -710,6 +717,7 @@ void __fastcall TPreferencesDialog::SaveConfiguration()
     Configuration->SessionReopenTimeout = (SessionReopenTimeoutEdit->AsInteger * MSecsPerSec);
 
     WinConfiguration->CustomCommandList = FCustomCommandList;
+    WinConfiguration->ExtensionList = FExtensionList;
 
     GUIConfiguration->PuttyPath = PuttyPathEdit->Text;
     GUIConfiguration->PuttyPassword = PuttyPasswordCheck2->Checked;
@@ -999,6 +1007,27 @@ UnicodeString __fastcall TPreferencesDialog::TabSample(UnicodeString Values)
   return Result;
 }
 //---------------------------------------------------------------------------
+TCustomCommandList * __fastcall TPreferencesDialog::GetCommandList(int Index)
+{
+  if (Index < FCustomCommandList->Count)
+  {
+    return FCustomCommandList;
+  }
+  else
+  {
+    return FExtensionList;
+  }
+}
+//---------------------------------------------------------------------------
+int __fastcall TPreferencesDialog::GetCommandIndex(int Index)
+{
+  if (Index >= FCustomCommandList->Count)
+  {
+    Index -= FCustomCommandList->Count;
+  }
+  return Index;
+}
+//---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::UpdateControls()
 {
   if (FNoUpdate == 0)
@@ -1055,13 +1084,15 @@ void __fastcall TPreferencesDialog::UpdateControls()
     }
     EditorFontLabel->Color = GetWindowColor(FEditorBackgroundColor);
 
+    TCustomCommandList * CommandList = GetCommandList(CustomCommandsView->ItemIndex);
+    int CommandIndex = GetCommandIndex(CustomCommandsView->ItemIndex);
     bool CommandSelected = (CustomCommandsView->Selected != NULL);
-    EnableControl(EditCommandButton, CommandSelected);
+    bool CustomCommandSelected = CommandSelected && (CommandList == FCustomCommandList);
+    bool ExtensionSelected = CommandSelected && (CommandList == FExtensionList);
+    EnableControl(EditCommandButton, CustomCommandSelected);
     EnableControl(RemoveCommandButton, CommandSelected);
-    EnableControl(UpCommandButton, CommandSelected &&
-      CustomCommandsView->ItemIndex > 0);
-    EnableControl(DownCommandButton, CommandSelected &&
-      (CustomCommandsView->ItemIndex < CustomCommandsView->Items->Count - 1));
+    EnableControl(UpCommandButton, CommandSelected && (CommandIndex > 0));
+    EnableControl(DownCommandButton, CommandSelected && (CommandIndex < CommandList->Count - 1));
 
     bool CopyParamSelected = (CopyParamListView->Selected != NULL);
     EnableControl(EditCopyParamButton, CopyParamSelected);
@@ -1321,11 +1352,10 @@ void __fastcall TPreferencesDialog::CustomCommandsViewData(TObject * /*Sender*/,
       TListItem * Item)
 {
   // WORKAROUND We get here on Wine after destructor is called
-  if (FCustomCommandList != NULL)
+  if ((FCustomCommandList != NULL) && (FExtensionList != NULL))
   {
     int Index = Item->Index;
-    DebugAssert(Index >= 0 && Index <= FCustomCommandList->Count);
-    const TCustomCommandType * Command = FCustomCommandList->Commands[Index];
+    const TCustomCommandType * Command = GetCommandList(Index)->Commands[GetCommandIndex(Index)];
     UnicodeString Caption = StripHotkey(Command->Name);
     if (Command->ShortCut != 0)
     {
@@ -1356,7 +1386,7 @@ void __fastcall TPreferencesDialog::ListViewSelectItem(
 //---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::UpdateCustomCommandsView()
 {
-  CustomCommandsView->Items->Count = FCustomCommandList->Count;
+  CustomCommandsView->Items->Count = FCustomCommandList->Count + FExtensionList->Count;
   AutoSizeListColumnsWidth(CustomCommandsView);
   CustomCommandsView->Invalidate();
 }
@@ -1392,9 +1422,9 @@ void __fastcall TPreferencesDialog::AddEditCommandButtonClick(TObject * Sender)
   if (Edit)
   {
     int Index = CustomCommandsView->ItemIndex;
-    DebugAssert(Index >= 0 && Index <= FCustomCommandList->Count);
+    DebugAssert(GetCommandList(Index) == FCustomCommandList);
 
-    Command = *FCustomCommandList->Commands[Index];
+    Command = *FCustomCommandList->Commands[GetCommandIndex(Index)];
   }
 
   TShortCuts ShortCuts;
@@ -1403,14 +1433,24 @@ void __fastcall TPreferencesDialog::AddEditCommandButtonClick(TObject * Sender)
     WinConfiguration->SharedBookmarks->ShortCuts(ShortCuts);
   }
   FCustomCommandList->ShortCuts(ShortCuts);
+  FExtensionList->ShortCuts(ShortCuts);
 
   if (DoCustomCommandDialog(Command, FCustomCommandList,
         (Edit ? ccmEdit : ccmAdd), 0, NULL, &ShortCuts))
   {
-    int Index = CustomCommandsView->ItemIndex;
+    int Index;
+    if (GetCommandList(CustomCommandsView->ItemIndex) == FCustomCommandList)
+    {
+      Index = GetCommandIndex(CustomCommandsView->ItemIndex);
+    }
+    else
+    {
+      Index = -1;
+    }
     TCustomCommandType * ACommand = new TCustomCommandType(Command);
     if (Edit)
     {
+      DebugAssert(Index < FCustomCommandList->Count);
       FCustomCommandList->Change(Index, ACommand);
     }
     else
@@ -1427,6 +1467,7 @@ void __fastcall TPreferencesDialog::AddEditCommandButtonClick(TObject * Sender)
     }
 
     UpdateCustomCommandsView();
+    // assumes that the custom command are the first
     CustomCommandsView->ItemIndex = Index;
     UpdateControls();
   }
@@ -1435,26 +1476,25 @@ void __fastcall TPreferencesDialog::AddEditCommandButtonClick(TObject * Sender)
 void __fastcall TPreferencesDialog::RemoveCommandButtonClick(
       TObject * /*Sender*/)
 {
-  DebugAssert(CustomCommandsView->ItemIndex >= 0 &&
-    CustomCommandsView->ItemIndex < FCustomCommandList->Count);
-  FCustomCommandList->Delete(CustomCommandsView->ItemIndex);
+  TCustomCommandList * List = GetCommandList(CustomCommandsView->ItemIndex);
+  int Index = GetCommandIndex(CustomCommandsView->ItemIndex);
+  List->Delete(Index);
   UpdateCustomCommandsView();
   UpdateControls();
 }
 //---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::CustomCommandMove(int Source, int Dest)
 {
-  if (Source >= 0 && Source < FCustomCommandList->Count &&
-      Dest >= 0 && Dest < FCustomCommandList->Count)
-  {
-    FCustomCommandList->Move(Source, Dest);
-    // workaround for bug in VCL
-    CustomCommandsView->ItemIndex = -1;
-    CustomCommandsView->ItemFocused = CustomCommandsView->Selected;
-    CustomCommandsView->ItemIndex = Dest;
-    UpdateCustomCommandsView();
-    UpdateControls();
-  }
+  TCustomCommandList * List = GetCommandList(CustomCommandsView->ItemIndex);
+  int SourceIndex = GetCommandIndex(Source);
+  int DestIndex = GetCommandIndex(Dest);
+  List->Move(SourceIndex, DestIndex);
+  // workaround for bug in VCL
+  CustomCommandsView->ItemIndex = -1;
+  CustomCommandsView->ItemFocused = CustomCommandsView->Selected;
+  CustomCommandsView->ItemIndex = Dest;
+  UpdateCustomCommandsView();
+  UpdateControls();
 }
 //---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::UpDownCommandButtonClick(TObject * Sender)
@@ -1492,10 +1532,15 @@ void __fastcall TPreferencesDialog::ListViewStartDrag(
   ScrollOnDragOver(Sender)->StartDrag();
 }
 //---------------------------------------------------------------------------
-bool __fastcall TPreferencesDialog::AllowListViewDrag(TObject * Sender, int X, int Y)
+static int __fastcall PointToListViewIndex(TObject * Sender, int X, int Y)
 {
   TListItem * Item = dynamic_cast<TListView*>(Sender)->GetItemAt(X, Y);
-  FListViewDragDest = Item ? Item->Index : -1;
+  return Item ? Item->Index : -1;
+}
+//---------------------------------------------------------------------------
+bool __fastcall TPreferencesDialog::AllowListViewDrag(TObject * Sender, int X, int Y)
+{
+  FListViewDragDest = PointToListViewIndex(Sender, X, Y);
   return (FListViewDragDest >= 0) && (FListViewDragDest != FListViewDragSource);
 }
 //---------------------------------------------------------------------------
@@ -1504,13 +1549,29 @@ void __fastcall TPreferencesDialog::CustomCommandsViewDragDrop(
 {
   if (Source == CustomCommandsView)
   {
-    if (AllowListViewDrag(Sender, X, Y))
+    if (AllowListViewDrag(Sender, X, Y) &&
+        (GetCommandList(FListViewDragSource) == GetCommandList(FListViewDragDest)))
     {
       CustomCommandMove(FListViewDragSource, FListViewDragDest);
     }
   }
 }
 //---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::CustomCommandsViewDragOver(
+  TObject *Sender, TObject *Source, int X, int Y, TDragState State, bool & Accept)
+{
+  ListViewDragOver(Sender, Source, X, Y, State, Accept);
+
+  if (Source == Sender)
+  {
+    int Dest = PointToListViewIndex(Sender, X, Y);
+    if (GetCommandList(FListViewDragSource) != GetCommandList(Dest))
+    {
+      Accept = false;
+    }
+  }
+}
+//---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::ListViewDragOver(
   TObject * Sender, TObject * Source, int X, int Y,
   TDragState /*State*/, bool & Accept)
@@ -2277,3 +2338,41 @@ void __fastcall TPreferencesDialog::UpdatesLinkClick(TObject * /*Sender*/)
   EnableAutomaticUpdates();
 }
 //---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::CustomCommandsViewWindowProc(TMessage & Message)
+{
+  FOrigCustomCommandsViewWindowProc(Message);
+
+  if (Message.Msg == CN_NOTIFY)
+  {
+    TWMNotify & NotifyMessage = reinterpret_cast<TWMNotify &>(Message);
+
+    if (NotifyMessage.NMHdr->code == NM_CUSTOMDRAW)
+    {
+      // request CDDS_ITEMPOSTPAINT notification
+      Message.Result |= CDRF_NOTIFYPOSTPAINT | CDRF_NOTIFYSUBITEMDRAW;
+
+      TNMLVCustomDraw * CustomDraw = reinterpret_cast<TNMLVCustomDraw *>(NotifyMessage.NMHdr);
+      int Index = CustomDraw->nmcd.dwItemSpec;
+      int CommandIndex = GetCommandIndex(Index);
+      TCustomCommandList * List = GetCommandList(Index);
+      // after end of every list, except for the last last list
+      if ((CommandIndex == List->Count - 1) && (Index < CustomCommandsView->Items->Count - 1) &&
+          FLAGSET(CustomDraw->nmcd.dwDrawStage, CDDS_ITEMPOSTPAINT))
+      {
+        TRect Rect;
+        Rect.Top = CustomDraw->iSubItem;
+        Rect.Left = LVIR_BOUNDS;
+        CustomCommandsView->Perform(LVM_GETSUBITEMRECT, CustomDraw->nmcd.dwItemSpec, reinterpret_cast<LPARAM>(&Rect));
+
+        HDC DC = CustomDraw->nmcd.hdc;
+
+        SelectObject(DC, GetStockObject(DC_PEN));
+        SetDCPenColor(DC, ColorToRGB(clWindowFrame));
+
+        MoveToEx(DC, Rect.Left, Rect.Bottom - 1, NULL);
+        LineTo(DC, Rect.Right, Rect.Bottom - 1);
+      }
+    }
+  }
+}
+//---------------------------------------------------------------------------

+ 1 - 1
source/forms/Preferences.dfm

@@ -1195,7 +1195,7 @@ object PreferencesDialog: TPreferencesDialog
             OnDblClick = CustomCommandsViewDblClick
             OnEndDrag = ListViewEndDrag
             OnDragDrop = CustomCommandsViewDragDrop
-            OnDragOver = ListViewDragOver
+            OnDragOver = CustomCommandsViewDragOver
             OnKeyDown = CustomCommandsViewKeyDown
             OnSelectItem = ListViewSelectItem
             OnStartDrag = ListViewStartDrag

+ 6 - 0
source/forms/Preferences.h

@@ -388,6 +388,7 @@ __published:
   void __fastcall EditorBackgroundColorButtonClick(TObject *Sender);
   void __fastcall UpdatesAuthenticationEmailEditExit(TObject *Sender);
   void __fastcall UpdatesLinkClick(TObject *Sender);
+  void __fastcall CustomCommandsViewDragOver(TObject *Sender, TObject *Source, int X, int Y, TDragState State, bool &Accept);
 
 private:
   TPreferencesMode FPreferencesMode;
@@ -395,6 +396,7 @@ private:
   TColor FEditorBackgroundColor;
   std::unique_ptr<TFont> FPanelFont;
   TCustomCommandList * FCustomCommandList;
+  TCustomCommandList * FExtensionList;
   TCopyParamList * FCopyParamList;
   TEditorList * FEditorList;
   bool FCustomCommandChanging;
@@ -412,6 +414,7 @@ private:
   std::unique_ptr<TPopupMenu> FColorPopupMenu;
   UnicodeString FVerifiedUpdatesAuthenticationEmail;
   bool FAutomaticUpdatesPossible;
+  TWndMethod FOrigCustomCommandsViewWindowProc;
   void __fastcall CMDialogKey(TWMKeyDown & Message);
   void __fastcall WMHelp(TWMHelp & Message);
   UnicodeString __fastcall TabSample(UnicodeString Values);
@@ -420,6 +423,9 @@ private:
   void __fastcall SelectPuttyRegistryStorageKey(const UnicodeString & Key);
   TInterface __fastcall GetInterface();
   TUpdatesConfiguration __fastcall SaveUpdates();
+  void __fastcall CustomCommandsViewWindowProc(TMessage & Message);
+  TCustomCommandList * __fastcall GetCommandList(int Index);
+  int __fastcall GetCommandIndex(int Index);
 public:
   virtual __fastcall ~TPreferencesDialog();
   bool __fastcall Execute(TPreferencesDialogData * DialogData);

+ 3 - 0
source/resource/TextsWin.h

@@ -84,6 +84,9 @@
 #define TIPS_NONE               1198
 #define KEYGEN_PUBLIC           1199
 #define FINGERPRINTSCAN_NEED_SECURE_SESSION 1200
+#define EXTENSION_DEPENDENCY_ERROR 1201
+#define EXTENSION_DIRECTIVE_ERROR 1202
+#define EXTENSION_DIRECTIVE_MISSING 1203
 
 #define WIN_CONFIRMATION_STRINGS 1300
 #define CONFIRM_OVERWRITE_SESSION 1301

+ 3 - 0
source/resource/TextsWin1.rc

@@ -575,6 +575,9 @@ BEGIN
         GENERATE_URL_TRANSFER_FILES, "Transfer files"
         GENERATE_URL_FILE_SAMPLE, "A sample file list is used only, consider using a file mask to select files for the transfer."
         CHECK_FOR_UPDATES_TITLE, "Check for Updates"
+        EXTENSION_DEPENDENCY_ERROR, "The extension requires %s"
+        EXTENSION_DIRECTIVE_ERROR, "Invalid value \"%s\" of extension directive %s"
+        EXTENSION_DIRECTIVE_MISSING, "Missing mandatory extension directive %s"
 
         WIN_VARIABLE_STRINGS, "WIN_VARIABLE"
         WINSCP_COPYRIGHT, "Copyright © 2000-2016 Martin Prikryl"

+ 0 - 5
source/windows/ConsoleRunner.cpp

@@ -2013,11 +2013,6 @@ void __fastcall TConsoleRunner::ConfigurationChange(TObject * /*Sender*/)
   }
 }
 //---------------------------------------------------------------------------
-void __fastcall LoadScriptFromFile(UnicodeString FileName, TStrings * Lines)
-{
-  Lines->LoadFromFile(FileName, TEncoding::UTF8);
-}
-//---------------------------------------------------------------------------
 void __fastcall ConsolePrintLine(TConsole * Console, const UnicodeString & Str)
 {
   Console->Print(Str + L"\n");

+ 1 - 5
source/windows/Setup.cpp

@@ -916,11 +916,7 @@ static bool __fastcall DoQueryUpdates(TUpdatesConfiguration & Updates, bool Coll
     UnicodeString Name = CutToChar(Line, L'=', false);
     if (AnsiSameText(Name, "Version"))
     {
-      int MajorVer = StrToInt(CutToChar(Line, L'.', false));
-      int MinorVer = StrToInt(CutToChar(Line, L'.', false));
-      int Release = StrToInt(CutToChar(Line, L'.', false));
-      int Build = StrToInt(CutToChar(Line, L'.', false));
-      int NewVersion = CalculateCompoundVersion(MajorVer, MinorVer, Release, Build);
+      int NewVersion = StrToCompoundVersion(Line);
       Changed |= (NewVersion != PrevResults.Version);
       if (NewVersion <= CurrentCompoundVer)
       {

+ 2 - 1
source/windows/UserInterface.cpp

@@ -514,10 +514,11 @@ void __fastcall LoadToolbarsLayoutStr(TComponent * OwnerComponent, UnicodeString
   }
 }
 //---------------------------------------------------------------------------
-void __fastcall AddMenuSeparator(TTBCustomItem * Menu)
+TTBXSeparatorItem * __fastcall AddMenuSeparator(TTBCustomItem * Menu)
 {
   TTBXSeparatorItem * Item = new TTBXSeparatorItem(Menu);
   Menu->Add(Item);
+  return Item;
 }
 //---------------------------------------------------------------------------
 static TComponent * LastPopupComponent = NULL;

+ 365 - 6
source/windows/WinConfiguration.cpp

@@ -18,6 +18,9 @@
 #include <InitGUID.h>
 #include <DragExt.h>
 #include <Math.hpp>
+#include <StrUtils.hpp>
+#include <Generics.Defaults.hpp>
+#include "FileInfo.h"
 //---------------------------------------------------------------------------
 #pragma package(smart_init)
 //---------------------------------------------------------------------------
@@ -433,6 +436,7 @@ __fastcall TWinConfiguration::TWinConfiguration(): TCustomWinConfiguration()
   FDDExtInstalled = -1;
   FBookmarks = new TBookmarks();
   FCustomCommandList = new TCustomCommandList();
+  FExtensionList = new TCustomCommandList();
   FEditorList = new TEditorList();
   FDefaultUpdatesPeriod = 0;
   FDontDecryptPasswords = 0;
@@ -460,6 +464,7 @@ __fastcall TWinConfiguration::~TWinConfiguration()
 
   delete FBookmarks;
   delete FCustomCommandList;
+  delete FExtensionList;
   delete FEditorList;
 }
 //---------------------------------------------------------------------------
@@ -553,6 +558,7 @@ void __fastcall TWinConfiguration::Default()
   FTipsSeen = L"";
   FTipsShown = Now();
   FRunsSinceLastTip = 0;
+  FExtensionsDeleted = L"";
 
   HonorDrivePolicy = true;
 
@@ -951,6 +957,8 @@ THierarchicalStorage * TWinConfiguration::CreateScpStorage(bool & SessionList)
     KEY(Integer,  RunsSinceLastTip); \
     KEY(Bool,     HonorDrivePolicy); \
     KEY(Integer,  LastMachineInstallations); \
+    KEYEX(String, FExtensionsDeleted, L"ExtensionsDeleted"); \
+    KEYEX(String, FExtensionsOrder, L"ExtensionsOrder"); \
   ); \
   BLOCK(L"Interface\\Editor", CANCREATE, \
     KEYEX(String,   Editor.Font.FontName, L"FontName2"); \
@@ -1174,6 +1182,85 @@ void __fastcall TWinConfiguration::LoadFrom(THierarchicalStorage * Storage)
   AddVersionToHistory();
 }
 //---------------------------------------------------------------------------
+void __fastcall TWinConfiguration::DoLoadExtensionList(const UnicodeString & Path, const UnicodeString & PathId, TStringList * DeletedExtensions)
+{
+  TSearchRecChecked SearchRec;
+  int FindAttrs = faReadOnly | faArchive;
+  if (FindFirstUnchecked(IncludeTrailingBackslash(Path) + L"*.*", FindAttrs, SearchRec) == 0)
+  {
+    try
+    {
+      const UnicodeString Ext(UpperCase(".WinSCPextension"));
+      do
+      {
+        int P = Pos(Ext, UpperCase(SearchRec.Name));
+        // Ends with Ext or there's another extension after it
+        if ((P > 1) &&
+            ((SearchRec.Name.Length() == (P + Ext.Length() - 1)) || (SearchRec.Name[P + Ext.Length()] == L'.')))
+        {
+          UnicodeString Id = IncludeTrailingBackslash(PathId) + SearchRec.Name.SubString(1, P - 1);
+          if (DeletedExtensions->IndexOf(Id) < 0)
+          {
+            std::unique_ptr<TCustomCommandType> CustomCommand(new TCustomCommandType());
+            CustomCommand->Id = Id;
+
+            try
+            {
+              CustomCommand->LoadExtension(IncludeTrailingBackslash(Path) + SearchRec.Name);
+
+              int Index = FExtensionList->FindIndex(CustomCommand->Name, false);
+              if (Index >= 0)
+              {
+                FExtensionList->Delete(Index);
+              }
+
+              FExtensionList->Add(CustomCommand.release());
+            }
+            catch (...)
+            {
+              // skip invalid extension files
+            }
+          }
+        }
+      }
+      while (FindNextChecked(SearchRec) == 0);
+    }
+    __finally
+    {
+      FindClose(SearchRec);
+    }
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall ParseExtensionList(TStrings * Strings, UnicodeString S)
+{
+  while (!S.IsEmpty())
+  {
+    UnicodeString Extension = CutToChar(S, L'|', false);
+    Strings->Add(Extension);
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TWinConfiguration::LoadExtensionList()
+{
+  FExtensionList->Clear();
+
+  std::unique_ptr<TStringList> DeletedExtensions(CreateSortedStringList());
+  ParseExtensionList(DeletedExtensions.get(), FExtensionsDeleted);
+
+  const UnicodeString ExtensionsSubFolder(L"Extensions");
+  UnicodeString ExeParentPath = ExcludeTrailingBackslash(ExtractFilePath(Application->ExeName));
+  DoLoadExtensionList(ExeParentPath, L"common", DeletedExtensions.get());
+  UnicodeString CommonExtensions = IncludeTrailingBackslash(ExeParentPath) + ExtensionsSubFolder;
+  DoLoadExtensionList(CommonExtensions, L"commonext", DeletedExtensions.get());
+  UnicodeString UserExtensions = IncludeTrailingBackslash(GetShellFolderPath(CSIDL_APPDATA)) + L"WinSCP\\" + ExtensionsSubFolder;
+  DoLoadExtensionList(UserExtensions, L"userext", DeletedExtensions.get());
+
+  std::unique_ptr<TStringList> OrderedExtensions(new TStringList());
+  ParseExtensionList(OrderedExtensions.get(), FExtensionsOrder);
+  FExtensionList->SortBy(OrderedExtensions.get());
+}
+//---------------------------------------------------------------------------
 void __fastcall TWinConfiguration::LoadData(THierarchicalStorage * Storage)
 {
   TCustomWinConfiguration::LoadData(Storage);
@@ -1185,6 +1272,9 @@ void __fastcall TWinConfiguration::LoadData(THierarchicalStorage * Storage)
   #pragma warn +eas
   #undef KEYEX
 
+  // Load after the ExtensionsDeleted and ExtensionsOrder
+  LoadExtensionList();
+
   // to reflect changes to PanelFont
   UpdateIconFont();
 
@@ -1998,6 +2088,42 @@ void __fastcall TWinConfiguration::SetCustomCommandList(TCustomCommandList * val
   }
 }
 //---------------------------------------------------------------------------
+void __fastcall TWinConfiguration::SetExtensionList(TCustomCommandList * value)
+{
+  if (!ExtensionList->Equals(value))
+  {
+    for (int Index = 0; Index < ExtensionList->Count; Index++)
+    {
+      const TCustomCommandType * CustomCommand = ExtensionList->Commands[Index];
+      if (!value->Find(CustomCommand->Name))
+      {
+        if (FileExists(CustomCommand->FileName) &&
+            !DeleteFile(CustomCommand->FileName))
+        {
+          AddToList(FExtensionsDeleted, CustomCommand->Id, L"|");
+        }
+      }
+    }
+
+    FExtensionsOrder = L"";
+    for (int Index = 0; Index < value->Count; Index++)
+    {
+      const TCustomCommandType * CustomCommand = value->Commands[Index];
+      AddToList(FExtensionsOrder, CustomCommand->Id, L"|");
+    }
+
+    FExtensionList->Assign(value);
+
+    Changed();
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TWinConfiguration::CustomCommandShortCuts(TShortCuts & ShortCuts) const
+{
+  CustomCommandList->ShortCuts(ShortCuts);
+  ExtensionList->ShortCuts(ShortCuts);
+}
+//---------------------------------------------------------------------------
 void __fastcall TWinConfiguration::SetBookmarks(UnicodeString Key,
   TBookmarkList * value)
 {
@@ -2361,6 +2487,7 @@ void __fastcall TWinConfiguration::UpdateStaticUsage()
 
   Usage->Set(L"Interface", Interface);
   Usage->Set(L"CustomCommandsCount", (FCustomCommandsDefaults ? 0 : FCustomCommandList->Count));
+  Usage->Set(L"ExtensionsCount", (FExtensionList->Count));
   Usage->Set(L"UsingLocationProfiles", UseLocationProfiles);
   Usage->Set(L"UsingMasterPassword", UseMasterPassword);
   Usage->Set(L"UsingAutoSaveWorkspace", AutoSaveWorkspace);
@@ -2425,7 +2552,9 @@ __fastcall TCustomCommandType::TCustomCommandType(const TCustomCommandType & Oth
   FName(Other.FName),
   FCommand(Other.FCommand),
   FParams(Other.FParams),
-  FShortCut(Other.FShortCut)
+  FShortCut(Other.FShortCut),
+  FId(Other.FId),
+  FFileName(Other.FFileName)
 {
 }
 //---------------------------------------------------------------------------
@@ -2435,6 +2564,8 @@ TCustomCommandType & TCustomCommandType::operator=(const TCustomCommandType & Ot
   FCommand = Other.FCommand;
   FParams = Other.FParams;
   FShortCut = Other.FShortCut;
+  FId = Other.FId;
+  FFileName = Other.FFileName;
   return *this;
 }
 //---------------------------------------------------------------------------
@@ -2444,7 +2575,179 @@ bool __fastcall TCustomCommandType::Equals(const TCustomCommandType * Other) con
     (FName == Other->FName) &&
     (FCommand == Other->FCommand) &&
     (FParams == Other->FParams) &&
-    (FShortCut == Other->FShortCut);
+    (FShortCut == Other->FShortCut) &&
+    (FId == Other->FId) &&
+    (FFileName == Other->FFileName);
+}
+//---------------------------------------------------------------------------
+const UnicodeString ExtensionNameDirective(L"name");
+const UnicodeString ExtensionCommandDirective(L"command");
+const wchar_t ExtensionMark = L'@';
+//---------------------------------------------------------------------------
+void __fastcall TCustomCommandType::LoadExtension(const UnicodeString & Path)
+{
+  std::unique_ptr<TStringList> Lines(new TStringList());
+  LoadScriptFromFile(Path, Lines.get());
+
+  Params = ccLocal;
+  FileName = Path;
+
+  for (int Index = 0; Index < Lines->Count; Index++)
+  {
+    UnicodeString Line = Lines->Strings[Index].Trim();
+    if (!Line.IsEmpty())
+    {
+      bool IsComment = false;
+      if (StartsText(ExtensionMark, Line))
+      {
+        IsComment = true;
+      }
+      else if (StartsText(L"rem ", Line))
+      {
+        IsComment = true;
+        Line.Delete(1, 4);
+      }
+      else if (StartsText(L"#", Line) || StartsText(L";", Line) || StartsText(L"'", Line))
+      {
+        IsComment = true;
+        Line.Delete(1, 1);
+      }
+      else if (StartsText(L"//", Line))
+      {
+        IsComment = true;
+        Line.Delete(1, 2);
+      }
+
+      if (IsComment)
+      {
+        Line = Line.Trim();
+        int P;
+        if (!Line.IsEmpty() && (Line[1] == ExtensionMark) && ((P = Pos(L" ", Line)) >= 2))
+        {
+          UnicodeString Key = Line.SubString(2, P - 2).LowerCase();
+          UnicodeString Directive = UnicodeString(ExtensionMark) + Key;
+          UnicodeString Value = Line.SubString(P + 1, Line.Length() - P).Trim();
+          if (Key == ExtensionNameDirective)
+          {
+            Name = Value;
+          }
+          else if (Key == ExtensionCommandDirective)
+          {
+            Value = ReplaceStr(Value, L"%EXTENSION_PATH%", Path);
+            Command = Value;
+          }
+          else if (Key == L"require")
+          {
+            UnicodeString DependencyVersion = Value;
+            UnicodeString Dependency = CutToChar(Value, L' ', true).LowerCase();
+            bool Failed = false;
+            if (Dependency == L"winscp")
+            {
+              int Version = StrToCompoundVersion(Value);
+              Failed = (Version > WinConfiguration->CompoundVersion);
+            }
+            else
+            {
+              Failed = true;
+            }
+
+            if (Failed)
+            {
+              throw Exception(FMTLOAD(EXTENSION_DEPENDENCY_ERROR, (DependencyVersion)));
+            }
+          }
+          else if (Key == L"side")
+          {
+            if (SameText(Value, L"Local"))
+            {
+              Params |= ccLocal;
+            }
+            else if (SameText(Value, L"Remote"))
+            {
+              Params &= ~ccLocal;
+            }
+            else
+            {
+              throw Exception(FMTLOAD(EXTENSION_DIRECTIVE_ERROR, (Directive, Value)));
+            }
+          }
+          else if (Key == L"flag")
+          {
+            if (SameText(Value, L"ApplyToDirectories"))
+            {
+              Params |= ccApplyToDirectories;
+            }
+            else if (SameText(Value, L"Recursive"))
+            {
+              Params |= ccRecursive;
+            }
+            else if (SameText(Value, L"ShowResults"))
+            {
+              Params |= ccShowResults;
+            }
+            else if (SameText(Value, L"CopyResults"))
+            {
+              Params |= ccCopyResults;
+            }
+            else if (SameText(Value, L"RemoteFiles"))
+            {
+              Params |= ccRemoteFiles;
+            }
+            else
+            {
+              throw Exception(FMTLOAD(EXTENSION_DIRECTIVE_ERROR, (Directive, Value)));
+            }
+          }
+          else if (Key == L"shortcut")
+          {
+            TShortCut AShortCut = TextToShortCut(Value);
+            if (IsCustomShortCut(AShortCut))
+            {
+              ShortCut = AShortCut;
+            }
+            else
+            {
+              throw Exception(FMTLOAD(EXTENSION_DIRECTIVE_ERROR, (Key, Value)));
+            }
+          }
+          else if (Key == L"description")
+          {
+            // noop
+          }
+          else if (Key == L"author")
+          {
+            // noop
+          }
+          else if (Key == L"version")
+          {
+            // noop
+          }
+          else if (Key == L"homepage")
+          {
+            // noop
+          }
+          else
+          {
+            // noop
+          }
+        }
+      }
+      else
+      {
+        break;
+      }
+    }
+  }
+
+  if (Name.IsEmpty())
+  {
+    throw Exception(FMTLOAD(EXTENSION_DIRECTIVE_MISSING, (UnicodeString(ExtensionMark) + ExtensionNameDirective)));
+  }
+
+  if (Command.IsEmpty())
+  {
+    throw Exception(FMTLOAD(EXTENSION_DIRECTIVE_MISSING, (UnicodeString(ExtensionMark) + ExtensionCommandDirective)));
+  }
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
@@ -2616,6 +2919,52 @@ void __fastcall TCustomCommandList::Delete(int Index)
   Modify();
 }
 //---------------------------------------------------------------------------
+class TCustomCommandCompareFunc : public TCppInterfacedObject<TListSortCompareFunc>
+{
+public:
+  TCustomCommandCompareFunc(TStrings * Ids)
+  {
+    FIds = Ids;
+  }
+
+  virtual int __fastcall Invoke(void * Item1, void * Item2)
+  {
+    TCustomCommandType * CustomCommand1 = static_cast<TCustomCommandType *>(Item1);
+    TCustomCommandType * CustomCommand2 = static_cast<TCustomCommandType *>(Item2);
+    int Index1 = FIds->IndexOf(CustomCommand1->Id);
+    int Index2 = FIds->IndexOf(CustomCommand2->Id);
+    int Result;
+    // new items to the end
+    if ((Index1 < 0) && (Index2 >= 0))
+    {
+      Result = 1;
+    }
+    else if ((Index2 < 0) && (Index1 >= 0))
+    {
+      Result = -1;
+    }
+    // fallback to comparing by name
+    else if ((Index1 < 0) && (Index2 < 0))
+    {
+      Result = TComparer__1<UnicodeString>::Default()->Compare(CustomCommand1->Name, CustomCommand2->Name);
+    }
+    else
+    {
+      Result = TComparer__1<int>::Default()->Compare(Index1, Index2);
+    }
+    return Result;
+  }
+
+private:
+  TStrings * FIds;
+};
+//---------------------------------------------------------------------------
+void __fastcall TCustomCommandList::SortBy(TStrings * Ids)
+{
+  TCustomCommandCompareFunc * Func = new TCustomCommandCompareFunc(Ids);
+  FCommands->SortList(Func);
+}
+//---------------------------------------------------------------------------
 int __fastcall TCustomCommandList::GetCount() const
 {
   return FCommands->Count;
@@ -2653,16 +3002,26 @@ void __fastcall TCustomCommandList::Assign(const TCustomCommandList * Other)
   Modify();
 }
 //---------------------------------------------------------------------------
-const TCustomCommandType * TCustomCommandList::Find(const UnicodeString Name) const
+int TCustomCommandList::FindIndex(const UnicodeString & Name, bool CaseSensitive) const
 {
   for (int Index = 0; Index < FCommands->Count; Index++)
   {
-    if (Commands[Index]->Name == Name)
+    if (CaseSensitive && (Commands[Index]->Name == Name))
     {
-      return Commands[Index];
+      return Index;
+    }
+    if (!CaseSensitive && SameText(Commands[Index]->Name, Name))
+    {
+      return Index;
     }
   }
-  return NULL;
+  return -1;
+}
+//---------------------------------------------------------------------------
+const TCustomCommandType * TCustomCommandList::Find(const UnicodeString Name) const
+{
+  int Index = FindIndex(Name, true);
+  return (Index >= 0) ? Commands[Index] : NULL;
 }
 //---------------------------------------------------------------------------
 const TCustomCommandType * TCustomCommandList::Find(TShortCut ShortCut) const

+ 16 - 0
source/windows/WinConfiguration.h

@@ -364,6 +364,9 @@ private:
   UnicodeString FTemporaryKeyFile;
   TBookmarks * FBookmarks;
   TCustomCommandList * FCustomCommandList;
+  TCustomCommandList * FExtensionList;
+  UnicodeString FExtensionsDeleted;
+  UnicodeString FExtensionsOrder;
   bool FCustomCommandsDefaults;
   TEditorConfiguration FEditor;
   TQueueViewConfiguration FQueueView;
@@ -473,6 +476,7 @@ private:
   void __fastcall SetQueueView(TQueueViewConfiguration value);
   void __fastcall SetEnableQueueByDefault(bool value);
   void __fastcall SetCustomCommandList(TCustomCommandList * value);
+  void __fastcall SetExtensionList(TCustomCommandList * value);
   void __fastcall SetTemporaryDirectoryAppendSession(bool value);
   void __fastcall SetTemporaryDirectoryAppendPath(bool value);
   void __fastcall SetTemporaryDirectoryDeterministic(bool value);
@@ -561,6 +565,7 @@ protected:
   bool __fastcall CanWriteToStorage();
   bool __fastcall DoIsBeta(const UnicodeString & ReleaseType);
   void __fastcall AskForMasterPassword();
+  void __fastcall DoLoadExtensionList(const UnicodeString & Path, const UnicodeString & PathId, TStringList * DeletedExtensions);
 
 public:
   __fastcall TWinConfiguration();
@@ -591,6 +596,8 @@ public:
   void __fastcall UpdateJumpList();
   virtual void __fastcall UpdateStaticUsage();
   void __fastcall MinimizeToTrayOnce();
+  void __fastcall LoadExtensionList();
+  void __fastcall CustomCommandShortCuts(TShortCuts & ShortCuts) const;
 
   static void __fastcall RestoreFont(const TFontConfiguration & Configuration, TFont * Font);
   static void __fastcall StoreFont(TFont * Font, TFontConfiguration & Configuration);
@@ -639,6 +646,7 @@ public:
   __property bool DefaultDirIsHome = { read = FDefaultDirIsHome, write = SetDefaultDirIsHome };
   __property bool DisableOpenEdit = { read = FDisableOpenEdit };
   __property TCustomCommandList * CustomCommandList = { read = FCustomCommandList, write = SetCustomCommandList };
+  __property TCustomCommandList * ExtensionList = { read = FExtensionList, write = SetExtensionList };
   __property int DDDeleteDelay = { read = FDDDeleteDelay };
   __property bool TemporaryDirectoryAppendSession = { read = FTemporaryDirectoryAppendSession, write = SetTemporaryDirectoryAppendSession };
   __property bool TemporaryDirectoryAppendPath = { read = FTemporaryDirectoryAppendPath, write = SetTemporaryDirectoryAppendPath };
@@ -698,16 +706,22 @@ public:
   TCustomCommandType & operator=(const TCustomCommandType & Other);
   bool __fastcall Equals(const TCustomCommandType * Other) const;
 
+  void __fastcall LoadExtension(const UnicodeString & Path);
+
   __property UnicodeString Name = { read = FName, write = FName };
   __property UnicodeString Command = { read = FCommand, write = FCommand };
   __property int Params = { read = FParams, write = FParams };
   __property TShortCut ShortCut = { read = FShortCut, write = FShortCut };
+  __property UnicodeString Id = { read = FId, write = FId };
+  __property UnicodeString FileName = { read = FFileName, write = FFileName };
 
 private:
   UnicodeString FName;
   UnicodeString FCommand;
   int FParams;
   TShortCut FShortCut;
+  UnicodeString FId;
+  UnicodeString FFileName;
 };
 //---------------------------------------------------------------------------
 class TCustomCommandList
@@ -728,7 +742,9 @@ public:
   void __fastcall Change(int Index, TCustomCommandType * Command);
   void __fastcall Move(int CurIndex, int NewIndex);
   void __fastcall Delete(int Index);
+  void __fastcall SortBy(TStrings * Ids);
 
+  int FindIndex(const UnicodeString & Name, bool CaseSensitive) const;
   const TCustomCommandType * Find(const UnicodeString Name) const;
   const TCustomCommandType * Find(TShortCut ShortCut) const;
 

+ 2 - 1
source/windows/WinInterface.h

@@ -92,7 +92,8 @@ UnicodeString __fastcall GetToolbarsLayoutStr(TComponent * OwnerComponent);
 void __fastcall LoadToolbarsLayoutStr(TComponent * OwnerComponent, UnicodeString LayoutStr);
 
 namespace Tb2item { class TTBCustomItem; }
-void __fastcall AddMenuSeparator(Tb2item::TTBCustomItem * Menu);
+namespace Tbx { class TTBXSeparatorItem; }
+Tbx::TTBXSeparatorItem * __fastcall AddMenuSeparator(Tb2item::TTBCustomItem * Menu);
 void __fastcall AddMenuLabel(Tb2item::TTBCustomItem * Menu, const UnicodeString & Label);
 
 // windows\WinHelp.cpp