mirror of
https://github.com/cookiengineer/audacity
synced 2025-10-20 17:41:13 +02:00
New library for registries
This commit is contained in:
290
libraries/lib-registries/AttachedVirtualFunction.h
Normal file
290
libraries/lib-registries/AttachedVirtualFunction.h
Normal file
@@ -0,0 +1,290 @@
|
||||
/*!********************************************************************
|
||||
|
||||
Audacity: A Digital Audio Editor
|
||||
|
||||
@file AttachedVirtualFunction.h
|
||||
@brief Utility for non-intrusive definition of a new method on a base class
|
||||
|
||||
Paul Licameli
|
||||
|
||||
**********************************************************************/
|
||||
|
||||
#ifndef __AUDACITY_ATTACHED_VIRTUAL_FUNCTION__
|
||||
#define __AUDACITY_ATTACHED_VIRTUAL_FUNCTION__
|
||||
|
||||
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include "InconsistencyException.h"
|
||||
|
||||
//! Class template generates single-dispatch, open method registry tables
|
||||
/*!
|
||||
Defines a "virtual" function with multiple bodies chosen by type-switch
|
||||
on the runtime class of the first argument, leaving the set of overrides
|
||||
open-ended for extension, but also requiring no modification of the definition
|
||||
of the base class of the type hierarchy that is switched on.
|
||||
|
||||
The invocation of the function is not as efficient as for true virtual functions
|
||||
but the advantage of this utility is greater compilation decoupling. Client code
|
||||
can attach its own virtual functions, non-intrusively, to the root of a class hierarchy
|
||||
defined in the core.
|
||||
|
||||
There is no collection of overriding function pointers into virtual function tables by the
|
||||
linker, but a registration of overrides into a table at static initialization time. This allows
|
||||
those implementations to be defined wherever is convenient, even in dynamically loaded
|
||||
libraries.
|
||||
|
||||
Beware that invocation of the function should not be done during initialization
|
||||
of file scope static objects. Dispatch might not go to the correct subclass
|
||||
case if initializations are not yet complete.
|
||||
|
||||
Example usage:
|
||||
|
||||
A core class:
|
||||
```
|
||||
// file Host.h
|
||||
class AbstractHost
|
||||
{
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Declare the attached function
|
||||
(in a header that Host.cpp need not include at all)
|
||||
as a specialization of the template:
|
||||
```
|
||||
// file Client.h
|
||||
enum class ErrorCode { Ok, Bad, // ...
|
||||
}; // a return type for our function
|
||||
|
||||
// First this empty structure serving just to distinguish among instantiations
|
||||
// of AttachedVirtualFunction with otherwise identical parameters
|
||||
// An incomplete type is enough
|
||||
struct DoSomethingTag;
|
||||
|
||||
// Now declare the "virtual function"
|
||||
using DoSomething =
|
||||
AttachedVirtualFunction<
|
||||
DoSomethingTag,
|
||||
ErrorCode,
|
||||
AbstractHost, // class to be switched on by runtime type
|
||||
int, double // other arguments
|
||||
>;
|
||||
```
|
||||
|
||||
Definitions needed:
|
||||
```
|
||||
//file Client.cpp
|
||||
// Define the default function body here (as a function returning a function!)
|
||||
template<> auto DoSomething::Implementation() -> Function {
|
||||
return [](AbstractHost &host, int arg1, double arg2) {
|
||||
return ErrorCode::Ok;
|
||||
};
|
||||
// or you could return nullptr instead of a lambda to force InconsistencyException
|
||||
// at runtime if the virtual function is invoked for a host subclass for which no override
|
||||
// was defined.
|
||||
}
|
||||
// Must also guarantee construction of an instance of class DoSomething at least
|
||||
// once before any use of DoSomething::Call()
|
||||
static DoSomething registerMe;
|
||||
```
|
||||
|
||||
Usage of the method somewhere else:
|
||||
```
|
||||
#include "Client.h"
|
||||
void UseDoSomething( AbstractHost &host )
|
||||
{
|
||||
// ...
|
||||
auto error = DoSomething::Call( host, 0, 1.0 );
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Derived classes from AbstractHost, not needing Client.h:
|
||||
```
|
||||
// file SpecialHost.h
|
||||
#include "Host.h"
|
||||
class SpecialHost : public AbstractHost
|
||||
{
|
||||
// ...
|
||||
};
|
||||
|
||||
class ExtraSpecialHost : public SpecialHost
|
||||
{
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Overrides of the method, defined in any other .cpp file:
|
||||
```
|
||||
#include "SpecialHost.h"
|
||||
#include "Client.h"
|
||||
// An override of the function, building up a hierarchy of function bodies parallel
|
||||
// to the host class hierarchy
|
||||
using DoSomethingSpecial = DoSomething::Override< SpecialHost >;
|
||||
template<> template<> auto DoSomethingSpecial::Implementation() -> Function {
|
||||
// The function can be defined without casting the first argument
|
||||
return [](SpecialHost &host, int arg1, double arg2) {
|
||||
return arg1 == 0 ? ErrorCode::Ok : ErrorCode::Bad;
|
||||
};
|
||||
}
|
||||
static DoSomethingSpecial registerMe;
|
||||
|
||||
// A further override, demonstrating call-through too
|
||||
using DoSomethingExtraSpecial =
|
||||
DoSomething::Override< ExtraSpecialHost, DoSomethingSpecial >;
|
||||
template<> template<>
|
||||
auto DoSomethingExtraSpecial::Implementation() -> Function {
|
||||
return [](ExtraSpecialHost &host, int arg1, double arg2){
|
||||
// Call the immediately overridden version of the function
|
||||
auto result = Callthrough( host, arg1, arg2 );
|
||||
if ( result == ErrorCode::OK ) {
|
||||
if ( arg2 != 1066.0 )
|
||||
result = ErrorCode::Bad;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
static DoSomethingExtraSpecial registerMe;
|
||||
```
|
||||
|
||||
@tparam Tag an incomplete type, to distinguish methods with otherwise identical parameters
|
||||
@tparam Return the value returned by each override
|
||||
@tparam This type of the first argument, a class with at least one true virtual function, the root of the hierarchy for the run-time type-switch
|
||||
@tparam Arguments any number of types for the second and later arguments
|
||||
|
||||
*/
|
||||
template< typename Tag, typename Return, typename This, typename... Arguments >
|
||||
class AttachedVirtualFunction
|
||||
{
|
||||
public:
|
||||
|
||||
//! This member name is declared in this class template and redeclared in each override
|
||||
using Object = This;
|
||||
//! This member name is declared in this class template and redeclared in each override
|
||||
using Function = std::function< Return( Object&, Arguments... ) >;
|
||||
//! A function returning a std::function, which you must define so that the program links
|
||||
/*! It may return nullptr in case this must act somewhat as a "pure virtual",
|
||||
throwing InconsistencyException if the function is invoked on a subclass
|
||||
for which no override was defined */
|
||||
static Function Implementation();
|
||||
|
||||
//! At least one static instance must be created; more instances are harmless
|
||||
/*! (There will be others if there are any overrides.) */
|
||||
AttachedVirtualFunction()
|
||||
{
|
||||
static std::once_flag flag;
|
||||
std::call_once( flag, []{ Register<This>( Implementation() ); } );
|
||||
}
|
||||
|
||||
//! For defining overrides of the method
|
||||
/*!
|
||||
@tparam Subclass the more specific subclass of @b This
|
||||
@tparam Overridden The immediately overridden version, defaulting to the base version
|
||||
*/
|
||||
template<
|
||||
typename Subclass, typename Overridden = AttachedVirtualFunction >
|
||||
struct Override : Overridden
|
||||
{
|
||||
//! Shadowing Overridden::Object
|
||||
using Object = Subclass;
|
||||
//! Shadowing Overridden::Function, giving the first argument a more specific type
|
||||
using Function = std::function< Return( Object&, Arguments... ) >;
|
||||
|
||||
// Check that inheritance is correct
|
||||
static_assert(
|
||||
std::is_base_of< typename Overridden::Object, Object >::value,
|
||||
"overridden class must be a base of the overriding class"
|
||||
);
|
||||
|
||||
//! A function returning a std::function that must be defined so that the program links
|
||||
static Function Implementation();
|
||||
//! May be used in the body of the overriding function, defining it in terms of the overridden one
|
||||
static Return Callthrough(
|
||||
typename Overridden::Object &object, Arguments &&...arguments )
|
||||
{
|
||||
return Overridden::Implementation()(
|
||||
object, std::forward< Arguments >( arguments )... );
|
||||
}
|
||||
//! At least one static instance must be created; more instances are harmless
|
||||
/*! (There will be others if there are any further overrides.) */
|
||||
Override()
|
||||
{
|
||||
static std::once_flag flag;
|
||||
std::call_once( flag, []{
|
||||
// Register in the table an adaptor thunk that downcasts the object
|
||||
auto implementation = Implementation();
|
||||
Register< Subclass >( [=]( This &obj, Arguments &&...arguments ){
|
||||
return implementation(
|
||||
static_cast< Subclass& >( obj ),
|
||||
std::forward< Arguments >( arguments )... );
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//! Invoke the method -- but only after static initialization time
|
||||
static Return Call(
|
||||
This &obj, //!< Object on which to type-switch at run-time
|
||||
Arguments &&...arguments //!< other arguments
|
||||
)
|
||||
{
|
||||
try {
|
||||
// Note that the constructors of this class and overrides cause
|
||||
// the registry to be topologically sorted, with functions for
|
||||
// less-derived classes earlier in the table; so take the last
|
||||
// one that matches the object. (The object might not be of the exact
|
||||
// class corresponding to any of the overrides, which is why this
|
||||
// solution involves calling the predicates generated in Register,
|
||||
// and wouldn't work just with hashing on std::type_index; but perhaps
|
||||
// such a cache could be memo-ized)
|
||||
auto ®istry = GetRegistry();
|
||||
auto iter = registry.rbegin(), end = registry.rend();
|
||||
for ( ; iter != end; ++iter ) {
|
||||
auto &entry = *iter;
|
||||
if ( entry.predicate( &obj ) )
|
||||
// This might throw std::bad_function_call on a null function
|
||||
return entry.function(
|
||||
obj, std::forward< Arguments >( arguments )... );
|
||||
}
|
||||
// If not found, also throw
|
||||
throw std::bad_function_call{};
|
||||
}
|
||||
catch ( const std::bad_function_call& ) {
|
||||
// No matching case found with a non-null function.
|
||||
// Translate the exception
|
||||
THROW_INCONSISTENCY_EXCEPTION;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
template< typename Subclass >
|
||||
static void Register( const Function &function )
|
||||
{
|
||||
// Push back a dynamic type test and corresponding function body
|
||||
GetRegistry().push_back({
|
||||
[]( This *b ){ return dynamic_cast< Subclass * >( b ) != nullptr; },
|
||||
function
|
||||
});
|
||||
}
|
||||
|
||||
using Predicate = std::function< bool( This* ) >;
|
||||
|
||||
//! Member of registry of implementations of the method
|
||||
struct Entry
|
||||
{
|
||||
Predicate predicate;
|
||||
Function function;
|
||||
};
|
||||
|
||||
using Registry = std::vector< Entry >;
|
||||
static Registry &GetRegistry()
|
||||
{
|
||||
static Registry registry;
|
||||
return registry;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
47
libraries/lib-registries/CMakeLists.txt
Normal file
47
libraries/lib-registries/CMakeLists.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
#[[
|
||||
Some utilities for allowing open-endedness and decoupling of designs, by
|
||||
maintaining global tables that can be populated at static initialization by
|
||||
code in scattered places, on which the registry has no build dependency.
|
||||
|
||||
AttachedVirtualFunction implements "open methods" -- functions that type-switch
|
||||
on the first argument, and dispatch to the correct implementation, but without
|
||||
intrusiveness into the base of the class hierarchy. This allows the set of
|
||||
methods and the set of subclasses both to be open-ended.
|
||||
|
||||
(That is unlike in the "Gang of Four" Visitor pattern, in which the set of
|
||||
methods becomes open-ended while the set of subclasses is closed-ended: the
|
||||
Visitor can't handle a new subclass without intrusion into the abstract
|
||||
Visitor definition.)
|
||||
|
||||
ClientData allows a "Site" class to act as a container of attached objects
|
||||
produced by registered factory functions, and retrieved by the various attaching
|
||||
modules. The Site's constructor is effectively hookable. This in particular
|
||||
allows the Project object to be low-level in the graph of file dependencies,
|
||||
while it is also the top-level object of an ownership tree of various important
|
||||
sub-structures. This is another "Dependency Inversion" of sorts (the D
|
||||
principle of SOLID).
|
||||
|
||||
Registry implements trees of objects identified by textual paths, and allows
|
||||
scattered code to insert items or subtrees at specified paths. Registry
|
||||
computes the merging of trees, and supports visitation. It is used notably
|
||||
by the tree of menus, allowing insertion of menu items in decoupled code.
|
||||
]]#
|
||||
|
||||
set( SOURCES
|
||||
AttachedVirtualFunction.h
|
||||
ClientData.cpp
|
||||
ClientData.h
|
||||
ClientDataHelpers.h
|
||||
Registrar.h
|
||||
Registry.cpp
|
||||
Registry.h
|
||||
)
|
||||
set( LIBRARIES
|
||||
lib-preferences-interface
|
||||
lib-exceptions
|
||||
PRIVATE
|
||||
wxBase
|
||||
)
|
||||
audacity_library( lib-registries "${SOURCES}" "${LIBRARIES}"
|
||||
"" ""
|
||||
)
|
15
libraries/lib-registries/ClientData.cpp
Normal file
15
libraries/lib-registries/ClientData.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
/*!********************************************************************
|
||||
|
||||
Audacity: A Digital Audio Editor
|
||||
|
||||
@file ClientData.cpp
|
||||
|
||||
Paul Licameli
|
||||
|
||||
**********************************************************************/
|
||||
|
||||
#include "ClientData.h"
|
||||
|
||||
// These are needed out-of-line for the Windows link
|
||||
ClientData::Base::~Base() = default;
|
||||
template<> ClientData::Cloneable<>::~Cloneable() = default;
|
568
libraries/lib-registries/ClientData.h
Normal file
568
libraries/lib-registries/ClientData.h
Normal file
@@ -0,0 +1,568 @@
|
||||
/*!********************************************************************
|
||||
|
||||
Audacity: A Digital Audio Editor
|
||||
|
||||
@file ClientData.h
|
||||
@brief Utility ClientData::Site to register hooks into a host class that attach client data
|
||||
|
||||
Paul Licameli
|
||||
|
||||
**********************************************************************/
|
||||
|
||||
#ifndef __AUDACITY_CLIENT_DATA__
|
||||
#define __AUDACITY_CLIENT_DATA__
|
||||
|
||||
#include "ClientDataHelpers.h"
|
||||
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "InconsistencyException.h"
|
||||
|
||||
//! @copydoc ClientData.h
|
||||
namespace ClientData {
|
||||
|
||||
//! A convenient default parameter for class template @b Site
|
||||
struct REGISTRIES_API Base
|
||||
{
|
||||
virtual ~Base();
|
||||
};
|
||||
|
||||
//! A one-argument alias template for the default template-template parameter of ClientData::Site
|
||||
/*! (std::unique_ptr has two, the second is defaulted) */
|
||||
template< typename Object > using UniquePtr = std::unique_ptr< Object >;
|
||||
|
||||
//! This template-template parameter for ClientData::Site risks dangling pointers, so be careful
|
||||
template< typename Object > using BarePtr = Object*;
|
||||
|
||||
//! A convenient base class defining abstract virtual Clone() for a given kind of pointer
|
||||
/*!
|
||||
@tparam Owner template-template parameter for the kind of smart pointer, like std::shared_ptr, returned by Clone()
|
||||
|
||||
@sa ClientData::DeepCopying
|
||||
*/
|
||||
template<
|
||||
template<typename> class Owner = UniquePtr
|
||||
> struct REGISTRIES_API Cloneable
|
||||
{
|
||||
using Base = Cloneable;
|
||||
using PointerType = Owner< Base >;
|
||||
|
||||
virtual ~Cloneable();
|
||||
virtual PointerType Clone() const = 0;
|
||||
};
|
||||
|
||||
//! Utility to register hooks into a host class that attach client data
|
||||
/*!
|
||||
This allows the host object to be the root of an ownership tree of sub-objects at
|
||||
run-time, but inverting the compile-time dependency among implementation files:
|
||||
The host's implementation is in low-level files, and cyclic file dependencies are avoided.
|
||||
The set of client objects attached to each host object is not fixed in the definition of
|
||||
the host class, but instead a system of registration of factories of client objects lets it
|
||||
be open-ended.
|
||||
|
||||
Besides mere storage and retrieval, this can also implement the [observer pattern](https://en.wikipedia.org/wiki/Observer_pattern),
|
||||
in which the host pushes notifications to some virtual function defined in
|
||||
each attached item.
|
||||
|
||||
@par Host side usage pattern
|
||||
|
||||
```
|
||||
class Host;
|
||||
class AbstractClientData // Abstract base class for attached data
|
||||
{
|
||||
virtual ~AbstractClientData(); // minimum for memory management
|
||||
|
||||
// optional extra observer protocols
|
||||
virtual void NotificationMethod(
|
||||
// maybe host passes reference to self, maybe not
|
||||
// Host &host
|
||||
) = 0;
|
||||
};
|
||||
|
||||
class Host
|
||||
: public ClientData::Site< Host, AbstractClientData >
|
||||
// That inheritance is a case of CRTP
|
||||
// (the "curiously recurring template pattern")
|
||||
// in which the base class takes the derived class as a template argument
|
||||
{
|
||||
public:
|
||||
Host()
|
||||
{
|
||||
// If using an Observer protocol, force early creation of all client
|
||||
// data:
|
||||
BuildAll();
|
||||
}
|
||||
|
||||
void NotifyAll()
|
||||
{
|
||||
// Visit all non-null objects
|
||||
ForEach( []( AbstractClientData &data ){
|
||||
data.NotificationMethod(
|
||||
// *this
|
||||
);
|
||||
} );
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@par Client side usage pattern
|
||||
|
||||
```
|
||||
class MyClientData : public AbstractClientData
|
||||
{
|
||||
public:
|
||||
MyClientData( Host &host )
|
||||
{
|
||||
// ... use host, maybe keep a back pointer to it, maybe not,
|
||||
// depending how Host declares NotificationMethod ...
|
||||
// ... Maybe Host too is an abstract class and we invoke some
|
||||
// virtual function of it ...
|
||||
}
|
||||
void NotificationMethod(
|
||||
// Host &host
|
||||
) override
|
||||
{
|
||||
// ... Observer actions
|
||||
// (If there is more than one separately compiled module using this
|
||||
// protocol, beware that the sequence of notifications is unspecified)
|
||||
}
|
||||
|
||||
private:
|
||||
int mExtraStuff;
|
||||
};
|
||||
|
||||
// Registration of a factory at static initialization time, to be called
|
||||
// when a Host uses BuildAll, or else lazily when client code uses
|
||||
// Host::Get()
|
||||
static const Host::RegisteredFactory key{
|
||||
[]( Host &host ){ return std::make_unique< MyClientData >( host ); }
|
||||
};
|
||||
|
||||
// Use of that key at other times, not dependent on notifications from
|
||||
// the core
|
||||
void DoSomething( Host &host )
|
||||
{
|
||||
// This may force lazy construction of MyClientData, always returning
|
||||
// an object (or else throwing)
|
||||
auto &data = host.Get< MyClientData >( key );
|
||||
auto val = pData->mExtraStuff;
|
||||
// ...
|
||||
}
|
||||
|
||||
void DoAnotherThing( Host &host )
|
||||
{
|
||||
// Does not force lazy construction of MyClientData
|
||||
auto *pData = host.Find< MyClientData >( key );
|
||||
if ( pData ) {
|
||||
auto val = data.mExtraStuff;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
void DoYetAnotherThing( Host &host )
|
||||
{
|
||||
// Reassign the pointer in this client's slot
|
||||
host.Assign( key, MyReplacementObject( host ) );
|
||||
}
|
||||
```
|
||||
|
||||
@par Lazy or eager construction
|
||||
|
||||
If the client only needs retrieval, it might need
|
||||
construction of data only on demand. But if the host is meant to push
|
||||
notifications to the clients, then the host class is responsible for forcing
|
||||
early building of all ClientData when host is constructed, as in the example
|
||||
above.
|
||||
|
||||
@par Unusual registration sequences
|
||||
|
||||
If registration of a factory
|
||||
happens after some host objects are already in existence, their associated
|
||||
client data fail to be created if you rely only on BuildAll in the @B Host
|
||||
constructor. Early deregistration of factories is permitted, in which case
|
||||
any later constructed host objects will carry null pointers in the associated
|
||||
slot, and a small "leak" in the space of per-host slots will persist until
|
||||
the program exits. All such usage is not recommended.
|
||||
|
||||
@tparam Host
|
||||
Type that derives from this base class; it
|
||||
supports hooks to invoke attached object factories. This is an example of the
|
||||
[curiously recurring template pattern](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern#General_form)
|
||||
|
||||
@tparam ClientData Common base class of attachments; must have a virtual destructor
|
||||
|
||||
@tparam CopyingPolicy @ref CopyingPolicy value Chooses deep, shallow, or (default) no-op copying of attachments
|
||||
|
||||
@tparam Pointer
|
||||
The kind of pointer @b Host will hold to ClientData; default is std::unique_ptr.
|
||||
You might want to use std::shared_ptr, std::weak_ptr, or wxWeakRef instead
|
||||
|
||||
@tparam ObjectLockingPolicy @ref LockingPolicy value chooses thread safety policy for array of attachments in each @b Host, default is unsafe
|
||||
|
||||
@tparam RegistryLockingPolicy @ref LockingPolicy value chooses thread safety policy for the static table of attachment factory functions, default is unsafe
|
||||
*/
|
||||
template<
|
||||
typename Host,
|
||||
typename ClientData = Base,
|
||||
|
||||
// Remaining parameters are often defaulted
|
||||
|
||||
CopyingPolicy ObjectCopyingPolicy = SkipCopying,
|
||||
|
||||
template<typename> class Pointer = UniquePtr,
|
||||
|
||||
LockingPolicy ObjectLockingPolicy = NoLocking,
|
||||
LockingPolicy RegistryLockingPolicy = NoLocking
|
||||
>
|
||||
class Site
|
||||
{
|
||||
public:
|
||||
~Site()
|
||||
{
|
||||
static_assert( std::has_virtual_destructor<ClientData>::value,
|
||||
"ClientData::Site requires a data class with a virtual destructor" );
|
||||
}
|
||||
|
||||
using DataType = ClientData;
|
||||
using DataPointer = Pointer< ClientData >;
|
||||
//! Type of function from which RegisteredFactory is constructed; it builds attachments
|
||||
using DataFactory = std::function< DataPointer( Host& ) >;
|
||||
|
||||
Site()
|
||||
{
|
||||
auto factories = GetFactories();
|
||||
auto size = factories.mObject.size();
|
||||
mData.reserve( size );
|
||||
}
|
||||
Site( const Site &other )
|
||||
: mData( other.mData )
|
||||
{ }
|
||||
Site& operator =( const Site & other )
|
||||
{ mData = other.mData; return *this; }
|
||||
Site( Site && other )
|
||||
: mData( std::move(other.mData) )
|
||||
{ }
|
||||
Site& operator =( Site && other )
|
||||
{ mData = std::move(other.mData); return *this; }
|
||||
|
||||
//! How many attachment pointers are in the Site
|
||||
size_t size() const { return mData.size(); }
|
||||
|
||||
//! How many static factories have been registered with this specialization of Site
|
||||
/*!
|
||||
Usually agrees with the size() of each site unless some registrations happened later
|
||||
than some Site's construction.
|
||||
*/
|
||||
static size_t slots() { return GetFactories().mObject.size(); }
|
||||
|
||||
//! Client code makes static instance from a factory of attachments; passes it to @ref Get or @ref Find as a retrieval key
|
||||
/*!
|
||||
It can be destroyed to de-register the factory, but usually not before
|
||||
destruction of statics at program exit.
|
||||
*/
|
||||
class RegisteredFactory
|
||||
{
|
||||
public:
|
||||
RegisteredFactory(
|
||||
DataFactory factory
|
||||
)
|
||||
{
|
||||
auto factories = GetFactories();
|
||||
mIndex = factories.mObject.size();
|
||||
factories.mObject.emplace_back( std::move( factory ) );
|
||||
}
|
||||
RegisteredFactory( RegisteredFactory &&other )
|
||||
{
|
||||
mIndex = other.mIndex;
|
||||
mOwner = other.mOwner;
|
||||
other.mOwner = false;
|
||||
}
|
||||
~RegisteredFactory()
|
||||
{
|
||||
if (mOwner) {
|
||||
auto factories = GetFactories();
|
||||
// Should always be true, the factory vector never shrinks:
|
||||
if ( mIndex < factories.mObject.size() )
|
||||
factories.mObject[mIndex] = nullptr;
|
||||
}
|
||||
}
|
||||
private:
|
||||
friend Site;
|
||||
bool mOwner{ true };
|
||||
size_t mIndex;
|
||||
};
|
||||
|
||||
//! @name Retrieval and reassignment of attachments
|
||||
//! @{
|
||||
|
||||
//! Get reference to an attachment, creating on demand if not present, down-cast it to @b Subclass
|
||||
/*!
|
||||
Uses static_cast. Throws on failure to create it.
|
||||
(Lifetime of the object may depend on the host's lifetime and also on the
|
||||
client's use of Assign(). Site is not responsible for guarantees.)
|
||||
@tparam Subclass Expected actual type of attachment, assumed to be correct
|
||||
@param key Reference to static object created in client code
|
||||
*/
|
||||
template< typename Subclass = ClientData >
|
||||
Subclass &Get( const RegisteredFactory &key )
|
||||
{
|
||||
auto data = GetData();
|
||||
return DoGet< Subclass >( data, key );
|
||||
}
|
||||
|
||||
//! @copydoc Get
|
||||
/*! const overload returns const references only. */
|
||||
template< typename Subclass = const ClientData >
|
||||
auto Get( const RegisteredFactory &key ) const -> typename
|
||||
std::enable_if< std::is_const< Subclass >::value, Subclass & >::type
|
||||
{
|
||||
auto data = GetData();
|
||||
return DoGet< Subclass >( data, key );
|
||||
}
|
||||
|
||||
//!Get a (bare) pointer to an attachment, or null, down-cast it to @b Subclass *; will not create on demand
|
||||
/*!
|
||||
(Lifetime of the object may depend on the host's lifetime and also on the
|
||||
client's use of Assign(). Site is not responsible for guarantees.)
|
||||
@tparam Subclass Expected actual type of attachment, assumed to be correct
|
||||
@param key Reference to static object created in client code
|
||||
*/
|
||||
template< typename Subclass = ClientData >
|
||||
Subclass *Find( const RegisteredFactory &key )
|
||||
{
|
||||
auto data = GetData();
|
||||
return DoFind< Subclass >( data, key );
|
||||
}
|
||||
|
||||
//! @copydoc Find
|
||||
/*! const overload returns pointers to const only. */
|
||||
template< typename Subclass = const ClientData >
|
||||
auto Find( const RegisteredFactory &key ) const -> typename
|
||||
std::enable_if< std::is_const< Subclass >::value, Subclass * >::type
|
||||
{
|
||||
auto data = GetData();
|
||||
return DoFind< Subclass >( data, key );
|
||||
}
|
||||
|
||||
//! Reassign Site's pointer to ClientData.
|
||||
/*!
|
||||
If @b ObjectLockingPolicy isn't default, then reassignments are serialized.
|
||||
@tparam ReplacementPointer @b Pointer<Subclass> where @b Subclass derives ClientData
|
||||
*/
|
||||
template< typename ReplacementPointer >
|
||||
void Assign(
|
||||
const RegisteredFactory &key, //!< Reference to static object created in client code
|
||||
ReplacementPointer &&replacement //!< A temporary or std::move'd pointer
|
||||
)
|
||||
{
|
||||
auto index = key.mIndex;
|
||||
auto data = GetData();
|
||||
EnsureIndex( data, index );
|
||||
auto iter = GetIterator( data, index );
|
||||
// Copy or move as appropriate:
|
||||
*iter = std::forward< ReplacementPointer >( replacement );
|
||||
}
|
||||
|
||||
//! @}
|
||||
|
||||
protected:
|
||||
//! @name member functions for use by @b Host
|
||||
//! @{
|
||||
|
||||
//! Invoke function on each ClientData object that has been created in @c this
|
||||
/*!
|
||||
@tparam Function takes reference to ClientData, return value is ignored
|
||||
@param function of type @b Function
|
||||
*/
|
||||
template< typename Function >
|
||||
void ForEach( const Function &function )
|
||||
{
|
||||
auto data = GetData();
|
||||
for( auto &pObject : data.mObject ) {
|
||||
const auto &ptr = Dereferenceable(pObject);
|
||||
if ( ptr )
|
||||
function( *ptr );
|
||||
}
|
||||
}
|
||||
|
||||
//! @copydoc ForEach
|
||||
/*! const overload only compiles with a function that takes reference to const ClientData. */
|
||||
template< typename Function >
|
||||
void ForEach( const Function &function ) const
|
||||
{
|
||||
auto data = GetData();
|
||||
for( auto &pObject : data.mObject ) {
|
||||
const auto &ptr = Dereferenceable(pObject);
|
||||
if ( ptr ) {
|
||||
const auto &c_ref = *ptr;
|
||||
function( c_ref );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! Return pointer to first attachment in @c this that is not null and satisfies a predicate, or nullptr
|
||||
/*!
|
||||
Beware that the sequence of visitation is not specified.
|
||||
@tparam Function takes reference to ClientData, returns value convertible to bool
|
||||
@param function of type @b Function
|
||||
*/
|
||||
template< typename Function >
|
||||
ClientData *FindIf( const Function &function )
|
||||
{
|
||||
auto data = GetData();
|
||||
for( auto &pObject : data.mObject ) {
|
||||
const auto &ptr = Dereferenceable(pObject);
|
||||
if ( ptr && function ( *ptr ) )
|
||||
return &*ptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//! @copydoc FindIf
|
||||
/*! const overload only compiles with a function callable with a const reference to ClientData. */
|
||||
template< typename Function >
|
||||
const ClientData *FindIf( const Function &function ) const
|
||||
{
|
||||
auto data = GetData();
|
||||
for( auto &pObject : data.mObject ) {
|
||||
const auto &ptr = Dereferenceable(pObject);
|
||||
if ( ptr ) {
|
||||
const auto &c_ref = *ptr;
|
||||
if ( function( c_ref ) )
|
||||
return &*c_ref;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//! For each RegisteredFactory, if the corresponding attachment is absent in @c this, build and store it
|
||||
void BuildAll()
|
||||
{
|
||||
// Note that we cannot call this function in the Site constructor as we
|
||||
// might wish, because we pass *this to the factories, but this is not yet
|
||||
// fully constructed as the ultimate derived class. So delayed calls to
|
||||
// this function are needed.
|
||||
size_t size;
|
||||
{
|
||||
auto factories = GetFactories();
|
||||
size = factories.mObject.size();
|
||||
// Release lock on factories before getting one on data -- otherwise
|
||||
// there would be a deadlock possibility inside Ensure
|
||||
}
|
||||
auto data = GetData();
|
||||
EnsureIndex( data, size - 1 );
|
||||
auto iter = GetIterator( data, 0 );
|
||||
for ( size_t ii = 0; ii < size; ++ii, ++iter )
|
||||
static_cast< void >( Build( data, iter, ii ) );
|
||||
}
|
||||
|
||||
//! @}
|
||||
|
||||
private:
|
||||
using DataFactories =
|
||||
Lockable< std::vector< DataFactory >, RegistryLockingPolicy >;
|
||||
using DataContainer =
|
||||
Lockable<
|
||||
Copyable< std::vector< DataPointer >, ObjectCopyingPolicy >,
|
||||
ObjectLockingPolicy
|
||||
>;
|
||||
|
||||
decltype( Dereferenceable( std::declval<DataPointer&>() ) )
|
||||
Slot( Locked<DataContainer> &data, const RegisteredFactory &key, bool create )
|
||||
{
|
||||
auto index = key.mIndex;
|
||||
EnsureIndex( data, index );
|
||||
auto iter = GetIterator( data, index );
|
||||
auto &pointer = create ? Build( data, iter, index ) : *iter;
|
||||
return Dereferenceable( pointer );
|
||||
}
|
||||
|
||||
template< typename Subclass >
|
||||
Subclass &DoGet( Locked<DataContainer> &data, const RegisteredFactory &key )
|
||||
{
|
||||
const auto &d = Slot( data, key, true );
|
||||
if (!d)
|
||||
// Oops, a factory was deregistered too soon, or returned a null, or
|
||||
// the pointer was reassigned null
|
||||
THROW_INCONSISTENCY_EXCEPTION;
|
||||
return static_cast< Subclass& >( *d );
|
||||
}
|
||||
|
||||
template< typename Subclass >
|
||||
Subclass *DoFind( Locked<DataContainer> &data, const RegisteredFactory &key )
|
||||
{
|
||||
const auto &d = Slot( data, key, false );
|
||||
if (!d)
|
||||
return nullptr;
|
||||
else
|
||||
return static_cast< Subclass* >( &*d );
|
||||
}
|
||||
|
||||
static Locked< DataFactories > GetFactories()
|
||||
{
|
||||
// C++11 does not need extra thread synch for static initialization
|
||||
// Note that linker eliminates duplicates of this function
|
||||
static DataFactories factories;
|
||||
|
||||
// But give back a scoped lock to the user of this function, in case
|
||||
// there is contention to resize the vector
|
||||
return Locked< DataFactories >{ factories };
|
||||
}
|
||||
|
||||
Locked<DataContainer> GetData()
|
||||
{
|
||||
return Locked< DataContainer >{ mData };
|
||||
}
|
||||
|
||||
Locked<const DataContainer> GetData() const
|
||||
{
|
||||
return Locked< const DataContainer >{ mData };
|
||||
}
|
||||
|
||||
static void EnsureIndex( Locked<DataContainer> &data, size_t index )
|
||||
{
|
||||
if (data.mObject.size() <= index)
|
||||
data.mObject.resize(index + 1);
|
||||
}
|
||||
|
||||
static typename DataContainer::iterator inline
|
||||
GetIterator( Locked<DataContainer> &data, size_t index )
|
||||
{
|
||||
// This function might help generalize Site with different kinds of
|
||||
// containers for pointers to ClientData that are not random-access.
|
||||
// Perhaps non-relocation of elements will be needed.
|
||||
// Perhaps another template-template parameter could vary the kind of
|
||||
// container.
|
||||
auto result = data.mObject.begin();
|
||||
std::advance( result, index );
|
||||
return result;
|
||||
}
|
||||
|
||||
DataPointer &Build( Locked<DataContainer> &,
|
||||
typename DataContainer::iterator iter, size_t index )
|
||||
{
|
||||
// If there is no object at index, then invoke the factory, else do
|
||||
// nothing.
|
||||
// The factory may be null or may return null, in which case do nothing.
|
||||
auto &result = *iter;
|
||||
if (!Dereferenceable(result)) {
|
||||
// creation on demand
|
||||
auto factories = GetFactories();
|
||||
auto &factory = factories.mObject[index];
|
||||
result = factory
|
||||
? factory( static_cast< Host& >( *this ) )
|
||||
: DataPointer{};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//! Container of pointers returned by factories, per instance of @b Host class
|
||||
/*! This is the only non-static data member that Site injects into the derived class. */
|
||||
DataContainer mData;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
153
libraries/lib-registries/ClientDataHelpers.h
Normal file
153
libraries/lib-registries/ClientDataHelpers.h
Normal file
@@ -0,0 +1,153 @@
|
||||
/*!********************************************************************
|
||||
|
||||
Audacity: A Digital Audio Editor
|
||||
|
||||
@file ClientDataHelpers.h
|
||||
@brief Some implementation details for ClientData
|
||||
|
||||
Paul Licameli
|
||||
|
||||
**********************************************************************/
|
||||
|
||||
#ifndef __AUDACITY_CLIENT_DATA_HELPERS__
|
||||
#define __AUDACITY_CLIENT_DATA_HELPERS__
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <type_traits>
|
||||
|
||||
namespace ClientData {
|
||||
// Helpers to define ClientData::Site class template
|
||||
|
||||
//! Statically specify whether there is mutual exclusion (separately for the table of factories, and for the per-host container of client objects).
|
||||
/*! Used as non-type template parameter of ClientData::Site */
|
||||
enum LockingPolicy {
|
||||
NoLocking,
|
||||
NonrecursiveLocking, //!< using std::mutex
|
||||
RecursiveLocking, //!< using std::recursive_mutex
|
||||
};
|
||||
|
||||
//! Statically specify how the ClientData::Site implements its copy constructor and assignment.
|
||||
/*! (Move construction and assignment always work.)
|
||||
Used as non-type template parameter of ClientData::Site */
|
||||
enum CopyingPolicy {
|
||||
SkipCopying, //!< ignore the source and leave empty
|
||||
ShallowCopying, //!< copy pointers only; won't compile for std::unique_ptr
|
||||
DeepCopying, //!< point to new sub-objects; these must define a Clone() member; won't compile for std::weak_ptr
|
||||
};
|
||||
|
||||
// forward declarations
|
||||
struct Base;
|
||||
template<
|
||||
template<typename> class Owner
|
||||
> struct Cloneable;
|
||||
|
||||
//! Conversion allowing operator * on any @b Pointer parameter of ClientData::Site
|
||||
/*! Return value should be bound to a const reference */
|
||||
template< typename Ptr > static inline
|
||||
const Ptr &Dereferenceable( Ptr &p )
|
||||
{ return p; }
|
||||
//! Overload of ClientData::Dereferenceable returns an rvalue
|
||||
template< typename Obj > static inline
|
||||
std::shared_ptr<Obj> Dereferenceable( std::weak_ptr<Obj> &p )
|
||||
{ return p.lock(); }
|
||||
|
||||
//! Decorator template injects type Lock and method lock() into interface of @b Object
|
||||
/*!
|
||||
@tparam Object decorated class
|
||||
@tparam LockingPolicy one of ClientData::LockingPolicy
|
||||
*/
|
||||
template< typename Object, LockingPolicy > struct Lockable{};
|
||||
//! Specialization for trivial, non-locking policy
|
||||
template< typename Object > struct Lockable< Object, NoLocking >
|
||||
: Object {
|
||||
//! Empty class
|
||||
struct Lock{};
|
||||
Lock lock() const { return {}; }
|
||||
};
|
||||
//! Specialization for real locking with std::mutex
|
||||
template< typename Object > struct Lockable< Object, NonrecursiveLocking >
|
||||
: Object, std::mutex {
|
||||
using Lock = std::unique_lock< std::mutex >;
|
||||
Lock lock() const { return Lock{ *this }; }
|
||||
};
|
||||
//! Specialization for real locking with std::recursive_mutex
|
||||
template< typename Object > struct Lockable< Object, RecursiveLocking >
|
||||
: Object, std::recursive_mutex {
|
||||
using Lock = std::unique_lock< std::recursive_mutex >;
|
||||
Lock lock() const { return Lock{ *this }; }
|
||||
};
|
||||
|
||||
//! Decorated reference to a ClientData::Lockable, with a current lock on it
|
||||
/*! Uses inheritance to benefit from the empty base class optimization if possible */
|
||||
template< typename Lockable > struct Locked
|
||||
: private Lockable::Lock
|
||||
{
|
||||
explicit Locked( Lockable &object )
|
||||
: Lockable::Lock( object.lock() )
|
||||
, mObject( object )
|
||||
{}
|
||||
Lockable &mObject;
|
||||
};
|
||||
|
||||
//! Decorator template injects copy and move operators for container of pointers
|
||||
template< typename Container, CopyingPolicy > struct Copyable{};
|
||||
//! Specialization that ignores contents of the source when copying (not when moving).
|
||||
template< typename Container > struct Copyable< Container, SkipCopying >
|
||||
: Container {
|
||||
Copyable() = default;
|
||||
Copyable( const Copyable & ) {}
|
||||
Copyable &operator=( const Copyable & ) { return *this; }
|
||||
Copyable( Copyable && ) = default;
|
||||
Copyable &operator=( Copyable&& ) = default;
|
||||
};
|
||||
//! Specialization that copies pointers, not sub-objects; [strong guarantee](@ref Strong-guarantee) for assignment
|
||||
template< typename Container > struct Copyable< Container, ShallowCopying >
|
||||
: Container {
|
||||
Copyable() = default;
|
||||
//! Call through to operator =
|
||||
Copyable( const Copyable &other )
|
||||
{ *this = other; }
|
||||
//! @excsafety{Strong}
|
||||
Copyable &operator=( const Copyable &other )
|
||||
{
|
||||
if (this != &other) {
|
||||
// Build then swap for strong exception guarantee
|
||||
Copyable temp;
|
||||
for ( auto &&ptr : other )
|
||||
temp.push_back( ptr );
|
||||
this->swap( temp );
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
Copyable( Copyable && ) = default;
|
||||
Copyable &operator=( Copyable&& ) = default;
|
||||
};
|
||||
//! Specialization that clones sub-objects when copying; [strong guarantee](@ref Strong-guarantee) for assignment
|
||||
template< typename Container > struct Copyable< Container, DeepCopying >
|
||||
: Container {
|
||||
Copyable() = default;
|
||||
//! Call through to operator =
|
||||
Copyable( const Copyable &other )
|
||||
{ *this = other; }
|
||||
//! @excsafety{Strong}
|
||||
Copyable &operator=( const Copyable &other )
|
||||
{
|
||||
if (this != &other) {
|
||||
// Build then swap for strong exception guarantee
|
||||
Copyable temp;
|
||||
for ( auto &&p : other ) {
|
||||
using Ptr = decltype( p->Clone() );
|
||||
temp.push_back( p ? p->Clone() : Ptr{} );
|
||||
}
|
||||
this->swap( temp );
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
Copyable( Copyable && ) = default;
|
||||
Copyable &operator=( Copyable&& ) = default;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
51
libraries/lib-registries/Registrar.h
Normal file
51
libraries/lib-registries/Registrar.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/**********************************************************************
|
||||
|
||||
Audacity: A Digital Audio Editor
|
||||
|
||||
Registrar.h
|
||||
|
||||
James Crook
|
||||
|
||||
*******************************************************************//**
|
||||
|
||||
\class Registrar
|
||||
\brief Base class for registration callback.
|
||||
Audacity will call providers RegisterNameOfThing() functions with
|
||||
an &Registrar as the argument. RegisterNameOfThing() is then
|
||||
responsible for calling the appropriate callback functions.
|
||||
|
||||
**********************************************************************/
|
||||
|
||||
|
||||
#ifndef __AUDACITY_REGISTRAR__
|
||||
#define __AUDACITY_REGISTRAR__
|
||||
|
||||
|
||||
|
||||
#include <memory>
|
||||
|
||||
class AudacityCommand;
|
||||
class LoadableModule;
|
||||
class ComponentInterface;
|
||||
class Effect;
|
||||
|
||||
class REGISTRIES_API Registrar
|
||||
{
|
||||
public:
|
||||
Registrar(){
|
||||
bWantsModules = false;
|
||||
bWantsCommands= false;
|
||||
bWantsCommandTypes= false;
|
||||
bWantsEffects= false;
|
||||
}
|
||||
bool bWantsModules;
|
||||
bool bWantsCommands;
|
||||
bool bWantsCommandTypes;
|
||||
bool bWantsEffects;
|
||||
virtual void AddCommandType(std::unique_ptr<ComponentInterface> && WXUNUSED(comDef) ){;};
|
||||
virtual void AddCommand(std::unique_ptr<AudacityCommand> && WXUNUSED(command) ){;};
|
||||
virtual void AddModule(std::unique_ptr<LoadableModule> && WXUNUSED(module) ){;};
|
||||
virtual void AddEffect(std::unique_ptr<Effect> && WXUNUSED(effect) ){;};
|
||||
};
|
||||
|
||||
#endif
|
817
libraries/lib-registries/Registry.cpp
Normal file
817
libraries/lib-registries/Registry.cpp
Normal file
@@ -0,0 +1,817 @@
|
||||
/**********************************************************************
|
||||
|
||||
Audacity: A Digital Audio Editor
|
||||
|
||||
Registry.cpp
|
||||
|
||||
Paul Licameli split from Menus.cpp
|
||||
|
||||
**********************************************************************/
|
||||
|
||||
#include "Registry.h"
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
#include <wx/log.h>
|
||||
|
||||
#include "BasicUI.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct ItemOrdering;
|
||||
|
||||
using namespace Registry;
|
||||
struct CollectedItems
|
||||
{
|
||||
struct Item{
|
||||
// Predefined, or merged from registry already:
|
||||
BaseItem *visitNow;
|
||||
// Corresponding item from the registry, its sub-items to be merged:
|
||||
GroupItem *mergeLater;
|
||||
// Ordering hint for the merged item:
|
||||
OrderingHint hint;
|
||||
};
|
||||
std::vector< Item > items;
|
||||
std::vector< BaseItemSharedPtr > &computedItems;
|
||||
|
||||
// A linear search. Smarter search may not be worth the effort.
|
||||
using Iterator = decltype( items )::iterator;
|
||||
auto Find( const Identifier &name ) -> Iterator
|
||||
{
|
||||
auto end = items.end();
|
||||
return name.empty()
|
||||
? end
|
||||
: std::find_if( items.begin(), end,
|
||||
[&]( const Item& item ){
|
||||
return name == item.visitNow->name; } );
|
||||
}
|
||||
|
||||
auto InsertNewItemUsingPreferences(
|
||||
ItemOrdering &itemOrdering, BaseItem *pItem ) -> bool;
|
||||
|
||||
auto InsertNewItemUsingHint(
|
||||
BaseItem *pItem, const OrderingHint &hint, size_t endItemsCount,
|
||||
bool force )
|
||||
-> bool;
|
||||
|
||||
auto MergeLater( Item &found, const Identifier &name ) -> GroupItem *;
|
||||
|
||||
auto SubordinateSingleItem( Item &found, BaseItem *pItem ) -> void;
|
||||
|
||||
auto SubordinateMultipleItems( Item &found, GroupItem *pItems ) -> void;
|
||||
|
||||
auto MergeWithExistingItem(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering, BaseItem *pItem ) -> bool;
|
||||
|
||||
using NewItem = std::pair< BaseItem*, OrderingHint >;
|
||||
using NewItems = std::vector< NewItem >;
|
||||
|
||||
auto MergeLikeNamedItems(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering,
|
||||
NewItems::const_iterator left, NewItems::const_iterator right,
|
||||
int iPass, size_t endItemsCount, bool force )
|
||||
-> bool;
|
||||
|
||||
auto MergeItemsAscendingNamesPass(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering,
|
||||
NewItems &newItems, int iPass, size_t endItemsCount, bool force )
|
||||
-> void;
|
||||
|
||||
auto MergeItemsDescendingNamesPass(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering,
|
||||
NewItems &newItems, int iPass, size_t endItemsCount, bool force )
|
||||
-> void;
|
||||
|
||||
auto MergeItems(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering,
|
||||
const BaseItemPtrs &toMerge, const OrderingHint &hint ) -> void;
|
||||
};
|
||||
|
||||
// When a computed or shared item, or nameless grouping, specifies a hint and
|
||||
// the subordinate does not, propagate the hint.
|
||||
const OrderingHint &ChooseHint(BaseItem *delegate, const OrderingHint &hint)
|
||||
{
|
||||
return !delegate || delegate->orderingHint.type == OrderingHint::Unspecified
|
||||
? hint
|
||||
: delegate->orderingHint;
|
||||
}
|
||||
|
||||
// "Collection" of items is the first pass of visitation, and resolves
|
||||
// delegation and delayed computation and splices transparent group nodes.
|
||||
// This first pass is done at each group, starting with a top-level group.
|
||||
// This pass does not descend to the leaves. Rather, the visitation passes
|
||||
// alternate as the entire tree is recursively visited.
|
||||
|
||||
// forward declaration for mutually recursive functions
|
||||
void CollectItem( Registry::Visitor &visitor,
|
||||
CollectedItems &collection, BaseItem *Item, const OrderingHint &hint );
|
||||
void CollectItems( Registry::Visitor &visitor,
|
||||
CollectedItems &collection, const BaseItemPtrs &items,
|
||||
const OrderingHint &hint )
|
||||
{
|
||||
for ( auto &item : items )
|
||||
CollectItem( visitor, collection, item.get(),
|
||||
ChooseHint( item.get(), hint ) );
|
||||
}
|
||||
void CollectItem( Registry::Visitor &visitor,
|
||||
CollectedItems &collection, BaseItem *pItem, const OrderingHint &hint )
|
||||
{
|
||||
if (!pItem)
|
||||
return;
|
||||
|
||||
using namespace Registry;
|
||||
if (const auto pShared =
|
||||
dynamic_cast<SharedItem*>( pItem )) {
|
||||
auto delegate = pShared->ptr.get();
|
||||
if ( delegate )
|
||||
// recursion
|
||||
CollectItem( visitor, collection, delegate,
|
||||
ChooseHint( delegate, pShared->orderingHint ) );
|
||||
}
|
||||
else
|
||||
if (const auto pComputed =
|
||||
dynamic_cast<ComputedItem*>( pItem )) {
|
||||
auto result = pComputed->factory( visitor );
|
||||
if (result) {
|
||||
// Guarantee long enough lifetime of the result
|
||||
collection.computedItems.push_back( result );
|
||||
// recursion
|
||||
CollectItem( visitor, collection, result.get(),
|
||||
ChooseHint( result.get(), pComputed->orderingHint ) );
|
||||
}
|
||||
}
|
||||
else
|
||||
if (auto pGroup = dynamic_cast<GroupItem*>(pItem)) {
|
||||
if (pGroup->Transparent() && pItem->name.empty())
|
||||
// nameless grouping item is transparent to path calculations
|
||||
// collect group members now
|
||||
// recursion
|
||||
CollectItems(
|
||||
visitor, collection, pGroup->items, ChooseHint( pGroup, hint ) );
|
||||
else
|
||||
// all other group items
|
||||
// defer collection of members until collecting at next lower level
|
||||
collection.items.push_back( {pItem, nullptr, hint} );
|
||||
}
|
||||
else {
|
||||
wxASSERT( dynamic_cast<SingleItem*>(pItem) );
|
||||
// common to all single items
|
||||
collection.items.push_back( {pItem, nullptr, hint} );
|
||||
}
|
||||
}
|
||||
|
||||
using Path = std::vector< Identifier >;
|
||||
|
||||
std::unordered_set< wxString > sBadPaths;
|
||||
void BadPath(
|
||||
const TranslatableString &format, const wxString &key, const Identifier &name )
|
||||
{
|
||||
// Warn, but not more than once in a session for each bad path
|
||||
auto badPath = key + '/' + name.GET();
|
||||
if ( sBadPaths.insert( badPath ).second ) {
|
||||
auto msg = TranslatableString{ format }.Format( badPath );
|
||||
// debug message
|
||||
wxLogDebug( msg.Translation() );
|
||||
#ifdef IS_ALPHA
|
||||
// user-visible message
|
||||
BasicUI::ShowMessageBox( msg );
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void ReportGroupGroupCollision( const wxString &key, const Identifier &name )
|
||||
{
|
||||
BadPath(
|
||||
XO("Plug-in group at %s was merged with a previously defined group"),
|
||||
key, name);
|
||||
}
|
||||
|
||||
void ReportItemItemCollision( const wxString &key, const Identifier &name )
|
||||
{
|
||||
BadPath(
|
||||
XO("Plug-in item at %s conflicts with a previously defined item and was discarded"),
|
||||
key, name);
|
||||
}
|
||||
|
||||
void ReportConflictingPlacements( const wxString &key, const Identifier &name )
|
||||
{
|
||||
BadPath(
|
||||
XO("Plug-in items at %s specify conflicting placements"),
|
||||
key, name);
|
||||
}
|
||||
|
||||
struct ItemOrdering {
|
||||
wxString key;
|
||||
|
||||
ItemOrdering( const Path &path )
|
||||
{
|
||||
// The set of path names determines only an unordered tree.
|
||||
// We want an ordering of the tree that is stable across runs.
|
||||
// The last used ordering for this node can be found in preferences at this
|
||||
// key:
|
||||
wxArrayString strings;
|
||||
for (const auto &id : path)
|
||||
strings.push_back( id.GET() );
|
||||
key = '/' + ::wxJoin( strings, '/', '\0' );
|
||||
}
|
||||
|
||||
// Retrieve the old ordering on demand, if needed to merge something.
|
||||
bool gotOrdering = false;
|
||||
wxString strValue;
|
||||
wxArrayString ordering;
|
||||
|
||||
auto Get() -> wxArrayString & {
|
||||
if ( !gotOrdering ) {
|
||||
gPrefs->Read(key, &strValue);
|
||||
ordering = ::wxSplit( strValue, ',' );
|
||||
gotOrdering = true;
|
||||
}
|
||||
return ordering;
|
||||
};
|
||||
};
|
||||
|
||||
// For each group node, this is called only in the first pass of merging of
|
||||
// items. It might fail to place an item in the first visitation of a
|
||||
// registry, but then succeed in later visitations in the same or later
|
||||
// runs of the program, because of persistent side-effects on the
|
||||
// preferences done at the very end of the visitation.
|
||||
auto CollectedItems::InsertNewItemUsingPreferences(
|
||||
ItemOrdering &itemOrdering, BaseItem *pItem )
|
||||
-> bool
|
||||
{
|
||||
// Note that if more than one plug-in registers items under the same
|
||||
// node, then it is not specified which plug-in is handled first,
|
||||
// the first time registration happens. It might happen that you
|
||||
// add a plug-in, run the program, then add another, then run again;
|
||||
// registration order determined by those actions might not
|
||||
// correspond to the order of re-loading of modules in later
|
||||
// sessions. But whatever ordering is chosen the first time some
|
||||
// plug-in is seen -- that ordering gets remembered in preferences.
|
||||
|
||||
if ( !pItem->name.empty() ) {
|
||||
// Check saved ordering first, and rebuild that as well as is possible
|
||||
auto &ordering = itemOrdering.Get();
|
||||
auto begin2 = ordering.begin(), end2 = ordering.end(),
|
||||
found2 = std::find( begin2, end2, pItem->name );
|
||||
if ( found2 != end2 ) {
|
||||
auto insertPoint = items.end();
|
||||
// Find the next name in the saved ordering that is known already
|
||||
// in the collection.
|
||||
while ( ++found2 != end2 ) {
|
||||
auto known = Find( *found2 );
|
||||
if ( known != insertPoint ) {
|
||||
insertPoint = known;
|
||||
break;
|
||||
}
|
||||
}
|
||||
items.insert( insertPoint, {pItem, nullptr,
|
||||
// Hints no longer matter:
|
||||
{}} );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// For each group node, this may be called in the second and later passes
|
||||
// of merging of items
|
||||
auto CollectedItems::InsertNewItemUsingHint(
|
||||
BaseItem *pItem, const OrderingHint &hint, size_t endItemsCount,
|
||||
bool force ) -> bool
|
||||
{
|
||||
auto begin = items.begin(), end = items.end(),
|
||||
insertPoint = end - endItemsCount;
|
||||
|
||||
// pItem should have a name; if not, ignore the hint, and put it at the
|
||||
// default place, but only if in the final pass.
|
||||
if ( pItem->name.empty() ) {
|
||||
if ( !force )
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
switch ( hint.type ) {
|
||||
case OrderingHint::Before:
|
||||
case OrderingHint::After: {
|
||||
// Default to the end if the name is not found.
|
||||
auto found = Find( hint.name );
|
||||
if ( found == end ) {
|
||||
if ( !force )
|
||||
return false;
|
||||
else
|
||||
insertPoint = found;
|
||||
}
|
||||
else {
|
||||
insertPoint = found;
|
||||
if ( hint.type == OrderingHint::After )
|
||||
++insertPoint;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OrderingHint::Begin:
|
||||
insertPoint = begin;
|
||||
break;
|
||||
case OrderingHint::End:
|
||||
insertPoint = end;
|
||||
break;
|
||||
case OrderingHint::Unspecified:
|
||||
default:
|
||||
if ( !force )
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the item; the hint has been used and no longer matters
|
||||
items.insert( insertPoint, {pItem, nullptr,
|
||||
// Hints no longer matter:
|
||||
{}} );
|
||||
return true;
|
||||
}
|
||||
|
||||
auto CollectedItems::MergeLater( Item &found, const Identifier &name )
|
||||
-> GroupItem *
|
||||
{
|
||||
auto subGroup = found.mergeLater;
|
||||
if ( !subGroup ) {
|
||||
auto newGroup = std::make_shared<TransparentGroupItem<>>( name );
|
||||
computedItems.push_back( newGroup );
|
||||
subGroup = found.mergeLater = newGroup.get();
|
||||
}
|
||||
return subGroup;
|
||||
}
|
||||
|
||||
auto CollectedItems::SubordinateSingleItem( Item &found, BaseItem *pItem )
|
||||
-> void
|
||||
{
|
||||
MergeLater( found, pItem->name )->items.push_back(
|
||||
std::make_unique<SharedItem>(
|
||||
// shared pointer with vacuous deleter
|
||||
std::shared_ptr<BaseItem>( pItem, [](void*){} ) ) );
|
||||
}
|
||||
|
||||
auto CollectedItems::SubordinateMultipleItems( Item &found, GroupItem *pItems )
|
||||
-> void
|
||||
{
|
||||
auto subGroup = MergeLater( found, pItems->name );
|
||||
for ( const auto &pItem : pItems->items )
|
||||
subGroup->items.push_back( std::make_unique<SharedItem>(
|
||||
// shared pointer with vacuous deleter
|
||||
std::shared_ptr<BaseItem>( pItem.get(), [](void*){} ) ) );
|
||||
}
|
||||
|
||||
auto CollectedItems::MergeWithExistingItem(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering, BaseItem *pItem ) -> bool
|
||||
{
|
||||
// Assume no null pointers remain after CollectItems:
|
||||
const auto &name = pItem->name;
|
||||
const auto found = Find( name );
|
||||
if (found != items.end()) {
|
||||
// Collision of names between collection and registry!
|
||||
// There are 2 * 2 = 4 cases, as each of the two are group items or
|
||||
// not.
|
||||
auto pCollectionGroup = dynamic_cast< GroupItem * >( found->visitNow );
|
||||
auto pRegistryGroup = dynamic_cast< GroupItem * >( pItem );
|
||||
if (pCollectionGroup) {
|
||||
if (pRegistryGroup) {
|
||||
// This is the expected case of collision.
|
||||
// Subordinate items from one of the groups will be merged in
|
||||
// another call to MergeItems at a lower level of path.
|
||||
// Note, however, that at most one of the two should be other
|
||||
// than a plain grouping item; if not, we must lose the extra
|
||||
// information carried by one of them.
|
||||
bool pCollectionGrouping = pCollectionGroup->Transparent();
|
||||
auto pRegistryGrouping = pRegistryGroup->Transparent();
|
||||
if ( !(pCollectionGrouping || pRegistryGrouping) )
|
||||
ReportGroupGroupCollision( itemOrdering.key, name );
|
||||
|
||||
if ( pCollectionGrouping && !pRegistryGrouping ) {
|
||||
// Swap their roles
|
||||
found->visitNow = pRegistryGroup;
|
||||
SubordinateMultipleItems( *found, pCollectionGroup );
|
||||
}
|
||||
else
|
||||
SubordinateMultipleItems( *found, pRegistryGroup );
|
||||
}
|
||||
else {
|
||||
// Registered non-group item collides with a previously defined
|
||||
// group.
|
||||
// Resolve this by subordinating the non-group item below
|
||||
// that group.
|
||||
SubordinateSingleItem( *found, pItem );
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (pRegistryGroup) {
|
||||
// Subordinate the previously merged single item below the
|
||||
// newly merged group.
|
||||
// In case the name occurred in two different static registries,
|
||||
// the final merge is the same, no matter which is treated first.
|
||||
auto demoted = found->visitNow;
|
||||
found->visitNow = pRegistryGroup;
|
||||
SubordinateSingleItem( *found, demoted );
|
||||
}
|
||||
else
|
||||
// Collision of non-group items is the worst case!
|
||||
// The later-registered item is lost.
|
||||
// Which one you lose might be unpredictable when both originate
|
||||
// from static registries.
|
||||
ReportItemItemCollision( itemOrdering.key, name );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
// A name is registered that is not known in the collection.
|
||||
return false;
|
||||
}
|
||||
|
||||
auto CollectedItems::MergeLikeNamedItems(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering,
|
||||
NewItems::const_iterator left, NewItems::const_iterator right,
|
||||
const int iPass, size_t endItemsCount, bool force )
|
||||
-> bool
|
||||
{
|
||||
// Try to place the first item of the range.
|
||||
// If such an item is a group, then we always retain the kind of
|
||||
// grouping that was registered. (Which doesn't always happen when
|
||||
// there is name collision in MergeWithExistingItem.)
|
||||
auto iter = left;
|
||||
auto &item = *iter;
|
||||
auto pItem = item.first;
|
||||
const auto &hint = item.second;
|
||||
bool success = false;
|
||||
if ( iPass == -1 )
|
||||
// A first pass consults preferences.
|
||||
success = InsertNewItemUsingPreferences( itemOrdering, pItem );
|
||||
else if ( iPass == hint.type ) {
|
||||
// Later passes for choosing placements.
|
||||
// Maybe it fails in this pass, because a placement refers to some
|
||||
// other name that has not yet been placed.
|
||||
success =
|
||||
InsertNewItemUsingHint( pItem, hint, endItemsCount, force );
|
||||
wxASSERT( !force || success );
|
||||
}
|
||||
|
||||
if ( success ) {
|
||||
// Resolve collisions among remaining like-named items.
|
||||
++iter;
|
||||
if ( iter != right && iPass != 0 &&
|
||||
iter->second.type != OrderingHint::Unspecified &&
|
||||
!( iter->second == hint ) ) {
|
||||
// A diagnostic message sometimes
|
||||
ReportConflictingPlacements( itemOrdering.key, pItem->name );
|
||||
}
|
||||
while ( iter != right )
|
||||
// Re-invoke MergeWithExistingItem for this item, which is known
|
||||
// to have a name collision, so ignore the return value.
|
||||
MergeWithExistingItem( visitor, itemOrdering, iter++ -> first );
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
inline bool MajorComp(
|
||||
const CollectedItems::NewItem &a, const CollectedItems::NewItem &b) {
|
||||
// Descending sort!
|
||||
return a.first->name > b.first->name;
|
||||
};
|
||||
inline bool MinorComp(
|
||||
const CollectedItems::NewItem &a, const CollectedItems::NewItem &b){
|
||||
// Sort by hint type.
|
||||
// This sorts items with unspecified hints last.
|
||||
return a.second < b.second;
|
||||
};
|
||||
inline bool Comp(
|
||||
const CollectedItems::NewItem &a, const CollectedItems::NewItem &b){
|
||||
if ( MajorComp( a, b ) )
|
||||
return true;
|
||||
if ( MajorComp( b, a ) )
|
||||
return false;
|
||||
return MinorComp( a, b );
|
||||
};
|
||||
|
||||
auto CollectedItems::MergeItemsAscendingNamesPass(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering, NewItems &newItems,
|
||||
const int iPass, size_t endItemsCount, bool force ) -> void
|
||||
{
|
||||
// Inner loop over ranges of like-named items.
|
||||
auto rright = newItems.rbegin();
|
||||
auto rend = newItems.rend();
|
||||
while ( rright != rend ) {
|
||||
// Find the range
|
||||
using namespace std::placeholders;
|
||||
auto rleft = std::find_if(
|
||||
rright + 1, rend, std::bind( MajorComp, _1, *rright ) );
|
||||
|
||||
bool success = MergeLikeNamedItems(
|
||||
visitor, itemOrdering, rleft.base(), rright.base(), iPass,
|
||||
endItemsCount, force );
|
||||
|
||||
if ( success ) {
|
||||
auto diff = rend - rleft;
|
||||
newItems.erase( rleft.base(), rright.base() );
|
||||
rend = newItems.rend();
|
||||
rleft = rend - diff;
|
||||
}
|
||||
rright = rleft;
|
||||
}
|
||||
}
|
||||
|
||||
auto CollectedItems::MergeItemsDescendingNamesPass(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering, NewItems &newItems,
|
||||
const int iPass, size_t endItemsCount, bool force ) -> void
|
||||
{
|
||||
// Inner loop over ranges of like-named items.
|
||||
auto left = newItems.begin();
|
||||
while ( left != newItems.end() ) {
|
||||
// Find the range
|
||||
using namespace std::placeholders;
|
||||
auto right = std::find_if(
|
||||
left + 1, newItems.end(), std::bind( MajorComp, *left, _1 ) );
|
||||
|
||||
bool success = MergeLikeNamedItems(
|
||||
visitor, itemOrdering, left, right, iPass,
|
||||
endItemsCount, force );
|
||||
|
||||
if ( success )
|
||||
left = newItems.erase( left, right );
|
||||
else
|
||||
left = right;
|
||||
}
|
||||
};
|
||||
|
||||
auto CollectedItems::MergeItems(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering,
|
||||
const BaseItemPtrs &toMerge, const OrderingHint &hint ) -> void
|
||||
{
|
||||
// First do expansion of nameless groupings, and caching of computed
|
||||
// items, just as for the previously collected items.
|
||||
CollectedItems newCollection{ {}, computedItems };
|
||||
CollectItems( visitor, newCollection, toMerge, hint );
|
||||
|
||||
// Try to merge each, resolving name collisions with items already in the
|
||||
// tree, and collecting those with names that don't collide.
|
||||
NewItems newItems;
|
||||
for ( const auto &item : newCollection.items )
|
||||
if ( !MergeWithExistingItem( visitor, itemOrdering, item.visitNow ) )
|
||||
newItems.push_back( { item.visitNow, item.hint } );
|
||||
|
||||
// Choose placements for items with NEW names.
|
||||
|
||||
// First sort so that like named items are together, and for the same name,
|
||||
// items with more specific ordering hints come earlier.
|
||||
std::sort( newItems.begin(), newItems.end(), Comp );
|
||||
|
||||
// Outer loop over trial passes.
|
||||
int iPass = -1;
|
||||
bool force = false;
|
||||
size_t oldSize = 0;
|
||||
size_t endItemsCount = 0;
|
||||
auto prevSize = newItems.size();
|
||||
while( !newItems.empty() )
|
||||
{
|
||||
// If several items have the same hint, we try to preserve the sort by
|
||||
// name (an internal identifier, not necessarily user visible), just to
|
||||
// have some determinacy. That requires passing one or the other way
|
||||
// over newItems.
|
||||
bool descending =
|
||||
( iPass == OrderingHint::After || iPass == OrderingHint::Begin );
|
||||
|
||||
if ( descending )
|
||||
MergeItemsDescendingNamesPass(
|
||||
visitor, itemOrdering, newItems, iPass, endItemsCount, force );
|
||||
else
|
||||
MergeItemsAscendingNamesPass(
|
||||
visitor, itemOrdering, newItems, iPass, endItemsCount, force );
|
||||
|
||||
auto newSize = newItems.size();
|
||||
++iPass;
|
||||
|
||||
if ( iPass == 0 )
|
||||
// Just tried insertion by preferences. Don't try it again.
|
||||
oldSize = newSize;
|
||||
else if ( iPass == OrderingHint::Unspecified ) {
|
||||
if ( !force ) {
|
||||
iPass = 0, oldSize = newSize;
|
||||
// Are we really ready for the final pass?
|
||||
bool progress = ( oldSize > newSize );
|
||||
if ( progress )
|
||||
// No. While some progress is made, don't force final placements.
|
||||
// Retry Before and After hints.
|
||||
;
|
||||
else
|
||||
force = true;
|
||||
}
|
||||
}
|
||||
else if ( iPass == OrderingHint::End && endItemsCount == 0 )
|
||||
// Remember the size before we put the ending items in place
|
||||
endItemsCount = newSize - prevSize;
|
||||
|
||||
prevSize = newSize;
|
||||
}
|
||||
}
|
||||
|
||||
// forward declaration for mutually recursive functions
|
||||
void VisitItem(
|
||||
Registry::Visitor &visitor, CollectedItems &collection,
|
||||
Path &path, BaseItem *pItem,
|
||||
const GroupItem *pToMerge, const OrderingHint &hint,
|
||||
bool &doFlush );
|
||||
void VisitItems(
|
||||
Registry::Visitor &visitor, CollectedItems &collection,
|
||||
Path &path, GroupItem *pGroup,
|
||||
const GroupItem *pToMerge, const OrderingHint &hint,
|
||||
bool &doFlush )
|
||||
{
|
||||
// Make a NEW collection for this subtree, sharing the memo cache
|
||||
CollectedItems newCollection{ {}, collection.computedItems };
|
||||
|
||||
// Gather items at this level
|
||||
// (The ordering hint is irrelevant when not merging items in)
|
||||
CollectItems( visitor, newCollection, pGroup->items, {} );
|
||||
|
||||
path.push_back( pGroup->name.GET() );
|
||||
|
||||
// Merge with the registry
|
||||
if ( pToMerge )
|
||||
{
|
||||
ItemOrdering itemOrdering{ path };
|
||||
newCollection.MergeItems( visitor, itemOrdering, pToMerge->items, hint );
|
||||
|
||||
// Remember the NEW ordering, if there was any need to use the old.
|
||||
// This makes a side effect in preferences.
|
||||
if ( itemOrdering.gotOrdering ) {
|
||||
wxString newValue;
|
||||
for ( const auto &item : newCollection.items ) {
|
||||
const auto &name = item.visitNow->name;
|
||||
if ( !name.empty() )
|
||||
newValue += newValue.empty()
|
||||
? name.GET()
|
||||
: ',' + name.GET();
|
||||
}
|
||||
if (newValue != itemOrdering.strValue) {
|
||||
gPrefs->Write( itemOrdering.key, newValue );
|
||||
doFlush = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now visit them
|
||||
for ( const auto &item : newCollection.items )
|
||||
VisitItem( visitor, collection, path,
|
||||
item.visitNow, item.mergeLater, item.hint,
|
||||
doFlush );
|
||||
|
||||
path.pop_back();
|
||||
}
|
||||
void VisitItem(
|
||||
Registry::Visitor &visitor, CollectedItems &collection,
|
||||
Path &path, BaseItem *pItem,
|
||||
const GroupItem *pToMerge, const OrderingHint &hint,
|
||||
bool &doFlush )
|
||||
{
|
||||
if (!pItem)
|
||||
return;
|
||||
|
||||
if (const auto pSingle =
|
||||
dynamic_cast<SingleItem*>( pItem )) {
|
||||
wxASSERT( !pToMerge );
|
||||
visitor.Visit( *pSingle, path );
|
||||
}
|
||||
else
|
||||
if (const auto pGroup =
|
||||
dynamic_cast<GroupItem*>( pItem )) {
|
||||
visitor.BeginGroup( *pGroup, path );
|
||||
// recursion
|
||||
VisitItems(
|
||||
visitor, collection, path, pGroup, pToMerge, hint, doFlush );
|
||||
visitor.EndGroup( *pGroup, path );
|
||||
}
|
||||
else
|
||||
wxASSERT( false );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Registry {
|
||||
|
||||
BaseItem::~BaseItem() {}
|
||||
|
||||
SharedItem::~SharedItem() {}
|
||||
|
||||
ComputedItem::~ComputedItem() {}
|
||||
|
||||
SingleItem::~SingleItem() {}
|
||||
|
||||
GroupItem::~GroupItem() {}
|
||||
|
||||
Visitor::~Visitor(){}
|
||||
void Visitor::BeginGroup(GroupItem &, const Path &) {}
|
||||
void Visitor::EndGroup(GroupItem &, const Path &) {}
|
||||
void Visitor::Visit(SingleItem &, const Path &) {}
|
||||
|
||||
void Visit( Visitor &visitor, BaseItem *pTopItem, const GroupItem *pRegistry )
|
||||
{
|
||||
std::vector< BaseItemSharedPtr > computedItems;
|
||||
bool doFlush = false;
|
||||
CollectedItems collection{ {}, computedItems };
|
||||
Path emptyPath;
|
||||
VisitItem(
|
||||
visitor, collection, emptyPath, pTopItem,
|
||||
pRegistry, pRegistry->orderingHint, doFlush );
|
||||
// Flush any writes done by MergeItems()
|
||||
if (doFlush)
|
||||
gPrefs->Flush();
|
||||
}
|
||||
|
||||
OrderingPreferenceInitializer::OrderingPreferenceInitializer(
|
||||
Literal root, Pairs pairs )
|
||||
: mPairs{ std::move( pairs ) }
|
||||
, mRoot{ root }
|
||||
{
|
||||
(*this)();
|
||||
}
|
||||
|
||||
void OrderingPreferenceInitializer::operator () ()
|
||||
{
|
||||
bool doFlush = false;
|
||||
for (const auto &pair : mPairs) {
|
||||
const auto key = wxString{'/'} + mRoot + pair.first;
|
||||
if ( gPrefs->Read(key).empty() ) {
|
||||
gPrefs->Write( key, pair.second );
|
||||
doFlush = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (doFlush)
|
||||
gPrefs->Flush();
|
||||
}
|
||||
|
||||
void RegisterItem( GroupItem ®istry, const Placement &placement,
|
||||
BaseItemPtr pItem )
|
||||
{
|
||||
// Since registration determines only an unordered tree of menu items,
|
||||
// we can sort children of each node lexicographically for our convenience.
|
||||
BaseItemPtrs *pItems;
|
||||
struct Comparator {
|
||||
bool operator()
|
||||
( const Identifier &component, const BaseItemPtr& pItem ) const {
|
||||
return component < pItem->name; }
|
||||
bool operator()
|
||||
( const BaseItemPtr& pItem, const Identifier &component ) const {
|
||||
return pItem->name < component; }
|
||||
};
|
||||
auto find = [&pItems]( const Identifier &component ){ return std::equal_range(
|
||||
pItems->begin(), pItems->end(), component, Comparator() ); };
|
||||
|
||||
auto pNode = ®istry;
|
||||
pItems = &pNode->items;
|
||||
|
||||
const auto pathComponents = ::wxSplit( placement.path, '/' );
|
||||
auto pComponent = pathComponents.begin(), end = pathComponents.end();
|
||||
|
||||
// Descend the registry hierarchy, while groups matching the path components
|
||||
// can be found
|
||||
auto debugPath = wxString{'/'} + registry.name.GET();
|
||||
while ( pComponent != end ) {
|
||||
const auto &pathComponent = *pComponent;
|
||||
|
||||
// Try to find an item already present that is a group item with the
|
||||
// same name; we don't care which if there is more than one.
|
||||
const auto range = find( pathComponent );
|
||||
const auto iter2 = std::find_if( range.first, range.second,
|
||||
[](const BaseItemPtr &pItem){
|
||||
return dynamic_cast< GroupItem* >( pItem.get() ); } );
|
||||
|
||||
if ( iter2 != range.second ) {
|
||||
// A matching group in the registry, so descend
|
||||
pNode = static_cast< GroupItem* >( iter2->get() );
|
||||
pItems = &pNode->items;
|
||||
debugPath += '/' + pathComponent;
|
||||
++pComponent;
|
||||
}
|
||||
else
|
||||
// Insert at this level;
|
||||
// If there are no more path components, and a name collision of
|
||||
// the added item with something already in the registry, don't resolve
|
||||
// it yet in this function, but see MergeItems().
|
||||
break;
|
||||
}
|
||||
|
||||
// Create path group items for remaining components
|
||||
while ( pComponent != end ) {
|
||||
auto newNode = std::make_unique<TransparentGroupItem<>>( *pComponent );
|
||||
pNode = newNode.get();
|
||||
pItems->insert( find( pNode->name ).second, std::move( newNode ) );
|
||||
pItems = &pNode->items;
|
||||
++pComponent;
|
||||
}
|
||||
|
||||
// Remember the hint, to be used later in merging.
|
||||
pItem->orderingHint = placement.hint;
|
||||
|
||||
// Now insert the item.
|
||||
pItems->insert( find( pItem->name ).second, std::move( pItem ) );
|
||||
}
|
||||
|
||||
}
|
295
libraries/lib-registries/Registry.h
Normal file
295
libraries/lib-registries/Registry.h
Normal file
@@ -0,0 +1,295 @@
|
||||
/**********************************************************************
|
||||
|
||||
Audacity: A Digital Audio Editor
|
||||
|
||||
Registry.h
|
||||
|
||||
Paul Licameli split from CommandManager.h
|
||||
|
||||
**********************************************************************/
|
||||
|
||||
#ifndef __AUDACITY_REGISTRY__
|
||||
#define __AUDACITY_REGISTRY__
|
||||
|
||||
#include "Prefs.h"
|
||||
|
||||
// Define classes and functions that associate parts of the user interface
|
||||
// with path names
|
||||
namespace Registry {
|
||||
// Items in the registry form an unordered tree, but each may also describe a
|
||||
// desired insertion point among its peers. The request might not be honored
|
||||
// (as when the other name is not found, or when more than one item requests
|
||||
// the same ordering), but this is not treated as an error.
|
||||
struct OrderingHint
|
||||
{
|
||||
// The default Unspecified hint is just like End, except that in case the
|
||||
// item is delegated to (by a SharedItem, ComputedItem, or nameless
|
||||
// transparent group), the delegating item's hint will be used instead
|
||||
enum Type : int {
|
||||
Before, After,
|
||||
Begin, End,
|
||||
Unspecified // keep this last
|
||||
} type{ Unspecified };
|
||||
|
||||
// name of some other BaseItem; significant only when type is Before or
|
||||
// After:
|
||||
Identifier name;
|
||||
|
||||
OrderingHint() {}
|
||||
OrderingHint( Type type_, const wxString &name_ = {} )
|
||||
: type(type_), name(name_) {}
|
||||
|
||||
bool operator == ( const OrderingHint &other ) const
|
||||
{ return name == other.name && type == other.type; }
|
||||
|
||||
bool operator < ( const OrderingHint &other ) const
|
||||
{
|
||||
// This sorts unspecified placements later
|
||||
return std::make_pair( type, name ) <
|
||||
std::make_pair( other.type, other.name );
|
||||
}
|
||||
};
|
||||
|
||||
// TODO C++17: maybe use std::variant (discriminated unions) to achieve
|
||||
// polymorphism by other means, not needing unique_ptr and dynamic_cast
|
||||
// and using less heap.
|
||||
// Most items in the table will be the large ones describing commands, so the
|
||||
// waste of space in unions for separators and sub-menus should not be
|
||||
// large.
|
||||
struct REGISTRIES_API BaseItem {
|
||||
// declare at least one virtual function so dynamic_cast will work
|
||||
explicit
|
||||
BaseItem( const Identifier &internalName )
|
||||
: name{ internalName }
|
||||
{}
|
||||
virtual ~BaseItem();
|
||||
|
||||
const Identifier name;
|
||||
|
||||
OrderingHint orderingHint;
|
||||
};
|
||||
using BaseItemPtr = std::unique_ptr<BaseItem>;
|
||||
using BaseItemSharedPtr = std::shared_ptr<BaseItem>;
|
||||
using BaseItemPtrs = std::vector<BaseItemPtr>;
|
||||
|
||||
class Visitor;
|
||||
|
||||
|
||||
// An item that delegates to another held in a shared pointer; this allows
|
||||
// static tables of items to be computed once and reused
|
||||
// The name of the delegate is significant for path calculations, but the
|
||||
// SharedItem's ordering hint is used if the delegate has none
|
||||
struct REGISTRIES_API SharedItem final : BaseItem {
|
||||
explicit SharedItem( const BaseItemSharedPtr &ptr_ )
|
||||
: BaseItem{ wxEmptyString }
|
||||
, ptr{ ptr_ }
|
||||
{}
|
||||
~SharedItem() override;
|
||||
|
||||
BaseItemSharedPtr ptr;
|
||||
};
|
||||
|
||||
// A convenience function
|
||||
inline std::unique_ptr<SharedItem> Shared( const BaseItemSharedPtr &ptr )
|
||||
{ return std::make_unique<SharedItem>( ptr ); }
|
||||
|
||||
// An item that computes some other item to substitute for it, each time
|
||||
// the ComputedItem is visited
|
||||
// The name of the substitute is significant for path calculations, but the
|
||||
// ComputedItem's ordering hint is used if the substitute has none
|
||||
struct REGISTRIES_API ComputedItem final : BaseItem {
|
||||
// The type of functions that generate descriptions of items.
|
||||
// Return type is a shared_ptr to let the function decide whether to
|
||||
// recycle the object or rebuild it on demand each time.
|
||||
// Return value from the factory may be null
|
||||
template< typename VisitorType >
|
||||
using Factory = std::function< BaseItemSharedPtr( VisitorType & ) >;
|
||||
|
||||
using DefaultVisitor = Visitor;
|
||||
|
||||
explicit ComputedItem( const Factory< DefaultVisitor > &factory_ )
|
||||
: BaseItem( wxEmptyString )
|
||||
, factory{ factory_ }
|
||||
{}
|
||||
~ComputedItem() override;
|
||||
|
||||
Factory< DefaultVisitor > factory;
|
||||
};
|
||||
|
||||
// Common abstract base class for items that are not groups
|
||||
struct REGISTRIES_API SingleItem : BaseItem {
|
||||
using BaseItem::BaseItem;
|
||||
~SingleItem() override = 0;
|
||||
};
|
||||
|
||||
// Common abstract base class for items that group other items
|
||||
struct REGISTRIES_API GroupItem : BaseItem {
|
||||
using BaseItem::BaseItem;
|
||||
|
||||
// Construction from an internal name and a previously built-up
|
||||
// vector of pointers
|
||||
GroupItem( const Identifier &internalName, BaseItemPtrs &&items_ )
|
||||
: BaseItem{ internalName }, items{ std::move( items_ ) }
|
||||
{}
|
||||
GroupItem( const GroupItem& ) PROHIBITED;
|
||||
~GroupItem() override = 0;
|
||||
|
||||
// Whether the item is non-significant for path naming
|
||||
// when it also has an empty name
|
||||
virtual bool Transparent() const = 0;
|
||||
|
||||
BaseItemPtrs items;
|
||||
};
|
||||
|
||||
// GroupItem adding variadic constructor conveniences
|
||||
template< typename VisitorType = ComputedItem::DefaultVisitor >
|
||||
struct InlineGroupItem : GroupItem {
|
||||
using GroupItem::GroupItem;
|
||||
// In-line, variadic constructor that doesn't require building a vector
|
||||
template< typename... Args >
|
||||
InlineGroupItem( const Identifier &internalName, Args&&... args )
|
||||
: GroupItem( internalName )
|
||||
{ Append( std::forward< Args >( args )... ); }
|
||||
|
||||
private:
|
||||
// nullary overload grounds the recursion
|
||||
void Append() {}
|
||||
// recursive overload
|
||||
template< typename Arg, typename... Args >
|
||||
void Append( Arg &&arg, Args&&... moreArgs )
|
||||
{
|
||||
// Dispatch one argument to the proper overload of AppendOne.
|
||||
// std::forward preserves rvalue/lvalue distinction of the actual
|
||||
// argument of the constructor call; that is, it inserts a
|
||||
// std::move() if and only if the original argument is rvalue
|
||||
AppendOne( std::forward<Arg>( arg ) );
|
||||
// recur with the rest of the arguments
|
||||
Append( std::forward<Args>(moreArgs)... );
|
||||
};
|
||||
|
||||
// Move one unique_ptr to an item into our array
|
||||
void AppendOne( BaseItemPtr&& ptr )
|
||||
{
|
||||
items.push_back( std::move( ptr ) );
|
||||
}
|
||||
// This overload allows a lambda or function pointer in the variadic
|
||||
// argument lists without any other syntactic wrapping, and also
|
||||
// allows implicit conversions to type Factory.
|
||||
// (Thus, a lambda can return a unique_ptr<BaseItem> rvalue even though
|
||||
// Factory's return type is shared_ptr, and the needed conversion is
|
||||
// applied implicitly.)
|
||||
void AppendOne( const ComputedItem::Factory<VisitorType> &factory )
|
||||
{
|
||||
auto adaptedFactory = [factory]( Registry::Visitor &visitor ){
|
||||
return factory( dynamic_cast< VisitorType& >( visitor ) );
|
||||
};
|
||||
AppendOne( std::make_unique<ComputedItem>( adaptedFactory ) );
|
||||
}
|
||||
// This overload lets you supply a shared pointer to an item, directly
|
||||
template<typename Subtype>
|
||||
void AppendOne( const std::shared_ptr<Subtype> &ptr )
|
||||
{ AppendOne( std::make_unique<SharedItem>(ptr) ); }
|
||||
};
|
||||
|
||||
// Inline group item also specifying transparency
|
||||
template< bool transparent,
|
||||
typename VisitorType = ComputedItem::DefaultVisitor >
|
||||
struct ConcreteGroupItem : InlineGroupItem< VisitorType >
|
||||
{
|
||||
using InlineGroupItem< VisitorType >::InlineGroupItem;
|
||||
~ConcreteGroupItem() {}
|
||||
bool Transparent() const override { return transparent; }
|
||||
};
|
||||
|
||||
// Concrete subclass of GroupItem that adds nothing else
|
||||
// TransparentGroupItem with an empty name is transparent to item path calculations
|
||||
// and propagates its ordering hint if subordinates don't specify hints
|
||||
// and it does specify one
|
||||
template< typename VisitorType = ComputedItem::DefaultVisitor >
|
||||
struct TransparentGroupItem final : ConcreteGroupItem< true, VisitorType >
|
||||
{
|
||||
using ConcreteGroupItem< true, VisitorType >::ConcreteGroupItem;
|
||||
~TransparentGroupItem() override {}
|
||||
};
|
||||
|
||||
// The /-separated path is relative to the GroupItem supplied to
|
||||
// RegisterItem.
|
||||
// For instance, wxT("Transport/Cursor") to locate an item under a sub-menu
|
||||
// of a main menu
|
||||
struct Placement {
|
||||
wxString path;
|
||||
OrderingHint hint;
|
||||
|
||||
Placement( const wxString &path_, const OrderingHint &hint_ = {} )
|
||||
: path( path_ ), hint( hint_ )
|
||||
{}
|
||||
};
|
||||
|
||||
// registry collects items, before consulting preferences and ordering
|
||||
// hints, and applying the merge procedure to them.
|
||||
// This function puts one more item into the registry.
|
||||
// The sequence of calls to RegisterItem has no significance for
|
||||
// determining the visitation ordering. When sequence is important, register
|
||||
// a GroupItem.
|
||||
REGISTRIES_API
|
||||
void RegisterItem( GroupItem ®istry, const Placement &placement,
|
||||
BaseItemPtr pItem );
|
||||
|
||||
// Define actions to be done in Visit.
|
||||
// Default implementations do nothing
|
||||
// The supplied path does not include the name of the item
|
||||
class REGISTRIES_API Visitor
|
||||
{
|
||||
public:
|
||||
virtual ~Visitor();
|
||||
using Path = std::vector< Identifier >;
|
||||
virtual void BeginGroup( GroupItem &item, const Path &path );
|
||||
virtual void EndGroup( GroupItem &item, const Path &path );
|
||||
virtual void Visit( SingleItem &item, const Path &path );
|
||||
};
|
||||
|
||||
// Top-down visitation of all items and groups in a tree rooted in
|
||||
// pTopItem, as merged with pRegistry.
|
||||
// The merger of the trees is recomputed in each call, not saved.
|
||||
// So neither given tree is modified.
|
||||
// But there may be a side effect on preferences to remember the ordering
|
||||
// imposed on each node of the unordered tree of registered items; each item
|
||||
// seen in the registry for the first time is placed somehere, and that
|
||||
// ordering should be kept the same thereafter in later runs (which may add
|
||||
// yet other previously unknown items).
|
||||
REGISTRIES_API void Visit(
|
||||
Visitor &visitor,
|
||||
BaseItem *pTopItem,
|
||||
const GroupItem *pRegistry = nullptr );
|
||||
|
||||
// Typically a static object. Constructor initializes certain preferences
|
||||
// if they are not present. These preferences determine an extrinsic
|
||||
// visitation ordering for registered items. This is needed in some
|
||||
// places that have migrated from a system of exhaustive listings, to a
|
||||
// registry of plug-ins, and something must be done to preserve old
|
||||
// behavior. It can be done in the central place using string literal
|
||||
// identifiers only, not requiring static compilation or linkage dependency.
|
||||
struct REGISTRIES_API
|
||||
OrderingPreferenceInitializer : PreferenceInitializer {
|
||||
using Literal = const wxChar *;
|
||||
using Pair = std::pair< Literal, Literal >;
|
||||
using Pairs = std::vector< Pair >;
|
||||
OrderingPreferenceInitializer(
|
||||
// Specifies the topmost preference section:
|
||||
Literal root,
|
||||
// Specifies /-separated Registry paths relative to root
|
||||
// (these should be blank or start with / and not end with /),
|
||||
// each with a ,-separated sequence of identifiers, which specify a
|
||||
// desired ordering at one node of the tree:
|
||||
Pairs pairs );
|
||||
|
||||
void operator () () override;
|
||||
|
||||
private:
|
||||
Pairs mPairs;
|
||||
Literal mRoot;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
Reference in New Issue
Block a user