text: /**********************************************************************
  
    AudMonkey: A Digital Audio Editor
  
    ExportMultiple.cpp
  
    Dominic Mazzoni
  
  *******************************************************************//**
  
  \class ExportMultipleDialog
  \brief Presents a dialog box allowing the user to export multiple files
    either by exporting each track as a separate file, or by
    exporting each label as a separate file.
  
  *//********************************************************************/
  
  
  #include "ExportMultiple.h"
  
  #include <wx/defs.h>
  #include <wx/button.h>
  #include <wx/checkbox.h>
  #include <wx/choice.h>
  #include <wx/dialog.h>
  #include <wx/dirdlg.h>
  #include <wx/event.h>
  #include <wx/listbase.h>
  #include <wx/filefn.h>
  #include <wx/filename.h>
  #include <wx/intl.h>
  #include <wx/log.h>
  #include <wx/radiobut.h>
  #include <wx/simplebook.h>
  #include <wx/sizer.h>
  #include <wx/statbox.h>
  #include <wx/stattext.h>
  #include <wx/textctrl.h>
  #include <wx/textdlg.h>
  
  #include "FileNames.h"
  #include "../LabelTrack.h"
  #include "../Project.h"
  #include "../ProjectSettings.h"
  #include "../ProjectWindow.h"
  #include "Prefs.h"
  #include "../SelectionState.h"
  #include "../ShuttleGui.h"
  #include "../Tags.h"
  #include "../WaveTrack.h"
  #include "../widgets/HelpSystem.h"
  #include "../widgets/AudMonkeyMessageBox.h"
  #include "../widgets/AudMonkeyTextEntryDialog.h"
  #include "../widgets/ProgressDialog.h"
  
  
  namespace {
  /** \brief A private class used to store the information needed to do an
      * export.
      *
      * We create a set of these during the interactive phase of the export
      * cycle, then use them when the actual exports are done. */
     class ExportKit
     {
     public:
        Tags filetags; /**< The set of metadata to use for the export */
        wxFileNameWrapper destfile; /**< The file to export to */
        double t0;           /**< Start time for the export */
        double t1;           /**< End time for the export */
        unsigned channels;   /**< Number of channels for ExportMultipleByTrack */
     };  // end of ExportKit declaration
     /* we are going to want an set of these kits, and don't know how many until
      * runtime. I would dearly like to use a std::vector, but it seems that
      * this isn't done anywhere else in AudMonkey, presumably for a reason?, so
      * I'm stuck with wxArrays, which are much harder, as well as non-standard.
      */
  }
  
  /* define our dynamic array of export settings */
  
  enum {
     FormatID = 10001,
     OptionsID,
     DirID,
     CreateID,
     ChooseID,
     LabelID,
     FirstID,
     FirstFileNameID,
     TrackID,
     ByNameAndNumberID,
     ByNameID,
     ByNumberID,
     PrefixID,
     OverwriteID
  };
  
  //
  // ExportMultipleDialog methods
  //
  
  BEGIN_EVENT_TABLE(ExportMultipleDialog, wxDialogWrapper)
     EVT_CHOICE(FormatID, ExportMultipleDialog::OnFormat)
  //   EVT_BUTTON(OptionsID, ExportMultipleDialog::OnOptions)
     EVT_BUTTON(CreateID, ExportMultipleDialog::OnCreate)
     EVT_BUTTON(ChooseID, ExportMultipleDialog::OnChoose)
     EVT_BUTTON(wxID_OK, ExportMultipleDialog::OnExport)
     EVT_BUTTON(wxID_CANCEL, ExportMultipleDialog::OnCancel)
     EVT_BUTTON(wxID_HELP, ExportMultipleDialog::OnHelp)
     EVT_RADIOBUTTON(LabelID, ExportMultipleDialog::OnLabel)
     EVT_RADIOBUTTON(TrackID, ExportMultipleDialog::OnTrack)
     EVT_RADIOBUTTON(ByNameAndNumberID, ExportMultipleDialog::OnByName)
     EVT_RADIOBUTTON(ByNameID, ExportMultipleDialog::OnByName)
     EVT_RADIOBUTTON(ByNumberID, ExportMultipleDialog::OnByNumber)
     EVT_CHECKBOX(FirstID, ExportMultipleDialog::OnFirst)
     EVT_TEXT(FirstFileNameID, ExportMultipleDialog::OnFirstFileName)
     EVT_TEXT(PrefixID, ExportMultipleDialog::OnPrefix)
  END_EVENT_TABLE()
  
  BEGIN_EVENT_TABLE(SuccessDialog, wxDialogWrapper)
     EVT_LIST_KEY_DOWN(wxID_ANY, SuccessDialog::OnKeyDown)
     EVT_LIST_ITEM_ACTIVATED(wxID_ANY, SuccessDialog::OnItemActivated) // happens when <enter> is pressed with list item having focus
  END_EVENT_TABLE()
  
  BEGIN_EVENT_TABLE(MouseEvtHandler, wxEvtHandler)
     EVT_LEFT_DCLICK(MouseEvtHandler::OnMouse)
  END_EVENT_TABLE()
  
  ExportMultipleDialog::ExportMultipleDialog(AudMonkeyProject *project)
  : wxDialogWrapper( &GetProjectFrame( *project ),
     wxID_ANY, XO("Export Multiple") )
  , mExporter{ *project }
  , mSelectionState{ SelectionState::Get( *project ) }
  {
     SetName();
  
     mProject = project;
     mTracks = &TrackList::Get( *project );
     // Construct an array of non-owning pointers
     for (const auto &plugin : mExporter.GetPlugins())
        mPlugins.push_back(plugin.get());
  
     this->CountTracksAndLabels();
  
     mBook = NULL;
  
     ShuttleGui S(this, eIsCreatingFromPrefs);
  
     // Creating some of the widgets cause events to fire
     // and we don't want that until after we're completely
     // created.  (Observed on Windows)
     mInitialized = false;
     PopulateOrExchange(S);
     mInitialized = true;
  
     Layout();
     Fit();
     SetMinSize(GetSize());
     Center();
  
     EnableControls();
  }
  
  ExportMultipleDialog::~ExportMultipleDialog()
  {
  }
  
  void ExportMultipleDialog::CountTracksAndLabels()
  {
     bool anySolo = !(( mTracks->Any<const WaveTrack>() + &WaveTrack::GetSolo ).empty());
  
     mNumWaveTracks =
        (mTracks->Leaders< const WaveTrack >() -
        (anySolo ? &WaveTrack::GetNotSolo : &WaveTrack::GetMute)).size();
  
     // only the first label track
     mLabels = *mTracks->Any< const LabelTrack >().begin();
     mNumLabels = mLabels ? mLabels->GetNumLabels() : 0;
  }
  
  int ExportMultipleDialog::ShowModal()
  {
     // Cannot export if all audio tracks are muted.
     if (mNumWaveTracks == 0)
     {
        ::AudMonkeyMessageBox(
           XO("All audio is muted."),
           XO("Cannot Export Multiple"),
           wxOK | wxCENTRE,
           this);
        return wxID_CANCEL;
     }
  
     if ((mNumWaveTracks < 1) && (mNumLabels < 1))
     {
        ::AudMonkeyMessageBox(
           XO(
  "You have no unmuted Audio Tracks and no applicable \
  \nlabels, so you cannot export to separate audio files."),
           XO("Cannot Export Multiple"),
           wxOK | wxCENTRE,
           this);
        return wxID_CANCEL;
     }
  
     bool bHasLabels = (mNumLabels > 0);
     bool bHasTracks = (mNumWaveTracks > 0);
  
     mLabel->Enable(bHasLabels && bHasTracks);
     mTrack->Enable(bHasTracks);
  
     // If you have 2 or more tracks, then it is export by tracks.
     // If you have no labels, then it is export by tracks.
     // Otherwise it is export by labels, by default.
     bool bPreferByLabels = bHasLabels && (mNumWaveTracks < 2);
     mLabel->SetValue(bPreferByLabels);
     mTrack->SetValue(!bPreferByLabels);
  
     EnableControls();
  
     return wxDialogWrapper::ShowModal();
  }
  
  void ExportMultipleDialog::PopulateOrExchange(ShuttleGui& S)
  {
     wxString name = mProject->GetProjectName();
     wxString defaultFormat = gPrefs->Read(wxT("/Export/Format"), wxT("WAV"));
  
     TranslatableStrings visibleFormats;
     wxArrayStringEx formats;
     mPluginIndex = -1;
     mFilterIndex = 0;
  
     {
        int i = -1;
        for (const auto &pPlugin : mPlugins)
        {
           ++i;
           for (int j = 0; j < pPlugin->GetFormatCount(); j++)
           {
              auto format = mPlugins[i]->GetDescription(j);
              visibleFormats.push_back( format );
              // use MSGID of description as a value too, written into config file
              // This is questionable.  A change in the msgid can make the
              // preference stored in old config files inapplicable
              formats.push_back( format.MSGID().GET() );
              if (mPlugins[i]->GetFormat(j) == defaultFormat) {
                 mPluginIndex = i;
                 mSubFormatIndex = j;
              }
              if (mPluginIndex == -1) mFilterIndex++;
           }
        }
     }
  
  
     // Bug 1304: Set the default file path.  It's used if none stored in config.
     auto DefaultPath = FileNames::FindDefaultPath(FileNames::Operation::Export);
  
     if (mPluginIndex == -1)
     {
        mPluginIndex = 0;
        mFilterIndex = 0;
        mSubFormatIndex = 0;
     }
  
     S.SetBorder(5);
     S.StartHorizontalLay(wxEXPAND, true);
     {
        S.SetBorder(5);
        S.StartStatic(XO("Export files to:"), true);
        {
           S.StartMultiColumn(4, true);
           {
              mDir = S.Id(DirID)
                 .AddTextBox(XXO("Folder:"),
                             DefaultPath,
                             64);
              S.Id(ChooseID).AddButton(XXO("Choose..."));
              S.Id(CreateID).AddButton(XXO("Create"));
  
              mFormat = S.Id(FormatID)
                 .TieChoice( XXO("Format:"),
                 {
                    wxT("/Export/MultipleFormat"),
                    {
                       ByColumns,
                       visibleFormats,
                       formats
                    },
                    mFilterIndex
                 }
              );
              S.AddVariableText( {}, false);
              S.AddVariableText( {}, false);
  
              S.AddPrompt(XXO("Options:"));
  
              mBook = S.Id(OptionsID)
                 .Style(wxBORDER_STATIC)
                 .StartSimplebook();
              if (S.GetMode() == eIsCreating)
              {
                 for (const auto &pPlugin : mPlugins)
                 {
                    for (int j = 0; j < pPlugin->GetFormatCount(); j++)
                    {
                       // Name of simple book page is not displayed
                       S.StartNotebookPage( {} );
                       pPlugin->OptionsCreate(S, j);
                       S.EndNotebookPage();
                    }
                 }
                 mBook->ChangeSelection(mFormat->GetSelection());
              }
              S.EndSimplebook();
              S.AddVariableText( {}, false);
              S.AddVariableText( {}, false);
           }
           S.EndMultiColumn();
        }
        S.EndStatic();
     }
     S.EndHorizontalLay();
  
     S.StartHorizontalLay(wxEXPAND, false);
     {
        S.SetBorder(5);
        S.StartStatic(XO("Split files based on:"), 1);
        {
           // Row 1
           S.SetBorder(1);
  
           // Bug 2692: Place button group in panel so tabbing will work and,
           // on the Mac, VoiceOver will announce as radio buttons.
           S.StartPanel();
           {
              mTrack = S.Id(TrackID)
                 .AddRadioButton(XXO("Tracks"));
  
              // Row 2
              S.SetBorder(1);
              mLabel = S.Id(LabelID)
                 .AddRadioButtonToGroup(XXO("Labels"));
           }
           S.EndPanel();
  
           S.SetBorder(3);
           S.StartMultiColumn(2, wxEXPAND);
           S.SetStretchyCol(1);
           {
              // Row 3 (indented)
              S.AddVariableText(Verbatim("   "), false);
              mFirst = S.Id(FirstID)
                 .AddCheckBox(XXO("Include audio before first label"), false);
  
              // Row 4
              S.AddVariableText( {}, false);
              S.StartMultiColumn(2, wxEXPAND);
              S.SetStretchyCol(1);
              {
                 mFirstFileLabel =
                    S.AddVariableText(XO("First file name:"), false);
                 mFirstFileName = S.Id(FirstFileNameID)
                    .Prop(1)
                    .Name(XO("First file name"))
                    .TieTextBox( {},
                                name,
                                30);
              }
              S.EndMultiColumn();
           }
           S.EndMultiColumn();
  
           S.SetBorder(3);
        }
        S.EndStatic();
  
        S.SetBorder(5);
        S.StartStatic(XO("Name files:"), 1);
        {
           S.SetBorder(2);
  
           // Bug 2692: Place button group in panel so tabbing will work and,
           // on the Mac, VoiceOver will announce as radio buttons.
           S.StartPanel();
           {
              S.StartRadioButtonGroup({
                 wxT("/Export/TrackNameWithOrWithoutNumbers"),
                 {
                    { wxT("labelTrack"), XXO("Using Label/Track Name") },
                    { wxT("numberBefore"), XXO("Numbering before Label/Track Name") },
                    { wxT("numberAfter"), XXO("Numbering after File name prefix") },
                 },
                 0 // labelTrack
              });
              {
                 mByName = S.Id(ByNameID).TieRadioButton();
  
                 mByNumberAndName = S.Id(ByNameAndNumberID).TieRadioButton();
  
                 mByNumber = S.Id(ByNumberID).TieRadioButton();
              }
              S.EndRadioButtonGroup();
           }
           S.EndPanel();
  
           S.StartMultiColumn(3, wxEXPAND);
           S.SetStretchyCol(2);
           {
              // Row 3 (indented)
              S.AddVariableText(Verbatim("   "), false);
              mPrefixLabel = S.AddVariableText(XO("File name prefix:"), false);
              mPrefix = S.Id(PrefixID)
                 .Name(XO("File name prefix"))
                 .TieTextBox( {},
                             name,
                             30);
           }
           S.EndMultiColumn();
        }
        S.EndStatic();
     }
     S.EndHorizontalLay();
  
     S.SetBorder(5);
     S.StartHorizontalLay(wxEXPAND, false);
     {
        mOverwrite = S.Id(OverwriteID).TieCheckBox(XXO("Overwrite existing files"),
                                                   {wxT("/Export/OverwriteExisting"),
                                                    false});
     }
     S.EndHorizontalLay();
  
     S.AddStandardButtons(eOkButton | eCancelButton | eHelpButton);
     mExport = (wxButton *)wxWindow::FindWindowById(wxID_OK, this);
     mExport->SetLabel(_("Export"));
  
  }
  
  void ExportMultipleDialog::EnableControls()
  {
     bool enable;
  
     if (!mInitialized) {
        return;
     }
  
     mFirst->Enable(mLabel->GetValue());
  
     enable =  mLabel->GetValue() &&
              (mByName->GetValue() || mByNumberAndName->GetValue()) &&
               mFirst->GetValue();
     mFirstFileLabel->Enable(enable);
     mFirstFileName->Enable(enable);
  
     enable = mByNumber->GetValue();
     mPrefixLabel->Enable(enable);
     mPrefix->Enable(enable);
  
     bool ok = true;
  
     if (mLabel->GetValue() && mFirst->GetValue() &&
         mFirstFileName->GetValue().empty() &&
         mPrefix->GetValue().empty())
        ok = false;
  
     if (mByNumber->GetValue() &&
         mPrefix->GetValue().empty())
        ok = false;
  
     mExport->Enable(ok);
  }
  
  void ExportMultipleDialog::OnFormat(wxCommandEvent& WXUNUSED(event))
  {
     mBook->ChangeSelection(mFormat->GetSelection());
  
     EnableControls();
  }
  
  void ExportMultipleDialog::OnOptions(wxCommandEvent& WXUNUSED(event))
  {
     const int sel = mFormat->GetSelection();
     if (sel != wxNOT_FOUND)
     {
       size_t c = 0;
       int i = -1;
       for (const auto &pPlugin : mPlugins)
       {
         ++i;
         for (int j = 0; j < pPlugin->GetFormatCount(); j++)
         {
           if ((size_t)sel == c)
           {
              mPluginIndex = i;
              mSubFormatIndex = j;
           }
           c++;
         }
       }
     }
     mPlugins[mPluginIndex]->DisplayOptions(this,mSubFormatIndex);
  }
  
  void ExportMultipleDialog::OnCreate(wxCommandEvent& WXUNUSED(event))
  {
     wxFileName fn;
  
     fn.AssignDir(mDir->GetValue());
  
     bool ok = fn.Mkdir(0777, wxPATH_MKDIR_FULL);
  
     if (!ok) {
        // Mkdir will produce an error dialog
        return;
     }
  
     ::AudMonkeyMessageBox(
        XO("\"%s\" successfully created.").Format( fn.GetPath() ),
        XO("Export Multiple"),
        wxOK | wxCENTRE,
        this);
  }
  
  void ExportMultipleDialog::OnChoose(wxCommandEvent& WXUNUSED(event))
  {
     wxDirDialogWrapper dlog(this,
        XO("Choose a location to save the exported files"),
        mDir->GetValue());
     dlog.ShowModal();
     if (!dlog.GetPath().empty())
        mDir->SetValue(dlog.GetPath());
  }
  
  void ExportMultipleDialog::OnLabel(wxCommandEvent& WXUNUSED(event))
  {
     EnableControls();
  }
  
  void ExportMultipleDialog::OnFirst(wxCommandEvent& WXUNUSED(event))
  {
     EnableControls();
  }
  
  void ExportMultipleDialog::OnFirstFileName(wxCommandEvent& WXUNUSED(event))
  {
     EnableControls();
  }
  
  void ExportMultipleDialog::OnTrack(wxCommandEvent& WXUNUSED(event))
  {
     EnableControls();
  }
  
  void ExportMultipleDialog::OnByName(wxCommandEvent& WXUNUSED(event))
  {
     EnableControls();
  }
  
  void ExportMultipleDialog::OnByNumber(wxCommandEvent& WXUNUSED(event))
  {
     EnableControls();
  }
  
  void ExportMultipleDialog::OnPrefix(wxCommandEvent& WXUNUSED(event))
  {
     EnableControls();
  }
  
  void ExportMultipleDialog::OnCancel(wxCommandEvent& WXUNUSED(event))
  {
     EndModal(0);
  }
  
  void ExportMultipleDialog::OnHelp(wxCommandEvent& WXUNUSED(event))
  {
     HelpSystem::ShowHelp(this, L"Export_Multiple", true);
  }
  
  void ExportMultipleDialog::OnExport(wxCommandEvent& WXUNUSED(event))
  {
     ShuttleGui S(this, eIsSavingToPrefs);
     PopulateOrExchange(S);
  
     gPrefs->Flush();
  
     FileNames::UpdateDefaultPath(FileNames::Operation::Export, mDir->GetValue());
  
     // Make sure the output directory is in good shape
     if (!DirOk()) {
        return;
     }
  
     mFilterIndex = mFormat->GetSelection();
     if (mFilterIndex != wxNOT_FOUND)
     {
        size_t c = 0;
        int i = -1;
        for (const auto &pPlugin : mPlugins)
        {
           ++i;
           for (int j = 0; j < pPlugin->GetFormatCount(); j++, c++)
           {
              if ((size_t)mFilterIndex == c)
              {  // this is the selected format. Store the plug-in and sub-format
                 // needed to achieve it.
                 mPluginIndex = i;
                 mSubFormatIndex = j;
                 mBook->GetPage(mFilterIndex)->TransferDataFromWindow();
              }
           }
        }
     }
  
  //   bool overwrite = mOverwrite->GetValue();
     ProgressResult ok = ProgressResult::Failed;
     mExported.clear();
  
     // Give 'em the result
     auto cleanup = finally( [&]
     {
        auto msg = (ok == ProgressResult::Success
           ? XO("Successfully exported the following %lld file(s).")
           : ok == ProgressResult::Failed
              ? XO("Something went wrong after exporting the following %lld file(s).")
              : ok == ProgressResult::Cancelled
                 ? XO("Export canceled after exporting the following %lld file(s).")
                 : ok == ProgressResult::Stopped
                    ? XO("Export stopped after exporting the following %lld file(s).")
                    : XO("Something went really wrong after exporting the following %lld file(s).")
           ).Format((long long) mExported.size());
  
        wxString FileList;
        for (size_t i = 0; i < mExported.size(); i++) {
           FileList += mExported[i];
           FileList += '\n';
        }
  
        // TODO: give some warning dialog first, when only some files exported
        // successfully.
  
        GuardedCall( [&] {
           // This results dialog is a child of this dialog.
           HelpSystem::ShowInfoDialog( this,
                                      XO("Export Multiple"),
                                      msg,
                                      FileList,
                                      450,400);
        } );
     } );
  
     if (mLabel->GetValue()) {
        ok = ExportMultipleByLabel(mByName->GetValue() || mByNumberAndName->GetValue(),
                                   mPrefix->GetValue(),
                                   mByNumberAndName->GetValue());
     }
     else {
        ok = ExportMultipleByTrack(mByName->GetValue() || mByNumberAndName->GetValue(),
                                   mPrefix->GetValue(),
                                   mByNumberAndName->GetValue());
     }
  
     if (ok == ProgressResult::Success || ok == ProgressResult::Stopped) {
        EndModal(1);
     }
  }
  
  bool ExportMultipleDialog::DirOk()
  {
     wxFileName fn;
  
     fn.AssignDir(mDir->GetValue());
  
     if (fn.DirExists()) {
        return true;
     }
  
     auto prompt = XO("\"%s\" doesn't exist.\n\nWould you like to create it?")
        .Format( fn.GetFullPath() );
  
     int action = AudMonkeyMessageBox(
        prompt,
        XO("Warning"),
        wxYES_NO | wxICON_EXCLAMATION);
     if (action != wxYES) {
        return false;
     }
  
     return fn.Mkdir(0777, wxPATH_MKDIR_FULL);
  }
  
  static unsigned GetNumExportChannels( const TrackList &tracks )
  {
     /* counters for tracks panned different places */
     int numLeft = 0;
     int numRight = 0;
     //int numMono = 0;
     /* track iteration kit */
  
     bool anySolo = !(( tracks.Any<const WaveTrack>() + &WaveTrack::GetSolo ).empty());
  
     // Want only unmuted wave tracks.
     for (auto tr :
           tracks.Any< const WaveTrack >() -
        (anySolo ? &WaveTrack::GetNotSolo : &WaveTrack::GetMute)
     ) {
        // Found a left channel
        if (tr->GetChannel() == Track::LeftChannel) {
           numLeft++;
        }
  
        // Found a right channel
        else if (tr->GetChannel() == Track::RightChannel) {
           numRight++;
        }
  
        // Found a mono channel, but it may be panned
        else if (tr->GetChannel() == Track::MonoChannel) {
           float pan = tr->GetPan();
  
           // Figure out what kind of channel it should be
           if (pan == -1.0) {   // panned hard left
              numLeft++;
           }
           else if (pan == 1.0) {  // panned hard right
              numRight++;
           }
           else if (pan == 0) { // panned dead center
              // numMono++;
           }
           else {   // panned somewhere else
              numLeft++;
              numRight++;
           }
        }
     }
  
     // if there is stereo content, report 2, else report 1
     if (numRight > 0 || numLeft > 0) {
        return 2;
     }
  
     return 1;
  }
  
  // TODO: JKC July2016: Merge labels/tracks duplicated export code.
  // TODO: JKC Apr2019: Doubly so merge these!  Too much duplication.
  ProgressResult ExportMultipleDialog::ExportMultipleByLabel(bool byName,
     const wxString &prefix, bool addNumber)
  {
     wxASSERT(mProject);
     int numFiles = mNumLabels;
     int l = 0;        // counter for files done
     std::vector<ExportKit> exportSettings; // dynamic array for settings.
     exportSettings.reserve(numFiles); // Allocate some guessed space to use.
  
     // Account for exporting before first label
     if( mFirst->GetValue() ) {
        l--;
        numFiles++;
     }
  
     // Figure out how many channels we should export.
     auto channels = GetNumExportChannels( *mTracks );
  
     FilePaths otherNames;  // keep track of file names we will use, so we
     // don't duplicate them
     ExportKit setting;   // the current batch of settings
     setting.destfile.SetPath(mDir->GetValue());
     setting.destfile.SetExt(mPlugins[mPluginIndex]->GetExtension(mSubFormatIndex));
     wxLogDebug(wxT("Plug-in index = %d, Sub-format = %d"), mPluginIndex, mSubFormatIndex);
     wxLogDebug(wxT("File extension is %s"), setting.destfile.GetExt());
     wxString name;    // used to hold file name whilst we mess with it
     wxString title;   // un-messed-with title of file for tagging with
  
     const LabelStruct *info = NULL;
     /* Examine all labels a first time, sort out all data but don't do any
      * exporting yet (so this run is quick but interactive) */
     while( l < mNumLabels ) {
  
        // Get file name and starting time
        if( l < 0 ) {
           // create wxFileName for output file
           name = (mFirstFileName->GetValue());
           setting.t0 = 0.0;
        } else {
           info = mLabels->GetLabel(l);
           name = (info->title);
           setting.t0 = info->selectedRegion.t0();
        }
  
        // Figure out the ending time
        if( info && !info->selectedRegion.isPoint() ) {
           setting.t1 = info->selectedRegion.t1();
        } else if( l < mNumLabels-1 ) {
           // Use start of next label as end
           const LabelStruct *info1 = mLabels->GetLabel(l+1);
           setting.t1 = info1->selectedRegion.t0();
        } else {
           setting.t1 = mTracks->GetEndTime();
        }
  
        if( name.empty() )
           name = _("untitled");
  
        // store title of label to use in tags
        title = name;
  
        // Numbering files...
        if( !byName ) {
           name.Printf(wxT("%s-%02d"), prefix, l+1);
        } else if( addNumber ) {
           // Following discussion with GA, always have 2 digits
           // for easy file-name sorting (on Windows)
           name.Prepend(wxString::Format(wxT("%02d-"), l+1));
        }
  
        // store sanitised and user checked name in object
        setting.destfile.SetName(MakeFileName(name));
        if( setting.destfile.GetName().empty() )
        {  // user cancelled dialogue, or deleted everything in field.
           // or maybe the label was empty??
           // So we ignore this one and keep going.
        }
        else
        {
           // FIXME: TRAP_ERR User could have given an illegal filename prefix.
           // in that case we should tell them, not fail silently.
           wxASSERT(setting.destfile.IsOk());     // burp if file name is broke
  
           // Make sure the (final) file name is unique within the set of exports
           FileNames::MakeNameUnique(otherNames, setting.destfile);
  
           /* do the metadata for this file */
           // copy project metadata to start with
           setting.filetags = Tags::Get( *mProject );
           setting.filetags.LoadDefaults();
           if (exportSettings.size()) {
              setting.filetags = exportSettings.back().filetags;
           }
           // over-ride with values
           setting.filetags.SetTag(TAG_TITLE, title);
           setting.filetags.SetTag(TAG_TRACK, l+1);
           // let the user have a crack at editing it, exit if cancelled
           auto &settings = ProjectSettings::Get( *mProject );
           bool bShowTagsDialog = settings.GetShowId3Dialog();
  
           bShowTagsDialog = bShowTagsDialog && mPlugins[mPluginIndex]->GetCanMetaData(mSubFormatIndex);
  
           if( bShowTagsDialog ){
              bool bCancelled = !setting.filetags.ShowEditDialog(
                 ProjectWindow::Find( mProject ),
                 XO("Edit Metadata Tags"), bShowTagsDialog);
              gPrefs->Read(wxT("/AudioFiles/ShowId3Dialog"), &bShowTagsDialog, true);
              settings.SetShowId3Dialog( bShowTagsDialog );
              if( bCancelled )
                 return ProgressResult::Cancelled;
           }
        }
  
        /* add the settings to the array of settings to be used for export */
        exportSettings.push_back(setting);
  
        l++;  // next label, count up one
     }
  
     auto ok = ProgressResult::Success;   // did it work?
     int count = 0; // count the number of successful runs
     ExportKit activeSetting;  // pointer to the settings in use for this export
     /* Go round again and do the exporting (so this run is slow but
      * non-interactive) */
     std::unique_ptr<ProgressDialog> pDialog;
     for (count = 0; count < numFiles; count++) {
        /* get the settings to use for the export from the array */
        activeSetting = exportSettings[count];
        // Bug 1440 fix.
        if( activeSetting.destfile.GetName().empty() )
           continue;
  
        // Export it
        ok = DoExport(pDialog, channels, activeSetting.destfile, false,
           activeSetting.t0, activeSetting.t1, activeSetting.filetags);
        if (ok == ProgressResult::Stopped) {
           AudMonkeyMessageDialog dlgMessage(
              nullptr,
              XO("Continue to export remaining files?"),
              XO("Export"),
              wxYES_NO | wxNO_DEFAULT | wxICON_WARNING);
           if (dlgMessage.ShowModal() != wxID_YES ) {
              // User decided not to continue - bail out!
              break;
           }
        }
        else if (ok != ProgressResult::Success) {
           break;
        }
     }
  
     return ok;
  }
  
  ProgressResult ExportMultipleDialog::ExportMultipleByTrack(bool byName,
     const wxString &prefix, bool addNumber)
  {
     wxASSERT(mProject);
     int l = 0;     // track counter
     auto ok = ProgressResult::Success;
     FilePaths otherNames;
     std::vector<ExportKit> exportSettings; // dynamic array we will use to store the
                                    // settings needed to do the exports with in
     exportSettings.reserve(mNumWaveTracks);   // Allocate some guessed space to use.
     ExportKit setting;   // the current batch of settings
     setting.destfile.SetPath(mDir->GetValue());
     setting.destfile.SetExt(mPlugins[mPluginIndex]->GetExtension(mSubFormatIndex));
  
     wxString name;    // used to hold file name whilst we mess with it
     wxString title;   // un-messed-with title of file for tagging with
  
     /* Remember which tracks were selected, and set them to deselected */
     SelectionStateChanger changer{ mSelectionState, *mTracks };
     for (auto tr : mTracks->Selected<WaveTrack>())
        tr->SetSelected(false);
  
     bool anySolo = !(( mTracks->Any<const WaveTrack>() + &WaveTrack::GetSolo ).empty());
  
     bool skipSilenceAtBeginning;
     gPrefs->Read(wxT("/AudioFiles/SkipSilenceAtBeginning"), &skipSilenceAtBeginning, false);
  
     /* Examine all tracks in turn, collecting export information */
     for (auto tr : mTracks->Leaders<WaveTrack>() -
        (anySolo ? &WaveTrack::GetNotSolo : &WaveTrack::GetMute)) {
  
        // Get the times for the track
        auto channels = TrackList::Channels(tr);
        setting.t0 = skipSilenceAtBeginning ? channels.min(&Track::GetStartTime) : 0;
        setting.t1 = channels.max( &Track::GetEndTime );
  
        // number of export channels?
        setting.channels = channels.size();
        if (setting.channels == 1 &&
            !(tr->GetChannel() == WaveTrack::MonoChannel &&
                    tr->GetPan() == 0.0))
           setting.channels = 2;
  
        // Get name and title
        title = tr->GetName();
        if( title.empty() )
           title = _("untitled");
  
        if (byName) {
           name = title;
           if (addNumber) {
              name.Prepend(
                 wxString::Format(wxT("%02d-"), l+1));
           }
        }
        else {
           name = (wxString::Format(wxT("%s-%02d"), prefix, l+1));
        }
  
        // store sanitised and user checked name in object
        setting.destfile.SetName(MakeFileName(name));
  
        if (setting.destfile.GetName().empty())
        {  // user cancelled dialogue, or deleted everything in field.
           // So we ignore this one and keep going.
        }
        else
        {
  
           // FIXME: TRAP_ERR User could have given an illegal track name.
           // in that case we should tell them, not fail silently.
           wxASSERT(setting.destfile.IsOk());     // burp if file name is broke
  
           // Make sure the (final) file name is unique within the set of exports
           FileNames::MakeNameUnique(otherNames, setting.destfile);
  
           /* do the metadata for this file */
           // copy project metadata to start with
           setting.filetags = Tags::Get( *mProject );
           setting.filetags.LoadDefaults();
           if (exportSettings.size()) {
              setting.filetags = exportSettings.back().filetags;
           }
           // over-ride with values
           setting.filetags.SetTag(TAG_TITLE, title);
           setting.filetags.SetTag(TAG_TRACK, l+1);
           // let the user have a crack at editing it, exit if cancelled
           auto &settings = ProjectSettings::Get( *mProject );
           bool bShowTagsDialog = settings.GetShowId3Dialog();
  
           bShowTagsDialog = bShowTagsDialog && mPlugins[mPluginIndex]->GetCanMetaData(mSubFormatIndex);
  
           if( bShowTagsDialog ){
              bool bCancelled = !setting.filetags.ShowEditDialog(
                 ProjectWindow::Find( mProject ),
                 XO("Edit Metadata Tags"), bShowTagsDialog);
              gPrefs->Read(wxT("/AudioFiles/ShowId3Dialog"), &bShowTagsDialog, true);
              settings.SetShowId3Dialog( bShowTagsDialog );
              if( bCancelled )
                 return ProgressResult::Cancelled;
           }
        }
        /* add the settings to the array of settings to be used for export */
        exportSettings.push_back(setting);
  
        l++;  // next track, count up one
     }
     // end of user-interactive data gathering loop, start of export processing
     // loop
     int count = 0; // count the number of successful runs
     ExportKit activeSetting;  // pointer to the settings in use for this export
     std::unique_ptr<ProgressDialog> pDialog;
  
     for (auto tr : mTracks->Leaders<WaveTrack>() -
        (anySolo ? &WaveTrack::GetNotSolo : &WaveTrack::GetMute)) {
  
        wxLogDebug( "Get setting %i", count );
        /* get the settings to use for the export from the array */
        activeSetting = exportSettings[count];
        if( activeSetting.destfile.GetName().empty() ){
           count++;
           continue;
        }
  
        /* Select the track */
        SelectionStateChanger changer2{ mSelectionState, *mTracks };
        const auto range = TrackList::Channels(tr);
        for (auto channel : range)
           channel->SetSelected(true);
  
        // Export the data. "channels" are per track.
        ok = DoExport(pDialog,
           activeSetting.channels, activeSetting.destfile, true,
           activeSetting.t0, activeSetting.t1, activeSetting.filetags);
        if (ok == ProgressResult::Stopped) {
           AudMonkeyMessageDialog dlgMessage(
              nullptr,
              XO("Continue to export remaining files?"),
              XO("Export"),
              wxYES_NO | wxNO_DEFAULT | wxICON_WARNING);
           if (dlgMessage.ShowModal() != wxID_YES ) {
              // User decided not to continue - bail out!
              break;
           }
        }
        else if (ok != ProgressResult::Success) {
           break;
        }
        // increment export counter
        count++;
  
     }
  
     return ok ;
  }
  
  ProgressResult ExportMultipleDialog::DoExport(std::unique_ptr<ProgressDialog> &pDialog,
                                unsigned channels,
                                const wxFileName &inName,
                                bool selectedOnly,
                                double t0,
                                double t1,
                                const Tags &tags)
  {
     wxFileName name;
  
     wxLogDebug(wxT("Doing multiple Export: File name \"%s\""), (inName.GetFullName()));
     wxLogDebug(wxT("Channels: %i, Start: %lf, End: %lf "), channels, t0, t1);
     if (selectedOnly)
        wxLogDebug(wxT("Selected Region Only"));
     else
        wxLogDebug(wxT("Whole Project"));
  
     wxFileName backup;
     if (mOverwrite->GetValue()) {
        name = inName;
        backup.Assign(name);
  
        int suffix = 0;
        do {
           backup.SetName(name.GetName() +
                             wxString::Format(wxT("%d"), suffix));
           ++suffix;
        }
        while (backup.FileExists());
        ::wxRenameFile(inName.GetFullPath(), backup.GetFullPath());
     }
     else {
        name = inName;
        int i = 2;
        wxString base(name.GetName());
        while (name.FileExists()) {
           name.SetName(wxString::Format(wxT("%s-%d"), base, i++));
        }
     }
  
     ProgressResult success = ProgressResult::Cancelled;
     const wxString fullPath{name.GetFullPath()};
  
     auto cleanup = finally( [&] {
        bool ok =
           success == ProgressResult::Stopped ||
           success == ProgressResult::Success;
        if (backup.IsOk()) {
           if ( ok )
              // Remove backup
              ::wxRemoveFile(backup.GetFullPath());
           else {
              // Restore original
              ::wxRemoveFile(fullPath);
              ::wxRenameFile(backup.GetFullPath(), fullPath);
           }
        }
        else {
           if ( ! ok )
              // Remove any new, and only partially written, file.
              ::wxRemoveFile(fullPath);
        }
     } );
  
     // Call the format export routine
     success = mPlugins[mPluginIndex]->Export(mProject,
                                              pDialog,
                                                  channels,
                                                  fullPath,
                                                  selectedOnly,
                                                  t0,
                                                  t1,
                                                  NULL,
                                                  &tags,
                                                  mSubFormatIndex);
  
     if (success == ProgressResult::Success || success == ProgressResult::Stopped) {
        mExported.push_back(fullPath);
     }
  
     Refresh();
     Update();
  
     return success;
  }
  
  wxString ExportMultipleDialog::MakeFileName(const wxString &input)
  {
     wxString newname = input; // name we are generating
  
     // strip out anything that isn't allowed in file names on this platform
     auto changed = Internat::SanitiseFilename(newname, wxT("_"));
  
     if(changed)
     {  // need to get user to fix file name
        // build the dialog
        TranslatableString msg;
        wxString excluded = ::wxJoin( Internat::GetExcludedCharacters(), wxT(' '), wxT('\0') );
        // TODO: For Russian language we should have separate cases for 2 and more than 2 letters.
        if( excluded.length() > 1 ){
           msg = XO(
  // i18n-hint: The second %s gives some letters that can't be used.
  "Label or track \"%s\" is not a legal file name.\nYou cannot use any of these characters:\n\n%s\n\nSuggested replacement:")
              .Format( input, excluded );
        } else {
           msg = XO(
  // i18n-hint: The second %s gives a letter that can't be used.
  "Label or track \"%s\" is not a legal file name. You cannot use \"%s\".\n\nSuggested replacement:")
              .Format( input, excluded );
        }
  
        AudMonkeyTextEntryDialog dlg( this, msg, XO("Save As..."), newname );
  
  
        // And tell the validator about excluded chars
        dlg.SetTextValidator( wxFILTER_EXCLUDE_CHAR_LIST );
        wxTextValidator *tv = dlg.GetTextValidator();
        tv->SetExcludes(Internat::GetExcludedCharacters());
  
        // Show the dialog and bail if the user cancels
        if( dlg.ShowModal() == wxID_CANCEL )
        {
            return wxEmptyString;
        }
        // Extract the name from the dialog
        newname = dlg.GetValue();
     }  // phew - end of file name sanitisation procedure
     return newname;
  }
  
  void SuccessDialog::OnKeyDown(wxListEvent& event)
  {
     if (event.GetKeyCode() == WXK_RETURN)
        EndModal(1);
     else
        event.Skip(); // allow standard behaviour
  }
  
  void SuccessDialog::OnItemActivated(wxListEvent& WXUNUSED(event))
  {
     EndModal(1);
  }
  
  void MouseEvtHandler::OnMouse(wxMouseEvent& event)
  {
     event.Skip(false);
  }
  