1 // Copyright (c) 2010, Thomas Goyne <plorkyeran@aegisub.org>
2 //
3 // Permission to use, copy, modify, and distribute this software for any
4 // purpose with or without fee is hereby granted, provided that the above
5 // copyright notice and this permission notice appear in all copies.
6 //
7 // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 
15 #pragma once
16 
17 #include <boost/config.hpp>
18 #include <functional>
19 #include <memory>
20 #include <vector>
21 
22 namespace agi { namespace signal {
23 class Connection;
24 
25 /// Implementation details; nothing outside this file should directly touch
26 /// anything in the detail namespace
27 namespace detail {
28 	class SignalBase;
29 	class ConnectionToken {
30 		friend class agi::signal::Connection;
31 		friend class SignalBase;
32 
33 		SignalBase *signal;
34 		bool blocked = false;
35 		bool claimed = false;
36 
ConnectionToken(SignalBase * signal)37 		ConnectionToken(SignalBase *signal) : signal(signal) { }
38 		inline void Disconnect();
39 	public:
~ConnectionToken()40 		~ConnectionToken() { Disconnect(); }
41 	};
42 }
43 
44 /// A connection which is not automatically closed
45 ///
46 /// Connections initially start out owned by the signal. If a slot knows that it
47 /// will outlive a signal and does not need to be able to block a connection, it
48 /// can simply ignore the return value of Connect.
49 ///
50 /// If a slot needs to be able to disconnect from a signal, it should store the
51 /// returned connection in a Connection, which transfers ownership of the
52 /// connection to the slot. If there is any chance that the signal will outlive
53 /// the slot, this must be done.
54 class UnscopedConnection {
55 	friend class Connection;
56 	detail::ConnectionToken *token;
57 public:
UnscopedConnection(detail::ConnectionToken * token)58 	UnscopedConnection(detail::ConnectionToken *token) : token(token) { }
59 };
60 
61 /// Object representing a connection to a signal
62 class Connection {
63 	std::unique_ptr<detail::ConnectionToken> token;
64 public:
65 	Connection() = default;
Connection(UnscopedConnection src)66 	Connection(UnscopedConnection src) BOOST_NOEXCEPT : token(src.token) { token->claimed = true; }
Connection(Connection && that)67 	Connection(Connection&& that) BOOST_NOEXCEPT : token(std::move(that.token)) { }
Connection(detail::ConnectionToken * token)68 	Connection(detail::ConnectionToken *token) BOOST_NOEXCEPT : token(token) { token->claimed = true; }
69 	Connection& operator=(Connection&& that) BOOST_NOEXCEPT { token = std::move(that.token); return *this; }
70 
71 	/// @brief End this connection
72 	///
73 	/// This normally does not need to be manually called, as a connection is
74 	/// automatically closed when all Connection objects referring to it are
75 	/// gone. To temporarily enable or disable a connection, use Block/Unblock
76 	/// instead
Disconnect()77 	void Disconnect() { if (token) token->Disconnect(); }
78 
79 	/// @brief Disable this connection until Unblock is called
Block()80 	void Block() { if (token) token->blocked = true; }
81 
82 	/// @brief Reenable this connection after it was disabled by Block
Unblock()83 	void Unblock() { if (token) token->blocked = false; }
84 };
85 
86 namespace detail {
87 	/// Polymorphic base class for slots
88 	///
89 	/// This class has two purposes: to avoid having to make Connection
90 	/// templated on what type of connection it is controlling, and to avoid
91 	/// some messiness with templated friend classes
92 	class SignalBase {
93 		friend class ConnectionToken;
94 		/// @brief Disconnect the passed slot from the signal
95 		/// @param tok Token to disconnect
96 		virtual void Disconnect(ConnectionToken *tok)=0;
97 
98 		/// Signals can't be copied
99 		SignalBase(SignalBase const&) = delete;
100 		SignalBase& operator=(SignalBase const&) = delete;
101 	protected:
102 		SignalBase() = default;
103 		/// @brief Notify a slot that it has been disconnected
104 		/// @param tok Token to disconnect
105 		///
106 		/// Used by the signal when the signal wishes to end a connection (such
107 		/// as if the signal is being destroyed while slots are still connected
108 		/// to it)
DisconnectToken(ConnectionToken * tok)109 		void DisconnectToken(ConnectionToken *tok) { tok->signal = nullptr; }
110 
111 		/// @brief Has a token been claimed by a scoped connection object?
TokenClaimed(ConnectionToken * tok)112 		bool TokenClaimed(ConnectionToken *tok) { return tok->claimed; }
113 
114 		/// @brief Create a new connection to this slot
MakeToken()115 		ConnectionToken *MakeToken() { return new ConnectionToken(this); }
116 
117 		/// @brief Check if a connection currently wants to receive signals
Blocked(ConnectionToken * tok)118 		bool Blocked(ConnectionToken *tok) { return tok->blocked; }
119 	};
120 
Disconnect()121 	inline void ConnectionToken::Disconnect() {
122 		if (signal) signal->Disconnect(this);
123 		signal = nullptr;
124 	}
125 }
126 
127 template<typename... Args>
128 class Signal final : private detail::SignalBase {
129 	using Slot = std::function<void(Args...)>;
130 	std::vector<std::pair<detail::ConnectionToken*, Slot>> slots; /// Signals currently connected to this slot
131 
Disconnect(detail::ConnectionToken * tok)132 	void Disconnect(detail::ConnectionToken *tok) override {
133 		for (auto it = begin(slots), e = end(slots); it != e; ++it) {
134 			if (tok == it->first) {
135 				slots.erase(it);
136 				return;
137 			}
138 		}
139 	}
140 
DoConnect(Slot sig)141 	UnscopedConnection DoConnect(Slot sig) {
142 		auto token = MakeToken();
143 		slots.emplace_back(token, sig);
144 		return UnscopedConnection(token);
145 	}
146 
147 public:
~Signal()148 	~Signal() {
149 		for (auto& slot : slots) {
150 			DisconnectToken(slot.first);
151 			if (!TokenClaimed(slot.first)) delete slot.first;
152 		}
153 	}
154 
155 	/// Trigger this signal
156 	///
157 	/// The order in which connected slots are called is undefined and should
158 	/// not be relied on
operator()159 	void operator()(Args... args) {
160 		for (size_t i = slots.size(); i > 0; --i) {
161 			if (!Blocked(slots[i - 1].first))
162 				slots[i - 1].second(args...);
163 		}
164 	}
165 
166 	/// @brief Connect a signal to this slot
167 	/// @param sig Signal to connect
168 	/// @return The connection object
Connect(Slot sig)169 	UnscopedConnection Connect(Slot sig) {
170 		return DoConnect(sig);
171 	}
172 
173 	// Convenience wrapper for a member function which matches the signal's signature
174 	template<typename T>
Connect(void (T::* func)(Args...),T * a1)175 	UnscopedConnection Connect(void (T::*func)(Args...), T* a1) {
176 		return DoConnect([=](Args... args) { (a1->*func)(args...); });
177 	}
178 
179 	// Convenience wrapper for a callable which does not use any signal args
180 	template<typename Thunk, typename = decltype((*(Thunk *)0)())>
Connect(Thunk && func)181 	UnscopedConnection Connect(Thunk&& func) {
182 		return DoConnect([=](Args... args) mutable { func(); });
183 	}
184 
185 	// Convenience wrapper for a member function which does not use any signal
186 	// args. The match is overly-broad to avoid having two methods with the
187 	// same signature when the signal has no args.
188 	template<typename T, typename MemberThunk>
Connect(MemberThunk func,T * obj)189 	UnscopedConnection Connect(MemberThunk func, T* obj) {
190 		return DoConnect([=](Args... args) { (obj->*func)(); });
191 	}
192 
193 	// Convenience wrapper for a member function which uses only the first
194 	// signal arg.
195 	template<typename T, typename Arg1>
Connect(void (T::* func)(Arg1),T * a1)196 	UnscopedConnection Connect(void (T::*func)(Arg1), T* a1) {
197 		return DoConnect(std::bind(func, a1, std::placeholders::_1));
198 	}
199 };
200 
201 /// Create a vector of scoped connections from an initializer list
202 ///
203 /// Required due to that initializer lists copy their input, and trying to pass
204 /// an initializer list directly to a vector results in a
205 /// std::initializer_list<Connection>, which can't be copied.
make_vector(std::initializer_list<UnscopedConnection> connections)206 inline std::vector<Connection> make_vector(std::initializer_list<UnscopedConnection> connections) {
207 	return std::vector<Connection>(std::begin(connections), std::end(connections));
208 }
209 
210 } }
211 
212 /// @brief Define functions which forward their arguments to the connect method
213 ///        of the named signal
214 /// @param sig Name of the signal
215 /// @param method Name of the connect method
216 ///
217 /// When a signal is a member of a class, we typically want other objects to be
218 /// able to connect to the signal, but not to be able to trigger it. To do this,
219 /// make this signal private then use this macro to create public subscription
220 /// methods
221 ///
222 /// Defines AddSignalNameListener
223 #define DEFINE_SIGNAL_ADDERS(sig, method) \
224 	template<typename... Args> \
225 	agi::signal::UnscopedConnection method(Args&&... args) { \
226 		return sig.Connect(std::forward<Args>(args)...); \
227 	}
228