/**********************************************************************

  Audacity: A Digital Audio Editor

  DeviceToolBar.cpp

  Dominic Mazzoni

*******************************************************************//*!

\class DeviceToolBar
\brief A toobar to allow easier changing of input and output devices .

*//*******************************************************************/


#include "../Audacity.h" // for USE_* macros
#include "DeviceToolBar.h"
#include "ToolManager.h"

// For compilers that support precompilation, includes "wx/wx.h".
#include <wx/wxprec.h>

#include <wx/setup.h> // for wxUSE_* macros

#ifndef WX_PRECOMP
#include <wx/choice.h>
#include <wx/event.h>
#include <wx/intl.h>
#include <wx/settings.h>
#include <wx/sizer.h>
#include <wx/statbmp.h>
#include <wx/stattext.h>
#include <wx/tooltip.h>
#endif

#include "ToolDock.h"
#include "../TrackPanel.h"

#include "../AColor.h"
#include "../AllThemeResources.h"
#include "../AudioIOBase.h"
#include "../ImageManipulation.h"
#include "../KeyboardCapture.h"
#include "../Prefs.h"
#include "../Project.h"
#include "../ShuttleGui.h"
#include "../widgets/Grabber.h"
#include "../DeviceManager.h"
#include "../widgets/AudacityMessageBox.h"
#include "../widgets/Grabber.h"

#if wxUSE_ACCESSIBILITY
#include "../widgets/WindowAccessible.h"
#endif

IMPLEMENT_CLASS(DeviceToolBar, ToolBar);

////////////////////////////////////////////////////////////
/// Methods for DeviceToolBar
////////////////////////////////////////////////////////////

BEGIN_EVENT_TABLE(DeviceToolBar, ToolBar)
   EVT_CHOICE(wxID_ANY, DeviceToolBar::OnChoice)
   EVT_COMMAND(wxID_ANY, EVT_CAPTURE_KEY, DeviceToolBar::OnCaptureKey)
END_EVENT_TABLE()

static int DeviceToolbarPrefsID()
{
   static int value = wxNewId();
   return value;
}

//Standard contructor
DeviceToolBar::DeviceToolBar( AudacityProject &project )
: ToolBar( project, DeviceBarID, XO("Device"), wxT("Device"), true )
{
   wxTheApp->Bind( EVT_RESCANNED_DEVICES,
      &DeviceToolBar::OnRescannedDevices, this );
}

DeviceToolBar::~DeviceToolBar()
{
}

DeviceToolBar &DeviceToolBar::Get( AudacityProject &project )
{
   auto &toolManager = ToolManager::Get( project );
   return *static_cast<DeviceToolBar*>( toolManager.GetToolBar(DeviceBarID) );
}

const DeviceToolBar &DeviceToolBar::Get( const AudacityProject &project )
{
   return Get( const_cast<AudacityProject&>( project )) ;
}

void DeviceToolBar::Create(wxWindow *parent)
{
   ToolBar::Create(parent);

   // Simulate a size event to set initial meter placement/size
   wxSizeEvent event(GetSize(), GetId());
   event.SetEventObject(this);
   GetEventHandler()->ProcessEvent(event);
}

void DeviceToolBar::DeinitChildren()
{
   mInput         = NULL;
   mOutput        = NULL;
   mInputChannels = NULL;
   mHost          = NULL;
}

