1
0
Fork 0
Everything2Everything/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs

336 lines
12 KiB
C#

using Everything2Everything.Core;
using Everything2Everything.Core.Providers;
using Microsoft.Win32;
namespace Everything2Everything.App.Shell;
public static class ContextMenuRegistrar
{
private const string MainVerb = "Everything2Everything";
private const string MainLabel = "Everything2Everything으로 변환";
private const string SubmenuKeyPrefix = "Everything2Everything.SubMenu.";
public const string ClassicContextMenuOverrideKey = @"Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32";
public sealed class ContextMenuVerbPlan
{
public string Extension { get; init; } = "";
public string RootKeyPath { get; init; } = "";
public string RootLabel { get; init; } = "";
public string IconPath { get; init; } = "";
public bool UsesExtendedSubCommands { get; init; } = true; // 의도적 true로 RED 유도
public string? SubCommandsValue { get; init; } = null;
public IReadOnlyList<ContextMenuItemPlan> Items { get; init; } = Array.Empty<ContextMenuItemPlan>();
}
public sealed class ContextMenuItemPlan
{
public string SubKeyPath { get; init; } = "";
public string VerbName { get; init; } = "";
public string Label { get; init; } = "";
public string IconPath { get; init; } = "";
public string Command { get; init; } = "";
}
public static ContextMenuVerbPlan BuildVerbPlan(ConversionEngine engine, string ext, string exePath)
{
var availableOutputs = GetAvailableOutputs(engine, ext);
var icon = exePath + ",0";
var rootKeyPath = $@"Software\Classes\SystemFileAssociations\{ext}\shell\{MainVerb}";
var items = new List<ContextMenuItemPlan>();
foreach (var (outExt, outLabel, sortPrefix) in availableOutputs)
{
var subVerbName = $"{sortPrefix}_{outExt.TrimStart('.')}";
var cliExt = outExt.TrimStart('.');
items.Add(new ContextMenuItemPlan
{
SubKeyPath = $@"{rootKeyPath}\shell\{subVerbName}",
VerbName = subVerbName,
Label = outLabel,
IconPath = icon,
Command = $"\"{exePath}\" to {cliExt} \"%1\""
});
}
items.Add(new ContextMenuItemPlan
{
SubKeyPath = $@"{rootKeyPath}\shell\98_dialog",
VerbName = "98_dialog",
Label = "변환… (옵션 선택)",
IconPath = icon,
Command = $"\"{exePath}\" dialog \"%1\""
});
return new ContextMenuVerbPlan
{
Extension = ext,
RootKeyPath = rootKeyPath,
RootLabel = MainLabel,
IconPath = icon,
UsesExtendedSubCommands = false, // Shift 불필요: 마우스 일반 우클릭으로 즉시 펼쳐짐
SubCommandsValue = "",
Items = items
};
}
private static readonly (string Ext, string Label, string SortPrefix)[] PopularOutputs =
{
(".jpg", "JPEG (.jpg)", "01"),
(".png", "PNG (.png)", "02"),
(".webp", "WebP (.webp)", "03"),
(".pdf", "PDF (.pdf)", "04"),
(".docx", "Word (.docx)", "05"),
(".html", "HTML (.html)", "06"),
(".md", "Markdown (.md)", "07"),
(".txt", "텍스트 (.txt)", "08"),
(".avif", "AVIF (.avif)", "09"),
(".gif", "GIF (.gif)", "10"),
(".tif", "TIFF (.tif)", "11"),
(".bmp", "BMP (.bmp)", "12"),
(".mp4", "MP4 (.mp4)", "13"),
(".mp3", "MP3 (.mp3)", "14"),
(".xlsx", "Excel (.xlsx)", "15"),
};
public static IReadOnlyList<(string Ext, string Label, string SortPrefix)> GetAvailableOutputs(ConversionEngine engine, string ext)
{
var outputs = engine.Providers.OutputsForInput(ext)
.Select(o => o.ToLowerInvariant())
.ToHashSet();
return PopularOutputs
.Where(p => outputs.Contains(p.Ext) && !string.Equals(p.Ext, ext, StringComparison.OrdinalIgnoreCase))
.ToList();
}
public static void Register(ConversionEngine engine)
{
var exe = GetAppExecutablePath();
foreach (var ext in CollectInputExtensions(engine))
{
var plan = BuildVerbPlan(engine, ext, exe);
if (plan.Items.Count == 0) continue;
WriteCascade(plan, ext);
}
TryRegisterWindows11SparsePackage();
NotifyShell();
}
public static void Unregister(ConversionEngine engine)
{
foreach (var ext in CollectInputExtensions(engine))
{
DeleteVerb(ext, MainVerb);
DeleteVerb(ext, "Everything2Everything.Quick");
DeleteVerb(ext, "Everything2Everything.Dialog");
DeleteSubmenuTree(ext);
}
TryUnregisterWindows11SparsePackage();
NotifyShell();
}
public static bool IsClassicContextMenuEnabled()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(ClassicContextMenuOverrideKey);
return key != null;
}
catch
{
return false;
}
}
public static void SetClassicContextMenuEnabled(bool enable)
{
try
{
if (enable)
{
using var key = Registry.CurrentUser.CreateSubKey(ClassicContextMenuOverrideKey, writable: true);
key?.SetValue(null, "", RegistryValueKind.String);
}
else
{
Registry.CurrentUser.DeleteSubKeyTree(@"Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}", throwOnMissingSubKey: false);
}
NotifyShell();
}
catch
{
}
}
private static IEnumerable<string> CollectInputExtensions(ConversionEngine engine)
{
return engine.Providers.Implemented
.Where(p => p.Capability.CanRegisterContextMenu)
.SelectMany(p => p.Capability.InputExtensions)
.Select(e => e.StartsWith('.') ? e : "." + e)
.Select(e => e.ToLowerInvariant())
.Distinct();
}
private static void WriteCascade(ContextMenuVerbPlan plan, string ext)
{
// 1. 루트 키 등록 (Shift 없는 일반 우클릭에서 즉시 서브메뉴 노출)
using (var verbKey = Registry.CurrentUser.CreateSubKey(plan.RootKeyPath, writable: true)
?? throw new InvalidOperationException($"레지스트리 키 생성 실패: {plan.RootKeyPath}"))
{
verbKey.SetValue(null, plan.RootLabel, RegistryValueKind.String);
verbKey.SetValue("MUIVerb", plan.RootLabel, RegistryValueKind.String);
verbKey.SetValue("Icon", plan.IconPath, RegistryValueKind.String);
verbKey.SetValue("SubCommands", plan.SubCommandsValue ?? "", RegistryValueKind.String);
// 구버전에서 Shift 키를 강제하던 ExtendedSubCommandsKey 제거
try { verbKey.DeleteValue("ExtendedSubCommandsKey", throwOnMissingValue: false); } catch { }
try { verbKey.DeleteSubKeyTree("command", throwOnMissingSubKey: false); } catch { }
}
// 2. 루트 직하위 shell 키 정리 및 서브메뉴 항목 등록
var shellRootPath = $@"{plan.RootKeyPath}\shell";
using (var existing = Registry.CurrentUser.OpenSubKey(shellRootPath, writable: true))
{
if (existing is not null)
{
foreach (var name in existing.GetSubKeyNames())
{
try { existing.DeleteSubKeyTree(name, throwOnMissingSubKey: false); } catch { }
}
}
}
foreach (var item in plan.Items)
{
using var key = Registry.CurrentUser.CreateSubKey(item.SubKeyPath, writable: true)
?? throw new InvalidOperationException($"서브메뉴 키 생성 실패: {item.SubKeyPath}");
key.SetValue(null, item.Label, RegistryValueKind.String);
key.SetValue("MUIVerb", item.Label, RegistryValueKind.String);
key.SetValue("Icon", item.IconPath, RegistryValueKind.String);
using var commandKey = key.CreateSubKey("command", writable: true)
?? throw new InvalidOperationException("command 하위 키 생성 실패");
commandKey.SetValue(null, item.Command, RegistryValueKind.String);
}
// 3. 구버전 독립 SubMenu 키 정리 (이전 버전 찌꺼기 제거)
DeleteSubmenuTree(ext);
}
private static void DeleteVerb(string ext, string verb)
{
var parentPath = $@"Software\Classes\SystemFileAssociations\{ext}\shell";
try
{
using var parent = Registry.CurrentUser.OpenSubKey(parentPath, writable: true);
parent?.DeleteSubKeyTree(verb, throwOnMissingSubKey: false);
}
catch
{
}
}
private static void DeleteSubmenuTree(string ext)
{
var submenuKeyName = SubmenuKeyPrefix + ext.TrimStart('.');
var path = $@"Software\Classes\{submenuKeyName}";
try
{
Registry.CurrentUser.DeleteSubKeyTree(path, throwOnMissingSubKey: false);
}
catch
{
}
}
private static string GetAppExecutablePath()
{
var exe = Environment.ProcessPath;
if (!string.IsNullOrEmpty(exe) && File.Exists(exe)) return exe;
return AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar + "Everything2Everything.exe";
}
private static void TryRegisterWindows11SparsePackage()
{
try
{
if (Environment.OSVersion.Version.Build < 22000) return;
var manifestPath = FindAppxManifestPath();
if (string.IsNullOrEmpty(manifestPath) || !File.Exists(manifestPath)) return;
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-NoProfile -NonInteractive -WindowStyle Hidden -Command \"Add-AppxPackage -Register '{manifestPath}'\"",
CreateNoWindow = true,
UseShellExecute = false
};
using var proc = System.Diagnostics.Process.Start(psi);
proc?.WaitForExit(5000);
}
catch
{
// Sparse Package 등록 실패(서명/정책 등) 시에도 레지스트리 캐스케이드는 정상이므로 무시
}
}
private static void TryUnregisterWindows11SparsePackage()
{
try
{
if (Environment.OSVersion.Version.Build < 22000) return;
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-NoProfile -NonInteractive -WindowStyle Hidden -Command \"Get-AppxPackage -Name '*Everything2Everything*' | Remove-AppxPackage\"",
CreateNoWindow = true,
UseShellExecute = false
};
using var proc = System.Diagnostics.Process.Start(psi);
proc?.WaitForExit(5000);
}
catch
{
}
}
public static string? FindAppxManifestPath()
{
var baseDir = AppContext.BaseDirectory;
var candidates = new[]
{
Path.Combine(baseDir, "AppxManifest.xml"),
Path.Combine(baseDir, "Package.appxmanifest"),
Path.Combine(baseDir, "..", "packaging", "Package.appxmanifest"),
Path.Combine(baseDir, "..", "..", "packaging", "Package.appxmanifest")
};
foreach (var candidate in candidates)
{
if (File.Exists(candidate)) return Path.GetFullPath(candidate);
}
return null;
}
private static void NotifyShell()
{
try { NativeMethods.SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero); }
catch { }
}
private static class NativeMethods
{
[System.Runtime.InteropServices.DllImport("shell32.dll")]
public static extern void SHChangeNotify(int wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
}
}