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

  Audacity: A Digital Audio Editor

  ToolManager.cpp

  Dominic Mazzoni
  Shane T. Mueller
  Leland Lucius

  See ToolManager.h for details.

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

\file ToolManager.cpp

  Implements ToolManager

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

\class ToolManager
\brief Manages the ToolDocks and handles the dragging, floating, and
  docking of ToolBars.

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

#include "../Audacity.h"
#include "ToolManager.h"

#include "../Experimental.h"

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

#ifndef WX_PRECOMP
#include <wx/app.h>
#include <wx/dcclient.h>
#include <wx/defs.h>
#include <wx/event.h>
#include <wx/frame.h>
#include <wx/gdicmn.h>
#include <wx/intl.h>
#include <wx/region.h>
#include <wx/settings.h>
#include <wx/sizer.h>
#include <wx/sysopt.h>
#include <wx/timer.h>
#include <wx/utils.h>
#include <wx/window.h>
#endif  /*  */

#include <wx/minifram.h>
#include <wx/popupwin.h>

#include "../AColor.h"
#include "../AllThemeResources.h"
#include "../ImageManipulation.h"
#include "../Prefs.h"
#include "../Project.h"
#include "../widgets/AButton.h"
#include "../widgets/ASlider.h"
#include "../widgets/MeterPanelBase.h"
#include "../widgets/Grabber.h"

////////////////////////////////////////////////////////////
/// Methods for ToolFrame
////////////////////////////////////////////////////////////
#define sizerW 11

//
// Constructor
//
ToolFrame::ToolFrame
   ( AudacityProject *parent, ToolManager *manager, ToolBar *bar, wxPoint pos )
   : wxFrame( FindProjectFrame( parent ),
          bar->GetId(),
          wxEmptyString,
          pos,
          wxDefaultSize,
          wxNO_BORDER |
          wxFRAME_NO_TASKBAR |
#if !defined(__WXMAC__) // bug1358
          wxFRAME_TOOL_WINDOW |
#endif
          wxFRAME_FLOAT_ON_PARENT )
   , mParent{ parent }
{
   int width = bar->GetSize().x;
   int border;

   // OSX doesn't need a border, but Windows and Linux do
   border = 1;
#if defined(__WXMAC__)
   border = 0;

   // WXMAC doesn't support wxFRAME_FLOAT_ON_PARENT, so we do
   //
   // LL:  I've commented this out because if you have, for instance, the meter
   //      toolbar undocked and large and then you open a dialog like an effect,
   //      the dialog may appear behind the dialog and you can't move either one.
   //
   //      However, I'm leaving it here because I don't remember why I'd included
   //      it in the first place.
   // SetWindowClass((WindowRef)d.MacGetWindowRef(), kFloatingWindowClass);
#endif

   // Save parameters
   mManager = manager;
   mBar = bar;

   // Transfer the bar to the ferry
   bar->Reparent(this);
   SetMinSize(bar->GetDockedSize());

   {
      // We use a sizer to maintain proper spacing
      auto s = std::make_unique<wxBoxSizer>(wxHORIZONTAL);

      // Add the bar to the sizer
      s->Add(bar, 1, wxEXPAND | wxALL, border);

      // Add space for the resize grabber
      if (bar->IsResizable())
      {
         s->Add(sizerW, 1);
         width += sizerW;
      }

      SetSize(width + 2 * ToolBarFloatMargin,
              bar->GetDockedSize().y + 2 * ToolBarFloatMargin);

      // Attach the sizer and resize the window to fit
      SetSizer(s.release());
   }

   Layout();

   // Inform toolbar of change
   bar->SetDocked( NULL, true );

   // Make sure resizable floaters don't get any smaller than initial size
   if( bar->IsResizable() )
   {
      // Calc the minimum size of the frame
      mMinSize = bar->GetMinSize() + ( GetSize() - bar->GetSize() );
   }
}

ToolFrame::~ToolFrame()
{
   if(HasCapture())
      ReleaseMouse();
}

void ToolFrame::OnGrabber( GrabberEvent & event )
{
   // Pass it on to the manager since it isn't in the handling hierarchy
   mManager->ProcessEvent( event );
}

// The current size determines the min size for resizing...
// the 'lock in' is at that aspect ratio.
void ToolFrame::LockInMinSize(ToolBar * pBar)
{
   mBar = pBar;

   wxSize sz = mBar->GetSize();
   SetClientSize( sz );
   int yDesiredMin = 26;
   int y = sz.GetHeight();
   if (y > yDesiredMin) {
      sz.SetWidth((sz.GetWidth() * yDesiredMin) / y);
      sz.SetHeight( yDesiredMin );
   }
   mMinSize = sz -wxSize( 10, 0);
}

void ToolFrame::OnToolBarUpdate( wxCommandEvent & event )
{
   // Resize floater window to exactly contain toolbar
   // use actual size rather than minimum size.
   if (mBar)
      mBar->GetParent()->SetClientSize( mBar->GetSize() );// ->GetMinSize() );

   // Allow it to propagate to our parent
   event.Skip();
}

void ToolFrame::OnPaint( wxPaintEvent & WXUNUSED(event) )
{
   wxPaintDC dc( this );
   wxSize sz = GetSize();
   wxRect r;

   dc.SetPen( theTheme.Colour( clrTrackPanelText) );
#if !defined(__WXMAC__)
   wxBrush clearer( theTheme.Colour( clrMedium ));
   dc.SetBackground( clearer );
   dc.Clear();
   dc.SetBrush( *wxTRANSPARENT_BRUSH );
   dc.DrawRectangle( 0, 0, sz.GetWidth(), sz.GetHeight() );
#endif

   if( mBar && mBar->IsResizable() )
   {
      r.x = sz.x - sizerW - 2,
      r.y = sz.y - sizerW - 2;
      r.width = sizerW + 2;
      r.height = sizerW + 2;

      AColor::Line(dc, r.GetLeft(), r.GetBottom(), r.GetRight(), r.GetTop() );
      AColor::Line(dc, r.GetLeft() + 3, r.GetBottom(), r.GetRight(), r.GetTop() + 3 );
      AColor::Line(dc, r.GetLeft() + 6, r.GetBottom(), r.GetRight(), r.GetTop() + 6 );
      AColor::Line(dc, r.GetLeft() + 9, r.GetBottom(), r.GetRight(), r.GetTop() + 9 );
   }

}