void DeviceToolBar::Populate()
{
   SetBackgroundColour( theTheme.Colour( clrMedium  ) );
   DeinitChildren();
   // Hosts
   mHost = safenew wxChoice(this,
                        wxID_ANY,
                        wxDefaultPosition,
                        wxDefaultSize);
#if wxUSE_ACCESSIBILITY
   // so that name can be set on a standard control
   mHost->SetAccessible(safenew WindowAccessible(mHost));
#endif
   Add(mHost, 0, wxALIGN_CENTER);

   // Input device
   Add(safenew AStaticBitmap(this,
                          wxID_ANY,
                          theTheme.Bitmap(bmpMic)), 0, wxALIGN_CENTER);
   mInput = safenew wxChoice(this,
                         wxID_ANY,
                         wxDefaultPosition,
                         wxDefaultSize);
#if wxUSE_ACCESSIBILITY
   // so that name can be set on a standard control
   mInput->SetAccessible(safenew WindowAccessible(mInput));
#endif
   // Input channels
   Add(mInput, 0, wxALIGN_CENTER);
   mInputChannels = safenew wxChoice(this,
                         wxID_ANY,
                         wxDefaultPosition,
                         wxDefaultSize);
#if wxUSE_ACCESSIBILITY
   // so that name can be set on a standard control
   mInputChannels->SetAccessible(safenew WindowAccessible(mInputChannels));
#endif
   Add(mInputChannels, 0, wxALIGN_CENTER);

   // Output device
   Add(safenew AStaticBitmap(this,
                          wxID_ANY,
                          theTheme.Bitmap(bmpSpeaker)), 0, wxALIGN_CENTER);
   mOutput = safenew wxChoice(this,
                               wxID_ANY,
                               wxDefaultPosition,
                               wxDefaultSize);
#if wxUSE_ACCESSIBILITY
   // so that name can be set on a standard control
   mOutput->SetAccessible(safenew WindowAccessible(mOutput));
#endif
   Add(mOutput, 0, wxALIGN_CENTER);




   mHost->Bind(wxEVT_SET_FOCUS,
                 &DeviceToolBar::OnFocus,
                 this);
   mHost->Bind(wxEVT_KILL_FOCUS,
                 &DeviceToolBar::OnFocus,
                 this);
   mOutput->Bind(wxEVT_SET_FOCUS,
                 &DeviceToolBar::OnFocus,
                 this);
   mOutput->Bind(wxEVT_KILL_FOCUS,
                 &DeviceToolBar::OnFocus,
                 this);
   mInput->Bind(wxEVT_SET_FOCUS,
                 &DeviceToolBar::OnFocus,
                 this);
   mInput->Bind(wxEVT_KILL_FOCUS,
                 &DeviceToolBar::OnFocus,
                 this);
   mInputChannels->Bind(wxEVT_SET_FOCUS,
                 &DeviceToolBar::OnFocus,
                 this);
   mInputChannels->Bind(wxEVT_KILL_FOCUS,
                 &DeviceToolBar::OnFocus,
                 this);

   SetNames();

   RefillCombos();
}

void DeviceToolBar::RefillCombos()
{
   FillHosts();
   FillHostDevices();
   FillInputChannels();
   // make the device display selection reflect the prefs if they exist
   UpdatePrefs();
}

void DeviceToolBar::OnFocus(wxFocusEvent &event)
{
   KeyboardCapture::OnFocus( *this, event );
}

void DeviceToolBar::OnCaptureKey(wxCommandEvent &event)
{
   wxKeyEvent *kevent = (wxKeyEvent *)event.GetEventObject();
   int keyCode = kevent->GetKeyCode();

   // Pass UP/DOWN/LEFT/RIGHT through for input/output choice
   if (FindFocus() == mInput && (keyCode == WXK_LEFT || keyCode == WXK_RIGHT
                                 || keyCode == WXK_UP || keyCode == WXK_DOWN)) {
      return;
   }

   if (FindFocus() == mOutput && (keyCode == WXK_LEFT || keyCode == WXK_RIGHT
                                 || keyCode == WXK_UP || keyCode == WXK_DOWN)) {
      return;
   }
   event.Skip();

   return;
}

