Audacity 3.2.0
BasicMenu.cpp
Go to the documentation of this file.
1/*!********************************************************************
2
3Audacity: A Digital Audio Editor
4
5@file BasicMenu.cpp
6
7Paul Licameli
8
9**********************************************************************/
10
11#include "BasicMenu.h"
12#include "Journal.h"
13#include "JournalOutput.h"
14#include "JournalRegistry.h"
15#include "JournalEvents.h"
16#include "MemoryX.h"
18#include "wxArrayStringEx.h"
19#include <wx/eventfilter.h>
20#include <wx/menu.h>
21#include <wx/weakref.h>
22#include <wx/window.h>
23#include <optional>
24
25namespace BasicMenu {
26
27namespace {
28
29const auto JournalCode = L"PopupMenu";
30
31using Menus = std::vector< wxWeakRef< wxMenu > >;
32
33// Each item in this stack lists a menu and all descendant sub-menus
34std::vector< Menus > sMenuStack;
35bool sHandledEvent = false;
36
37Menus FindDescendants( wxMenu &menu )
38{
39 Menus result{ &menu };
40 // We can discover them breadth-first
41 for ( size_t ii = 0; ii < result.size(); ++ii ) {
42 if ( auto pMenu = result[ii] ) {
43 for ( const auto &pItem : pMenu->GetMenuItems() ) {
44 if ( const auto pSubMenu = pItem->GetSubMenu() )
45 result.push_back( pSubMenu );
46 }
47 }
48 }
49 return result;
50}
51
52inline bool ContainsMenu( const Menus &menus, void *pObj )
53{
54 return std::count( menus.begin(), menus.end(), pObj );
55}
56
57// Find a path name for the id in the given menu, but only if it, and
58// each sub-menu above it, is uniquely named among peer items
59std::optional< wxArrayStringEx > FindPathName( wxMenu &theMenu, int id )
60{
61 wxMenuItem *pItem = nullptr;
62 wxMenu *pSubMenu = nullptr;
63 if ( !( pItem = theMenu.FindItem( id, &pSubMenu ) ) )
64 return std::nullopt;
65
66 // Gather path components, checking uniqueness at each level
68 for ( ; pSubMenu; pSubMenu = pSubMenu->GetParent() ) {
69 const auto &items = pSubMenu->GetMenuItems();
70 const auto begin = items.begin(), end = items.end();
71 if ( !names.empty() ) {
72 // Update pItem on second and later passes
73 if ( const auto iter = std::find_if( begin, end,
74 [&]( auto pNewItem ){
75 return pNewItem->GetSubMenu() == pItem->GetMenu(); } );
76 iter == end )
77 return std::nullopt;
78 else
79 pItem = *iter;
80 }
81 auto name = pItem->GetItemLabelText();
82 if ( 1 != std::count_if( begin, end, [&](auto item){
83 return item->GetItemLabelText() == name; } ) )
84 // nonuniqueness
85 return std::nullopt;
86 names.push_back( name );
87 }
88 std::reverse( names.begin(), names.end() );
89 return { names };
90}
91
93struct Watcher : wxEventFilter
94{
96 {
97 wxEvtHandler::AddFilter( this );
98 }
99
101 {
102 wxEvtHandler::RemoveFilter( this );
103 }
104
105 int FilterEvent( wxEvent &event ) override
106 {
107 using namespace Journal::Events;
108
109 // Record something only if we are recording events, this is a menu
110 // event, there is an outstanding popup menu, and that or a descendant
111 // is the event object
112 auto pObj = event.GetEventObject();
113 if (!(IsWatching() &&
114 event.GetEventType() == wxEVT_MENU &&
115 !sMenuStack.empty() &&
116 ContainsMenu( sMenuStack.back(), pObj ) ))
117 return Event_Skip;
118
119 // Find a path identifying the object
120 auto pPath = FindPathName( *static_cast<wxMenu*>(pObj), event.GetId() );
121 if ( !pPath ) {
123 return Event_Skip;
124 }
125
126 // Write a representation to the journal.
127 // Write names, not numerical ids, so the journal is not
128 // fragile if the assignment of ids to commands changes.
129 pPath->insert( pPath->begin(), JournalCode );
130 Journal::Output( *pPath );
131 sHandledEvent = true;
132
133 return Event_Skip;
134 }
135};
136
137void Watch()
138{
139 static Watcher instance;
140}
141
142// Add a callback for startup of journalling
144 using namespace Journal;
145
146 if ( !GetError() && IsRecording() )
147 // one time installation
148 Watch();
149
150 return true;
151} };
152
153void ReplayPopup( wxMenu *theMenu )
154{
155 // Expect JournalCode and maybe a path.
156 const auto fields = Journal::GetTokens();
157 if ( fields[0] == JournalCode ) {
158 if ( fields.size() == 1)
159 // No command, so just eat the journal line
160 return;
161
162 // Locate the menu item by name in the current popup menu or descendant.
163 auto found = [&]() -> std::pair<wxMenuItem *, wxMenu*> {
164 wxMenuItem *pItem = nullptr;
165 auto pMenu = theMenu;
166 for ( auto pField = fields.begin() + 1, endFields = fields.end();
167 pMenu && pField != endFields; ++pField ) {
168 auto &name = *pField;
169 const auto &list = pMenu->GetMenuItems();
170 const auto pred = [&name](auto &pItem){
171 return pItem->GetItemLabelText() == name; };
172 const auto begin = list.begin(), end = list.end(),
173 iter = std::find_if(begin, end, pred);
174
175 // Check existence and uniqueness
176 if ( auto next = iter;
177 end == next || end != std::find_if(++next, end, pred) )
178 return { nullptr, nullptr };
179
180 pItem = *iter;
181 if ( pField + 1 != endFields )
182 pMenu = pItem->GetSubMenu();
183 }
184 return { pItem, pMenu };
185 }();
186
187 if ( auto [pItem, pMenu] = found; pItem && pMenu ) {
188 // Don't really pop up the menu, which uses native event handling
189 // that we can't filter. Simulate an event instead.
190 // Require that some event is bound to the item, so it is
191 // handled, or else the journal fails replay.
192 wxCommandEvent event{ wxEVT_MENU, pItem->GetId() };
193 event.SetEventObject( pMenu );
194 if ( pMenu->ProcessEvent( event ) ) {
195 sHandledEvent = true;
196 return;
197 }
198 }
199 }
200
201 // Replay did not find all as expected
203}
204
205}
206
207void Handle::Popup( const BasicUI::WindowPlacement &window, const Point &pos )
208{
209 wxMenu *const pMenu = mpMenu;
210 if ( !pMenu )
211 return;
212
213 if ( auto pWindow = wxWidgetsWindowPlacement::GetParent( window ) ) {
214 sHandledEvent = false;
215
216 // Put the menu pointers where the event filter can find them
217 sMenuStack.push_back( FindDescendants( *pMenu ) );
218 auto cleanup = finally( []{ sMenuStack.pop_back(); } );
219
220 if ( Journal::IsReplaying() )
221 ReplayPopup( pMenu );
222 else
223 pWindow->PopupMenu( pMenu, { pos.x, pos.y } );
224
225 if ( !sHandledEvent )
226 // Menu popped but no command was selected. Record that.
228 }
229}
230
231}
Abstractions of menus and their items.
const TranslatableString name
Definition: Distortion.cpp:82
The output stream of the journal system.
Journal system's error status, command dictionary, initializers.
static TranslatableStrings names
Definition: TagsEditor.cpp:151
wxMenu * mpMenu
Definition: BasicMenu.h:39
void Popup(const BasicUI::WindowPlacement &window, const Point &pos={})
Display the menu at pos, invoke at most one action, then hide it.
Definition: BasicMenu.cpp:207
Subclasses may hold information such as a parent window pointer for a dialog.
Definition: BasicUI.h:29
Extend wxArrayString with move operations and construction and insertion fromstd::initializer_list.
bool ContainsMenu(const Menus &menus, void *pObj)
Definition: BasicMenu.cpp:52
std::optional< wxArrayStringEx > FindPathName(wxMenu &theMenu, int id)
Definition: BasicMenu.cpp:59
Journal::RegisteredInitializer initializer
Definition: BasicMenu.cpp:143
std::vector< wxWeakRef< wxMenu > > Menus
Definition: BasicMenu.cpp:31
void FailedEventSerialization()
Facilities for recording and playback of sequences of user interaction.
bool IsRecording()
bool GetError()
void Output(const wxString &string)
wxArrayStringEx GetTokens()
Definition: Journal.cpp:192
bool IsReplaying()
Definition: Journal.cpp:131
auto end(const Ptr< Type, BaseDeleter > &p)
Enables range-for.
Definition: PackedArray.h:159
auto begin(const Ptr< Type, BaseDeleter > &p)
Enables range-for.
Definition: PackedArray.h:150
Singleton object listens to global wxEvent stream.
Definition: BasicMenu.cpp:94
A pair of screen coordinates, x increasing rightward, y downward.
Definition: BasicUIPoint.h:18
static wxWindow * GetParent(const WindowPlacement &placement)
Retrieve the pointer to window, if placement is of this type; else null.