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

  Audacity: A Digital Audio Editor

  ASlider.cpp

  Dominic Mazzoni

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

\class ASlider
\brief ASlider is a custom slider, allowing for a slicker look and
feel.

It allows you to use images for the slider background and
the thumb.

\class LWSlider
\brief Lightweight version of ASlider.  In other words it does not
have a window permanently associated with it.

\class SliderDialog
\brief Pop up dialog used with an LWSlider.

\class TipWindow
\brief A wxPopupWindow used to give the numerical value of an LWSlider
or ASlider.

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



#include "ASlider.h"

#include <math.h>

#include <wx/setup.h> // for wxUSE_* macros
#include <wx/defs.h>
#include <wx/dcbuffer.h>
#include <wx/frame.h>
#include <wx/graphics.h>
#include <wx/panel.h>
#include <wx/tooltip.h>
#include <wx/debug.h>
#include <wx/textctrl.h>
#include <wx/valtext.h>
#include <wx/statline.h>
#include <wx/settings.h>
#include <wx/popupwin.h>
#include <wx/window.h>
#include <wx/display.h>

#include "AColor.h"
#include "ImageManipulation.h"
#include "Project.h"
#include "ProjectStatus.h"
#include "../ProjectWindowBase.h"
#include "ShuttleGui.h"
#include "Theme.h"
#include "valnum.h"

#include "AllThemeResources.h"

#if wxUSE_ACCESSIBILITY
#include "WindowAccessible.h"

class AUDACITY_DLL_API ASliderAx final : public WindowAccessible
{
public:
   ASliderAx(wxWindow * window);

   virtual ~ ASliderAx();

   // Retrieves the address of an IDispatch interface for the specified child.
   // All objects must support this property.
   wxAccStatus GetChild(int childId, wxAccessible** child) override;

   // Gets the number of children.
   wxAccStatus GetChildCount(int* childCount) override;

   // Gets the default action for this object (0) or > 0 (the action for a child).
   // Return wxACC_OK even if there is no action. actionName is the action, or the empty
   // string if there is no action.
   // The retrieved string describes the action that is performed on an object,
   // not what the object does as a result. For example, a toolbar button that prints
   // a document has a default action of "Press" rather than "Prints the current document."
   wxAccStatus GetDefaultAction(int childId, wxString *actionName) override;

   // Returns the description for this object or a child.
   wxAccStatus GetDescription(int childId, wxString *description) override;

   // Gets the window with the keyboard focus.
   // If childId is 0 and child is NULL, no object in
   // this subhierarchy has the focus.
   // If this object has the focus, child should be 'this'.
   wxAccStatus GetFocus(int *childId, wxAccessible **child) override;

   // Returns help text for this object or a child, similar to tooltip text.
   wxAccStatus GetHelpText(int childId, wxString *helpText) override;

   // Returns the keyboard shortcut for this object or child.
   // Return e.g. ALT+K
   wxAccStatus GetKeyboardShortcut(int childId, wxString *shortcut) override;

   // Returns the rectangle for this object (id = 0) or a child element (id > 0).
   // rect is in screen coordinates.
   wxAccStatus GetLocation(wxRect& rect, int elementId) override;

   // Gets the name of the specified object.
   wxAccStatus GetName(int childId, wxString *name) override;

   // Returns a role constant.
   wxAccStatus GetRole(int childId, wxAccRole *role) override;

   // Gets a variant representing the selected children
   // of this object.
   // Acceptable values:
   // - a null variant (IsNull() returns TRUE)
   // - a list variant (GetType() == wxT("list"))
   // - an integer representing the selected child element,
   //   or 0 if this object is selected (GetType() == wxT("long"))
   // - a "void*" pointer to a wxAccessible child object
   wxAccStatus GetSelections(wxVariant *selections) override;

   // Returns a state constant.
   wxAccStatus GetState(int childId, long* state) override;

   // Returns a localized string representing the value for the object
   // or child.
   wxAccStatus GetValue(int childId, wxString* strValue) override;

};

#endif // wxUSE_ACCESSIBILITY
#if defined __WXMSW__
const int sliderFontSize = 10;
#else
const int sliderFontSize = 12;
#endif

#define OPTIONAL_SLIDER_TICKS

//
// TipWindow
//

class TipWindow final : public wxFrame
{
 public:
   TipWindow(wxWindow *parent, const TranslatableStrings & labels);
   virtual ~TipWindow() {}

   wxSize GetSize() const;
   void SetPos(const wxPoint & pos);
   void SetLabel(const TranslatableString & label);

private:
   void OnPaint(wxPaintEvent & event);

private:
   TranslatableString mLabel;
   int mWidth;
   int mHeight;
   wxFont mFont;

   DECLARE_EVENT_TABLE()
};

BEGIN_EVENT_TABLE(TipWindow, wxFrame)
   EVT_PAINT(TipWindow::OnPaint)
END_EVENT_TABLE()

TipWindow::TipWindow(wxWindow *parent, const TranslatableStrings & labels)
:  wxFrame(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
           wxFRAME_SHAPED | wxNO_BORDER | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT )
{
   SetBackgroundStyle(wxBG_STYLE_PAINT);
   SetBackgroundColour(wxTransparentColour);

   mFont.SetPointSize(sliderFontSize);
   mFont.SetFamily(wxFONTFAMILY_SWISS);
   mFont.SetStyle(wxFONTSTYLE_NORMAL);
   mFont.SetWeight(wxFONTWEIGHT_NORMAL);

   mWidth = mHeight = 0;
   for ( const auto &label : labels ) {
      int width, height;
      GetTextExtent(label.Translation(), &width, &height, NULL, NULL, &mFont);
      mWidth =  std::max( mWidth,  width );
      mHeight = std::max( mHeight, height );
   }

   // Pad to allow for curved corners
   mWidth += 8;
   mHeight += 8;

#if defined(__WXMAC__)
   // Use a bitmap region to set the shape since just adding an unfilled path
   // will make the window transparent
   wxBitmap shape(mWidth, mHeight);
   wxMemoryDC dc(shape);

   dc.SetPen(*wxBLACK_PEN);
   dc.SetBrush(*wxBLACK_BRUSH);
   dc.DrawRoundedRectangle(0, 0, mWidth, mHeight, 5);
   dc.SelectObject(wxNullBitmap);

   SetShape(wxRegion(shape, *wxWHITE));
#else
   wxGraphicsPath path = wxGraphicsRenderer::GetDefaultRenderer()->CreatePath();
   path.AddRoundedRectangle(0, 0, mWidth, mHeight, 5);
   SetShape(path);
#endif
}

wxSize TipWindow::GetSize() const
{
   return wxSize(mWidth, mHeight);
}

void TipWindow::SetPos(const wxPoint & pos)
{
   SetSize(pos.x, pos.y, mWidth, mHeight);
}

void TipWindow::SetLabel(const TranslatableString & label)
{
   mLabel = label;
}

void TipWindow::OnPaint(wxPaintEvent & WXUNUSED(event))
{
   wxAutoBufferedPaintDC dc(this);

   dc.SetPen(*wxBLACK_PEN);
   dc.SetBrush(AColor::tooltipBrush);
   dc.DrawRoundedRectangle(0, 0, mWidth, mHeight, 5);

   dc.SetFont(mFont);
   dc.SetTextForeground(AColor::tooltipPen.GetColour());

   int textWidth, textHeight;
   const auto visibleLabel = mLabel.Translation();
   dc.GetTextExtent(visibleLabel, &textWidth, &textHeight);
   dc.DrawText(visibleLabel, (mWidth - textWidth) / 2, (mHeight - textHeight) / 2);
}

