UI 및 미리보기 확장
이 장에서는 검색 창 사이드바를 확장하고, 커스텀 테이블 열을 추가하며, 퀵 패널 동적 탭을 제공하고, 검색 창 시계 줄의 날짜 텍스트를 제공하며, QuickLook 파일 미리보기 렌더러와 썸네일 추출자를 만들고, WPF 테마와 다국어 언어 팩을 배포하기 위한 Lertaro.PluginSdk 인터페이스를 다룹니다.
이 인터페이스들은 모두 Lertaro.PluginSdk.Abstractions.Plugins 아래에 위치하며(미리보기 제공자는 …Abstractions.Plugins.Preview) 전부 IPluginComponent에서 파생됩니다. IPluginComponent가 제공하는 Name이 설정 → 플러그인에 표시되는 이름입니다.
1. 사이드바 필터 제공자 ISidebarFilterProvider
검색 창 좌측 사이드바에 커스텀 필터 카테고리를 주입합니다. 제공자가 반환하는 행은 그대로 퀵 윈도우 검색창 아래에 있는 유형 필터 바에 표시되는 행이며, 같은 행이 두 곳에 적용됩니다:
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface ISidebarFilterProvider : IPluginComponent
{
IEnumerable<SidebarFilterGroup> GetFilterGroups();
// 정렬 가중치. 값이 낮을수록 먼저 렌더링된다.
int SortOrder => 100;
}
public class SidebarFilterGroup
{
// 호스트가 잘 알려진 그룹에 대해 인식하는 선택적 안정 ID (내장 결과 유형 필터의 "Type" 등).
// 그룹이 전적으로 플러그인에서 정의된 것이라면 비워 둔다.
public string Id { get; set; } = string.Empty;
public string Header { get; set; } = string.Empty;
public List<SidebarFilterItem> Items { get; set; } = new();
// 이 그룹에서 여러 항목을 동시에 활성 상태로 둘 수 있는지 여부.
public bool AllowMultiSelect { get; set; }
}
public class SidebarFilterItem
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
// 아이콘 경로 두 가지, 모두 테마를 인식한다. IconData는 활성 테마의 텍스트 색으로 그리는
// 글리프, IconKey는 호스트가 이미 소유한 리소스 이름이다. 아이콘이 없으려면 둘 다 null로 둔다.
public string? IconData { get; set; }
public string? IconKey { get; set; }
// 이 항목과 일치하려면 결과가 만족해야 하는 조건자. 기본값은 "아무것도 일치시키지 않음"이므로,
// 한 번도 설정하지 않은 항목은 표시되지만 아무것도 선택할 수 없다.
public Func<ISearchResult, bool> MatchPredicate { get; set; } = _ => false;
// 이 항목이 나타내는 항목 종류(있는 경우). 설정하면 검색이 인덱스 순회를 그 종류로 좁히지,
// 엔진이 이미 순위를 매기고 잘라 낸 페이지만 필터링하지는 않는다. 어떤 분류로도 표현할 수
// 없는 것(크기 범위, 사용자 지정 와일드카드 규칙)은 null(기본값)로 두면 이 항목은
// 조건자만으로 결과를 필터링한다.
public SearchCategoryFilter? Category { get; set; }
}그룹과 항목은 record가 아니라 변경 가능한 클래스입니다: 필요한 속성만 채우고 나머지는 기본값으로 두면 됩니다.
2. 테이블 커스텀 열 제공자 IResultColumnProvider
전체 검색 창 "자세히" 테이블 뷰에 커스텀 데이터 열을 추가합니다(예: 미디어 재생 시간, 코드 줄 수, Git 브랜치). 제공자는 열을 한 번 기술하고, 셀 값은 요청될 때마다 반환합니다:
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface IResultColumnProvider : IPluginComponent
{
IEnumerable<ResultColumnDefinition> GetColumns();
string GetCellValue(ISearchResult result, string columnId);
}
public class ResultColumnDefinition
{
public string ColumnId { get; set; } = string.Empty;
public string HeaderText { get; set; } = string.Empty;
public double Width { get; set; } = 120;
// 선택 사항: 이 열이 적용되지 않는 결과에서는 열을 숨린다.
public Func<ISearchResult, bool>? VisibilityPredicate { get; set; }
// 선택 사항: 헤더 클릭 시 커스텀 정렬. x < y이면 음수, x > y이면 양수.
public Func<ISearchResult, ISearchResult, int>? SortComparer { get; set; }
// 선택 사항: 전체 검색 창에서 이 열의 셀을 더블클릭했을 때의 동작.
// 설정하지 않으면 셀 더블클릭이 행의 다른 어디를 더블클릭하든 같은 동작이 된다.
public Action<ISearchResult>? OnDoubleClick { get; set; }
}GetCellValue는 목록 렌더링 중에 호출되므로 값이 싸야 합니다. 디스크를 건드리지 말고 미리 계산해 둔 값이나 캐시를 반환하세요.
3. 퀵 패널 동적 탭 제공자 IQuickPanelTabProvider
퀵 패널에 동적 워크스페이스 탭을 제공합니다:
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface IQuickPanelTabProvider : IPluginComponent
{
// 지금 표시할 항목. 패널이 소환될 때마다 호출된다.
Task<IReadOnlyList<ISearchResult>> GetEntriesAsync(CancellationToken cancellationToken = default);
}그 단 하나의 메서드가 계약의 전부입니다 — 드롭 처리나 순서 재배치나 액션 컨텍스트처럼 구현할 다른 것은 없습니다.
CancellationToken은 패널이 닫힐 때 취소됩니다. 이를 관찰하는 것은 탭 자신의 목록뿐이고, 플러그인의 그 밖의 동작은 바뀌지 않습니다.- 소스가 알고 있다면
ISearchResult의Metadata.Modified를 채우세요. 기본 최신 우선 정렬이 이 값을 사용하기 때문입니다. 기본값으로 두면 반환한 순서가 그대로 유지됩니다. - 아무것도 반환하지 않는 제공자에는 탭이 생기지 않으며, 이를 위해 설정할 항목은 없습니다.
- 탭은 사용자가 직접 추가해야 하는 폴더와 달리 플러그인이 존재하는 순간 함께 존재합니다. 탭 스트립에서 닫고 설정 → 퀵 패널에서 다시 열 수 있는데, 이는 설정 → 플러그인에서 컴포넌트를 비활성화하는 것(아예 로드되지 않게 함)과는 별개의 문제입니다.
4. 달력 텍스트 제공자 ICalendarTextProvider
검색 창 시계 줄에 표시할 날짜 설명을 제공합니다. 빠른 검색 상자가 비어 있을 때 호스트는 그 줄의 날짜와 시간 뒤에 이 텍스트를 덧붙입니다.
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface ICalendarTextProvider : IPluginComponent
{
// 이 날짜에 대한 추가 설명. 빈 문자열은 이번에는 덧붙일 것이 없다는 뜻입니다.
string GetCalendarText(DateTime date);
}- 비어 있지 않은 텍스트를 처음 돌려준 제공자가 선택되므로, 두 번째 달력 플러그인이 같은 줄을 두고 경쟁하지 않습니다.
- 날짜는 호스트가 넘겨줍니다. 구현이 시계를 읽지 않고 인자에만 의존하므로 그대로 테스트할 수 있습니다.
- 음력·절기·명절 같은 표는 플러그인이 보유하며 호스트는 사본을 두지 않습니다. 이 줄이 호스트 내장 표가 아니라 플러그인에 묻는 이유입니다.
- 설정 → 플러그인에서 이 컴포넌트를 끄거나 플러그인 자체를 사용하지 않으면 호스트는 더 이상 호출하지 않습니다.
5. 파일 미리보기 및 썸네일
커스텀 파일 미리보기 제공자 IFilePreviewProvider
QuickLook 패널 내부의 미리보기를 렌더링합니다. 사용자는 Alt+P 또는 미리보기 가능한 행 휠클릭으로 패널을 엽니다(참고: 액션 메뉴 및 즉시 미리보기):
namespace Lertaro.PluginSdk.Abstractions.Plugins.Preview;
public interface IFilePreviewProvider : IPluginComponent
{
// Priority는 동률 처리에만 쓰입니다. 사용자가 설정해 둔 제공자 순서(설정 → 일반 →
// 미리보기 및 썸네일)가 먼저 적용되고, Priority는 그 안에서 정렬하며 높은 쪽이 앞에 선다.
int Priority => 0;
bool CanPreview(string path, bool isDir);
UIElement CreatePreview(string path, bool isDir);
// 패널 안에 배치할 WPF 콘텐츠를 반환하는 대신 외부 창 자체를 호스팅하는 경우 true
// (QuickLook 브리지 플러그인이 이렇게 한다).
bool RendersExternally => false;
}미리보기 생명주기 및 재사용 계약
아래 첫 계약을 제공자가 구현하거나, 반환하는 UIElement가 두 번째 계약을 구현하면 호스트가 미리보기 생명주기를 최적화합니다.
IPreviewSessionAware— 반환한 컨트롤이 아니라 제공자에 캐스팅되는void EndPreviewSession();. 제공자는 프로세스 안의 컨트롤만이 아니라 실제 외부 창을 소유합니다(HwndHost, 또는 네이티브IPreviewHandler와 그prevhost서로게이트). 그래서 소유 창이 닫힐 때 세션을 종료하도록 통보받고, 프로세스 안에서 렌더링하는 제공자의 경우에는 미리보기 패널이 가려지거나 세션이 끝날 때만 통보받습니다. 이것이 없으면 호스트의 창이 아무 참조도 없이 남게 됩니다.IReusablePreview— 반환한 요소에 캐스팅되는bool TrySetTarget(string path, bool isDir);사용자가 방향키로 유사한 파일 사이를 이동할 때 호스트는 컨트롤을 폐기하고 다시 만드는 대신 같은 컨트롤에 대상 재지정을 요청하며, 이것이 깜빡임을 없애 줍니다. 새 대상이 이 인스턴스에 맞지 않으면false를 반환하고, 호스트는 새 미리보기를 만드는 방식으로 물러납니다.IReceivesPreviewPanelBounds—void OnPreviewPanelBoundsAvailable(int left, int top, int width, int height);자기 프로세스 밖의 창을 호스팅하는 제공자는 패널이 차지하는 사각형을 알아야 그 안에 창을 넣거나 배치할 수 있습니다. 그 사각형이 확인되는 대로 통보받으려면 이 계약을 구현하세요.
커스텀 썸네일 제공자 IThumbnailProvider
네이티브 Shell 핸들러가 없는 형식(.blend, .psd, .dwg)의 썸네일을 추출합니다:
namespace Lertaro.PluginSdk.Abstractions.Plugins.Preview;
public interface IThumbnailProvider : IPluginComponent
{
// 미리보기와 같은 규칙이다. 사용자가 설정해 둔 썸네일 제공자 순서가 먼저 정하고,
// Priority는 그 안에서만 정렬한다.
int Priority => 0;
bool CanProvideThumbnail(string path, bool isDir);
// 동기식이다. 결과 목록 렌더링 경로에서 실행되기 때문이다 -- 빠르게 유지할 것.
// 메모이제이션할 필요가 없다. 호스트가 반환한 결과를 캐시하기 때문이다(물리 항목이나
// 가상 항목은 경로 기준으로, 그 외에는 확장자 기준으로). 결론은 두 가지 -- `size`는 셸
// 이미지 목록에서 가져온 호스트 자신의 선택이므로 특정한 값을 기대하지 말 것, 그리고
// 제공자는 디렉터리에 대해 아예 질문받지 않는다.
ImageSource? GetThumbnail(string path, int size);
}6. 테마 및 다국어
테마 제공자 IThemeProvider
색 팔레트와 WPF 리소스 딕셔너리를 제공합니다:
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface IThemeProvider : IPluginComponent
{
IEnumerable<ITheme> GetThemes();
}namespace Lertaro.PluginSdk.Abstractions; // 참고: 테마 자체는 한 단계 위입니다
public interface ITheme
{
string Id { get; }
string DisplayName { get; }
bool IsDark { get; }
ResourceDictionary GetResources();
// 어떤 창에도 적용되지 않는다. AllowsTransparency가 설정된 창은 레이어드 창이고,
// 레이어드 창은 ClearType을 그릴 수 없으므로 이 앱의 창은 모두 불투명하며 어떤 테마의
// 불투명도도 읽지 않는다. 테마가 구현하고 있으므로 멤버는 남겨 두지만 값은 무시된다.
[Obsolete("Window transparency is no longer supported; this value is ignored.")]
double WindowOpacity => 1.0;
}제공자 하나는 원하는 만큼 많은 테마를 제공할 수 있고, 제공자가 어두운 변형을 따로 노출하는 대신 각 테마가 스스로 명암 여부를 나타내는 플래그를 가집니다.
다국어 현지화 제공자 ITranslationProvider
번역 딕셔너리를 동적으로 제공합니다:
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface ITranslationProvider : IPluginComponent
{
// 이 제공자가 처리할 수 있는 컬처 코드로, 호스트가 아무것도 로드되기 전에 설정에서 이를
// 제시할 수 있다. 기본값은 비어 있으며 "요청되는 것으로부터 발견한다"를 뜻한다.
IReadOnlyList<string> SupportedCultures => Array.Empty<string>();
IReadOnlyDictionary<string, string> GetTranslations(string cultureName);
}