|
| 1 | +# Notifications |
| 2 | + |
| 3 | +aIDE's notification system covers four surfaces sharing one severity model (`NotificationType`) and one action model (`NotificationAction`): |
| 4 | + |
| 5 | +| Surface | Logged? | Driven by | Auto-dismiss | |
| 6 | +|---|---|---|---| |
| 7 | +| **Balloon** | yes | `NotificationManager::post()` | transient balloons only | |
| 8 | +| **Notification log** (`NotificationView`) | — | reads the manager's history | never | |
| 9 | +| **Banner** | no | caller, direct widget use | caller-controlled | |
| 10 | +| **"Got it" tooltip** (`GotItTooltip`) | no | caller, direct widget use | on click / timeout | |
| 11 | + |
| 12 | +Only the balloon path goes through `NotificationManagerInterface::post()` and is recorded in the log. Banners and "Got it" tooltips are caller-driven, ephemeral, and never logged. |
| 13 | + |
| 14 | +**Headers:** `aide/notificationmanagerinterface.hpp`, `aide/notification.hpp`, `aide/notificationgroup.hpp`, `aide/gui/widgets/notificationballoon.hpp`, `aide/gui/widgets/banner.hpp`, `aide/gui/widgets/gotittooltip.hpp`, `aide/gui/widgets/notificationview.hpp` |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## NotificationManagerInterface |
| 19 | + |
| 20 | +Reachable via `app.notificationManager()` (or `ApplicationBuilder::notificationManager()`). Decouples posting/querying code from the concrete `NotificationManager`. |
| 21 | + |
| 22 | +### Groups |
| 23 | + |
| 24 | +Every notification belongs to a `NotificationGroup` — the unit configured on the settings page and used for routing: |
| 25 | + |
| 26 | +```cpp |
| 27 | +aide::NotificationGroup group; |
| 28 | +group.id = aide::HierarchicalId("myapp.build"); |
| 29 | +group.displayName = "Build"; |
| 30 | +group.defaultDisplayType = aide::NotificationDisplayType::Balloon; |
| 31 | +group.logByDefault = true; |
| 32 | + |
| 33 | +app.notificationManager().registerGroup(group); |
| 34 | +``` |
| 35 | + |
| 36 | +Register groups once, at startup, before posting to them. |
| 37 | + |
| 38 | +### Posting |
| 39 | + |
| 40 | +```cpp |
| 41 | +aide::Notification notification; |
| 42 | +notification.groupId = aide::HierarchicalId("myapp.build"); |
| 43 | +notification.type = aide::NotificationType::Success; |
| 44 | +notification.title = "Build finished"; |
| 45 | +notification.content = "0 errors, 3 warnings"; |
| 46 | +notification.actions.push_back({"Open log", [] { /* ... */ }}); |
| 47 | + |
| 48 | +const auto id = app.notificationManager().post(std::move(notification)); |
| 49 | +``` |
| 50 | +
|
| 51 | +`post()` assigns `id` and `timestamp`, resolves and freezes the display type (see below), and appends the notification to the log. A `Notification` is an immutable snapshot once posted — it carries no read/dismissed flag; that lifecycle state lives in the manager. |
| 52 | +
|
| 53 | +### Display type resolution |
| 54 | +
|
| 55 | +`resolvedDisplayType(id)` is computed once, at `post()` time, and frozen — later settings changes never retroactively change how an already-posted notification displayed: |
| 56 | +
|
| 57 | +1. If Do Not Disturb is enabled, resolves to `None` (log-only). |
| 58 | +2. Otherwise, a per-group settings override (`notifications.<group-id>.displayType`), if the user set one. |
| 59 | +3. Otherwise, the group's `defaultDisplayType`. |
| 60 | +4. `None` if the group was never registered. |
| 61 | +
|
| 62 | +`NotificationDisplayType` values: `None`, `Balloon`, `StickyBalloon`. These are group-routed popup destinations only — editor banners, dialog banners and "Got it" tooltips are caller-driven surfaces and deliberately have no enumerator here. |
| 63 | +
|
| 64 | +### Querying and lifecycle |
| 65 | +
|
| 66 | +```cpp |
| 67 | +auto& mgr = app.notificationManager(); |
| 68 | +
|
| 69 | +mgr.notifications(); // full log, for NotificationView |
| 70 | +mgr.notification(id); // std::optional<Notification> |
| 71 | +mgr.isRead(id); |
| 72 | +mgr.markRead(id); // per-notification |
| 73 | +mgr.markRead(); // global: clears hasUnread() |
| 74 | +mgr.hasUnread(); // boolean, NOT a count — bind to an opener button's unread dot |
| 75 | +mgr.remove(id); |
| 76 | +mgr.clearAll(); |
| 77 | +
|
| 78 | +mgr.setDoNotDisturb(true); |
| 79 | +mgr.doNotDisturb(); |
| 80 | +``` |
| 81 | + |
| 82 | +`hasUnread()` is intentionally boolean rather than a count (per an amendment to the original design): `NotificationView` clears it via `markRead()` whenever it becomes visible, and again on every post that arrives while still visible. |
| 83 | + |
| 84 | +### Signals (concrete `NotificationManager`) |
| 85 | + |
| 86 | +`NotificationManagerInterface` itself declares no signals — connect to the concrete `aide::NotificationManager` (as `ApplicationBuilder`/`Application` already do internally for the balloon host): |
| 87 | + |
| 88 | +```cpp |
| 89 | +notificationPosted(aide::NotificationId id); |
| 90 | +notificationUpdated(aide::NotificationId id); |
| 91 | +notificationRemoved(aide::NotificationId id); |
| 92 | +notificationsCleared(); |
| 93 | +unreadChanged(bool hasUnread); |
| 94 | +``` |
| 95 | +
|
| 96 | +--- |
| 97 | +
|
| 98 | +## Balloons |
| 99 | +
|
| 100 | +`NotificationBalloonHost` (wired automatically by `ApplicationBuilder`) listens for `notificationPosted` and shows a `NotificationBalloon` for every notification whose frozen `resolvedDisplayType` is `Balloon` or `StickyBalloon`; `None` is log-only and never reaches this surface. |
| 101 | +
|
| 102 | +- Transient balloons (`Balloon`) auto-dismiss after a fixed delay with a countdown progress bar that pauses on hover. |
| 103 | +- Sticky balloons (`StickyBalloon`) never time out — the user closes them explicitly. |
| 104 | +- Corner placement is one global setting (`notifications.balloonPlacement`, a `NotificationBalloonPlacement`: `BottomRight`, `BottomLeft`, `TopRight`, `TopLeft`). Stacking direction derives from that corner alone — balloons anchor there and grow toward screen center as more stack up. |
| 105 | +
|
| 106 | +You never construct `NotificationBalloon` directly — it's created and owned exclusively by `NotificationBalloonHost`. |
| 107 | +
|
| 108 | +--- |
| 109 | +
|
| 110 | +## Notification log view |
| 111 | +
|
| 112 | +`aide::widgets::NotificationView` is a plain embeddable `QWidget` showing the notification log — a flat, newest-first timeline (JetBrains Notifications-style), **not** a docked tool window. It is in-memory and session-only: nothing is persisted, and the timeline starts empty on every launch. |
| 113 | +
|
| 114 | +```cpp |
| 115 | +auto* view = new aide::widgets::NotificationView( |
| 116 | + app.notificationManager(), *app.settingsProvider(), parent); |
| 117 | +
|
| 118 | +connect(view, &aide::widgets::NotificationView::settingsRequested, ...); // "Notification Settings..." entry |
| 119 | +connect(view, &aide::widgets::NotificationView::groupSettingsRequested, ...); // jump to a group's settings row |
| 120 | +``` |
| 121 | + |
| 122 | +Bind an unread indicator on whatever opener button toggles this view to `NotificationManagerInterface::hasUnread()`; cards render uniform, with no per-entry read/unread styling. |
| 123 | + |
| 124 | +--- |
| 125 | + |
| 126 | +## Banners |
| 127 | + |
| 128 | +`aide::widgets::Banner` is a severity-tinted inline strip (coloured left stripe, type icon, message, optional action links, close button) for embedding directly in a window — e.g. a dialog banner or an editor banner. It reuses `NotificationType`/`NotificationAction` but never goes through `post()` and is never logged. |
| 129 | + |
| 130 | +```cpp |
| 131 | +auto* banner = new aide::widgets::Banner( |
| 132 | + aide::NotificationType::Warning, "Unsaved changes will be lost", parent); |
| 133 | +banner->addAction({"Save", [] { /* ... */ }}); |
| 134 | +banner->setClosable(true); |
| 135 | +connect(banner, &aide::widgets::Banner::closed, banner, &QObject::deleteLater); |
| 136 | +``` |
| 137 | +
|
| 138 | +Use banners for state tied to a specific window or editor, not for events that belong in the global notification log. |
| 139 | +
|
| 140 | +--- |
| 141 | +
|
| 142 | +## "Got it" tooltip |
| 143 | +
|
| 144 | +`aide::widgets::GotItTooltip` is a one-shot onboarding tooltip anchored to a target widget, arrow pointing at it. Caller-driven: it never posts and never reaches the log, and depends only on `SettingsInterface` and the target widget — no `NotificationManager` dependency. |
| 145 | +
|
| 146 | +```cpp |
| 147 | +auto* tip = new aide::widgets::GotItTooltip( |
| 148 | + *app.settingsProvider(), "myapp.newFeatureButton", "Click here to try the new feature", parent); |
| 149 | +tip->withHeader("New!") |
| 150 | + .withLink("Learn more", [] { /* ... */ }); |
| 151 | +
|
| 152 | +tip->showGotIt(targetButton, aide::widgets::GotItPosition::Below); |
| 153 | +``` |
| 154 | + |
| 155 | +- Seen state is an integer show-count stored at settings key `aide/gotit/<id>` (`canShow() == count < maxCount`, default `maxCount` of 1). `withShowCount()` raises the limit. |
| 156 | +- To reshow a changed tip, bump `id` (the `.v2` convention) — a fresh key starts its count at 0. |
| 157 | +- Pass `id` as a string literal: it is threaded through `HierarchicalId`, which stores a raw pointer rather than copying. |
| 158 | +- Only one tooltip shows at a time; closing one advances a single process-wide queue. |
| 159 | +- `showGotIt()` is a no-op (and deletes the tooltip) if `canShow()` is false or the target is null. |
| 160 | + |
| 161 | +`GotItTooltip::isFirstRunAfterUpgrade(settings)` is a separate static helper: it compares the stored previous-run version against `QCoreApplication::applicationVersion()`, records the current version, and reports whether this run follows an upgrade — useful for gating "what's new" tooltips. |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## Settings page |
| 166 | + |
| 167 | +`aide::gui::NotificationsSettingsPage` is the built-in settings page (registered automatically by `ApplicationBuilder`): Do Not Disturb + balloon placement controls above a flat table with one row per registered `NotificationGroup`, letting users override each group's display type. |
| 168 | + |
| 169 | +Settings keys (see `aide/core/settings/notifications/notificationsettingskeys.hpp`): |
| 170 | + |
| 171 | +| Key | Meaning | |
| 172 | +|---|---| |
| 173 | +| `notifications.doNotDisturb` | global suppress-all toggle | |
| 174 | +| `notifications.balloonPlacement` | global corner anchor | |
| 175 | +| `notifications.<group-id>.displayType` | per-group override of `defaultDisplayType` | |
| 176 | + |
| 177 | +--- |
| 178 | + |
| 179 | +## Demo reference |
| 180 | + |
| 181 | +`demo/src/notificationlauncherdialog.{hpp,cpp}` (opened via the demo's "Demo → Notifications" menu action) exercises every surface end-to-end: balloon/sticky-balloon posting with and without actions, the "Got it" tooltip with position controls, dialog banners, and editor banners. Use it as a live reference for wiring each surface. |
0 commit comments