Skip to content

Commit e9dea50

Browse files
committed
improve event documentation
1 parent 079db9b commit e9dea50

File tree

1 file changed

+146
-50
lines changed

1 file changed

+146
-50
lines changed

tutorials/events.md

Lines changed: 146 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,76 +6,136 @@ As an alternative to this system, Geode introduces **events**. Events are essent
66

77
Events are primarily interacted with through three classes: [`Event`](/classes/geode/Event), [`EventListener`](/classes/geode/EventListener), and [`EventFilter`](/classes/geode/EventFilter). `Event` is the base class for the events that are being broadcast; `EventListener` listens for events, and it uses an `EventFilter` to decide which events to listen to. Let's explore how they work through **an example**.
88

9+
## Creating events
10+
911
Consider a system where one mod introduces a **drag-and-drop API** to Geode, so the user can just drag files over the GD window. Now, this mod itself probably won't know how to handle every single file type ever; instead, it exposes an API so other mods can handle actually dealing with different file types. However, using a delegate-based system here would be quite undesirable - there is definitely more than one file type in existence. While the mod could just have a list of delegates instead of a single one, those delegates have to manually deal with telling the mod to remove themselves from the list when they want to stop listening for events.
1012

1113
Instead, the drag-and-drop API should leveradge the Geode event system by first defining a new type of event based on the `Event` base class:
1214

1315
```cpp
16+
// DragDropEvent.hpp
17+
18+
#include <Geode/loader/Event.hpp> // Event
19+
#include <Geode/cocos/cocoa/CCGeometry.h> // CCPoint
20+
21+
#include <vector> // std::vector
22+
#include <filesystem> // std::filesystem::path
23+
1424
using namespace geode::prelude;
1525

1626
class DragDropEvent : public Event {
17-
public:
18-
using Files = std::vector<ghc::filesystem::path>;
19-
2027
protected:
21-
Files m_files;
28+
std::vector<std::filesystem::path> m_files;
2229
CCPoint m_location;
2330

2431
public:
25-
DragDropEvent(Files const& files, CCPoint const& location);
32+
DragDropEvent(std::vector<std::filesystem::path> const& files, CCPoint const& location);
2633

27-
Files getFiles() const;
34+
std::vector<std::filesystem::path> getFiles() const;
2835
CCPoint getLocation() const;
2936
};
3037
```
3138
39+
> :warning: Note that we have structured the event this way (with protected variables and getters) so that it is **read-only**.
40+
3241
Now, the drag-and-drop mod can post new events by simple creating a `DragDropEvent` and calling `post` on it.
3342
3443
```cpp
35-
void handleFilesDroppedOnWindow(...) {
36-
...
44+
// Assume those variables actually have useful values
45+
std::vector<std::filesystem::path> files;
46+
CCPoint location = CCPoint { 0.0f, 0.0f };
47+
48+
DragDropEvent(files, location).post();
3749
38-
DragDropEvent(...).post();
39-
}
4050
```
4151

4252
That's all - the drag-and-drop mod can now rest assured that any mod expecting drag-and-drop events has received them.
4353

44-
Let's see how that listening part goes, through an example mod that expects [GDShare](https://github.com/hjfod/GDShare-mod) level files and imports them:
54+
## Listening to events
55+
56+
Listening to events is done using an `EventListener`. An event listener needs an `EventFilter`, so that it knows what events to listen to. This default EventFilter will pass a **pointer** to the event as the parameter to the callback we have to define. Here is a very simple example that listens to events for the **entire runtime of the game** - a **global listener**, as you might call it.
57+
58+
```cpp
59+
// main.cpp
60+
61+
#include <Geode/DefaultInclude.hpp> // $execute
62+
#include <Geode/loader/Event.hpp> // EventListener, EventFilter
63+
64+
#include "DragDropEvent.hpp" // Our created event
65+
66+
using namespace geode::prelude;
67+
68+
// Execute runs the code inside **when your mod is loaded**
69+
$execute {
70+
// This technically doesn't leak memory, since the listener should live for the entirety of the program
71+
new EventListener<EventFilter<DragDropEvent>>(+[](DragDropEvent* ev) {
72+
for (std::filesystem::path& file : ev->getFiles()) {
73+
log::debug("File dropped: {}", file);
74+
75+
// ... handle the files here
76+
}
77+
78+
// We have to propagate the event further, so that other listeners
79+
// can handle this event
80+
return ListenerResult::Propagate;
81+
});
82+
}
83+
```
84+
85+
Notice that our callback returns a `ListenerResult`, more specifically `ListenerResult::Propagate`. This tells the event system that this specific event should **propagate** to the next listeners that are expecting this type of event. If you wish to **stop** this propagation from happening (let's say you don't want **.gmd** files to be propagated to other listeners), then you can return `ListenerResult::Stop`.
4586

