OSXにある便利機能のQuickLook。それのwindows版のフリーソフトQuickLookのプラグイン部分のロジックを読んでみた。
調査したレポジトリのコミットはこちらgithub.com/QL-Win/QuickLook。2019年11月12日 22:32 JST時点のコミット。
まとめ
- Namespaceプロパティが重要で、バージョンは関係なかった。UIに表示するだけで、値の大小比較はしていない
- プラグインのインストール先はユーザーがインストールするもの、システムにプリインストールされているものの2つある。
- インストール先は2つに別れているが、読み込み後に同じ配列に追加されてPriorityプロパティだけでソートしている。システムにプリインストールされているプラグインが優先される事も無いし、Priorityが同じ時にどうなるかは未定義。
QuickLookとは
OSXはファイルを選択してスペースキーを押すとプレビューが表示される機能がある。プレビューが表示されてもフォーカスは元のファイラー側にあり、表示中にカーソルキーで選択状態のファイルを変更するとプレビューの内容も更新される。
これをwindowsで再現したソフトがQuickLook。OSXの機能と同じ名前でちょっと紛らわしい。OS標準の機能ではないのでサクサク感は少し劣る。
プラグインをインストールするロジック
QuickLook.Plugin.PluginInstallerプロジェクトがプラグインインストーラー。
最初に実行されるのはPlugin.csのinitメソッド。この中でApp.UserPluginPathの中の"*.to_be_deleted"ファイルを削除している。
QuickLookのプラグインはインストール時に同じ名前のプラグインがあると、そのプラグインのファイルを削除するのではなく、xxx.to_be_deleted というファイルにリネームする処理がある。
public void Init()
{
CleanupOldPlugins(App.UserPluginPath);
}
private static void CleanupOldPlugins(string folder)
{
if (!Directory.Exists(folder))
return;
Directory.GetFiles(folder, "*.to_be_deleted", SearchOption.AllDirectories).ForEach(file =>
{
try
{
File.Delete(file);
}
catch (Exception)
{
// ignored
}
});
}
次はPluginInfoPanel.xaml.csのReadInfoメソッド。_path変数にはプラグインファイルのパスが入っている。
プラグインファイルを解凍し、"QuickLook.Plugin.Metadata.config"という名前固定のxmlファイルを読み込み。xmlの名前空間、説明文、バージョン情報を読み込む。
名前空間は"QuickLook.Plugin."から始まらない場合はエラーとなる。バージョン情報や説明文はUIに表示するだけ。
private void ReadInfo()
{
filename.Text = Path.GetFileNameWithoutExtension(_path);
var xml = LoadXml(GetFileFromZip(_path, "QuickLook.Plugin.Metadata.config"));
_namespace = GetString(xml, @"/Metadata/Namespace");
var okay = _namespace != null && _namespace.StartsWith("QuickLook.Plugin.");
filename.Text = okay ? _namespace : "Invalid plugin.";
version.Text = "Version " + GetString(xml, @"/Metadata/Version", "not defined");
description.Text = GetString(xml, @"/Metadata/Description", string.Empty);
btnInstall.Visibility = okay ? Visibility.Visible : Visibility.Collapsed;
}
プラグインをインストールボタンが押されたら同ファイルのDoInstallが実行。CleanUp()は既存のプラグインを.to_be_deletedファイルにリネームする。その後、インストールファイルを名前空間のディレクトリに解凍する。この後再起動を促すメッセージを表示してインストールは終了。
private Task DoInstall()
{
var targetFolder = Path.Combine(App.UserPluginPath, _namespace);
return new Task(() =>
{
CleanUp();
try
{
ZipFile.ExtractToDirectory(_path, targetFolder);
}
catch (Exception ex)
{
Dispatcher.BeginInvoke(new Action(() => description.Text = ex.Message));
Dispatcher.BeginInvoke(new Action(() => btnInstall.Content = "Installation failed."));
CleanUp();
}
});
void CleanUp()
{
if (!Directory.Exists(targetFolder))
{
Directory.CreateDirectory(targetFolder);
return;
}
try
{
Directory.GetFiles(targetFolder, "*", SearchOption.AllDirectories)
.ForEach(file => File.Move(file,
Path.Combine(Path.GetDirectoryName(file), Guid.NewGuid() + ".to_be_deleted")));
}
catch (Exception ex)
{
Dispatcher.BeginInvoke(new Action(() => description.Text = ex.Message));
Dispatcher.BeginInvoke(new Action(() => btnInstall.Content = "Installation failed."));
}
}
}
プラグインをロードするロジック
PluginManager.cs のコンストラクタでユーザが追加したプラグイン・システムデフォルトのプラグインを読み込んで初期化する。
App.UserPluginPathはC:\Users\UserName\AppData\Roaming\pooi.moe\QuickLook\QuickLook.Plugin\
。App.AppPathはexeファイルの位置。
private PluginManager()
{
LoadPlugins(App.UserPluginPath);
LoadPlugins(Path.Combine(App.AppPath, "QuickLook.Plugin\\"));
InitLoadedPlugins();
}
LoadPluginsの中身。フォルダに対して"QuickLook.Plugin.*.dll"のファイルを全てリストアップ。dllファイルを読み込んで、Priorityプロパティでソート。
private void LoadPlugins(string folder)
{
if (!Directory.Exists(folder))
return;
Directory.GetFiles(folder, "QuickLook.Plugin.*.dll",
SearchOption.AllDirectories)
.ToList()
.ForEach(
lib =>
{
(from t in Assembly.LoadFrom(lib).GetExportedTypes()
where !t.IsInterface && !t.IsAbstract
where typeof(IViewer).IsAssignableFrom(t)
select t).ToList()
.ForEach(type => LoadedPlugins.Add(type.CreateInstance<IViewer>()));
});
LoadedPlugins = LoadedPlugins.OrderByDescending(i => i.Priority).ToList();
}
その後各プラグインのinitメソッドを呼ぶ。
private void InitLoadedPlugins()
{
LoadedPlugins.ForEach(i =>
{
try
{
i.Init();
}
catch (Exception e)
{
ProcessHelper.WriteLog(e.ToString());
}
});
}