//
// SliderDialog
//

BEGIN_EVENT_TABLE(SliderDialog, wxDialogWrapper)
   EVT_TEXT( wxID_ANY, SliderDialog::OnTextChange )
   EVT_SLIDER(wxID_ANY,SliderDialog::OnSlider)
END_EVENT_TABLE();

SliderDialog::SliderDialog(wxWindow * parent, wxWindowID id,
                           const TranslatableString & title,
                           wxPoint position,
                           wxSize size,
                           int style,
                           float value,
                           float line,
                           float page,
                           LWSlider * pSource):
   wxDialogWrapper(parent,id,title,position),
   mStyle(style)
{
   SetName();
   mpOrigin = pSource;
   mValue = mpOrigin->Get(false);

   auto prec = 2;
   auto trailing = NumValidatorStyle::TWO_TRAILING_ZEROES;
   if (style == DB_SLIDER)
   {
      prec = 1;
      trailing = NumValidatorStyle::ONE_TRAILING_ZERO;
   }
   if (style == SPEED_SLIDER)
   {
      prec = 3;
      trailing = NumValidatorStyle::THREE_TRAILING_ZEROES;
   }

   ShuttleGui S(this, eIsCreating);

   S.StartVerticalLay();
   {
      if (style == PAN_SLIDER)
      {
         mTextCtrl = S
            .Validator<IntegerValidator<float>>(
               &mValue, NumValidatorStyle::DEFAULT, -100.0, 100.0)
            .AddTextBox({}, wxEmptyString, 15);
      }
      else if (style == VEL_SLIDER)
      {
         mTextCtrl = S
            .Validator<IntegerValidator<float>>(
               &mValue, NumValidatorStyle::DEFAULT, -50.0, 50.0)
            .AddTextBox({}, wxEmptyString, 15);
      }
      else if(style == PERCENT_SLIDER)
      {
         mTextCtrl = S
            .Validator<IntegerValidator<float>>(
               &mValue, NumValidatorStyle::DEFAULT, 0.0, 100.0)
            .AddTextBox({}, wxEmptyString, 15);
      }
      else
      {
         mTextCtrl = S
            .Validator<FloatingPointValidator<float>>(
               prec, &mValue, trailing, mpOrigin->GetMinValue(), mpOrigin->GetMaxValue())
            .AddTextBox({}, wxEmptyString, 15);
      }
      mSlider = safenew ASlider(S.GetParent(),
                            wxID_ANY,
                            title,
                            wxDefaultPosition,
                            size,
                            ASlider::Options{}
                               .Style( style ).Line( line ).Page( page ).Popup( false) );
      S.Position(wxEXPAND)
         .AddWindow(mSlider);
   }
   S.EndVerticalLay();

   S.AddStandardButtons(eOkButton | eCancelButton);

   Fit();

   mSlider->Set(value);
}

SliderDialog::~SliderDialog()
{
}

bool SliderDialog::TransferDataToWindow()
{
   float value = mSlider->Get(false);
   mValue = mStyle == PAN_SLIDER || mStyle == PERCENT_SLIDER
      ? value * 100.0
      : value;
   mTextCtrl->GetValidator()->TransferToWindow();
   mTextCtrl->SetSelection(-1, -1);
   if (mpOrigin) {
      mpOrigin->Set(value);
      mpOrigin->SendUpdate(value);
   }

   return true;
}

bool SliderDialog::TransferDataFromWindow()
{
   if (mTextCtrl->GetValidator()->TransferFromWindow())
   {
      float value = mValue;
      if (mStyle == DB_SLIDER)
         value = DB_TO_LINEAR(value);
      else if (mStyle == PAN_SLIDER || mStyle == PERCENT_SLIDER)
         value /= 100.0;
      mSlider->Set(value);
      if (mpOrigin) {
         mpOrigin->Set(value);
         mpOrigin->SendUpdate(value);
      }
   }

   return true;
}

void SliderDialog::OnSlider(wxCommandEvent & event)
{
   TransferDataToWindow();
   event.Skip(false);
}

void SliderDialog::OnTextChange(wxCommandEvent & event)
{
   if (mTextCtrl->GetValidator()->TransferFromWindow())
   {
      TransferDataFromWindow();
   }
   event.Skip(false);
}

float SliderDialog::Get()
{
   return mSlider->Get(false);
}

//
// LWSlider
//

// Define the thumb outline
static const wxPoint2DDouble outer[] =
{
   wxPoint2DDouble(  2,  0 ),
   wxPoint2DDouble(  8,  0 ),
   wxPoint2DDouble( 10,  2 ),
   wxPoint2DDouble( 10,  8 ),
   wxPoint2DDouble(  5, 13 ),
   wxPoint2DDouble(  0,  8 ),
   wxPoint2DDouble(  0,  2 ),
   wxPoint2DDouble(  2,  0 )
};

// Define the left and top interior components when enabled
static const wxPoint2DDouble enabledLeftBegin[] =
{
   wxPoint2DDouble(  2,  1 ),
   wxPoint2DDouble(  1,  2 ),
   wxPoint2DDouble(  1,  8 ),
   wxPoint2DDouble(  4,  4 ),
   wxPoint2DDouble(  4,  7 )
};
static const wxPoint2DDouble enabledLeftEnd[] =
{
   wxPoint2DDouble(  8,  1 ),
   wxPoint2DDouble(  1,  8 ),
   wxPoint2DDouble(  5, 12 ),
   wxPoint2DDouble(  6,  4 ),
   wxPoint2DDouble(  6,  7 )
};

// Define right and bottom interior components when enabled
static const wxPoint2DDouble enabledRightBegin[] =
{
   wxPoint2DDouble(  9,  2 ),
   wxPoint2DDouble(  9,  8 ),
   wxPoint2DDouble(  4,  5 ),
   wxPoint2DDouble(  4,  8 ),
};
static const wxPoint2DDouble enabledRightEnd[] =
{
   wxPoint2DDouble(  9,  8 ),
   wxPoint2DDouble(  6, 11 ),
   wxPoint2DDouble(  6,  5 ),
   wxPoint2DDouble(  6,  8 )
};

// Define the interior stripes when disabled
static const wxPoint2DDouble disabledStripesBegin[] =
{
   wxPoint2DDouble(  3,  2 ),
   wxPoint2DDouble(  5,  2 ),
   wxPoint2DDouble(  7,  2 ),
   wxPoint2DDouble(  2,  3 ),
   wxPoint2DDouble(  2,  5 ),
   wxPoint2DDouble(  2,  7 ),
};
static const wxPoint2DDouble disabledStripesEnd[] =
{
   wxPoint2DDouble(  8,  7 ),
   wxPoint2DDouble(  8,  5 ),
   wxPoint2DDouble(  8,  3 ),
   wxPoint2DDouble(  7,  8 ),
   wxPoint2DDouble(  6,  9 ),
   wxPoint2DDouble(  5, 10 ),
};

// Define the right and bottom interior components when disabled
static const wxPoint2DDouble disabledRightBegin[] =
{
   wxPoint2DDouble(  9,  2 ),
   wxPoint2DDouble(  9,  8 ),
};
static const wxPoint2DDouble disabledRightEnd[] =
{
   wxPoint2DDouble(  9,  8 ),
   wxPoint2DDouble(  6, 11 ),
};