void DeviceToolBar::UpdatePrefs()
{
   wxString hostName;
   wxString devName;
   wxString sourceName;
   wxString desc;
   const std::vector<DeviceSourceMap> &inMaps  = DeviceManager::Instance()->GetInputDeviceMaps();
   const std::vector<DeviceSourceMap> &outMaps = DeviceManager::Instance()->GetOutputDeviceMaps();


   int hostSelectionIndex = mHost->GetSelection();
   wxString oldHost = hostSelectionIndex >= 0 ? mHost->GetString(hostSelectionIndex) :
                                                wxT("");
   hostName = gPrefs->Read(wxT("/AudioIO/Host"), wxT(""));

   // if the prefs host name doesn't match the one displayed, it changed
   // in another project's DeviceToolBar, so we need to repopulate everything.
   if (oldHost != hostName)
      FillHostDevices();

   devName = gPrefs->Read(wxT("/AudioIO/RecordingDevice"), wxT(""));
   sourceName = gPrefs->Read(wxT("/AudioIO/RecordingSource"), wxT(""));
   if (sourceName.empty())
      desc = devName;
   else
      desc = devName + wxT(": ") + sourceName;

   if (mInput->GetStringSelection() != desc &&
       mInput->FindString(desc) != wxNOT_FOUND) {
      mInput->SetStringSelection(desc);
      FillInputChannels();
   } else if (mInput->GetStringSelection() != desc && mInput->GetCount()) {
      for (size_t i = 0; i < inMaps.size(); i++) {
         if (inMaps[i].hostString == hostName &&
             MakeDeviceSourceString(&inMaps[i]) == mInput->GetString(0)) {
            // use the default.  It should exist but check just in case, falling back on the 0 index.
            DeviceSourceMap *defaultMap = DeviceManager::Instance()->GetDefaultInputDevice(inMaps[i].hostIndex);
            if (defaultMap) {
               mInput->SetStringSelection(MakeDeviceSourceString(defaultMap));
               SetDevices(defaultMap, NULL);
            } else {
               //use the first item (0th index) if we have no familiar devices
               mInput->SetSelection(0);
               SetDevices(&inMaps[i], NULL);
            }
            break;
         }
      }
   }

   devName = gPrefs->Read(wxT("/AudioIO/PlaybackDevice"), wxT(""));
   sourceName = gPrefs->Read(wxT("/AudioIO/PlaybackSource"), wxT(""));
   if (sourceName.empty())
      desc = devName;
   else
      desc = devName + wxT(": ") + sourceName;

   if (mOutput->GetStringSelection() != desc &&
       mOutput->FindString(desc) != wxNOT_FOUND) {
      mOutput->SetStringSelection(desc);
   } else if (mOutput->GetStringSelection() != desc &&
              mOutput->GetCount()) {
      for (size_t i = 0; i < outMaps.size(); i++) {
         if (outMaps[i].hostString == hostName &&
             MakeDeviceSourceString(&outMaps[i]) == mOutput->GetString(0)) {
            // use the default.  It should exist but check just in case, falling back on the 0 index.
            DeviceSourceMap *defaultMap = DeviceManager::Instance()->GetDefaultOutputDevice(outMaps[i].hostIndex);
            if (defaultMap) {
               mOutput->SetStringSelection(MakeDeviceSourceString(defaultMap));
               SetDevices(NULL, defaultMap);
            } else {
               //use the first item (0th index) if we have no familiar devices
               mOutput->SetSelection(0);
               SetDevices(NULL, &outMaps[i]);
            }
            break;
         }
      }
   }

   long oldChannels, newChannels;
   oldChannels = mInputChannels->GetSelection() + 1;
   gPrefs->Read(wxT("/AudioIO/RecordChannels"), &newChannels, 0);
   if (newChannels > 0 && oldChannels != newChannels)
      mInputChannels->SetSelection(newChannels - 1);

   if (!hostName.empty() && mHost->GetStringSelection() != hostName)
      mHost->SetStringSelection(hostName);

   RegenerateTooltips();

   // Set label to pull in language change
   SetLabel(XO("Device"));

   // Give base class a chance
   ToolBar::UpdatePrefs();

   Layout();
   Refresh();
}

void DeviceToolBar::UpdateSelectedPrefs( int id )
{
   if (id == DeviceToolbarPrefsID())
      UpdatePrefs();
   ToolBar::UpdateSelectedPrefs( id );
}