void ToolFrame::OnMotion( wxMouseEvent & event )
{
   // Don't do anything if we're docked or not resizeable
   if( !mBar || mBar->IsDocked() || !mBar->IsResizable() )
   {
      return;
   }

   // Retrieve the mouse position
   wxPoint pos = ClientToScreen( event.GetPosition() );
   if( HasCapture() && event.Dragging() )
   {
      wxRect rect = GetRect();

      rect.SetBottomRight( pos );
      if( rect.width < mMinSize.x )
      {
         rect.width = mMinSize.x;
      }

      if( rect.height < mMinSize.y )
      {
         rect.height = mMinSize.y;
      }

      Resize( rect.GetSize() );
   }
   else if( HasCapture() && event.LeftUp() )
   {
      ReleaseMouse();
   }
   else if( !HasCapture() )
   {
      wxRect rect = GetRect();
      wxRect r;

      r.x = rect.GetRight() - sizerW - 2,
      r.y = rect.GetBottom() - sizerW - 2;
      r.width = sizerW + 2;
      r.height = sizerW + 2;

      // Is left click within resize grabber?
      if( r.Contains( pos ) && !event.Leaving() )
      {
         mOrigSize = GetSize();

         SetCursor( wxCURSOR_SIZENWSE );
         if( event.LeftDown() )
         {
            CaptureMouse();
         }
      }
      else
      {
         SetCursor( wxCURSOR_ARROW );
      }
   }
}

void ToolFrame::OnCaptureLost( wxMouseCaptureLostEvent & WXUNUSED(event) )
{
   if( HasCapture() )
   {
      ReleaseMouse();
   }
}

void ToolFrame::OnClose( wxCloseEvent & event )
{
   event.Veto();
}

void ToolFrame::OnKeyDown( wxKeyEvent &event )
{
   event.Skip();
   if( HasCapture() && event.GetKeyCode() == WXK_ESCAPE ) {
      Resize( mOrigSize );
      ReleaseMouse();
   }
}

void ToolFrame::Resize( const wxSize &size )
{
   SetMinSize( size );
   SetSize( size );
   Layout();
   Refresh( false );
}

IMPLEMENT_CLASS( ToolFrame, wxFrame );

BEGIN_EVENT_TABLE( ToolFrame, wxFrame )
   EVT_GRABBER( wxID_ANY, ToolFrame::OnGrabber )
   EVT_PAINT( ToolFrame::OnPaint )
   EVT_MOUSE_EVENTS( ToolFrame::OnMotion )
   EVT_MOUSE_CAPTURE_LOST( ToolFrame::OnCaptureLost )
   EVT_CLOSE( ToolFrame::OnClose )
   EVT_COMMAND( wxID_ANY, EVT_TOOLBAR_UPDATED, ToolFrame::OnToolBarUpdate )
   EVT_KEY_DOWN( ToolFrame::OnKeyDown )
END_EVENT_TABLE()

IMPLEMENT_CLASS( ToolManager, wxEvtHandler );

////////////////////////////////////////////////////////////
/// Methods for ToolManager
////////////////////////////////////////////////////////////

BEGIN_EVENT_TABLE( ToolManager, wxEvtHandler )
   EVT_GRABBER( wxID_ANY, ToolManager::OnGrabber )
   EVT_TIMER( wxID_ANY, ToolManager::OnTimer )
END_EVENT_TABLE()

static ToolManager::GetTopPanelHook &getTopPanelHook()
{
   static ToolManager::GetTopPanelHook theHook;
   return theHook;
}

auto ToolManager::SetGetTopPanelHook( const GetTopPanelHook &hook )
   -> GetTopPanelHook
{
   auto &theHook = getTopPanelHook();
   auto result = theHook;
   theHook = hook;
   return result;
}

static const AudacityProject::AttachedObjects::RegisteredFactory key{
  []( AudacityProject &parent ){
     auto &window = GetProjectFrame( parent );
     return std::make_shared< ToolManager >(
        &parent, getTopPanelHook()( window ) ); }
};

ToolManager &ToolManager::Get( AudacityProject &project )
{
   return project.AttachedObjects::Get< ToolManager >( key );
}

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