// Construct customizable slider
LWSlider::LWSlider(wxWindow * parent,
                     const TranslatableString &name,
                     const wxPoint &pos,
                     const wxSize &size,
                     float minValue,
                     float maxValue,
                     float stepValue,
                     bool canUseShift,
                     int style,
                     bool showlabels /* = true */,
                     bool drawticks /* = true */,
                     bool drawtrack /* = true */,
                     bool alwayshidetip /* = false */,
                     bool heavyweight /* = false */,
                     bool popup /* = true */,
                     int orientation /* = wxHORIZONTAL */) // wxHORIZONTAL or wxVERTICAL. wxVERTICAL is currently only for DB_SLIDER.
{
   Init(parent, name, pos, size, minValue, maxValue, stepValue,
        canUseShift, style, showlabels, drawticks, drawtrack,
        alwayshidetip, heavyweight, popup, 1.0, orientation);
}

// Construct predefined slider
LWSlider::LWSlider(wxWindow *parent,
                   const TranslatableString &name,
                   const wxPoint &pos,
                   const wxSize &size,
                   int style,
                   bool showlabels /* = true */,
                   bool drawticks /* = true */,
                   bool drawtrack /* = true */,
                   bool alwayshidetip /* = false */,
                   bool heavyweight /* = false */,
                   bool popup /* = true */,
                   int orientation /* = wxHORIZONTAL */) // wxHORIZONTAL or wxVERTICAL. wxVERTICAL is currently only for DB_SLIDER.
{
   wxString leftLabel, rightLabel;

   float minValue, maxValue, stepValue;
   float speed = 1.0;

   switch(style)
   {
   case PAN_SLIDER:
      minValue = -1.0f;
      maxValue = +1.0f;
      stepValue = 0.1f;
      orientation = wxHORIZONTAL; //v Vertical PAN_SLIDER currently not handled, forced to horizontal.
      break;
   case DB_SLIDER:
      minValue = -36.0f;
      //if (orientation == wxHORIZONTAL)
         maxValue = 36.0f;
      //else
         //maxValue = 36.0f; // for MixerBoard //v Previously was 6dB for MixerBoard, but identical for now.
      stepValue = 1.0f;
      speed = 0.5;
      break;
   case FRAC_SLIDER:
   case PERCENT_SLIDER:
      minValue = 0.0f;
      maxValue = 1.0f;
      stepValue = STEP_CONTINUOUS;
      break;
   case SPEED_SLIDER:
      minValue = 0.01f;
      maxValue = 3.000f;
      stepValue = STEP_CONTINUOUS;
      break;
   case VEL_SLIDER:
      minValue = VEL_MIN;
      maxValue = VEL_MAX;
      stepValue = 1.0f;
      speed = 0.5;
      break;
   default:
      minValue = 0.0f;
      maxValue = 1.0f;
      stepValue = 0.0f;
      wxASSERT(false); // undefined style
   }

   Init(parent, name, pos, size, minValue, maxValue, stepValue,
        true, style, showlabels, drawticks, drawtrack, alwayshidetip,
        heavyweight, popup, speed, orientation);
}

void LWSlider::Init(wxWindow * parent,
                    const TranslatableString &name,
                    const wxPoint &pos,
                    const wxSize &size,
                    float minValue,
                    float maxValue,
                    float stepValue,
                    bool canUseShift,
                    int style,
                    bool showlabels,
                    bool drawticks,
                    bool drawtrack,
                    bool alwayshidetip,
                    bool heavyweight,
                    bool popup,
                    float speed,
                    int orientation /* = wxHORIZONTAL */) // wxHORIZONTAL or wxVERTICAL. wxVERTICAL is currently only for DB_SLIDER.
{
   mEnabled = true;
   mName = name;
   mStyle = style;
   mOrientation = orientation;
   mIsDragging = false;
   mParent = parent;
   mShowLabels = showlabels;
   mDrawTicks = drawticks;
   mDrawTrack = drawtrack;
   mAlwaysHideTip = alwayshidetip;
   mHW = heavyweight;
   mPopup = popup;
   mSpeed = speed;
   mID = wxID_ANY;
   mMinValue = minValue;
   mMaxValue = maxValue;
   mStepValue = stepValue;
   mCanUseShift = canUseShift;
   mCurrentValue = 0.0f;
   mDefaultValue = 0.0f;
   mDefaultShortcut = false;
   mBitmap = nullptr;
   mThumbBitmap = nullptr;
   mThumbBitmapHilited = nullptr;
   mScrollLine = 1.0f;
   mScrollPage = 5.0f;

   AdjustSize(size);

   Move(pos);

   CreatePopWin();
}

LWSlider::~LWSlider()
{
}

wxWindowID LWSlider::GetId()
{
   return mID;
}

void LWSlider::SetId(wxWindowID id)
{
   mID = id;
}

void LWSlider::SetName(const TranslatableString& name)
{
   mName = name;
   if(mTipPanel)
   {
      mTipPanel->Destroy();
      mTipPanel = nullptr;
   }
   CreatePopWin();
}

void LWSlider::SetDefaultValue(float value)
{
   SetDefaultShortcut(true);
   mDefaultValue = value;
}

void LWSlider::SetDefaultShortcut(bool value)
{
   mDefaultShortcut = value;
}

void LWSlider::GetScroll(float & line, float & page)
{
   line = mScrollLine;
   page = mScrollPage;
}

void LWSlider::SetScroll(float line, float page)
{
   mScrollLine = line;
   mScrollPage = page;
}

void LWSlider::Move(const wxPoint &newpos)
{
   mLeft = newpos.x;
   mTop = newpos.y;
}

void LWSlider::AdjustSize(const wxSize & sz)
{
   mWidth = sz.GetWidth();
   mHeight = sz.GetHeight();

   if( mBitmap ){
      mBitmap.reset();
   }
   mThumbWidth = 11;
   mThumbHeight = 20;

   if (mShowLabels || mDrawTicks)
   {
      if (mOrientation == wxHORIZONTAL)
      {
         mCenterY = mHeight - 9;
      }
      else
      {
         mCenterX = mWidth - 9;
      }
   }
   else
   {
      if (mOrientation == wxHORIZONTAL)
      {
         mCenterY = mHeight / 2;
      }
      else
      {
         mCenterX = mWidth / 2;
      }
   }

   if (mOrientation == wxHORIZONTAL)
   {
      mLeftX = mThumbWidth/2;
      mRightX = mWidth - mThumbWidth/2 - 1;
      mWidthX = mRightX - mLeftX;
   }
   else
   {
      mTopY = mThumbWidth/2;
      mBottomY = mHeight - mThumbWidth/2 - 1;
      mHeightY = mBottomY - mTopY;
   }

   Refresh();
}

void LWSlider::OnPaint(wxDC &dc, bool highlight)
{
   // The dc will be a paint DC
   if (!mBitmap || !mThumbBitmap || !mThumbBitmapHilited)
   {
      DrawToBitmap(dc);
   }

   //thumbPos should be in pixels
   int thumbPos = ValueToPosition(mCurrentValue);
   int thumbOrtho; // position in axis orthogonal to mOrientation
   if (mOrientation == wxHORIZONTAL){
      thumbOrtho = mCenterY - (mThumbHeight/2);
      thumbPos += 1-mThumbWidth/2;
   }
   else{
      thumbOrtho = mCenterX - (mThumbWidth/2);
      thumbPos += 8-mThumbHeight/2;
   }

   // Draw the background.
   // If we are lightweight, this has already been done for us.
   if( mHW ){
      dc.SetBackground( *wxTRANSPARENT_BRUSH );
      dc.Clear();
   }

   dc.DrawBitmap(*mBitmap, mLeft, mTop, true);
   const auto &thumbBitmap =
      highlight ? *mThumbBitmapHilited : *mThumbBitmap;
   if (mOrientation == wxHORIZONTAL)
   {
      dc.DrawBitmap(thumbBitmap, mLeft+thumbPos, mTop+thumbOrtho, true);
   }
   else
   {
      // TODO: Don't use pixel-count hack in positioning.
      dc.DrawBitmap(thumbBitmap, mLeft+thumbOrtho-5, mTop+thumbPos, true);
   }

   if (mTipPanel)
   {
      mTipPanel->Update();
   }
}

