336 lines
12 KiB
C#
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);
|
|
}
|
|
}
|