Ubisam-MergeTool/src/MergeHub/Services/ShellIntegration.cs
ksj09406 5210ead661 11차 F-3: 탐색기 보내기로 폴더 두 개 바로 비교 · 알림 · 바로가기 저절로 고침
- 잰 것: 탐색기 [보내기] 는 여러 개를 프로세스 하나에 인자로 모두 넘긴다(일곱 경우).
  폴더 둘은 창 하나에서 폴더 비교까지 된다. 설치 파일 필요 없음
- 3개 이상: 앞의 둘 + 알림(StartupArgs.Extra · Notice) - 예전엔 말없이 버림
- 260자 넘는 경로(8.3 이름으로 넘어와 net48 이 못 찾음) 등 없는 경로: 아무것도
  열지 않고 까닭 알림(Missing) - 예전엔 폴더인데 파일 비교로 가서 빈 화면
- 알림은 사용자 이름 창 뒤로 지나가지 않게 본 창이 활성이 된 뒤에
- 켤 때 보내기 바로가기가 옛 자리를 가리키면 지금 실행 파일로(RepairIfMoved,
  등록한 사람만 · 바로가기 파일만 · 레지스트리 안 씀)
- 사용자 결정: 떠 있을 때는 새 창 그대로. ArgsTest +16(1,411건)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:57:58 +09:00

176 lines
8.2 KiB
C#