4687
```cpp
88+
// main.cpp
89+
90+
#include <Geode/DefaultInclude.hpp> // $execute
91+
#include <Geode/loader/Event.hpp> // EventListener, EventFilter
92+
93+
#include "DragDropEvent.hpp" // Our created event
94+
4795
using namespace geode::prelude;
4896

4997
$execute {
5098
new EventListener<EventFilter<DragDropEvent>>(+[](DragDropEvent* ev) {
51-
for (auto& file : ev->getFiles()) {
99+
for (std::filesystem::path& file : ev->getFiles()) {
100+
log::debug("File dropped: {}", file);
52101
if (file.extension() == ".gmd") {
53-
handleGMDImport(file);
102+
log::info("Detected .gmd file: {}", file);
54103

55-
// This stops event propagation, marking the file as handled.
56-
// Stopping propagation is usually not needed, and shouldn't be
57-
// done by default, however sometimes you want to stop other
58-
// listeners from dealing with the event - such as here, where
59-
// importing the same file twice would be undesirable
104+
// Stop event propagation after this listener.
60105
return ListenerResult::Stop;
61106
}
62107
}
63-
// Propagate this event down the chain; aka, let other listeners see
64-
// the event
108+
109+
// If no .gmd file was detected, propagate the event further
65110
return ListenerResult::Propagate;
66111
});
67112
}
68113
```
69114

70115
This is all the mod needs to do to set up a **global listener** - one that exists for the entire duration of the mod. Now, whenever a `DragDropEvent` is posted, the mod catches it and can do whatever it wants with it.
71116

72-
This code also uses the default templated `EventFilter` class, which just checks if an event is right type and then calls the callback if that is the case, stopping propagation if the callback requests it to do so. However, we sometimes also want to create custom filters.
117+
This code also uses the default templated `EventFilter` class, which just checks if an event is right type and then calls the callback if that is the case, stopping propagation if the callback requests it to do so. However, we sometimes also want to create **custom filters**.
118+
119+
## Creating custom filters
73120

74-
For example, let's say another mod wants to use the drag-and-drop API, but instead of always listening for events, it wants to have a specific node in the UI that the user should drop files over. In this case, the listener should only exist while the node exists, and only accept events if it's over the node. We could of course deal with this in the global callback, however we can simplify our code by creating a custom filter that handles accepting the event. We can also include the file types to listen for in the filter itself, simplifying our code even further.
121+
For example, let's say another mod wants to use the drag-and-drop API, but instead of always listening for events, it wants to have a **specific node in the UI that the user should drop files over**. In this case, the listener should only exist **while the node exists**, and only accept events if it's over the node. We could of course deal with this in the global callback, however we can simplify our code by creating a custom filter that handles accepting the event. We can also include the file types to listen for in the filter itself, simplifying our code even further.
75122

76123
We can create a custom `EventFilter` by inheriting from it:
77124

78125
```cpp
126+
// DragDropOnNodeFilter.hpp
127+
128+
#include <Geode/cocos/base_nodes/CCNode.h> // CCNode
129+
#include <Geode/loader/Event.hpp> // EventFilter
130+
131+
#include "DragDropEvent.hpp" // Our event
132+
133+
#include <filesystem> // std::filesystem::path
134+
#include <functional> // std::function
135+
#include <string> // std::string
136+
#include <unordered_set> // std::unordered_set
137+
#include <vector> // std::vector
138+
79139
using namespace geode::prelude;
80140

81141
class DragDropOnNodeFilter : public EventFilter<DragDropEvent> {
@@ -84,74 +144,110 @@ protected:
84144
std::unordered_set<std::string> m_filetypes;
85145

86146
public:
87-
// The callback does not need to return ListenerResult nor take the whole
88-
// DragDropEvent as a parameter - if the callback is called, then we know
89-
// the event was over the node and the file types were correct already
90-
using Callback = void(std::vector<ghc::filesystem::path> const&);
91-
92-
ListenerResult handle(MiniFunction<Callback> fn, DragDropEvent* event);
93-
DragDropOnNodeFilter(CCNode* target, std::unordered_set<std::string> const& types);
147+
// We HAVE to specify this alias, the EventListener makes use of it.
148+
// The default EventFilter<DragDropEvent> would send the event itself as the callback argument
149+
// For the default EventFilter<DragDropEvent>, the Callback alias looks like:
150+
// using Callback = ListenerResult(DragDropEvent*)
151+
//
152+
// In this case, though, we want to filter the files that we need, so we will only use a vector of files.
153+
using Callback = ListenerResult(std::vector<std::filesystem::path> const&);
154+
155+
// This method also needs to exist
156+
ListenerResult handle(std::function<Callback> fn, DragDropEvent* event);
157+
DragDropOnNodeFilter(CCNode* target, std::unordered_set<std::string> const& types)
158+
: m_target(target),
159+
m_types(types) {}
94160
};
95161
```
96162

163+
> :warning: You have a lot of freedom when defining the EventFilter callback. Remember that the default `EventFilter<Event>` is of type `std::function<ListenerResult(Event*)>`, but your callback can look differently.
164+
97165
For the implementation of `handle`, we need to check that the event occurred on top of the target node:
98166