//
// Constructor
//
ToolManager::ToolManager( AudacityProject *parent, wxWindow *topDockParent )
: wxEvtHandler()
{
   if ( !topDockParent )
      THROW_INCONSISTENCY_EXCEPTION;

   auto &window = GetProjectFrame( *parent );
   wxPoint pt[ 3 ];

#if defined(__WXMAC__)
   // Save original transition
   mTransition = wxSystemOptions::GetOptionInt( wxMAC_WINDOW_PLAIN_TRANSITION );
#endif

   // Initialize everything
   mParent = parent;
   mLastPos.x = mBarPos.x = -1;
   mLastPos.y = mBarPos.y = -1;
   mDragWindow = NULL;
   mDragDock = NULL;
   mDragBar = NULL;

   // Create the down arrow
   pt[ 0 ].x = 0;
   pt[ 0 ].y = 0;
   pt[ 1 ].x = 9;
   pt[ 1 ].y = 9;
   pt[ 2 ].x = 18;
   pt[ 2 ].y = 0;

   // Create the shaped region
   mDown = std::make_unique<wxRegion>( 3, &pt[0] );

   // Create the down arrow
   pt[ 0 ].x = 9;
   pt[ 0 ].y = 0;
   pt[ 1 ].x = 0;
   pt[ 1 ].y = 9;
   pt[ 2 ].x = 9;
   pt[ 2 ].y = 18;

   // Create the shaped region
   mLeft = std::make_unique<wxRegion>( 3, &pt[0] );

   // Create the indicator frame
   // parent is null but FramePtr ensures destruction
   mIndicator = FramePtr{ safenew wxFrame( NULL,
                             wxID_ANY,
                             wxEmptyString,
                             wxDefaultPosition,
                             wxSize( 32, 32 ),
                             wxFRAME_TOOL_WINDOW |
                             wxFRAME_SHAPED |
                             wxNO_BORDER |
                             wxFRAME_NO_TASKBAR |
                             wxSTAY_ON_TOP )
   };

   // Hook the creation event...only needed on GTK, but doesn't hurt for all
   mIndicator->Bind( wxEVT_CREATE,
                        &ToolManager::OnIndicatorCreate,
                        this );

   // Hook the paint event...needed for all
   mIndicator->Bind( wxEVT_PAINT,
                        &ToolManager::OnIndicatorPaint,
                        this );

   // It's a little shy
   mIndicator->Hide();

   // Hook the parents mouse events...using the parent helps greatly
   // under GTK
   window.Bind( wxEVT_LEFT_UP,
                     &ToolManager::OnMouse,
                     this );
   window.Bind( wxEVT_MOTION,
                     &ToolManager::OnMouse,
                     this );
   window.Bind( wxEVT_MOUSE_CAPTURE_LOST,
                     &ToolManager::OnCaptureLost,
                     this );

   // Create the top and bottom docks
   mTopDock = safenew ToolDock( this, topDockParent, TopDockID );
   mBotDock = safenew ToolDock( this, &window, BotDockID );

   // Create all of the toolbars
   // All have the project as parent window
   wxASSERT(parent);

   size_t ii = 0;
   for (const auto &factory : RegisteredToolbarFactory::GetFactories()) {
      if (factory) {
         mBars[ii] = factory( *parent );
      }
      else
         wxASSERT( false );
      ++ii;
   }

   // We own the timer
   mTimer.SetOwner( this );

   // Process the toolbar config settings
   ReadConfig();

   wxEvtHandler::AddFilter(this);
}

//
// Destructor
//

void ToolManager::Destroy()
{
   if ( mTopDock || mBotDock ) { // destroy at most once
      wxEvtHandler::RemoveFilter(this);

      // Save the toolbar states
      WriteConfig();

      // This function causes the toolbars to be destroyed, so
      // clear the configuration of the ToolDocks which refer to
      // these toolbars. This change was needed to stop Audacity
      // crashing when running with Jaws on Windows 10 1703.
      mTopDock->GetConfiguration().Clear();
      mBotDock->GetConfiguration().Clear();

      mTopDock = mBotDock = nullptr; // indicate that it has been destroyed

      for ( size_t ii = 0; ii < ToolBarCount; ++ii )
         mBars[ii].reset();

      mIndicator.reset();
   }
}

ToolManager::~ToolManager()
{
   Destroy();
}

// This table describes the default configuration of the toolbars as
// a "tree" and must be kept in pre-order traversal.

// In fact this tree is more of a broom -- nothing properly branches except
// at the root.

// "Root" corresponds to left edge of the main window, and successive siblings
// go from top to bottom.  But in practice layout may wrap this abstract
// configuration if the window size is narrow.

static struct DefaultConfigEntry {
   int barID;
   int rightOf; // parent
   int below;   // preceding sibling
} DefaultConfigTable [] = {
   // Top dock row, may wrap
   { TransportBarID,         NoBarID,                NoBarID                },
   { ToolsBarID,             TransportBarID,         NoBarID                },
   { RecordMeterBarID,       ToolsBarID,             NoBarID                },
   { PlayMeterBarID,         RecordMeterBarID,       NoBarID                },
   { MixerBarID,             PlayMeterBarID,         NoBarID                },
   { EditBarID,              MixerBarID,             NoBarID                },

// DA: Transcription Toolbar not docked, by default.
#ifdef EXPERIMENTAL_DA
   { TranscriptionBarID,     NoBarID,                NoBarID                },
#else
   { TranscriptionBarID,     EditBarID,              NoBarID                },
#endif

   // start another top dock row
   { ScrubbingBarID,         NoBarID,                TransportBarID         },
   { DeviceBarID,            ScrubbingBarID,         TransportBarID         },

   // Hidden by default in top dock
   { MeterBarID,             NoBarID,                NoBarID                },

   // Bottom dock
   { SelectionBarID,         NoBarID,                NoBarID                },
   { TimeBarID,              SelectionBarID,         NoBarID                },

   // Hidden by default in bottom dock
   { SpectralSelectionBarID, NoBarID,                NoBarID                },
};

