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