void DeviceToolBar::EnableDisableButtons()
{
   auto gAudioIO = AudioIOBase::Get();
   if (gAudioIO) {
      // we allow changes when monitoring, but not when recording
      bool audioStreamActive = gAudioIO->IsStreamActive() && !gAudioIO->IsMonitoring();

      // Here we should relinquish focus
      if (audioStreamActive) {
         wxWindow *focus = wxWindow::FindFocus();
         if (focus == mHost || focus == mInput || focus == mOutput || focus == mInputChannels)
            TrackPanel::Get( mProject ).SetFocus();
      }

      mHost->Enable(!audioStreamActive);
      mInput->Enable(!audioStreamActive);
      mOutput->Enable(!audioStreamActive);
      mInputChannels->Enable(!audioStreamActive);
   }
}

void DeviceToolBar::SetNames()
{
   /* i18n-hint: (noun) It's the device used for playback.*/
   mOutput->SetName(_("Playback Device"));
   /* i18n-hint: (noun) It's the device used for recording.*/
   mInput->SetName(_("Recording Device"));
   mHost->SetName(_("Audio Host"));
   mInputChannels->SetName(_("Recording Channels"));
}

void DeviceToolBar::RegenerateTooltips()
{
#if wxUSE_TOOLTIPS
   SetNames();
   mOutput->SetToolTip(mOutput->GetName() + wxT(" - ") + mOutput->GetStringSelection());
   mInput->SetToolTip(mInput->GetName() + wxT(" - ") + mInput->GetStringSelection());
   mHost->SetToolTip(mHost->GetName() + wxT(" - ") + mHost->GetStringSelection());
   mInputChannels->SetToolTip(mInputChannels->GetName() + wxT(" - ") + mInputChannels->GetStringSelection());
#endif
}

bool DeviceToolBar::Layout()
{
   bool ret;
   ret = ToolBar::Layout();
   RepositionCombos();
   return ret;
}

// returns true if the combo is constrained and false otherwise
// @param toolbarWidth the width of the toolbar in pixels
// @param ratio an in/out for the desired and resultant width ratio.
// @param flex the amount of extra space allowed to have past the available.
//             the amount used is subtracted.
static bool RepositionCombo(wxWindow *combo, int toolbarWidth, wxSize desiredSize,
                            float &ratio, float &flex, int marginPixels, bool changesRatio)
{
   float ratioChange;
   bool constrained = false;

   // push margin pixels
   desiredSize.x += marginPixels;

   // truncate the window size if necessary
   if (desiredSize.x > toolbarWidth * (flex + ratio)) {
      constrained = true;
      desiredSize.SetWidth(toolbarWidth * (flex + ratio));
      if (desiredSize.GetWidth() - marginPixels < 0)
         desiredSize.SetWidth(marginPixels);
   }

   // keep track of how much space gained or lost so it can be used by other combos.
   if (changesRatio) {
      ratioChange = (desiredSize.x / ((float) toolbarWidth)) - ratio;
      ratio += ratioChange;
      flex  -= ratioChange;
   }

   // pop the margin pixels
   desiredSize.x -= marginPixels;

   combo->SetMinSize(desiredSize);

   return constrained;
}

//These don't add up to 1 because there is a bit of margin that we allow
//the layout sizer to handle.
#define kHostWidthRatio 0.13f
#define kInputWidthRatio 0.32f
#define kOutputWidthRatio 0.32f
#define kChannelsWidthRatio 0.18f

