Explorar el Código

Search for Preferences options

Source commit: 1dbd33e717c5f59fe9c0ba9cc8b03299cc001697
Martin Prikryl hace 1 año
padre
commit
69b6a18593

+ 1 - 4
source/forms/Authenticate.cpp

@@ -257,10 +257,7 @@ void TAuthenticateForm::ExternalLabel(TLabel * Label)
 TList * __fastcall TAuthenticateForm::GeneratePrompt(
   TPromptKind Kind, const UnicodeString & Instructions, TStrings * Prompts)
 {
-  while (FPromptParent->ControlCount > 0)
-  {
-    delete FPromptParent->Controls[0];
-  }
+  DeleteChildren(FPromptParent);
   TList * Result = new TList;
 
   int Current = FPromptTop;

+ 1 - 14
source/forms/Custom.cpp

@@ -336,20 +336,7 @@ void __fastcall TCustomDialog::AddText(TLabel * Label)
   Label->Top = FPos;
   Label->ShowAccelChar = false;
 
-  TRect TextRect;
-  SetRect(&TextRect, 0, 0, Label->Width, 0);
-  DrawText(Label->Canvas->Handle, Label->Caption.c_str(), Label->Caption.Length() + 1, &TextRect,
-    DT_EXPANDTABS | DT_CALCRECT | DT_WORDBREAK | DT_NOPREFIX |
-    Label->DrawTextBiDiModeFlagsReadingOnly());
-  if (TextRect.Height() > Label->Height)
-  {
-    Label->Height = TextRect.Height();
-    Label->AutoSize = false;
-  }
-  else
-  {
-    Label->WordWrap = false;
-  }
+  AutoSizeLabel(Label);
 
   AdjustHeight(Label);
 }

+ 413 - 33
source/forms/Preferences.cpp

@@ -83,6 +83,7 @@ __fastcall TPreferencesDialog::TPreferencesDialog(
   FCustomCommandsHintItem = NULL;
   FAddedExtensions.reset(CreateSortedStringList());
   FCustomCommandOptions.reset(new TStringList());
+  FHideFocus = false;
   UseSystemSettings(this);
 
   FCustomCommandsScrollOnDragOver = new TListViewScrollOnDragOver(CustomCommandsView, true);
@@ -235,6 +236,22 @@ TTabSheet * __fastcall TPreferencesDialog::FindPageForTreeNode(TTreeNode * Node)
   return NULL;
 }
 //---------------------------------------------------------------------------
+TTreeNode * TPreferencesDialog::FindTreeNodeForPage(TTabSheet * Sheet)
+{
+  TTreeNode * Result = NULL;
+  for (int Index = 0; Index < NavigationTree->Items->Count; Index++)
+  {
+    TTreeNode * Node = NavigationTree->Items->Item[Index];
+    if (Node->SelectedIndex == Sheet->Tag)
+    {
+      Result = Node;
+      break;
+    }
+  }
+
+  return Result;
+}
+//---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::PrepareNavigationTree(TTreeView * Tree)
 {
   Tree->FullExpand();
@@ -1137,6 +1154,12 @@ TInterface __fastcall TPreferencesDialog::GetInterface()
   return CommanderInterfaceButton2->Checked ? ifCommander : ifExplorer;
 }
 //---------------------------------------------------------------------------
+void TPreferencesDialog::SetActivePage(TTabSheet * Page)
+{
+  PageControl->ActivePage = Page;
+  PageControlChange(NULL);
+}
+//---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::FormShow(TObject * /*Sender*/)
 {
   // Load only now, particularly so that we have handles allocated already.
@@ -1154,20 +1177,24 @@ void __fastcall TPreferencesDialog::FormShow(TObject * /*Sender*/)
 
   PrepareNavigationTree(NavigationTree);
 
+  UnicodeString SearchBanner = LoadStr(SEARCH_EDIT);
+  SearchEdit->Perform(EM_SETCUEBANNER, false, SearchBanner.c_str());
+
+  TTabSheet * InitialPage;
   switch (FPreferencesMode) {
-    case pmEditor: PageControl->ActivePage = EditorSheet; break;
-    case pmCustomCommands: PageControl->ActivePage = CustomCommandsSheet; break;
-    case pmQueue: PageControl->ActivePage = QueueSheet; break;
-    case pmLogging: PageControl->ActivePage = LogSheet; break;
-    case pmUpdates: PageControl->ActivePage = UpdatesSheet; break;
-    case pmPresets: PageControl->ActivePage = CopyParamListSheet; break;
-    case pmEditors: PageControl->ActivePage = EditorSheet; break;
-    case pmCommander: PageControl->ActivePage = CommanderSheet; break;
-    case pmEditorInternal: PageControl->ActivePage = EditorInternalSheet; break;
-    case pmFileColors: PageControl->ActivePage = FileColorsSheet; break;
-    default: PageControl->ActivePage = PreferencesSheet; break;
-  }
-  PageControlChange(NULL);
+    case pmEditor: InitialPage = EditorSheet; break;
+    case pmCustomCommands: InitialPage = CustomCommandsSheet; break;
+    case pmQueue: InitialPage = QueueSheet; break;
+    case pmLogging: InitialPage = LogSheet; break;
+    case pmUpdates: InitialPage = UpdatesSheet; break;
+    case pmPresets: InitialPage = CopyParamListSheet; break;
+    case pmEditors: InitialPage = EditorSheet; break;
+    case pmCommander: InitialPage = CommanderSheet; break;
+    case pmEditorInternal: InitialPage = EditorInternalSheet; break;
+    case pmFileColors: InitialPage = FileColorsSheet; break;
+    default: InitialPage = PreferencesSheet; break;
+  }
+  SetActivePage(InitialPage);
   ActiveControl = NavigationTree;
 }
 //---------------------------------------------------------------------------
