- 잰 것: 탐색기 [보내기] 는 여러 개를 프로세스 하나에 인자로 모두 넘긴다(일곱 경우). 폴더 둘은 창 하나에서 폴더 비교까지 된다. 설치 파일 필요 없음 - 3개 이상: 앞의 둘 + 알림(StartupArgs.Extra · Notice) - 예전엔 말없이 버림 - 260자 넘는 경로(8.3 이름으로 넘어와 net48 이 못 찾음) 등 없는 경로: 아무것도 열지 않고 까닭 알림(Missing) - 예전엔 폴더인데 파일 비교로 가서 빈 화면 - 알림은 사용자 이름 창 뒤로 지나가지 않게 본 창이 활성이 된 뒤에 - 켤 때 보내기 바로가기가 옛 자리를 가리키면 지금 실행 파일로(RepairIfMoved, 등록한 사람만 · 바로가기 파일만 · 레지스트리 안 씀) - 사용자 결정: 떠 있을 때는 새 창 그대로. ArgsTest +16(1,411건) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
176 lines
8.2 KiB
C#
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!;
|
|
}
|
|
}
|
|
}
|