void DeviceToolBar::RepositionCombos()
{
   int w, h, dockw, dockh;
   float ratioUnused;
   bool constrained = true;
   wxWindow *window;
   wxSize desiredInput, desiredOutput, desiredHost, desiredChannels;
   float hostRatio, outputRatio, inputRatio, channelsRatio;
   // if the toolbar is docked then the width we should use is the project width.
   // as the toolbar's width can extend past this.
   GetClientSize(&w, &h);

   // FIXME: Note that there's some bug in here, in that even if the prefs show the toolbar
   // docked, on initialization, this call to IsDocked() returns false.
   if (IsDocked()) {
      // If the toolbar is docked its width can be larger than what is actually viewable
      // So take the min.  We don't need to worry about having another toolbar to the left off us
      // because if we are larger than the dock size we always get our own row.
      // and if smaller then we don't use the dock size (because we take the min).
      window = GetDock();
      window->GetClientSize(&dockw, &dockh);
      // The fudge factor here is because the docked toolbar is 2 pixels smaller.
      int kFudge1 = -2;
      // Grabber is Included, but not resizer if docked.
      w-= grabberWidth + kFudge1; 
      if (dockw < w)
         w = dockw;
   } else {
      // If not docked, we have a holding window.
      wxWindow *pParent = GetParent();
      if( pParent ){
         int w1, h1;
         pParent->GetSize( &w1, &h1 );
         // This Resizer is considerably bigger than the 4px docked version.
         // It's the diagonal striped resizer, not the veertical bars.
         int kStripyResizerSize = 15;
         // Grabber AND resizer included.
         w = w1- (kStripyResizerSize + grabberWidth );
      }
   }
   if (w <= 0)
      return;

   // set up initial sizes and ratios
   // Note that the y values of the desired sizes are not changed, so that the height
   // of the toolbar is not changed
   hostRatio     = kHostWidthRatio;
   inputRatio    = kInputWidthRatio;
   outputRatio   = kOutputWidthRatio;
   channelsRatio = kChannelsWidthRatio;

   desiredHost.x     = mHost->GetBestSize().x *4;
   desiredHost.y     = mHost->GetSize().y;
   desiredInput.x    = mInput->GetBestSize().x *4;
   desiredInput.y    = mInput->GetSize().y;
   desiredOutput.x   = mOutput->GetBestSize().x *4;
   desiredOutput.y   = mOutput->GetSize().y;
   desiredChannels.x = mInputChannels->GetBestSize().x *4;
   desiredChannels.y = mInputChannels->GetSize().y;

   // wxGtk (Gnome) has larger comboboxes than the other platforms.  For DeviceToolBar this prevents
   // the toolbar docking on a single height row. So we shrink it to prevent this.
#ifdef __WXGTK__
   desiredHost.SetHeight(mHost->GetBestSize().y -4);
   desiredInput.SetHeight(desiredHost.GetHeight());
   desiredOutput.SetHeight(desiredHost.GetHeight());
   desiredChannels.SetHeight(desiredHost.GetHeight());
#endif

   ratioUnused = 0.995f - (kHostWidthRatio + kInputWidthRatio + kOutputWidthRatio + kChannelsWidthRatio);
   int i = 0;
   // limit the amount of times we solve contraints to 5
   // As we now ask for more than is available, we only do this iteration once.
   while (constrained && ratioUnused > 0.01f && i < 5) {
      i++;
      constrained = RepositionCombo(mHost,   w,   desiredHost,   hostRatio, ratioUnused, 0, true);
      constrained |= RepositionCombo(mInput,  w,  desiredInput,  inputRatio, ratioUnused, theTheme.Bitmap(bmpMic).GetWidth(), true);
      constrained |= RepositionCombo(mOutput, w, desiredOutput, outputRatio, ratioUnused, theTheme.Bitmap(bmpSpeaker).GetWidth(), true);
      constrained |= RepositionCombo(mInputChannels, w, desiredChannels, channelsRatio, ratioUnused, 0, true);
   }

   Update();
}

void DeviceToolBar::FillHosts()
{
   wxArrayString hosts;
   size_t i;

   const std::vector<DeviceSourceMap> &inMaps  = DeviceManager::Instance()->GetInputDeviceMaps();
   const std::vector<DeviceSourceMap> &outMaps = DeviceManager::Instance()->GetOutputDeviceMaps();
   // go over our lists add the host to the list if it isn't there yet
   for (i = 0; i < inMaps.size(); i++)
      if ( ! make_iterator_range( hosts ).contains( inMaps[i].hostString ) )
         hosts.push_back(inMaps[i].hostString);
   for (i = 0; i < outMaps.size(); i++)
      if ( ! make_iterator_range( hosts ).contains( outMaps[i].hostString ) )
         hosts.push_back(outMaps[i].hostString);

   mHost->Clear();
   mHost->Append(hosts);

   if (hosts.size() == 0)
      mHost->Enable(false);

   mHost->InvalidateBestSize();
   mHost->SetMaxSize(mHost->GetBestSize()*4);
}

