File context menu, element preview improvements

This commit is contained in:
2023-07-27 23:51:03 +02:00
parent fd88f6353d
commit b7e9bfc3dd
7 changed files with 411 additions and 166 deletions

View File

@@ -5,4 +5,5 @@ namespace FileTime.GuiApp.Services;
public interface IContextMenuProvider
{
List<object> GetContextMenuForFolder(IContainer container);
List<object> GetContextMenuForFile(IElement element);
}

View File

@@ -2,6 +2,7 @@ using System.Globalization;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using FileTime.App.Core.ViewModels;
using FileTime.Core.Models;
using FileTime.GuiApp.Services;
using Microsoft.Extensions.DependencyInjection;
@@ -15,12 +16,16 @@ public class ContextMenuGenerator : IValueConverter
{
_contextMenuProvider ??= DI.ServiceProvider.GetRequiredService<IContextMenuProvider>();
if (value is IContainerViewModel containerViewModel)
if (value is IContainerViewModel {Container: { } container})
{
return _contextMenuProvider.GetContextMenuForFolder(containerViewModel.Container);
return _contextMenuProvider.GetContextMenuForFolder(container);
}
else if (value is IElementViewModel {Element: { } element})
{
return _contextMenuProvider.GetContextMenuForFile(element);
}
return new object[] { new MenuItem() { Header = "asd" } };
return new object[] {new MenuItem {Header = "asd"}};
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)

View File

