1 /**********************************************************************
2 
3   Audacity: A Digital Audio Editor
4 
5   MacroCommands.cpp
6 
7   Dominic Mazzoni
8   James Crook
9 
10 ********************************************************************//*!
11 
12 \class MacroCommands
13 \brief Maintains the list of commands for batch/macro
14 processing.  See also MacrosWindow and ApplyMacroDialog.
15 
16 *//*******************************************************************/
17 
18 #define wxLOG_COMPONENT "MacroCommands"
19 
20 
21 #include "BatchCommands.h"
22 
23 #include <wx/defs.h>
24 #include <wx/datetime.h>
25 #include <wx/dir.h>
26 #include <wx/log.h>
27 #include <wx/textfile.h>
28 #include <wx/time.h>
29 
30 #include "Project.h"
31 #include "ProjectAudioManager.h"
32 #include "ProjectHistory.h"
33 #include "ProjectSettings.h"
34 #include "ProjectWindow.h"
35 #include "commands/CommandManager.h"
36 #include "effects/EffectManager.h"
37 #include "effects/EffectUI.h"
38 #include "FileNames.h"
39 #include "Menus.h"
40 #include "PluginManager.h"
41 #include "Prefs.h"
42 #include "SelectFile.h"
43 #include "SelectUtilities.h"
44 #include "Shuttle.h"
45 #include "Track.h"
46 #include "UndoManager.h"
47 
48 #include "AllThemeResources.h"
49 
50 #include "widgets/AudacityMessageBox.h"
51 
52 #include "commands/CommandContext.h"
53 
MacroCommands(AudacityProject & project)54 MacroCommands::MacroCommands( AudacityProject &project )
55 : mProject{ project }
56 , mExporter{ project }
57 {
58    ResetMacro();
59 
60    auto names = GetNames();
61    auto defaults = GetNamesOfDefaultMacros();
62 
63    for( size_t i = 0;i<defaults.size();i++){
64       wxString name = defaults[i];
65       if ( ! make_iterator_range( names ).contains(name) ) {
66          AddMacro(name);
67          RestoreMacro(name);
68          WriteMacro(name);
69       }
70    }
71 }
72 
73 static const auto MP3Conversion = XO("MP3 Conversion");
74 static const auto FadeEnds      = XO("Fade Ends");
75 
GetNamesOfDefaultMacros()76 wxArrayStringEx MacroCommands::GetNamesOfDefaultMacros()
77 {
78    return {
79       MP3Conversion.Translation() ,
80       FadeEnds.Translation() ,
81    };
82 }
83 
RestoreMacro(const wxString & name)84 void MacroCommands::RestoreMacro(const wxString & name)
85 {
86 // TIDY-ME: Effects change their name with localisation.
87 // Commands (at least currently) don't.  Messy.
88    ResetMacro();
89    if (name == MP3Conversion.Translation() ){
90         AddToMacro( wxT("Normalize") );
91         AddToMacro( wxT("ExportMP3") );
92    } else if (name == FadeEnds.Translation() ){
93         AddToMacro( wxT("Select"), wxT("Start=\"0\" End=\"1\"") );
94         AddToMacro( wxT("FadeIn") );
95         AddToMacro( wxT("Select"), wxT("Start=\"0\" End=\"1\" RelativeTo=\"ProjectEnd\"") );
96         AddToMacro( wxT("FadeOut") );
97         AddToMacro( wxT("Select"), wxT("Start=\"0\" End=\"0\"") );
98    }
99 }
100 
GetCommand(int index)101 CommandID MacroCommands::GetCommand(int index)
102 {
103    if (index < 0 || index >= (int)mCommandMacro.size()) {
104       return wxT("");
105    }
106 
107    return mCommandMacro[index];
108 }
109 
GetParams(int index)110 wxString MacroCommands::GetParams(int index)
111 {
112    if (index < 0 || index >= (int)mParamsMacro.size()) {
113       return wxT("");
114    }
115 
116    return mParamsMacro[index];
117 }
118 
GetCount()119 int MacroCommands::GetCount()
120 {
121    return (int)mCommandMacro.size();
122 }
123 
ReadMacro(const wxString & macro,wxWindow * parent)124 wxString MacroCommands::ReadMacro(const wxString & macro, wxWindow *parent)
125 {
126    // Clear any previous macro
127    ResetMacro();
128 
129    // Build the filename
130    wxFileName name(FileNames::MacroDir(), macro, wxT("txt"));
131 
132    // But, ask the user for the real name if we're importing
133    if (parent) {
134       FilePath fn = SelectFile(FileNames::Operation::_None,
135          XO("Import Macro"),
136          wxEmptyString,
137          name.GetName(),
138          wxT("txt"),
139          { FileNames::TextFiles },
140          wxFD_OPEN | wxRESIZE_BORDER,
141          parent);
142 
143       // User canceled...
144       if (fn.empty()) {
145          return wxEmptyString;
146       }
147 
148       wxFileName check(fn);
149       check.SetPath(name.GetPath());
150       if (check.FileExists())
151       {
152          int id = AudacityMessageBox(
153             XO("Macro %s already exists. Would you like to replace it?").Format(check.GetName()),
154             XO("Import Macro"),
155             wxYES_NO);
156          if (id == wxNO) {
157             return wxEmptyString;
158          }
159       }
160 
161       name.Assign(fn);
162    }
163 
164    // Set the file name
165    wxTextFile tf(name.GetFullPath());
166 
167    // Open and check
168    tf.Open();
169    if (!tf.IsOpened()) {
170       // wxTextFile will display any errors
171       return wxEmptyString;
172    }
173 
174    // Load commands from the file
175    int lines = tf.GetLineCount();
176    if (lines > 0) {
177       for (int i = 0; i < lines; i++) {
178 
179          // Find the command name terminator...ignore line if not found
180          int splitAt = tf[i].Find(wxT(':'));
181          if (splitAt < 0) {
182             continue;
183          }
184 
185          // Parse and clean
186          wxString cmd = tf[i].Left(splitAt).Strip(wxString::both);
187          wxString parm = tf[i].Mid(splitAt + 1).Strip(wxString::trailing);
188 
189          // Add to lists
190          mCommandMacro.push_back(cmd);
191          mParamsMacro.push_back(parm);
192       }
193    }
194 
195    // Done with the file
196    tf.Close();
197 
198    // Write to macro directory if importing
199    if (parent) {
200       return WriteMacro(name.GetName());
201    }
202 
203    return name.GetName();
204 }
205 
WriteMacro(const wxString & macro,wxWindow * parent)206 wxString MacroCommands::WriteMacro(const wxString & macro, wxWindow *parent)
207 {
208    // Build the default filename
209    wxFileName name(FileNames::MacroDir(), macro, wxT("txt"));
210 
211    // But, ask the user for the real name if we're exporting
212    if (parent) {
213       FilePath fn = SelectFile(FileNames::Operation::_None,
214          XO("Export Macro"),
215          wxEmptyString,
216          name.GetName(),
217          wxT("txt"),
218          { FileNames::TextFiles },
219          wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER,
220          parent);
221 
222       // User canceled...
223       if (fn.empty()) {
224          return wxEmptyString;
225       }
226 
227       name.Assign(fn);
228    }
229 
230    // Set the file name
231    wxTextFile tf(name.GetFullPath());
232 
233    // Create the file (Create() doesn't leave the file open)
234    if (!tf.Exists()) {
235       tf.Create();
236    }
237 
238    // Open it
239    tf.Open();
240 
241    if (!tf.IsOpened()) {
242       // wxTextFile will display any errors
243       return wxEmptyString;
244    }
245 
246    // Start with a clean slate
247    tf.Clear();
248 
249    // Copy over the commands
250    int lines = mCommandMacro.size();
251    for (int i = 0; i < lines; i++) {
252       // using GET to serialize macro definition to a text file
253       tf.AddLine(mCommandMacro[i].GET() + wxT(":") + mParamsMacro[ i ]);
254    }
255 
256    // Write the macro
257    tf.Write();
258 
259    // Done with the file
260    tf.Close();
261 
262    return name.GetName();
263 }
264 
AddMacro(const wxString & macro)265 bool MacroCommands::AddMacro(const wxString & macro)
266 {
267    // Build the filename
268    wxFileName name(FileNames::MacroDir(), macro, wxT("txt"));
269 
270    // Set the file name
271    wxTextFile tf(name.GetFullPath());
272 
273    // Create it..Create will display errors
274    return tf.Create();
275 }
276 
DeleteMacro(const wxString & macro)277 bool MacroCommands::DeleteMacro(const wxString & macro)
278 {
279    // Build the filename
280    wxFileName name(FileNames::MacroDir(), macro, wxT("txt"));
281 
282    // Delete it...wxRemoveFile will display errors
283    auto result = wxRemoveFile(name.GetFullPath());
284 
285    // Delete any legacy chain that it shadowed
286    auto oldPath = wxFileName{ FileNames::LegacyChainDir(), macro, wxT("txt") };
287    wxRemoveFile(oldPath.GetFullPath()); // Don't care about this return value
288 
289    return result;
290 }
291 
RenameMacro(const wxString & oldmacro,const wxString & newmacro)292 bool MacroCommands::RenameMacro(const wxString & oldmacro, const wxString & newmacro)
293 {
294    // Build the filenames
295    wxFileName oname(FileNames::MacroDir(), oldmacro, wxT("txt"));
296    wxFileName nname(FileNames::MacroDir(), newmacro, wxT("txt"));
297 
298    // Rename it...wxRenameFile will display errors
299    return wxRenameFile(oname.GetFullPath(), nname.GetFullPath());
300 }
301 
302 // Gets all commands that are valid for this mode.
MacroCommandsCatalog(const AudacityProject * project)303 MacroCommandsCatalog::MacroCommandsCatalog( const AudacityProject *project )
304 {
305    if (!project)
306       return;
307 
308    Entries commands;
309 
310    PluginManager & pm = PluginManager::Get();
311    EffectManager & em = EffectManager::Get();
312    {
313       for (auto &plug
314            : pm.PluginsOfType(PluginTypeEffect|PluginTypeAudacityCommand)) {
315          auto command = em.GetCommandIdentifier(plug.GetID());
316          if (!command.empty())
317             commands.push_back( {
318                { command, plug.GetSymbol().Msgid() },
319                plug.GetPluginType() == PluginTypeEffect ?
320                   XO("Effect") : XO("Menu Command (With Parameters)")
321             } );
322       }
323    }
324 
325    auto &manager = CommandManager::Get( *project );
326    TranslatableStrings mLabels;
327    CommandIDs mNames;
328    std::vector<bool> vExcludeFromMacros;
329    mLabels.clear();
330    mNames.clear();
331    manager.GetAllCommandLabels(mLabels, vExcludeFromMacros, true);
332    manager.GetAllCommandNames(mNames, true);
333 
334    const bool english = wxGetLocale()->GetCanonicalName().StartsWith(wxT("en"));
335 
336    for(size_t i=0; i<mNames.size(); i++) {
337       if( !vExcludeFromMacros[i] ){
338          auto label = mLabels[i];
339          label.Strip();
340          bool suffix;
341          if (!english)
342             suffix = false;
343          else {
344             // We'll disambiguate if the squashed name is short and shorter than the internal name.
345             // Otherwise not.
346             // This means we won't have repetitive items like "Cut (Cut)"
347             // But we will show important disambiguation like "All (SelectAll)" and "By Date (SortByDate)"
348             // Disambiguation is no longer essential as the details box will show it.
349             // PRL:  I think this reasoning applies only when locale is English.
350             // For other locales, show the (CamelCaseCodeName) always.  Or, never?
351             wxString squashed = label.Translation();
352             squashed.Replace( " ", "" );
353 
354             // uh oh, using GET for dubious comparison of (lengths of)
355             // user-visible name and internal CommandID!
356             // and doing this only for English locale!
357             suffix = squashed.length() < wxMin( 18, mNames[i].GET().length());
358          }
359 
360          if( suffix )
361             // uh oh, using GET to expose CommandID to the user, as a
362             // disambiguating suffix on a name, but this is only ever done if
363             // the locale is English!
364             // PRL:  In case this logic does get fixed for other locales,
365             // localize even this punctuation format.  I'm told Chinese actually
366             // prefers slightly different parenthesis characters
367             label.Join( XO("(%s)").Format( mNames[i].GET() ), wxT(" ") );
368 
369          // Bug 2294.  The Close command pulls the rug out from under
370          // batch processing, because it destroys the project.
371          // So it is UNSAFE for scripting, and therefore excluded from
372          // the catalog.
373          if (mNames[i] == "Close")
374             continue;
375 
376          commands.push_back(
377             {
378                {
379                   mNames[i], // Internal name.
380                   label // User readable name
381                },
382                XO("Menu Command (No Parameters)")
383             }
384          );
385       }
386    }
387 
388    // Sort commands by their user-visible names.
389    // PRL:  What exactly should happen if first members of pairs are not unique?
390    // I'm not sure, but at least I can sort stably for a better defined result.
391    auto less =
392       [](const Entry &a, const Entry &b)
393          { return a.name.StrippedTranslation() <
394             b.name.StrippedTranslation(); };
395    std::stable_sort(commands.begin(), commands.end(), less);
396 
397    // Now uniquify by friendly name
398    auto equal =
399       [](const Entry &a, const Entry &b)
400          { return a.name.StrippedTranslation() ==
401             b.name.StrippedTranslation(); };
402    std::unique_copy(
403       commands.begin(), commands.end(), std::back_inserter(mCommands), equal);
404 }
405 
406 // binary search
ByFriendlyName(const TranslatableString & friendlyName) const407 auto MacroCommandsCatalog::ByFriendlyName( const TranslatableString &friendlyName ) const
408    -> Entries::const_iterator
409 {
410    const auto less = [](const Entry &entryA, const Entry &entryB)
411       { return entryA.name.StrippedTranslation() <
412          entryB.name.StrippedTranslation(); };
413    auto range = std::equal_range(
414       begin(), end(), Entry{ { {}, friendlyName }, {} }, less
415    );
416    if (range.first != range.second) {
417       wxASSERT_MSG( range.first + 1 == range.second,
418                     "Non-unique user-visible command name" );
419       return range.first;
420    }
421    else
422       return end();
423 }
424 
425 // linear search
ByCommandId(const CommandID & commandId) const426 auto MacroCommandsCatalog::ByCommandId( const CommandID &commandId ) const
427    -> Entries::const_iterator
428 {
429    // Maybe this too should have a uniqueness check?
430    return std::find_if( begin(), end(),
431       [&](const Entry &entry)
432          { return entry.name.Internal() == commandId; });
433 }
434 
GetCurrentParamsFor(const CommandID & command)435 wxString MacroCommands::GetCurrentParamsFor(const CommandID & command)
436 {
437    const PluginID & ID =
438       EffectManager::Get().GetEffectByIdentifier(command);
439    if (ID.empty())
440    {
441       return wxEmptyString;   // effect not found.
442    }
443 
444    return EffectManager::Get().GetEffectParameters(ID);
445 }
446 
PromptForParamsFor(const CommandID & command,const wxString & params,wxWindow & parent)447 wxString MacroCommands::PromptForParamsFor(
448    const CommandID & command, const wxString & params, wxWindow &parent)
449 {
450    const PluginID & ID =
451       EffectManager::Get().GetEffectByIdentifier(command);
452    if (ID.empty())
453    {
454       return wxEmptyString;   // effect not found
455    }
456 
457    wxString res = params;
458 
459    auto cleanup = EffectManager::Get().SetBatchProcessing(ID);
460 
461    if (EffectManager::Get().SetEffectParameters(ID, params))
462    {
463       if (EffectManager::Get().PromptUser(ID, EffectUI::DialogFactory, parent))
464       {
465          res = EffectManager::Get().GetEffectParameters(ID);
466       }
467    }
468 
469    return res;
470 }
471 
PromptForPresetFor(const CommandID & command,const wxString & params,wxWindow * parent)472 wxString MacroCommands::PromptForPresetFor(const CommandID & command, const wxString & params, wxWindow *parent)
473 {
474    const PluginID & ID =
475       EffectManager::Get().GetEffectByIdentifier(command);
476    if (ID.empty())
477    {
478       return wxEmptyString;   // effect not found.
479    }
480 
481    wxString preset = EffectManager::Get().GetPreset(ID, params, parent);
482 
483    // Preset will be empty if the user cancelled the dialog, so return the original
484    // parameter value.
485    if (preset.empty())
486    {
487       return params;
488    }
489 
490    return preset;
491 }
492 
493 /// DoAudacityCommand() takes a PluginID and executes the associated command.
494 ///
495 /// At the moment flags are used only to indicate whether to prompt for
496 /// parameters
DoAudacityCommand(const PluginID & ID,const CommandContext & context,unsigned flags)497 bool MacroCommands::DoAudacityCommand(
498    const PluginID & ID, const CommandContext & context, unsigned flags )
499 {
500    auto &project = context.project;
501    auto &window = ProjectWindow::Get( project );
502    const PluginDescriptor *plug = PluginManager::Get().GetPlugin(ID);
503    if (!plug)
504       return false;
505 
506    if (flags & EffectManager::kConfigured)
507    {
508       ProjectAudioManager::Get( project ).Stop();
509 //    SelectAllIfNone();
510    }
511 
512    EffectManager & em = EffectManager::Get();
513    bool success = em.DoAudacityCommand(ID,
514       context,
515       &window,
516       (flags & EffectManager::kConfigured) == 0);
517 
518    if (!success)
519       return false;
520 
521 /*
522    if (em.GetSkipStateFlag())
523       flags = flags | OnEffectFlags::kSkipState;
524 
525    if (!(flags & OnEffectFlags::kSkipState))
526    {
527       wxString shortDesc = em.GetCommandName(ID);
528       wxString longDesc = em.GetCommandDescription(ID);
529       PushState(longDesc, shortDesc);
530    }
531 */
532    window.RedrawProject();
533    return true;
534 }
535 
ApplyEffectCommand(const PluginID & ID,const TranslatableString & friendlyCommand,const CommandID & command,const wxString & params,const CommandContext & Context)536 bool MacroCommands::ApplyEffectCommand(
537    const PluginID & ID, const TranslatableString &friendlyCommand,
538    const CommandID & command, const wxString & params,
539    const CommandContext & Context)
540 {
541    static_cast<void>(command);//compiler food.
542 
543    //Possibly end processing here, if in batch-debug
544    if( ReportAndSkip(friendlyCommand, params))
545       return true;
546 
547    const PluginDescriptor *plug = PluginManager::Get().GetPlugin(ID);
548    if (!plug)
549       return false;
550 
551    AudacityProject *project = &mProject;
552 
553    // IF nothing selected, THEN select everything depending
554    // on preferences setting.
555    // (most effects require that you have something selected).
556    if( plug->GetPluginType() != PluginTypeAudacityCommand )
557    {
558       if( !SelectUtilities::SelectAllIfNoneAndAllowed( *project ) )
559       {
560          AudacityMessageBox(
561             // i18n-hint: %s will be replaced by the name of an action, such as "Remove Tracks".
562             XO("\"%s\" requires one or more tracks to be selected.").Format(friendlyCommand));
563          return false;
564       }
565    }
566 
567    bool res = false;
568 
569    auto cleanup = EffectManager::Get().SetBatchProcessing(ID);
570 
571    // transfer the parameters to the effect...
572    if (EffectManager::Get().SetEffectParameters(ID, params))
573    {
574       if( plug->GetPluginType() == PluginTypeAudacityCommand )
575          // and apply the effect...
576          res = DoAudacityCommand(ID,
577             Context,
578             EffectManager::kConfigured |
579             EffectManager::kSkipState |
580             EffectManager::kDontRepeatLast);
581       else
582          // and apply the effect...
583          res = EffectUI::DoEffect(ID,
584             Context,
585             EffectManager::kConfigured |
586             EffectManager::kSkipState |
587             EffectManager::kDontRepeatLast);
588    }
589 
590    return res;
591 }
592 
HandleTextualCommand(CommandManager & commandManager,const CommandID & Str,const CommandContext & context,CommandFlag flags,bool alwaysEnabled)593 bool MacroCommands::HandleTextualCommand( CommandManager &commandManager,
594    const CommandID & Str,
595    const CommandContext & context, CommandFlag flags, bool alwaysEnabled)
596 {
597    switch ( commandManager.HandleTextualCommand(
598       Str, context, flags, alwaysEnabled) ) {
599    case CommandManager::CommandSuccess:
600       return true;
601    case CommandManager::CommandFailure:
602       return false;
603    case CommandManager::CommandNotFound:
604    default:
605       break;
606    }
607 
608    // Not one of the singleton commands.
609    // We could/should try all the list-style commands.
610    // instead we only try the effects.
611    EffectManager & em = EffectManager::Get();
612    for (auto &plug : PluginManager::Get().PluginsOfType(PluginTypeEffect))
613       if (em.GetCommandIdentifier(plug.GetID()) == Str)
614          return EffectUI::DoEffect(
615             plug.GetID(), context,
616             EffectManager::kConfigured);
617 
618    return false;
619 }
620 
ApplyCommand(const TranslatableString & friendlyCommand,const CommandID & command,const wxString & params,CommandContext const * pContext)621 bool MacroCommands::ApplyCommand( const TranslatableString &friendlyCommand,
622    const CommandID & command, const wxString & params,
623    CommandContext const * pContext)
624 {
625    // Test for an effect.
626    const PluginID & ID =
627       EffectManager::Get().GetEffectByIdentifier( command );
628    if (!ID.empty())
629    {
630       if( pContext )
631          return ApplyEffectCommand(
632             ID, friendlyCommand, command, params, *pContext);
633       const CommandContext context( mProject );
634       return ApplyEffectCommand(
635          ID, friendlyCommand, command, params, context);
636    }
637 
638    AudacityProject *project = &mProject;
639    auto &manager = CommandManager::Get( *project );
640    if( pContext ){
641       if( HandleTextualCommand(
642          manager, command, *pContext, AlwaysEnabledFlag, true ) )
643          return true;
644       pContext->Status( wxString::Format(
645          _("Your batch command of %s was not recognized."), friendlyCommand.Translation() ));
646       return false;
647    }
648    else
649    {
650       const CommandContext context(  mProject );
651       if( HandleTextualCommand(
652          manager, command, context, AlwaysEnabledFlag, true ) )
653          return true;
654    }
655 
656    AudacityMessageBox(
657       XO("Your batch command of %s was not recognized.")
658          .Format( friendlyCommand ) );
659 
660    return false;
661 }
662 
ApplyCommandInBatchMode(const TranslatableString & friendlyCommand,const CommandID & command,const wxString & params,CommandContext const * pContext)663 bool MacroCommands::ApplyCommandInBatchMode(
664    const TranslatableString &friendlyCommand,
665    const CommandID & command, const wxString &params,
666    CommandContext const * pContext)
667 {
668    AudacityProject *project = &mProject;
669    auto &settings = ProjectSettings::Get( *project );
670    // Recalc flags and enable items that may have become enabled.
671    MenuManager::Get(*project).UpdateMenus(false);
672    // enter batch mode...
673    bool prevShowMode = settings.GetShowId3Dialog();
674    project->mBatchMode++;
675    auto cleanup = finally( [&] {
676       // exit batch mode...
677       settings.SetShowId3Dialog(prevShowMode);
678       project->mBatchMode--;
679    } );
680 
681    return ApplyCommand( friendlyCommand, command, params, pContext );
682 }
683 
684 static int MacroReentryCount = 0;
685 // ApplyMacro returns true on success, false otherwise.
686 // Any error reporting to the user in setting up the macro
687 // has already been done.
ApplyMacro(const MacroCommandsCatalog & catalog,const wxString & filename)688 bool MacroCommands::ApplyMacro(
689    const MacroCommandsCatalog &catalog, const wxString & filename)
690 {
691    // Check for reentrant ApplyMacro commands.
692    // We'll allow 1 level of reentry, but not more.
693    // And we treat ignoring deeper levels as a success.
694    if (MacroReentryCount > 1) {
695       return true;
696    }
697 
698    // Restore the reentry counter (to zero) when we exit.
699    auto cleanup1 = valueRestorer(MacroReentryCount);
700    MacroReentryCount++;
701 
702    AudacityProject *proj = &mProject;
703    bool res = false;
704 
705    // Only perform this group on initial entry.  They should not be done
706    // while recursing.
707    if (MacroReentryCount == 1) {
708       mFileName = filename;
709 
710       TranslatableString longDesc, shortDesc;
711       wxString name = gPrefs->Read(wxT("/Batch/ActiveMacro"), wxEmptyString);
712       if (name.empty()) {
713          /* i18n-hint: active verb in past tense */
714          longDesc = XO("Applied Macro");
715          shortDesc = XO("Apply Macro");
716       }
717       else {
718          /* i18n-hint: active verb in past tense */
719          longDesc = XO("Applied Macro '%s'").Format(name);
720          shortDesc = XO("Apply '%s'").Format(name);
721       }
722 
723       // Save the project state before making any changes.  It will be rolled
724       // back if an error occurs.
725       // It also causes any calls to ModifyState (such as by simple
726       // view-changing commands) to append changes to this state, not to the
727       // previous state in history.  See Bug 2076
728       if (proj) {
729          ProjectHistory::Get(*proj).PushState(longDesc, shortDesc);
730       }
731    }
732 
733    // Upon exit of the top level apply, roll back the state if an error occurs.
734    auto cleanup2 = finally([&, macroReentryCount = MacroReentryCount] {
735       if (macroReentryCount == 1 && !res && proj) {
736          // Be sure that exceptions do not escape this destructor
737          GuardedCall([&]{
738             // Macro failed or was cancelled; revert to the previous state
739             auto &history = ProjectHistory::Get(*proj);
740             history.RollbackState();
741             // The added undo state is now vacuous.  Remove it (Bug 2759)
742             auto &undoManager = UndoManager::Get(*proj);
743             undoManager.Undo(
744                [&]( const UndoStackElem &elem ){
745                   history.PopState( elem.state ); } );
746             undoManager.AbandonRedo();
747          });
748       }
749    });
750 
751    mAbort = false;
752 
753    // Is tracing enabled?
754    bool trace;
755    gPrefs->Read(wxT("/EnableMacroTracing"), &trace, false);
756 
757    // If so, then block most other messages while running the macro
758    wxLogLevel prevLevel = wxLog::GetComponentLevel("");
759    if (trace) {
760       wxLog::SetComponentLevel("",  wxLOG_FatalError);
761       wxLog::SetComponentLevel(wxLOG_COMPONENT,  wxLOG_Info);
762    }
763 
764    size_t i = 0;
765    for (; i < mCommandMacro.size(); i++) {
766       const auto &command = mCommandMacro[i];
767       auto iter = catalog.ByCommandId(command);
768       const auto friendly = (iter == catalog.end())
769          ?
770            // uh oh, using GET to expose an internal name to the user!
771            // in default of any better friendly name
772            Verbatim( command.GET() )
773          : iter->name.Msgid().Stripped();
774 
775       wxTimeSpan before;
776       if (trace) {
777          before = wxTimeSpan(0, 0, 0, wxGetUTCTimeMillis());
778       }
779 
780       bool success = ApplyCommandInBatchMode(friendly, command, mParamsMacro[i]);
781 
782       if (trace) {
783          auto after = wxTimeSpan(0, 0, 0, wxGetUTCTimeMillis());
784          wxLogMessage(wxT("Macro line #%ld took %s : %s:%s"),
785             i + 1,
786             (after - before).Format(wxT("%H:%M:%S.%l")),
787             command.GET(),
788             mParamsMacro[i]);
789       }
790 
791       if (!success || mAbort)
792          break;
793    }
794 
795    // Restore message level
796    if (trace) {
797       wxLog::SetComponentLevel("", prevLevel);
798    }
799 
800    res = (i == mCommandMacro.size());
801    if (!res)
802       return false;
803 
804    if (MacroReentryCount == 1) {
805       mFileName.Empty();
806 
807       if (proj)
808          ProjectHistory::Get(*proj).ModifyState(true);
809    }
810 
811    return true;
812 }
813 
814 // AbortBatch() allows a premature terminatation of a batch.
AbortBatch()815 void MacroCommands::AbortBatch()
816 {
817    mAbort = true;
818 }
819 
AddToMacro(const CommandID & command,int before)820 void MacroCommands::AddToMacro(const CommandID &command, int before)
821 {
822    AddToMacro(command, GetCurrentParamsFor(command), before);
823 }
824 
AddToMacro(const CommandID & command,const wxString & params,int before)825 void MacroCommands::AddToMacro(const CommandID &command, const wxString &params, int before)
826 {
827    if (before == -1) {
828       before = (int)mCommandMacro.size();
829    }
830 
831    mCommandMacro.insert(mCommandMacro.begin() + before, command);
832    mParamsMacro.insert(mParamsMacro.begin() + before, params);
833 }
834 
DeleteFromMacro(int index)835 void MacroCommands::DeleteFromMacro(int index)
836 {
837    if (index < 0 || index >= (int)mCommandMacro.size()) {
838       return;
839    }
840 
841    mCommandMacro.erase( mCommandMacro.begin() + index );
842    mParamsMacro.erase( mParamsMacro.begin() + index );
843 }
844 
ResetMacro()845 void MacroCommands::ResetMacro()
846 {
847    mCommandMacro.clear();
848    mParamsMacro.clear();
849 }
850 
851 // ReportAndSkip() is a diagnostic function that avoids actually
852 // applying the requested effect if in batch-debug mode.
ReportAndSkip(const TranslatableString & friendlyCommand,const wxString & params)853 bool MacroCommands::ReportAndSkip(
854    const TranslatableString & friendlyCommand, const wxString & params)
855 {
856    int bDebug;
857    gPrefs->Read(wxT("/Batch/Debug"), &bDebug, false);
858    if( bDebug == 0 )
859       return false;
860 
861    //TODO: Add a cancel button to these, and add the logic so that we can abort.
862    if( !params.empty() )
863    {
864       AudacityMessageBox(
865          XO("Apply %s with parameter(s)\n\n%s")
866             .Format( friendlyCommand, params ),
867          XO("Test Mode"));
868    }
869    else
870    {
871       AudacityMessageBox(
872          XO("Apply %s").Format( friendlyCommand ),
873          XO("Test Mode"));
874    }
875    return true;
876 }
877 
MigrateLegacyChains()878 void MacroCommands::MigrateLegacyChains()
879 {
880    static bool done = false;
881    if (!done) {
882       // Check once per session at most
883 
884       // Copy chain files from the old Chains into the new Macros directory,
885       // but only if like-named files are not already present in Macros.
886 
887       // Leave the old copies in place, in case a user wants to go back to
888       // an old Audacity version.  They will have their old chains intact, but
889       // won't have any edits they made to the copy that now lives in Macros
890       // which old Audacity will not read.
891 
892       const auto oldDir = FileNames::LegacyChainDir();
893       FilePaths files;
894       wxDir::GetAllFiles(oldDir, &files, wxT("*.txt"), wxDIR_FILES);
895 
896       // add a dummy path component to be overwritten by SetFullName
897       wxFileName newDir{ FileNames::MacroDir(), wxT("x") };
898 
899       for (const auto &file : files) {
900          auto name = wxFileName{file}.GetFullName();
901          newDir.SetFullName(name);
902          const auto newPath = newDir.GetFullPath();
903          if (!wxFileExists(newPath))
904             FileNames::DoCopyFile(file, newPath);
905       }
906       done = true;
907    }
908    // To do:  use std::once
909 }
910 
GetNames()911 wxArrayString MacroCommands::GetNames()
912 {
913    MigrateLegacyChains();
914 
915    wxArrayString names;
916    FilePaths files;
917    wxDir::GetAllFiles(FileNames::MacroDir(), &files, wxT("*.txt"), wxDIR_FILES);
918    size_t i;
919 
920    wxFileName ff;
921    for (i = 0; i < files.size(); i++) {
922       ff = (files[i]);
923       names.push_back(ff.GetName());
924    }
925 
926    std::sort( names.begin(), names.end() );
927 
928    return names;
929 }
930 
IsFixed(const wxString & name)931 bool MacroCommands::IsFixed(const wxString & name)
932 {
933    auto defaults = GetNamesOfDefaultMacros();
934    if( make_iterator_range( defaults ).contains( name ) )
935       return true;
936    return false;
937 }
938 
Split(const wxString & str,wxString & command,wxString & param)939 void MacroCommands::Split(const wxString & str, wxString & command, wxString & param)
940 {
941    int splitAt;
942 
943    command.Empty();
944    param.Empty();
945 
946    if (str.empty()) {
947       return;
948    }
949 
950    splitAt = str.Find(wxT(':'));
951    if (splitAt < 0) {
952       return;
953    }
954 
955    command = str.Mid(0, splitAt);
956    param = str.Mid(splitAt + 1);
957 
958    return;
959 }
960 
Join(const wxString & command,const wxString & param)961 wxString MacroCommands::Join(const wxString & command, const wxString & param)
962 {
963    return command + wxT(": ") + param;
964 }
965