using System;
using System.IO;
using Microsoft.Win32;
namespace MergeHub.Services
{
/// <summary>
/// 탐색기에서 이 프로그램을 부를 수 있게 한다 - <b>보내기(SendTo)</b> 메뉴에 바로가기를 둔다.
///
/// 처음에는 우클릭 메뉴에 바로 넣으려고 레지스트리에 정적 verb 를 등록했다.
/// (<c>HKCU\Software\Classes\AllFilesystemObjects\shell\...</c>)
/// <b>이 PC 에서는 뜨지 않았다.</b> 확인해 보니 우클릭 메뉴에 나오는 다른 프로그램
/// (Notepad++ · Git Bash · WinMerge)은 전부 HKLM(전체 사용자)에 등록돼 있었고,
/// HKCU 에 넣은 항목은 - 우리 것이든 실험용으로 넣은 것이든 - 하나도 그려지지 않았다.
/// HKLM 은 관리자 권한이 필요해 쓸 수 없다.
///
/// 그래서 보내기로 간다. 이쪽은 레지스트리를 아예 쓰지 않고
/// <c>%APPDATA%\Microsoft\Windows\SendTo</c> 에 바로가기 파일 하나를 두는 것이 전부라
/// 권한도 필요 없고 윈도우 판을 타지 않는다. 덤이 하나 있는데, <b>여러 개를 골라
/// 한 번에 보낼 수 있다</b> - 파일 두 개를 골라 바로 비교하는 것이 그냥 된다.
///
/// 단점은 한 단계 더 들어간다는 것뿐이다(우클릭 → 보내기 → MergeHub 로 비교).
/// </summary>
public static class ShellIntegration
{
/// <summary>보내기 메뉴에 보일 이름.</summary>
public const string MenuLabel = "MergeHub 로 비교";
private static string ShortcutPath =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SendTo), MenuLabel + ".lnk");
public static bool IsRegistered
{
get
{
try
{
return File.Exists(ShortcutPath);
}
catch (IOException)
{
return false;
}
}
}
/// <summary>보내기 메뉴에 바로가기를 만든다. 이미 있으면 경로만 새로 쓴다(프로그램을 옮겼을 때).</summary>
public static void Register(string executablePath)
{
if (string.IsNullOrWhiteSpace(executablePath)) throw new ArgumentException("실행 파일 경로가 비었습니다.");
var shell = Type.GetTypeFromProgID("WScript.Shell")
?? throw new InvalidOperationException("WScript.Shell 을 찾을 수 없습니다.");
var host = Activator.CreateInstance(shell)
?? throw new InvalidOperationException("WScript.Shell 을 만들지 못했습니다.");
// 바로가기(.lnk)는 COM 으로만 만들 수 있다. 참조를 더하지 않으려고 늦은 바인딩으로 부른다.
var link = shell.InvokeMember("CreateShortcut", System.Reflection.BindingFlags.InvokeMethod,
null, host, new object[] { ShortcutPath })
?? throw new InvalidOperationException("바로가기를 만들지 못했습니다.");
var type = link.GetType();
Set(type, link, "TargetPath", executablePath);
Set(type, link, "WorkingDirectory", Path.GetDirectoryName(executablePath) ?? string.Empty);
Set(type, link, "Description", "고른 파일·폴더를 MergeHub 으로 비교합니다");
type.InvokeMember("Save", System.Reflection.BindingFlags.InvokeMethod, null, link, null);
// 예전 판이 레지스트리에 써 둔 것이 남아 있으면 같이 치운다.
RemoveLegacyRegistryEntries();
}
/// <summary>
/// 등록해 둔 바로가기가 옛 자리를 가리키면 지금 실행 파일로 고친다(11차 F-3, 사용자 결정 "켜질 때 저절로").
/// 배포 묶음을 다른 폴더로 옮기면 바로가기가 없는 파일을 가리켜 보내기가 동작하지 않았다.
///
/// 등록하지 않은 사람은 건드리지 않는다. 바로가기 파일만 다시 쓰고 레지스트리는 손대지 않는다
/// (<see cref="Register"/> 의 옛 흔적 치우기도 부르지 않는다).
/// </summary>
/// <returns>고쳤으면 옛 대상, 아니면 null.</returns>
public static string? RepairIfMoved(string executablePath)
{
if (!IsRegistered || string.IsNullOrWhiteSpace(executablePath)) return null;
var shell = Type.GetTypeFromProgID("WScript.Shell");
if (shell is null) return null;
var host = Activator.CreateInstance(shell);
if (host is null) return null;
var link = shell.InvokeMember("CreateShortcut", System.Reflection.BindingFlags.InvokeMethod,
null, host, new object[] { ShortcutPath });
if (link is null) return null;
var type = link.GetType();
var old = type.InvokeMember("TargetPath", System.Reflection.BindingFlags.GetProperty, null, link, null) as string ?? string.Empty;
if (!PointsElsewhere(old, executablePath)) return null;
Set(type, link, "TargetPath", executablePath);
Set(type, link, "WorkingDirectory", Path.GetDirectoryName(executablePath) ?? string.Empty);
type.InvokeMember("Save", System.Reflection.BindingFlags.InvokeMethod, null, link, null);
return old;
}
/// <summary>바로가기 대상이 지금 실행 파일과 다른 곳인지(대소문자 · 경로 모양은 가리지 않음).</summary>
public static bool PointsElsewhere(string? linkTarget, string? currentExecutable)
{
if (string.IsNullOrWhiteSpace(currentExecutable)) return false;
if (string.IsNullOrWhiteSpace(linkTarget)) return true;
try
{
return !string.Equals(Path.GetFullPath(linkTarget!.Trim()), Path.GetFullPath(currentExecutable!.Trim()),
StringComparison.OrdinalIgnoreCase);
}
catch (Exception e) when (e is ArgumentException || e is NotSupportedException || e is PathTooLongException)
{
return true;
}
}
private static void Set(Type type, object link, string name, string value) =>
type.InvokeMember(name, System.Reflection.BindingFlags.SetProperty, null, link, new object[] { value });
public static void Unregister()
{
try
{
if (File.Exists(ShortcutPath)) File.Delete(ShortcutPath);
}
catch (IOException)
{
// 지우지 못하면 그대로 둔다. 다시 누르면 된다.
}
RemoveLegacyRegistryEntries();
}
/// <summary>
/// 레지스트리로 등록하던 시절의 흔적. 이 PC 에서는 뜨지도 않던 것이라 남겨 둘 이유가 없다.
/// </summary>
private static void RemoveLegacyRegistryEntries()
{
string[] legacy =
{
@"Software\Classes\AllFilesystemObjects\shell\MergeHub.Compare",
@"Software\Classes\Directory\Background\shell\MergeHub.Compare",
@"Software\Classes\*\shell\MergeHub.Compare",
@"Software\Classes\Directory\shell\MergeHub.Compare",
};
foreach (var path in legacy)
{
try
{
Registry.CurrentUser.DeleteSubKeyTree(path, throwOnMissingSubKey: false);
}
catch (Exception e) when (e is ArgumentException || e is UnauthorizedAccessException)
{
// 이미 없거나 손댈 수 없으면 그만이다.
}
}
}
/// <summary>지금 돌고 있는 실행 파일. net48 에는 Environment.ProcessPath 가 없다.</summary>
public static string CurrentExecutable()
{
var path = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
return string.IsNullOrEmpty(path)
? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UbisamBase.Shell.exe")
: path!;
}
}
}