text: /**********************************************************************
  
    Sneedacity: A Digital Audio Editor
  
    ApplyMacroDialog.cpp
  
    Dominic Mazzoni
    James Crook
  
  *******************************************************************//*!
  
  \class ApplyMacroDialog
  \brief Shows progress in executing commands in MacroCommands.
  
  *//*******************************************************************/
  
  
  #include "BatchProcessDialog.h"
  
  #include <wx/setup.h> // for wxUSE_* macros
  
  #ifdef __WXMSW__
      #include  <wx/ownerdrw.h>
  #endif
  
  #include <wx/defs.h>
  #include <wx/checkbox.h>
  #include <wx/choice.h>
  #include <wx/intl.h>
  #include <wx/sizer.h>
  #include <wx/statbox.h>
  #include <wx/stattext.h>
  #include <wx/textctrl.h>
  #include <wx/listctrl.h>
  #include <wx/radiobut.h>
  #include <wx/button.h>
  #include <wx/imaglist.h>
  #include <wx/settings.h>
  
  #include "Clipboard.h"
  #include "ShuttleGui.h"
  #include "Menus.h"
  #include "Prefs.h"
  #include "Project.h"
  #include "ProjectFileManager.h"
  #include "ProjectHistory.h"
  #include "ProjectManager.h"
  #include "ProjectWindow.h"
  #include "SelectUtilities.h"
  #include "commands/CommandManager.h"
  #include "effects/Effect.h"
  #include "../images/Arrow.xpm"
  #include "../images/Empty9x16.xpm"
  #include "UndoManager.h"
  
  #include "AllThemeResources.h"
  
  #include "widgets/FileDialog/FileDialog.h"
  #include "FileNames.h"
  #include "import/Import.h"
  #include "widgets/ErrorDialog.h"
  #include "widgets/SneedacityMessageBox.h"
  #include "widgets/HelpSystem.h"
  
  #if wxUSE_ACCESSIBILITY
  #include "widgets/WindowAccessible.h"
  #endif
  
  #define MacrosPaletteTitle XO("Macros Palette")
  #define ManageMacrosTitle XO("Manage Macros")
  
  
  // Separate numerical range from the additional buttons
  // in the expanded view (which start at 10,000).
  #define MacrosListID       7001
  #define CommandsListID     7002
  #define ApplyToProjectID   7003
  #define ApplyToFilesID     7004
  #define ExpandID           7005
  #define ShrinkID           7006
  
  BEGIN_EVENT_TABLE(ApplyMacroDialog, wxDialogWrapper)
     EVT_BUTTON(ApplyToProjectID, ApplyMacroDialog::OnApplyToProject)
     EVT_BUTTON(ApplyToFilesID, ApplyMacroDialog::OnApplyToFiles)
     EVT_BUTTON(wxID_CANCEL, ApplyMacroDialog::OnCancel)
     EVT_BUTTON(wxID_CLOSE, ApplyMacroDialog::OnCancel)
     EVT_BUTTON(wxID_HELP, ApplyMacroDialog::OnHelp)
  END_EVENT_TABLE()
  
  ApplyMacroDialog::ApplyMacroDialog(
     wxWindow * parent, SneedacityProject &project, bool bInherited):
     wxDialogWrapper(parent, wxID_ANY, MacrosPaletteTitle,
              wxDefaultPosition, wxDefaultSize,
              wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
     , mMacroCommands{ project }
     , mProject{ project }
     , mCatalog( &project )
  {
     mAbort = false;
     mbExpanded = false;
     if( bInherited )
        return;
     SetLabel(MacrosPaletteTitle);          // Provide visual label
     SetName(MacrosPaletteTitle);           // Provide audible label
     Populate();
  
  }
  
  ApplyMacroDialog::~ApplyMacroDialog()
  {
  }
  
  void ApplyMacroDialog::Populate()
  {
     //------------------------- Main section --------------------
     ShuttleGui S(this, eIsCreating);
     PopulateOrExchange(S);
     // ----------------------- End of main section --------------
     // Get and validate the currently active macro
     mActiveMacro = gPrefs->Read(wxT("/Batch/ActiveMacro"), wxT(""));
     // Go populate the macros list.
     PopulateMacros();
  
     Layout();
     Fit();
     wxSize sz = GetSize();
     SetSizeHints( sz );
  
     // Size and place window
     SetSize(std::min(wxSystemSettings::GetMetric(wxSYS_SCREEN_X) * 3 / 4, sz.GetWidth()),
             std::min(wxSystemSettings::GetMetric(wxSYS_SCREEN_Y) * 4 / 5, 400));
  
     Center();
  
     // Set the column size for the macros list.
     sz = mMacros->GetClientSize();
     mMacros->SetColumnWidth(0, sz.x);
  }
  
  /// Defines the dialog and does data exchange with it.
  void ApplyMacroDialog::PopulateOrExchange(ShuttleGui &S)
  {
     /*i18n-hint: A macro is a sequence of commands that can be applied
        * to one or more audio files.*/
     S.StartStatic(XO("Select Macro"), 1);
     {
        mMacros = S.Id(MacrosListID).Prop(1)
           .Style(wxSUNKEN_BORDER | wxLC_REPORT | wxLC_HRULES | wxLC_VRULES |
               wxLC_SINGLE_SEL)
                // i18n-hint: This is the heading for a column in the edit macros dialog
                .AddListControlReportMode( { XO("Macro") } );
     }
     S.EndStatic();
  
     S.StartHorizontalLay(wxEXPAND, 0);
     {
        S.AddPrompt( XXO("Apply Macro to:") );
        wxButton* btn = S.Id(ApplyToProjectID)
           .Name(XO("Apply macro to project"))
           .AddButton(XXO("&Project"));
  #if wxUSE_ACCESSIBILITY
        // so that name can be set on a standard control
        btn->SetAccessible(safenew WindowAccessible(btn));
  #endif
  
        btn = S.Id(ApplyToFilesID)
           .Name(XO("Apply macro to files..."))
           .AddButton(XXO("&Files..."));
  #if wxUSE_ACCESSIBILITY
        // so that name can be set on a standard control
        btn->SetAccessible(safenew WindowAccessible(btn));
  #endif
     }
     S.EndHorizontalLay();
  
     S.StartHorizontalLay(wxEXPAND, 0);
     {
        /* i18n-hint: The Expand button makes the dialog bigger, with more in it */
        mResize = S.Id(ExpandID).AddButton(XXO("&Expand"));
        S.AddSpace( 10,10,1 );
        S.AddStandardButtons( eCloseButton | eHelpButton);
     }
     S.EndHorizontalLay();
  }
  
  /// This clears and updates the contents of mMacros, the list of macros.
  /// It has cut-and-paste code from PopulateList, and both should call 
  /// a shared function.
  void ApplyMacroDialog::PopulateMacros()
  {
     auto names = mMacroCommands.GetNames();
     int i;
  
     int topItem = mMacros->GetTopItem();
     mMacros->DeleteAllItems();
     for (i = 0; i < (int)names.size(); i++) {
        mMacros->InsertItem(i, names[i]);
     }
  
     int item = mMacros->FindItem(-1, mActiveMacro);
     bool bFound = item >=0;
     if (item == -1) {
        item = 0;
        mActiveMacro = mMacros->GetItemText(0);
     }
  
     // Select the name in the list...this will fire an event.
     mMacros->SetItemState(item, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
        wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED);
  
     if( 0 <= topItem && topItem < (int)mMacros->GetItemCount())
     {
        // Workaround for scrolling being windows only.
        // Try to scroll back to where we once were...
        mMacros->EnsureVisible( (int)mMacros->GetItemCount() -1 );
        mMacros->EnsureVisible( topItem );
        // And then make sure whatever is selected is still visible...
        if( bFound )
           mMacros->EnsureVisible( item );
     }
  }
  
  void ApplyMacroDialog::OnHelp(wxCommandEvent & WXUNUSED(event))
  {
     const auto &page = GetHelpPageName();
     HelpSystem::ShowHelp(this, page, true);
  }
  
  void ApplyMacroDialog::OnApplyToProject(wxCommandEvent & WXUNUSED(event))
  {
     long item = mMacros->GetNextItem(-1,
                                      wxLIST_NEXT_ALL,
                                      wxLIST_STATE_SELECTED);
  
     if (item == -1) {
        SneedacityMessageBox(XO("No macro selected"));
        return;
     }
     ApplyMacroToProject( item );
  }
  
  CommandID ApplyMacroDialog::MacroIdOfName( const wxString & MacroName )
  {
     wxString Temp = MacroName;
     Temp.Replace(" ","");
     Temp = wxString( "Macro_" ) + Temp;
     return Temp;
  }
  
  // Apply macro, given its ID.
  // Does nothing if not found, rather than returning an error.
  void ApplyMacroDialog::ApplyMacroToProject( const CommandID & MacroID, bool bHasGui )
  {
     for( int i=0;i<mMacros->GetItemCount();i++){
        wxString name = mMacros->GetItemText(i);
        if( MacroIdOfName( name ) == MacroID ){
           ApplyMacroToProject( i, bHasGui );
           return;
        }
     }
  }
  
  // Apply macro, given its number in the list.
  void ApplyMacroDialog::ApplyMacroToProject( int iMacro, bool bHasGui )
  {
     wxString name = mMacros->GetItemText(iMacro);
     if( name.empty() )
        return;
  
  #ifdef OPTIONAL_ACTIVITY_WINDOW
     wxDialogWrapper activityWin( this, wxID_ANY, GetTitle());
     activityWin.SetName();
     ShuttleGui S(&activityWin, eIsCreating);
  
     S.StartHorizontalLay(wxCENTER, false);
     {
        S.StartStatic( {}, false);   // deliberately not translated (!)
        {
           S.SetBorder(20);
           S.AddFixedText(XO("Applying '%s' to current project")
              .Format( name ) );
        }
        S.EndStatic();
     }
     S.EndHorizontalLay();
  
     activityWin.Layout();
     activityWin.Fit();
     activityWin.CenterOnScreen();
     // Avoid overlap with progress.
     int x,y;
     activityWin.GetPosition( &x, &y );
     activityWin.Move(wxMax(0,x-300), 0);
     activityWin.Show();
  
     // Without this the newly created dialog may not show completely.
     wxYield();
  #endif
  
     //Since we intend to keep this dialog open, there is no reason to hide it 
     //and then show it again.
     //if( bHasGui )
     //   Hide();
  
     gPrefs->Write(wxT("/Batch/ActiveMacro"), name);
     gPrefs->Flush();
  
     mMacroCommands.ReadMacro(name);
  
     // The disabler must get deleted before the EndModal() call.  Otherwise,
     // the menus on OSX will remain disabled.
     bool success;
     {
  #ifdef OPTIONAL_ACTIVITY_WINDOW
        wxWindowDisabler wd(&activityWin);
  #endif
        success = GuardedCall< bool >(
           [this]{ return mMacroCommands.ApplyMacro(mCatalog); } );
     }
  
     if( !bHasGui )
        return;
  
     Show();
     Raise();
  }
  
  void ApplyMacroDialog::OnApplyToFiles(wxCommandEvent & WXUNUSED(event))
  {
     long item = mMacros->GetNextItem(-1,
                                      wxLIST_NEXT_ALL,
                                      wxLIST_STATE_SELECTED);
     if (item == -1) {
        SneedacityMessageBox( XO("No macro selected") );
        return;
     }
  
     wxString name = mMacros->GetItemText(item);
     gPrefs->Write(wxT("/Batch/ActiveMacro"), name);
     gPrefs->Flush();
  
     SneedacityProject *project = &mProject;
     if (!TrackList::Get( *project ).empty()) {
        SneedacityMessageBox(
           XO("Please save and close the current project first.") );
        return;
     }
  
     // This insures that we start with an empty and temporary project
     ProjectFileManager::Get(*project).CloseProject();
     ProjectFileManager::Get(*project).OpenProject();
  
     auto prompt =  XO("Select file(s) for batch processing...");
  
     const auto fileTypes = Importer::Get().GetFileTypes();
  
     auto path = FileNames::FindDefaultPath(FileNames::Operation::Open);
     FileDialogWrapper dlog(this,
        prompt,
        path,
        wxT(""),
        fileTypes,
        wxFD_OPEN | wxFD_MULTIPLE | wxRESIZE_BORDER);
  
     dlog.SetFilterIndex( Importer::SelectDefaultOpenType( fileTypes ) );
     if (dlog.ShowModal() != wxID_OK) {
        Raise();
        return;
     }
     Raise();
     
     wxArrayString files;
     dlog.GetPaths(files);
  
     files.Sort();
  
     wxDialogWrapper activityWin(this, wxID_ANY, Verbatim( GetTitle() ),
        wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER
        );
     activityWin.SetName();
     ShuttleGui S(&activityWin, eIsCreating);
  
     wxListCtrl * fileList = NULL;
  
     S.StartVerticalLay(1);
     {
        S.StartStatic(XO("Applying..."), 1);
        {
           auto imageList = std::make_unique<wxImageList>(9, 16);
           imageList->Add(wxIcon(empty9x16_xpm));
           imageList->Add(wxIcon(arrow_xpm));
  
           fileList = S.Id(CommandsListID)
              .Style(wxSUNKEN_BORDER | wxLC_REPORT | wxLC_HRULES | wxLC_VRULES |
                  wxLC_SINGLE_SEL)
              .AddListControlReportMode( { XO("File") } );
           // AssignImageList takes ownership
           fileList->AssignImageList(imageList.release(), wxIMAGE_LIST_SMALL);
        }
        S.EndStatic();
  
        S.StartHorizontalLay(wxCENTER, 0);
        {
           S.Id(wxID_CANCEL).AddButton(XXO("&Cancel"));
        }
        S.EndHorizontalLay();
     }
     S.EndVerticalLay();
  
     int i;
     for (i = 0; i < (int)files.size(); i++ ) {
        fileList->InsertItem(i, files[i], i == 0);
     }
  
     // Set the column size for the files list.
     fileList->SetColumnWidth(0, wxLIST_AUTOSIZE);
  
     int width = wxMin( fileList->GetColumnWidth(0), 1000);
     wxSize sz = fileList->GetClientSize();
     if (sz.GetWidth() < width ) {
        sz.SetWidth(width);
        if (sz.GetHeight() < width *0.7)
           sz.SetHeight(width * 0.7);
        fileList->SetInitialSize(sz);
     }
  
     activityWin.Layout();
     activityWin.Fit();
     activityWin.CenterOnScreen();
     // Avoid overlap with progress.
     int x,y;
     activityWin.GetPosition( &x, &y );
     activityWin.Move(wxMax(0,x-300), 0);
     activityWin.Show();
  
     // Without this the newly created dialog may not show completely.
     wxYield();
     // We could avoid hiding, but there are many dialogs on screen,
     // and hiding this one temporarily has some advantages.
     Hide();
  
     mMacroCommands.ReadMacro(name); 
     {
        // Move global clipboard contents aside temporarily
        Clipboard tempClipboard;
        auto &globalClipboard = Clipboard::Get();
        globalClipboard.Swap(tempClipboard);
        auto cleanup = finally([&]{
           globalClipboard.Swap(tempClipboard);
        });
  
        wxWindowDisabler wd(&activityWin);
        for (i = 0; i < (int)files.size(); i++) {
           if (i > 0) {
              //Clear the arrow in previous item.
              fileList->SetItemImage(i - 1, 0, 0);
           }
           fileList->SetItemImage(i, 1, 1);
           fileList->EnsureVisible(i);
  
           auto success = GuardedCall< bool >([&] {
              ProjectFileManager::Get(*project).Import(files[i]);
              ProjectWindow::Get(*project).ZoomAfterImport(nullptr);
              SelectUtilities::DoSelectAll(*project);
              if (!mMacroCommands.ApplyMacro(mCatalog))
                 return false;
  
              if (!activityWin.IsShown() || mAbort)
                 return false;
  
              return true;
           });
  
           // Ensure project is completely reset
           ProjectManager::Get(*project).ResetProjectToEmpty();
           // Bug2567:
           // Must also destroy the clipboard, to be sure sample blocks are
           // all freed and their ids can be reused safely in the next pass
           globalClipboard.Clear();
  
           if (!success)
              break;
        }
     }
  
     Show();
     Raise();
  }
  
  void ApplyMacroDialog::OnCancel(wxCommandEvent & WXUNUSED(event))
  {
     Hide();
  }
  
  /////////////////////////////////////////////////////////////////////
  #include <wx/textdlg.h>
  #include "BatchCommandDialog.h"
  
  enum {
     AddButtonID = 10000,
     RemoveButtonID,
     RenameButtonID,
     RestoreButtonID,
     ImportButtonID,
     ExportButtonID,
     SaveButtonID,
  
     DefaultsButtonID,
  
     InsertButtonID,
     EditButtonID,
     DeleteButtonID,
     UpButtonID,
     DownButtonID,
  
  // MacrosListID             7005
  // CommandsListID,       7002
  // Re-Use IDs from ApplyMacroDialog.
     ApplyToProjectButtonID = ApplyToProjectID,
     ApplyToFilesButtonID = ApplyToFilesID,
  };
  
  BEGIN_EVENT_TABLE(MacrosWindow, ApplyMacroDialog)
     EVT_LIST_ITEM_SELECTED(MacrosListID, MacrosWindow::OnMacroSelected)
     EVT_LIST_ITEM_SELECTED(CommandsListID, MacrosWindow::OnListSelected)
     EVT_LIST_BEGIN_LABEL_EDIT(MacrosListID, MacrosWindow::OnMacrosBeginEdit)
     EVT_LIST_END_LABEL_EDIT(MacrosListID, MacrosWindow::OnMacrosEndEdit)
     EVT_BUTTON(AddButtonID, MacrosWindow::OnAdd)
     EVT_BUTTON(RemoveButtonID, MacrosWindow::OnRemove)
     EVT_BUTTON(RenameButtonID, MacrosWindow::OnRename)
     EVT_BUTTON(RestoreButtonID, MacrosWindow::OnRestore)
     EVT_BUTTON(ImportButtonID, MacrosWindow::OnImport)
     EVT_BUTTON(ExportButtonID, MacrosWindow::OnExport)
     EVT_BUTTON(SaveButtonID, MacrosWindow::OnSave)
  
     EVT_BUTTON(ExpandID, MacrosWindow::OnExpand)
     EVT_BUTTON(ShrinkID, MacrosWindow::OnShrink)
     EVT_SIZE(MacrosWindow::OnSize)
  
     EVT_LIST_ITEM_ACTIVATED(CommandsListID, MacrosWindow::OnCommandActivated)
     EVT_BUTTON(InsertButtonID, MacrosWindow::OnInsert)
     EVT_BUTTON(EditButtonID, MacrosWindow::OnEditCommandParams)
     EVT_BUTTON(DeleteButtonID, MacrosWindow::OnDelete)
     EVT_BUTTON(UpButtonID, MacrosWindow::OnUp)
     EVT_BUTTON(DownButtonID, MacrosWindow::OnDown)
  
     EVT_BUTTON(wxID_OK, MacrosWindow::OnOK)
     EVT_BUTTON(wxID_CANCEL, MacrosWindow::OnCancel)
     EVT_BUTTON(wxID_CLOSE, MacrosWindow::OnCancel)
  
     EVT_KEY_DOWN(MacrosWindow::OnKeyDown)
  END_EVENT_TABLE()
  
  enum {
     ItemNumberColumn,
     ActionColumn,
     ParamsColumn,
  };
  
  /// Constructor
  MacrosWindow::MacrosWindow(
     wxWindow * parent, SneedacityProject &project, bool bExpanded):
     ApplyMacroDialog(parent, project, true)
     , mProject{ project }
  {
     mbExpanded = bExpanded;
     auto Title = WindowTitle();
     SetLabel( Title );   // Provide visual label
     SetName(  Title );   // Provide audible label
     SetTitle( Title );
  
     mChanged = false;
     mSelectedCommand = 0;
  
     if( mbExpanded )
        Populate();
     else
        ApplyMacroDialog::Populate();
  }
  
  MacrosWindow::~MacrosWindow()
  {
  }
  
  /// Creates the dialog and its contents.
  void MacrosWindow::Populate()
  {
     //------------------------- Main section --------------------
     ShuttleGui S(this, eIsCreating);
     PopulateOrExchange(S);
     // ----------------------- End of main section --------------
  
     // Get and validate the currently active macro
     mActiveMacro = gPrefs->Read(wxT("/Batch/ActiveMacro"), wxT(""));
     // Go populate the macros list.
     PopulateMacros();
  
     // We have a bare list.  We need to add columns and content.
     PopulateList();
  
     // Layout and set minimum size of window
     Layout();
     Fit();
     SetSizeHints(GetSize());
  
     // Size and place window
     SetSize(std::min(wxSystemSettings::GetMetric(wxSYS_SCREEN_X) * 3 / 4, 800),
             std::min(wxSystemSettings::GetMetric(wxSYS_SCREEN_Y) * 4 / 5, 400));
     Center();
  
     // Set the column size for the macros list.
     wxSize sz = mMacros->GetClientSize();
     mMacros->SetColumnWidth(0, sz.x);
  
     // Size columns properly
     FitColumns();
  }
  
  /// Defines the dialog and does data exchange with it.
  void MacrosWindow::PopulateOrExchange(ShuttleGui & S)
  {
     S.StartHorizontalLay(wxEXPAND, 1);
     {
        S.StartStatic(XO("Select Macro"),0);
        {
           S.StartHorizontalLay(wxEXPAND,1);
           {
              mMacros = S.Id(MacrosListID).Prop(1)
                 .Style(wxSUNKEN_BORDER | wxLC_REPORT | wxLC_HRULES
                        | wxLC_SINGLE_SEL | wxLC_EDIT_LABELS)
                // i18n-hint: This is the heading for a column in the edit macros dialog
                .AddListControlReportMode( { XO("Macro") } );
              S.StartVerticalLay(wxALIGN_TOP, 0);
              {
                 S.Id(AddButtonID).AddButton(XXO("&New"), wxALIGN_LEFT);
                 mRemove = S.Id(RemoveButtonID).AddButton(XXO("Remo&ve"), wxALIGN_LEFT);
                 mRename = S.Id(RenameButtonID).AddButton(XXO("&Rename..."), wxALIGN_LEFT);
                 mRestore = S.Id(RestoreButtonID).AddButton(XXO("Re&store"), wxALIGN_LEFT);
                 mImport = S.Id(ImportButtonID).AddButton(XO("I&mport..."), wxALIGN_LEFT);
                 mExport = S.Id(ExportButtonID).AddButton(XO("E&xport..."), wxALIGN_LEFT);
              }
              S.EndVerticalLay();
           }
           S.EndHorizontalLay();
        }
        S.EndStatic();
  
        S.StartStatic(XO("Edit Steps"), true);
        {
           S.StartHorizontalLay(wxEXPAND,1);
           {
              mList = S.Id(CommandsListID)
                 .Style(wxSUNKEN_BORDER | wxLC_REPORT | wxLC_HRULES | wxLC_VRULES |
                     wxLC_SINGLE_SEL)
                 .AddListControlReportMode({
                    /* i18n-hint: This is the number of the command in the list */
                    { XO("Num"), wxLIST_FORMAT_RIGHT },
                    { XO("Command  "), wxLIST_FORMAT_RIGHT },
                    { XO("Parameters"), wxLIST_FORMAT_LEFT }
                  });
  
              S.StartVerticalLay(wxALIGN_TOP, 0);
              {
                 S.Id(InsertButtonID).AddButton(XXO("&Insert"), wxALIGN_LEFT);
                 S.Id(EditButtonID).AddButton(XXO("&Edit..."), wxALIGN_LEFT);
                 S.Id(DeleteButtonID).AddButton(XXO("De&lete"), wxALIGN_LEFT);
                 S.Id(UpButtonID).AddButton(XXO("Move &Up"), wxALIGN_LEFT);
                 S.Id(DownButtonID).AddButton(XXO("Move &Down"), wxALIGN_LEFT);
                 mSave = S.Id(SaveButtonID).AddButton(XO("&Save"), wxALIGN_LEFT);
                 mSave->Enable( mChanged );
              }
              S.EndVerticalLay();
           }
           S.EndHorizontalLay();
        }
        S.EndStatic();
     }
     S.EndHorizontalLay();
  
     S.StartHorizontalLay(wxEXPAND, 0);
     {  
        /* i18n-hint: The Shrink button makes the dialog smaller, with less in it */
        mResize = S.Id(ShrinkID).AddButton(XXO("Shrin&k"));
        // Using variable text just to get the positioning options.
        S.Prop(0).AddVariableText(
           XO("Apply Macro to:"), false, wxALL | wxALIGN_CENTRE_VERTICAL );
        wxButton* btn = S.Id(ApplyToProjectID)
           .Name(XO("Apply macro to project"))
           .AddButton(XXO("&Project"));
  #if wxUSE_ACCESSIBILITY
        // so that name can be set on a standard control
        btn->SetAccessible(safenew WindowAccessible(btn));
  #endif
  
        btn = S.Id(ApplyToFilesID)
           .Name(XO("Apply macro to files..."))
           .AddButton(XXO("&Files..."));
  #if wxUSE_ACCESSIBILITY
        // so that name can be set on a standard control
        btn->SetAccessible(safenew WindowAccessible(btn));
  #endif
        S.AddSpace( 10,10,1 );
        // Bug 2524 OK button does much the same as cancel, so remove it.
        // OnCancel prompts you if there has been a change.
        // OnOK saves without prompting.
        // That difference is too slight to merit a button, and with the OK
        // button, people might expect the dialog to apply the macro too.
        S.AddStandardButtons( /*eOkButton |*/ eCloseButton | eHelpButton);
     }
  
     S.EndHorizontalLay();
  
     
     return;
  }
  
  /// This clears and updates the contents of mList, the commands for the current macro.
  void MacrosWindow::PopulateList()
  {
     int topItem = mList->GetTopItem();
     mList->DeleteAllItems();
  
     for (int i = 0; i < mMacroCommands.GetCount(); i++) {
        AddItem(mMacroCommands.GetCommand(i),
                mMacroCommands.GetParams(i));
     }
     /*i18n-hint: This is the last item in a list.*/
     AddItem(_("- END -"), wxT(""));
  
     // Select the name in the list...this will fire an event.
     if (mSelectedCommand >= (int)mList->GetItemCount()) {
        mSelectedCommand = 0;
     }
     mList->SetItemState(mSelectedCommand,
        wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
        wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED);
     if( 0 <= topItem && topItem < (int)mList->GetItemCount())
     {
        // Workaround for scrolling being windows only.
        // Try to scroll back to where we once were...
        mList->EnsureVisible( (int)mList->GetItemCount() -1 );
        mList->EnsureVisible( topItem );
        // And then make sure whatever is selected is still visible...
        if (mSelectedCommand >= 0) {
           mList->EnsureVisible( mSelectedCommand );
        }
     }
  }
  
  /// Add one item into mList
  void MacrosWindow::AddItem(const CommandID &Action, const wxString &Params)
  {
     auto entry = mCatalog.ByCommandId(Action);
     auto friendlyName = entry != mCatalog.end()
        ? entry->name.StrippedTranslation()
        :
           // uh oh, using GET to expose an internal name to the user!
           // in default of any better friendly name
          Action.GET();
  
     int i = mList->GetItemCount();
  
     mList->InsertItem(i, wxString::Format(wxT(" %02i"), i + 1));
     mList->SetItem(i, ActionColumn, friendlyName );
     mList->SetItem(i, ParamsColumn, Params );
  }
  
  void MacrosWindow::UpdateMenus()
  {
     // OK even on mac, as dialog is modal.
     auto p = &mProject;
     MenuManager::Get(*p).RebuildMenuBar(*p);
  }
  
  void MacrosWindow::UpdateDisplay( bool bExpanded )
  {
     // If we failed to save changes, we abandon the attempt to 
     // change the expand/shrink state of the GUI.
     if( !SaveChanges() )
        return;
  
     mbExpanded = bExpanded;
  
     mChanged = false;
     // if we try to access the about to be destroyed mSave button 
     // inappropriately, we need to crash rather than (sometimes) silently 
     // succeed.
     mSave = nullptr;
  
     DestroyChildren();
     SetSizer( nullptr );
  
     mSelectedCommand = 0;
     SetMinSize( wxSize( 200,200 ));
  
     // Get and set position for optical stability.
     // Expanded and shrunk dialogs 'stay where they were'.
     // That's OK , and what we want, even if we exapnd off-screen.
     // We won't shrink to being off-screen, since the shrink button 
     // was clicked, so must have been on screen.
     wxPoint p = GetPosition( );
     if( mbExpanded )
        Populate();
     else
        ApplyMacroDialog::Populate();
     SetPosition( p );
     mResize->SetFocus();
  
     auto Title = WindowTitle();
     SetLabel( Title );         // Provide visual label
     SetName( Title );          // Provide audible label
     SetTitle( Title );
  }
  
  void MacrosWindow::OnExpand(wxCommandEvent &WXUNUSED(event))
  {  UpdateDisplay( true );}
  
  void MacrosWindow::OnShrink(wxCommandEvent &WXUNUSED(event))
  {  
     if( ChangeOK() )
        UpdateDisplay( false );
  }
  
  
  bool MacrosWindow::ChangeOK()
  {
     if (mChanged) {
        int id;
  
        auto title = XO("%s changed").Format( mActiveMacro );
        auto msg = XO("Do you want to save the changes?");
  
        id = SneedacityMessageBox(
           msg,
           title,
           wxYES_NO | wxCANCEL);
        if (id == wxCANCEL) {
           return false;
        }
  
        if (id == wxYES) {
           if (!mMacroCommands.WriteMacro(mActiveMacro)) {
              return false;
           }
        }
  
        mChanged = false;
        mSave->Enable( mChanged );
     }
  
     return true;
  }
  /// An item in the macros list has been selected.
  void MacrosWindow::OnMacroSelected(wxListEvent & event)
  {
     if (!ChangeOK()) {
        event.Veto();
        return;
     }
  
     int item = event.GetIndex();
  
     mActiveMacro = mMacros->GetItemText(item);
     ShowActiveMacro();
  }
  
  void MacrosWindow::ShowActiveMacro()
  {
     mMacroCommands.ReadMacro(mActiveMacro);
     if( !mbExpanded )
        return;
     
     if (mMacroCommands.IsFixed(mActiveMacro)) {
        mRemove->Disable();
        mRename->Disable();
        mRestore->Enable();
     }
     else {
        mRemove->Enable();
        mRename->Enable();
        mRestore->Disable();
     }
  
     PopulateList();
  }
  
  /// An item in the macros list has been selected.
  void MacrosWindow::OnListSelected(wxListEvent & WXUNUSED(event))
  {
     FitColumns();
  }
  
  /// The window has been resized.
  void MacrosWindow::OnSize(wxSizeEvent & WXUNUSED(event))
  {
     // Refresh the layout and re-fit the columns.
     Layout();
     if( !mbExpanded )
        return;
     FitColumns();
  }
  
  void MacrosWindow::FitColumns()
  {
  
  #if defined(__WXMAC__)
     // wxMac uses a hard coded width of 150 when wxLIST_AUTOSIZE_USEHEADER
     // is specified, so we calculate the width ourselves. This method may
     // work equally well on other platforms.
     for (size_t c = 0; c < mList->GetColumnCount(); c++) {
        wxListItem info;
        int width;
  
        mList->SetColumnWidth(c, wxLIST_AUTOSIZE);
        info.Clear();
        info.SetId(c);
        info.SetMask(wxLIST_MASK_TEXT | wxLIST_MASK_WIDTH);
        mList->GetColumn(c, info);
  
        mList->GetTextExtent(info.GetText(), &width, NULL);
        width += 2 * 4;    // 2 * kItemPadding - see listctrl_mac.cpp
        width += 16;       // kIconWidth - see listctrl_mac.cpp
  
        mList->SetColumnWidth(c, wxMax(width, mList->GetColumnWidth(c)));
     }
  
     // Looks strange, but it forces the horizontal scrollbar to get
     // drawn.  If not done, strange column sizing can occur if the
     // user attempts to resize the columns.
     mList->SetClientSize(mList->GetClientSize());
  #else
     mList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER);
     mList->SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER);
     mList->SetColumnWidth(2, wxLIST_AUTOSIZE);
  #endif
  
     int bestfit = mList->GetColumnWidth(2);
     int clientsize = mList->GetClientSize().GetWidth();
     int col0 = mList->GetColumnWidth(0);
     int col1 = mList->GetColumnWidth(1);
     bestfit = (bestfit > clientsize-col0-col1)? bestfit : clientsize-col0-col1;
     mList->SetColumnWidth(2, bestfit);
  
  }
  
  ///
  void MacrosWindow::OnMacrosBeginEdit(wxListEvent &event)
  {
     int itemNo = event.GetIndex();
  
     wxString macro = mMacros->GetItemText(itemNo);
  
     if (mMacroCommands.IsFixed(macro)) {
        wxBell();
        event.Veto();
     }
     if( mMacroBeingRenamed.IsEmpty())
        mMacroBeingRenamed = macro;
  }
  
  ///
  void MacrosWindow::OnMacrosEndEdit(wxListEvent &event)
  {
     if (event.IsEditCancelled()) {
        mMacroBeingRenamed = "";
        return;
     }
  
     if( mMacroBeingRenamed.IsEmpty())
        return;
  
     wxString newname = event.GetLabel();
  
     mMacroCommands.RenameMacro(mMacroBeingRenamed, newname);
     if( mMacroBeingRenamed == mActiveMacro )
        mActiveMacro = newname;
     mMacroBeingRenamed="";
     PopulateMacros();
     UpdateMenus();   
     event.Veto();
  }
  
  ///
  void MacrosWindow::OnAdd(wxCommandEvent & WXUNUSED(event))
  {
     // Similar to Bug 2284 we may need to save a changed macro.
     if (!ChangeOK()) {
        return;
     }
  
     while (true) {
        SneedacityTextEntryDialog d(this,
           XO("Enter name of new macro"),
           XO("Name of new macro"));
        d.SetName(d.GetTitle());
        wxString name;
  
        if (d.ShowModal() == wxID_CANCEL) {
           Raise();
           return;
        }
        Raise();
  
        name = d.GetValue().Strip(wxString::both);
  
        if (name.length() == 0) {
           SneedacityMessageBox(
              XO("Name must not be blank"),
              WindowTitle(),
              wxOK | wxICON_ERROR,
              this);
           continue;
        }
  
        if (name.Contains(wxFILE_SEP_PATH) ||
            name.Contains(wxFILE_SEP_PATH_UNIX)) {
           SneedacityMessageBox(
              /*i18n-hint: The %c will be replaced with 'forbidden characters', like '/' and '\'.*/
              XO("Names may not contain '%c' and '%c'")
                 .Format(wxFILE_SEP_PATH, wxFILE_SEP_PATH_UNIX),
              WindowTitle(),
              wxOK | wxICON_ERROR,
              this);
           continue;
        }
  
        mMacroCommands.AddMacro(name);
  
        mActiveMacro = name;
  
        PopulateMacros();
        UpdateMenus();
  
        break;
     }
  }
  
  ///
  void MacrosWindow::OnRemove(wxCommandEvent & WXUNUSED(event))
  {
     long item = mMacros->GetNextItem(-1,
                                      wxLIST_NEXT_ALL,
                                      wxLIST_STATE_SELECTED);
     if (item == -1) {
        return;
     }
  
     wxString name = mMacros->GetItemText(item);
     SneedacityMessageDialog m(
        this,
        /*i18n-hint: %s will be replaced by the name of a file.*/
        XO("Are you sure you want to delete %s?").Format( name ),
        Verbatim( GetTitle() ),
        wxYES_NO | wxICON_QUESTION );
     if (m.ShowModal() == wxID_NO) {
        Raise();
        return;
     }
     Raise();
  
     mMacroCommands.DeleteMacro(name);
  
     item++;
     if (item >= (mMacros->GetItemCount() - 1) && item >= 0) {
        item--;
     }
  
     // Bug 2284.  The macro we have just removed might have been 
     // changed.  Since we've just deleted the macro, we should
     // forget about that change.
     mChanged = false;
     mSave->Enable( mChanged );
     mActiveMacro = mMacros->GetItemText(item);
  
     PopulateMacros();
     UpdateMenus();
  }
  
  ///
  void MacrosWindow::OnRename(wxCommandEvent & WXUNUSED(event))
  {
     long item = mMacros->GetNextItem(-1,
                                      wxLIST_NEXT_ALL,
                                      wxLIST_STATE_SELECTED);
     if (item == -1) {
        return;
     }
  
     mMacros->EditLabel(item);
     UpdateMenus();
  }
  
  /// Reset a built in macro.
  void MacrosWindow::OnRestore(wxCommandEvent & WXUNUSED(event))
  {
     mMacroCommands.RestoreMacro(mActiveMacro);
  
     mChanged = true;
     mSave->Enable( mChanged );
  
     PopulateList();
  }
  
  ///
  void MacrosWindow::OnImport(wxCommandEvent & WXUNUSED(event))
  {
     if (!ChangeOK()) {
        return;
     }
  
     long item = mMacros->GetNextItem(-1,
                                      wxLIST_NEXT_ALL,
                                      wxLIST_STATE_SELECTED);
     if (item == -1) {
        return;
     }
  
     wxString name = mMacros->GetItemText(item);
  
     name = mMacroCommands.ReadMacro({}, this);
     if (name == wxEmptyString) {
        return;
     }
  
     mActiveMacro = name;
  
     PopulateMacros();
     UpdateMenus();
  }
  
  ///
  void MacrosWindow::OnExport(wxCommandEvent & WXUNUSED(event))
  {
     long item = mMacros->GetNextItem(-1,
                                      wxLIST_NEXT_ALL,
                                      wxLIST_STATE_SELECTED);
     if (item == -1) {
        return;
     }
  
     mMacroCommands.WriteMacro(mMacros->GetItemText(item), this);
  }
  
  void MacrosWindow::OnSave(wxCommandEvent & WXUNUSED(event))
  {
     SaveChanges();
  }
  
  
  /// An item in the list has been selected.
  /// Bring up a dialog to allow its parameters to be edited.
  void MacrosWindow::OnCommandActivated(wxListEvent & WXUNUSED(event))
  {
     wxCommandEvent dummy;
     OnEditCommandParams( dummy );
  }
  
  ///
  void MacrosWindow::OnInsert(wxCommandEvent & WXUNUSED(event))
  {
     long item = mList->GetNextItem(-1,
                                    wxLIST_NEXT_ALL,
                                    wxLIST_STATE_SELECTED);
     if (item == -1) {
        item = mList->GetItemCount()-1;
     }
     InsertCommandAt( item );
  }
  
  void MacrosWindow::InsertCommandAt(int item)
  {
     if (item == -1) {
        return;
     }
  
     MacroCommandDialog d(this, wxID_ANY, mProject);
  
     if (!d.ShowModal()) {
        Raise();
        return;
     }
     Raise();
  
     if(!d.mSelectedCommand.empty())
     {
        mMacroCommands.AddToMacro(d.mSelectedCommand,
                                  d.mSelectedParameters,
                                  item);
        mChanged = true;
        mSave->Enable( mChanged );
  
        mSelectedCommand = item + 1;
        PopulateList();
     }
  
  }
  
  void MacrosWindow::OnEditCommandParams(wxCommandEvent & WXUNUSED(event))
  {
     int item = mList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED );
  
     // LAST command in list is END.
     // If nothing selected, add at END.
     // If END selected, add at END.
     // When adding at end we use InsertCommandAt, so that a new command
     // can be chosen.
     int lastItem = mList->GetItemCount()-1;
     if( (item<0) || (item+1) == mList->GetItemCount() )
     {
        InsertCommandAt( lastItem );
        return;
     }
  
     // Just edit the parameters, and not the command.
     auto command = mMacroCommands.GetCommand(item);
     wxString params  = mMacroCommands.GetParams(item);
     wxString oldParams = params;
  
     params = MacroCommands::PromptForParamsFor(command, params, *this).Trim();
     Raise();
  
     if (oldParams == params)
        return; // They did not actually make any changes..
  
     mMacroCommands.DeleteFromMacro(item);
     mMacroCommands.AddToMacro(command,
        params,
        item);
  
     mChanged = true;
     mSave->Enable( mChanged );
  
     mSelectedCommand = item;
     PopulateList();
  }
  
  ///
  void MacrosWindow::OnDelete(wxCommandEvent & WXUNUSED(event))
  {
     long item = mList->GetNextItem(-1,
                                    wxLIST_NEXT_ALL,
                                    wxLIST_STATE_SELECTED);
     if (item == -1 || item + 1 == mList->GetItemCount()) {
        return;
     }
  
     mMacroCommands.DeleteFromMacro(item);
  
     mChanged = true;
     mSave->Enable( mChanged );
  
     if (item >= (mList->GetItemCount() - 2) && item >= 0) {
        item--;
     }
     mSelectedCommand = item;
     PopulateList();
  }
  
  ///
  void MacrosWindow::OnUp(wxCommandEvent & WXUNUSED(event))
  {
     long item = mList->GetNextItem(-1,
                                    wxLIST_NEXT_ALL,
                                    wxLIST_STATE_SELECTED);
     if (item == -1 || item == 0 || item + 1 == mList->GetItemCount()) {
        return;
     }
  
     mMacroCommands.AddToMacro(mMacroCommands.GetCommand(item),
                               mMacroCommands.GetParams(item),
                               item - 1);
     mMacroCommands.DeleteFromMacro(item + 1);
  
     mChanged = true;
     mSave->Enable( mChanged );
  
     mSelectedCommand = item - 1;
     PopulateList();
  }
  
  ///
  void MacrosWindow::OnDown(wxCommandEvent & WXUNUSED(event))
  {
     long item = mList->GetNextItem(-1,
                                    wxLIST_NEXT_ALL,
                                    wxLIST_STATE_SELECTED);
     if (item == -1 || item + 2 >= mList->GetItemCount()) {
        return;
     }
  
     mMacroCommands.AddToMacro(mMacroCommands.GetCommand(item),
                               mMacroCommands.GetParams(item),
                               item + 2);
     mMacroCommands.DeleteFromMacro(item);
  
     mChanged = true;
     mSave->Enable( mChanged );
  
     mSelectedCommand = item + 1;
     PopulateList();
  }
  
  void MacrosWindow::OnApplyToProject(wxCommandEvent & event)
  {
     if( !SaveChanges() )
        return;
     ApplyMacroDialog::OnApplyToProject( event );
  }
  
  void MacrosWindow::OnApplyToFiles(wxCommandEvent & event)
  {
     if( !SaveChanges() )
        return;
     ApplyMacroDialog::OnApplyToFiles( event );
  }
  
  bool MacrosWindow::SaveChanges(){
     gPrefs->Write(wxT("/Batch/ActiveMacro"), mActiveMacro);
     gPrefs->Flush();
  
     if (mChanged) {
        if (!mMacroCommands.WriteMacro(mActiveMacro)) {
           return false;
        }
     }
  
     mChanged = false;
     if( mSave )
        mSave->Enable( mChanged );
  
     return true;
  }
  
  /// Send changed values back to Prefs, and update Sneedacity.
  void MacrosWindow::OnOK(wxCommandEvent & WXUNUSED(event))
  {
     if( !SaveChanges() )
        return;
     Hide();
     //EndModal(true);
  }
  
  ///
  void MacrosWindow::OnCancel(wxCommandEvent &WXUNUSED(event))
  {
     bool bWasChanged = mChanged;
     if (!ChangeOK()) {
        return;
     }
     // If we've rejected a change, we need to restore the display
     // of the active macro.
     // That's because next time we open this dialog we want to see the 
     // unedited macro.
     if( bWasChanged )
        ShowActiveMacro();
     Hide();
  }
  
  ///
  void MacrosWindow::OnKeyDown(wxKeyEvent &event)
  {
     if (event.GetKeyCode() == WXK_DELETE) {
        wxLogDebug(wxT("wxKeyEvent"));
     }
  
     event.Skip();
  }
  
  TranslatableString MacrosWindow::WindowTitle() const
  {
     return mbExpanded ? ManageMacrosTitle : MacrosPaletteTitle;
  }
  
  // PrefsListener implementation
  void MacrosWindow::UpdatePrefs()
  {
     UpdateDisplay(mbExpanded);
  }
  