2017-03-08 04:25:33 +01:00
|
|
|
/*
|
2017-03-18 01:55:45 +01:00
|
|
|
CommandSetup - Implementation of command registration and instance management
|
2017-03-08 04:25:33 +01:00
|
|
|
|
|
|
|
|
Copyright (C) Lumiera.org
|
|
|
|
|
2017, Hermann Vosseler <Ichthyostega@web.de>
|
|
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
|
|
|
modify it under the terms of the GNU General Public License as
|
|
|
|
|
published by the Free Software Foundation; either version 2 of
|
|
|
|
|
the License, or (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
|
along with this program; if not, write to the Free Software
|
|
|
|
|
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
|
|
|
|
|
|
|
|
* *****************************************************/
|
|
|
|
|
|
|
|
|
|
|
2017-03-18 01:55:45 +01:00
|
|
|
/** @file command-setup.cpp
|
2017-03-08 04:25:33 +01:00
|
|
|
** Implementation details of instance management for command invocation by the GUI.
|
2017-04-08 15:42:51 +02:00
|
|
|
** This translation unit combines the implementation code of several facilities
|
|
|
|
|
** related to definition and instantiation of concrete command instances.
|
2017-03-08 04:25:33 +01:00
|
|
|
**
|
2017-03-18 01:55:45 +01:00
|
|
|
** @see command-setup.hpp
|
|
|
|
|
** @see command-instance-manager.hpp
|
2017-04-08 15:42:51 +02:00
|
|
|
** @see CommandInstanceManager_test
|
2017-03-08 04:25:33 +01:00
|
|
|
** @see command.hpp
|
|
|
|
|
** @see command-registry.hpp
|
|
|
|
|
**
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "lib/error.hpp"
|
2017-03-18 05:28:56 +01:00
|
|
|
#include "include/logging.h"
|
2017-03-31 04:36:26 +02:00
|
|
|
#include "include/lifecycle.h"
|
2018-11-15 23:42:43 +01:00
|
|
|
#include "steam/control/command.hpp"
|
|
|
|
|
#include "steam/control/command-setup.hpp"
|
|
|
|
|
#include "steam/control/command-instance-manager.hpp"
|
|
|
|
|
#include "steam/control/command-def.hpp"
|
2017-03-31 23:59:22 +02:00
|
|
|
#include "lib/symbol.hpp"
|
|
|
|
|
#include "lib/format-string.hpp"
|
|
|
|
|
#include "lib/util.hpp"
|
2017-03-18 04:40:16 +01:00
|
|
|
|
|
|
|
|
#include <tuple>
|
|
|
|
|
#include <utility>
|
2017-03-08 04:25:33 +01:00
|
|
|
|
2017-03-18 04:40:16 +01:00
|
|
|
using std::tuple;
|
2017-03-18 05:28:56 +01:00
|
|
|
using std::get;
|
2017-03-18 03:52:18 +01:00
|
|
|
using std::function;
|
2017-03-18 04:40:16 +01:00
|
|
|
using std::move;
|
2017-03-18 05:28:56 +01:00
|
|
|
using lib::Symbol;
|
2017-03-31 04:36:26 +02:00
|
|
|
using lumiera::LifecycleHook;
|
|
|
|
|
using lumiera::ON_GLOBAL_INIT;
|
2017-03-31 23:59:22 +02:00
|
|
|
using std::string;
|
2017-04-01 00:37:12 +02:00
|
|
|
using util::unConst;
|
2017-03-31 23:59:22 +02:00
|
|
|
using util::_Fmt;
|
2017-03-08 04:25:33 +01:00
|
|
|
|
|
|
|
|
|
2018-11-15 23:55:13 +01:00
|
|
|
namespace steam {
|
2017-03-08 04:25:33 +01:00
|
|
|
namespace control {
|
|
|
|
|
namespace error = lumiera::error;
|
|
|
|
|
|
2017-03-31 18:15:02 +02:00
|
|
|
namespace { // implementation details: storage for pending static command definitions...
|
2017-03-18 04:40:16 +01:00
|
|
|
|
2017-03-18 05:28:56 +01:00
|
|
|
using CmdDefEntry = std::tuple<Symbol, DefinitionClosure>;
|
2017-03-18 04:40:16 +01:00
|
|
|
|
2017-05-02 01:37:17 +02:00
|
|
|
std::deque<CmdDefEntry>&
|
|
|
|
|
pendingCmdDefinitions()
|
|
|
|
|
{
|
|
|
|
|
static std::deque<CmdDefEntry> definitionQueue;
|
|
|
|
|
return definitionQueue;
|
|
|
|
|
}
|
2017-03-18 04:40:16 +01:00
|
|
|
|
|
|
|
|
}//(End) implementation details
|
2017-03-08 04:25:33 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-03-18 03:20:05 +01:00
|
|
|
CommandSetup::~CommandSetup() { }
|
|
|
|
|
|
2018-11-15 21:13:52 +01:00
|
|
|
/** Start a command setup for defining a Steam-Layer command with the given cmdID
|
2017-03-31 18:15:02 +02:00
|
|
|
* @param cmdID the ID under with the new command will be registered
|
|
|
|
|
* @note after defining a static variable of type CommandSetup,
|
|
|
|
|
* a functor or lambda should be assigned, which then
|
|
|
|
|
* provides the actual setup of the CommandDef
|
|
|
|
|
*/
|
2017-03-31 04:14:45 +02:00
|
|
|
CommandSetup::CommandSetup(Symbol cmdID)
|
2017-03-18 03:20:05 +01:00
|
|
|
: cmdID_(cmdID)
|
|
|
|
|
{ }
|
2017-03-18 03:52:18 +01:00
|
|
|
|
|
|
|
|
|
2017-03-18 04:40:16 +01:00
|
|
|
/**
|
|
|
|
|
* @param definitionBlock anything assignable to `function<void(CommandDef&)>`
|
|
|
|
|
* @remarks this operation is intended for a very specific usage pattern, as established
|
|
|
|
|
* by the macro #COMMAND_DEFINITION. The purpose is to feed a given code block
|
|
|
|
|
* into the hidden queue for command definitions, from where it will be issued
|
|
|
|
|
* at the lifecycle event ON_BASIC_INIT (typically at start of application `main()`).
|
|
|
|
|
* On invocation, the code block is provided with an still unbound CommandDef object,
|
|
|
|
|
* which has been registered under the Command-ID as stored in this CommandSetup object.
|
|
|
|
|
* The assumption is that this _definition closure_ will care to define the command,
|
|
|
|
|
* state capturing and undo operations for the command definition in question. Thus,
|
|
|
|
|
* the result of invoking this closure will be to store a complete command prototype
|
|
|
|
|
* into the proc::control::CommandRegistry.
|
|
|
|
|
* @note this operation works by side-effect; the given argument is fed into a hidden static
|
|
|
|
|
* queue, but not stored within the object instance.
|
|
|
|
|
* @warning invoking this assignment _several times on the same CommandSetup object_ will likely
|
|
|
|
|
* lead to an invalid state, causing the Lumiera application to fail on start-up. The
|
|
|
|
|
* reason for this is the fact that CommandDef rejects duplicate command definitions.
|
|
|
|
|
* Moreover, please note that invoking this operation at any point _after_ the
|
|
|
|
|
* lifecycle event ON_BASIC_INIT will likely have no effect at all, since the
|
2017-03-31 18:15:02 +02:00
|
|
|
* given closure will then just sit in the static queue and never be invoked.
|
2017-03-18 04:40:16 +01:00
|
|
|
*/
|
2017-03-18 03:52:18 +01:00
|
|
|
CommandSetup&
|
2017-03-18 04:40:16 +01:00
|
|
|
CommandSetup::operator= (DefinitionClosure definitionBlock)
|
2017-03-18 03:52:18 +01:00
|
|
|
{
|
2017-03-18 04:40:16 +01:00
|
|
|
if (not definitionBlock)
|
|
|
|
|
throw error::Invalid ("unbound function/closure provided for CommandSetup"
|
2018-04-02 01:48:51 +02:00
|
|
|
, error::LERR_(BOTTOM_VALUE));
|
2017-03-18 04:40:16 +01:00
|
|
|
|
2017-05-02 01:37:17 +02:00
|
|
|
pendingCmdDefinitions().emplace_front (cmdID_, move(definitionBlock));
|
2017-03-18 04:40:16 +01:00
|
|
|
return *this;
|
2017-03-18 03:52:18 +01:00
|
|
|
}
|
|
|
|
|
|
2017-03-31 18:15:02 +02:00
|
|
|
|
2017-03-18 05:28:56 +01:00
|
|
|
size_t
|
2017-03-31 04:36:26 +02:00
|
|
|
CommandSetup::pendingCnt()
|
|
|
|
|
{
|
2017-05-02 01:37:17 +02:00
|
|
|
return pendingCmdDefinitions().size();
|
2017-03-31 04:36:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
2017-03-18 05:28:56 +01:00
|
|
|
CommandSetup::invokeDefinitionClosures()
|
|
|
|
|
{
|
2017-05-02 01:37:17 +02:00
|
|
|
while (not pendingCmdDefinitions().empty())
|
2017-03-18 05:28:56 +01:00
|
|
|
{
|
2017-05-02 01:37:17 +02:00
|
|
|
CmdDefEntry& entry = pendingCmdDefinitions().back();
|
2017-03-18 05:28:56 +01:00
|
|
|
Symbol& cmdID{get<Symbol>(entry)};
|
|
|
|
|
DefinitionClosure& buildDefinition{get<DefinitionClosure> (entry)};
|
|
|
|
|
|
2017-03-31 18:32:42 +02:00
|
|
|
TRACE (command, "defining Command(%s)...", cmdID.c());
|
2017-03-18 05:28:56 +01:00
|
|
|
CommandDef def(cmdID);
|
|
|
|
|
buildDefinition(def);
|
2017-05-02 01:37:17 +02:00
|
|
|
pendingCmdDefinitions().pop_back();
|
2017-03-18 05:28:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
2017-03-31 04:36:26 +02:00
|
|
|
|
|
|
|
|
namespace { // automatically invoke static command definitions
|
|
|
|
|
|
|
|
|
|
LifecycleHook schedule_ (ON_GLOBAL_INIT, &CommandSetup::invokeDefinitionClosures);
|
2017-03-18 03:52:18 +01:00
|
|
|
}
|
|
|
|
|
|
2017-03-18 03:20:05 +01:00
|
|
|
|
2017-03-31 18:15:02 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-03-08 04:25:33 +01:00
|
|
|
// emit dtors of embedded objects here....
|
|
|
|
|
CommandInstanceManager::~CommandInstanceManager() { }
|
|
|
|
|
|
2017-04-08 15:42:51 +02:00
|
|
|
/** create a CommandInstanceManager and wire it with the given CommandDispatch implementation.
|
|
|
|
|
* Typically, this is done in SessionCommandService. The embedded hash table for pending
|
|
|
|
|
* command instances is sized in relation to the number of registered global Command definitions.
|
|
|
|
|
*/
|
2017-03-31 23:15:29 +02:00
|
|
|
CommandInstanceManager::CommandInstanceManager (CommandDispatch& dispatcher)
|
|
|
|
|
: dispatcher_{dispatcher}
|
2017-03-31 23:59:22 +02:00
|
|
|
, table_{2 * Command::definition_count()}
|
2017-03-31 23:15:29 +02:00
|
|
|
{ }
|
2017-03-08 04:25:33 +01:00
|
|
|
|
|
|
|
|
|
2017-04-08 15:42:51 +02:00
|
|
|
/** Create and thus "open" a new anonymous command instance.
|
|
|
|
|
* @param prototypeID the underlying Command definition to create a clone copy
|
|
|
|
|
* @param invocationID used to decorate the prototypeID to from an unique instanceID
|
|
|
|
|
* @throws error::Logic in case an instance is for this ID combination is already "open"
|
|
|
|
|
*/
|
2017-03-31 23:15:29 +02:00
|
|
|
Symbol
|
2017-04-09 02:59:16 +02:00
|
|
|
CommandInstanceManager::newInstance (Symbol prototypeID, string const& invocationID)
|
2017-03-31 23:15:29 +02:00
|
|
|
{
|
2017-04-07 06:34:41 +02:00
|
|
|
Symbol instanceID{prototypeID, invocationID};
|
2017-03-31 23:59:22 +02:00
|
|
|
Command& instance = table_[instanceID];
|
2017-04-06 18:32:01 +02:00
|
|
|
if (instance)
|
|
|
|
|
throw new error::Logic (_Fmt{"Attempt to create a new Command instance '%s', "
|
|
|
|
|
"while an instance for this invocationID %s "
|
|
|
|
|
"is currently open for parametrisation and "
|
|
|
|
|
"not yet dispatched for execution."}
|
|
|
|
|
% instanceID % invocationID
|
2018-04-02 01:48:51 +02:00
|
|
|
, LERR_(DUPLICATE_COMMAND)
|
2017-04-06 18:32:01 +02:00
|
|
|
);
|
|
|
|
|
// create new clone from the prototype
|
|
|
|
|
table_[instanceID] = move (Command::get(prototypeID).newInstance());
|
|
|
|
|
ENSURE (instance, "cloning of command prototype failed");
|
2017-03-31 23:59:22 +02:00
|
|
|
return instanceID;
|
2017-03-31 23:15:29 +02:00
|
|
|
}
|
2017-03-08 04:25:33 +01:00
|
|
|
|
2017-03-31 23:59:22 +02:00
|
|
|
|
2017-04-09 19:11:40 +02:00
|
|
|
/** access the currently "opened" instance with the given instanceID
|
|
|
|
|
* @param instanceID ID as returned from #newInstance, or a global commandID
|
|
|
|
|
* @return instance handle of handle of a global command as fallback
|
|
|
|
|
* @note when given a commandID, which is not known as (decorated) instanceID
|
|
|
|
|
* within our local registration table, just the globally registered
|
|
|
|
|
* Command instance is returned.
|
|
|
|
|
* @remark deliberately this function returns by-value. Returning a reference
|
|
|
|
|
* into the global command registry would be dangerous under concurrency.
|
|
|
|
|
* @throw error::Invalid when the given cmdID unknown both locally and globally.
|
|
|
|
|
* @throw error::Logic when accessing an instance that _was_ known but is
|
|
|
|
|
* currently no longer "open" (already dispatched command instance)
|
|
|
|
|
*/
|
|
|
|
|
Command
|
2017-04-01 00:37:12 +02:00
|
|
|
CommandInstanceManager::getInstance (Symbol instanceID)
|
|
|
|
|
{
|
2017-04-09 03:58:38 +02:00
|
|
|
auto entry = table_.find(instanceID);
|
|
|
|
|
if (entry == table_.end())
|
2017-04-09 19:11:40 +02:00
|
|
|
return Command::get(instanceID);
|
2017-04-09 03:58:38 +02:00
|
|
|
if (not entry->second)
|
2017-04-09 19:11:40 +02:00
|
|
|
throw error::Logic (_Fmt{"Command instance '%s' is not (yet/anymore) active"}
|
|
|
|
|
% instanceID
|
2018-04-02 01:48:51 +02:00
|
|
|
, error::LERR_(LIFECYCLE));
|
2017-04-09 03:58:38 +02:00
|
|
|
return entry->second;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-04-16 18:27:05 +02:00
|
|
|
/** @internal retrieve either global or local command instance
|
|
|
|
|
* When matching a globally defined command, an anonymous clone instance
|
|
|
|
|
* will be created. Otherwise a lookup in the local instance table is performed
|
|
|
|
|
* and a matching entry is _moved out of the table_.
|
2017-04-09 19:11:40 +02:00
|
|
|
*/
|
2017-04-16 18:27:05 +02:00
|
|
|
Command
|
2017-04-17 01:20:28 +02:00
|
|
|
CommandInstanceManager::getCloneOrInstance (Symbol instanceID, bool must_be_bound)
|
2017-03-31 23:15:29 +02:00
|
|
|
{
|
2017-04-16 18:27:05 +02:00
|
|
|
Command instance = Command::maybeGetNewInstance (instanceID);
|
2017-03-31 23:59:22 +02:00
|
|
|
if (not instance)
|
2017-04-16 18:27:05 +02:00
|
|
|
{ // second attempt: search of a locally "opened" instance
|
|
|
|
|
auto entry = table_.find(instanceID);
|
|
|
|
|
if (entry == table_.end())
|
|
|
|
|
throw error::Invalid(_Fmt("Command-ID \"%s\" refers neither to a "
|
|
|
|
|
"globally registered command definition, "
|
|
|
|
|
"nor to an previously opened command instance")
|
|
|
|
|
% instanceID
|
2018-04-02 01:48:51 +02:00
|
|
|
, LERR_(INVALID_COMMAND));
|
2017-04-16 20:12:01 +02:00
|
|
|
if (not entry->second.isValid())
|
|
|
|
|
throw error::Logic (_Fmt{"Command instance '%s' is not (yet/anymore) active"}
|
|
|
|
|
% instanceID
|
2018-04-02 01:48:51 +02:00
|
|
|
, error::LERR_(LIFECYCLE));
|
2017-04-17 01:20:28 +02:00
|
|
|
if (not must_be_bound or entry->second.canExec())
|
|
|
|
|
instance = move(entry->second);
|
2017-04-16 18:27:05 +02:00
|
|
|
}
|
2017-04-17 01:20:28 +02:00
|
|
|
if (must_be_bound and not instance.canExec())
|
|
|
|
|
throw error::State (_Fmt{"attempt to dispatch command instance '%s' "
|
|
|
|
|
"without binding all arguments properly beforehand"}
|
|
|
|
|
% instanceID
|
2018-04-02 01:48:51 +02:00
|
|
|
, LERR_(UNBOUND_ARGUMENTS));
|
2017-04-17 01:20:28 +02:00
|
|
|
|
|
|
|
|
ENSURE (instance.isValid() and
|
|
|
|
|
(instance.canExec() or not must_be_bound));
|
2017-04-16 18:27:05 +02:00
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** @internal hand a command over to the dispatcher */
|
|
|
|
|
void
|
2017-04-17 01:20:28 +02:00
|
|
|
CommandInstanceManager::handOver (Command&& toDispatch)
|
2017-04-16 18:27:05 +02:00
|
|
|
{
|
|
|
|
|
REQUIRE (toDispatch and toDispatch.canExec());
|
|
|
|
|
dispatcher_.enqueue(move (toDispatch));
|
|
|
|
|
ENSURE (not toDispatch);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** hand over the designated command instance to the dispatcher installed
|
|
|
|
|
* on construction. Either the given ID corresponds to a global command definition,
|
|
|
|
|
* in which case an anonymous clone copy is created from this command. Alternatively
|
|
|
|
|
* the given ID matches a previously "opened" local instance (known only to this
|
|
|
|
|
* instance manager). In this case, the instance will really be _moved_ over into
|
|
|
|
|
* the dispatcher, which also means this instance is no longer "open" for
|
|
|
|
|
* parametrisation.
|
2017-04-16 20:12:01 +02:00
|
|
|
* @throw error::Logic when the command's arguments aren't bound
|
2017-04-16 18:27:05 +02:00
|
|
|
*/
|
|
|
|
|
void
|
|
|
|
|
CommandInstanceManager::dispatch (Symbol instanceID)
|
|
|
|
|
{
|
2017-04-17 01:20:28 +02:00
|
|
|
handOver (getCloneOrInstance (instanceID, true));
|
2017-03-31 23:15:29 +02:00
|
|
|
}
|
2017-03-08 04:25:33 +01:00
|
|
|
|
2017-03-31 23:59:22 +02:00
|
|
|
|
2017-04-16 02:28:25 +02:00
|
|
|
/** fire and forget anonymous command instance.
|
|
|
|
|
* This is a simplified interface, allowing to create a clone instance
|
|
|
|
|
* from a global command definition (prototype), bind the arguments and
|
|
|
|
|
* pass this instance to the dispatcher in one shot. To integrate with the
|
|
|
|
|
* extended usage cycle, as a variation the given ID may indicate a previously
|
|
|
|
|
* opened instance, which will then be bound and dispatched likewise.
|
|
|
|
|
* @param instanceID global commandID or previously opened local instanceID
|
|
|
|
|
* @param argSeq command argument tuple packaged as Record<GenNode>, which
|
2018-09-21 13:46:42 +02:00
|
|
|
* is the standard format [sent](\ref BusTerm::act(GenNode)) for
|
|
|
|
|
* command execution via [UI-bus](\ref ui-bus.hpp)
|
2017-04-16 02:28:25 +02:00
|
|
|
*/
|
|
|
|
|
void
|
|
|
|
|
CommandInstanceManager::bindAndDispatch (Symbol instanceID, Rec const& argSeq)
|
|
|
|
|
{
|
2017-04-17 01:20:28 +02:00
|
|
|
Command instance{getCloneOrInstance (instanceID, false)};
|
2017-04-16 18:27:05 +02:00
|
|
|
REQUIRE (instance);
|
|
|
|
|
instance.bindArg (argSeq);
|
|
|
|
|
ENSURE (instance.canExec());
|
2017-04-17 01:20:28 +02:00
|
|
|
handOver (move (instance));
|
2017-04-16 02:28:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-03-31 23:15:29 +02:00
|
|
|
bool
|
|
|
|
|
CommandInstanceManager::contains (Symbol instanceID) const
|
|
|
|
|
{
|
2017-04-01 00:37:12 +02:00
|
|
|
return util::contains (table_, instanceID)
|
|
|
|
|
and unConst(this)->table_[instanceID].isValid();
|
2017-03-31 23:15:29 +02:00
|
|
|
}
|
2017-03-08 04:25:33 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-11-15 23:55:13 +01:00
|
|
|
}} // namespace steam::control
|