void ToolManager::Reset()
{
   // Disconnect all docked bars
   for ( const auto &entry : DefaultConfigTable )
   {
      int ndx = entry.barID;
      ToolBar *bar = mBars[ ndx ].get();

      ToolBarConfiguration::Position position {
         (entry.rightOf == NoBarID) ? nullptr : mBars[ entry.rightOf ].get(),
         (entry.below == NoBarID) ? nullptr : mBars[ entry.below ].get()
      };
      bar->SetSize( 20,20 );

      wxWindow *floater;
      ToolDock *dock;
      bool expose = true;

      // Disconnect the bar
      if( bar->IsDocked() )
      {
         bar->GetDock()->Undock( bar );
         floater = NULL;
      }
      else
      {
         floater = bar->GetParent();
      }

      // Decide which dock.
      if (ndx == SelectionBarID
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
         || ndx == SpectralSelectionBarID
#endif
         || ndx == TimeBarID
         )
         dock = mBotDock;
      else
         dock = mTopDock;

      // PRL: Destroy the tool frame before recreating buttons.
      // This fixes some subtle sizing problems on macOs.
      bar->Reparent( dock );
      //OK (and good) to DELETE floater, as bar is no longer in it.
      if( floater )
         floater->Destroy();

      // Recreate bar buttons (and resize it)
      bar->SetToDefaultSize();
      bar->ReCreateButtons();
      bar->EnableDisableButtons();

#if 0
      if( bar->IsResizable() )
      {
         bar->SetSize(bar->GetBestFittingSize());
      }
#endif

      // Hide some bars.
      if( ndx == MeterBarID
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
         || ndx == SpectralSelectionBarID
#endif
         || ndx == ScrubbingBarID
// DA: Hides three more toolbars.
#ifdef EXPERIMENTAL_DA
         || ndx == DeviceBarID
         || ndx == TranscriptionBarID
         || ndx == SelectionBarID
#endif
         )
         expose = false;

      // Next condition will alwys (?) be true, as the reset configuration is
      // with no floating toolbars.
      if( dock != NULL )
      {
         // when we dock, we reparent, so bar is no longer a child of floater.
         dock->Dock( bar, false, position );
         Expose( ndx, expose );
      }
      else
      {
         // The (tool)bar has a dragger window round it, the floater.
         // in turn floater will have mParent (the entire App) as its
         // parent.

         // Maybe construct a NEW floater
         // this happens if we have just been bounced out of a dock.
         if( floater == NULL ) {
            wxASSERT(mParent);
            floater = safenew ToolFrame( mParent, this, bar, wxPoint(-1,-1) );
            bar->Reparent( floater );
         }

         // This bar is undocked and invisible.
         // We are doing a reset toolbars, so even the invisible undocked bars should
         // be moved somewhere sensible. Put bar near center of window.
         // If there were multiple hidden toobars the ndx * 10 adjustment means
         // they won't overlap too much.
         floater->CentreOnParent( );
         floater->Move( floater->GetPosition() + wxSize( ndx * 10 - 200, ndx * 10 ));
         bar->SetDocked( NULL, false );
         Expose( ndx, false );
      }

   }
   // TODO:??
   // If audio was playing, we stopped the VU meters,
   // It would be nice to show them again, but hardly essential as
   // they will show up again on the next play.
   // SetVUMeters(AudacityProject *p);
   Updated();
}

void ToolManager::RegenerateTooltips()
{
   for (const auto &bar : mBars) {
      if (bar)
         bar->RegenerateTooltips();
   }
}

int ToolManager::FilterEvent(wxEvent &event)
{
   // Snoop the global event stream for changes of focused window.  Remember
   // the last one of our own that is not a grabber.

   if (event.GetEventType() == wxEVT_KILL_FOCUS) {
      auto &focusEvent = static_cast<wxFocusEvent&>(event);
      auto window = focusEvent.GetWindow();
      auto top = wxGetTopLevelParent(window);
      if(auto toolFrame = dynamic_cast<ToolFrame*>(top))
         top = toolFrame->GetParent();
      // window is that which will GET the focus
      if ( window &&
           !dynamic_cast<Grabber*>( window ) &&
           !dynamic_cast<ToolFrame*>( window ) &&
           top == FindProjectFrame( mParent ) )
         // Note this is a dangle-proof wxWindowRef:
         mLastFocus = window;
   }

   return Event_Skip;
}

