1
0
Fork 0

feat: 배치 결합 모드 + IExplorerCommand DLL 카스케이드

#10 다중 이미지 → 단일 PDF/TIFF/GIF 결합
- ConversionEngine: BatchMode { Independent, CombineToSingle } enum 추가
- ConvertManyAsync에 batchMode 파라미터, CombineToSingle 시 N:1 라우팅
- CombineAsync: MagickImageCollection으로 모든 입력 디코드 후
  단일 다중-페이지 파일로 출력 (PDF/TIFF/GIF만 지원)
- 결합 가능 검증: ImageMagick 디코드 가능 입력 + 결합 가능 출력
- 정적 헬퍼 CanCombine(outputExt), CanCombineInput(sourcePath)

UI
- MainWindow.xaml에 '단일 파일로 결합' CheckBox 추가
  (출력이 결합 가능 + 큐 ≥2 + 모든 입력 결합 가능일 때 활성화)
- 큐 변경/형식 변경 시 토글 상태 + 안내 텍스트 자동 갱신
- MainWindow.xaml.cs UpdateCombineState / OnCombineToggleChanged

#9 IExplorerCommand DLL 카스케이드 (MSIX 메인 메뉴)
- dllmain.cpp 전면 재작성:
  RootCascadeCommand (CLSID F1A2B3C4-...-2B3C4D5E6F70)
  + 11개 동적 SubVerbCommand
  + SubCommandEnumerator (IEnumExplorerCommand)
  + ECF_HASSUBCOMMANDS 플래그
- 서브메뉴 항목: JPEG/PNG/WebP/PDF/TXT/DOCX/AVIF/GIF/TIFF/BMP/변환…
- 기존 Quick/Dialog 핸들러는 backward compat용으로 유지
- 빌드: x64 Release Everything2Everything.Shell.dll (207KB)

Package.appxmanifest
- com:Class에 RootCascadeCommand CLSID 등록
- 모든 desktop5:ItemType verb를 Quick+Dialog 두 개에서 단일 Convert로 통합
  (29개 ItemType × 2 verbs → 29 × 1 verb)

quick CLI는 기본 .jpg 호환 유지, dialog는 메인 창에서 형식 선택

빌드: dotnet 0 errors, MSBuild C++ 0 errors
This commit is contained in:
Yun Chan 2026-05-07 15:46:38 +09:00
parent 9b7e5f0d0c
commit 2633a928c1
7 changed files with 452 additions and 68 deletions

View file

