通知表示
Lertaro.PluginSdk.Services.PluginNotificationService は、プラグインがホスト自身のウィンドウを使ってバックグラウンドからユーザーの注意を引くことを可能にします。追加のプロセスも、OS のトーストチャネルも、ホスト UI アセンブリへの結合も不要です。画面に何を出し、そこにどれだけの間留めるかを決めるのはプラグインではなくホストです。
1. 呼び出し口
| シグネチャ | 返すもの | 使う場面 |
|---|---|---|
INotificationHandle Show(NotificationRequest request) | 即時にハンドル | この通知をあとで置換またはキャンセルしたい場合 |
Task<NotificationResult> ShowAsync(NotificationRequest request) | 最終状態 | 終わり方だけに興味がある場合 |
bool Show(string title, string text, Action? onClick = null) | ホストが受け付けたかどうか | 旧式の 2 引数形式。false はホストが結線されていないことを意味するため、フォールバックが使えます |
どの呼び出し口もプラグイン自身のバックグラウンドスレッドから安全に呼べて、例外を絶対に投げません。外へ逃げた例外は呼び出し元のループも道連れにするためです。ホスト側デリゲートがまったく未代入の場合(ランチャー外で実行されているプラグイン)は、呼び出しは静かな no-op となり、返されるのはすでに完了した Unavailable を載せたハンドルです。要求がログに残るのはホスト自身のデリゲートが例外を投げたときに限られ、その場合 Warn で記録されます。
INotificationHandle のメンバーは Task<NotificationResult> Completion と void Dismiss() の正確に 2 つです。Completion は必ず完了します。画面に何も出なかった場合も含むため、これを await してもスレッドを留め置くことはできません。Dismiss() は表示済みのカードを、ユーザーが閉じたのと同じ形で閉じます。カードがすでに無ければ何もせず、すでに渡された結果を覆すこともありません。
自身のウィンドウが必要なプラグインは Windows.PluginWindow を使い、告知ではなく返答が必要なプラグインは PluginMessageBoxService を使います。
2. NotificationRequest
| プロパティ | 既定値 | 動作 |
|---|---|---|
string? Id | null | 同じ Id を持つ 2 つの要求は、同じ通知を指します。Id による置換(§4)を参照してください。 |
string Title | string.Empty | カードのヘッダー。タイトル行を持たない BottomNotice では無視されます。 |
string Message | string.Empty | 本文テキスト。Title と Message の両方が空、または空白文字のみの要求は、表示するものがないため InvalidRequest として拒否されます。 |
NotificationLevel Level | Info | Info / Warn / Error。アイコンと意味づけされた色を決めるだけで、他には影響しません。 |
NotificationPosition Position | CardStack | CardStack または BottomNotice。それぞれが自身のキューと自身のルールを持ち、互いを決してブロックしません。 |
double? DurationSeconds | null | null はその位置自身の既定値を採用します。明示した値がその位置の範囲外なら、最も近い境界へクランプされ、拒否はされません。NaN または負値は短い側の端に着きます。 |
Action? OnClick | null | 閉じる動作に加えて実行されます。クリックコールバック(§6)を参照してください。 |
3. 2 つの位置
CardStack(右下) | BottomNotice(下部中央) | |
|---|---|---|
| 形式 | タイトル、送信者名、✕、「すべて既読にする」を持つカード | 1 行のみ。タイトルも送信者名もコントロールもなし |
| 同時表示 | 5 枚を表示し、残りはキューへ | 1 件のみ。新しいものが現在のものを即座に置換します。旧カードの解体をやり過ごす必要はありません |
| 既定の表示時間 | 8 秒 | 4 秒 |
| クランプ範囲 | 2〜30 秒 | 2〜10 秒 |
OnClick | 尊重される | 無視される(操作できる対象がない) |
| 全画面アプリが画面を占有 | 1行表示に縮み、5 秒まで | 影響なし |
連続して通知を出すときに知っておくべき、間隔を決めるほかに 2 つの数値があります。
- ホストは 100 ms ごとに残り時間を確認します。待機中のカードは、このタイマーだけが 1 回に最大 1 枚表示し、実行中の 200 ms の移動が終わるまで待ちます。自動終了も現在の移動を待ち、期限切れのカードをまとめて閉じます。カードの終了やプラグインのアンロード中には補充しません。ホバー時とセッションロック中は引き続き計時を停止します。
- 1 つのプラグインが受理できるカード要求は最大 10 件(画面上のものと待機中のものの合計)です。11 件目は
QueueFullとして破棄され、ログされます。この上限はカードの受け入れ経路でのみ適用されます。BottomNoticeの要求は決してカウントされません。1行表示は自分自身を上書きする 1 スロットにすぎないためです。プラグイン間を跨ぐグローバルな保留上限は存在しないため、あるプラグインの連続出力が他を飢えさせることはありませんが、逆に暴走する生成者をグローバルに制限する仕組みもありません。
4. Id による置換
要求に Id を付けることは、繰り返し実行されるステータス更新が自分自身の複製で画面を埋めないための手段です。Id が表示中のカードと一致する新しい要求は、そのカードをその場で上書きします。スタック内の同じスロットに留まり、最上位へは飛びません。以前の呼び出し元のハンドルは Replaced として完了します。置き換えられた事実を知る必要がある呼び出し元は、まさにハンドルを保持している側です。
守るべき制限が 1 つあります。重複排除は現在画面にあるカードに対して行われ、キューで待機中のカードに対しては行われません。画面が埋まっている間に同じ Id の複製を 5 件連続で発するプラグインは、5 件すべてがキューに入ります。
5. 結果と失敗理由
NotificationResult は Succeeded に加えて任意の NotificationFailure を持ちます。項目に到達した最初の終了状態が勝ち、そのあとに書き換えられることはありません。
NotificationFailure | 発生条件 |
|---|---|
Unavailable | 要求を処理したホストがない場合。ランチャー外で実行されているプラグイン、またはホスト側デリゲートが例外を投げた場合。 |
InvalidRequest | Title と Message の両方が空、または空白文字のみだった場合。 |
QueueFull | このプラグインがすでに 10 件のカード要求を受理していた場合。 |
Replaced | より新しい要求が同じ Id を持っていた場合。BottomNotice の上書きでは、どちらにも Id がなくても上書き前の要求はこの形で完了します。1行表示は常に上書きされる 1 スロットだからです。 |
CancelledByPluginUnload | カードの表示中またはキュー待機中に、プラグインが無効化・アンロードされた場合。 |
HostShuttingDown | ランチャーが終了し、通知も一緒に連れて行った場合。 |
画面に到達して表示時間を使い切った通知、またはユーザーや Dismiss() で閉じられた通知は、NotificationResult.Success として完了します。これらは終わりであって失敗ではありません。
6. クリックコールバック
OnClick はユーザーが閉じるあらゆる操作で実行されます。コード上それらは 1 つの結果として扱われ、カード本文のクリックも ✕ のクリックも同じ経路を通ります。単に経過したカウントダウンは実行しません。§5 にある失敗のいずれも実行しません。
コールバックはプラグインのコードであるため、その内部の例外は捕捉され、送信者名とともにログされ、ランチャーの入力パイプラインへ運び返されることはありません。失敗しうる作業本体はコールバックの外で実行してください。コールバックの仕事は、ユーザーをあなたのウィンドウへ連れてくることです(リポジトリ内の例は Plugins/Calendar で、カレンダービューをアクティブ化するために使っています)。
7. プラグインが偽造できない送信者表記
カードに印字される名前は Assembly.GetCallingAssembly() に由来します。ファサードの各公開フレームで読み取られ、ホストが次のようにマッピングします。名前が Lertaro.App* のアセンブリは Lertaro として表示し、登録済みプラグインはその管理下の表示名で表示し、それ以外は Lertaro.Plugins. プレフィックスを除去したアセンブリ名へフォールバックします。送信者名を文字列で渡す方法は意図的に提供していません。プラグインが選んではいけない唯一の帰属だからです。
8. 配置とサーフェス
- どの画面か:最前面ウィンドウの画面、無ければカーソルの画面、それも無ければプライマリー画面。作業領域はそのモニターの DPI を通して DIP へ変換されるため、カードは 200 % スケールのパネルでも 100 % のパネルでも同じ物理サイズになります。
- 重ね順:右下に、到着順で並びます。削除されたカードより上のカードは再スライドします。これが唯一のアニメーションで、約 200 ms、登場時のドロップは 40 DIP です。
- サイズ:カードの高さは作業領域の半分以下(
min(260 DIP, 50 %))に収まるため、長いメッセージが小さなモニターを食い尽くすことはありません。 - ドラッグ:カードはタイトルバーでのみドラッグでき、ドラッグされたカードは自身のコーナーを保ちます。表示設定が変わるとスタックは再アンカーされ、ユーザーのドラッグは失われます。カードを画面外に留めておくより安いトレードです。
- サーフェスの種類:常に不透明です。カードはそれ自体がウィンドウなので、角はウィンドウマネージャーが丸め、文字は ClearType で描画されます。テーマの
WindowOpacityはウィンドウの種類を決めなくなり、どのコードも適用しません。 - 最前面表示は常にオンで、ウィンドウは Alt+Tab から除外されます(
WS_EX_TOOLWINDOW)。通知はフェードしません。Window.Opacityをアニメーションさせるものが何もありません。それは ClearType を失い、毎フレームのピクセル単位コンポジションのコストを払うことになるためです。到着の目印は、すでに不透明なウィンドウ内での縁(リム)のフラッシュ(約 750 ms)です。
9. スレッド処理とライフサイクル
- どのスレッドからでも呼んでください。ホストはすべての通知呼び出し口を 1 つのゲートで直列化するため、
Showは短いブロッキング呼び出しです。項目ごとのホットループで駆動せず、代わりに 1 枚の要約カードを送ってください。 - ウィンドウは
DispatcherPriority.Backgroundで提示されるため、通知の連続出力がランチャー自身の入力処理を枯らすことはありません。 - セッションがロック中(スクリーンセーバー、
Win+L)は、カウントダウンが凍結されカードは隠されます。ロックが解けると、与えられた時間を残したまま復帰します。 - プラグインの無効化またはアンロードは、その表示中およびキュー待機中のカードをキャンセルします(
CancelledByPluginUnload)。ランチャーの秩序ある終了は、未完了のすべてに対して同様に行います(HostShuttingDown)。 - このビルドにはユーザー向けの通知設定が一切ありません。通知を受け取らない設定も、プラグインごとのスイッチもなく、上記の位置と表示時間のルールは設定できないホスト側のものです。要求はこれに合わせて設計してください。頻繁に語るプラグインは、ユーザーに黙らせるのではなく自分自身を静めるべきです。
10. 例
using Lertaro.PluginSdk.Abstractions;
using Lertaro.PluginSdk.Services;
// リマインダー 1 件につきカード 1 枚。まだ画面にあるなら繰り返すのではなく置換される。
var handle = PluginNotificationService.Show(new NotificationRequest
{
Id = $"reminder-{due.Item.Id}",
Title = TranslationService.Get("Reminder_Title"),
Message = due.Item.Text,
Level = NotificationLevel.Warn,
DurationSeconds = 12, // ホストによって 2..30 の範囲にクランプされる
OnClick = () => CalendarView.ShowOrActivate(),
});
// 本当に最終状態が必要な場所でのみ待機する。タスクは必ず完了する。
_ = handle.Completion.ContinueWith(t =>
{
if (t.Result.Failure is NotificationFailure.Replaced)
Logger.Log($"Superseded by a newer reminder: {due.Item.Id}", LogLevel.Debug);
});NOTE
このページのシグネチャ、既定値、上限は PluginSdk/Abstractions/NotificationRequest.cs、PluginSdk/Abstractions/INotificationHandle.cs、PluginSdk/Services/PluginNotificationService.cs、およびホスト側の App/Services/Notifications/ と App/Views/Notifications/ の実装を読み出して確認しています。