浏览代码

Issue 912 – Thumbnail view in file panel (local)

https://winscp.net/tracker/912

Source commit: 4326a2efd8333867e5697a63f320f4cdf919708b
Martin Prikryl 1 年之前
父节点
当前提交
f880c68e23

+ 1 - 0
source/components/UnixDirView.h

@@ -159,6 +159,7 @@ __published:
   __property MultiSelect;
   __property TNotifyEvent OnDisplayProperties = { read = FOnDisplayProperties, write = FOnDisplayProperties };
   __property ReadOnly;
+  __property DirViewStyle;
   __property TNotifyEvent OnRead = { read = FOnRead, write = FOnRead };
 
   // The only way to make Items stored automatically and survive handle recreation.

+ 10 - 4
source/forms/CustomScpExplorer.cpp

@@ -9327,6 +9327,13 @@ TColor __fastcall TCustomScpExplorerForm::DisabledPanelColor()
   return Result;
 }
 //---------------------------------------------------------------------------
+void TCustomScpExplorerForm::UpdatePanelControls(TCustomDirView * ADirView, TCustomDriveView * ADriveView)
+{
+  bool UseDarkTheme = WinConfiguration->UseDarkTheme();
+  ADirView->DarkMode = UseDarkTheme;
+  ADriveView->DarkMode = UseDarkTheme;
+}
+//---------------------------------------------------------------------------
 void __fastcall TCustomScpExplorerForm::UpdateControls()
 {
   if (!FSessionChanging)
@@ -9371,9 +9378,8 @@ void __fastcall TCustomScpExplorerForm::UpdateControls()
     QueueFileList->Font->Color = QueueView3->Font->Color;
     QueueLabelUpdateStatus();
 
+    UpdatePanelControls(RemoteDirView, RemoteDriveView);
     bool UseDarkTheme = WinConfiguration->UseDarkTheme();
-    RemoteDirView->DarkMode = UseDarkTheme;
-    RemoteDriveView->DarkMode = RemoteDirView->DarkMode;
     if (FImmersiveDarkMode != UseDarkTheme)
     {
       UpdateDarkMode();
@@ -11671,11 +11677,11 @@ TListItem * __fastcall TCustomScpExplorerForm::SearchFile(const UnicodeString &
           Matches = ContainsTextSemiCaseSensitive(ADirView->ItemFileName(Item), Text);
           break;
         case isAll:
-          int ColCount = (ADirView->ViewStyle == vsReport) ? ADirView->ColProperties->Count : 1;
+          int ColCount = (ADirView->DirViewStyle == dvsReport) ? ADirView->ColProperties->Count : 1;
           int Index = 0;
           while ((Index < ColCount) && !Matches)
           {
-            bool Visible = (ADirView->ViewStyle == vsReport) ? ADirView->ColProperties->Visible[Index] : true;
+            bool Visible = (ADirView->DirViewStyle == dvsReport) ? ADirView->ColProperties->Visible[Index] : true;
             if (Visible)
             {
               Matches = ContainsTextSemiCaseSensitive(ADirView->GetColumnText(Item, Index), Text);

+ 1 - 1
source/forms/CustomScpExplorer.dfm

@@ -86,7 +86,6 @@ object CustomScpExplorerForm: TCustomScpExplorerForm
         ParentDoubleBuffered = False
         PopupMenu = NonVisualDataModule.RemoteDirViewPopup
         TabOrder = 0
-        ViewStyle = vsReport
         OnColumnRightClick = DirViewColumnRightClick
         OnEditing = DirViewEditing
         OnEnter = RemoteDirViewEnter
@@ -118,6 +117,7 @@ object CustomScpExplorerForm: TCustomScpExplorerForm
         OnContextPopup = RemoteDirViewContextPopup
         OnHistoryChange = DirViewHistoryChange
         OnDisplayProperties = RemoteDirViewDisplayProperties
+        DirViewStyle = dvsReport
         OnRead = RemoteDirViewRead
       end
       object ReconnectToolbar: TTBXToolbar

+ 2 - 1
source/forms/CustomScpExplorer.h

@@ -592,7 +592,7 @@ protected:
     TTBXStatusBar * StatusBar, const TStatusFileInfo & FileInfo);
   void __fastcall FileStatusBarPanelClick(TTBXStatusPanel * Panel, TOperationSide Side);
   virtual void __fastcall DoDirViewLoaded(TCustomDirView * Sender);
-  virtual void __fastcall UpdateControls();
+  virtual void UpdatePanelControls(TCustomDirView * ADirView, TCustomDriveView * ADriveView);
   void __fastcall UpdateTransferList();
   void __fastcall UpdateTransferLabel();
   void __fastcall StartUpdates();
@@ -922,6 +922,7 @@ public:
   void IncrementalSearchStart();
   virtual void * SaveFocus();
   virtual void RestoreFocus(void * Focus);
+  virtual void __fastcall UpdateControls();
 
   __property bool ComponentVisible[Byte Component] = { read = GetComponentVisible, write = SetComponentVisible };
   __property bool EnableFocusedOperation[TOperationSide Side] = { read = GetEnableFocusedOperation, index = 0 };

+ 24 - 17
source/forms/NonVisual.cpp

@@ -266,14 +266,17 @@ void __fastcall TNonVisualDataModule::ExplorerActionsUpdate(
   UPD(RemoteSelectAllAction2, DirView(osRemote)->FilesCount)
 
   //style
-  UPDACT(CurrentCycleStyleAction,
-    CurrentCycleStyleAction->ImageIndex = 8 + (DirView(osCurrent)->ViewStyle + 1) % 4)
-  #define STYLEACTION(Style) UPDACT(Current ## Style ## Action, \
-    Current ## Style ## Action->Checked = (DirView(osCurrent)->ViewStyle == vs ## Style))
-  STYLEACTION(Icon)
-  STYLEACTION(SmallIcon)
-  STYLEACTION(List)
-  STYLEACTION(Report)
+  UPDACT(RemoteCycleStyleAction,
+    RemoteCycleStyleAction->ImageIndex = 8 + (DirView(osRemote)->DirViewStyle + 1) % 4)
+  #define STYLEACTION(SIDE, STYLE) UPDACT(SIDE ## STYLE ## Action, \
+    SIDE ## STYLE ## Action->Checked = (DirView(os ## SIDE)->DirViewStyle == dvs ## STYLE))
+  STYLEACTION(Remote, Icon)
+  STYLEACTION(Remote, SmallIcon)
+  STYLEACTION(Remote, List)
+  STYLEACTION(Remote, Report)
+  STYLEACTION(Remote, Thumbnail)
+  STYLEACTION(Local, Report)
+  STYLEACTION(Local, Thumbnail)
   #undef STYLEACTION
 
   // REMOTE+LOCAL
@@ -627,16 +630,20 @@ void __fastcall TNonVisualDataModule::ExplorerActionsExecute(
     EXE(PasteAction3, ScpExplorer->PasteFromClipBoard())
 
     // style
-    EXE(CurrentCycleStyleAction,
-      if (DirView(osCurrent)->ViewStyle == vsReport) DirView(osCurrent)->ViewStyle = vsIcon;
-        else DirView(osCurrent)->ViewStyle = (TViewStyle)(DirView(osCurrent)->ViewStyle + 1);
+    EXE(RemoteCycleStyleAction,
+      if (DirView(osRemote)->DirViewStyle == dvsReport) DirView(osRemote)->DirViewStyle = dvsIcon;
+        else DirView(osRemote)->DirViewStyle = (TDirViewStyle)(DirView(osRemote)->DirViewStyle + 1);
+      ScpExplorer->UpdateControls();
     )
-    #define STYLEACTION(Style) EXE(Current ## Style ## Action, \
-      DirView(osCurrent)->ViewStyle = vs ## Style)
-    STYLEACTION(Icon)
-    STYLEACTION(SmallIcon)
-    STYLEACTION(List)
-    STYLEACTION(Report)
+    #define STYLEACTION(SIDE, STYLE) EXE(SIDE ## STYLE ## Action, \
+      DirView(os ## SIDE)->DirViewStyle = dvs ## STYLE; ScpExplorer->UpdateControls())
+    STYLEACTION(Remote, Icon)
+    STYLEACTION(Remote, SmallIcon)
+    STYLEACTION(Remote, List)
+    STYLEACTION(Remote, Report)
+    STYLEACTION(Remote, Thumbnail)
+    STYLEACTION(Local, Report)
+    STYLEACTION(Local, Thumbnail)
     #undef STYLEACTION
 
     #define PANEL_ACTIONS(SIDE) \

+ 32 - 10
source/forms/NonVisual.dfm

@@ -343,40 +343,40 @@ object NonVisualDataModule: TNonVisualDataModule
       ImageIndex = 6
       ShortCut = 32805
     end
-    object CurrentCycleStyleAction: TAction
-      Tag = 7
+    object RemoteCycleStyleAction: TAction
+      Tag = 15
       Category = 'Style'
       Caption = 'View'
       HelpKeyword = 'ui_file_panel#view_style'
       Hint = 'View|Cycle thru directory view styles'
       ImageIndex = 8
     end
-    object CurrentIconAction: TAction
-      Tag = 7
+    object RemoteIconAction: TAction
+      Tag = 15
       Category = 'Style'
       Caption = '&Large Icons'
       HelpKeyword = 'ui_file_panel#view_style'
       Hint = 'Large Icons|View large icons'
       ImageIndex = 8
     end
-    object CurrentSmallIconAction: TAction
-      Tag = 7
+    object RemoteSmallIconAction: TAction
+      Tag = 15
       Category = 'Style'
       Caption = '&Small Icons'
       HelpKeyword = 'ui_file_panel#view_style'
       Hint = 'Small Icons|View small icons'
       ImageIndex = 9
     end
-    object CurrentListAction: TAction
-      Tag = 7
+    object RemoteListAction: TAction
+      Tag = 15
       Category = 'Style'
       Caption = 'Lis&t'
       HelpKeyword = 'ui_file_panel#view_style'
       Hint = 'List|View list'
       ImageIndex = 10
     end
-    object CurrentReportAction: TAction
-      Tag = 7
+    object RemoteReportAction: TAction
+      Tag = 15
       Category = 'Style'
       Caption = '&Details'
       HelpKeyword = 'ui_file_panel#view_style'
@@ -2465,6 +2465,28 @@ object NonVisualDataModule: TNonVisualDataModule
       Caption = 'IncrementalSearchStartAction'
       ShortCut = 16454
     end
+    object RemoteThumbnailAction: TAction
+      Tag = 15
+      Category = 'Style'
+      Caption = '&Thumbnails'
+      HelpKeyword = 'ui_file_panel#view_style'
+      Hint = 'Thumbnails|View thumbnails'
+    end
+    object LocalReportAction: TAction
+      Tag = 15
+      Category = 'Style'
+      Caption = '&Details'
+      HelpKeyword = 'ui_file_panel#view_style'
+      Hint = 'Details|View details'
+      ImageIndex = 11
+    end
+    object LocalThumbnailAction: TAction
+      Tag = 15
+      Category = 'Style'
+      Caption = '&Thumbnails'
+      HelpKeyword = 'ui_file_panel#view_style'
+      Hint = 'Thumbnails|View thumbnails'
+    end
   end
   object ExplorerBarPopup: TTBXPopupMenu
     Images = GlyphsModule.ExplorerImages

+ 8 - 5
source/forms/NonVisual.h

@@ -81,11 +81,11 @@ __published:    // IDE-managed Components
   TAction *CurrentRenameAction;
   TAction *CurrentDeleteAction;
   TAction *CurrentCreateDirAction;
-  TAction *CurrentCycleStyleAction;
-  TAction *CurrentIconAction;
-  TAction *CurrentSmallIconAction;
-  TAction *CurrentReportAction;
-  TAction *CurrentListAction;
+  TAction *RemoteCycleStyleAction;
+  TAction *RemoteIconAction;
+  TAction *RemoteSmallIconAction;
+  TAction *RemoteReportAction;
+  TAction *RemoteListAction;
   TAction *CurrentDeleteFocusedAction;
   TAction *CurrentPropertiesFocusedAction;
   TAction *CurrentPropertiesAction;
@@ -711,6 +711,9 @@ __published:    // IDE-managed Components
   TTBXSeparatorItem *TBXSeparatorItem23;
   TTBXItem *TBXItem116;
   TAction *IncrementalSearchStartAction;
+  TAction *RemoteThumbnailAction;
+  TAction *LocalReportAction;
+  TAction *LocalThumbnailAction;
   void __fastcall ExplorerActionsUpdate(TBasicAction *Action, bool &Handled);
   void __fastcall ExplorerActionsExecute(TBasicAction *Action, bool &Handled);
   void __fastcall SessionIdleTimerTimer(TObject *Sender);

+ 15 - 4
source/forms/ScpCommander.cpp

@@ -165,6 +165,7 @@ void __fastcall TScpCommanderForm::RestorePanelParams(
   const TScpCommanderPanelConfiguration & PanelConfiguration)
 {
   DirView->ColProperties->ParamsStr = PanelConfiguration.DirViewParams;
+  DirView->DirViewStyle = (TDirViewStyle)PanelConfiguration.ViewStyle;
   StatusBar->Visible = PanelConfiguration.StatusBar;
   DriveControl->Visible = PanelConfiguration.DriveView;
   if (DriveControl->Align == alTop)
@@ -194,6 +195,7 @@ void __fastcall TScpCommanderForm::RestoreParams()
   RestorePanelParams(LocalDirView, LocalDriveView, LocalStatusBar, WinConfiguration->ScpCommander.LocalPanel);
   RestorePanelParams(RemoteDirView, RemoteDrivePanel, RemoteStatusBar, WinConfiguration->ScpCommander.RemotePanel);
   OtherLocalDirView->ColProperties->ParamsStr = WinConfiguration->ScpCommander.OtherLocalPanelDirViewParams;
+  OtherLocalDirView->DirViewStyle = (TDirViewStyle)WinConfiguration->ScpCommander.OtherLocalPanelViewStyle;
   FPanelsRestored = true;
 
   // just to make sure
@@ -207,6 +209,7 @@ void __fastcall TScpCommanderForm::StorePanelParams(
   TScpCommanderPanelConfiguration & PanelConfiguration)
 {
   PanelConfiguration.DirViewParams = DirView->ColProperties->ParamsStr;
+  PanelConfiguration.ViewStyle = DirView->DirViewStyle;
   PanelConfiguration.StatusBar = StatusBar->Visible;
   PanelConfiguration.DriveView = DriveControl->Visible;
   if (DriveControl->Align == alTop)
@@ -242,6 +245,7 @@ void __fastcall TScpCommanderForm::StoreParams()
     StorePanelParams(LocalDirView, LocalDriveView, LocalStatusBar, CommanderConfiguration.LocalPanel);
     StorePanelParams(RemoteDirView, RemoteDrivePanel, RemoteStatusBar, CommanderConfiguration.RemotePanel);
     CommanderConfiguration.OtherLocalPanelDirViewParams = OtherLocalDirView->ColProperties->ParamsStr;
+    CommanderConfiguration.OtherLocalPanelViewStyle = reinterpret_cast<TDirViewStyle>(OtherLocalDirView->DirViewStyle);
 
     CommanderConfiguration.LocalPanel.LastPath = LocalDirView->Path;
     CommanderConfiguration.OtherLocalPanelLastPath = OtherLocalDirView->Path;
@@ -1024,6 +1028,12 @@ void __fastcall TScpCommanderForm::SetToolbar2ItemAction(TTBXItem * Item, TBasic
   }
 }
 //---------------------------------------------------------------------------
+void TScpCommanderForm::UpdatePanelControls(TCustomDirView * ADirView, TCustomDriveView * ADriveView)
+{
+  TCustomScpExplorerForm::UpdatePanelControls(ADirView, ADriveView);
+  ADirView->AddParentDir = (ADirView->DirViewStyle == dvsReport);
+}
+//---------------------------------------------------------------------------
 void __fastcall TScpCommanderForm::UpdateControls()
 {
   // Before TCustomScpExplorerForm disables them (when disconnecting)
@@ -1057,10 +1067,8 @@ void __fastcall TScpCommanderForm::UpdateControls()
     // command line combo width needs to be updated as caption width has probably changed
     ToolBarResize(CommandLineToolbar);
   }
-  LocalDirView->DarkMode = WinConfiguration->UseDarkTheme();
-  LocalDriveView->DarkMode = LocalDirView->DarkMode;
-  OtherLocalDirView->DarkMode = LocalDirView->DarkMode;
-  OtherLocalDriveView->DarkMode = LocalDirView->DarkMode;
+  UpdatePanelControls(LocalDirView, LocalDriveView);
+  UpdatePanelControls(OtherLocalDirView, OtherLocalDriveView);
   LocalDirView->Color = PanelColor();
   LocalDriveView->Color = LocalDirView->Color;
   OtherLocalDirView->Color = LocalDirView->Color;
@@ -1070,6 +1078,9 @@ void __fastcall TScpCommanderForm::UpdateControls()
   OtherLocalDirView->Font->Color = LocalDirView->Font->Color;
   OtherLocalDriveView->Font->Color = LocalDirView->Font->Color;
 
+  LocalColumnsSubmenuItem->Enabled = (DirView(osLocal)->DirViewStyle == dvsReport);
+  RemoteColumnsSubmenuItem->Enabled = (DirView(osRemote)->DirViewStyle == dvsReport);
+
   if (IsLocalBrowserMode())
   {
     CurrentCopyItem->Visible = false;

+ 29 - 3
source/forms/ScpCommander.dfm

@@ -93,6 +93,19 @@ inherited ScpCommanderForm: TScpCommanderForm
         end
         object TBXSeparatorItem4: TTBXSeparatorItem
         end
+        object TBXSubmenuItem17: TTBXSubmenuItem
+          Caption = '&View'
+          HelpKeyword = 'ui_file_panel#view_style'
+          Hint = 'Change directory view style'
+          object TBXItem270: TTBXItem
+            Action = NonVisualDataModule.LocalReportAction
+          end
+          object TBXSeparatorItem76: TTBXSeparatorItem
+          end
+          object TBXItem271: TTBXItem
+            Action = NonVisualDataModule.LocalThumbnailAction
+          end
+        end
         object TBXSubmenuItem3: TTBXSubmenuItem
           Caption = '&Sort'
           HelpKeyword = 'ui_file_panel#sorting_files'
@@ -1195,7 +1208,6 @@ inherited ScpCommanderForm: TScpCommanderForm
         ParentDoubleBuffered = False
         PopupMenu = NonVisualDataModule.RemoteDirViewPopup
         TabOrder = 1
-        ViewStyle = vsReport
         OnColumnRightClick = DirViewColumnRightClick
         OnEditing = DirViewEditing
         OnEnter = OtherLocalDirViewEnter
@@ -1224,6 +1236,7 @@ inherited ScpCommanderForm: TScpCommanderForm
         OnPathChange = OtherLocalDirViewPathChange
         OnBusy = DirViewBusy
         OnChangeFocus = DirViewChangeFocus
+        DirViewStyle = dvsReport
       end
     end
     inherited RemoteDrivePanel: TPanel
@@ -1600,7 +1613,6 @@ inherited ScpCommanderForm: TScpCommanderForm
       ParentDoubleBuffered = False
       PopupMenu = NonVisualDataModule.LocalDirViewPopup
       TabOrder = 1
-      ViewStyle = vsReport
       OnColumnRightClick = DirViewColumnRightClick
       OnEditing = DirViewEditing
       OnEnter = LocalDirViewEnter
@@ -1630,6 +1642,7 @@ inherited ScpCommanderForm: TScpCommanderForm
       OnPathChange = LocalDirViewPathChange
       OnBusy = DirViewBusy
       OnChangeFocus = DirViewChangeFocus
+      DirViewStyle = dvsReport
     end
     object LocalTopDock: TTBXDock
       Left = 0
@@ -1686,6 +1699,19 @@ inherited ScpCommanderForm: TScpCommanderForm
         object TBXItem164: TTBXItem
           Action = NonVisualDataModule.LocalTreeAction
         end
+        object TBXSubmenuItem4: TTBXSubmenuItem
+          Caption = 'Change directory view style'
+          ImageIndex = 11
+          Options = [tboDropdownArrow]
+          object TBXItem269: TTBXItem
+            Action = NonVisualDataModule.LocalReportAction
+          end
+          object TBXSeparatorItem75: TTBXSeparatorItem
+          end
+          object TBXItem268: TTBXItem
+            Action = NonVisualDataModule.LocalThumbnailAction
+          end
+        end
       end
       object LocalPathToolbar: TTBXToolbar
         Left = 0
@@ -1805,7 +1831,7 @@ inherited ScpCommanderForm: TScpCommanderForm
         end
       end
       object LocalSelectionToolbar: TTBXToolbar
-        Left = 217
+        Left = 244
         Top = 27
         Caption = 'Local Selection'
         DockPos = 217

+ 10 - 1
source/forms/ScpCommander.h

@@ -468,6 +468,14 @@ __published:
   TTBXItem *TBXItem266;
   TTBXSeparatorItem *TBXSeparatorItem74;
   TTBXItem *TBXItem267;
+  TTBXSubmenuItem *TBXSubmenuItem4;
+  TTBXItem *TBXItem269;
+  TTBXSeparatorItem *TBXSeparatorItem75;
+  TTBXItem *TBXItem268;
+  TTBXSubmenuItem *TBXSubmenuItem17;
+  TTBXItem *TBXItem270;
+  TTBXSeparatorItem *TBXSeparatorItem76;
+  TTBXItem *TBXItem271;
   void __fastcall SplitterMoved(TObject *Sender);
   void __fastcall SplitterCanResize(TObject *Sender, int &NewSize,
     bool &Accept);
@@ -592,7 +600,7 @@ protected:
   virtual void __fastcall ConfigurationChanged();
   virtual bool __fastcall GetHasDirView(TOperationSide Side);
   virtual TCustomDirView * GetCurrentLocalBrowser();
-  virtual void __fastcall UpdateControls();
+  virtual void UpdatePanelControls(TCustomDirView * ADirView, TCustomDriveView * ADriveView);
   virtual void __fastcall FileOperationProgress(
     TFileOperationProgressType & ProgressData);
   virtual void __fastcall DoOpenDirectoryDialog(TOpenDirectoryMode Mode,
@@ -695,6 +703,7 @@ public:
   virtual void ResetLayoutColumns(TOperationSide Side);
   virtual void * SaveFocus();
   virtual void RestoreFocus(void * Focus);
+  virtual void __fastcall UpdateControls();
 
   __property double LeftPanelWidth = { read = GetLeftPanelWidth, write = SetLeftPanelWidth };
 };

+ 3 - 3
source/forms/ScpExplorer.cpp

@@ -86,7 +86,7 @@ void __fastcall TScpExplorerForm::RestoreParams()
   bool HadHandleAllocated = RemoteDirView->HandleAllocated();
   RemoteDirView->UnixColProperties->ParamsStr = WinConfiguration->ScpExplorer.DirViewParams;
   RemoteDirView->UnixColProperties->ExtVisible = false; // just to make sure
-  RemoteDirView->ViewStyle = (TViewStyle)WinConfiguration->ScpExplorer.ViewStyle;
+  RemoteDirView->DirViewStyle = (TDirViewStyle)WinConfiguration->ScpExplorer.ViewStyle;
   if (HadHandleAllocated)
   {
     // This is here to make our persistence checks in VerifyControl pass,
@@ -121,7 +121,7 @@ void __fastcall TScpExplorerForm::StoreParams()
 
     WinConfiguration->ScpExplorer.WindowParams = StoreForm(this);
     WinConfiguration->ScpExplorer.DirViewParams = RemoteDirView->UnixColProperties->ParamsStr;
-    WinConfiguration->ScpExplorer.ViewStyle = RemoteDirView->ViewStyle;
+    WinConfiguration->ScpExplorer.ViewStyle = RemoteDirView->DirViewStyle;
     WinConfiguration->ScpExplorer.DriveView = RemoteDrivePanel->Visible;
     WinConfiguration->ScpExplorer.DriveViewWidth = RemoteDrivePanel->Width;
     WinConfiguration->ScpExplorer.DriveViewWidthPixelsPerInch = GetControlPixelsPerInch(this);
@@ -444,5 +444,5 @@ void __fastcall TScpExplorerForm::UpdateControls()
     ActiveControl = RemoteDirView;
   }
 
-  ColumndsSubmenuItem->Enabled = (RemoteDirView->ViewStyle == vsReport);
+  ColumndsSubmenuItem->Enabled = (RemoteDirView->DirViewStyle == dvsReport);
 }

+ 9 - 9
source/forms/ScpExplorer.dfm

@@ -460,16 +460,16 @@ inherited ScpExplorerForm: TScpExplorerForm
         object TBXSeparatorItem3: TTBXSeparatorItem
         end
         object TBXItem16: TTBXItem
-          Action = NonVisualDataModule.CurrentIconAction
+          Action = NonVisualDataModule.RemoteIconAction
         end
         object TBXItem17: TTBXItem
-          Action = NonVisualDataModule.CurrentSmallIconAction
+          Action = NonVisualDataModule.RemoteSmallIconAction
         end
         object TBXItem18: TTBXItem
-          Action = NonVisualDataModule.CurrentListAction
+          Action = NonVisualDataModule.RemoteListAction
         end
         object TBXItem19: TTBXItem
-          Action = NonVisualDataModule.CurrentReportAction
+          Action = NonVisualDataModule.RemoteReportAction
         end
         object TBXSeparatorItem4: TTBXSeparatorItem
         end
@@ -833,19 +833,19 @@ inherited ScpExplorerForm: TScpExplorerForm
       object TBXSeparatorItem36: TTBXSeparatorItem
       end
       object TBXSubmenuItem3: TTBXSubmenuItem
-        Action = NonVisualDataModule.CurrentCycleStyleAction
+        Action = NonVisualDataModule.RemoteCycleStyleAction
         DropdownCombo = True
         object TBXItem72: TTBXItem
-          Action = NonVisualDataModule.CurrentIconAction
+          Action = NonVisualDataModule.RemoteIconAction
         end
         object TBXItem73: TTBXItem
-          Action = NonVisualDataModule.CurrentSmallIconAction
+          Action = NonVisualDataModule.RemoteSmallIconAction
         end
         object TBXItem74: TTBXItem
-          Action = NonVisualDataModule.CurrentListAction
+          Action = NonVisualDataModule.RemoteListAction
         end
         object TBXItem75: TTBXItem
-          Action = NonVisualDataModule.CurrentReportAction
+          Action = NonVisualDataModule.RemoteReportAction
         end
       end
       object TBXSubmenuItem24: TTBXSubmenuItem

+ 1 - 1
source/forms/ScpExplorer.h

@@ -373,7 +373,6 @@ protected:
   virtual void __fastcall ToolbarItemResize(TTBXCustomDropDownItem * Item, int Width);
   virtual bool __fastcall UpdateToolbarDisplayMode();
   virtual void __fastcall UpdateImages();
-  virtual void __fastcall UpdateControls();
 
 public:
   __fastcall TScpExplorerForm(TComponent* Owner);
@@ -386,6 +385,7 @@ public:
   virtual UnicodeString __fastcall DefaultDownloadTargetDirectory();
   virtual bool SupportedSession(TSessionData * SessionData);
   virtual void ResetLayoutColumns(TOperationSide Side);
+  virtual void __fastcall UpdateControls();
 };
 //---------------------------------------------------------------------------
 #endif

+ 271 - 14
source/packages/filemng/CustomDirView.pas

@@ -17,6 +17,7 @@ uses
 const
   clDefaultItemColor = -(COLOR_ENDCOLORS + 1);
   WM_USER_RENAME = WM_USER + 57;
+  WM_USER_INVALIDATEITEM = WM_USER + $2000 + 16;
   oiNoOverlay = $00;
   oiDirUp = $01;
   oiLink = $02;
@@ -88,6 +89,8 @@ type
   TDVHistoryGoEvent = procedure(Sender: TCustomDirView; Index: Integer; var Cancel: Boolean) of object;
   TCompareCriteria = (ccTime, ccSize);
   TCompareCriterias = set of TCompareCriteria;
+  // First four must match TViewStyle
+  TDirViewStyle = (dvsIcon, dvsSmallIcon, dvsList, dvsReport, dvsThumbnail);
 
   TWMXMouse = packed record
     Msg: Cardinal;
@@ -149,6 +152,8 @@ type
     FHistoryPaths: TStrings;
     FOverlaySmallImages: TImageList;
     FOverlayLargeImages: TImageList;
+    FThumbnailShellImages: TImageList;
+    FThumbnailImages: TImageList;
     FMaxHistoryCount: Integer;
     FPathLabel: TCustomPathLabel;
     FOnUpdateStatusBar: TDirViewUpdateStatusBarEvent;
@@ -173,6 +178,8 @@ type
     FDoubleBufferedScrollingWorkaround: Boolean;
     FOnBusy: TDirViewBusy;
     FOnChangeFocus: TDirViewChangeFocusEvent;
+    FFallbackThumbnail: array[Boolean] of TBitmap;
+    FFallbackThumbnailSize: TSize;
 
     procedure CNNotify(var Message: TWMNotify); message CN_NOTIFY;
     procedure WMNotify(var Msg: TWMNotify); message WM_NOTIFY;
@@ -190,6 +197,7 @@ type
     procedure CMDPIChanged(var Message: TMessage); message CM_DPICHANGED;
     procedure CMEnabledChanged(var Message: TMessage); message CM_ENABLEDCHANGED;
     procedure CNKeyDown(var Message: TWMKeyDown); message CN_KEYDOWN;
+    procedure WMUserInvalidateItem(var Message: TMessage); message WM_USER_INVALIDATEITEM;
 
     procedure DumbCustomDrawItem(Sender: TCustomListView; Item: TListItem;
       State: TCustomDrawState; var DefaultDraw: Boolean);
@@ -201,6 +209,9 @@ type
     function GetHistoryPath(Index: Integer): string;
     function GetSelectedNamesSaved: Boolean;
 
+    function GetDirViewStyle: TDirViewStyle;
+    procedure SetDirViewStyle(Value: TDirViewStyle);
+
     function GetTargetPopupMenu: Boolean;
     function GetUseDragImages: Boolean;
     procedure SetMaxHistoryCount(Value: Integer);
@@ -223,6 +234,7 @@ type
     FInvalidNameChars: string;
     FDragDrive: string;
     FAnnouncedState: TObject;
+    FThumbnail: Boolean;
 
     procedure AddToDragFileList(FileList: TFileList; Item: TListItem); virtual;
     function CanEdit(Item: TListItem): Boolean; override;
@@ -276,6 +288,10 @@ type
     function DoItemColor(Item: TListItem): TColor;
     function ItemColor(Item: TListItem): TColor; virtual;
     function ItemImageIndex(Item: TListItem; Cache: Boolean): Integer; virtual; abstract;
+    function ItemThumbnail(Item: TListItem; Size: TSize): TBitmap; virtual;
+    procedure FreeThumbnails;
+    function FallbackThumbnail(Dir: Boolean; Size: TSize): TBitmap;
+    procedure DrawThumbnail(Item: TListItem; DC: HDC);
     // ItemIsDirectory and ItemFullFileName is in public block
     function ItemIsRecycleBin(Item: TListItem): Boolean; virtual;
     procedure KeyDown(var Key: Word; Shift: TShiftState); override;
@@ -299,6 +315,7 @@ type
     function ItemIsFile(Item: TListItem): Boolean; virtual; abstract;
     function ItemMatchesFilter(Item: TListItem; const Filter: TFileFilter): Boolean; virtual; abstract;
     function ItemOverlayIndexes(Item: TListItem): Word; virtual;
+    function IsItemVisible(Item: TListItem): Boolean;
     procedure LimitHistorySize;
     procedure Notification(AComponent: TComponent; Operation: TOperation); override;
     procedure PathChanged; virtual;
@@ -342,6 +359,8 @@ type
     procedure DoUpdateStatusBar(Force: Boolean = False);
     procedure DoCustomDrawItem(Item: TListItem; Stage: TCustomDrawStage);
     procedure ItemCalculatedSizeUpdated(Item: TListItem; OldSize, NewSize: Int64);
+    function GetThumbnail(Path: string; Size: TSize): TBitmap;
+    procedure InvalidateItem(Item: TListItem);
   public
     constructor Create(AOwner: TComponent); override;
     destructor Destroy; override;
@@ -436,6 +455,7 @@ type
     property NaturalOrderNumericalSorting: Boolean read FNaturalOrderNumericalSorting write SetNaturalOrderNumericalSorting;
     property AlwaysSortDirectoriesByName: Boolean read FAlwaysSortDirectoriesByName write SetAlwaysSortDirectoriesByName;
     property DarkMode: Boolean read FDarkMode write SetDarkMode;
+    property DirViewStyle: TDirViewStyle read GetDirViewStyle write SetDirViewStyle;
 
     property OnContextPopup;
     property OnStartLoading: TNotifyEvent read FOnStartLoading write FOnStartLoading;
@@ -548,7 +568,11 @@ const
 implementation
 
 uses
-  Math, DirViewColProperties, UITypes, Types, OperationWithTimeout, Winapi.UxTheme, Vcl.Themes;
+  Math, DirViewColProperties, UITypes, Types, OperationWithTimeout, Winapi.UxTheme, Vcl.Themes, System.IOUtils;
+
+type
+  PRGBQuadArray = ^TRGBQuadArray;    // From graphics.pas
+  TRGBQuadArray = array[Byte] of TRGBQuad;  // From graphics.pas
 
 const
   ResDirUp = 'DIRUP%2.2d';
@@ -957,6 +981,47 @@ begin
     else inherited;
 end;
 
+procedure TCustomDirView.DrawThumbnail(Item: TListItem; DC: HDC);
+var
+  Rect: TRect;
+  Thumbnail: TBitmap;
+  Size: TSize;
+  Left, Top: Integer;
+  BlendFunction: TBlendFunction;
+  ThumbnailDC: HDC;
+begin
+  Rect := Item.DisplayRect(drIcon);
+
+  // For thumbnails: The larger side (e.g. height for portrait oriented images) is rescaled to Size.Width
+  // For icons: The "generated" images is exactly as requested
+  Size.Height := MulDiv(Min(Rect.Width, Rect.Height), 9, 10);
+  Size.Width := Size.Height;
+
+  Thumbnail := ItemThumbnail(Item, Size);
+  if not Assigned(Thumbnail) then
+    Thumbnail := FallbackThumbnail(ItemIsDirectory(Item), Size);
+
+  if Assigned(Thumbnail) then
+  begin
+    Left := Rect.Left + ((Rect.Width - Thumbnail.Width) div 2);
+    Top := Rect.Bottom - Thumbnail.Height - MulDiv(Rect.Height, 1, 20); // Bottom-aligned, as Explorer does
+
+    // https://stackoverflow.com/q/24595717/850848
+    // https://stackoverflow.com/q/10028531/850848#10044325
+    ThumbnailDC := CreateCompatibleDC(0);
+    try
+      SelectObject(ThumbnailDC, Thumbnail.Handle);
+      BlendFunction.BlendOp := AC_SRC_OVER;
+      BlendFunction.BlendFlags := 0;
+      BlendFunction.SourceConstantAlpha := 255;
+      BlendFunction.AlphaFormat := AC_SRC_ALPHA;
+      AlphaBlend(DC, Left, Top, Thumbnail.Width, Thumbnail.Height, ThumbnailDC, 0, 0, Thumbnail.Width, Thumbnail.Height, BlendFunction);
+    finally
+      DeleteDC(ThumbnailDC);
+    end;
+  end;
+end;
+
 procedure TCustomDirView.CNNotify(var Message: TWMNotify);
 
   procedure DrawOverlayImage(DC: HDC; Image: Integer; Item: TListItem);
@@ -1072,22 +1137,39 @@ begin
   begin
     Nmcd := @PNMLVCustomDraw(Message.NMHdr).nmcd;
     try
-      Message.Result := Message.Result or CDRF_NOTIFYPOSTPAINT;
-      if (Nmcd.dwDrawStage = CDDS_ITEMPOSTPAINT) and
-         ((Nmcd.dwDrawStage and CDDS_SUBITEM) = 0) then
+      if (Nmcd.dwDrawStage and CDDS_SUBITEM) = 0 then
       begin
-        Item := Items[Nmcd.dwItemSpec];
-        Assert(Assigned(Item));
-        OverlayIndexes := ItemOverlayIndexes(Item);
-        OverlayIndex := 1;
-        while OverlayIndexes > 0 do
+        if (Nmcd.dwDrawStage and CDDS_ITEMPREPAINT) = CDDS_ITEMPREPAINT then
         begin
-          if (OverlayIndex and OverlayIndexes) <> 0 then
+          Message.Result := Message.Result or CDRF_NOTIFYPOSTPAINT;
+        end
+          else
+        if ((Nmcd.dwDrawStage and CDDS_ITEMPOSTPAINT) = CDDS_ITEMPOSTPAINT) and
+           (Nmcd.rc.Width > 0) then // We get called many times with empty rect while view is recreating (e.g. when switching to thumnails mode)
+        begin
+          Item := Items[Nmcd.dwItemSpec];
+
+          if IsItemVisible(Item) then // particularly the thumbnail drawing is expensive
           begin
-            DrawOverlayImage(Nmcd.hdc, OverlayIndex, Item);
-            Dec(OverlayIndexes, OverlayIndex);
+            if FThumbnail then
+            begin
+              DrawThumbnail(Item, Nmcd.hdc);
+            end
+              else
+            begin
+              OverlayIndexes := ItemOverlayIndexes(Item);
+              OverlayIndex := 1;
+              while OverlayIndexes > 0 do
+              begin
+                if (OverlayIndex and OverlayIndexes) <> 0 then
+                begin
+                  DrawOverlayImage(Nmcd.hdc, OverlayIndex, Item);
+                  Dec(OverlayIndexes, OverlayIndex);
+                end;
+                OverlayIndex := OverlayIndex shl 1;
+              end;
+            end;
           end;
-          OverlayIndex := OverlayIndex shl 1;
         end;
       end;
     except
@@ -1186,9 +1268,31 @@ begin
 end;
 
 procedure TCustomDirView.NeedImageLists(Recreate: Boolean);
+var
+  ALargeImages: TImageList;
+  ThumbnailSize: Integer;
 begin
   SmallImages := NeedImageList(ilsSmall, Recreate, FOverlaySmallImages);
-  LargeImages := NeedImageList(ilsLarge, Recreate, FOverlayLargeImages);
+  ALargeImages := NeedImageList(ilsLarge, Recreate, FOverlayLargeImages);
+
+  if FThumbnail then
+  begin
+    ThumbnailSize := ScaleByPixelsPerInch(128, Self);
+    // ShellImageListForSize would normally prefer smaller icons (for row sizing purposes).
+    // But for thumbnails, we prefer larger version as will will scale it when painting.
+    // The *2 is hackish way to achieve that.
+    FThumbnailShellImages := ShellImageListForSize(ThumbnailSize * 2);
+    if (not Assigned(FThumbnailImages)) or (FThumbnailImages.Width <> ThumbnailSize) then
+    begin
+      if Assigned(FThumbnailImages) then
+        FreeAndNil(FThumbnailImages);
+      // Dummy image list, whose sole purpose it to autosize the items in the view
+      FThumbnailImages := TImageList.CreateSize(ThumbnailSize, ThumbnailSize);
+    end;
+
+    LargeImages := FThumbnailImages
+  end
+    else LargeImages := ALargeImages;
 end;
 
 procedure TCustomDirView.CMDPIChanged(var Message: TMessage);
@@ -1222,6 +1326,7 @@ procedure TCustomDirView.FreeImageLists;
 begin
   FreeAndNil(FOverlaySmallImages);
   FreeAndNil(FOverlayLargeImages);
+  FreeAndNil(FThumbnailImages);
 
   SmallImages := nil;
   LargeImages := nil;
@@ -1383,6 +1488,7 @@ begin
 
   FreeAndNil(FDragDropFilesEx);
   FreeImageLists;
+  FreeThumbnails;
 
   inherited;
 end;
@@ -1465,6 +1571,104 @@ begin
   Result := clDefaultItemColor;
 end;
 
+function TCustomDirView.ItemThumbnail(Item: TListItem; Size: TSize): TBitmap;
+begin
+  Result := nil;
+end;
+
+function TCustomDirView.GetThumbnail(Path: string; Size: TSize): TBitmap;
+var
+  ImageFactory: IShellItemImageFactory;
+  X, Y: Integer;
+  Row: PRGBQuadArray;
+  Pixel: PRGBQuad;
+  Alpha: Byte;
+  Handle: HBITMAP;
+begin
+  Result := nil;
+  SHCreateItemFromParsingName(PChar(Path), nil, IShellItemImageFactory, ImageFactory);
+  if Assigned(ImageFactory) then
+  begin
+    if Succeeded(ImageFactory.GetImage(Size, SIIGBF_RESIZETOFIT, Handle)) then
+    begin
+      Result := TBitmap.Create;
+      try
+        Result.Handle := Handle;
+        Result.PixelFormat := pf32bit;
+
+        for Y := 0 to Result.Height - 1 do
+        begin
+          Row  := Result.ScanLine[Y];
+          for X := 0 to Result.Width - 1 do
+          begin
+            Pixel := @Row[X];
+            Alpha := Pixel.rgbReserved;
+            Pixel.rgbBlue := (Pixel.rgbBlue * Alpha) div 255;
+            Pixel.rgbGreen := (Pixel.rgbGreen * Alpha) div 255;
+            Pixel.rgbRed := (Pixel.rgbRed * Alpha) div 255;
+          end;
+        end;
+      except
+        Result.Free;
+        raise;
+      end;
+    end;
+
+    ImageFactory := nil; // Redundant?
+  end;
+end;
+
+procedure TCustomDirView.FreeThumbnails;
+begin
+  FreeAndNil(FFallbackThumbnail[True]);
+  FreeAndNil(FFallbackThumbnail[False]);
+end;
+
+function TCustomDirView.FallbackThumbnail(Dir: Boolean; Size: TSize): TBitmap;
+var
+  FallbackPath: string;
+  Existed: Boolean;
+  Index: Integer;
+begin
+  Result := nil;
+  try
+    if FFallbackThumbnailSize <> Size then
+    begin
+      FreeThumbnails;
+    end;
+
+    if not Assigned(FFallbackThumbnail[Dir]) then
+    begin
+      Index := 1;
+      repeat
+        FallbackPath := TPath.Combine(TempDir, 'default.' + IntToStr(Index) + '.thumbnailimage');
+        Existed := FileExists(FallbackPath) or DirectoryExists(FallbackPath);
+        if not Existed then
+        begin
+          if Dir then
+            CreateDir(FallbackPath)
+          else
+            TFile.WriteAllText(FallbackPath, '');
+        end;
+        Inc(Index);
+      until not Existed;
+
+      FFallbackThumbnailSize := Size;
+      FFallbackThumbnail[Dir] := GetThumbnail(FallbackPath, Size);
+      if Existed then
+      begin
+        if Dir then
+          RemoveDir(FallbackPath)
+        else
+          DeleteFile(FallbackPath);
+      end;
+    end;
+
+    Result := FFallbackThumbnail[Dir];
+  except
+  end;
+end;
+
 function TCustomDirView.GetFilesMarkedSize: Int64;
 begin
   if SelCount > 0 then Result := FilesSelSize
@@ -1485,6 +1689,11 @@ begin
     OnGetOverlay(Self, Item, Result);
 end;
 
+function TCustomDirView.IsItemVisible(Item: TListItem): Boolean;
+begin
+  Result := (ListView_IsItemVisible(Handle, Item.Index) <> 0);
+end;
+
 procedure TCustomDirView.WMKeyDown(var Message: TWMKeyDown);
 begin
   if DoubleBuffered and (Message.CharCode in [VK_PRIOR, VK_NEXT]) and
@@ -3482,6 +3691,54 @@ begin
   UpdateStatusBar;
 end;
 
+function TCustomDirView.GetDirViewStyle: TDirViewStyle;
+begin
+  if (ViewStyle = vsIcon) and FThumbnail then Result := dvsThumbnail
+    else Result := TDirViewStyle(ViewStyle);
+end;
+
+procedure TCustomDirView.SetDirViewStyle(Value: TDirViewStyle);
+var
+  NewViewStyle: TViewStyle;
+begin
+  if DirViewStyle <> Value then
+  begin
+    FThumbnail := (Value = dvsThumbnail);
+    // Create thumbnail images before recreating the view
+    NeedImageLists(False);
+    if FThumbnail then NewViewStyle := vsIcon
+      else NewViewStyle := TViewStyle(Value);
+    if ViewStyle <> NewViewStyle then
+    begin
+      ViewStyle := NewViewStyle;
+    end
+      else
+    begin
+      // Changing ViewStyle recreates the view, we want to be consistent.
+      if not (csLoading in ComponentState) then
+        RecreateWnd;
+    end;
+  end;
+end;
+
+procedure TCustomDirView.InvalidateItem(Item: TListItem);
+var
+  R: TRect;
+begin
+  R := Item.DisplayRect(drBounds);
+  // alternative to TListItem.Update (which causes flicker)
+  InvalidateRect(Handle, @R, True);
+end;
+
+procedure TCustomDirView.WMUserInvalidateItem(var Message: TMessage);
+var
+  Index: Integer;
+begin
+  Index := Integer(Message.WParam);
+  if (Index >= 0) and (Index < Items.Count) then
+    InvalidateItem(Items[Index]);
+end;
+
 initialization
   DropSourceControl := nil;
 

+ 143 - 55
source/packages/filemng/DirView.pas

@@ -74,6 +74,8 @@ type
     FileExt: string;
     TypeName: string;
     ImageIndex: Integer;
+    Thumbnail: TBitmap;
+    ThumbnailSize: TSize;
     Size: Int64;
     Attr: LongWord;
     FileTime: TFileTime;
@@ -94,6 +96,7 @@ type
   private
     FOwner: TDirView;
     FSyncIcon: Integer;
+    FSyncThumbnail: TBitmap;
     FCurrentIndex: Integer;
     FCurrentFilePath: string;
     FCurrentItemData: TFileRec;
@@ -101,6 +104,9 @@ type
   protected
     constructor Create(Owner: TDirView);
     procedure Execute; override;
+
+  public
+    destructor Destroy; override;
   end;
 
   // WORKAROUND: TQueue<Integer>.Create fails when TDirView is created in IDE, while TQueue<TIconUpdateSchedule>.Create works
@@ -200,6 +206,7 @@ type
     function GetIsRoot: Boolean; override;
     procedure InternalEdit(const HItem: TLVItem); override;
     function ItemColor(Item: TListItem): TColor; override;
+    function ItemThumbnail(Item: TListItem; Size: TSize): TBitmap; override;
     function ItemFileExt(Item: TListItem): string;
     function ItemFileNameOnly(Item: TListItem): string;
     function ItemImageIndex(Item: TListItem; Cache: Boolean): Integer; override;
@@ -211,9 +218,11 @@ type
     procedure LoadFiles; override;
     procedure PerformItemDragDropOperation(Item: TListItem; Effect: Integer; Paste: Boolean); override;
     procedure SortItems; override;
+    function ThumbnailNeeded(ItemData: PFileRec): Boolean;
     procedure DoFetchIconUpdate;
     procedure DoUpdateIcon;
     procedure StartFileDeleteThread;
+    procedure IconUpdateEnqueue(ListItem: TListItem);
     function IconUpdatePeek: Integer;
     procedure IconUpdateDequeue;
     procedure WMDestroy(var Msg: TWMDestroy); message WM_DESTROY;
@@ -363,6 +372,7 @@ type
     property ColumnClick;
     property MultiSelect;
     property ReadOnly;
+    property DirViewStyle;
 
     // The only way to make Items stored automatically and survive handle recreation.
     // Though we should implement custom persisting to avoid publishing this
@@ -395,6 +405,9 @@ uses
   ShellDialogs, IEDriveInfo,
   FileChanges, Math, PasTools, StrUtils, Types, UITypes;
 
+var
+  ThumbnailNotNeeded: TSize;
+
 var
   DaylightHack: Boolean;
 
@@ -617,6 +630,12 @@ begin
   FOwner := Owner;
 end; {TIconUpdateThread.Create}
 
+destructor TIconUpdateThread.Destroy;
+begin
+  FreeAndNil(FSyncThumbnail);
+  inherited;
+end;
+
 procedure TIconUpdateThread.Execute;
 var
   IsSpecialExt: Boolean;
@@ -631,19 +650,32 @@ begin
     if (FCurrentIndex >= 0) and
        (not Terminated) then
     begin
-      PrevPIDL := FCurrentItemData.PIDL;
-      try
-        IsSpecialExt := MatchesFileExt(FCurrentItemData.FileExt, SpecialExtensions);
-        FOwner.DoFetchIcon(FCurrentFilePath, IsSpecialExt, False, @FCurrentItemData, FSyncIcon, TypeName);
-      except
-        {Capture exceptions generated by the shell}
-        FSyncIcon := UnknownFileIcon;
+      FSyncIcon := -1;
+      if Assigned(FSyncThumbnail) then
+        FreeAndNil(FSyncThumbnail);
+
+      if FCurrentItemData.IconEmpty then
+      begin
+        PrevPIDL := FCurrentItemData.PIDL;
+        try
+          IsSpecialExt := MatchesFileExt(FCurrentItemData.FileExt, SpecialExtensions);
+          FOwner.DoFetchIcon(FCurrentFilePath, IsSpecialExt, False, @FCurrentItemData, FSyncIcon, TypeName);
+        except
+          {Capture exceptions generated by the shell}
+          FSyncIcon := UnknownFileIcon;
+        end;
+        if Assigned(FCurrentItemData.PIDL) and
+           (PrevPIDL <> FCurrentItemData.PIDL) then
+        begin
+          FreePIDL(FCurrentItemData.PIDL);
+        end;
       end;
-      if Assigned(FCurrentItemData.PIDL) and
-         (PrevPIDL <> FCurrentItemData.PIDL) then
+
+      if FOwner.ThumbnailNeeded(@FCurrentItemData) then
       begin
-        FreePIDL(FCurrentItemData.PIDL);
+        FSyncThumbnail := FOwner.GetThumbnail(FCurrentFilePath, FCurrentItemData.ThumbnailSize);
       end;
+
       if not Terminated then
       begin
         Synchronize(FOwner.DoUpdateIcon);
@@ -907,6 +939,8 @@ begin
 {$WARNINGS ON}
     Empty := True;
     IconEmpty := True;
+    Thumbnail := nil;
+    ThumbnailSize := ThumbnailNotNeeded;
     if Size > 0 then Inc(FFilesSize, Size);
     PIDL := nil;
     CalculatedSize := -1;
@@ -955,6 +989,8 @@ begin
 {$WARNINGS ON}
     Empty := True;
     IconEmpty := False;
+    Thumbnail := nil;
+    ThumbnailSize := ThumbnailNotNeeded;
     PIDL := nil;
 
     ImageIndex := StdDirIcon;
@@ -1332,7 +1368,6 @@ var
   SaveCursor: TCursor;
   FSize: Int64;
   FocusedIsVisible: Boolean;
-  R: TRect;
 begin
   if (not Loading) and LoadEnabled then
   begin
@@ -1347,29 +1382,7 @@ begin
       end
         else
       begin
-        if Assigned(ItemFocused) then
-        begin
-          R := ItemFocused.DisplayRect(drBounds);
-          // btw, we use vsReport only, nothing else was tested
-          // (We can use ListView_IsItemVisible here)
-          Assert(ViewStyle = vsReport);
-          case ViewStyle of
-            vsReport:
-              FocusedIsVisible := (TopItem.Index <= ItemFocused.Index) and
-                (ItemFocused.Index < TopItem.Index + VisibleRowCount);
-
-            vsList:
-              // do not know how to implement that
-              FocusedIsVisible := False;
-
-            else // vsIcon and vsSmallIcon
-              FocusedIsVisible :=
-                IntersectRect(R,
-                  Classes.Rect(ViewOrigin, Point(ViewOrigin.X + ClientWidth, ViewOrigin.Y + ClientHeight)),
-                  ItemFocused.DisplayRect(drBounds));
-          end;
-        end
-          else FocusedIsVisible := False; // shut up
+        FocusedIsVisible := Assigned(ItemFocused) and IsItemVisible(ItemFocused);
 
         SaveCursor := Screen.Cursor;
         Screen.Cursor := crHourGlass;
@@ -1446,9 +1459,7 @@ begin
                       FileTime := SRec.FindData.ftLastWriteTime;
 {$WARNINGS ON}
                     end;
-                    // alternative to TListItem.Update (which causes flicker)
-                    R := Items[iIndex].DisplayRect(drBounds);
-                    InvalidateRect(Handle, @R, True);
+                    InvalidateItem(Items[iIndex]);
                     AnyUpdate := True;
                   end;
                   FItems.Add(Srec.Name);
@@ -2299,7 +2310,6 @@ var
   Value: string;
   ASize: Int64;
   FetchIcon: Boolean;
-  Schedule: TIconUpdateSchedule;
 begin
   Assert(Assigned(ListItem) and Assigned(ListItem.Data));
   with PFileRec(ListItem.Data)^, DispInfo  do
@@ -2320,13 +2330,9 @@ begin
       GetDisplayData(ListItem, True);
 
     {Set IconUpdatethread :}
-    if IconEmpty and UseIconUpdateThread and (not FIsRecycleBin) and
-       (not FIconUpdateSet.ContainsKey(ListItem.Index)) then
+    if IconEmpty and UseIconUpdateThread and (not FIsRecycleBin) then
     begin
-      FIconUpdateSet.Add(ListItem.Index, False);
-      Schedule.Index := ListItem.Index;
-      FIconUpdateQueue.Enqueue(Schedule);
-      StartIconUpdateThread;
+      IconUpdateEnqueue(ListItem);
     end;
 
     if (DispInfo.Mask and LVIF_TEXT) <> 0 then
@@ -2381,6 +2387,33 @@ begin
   Result := clDefaultItemColor;
 end;
 
+function TDirView.ItemThumbnail(Item: TListItem; Size: TSize): TBitmap;
+var
+  ItemData: PFileRec;
+begin
+  if not UseIconUpdateThread then
+  begin
+    // not supported without update thread
+    Result := nil;
+  end
+    else
+  begin
+    ItemData := PFileRec(Item.Data);
+    if (not Assigned(ItemData.Thumbnail)) or
+       (ItemData.ThumbnailSize <> Size) then
+    begin
+      FreeAndNil(ItemData.Thumbnail);
+      ItemData.ThumbnailSize := Size;
+      Result := nil;
+      IconUpdateEnqueue(Item);
+    end
+      else
+    begin
+      Result := ItemData.Thumbnail;
+    end;
+  end;
+end;
+
 procedure TDirView.StartFileDeleteThread;
 var
   Files: TStringList;
@@ -2394,6 +2427,20 @@ begin
   end;
 end;
 
+procedure TDirView.IconUpdateEnqueue(ListItem: TListItem);
+var
+  Schedule: TIconUpdateSchedule;
+begin
+  if not FIconUpdateSet.ContainsKey(ListItem.Index) then
+  begin
+    FIconUpdateSet.Add(ListItem.Index, False);
+    Schedule.Index := ListItem.Index;
+    FIconUpdateQueue.Enqueue(Schedule);
+    Assert(FIconUpdateSet.Count = FIconUpdateQueue.Count + FIconUpdateQueueDeferred.Count);
+  end;
+  StartIconUpdateThread;
+end;
+
 function TDirView.IconUpdatePeek: Integer;
 begin
   if FIconUpdateQueue.Count > 0 then
@@ -2411,12 +2458,19 @@ begin
   else
     FIconUpdateQueueDeferred.Dequeue;
   FIconUpdateSet.Remove(FIconUpdateThread.FCurrentIndex);
+  Assert(FIconUpdateSet.Count = FIconUpdateQueue.Count + FIconUpdateQueueDeferred.Count);
+end;
+
+function TDirView.ThumbnailNeeded(ItemData: PFileRec): Boolean;
+begin
+  Result := (not Assigned(ItemData.Thumbnail)) and (ItemData.ThumbnailSize <> ThumbnailNotNeeded);
 end;
 
 procedure TDirView.DoFetchIconUpdate;
 var
   Item: TListItem;
   ItemData: PFileRec;
+  Invisible: Boolean;
 begin
   FIconUpdateThread.FCurrentIndex := IconUpdatePeek;
   if FIconUpdateThread.FCurrentIndex < 0 then
@@ -2432,27 +2486,50 @@ begin
       IconUpdateDequeue;
     end
       else
-    // Deprioritizing unvisible item taken from priority queue
-    if (FIconUpdateQueue.Count > 0) and
-       IsVista and (ListView_IsItemVisible(Handle, FIconUpdateThread.FCurrentIndex) = 0) then
-    begin
-      FIconUpdateQueueDeferred.Enqueue(FIconUpdateQueue.Dequeue);
-    end
-      else
     begin
       Item := Items[FIconUpdateThread.FCurrentIndex];
       Assert(Assigned(Item));
       Assert(Assigned(Item.Data));
       ItemData := PFileRec(Item.Data);
-      if not ItemData.IconEmpty then
+
+      Invisible := IsVista and (not IsItemVisible(Item));
+
+      if (Invisible or (not FThumbnail)) and
+         (not Assigned(ItemData.Thumbnail)) then
+      begin
+        ItemData.ThumbnailSize := ThumbnailNotNeeded; // not needed anymore
+      end;
+
+      // Deprioritizing unvisible item taken from priority queue.
+      // As we ask Window to cache the image index (LVIF_DI_SETITEM), we won't get asked again,
+      // so we have to retrieve the image eventually (but not thumbnail)
+      if (FIconUpdateQueue.Count > 0) and
+         Invisible then
       begin
+        Assert(not ThumbnailNeeded(ItemData));
+        if ItemData.IconEmpty then
+        begin
+          FIconUpdateQueueDeferred.Enqueue(FIconUpdateQueue.Dequeue);
+        end
+          else
+        begin
+          IconUpdateDequeue;
+        end;
         FIconUpdateThread.FCurrentIndex := -1;
-        IconUpdateDequeue;
       end
         else
       begin
-        FIconUpdateThread.FCurrentFilePath := ItemFullFileName(Item);
-        FIconUpdateThread.FCurrentItemData := ItemData^;
+        if ItemData.IconEmpty or
+           ThumbnailNeeded(ItemData) then
+        begin
+          FIconUpdateThread.FCurrentFilePath := ItemFullFileName(Item);
+          FIconUpdateThread.FCurrentItemData := ItemData^;
+        end
+          else
+        begin
+          IconUpdateDequeue;
+          FIconUpdateThread.FCurrentIndex := -1;
+        end;
       end;
     end;
   end;
@@ -2498,6 +2575,16 @@ begin
           end;
         end;
         ItemData^.IconEmpty := False;
+
+        if Assigned(FIconUpdateThread.FSyncThumbnail) then
+        begin
+          ItemData^.Thumbnail := FIconUpdateThread.FSyncThumbnail;
+          FIconUpdateThread.FSyncThumbnail := nil;
+          // It can happen that this is called while the very item is being painted,
+          // (particularly ImageFactory.GetImage pumps the queue).
+          // Calling InvalidateRect directly would get ignored then.
+          PostMessage(WindowHandle, WM_USER_INVALIDATEITEM, Item.Index, 0);
+        end;
       end;
     end;
   end;
@@ -2801,6 +2888,7 @@ begin
       SetLength(TypeName, 0);
       SetLength(DisplayName, 0);
       if Assigned(PIDL) then FreePIDL(PIDL);
+      FreeAndNil(Thumbnail);
       Dispose(PFileRec(Item.Data));
       Item.Data := nil;
     end;

+ 0 - 1
source/packages/my/IEListView.pas

@@ -147,7 +147,6 @@ type
     property ShowHint;
     property TabOrder;
     property TabStop default True;
-    property ViewStyle;
     property Visible;
     property OnChange;
     property OnChanging;

+ 21 - 10
source/packages/my/PasTools.pas

@@ -64,6 +64,7 @@ type
   TImageListSize = (ilsSmall, ilsLarge);
 
 procedure NeedShellImageLists;
+function ShellImageListForSize(Width: Integer): TImageList;
 function ShellImageListForControl(Control: TControl; Size: TImageListSize): TImageList;
 
 function ControlHasRecreationPersistenceData(Control: TControl): Boolean;
@@ -504,6 +505,7 @@ end;
 var
   ShellImageLists: TDictionary<Integer, TImageList> = nil;
 
+// This should be replaced with IShellItemImageFactory, as already used for thumbnails
 procedure InitializeShellImageLists;
 type
   TSHGetImageList = function (iImageList: integer; const riid: TGUID; var ppv: Pointer): hResult; stdcall;
@@ -551,23 +553,15 @@ begin
   end;
 end;
 
-function ShellImageListForControl(Control: TControl; Size: TImageListSize): TImageList;
+function ShellImageListForSize(Width: Integer): TImageList;
 var
   ImageListPair: TPair<Integer, TImageList>;
-  Width, ImageListWidth: Integer;
+  ImageListWidth: Integer;
   Diff, BestDiff: Integer;
 begin
   // Delay load image lists, not to waste resources in console/scripting mode
   NeedShellImageLists;
 
-  case Size of
-    ilsSmall: Width := 16;
-    ilsLarge: Width := 32;
-    else Width := 0; Assert(False);
-  end;
-
-  Width := ScaleByPixelsPerInch(Width, Control);
-
   Result := nil;
   BestDiff := -1;
   for ImageListPair in ShellImageLists do
@@ -580,6 +574,7 @@ begin
       else
     begin
       // Prefer smaller images over larger, so for 150%, we use 100% images, not 200%
+      // (a larger icon would make the item row higher)
       Diff := ImageListWidth - Width + 1;
     end;
 
@@ -591,6 +586,22 @@ begin
   end;
 end;
 
+function ShellImageListForControl(Control: TControl; Size: TImageListSize): TImageList;
+var
+  Width: Integer;
+begin
+  case Size of
+    ilsSmall: Width := 16;
+    ilsLarge: Width := 32;
+    else Width := 0; Assert(False);
+  end;
+
+  Width := ScaleByPixelsPerInch(Width, Control);
+
+  Result := ShellImageListForSize(Width);
+
+end;
+
 type
   TListViewHelper = class helper for TCustomListView
   public

+ 1 - 0
source/windows/CustomWinConfiguration.h

@@ -18,6 +18,7 @@
 // WM_USER_SHCHANGENOTIFY + 13 (packages/filemng/DriveView.pas)
 // WM_PASTE_FILES + 14 (forms/CustomScpExplorer.cpp)
 #define WM_IS_HIDDEN (WM_WINSCP_USER + 15)
+// WM_USER_INVALIDATEITEM + 16 (packages/filemng/DirView.pas)
 //---------------------------------------------------------------------------
 #define C(Property) (Property != rhc.Property) ||
 struct TSynchronizeChecklistConfiguration

+ 9 - 3
source/windows/WinConfiguration.cpp

@@ -723,7 +723,7 @@ void __fastcall TWinConfiguration::Default()
   FScpExplorer.SessionsTabs = true;
   FScpExplorer.StatusBar = true;
   FScpExplorer.LastLocalTargetDirectory = GetPersonalFolder();
-  FScpExplorer.ViewStyle = 0; /* vsIcon */
+  FScpExplorer.ViewStyle = dvsIcon;
   FScpExplorer.ShowFullAddress = true;
   FScpExplorer.DriveView = true;
   FScpExplorer.DriveViewWidth = 180;
@@ -771,6 +771,7 @@ void __fastcall TWinConfiguration::Default()
   FScpCommander.ExplorerKeyboardShortcuts = false;
   FScpCommander.SystemContextMenu = false;
   FScpCommander.RemotePanel.DirViewParams = ScpCommanderRemotePanelDirViewParamsDefault;
+  FScpCommander.RemotePanel.ViewStyle = dvsReport;
   FScpCommander.RemotePanel.StatusBar = true;
   FScpCommander.RemotePanel.DriveView = false;
   FScpCommander.RemotePanel.DriveViewHeight = 100;
@@ -779,6 +780,7 @@ void __fastcall TWinConfiguration::Default()
   FScpCommander.RemotePanel.DriveViewWidthPixelsPerInch = USER_DEFAULT_SCREEN_DPI;
   FScpCommander.RemotePanel.LastPath = UnicodeString();
   FScpCommander.LocalPanel.DirViewParams = ScpCommanderLocalPanelDirViewParamsDefault;
+  FScpCommander.LocalPanel.ViewStyle = dvsReport;
   FScpCommander.LocalPanel.StatusBar = true;
   FScpCommander.LocalPanel.DriveView = false;
   FScpCommander.LocalPanel.DriveViewHeight = 100;
@@ -787,6 +789,7 @@ void __fastcall TWinConfiguration::Default()
   FScpCommander.LocalPanel.DriveViewWidthPixelsPerInch = USER_DEFAULT_SCREEN_DPI;
   FScpCommander.LocalPanel.LastPath = UnicodeString();
   FScpCommander.OtherLocalPanelDirViewParams = FScpCommander.LocalPanel.DirViewParams;
+  FScpCommander.OtherLocalPanelViewStyle = FScpCommander.LocalPanel.ViewStyle;
   FScpCommander.OtherLocalPanelLastPath = UnicodeString();
 
   FBookmarks->Clear();
@@ -1229,6 +1232,7 @@ THierarchicalStorage * TWinConfiguration::CreateScpStorage(bool & SessionList)
   ); \
   BLOCK(L"Interface\\Commander\\LocalPanel", CANCREATE, \
     KEY(String,  ScpCommander.LocalPanel.DirViewParams); \
+    KEY(Integer, ScpCommander.LocalPanel.ViewStyle); \
     KEY(Bool,    ScpCommander.LocalPanel.StatusBar); \
     KEY(Bool,    ScpCommander.LocalPanel.DriveView); \
     KEY(Integer, ScpCommander.LocalPanel.DriveViewHeight); \
@@ -1239,6 +1243,7 @@ THierarchicalStorage * TWinConfiguration::CreateScpStorage(bool & SessionList)
   ); \
   BLOCK(L"Interface\\Commander\\RemotePanel", CANCREATE, \
     KEY(String,  ScpCommander.RemotePanel.DirViewParams); \
+    KEY(Integer, ScpCommander.RemotePanel.ViewStyle); \
     KEY(Bool,    ScpCommander.RemotePanel.StatusBar); \
     KEY(Bool,    ScpCommander.RemotePanel.DriveView); \
     KEY(Integer, ScpCommander.RemotePanel.DriveViewHeight); \
@@ -1248,8 +1253,9 @@ THierarchicalStorage * TWinConfiguration::CreateScpStorage(bool & SessionList)
     KEY(String,  ScpCommander.RemotePanel.LastPath); \
   ); \
   BLOCK(L"Interface\\Commander\\OtherLocalPanel", CANCREATE, \
-    KEYEX(String, ScpCommander.OtherLocalPanelDirViewParams, L"DirViewParams"); \
-    KEYEX(String, ScpCommander.OtherLocalPanelLastPath, L"LastPath"); \
+    KEYEX(String,  ScpCommander.OtherLocalPanelDirViewParams, L"DirViewParams"); \
+    KEYEX(Integer, ScpCommander.OtherLocalPanelViewStyle, L"ViewStyle"); \
+    KEYEX(String,  ScpCommander.OtherLocalPanelLastPath, L"LastPath"); \
   ); \
   BLOCK(L"Security", CANCREATE, \
     KEY(Bool,    FUseMasterPassword); \

+ 4 - 2
source/windows/WinConfiguration.h

@@ -35,6 +35,7 @@ struct TScpExplorerConfiguration {
 //---------------------------------------------------------------------------
 struct TScpCommanderPanelConfiguration {
   UnicodeString DirViewParams;
+  int ViewStyle;
   bool StatusBar;
   bool DriveView;
   int DriveViewHeight;
@@ -43,7 +44,7 @@ struct TScpCommanderPanelConfiguration {
   int DriveViewWidthPixelsPerInch;
   UnicodeString LastPath;
   bool __fastcall operator !=(TScpCommanderPanelConfiguration & rhc)
-    { return C(DirViewParams) C(StatusBar)
+    { return C(DirViewParams) C(ViewStyle) C(StatusBar)
         C(DriveView) C(DriveViewHeight) C(DriveViewHeightPixelsPerInch)
         C(DriveViewWidth) C(DriveViewWidthPixelsPerInch) C(LastPath) 0; };
 };
@@ -67,6 +68,7 @@ struct TScpCommanderConfiguration {
   bool ExplorerKeyboardShortcuts;
   bool SystemContextMenu;
   UnicodeString OtherLocalPanelDirViewParams;
+  int OtherLocalPanelViewStyle;
   UnicodeString OtherLocalPanelLastPath;
   bool __fastcall operator !=(TScpCommanderConfiguration & rhc)
     { return C(WindowParams) C(LocalPanelWidth) C(ToolbarsLayout) C(ToolbarsButtons)
@@ -75,7 +77,7 @@ struct TScpCommanderConfiguration {
       C(NortonLikeMode) C(PreserveLocalDirectory)
       C(CompareBySize) C(CompareByTime) C(SwappedPanels)
       C(TreeOnLeft) C(ExplorerKeyboardShortcuts) C(SystemContextMenu)
-      C(OtherLocalPanelDirViewParams) C(OtherLocalPanelLastPath) 0; };
+      C(OtherLocalPanelDirViewParams) C(OtherLocalPanelViewStyle) C(OtherLocalPanelLastPath) 0; };
 
   TCompareCriterias __fastcall CompareCriterias()
   {