//
// Read the toolbar states
//
void ToolManager::ReadConfig()
{
   wxString oldpath = gPrefs->GetPath();
   std::vector<int> unordered[ DockCount ];
   std::vector<ToolBar*> dockedAndHidden;
   bool show[ ToolBarCount ];
   int width[ ToolBarCount ];
   int height[ ToolBarCount ];
   int x, y;
   int dock, ndx;
   bool someFound { false };

#if defined(__WXMAC__)
   // Disable window animation
   wxSystemOptions::SetOption( wxMAC_WINDOW_PLAIN_TRANSITION, 1 );
#endif

   // Change to the bar root
   gPrefs->SetPath( wxT("/GUI/ToolBars") );

   ToolBarConfiguration::Legacy topLegacy, botLegacy;

   int vMajor, vMinor, vMicro;
   gPrefs->GetVersionKeysInit(vMajor, vMinor, vMicro);
   bool useLegacyDock = false;
   // note that vMajor, vMinor, and vMicro will all be zero if either it's a new audacity.cfg file
   // or the version is less than 1.3.13 (when there were no version keys according to the comments in
   // InitPreferences()). So for new audacity.cfg
   // file useLegacyDock will be true, but this doesn't matter as there are no Dock or DockV2 keys in the file yet.
   if (vMajor <= 1 ||
      (vMajor == 2 && (vMinor <= 1 || (vMinor == 2 && vMicro <= 1))))   // version <= 2.2.1
      useLegacyDock = true;


   // Load and apply settings for each bar
   for( ndx = 0; ndx < ToolBarCount; ndx++ )
   {
      ToolBar *bar = mBars[ ndx ].get();
      //wxPoint Center = mParent->GetPosition() + (mParent->GetSize() * 0.33);
      //wxPoint Center(
      //   wxSystemSettings::GetMetric( wxSYS_SCREEN_X ) /2 ,
      //   wxSystemSettings::GetMetric( wxSYS_SCREEN_Y ) /2 );

      // Change to the bar subkey
      gPrefs->SetPath( bar->GetSection() );

      bool bShownByDefault = true;
      int defaultDock = TopDockID;

      if( ndx == SelectionBarID )
         defaultDock = BotDockID;
      if( ndx == MeterBarID )
         bShownByDefault = false;
      if( ndx == ScrubbingBarID )
         bShownByDefault = false;
      if( ndx == TimeBarID )
         defaultDock = BotDockID;

#ifdef EXPERIMENTAL_SPECTRAL_EDITING
      if( ndx == SpectralSelectionBarID ){
         defaultDock = BotDockID;
         bShownByDefault = false; // Only show if asked for.
      }
#endif

      // Read in all the settings

      if (useLegacyDock)
         gPrefs->Read( wxT("Dock"), &dock, -1);       // legacy version of DockV2
      else
         gPrefs->Read( wxT("DockV2"), &dock, -1);

      const bool found = (dock != -1);
      if (found)
         someFound = true;
      if (!found)
         dock = defaultDock;

      ToolDock *d;
      ToolBarConfiguration::Legacy *pLegacy;
      switch(dock)
      {
         case TopDockID: d = mTopDock; pLegacy = &topLegacy; break;
         case BotDockID: d = mBotDock; pLegacy = &botLegacy;  break;
         default:        d = nullptr; pLegacy = nullptr; break;
      }

      bool ordered = ToolBarConfiguration::Read(
         d ? &d->GetConfiguration() : nullptr,
         pLegacy,
         bar, show[ ndx ], bShownByDefault)
      && found;

      gPrefs->Read( wxT("X"), &x, -1 );
      gPrefs->Read( wxT("Y"), &y, -1 );
      gPrefs->Read( wxT("W"), &width[ ndx ], -1 );
      gPrefs->Read( wxT("H"), &height[ ndx ], -1 );

      bar->SetVisible( show[ ndx ] );

      // Docked or floating?
      if( dock )
      {
         // Default to top dock if the ID isn't valid
         if( dock < NoDockID || dock > DockCount ) {
            dock = TopDockID;
         }

         // Create the bar with the correct parent
         if( dock == TopDockID )
         {
            bar->Create( mTopDock );
         }
         else
         {
            bar->Create( mBotDock );
         }

         // Set the width and height
         if( width[ ndx ] != -1 && height[ ndx ] != -1 )
         {
            wxSize sz( width[ ndx ], height[ ndx ] );
            bar->SetSize( sz );
            bar->ResizingDone();
         }

#ifdef EXPERIMENTAL_SYNC_LOCK
         // Set the width
         if( width[ ndx ] >= bar->GetSize().x )
         {
            wxSize sz( width[ ndx ], bar->GetSize().y );
            bar->SetSize( sz );
            bar->Layout();
         }
#else
         // note that this section is here because if you had been using sync-lock and now you aren't,
         // the space for the extra button is stored in audacity.cfg, and so you get an extra space
         // in the EditToolbar.
         // It is needed so that the meterToolbar size gets preserved.
         // Longer-term we should find a better fix for this.
         wxString thisBar = bar->GetSection();
         if( thisBar != wxT("Edit"))
         {
            // Set the width
            if( width[ ndx ] >= bar->GetSize().x )
            {
               wxSize sz( width[ ndx ], bar->GetSize().y );
               bar->SetSize( sz );
               bar->Layout();
            }
         }
#endif
         // make a note of docked and hidden toolbars
         if (!show[ndx])
            dockedAndHidden.push_back(bar);

         if (!ordered)
         {
            // These must go at the end
            unordered[ dock - 1 ].push_back( ndx );
         }
      }
      else
      {
         // Create the bar (with the top dock being temporary parent)
         bar->Create( mTopDock );

         // Construct a NEW floater
         wxASSERT(mParent);
         ToolFrame *f = safenew ToolFrame( mParent, this, bar, wxPoint( x, y ) );

         // Set the width and height
         if( width[ ndx ] != -1 && height[ ndx ] != -1 )
         {
            wxSize sz( width[ ndx ], height[ ndx ] );
            f->SetSizeHints( sz );
            f->SetSize( sz );
            f->Layout();
            if( (x!=-1) && (y!=-1) )
               bar->SetPositioned();
         }

         // Required on Linux Xfce
         wxSize msz(width[ndx],height[ndx]-1);
         bar->GetParent()->SetMinSize(msz);

         // Inform toolbar of change
         bar->SetDocked( NULL, false );

         // Show or hide it
         Expose( ndx, show[ ndx ] );
      }

      // Change back to the bar root
      //gPrefs->SetPath( wxT("..") );  <-- Causes a warning...
      // May or may not have gone into a subdirectory,
      // so use an absolute path.
      gPrefs->SetPath( wxT("/GUI/ToolBars") );
   }

   mTopDock->GetConfiguration().PostRead(topLegacy);
   mBotDock->GetConfiguration().PostRead(botLegacy);

   // Add all toolbars to their target dock
   for( dock = 0; dock < DockCount; dock++ )
   {
      ToolDock *d = ( dock + 1 == TopDockID ? mTopDock : mBotDock );

      d->LoadConfig();

      // Add all unordered toolbars
      for( int ord = 0; ord < (int) unordered[ dock ].size(); ord++ )
      {
         ToolBar *t = mBars[ unordered[ dock ][ ord ] ].get();

         // Dock it
         d->Dock( t, false );

         // Show or hide the bar
         Expose( t->GetId(), show[ t->GetId() ] );
      }
   }

   // hidden docked toolbars
   for (auto bar : dockedAndHidden) {
      bar->SetVisible(false );
      bar->GetDock()->Dock(bar, false);
      bar->Expose(false);
   }

   // Restore original config path
   gPrefs->SetPath( oldpath );

#if defined(__WXMAC__)
   // Reinstate original transition
   wxSystemOptions::SetOption( wxMAC_WINDOW_PLAIN_TRANSITION, mTransition );
#endif

   if (!someFound)
      Reset();
}

