From 4fc1ff6ad69a44771a765e5f9e2be9fc7c8ee893 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Tue, 7 Dec 2010 19:09:08 +0000 Subject: [PATCH] Add simple signal/slot implementation loosly based on boost.sigal Originally committed to SVN as r4898. --- .../libaegisub_vs2008.vcproj | 126 ++++++-- .../build/tests_vs2008/tests_vs2008.vcproj | 122 +++++-- .../libaegisub/include/libaegisub/signals.h | 297 ++++++++++++++++++ aegisub/tests/Makefile | 1 + aegisub/tests/libaegisub_signals.cpp | 105 +++++++ 5 files changed, 609 insertions(+), 42 deletions(-) create mode 100644 aegisub/libaegisub/include/libaegisub/signals.h create mode 100644 aegisub/tests/libaegisub_signals.cpp diff --git a/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj b/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj index 251fbf2bc..a6f8013ac 100644 --- a/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj +++ b/aegisub/build/libaegisub_vs2008/libaegisub_vs2008.vcproj @@ -76,6 +76,65 @@ Name="VCPostBuildEventTool" /> + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + @@ -259,7 +337,7 @@ /> + + diff --git a/aegisub/build/tests_vs2008/tests_vs2008.vcproj b/aegisub/build/tests_vs2008/tests_vs2008.vcproj index 4d24de1a7..c8efa5d66 100644 --- a/aegisub/build/tests_vs2008/tests_vs2008.vcproj +++ b/aegisub/build/tests_vs2008/tests_vs2008.vcproj @@ -89,6 +89,65 @@ CommandLine="cd "$(ExecutableOutDir)" "$(ProjectDir)\..\..\tests\setup.bat" "$(ProjectDir)\..\..\tests" " /> + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + @@ -245,6 +323,10 @@ RelativePath="..\..\tests\libaegisub_option.cpp" > + + diff --git a/aegisub/libaegisub/include/libaegisub/signals.h b/aegisub/libaegisub/include/libaegisub/signals.h new file mode 100644 index 000000000..c5ee851fa --- /dev/null +++ b/aegisub/libaegisub/include/libaegisub/signals.h @@ -0,0 +1,297 @@ +// Copyright (c) 2010, Thomas Goyne +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// $Id$ + +/// @file signals.h +/// @brief +/// @ingroup libaegisub + +#pragma once + +#if !defined(AGI_PRE) && !defined(LAGI_PRE) +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#include +#endif +#endif + +namespace agi { + namespace signal { + +using namespace std::tr1::placeholders; + +class Connection; + +/// Implementation details; nothing outside this file should directly touch +/// anything in the detail namespace +namespace detail { + class SignalBase; + class ConnectionToken { + friend class agi::signal::Connection; + friend class SignalBase; + + SignalBase *signal; + bool blocked; + bool claimed; + + ConnectionToken(SignalBase *signal) : signal(signal), blocked(false), claimed(false) { } + inline void Disconnect(); + public: + ~ConnectionToken() { Disconnect(); } + }; +} + +/// @class Connection +/// @brief Object representing a connection to a signal +class Connection { + std::tr1::shared_ptr token; +public: + Connection() { } + Connection(detail::ConnectionToken *token) : token(token) { token->claimed = true; } + + /// @brief End this connection + /// + /// This normally does not need to be manually called, as a connection is + /// automatically closed when all Connection objects referring to it are + /// gone. To temporarily enable or disable a connection, use Block/Unblock + /// instead + void Disconnect() { if (token.get()) token->Disconnect(); } + + /// @brief Disable this connection until Unblock is called + void Block() { if (token.get()) token->blocked = true; } + + /// @brief Reenable this connection after it was disabled by Block + void Unblock() { if (token.get()) token->blocked = false; } +}; + +/// @class UnscopedConnection +/// @brief A connection which is not automatically closed +/// +/// Connections initially start out owned by the signal. If a slot knows that it +/// will outlive a signal and does not need to be able to block a connection, it +/// can simply ignore the return value of Connect. +/// +/// If a slot needs to be able to disconnect from a signal, it should store the +/// returned connection in a Connection, which transfers ownership of the +/// connection to the slot. If there is any chance that the signal will outlive +/// the slot, this must be done. +class UnscopedConnection { + detail::ConnectionToken *token; +public: + UnscopedConnection(detail::ConnectionToken *token) : token(token) { } + operator Connection() { return Connection(token); } +}; + +namespace detail { + /// @brief Polymorphic base class for slots + /// + /// This class has two purposes: to avoid having to make Connection + /// templated on what type of connection it is controlling, and to avoid + /// some messiness with templated friend classes + class SignalBase { + friend class ConnectionToken; + /// @brief Disconnect the passed slot from the signal + /// @param tok Token to disconnect + virtual void Disconnect(ConnectionToken *tok)=0; + protected: + /// @brief Notify a slot that it has been disconnected + /// @param tok Token to disconnect + /// + /// Used by the signal when the signal wishes to end a connection (such + /// as if the signal is being destroyed while slots are still connected + /// to it) + void DisconnectToken(ConnectionToken *tok) { tok->signal = NULL; } + + /// @brief Has a token been claimed by a scoped connection object? + bool TokenClaimed(ConnectionToken *tok) { return tok->claimed; } + + /// @brief Create a new connection to this slot + ConnectionToken *MakeToken() { return new ConnectionToken(this); } + + /// @brief Check if a connection currently wants to receive signals + bool Blocked(ConnectionToken *tok) { return tok->blocked; } + }; + + inline void ConnectionToken::Disconnect() { + if (signal) signal->Disconnect(this); + signal = NULL; + } + + /// @brief Templated common code for signals + template + class SignalBaseImpl : public SignalBase { + protected: + typedef std::map SlotMap; + + SlotMap slots; /// Signals currently connected to this slot + + void Disconnect(ConnectionToken *tok) { + slots.erase(tok); + } + + /// Protected destructor so that we don't need a virtual destructor + ~SignalBaseImpl() { + for (typename SlotMap::iterator cur = slots.begin(); cur != slots.end(); ++cur) { + DisconnectToken(cur->first); + if (!TokenClaimed(cur->first)) delete cur->first; + } + } + public: + /// @brief Connect a signal to this slot + /// @param sig Signal to connect + /// @return The connection object + UnscopedConnection Connect(Slot sig) { + ConnectionToken *token = MakeToken(); + slots.insert(std::make_pair(token, sig)); + return UnscopedConnection(token); + } + template + UnscopedConnection Connect(F func, Arg1 a1) { + return Connect(std::tr1::bind(func, a1)); + } + template + UnscopedConnection Connect(F func, Arg1 a1, Arg2 a2) { + return Connect(std::tr1::bind(func, a1, a2)); + } + template + UnscopedConnection Connect(F func, Arg1 a1, Arg2 a2, Arg3 a3) { + return Connect(std::tr1::bind(func, a1, a2, a3)); + } + }; +} + +#define SIGNALS_H_FOR_EACH_SIGNAL(...) \ + for (typename super::SlotMap::iterator cur = slots.begin(); cur != slots.end(); ++cur) { \ + if (!Blocked(cur->first)) cur->second(__VA_ARGS__); \ + } + +/// @class Signal +/// @brief Two-argument signal +/// @param Arg1 Type of first argument to pass to slots +/// @param Arg2 Type of second argument to pass to slots +template +class Signal : public detail::SignalBaseImpl > { + typedef detail::SignalBaseImpl > super; + using super::Blocked; + using super::slots; +public: + Signal() { } + + /// @brief Trigger this signal + /// @param a1 First argument to the signal + /// @param a2 Second argument to the signal + /// + /// The order in which connected slots are called is undefined and should + /// not be relied on + void operator()(Arg1 a1, Arg2 a2) { SIGNALS_H_FOR_EACH_SIGNAL(a1, a2) } + + // Don't hide the base overloads + using super::Connect; + + /// @brief Connect a member function with the correct signature to this signal + /// @param func Function to connect + /// @param a1 Object + /// + /// This overload is purely for convenience so that classes can do + /// sig.Connect(&Class::Foo, this) rather than + /// sig.Connect(&Class::Foo, this, _1, _2) + template + UnscopedConnection Connect(void (T::*func)(Arg1, Arg2), T* a1) { + return Connect(std::tr1::bind(func, a1, _1, _2)); + } +}; + +/// @class Signal +/// @brief One-argument signal +/// @param Arg1 Type of the argument to pass to slots +template +class Signal : public detail::SignalBaseImpl > { + typedef detail::SignalBaseImpl > super; + using super::Blocked; + using super::slots; +public: + Signal() { } + + /// @brief Trigger this signal + /// @param a1 The argument to the signal + /// + /// The order in which connected slots are called is undefined and should + /// not be relied on + void operator()(Arg1 a1) { SIGNALS_H_FOR_EACH_SIGNAL(a1) } + + // Don't hide the base overloads + using super::Connect; + + /// @brief Connect a member function with the correct signature to this signal + /// @param func Function to connect + /// @param a1 Object + /// + /// This overload is purely for convenience so that classes can do + /// sig.Connect(&Class::Foo, this) rather than sig.Connect(&Class::Foo, this, _1) + template + UnscopedConnection Connect(void (T::*func)(Arg1), T* a1) { + return Connect(std::tr1::bind(func, a1, _1)); + } +}; + +/// @class Signal +/// @brief Zero-argument signal +template<> +class Signal : public detail::SignalBaseImpl > { + typedef detail::SignalBaseImpl > super; + using super::Blocked; + using super::slots; +public: + Signal() { } + +#ifdef _WIN32 +// MSVC incorrectly considers this not a template context due to it being fully +// specified, making typename invalid here +#define typename +#endif + + /// @brief Trigger this signal + /// + /// The order in which connected slots are called is undefined and should + /// not be relied on + void operator()() { SIGNALS_H_FOR_EACH_SIGNAL() } +#undef typename +}; + +#undef SIGNALS_H_FOR_EACH_SIGNAL + } +} + +/// @brief Define functions which forward their arguments to the connect method +/// of the named signal +/// @param sig Name of the signal +/// @param method Name of the connect method +/// +/// When a signal is a member of a class, we typically want other objects to be +/// able to connect to the signal, but not to be able to trigger it. To do this, +/// make this signal private then use this macro to create public subscription +/// methods +/// +/// Defines AddSignalNameListener +#define DEFINE_SIGNAL_ADDERS(sig, method) \ + template agi::signal::UnscopedConnection method(A a) { return sig.Connect(a); } \ + template agi::signal::UnscopedConnection method(A a,B b) { return sig.Connect(a,b); } \ + template agi::signal::UnscopedConnection method(A a,B b,C c) { return sig.Connect(a,b,c); } \ + template agi::signal::UnscopedConnection method(A a,B b,C c,D d) { return sig.Connect(a,b,c,d); } diff --git a/aegisub/tests/Makefile b/aegisub/tests/Makefile index 8f6a69b16..003f5ac6b 100644 --- a/aegisub/tests/Makefile +++ b/aegisub/tests/Makefile @@ -22,6 +22,7 @@ SRC = \ libaegisub_iconv.cpp \ libaegisub_line_iterator.cpp \ libaegisub_mru.cpp \ + libaegisub_signals.cpp \ libaegisub_util.cpp \ libaegisub_vfr.cpp diff --git a/aegisub/tests/libaegisub_signals.cpp b/aegisub/tests/libaegisub_signals.cpp new file mode 100644 index 000000000..fc83cc7ac --- /dev/null +++ b/aegisub/tests/libaegisub_signals.cpp @@ -0,0 +1,105 @@ +// Copyright (c) 2010, Thomas Goyne +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// $Id$ + +/// @file libaegisub_signals.cpp +/// @brief agi::signals tests +/// @ingroup + +#include + +#include "main.h" + +using namespace agi::signal; + +struct increment { + int *num; + increment(int &num) : num(&num) { } + void operator()() const { ++*num; } + void operator()(int n) const { *num += n; } +}; + +TEST(lagi_signal, basic) { + Signal<> s; + int x = 0; + Connection c = s.Connect(increment(x)); + + EXPECT_EQ(0, x); + s(); + EXPECT_EQ(1, x); +} + +TEST(lagi_signal, multiple) { + Signal<> s; + int x = 0; + Connection c1 = s.Connect(increment(x)); + Connection c2 = s.Connect(increment(x)); + + EXPECT_EQ(0, x); + s(); + EXPECT_EQ(2, x); +} +TEST(lagi_signal, manual_disconnect) { + Signal<> s; + int x = 0; + Connection c1 = s.Connect(increment(x)); + EXPECT_EQ(0, x); + s(); + EXPECT_EQ(1, x); + c1.Disconnect(); + s(); + EXPECT_EQ(1, x); +} + +TEST(lagi_signal, auto_disconnect) { + Signal<> s; + int x = 0; + + EXPECT_EQ(0, x); + { + Connection c = s.Connect(increment(x)); + s(); + EXPECT_EQ(1, x); + } + s(); + EXPECT_EQ(1, x); +} + +TEST(lagi_signal, connection_outlives_slot) { + int x = 0; + Connection c; + + EXPECT_EQ(0, x); + { + Signal<> s; + c = s.Connect(increment(x)); + s(); + EXPECT_EQ(1, x); + } + c.Disconnect(); +} + +TEST(lagi_signal, one_arg) { + Signal s; + int x = 0; + + Connection c = s.Connect(increment(x)); + s(0); + EXPECT_EQ(0, x); + s(10); + EXPECT_EQ(10, x); + s(20); + EXPECT_EQ(30, x); +}