@ -1,7 +1,11 @@
// Everything2Everything shell extension — IExplorerCommand handlers
// Two verbs:
// - QuickCommandHandler → "Everything2Everything.exe quick "<paths>""
// - DialogCommandHandler → "Everything2Everything.exe dialog "<paths>""
// Everything2Everything shell extension — IExplorerCommand cascade
//
// Root verb (cascade) → "Everything2Everything으로 변환"
// ├─ ToJpg / ToPng / ... → "exe to <ext> <paths>"
// └─ Dialog → "exe dialog <paths>"
//
// Legacy CLSIDs (Quick, Dialog) are kept for backward compat with any
// external registrations that may still reference them.
#include "pch.h"
@ -10,6 +14,8 @@
using Microsoft::WRL::ClassicCom;
using Microsoft::WRL::ComPtr;
using Microsoft::WRL::InhibitRoOriginateError;
using Microsoft::WRL::Make;
using Microsoft::WRL::MakeAndInitialize;
using Microsoft::WRL::Module;
using Microsoft::WRL::ModuleType;
using Microsoft::WRL::RuntimeClass;
@ -101,6 +107,201 @@ HRESULT LaunchAppWithItems(const wchar_t* verb, IShellItemArray* items) {
return S_OK;
}
// ---------------------------------------------------------------------
// Sub-command — leaf in the cascade (e.g., "JPEG (.jpg)" → "to jpg")
// ---------------------------------------------------------------------
class SubVerbCommand : public RuntimeClass<
RuntimeClassFlags<ClassicCom | InhibitRoOriginateError>,
IExplorerCommand>
{
public:
HRESULT RuntimeClassInitialize() { return S_OK; }
void Configure(std::wstring title, std::wstring verb) {
title_ = std::move(title);
verb_ = std::move(verb);
}
IFACEMETHODIMP GetTitle(IShellItemArray*, PWSTR* name) override {
return SHStrDupW(title_.c_str(), name);
}
IFACEMETHODIMP GetIcon(IShellItemArray*, PWSTR* icon) override {
auto exe = ResolveExePath();
return SHStrDupW(exe.c_str(), icon);
}
IFACEMETHODIMP GetToolTip(IShellItemArray*, PWSTR* infoTip) override {
*infoTip = nullptr;
return E_NOTIMPL;
}
IFACEMETHODIMP GetCanonicalName(GUID* guidCommandName) override {
*guidCommandName = GUID_NULL;
return S_OK;
}
IFACEMETHODIMP GetState(IShellItemArray*, BOOL, EXPCMDSTATE* cmdState) override {
*cmdState = ECS_ENABLED;
return S_OK;
}
IFACEMETHODIMP GetFlags(EXPCMDFLAGS* flags) override {
*flags = ECF_DEFAULT;
return S_OK;
}
IFACEMETHODIMP EnumSubCommands(IEnumExplorerCommand** enumCommands) override {
*enumCommands = nullptr;
return E_NOTIMPL;
}
IFACEMETHODIMP Invoke(IShellItemArray* items, IBindCtx*) override {
return LaunchAppWithItems(verb_.c_str(), items);
}
private:
std::wstring title_;
std::wstring verb_;
};
// ---------------------------------------------------------------------
// IEnumExplorerCommand — feeds children to Explorer
// ---------------------------------------------------------------------
class SubCommandEnumerator : public RuntimeClass<
RuntimeClassFlags<ClassicCom | InhibitRoOriginateError>,
IEnumExplorerCommand>
{
public:
HRESULT RuntimeClassInitialize() { return S_OK; }
void Configure(std::vector<ComPtr<IExplorerCommand>> commands) {
commands_ = std::move(commands);
index_ = 0;
}
IFACEMETHODIMP Next(ULONG celt, IExplorerCommand** apUICommand, ULONG* pceltFetched) override {
if (!apUICommand) return E_POINTER;
ULONG fetched = 0;
for (; fetched < celt && index_ < commands_.size(); ++fetched, ++index_) {
apUICommand[fetched] = commands_[index_].Get();
apUICommand[fetched]->AddRef();
}
if (pceltFetched) *pceltFetched = fetched;
return (fetched == celt) ? S_OK : S_FALSE;
}
IFACEMETHODIMP Skip(ULONG celt) override {
size_t remaining = commands_.size() - index_;
index_ += static_cast<size_t>(std::min<ULONG>(celt, static_cast<ULONG>(remaining)));
return S_OK;
}
IFACEMETHODIMP Reset() override {
index_ = 0;
return S_OK;
}
IFACEMETHODIMP Clone(IEnumExplorerCommand** ppenum) override {
if (!ppenum) return E_POINTER;
ComPtr<SubCommandEnumerator> clone;
RETURN_IF_FAILED(MakeAndInitialize<SubCommandEnumerator>(&clone));
clone->Configure(commands_);
clone->index_ = index_;
return clone.CopyTo(ppenum);
}
private:
std::vector<ComPtr<IExplorerCommand>> commands_;
size_t index_ = 0;
};
// ---------------------------------------------------------------------
// Root cascade — "Everything2Everything으로 변환" with 11 sub-items
// ---------------------------------------------------------------------
struct SubItem {
const wchar_t* Title;
const wchar_t* Verb;
};
inline ComPtr<SubVerbCommand> MakeSub(const wchar_t* title, const wchar_t* verb) {
ComPtr<SubVerbCommand> cmd;
MakeAndInitialize<SubVerbCommand>(&cmd);
cmd->Configure(title, verb);
return cmd;
}
} // namespace
class __declspec(uuid("F1A2B3C4-D5E6-4789-9A01-2B3C4D5E6F70"))
RootCascadeCommand final
: public RuntimeClass<
RuntimeClassFlags<ClassicCom | InhibitRoOriginateError>,
IExplorerCommand>
{
public:
IFACEMETHODIMP GetTitle(IShellItemArray*, PWSTR* name) override {
return SHStrDupW(L"Everything2Everything으로 변환", name);
}
IFACEMETHODIMP GetIcon(IShellItemArray*, PWSTR* icon) override {
auto exe = ResolveExePath();
return SHStrDupW(exe.c_str(), icon);
}
IFACEMETHODIMP GetToolTip(IShellItemArray*, PWSTR* infoTip) override {
*infoTip = nullptr;
return E_NOTIMPL;
}
IFACEMETHODIMP GetCanonicalName(GUID* guidCommandName) override {
*guidCommandName = __uuidof(RootCascadeCommand);
return S_OK;
}
IFACEMETHODIMP GetState(IShellItemArray*, BOOL, EXPCMDSTATE* cmdState) override {
*cmdState = ECS_ENABLED;
return S_OK;
}
IFACEMETHODIMP GetFlags(EXPCMDFLAGS* flags) override {
*flags = ECF_HASSUBCOMMANDS;
return S_OK;
}
IFACEMETHODIMP EnumSubCommands(IEnumExplorerCommand** enumCommands) override {
if (!enumCommands) return E_POINTER;
*enumCommands = nullptr;
std::vector<ComPtr<IExplorerCommand>> cmds;
cmds.reserve(11);
cmds.push_back(MakeSub(L"JPEG (.jpg)", L"to jpg"));
cmds.push_back(MakeSub(L"PNG (.png)", L"to png"));
cmds.push_back(MakeSub(L"WebP (.webp)", L"to webp"));
cmds.push_back(MakeSub(L"PDF (.pdf)", L"to pdf"));
cmds.push_back(MakeSub(L"텍스트 (.txt) — OCR", L"to txt"));
cmds.push_back(MakeSub(L"Word (.docx) — OCR", L"to docx"));
cmds.push_back(MakeSub(L"AVIF (.avif)", L"to avif"));
cmds.push_back(MakeSub(L"GIF (.gif)", L"to gif"));
cmds.push_back(MakeSub(L"TIFF (.tif)", L"to tif"));
cmds.push_back(MakeSub(L"BMP (.bmp)", L"to bmp"));
cmds.push_back(MakeSub(L"변환… (옵션 선택)", L"dialog"));
ComPtr<SubCommandEnumerator> enumerator;
RETURN_IF_FAILED(MakeAndInitialize<SubCommandEnumerator>(&enumerator));
enumerator->Configure(std::move(cmds));
return enumerator.CopyTo(enumCommands);
}
IFACEMETHODIMP Invoke(IShellItemArray*, IBindCtx*) override {
// Root verb itself does nothing — children carry the action.
return S_OK;
}
};
// ---------------------------------------------------------------------
// Legacy non-cascade verbs — kept so any older registrations still work.
// ---------------------------------------------------------------------
template <typename Derived>
class CommandHandlerBase : public RuntimeClass<
RuntimeClassFlags<ClassicCom | InhibitRoOriginateError>,
@ -146,8 +347,6 @@ public:
}
};
} // namespace
class __declspec(uuid("801B2DD3-632C-4731-9510-AEAE09345264"))
QuickCommandHandler final
: public CommandHandlerBase<QuickCommandHandler>
@ -166,8 +365,10 @@ public:
static constexpr const wchar_t* Verb() { return L"dialog"; }
};
CoCreatableClass(RootCascadeCommand)
CoCreatableClass(QuickCommandHandler)
CoCreatableClass(DialogCommandHandler)
CoCreatableClassWrlCreatorMapInclude(RootCascadeCommand)
CoCreatableClassWrlCreatorMapInclude(QuickCommandHandler)
CoCreatableClassWrlCreatorMapInclude(DialogCommandHandler)