UI & Preview Extensions
This chapter introduces Lertaro.PluginSdk interfaces for extending the search window sidebar, adding custom table columns, providing dynamic Quick Panel tabs, supplying the date text shown on the search window's clock line, building QuickLook file previewers and thumbnail extractors, and shipping WPF themes and i18n localization packs.
All of these live under Lertaro.PluginSdk.Abstractions.Plugins (preview providers under …Abstractions.Plugins.Preview) and every one of them derives from IPluginComponent, which supplies the Name the host shows in Settings → Plugins.
1. Sidebar Filter Provider ISidebarFilterProvider
Injects custom filter categories into the left sidebar of the search window. The rows a provider returns are also what the quick window's type filter bar shows, so one row serves both surfaces:
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface ISidebarFilterProvider : IPluginComponent
{
IEnumerable<SidebarFilterGroup> GetFilterGroups();
// Ordering weight; lower values render first.
int SortOrder => 100;
}
public class SidebarFilterGroup
{
// Optional stable id the host recognises for well-known groups (e.g. "Type" for the
// built-in result-type filter). Empty when the group is entirely plugin-defined.
public string Id { get; set; } = string.Empty;
public string Header { get; set; } = string.Empty;
public List<SidebarFilterItem> Items { get; set; } = new();
// Whether several items in this group can be active at once.
public bool AllowMultiSelect { get; set; }
}
public class SidebarFilterItem
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
// Two icon routes, both theme-aware: IconData is a glyph drawn in the active theme's
// text colour, IconKey names a resource the host already owns. Leave both null for none.
public string? IconData { get; set; }
public string? IconKey { get; set; }
// The predicate a result must satisfy for this item to match. Defaults to "matches nothing",
// so an item that never sets it is shown but can never select anything.
public Func<ISearchResult, bool> MatchPredicate { get; set; } = _ => false;
// The entry kind this row stands for, when one exists. Set it and the search narrows the index
// walk to that kind instead of only filtering the page the engine already ranked and capped;
// leave it null (the default) for anything no category can express -- a size range, a custom
// wildcard rule -- and the row filters results by its predicate alone.
public SearchCategoryFilter? Category { get; set; }
}Groups and items are mutable classes, not records: fill in the properties you need and leave the rest at their defaults.
2. Custom Table Column Provider IResultColumnProvider
Appends custom data columns to the "Details" table view of the Full Search window (e.g. media duration, lines of code, Git branch). The provider describes its columns once and answers per-cell values on demand:
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;
// Optional: hide the column for results it does not apply to.
public Func<ISearchResult, bool>? VisibilityPredicate { get; set; }
// Optional: custom sort on header click. Negative when x < y, positive when x > y.
public Func<ISearchResult, ISearchResult, int>? SortComparer { get; set; }
// Optional: a double-click on this column's cell in the Full window. Leave it unset and
// double-clicking the cell behaves like double-clicking anywhere else on the row.
public Action<ISearchResult>? OnDoubleClick { get; set; }
}GetCellValue is called during list rendering, so it must be cheap; hand back pre-computed values or read a cache rather than touching disk.
3. Quick Panel Tab Provider IQuickPanelTabProvider
Contributes a dynamic workspace tab to the Quick Panel:
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface IQuickPanelTabProvider : IPluginComponent
{
// The entries to show right now. Called each time the panel is summoned.
Task<IReadOnlyList<ISearchResult>> GetEntriesAsync(CancellationToken cancellationToken = default);
}That single method is the whole contract — there is no drop handling, reordering, or action context to implement.
- The
CancellationTokenis cancelled when the panel closes. Only the tab's own list observes it; nothing else about your plugin changes. - Fill in
ISearchResult'sMetadata.Modifiedwhere the source knows one, because the default newest-first ordering uses it. Leave it at its default and the entries keep the order you returned them in. - A provider that returns nothing gets no tab, and there is nothing to configure for that.
- The tab exists as soon as the plugin does, unlike a folder the user has to add. It can be closed from the strip and reopened under Settings → Quick Panel, which is a different question from disabling the component under Settings → Plugins (that stops it loading at all).
4. Calendar Text Provider ICalendarTextProvider
Supplies the date description for the search window's clock line, which the host appends after its own date and time while the quick search box is empty:
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface ICalendarTextProvider : IPluginComponent
{
// The extra description of this date. An empty string means nothing to add this time.
string GetCalendarText(DateTime date);
}- The first provider that returns non-empty text wins, so a second calendar plugin does not race the first for the same line.
- The host passes the date in rather than letting you read the clock, so the answer stays a function of its argument and can be tested.
- Tables of lunar dates, solar terms and festivals stay with the plugin; the host keeps no copy of them, which is the reason this line asks a plugin instead of carrying a table of its own.
- The host stops calling it once this component is switched off under Settings → Plugins, or the whole plugin is disabled.
5. File Previews & Thumbnails
Custom File Preview Provider IFilePreviewProvider
Renders previews inside the QuickLook panel, which the user opens with Alt+P or a middle-click on a previewable row (see Actions & Preview):
namespace Lertaro.PluginSdk.Abstractions.Plugins.Preview;
public interface IFilePreviewProvider : IPluginComponent
{
// Breaks ties only. The user's configured provider order (Settings → General →
// Previews & Thumbnails) is applied first; Priority sorts inside it, higher first.
int Priority => 0;
bool CanPreview(string path, bool isDir);
UIElement CreatePreview(string path, bool isDir);
// True when the provider hosts an external window of its own rather than returning WPF
// content to be laid out inside the panel (the QuickLook bridge plugin does this).
bool RendersExternally => false;
}Preview Lifecycle & Reuse Contracts
When your provider implements the first contract below, or the UIElement you return implements the second, the host optimizes the preview lifecycle:
IPreviewSessionAware— cast on the provider, not on the control it returned:void EndPreviewSession();. The provider owns a real external window (anHwndHost, a nativeIPreviewHandlerand itsprevhostsurrogate), not just an in-process control, so it is told to end its session when the owner window closes, and — for providers that render in-process — only when the preview panel hides or the session ends. Without this the host's window would linger with nothing pointing at it.IReusablePreview— cast on the returned element:bool TrySetTarget(string path, bool isDir);. When the user steps between similar files with the arrow keys, the host asks the same control to retarget instead of destroying and rebuilding it, which is what removes the flicker. Returnfalsewhen the new target does not suit this instance and the host falls back to building a fresh preview.IReceivesPreviewPanelBounds—void OnPreviewPanelBoundsAvailable(int left, int top, int width, int height);. A provider hosting its own out-of-process window needs to know the rectangle the panel occupies so it can parent or position into it; implement this to be handed that rectangle once it is known.
Custom Thumbnail Provider IThumbnailProvider
Extracts thumbnails for formats with no native Shell handler (.blend, .psd, .dwg):
namespace Lertaro.PluginSdk.Abstractions.Plugins.Preview;
public interface IThumbnailProvider : IPluginComponent
{
// Same rule as previews: the user's configured thumbnail-provider order decides first,
// Priority only sorts within it.
int Priority => 0;
bool CanProvideThumbnail(string path, bool isDir);
// Synchronous, because it runs on the result-list rendering path -- keep it fast.
// You do NOT have to memoise: the host caches what you return (keyed by path for a
// physical or virtual item, by extension otherwise). Two consequences: `size` is the
// host's own choice, taken from the shell image list, so do not expect a particular
// number; and a provider is never asked about a directory at all.
ImageSource? GetThumbnail(string path, int size);
}6. Themes & Localization
Theme Provider IThemeProvider
Contributes color palettes and WPF resource dictionaries:
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface IThemeProvider : IPluginComponent
{
IEnumerable<ITheme> GetThemes();
}namespace Lertaro.PluginSdk.Abstractions; // note: the theme itself is one level up
public interface ITheme
{
string Id { get; }
string DisplayName { get; }
bool IsDark { get; }
ResourceDictionary GetResources();
// Not applied to any window. A window with AllowsTransparency set is a layered window, and a
// layered window cannot render ClearType, so every window in this app is opaque and reads no
// theme's opacity. The member stays because themes implement it; its value is ignored.
[Obsolete("Window transparency is no longer supported; this value is ignored.")]
double WindowOpacity => 1.0;
}One provider can contribute any number of themes, and each theme carries its own light-or-dark flag rather than the provider exposing a dark variant.
Localization Provider ITranslationProvider
Supplies translation dictionaries dynamically:
namespace Lertaro.PluginSdk.Abstractions.Plugins;
public interface ITranslationProvider : IPluginComponent
{
// The culture codes this provider can serve, so the host can offer them in Settings
// before anything is loaded. Empty by default, which means "discover from what is asked".
IReadOnlyList<string> SupportedCultures => Array.Empty<string>();
IReadOnlyDictionary<string, string> GetTranslations(string cultureName);
}