//
// Save the toolbar states
//
void ToolManager::WriteConfig()
{
   if( !gPrefs )
   {
      return;
   }

   wxString oldpath = gPrefs->GetPath();
   int ndx;

   // Change to the bar root
   gPrefs->SetPath( wxT("/GUI/ToolBars") );

   // Save state of each bar
   for( ndx = 0; ndx < ToolBarCount; ndx++ )
   {
      ToolBar *bar = mBars[ ndx ].get();

      // Change to the bar subkey
      gPrefs->SetPath( bar->GetSection() );

      // Search both docks for toolbar order
      bool to = mTopDock->GetConfiguration().Contains( bar );
      bool bo = mBotDock->GetConfiguration().Contains( bar );

      // Save
      // Note that DockV2 was introduced in 2.2.2 to fix bug #1554. Dock is retained so that
      // the toolbar layout is not changed when opening a version before 2.2.2, and in particular
      // its value is compatible with versions 2.1.3 to 2.2.1 which have this bug.
      ToolDock* dock = bar->GetDock();       // dock for both shown and hidden toolbars
      gPrefs->Write( wxT("DockV2"), static_cast<int>(dock == mTopDock ? TopDockID : dock == mBotDock ? BotDockID : NoDockID ));

      gPrefs->Write( wxT("Dock"), static_cast<int>( to ? TopDockID : bo ? BotDockID : NoDockID));

      dock = to ? mTopDock : bo ? mBotDock : nullptr;    // dock for shown toolbars
      ToolBarConfiguration::Write
         (dock ? &dock->GetConfiguration() : nullptr, bar);

      wxPoint pos( -1, -1 );
      wxSize sz = bar->GetSize();
      if( !bar->IsDocked() && bar->IsPositioned() )
      {
         pos = bar->GetParent()->GetPosition();
         sz = bar->GetParent()->GetSize();
      }
      gPrefs->Write( wxT("X"), pos.x );
      gPrefs->Write( wxT("Y"), pos.y );
      gPrefs->Write( wxT("W"), sz.x );
      gPrefs->Write( wxT("H"), sz.y );

      // Change back to the bar root
      gPrefs->SetPath( wxT("..") );
   }

   // Restore original config path
   gPrefs->SetPath( oldpath );
   gPrefs->Flush();
}

//
// Return a pointer to the specified toolbar
//
ToolBar *ToolManager::GetToolBar( int type ) const
{
   return mBars[ type ].get();
}

//
// Return a pointer to the top dock
//
ToolDock *ToolManager::GetTopDock()
{
   return mTopDock;
}

const ToolDock *ToolManager::GetTopDock() const
{
   return mTopDock;
}

//
// Return a pointer to the bottom dock
//
ToolDock *ToolManager::GetBotDock()
{
   return mBotDock;
}

const ToolDock *ToolManager::GetBotDock() const
{
   return mBotDock;
}

//
// Queues an EVT_TOOLBAR_UPDATED command event to notify any
// interest parties of an updated toolbar or dock layout
//
void ToolManager::Updated()
{
   // Queue an update event
   wxCommandEvent e( EVT_TOOLBAR_UPDATED );
   GetProjectFrame( *mParent ).GetEventHandler()->AddPendingEvent( e );
}

//
// Return docked state of specified toolbar
//
bool ToolManager::IsDocked( int type )
{
   return mBars[ type ]->IsDocked();
}

//
// Returns the visibility of the specified toolbar
//
bool ToolManager::IsVisible( int type )
{
   ToolBar *t = mBars[ type ].get();

   return t && t->IsVisible();

#if 0
   // If toolbar is floating
   if( !t->IsDocked() )
   {
      // Must return state of floater window
      return t->GetParent()->IsShown();
   }

   // Return state of docked toolbar
   return t->IsShown();
#endif
}

//
// Toggles the visible/hidden state of a toolbar
//
void ToolManager::ShowHide( int type )
{
   Expose( type, !mBars[ type ]->IsVisible() );
   Updated();
}

//
// Set the visible/hidden state of a toolbar
//
void ToolManager::Expose( int type, bool show )
{
   ToolBar *t = mBars[ type ].get();

   // Handle docked and floaters differently
   if( t->IsDocked() )
   {
      t->GetDock()->Expose( type, show );
   }
   else
   {
      t->Expose( show );
   }
}

//
// Ask both docks to (re)layout their bars
//
void ToolManager::LayoutToolBars()
{
   // Update the layout
   mTopDock->LayoutToolBars();
   mBotDock->LayoutToolBars();
}