void DeviceToolBar::FillHostDevices()
{
   const std::vector<DeviceSourceMap> &inMaps  = DeviceManager::Instance()->GetInputDeviceMaps();
   const std::vector<DeviceSourceMap> &outMaps = DeviceManager::Instance()->GetOutputDeviceMaps();

   //read what is in the prefs
   wxString host = gPrefs->Read(wxT("/AudioIO/Host"), wxT(""));
   size_t i;
   int foundHostIndex = -1;

   // if the host is not in the hosts combo then we rescanned.
   // set it to blank so we search for another host.
   if (mHost->FindString(host) == wxNOT_FOUND)
      host = wxT("");

   for (i = 0; i < outMaps.size(); i++) {
      if (outMaps[i].hostString == host) {
         foundHostIndex = outMaps[i].hostIndex;
         break;
      }
   }

   if (foundHostIndex == -1) {
      for (i = 0; i < inMaps.size(); i++) {
         if (inMaps[i].hostString == host) {
            foundHostIndex = inMaps[i].hostIndex;
            break;
         }
      }
   }

   // If no host was found based on the prefs device host, load the first available one
   if (foundHostIndex == -1) {
      if (outMaps.size())
         foundHostIndex = outMaps[0].hostIndex;
      else if (inMaps.size())
         foundHostIndex = inMaps[0].hostIndex;
   }

   // Make sure in/out are clear in case no host was found
   mInput->Clear();
   mOutput->Clear();

   // If we still have no host it means no devices, in which case do nothing.
   if (foundHostIndex == -1)
      return;

   // Repopulate the Input/Output device list available to the user
   for (i = 0; i < inMaps.size(); i++) {
      if (foundHostIndex == inMaps[i].hostIndex) {
         mInput->Append(MakeDeviceSourceString(&inMaps[i]));
         if (host.empty()) {
            host = inMaps[i].hostString;
            gPrefs->Write(wxT("/AudioIO/Host"), host);
            mHost->SetStringSelection(host);
         }
      }
   }
   mInput->Enable(mInput->GetCount() ? true : false);

   mInput->InvalidateBestSize();
   mInput->SetMaxSize(mInput->GetBestSize()*4);

   for (i = 0; i < outMaps.size(); i++) {
      if (foundHostIndex == outMaps[i].hostIndex) {
         mOutput->Append(MakeDeviceSourceString(&outMaps[i]));
         if (host.empty()) {
            host = outMaps[i].hostString;
            gPrefs->Write(wxT("/AudioIO/Host"), host);
            gPrefs->Flush();
            mHost->SetStringSelection(host);
         }
      }
   }
   mOutput->Enable(mOutput->GetCount() ? true : false);

   mOutput->InvalidateBestSize();
   mOutput->SetMaxSize(mOutput->GetBestSize()*4);

   // The setting of the Device is left up to OnChoice
}

void DeviceToolBar::OnRescannedDevices( wxCommandEvent &event )
{
   event.Skip();
   // Hosts may have disappeared or appeared so a complete repopulate is needed.
   RefillCombos();
}

//return 1 if host changed, 0 otherwise.
int DeviceToolBar::ChangeHost()
{
   int hostSelectionIndex;
   hostSelectionIndex = mHost->GetSelection();

   wxString oldHost = gPrefs->Read(wxT("/AudioIO/Host"), wxT(""));
   wxString newHost = hostSelectionIndex >= 0 ? mHost->GetString(hostSelectionIndex) :
                                                oldHost;

   if (oldHost == newHost)
      return 0;

   //change the host and switch to correct devices.
   gPrefs->Write(wxT("/AudioIO/Host"), newHost);
   gPrefs->Flush();

   // populate the devices
   FillHostDevices();

   return 1;
}

