1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-06-26 17:18:41 +02:00

Generalize menu tree visitation beyond populating of menus...

... and apply it to implement a new alpha-only command, that dumps the menu
tree as text
This commit is contained in:
Paul Licameli 2020-01-25 15:21:29 -05:00
commit cd907ad80d
4 changed files with 301 additions and 67 deletions

View File

@ -100,7 +100,7 @@ void MenuManager::UpdatePrefs()
} }
/// Namespace for structures that go into building a menu /// Namespace for structures that go into building a menu
namespace MenuTable { namespace Registry {
BaseItem::~BaseItem() {} BaseItem::~BaseItem() {}
@ -121,6 +121,12 @@ void GroupItem::AppendOne( BaseItemPtr&& ptr )
GroupItem::~GroupItem() {} GroupItem::~GroupItem() {}
TransparentGroupItem::~TransparentGroupItem() {} TransparentGroupItem::~TransparentGroupItem() {}
Visitor::~Visitor(){}
void Visitor::BeginGroup(GroupItem &, const Path &) {}
void Visitor::EndGroup(GroupItem &, const Path &) {}
void Visitor::Visit(SingleItem &, const Path &) {}
} }
namespace MenuTable { namespace MenuTable {
@ -183,90 +189,112 @@ namespace {
const auto MenuPathStart = wxT("MenuBar"); const auto MenuPathStart = wxT("MenuBar");
void VisitItem( AudacityProject &project, MenuTable::BaseItem *pItem ); using namespace MenuTable;
struct CollectedItems
void VisitItems(
AudacityProject &project, const MenuTable::BaseItemPtrs &items )
{ {
for ( auto &pSubItem : items ) std::vector< BaseItem * > items;
VisitItem( project, pSubItem.get() ); std::vector< BaseItemSharedPtr > &computedItems;
} };
void VisitItem( AudacityProject &project, MenuTable::BaseItem *pItem ) // "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( AudacityProject &project,
CollectedItems &collection, BaseItem *Item );
void CollectItems( AudacityProject &project,
CollectedItems &collection, const BaseItemPtrs &items )
{
for ( auto &item : items )
CollectItem( project, collection, item.get() );
}
void CollectItem( AudacityProject &project,
CollectedItems &collection, BaseItem *pItem )
{ {
if (!pItem) if (!pItem)
return; return;
auto &manager = CommandManager::Get( project );
using namespace MenuTable; using namespace MenuTable;
if (const auto pShared = if (const auto pShared =
dynamic_cast<SharedItem*>( pItem )) { dynamic_cast<SharedItem*>( pItem )) {
auto delegate = pShared->ptr; auto &delegate = pShared->ptr;
VisitItem( project, delegate.get() ); // recursion
CollectItem( project, collection, delegate.get() );
} }
else else
if (const auto pComputed = if (const auto pComputed =
dynamic_cast<ComputedItem*>( pItem )) { dynamic_cast<ComputedItem*>( pItem )) {
// TODO maybe? memo-ize the results of the function, but that requires
// invalidating the memo at the right times
auto result = pComputed->factory( project ); auto result = pComputed->factory( project );
if (result) if (result) {
// Guarantee long enough lifetime of the result
collection.computedItems.push_back( result );
// recursion // recursion
VisitItem( project, result.get() ); CollectItem( project, collection, result.get() );
}
} }
else else
if (const auto pCommand = if (auto pGroup = dynamic_cast<GroupItem*>(pItem)) {
dynamic_cast<CommandItem*>( pItem )) { if (dynamic_cast<TransparentGroupItem*>(pItem) && pItem->name.empty())
manager.AddItem( project, // nameless grouping item is transparent to path calculations
pCommand->name, pCommand->label_in,
pCommand->finder, pCommand->callback,
pCommand->flags, pCommand->options
);
}
else
if (const auto pCommandList =
dynamic_cast<CommandGroupItem*>( pItem ) ) {
manager.AddItemList(pCommandList->name,
pCommandList->items.data(), pCommandList->items.size(),
pCommandList->finder, pCommandList->callback,
pCommandList->flags, pCommandList->isEffect);
}
else
if (const auto pMenu =
dynamic_cast<MenuItem*>( pItem )) {
manager.BeginMenu( pMenu->title );
// recursion // recursion
VisitItems( project, pMenu->items ); CollectItems( project, collection, pGroup->items );
manager.EndMenu();
}
else else
if (const auto pConditionalGroup = // all other group items
dynamic_cast<ConditionalGroupItem*>( pItem )) { collection.items.push_back( pItem );
const auto flag = pConditionalGroup->condition(); }
if (!flag) else {
manager.BeginOccultCommands(); wxASSERT( dynamic_cast<SingleItem*>(pItem) );
// recursion // common to all single items
VisitItems( project, pConditionalGroup->items ); collection.items.push_back( pItem );
if (!flag) }
manager.EndOccultCommands(); }
using Path = std::vector< Identifier >;
// forward declaration for mutually recursive functions
void VisitItem(
Registry::Visitor &visitor,
AudacityProject &project, CollectedItems &collection,
Path &path, BaseItem *pItem );
void VisitItems(
Registry::Visitor &visitor,
AudacityProject &project, CollectedItems &collection,
Path &path, GroupItem *pGroup )
{
// Make a new collection for this subtree, sharing the memo cache
CollectedItems newCollection{ {}, collection.computedItems };
// Gather items at this level
CollectItems( project, newCollection, pGroup->items );
// Now visit them
path.push_back( pGroup->name );
for ( const auto &pSubItem : newCollection.items )
VisitItem( visitor, project, collection, path, pSubItem );
path.pop_back();
}
void VisitItem(
Registry::Visitor &visitor,
AudacityProject &project, CollectedItems &collection,
Path &path, BaseItem *pItem )
{
if (!pItem)
return;
if (const auto pSingle =
dynamic_cast<SingleItem*>( pItem )) {
visitor.Visit( *pSingle, path );
} }
else else
if (const auto pGroup = if (const auto pGroup =
dynamic_cast<TransparentGroupItem*>( pItem )) { dynamic_cast<GroupItem*>( pItem )) {
visitor.BeginGroup( *pGroup, path );
// recursion // recursion
VisitItems( project, pGroup->items ); VisitItems( visitor, project, collection, path, pGroup );
} visitor.EndGroup( *pGroup, path );
else
if (dynamic_cast<SeparatorItem*>( pItem )) {
manager.AddSeparator();
}
else
if (const auto pSpecial =
dynamic_cast<SpecialItem*>( pItem )) {
const auto pCurrentMenu = manager.CurrentMenu();
wxASSERT( pCurrentMenu );
pSpecial->fn( project, *pCurrentMenu );
} }
else else
wxASSERT( false ); wxASSERT( false );
@ -274,6 +302,20 @@ void VisitItem( AudacityProject &project, MenuTable::BaseItem *pItem )
} }
namespace Registry {
void Visit(
Visitor &visitor, AudacityProject &project,
BaseItem *pTopItem )
{
std::vector< BaseItemSharedPtr > computedItems;
CollectedItems collection{ {}, computedItems };
Path emptyPath;
VisitItem( visitor, project, collection, emptyPath, pTopItem );
}
}
/// CreateMenusAndCommands builds the menus, and also rebuilds them after /// CreateMenusAndCommands builds the menus, and also rebuilds them after
/// changes in configured preferences - for example changes in key-bindings /// changes in configured preferences - for example changes in key-bindings
/// affect the short-cut key legend that appears beside each command, /// affect the short-cut key legend that appears beside each command,
@ -319,6 +361,99 @@ static const auto menuTree = MenuTable::Items( MenuPathStart
, HelpMenu() , HelpMenu()
); );
namespace {
struct MenuItemVisitor : Registry::Visitor
{
MenuItemVisitor( AudacityProject &proj, CommandManager &man )
: project(proj), manager( man ) {}
void BeginGroup( GroupItem &item, const Path& ) override
{
auto pItem = &item;
if (const auto pMenu =
dynamic_cast<MenuItem*>( pItem )) {
manager.BeginMenu( pMenu->title );
}
else
if (const auto pConditionalGroup =
dynamic_cast<ConditionalGroupItem*>( pItem )) {
const auto flag = pConditionalGroup->condition();
if (!flag)
manager.BeginOccultCommands();
// to avoid repeated call of condition predicate in EndGroup():
flags.push_back(flag);
}
else
if (const auto pGroup =
dynamic_cast<TransparentGroupItem*>( pItem )) {
}
else
wxASSERT( false );
}
void EndGroup( GroupItem &item, const Path& ) override
{
auto pItem = &item;
if (const auto pMenu =
dynamic_cast<MenuItem*>( pItem )) {
manager.EndMenu();
}
else
if (const auto pConditionalGroup =
dynamic_cast<ConditionalGroupItem*>( pItem )) {
const bool flag = flags.back();
if (!flag)
manager.EndOccultCommands();
flags.pop_back();
}
else
if (const auto pGroup =
dynamic_cast<TransparentGroupItem*>( pItem )) {
}
else
wxASSERT( false );
}
void Visit( SingleItem &item, const Path& ) override
{
auto pItem = &item;
if (const auto pCommand =
dynamic_cast<CommandItem*>( pItem )) {
manager.AddItem( project,
pCommand->name, pCommand->label_in,
pCommand->finder, pCommand->callback,
pCommand->flags, pCommand->options
);
}
else
if (const auto pCommandList =
dynamic_cast<CommandGroupItem*>( pItem ) ) {
manager.AddItemList(pCommandList->name,
pCommandList->items.data(), pCommandList->items.size(),
pCommandList->finder, pCommandList->callback,
pCommandList->flags, pCommandList->isEffect);
}
else
if (dynamic_cast<SeparatorItem*>( pItem )) {
manager.AddSeparator();
}
else
if (const auto pSpecial =
dynamic_cast<SpecialItem*>( pItem )) {
const auto pCurrentMenu = manager.CurrentMenu();
wxASSERT( pCurrentMenu );
pSpecial->fn( project, *pCurrentMenu );
}
else
wxASSERT( false );
}
AudacityProject &project;
CommandManager &manager;
std::vector<bool> flags;
};
}
void MenuCreator::CreateMenusAndCommands(AudacityProject &project) void MenuCreator::CreateMenusAndCommands(AudacityProject &project)
{ {
auto &commandManager = CommandManager::Get( project ); auto &commandManager = CommandManager::Get( project );
@ -330,7 +465,9 @@ void MenuCreator::CreateMenusAndCommands(AudacityProject &project)
auto menubar = commandManager.AddMenuBar(wxT("appmenu")); auto menubar = commandManager.AddMenuBar(wxT("appmenu"));
wxASSERT(menubar); wxASSERT(menubar);
VisitItem( project, menuTree.get() ); MenuItemVisitor visitor{ project, commandManager };
MenuManager::Visit( visitor, project );
GetProjectFrame( project ).SetMenuBar(menubar.release()); GetProjectFrame( project ).SetMenuBar(menubar.release());
mLastFlags = AlwaysEnabledFlag; mLastFlags = AlwaysEnabledFlag;
@ -340,6 +477,11 @@ void MenuCreator::CreateMenusAndCommands(AudacityProject &project)
#endif #endif
} }
void MenuManager::Visit( Registry::Visitor &visitor, AudacityProject &project )
{
Registry::Visit( visitor, project, menuTree.get() );
}
// TODO: This surely belongs in CommandManager? // TODO: This surely belongs in CommandManager?
void MenuManager::ModifyUndoMenuItems(AudacityProject &project) void MenuManager::ModifyUndoMenuItems(AudacityProject &project)
{ {

View File

@ -35,6 +35,8 @@ enum EffectType : int;
typedef wxString PluginID; typedef wxString PluginID;
typedef wxArrayString PluginIDs; typedef wxArrayString PluginIDs;
namespace Registry{ class Visitor; }
class MenuCreator class MenuCreator
{ {
public: public:
@ -68,6 +70,9 @@ public:
MenuManager &operator=( const MenuManager & ) PROHIBITED; MenuManager &operator=( const MenuManager & ) PROHIBITED;
~MenuManager(); ~MenuManager();
static void Visit(
Registry::Visitor &visitor, AudacityProject &project );
static void ModifyUndoMenuItems(AudacityProject &project); static void ModifyUndoMenuItems(AudacityProject &project);
static void ModifyToolbarMenus(AudacityProject &project); static void ModifyToolbarMenus(AudacityProject &project);
// Calls ModifyToolbarMenus() on all projects // Calls ModifyToolbarMenus() on all projects

View File

@ -407,7 +407,7 @@ private:
// Define classes and functions that associate parts of the user interface // Define classes and functions that associate parts of the user interface
// with path names // with path names
namespace MenuTable { namespace Registry {
// TODO C++17: maybe use std::variant (discriminated unions) to achieve // TODO C++17: maybe use std::variant (discriminated unions) to achieve
// polymorphism by other means, not needing unique_ptr and dynamic_cast // polymorphism by other means, not needing unique_ptr and dynamic_cast
// and using less heap. // and using less heap.
@ -525,7 +525,27 @@ namespace MenuTable {
~TransparentGroupItem() override; ~TransparentGroupItem() override;
}; };
// Define actions to be done in Visit.
// Default implementations do nothing
// The supplied path does not include the name of the item
class 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
void Visit(
Visitor &visitor, AudacityProject &project, BaseItem *pTopItem );
}
// Define items that populate tables that specifically describe menu trees // Define items that populate tables that specifically describe menu trees
namespace MenuTable {
using namespace Registry;
// Describes a main menu in the toolbar, or a sub-menu // Describes a main menu in the toolbar, or a sub-menu
struct MenuItem final : GroupItem { struct MenuItem final : GroupItem {

View File

@ -14,6 +14,7 @@
#include "../Dependencies.h" #include "../Dependencies.h"
#include "../FileNames.h" #include "../FileNames.h"
#include "../HelpText.h" #include "../HelpText.h"
#include "../Menus.h"
#include "../Prefs.h" #include "../Prefs.h"
#include "../Project.h" #include "../Project.h"
#include "../ProjectSelectionManager.h" #include "../ProjectSelectionManager.h"
@ -35,7 +36,8 @@ namespace {
void ShowDiagnostics( void ShowDiagnostics(
AudacityProject &project, const wxString &info, AudacityProject &project, const wxString &info,
const TranslatableString &description, const wxString &defaultPath) const TranslatableString &description, const wxString &defaultPath,
bool fixedWidth = false)
{ {
auto &window = GetProjectFrame( project ); auto &window = GetProjectFrame( project );
wxDialogWrapper dlg( &window, wxID_ANY, description); wxDialogWrapper dlg( &window, wxID_ANY, description);
@ -46,12 +48,21 @@ void ShowDiagnostics(
S.StartVerticalLay(); S.StartVerticalLay();
{ {
text = S.Id(wxID_STATIC) text = S.Id(wxID_STATIC)
.Style(wxTE_MULTILINE | wxTE_READONLY) .Style(wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH)
.AddTextWindow(info); .AddTextWindow("");
S.AddStandardButtons(eOkButton | eCancelButton); S.AddStandardButtons(eOkButton | eCancelButton);
} }
S.EndVerticalLay(); S.EndVerticalLay();
if (fixedWidth) {
auto style = text->GetDefaultStyle();
style.SetFontFamily( wxFONTFAMILY_TELETYPE );
text->SetDefaultStyle(style);
}
*text << info;
dlg.FindWindowById(wxID_OK)->SetLabel(_("&Save")); dlg.FindWindowById(wxID_OK)->SetLabel(_("&Save"));
dlg.SetSize(350, 450); dlg.SetSize(350, 450);
@ -364,6 +375,54 @@ void OnCheckDependencies(const CommandContext &context)
::ShowDependencyDialogIfNeeded(&project, false); ::ShowDependencyDialogIfNeeded(&project, false);
} }
void OnMenuTree(const CommandContext &context)
{
auto &project = context.project;
using namespace MenuTable;
struct MyVisitor : Visitor
{
enum : unsigned { TAB = 3 };
void BeginGroup( GroupItem &item, const Path& ) override
{
Indent();
// using GET for alpha only diagnostic tool
info += item.name.GET();
Return();
indentation = wxString{ ' ', TAB * ++level };
}
void EndGroup( GroupItem &, const Path& ) override
{
indentation = wxString{ ' ', TAB * --level };
}
void Visit( SingleItem &item, const Path& ) override
{
static const wxString separatorName{ '=', 20 };
Indent();
info += dynamic_cast<SeparatorItem*>(&item)
? separatorName
// using GET for alpha only diagnostic tool
: item.name.GET();
Return();
}
void Indent() { info += indentation; }
void Return() { info += '\n'; }
unsigned level{};
wxString indentation;
wxString info;
} visitor;
MenuManager::Visit( visitor, project );
ShowDiagnostics( project, visitor.info,
XO("Menu Tree"), wxT("menutree.txt"), true );
}
void OnCheckForUpdates(const CommandContext &WXUNUSED(context)) void OnCheckForUpdates(const CommandContext &WXUNUSED(context))
{ {
::OpenInDefaultBrowser( VerCheckUrl()); ::OpenInDefaultBrowser( VerCheckUrl());
@ -463,6 +522,14 @@ MenuTable::BaseItemSharedPtr HelpMenu()
Command( wxT("CheckDeps"), XXO("Chec&k Dependencies..."), Command( wxT("CheckDeps"), XXO("Chec&k Dependencies..."),
FN(OnCheckDependencies), FN(OnCheckDependencies),
AudioIONotBusyFlag ) AudioIONotBusyFlag )
#ifdef IS_ALPHA
,
// Menu explorer. Perhaps this should become a macro command
Command( wxT("MenuTree"), XXO("Menu Tree..."),
FN(OnMenuTree),
AlwaysEnabledFlag )
#endif
), ),
#ifndef __WXMAC__ #ifndef __WXMAC__