mirror of
https://github.com/cookiengineer/audacity
synced 2025-04-30 15:49:41 +02:00
Registration of plugin menu items; redo null & Nyquist modules...
... without ugly fragile hacks making assumptions about number and sequence of other menu items. Instead, specify where new items go with textual paths.
This commit is contained in:
commit
f949a05952
@ -146,33 +146,6 @@ int ModuleDispatch(ModuleDispatchTypes type)
|
||||
break;
|
||||
case AppQuiting:
|
||||
break;
|
||||
case ProjectInitialized:
|
||||
case MenusRebuilt:
|
||||
{
|
||||
AudacityProject *p = GetActiveProject();
|
||||
if( p== NULL )
|
||||
return 0;
|
||||
|
||||
wxMenuBar * pBar = GetProjectFrame( *p ).GetMenuBar();
|
||||
wxMenu * pMenu = pBar->GetMenu( 8 ); // Menu 8 is the Analyze Menu.
|
||||
CommandManager * c = &CommandManager::Get( *p );
|
||||
|
||||
c->SetCurrentMenu( pMenu );
|
||||
c->AddSeparator();
|
||||
// We add two new commands into the Analyze menu.
|
||||
c->AddItem( *p,
|
||||
_T("A New Command"), // internal name
|
||||
XO("1st Experimental Command..."), //displayed name
|
||||
ident, ModNullFN( OnFuncFirst ),
|
||||
AudioIONotBusyFlag() );
|
||||
c->AddItem( *p,
|
||||
_T("Another New Command"),
|
||||
XO("2nd Experimental Command"),
|
||||
ident, ModNullFN( OnFuncSecond ),
|
||||
AudioIONotBusyFlag() );
|
||||
c->ClearCurrentMenu();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -180,6 +153,26 @@ int ModuleDispatch(ModuleDispatchTypes type)
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Register our extra menu items
|
||||
namespace {
|
||||
using namespace MenuTable;
|
||||
// We add two new commands into the Analyze menu.
|
||||
AttachedItem sAttachment{ wxT("Analyze"),
|
||||
( FinderScope( ident ), Section( wxT("NullModule"),
|
||||
Command(
|
||||
_T("A New Command"), // internal name
|
||||
XO("1st Experimental Command..."), //displayed name
|
||||
ModNullFN( OnFuncFirst ),
|
||||
AudioIONotBusyFlag() ),
|
||||
Command(
|
||||
_T("Another New Command"),
|
||||
XO("2nd Experimental Command"),
|
||||
ModNullFN( OnFuncSecond ),
|
||||
AudioIONotBusyFlag() )
|
||||
) )
|
||||
};
|
||||
}
|
||||
|
||||
//Example code commented out.
|
||||
#if 0
|
||||
// This is an example function to hijack the main panel
|
||||
|
@ -35,6 +35,7 @@
|
||||
#include "effects/nyquist/Nyquist.h"
|
||||
#include "../images/AudacityLogo.xpm"
|
||||
#include "../../src/commands/CommandContext.h"
|
||||
#include "../../src/commands/CommandManager.h"
|
||||
#include "widgets/AudacityMessageBox.h"
|
||||
|
||||
#include "NyqBench.h"
|
||||
@ -167,8 +168,7 @@ extern "C"
|
||||
|
||||
extern int DLL_API ModuleDispatch(ModuleDispatchTypes type);
|
||||
// ModuleDispatch
|
||||
// is called by Audacity to initialize/terminmate the module,
|
||||
// and ask if it has anything for the menus.
|
||||
// is called by Audacity to initialize/terminate the module
|
||||
int ModuleDispatch(ModuleDispatchTypes type){
|
||||
switch (type){
|
||||
case AppQuiting: {
|
||||
@ -181,29 +181,6 @@ extern "C"
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ProjectInitialized:
|
||||
case MenusRebuilt: {
|
||||
AudacityProject *p = GetActiveProject();
|
||||
wxASSERT(p != NULL);
|
||||
CommandManager *c = &CommandManager::Get( *p );
|
||||
wxASSERT(c != NULL);
|
||||
|
||||
wxMenuBar * pBar = GetProjectFrame( *p ).GetMenuBar();
|
||||
wxASSERT(pBar != NULL );
|
||||
wxMenu * pMenu = pBar->GetMenu( 9 ); // Menu 9 is the Tools Menu.
|
||||
wxASSERT( pMenu != NULL );
|
||||
|
||||
c->SetCurrentMenu(pMenu);
|
||||
c->AddSeparator();
|
||||
c->AddItem( *p, wxT("NyqBench"),
|
||||
XO("&Nyquist Workbench..."),
|
||||
findme,
|
||||
static_cast<CommandFunctorPointer>(&NyqBench::ShowNyqBench),
|
||||
AudioIONotBusyFlag());
|
||||
|
||||
c->ClearCurrentMenu();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -211,6 +188,18 @@ extern "C"
|
||||
}
|
||||
};
|
||||
|
||||
// Register our extra menu item
|
||||
namespace {
|
||||
using namespace MenuTable;
|
||||
AttachedItem sAttachment{ wxT("Tools"),
|
||||
( FinderScope( findme ), Section( wxT("NyquistWorkBench"),
|
||||
Command( wxT("NyqBench"), XO("&Nyquist Workbench..."),
|
||||
static_cast<CommandFunctorPointer>(&NyqBench::ShowNyqBench),
|
||||
AudioIONotBusyFlag())
|
||||
) )
|
||||
};
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// NyqTextCtrl
|
||||
//----------------------------------------------------------------------------
|
||||
|
@ -84,8 +84,7 @@ typedef enum
|
||||
AppInitialized,
|
||||
AppQuiting,
|
||||
ProjectInitialized,
|
||||
ProjectClosing,
|
||||
MenusRebuilt
|
||||
ProjectClosing
|
||||
} ModuleDispatchTypes;
|
||||
|
||||
|
||||
@ -108,8 +107,7 @@ DLL_API const wxChar * GetVersionString()
|
||||
|
||||
extern int DLL_API ModuleDispatch(ModuleDispatchTypes type);
|
||||
// ModuleDispatch
|
||||
// is called by Audacity to initialize/terminmate the module,
|
||||
// and ask if it has anything for the menus.
|
||||
// is called by Audacity to initialize/terminate the module
|
||||
// We don't (yet) do anything in this, since we have a special function for the scripter
|
||||
// all we need to do is return 1.
|
||||
int ModuleDispatch(ModuleDispatchTypes type){
|
||||
@ -120,8 +118,7 @@ int ModuleDispatch(ModuleDispatchTypes type){
|
||||
case AppQuiting: {
|
||||
}
|
||||
break;
|
||||
case ProjectInitialized:
|
||||
case MenusRebuilt: {
|
||||
case ProjectInitialized: {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
567
src/Menus.cpp
567
src/Menus.cpp
@ -32,7 +32,6 @@
|
||||
|
||||
#include <wx/frame.h>
|
||||
|
||||
#include "ModuleManager.h"
|
||||
#include "Project.h"
|
||||
#include "ProjectHistory.h"
|
||||
#include "ProjectSettings.h"
|
||||
@ -40,8 +39,11 @@
|
||||
#include "commands/CommandManager.h"
|
||||
#include "prefs/TracksPrefs.h"
|
||||
#include "toolbars/ToolManager.h"
|
||||
#include "widgets/AudacityMessageBox.h"
|
||||
#include "widgets/ErrorDialog.h"
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
#include <wx/menu.h>
|
||||
#include <wx/windowptr.h>
|
||||
|
||||
@ -175,17 +177,135 @@ CommandHandlerFinder FinderScope::sFinder =
|
||||
|
||||
}
|
||||
|
||||
|
||||
namespace Registry {
|
||||
|
||||
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;
|
||||
|
||||
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( newNode->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 ) );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const auto MenuPathStart = wxT("MenuBar");
|
||||
|
||||
using namespace MenuTable;
|
||||
struct ItemOrdering;
|
||||
|
||||
using namespace Registry;
|
||||
struct CollectedItems
|
||||
{
|
||||
std::vector< BaseItem * > items;
|
||||
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, bool force ) -> bool;
|
||||
|
||||
auto SubordinateSingleItem( Item &found, BaseItem *pItem ) -> void;
|
||||
|
||||
auto MergeItems(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering,
|
||||
const BaseItemPtrs &toMerge, const OrderingHint &hint ) -> void;
|
||||
|
||||
auto MergeItem(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering, BaseItem *pItem ) -> bool;
|
||||
};
|
||||
|
||||
// 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.
|
||||
@ -194,25 +314,29 @@ struct CollectedItems
|
||||
|
||||
// forward declaration for mutually recursive functions
|
||||
void CollectItem( Registry::Visitor &visitor,
|
||||
CollectedItems &collection, BaseItem *Item );
|
||||
CollectedItems &collection, BaseItem *Item, const OrderingHint &hint );
|
||||
void CollectItems( Registry::Visitor &visitor,
|
||||
CollectedItems &collection, const BaseItemPtrs &items )
|
||||
CollectedItems &collection, const BaseItemPtrs &items,
|
||||
const OrderingHint &hint )
|
||||
{
|
||||
for ( auto &item : items )
|
||||
CollectItem( visitor, collection, item.get() );
|
||||
CollectItem( visitor, collection, item.get(),
|
||||
ChooseHint( item.get(), hint ) );
|
||||
}
|
||||
void CollectItem( Registry::Visitor &visitor,
|
||||
CollectedItems &collection, BaseItem *pItem )
|
||||
CollectedItems &collection, BaseItem *pItem, const OrderingHint &hint )
|
||||
{
|
||||
if (!pItem)
|
||||
return;
|
||||
|
||||
using namespace MenuTable;
|
||||
using namespace Registry;
|
||||
if (const auto pShared =
|
||||
dynamic_cast<SharedItem*>( pItem )) {
|
||||
auto &delegate = pShared->ptr;
|
||||
// recursion
|
||||
CollectItem( visitor, collection, delegate.get() );
|
||||
auto delegate = pShared->ptr.get();
|
||||
if ( delegate )
|
||||
// recursion
|
||||
CollectItem( visitor, collection, delegate,
|
||||
ChooseHint( delegate, pShared->orderingHint ) );
|
||||
}
|
||||
else
|
||||
if (const auto pComputed =
|
||||
@ -222,51 +346,416 @@ void CollectItem( Registry::Visitor &visitor,
|
||||
// Guarantee long enough lifetime of the result
|
||||
collection.computedItems.push_back( result );
|
||||
// recursion
|
||||
CollectItem( visitor, collection, result.get() );
|
||||
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 );
|
||||
CollectItems(
|
||||
visitor, collection, pGroup->items, pGroup->orderingHint );
|
||||
else
|
||||
// all other group items
|
||||
collection.items.push_back( pItem );
|
||||
// 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 );
|
||||
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
|
||||
AudacityMessageBox( 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;
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
auto CollectedItems::InsertNewItemUsingHint(
|
||||
BaseItem *pItem, const OrderingHint &hint, bool force ) -> bool
|
||||
{
|
||||
auto begin = items.begin(), end = items.end();
|
||||
|
||||
// pItem should have a name; if not, ignore the hint and put it at the
|
||||
// end.
|
||||
auto insertPoint = end;
|
||||
|
||||
if ( !pItem->name.empty() ) {
|
||||
// Use the placement hint.
|
||||
// If more than one item request the same placement, they
|
||||
// end up in the sequence in which they were merged (at least in
|
||||
// cases other than After)
|
||||
switch ( hint.type ) {
|
||||
case OrderingHint::Before:
|
||||
case OrderingHint::After:
|
||||
// Default to the end if the name is not found.
|
||||
insertPoint = Find( hint.name );
|
||||
if ( insertPoint == end ) {
|
||||
if ( !force )
|
||||
return false;
|
||||
}
|
||||
else if ( hint.type == OrderingHint::After )
|
||||
++insertPoint;
|
||||
break;
|
||||
case OrderingHint::Begin:
|
||||
insertPoint = begin;
|
||||
break;
|
||||
case OrderingHint::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::SubordinateSingleItem( Item &found, BaseItem *pItem )
|
||||
-> void
|
||||
{
|
||||
auto subGroup = std::make_shared<TransparentGroupItem<>>( pItem->name,
|
||||
std::make_unique<SharedItem>(
|
||||
// shared pointer with vacuous deleter
|
||||
std::shared_ptr<BaseItem>( pItem, [](void*){} ) ) );
|
||||
found.mergeLater = subGroup.get();
|
||||
computedItems.push_back( subGroup );
|
||||
}
|
||||
|
||||
auto CollectedItems::MergeItem(
|
||||
Visitor &visitor, ItemOrdering &itemOrdering, BaseItem *pItem ) -> bool
|
||||
{
|
||||
// Assume no null pointers in the registry
|
||||
const auto &name = pItem->name;
|
||||
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;
|
||||
found->mergeLater = pCollectionGroup;
|
||||
}
|
||||
else
|
||||
found->mergeLater = 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::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 defined menus.
|
||||
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.
|
||||
using NewItem = std::pair< BaseItem*, OrderingHint >;
|
||||
std::vector< NewItem > newItems;
|
||||
for ( const auto &item : newCollection.items )
|
||||
if ( !MergeItem( visitor, itemOrdering, item.visitNow ) )
|
||||
newItems.push_back( { item.visitNow, item.hint } );
|
||||
|
||||
// There may still be unresolved name collisions among the NEW items,
|
||||
// so first find their sorted order.
|
||||
static auto majorComp = [](const NewItem &a, const NewItem &b) {
|
||||
// Descending sort!
|
||||
return a.first->name > b.first->name;
|
||||
};
|
||||
auto minorComp = [](const NewItem &a, const NewItem &b){
|
||||
// Sort items with specified hints earlier.
|
||||
return a.second < b.second;
|
||||
};
|
||||
auto comp = [&](const NewItem &a, const NewItem &b){
|
||||
if ( majorComp( a, b ) )
|
||||
return true;
|
||||
if ( majorComp( b, a ) )
|
||||
return false;
|
||||
return minorComp( a, b );
|
||||
};
|
||||
std::sort( newItems.begin(), newItems.end(), comp );
|
||||
|
||||
// Choose placements for items with NEW names.
|
||||
// Outer loop over trial passes.
|
||||
int iPass = 0;
|
||||
while( !newItems.empty() ) {
|
||||
// Inner loop over ranges of like-named items.
|
||||
// Do it right to left, to shrink array faster and avoid invalidating rend.
|
||||
auto right = newItems.rbegin();
|
||||
auto rend = newItems.rend();
|
||||
bool forceNext = true;
|
||||
while ( right != rend ) {
|
||||
// Find the range
|
||||
using namespace std::placeholders;
|
||||
auto left = std::find_if(
|
||||
right + 1, rend, std::bind( majorComp, _1, *right ) );
|
||||
|
||||
// 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 MergeItem.)
|
||||
auto iter = left.base();
|
||||
auto &item = *iter;
|
||||
auto pItem = item.first;
|
||||
const auto &hint = item.second;
|
||||
bool success = true;
|
||||
if ( iPass == 0 )
|
||||
// A first pass consults preferences.
|
||||
success = InsertNewItemUsingPreferences( itemOrdering, pItem );
|
||||
else {
|
||||
// Later passes for choosing placements.
|
||||
bool forceNow = iPass == -1;
|
||||
// Maybe it fails in this pass, because a placement refers to some
|
||||
// other name that has not yet been placed.
|
||||
success = InsertNewItemUsingHint( pItem, hint, forceNow );
|
||||
wxASSERT( !forceNow || success );
|
||||
// While some progress is made, don't force final placements.
|
||||
if ( success )
|
||||
forceNext = false;
|
||||
}
|
||||
|
||||
if ( success ) {
|
||||
// Resolve collisions among remaining like-named items.
|
||||
++iter;
|
||||
if ( iter != right.base() && iPass != 0 &&
|
||||
iter->second.type != OrderingHint::Unspecified &&
|
||||
!( iter->second == hint ) ) {
|
||||
// A diagnostic message sometimes
|
||||
ReportConflictingPlacements( itemOrdering.key, pItem->name );
|
||||
}
|
||||
while ( iter != right.base() )
|
||||
// Re-invoke MergeItem for this item, which is known to have a name
|
||||
// collision, so ignore the return value.
|
||||
MergeItem( visitor, itemOrdering, iter++ -> first );
|
||||
newItems.erase( left.base(), right.base() );
|
||||
}
|
||||
|
||||
right = left;
|
||||
}
|
||||
iPass = forceNext ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
// forward declaration for mutually recursive functions
|
||||
void VisitItem(
|
||||
Registry::Visitor &visitor, CollectedItems &collection,
|
||||
Path &path, BaseItem *pItem );
|
||||
Path &path, BaseItem *pItem,
|
||||
GroupItem *pToMerge, const OrderingHint &hint,
|
||||
bool &doFlush );
|
||||
void VisitItems(
|
||||
Registry::Visitor &visitor, CollectedItems &collection,
|
||||
Path &path, GroupItem *pGroup )
|
||||
Path &path, GroupItem *pGroup,
|
||||
GroupItem *pToMerge, const OrderingHint &hint,
|
||||
bool &doFlush )
|
||||
{
|
||||
// Make a new collection for this subtree, sharing the memo cache
|
||||
// Make a NEW collection for this subtree, sharing the memo cache
|
||||
CollectedItems newCollection{ {}, collection.computedItems };
|
||||
|
||||
// Gather items at this level
|
||||
CollectItems( visitor, newCollection, pGroup->items );
|
||||
// (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
|
||||
path.push_back( pGroup->name );
|
||||
for ( const auto &pSubItem : newCollection.items )
|
||||
VisitItem( visitor, collection, path, pSubItem );
|
||||
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 )
|
||||
Path &path, BaseItem *pItem,
|
||||
GroupItem *pToMerge, const OrderingHint &hint,
|
||||
bool &doFlush )
|
||||
{
|
||||
if (!pItem)
|
||||
return;
|
||||
@ -280,7 +769,8 @@ void VisitItem(
|
||||
dynamic_cast<GroupItem*>( pItem )) {
|
||||
visitor.BeginGroup( *pGroup, path );
|
||||
// recursion
|
||||
VisitItems( visitor, collection, path, pGroup );
|
||||
VisitItems(
|
||||
visitor, collection, path, pGroup, pToMerge, hint, doFlush );
|
||||
visitor.EndGroup( *pGroup, path );
|
||||
}
|
||||
else
|
||||
@ -291,12 +781,18 @@ void VisitItem(
|
||||
|
||||
namespace Registry {
|
||||
|
||||
void Visit( Visitor &visitor, BaseItem *pTopItem )
|
||||
void Visit( Visitor &visitor, BaseItem *pTopItem, GroupItem *pRegistry )
|
||||
{
|
||||
std::vector< BaseItemSharedPtr > computedItems;
|
||||
bool doFlush = false;
|
||||
CollectedItems collection{ {}, computedItems };
|
||||
Path emptyPath;
|
||||
VisitItem( visitor, collection, emptyPath, pTopItem );
|
||||
VisitItem(
|
||||
visitor, collection, emptyPath, pTopItem,
|
||||
pRegistry, pRegistry->orderingHint, doFlush );
|
||||
// Flush any writes done by MergeItems()
|
||||
if (doFlush)
|
||||
gPrefs->Flush();
|
||||
}
|
||||
|
||||
}
|
||||
@ -328,6 +824,20 @@ MenuTable::BaseItemSharedPtr ExtraMenu();
|
||||
|
||||
MenuTable::BaseItemSharedPtr HelpMenu();
|
||||
|
||||
namespace {
|
||||
static Registry::GroupItem &sRegistry()
|
||||
{
|
||||
static Registry::TransparentGroupItem<> registry{ MenuPathStart };
|
||||
return registry;
|
||||
}
|
||||
}
|
||||
|
||||
MenuTable::AttachedItem::AttachedItem(
|
||||
const Placement &placement, BaseItemPtr pItem )
|
||||
{
|
||||
Registry::RegisterItem( sRegistry(), placement, std::move( pItem ) );
|
||||
}
|
||||
|
||||
// Table of menu factories.
|
||||
// TODO: devise a registration system instead.
|
||||
static const auto menuTree = MenuTable::Items( MenuPathStart
|
||||
@ -347,6 +857,7 @@ static const auto menuTree = MenuTable::Items( MenuPathStart
|
||||
);
|
||||
|
||||
namespace {
|
||||
using namespace MenuTable;
|
||||
struct MenuItemVisitor : MenuVisitor
|
||||
{
|
||||
MenuItemVisitor( AudacityProject &proj, CommandManager &man )
|
||||
@ -465,7 +976,7 @@ void MenuCreator::CreateMenusAndCommands(AudacityProject &project)
|
||||
|
||||
void MenuManager::Visit( MenuVisitor &visitor )
|
||||
{
|
||||
Registry::Visit( visitor, menuTree.get() );
|
||||
Registry::Visit( visitor, menuTree.get(), &sRegistry() );
|
||||
}
|
||||
|
||||
// TODO: This surely belongs in CommandManager?
|
||||
@ -534,8 +1045,6 @@ void MenuCreator::RebuildMenuBar(AudacityProject &project)
|
||||
CommandManager::Get( project ).PurgeData();
|
||||
|
||||
CreateMenusAndCommands(project);
|
||||
|
||||
ModuleManager::Get().Dispatch(MenusRebuilt);
|
||||
}
|
||||
|
||||
void MenuManager::OnUndoRedo( wxCommandEvent &evt )
|
||||
|
@ -38,8 +38,7 @@ typedef enum
|
||||
AppInitialized,
|
||||
AppQuiting,
|
||||
ProjectInitialized,
|
||||
ProjectClosing,
|
||||
MenusRebuilt
|
||||
ProjectClosing
|
||||
} ModuleDispatchTypes;
|
||||
|
||||
typedef int (*fnModuleDispatch)(ModuleDispatchTypes type);
|
||||
|
@ -477,26 +477,6 @@ wxMenu * CommandManager::CurrentMenu() const
|
||||
return tmpCurrentSubMenu;
|
||||
}
|
||||
|
||||
void CommandManager::SetCurrentMenu(wxMenu * menu)
|
||||
{
|
||||
// uCurrentMenu ought to be null in correct usage
|
||||
wxASSERT(!uCurrentMenu);
|
||||
// Make sure of it anyway
|
||||
uCurrentMenu.reset();
|
||||
|
||||
mCurrentMenu = menu;
|
||||
}
|
||||
|
||||
void CommandManager::ClearCurrentMenu()
|
||||
{
|
||||
// uCurrentMenu ought to be null in correct usage
|
||||
wxASSERT(!uCurrentMenu);
|
||||
// Make sure of it anyway
|
||||
uCurrentMenu.reset();
|
||||
|
||||
mCurrentMenu = nullptr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void CommandManager::AddItem(AudacityProject &project,
|
||||
|
@ -133,8 +133,6 @@ class AUDACITY_DLL_API CommandManager final
|
||||
|
||||
std::unique_ptr<wxMenuBar> AddMenuBar(const wxString & sMenu);
|
||||
|
||||
// You may either called SetCurrentMenu later followed by ClearCurrentMenu,
|
||||
// or else BeginMenu followed by EndMenu. Don't mix them.
|
||||
wxMenu *BeginMenu(const TranslatableString & tName);
|
||||
void EndMenu();
|
||||
|
||||
@ -235,11 +233,6 @@ class AUDACITY_DLL_API CommandManager final
|
||||
void Check(const CommandID &name, bool checked);
|
||||
void Modify(const wxString &name, const TranslatableString &newLabel);
|
||||
|
||||
// You may either called SetCurrentMenu later followed by ClearCurrentMenu,
|
||||
// or else BeginMenu followed by EndMenu. Don't mix them.
|
||||
void SetCurrentMenu(wxMenu * menu);
|
||||
void ClearCurrentMenu();
|
||||
|
||||
//
|
||||
// Modifying accelerators
|
||||
//
|
||||
@ -408,6 +401,40 @@ private:
|
||||
// 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.
|
||||
@ -423,6 +450,8 @@ namespace Registry {
|
||||
virtual ~BaseItem();
|
||||
|
||||
const Identifier name;
|
||||
|
||||
OrderingHint orderingHint;
|
||||
};
|
||||
using BaseItemPtr = std::unique_ptr<BaseItem>;
|
||||
using BaseItemSharedPtr = std::shared_ptr<BaseItem>;
|
||||
@ -433,7 +462,8 @@ namespace Registry {
|
||||
|
||||
// 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
|
||||
// The name of the delegate is significant for path calculations, but the
|
||||
// SharedItem's ordering hint is used if the delegate has none
|
||||
struct SharedItem final : BaseItem {
|
||||
explicit SharedItem( const BaseItemSharedPtr &ptr_ )
|
||||
: BaseItem{ wxEmptyString }
|
||||
@ -450,7 +480,8 @@ namespace Registry {
|
||||
|
||||
// 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
|
||||
// The name of the substitute is significant for path calculations, but the
|
||||
// ComputedItem's ordering hint is used if the substitute has none
|
||||
struct 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
|
||||
@ -482,7 +513,7 @@ namespace Registry {
|
||||
|
||||
// Construction from an internal name and a previously built-up
|
||||
// vector of pointers
|
||||
GroupItem( const wxString &internalName, BaseItemPtrs &&items_ )
|
||||
GroupItem( const Identifier &internalName, BaseItemPtrs &&items_ )
|
||||
: BaseItem{ internalName }, items{ std::move( items_ ) }
|
||||
{}
|
||||
~GroupItem() override = 0;
|
||||
@ -500,7 +531,7 @@ namespace Registry {
|
||||
using GroupItem::GroupItem;
|
||||
// In-line, variadic constructor that doesn't require building a vector
|
||||
template< typename... Args >
|
||||
InlineGroupItem( const wxString &internalName, Args&&... args )
|
||||
InlineGroupItem( const Identifier &internalName, Args&&... args )
|
||||
: GroupItem( internalName )
|
||||
{ Append( std::forward< Args >( args )... ); }
|
||||
|
||||
@ -556,6 +587,7 @@ namespace Registry {
|
||||
|
||||
// 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
|
||||
template< typename VisitorType = ComputedItem::DefaultVisitor >
|
||||
struct TransparentGroupItem final : ConcreteGroupItem< true, VisitorType >
|
||||
{
|
||||
@ -563,6 +595,25 @@ namespace Registry {
|
||||
~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.
|
||||
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
|
||||
@ -576,8 +627,19 @@ namespace Registry {
|
||||
virtual void Visit( SingleItem &item, const Path &path );
|
||||
};
|
||||
|
||||
// Top-down visitation of all items and groups in a tree
|
||||
void Visit( Visitor &visitor, BaseItem *pTopItem );
|
||||
// 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).
|
||||
void Visit(
|
||||
Visitor &visitor,
|
||||
BaseItem *pTopItem,
|
||||
GroupItem *pRegistry = nullptr );
|
||||
}
|
||||
|
||||
struct MenuVisitor : Registry::Visitor
|
||||
@ -750,7 +812,8 @@ namespace MenuTable {
|
||||
using MenuItems = MenuPart< true >;
|
||||
using MenuSection = MenuPart< false >;
|
||||
|
||||
// Following are the functions to use directly in writing table definitions.
|
||||
// The following, and Shared(), are the functions to use directly
|
||||
// in writing table definitions.
|
||||
|
||||
// Group items can be constructed two ways.
|
||||
// Pointers to subordinate items are moved into the result.
|
||||
@ -775,7 +838,7 @@ namespace MenuTable {
|
||||
const wxString &internalName, Args&&... args )
|
||||
{ return std::make_unique< MenuSection >(
|
||||
internalName, std::forward<Args>(args)... ); }
|
||||
|
||||
|
||||
// Menu items can be constructed two ways, as for group items
|
||||
// Items will appear in a main toolbar menu or in a sub-menu.
|
||||
// The name is untranslated. Try to keep the name stable across Audacity
|
||||
@ -863,6 +926,20 @@ namespace MenuTable {
|
||||
inline std::unique_ptr<SpecialItem> Special(
|
||||
const wxString &name, const SpecialItem::Appender &fn )
|
||||
{ return std::make_unique<SpecialItem>( name, fn ); }
|
||||
|
||||
// Typically you make a static object of this type in the .cpp file that
|
||||
// also defines the added menu actions.
|
||||
// pItem can be specified by an expression using the inline functions above.
|
||||
struct AttachedItem final
|
||||
{
|
||||
AttachedItem( const Placement &placement, BaseItemPtr pItem );
|
||||
|
||||
AttachedItem( const wxString &path, BaseItemPtr pItem )
|
||||
// Delegating constructor
|
||||
: AttachedItem( Placement{ path }, std::move( pItem ) )
|
||||
{}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
Loading…
x
Reference in New Issue
Block a user