void DeviceToolBar::FillInputChannels()
{
   const std::vector<DeviceSourceMap> &inMaps  = DeviceManager::Instance()->GetInputDeviceMaps();
   wxString host     = gPrefs->Read(wxT("/AudioIO/Host"), wxT(""));
   wxString device   = gPrefs->Read(wxT("/AudioIO/RecordingDevice"), wxT(""));
   wxString source   = gPrefs->Read(wxT("/AudioIO/RecordingSource"), wxT(""));
   long oldChannels = 2, newChannels;

   gPrefs->Read(wxT("/AudioIO/RecordChannels"), &oldChannels);
   int index = -1;
   size_t i, j;
   mInputChannels->Clear();
   for (i = 0; i < inMaps.size(); i++) {
      if (source == inMaps[i].sourceString &&
          device == inMaps[i].deviceString &&
          host   == inMaps[i].hostString) {

         // add one selection for each channel of this source
         for (j = 0; j < (unsigned int) inMaps[i].numChannels; j++) {
            wxString name;

            if (j == 0) {
               name = _("1 (Mono) Recording Channel");
            }
            else if (j == 1) {
               name = _("2 (Stereo) Recording Channels");
            }
            else {
               name = wxString::Format(wxT("%d"), (int) j + 1);
            }
            mInputChannels->Append(name);
         }
         newChannels = inMaps[i].numChannels;
         if (oldChannels <= newChannels && oldChannels >= 1)
            newChannels = oldChannels;
         if (newChannels >= 1)
            mInputChannels->SetSelection(newChannels - 1);
         gPrefs->Write(wxT("/AudioIO/RecordChannels"), newChannels);
         mInputChannels->Enable(mInputChannels->GetCount() ? true : false);
         index = i;
         break;
      }
   }
   if (index == -1)
      mInputChannels->Enable(false);

   mInputChannels->InvalidateBestSize();
   mInputChannels->SetMaxSize(mInputChannels->GetBestSize()*4);
}

void DeviceToolBar::SetDevices(const DeviceSourceMap *in, const DeviceSourceMap *out)
{
   if (in) {
      gPrefs->Write(wxT("/AudioIO/RecordingDevice"), in->deviceString);
      gPrefs->Write(wxT("/AudioIO/RecordingSourceIndex"), in->sourceIndex);
      if (in->totalSources >= 1) {
         gPrefs->Write(wxT("/AudioIO/RecordingSource"), in->sourceString);
      } else {
         gPrefs->Write(wxT("/AudioIO/RecordingSource"), wxT(""));
      }
      gPrefs->Flush();

      FillInputChannels();
   }

   if (out) {
      gPrefs->Write(wxT("/AudioIO/PlaybackDevice"), out->deviceString);
      if (out->totalSources >= 1) {
         gPrefs->Write(wxT("/AudioIO/PlaybackSource"), out->sourceString);
      } else {
         gPrefs->Write(wxT("/AudioIO/PlaybackSource"), wxT(""));
      }
      gPrefs->Flush();
   }
}

void DeviceToolBar::ChangeDevice(bool isInput)
{
   int newIndex = -1;
   wxChoice *combo = isInput ? mInput :mOutput;
   size_t i;

   int selectionIndex  = combo->GetSelection();
   wxString host       = gPrefs->Read(wxT("/AudioIO/Host"), wxT(""));
   const std::vector<DeviceSourceMap> &maps = isInput ? DeviceManager::Instance()->GetInputDeviceMaps()
                                                      : DeviceManager::Instance()->GetOutputDeviceMaps();

   // Find device indices for input and output
   if (selectionIndex >= 0 ) {
      wxString newDevice = combo->GetStringSelection();
      for (i = 0; i < maps.size(); ++i) {
         wxString name;
         name = MakeDeviceSourceString(&maps[i]);
         if (name == newDevice && maps[i].hostString == host) {
            newIndex = i;
         }
      }
   }

   if (newIndex < 0) {
      wxLogDebug(wxT("DeviceToolBar::OnChoice(): couldn't find device indices"));
      return;
   }

   SetDevices(isInput ? &maps[newIndex] : NULL,
              isInput ? NULL            : &maps[newIndex]);
}