@@ -1243,6 +1270,13 @@ void __fastcall TPreferencesDialog::UpdateControls()
 {
   if (FNoUpdate == 0)
   {
+    NavigationTree->HideSelection = (FSearchResults.get() != NULL);
+    if ((SearchEdit->ButtonWidth != 0) != !SearchEdit->Text.IsEmpty()) // optimization
+    {
+      SearchEdit->ButtonWidth = SearchEdit->Text.IsEmpty() ? 0 : ScaleByTextHeight(SearchEdit, 19);
+    }
+    NavigationTree->TabStop = !SearchEdit->Focused() || (FSearchResults.get() == NULL) || (FSearchResults->Count == 0);
+
     EnableControl(DDTransferConfirmationCheck2, ConfirmTransferringCheck->Checked);
     EnableControl(BeepOnFinishAfterEdit, BeepOnFinishCheck->Checked);
     EnableControl(BeepOnFinishAfterText, BeepOnFinishCheck->Checked);
@@ -2162,28 +2196,25 @@ void __fastcall TPreferencesDialog::NavigationTreeChange(TObject * /*Sender*/,
 void __fastcall TPreferencesDialog::PageControlChange(TObject * /*Sender*/)
 {
   // this is probably only ever called from FormShow (explicitly)
-  bool Found = false;
   if (DebugAlwaysTrue(PageControl->ActivePage->Tag > 0))
   {
-    for (int Index = 0; Index < NavigationTree->Items->Count; Index++)
+    TTreeNode * Node = FindTreeNodeForPage(PageControl->ActivePage);
+    if (Node != NULL)
     {
-      if (NavigationTree->Items->Item[Index]->SelectedIndex ==
-            PageControl->ActivePage->Tag)
-      {
-        NavigationTree->Items->Item[Index]->Selected = true;
-        Found = true;
-      }
+      Node->Selected = true;
+      UpdateControls();
     }
   }
-
-  if (DebugAlwaysTrue(Found))
-  {
-    UpdateControls();
-  }
 }
 //---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::CMDialogKey(TWMKeyDown & Message)
 {
+  if ((Message.CharCode == VK_TAB) ||
+      ((Message.CharCode >= VK_LEFT) && (Message.CharCode >= VK_DOWN)))
+  {
+    FHideFocus = false;
+  }
+
   if (Message.CharCode == VK_TAB)
   {
     TShiftState Shift = KeyDataToShiftState(Message.KeyData);
@@ -2206,6 +2237,13 @@ void __fastcall TPreferencesDialog::CMDialogKey(TWMKeyDown & Message)
       return;
     }
   }
+  else if ((Message.CharCode == VK_ESCAPE) && (FSearchResults.get() != NULL))
+  {
+    SearchEdit->Clear();
+    NavigationTree->SetFocus();
+    Message.Result = 1;
+    return;
+  }
   TForm::Dispatch(&Message);
 }
 //---------------------------------------------------------------------------
@@ -2213,10 +2251,13 @@ void __fastcall TPreferencesDialog::WMHelp(TWMHelp & Message)
 {
   DebugAssert(Message.HelpInfo != NULL);
 
-  if (Message.HelpInfo->iContextType == HELPINFO_WINDOW)
+  TControl * Control = FindControl(static_cast<HWND>(Message.HelpInfo->hItemHandle));
+  // Invoke help for the active page (not for whole form), regardless of a focus
+  // (e.g. even if the focus is on a control outside pagecontrol).
+  // Except when the control has its own help (SearchEdit).
+  if (((Control == NULL) || Control->HelpKeyword.IsEmpty()) &&
+      (Message.HelpInfo->iContextType == HELPINFO_WINDOW))
   {
-    // invoke help for active page (not for whole form), regardless of focus
-    // (e.g. even if focus is on control outside pagecontrol)
     Message.HelpInfo->hItemHandle = PageControl->ActivePage->Handle;
   }
   TForm::Dispatch(&Message);
@@ -2239,6 +2280,17 @@ void TPreferencesDialog::WMActivate(TWMActivate & Message)
   TForm::Dispatch(&Message);
 }
 //---------------------------------------------------------------------------
+void TPreferencesDialog::CMFocusChanged(TMessage & Message)
+{
+  if (FHideFocus)
+  {
+    FHideFocus = false;
+    HideFocus(UIS_SET);
+  }
+  UpdateControls();
+  TForm::Dispatch(&Message);
+}
+//---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::Dispatch(void *Message)
 {
   TMessage * M = reinterpret_cast<TMessage*>(Message);
@@ -2259,6 +2311,10 @@ void __fastcall TPreferencesDialog::Dispatch(void *Message)
   {
     WMActivate(*((TWMActivate *)Message));
   }
+  else if (M->Msg == CM_FOCUSCHANGED)
+  {
+    CMFocusChanged(*M);
+  }
   else
   {
     TForm::Dispatch(Message);
@@ -2986,12 +3042,78 @@ void __fastcall TPreferencesDialog::CustomCommandsViewMouseMove(TObject * /*Send
   }
 }
 //---------------------------------------------------------------------------
+void TPreferencesDialog::HideFocus(int State)
+{
+  Perform(WM_CHANGEUISTATE, MAKEWPARAM(State, UISF_HIDEFOCUS), 0);
+}
+//---------------------------------------------------------------------------
+void TPreferencesDialog::SetFocusIfEnabled(TControl * Control)
+{
+  TWinControl * WinControl = dynamic_cast<TWinControl *>(Control);
+  if ((WinControl != NULL) && WinControl->Enabled)
+  {
+    WinControl->SetFocus();
+  }
+  else if (Control->Parent != NULL)
+  {
+    SetFocusIfEnabled(Control->Parent);
+  }
+}
+//---------------------------------------------------------------------------
+void TPreferencesDialog::FocusAndHighlightControl(TControl * Control, const UnicodeString & Text)
+{
+  bool HighlightInside = true;
+  TLabel * Label = dynamic_cast<TLabel *>(Control);
+  if ((Label != NULL) && (Label->FocusControl != NULL))
+  {
+    Control = Label->FocusControl;
+    HighlightInside = false;
+  }
+
+  TRadioButton * RadioButton = dynamic_cast<TRadioButton *>(Control);
+  // cannot focus non-checked radio as that would check it
+  if ((RadioButton != NULL) && !RadioButton->Checked)
+  {
+    TWinControl * RadioParent = RadioButton->Parent;
+    bool Found = false;
+    for (int Index = 0; Index < RadioParent->ControlCount; Index++)
+    {
+      TRadioButton * RadioSibling = dynamic_cast<TRadioButton *>(RadioParent->Controls[Index]);
+      if ((RadioSibling != NULL) && RadioSibling->Checked)
+      {
+        SetFocusIfEnabled(RadioSibling);
+        Found = true;
+        break;
+      }
+    }
+
+    if (!Found)
+    {
+      SetFocusIfEnabled(RadioParent);
+    }
+  }
+  else
+  {
+    SetFocusIfEnabled(Control);
+
+    if (HighlightInside)
+    {
+      TComboBox * ComboBox = dynamic_cast<TComboBox *>(Control);
+      if ((ComboBox != NULL) && (ComboBox->Style == csDropDownList) && (ComboBox->Text != Text))
+      {
+        ComboBox->DroppedDown = true;
+      }
+    }
+  }
+
+  FHideFocus = ((Perform(WM_QUERYUISTATE, 0, 0) & UISF_HIDEFOCUS) != 0);
+  HideFocus(UIS_CLEAR);
+}
+//---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::BackgroundConfirmationsLinkClick(TObject * /*Sender*/)
 {
-  PageControl->ActivePage = QueueSheet;
-  PageControlChange(NULL);
-  QueueNoConfirmationCheck->SetFocus();
-  QueueNoConfirmationCheck->Perform(WM_CHANGEUISTATE, MAKEWPARAM(UIS_CLEAR, UISF_HIDEFOCUS), 0);
+  SetActivePage(QueueSheet);
+  FocusAndHighlightControl(QueueNoConfirmationCheck, EmptyStr);
 }
 //---------------------------------------------------------------------------
 void __fastcall TPreferencesDialog::ConfigureCommandButtonClick(TObject * /*Sender*/)
@@ -3411,3 +3533,261 @@ void __fastcall TPreferencesDialog::ConfigureSshHostCAsButtonClick(TObject *)
   ExecuteShellChecked(Program, L"-host-ca");
 }
 //---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::SearchEditButtonClick(TObject *)
+{
+  SearchEdit->Clear();
+  SearchEdit->SetFocus();
+}
+//---------------------------------------------------------------------------
+int __fastcall TPreferencesDialog::CompareControlByLocation(void * Item1, void * Item2)
+{
+  TControl * Control1 = static_cast<TControl *>(Item1);
+  TControl * Control2 = static_cast<TControl *>(Item2);
+
+  int Result = Control1->Top - Control2->Top;
+  if (Result == 0)
+  {
+    Result = Control1->Left - Control2->Left;
+    if (Result == 0)
+    {
+      Result = reinterpret_cast<IntPtr>(Control1) - reinterpret_cast<IntPtr>(Control2);
+    }
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+void TPreferencesDialog::AddSearchResult(TStrings * Results, UnicodeString & Caption, TControl * Control, bool & NewResults)
+{
+  if (ContainsTextSemiCaseSensitive(Caption, SearchEdit->Text))
+  {
+    if ((FSearchResults.get() == NULL) ||
+        (FSearchResults->Count <= Results->Count) ||
+        (FSearchResults->Strings[Results->Count] != Caption) ||
+        (FSearchResults->Objects[Results->Count] != Control))
+    {
+      NewResults = true;
+    }
+
+    Results->AddObject(Caption, Control);
+  }
+}
+//---------------------------------------------------------------------------
+UnicodeString TPreferencesDialog::GetControlText(TControl * Control)
+{
+  UnicodeString Result;
+  TTabSheet * TabSheet = dynamic_cast<TTabSheet *>(Control);
+  if (TabSheet != NULL)
+  {
+    Result = DebugNotNull(FindTreeNodeForPage(TabSheet))->Text;
+  }
+  else
+  {
+    int TextLen = Control->GetTextLen();
+    Result.SetLength(TextLen);
+    Control->GetTextBuf(Result.c_str(), TextLen + 1);
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+void TPreferencesDialog::Search(TControl * Control, TStrings * Results, bool & NewResults)
+{
+  DebugAssert(Control != SearchSheet);
+
+  TCustomEdit * Edit = dynamic_cast<TCustomEdit *>(Control);
+  TComboBox * ComboBox = dynamic_cast<TComboBox *>(Control);
+  if ((Edit == NULL) &&
+      ((ComboBox == NULL) || (ComboBox->Style == csDropDownList)))
+  {
+    if (ComboBox != NULL)
+    {
+      for (int Index = 0; Index < ComboBox->Items->Count; Index++)
+      {
+        UnicodeString Item = ComboBox->Items->Strings[Index];
+        AddSearchResult(Results, Item, Control, NewResults);
+      }
+    }
+    else
+    {
+      UnicodeString Caption = StripHotkey(GetControlText(Control));
+      if (Caption.Length() >= 4) // do not search unit labels (like "s")
+      {
+        AddSearchResult(Results, Caption, Control, NewResults);
+      }
+    }
+
+    TWinControl * WinControl = dynamic_cast<TWinControl *>(Control);
+    if (WinControl != NULL)
+    {
+      std::unique_ptr<TList> Controls(new TList());
+      for (int Index = 0; Index < WinControl->ControlCount; Index++)
+      {
+        TControl * ChildControl = WinControl->Controls[Index];
+        if (ChildControl->Visible)
+        {
+          Controls->Add(ChildControl);
+        }
+      }
+
+      Controls->Sort(CompareControlByLocation);
+
+      for (int Index = 0; Index < Controls->Count; Index++)
+      {
+        TControl * ChildControl = static_cast<TControl *>(Controls->Items[Index]);
+        Search(ChildControl, Results, NewResults);
+      }
+    }
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::SearchResultClick(TObject * Sender)
+{
+  TStaticText * LinkLabel = DebugNotNull(dynamic_cast<TStaticText *>(Sender));
+  UnicodeString Caption = FSearchResults->Strings[LinkLabel->Tag];
+  TControl * Control = dynamic_cast<TControl *>(FSearchResults->Objects[LinkLabel->Tag]);
+
+  FSearchResults.reset(NULL);
+
+  TControl * Parent = Control;
+  TTabSheet * Page;
+  while ((Page = dynamic_cast<TTabSheet *>(Parent)) == NULL)
+  {
+    Parent = Parent->Parent;
+  }
+
+  SetActivePage(Page);
+  FocusAndHighlightControl(Control, Caption);
+}
+//---------------------------------------------------------------------------
+TWinControl * TPreferencesDialog::GetSearchParent(TControl * Control)
+{
+  TWinControl * Result;
+  TTabSheet * ParentTabSheet = dynamic_cast<TTabSheet *>(Control);
+  if (ParentTabSheet != NULL)
+  {
+    TTreeNode * TreeNode = FindTreeNodeForPage(ParentTabSheet);
+    Result = (TreeNode->Parent != NULL) ? FindPageForTreeNode(TreeNode->Parent) : NULL;
+  }
+  else
+  {
+    Result = Control->Parent;
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+void TPreferencesDialog::UpdateSearching(bool Searching)
+{
+  if (!Searching)
+  {
+    if (FSearchResults.get() != NULL)
+    {
+      NavigationTreeChange(NULL, NavigationTree->Selected);
+      FSearchResults.reset(NULL);
+      DeleteChildren(SearchGroup);
+      UpdateControls();
+    }
+  }
+  else
+  {
+    std::unique_ptr<TStrings> Results(new TStringList());
+    bool NewResults = false;
+
+    // search the pages in the tree order
+    for (int Index = 0; Index < NavigationTree->Items->Count; Index++)
+    {
+      Search(PageControl->Pages[Index], Results.get(), NewResults);
+    }
+
+    if (NewResults || (FSearchResults.get() == NULL) || (FSearchResults->Count > Results->Count))
+    {
+      FSearchResults.reset(Results.release());
+
+      DeleteChildren(SearchGroup);
+
+      int Top = PuttyPathLabel->Top;
+      int Left = PuttyPathLabel->Left;
+      if (FSearchResults->Count == 0)
+      {
+        TLabel * Label = new TLabel(this);
+        Label->Parent = SearchGroup;
+        Label->Top = Top;
+        Label->Left = Left;
+        Label->Caption = LoadStr(SEARCH_NO_RESULTS);
+      }
+      else
+      {
+        int Padding = ScaleByTextHeight(this, 8);
+        int Width = SearchGroup->ClientWidth - Left;
+        for (int Index = 0; Index < FSearchResults->Count; Index++)
+        {
+          UnicodeString Caption = FSearchResults->Strings[Index];
+          TControl * Control = dynamic_cast<TControl *>(FSearchResults->Objects[Index]);
+
+          UnicodeString Context;
+          TWinControl * Parent = GetSearchParent(Control);
+          while ((Parent != NULL) && DebugAlwaysTrue(Parent != PageControl))
+          {
+            UnicodeString ParentCaption = GetControlText(Parent);
+            if (!ParentCaption.IsEmpty())
+            {
+              Context = ParentCaption + L" " + ContextSeparator + L" " + Context;
+            }
+            Parent = GetSearchParent(Parent);
+          }
+
+          Context = DefaultStr(Context, ContextSeparator);
+
+          TLabel * ContextLabel = new TLabel(this);
+          ContextLabel->Parent = SearchGroup;
+          ContextLabel->Top = Top;
+          ContextLabel->Left = Left;
+          ContextLabel->ShowAccelChar = false;
+          ContextLabel->Caption = Context;
+
+          TStaticText * LinkLabel = new TStaticText(this);
+          LinkLabel->Parent = SearchGroup;
+          LinkLabel->Top = ContextLabel->Top + ContextLabel->Height;
+          LinkLabel->Left = Left;
+          LinkLabel->ShowAccelChar = false;
+          LinkLabel->Caption = Caption;
+          LinkLabel->Width = Width;
+          LinkLabel->Tag = Index;
+          LinkLabel->TabStop = true;
+          LinkLabel->OnClick = SearchResultClick;
+          AutoSizeLabel(LinkLabel);
+          if (LinkLabel->Top + LinkLabel->Height > SearchGroup->ClientHeight)
+          {
+            delete ContextLabel;
+            delete LinkLabel;
+            break;
+          }
+          LinkActionLabel(LinkLabel);
+
+          Top = LinkLabel->Top + LinkLabel->Height + Padding;
+        }
+      }
+    }
+
+    PageControl->ActivePage = SearchSheet;
+    UpdateControls();
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::SearchEditChangeEnter(TObject *)
+{
+  UpdateSearching(!SearchEdit->Text.IsEmpty());
+}
+//---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::NavigationTreeEnter(TObject *)
+{
+  UpdateSearching(false);
+}
+//---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::FormShortCut(TWMKey & Msg, bool & Handled)
+{
+  if ((Msg.CharCode == L'F') && KeyDataToShiftState(Msg.KeyData).Contains(ssCtrl))
+  {
+    SearchEdit->SetFocus();
+    Handled = true;
+  }
+}
+//---------------------------------------------------------------------------

+ 45 - 3
source/forms/Preferences.dfm

@@ -13,6 +13,7 @@ object PreferencesDialog: TPreferencesDialog
   OldCreateOrder = True
   Position = poOwnerFormCenter
   OnCloseQuery = FormCloseQuery
+  OnShortCut = FormShortCut
   OnShow = FormShow
   DesignSize = (
     545
@@ -547,6 +548,7 @@ object PreferencesDialog: TPreferencesDialog
               'ry)'#13#10'- keyboard shortcuts like in Norton Commander (and other si' +
               'milar programs as Total Commander, Midnight Commander...)'#13#10'- dra' +
               'g && drop to/from both panels'
+            FocusControl = CommanderInterfaceButton2
             WordWrap = True
             OnClick = CommanderClick
           end
@@ -560,6 +562,7 @@ object PreferencesDialog: TPreferencesDialog
             Caption = 
               '- only remote directory'#13#10'- keyboard shortcuts like in Windows Fi' +
               'le Explorer'#13#10'- drag && drop'
+            FocusControl = ExplorerInterfaceButton2
             WordWrap = True
             OnClick = ExplorerClick
           end
@@ -1429,6 +1432,7 @@ object PreferencesDialog: TPreferencesDialog
               'ile Explorer). Does not allow downloads to other destinations (Z' +
               'IP archives,  FTP, etc.). Uses drag&&drop shell extension, when ' +
               'available.'
+            FocusControl = DDFakeFileEnabledButton
             WordWrap = True
             OnClick = DDLabelClick
           end
@@ -1443,6 +1447,7 @@ object PreferencesDialog: TPreferencesDialog
               'Allows downloads to any destinations (regular folders, ZIP archi' +
               'ves,  FTP, etc.). Files are downloaded first to temporary folder' +
               ', from where they are delivered to the destination.'
+            FocusControl = DDFakeFileDisabledButton
             WordWrap = True
             OnClick = DDLabelClick
           end
@@ -1453,6 +1458,7 @@ object PreferencesDialog: TPreferencesDialog
             Height = 13
             Anchors = [akLeft, akTop, akRight]
             Caption = 'DragExtStatusLabel'
+            FocusControl = DDFakeFileEnabledButton
             ShowAccelChar = False
             OnClick = DDLabelClick
           end
@@ -1812,6 +1818,7 @@ object PreferencesDialog: TPreferencesDialog
             Anchors = [akLeft, akTop, akRight]
             AutoSize = False
             Caption = 'Specify where to temporarily store edited and downloaded files.'
+            FocusControl = DDSystemTemporaryDirectoryButton
             ShowAccelChar = False
             WordWrap = True
           end
@@ -3505,6 +3512,25 @@ object PreferencesDialog: TPreferencesDialog
           end
         end
       end
+      object SearchSheet: TTabSheet
+        HelpType = htKeyword
+        HelpKeyword = 'ui_pref_search'
+        Caption = 'SearchSheet'
+        ImageIndex = 24
+        TabVisible = False
+        DesignSize = (
+          405
+          422)
+        object SearchGroup: TGroupBox
+          Left = 8
+          Top = 8
+          Width = 389
+          Height = 414
+          Anchors = [akLeft, akTop, akRight, akBottom]
+          Caption = 'Search'
+          TabOrder = 0
+        end
+      end
     end
     object LeftPanel: TPanel
       Left = 0
@@ -3519,9 +3545,9 @@ object PreferencesDialog: TPreferencesDialog
         432)
       object NavigationTree: TTreeView
         Left = 8
-        Top = 9
+        Top = 36
         Width = 116
-        Height = 422
+        Height = 395
         Anchors = [akLeft, akTop, akRight, akBottom]
         DoubleBuffered = True
         HideSelection = False
@@ -3531,10 +3557,11 @@ object PreferencesDialog: TPreferencesDialog
         ReadOnly = True
         ShowButtons = False
         ShowRoot = False
-        TabOrder = 0
+        TabOrder = 1
         OnChange = NavigationTreeChange
         OnChanging = NavigationTreeChanging
         OnCollapsing = NavigationTreeCollapsing
+        OnEnter = NavigationTreeEnter
         Items.NodeData = {
           030B000000360000000000000001000000FFFFFFFFFFFFFFFF00000000000000
           0005000000010C45006E007600690072006F006E006D0065006E007400580032
@@ -3577,6 +3604,21 @@ object PreferencesDialog: TPreferencesDialog
           0058002E000000000000000F000000FFFFFFFFFFFFFFFF000000000000000000
           000000010855007000640061007400650073005800}
       end
+      object SearchEdit: TComboEdit
+        Left = 10
+        Top = 9
+        Width = 116
+        Height = 21
+        HelpType = htKeyword
+        HelpKeyword = 'ui_pref_search'
+        ButtonTabStop = False
+        ButtonCaption = #10005
+        ClickKey = 0
+        TabOrder = 0
+        OnButtonClick = SearchEditButtonClick
+        OnChange = SearchEditChangeEnter
+        OnEnter = SearchEditChangeEnter
+      end
     end
   end
   object HelpButton: TButton

+ 22 - 0
source/forms/Preferences.h

@@ -357,6 +357,9 @@ __published:
   TCheckBox *SessionTabCaptionTruncationCheck;
   TGroupBox *InternalEditorBehaviourGroup;
   TCheckBox *EditorDisableSmoothScrollCheck;
+  TComboEdit *SearchEdit;
+  TTabSheet *SearchSheet;
+  TGroupBox *SearchGroup;
   void __fastcall FormShow(TObject *Sender);
   void __fastcall ControlChange(TObject *Sender);
   void __fastcall EditorFontButtonClick(TObject *Sender);
@@ -471,6 +474,10 @@ __published:
   void __fastcall RemoveSshHostCAButtonClick(TObject *Sender);
   void __fastcall SshHostCAsFromPuTTYCheckClick(TObject *Sender);
   void __fastcall ConfigureSshHostCAsButtonClick(TObject *Sender);
+  void __fastcall SearchEditButtonClick(TObject *Sender);
+  void __fastcall SearchEditChangeEnter(TObject *Sender);
+  void __fastcall NavigationTreeEnter(TObject *Sender);
+  void __fastcall FormShortCut(TWMKey &Msg, bool &Handled);
 
 private:
   TPreferencesMode FPreferencesMode;
@@ -504,6 +511,8 @@ private:
   UnicodeString FCustomIniFileStorageName;
   TFileColorData::TList FFileColors;
   TSshHostCA::TList FSshHostCAPlainList;
+  std::unique_ptr<TStrings> FSearchResults;
+  bool FHideFocus;
   void __fastcall CMDialogKey(TWMKeyDown & Message);
   void __fastcall WMHelp(TWMHelp & Message);
   void __fastcall CMDpiChanged(TMessage & Message);
@@ -522,6 +531,18 @@ private:
   UnicodeString __fastcall GetSessionKey();
   void __fastcall ExtensionHttpError(THttp * Sender, int Status, const UnicodeString & Message);
   const TSshHostCA::TList & GetSshHostCAPlainList();
+  void UpdateSearching(bool ASearching);
+  void Search(TControl * Control, TStrings * Results, bool & NewResults);
+  void __fastcall SearchResultClick(TObject * Sender);
+  void SetActivePage(TTabSheet * Page);
+  void FocusAndHighlightControl(TControl * Control, const UnicodeString & Text);
+  void CMFocusChanged(TMessage & Message);
+  void HideFocus(int State);
+  static int __fastcall CompareControlByLocation(void * Item1, void * Item2);
+  void AddSearchResult(TStrings * Results, UnicodeString & Caption, TControl * Control, bool & NewResults);
+  void SetFocusIfEnabled(TControl * Control);
+  UnicodeString GetControlText(TControl * Control);
+  TWinControl * GetSearchParent(TControl * Control);
 public:
   virtual __fastcall ~TPreferencesDialog();
   bool __fastcall Execute(TPreferencesDialogData * DialogData);
@@ -542,6 +563,7 @@ protected:
   TListViewScrollOnDragOver * __fastcall ScrollOnDragOver(TObject * ListView);
   void __fastcall LoadLanguages();
   TTabSheet * __fastcall FindPageForTreeNode(TTreeNode * Node);
+  TTreeNode * FindTreeNodeForPage(TTabSheet * Sheet);
   bool __fastcall CanSetMasterPassword();
   void __fastcall ChangeMasterPassword(UnicodeString Message);
   void __fastcall MasterPasswordChanged(

+ 38 - 3
source/packages/my/ComboEdit.pas

@@ -14,6 +14,7 @@ const
   scAltDown = scAlt + vk_Down;
   scCtrlEnter = scCtrl + vk_Return;
   DefEditBtnWidth = 25;
+  DefButtonCaption = '...';
 
 resourcestring
   SBrowse = 'Browse';
@@ -39,9 +40,14 @@ type
     function GetTextHeight: Integer;
     function GetButtonWidth: Integer;
     procedure SetButtonWidth(Value: Integer);
+    function BtnWidthStored: Boolean;
+    function GetButtonCaption: string;
+    function ButtonCaptionStored: Boolean;
+    procedure SetButtonCaption(Value: string);
     function GetButtonHint: string;
     procedure SetButtonHint(const Value: string);
-    function BtnWidthStored: Boolean;
+    function GetButtonTabStop: Boolean;
+    procedure SetButtonTabStop(Value: Boolean);
     procedure CMEnabledChanged(var Message: TMessage); message CM_ENABLEDCHANGED;
     procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED;
     procedure CNCtlColor(var Message: TMessage); message CN_CTLCOLOREDIT;
@@ -57,7 +63,9 @@ type
       default scAltDown;
     property ButtonWidth: Integer read GetButtonWidth write SetButtonWidth
       stored BtnWidthStored;
+    property ButtonCaption: string read GetButtonCaption write SetButtonCaption stored ButtonCaptionStored;
     property ButtonHint: string read GetButtonHint write SetButtonHint;
+    property ButtonTabStop: Boolean read GetButtonTabStop write SetButtonTabStop default True;
     property OnButtonClick: TNotifyEvent read FOnButtonClick write FOnButtonClick;
   public
     constructor Create(AOwner: TComponent); override;
@@ -70,6 +78,8 @@ type
   published
     property AutoSelect;
     property ButtonHint;
+    property ButtonTabStop;
+    property ButtonCaption;
     property BorderStyle;
     property CharCase;
     property ClickKey;
@@ -404,7 +414,7 @@ begin
     SetBounds(0, 0, FBtnControl.Width, FBtnControl.Height);
     ControlStyle := ControlStyle + [csReplicatable];
     ParentShowHint := True;
-    Caption := '...';
+    Caption := DefButtonCaption;
     Visible := True;
     Parent := FBtnControl;
     OnClick := EditButtonClick;
@@ -462,11 +472,26 @@ begin
       FButton.Width := Value;
       with FButton do
         ControlStyle := ControlStyle - [csFixedWidth];
-      if HandleAllocated then RecreateWnd;
+      if HandleAllocated then UpdateBtnBounds;
     end;
   end;
 end;
 
+function TCustomComboEdit.GetButtonCaption: string;
+begin
+  Result := FButton.Caption;
+end;
+
+procedure TCustomComboEdit.SetButtonCaption(Value: string);
+begin
+  FButton.Caption := Value;
+end;
+
+function TCustomComboEdit.ButtonCaptionStored: Boolean;
+begin
+  Result := (FButton.Caption <> DefButtonCaption);
+end;
+
 function TCustomComboEdit.GetButtonHint: string;
 begin
   Result := FButton.Hint;
@@ -477,6 +502,16 @@ begin
   FButton.Hint := Value;
 end;
 
+function TCustomComboEdit.GetButtonTabStop: Boolean;
+begin
+  Result := FButton.TabStop;
+end;
+
+procedure TCustomComboEdit.SetButtonTabStop(Value: Boolean);
+begin
+  FButton.TabStop := Value;
+end;
+
 procedure TCustomComboEdit.SetEditRect;
 var
   RMargin: Integer;

+ 2 - 0
source/resource/TextsWin.h

@@ -693,6 +693,8 @@
 #define LOGIN_NOT_SHOWING_AGAIN 6206
 #define IMPORT_INI_TITLE        6207
 #define INC_SEARCH_TYPE         6208
+#define SEARCH_EDIT             6209
+#define SEARCH_NO_RESULTS       6210
 
 // 2xxx is reserved for TextsFileZilla.h
 

+ 2 - 0
source/resource/TextsWin1.rc

@@ -698,6 +698,8 @@ BEGIN
         LOGIN_NOT_SHOWING_AGAIN, "**Stop showing Login dialog automatically?** Please confirm if you really want to WinSCP stop showing Login dialog automatically on startup and when the last session is closed.\n\nIf you change your mind later, you can revert this in Preferences on Environment > Window page.\n\nTo open the Login dialog manually, go to Tabs > New Tab [> Remote Tab] or use corresponding toolbar button."
         IMPORT_INI_TITLE, "Select file to import sites from"
         INC_SEARCH_TYPE, "(start typing)"
+        SEARCH_EDIT, "Search"
+        SEARCH_NO_RESULTS, "No search results found."
 
         WIN_VARIABLE_STRINGS, "WIN_VARIABLE"
         WINSCP_COPYRIGHT, "Copyright © 2000–2024 Martin Prikryl"

+ 40 - 1
source/windows/VCLCommon.cpp

@@ -27,7 +27,8 @@
 #include <WinApi.h>
 #include <vssym32.h>
 //---------------------------------------------------------------------------
-const UnicodeString LinkAppLabelMark(TraceInitStr(L" \x00BB"));
+const UnicodeString ContextSeparator(TraceInitStr(L"\x00BB"));
+const UnicodeString LinkAppLabelMark(TraceInitStr(UnicodeString(L" ") + ContextSeparator));
 //---------------------------------------------------------------------------
 #pragma package(smart_init)
 //---------------------------------------------------------------------------
@@ -2972,6 +2973,36 @@ void AutoSizeButton(TButton * Button)
   }
 }
 //---------------------------------------------------------------------------
+template<class T>
+bool DoAutoSizeLabel(T * Label, TCanvas * Canvas)
+{
+  TRect TextRect;
+  SetRect(&TextRect, 0, 0, Label->Width, 0);
+  DrawText(Canvas->Handle, Label->Caption.c_str(), Label->Caption.Length() + 1, &TextRect,
+    DT_EXPANDTABS | DT_CALCRECT | DT_WORDBREAK | DT_NOPREFIX |
+    Label->DrawTextBiDiModeFlagsReadingOnly());
+  bool Result = (TextRect.Height() > Label->Height);
+  if (Result)
+  {
+    Label->Height = TextRect.Height();
+    Label->AutoSize = false;
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+void AutoSizeLabel(TLabel * Label)
+{
+  if (!DoAutoSizeLabel(Label, Label->Canvas))
+  {
+    Label->WordWrap = false;
+  }
+}
+//---------------------------------------------------------------------------
+void AutoSizeLabel(TStaticText * Label)
+{
+  std::unique_ptr<TCanvas> Canvas(CreateControlCanvas(Label));
+  DoAutoSizeLabel(Label, Canvas.get());
+}//---------------------------------------------------------------------------
 void GiveTBItemPriority(TTBCustomItem * Item)
 {
   DebugAssert(Item->GetTopComponent() != NULL);
@@ -2984,3 +3015,11 @@ void GiveTBItemPriority(TTBCustomItem * Item)
     ToolbarComponent->View->GivePriority(Viewer);
   }
 }
+//---------------------------------------------------------------------------
+void DeleteChildren(TWinControl * Control)
+{
+  while (Control->ControlCount > 0)
+  {
+    delete Control->Controls[0];
+  }
+}

+ 4 - 0
source/windows/VCLCommon.h

@@ -9,6 +9,7 @@
 #include <HistoryComboBox.hpp>
 //---------------------------------------------------------------------------
 const TColor LinkColor = clBlue;
+extern const UnicodeString ContextSeparator;
 //---------------------------------------------------------------------------
 void __fastcall FixListColumnWidth(TListView * TListView, int Index);
 void __fastcall AutoSizeListColumnsWidth(TListView * ListView, int ColumnToShrinkIndex = -1);
@@ -98,5 +99,8 @@ TCanvas * CreateControlCanvas(TControl * Control);
 void AutoSizeButton(TButton * Button);
 namespace Tb2item { class TTBCustomItem; }
 void GiveTBItemPriority(Tb2item::TTBCustomItem * Item);
+void DeleteChildren(TWinControl * Control);
+void AutoSizeLabel(TLabel * Label);
+void AutoSizeLabel(TStaticText * Label);
 //---------------------------------------------------------------------------
 #endif  // VCLCommonH