From fbf36b890b6d05fa466b876ea5e6c00bd23559f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Sat, 28 May 2022 21:59:43 +0200 Subject: [PATCH] ContextMenu (windows only) --- .../Services/IContextMenuProvider.cs | 8 + .../Avalonia/FileTime.GuiApp.App/App.axaml | 5 +- .../Avalonia/FileTime.GuiApp.App/Startup.cs | 10 + .../Converters/ContextMenuGenerator.cs | 30 +++ .../FileTime.GuiApp/FileTime.GuiApp.csproj | 2 +- .../Helper/NativeMethodHelpers.cs | 71 +++++ .../IconProviders/WindowsSystemIconHelper.cs | 79 ++++++ .../Resources/Converters.axaml | 8 + .../FileTime.GuiApp/Resources/Styles.axaml | 16 +- .../Services/LinuxContextMenuProvider.cs | 11 + .../Services/WindowsContextMenuProvider.cs | 249 ++++++++++++++++++ .../FileTime.GuiApp/Views/MainWindow.axaml | 49 ++-- 12 files changed, 508 insertions(+), 30 deletions(-) create mode 100644 src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IContextMenuProvider.cs create mode 100644 src/GuiApp/Avalonia/FileTime.GuiApp/Converters/ContextMenuGenerator.cs create mode 100644 src/GuiApp/Avalonia/FileTime.GuiApp/Helper/NativeMethodHelpers.cs create mode 100644 src/GuiApp/Avalonia/FileTime.GuiApp/IconProviders/WindowsSystemIconHelper.cs create mode 100644 src/GuiApp/Avalonia/FileTime.GuiApp/Services/LinuxContextMenuProvider.cs create mode 100644 src/GuiApp/Avalonia/FileTime.GuiApp/Services/WindowsContextMenuProvider.cs diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IContextMenuProvider.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IContextMenuProvider.cs new file mode 100644 index 0000000..8b62efa --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IContextMenuProvider.cs @@ -0,0 +1,8 @@ +using FileTime.Core.Models; + +namespace FileTime.GuiApp.Services; + +public interface IContextMenuProvider +{ + List GetContextMenuForFolder(IContainer container); +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml index 35ebcd0..ad373e1 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml @@ -1,9 +1,8 @@ + @@ -13,7 +12,7 @@ - + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs index 60e76df..2b9201d 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Runtime.InteropServices; using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; using FileTime.Core.Interactions; @@ -42,6 +43,15 @@ public static class Startup serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(s => s.GetRequiredService()); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + serviceCollection.AddSingleton(); + } + else + { + serviceCollection.AddSingleton(); + } + return serviceCollection .AddSingleton() .AddSingleton(); diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/ContextMenuGenerator.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/ContextMenuGenerator.cs new file mode 100644 index 0000000..184c8fd --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/ContextMenuGenerator.cs @@ -0,0 +1,30 @@ +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using FileTime.App.Core.ViewModels; +using FileTime.GuiApp.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace FileTime.GuiApp.Converters; + +public class ContextMenuGenerator : IValueConverter +{ + private IContextMenuProvider? _contextMenuProvider; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + _contextMenuProvider ??= DI.ServiceProvider.GetRequiredService(); + + if (value is IContainerViewModel containerViewModel) + { + return _contextMenuProvider.GetContextMenuForFolder(containerViewModel.Container); + } + + return new object[] { new MenuItem() { Header = "asd" } }; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj b/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj index 42bbdb3..afce661 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Helper/NativeMethodHelpers.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Helper/NativeMethodHelpers.cs new file mode 100644 index 0000000..cd1fd02 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Helper/NativeMethodHelpers.cs @@ -0,0 +1,71 @@ +using System.Drawing; +using System.Runtime.InteropServices; +using System.Text; + +namespace FileTime.GuiApp.Helper; + +public static class WindowsNativeMethods +{ + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] + internal static extern IntPtr LoadLibrary(string lpLibFileName); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] + internal static extern int LoadString(IntPtr hInstance, uint wID, StringBuilder lpBuffer, int nBufferMax); + + [DllImport("user32.dll")] + public static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName); + + [DllImport("kernel32.dll")] + public static extern int FreeLibrary(IntPtr hLibModule); + + [DllImport("shell32.dll")] + public static extern IntPtr ExtractAssociatedIcon(IntPtr hInst, StringBuilder lpIconPath, out ushort lpiIcon); +} + +public static class NativeMethodHelpers +{ + public static string GetStringResource(string fileName, uint resourceId) + { + IntPtr? handle = null; + try + { + handle = WindowsNativeMethods.LoadLibrary(fileName); + StringBuilder buffer = new(8192); //Buffer for output from LoadString() + int length = WindowsNativeMethods.LoadString(handle.Value, resourceId, buffer, buffer.Capacity); + return buffer.ToString(0, length); //Return the part of the buffer that was used. + } + finally + { + if (handle is IntPtr validHandle) + { + WindowsNativeMethods.FreeLibrary(validHandle); + } + } + } + + public static Icon GetIconResource(string fileName, uint resourceId) + { + IntPtr? handle = null; + try + { + handle = WindowsNativeMethods.LoadLibrary(fileName); + IntPtr handle2 = WindowsNativeMethods.LoadIcon(handle.Value, new IntPtr(resourceId)); + return Icon.FromHandle(handle2); + } + finally + { + if (handle is IntPtr validHandle) + { + WindowsNativeMethods.FreeLibrary(validHandle); + } + } + } + + /*public static Icon GetAssociatedIcon() + { + ushort uicon; + StringBuilder strB = new StringBuilder(fileName); + IntPtr handle = WindowsNativeMethods.ExtractAssociatedIcon(this.Handle, strB, out uicon); + return Icon.FromHandle(handle); + }*/ +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/IconProviders/WindowsSystemIconHelper.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/IconProviders/WindowsSystemIconHelper.cs new file mode 100644 index 0000000..9d460ac --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/IconProviders/WindowsSystemIconHelper.cs @@ -0,0 +1,79 @@ +using System.Drawing.Imaging; +using Avalonia.Media.Imaging; +using FileTime.Core.Models; +using FileTime.GuiApp.Helper; +using FileTime.GuiApp.Models; + +namespace FileTime.GuiApp.IconProviders; + +public static class WindowsSystemIconHelper + { + public static ImagePath? GetImageByDesktopIni(IContainer folder) + { + var file = new FileInfo(Path.Combine(folder.NativePath.Path, "desktop.ini")); + if (file.Exists) + { + var lines = File.ReadAllLines(file.FullName); + if (Array.Find(lines, l => l.StartsWith("iconresource", StringComparison.OrdinalIgnoreCase)) is string iconLine) + { + var nameLineValue = string.Join('=', iconLine.Split('=')[1..]); + return GetImagePathByIconPath(nameLineValue); + } + } + + return null; + } + + public static ImagePath GetImagePathByIconPath(string path) + { + var environemntVariables = Environment.GetEnvironmentVariables(); + foreach (var keyo in environemntVariables.Keys) + { + if (keyo is string key && environemntVariables[key] is string value) + { + path = path.Replace($"%{key}%", value); + } + } + + var parts = path.Split(','); + (var parsedResourceId, var path2) = parts.Length >= 2 && long.TryParse(parts[^1], out var id) + ? (id, NormalizePath(string.Join(',', parts[..^1]))) + : (0, NormalizePath(path)); + + if (parsedResourceId == 0) + { + using var extractedIconAsStream = new MemoryStream(); + using var extractedIcon = System.Drawing.Icon.ExtractAssociatedIcon(path2).ToBitmap(); + extractedIcon.Save(extractedIconAsStream, ImageFormat.Png); + extractedIconAsStream.Position = 0; +#pragma warning disable IDISP004 // Don't ignore created IDisposable + return new ImagePath(ImagePathType.Raw, new Bitmap(extractedIconAsStream)); +#pragma warning restore IDISP004 // Don't ignore created IDisposable + } + else + { + if (parsedResourceId < 0) parsedResourceId *= -1; + + using var extractedIcon = NativeMethodHelpers.GetIconResource(path2, (uint)parsedResourceId).ToBitmap(); + + using var extractedIconAsStream = new MemoryStream(); + extractedIcon.Save(extractedIconAsStream, ImageFormat.Png); + extractedIconAsStream.Position = 0; + +#pragma warning disable IDISP004 // Don't ignore created IDisposable + return new ImagePath(ImagePathType.Raw, new Bitmap(extractedIconAsStream)); +#pragma warning restore IDISP004 // Don't ignore created IDisposable + + } + } + + private static string NormalizePath(string path) + { + if (path.StartsWith('\"') && path.EndsWith('\"')) + { + return path[1..^1]; + } + + return path; + } + } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Converters.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Converters.axaml index 5804180..381e21d 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Converters.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Converters.axaml @@ -1,6 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Styles.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Styles.axaml index 23b3bb2..f3d2e0a 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Styles.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Resources/Styles.axaml @@ -1,5 +1,13 @@ + + + + + + + + -