@@ -1,4 +1,5 @@
using System.Drawing.Imaging;
using System.Runtime.Versioning;
using Avalonia.Media.Imaging;
using FileTime.Core.Models;
using FileTime.GuiApp.Helper;
@@ -24,19 +25,20 @@ public static class WindowsSystemIconHelper
return null;
}
[SupportedOSPlatform("windows")]
public static ImagePath GetImagePathByIconPath(string path)
{
var environemntVariables = Environment.GetEnvironmentVariables();
foreach (var keyo in environemntVariables.Keys)
var environmentVariables = Environment.GetEnvironmentVariables();
foreach (var keyObject in environmentVariables.Keys)
{
if (keyo is string key && environemntVariables[key] is string value)
if (keyObject is string key && environmentVariables[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)
var (parsedResourceId, path2) = parts.Length >= 2 && long.TryParse(parts[^1], out var id)
? (id, NormalizePath(string.Join(',', parts[..^1])))
: (0, NormalizePath(path));

View File

@@ -8,4 +8,9 @@ public class LinuxContextMenuProvider : IContextMenuProvider
{
return new List<object>();
}
public List<object> GetContextMenuForFile(IElement element)
{
return new List<object>();
}
}

View File

@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Avalonia.Controls;
using Avalonia.Media;
using FileTime.Core.Models;
@@ -10,6 +11,7 @@ using Microsoft.Win32;
namespace FileTime.GuiApp.Services;
[SupportedOSPlatform("windows")]
public class WindowsContextMenuProvider : IContextMenuProvider
{
public List<object> GetContextMenuForFolder(IContainer container)
@@ -17,17 +19,15 @@ public class WindowsContextMenuProvider : IContextMenuProvider
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) throw new NotSupportedException();
var menuItems = new List<object>();
if (container.Provider is not ILocalContentProvider) return menuItems;
if (container.Provider is ILocalContentProvider)
{
using var directoryKey = Registry.ClassesRoot.OpenSubKey("Directory");
ProcessRegistryKey(directoryKey, menuItems, container!.NativePath!.Path);
}
using var directoryKey = Registry.ClassesRoot.OpenSubKey("Directory");
ProcessRegistryKeyForContainer(directoryKey, menuItems, container!.NativePath!.Path);
return menuItems;
}
private void ProcessRegistryKey(RegistryKey? contextMenuContainer, List<object> menuItems, string folderPath)
private void ProcessRegistryKeyForContainer(RegistryKey? contextMenuContainer, List<object> menuItems, string folderPath)
{
using var shell = contextMenuContainer?.OpenSubKey("shell");
if (shell == null) return;
@@ -36,119 +36,253 @@ public class WindowsContextMenuProvider : IContextMenuProvider
foreach (var shellKey in shellSubKeys.Select(k => shell.OpenSubKey(k)).OfType<RegistryKey>())
{
var textBase = shellKey.GetValue(null) as string ?? shellKey.GetValue("MUIVerb") as string;
var displayTextBase =
shellKey.GetValue(null) as string
?? shellKey.GetValue("MUIVerb") as string
?? shellKey.Name.Split('\\').Last();
if (textBase == null) continue;
string? displayText = null;
displayText = displayTextBase.StartsWith("@")
? ResolveText(displayTextBase)
: displayTextBase;
string? text = null;
if (textBase.StartsWith("@"))
if (displayText is null) continue;
displayText = displayText.Replace("&", "");
var image = shellKey.GetValue("Icon") is string iconPath
? ResolveImage(iconPath)
: null;
using var commandKey = shellKey.OpenSubKey("command");
if (commandKey?.GetValueNames().Contains("DelegateExecute") ?? false) continue;
if (GetCommandKey(shellKey, commandKey) is { } commandString)
{
var parts = textBase[1..].Split(',');
if (parts.Length == 2 && long.TryParse(parts[1], out var parsedResourceId))
{
if (parsedResourceId < 0) parsedResourceId *= -1;
text = NativeMethodHelpers.GetStringResource(string.Join(',', parts[..^1]), (uint) parsedResourceId);
}
var item = new MenuItem {Header = displayText, Icon = image};
item.Click += (o, e) => HandleStartCommandMenuItemClick(folderPath, commandString);
menuItems.Add(item);
}
else
else if (shellKey.GetValue("ExtendedSubCommandsKey") is string extendedCommands)
{
text = textBase;
var rootMenuItems = new List<object>();
ProcessRegistryKeyForContainer(Registry.ClassesRoot.OpenSubKey(extendedCommands), rootMenuItems, folderPath);
if (rootMenuItems.Count == 0) continue;
var rootMenu = new MenuItem {Header = displayText, Icon = image};
foreach (var item in rootMenuItems)
{
rootMenu.Items.Add(item);
}
menuItems.Add(rootMenu);
}
}
static string? ResolveText(string textBase)
{
var parts = textBase[1..].Split(',');
if (parts.Length == 2 && long.TryParse(parts[1], out var parsedResourceId))
{
if (parsedResourceId < 0) parsedResourceId *= -1;
return NativeMethodHelpers.GetStringResource(string.Join(',', parts[..^1]), (uint) parsedResourceId);
}
if (text != null)
{
text = text.Replace("&", "");
return null;
}
object? image = null;
try
{
if (shellKey.GetValue("Icon") is string iconPath)
{
var imagePath = WindowsSystemIconHelper.GetImagePathByIconPath(iconPath);
if (imagePath.Type == Models.ImagePathType.Raw)
{
image = new Image()
{
Source = (IImage) imagePath.Image!
};
}
}
}
catch
{
}
using var commandKey = shellKey.OpenSubKey("command");
if (shellKey.GetSubKeyNames().Contains("command") && commandKey?.GetValue(null) is string commandString)
{
var item = new MenuItem() {Header = text, Icon = image};
item.Click += (o, e) => MenuItemClick(folderPath, commandString);
menuItems.Add(item);
}
else if (shellKey.GetValue("ExtendedSubCommandsKey") is string extendedCommands)
{
var rootMenuItems = new List<object>();
ProcessRegistryKey(Registry.ClassesRoot.OpenSubKey(extendedCommands), rootMenuItems, folderPath);
var rootMenu = new MenuItem {Header = text, Icon = image};
foreach (var item in rootMenuItems)
{
rootMenu.Items.Add(item);
}
menuItems.Add(rootMenu);
}
}
static string? GetCommandKey(RegistryKey shellKey, RegistryKey? commandKey)
{
return
shellKey.GetSubKeyNames().Contains("command")
&& commandKey?.GetValue(null) is string commandString
? commandString
: null;
}
}
private static void MenuItemClick(string folderPath, string commandString)
public List<object> GetContextMenuForFile(IElement element)
{
var commandPartsWithoutAp = commandString.Split('\"').ToList();
var commandParts = new List<List<string>>();
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) throw new NotSupportedException();
for (var i = 0; i < commandPartsWithoutAp.Count; i++)
var menuItems = new List<object>();
if (element.Provider is not ILocalContentProvider) return menuItems;
var extension = element.Name.Split('.').LastOrDefault();
if (extension is null) return menuItems;
using var extensionKey = Registry.ClassesRoot.OpenSubKey("." + extension);
ProcessRegistryKeyForElement(extensionKey, menuItems, element!.NativePath!.Path);
return menuItems;
}
private void ProcessRegistryKeyForElement(RegistryKey? extensionKey, List<object> menuItems, string path)
{
var openWithItems = GetElementOpenWithItems(extensionKey, path);
if (openWithItems.Count > 0)
{
if (i % 2 == 0)
var openWithMenuItem = new MenuItem {Header = "Open with"};
foreach (var openWithItem in openWithItems)
{
commandParts.Add(commandPartsWithoutAp[i].Split(' ').ToList());
}
else
{
commandParts.Add(new List<string> {commandPartsWithoutAp[i]});
openWithMenuItem.Items.Add(openWithItem);
}
menuItems.Add(openWithMenuItem);
}
}
private List<object> GetElementOpenWithItems(RegistryKey? extensionKey, string path)
{
List<object> menuItems = new();
var openWithProgIds = extensionKey?.OpenSubKey("OpenWithProgids");
if (openWithProgIds is null) return menuItems;
foreach (var valueName in openWithProgIds.GetValueNames())
{
var programRegistryKey = Registry.ClassesRoot.OpenSubKey(valueName);
if (programRegistryKey is null) continue;
var programOpenKey = programRegistryKey.OpenSubKey("shell")?.OpenSubKey("open");
if (programOpenKey is null) continue;
//Try get display name
var displayText = GetDisplayText(programRegistryKey, programOpenKey);
//Try get executable path
var programCommandKey = programOpenKey.OpenSubKey("command");
if (programCommandKey?.GetValue(null) is not string command) continue;
var commandParts = ChopCommand(command);
var (executable, _) = TryGetExecutablePath(commandParts);
if (executable is null) continue;
displayText ??= Registry.ClassesRoot
.OpenSubKey("Local Settings")
?.OpenSubKey("Software")
?.OpenSubKey("Microsoft")
?.OpenSubKey("Windows")
?.OpenSubKey("Shell")
?.OpenSubKey("MuiCache")
?.GetValue(executable + ".FriendlyAppName") as string;
if (displayText is null) continue;
var menuItem = new MenuItem {Header = displayText, Icon = ResolveImage(executable)};
menuItem.Click +=
(_, _) => HandleStartCommandMenuItemClick(
path,
command,
commandParts: commandParts);
menuItems.Add(menuItem);
}
for (var i = 0; i < commandParts.Count; i++)
{
for (var i2 = 0; i2 < commandParts[i].Count; i2++)
{
commandParts[i][i2] = commandParts[i][i2].Replace("%1", folderPath).Replace("%V", folderPath);
}
}
return menuItems;
var commandPartsWithoutEmpty = commandParts.SelectMany(c => c).Where(c => !string.IsNullOrWhiteSpace(c)).ToList();
static string? GetDisplayText(RegistryKey programRegistryKey, RegistryKey programOpenKey)
{
if (programRegistryKey.GetValue("FriendlyAppName") is string rootFriendAppName)
return rootFriendAppName;
if (programOpenKey.GetValue("FriendlyAppName") is string openFriendAppName) return openFriendAppName;
return null;
}
}
private static void HandleStartCommandMenuItemClick(
string placeholderValue,
string commandString,
List<List<string>>? commandParts = null
)
{
commandParts ??= ChopCommand(commandString);
ReplacePlaceholders(commandParts, placeholderValue);
var commandPartsWithoutEmpty = commandParts
.SelectMany(c => c)
.Where(c => !string.IsNullOrWhiteSpace(c))
.ToList();
if (commandPartsWithoutEmpty.Count == 0)
return;
if (commandPartsWithoutEmpty.Count == 1)
{
Process.Start(commandPartsWithoutEmpty[0]);
return;
}
else if (commandPartsWithoutEmpty.Count > 1)
if (commandParts[0].Count > 0)
{
var paramStartIndex1 = -1;
var paramStartIndex2 = -1;
var (executable, lastExecutablePart) = TryGetExecutablePath(commandParts);
if (executable is not null)
{
try
{
var (paramStartX, paramStartY) = GetPositionInArrayFromBySkipping(commandParts, 1, 0, lastExecutablePart);
var arguments = SumList(commandParts, paramStartX, paramStartY);
using var process = new Process();
process.StartInfo.FileName = executable;
process.StartInfo.Arguments = arguments.TrimStart();
process.Start();
return;
}
catch
{
//TODO: error message
}
}
}
var (argumentsStartIndexX, argumentsStartIndexY) = FindArgumentStartPosition(commandParts, commandPartsWithoutEmpty);
var arguments2 = SumList(commandParts, argumentsStartIndexX, argumentsStartIndexY);
using var process2 = new Process();
process2.StartInfo.FileName = commandPartsWithoutEmpty[0];
process2.StartInfo.Arguments = arguments2;
process2.Start();
static void ReplacePlaceholders(List<List<string>> commandParts, string placeholderValue)
{
foreach (var commandPart in commandParts)
{
for (var i2 = 0; i2 < commandPart.Count; i2++)
{
commandPart[i2] = commandPart[i2]
.Replace("%1", placeholderValue)
.Replace("%V", placeholderValue);
}
}
}
static (int argumentsStartIndexX, int argumentsStartIndexY) FindArgumentStartPosition(
List<List<string>> commandParts,
List<string> commandPartsWithoutEmpty)
{
var argumentsStartIndexX = -1;
var argumentsStartIndexY = -1;
var found = false;
for (var x = 0; x < commandParts.Count && paramStartIndex1 == -1; x++)
for (var x = 0; x < commandParts.Count && argumentsStartIndexX == -1; x++)
{
for (var y = 0; y < commandParts[x].Count; y++)
{
if (found)
{
paramStartIndex1 = x;
paramStartIndex2 = y;
argumentsStartIndexX = x;
argumentsStartIndexY = y;
break;
}
@@ -159,49 +293,102 @@ public class WindowsContextMenuProvider : IContextMenuProvider
}
}
var arguments = SumList(commandParts, paramStartIndex1, paramStartIndex2);
return (argumentsStartIndexX, argumentsStartIndexY);
}
try
static (int positionX, int positionY) GetPositionInArrayFromBySkipping(
List<List<string>> data,
int skip,
int startX = 0,
int startY = 0
)
{
int skipping = 0;
var x = startX;
var y = startY;
var resultX = -1;
var resultY = -1;
for (; x < data.Count && resultX == -1; x++, y = 0)
{
using var process = new Process();
process.StartInfo.FileName = commandPartsWithoutEmpty[0];
process.StartInfo.Arguments = arguments;
process.Start();
}
catch
{
if (commandParts[0].Count > 0)
for (; y < data[x].Count; y++)
{
var executable = "";
var lastExecutablePart = 0;
for (lastExecutablePart = 0; !File.Exists(executable) && lastExecutablePart < commandParts[0].Count; lastExecutablePart++)
if (skipping == skip)
{
executable += (lastExecutablePart == 0 ? "" : " ") + commandParts[0][lastExecutablePart];
resultX = x;
resultY = y;
break;
}
lastExecutablePart--;
if (File.Exists(executable))
{
try
{
var (paramStartX, paramStartY) = GetCoordinatesFrom(commandParts, 1, 0, lastExecutablePart);
arguments = SumList(commandParts, paramStartX, paramStartY);
using var process = new Process();
process.StartInfo.FileName = executable;
process.StartInfo.Arguments = arguments;
process.Start();
}
catch
{
//TODO: error message
}
}
skipping++;
}
//TODO: ELSE error message
}
return (resultX, resultY);
}
}
private static List<List<string>> ChopCommand(string commandString)
{
var commandPartsWithoutQuotationMark = commandString.Split('\"').ToList();
var commandParts = new List<List<string>>();
for (var i = 0; i < commandPartsWithoutQuotationMark.Count; i++)
{
if (i % 2 == 0)
{
commandParts.Add(commandPartsWithoutQuotationMark[i].Split(' ').ToList());
}
else
{
commandParts.Add(new List<string> {commandPartsWithoutQuotationMark[i]});
}
}
return commandParts;
}
static (string? executable, int lastExecutablePart) TryGetExecutablePath(List<List<string>> commandParts)
{
var executable = "";
var lastExecutablePart = 0;
//Note: If the first block is empty, we will use the second one
//Note: This can happen (or rather, the common case) when the command starts with ", for example
// `"c:\...\...\xyz.exe" someParam` (without the ``)
var executableIndex = commandParts[0].Count == 0 || commandParts[0].All(string.IsNullOrWhiteSpace)
? 1
: 0;
for (; !File.Exists(executable) && lastExecutablePart < commandParts[executableIndex].Count; lastExecutablePart++)
{
executable += (lastExecutablePart == 0 ? "" : " ") + commandParts[executableIndex][lastExecutablePart];
}
if (executableIndex == 1) lastExecutablePart += commandParts[0].Count;
lastExecutablePart--;
return (File.Exists(executable) ? executable : null, lastExecutablePart);
}
private static Image? ResolveImage(string iconPath)
{
try
{
var imagePath = WindowsSystemIconHelper.GetImagePathByIconPath(iconPath);
if (imagePath.Type != Models.ImagePathType.Raw) return null;
return new Image
{
Source = (IImage) imagePath.Image!
};
}
catch
{
}
return null;
}
private static string SumList(List<List<string>> data, int paramStartIndex1, int paramStartIndex2)
@@ -224,30 +411,4 @@ public class WindowsContextMenuProvider : IContextMenuProvider
return result;
}
private static (int, int) GetCoordinatesFrom(List<List<string>> data, int skip, int startX = 0, int startY = 0)
{
int skipping = 0;
var x = startX;
var y = startY;
var resultX = -1;
var resultY = -1;
for (; x < data.Count && resultX == -1; x++, y = 0)
{
for (; y < data[x].Count; y++)
{
if (skipping == skip)
{
resultX = x;
resultY = y;
break;
}
skipping++;
}
}
return (resultX, resultY);
}
}

