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 ¶ms,
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 ¶ms, 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