//
// Handle toolbar dragging
//
void ToolManager::OnMouse( wxMouseEvent & event )
{
   // Go ahead and set the event to propagate
   event.Skip();

   // Can't do anything if we're not dragging.  This also prevents
   // us from intercepting events that don't belong to us from the
   // parent since we're Connect()ed to a couple.
   if( !mClicked )
   {
      return;
   }

#if defined(__WXMAC__)
   // Disable window animation
   wxSystemOptions::SetOption( wxMAC_WINDOW_PLAIN_TRANSITION, 1 );
#endif

   // Retrieve the event position
   wxPoint pos =
      ( (wxWindow *)event.GetEventObject() )->ClientToScreen( event.GetPosition() ) - mDragOffset;


   if( !event.LeftIsDown() )
   {
      // Button was released...finish the drag
      // Transition the bar to a dock
      if (!mDidDrag) {
         if (mPrevDock)
            mPrevDock->RestoreConfiguration(mPrevConfiguration);
         DoneDragging();
         return;
      }
      else if( mDragDock && !event.ShiftDown() )
      {
         // Trip over...everyone ashore that's going ashore...
         mDragDock->Dock( mDragBar, true, mDragBefore );
         Updated();
         mDragWindow->ClearBar();

         // Done with the floater
         mDragWindow->Destroy();
         mDragWindow = nullptr;
         mDragBar->Refresh(false);
      }
      else
      {
         // Calling SetDocked() to force the grabber button to popup
         mDragBar->SetDocked( NULL, false );
      }

      DoneDragging();
   }
   else if( event.Dragging() && pos != mLastPos )
   {
      if (!mDidDrag) {
         // Must set the bar afloat if it's currently docked
         mDidDrag = true;
         wxPoint mp = event.GetPosition();
         mp = GetProjectFrame( *mParent ).ClientToScreen(mp);
         if (!mDragWindow) {
            // We no longer have control
            if (mPrevDock)
               mPrevDock->GetConfiguration().Remove( mDragBar );
            UndockBar(mp);
         }
      }

      // Make toolbar follow the mouse
      mDragWindow->Move( pos  );

      // Remember to prevent excessive movement
      mLastPos = pos;

      // Calc the top dock hittest rectangle
      wxRect tr = mTopDock->GetRect();
      tr.SetBottom( tr.GetBottom() + 10 );
      tr.SetPosition( mTopDock->GetParent()->ClientToScreen( tr.GetPosition() ) );

      // Calc the bottom dock hittest rectangle
      wxRect br = mBotDock->GetRect();
      br.SetTop( br.GetTop() - 10 );
      br.SetBottom( br.GetBottom() + 20 );
      br.SetPosition( mBotDock->GetParent()->ClientToScreen( br.GetPosition() ) );


      // Add half the bar height.  We could use the actual bar height, but that would be confusing as a
      // bar removed at a place might not dock back there if just let go.
      // Also add 5 pixels in horizontal direction, so that a click without a move (or a very small move)
      // lands back where we started.
      pos +=  wxPoint( 5, 20 );


      // To find which dock, rather than test against pos, test against the whole dragger rect.
      // This means it is enough to overlap the dock to dock with it.
      wxRect barRect = mDragWindow->GetRect();
      ToolDock *dock = NULL;
      if( tr.Intersects( barRect ) )
         dock = mTopDock;
      else if( br.Intersects( barRect ) )
         dock = mBotDock;

      // Looks like we have a winner...
      if( dock )
      {
         wxPoint p;
         wxRect r;

         // Calculate where the bar would be placed
         mDragBefore = dock->PositionBar( mDragBar, pos, r );

         // If different than the last time, the indicator must be moved
         if( r != mBarPos )
         {
            wxRect dr = dock->GetRect();

            // Hide the indicator before changing the shape
            mIndicator->Hide();

            // Decide which direction the arrow should point
            if( r.GetTop() >= dr.GetHeight() )
            {
               const auto &box = mDown->GetBox();
               p.x = dr.GetLeft() + ( dr.GetWidth() / 2 )
                 - (box.GetWidth() / 2);
               p.y = dr.GetBottom() - box.GetHeight();
               mCurrent = mDown.get();
            }
            else
            {
               // r is the rectangle of the toolbar being dragged.
               // A tall undocked toolbar will become at most 2 tbs
               // high when docked, so the triangular drop indicator
               // needs to use that height, h, not the bar height
               // for calculating where to be drawn.
               const int tbs = toolbarSingle + toolbarGap;
               int h = wxMin(r.GetHeight(), 2*tbs-1);
               p.x = dr.GetLeft() + r.GetLeft();
               p.y = dr.GetTop() + r.GetTop() +
                  ( ( h - mLeft->GetBox().GetHeight() ) / 2 );
               mCurrent = mLeft.get();
            }

            // Change the shape while hidden and then show it if okay
            mIndicator->SetShape( *mCurrent );
            if( !event.ShiftDown() )
            {
               mIndicator->Show();
               mIndicator->Update();
            }

            // Move it into position
            // LL:  Do this after the Show() since KDE doesn't move the window
            //      if it's not shown.  (Do it outside if the previous IF as well)
            mIndicator->Move( dock->GetParent()->ClientToScreen( p ) );

            // Remember for next go round
            mBarPos = r;
         }
      }
      else
      {
         // Hide the indicator if it's still shown
         if( mBarPos.x != -1 )
         {
            // Hide any
            mIndicator->Hide();
            mBarPos.x = -1;
            mBarPos.y = -1;
         }
      }

      // Remember to which dock the drag bar belongs.
      mDragDock = dock;
   }

#if defined(__WXMAC__)
   // Reinstate original transition
   wxSystemOptions::SetOption( wxMAC_WINDOW_PLAIN_TRANSITION, mTransition );
#endif
}

//
// Deal with NEW capture lost event
//
void ToolManager::OnCaptureLost( wxMouseCaptureLostEvent & event )
{
   // Can't do anything if we're not dragging.  This also prevents
   // us from intercepting events that don't belong to us from the
   // parent since we're Connect()ed to a couple.
   if( !mDragWindow )
   {
      event.Skip();
      return;
   }

   // Simulate button up
   wxMouseEvent e(wxEVT_LEFT_UP);
   e.SetEventObject(mParent);
   OnMouse(e);
}

//
// Watch for shift key changes
//
void ToolManager::OnTimer( wxTimerEvent & event )
{
   // Go ahead and set the event to propagate
   event.Skip();

   // Can't do anything if we're not dragging.  This also prevents
   // us from intercepting events that don't belong to us from the
   // parent since we're Connect()ed to a couple.
   if( !mDragWindow )
   {
      return;
   }

   bool state = wxGetKeyState( WXK_SHIFT );
   if( mLastState != state )
   {
      mLastState = state;

#if defined(__WXMAC__)
      // Disable window animation
      wxSystemOptions::SetOption( wxMAC_WINDOW_PLAIN_TRANSITION, 1 );
#endif

      mIndicator->Show( !state );

#if defined(__WXMAC__)
      // Disable window animation
      wxSystemOptions::SetOption( wxMAC_WINDOW_PLAIN_TRANSITION, mTransition );
#endif
   }

   return;
}

//
// Handle Indicator paint events
//
// Really only needed for the Mac since SetBackgroundColour()
// doesn't seem to work with shaped frames.
//
void ToolManager::OnIndicatorPaint( wxPaintEvent & event )
{
   // TODO: Better to use a bitmap than a triangular region.
   wxWindow *w = (wxWindow *)event.GetEventObject();
   wxPaintDC dc( w );
   // TODO: Better (faster) to use the existing spare brush.
   wxBrush brush( theTheme.Colour( clrTrackPanelText ) );
   dc.SetBackground( brush );
   dc.Clear();
}

//
// Handle Indicator creation event
//
// Without this, the initial Indicator window will be a solid blue square
// until the next time it changes.
//
void ToolManager::OnIndicatorCreate( wxWindowCreateEvent & event )
{
#if defined(__WXGTK__)
   mIndicator->SetShape( *mCurrent );
#endif
   event.Skip();
}