View File

@@ -553,12 +553,19 @@
HorizontalAlignment="Center"
IsVisible="{Binding ItemPreviewService.ItemPreview^.Mode, Converter={StaticResource EqualityConverter}, ConverterParameter={x:Static appCoreModels:ItemPreviewMode.Empty}, FallbackValue={x:Static appCoreModels:ItemPreviewMode.Unknown}}"
Text="Empty" />
<ScrollViewer IsVisible="{Binding ItemPreviewService.ItemPreview^.Mode, Converter={StaticResource EqualityConverter}, ConverterParameter={x:Static appCoreModels:ItemPreviewMode.Text}, FallbackValue={x:Static appCoreModels:ItemPreviewMode.Unknown}}">
<TextBox
IsReadOnly="True"
Text="{Binding ItemPreviewService.ItemPreview^.TextContent}"
<Grid IsVisible="{Binding ItemPreviewService.ItemPreview^.Mode, Converter={StaticResource EqualityConverter}, ConverterParameter={x:Static appCoreModels:ItemPreviewMode.Text}, FallbackValue={x:Static appCoreModels:ItemPreviewMode.Unknown}}" RowDefinitions="*, Auto">
<ScrollViewer>
<TextBox
IsReadOnly="True"
Text="{Binding ItemPreviewService.ItemPreview^.TextContent}"
x:CompileBindings="False" />
</ScrollViewer>
<TextBlock
Grid.Row="1"
Margin="5"
Text="{Binding ItemPreviewService.ItemPreview^.TextEncoding, StringFormat=Encoding: {0}}"
x:CompileBindings="False" />
</ScrollViewer>
</Grid>
</Grid>
</Grid>
</Grid>