99167
```cpp
100-
ListenerResult DragDropOnNodeFilter::handle(MiniFunction<Callback> fn, DragDropEvent* event) {
101-
// Check if the event happened over the node
102-
if (m_target->boundingBox().containsPoint(event->getLocation())) {
103-
// Filter out only file types we can accept
104-
std::vector<ghc::filesystem::path> valid;
105-
for (auto& file : event->getFiles()) {
106-
if (m_filetypes.contains(file.extension().string())) {
107-
valid.push(file);
108-
}
168+
ListenerResult DragDropOnNodeFilter::handle(std::function<Callback> fn, DragDropEvent* event) {
169+
// If the event didn't happen over the node, just propagate the event further
170+
if (!m_target->boundingBox().containsPoint(event->getLocation())) {
171+
return ListenerResult::Propagate;
172+
}
173+
174+
std::vector<std::filesystem::path> valid;
175+
176+
// Filter the files to only include valid ones
177+
for (std::filesystem::path& file : event->getFiles()) {
178+
if (m_filetypes.contains(file.extension().string())) {
179+
valid.push_back(file);
109180
}
110-
fn(valid);
111-
// Mark dropped files as handled
112-
return ListenerResult::Stop;
113181
}
114-
// Otherwise let other listeners handle it
115-
return ListenerResult::Propagate;
182+
183+
// If there are no valid files, propagate the event further
184+
if (valid.size() == 0) {
185+
return ListenerResult::Propagate;
186+
}
187+
188+
// Call the EventListener callback and return the ListenerResult that it gives us
189+
return fn(valid);
116190
}
117191
```
118192
119193
Now, to install an event listener on a specific node, we have two options. If the node is our own class, we can just add it as a class member:
120194
121195
```cpp
196+
// DragDropNode.hpp
197+
198+
#include <Geode/cocos/base_nodes/CCNode.h> // CCNode
199+
#include <Geode/loader/Event.hpp> // EventListener
200+
201+
#include "DragDropOnNodeFilter.hpp" // Our filter
202+
122203
class DragDropNode : public CCNode {
123204
protected:
124205
EventListener<DragDropOnNodeFilter> m_listener = {
125-
// You can bind member functions as event listeners too!
126206
this, &DragDropNode::onDrop,
127-
// The filter requires some args so we have to explicitly construct it
207+
// We defined a constructor with some arguments, so we have to construct our filter now
128208
DragDropOnNodeFilter(this, { ".gmd", ".gmd2", ".lvl" })
129209
};
130210
131-
void onDrop(std::vector<ghc::filesystem::path> const& files) {
211+
ListenerResult onDrop(std::vector<std::filesystem::path> const& files) {
132212
// Handle dropped files
213+
214+
return ListenerResult::Propagate;
133215
}
134216
};
135217
```
136218

137-
When `DragDropNode` is destroyed, the listener is automatically destroyed and unregistered aswell, so you don't need to do anything else.
219+
There are multiple ways to define a callback for our listener. The method above uses a **member function**. We can also define a **lambda**:
220+
```cpp
221+
EventListener<DragDropOnNodeFilter> m_listener = {
222+
[](std::vector<std::filesystem::path> const& files) {
223+
// Handle dropped files...
224+
225+
return ListenerResult::Propagate;
226+
},
227+
DragDropOnNodeFilter(this, { ".gmd", ".gmd2", ".lvl" })
228+
};
229+
```
230+
231+
When our `DragDropNode` is destroyed, the EventListener is automatically destroyed and unregistered aswell, so you don't need to do anything else.
138232

139233
However, using a member function is not always possible. For example, if you're hooking a class, [event listeners don't work in fields](/tutorials/fields.md#note-about-addresses); or if you want to listen for events on an existing node whose class you don't control.
140234

141235
In these cases, there exists a Geode-specific helper called [`CCNode::addEventListener`](/classes/cocos2d/CCNode#addEventListener). You can use this to **add event listeners to any node** - including existing ones by GD!
142236

237+
> :warning: Any `EventFilter` that is used in `addEventListener` **must** have their first **constructor param** as a `CCNode*`. The callback **lambda** should be the first argument passed to `addEventListener`, then you have to pass the next **constructor arguments** for your `EventFilter`
238+
143239
```cpp
144240
auto dragDropNode = CCNode::create();
145241
dragDropNode->addEventListener<DragDropOnNodeFilter>(
146-
[dragDropNode](auto const& files) {
242+
[dragDropNode](std::vector<std::filesystem::path> const& files) {
147243
// Handle dropped files
244+
245+
return ListenerResult::Propagate;
148246
},
149247
{ ".gmd", ".gmd2", ".lvl" }
150248
);
151249
```
152250
153-
`addEventListener` is meant only for events that are have a target node - it assumes that the first parameter of the filter's constructor takes `this` as the argument. Other parameters to the filter's constructor, such as the file types here, can be passed as the rest of the argument list to `addEventListener`.
154-
155251
Any event listener added with `addEventListener` is automatically destroyed aswell when the node is destroyed. You can also provide a string ID for the event listener as the first argument to `addEventListener`, and then manually remove the listener later using [`removeEventListener`](/classes/cocos2d/CCNode#removeEventListener).
156252
157253
## Dispatched events

0 commit comments

Comments
 (0)