void ToolManager::UndockBar( wxPoint mp )
{
#if defined(__WXMAC__)
   // Disable window animation
   wxSystemOptions::SetOption( wxMAC_WINDOW_PLAIN_TRANSITION, 1 );
#endif

   // Adjust the starting position
   mp -= mDragOffset;

   // Inform toolbar of change
   mDragBar->SetDocked( NULL, true );
   mDragBar->SetPositioned();

   // Construct a NEW floater
   wxASSERT(mParent);
   mDragWindow = safenew ToolFrame( mParent, this, mDragBar, mp );
   mDragWindow->SetLayoutDirection(wxLayout_LeftToRight);
   // Make sure the ferry is visible
   mDragWindow->Show();

   // Notify parent of change
   Updated();

#if defined(__WXMAC__)
   // Reinstate original transition
   wxSystemOptions::SetOption( wxMAC_WINDOW_PLAIN_TRANSITION, mTransition );
#endif
}

//
// Transition a toolbar from float to dragging
//
void ToolManager::OnGrabber( GrabberEvent & event )
{
   // No need to propagate any further
   event.Skip( false );

   if(event.IsEscaping())
      return HandleEscapeKey();

   // Remember which bar we're dragging
   mDragBar = mBars[ event.GetId() ].get();

   // Remember state, in case of ESCape key later
   if (mDragBar->IsDocked()) {
      mPrevDock = dynamic_cast<ToolDock*>(mDragBar->GetParent());
      wxASSERT(mPrevDock);
      mPrevSlot = mPrevDock->GetConfiguration().Find(mDragBar);
      mPrevDock->WrapConfiguration(mPrevConfiguration);
   }
   else
      mPrevPosition = mDragBar->GetParent()->GetPosition();

   // Calculate the drag offset
   wxPoint mp = event.GetPosition();
   mDragOffset = mp -
                 mDragBar->GetParent()->ClientToScreen( mDragBar->GetPosition() ) +
      wxPoint( 1, 1 );

   mClicked = true;
   if( mPrevDock )
   {
      mDragWindow = nullptr;
   }
   else
   {
      mDragWindow = (ToolFrame *) mDragBar->GetParent();
   }

   // We want all mouse events from this point on
   auto &window = GetProjectFrame( *mParent );
   if( !window.HasCapture() )
      window.CaptureMouse();

   // Start monitoring shift key changes
   mLastState = wxGetKeyState( WXK_SHIFT );
   mTimer.Start( 100 );
}


void ToolManager::HandleEscapeKey()
{
   if (mDragBar) {
      if(mPrevDock) {
         // Sheriff John Stone,
         // Why don't you leave me alone?
         // Well, I feel so break up
         // I want to go home.
         mPrevDock->RestoreConfiguration(mPrevConfiguration);
         mPrevDock->Dock( mDragBar, true, mPrevSlot );
         Updated();

         // Done with the floater
         mDragWindow->ClearBar();
         mDragWindow->Destroy();
         mDragWindow = nullptr;
         mDragBar->Refresh(false);
      }
      else {
         // Floater remains, and returns to where it begain
         auto parent = mDragBar->GetParent();
         parent->SetPosition(mPrevPosition);
         mDragBar->SetDocked(NULL, false);
      }

      DoneDragging();
   }
}

void ToolManager::DoneDragging()
{
   // Done dragging
   // Release capture
   auto &window = GetProjectFrame( *mParent );
   if( window.HasCapture() )
   {
      window.ReleaseMouse();
   }

   // Hide the indicator
   mIndicator->Hide();

   mDragWindow = NULL;
   mDragDock = NULL;
   mDragBar = NULL;
   mPrevDock = NULL;
   mPrevSlot = { ToolBarConfiguration::UnspecifiedPosition };
   mPrevConfiguration.Clear();
   mLastPos.x = mBarPos.x = -1;
   mLastPos.y = mBarPos.y = -1;
   mTimer.Stop();
   mDidDrag = false;
   mClicked = false;

   RestoreFocus();
}

bool ToolManager::RestoreFocus()
{
   if (mLastFocus) {
      auto temp1 = AButton::TemporarilyAllowFocus();
      auto temp2 = ASlider::TemporarilyAllowFocus();
      auto temp3 = MeterPanelBase::TemporarilyAllowFocus();
      mLastFocus->SetFocus();
      return true;
   }
   return false;
}

#include "../commands/CommandContext.h"
#include "../Menus.h"

AttachedToolBarMenuItem::AttachedToolBarMenuItem(
   ToolBarID id, const CommandID &name, const TranslatableString &label_in,
   const Registry::OrderingHint &hint,
   std::vector< ToolBarID > excludeIDs )
   : mId{ id }
   , mAttachedItem{
      Registry::Placement{ wxT("View/Other/Toolbars/Toolbars/Other"), hint },
      (  MenuTable::FinderScope(
            [this](AudacityProject &) -> CommandHandlerObject&
               { return *this; } ),
         MenuTable::Command( name, label_in,
            &AttachedToolBarMenuItem::OnShowToolBar,
            AlwaysEnabledFlag,
            CommandManager::Options{}.CheckTest( [id](AudacityProject &project){
               auto &toolManager = ToolManager::Get( project );
               return toolManager.IsVisible( id ); } ) ) ) }
   , mExcludeIds{ std::move( excludeIDs ) }
{}

void AttachedToolBarMenuItem::OnShowToolBar( const CommandContext &context )
{
   auto &project = context.project;
   auto &toolManager = ToolManager::Get( project );

   if( !toolManager.IsVisible( mId ) )
   {
      for ( const auto excludedID : mExcludeIds )
         toolManager.Expose( excludedID, false );
   }

   toolManager.ShowHide(mId);
   MenuManager::Get(project).ModifyToolbarMenus(project);
}
