Skip to content

Commit 2e3acb5

Browse files
committed
Add event listening lifecycle hooks to EventEmitter
Introduces StartEventListening() and StopEventListening() virtual hooks to EventEmitter, called automatically when the first listener is added and the last is removed. This enables lazy initialization and cleanup of platform-specific event monitoring, improving resource efficiency and simplifying management for subclasses.
1 parent 585e871 commit 2e3acb5

File tree

2 files changed

+237
-7
lines changed

2 files changed

+237
-7
lines changed

.cursor/rules/event-system.mdc

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,188 @@ emitter.AddListener<WindowEvent>([](const WindowEvent& event) {
150150
}
151151
});
152152
```
153+
154+
### Event Listening Lifecycle Management
155+
156+
Managers can efficiently manage platform-specific event monitoring by overriding `StartEventListening()` and `StopEventListening()` hooks. These hooks are automatically called when the first listener is added and when the last listener is removed, respectively.
157+
158+
#### Purpose
159+
160+
This pattern allows managers to:
161+
- **Lazy initialization** - Only start platform event monitoring when needed
162+
- **Resource efficiency** - Stop monitoring when no listeners exist
163+
- **Automatic management** - No manual tracking of listener count required
164+
165+
#### Implementation Pattern
166+
167+
```cpp
168+
// tray_icon.h
169+
class TrayIcon : public EventEmitter<TrayIconEvent>, public NativeObjectProvider {
170+
public:
171+
TrayIcon();
172+
virtual ~TrayIcon();
173+
174+
// ... public API ...
175+
176+
protected:
177+
// Override these to control platform event monitoring
178+
void StartEventListening() override;
179+
void StopEventListening() override;
180+
181+
private:
182+
class Impl;
183+
std::unique_ptr<Impl> pimpl_;
184+
};
185+
186+
// tray_icon.cpp
187+
TrayIcon::TrayIcon() : pimpl_(std::make_unique<Impl>()) {
188+
// DON'T call SetupEventMonitoring() here anymore!
189+
// It will be called automatically when first listener is added
190+
}
191+
192+
void TrayIcon::StartEventListening() {
193+
// Called automatically when first listener is added
194+
pimpl_->SetupEventMonitoring();
195+
}
196+
197+
void TrayIcon::StopEventListening() {
198+
// Called automatically when last listener is removed
199+
pimpl_->CleanupEventMonitoring();
200+
}
201+
```
202+
203+
#### Platform Implementation Example
204+
205+
```objc
206+
// platform/macos/tray_icon_macos.mm
207+
class TrayIcon::Impl {
208+
public:
209+
Impl(NSStatusItem* status_item)
210+
: ns_status_item_(status_item),
211+
ns_status_bar_button_target_(nil),
212+
click_handler_setup_(false) {}
213+
214+
void SetupEventMonitoring() {
215+
if (click_handler_setup_) {
216+
return; // Already monitoring
217+
}
218+
219+
if (!ns_status_item_ || !ns_status_item_.button) {
220+
return;
221+
}
222+
223+
// Create and set up button target
224+
ns_status_bar_button_target_ = [[NSStatusBarButtonTarget alloc] init];
225+
226+
// Set up event handlers
227+
[ns_status_item_.button setTarget:ns_status_bar_button_target_];
228+
[ns_status_item_.button setAction:@selector(handleStatusItemEvent:)];
229+
230+
// Enable click handling
231+
[ns_status_item_.button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp];
232+
233+
click_handler_setup_ = true;
234+
}
235+
236+
void CleanupEventMonitoring() {
237+
if (!click_handler_setup_) {
238+
return; // Not monitoring
239+
}
240+
241+
// Remove event handlers
242+
if (ns_status_item_ && ns_status_item_.button) {
243+
[ns_status_item_.button setTarget:nil];
244+
[ns_status_item_.button setAction:nil];
245+
}
246+
247+
// Clean up button target
248+
ns_status_bar_button_target_ = nil;
249+
250+
click_handler_setup_ = false;
251+
}
252+
253+
private:
254+
NSStatusItem* ns_status_item_;
255+
NSStatusBarButtonTarget* ns_status_bar_button_target_;
256+
bool click_handler_setup_;
257+
};
258+
```
259+
260+
#### Usage Flow
261+
262+
```cpp
263+
// Application code
264+
auto tray_icon = std::make_shared<TrayIcon>();
265+
tray_icon->SetIcon(icon);
266+
tray_icon->SetTooltip("My Application");
267+
268+
// At this point, NO platform event monitoring is active
269+
// (saves system resources)
270+
271+
// Add first listener - triggers StartEventListening()
272+
auto listener_id = tray_icon->AddListener<TrayIconClickedEvent>(
273+
[](const TrayIconClickedEvent& event) {
274+
std::cout << "Tray icon clicked: " << event.GetTrayIconId() << std::endl;
275+
}
276+
);
277+
278+
// Platform event monitoring is now ACTIVE
279+
280+
// Add more listeners - StartEventListening() NOT called again
281+
auto right_click_id = tray_icon->AddListener<TrayIconRightClickedEvent>(
282+
[](const TrayIconRightClickedEvent& event) {
283+
std::cout << "Tray icon right clicked" << std::endl;
284+
}
285+
);
286+
287+
// Remove one listener - StopEventListening() NOT called (still have listeners)
288+
tray_icon->RemoveListener(right_click_id);
289+
290+
// Remove last listener - triggers StopEventListening()
291+
tray_icon->RemoveListener(listener_id);
292+
293+
// Platform event monitoring is now STOPPED
294+
// (saves system resources again)
295+
```
296+
297+
#### Important Considerations
298+
299+
1. **Mutex Held**: `StartEventListening()` and `StopEventListening()` are called while holding `listeners_mutex_`. Keep the implementation fast and avoid acquiring other locks that could cause deadlocks.
300+
301+
2. **Default Implementation**: The default implementations are empty, so existing subclasses don't need to change unless they want to use this feature.
302+
303+
3. **Transitional Calls**: These methods are only called on transitions (0 → 1+ listeners and 1+ → 0 listeners), not on every add/remove operation.
304+
305+
4. **Destructor Cleanup**: If the emitter is destroyed while listeners exist, `StopEventListening()` is NOT called. Clean up resources in the destructor if needed.
306+
307+
5. **Idempotent Operations**: Implement setup/cleanup methods to be safe to call multiple times without side effects.
308+
309+
#### Migration from Old Pattern
310+
311+
##### Before (Always Monitoring)
312+
313+
```cpp
314+
TrayIcon::TrayIcon() : pimpl_(std::make_unique<Impl>()) {
315+
SetupEventMonitoring(); // Always monitoring
316+
}
317+
318+
TrayIcon::~TrayIcon() {
319+
CleanupEventMonitoring();
320+
}
321+
```
322+
323+
##### After (Lazy Monitoring)
324+
325+
```cpp
326+
TrayIcon::TrayIcon() : pimpl_(std::make_unique<Impl>()) {
327+
// No need to call SetupEventMonitoring()
328+
}
329+
330+
void TrayIcon::StartEventListening() {
331+
pimpl_->SetupEventMonitoring(); // Called when first listener added
332+
}
333+
334+
void TrayIcon::StopEventListening() {
335+
pimpl_->CleanupEventMonitoring(); // Called when last listener removed
336+
}
337+
```

src/foundation/event_emitter.h

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ class EventEmitter {
154154

155155
if (it != listeners.end()) {
156156
listeners.erase(it);
157+
158+
// Clean up empty vector
159+
if (listeners.empty()) {
160+
listeners_.erase(type);
161+
}
162+
163+
// Check if this was the last listener
164+
if (GetTotalListenerCountUnlocked() == 0) {
165+
StopEventListening();
166+
}
167+
157168
return true;
158169
}
159170
}
@@ -177,7 +188,14 @@ class EventEmitter {
177188
*/
178189
void RemoveAllListeners() {
179190
std::lock_guard<std::mutex> lock(listeners_mutex_);
191+
192+
bool had_listeners = GetTotalListenerCountUnlocked() > 0;
193+
180194
listeners_.clear();
195+
196+
if (had_listeners) {
197+
StopEventListening();
198+
}
181199
}
182200

183201
/**
@@ -196,13 +214,7 @@ class EventEmitter {
196214
*/
197215
size_t GetTotalListenerCount() const {
198216
std::lock_guard<std::mutex> lock(listeners_mutex_);
199-
200-
size_t count = 0;
201-
for (const auto& [type, listeners] : listeners_) {
202-
count += listeners.size();
203-
}
204-
205-
return count;
217+
return GetTotalListenerCountUnlocked();
206218
}
207219

208220
/**
@@ -292,6 +304,20 @@ class EventEmitter {
292304
}
293305

294306
protected:
307+
/**
308+
* Called when the first listener is added.
309+
* Subclasses can override this to start platform-specific event monitoring.
310+
* This is called while holding the listeners_mutex_ lock.
311+
*/
312+
virtual void StartEventListening() {}
313+
314+
/**
315+
* Called when the last listener is removed.
316+
* Subclasses can override this to stop platform-specific event monitoring.
317+
* This is called while holding the listeners_mutex_ lock.
318+
*/
319+
virtual void StopEventListening() {}
320+
295321
/**
296322
* Emit an event synchronously using perfect forwarding.
297323
* This creates the event object and emits it immediately.
@@ -356,9 +382,15 @@ class EventEmitter {
356382
size_t AddListener(std::type_index event_type, std::unique_ptr<EventListenerBase> listener) {
357383
std::lock_guard<std::mutex> lock(listeners_mutex_);
358384

385+
bool was_empty = GetTotalListenerCountUnlocked() == 0;
386+
359387
size_t listener_id = next_listener_id_.fetch_add(1);
360388
listeners_[event_type].push_back({std::move(listener), listener_id});
361389

390+
if (was_empty) {
391+
StartEventListening();
392+
}
393+
362394
return listener_id;
363395
}
364396

@@ -368,6 +400,11 @@ class EventEmitter {
368400
auto it = listeners_.find(event_type);
369401
if (it != listeners_.end()) {
370402
listeners_.erase(it);
403+
404+
// Check if this was the last listener
405+
if (GetTotalListenerCountUnlocked() == 0) {
406+
StopEventListening();
407+
}
371408
}
372409
}
373410

@@ -382,6 +419,14 @@ class EventEmitter {
382419
return 0;
383420
}
384421

422+
size_t GetTotalListenerCountUnlocked() const {
423+
size_t count = 0;
424+
for (const auto& [type, listeners] : listeners_) {
425+
count += listeners.size();
426+
}
427+
return count;
428+
}
429+
385430
// Background thread function for processing async events
386431
void ProcessAsyncEvents() {
387432
while (true) {

0 commit comments

Comments
 (0)