1
0
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:
Paul Licameli
2021-02-18 18:06:00 -05:00
parent 31515085c5
commit ed3e4de17b
26 changed files with 96 additions and 39 deletions

View 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 &registry = 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

View 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}"
"" ""
)

View 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;

View 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

View 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

View 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

View 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 &registry, 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 = &registry;
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 ) );
}
}

View 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 &registry, 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