Skip to content

Commit fe42dcd

Browse files
authored
Added support for choosing custom icons (#6639)
1 parent a38471f commit fe42dcd

File tree

13 files changed

+489
-11
lines changed

13 files changed

+489
-11
lines changed

Files.Launcher/MessageHandlers/FileOperationsHandler.cs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -498,9 +498,9 @@ await Win32API.StartSTATask(() =>
498498
break;
499499

500500
case "ParseLink":
501-
var linkPath = (string)message["filepath"];
502501
try
503502
{
503+
var linkPath = (string)message["filepath"];
504504
if (linkPath.EndsWith(".lnk"))
505505
{
506506
using var link = new ShellLink(linkPath, LinkResolution.NoUIWithMsgPump, null, TimeSpan.FromMilliseconds(100));
@@ -537,22 +537,23 @@ await Win32API.StartSTATask(() =>
537537
// Could not parse shortcut
538538
Program.Logger.Warn(ex, ex.Message);
539539
await Win32API.SendMessageAsync(connection, new ValueSet()
540-
{
541-
{ "TargetPath", null },
542-
{ "Arguments", null },
543-
{ "WorkingDirectory", null },
544-
{ "RunAsAdmin", false },
545-
{ "IsFolder", false }
546-
}, message.Get("RequestID", (string)null));
540+
{
541+
{ "TargetPath", null },
542+
{ "Arguments", null },
543+
{ "WorkingDirectory", null },
544+
{ "RunAsAdmin", false },
545+
{ "IsFolder", false }
546+
}, message.Get("RequestID", (string)null));
547547
}
548548
break;
549549

550550
case "CreateLink":
551551
case "UpdateLink":
552-
var linkSavePath = (string)message["filepath"];
553-
var targetPath = (string)message["targetpath"];
554552
try
555553
{
554+
var linkSavePath = (string)message["filepath"];
555+
var targetPath = (string)message["targetpath"];
556+
556557
bool success = false;
557558
if (linkSavePath.EndsWith(".lnk"))
558559
{
@@ -584,6 +585,23 @@ await Win32API.StartSTATask(() =>
584585
}
585586
break;
586587

588+
case "SetLinkIcon":
589+
try
590+
{
591+
var linkPath = (string)message["filepath"];
592+
using var link = new ShellLink(linkPath, LinkResolution.NoUIWithMsgPump, null, TimeSpan.FromMilliseconds(100));
593+
link.IconLocation = new IconLocation((string)message["iconFile"], (int)message.Get("iconIndex", 0L));
594+
link.SaveAs(linkPath); // Overwrite if exists
595+
await Win32API.SendMessageAsync(connection, new ValueSet() { { "Success", true } }, message.Get("RequestID", (string)null));
596+
}
597+
catch (Exception ex)
598+
{
599+
// Could not create shortcut
600+
Program.Logger.Warn(ex, ex.Message);
601+
await Win32API.SendMessageAsync(connection, new ValueSet() { { "Success", false } }, message.Get("RequestID", (string)null));
602+
}
603+
break;
604+
587605
case "GetFilePermissions":
588606
{
589607
var filePathForPerm = (string)message["filepath"];

Files.Launcher/MessageHandlers/Win32MessageHandler.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ public async Task ParseArgumentsAsync(PipeStream connection, Dictionary<string,
118118
await Win32API.SendMessageAsync(connection, responseEnum, message.Get("RequestID", (string)null));
119119
break;
120120

121+
case "GetFolderIconsFromDLL":
122+
var iconInfos = Win32API.ExtractIconsFromDLL((string)message["iconFile"]);
123+
await Win32API.SendMessageAsync(connection, new ValueSet()
124+
{
125+
{ "IconInfos", JsonConvert.SerializeObject(iconInfos) },
126+
}, message.Get("RequestID", (string)null));
127+
break;
128+
129+
case "SetCustomFolderIcon":
130+
await Win32API.SendMessageAsync(connection, new ValueSet()
131+
{
132+
{ "Success", Win32API.SetCustomDirectoryIcon((string)message["folder"], (string)message["iconFile"], (int)message.Get("iconIndex", 0L)) },
133+
}, message.Get("RequestID", (string)null));
134+
break;
135+
121136
case "GetSelectedIconsFromDLL":
122137
var selectedIconInfos = Win32API.ExtractSelectedIconsFromDLL((string)message["iconFile"], JsonConvert.DeserializeObject<List<int>>((string)message["iconIndexes"]), Convert.ToInt32(message["requestedIconSize"]));
123138
await Win32API.SendMessageAsync(connection, new ValueSet()

Files.Launcher/Win32API.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,59 @@ public static IList<IconFileInfo> ExtractSelectedIconsFromDLL(string file, IList
274274
return iconsList;
275275
}
276276

277+
public static IList<IconFileInfo> ExtractIconsFromDLL(string file)
278+
{
279+
var iconsList = new List<IconFileInfo>();
280+
var currentProc = Process.GetCurrentProcess();
281+
using var icoCnt = Shell32.ExtractIcon(currentProc.Handle, file, -1);
282+
if (icoCnt == null)
283+
{
284+
return null;
285+
}
286+
int count = icoCnt.DangerousGetHandle().ToInt32();
287+
int maxIndex = count - 1;
288+
if (maxIndex == 0)
289+
{
290+
using (var icon = Shell32.ExtractIcon(currentProc.Handle, file, 0))
291+
{
292+
using var image = icon.ToBitmap();
293+
byte[] bitmapData = (byte[])new ImageConverter().ConvertTo(image, typeof(byte[]));
294+
var icoStr = Convert.ToBase64String(bitmapData, 0, bitmapData.Length);
295+
iconsList.Add(new IconFileInfo(icoStr, 0));
296+
}
297+
}
298+
else if (maxIndex > 0)
299+
{
300+
for (int i = 0; i <= maxIndex; i++)
301+
{
302+
using (var icon = Shell32.ExtractIcon(currentProc.Handle, file, i))
303+
{
304+
using var image = icon.ToBitmap();
305+
byte[] bitmapData = (byte[])new ImageConverter().ConvertTo(image, typeof(byte[]));
306+
var icoStr = Convert.ToBase64String(bitmapData, 0, bitmapData.Length);
307+
iconsList.Add(new IconFileInfo(icoStr, i));
308+
}
309+
}
310+
}
311+
else
312+
{
313+
return null;
314+
}
315+
return iconsList;
316+
}
317+
318+
public static bool SetCustomDirectoryIcon(string folderPath, string iconFile, int iconIndex = 0)
319+
{
320+
var fcs = new Shell32.SHFOLDERCUSTOMSETTINGS();
321+
fcs.dwSize = (uint)Marshal.SizeOf(fcs);
322+
fcs.dwMask = Shell32.FOLDERCUSTOMSETTINGSMASK.FCSM_ICONFILE;
323+
fcs.pszIconFile = iconFile;
324+
fcs.cchIconFile = 0;
325+
fcs.iIconIndex = iconIndex;
326+
327+
return Shell32.SHGetSetFolderCustomSettings(ref fcs, folderPath, Shell32.FCS.FCS_FORCEWRITE).Succeeded;
328+
}
329+
277330
public static void UnlockBitlockerDrive(string drive, string password)
278331
{
279332
RunPowershellCommand($"-command \"$SecureString = ConvertTo-SecureString '{password}' -AsPlainText -Force; Unlock-BitLocker -MountPoint '{drive}' -Password $SecureString\"", true);

Files/Files.csproj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@
280280
<Compile Include="UserControls\FolderEmptyIndicator.xaml.cs">
281281
<DependentUpon>FolderEmptyIndicator.xaml</DependentUpon>
282282
</Compile>
283+
<Compile Include="UserControls\ImageFromBytes.cs" />
283284
<Compile Include="UserControls\InnerNavigationToolbar.xaml.cs">
284285
<DependentUpon>InnerNavigationToolbar.xaml</DependentUpon>
285286
</Compile>
@@ -546,12 +547,18 @@
546547
<Compile Include="Views\ColumnShellPage.xaml.cs">
547548
<DependentUpon>ColumnShellPage.xaml</DependentUpon>
548549
</Compile>
550+
<Compile Include="Views\CustomFolderIcons.xaml.cs">
551+
<DependentUpon>CustomFolderIcons.xaml</DependentUpon>
552+
</Compile>
549553
<Compile Include="Views\LayoutModes\ColumnViewBase.xaml.cs">
550554
<DependentUpon>ColumnViewBase.xaml</DependentUpon>
551555
</Compile>
552556
<Compile Include="Views\LayoutModes\ColumnViewBrowser.xaml.cs">
553557
<DependentUpon>ColumnViewBrowser.xaml</DependentUpon>
554558
</Compile>
559+
<Compile Include="Views\Pages\PropertiesCustomization.xaml.cs">
560+
<DependentUpon>PropertiesCustomization.xaml</DependentUpon>
561+
</Compile>
555562
<Compile Include="Views\LayoutModes\DetailsLayoutBrowser.xaml.cs">
556563
<DependentUpon>DetailsLayoutBrowser.xaml</DependentUpon>
557564
</Compile>
@@ -1311,6 +1318,10 @@
13111318
<SubType>Designer</SubType>
13121319
<Generator>MSBuild:Compile</Generator>
13131320
</Page>
1321+
<Page Include="Views\CustomFolderIcons.xaml">
1322+
<SubType>Designer</SubType>
1323+
<Generator>MSBuild:Compile</Generator>
1324+
</Page>
13141325
<Page Include="Views\LayoutModes\ColumnViewBase.xaml">
13151326
<SubType>Designer</SubType>
13161327
<Generator>MSBuild:Compile</Generator>
@@ -1323,6 +1334,10 @@
13231334
<Generator>MSBuild:Compile</Generator>
13241335
<SubType>Designer</SubType>
13251336
</Page>
1337+
<Page Include="Views\Pages\PropertiesCustomization.xaml">
1338+
<SubType>Designer</SubType>
1339+
<Generator>MSBuild:Compile</Generator>
1340+
</Page>
13261341
<Page Include="Views\Pages\PropertiesLibrary.xaml">
13271342
<Generator>MSBuild:Compile</Generator>
13281343
<SubType>Designer</SubType>

Files/ResourceDictionaries/PropertiesStyles.xaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
<Setter Property="VerticalAlignment" Value="Top" />
99
</Style>
1010

11+
<Style x:Key="PropertiesTabGrid" TargetType="Grid">
12+
<Setter Property="Padding" Value="14,0,14,14" />
13+
<Setter Property="VerticalAlignment" Value="Top" />
14+
</Style>
15+
1116
<Thickness x:Key="PropertyNameMargin">7</Thickness>
1217
<Thickness x:Key="PropertyValueMargin">20,0,0,0</Thickness>
1318
<VerticalAlignment x:Key="PropertyValueVerticalAlignment">Center</VerticalAlignment>

Files/Strings/en-US/Resources.resw

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2791,4 +2791,13 @@ We use App Center to keep track of app usage, find bugs, and fix crashes. All in
27912791
<data name="SettingsImportErrorTitle" xml:space="preserve">
27922792
<value>Error importing settings</value>
27932793
</data>
2794+
<data name="ChooseCustomIcon" xml:space="preserve">
2795+
<value>Choose a custom folder icon</value>
2796+
</data>
2797+
<data name="Customization" xml:space="preserve">
2798+
<value>Customization</value>
2799+
</data>
2800+
<data name="RestoreDefault" xml:space="preserve">
2801+
<value>Restore default</value>
2802+
</data>
27942803
</root>

Files/UserControls/ImageFromBytes.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Files.Helpers;
2+
using Windows.UI.Xaml;
3+
using Windows.UI.Xaml.Controls;
4+
5+
namespace Files.UserControls
6+
{
7+
public class ImageFromBytes : DependencyObject
8+
{
9+
public static byte[] GetSourceBytes(DependencyObject obj)
10+
{
11+
return (byte[])obj.GetValue(SourceBytesProperty);
12+
}
13+
14+
public static void SetSourceBytes(DependencyObject obj, byte[] value)
15+
{
16+
obj.SetValue(SourceBytesProperty, value);
17+
}
18+
19+
public static readonly DependencyProperty SourceBytesProperty =
20+
DependencyProperty.RegisterAttached("SourceBytes", typeof(byte[]), typeof(ImageFromBytes), new PropertyMetadata(null, OnSourceBytesChanged));
21+
22+
private static async void OnSourceBytesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
23+
{
24+
if (d is Image image)
25+
{
26+
image.Source = await ((byte[])e.NewValue).ToBitmapAsync();
27+
}
28+
}
29+
}
30+
}

Files/Views/CustomFolderIcons.xaml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<Page
2+
x:Class="Files.Views.CustomFolderIcons"
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:UserControls="using:Files.UserControls"
6+
xmlns:common="using:Files.Common"
7+
xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters"
8+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
9+
xmlns:helpers="using:Files.Helpers"
10+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
11+
Background="Transparent"
12+
mc:Ignorable="d">
13+
14+
<Page.Resources>
15+
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
16+
</Page.Resources>
17+
18+
<Grid ColumnSpacing="4" RowSpacing="4">
19+
<Grid.ColumnDefinitions>
20+
<ColumnDefinition Width="*" />
21+
<ColumnDefinition Width="Auto" />
22+
</Grid.ColumnDefinitions>
23+
<Grid.RowDefinitions>
24+
<RowDefinition Height="Auto" />
25+
<RowDefinition Height="Auto" />
26+
<RowDefinition Height="*" />
27+
<RowDefinition Height="Auto" />
28+
29+
</Grid.RowDefinitions>
30+
31+
<TextBlock
32+
Grid.Row="0"
33+
Grid.ColumnSpan="2"
34+
FontWeight="SemiBold"
35+
Text="{helpers:ResourceString Name=ChooseCustomIcon}" />
36+
37+
<TextBox
38+
x:Name="ItemDisplayedPath"
39+
Grid.Row="1"
40+
Grid.Column="0"
41+
IsReadOnly="True" />
42+
<Button
43+
x:Name="PickDllButton"
44+
Grid.Row="1"
45+
Grid.Column="1"
46+
Click="PickDllButton_Click"
47+
Content="{helpers:ResourceString Name=Browse}" />
48+
49+
<GridView
50+
x:Name="IconSelectionGrid"
51+
Grid.Row="2"
52+
Grid.ColumnSpan="3"
53+
MaxHeight="300"
54+
HorizontalAlignment="Stretch"
55+
VerticalAlignment="Stretch"
56+
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
57+
ScrollViewer.HorizontalScrollMode="Disabled"
58+
ScrollViewer.VerticalScrollBarVisibility="Auto"
59+
ScrollViewer.VerticalScrollMode="Auto"
60+
SelectionChanged="IconSelectionGrid_SelectionChanged"
61+
SelectionMode="Single">
62+
<GridView.ItemsPanel>
63+
<ItemsPanelTemplate>
64+
<ItemsWrapGrid Orientation="Horizontal" />
65+
</ItemsPanelTemplate>
66+
</GridView.ItemsPanel>
67+
<GridView.ItemTemplate>
68+
<DataTemplate x:DataType="common:IconFileInfo">
69+
<Grid Width="16" Height="16">
70+
<Image UserControls:ImageFromBytes.SourceBytes="{x:Bind IconDataBytes, Mode=OneWay}" />
71+
</Grid>
72+
</DataTemplate>
73+
</GridView.ItemTemplate>
74+
</GridView>
75+
76+
<Button
77+
x:Name="RestoreDefaultButton"
78+
Grid.Row="3"
79+
Grid.Column="0"
80+
x:Load="{x:Bind IsShortcutItem, Converter={StaticResource BoolNegationConverter}}"
81+
Command="{x:Bind RestoreDefaultIconCommand}"
82+
Content="{helpers:ResourceString Name=RestoreDefault}" />
83+
</Grid>
84+
</Page>

0 commit comments

Comments
 (0)