void DeviceToolBar::OnChoice(wxCommandEvent &event)
{
   wxObject *eventObject = event.GetEventObject();
   //if we've changed hosts, we've handled the device switching already.
   if (eventObject == mHost) {
      ChangeHost();
   } else if (eventObject == mInputChannels) {
      int channelsSelectionIndex = mInputChannels->GetSelection();
      if (channelsSelectionIndex >= 0)
         gPrefs->Write(wxT("/AudioIO/RecordChannels"),channelsSelectionIndex + 1);
   } else if (eventObject == mInput) {
      ChangeDevice(true);
   }
   else if (eventObject == mOutput) {
      ChangeDevice(false);
   }

   auto gAudioIO = AudioIOBase::Get();
   if (gAudioIO) {
      // We cannot have gotten here if gAudioIO->IsAudioTokenActive(),
      // per the setting of AudioIONotBusyFlag and AudioIOBusyFlag in
      // AudacityProject::GetUpdateFlags().
      // However, we can have an invalid audio token (so IsAudioTokenActive()
      // is false), but be monitoring.
      // If monitoring, have to stop the stream, so HandleDeviceChange() can work.
      // We could disable the Preferences command while monitoring, i.e.,
      // set AudioIONotBusyFlag/AudioIOBusyFlag according to monitoring, as well.
      // Instead allow it because unlike recording, for example, monitoring
      // is not clearly something that should prohibit changing device.
      // TODO: We *could* be smarter in this method and call HandleDeviceChange()
      // only when the device choices actually changed. True of lots of prefs!
      // As is, we always stop monitoring before handling the device change.
      if (gAudioIO->IsMonitoring())
      {
         gAudioIO->StopStream();
         while (gAudioIO->IsBusy())
            wxMilliSleep(100);
      }
      gAudioIO->HandleDeviceChange();
   }

   wxTheApp->AddPendingEvent(wxCommandEvent{
      EVT_PREFS_UPDATE, DeviceToolbarPrefsID() });
}

void DeviceToolBar::ShowInputDialog()
{
   ShowComboDialog(mInput, XO("Select Recording Device"));
}
void DeviceToolBar::ShowOutputDialog()
{
   ShowComboDialog(mOutput, XO("Select Playback Device"));
}
void DeviceToolBar::ShowHostDialog()
{
   ShowComboDialog(mHost, XO("Select Audio Host"));
}
void DeviceToolBar::ShowChannelsDialog()
{
   ShowComboDialog(mInputChannels, XO("Select Recording Channels"));
}

void DeviceToolBar::ShowComboDialog(wxChoice *combo, const TranslatableString &title)
{
   if (!combo || combo->GetCount() == 0) {
      AudacityMessageBox( XO("Device information is not available.") );
      return;
   }

#if USE_PORTMIXER
   wxArrayStringEx inputSources = combo->GetStrings();

   wxDialogWrapper dlg(nullptr, wxID_ANY, title);
   dlg.SetName();
   ShuttleGui S(&dlg, eIsCreating);
   wxChoice *c;

   S.StartVerticalLay(true);
   {
      S.StartHorizontalLay(wxCENTER, false);
      {
         c = S.AddChoice( Verbatim( combo->GetName() ),
            transform_container<TranslatableStrings>( inputSources, Verbatim ),
            combo->GetSelection());
      }
      S.EndHorizontalLay();
   }
   S.EndVerticalLay();
   S.AddStandardButtons();

   dlg.GetSizer()->SetSizeHints(&dlg);
   dlg.Center();

   if (dlg.ShowModal() == wxID_OK)
   {
      wxCommandEvent dummyEvent;
      dummyEvent.SetEventObject(combo);
      // SetSelection() doesn't send an event, so we call OnChoice explicitly
      combo->SetSelection(c->GetSelection());
      OnChoice(dummyEvent);
   }
#endif
}

static RegisteredToolbarFactory factory{ DeviceBarID,
   []( AudacityProject &project ){
      return ToolBar::Holder{ safenew DeviceToolBar{ project } }; }
};

namespace {
AttachedToolBarMenuItem sAttachment{
   /* i18n-hint: Clicking this menu item shows the toolbar
      that manages devices */
   DeviceBarID, wxT("ShowDeviceTB"), XXO("&Device Toolbar")
};
}