void LWSlider::OnSize( wxSizeEvent & event )
{
   AdjustSize(event.GetSize());

   Refresh();
}

// This function only uses the paintDC to determine what kind of bitmap
// to draw to and nothing else.  It does not draw to the paintDC.
void LWSlider::DrawToBitmap(wxDC & paintDC)
{
   // Get correctly oriented thumb.
   if (mOrientation == wxVERTICAL){
      mThumbBitmap = std::make_unique<wxBitmap>(wxBitmap( theTheme.Bitmap( bmpSliderThumbRotated )));
      mThumbBitmapHilited = std::make_unique<wxBitmap>(wxBitmap( theTheme.Bitmap( bmpSliderThumbRotatedHilited )));
   }
   else {
      mThumbBitmap = std::make_unique<wxBitmap>(wxBitmap( theTheme.Bitmap( bmpSliderThumb )));
      mThumbBitmapHilited = std::make_unique<wxBitmap>(wxBitmap( theTheme.Bitmap( bmpSliderThumbHilited )));
   }

   // Now the background bitmap
   mBitmap = std::make_unique<wxBitmap>();
   mBitmap->Create(mWidth, mHeight, paintDC);

#if defined(__WXMAC__)
   mBitmap->UseAlpha();
#endif

   // Set up the memory DC
   // We draw to it, not the paintDC.
   wxMemoryDC dc;
   dc.SelectObject(*mBitmap);

   dc.SetBackground(wxBrush(mParent->GetBackgroundColour()));
   dc.Clear();

   // Draw the line along which the thumb moves.
   AColor::UseThemeColour(&dc, clrSliderMain );

   if (mDrawTrack)
   {
      if (mOrientation == wxHORIZONTAL)
      {
         AColor::Line(dc, mLeftX, mCenterY, mRightX+2, mCenterY);
         AColor::Line(dc, mLeftX, mCenterY+1, mRightX+2, mCenterY+1);
      }
      else
      {
         AColor::Line(dc, mCenterX, mTopY, mCenterX, mBottomY+2);
         AColor::Line(dc, mCenterX+1, mTopY, mCenterX+1, mBottomY+2);
      }
   }

   if (mShowLabels)
   {
      // Draw +/- or L/R first.  We need to draw these before the tick marks.
      if (mStyle == PAN_SLIDER)
      {
         //VJ Vertical PAN_SLIDER currently not handled, forced to horizontal.

         // sliderFontSize is for the tooltip.
         // we need something smaller here...
         wxFont labelFont(7, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
         dc.SetFont(labelFont);

         // Color
         dc.SetTextForeground(theTheme.Colour(clrTrackPanelText));

         /* i18n-hint: One-letter abbreviation for Left, in the Pan slider */
         dc.DrawText(_("L"), mLeftX, 0);

         /* i18n-hint: One-letter abbreviation for Right, in the Pan slider */
         dc.DrawText(_("R"), mRightX - dc.GetTextExtent(_("R")).GetWidth(), 0);
      }
      else
      {
         // draw the '-' and the '+'
         // These are drawn with lines, rather tha nwith a font.
         AColor::UseThemeColour(&dc, clrTrackPanelText);

         if (mOrientation == wxHORIZONTAL)
         {
            AColor::Line(dc, mLeftX, mCenterY-10, mLeftX+4, mCenterY-10);
            AColor::Line(dc, mRightX-5, mCenterY-10, mRightX-1, mCenterY-10);
            AColor::Line(dc, mRightX-3, mCenterY-12, mRightX-3, mCenterY-8);
         }
         else
         {
            // Vertical DB_SLIDER is for gain slider in MixerBoard.
            // We use a Ruler instead of marks & ticks.
            // Draw '+' and '-' only for other vertical sliders.
            if (mStyle != DB_SLIDER)
            {
               AColor::Line(dc, mCenterX-12, mBottomY-3, mCenterX-8, mBottomY-3);
               AColor::Line(dc, mCenterX-12, mTopY+3, mCenterX-8, mTopY+3);
               AColor::Line(dc, mCenterX-10, mTopY, mCenterX-10, mTopY+5);
            }
         }
      }
   }

   // Use special colour to indicate no ticks.
   wxColour TickColour = theTheme.Colour( clrSliderLight );
   bool bTicks = TickColour != wxColour(60,60,60);

   if( mDrawTicks && bTicks ) {
      // tick marks
      int divs = 10;
      double upp;
      if (mOrientation == wxHORIZONTAL)
      {
         // Bug #2446 - A bit of a hack, but it should suffice.
         divs = (mWidth - 1) / 10;
         upp = divs / (double)(mWidthX-1);
      }
      else
      {
         if (mStyle == DB_SLIDER)
            divs = mMaxValue - mMinValue + 1;
         upp = divs / (double)(mHeightY-1);
      }
#ifdef OPTIONAL_SLIDER_TICKS
      double d = 0.0;
      int int_d = -1;
      const int kMax = (mOrientation == wxHORIZONTAL) ? mWidthX : mHeightY;
      for(int p = 0; p <= kMax; p++)
      {
         if (((int)d) > int_d)
         {
            int_d = (int)d;
            int tickLength = ((int_d == 0) || (int_d == divs)) ? 5: 3; // longer ticks at extremes
            AColor::UseThemeColour(&dc, clrSliderLight );

            if (mOrientation == wxHORIZONTAL)
            {
               AColor::Line(dc, mLeftX+p, mCenterY-tickLength, mLeftX+p, mCenterY-1); // ticks above
            }
            else
            {
               AColor::Line(dc, mCenterX-tickLength, mTopY+p, mCenterX-1, mTopY+p); // ticks at left
            }

            AColor::UseThemeColour(&dc, clrSliderDark );

            if (mOrientation == wxHORIZONTAL)
            {
               AColor::Line(dc, mLeftX+p+1, mCenterY-tickLength+1, mLeftX+p+1, mCenterY-1); // ticks above
            }
            else
            {
               AColor::Line(dc, mCenterX-tickLength+1, mTopY+p+1, mCenterX-1, mTopY+p+1); // ticks at left
            }
         }
         d += upp;
      }
#endif
   }

   dc.SelectObject(wxNullBitmap);

   // safenew, because SetMask takes ownership
   // We always mask.  If we are HeavyWeight, the ASlider draws the
   // background.
   if( !mHW )
   {
      mBitmap->SetMask(safenew wxMask(*mBitmap, dc.GetBackground().GetColour()));
   }
}

void LWSlider::SetToolTipTemplate(const TranslatableString & tip)
{
   mTipTemplate = tip;
}

void LWSlider::ShowTip(bool show)
{
   if(!mTipPanel)
      return;

   if(show)
   {
      mTipPanel->SetLabel(GetTip(mCurrentValue));
      SetPopWinPosition();
      mTipPanel->ShowWithoutActivating();
   }
   else
      mTipPanel->Hide();
}

void LWSlider::CreatePopWin()
{
   if(mTipPanel || mAlwaysHideTip || mParent == nullptr)
      return;

   mTipPanel = safenew TipWindow(mParent, GetWidestTips());
   mTipPanel->Hide();
}

void LWSlider::SetPopWinPosition()
{
   if (mTipPanel)
   {
      wxSize sz = mTipPanel->GetSize();
      wxPoint pt;

      if (mOrientation == wxHORIZONTAL)
      {
         pt.x = mLeft + ((mWidth - sz.x) / 2);
         pt.y = mTop + mHeight + 1;
      }
      else
      {
         pt.x = mLeft + mWidth + 1;
         pt.y = mTop + ((mHeight - sz.y) / 2);
      }

      mTipPanel->SetPos(mParent->ClientToScreen(pt));
   }
}

void LWSlider::FormatPopWin()
{
   if (!mTipPanel)
   {
      return;
   }

   mTipPanel->SetLabel(GetTip(mCurrentValue));
   mTipPanel->Refresh();
}

TranslatableString LWSlider::GetTip(float value) const
{
   TranslatableString label;

   if (mTipTemplate.empty())
   {
      TranslatableString val;

      switch(mStyle)
      {
      case FRAC_SLIDER:
         val = Verbatim("%.2f").Format( value );
         break;
      case PERCENT_SLIDER:
         val = Verbatim("%.0f%%").Format(value * 100.0f);
         break;

      case DB_SLIDER:
         /* i18n-hint dB abbreviates decibels */
         val = XO("%+.1f dB").Format( value );
         break;

      case PAN_SLIDER:
         if (value == 0.0)
         {
            val = XO("Center");
         }
         else
         {
            const auto v = 100.0f * fabsf(value);
            if (value < 0.0)
               /* i18n-hint: Stereo pan setting */
               val = XO("%.0f%% Left").Format( v );
            else
               /* i18n-hint: Stereo pan setting */
               val = XO("%.0f%% Right").Format( v );
         }
         break;

      case SPEED_SLIDER:
         /* i18n-hint: "x" suggests a multiplicative factor */
         val = XO("%.3fx").Format( value );
         break;

      case VEL_SLIDER:
         if (value > 0.0f)
            // Signed
            val = Verbatim("%+d").Format( (int) value );
         else
            // Zero, or signed negative
            val = Verbatim("%d").Format( (int) value );
         break;
      }

      if(!mName.empty())
      {
         /* i18n-hint: An item name followed by a value, with appropriate separating punctuation */
         label = XO("%s: %s").Format( mName, val );
      }
      else
         label = val;
   }
   else
   {
      label = mTipTemplate;
      label.Format( value );
   }

   return label;
}

TranslatableStrings LWSlider::GetWidestTips() const
{
   TranslatableStrings results;

   if (mTipTemplate.empty())
   {
      wxString val;

      switch(mStyle)
      {
      case FRAC_SLIDER:
         results.push_back( GetTip( -1.99f ) );
         results.push_back( GetTip( +1.99f ) );
         break;

      case PERCENT_SLIDER:
         results.push_back(GetTip(1.0f));

      case DB_SLIDER:
         results.push_back( GetTip( -99.9f ) );
         results.push_back( GetTip( +99.9f ) );
         break;

      case PAN_SLIDER:
         // Don't assume we know which of "Left", "Right", or "Center"
         // is the longest string, when localized
         results.push_back( GetTip(  0.f ) );
         results.push_back( GetTip( +1.f ) );
         results.push_back( GetTip( -1.f ) );
         break;

      case SPEED_SLIDER:
         results.push_back( GetTip( 9.99f ) );
         break;

      case VEL_SLIDER:
          results.push_back( GetTip( 999.f ) );
          break;
      }
   }
   else
   {
      results.push_back( GetTip( floor(mMaxValue - mMinValue) + 0.999 ) );
   }

   return results;
}

bool LWSlider::ShowDialog()
{
   return DoShowDialog( mParent->ClientToScreen(wxPoint( mLeft, mTop ) ) );
}

bool LWSlider::ShowDialog(wxPoint pos)
{
   return DoShowDialog( pos );
}

bool LWSlider::DoShowDialog(wxPoint pos)
{
   float value = mCurrentValue;
   bool changed = false;

   SliderDialog dlg( NULL,
                     wxID_ANY,
                     mName,
                     pos,
                     // Bug 2087.  wxMin takes care of case where
                     // slider is vertical (tall and narrow)
                     wxSize( mWidth, wxMin( mWidth, mHeight) ),
                     mStyle,
                     Get(),
                     mScrollLine,
                     mScrollPage,
                     this);
   if (pos == wxPoint(-1, -1)) {
      dlg.CenterOnParent();
      pos = dlg.GetPosition();
   }
   wxRect screenRect = wxDisplay(wxDisplay::GetFromPoint(pos)).GetClientArea();
   int screenOffset = 8;
   int dlgWidth = dlg.GetSize().GetWidth();
   int dlgHeight = dlg.GetSize().GetHeight();

   if (pos.x + dlgWidth > screenRect.GetRight()) {
      pos.x = screenRect.GetRight() - dlgWidth - screenOffset;
   }
   if (pos.x < screenRect.GetLeft()) {
      pos.x = screenRect.GetLeft() + screenOffset;
   }
   if (pos.y + dlgHeight > screenRect.GetBottom()) {
      pos.y = screenRect.GetBottom() - dlgHeight - screenOffset;
   }
   if (pos.y < screenRect.GetTop()) {
      pos.y = screenRect.GetTop() + screenOffset;
   }
   dlg.SetPosition(pos);

   changed = (dlg.ShowModal() == wxID_OK);
   if( changed )
      value = dlg.Get();

   // We now expect the pop up dialog to be
   // sending updates as we go.
   // So this code is needed to possibly restore the old
   // value, on a cancel.
   if (mCurrentValue != value) {
      mCurrentValue = value;
      SendUpdate(value);
   }

   return changed;
}

void LWSlider::OnMouseEvent(wxMouseEvent & event)
{
   if (event.Entering())
   {
      // Display the tooltip in the status bar
      auto tip = GetTip(mCurrentValue);
      auto pProject = FindProjectFromWindow( mParent );
      if (pProject)
         ProjectStatus::Get( *pProject ).Set( tip );
      Refresh();
   }
   else if (event.Leaving())
   {
      if (!mIsDragging)
      {
         ShowTip(false);
      }
      auto pProject = FindProjectFromWindow( mParent );
      if (pProject)
         ProjectStatus::Get( *pProject ).Set({});
      Refresh();
   }

   // Events other than mouse-overs are ignored when we are disabled
   if (!mEnabled)
      return;

   // Windows sends a right button mouse event when you press the context menu
   // key, so ignore it.
   if ((event.RightDown() && !event.RightIsDown()) ||
       (event.RightUp() && event.GetPosition() == wxPoint(-1, -1)))
   {
      event.Skip(false);
      return;
   }

   float prevValue = mCurrentValue;

   // Figure out the thumb position
   wxRect r;
   if (mOrientation == wxHORIZONTAL)
   {
      r.x = mLeft + ValueToPosition(mCurrentValue);
      r.y = mTop + (mCenterY - (mThumbHeight / 2));
   }
   else
   {
      r.x = mLeft + (mCenterX - (mThumbWidth / 2));
      r.y = mTop + ValueToPosition(mCurrentValue);
   }
   r.width = mThumbWidth;
   r.height = mThumbHeight;

   wxRect tolerantThumbRect = r;
   tolerantThumbRect.Inflate(3, 3);

   // Should probably use a right click instead/also
   if( event.ButtonDClick() && mPopup )
   {
      //On a double-click, we should pop up a dialog.
      DoShowDialog(mParent->ClientToScreen(wxPoint(event.m_x,event.m_y)));
   }
   else if( event.ButtonDown() )
   {
      if( mDefaultShortcut && event.ControlDown() )
      {
         mCurrentValue = mDefaultValue;
      }

      if( event.RightDown() ) {
         mParent->SetFocus();
      }

      // Thumb clicked?
      //
      // Do not change position until first drag.  This helps
      // with unintended value changes.
      if( tolerantThumbRect.Contains( event.GetPosition() ) )
      {
         // Remember mouse position and current value
         mClickPos = (mOrientation == wxHORIZONTAL) ? event.m_x : event.m_y;
         mClickValue = mCurrentValue;

         mIsDragging = true;
      }
      // Clicked to set location?
      else
      {
         mCurrentValue =
            ClickPositionToValue(
               (mOrientation == wxHORIZONTAL) ? event.m_x : event.m_y,
               event.ShiftDown());
      }

      if (!mParent->HasCapture()) {
         mParent->CaptureMouse();
      }

      ShowTip(true);
   }
   else if( event.ButtonUp() )
   {
      mIsDragging = false;
      if (mParent->HasCapture())
         mParent->ReleaseMouse();

      ShowTip(false);
   }
   else if (event.Dragging() && mIsDragging)
   {
      if (mOrientation == wxHORIZONTAL)
      {
         if (event.m_y < (r.y - 2 * r.height) ||
             event.m_y > (r.y + 3 * r.height)) {
            // If the mouse y coordinate is relatively far from the slider,
            // snap back to the original position
            mCurrentValue = mClickValue;
         }
         else {
            // Otherwise, move the slider to the right position based
            // on the mouse position
            mCurrentValue = DragPositionToValue(event.m_x, event.ShiftDown());
         }
      }
      else // (mOrientation == wxVERTICAL)
      {
         if (event.m_x < (r.x - 2 * r.width) ||
             event.m_x > (r.x + 3 * r.width)) {
            // If the mouse x coordinate is relatively far from the slider,
            // snap back to the original position
            mCurrentValue = mClickValue;
         }
         else {
            // Otherwise, move the slider to the right position based
            // on the mouse position
            mCurrentValue = DragPositionToValue(event.m_y, event.ShiftDown());
         }
      }
   }
   else if( event.m_wheelRotation != 0 )
   {
      //Calculate the number of steps in a given direction this event
      //represents (allows for two or more clicks on a single event.)
      double steps =  event.m_wheelRotation /
         (event.m_wheelDelta > 0 ? (double)event.m_wheelDelta : 120.0);

      if( steps < 0.0 )
      {
         Decrease( (float)-steps );
      }
      else
      {
         Increase( (float)steps );
      }
      SendUpdate( mCurrentValue );
   }

   if( prevValue != mCurrentValue )
      SendUpdate( mCurrentValue );
}

void LWSlider::OnKeyDown(wxKeyEvent & event)
{
   if (mEnabled)
   {
      switch( event.GetKeyCode() )
      {
         case WXK_TAB:
            mParent->Navigate(event.ShiftDown()
                              ? wxNavigationKeyEvent::IsBackward
                              : wxNavigationKeyEvent::IsForward);
            break;

         case WXK_RIGHT:
         case WXK_UP:
            Increase( mScrollLine );
            SendUpdate( mCurrentValue );
            ShowTip(true);
            break;

         case WXK_LEFT:
         case WXK_DOWN:
            Decrease( mScrollLine );
            SendUpdate( mCurrentValue );
            ShowTip(true);
            break;

         case WXK_PAGEUP:
            Increase( mScrollPage );
            SendUpdate( mCurrentValue );
            ShowTip(true);
            break;

         case WXK_PAGEDOWN:
            Decrease( mScrollPage );
            SendUpdate( mCurrentValue );
            ShowTip(true);
            break;

         case WXK_HOME:
            SendUpdate( mMinValue );
            ShowTip(true);
            break;

         case WXK_END:
            SendUpdate( mMaxValue );
            ShowTip(true);
            break;

         case WXK_RETURN:
         case WXK_NUMPAD_ENTER:
            {
               wxTopLevelWindow *tlw = wxDynamicCast(wxGetTopLevelParent(mParent), wxTopLevelWindow);
               wxWindow *def = tlw->GetDefaultItem();
               if (def && def->IsEnabled()) {
                  wxCommandEvent cevent(wxEVT_COMMAND_BUTTON_CLICKED,
                        def->GetId());
                  cevent.SetEventObject( def );
                  mParent->GetEventHandler()->ProcessEvent(cevent);
               }
            }

         default:
            // Allow it to propagate
            event.Skip();
            break;
      }
   }
   else
   {
      event.Skip();
   }
}

void LWSlider::SetParent(wxWindow* parent)
{
   mParent = parent;
   //VS: create pop win if there is no one, don't re-parent
   //as it seem to be a workaround for DC drawing purposes
   //(see `WaveTrackControls::GainSlider`)
   CreatePopWin();
}

void LWSlider::SendUpdate( float newValue )
{
   mCurrentValue = newValue;

   FormatPopWin();

   Refresh();

   // Update the project's status bar as well
   if (mTipPanel) {
      auto tip = GetTip(mCurrentValue);
      auto pProject = FindProjectFromWindow( mParent );
      if (pProject)
         ProjectStatus::Get( *pProject ).Set( tip );
   }

   wxCommandEvent e( wxEVT_COMMAND_SLIDER_UPDATED, mID );
   int intValue = (int)( ( mCurrentValue - mMinValue ) * 1000.0f /
                         ( mMaxValue - mMinValue ) );
   e.SetInt( intValue );
   mParent->GetEventHandler()->ProcessEvent(e);
}

wxString LWSlider::GetStringValue() const
{
   switch(mStyle)
   {
   case FRAC_SLIDER:
      return wxString::Format(wxT("%.0f"), mCurrentValue * 100);
   case PERCENT_SLIDER:
      return wxString::Format(wxT("%.0f%%"), mCurrentValue * 100.0f);
   case DB_SLIDER:
      return wxString::Format(wxT("%.0f"), mCurrentValue);
   case PAN_SLIDER:
      return wxString::Format(wxT("%.0f"), mCurrentValue * 100);
   case SPEED_SLIDER:
      return wxString::Format(wxT("%.0f"), mCurrentValue * 100 );
   case VEL_SLIDER:
      return wxString::Format(wxT("%.0f"), mCurrentValue);
   default:
      return {};
   }
}

void LWSlider::OnKillFocus()
{
   ShowTip(false);
}


int LWSlider::ValueToPosition(float val)
{
   float fRange = mMaxValue - mMinValue;
   if (mOrientation == wxHORIZONTAL)
      return (int)rint((val - mMinValue) * mWidthX / fRange);
   else
      // low values at bottom
      return (int)rint((mMaxValue - val) * mHeightY / fRange);
}

void LWSlider::SetSpeed(float speed)
{
   mSpeed = speed;
}

// Given the mouse slider coordinate in fromPos, compute the NEW value
// of the slider when clicking to set a NEW position.
float LWSlider::ClickPositionToValue(int fromPos, bool shiftDown)
{
   int nSpan;
   int pos;
   if (mOrientation == wxHORIZONTAL)
   {
      pos = (fromPos - mLeft - (mThumbWidth / 2));
      nSpan = mWidthX;
   }
   else
   {
      // wxVERTICAL => Low values at bottom.
      pos = mBottomY - fromPos;
      nSpan = mHeightY;
   }

   // MM: Special cases: If position is at the very left or the
   // very right (or top/bottom for wxVERTICAL), set minimum/maximum value without other checks
   if (pos <= 0)
      return mMinValue;
   if (pos >= nSpan)
      return mMaxValue;

   float val = (pos / (float)nSpan)
      * (mMaxValue - mMinValue) + mMinValue;

   if (val < mMinValue)
      val = mMinValue;
   if (val > mMaxValue)
      val = mMaxValue;

   if (!(mCanUseShift && shiftDown) && mStepValue != STEP_CONTINUOUS)
   {
      // MM: If shift is not down, or we don't allow usage
      // of shift key at all, trim value to steps of
      // provided size.
      val = (int)(val / mStepValue + 0.5 * (val>0?1.0f:-1.0f)) * mStepValue;
   }

   return val;
}

// Given the mouse slider coordinate in fromPos, compute the NEW value
// of the slider during a drag.
float LWSlider::DragPositionToValue(int fromPos, bool shiftDown)
{
   int delta = (fromPos - mClickPos);

   float speed = mSpeed;
   // Precision enhancement for Shift drags
   if (mCanUseShift && shiftDown)
      speed *= 0.4f;

   // wxVERTICAL => Low values at bottom, so throw in the minus sign here with -mHeightY.
   float denominator = (mOrientation == wxHORIZONTAL) ? mWidthX : -mHeightY;
   float val = mClickValue +
      speed * (delta / denominator) * (mMaxValue - mMinValue);

   if (val < mMinValue)
      val = mMinValue;
   if (val > mMaxValue)
      val = mMaxValue;

   if (!(mCanUseShift && shiftDown) && mStepValue != STEP_CONTINUOUS)
   {
      // MM: If shift is not down, or we don't allow usage
      // of shift key at all, and the slider has not continuous values,
      // trim value to steps of provided size.
      val = (int)(val / mStepValue + 0.5 * (val>0?1.0f:-1.0f)) * mStepValue;
   }

   return val;
}

float LWSlider::Get( bool convert )
{
   if (mStyle == DB_SLIDER)
      return (convert ? DB_TO_LINEAR(mCurrentValue) : mCurrentValue);
   else
      return mCurrentValue;
}

void LWSlider::Set(float value)
{
   if (mIsDragging)
      return;
   if (mStyle == DB_SLIDER)
      mCurrentValue = LINEAR_TO_DB(value);
   else
      mCurrentValue = value;

   if (mCurrentValue < mMinValue)
      mCurrentValue = mMinValue;
   if (mCurrentValue > mMaxValue)
      mCurrentValue = mMaxValue;

   Refresh();
}

void LWSlider::Increase(float steps)
{
   float stepValue = mStepValue;

   if ( stepValue == 0.0 )
   {
      stepValue = ( mMaxValue - mMinValue ) / 10.0;
   }

   mCurrentValue += ( steps * stepValue );

   if ( mCurrentValue < mMinValue )
   {
      mCurrentValue = mMinValue;
   }
   else if ( mCurrentValue > mMaxValue )
   {
      mCurrentValue = mMaxValue;
   }

   Refresh();
}

void LWSlider::Decrease(float steps)
{
   float stepValue = mStepValue;

   if ( stepValue == 0.0 )
   {
      stepValue = ( mMaxValue - mMinValue ) / 10.0;
   }

   mCurrentValue -= ( steps * stepValue );

   if ( mCurrentValue < mMinValue )
   {
      mCurrentValue = mMinValue;
   }
   else if ( mCurrentValue > mMaxValue )
   {
      mCurrentValue = mMaxValue;
   }

   Refresh();
}

void LWSlider::Refresh()
{
   if (mHW)
      mParent->Refresh(false);
}

void LWSlider::Redraw()
{
   mBitmap.reset();
   mThumbBitmap.reset();
   mThumbBitmapHilited.reset();

   Refresh();
}

bool LWSlider::GetEnabled() const
{
   return mEnabled;
}

void LWSlider::SetEnabled(bool enabled)
{
   mEnabled = enabled;

   mThumbBitmap.reset();
   mThumbBitmapHilited.reset();

   Refresh();
}

float LWSlider::GetMinValue() const
{
   return mMinValue;
}

float LWSlider::GetMaxValue() const
{
   return mMaxValue;
}

//
// ASlider
//

BEGIN_EVENT_TABLE(ASlider, wxPanel)
   EVT_KEY_DOWN(ASlider::OnKeyDown)
   EVT_MOUSE_EVENTS(ASlider::OnMouseEvent)
   EVT_MOUSE_CAPTURE_LOST(ASlider::OnCaptureLost)
   EVT_PAINT(ASlider::OnPaint)
   EVT_SIZE(ASlider::OnSize)
   EVT_ERASE_BACKGROUND(ASlider::OnErase)
   EVT_SLIDER(wxID_ANY, ASlider::OnSlider)
   EVT_SET_FOCUS(ASlider::OnSetFocus)
   EVT_KILL_FOCUS(ASlider::OnKillFocus)
   EVT_TIMER(wxID_ANY, ASlider::OnTimer)
END_EVENT_TABLE()

ASlider::ASlider( wxWindow * parent,
                  wxWindowID id,
                  const TranslatableString &name,
                  const wxPoint & pos,
                  const wxSize & size,
                  const Options &options)
: wxPanel( parent, id, pos, size, wxWANTS_CHARS )
{
   //wxColour Col(parent->GetBackgroundColour());
   //SetBackgroundColour( Col );
   SetBackgroundColour( theTheme.Colour( clrMedium ) );
   mLWSlider = std::make_unique<LWSlider>( this,
                             name,
                             wxPoint(0,0),
                             size,
                             options.style,
                             options.showLabels,
                             options.drawTicks,
                             options.drawTrack,
                             options.alwaysHideTip,
                             true, // ASlider is always a heavyweight LWSlider
                             options.popup,
                             options.orientation);
   mLWSlider->mStepValue = options.stepValue;
   mLWSlider->SetId( id );
   SetName( name.Translation() );

   mSliderIsFocused = false;
   mStyle = options.style;

   mTimer.SetOwner(this);

#if wxUSE_ACCESSIBILITY
   SetAccessible( safenew ASliderAx( this ) );
#endif

   mLWSlider->SetScroll( options.line, options.page );
}


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

bool ASlider::SetBackgroundColour(const wxColour& colour)
{
   auto res = wxPanel::SetBackgroundColour(colour);

   if (res && mLWSlider)
   {
      mLWSlider->Redraw();
   }

   return res;
}

void ASlider::OnSlider(wxCommandEvent &event)
{

   if ( event.GetId() == mLWSlider->GetId() )
   {
#if wxUSE_ACCESSIBILITY
      GetAccessible()->NotifyEvent( wxACC_EVENT_OBJECT_VALUECHANGE,
                                    this,
                                    wxOBJID_CLIENT,
                                    wxACC_SELF );
#endif
   }

   event.Skip();
}

void ASlider::OnSize(wxSizeEvent &event)
{
   mLWSlider->OnSize( event );
}

void ASlider::OnErase(wxEraseEvent & WXUNUSED(event))
{
   // Ignore it to prevent flashing
}

void ASlider::OnPaint(wxPaintEvent & WXUNUSED(event))
{
   wxBufferedPaintDC dc(this);

   bool highlighted =
      GetClientRect().Contains(
         ScreenToClient(
            ::wxGetMousePosition() ) );
   mLWSlider->OnPaint(dc, highlighted);

   if ( mSliderIsFocused )
   {
      wxRect r( 0, 0, mLWSlider->mWidth, mLWSlider->mHeight );

      r.Deflate( 1, 1 );

      AColor::DrawFocus( dc, r );
   }
}

void ASlider::OnMouseEvent(wxMouseEvent &event)
{
   if (event.Entering())
   {
      mTimer.StartOnce(1000);
   }
   else if (event.Leaving())
   {
      mTimer.Stop();
   }

   mLWSlider->OnMouseEvent(event);
}

void ASlider::OnCaptureLost(wxMouseCaptureLostEvent & WXUNUSED(event))
{
   wxMouseEvent e(wxEVT_LEFT_UP);
   mLWSlider->OnMouseEvent(e);
}

void ASlider::OnKeyDown(wxKeyEvent &event)
{
   mLWSlider->OnKeyDown(event);
}

void ASlider::OnSetFocus(wxFocusEvent & WXUNUSED(event))
{
   mSliderIsFocused = true;
   Refresh();
}

void ASlider::OnKillFocus(wxFocusEvent & WXUNUSED(event))
{
   mLWSlider->OnKillFocus();
   mSliderIsFocused = false;
   Refresh();
}

void ASlider::OnTimer(wxTimerEvent & WXUNUSED(event))
{
   mLWSlider->ShowTip(true);
}

void ASlider::GetScroll(float & line, float & page)
{
   mLWSlider->GetScroll(line, page);
}

void ASlider::SetScroll(float line, float page)
{
   mLWSlider->SetScroll(line, page);
}

void ASlider::SetToolTipTemplate(const TranslatableString & tip)
{
   mLWSlider->SetToolTipTemplate(tip);
}

float ASlider::Get( bool convert )
{
   return mLWSlider->Get( convert );
}

void ASlider::Set(float value)
{
   mLWSlider->Set(value);
}

void ASlider::Increase(float steps)
{
   mLWSlider->Increase(steps);
}

void ASlider::Decrease(float steps)
{
   mLWSlider->Decrease(steps);
}

bool ASlider::ShowDialog(wxPoint pos)
{
   return mLWSlider->ShowDialog(pos);
}

void ASlider::SetSpeed(float speed)
{
   mLWSlider->SetSpeed(speed);
}

bool ASlider::Enable(bool enable)
{
   if (mLWSlider->GetEnabled() == enable)
      return false;

   mLWSlider->SetEnabled(enable);

   wxWindow::Enable(enable);

   return true;
}

bool ASlider::IsEnabled() const
{
   return mLWSlider->GetEnabled();
}

bool ASlider::s_AcceptsFocus{ false };

auto ASlider::TemporarilyAllowFocus() -> TempAllowFocus {
   s_AcceptsFocus = true;
   return TempAllowFocus{ &s_AcceptsFocus };
}

// This compensates for a but in wxWidgets 3.0.2 for mac:
// Couldn't set focus from keyboard when AcceptsFocus returns false;
// this bypasses that limitation
void ASlider::SetFocusFromKbd()
{
   auto temp = TemporarilyAllowFocus();
   SetFocus();
}

#if wxUSE_ACCESSIBILITY

ASliderAx::ASliderAx( wxWindow * window ) :
   WindowAccessible( window )
{
}

ASliderAx::~ASliderAx()
{
}

// Retrieves the address of an IDispatch interface for the specified child.
// All objects must support this property.
wxAccStatus ASliderAx::GetChild( int childId, wxAccessible** child )
{
   if ( childId == wxACC_SELF )
   {
      *child = this;
   }
   else
   {
      *child = NULL;
   }

   return wxACC_OK;
}

// Gets the number of children.
wxAccStatus ASliderAx::GetChildCount(int* childCount)
{
   *childCount = 3;

   return wxACC_OK;
}

// Gets the default action for this object (0) or > 0 (the action for a child).
// Return wxACC_OK even if there is no action. actionName is the action, or the empty
// string if there is no action.
// The retrieved string describes the action that is performed on an object,
// not what the object does as a result. For example, a toolbar button that prints
// a document has a default action of "Press" rather than "Prints the current document."
wxAccStatus ASliderAx::GetDefaultAction( int WXUNUSED(childId), wxString *actionName )
{
   actionName->clear();

   return wxACC_OK;
}

// Returns the description for this object or a child.
wxAccStatus ASliderAx::GetDescription( int WXUNUSED(childId), wxString *description )
{
   description->clear();

   return wxACC_OK;
}

// Gets the window with the keyboard focus.
// If childId is 0 and child is NULL, no object in
// this subhierarchy has the focus.
// If this object has the focus, child should be 'this'.
wxAccStatus ASliderAx::GetFocus(int* childId, wxAccessible** child)
{
   *childId = 0;
   *child = this;

   return wxACC_OK;
}

// Returns help text for this object or a child, similar to tooltip text.
wxAccStatus ASliderAx::GetHelpText( int WXUNUSED(childId), wxString *helpText )
{
   helpText->clear();

   return wxACC_OK;
}

// Returns the keyboard shortcut for this object or child.
// Return e.g. ALT+K
wxAccStatus ASliderAx::GetKeyboardShortcut( int WXUNUSED(childId), wxString *shortcut )
{
   shortcut->clear();

   return wxACC_OK;
}

// Returns the rectangle for this object (id = 0) or a child element (id > 0).
// rect is in screen coordinates.
wxAccStatus ASliderAx::GetLocation( wxRect& rect, int WXUNUSED(elementId) )
{
   ASlider *as = wxDynamicCast( GetWindow(), ASlider );

   rect = as->GetRect();
   rect.SetPosition( as->GetParent()->ClientToScreen( rect.GetPosition() ) );

   return wxACC_OK;
}

// Gets the name of the specified object.
wxAccStatus ASliderAx::GetName(int WXUNUSED(childId), wxString* name)
{
   ASlider *as = wxDynamicCast( GetWindow(), ASlider );

   *name = as->GetName();

   return wxACC_OK;
}

// Returns a role constant.
wxAccStatus ASliderAx::GetRole(int childId, wxAccRole* role)
{
   switch( childId )
   {
      case 0:
         *role = wxROLE_SYSTEM_SLIDER;
         break;

      case 1:
      case 3:
         *role = wxROLE_SYSTEM_PUSHBUTTON;
         break;

      case 2:
         *role = wxROLE_SYSTEM_INDICATOR;
         break;
   }

   return wxACC_OK;
}

// Gets a variant representing the selected children
// of this object.
// Acceptable values:
// - a null variant (IsNull() returns TRUE)
// - a list variant (GetType() == wxT("list"))
// - an integer representing the selected child element,
//   or 0 if this object is selected (GetType() == wxT("long"))
// - a "void*" pointer to a wxAccessible child object
wxAccStatus ASliderAx::GetSelections( wxVariant * WXUNUSED(selections) )
{
   return wxACC_NOT_IMPLEMENTED;
}

// Returns a state constant.
wxAccStatus ASliderAx::GetState(int childId, long* state)
{
   ASlider *as = wxDynamicCast( GetWindow(), ASlider );

   switch( childId )
   {
      case 0:
         *state = wxACC_STATE_SYSTEM_FOCUSABLE;
         break;

      case 1:
         if ( as->mLWSlider->mCurrentValue == as->mLWSlider->mMinValue )
         {
            *state = wxACC_STATE_SYSTEM_INVISIBLE;
         }
         break;

      case 3:
         if ( as->mLWSlider->mCurrentValue == as->mLWSlider->mMaxValue )
         {
            *state = wxACC_STATE_SYSTEM_INVISIBLE;
         }
         break;
   }

   // Do not use mSliderIsFocused is not set until after this method
   // is called.
   *state |= ( as == wxWindow::FindFocus() ? wxACC_STATE_SYSTEM_FOCUSED : 0 );

   return wxACC_OK;
}

// Returns a localized string representing the value for the object
// or child.
wxAccStatus ASliderAx::GetValue(int childId, wxString* strValue)
{
   ASlider *as = wxDynamicCast( GetWindow(), ASlider );

   if ( childId == 0 )
   {
      *strValue = as->mLWSlider->GetStringValue();
      return wxACC_OK;
   }
   return wxACC_NOT_SUPPORTED;
}

#endif
