text: /**********************************************************************
  
  Audacity: A Digital Audio Editor
  
  LabelTrackView.cpp
  
  Paul Licameli split from TrackPanel.cpp
  
  **********************************************************************/
  
  #include "../../../Audacity.h"
  #include "LabelTrackView.h"
  
  #include "../../../Experimental.h"
  
  #include "LabelTrackVRulerControls.h"
  #include "LabelGlyphHandle.h"
  #include "LabelTextHandle.h"
  
  #include "../../../LabelTrack.h"
  
  #include "../../../AColor.h"
  #include "../../../AllThemeResources.h"
  #include "../../../HitTestResult.h"
  #include "../../../Project.h"
  #include "../../../ProjectHistory.h"
  #include "../../../ProjectSettings.h"
  #include "../../../ProjectWindow.h"
  #include "../../../RefreshCode.h"
  #include "../../../Theme.h"
  #include "../../../TrackArtist.h"
  #include "../../../TrackPanelAx.h"
  #include "../../../TrackPanel.h"
  #include "../../../TrackPanelMouseEvent.h"
  #include "../../../UndoManager.h"
  #include "../../../ViewInfo.h"
  #include "../../../widgets/ErrorDialog.h"
  
  #include <wx/clipbrd.h>
  #include <wx/dcclient.h>
  #include <wx/dcmemory.h>
  #include <wx/font.h>
  #include <wx/frame.h>
  #include <wx/menu.h>
  
  LabelTrackView::LabelTrackView( const std::shared_ptr<Track> &pTrack )
     : CommonTrackView{ pTrack }
  {
     ResetFont();
     CreateCustomGlyphs();
     ResetFlags();
  
     // Events will be emitted by the track
     const auto pLabelTrack = FindLabelTrack();
     BindTo( pLabelTrack.get() );
  }
  
  LabelTrackView::~LabelTrackView()
  {
  }
  
  void LabelTrackView::Reparent( const std::shared_ptr<Track> &parent )
  {
     auto oldParent = FindLabelTrack();
     auto newParent = track_cast<LabelTrack*>(parent.get());
     if (oldParent.get() != newParent) {
        UnbindFrom( oldParent.get() );
        BindTo( newParent );
     }
     CommonTrackView::Reparent( parent );
  }
  
  void LabelTrackView::BindTo( LabelTrack *pParent )
  {
     pParent->Bind(
        EVT_LABELTRACK_ADDITION, &LabelTrackView::OnLabelAdded, this );
     pParent->Bind(
        EVT_LABELTRACK_DELETION, &LabelTrackView::OnLabelDeleted, this );
     pParent->Bind(
        EVT_LABELTRACK_PERMUTED, &LabelTrackView::OnLabelPermuted, this );
     pParent->Bind(
        EVT_LABELTRACK_SELECTION, &LabelTrackView::OnSelectionChange, this );
  }
  
  void LabelTrackView::UnbindFrom( LabelTrack *pParent )
  {
     pParent->Unbind(
        EVT_LABELTRACK_ADDITION, &LabelTrackView::OnLabelAdded, this );
     pParent->Unbind(
        EVT_LABELTRACK_DELETION, &LabelTrackView::OnLabelDeleted, this );
     pParent->Unbind(
        EVT_LABELTRACK_PERMUTED, &LabelTrackView::OnLabelPermuted, this );
     pParent->Unbind(
        EVT_LABELTRACK_SELECTION, &LabelTrackView::OnSelectionChange, this );
  }
  
  void LabelTrackView::CopyTo( Track &track ) const
  {
     TrackView::CopyTo( track );
     auto &other = TrackView::Get( track );
  
     if ( const auto pOther = dynamic_cast< const LabelTrackView* >( &other ) ) {
        // only one field is important to preserve in undo/redo history
        pOther->mSelIndex = mSelIndex;
     }
  }
  
  LabelTrackView &LabelTrackView::Get( LabelTrack &track )
  {
     return static_cast< LabelTrackView& >( TrackView::Get( track ) );
  }
  
  const LabelTrackView &LabelTrackView::Get( const LabelTrack &track )
  {
     return static_cast< const LabelTrackView& >( TrackView::Get( track ) );
  }
  
  std::shared_ptr<LabelTrack> LabelTrackView::FindLabelTrack()
  {
     return std::static_pointer_cast<LabelTrack>( FindTrack() );
  }
  
  std::shared_ptr<const LabelTrack> LabelTrackView::FindLabelTrack() const
  {
     return const_cast<LabelTrackView*>(this)->FindLabelTrack();
  }
  
  std::vector<UIHandlePtr> LabelTrackView::DetailedHitTest
  (const TrackPanelMouseState &st,
   const AudacityProject *WXUNUSED(pProject), int, bool)
  {
     UIHandlePtr result;
     std::vector<UIHandlePtr> results;
     const wxMouseState &state = st.state;
  
     const auto pTrack = FindLabelTrack();
     result = LabelGlyphHandle::HitTest(
        mGlyphHandle, state, pTrack, st.rect);
     if (result)
        results.push_back(result);
  
     result = LabelTextHandle::HitTest(
        mTextHandle, state, pTrack);
     if (result)
        results.push_back(result);
  
     return results;
  }
  
  // static member variables.
  bool LabelTrackView::mbGlyphsReady=false;
  
  wxFont LabelTrackView::msFont;
  
  /// We have several variants of the icons (highlighting).
  /// The icons are draggable, and you can drag one boundary
  /// or all boundaries at the same timecode depending on whether you
  /// click the centre (for all) or the arrow part (for one).
  /// Currently we have twelve variants but we're only using six.
  wxBitmap LabelTrackView::mBoundaryGlyphs[ NUM_GLYPH_CONFIGS * NUM_GLYPH_HIGHLIGHTS ];
  int LabelTrackView::mIconHeight;
  int LabelTrackView::mIconWidth;
  int LabelTrackView::mTextHeight;
  
  int LabelTrackView::mFontHeight=-1;
  
  void LabelTrackView::ResetFlags()
  {
     mInitialCursorPos = 1;
     mCurrentCursorPos = 1;
     mDrawCursor = false;
  }
  
  void LabelTrackView::RestoreFlags( const Flags& flags )
  {
     mInitialCursorPos = flags.mInitialCursorPos;
     mCurrentCursorPos = flags.mCurrentCursorPos;
     mSelIndex = flags.mSelIndex;
     mDrawCursor = flags.mDrawCursor;
  }
  
  wxFont LabelTrackView::GetFont(const wxString &faceName, int size)
  {
     wxFontEncoding encoding;
     if (faceName.empty())
        encoding = wxFONTENCODING_DEFAULT;
     else
        encoding = wxFONTENCODING_SYSTEM;
     return wxFont(size, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL,
                   wxFONTWEIGHT_NORMAL, false, faceName, encoding);
  }
  
  void LabelTrackView::ResetFont()
  {
     mFontHeight = -1;
     wxString facename = gPrefs->Read(wxT("/GUI/LabelFontFacename"), wxT(""));
     int size = gPrefs->Read(wxT("/GUI/LabelFontSize"), DefaultFontSize);
     msFont = GetFont(facename, size);
  }
  
  /// ComputeTextPosition is 'smart' about where to display
  /// the label text.
  ///
  /// The text must be displayed between its endpoints x and x1
  /// We'd also like it centered between them, and we'd like it on
  /// screen.  It isn't always possible to achieve all of this,
  /// so we do the best we can.
  ///
  /// This function has a number of tests and adjustments to the
  /// text start position.  The tests later in the function will
  /// take priority over the ones earlier, so because centering
  /// is the first thing we do, it's the first thing we lose if
  /// we can't do everything we want to.
  void LabelTrackView::ComputeTextPosition(const wxRect & r, int index) const
  {
     const auto pTrack = FindLabelTrack();
     const auto &mLabels = pTrack->GetLabels();
  
     const auto &labelStruct = mLabels[index];
  
     // xExtra is extra space
     // between the text and the endpoints.
     const int xExtra=mIconWidth;
     int x     = labelStruct.x;  // left endpoint
     int x1    = labelStruct.x1; // right endpoint.
     int width = labelStruct.width;
  
     int xText; // This is where the text will end up.
  
     // Will the text all fit at this zoom?
     bool bTooWideForScreen = width > (r.width-2*xExtra);
  // bool bSimpleCentering = !bTooWideForScreen;
     bool bSimpleCentering = false;
  
     //TODO (possibly):
     // Add configurable options as to when to use simple
     // and when complex centering.
     //
     // Simple centering does its best to keep the text
     // centered between the label limits.
     //
     // Complex centering positions the text proportionally
     // to how far we are through the label.
     //
     // If we add preferences for this, we want to be able to
     // choose separately whether:
     //   a) Wide text labels centered simple/complex.
     //   b) Other text labels centered simple/complex.
     //
  
     if( bSimpleCentering )
     {
        // Center text between the two end points.
        xText = (x+x1-width)/2;
     }
     else
     {
        // Calculate xText position to make text line
        // scroll sideways evenly as r moves right.
  
        // xText is a linear function of r.x.
        // These variables are used to compute that function.
        int rx0,rx1,xText0,xText1;
  
        // Since we will be using a linear function,
        // we should blend smoothly between left and right
        // aligned text as r, the 'viewport' moves.
        if( bTooWideForScreen )
        {
           rx0=x;           // when viewport at label start.
           xText0=x+xExtra; // text aligned left.
           rx1=x1-r.width;  // when viewport end at label end
           xText1=x1-(width+xExtra); // text aligned right.
        }
        else
        {
           // when label start + width + extra spacing at viewport end..
           rx0=x-r.width+width+2*xExtra;
           // ..text aligned left.
           xText0=x+xExtra;
           // when viewport start + width + extra spacing at label end..
           rx1=x1-(width+2*xExtra);
           // ..text aligned right.
           xText1=x1-(width+xExtra);
        }
  
        if( rx1 > rx0 ) // Avoid divide by zero case.
        {
           // Compute the blend between left and right aligned.
  
           // Don't use:
           //
           // xText = xText0 + ((xText1-xText0)*(r.x-rx0))/(rx1-rx0);
           //
           // The problem with the above is that it integer-oveflows at
           // high zoom.
  
           // Instead use:
           xText = xText0 + (int)((xText1-xText0)*(((float)(r.x-rx0))/(rx1-rx0)));
        }
        else
        {
           // Avoid divide by zero by reverting to
           // simple centering.
           //
           // We could also fall into this case if x and x1
           // are swapped, in which case we'll end up
           // left aligned anyway because code later on
           // will catch that.
           xText = (x+x1-width)/2;
        }
     }
  
     // Is the text now appearing partly outside r?
     bool bOffLeft = xText < r.x+xExtra;
     bool bOffRight = xText > r.x+r.width-width-xExtra;
  
     // IF any part of the text is offscreen
     // THEN we may bring it back.
     if( bOffLeft == bOffRight )
     {
        //IF both sides on screen, THEN nothing to do.
        //IF both sides off screen THEN don't do
        //anything about it.
        //(because if we did, you'd never get to read
        //all the text by scrolling).
     }
     else if( bOffLeft != bTooWideForScreen)
     {
        // IF we're off on the left, OR we're
        // too wide for the screen and off on the right
        // (only) THEN align left.
        xText = r.x+xExtra;
     }
     else
     {
        // We're off on the right, OR we're
        // too wide and off on the left (only)
        // SO align right.
        xText =r.x+r.width-width-xExtra;
     }
  
     // But if we've taken the text out from its endpoints
     // we must move it back so that it's between the endpoints.
  
     // We test the left end point last because the
     // text might not even fit between the endpoints (at this
     // zoom factor), and in that case we'd like to position
     // the text at the left end point.
     if( xText > (x1-width-xExtra))
        xText=(x1-width-xExtra);
     if( xText < x+xExtra )
        xText=x+xExtra;
  
     labelStruct.xText = xText;
  }
  
  /// ComputeLayout determines which row each label
  /// should be placed on, and reserves space for it.
  /// Function assumes that the labels are sorted.
  void LabelTrackView::ComputeLayout(const wxRect & r, const ZoomInfo &zoomInfo) const
  {
     int xUsed[MAX_NUM_ROWS];
  
     int iRow;
     // Rows are the 'same' height as icons or as the text,
     // whichever is taller.
     const int yRowHeight = wxMax(mTextHeight,mIconHeight)+3;// pixels.
     // Extra space at end of rows.
     // We allow space for one half icon at the start and two
     // half icon widths for extra x for the text frame.
     // [we don't allow half a width space for the end icon since it is
     // allowed to be obscured by the text].
     const int xExtra= (3 * mIconWidth)/2;
  
     bool bAvoidName = false;
     const int nRows = wxMin((r.height / yRowHeight) + 1, MAX_NUM_ROWS);
     if( nRows > 2 )
        bAvoidName = gPrefs->ReadBool(wxT("/GUI/ShowTrackNameInWaveform"), false);
     // Initially none of the rows have been used.
     // So set a value that is less than any valid value.
     {
        // Bug 502: With dragging left of zeros, labels can be in 
        // negative space.  So set least possible value as starting point.
        const int xStart = INT_MIN;
        for (auto &x : xUsed)
           x = xStart;
     }
     int nRowsUsed=0;
  
     const auto pTrack = FindLabelTrack();
     const auto &mLabels = pTrack->GetLabels();
  
     { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
        const int x = zoomInfo.TimeToPosition(labelStruct.getT0(), r.x);
        const int x1 = zoomInfo.TimeToPosition(labelStruct.getT1(), r.x);
        int y = r.y;
  
        labelStruct.x=x;
        labelStruct.x1=x1;
        labelStruct.y=-1;// -ve indicates nothing doing.
        iRow=0;
        // Our first preference is a row that ends where we start.
        // (This is to encourage merging of adjacent label boundaries).
        while( (iRow<nRowsUsed) && (xUsed[iRow] != x ))
           iRow++;
  
        // IF we didn't find one THEN
        // find any row that can take a span starting at x.
        if( iRow >= nRowsUsed )
        {
           iRow=0;
           while( (iRow<nRows) && (xUsed[iRow] > x ))
              iRow++;
        }
        // IF we found such a row THEN record a valid position.
        if( iRow<nRows )
        {
           // Logic to ameliorate case where first label is under the 
           // (on track) track name.  For later labels it does not matter
           // as we can scroll left or right and/or zoom.
           // A possible alternative idea would be to (instead) increase the 
           // translucency of the track name, when the mouse is inside it.
           if( (i==0 ) && (iRow==0) && bAvoidName ){
              // reserve some space in first row.
              // reserve max of 200px or t1, or text box right edge.
              const int x2 = zoomInfo.TimeToPosition(0.0, r.x) + 200;
              xUsed[iRow]=x+labelStruct.width+xExtra;
              if( xUsed[iRow] < x1 ) xUsed[iRow]=x1;
              if( xUsed[iRow] < x2 ) xUsed[iRow]=x2;
              iRow=1;
           }
  
           // Possibly update the number of rows actually used.
           if( iRow >= nRowsUsed )
              nRowsUsed=iRow+1;
           // Record the position for this label
           y= r.y + iRow * yRowHeight +(yRowHeight/2)+1;
           labelStruct.y=y;
           // On this row we have used up to max of end marker and width.
           // Plus also allow space to show the start icon and
           // some space for the text frame.
           xUsed[iRow]=x+labelStruct.width+xExtra;
           if( xUsed[iRow] < x1 ) xUsed[iRow]=x1;
           ComputeTextPosition( r, i );
        }
     }}
  }
  
  /// Draw vertical lines that go exactly through the position
  /// of the start or end of a label.
  ///   @param  dc the device context
  ///   @param  r  the LabelTrack rectangle.
  void LabelTrackView::DrawLines(
     wxDC & dc, const LabelStruct &ls, const wxRect & r)
  {
     auto &x = ls.x;
     auto &x1 = ls.x1;
     auto &y = ls.y;
  
     // How far out from the centre line should the vertical lines
     // start, i.e. what is the y position of the icon?
     // We adjust this so that the line encroaches on the icon
     // slightly (there is white space in the design).
     const int yIconStart = y - (mIconHeight /2)+1+(mTextHeight+3)/2;
     const int yIconEnd   = yIconStart + mIconHeight-2;
  
     // If y is positive then it is the center line for the
     // Label.
     if( y >= 0 )
     {
        if((x  >= r.x) && (x  <= (r.x+r.width)))
        {
           // Draw line above and below left dragging widget.
           AColor::Line(dc, x, r.y,  x, yIconStart - 1);
           AColor::Line(dc, x, yIconEnd, x, r.y + r.height);
        }
        if((x1 >= r.x) && (x1 <= (r.x+r.width)))
        {
           // Draw line above and below right dragging widget.
           AColor::Line(dc, x1, r.y,  x1, yIconStart - 1);
           AColor::Line(dc, x1, yIconEnd, x1, r.y + r.height);
        }
     }
     else
     {
        // Draw the line, even though the widget is off screen
        AColor::Line(dc, x, r.y,  x, r.y + r.height);
        AColor::Line(dc, x1, r.y,  x1, r.y + r.height);
     }
  }
  
  /// DrawGlyphs draws the wxIcons at the start and end of a label.
  ///   @param  dc the device context
  ///   @param  r  the LabelTrack rectangle.
  void LabelTrackView::DrawGlyphs(
     wxDC & dc, const LabelStruct &ls, const wxRect & r,
     int GlyphLeft, int GlyphRight)
  {
     auto &y = ls.y;
  
     const int xHalfWidth=mIconWidth/2;
     const int yStart=y-mIconHeight/2+(mTextHeight+3)/2;
  
     // If y == -1, nothing to draw
     if( y == -1 )
        return;
  
     auto &x = ls.x;
     auto &x1 = ls.x1;
  
     if((x  >= r.x) && (x  <= (r.x+r.width)))
        dc.DrawBitmap(GetGlyph(GlyphLeft), x-xHalfWidth,yStart, true);
     // The extra test commented out here would suppress right hand markers
     // when they overlap the left hand marker (e.g. zoomed out) or to the left.
     if((x1 >= r.x) && (x1 <= (r.x+r.width)) /*&& (x1>x+mIconWidth)*/)
        dc.DrawBitmap(GetGlyph(GlyphRight), x1-xHalfWidth,yStart, true);
  }
  
  /// Draw the text of the label and also draw
  /// a long thin rectangle for its full extent
  /// from x to x1 and a rectangular frame
  /// behind the text itself.
  ///   @param  dc the device context
  ///   @param  r  the LabelTrack rectangle.
  void LabelTrackView::DrawText(wxDC & dc, const LabelStruct &ls, const wxRect & r)
  {
     //If y is positive then it is the center line for the
     //text we are about to draw.
     //if it isn't, nothing to draw.
  
     auto &y = ls.y;
     if( y == -1 )
        return;
  
     // Draw frame for the text...
     // We draw it half an icon width left of the text itself.
     {
        auto &xText = ls.xText;
        const int xStart=wxMax(r.x,xText-mIconWidth/2);
        const int xEnd=wxMin(r.x+r.width,xText+ls.width+mIconWidth/2);
        const int xWidth = xEnd-xStart;
  
        if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
        {
           // Now draw the text itself.
           dc.DrawText(ls.title, xText, y-mTextHeight/2);
        }
     }
  
  }
  
  void LabelTrackView::DrawTextBox(
     wxDC & dc, const LabelStruct &ls, const wxRect & r)
  {
     //If y is positive then it is the center line for the
     //text we are about to draw.
     const int yBarHeight=3;
     const int yFrameHeight = mTextHeight+3;
     const int xBarShorten  = mIconWidth+4;
     auto &y = ls.y;
     if( y == -1 )
        return;
  
     {
        auto &x = ls.x;
        auto &x1 = ls.x1;
        const int xStart=wxMax(r.x,x+xBarShorten/2);
        const int xEnd=wxMin(r.x+r.width,x1-xBarShorten/2);
        const int xWidth = xEnd-xStart;
  
        if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
        {
  
           wxRect bar( xStart,y-yBarHeight/2+yFrameHeight/2,
              xWidth,yBarHeight);
           if( x1 > x+xBarShorten )
              dc.DrawRectangle(bar);
        }
     }
  
     // In drawing the bar and the frame, we compute the clipping
     // to the viewport ourselves.  Under Win98 the GDI does its
     // calculations in 16 bit arithmetic, and so gets it completely
     // wrong at higher zooms where the bar can easily be
     // more than 65536 pixels wide.
  
     // Draw bar for label extent...
     // We don't quite draw from x to x1 because we allow
     // half an icon width at each end.
     {
        auto &xText = ls.xText;
        const int xStart=wxMax(r.x,xText-mIconWidth/2);
        const int xEnd=wxMin(r.x+r.width,xText+ls.width+mIconWidth/2);
        const int xWidth = xEnd-xStart;
  
        if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
        {
            wxRect frame(
              xStart,y-yFrameHeight/2,
              xWidth,yFrameHeight );
           dc.DrawRectangle(frame);
        }
     }
  }
  
  /// Draws text-selected region within the label
  void LabelTrackView::DrawHighlight( wxDC & dc, const LabelStruct &ls,
     int xPos1, int xPos2, int charHeight)
  {
     wxPen curPen = dc.GetPen();
     curPen.SetColour(wxString(wxT("BLUE")));
     wxBrush curBrush = dc.GetBrush();
     curBrush.SetColour(wxString(wxT("BLUE")));
     auto &y = ls.y;
     if (xPos1 < xPos2)
        dc.DrawRectangle(xPos1-1, y-charHeight/2, xPos2-xPos1+1, charHeight);
     else
        dc.DrawRectangle(xPos2-1, y-charHeight/2, xPos1-xPos2+1, charHeight);
  }
  
  namespace {
  void getXPos( const LabelStruct &ls, wxDC & dc, int * xPos1, int cursorPos)
  {
     *xPos1 = ls.xText;
     if( cursorPos > 0)
     {
        int partWidth;
        // Calculate the width of the substring and add it to Xpos
        dc.GetTextExtent(ls.title.Left(cursorPos), &partWidth, NULL);
        *xPos1 += partWidth;
     }
  }
  }
  
  bool LabelTrackView::CalcCursorX( AudacityProject &project, int * x) const
  {
     if ( HasSelection( project ) ) {
        wxMemoryDC dc;
  
        if (msFont.Ok()) {
           dc.SetFont(msFont);
        }
  
        const auto pTrack = FindLabelTrack();
        const auto &mLabels = pTrack->GetLabels();
  
        getXPos(mLabels[mSelIndex], dc, x, mCurrentCursorPos);
        *x += mIconWidth / 2;
        return true;
     }
  
     return false;
  }
  
  void LabelTrackView::CalcHighlightXs(int *x1, int *x2) const
  {
     wxMemoryDC dc;
  
     if (msFont.Ok()) {
        dc.SetFont(msFont);
     }
  
     int pos1 = mInitialCursorPos, pos2 = mCurrentCursorPos;
     if (pos1 > pos2)
        std::swap(pos1, pos2);
  
     const auto pTrack = FindLabelTrack();
     const auto &mLabels = pTrack->GetLabels();
     const auto &labelStruct = mLabels[mSelIndex];
  
     // find the left X pos of highlighted area
     getXPos(labelStruct, dc, x1, pos1);
     // find the right X pos of highlighted area
     getXPos(labelStruct, dc, x2, pos2);
  }
  
  #include "LabelGlyphHandle.h"
  // TODO:  don't rely on the global ::GetActiveProject() to find this.
  // Rather, give TrackPanelCell a drawing function and pass context into it.
  namespace {
     LabelTrackHit *findHit()
     {
        // Fetch the highlighting state
        auto target = TrackPanel::Get( *GetActiveProject() ).Target();
        if (target) {
           auto handle = dynamic_cast<LabelGlyphHandle*>( target.get() );
           if (handle)
              return &*handle->mpHit;
        }
        return nullptr;
     }
  }
  
  #include "../../../TrackPanelDrawingContext.h"
  #include "LabelTextHandle.h"
  
  /// Draw calls other functions to draw the LabelTrack.
  ///   @param  dc the device context
  ///   @param  r  the LabelTrack rectangle.
  void LabelTrackView::Draw
  ( TrackPanelDrawingContext &context, const wxRect & r ) const
  {
     auto &dc = context.dc;
     const auto artist = TrackArtist::Get( context );
     const auto &zoomInfo = *artist->pZoomInfo;
  
     auto pHit = findHit();
  
     if(msFont.Ok())
        dc.SetFont(msFont);
  
     if (mFontHeight == -1)
        calculateFontHeight(dc);
  
     const auto pTrack = std::static_pointer_cast< const LabelTrack >(
        FindTrack()->SubstitutePendingChangedTrack());
     const auto &mLabels = pTrack->GetLabels();
  
     TrackArt::DrawBackgroundWithSelection( context, r, pTrack.get(),
        AColor::labelSelectedBrush, AColor::labelUnselectedBrush,
        ( pTrack->GetSelected() || pTrack->IsSyncLockSelected() ) );
  
     wxCoord textWidth, textHeight;
  
     // Get the text widths.
     // TODO: Make more efficient by only re-computing when a
     // text label title changes.
     for (const auto &labelStruct : mLabels) {
        dc.GetTextExtent(labelStruct.title, &textWidth, &textHeight);
        labelStruct.width = textWidth;
     }
  
     // TODO: And this only needs to be done once, but we
     // do need the dc to do it.
     // We need to set mTextHeight to something sensible,
     // guarding against the case where there are no
     // labels or all are empty strings, which for example
     // happens with a NEW label track.
     dc.GetTextExtent(wxT("Demo Text x^y"), &textWidth, &textHeight);
     mTextHeight = (int)textHeight;
     ComputeLayout( r, zoomInfo );
     dc.SetTextForeground(theTheme.Colour( clrLabelTrackText));
     dc.SetBackgroundMode(wxTRANSPARENT);
     dc.SetBrush(AColor::labelTextNormalBrush);
     dc.SetPen(AColor::labelSurroundPen);
     int GlyphLeft;
     int GlyphRight;
     // Now we draw the various items in this order,
     // so that the correct things overpaint each other.
  
     // Draw vertical lines that show where the end positions are.
     for (const auto &labelStruct : mLabels)
        DrawLines( dc, labelStruct, r );
  
     // Draw the end glyphs.
     { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
        GlyphLeft=0;
        GlyphRight=1;
        if( pHit && i == pHit->mMouseOverLabelLeft )
           GlyphLeft = (pHit->mEdge & 4) ? 6:9;
        if( pHit && i == pHit->mMouseOverLabelRight )
           GlyphRight = (pHit->mEdge & 4) ? 7:4;
        DrawGlyphs( dc, labelStruct, r, GlyphLeft, GlyphRight );
     }}
  
     auto &project = *artist->parent->GetProject();
  
     // Draw the label boxes.
     {
  #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
        bool highlightTrack = false;
        auto target = dynamic_cast<LabelTextHandle*>(context.target.get());
        highlightTrack = target && target->GetTrack().get() == this;
  #endif
        int i = -1; for (const auto &labelStruct : mLabels) { ++i;
           bool highlight = false;
  #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
           highlight = highlightTrack && target->GetLabelNum() == i;
  #endif
           bool selected = GetSelectedIndex( project ) == i;
  
           if( selected )
              dc.SetBrush( AColor::labelTextEditBrush );
           else if ( highlight )
              dc.SetBrush( AColor::uglyBrush );
           DrawTextBox( dc, labelStruct, r );
  
           if (highlight || selected)
              dc.SetBrush(AColor::labelTextNormalBrush);
        }
     }
  
     // Draw highlights
     if ( (mInitialCursorPos != mCurrentCursorPos) && HasSelection( project ) )
     {
        int xpos1, xpos2;
        CalcHighlightXs(&xpos1, &xpos2);
        DrawHighlight(dc, mLabels[mSelIndex],
           xpos1, xpos2, mFontHeight);
     }
  
     // Draw the text and the label boxes.
     { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
        if( GetSelectedIndex( project ) == i )
           dc.SetBrush(AColor::labelTextEditBrush);
        DrawText( dc, labelStruct, r );
        if( GetSelectedIndex( project ) == i )
           dc.SetBrush(AColor::labelTextNormalBrush);
     }}
  
     // Draw the cursor, if there is one.
     if( mDrawCursor && HasSelection( project ) )
     {
        const auto &labelStruct = mLabels[mSelIndex];
        int xPos = labelStruct.xText;
  
        if( mCurrentCursorPos > 0)
        {
           // Calculate the width of the substring and add it to Xpos
           int partWidth;
           dc.GetTextExtent(labelStruct.title.Left(mCurrentCursorPos), &partWidth, NULL);
           xPos += partWidth;
        }
  
        wxPen currentPen = dc.GetPen();
        const int CursorWidth=2;
        currentPen.SetWidth(CursorWidth);
        AColor::Line(dc,
                     xPos-1, labelStruct.y - mFontHeight/2 + 1,
                     xPos-1, labelStruct.y + mFontHeight/2 - 1);
        currentPen.SetWidth(1);
     }
  }
  
  void LabelTrackView::Draw(
     TrackPanelDrawingContext &context,
     const wxRect &rect, unsigned iPass )
  {
     if ( iPass == TrackArtist::PassTracks )
        Draw( context, rect );
     CommonTrackView::Draw( context, rect, iPass );
  }
  
  void LabelTrackView::SetSelectedIndex( int index )
  {
     if ( index >= 0 && index < (int)FindLabelTrack()->GetLabels().size() )
        mSelIndex = index;
     else
        mSelIndex = -1;
  }
  
  /// uses GetTextExtent to find the character position
  /// corresponding to the x pixel position.
  int LabelTrackView::FindCursorPosition(wxCoord xPos)
  {
     int result = -1;
     wxMemoryDC dc;
     if(msFont.Ok())
        dc.SetFont(msFont);
  
     // A bool indicator to see if set the cursor position or not
     bool finished = false;
     int charIndex = 1;
     int partWidth;
     int oneWidth;
     double bound;
     wxString subString;
  
     const auto pTrack = FindLabelTrack();
     const auto &mLabels = pTrack->GetLabels();
     const auto &labelStruct = mLabels[mSelIndex];
     const auto &title = labelStruct.title;
     const int length = title.length();
     while (!finished && (charIndex < length + 1))
     {
        subString = title.Left(charIndex);
        // Get the width of substring
        dc.GetTextExtent(subString, &partWidth, NULL);
  
        // Get the width of the last character
        dc.GetTextExtent(subString.Right(1), &oneWidth, NULL);
        bound = labelStruct.xText + partWidth - oneWidth * 0.5;
  
        if (xPos <= bound)
        {
           // Found
           result = charIndex - 1;
           finished = true;
        }
        else
        {
           // Advance
           charIndex++;
        }
     }
     if (!finished)
        // Cursor should be in the last position
        result = length;
  
     return result;
  }
  
  void LabelTrackView::SetCurrentCursorPosition(int pos)
  {
     mCurrentCursorPos = pos;
  }
  
  void LabelTrackView::SetTextHighlight(
     int initialPosition, int currentPosition )
  {
     mInitialCursorPos = initialPosition;
     mCurrentCursorPos = currentPosition;
     mDrawCursor = true;
  }
  
  void LabelTrackView::calculateFontHeight(wxDC & dc)
  {
     int charDescent;
     int charLeading;
  
     // Calculate the width of the substring and add it to Xpos
     dc.GetTextExtent(wxT("(Test String)|[yp]"), NULL, &mFontHeight, &charDescent, &charLeading);
  
     // The cursor will have height charHeight.  We don't include the descender as
     // part of the height because for phonetic fonts this leads to cursors which are
     // too tall.  We don't include leading either - it is usually 0.
     // To make up for ignoring the descender height, we add one pixel above and below
     // using CursorExtraHeight so that the cursor is just a little taller than the
     // body of the characters.
     const int CursorExtraHeight=2;
     mFontHeight += CursorExtraHeight - (charLeading+charDescent);
  }
  
  bool LabelTrackView::IsTextSelected( AudacityProject &project ) const
  {
     if ( !HasSelection( project ) )
        return false;
     if (mCurrentCursorPos == mInitialCursorPos)
        return false;
     return true;
  }
  
  /// Cut the selected text in the text box
  ///  @return true if text is selected in text box, false otherwise
  bool LabelTrackView::CutSelectedText( AudacityProject &project )
  {
     if (!IsTextSelected( project ))
        return false;
  
     const auto pTrack = FindLabelTrack();
     const auto &mLabels = pTrack->GetLabels();
  
     wxString left, right;
     auto labelStruct = mLabels[mSelIndex];
     auto &text = labelStruct.title;
  
     int init = mInitialCursorPos;
     int cur = mCurrentCursorPos;
     if (init > cur)
        std::swap(init, cur);
  
     // data for cutting
     wxString data = text.Mid(init, cur - init);
  
     // get left-remaining text
     if (init > 0)
        left = text.Left(init);
  
     // get right-remaining text
     if (cur < (int)text.length())
        right = text.Mid(cur);
  
     // set title to the combination of the two remainders
     text = left + right;
  
     pTrack->SetLabel( mSelIndex, labelStruct );
  
     // copy data onto clipboard
     if (wxTheClipboard->Open()) {
        // Clipboard owns the data you give it
        wxTheClipboard->SetData(safenew wxTextDataObject(data));
        wxTheClipboard->Close();
     }
  
     // set cursor positions
     mInitialCursorPos = mCurrentCursorPos = left.length();
     return true;
  }
  
  /// Copy the selected text in the text box
  ///  @return true if text is selected in text box, false otherwise
  bool LabelTrackView::CopySelectedText( AudacityProject &project )
  {
     if ( !HasSelection( project ) )
        return false;
  
     const auto pTrack = FindLabelTrack();
     const auto &mLabels = pTrack->GetLabels();
  
     const auto &labelStruct = mLabels[mSelIndex];
  
     int init = mInitialCursorPos;
     int cur = mCurrentCursorPos;
     if (init > cur)
        std::swap(init, cur);
  
     if (init == cur)
        return false;
  
     // data for copying
     wxString data = labelStruct.title.Mid(init, cur-init);
  
     // copy the data on clipboard
     if (wxTheClipboard->Open()) {
        // Clipboard owns the data you give it
        wxTheClipboard->SetData(safenew wxTextDataObject(data));
        wxTheClipboard->Close();
     }
  
     return true;
  }
  
  // PRL:  should this set other fields of the label selection?
  /// Paste the text on the clipboard to text box
  ///  @return true if mouse is clicked in text box, false otherwise
  bool LabelTrackView::PasteSelectedText(
     AudacityProject &project, double sel0, double sel1 )
  {
     const auto pTrack = FindLabelTrack();
  
     if ( !HasSelection( project ) )
        AddLabel(SelectedRegion(sel0, sel1));
  
     wxString text, left, right;
  
     // if text data is available
     if (IsTextClipSupported()) {
        if (wxTheClipboard->Open()) {
           wxTextDataObject data;
           wxTheClipboard->GetData(data);
           wxTheClipboard->Close();
           text = data.GetText();
        }
  
        // Convert control characters to blanks
        for (int i = 0; i < (int)text.length(); i++) {
           if (wxIscntrl(text[i])) {
              text[i] = wxT(' ');
           }
        }
     }
  
     const auto &mLabels = pTrack->GetLabels();
     auto labelStruct = mLabels[mSelIndex];
     auto &title = labelStruct.title;
     int cur = mCurrentCursorPos, init = mInitialCursorPos;
     if (init > cur)
        std::swap(init, cur);
     left = title.Left(init);
     if (cur < (int)title.length())
        right = title.Mid(cur);
  
     title = left + text + right;
  
     pTrack->SetLabel( mSelIndex, labelStruct );
  
     mInitialCursorPos =  mCurrentCursorPos = left.length() + text.length();
     return true;
  }
  
  /// @return true if the text data is available in the clipboard, false otherwise
  bool LabelTrackView::IsTextClipSupported()
  {
     return wxTheClipboard->IsSupported(wxDF_TEXT);
  }
  
  
  int LabelTrackView::GetSelectedIndex( AudacityProject &project ) const
  {
     // may make delayed update of mutable mSelIndex after track selection change
     auto track = FindLabelTrack();
     if ( track->GetSelected() ||
        TrackFocus::Get(
           // unhappy const_cast because because focus may be set if undefined
           const_cast<AudacityProject&>( project )
        ).Get() == track.get()
     )
        return mSelIndex = std::max( -1,
           std::min<int>( track->GetLabels().size() - 1, mSelIndex ) );
     else
        return mSelIndex = -1;
  }
  
  /// TODO: Investigate what happens with large
  /// numbers of labels, might need a binary search
  /// rather than a linear one.
  void LabelTrackView::OverGlyph(
     const LabelTrack &track, LabelTrackHit &hit, int x, int y)
  {
     //Determine the NEW selection.
     int result=0;
     const int d1=10; //distance in pixels, used for have we hit drag handle.
     const int d2=5;  //distance in pixels, used for have we hit drag handle center.
  
     //If not over a label, reset it
     hit.mMouseOverLabelLeft  = -1;
     hit.mMouseOverLabelRight = -1;
     hit.mEdge = 0;
  
     const auto pTrack = &track;
     const auto &mLabels = pTrack->GetLabels();
     { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
        //over left or right selection bound
        //Check right bound first, since it is drawn after left bound,
        //so give it precedence for matching/highlighting.
        if( abs(labelStruct.y - (y - (mTextHeight+3)/2)) < d1 &&
                 abs(labelStruct.x1 - d2 -x) < d1)
        {
           hit.mMouseOverLabelRight = i;
           if(abs(labelStruct.x1 - x) < d2 )
           {
              result |= 4;
              // If left and right co-incident at this resolution, then we drag both.
              // We could be a little less stringent about co-incidence here if we liked.
              if( abs(labelStruct.x1-labelStruct.x) < 1.0 )
              {
                 result |=1;
                 hit.mMouseOverLabelLeft = i;
              }
           }
           result |= 2;
        }
        // Use else-if here rather than else to avoid detecting left and right
        // of the same label.
        else if(   abs(labelStruct.y - (y - (mTextHeight+3)/2)) < d1 &&
              abs(labelStruct.x + d2 - x) < d1 )
        {
           hit.mMouseOverLabelLeft = i;
           if(abs(labelStruct.x - x) < d2 )
              result |= 4;
           result |= 1;
        }
  
        // give text box better priority for selecting
        if(OverTextBox(&labelStruct, x, y))
        {
           result = 0;
        }
  
     }}
     hit.mEdge = result;
  }
  
  int LabelTrackView::OverATextBox( const LabelTrack &track, int xx, int yy )
  {
     const auto pTrack = &track;
     const auto &mLabels = pTrack->GetLabels();
     for (int nn = (int)mLabels.size(); nn--;) {
        const auto &labelStruct = mLabels[nn];
        if ( OverTextBox( &labelStruct, xx, yy ) )
           return nn;
     }
  
     return -1;
  }
  
  // return true if the mouse is over text box, false otherwise
  bool LabelTrackView::OverTextBox(const LabelStruct *pLabel, int x, int y)
  {
     if( (pLabel->xText-(mIconWidth/2) < x) &&
              (x<pLabel->xText+pLabel->width+(mIconWidth/2)) &&
              (abs(pLabel->y-y)<mIconHeight/2))
     {
        return true;
     }
     return false;
  }
  
  /// Returns true for keys we capture to start a label.
  static bool IsGoodLabelFirstKey(const wxKeyEvent & evt)
  {
     int keyCode = evt.GetKeyCode();
     return (keyCode < WXK_START
                    && keyCode != WXK_SPACE && keyCode != WXK_DELETE && keyCode != WXK_RETURN) ||
            (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) ||
            (keyCode >= WXK_NUMPAD_EQUAL && keyCode <= WXK_NUMPAD_DIVIDE) ||
  #if defined(__WXMAC__)
            (keyCode > WXK_RAW_CONTROL) ||
  #endif
            (keyCode > WXK_WINDOWS_MENU);
  }
  
  /// This returns true for keys we capture for label editing.
  static bool IsGoodLabelEditKey(const wxKeyEvent & evt)
  {
     int keyCode = evt.GetKeyCode();
  
     // Accept everything outside of WXK_START through WXK_COMMAND, plus the keys
     // within that range that are usually printable, plus the ones we use for
     // keyboard navigation.
     return keyCode < WXK_START ||
            (keyCode >= WXK_END && keyCode < WXK_UP) ||
            (keyCode == WXK_RIGHT) ||
            (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) ||
            (keyCode >= WXK_NUMPAD_SPACE && keyCode <= WXK_NUMPAD_ENTER) ||
            (keyCode >= WXK_NUMPAD_HOME && keyCode <= WXK_NUMPAD_END) ||
            (keyCode >= WXK_NUMPAD_DELETE && keyCode <= WXK_NUMPAD_DIVIDE) ||
  #if defined(__WXMAC__)
            (keyCode > WXK_RAW_CONTROL) ||
  #endif
            (keyCode > WXK_WINDOWS_MENU);
  }
  
  // Check for keys that we will process
  bool LabelTrackView::DoCaptureKey(
     AudacityProject &project, wxKeyEvent & event )
  {
     // Check for modifiers and only allow shift
     int mods = event.GetModifiers();
     if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
        return false;
     }
  
     // Always capture the navigation keys, if we have any labels
     auto code = event.GetKeyCode();
     const auto pTrack = FindLabelTrack();
     const auto &mLabels = pTrack->GetLabels();
     if ((code == WXK_TAB || code == WXK_NUMPAD_TAB) &&
         !mLabels.empty())
        return true;
  
     if ( HasSelection( project ) ) {
        if (IsGoodLabelEditKey(event)) {
           return true;
        }
     }
     else {
        bool typeToCreateLabel;
        gPrefs->Read(wxT("/GUI/TypeToCreateLabel"), &typeToCreateLabel, false);
        if (IsGoodLabelFirstKey(event) && typeToCreateLabel) {
  
  
  // The commented out code can prevent label creation, causing bug 1551
  // We should only be in DoCaptureKey IF this label track has focus,
  // and in that case creating a Label is the expected/intended thing.
  #if 0
           // If we're playing, don't capture if the selection is the same as the
           // playback region (this helps prevent label track creation from
           // stealing unmodified kbd. shortcuts)
           auto gAudioIO = AudioIOBase::Get();
           if (pProj->GetAudioIOToken() > 0 &&
                 gAudioIO->IsStreamActive(pProj->GetAudioIOToken()))
           {
              double t0, t1;
              pProj->GetPlayRegion(&t0, &t1);
              if (pProj->mViewInfo.selectedRegion.t0() == t0 &&
                  pProj->mViewInfo.selectedRegion.t1() == t1) {
                 return false;
              }
           }
  #endif
  
           // If there's a label there already don't capture
           auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
           if( GetLabelIndex(selectedRegion.t0(),
                             selectedRegion.t1()) != wxNOT_FOUND ) {
              return false;
           }
  
           return true;
        }
     }
  
     return false;
  }
  
  unsigned LabelTrackView::CaptureKey(
     wxKeyEvent & event, ViewInfo &, wxWindow *, AudacityProject *project )
  {
     event.Skip(!DoCaptureKey( *project, event ));
     return RefreshCode::RefreshNone;
  }
  
  unsigned LabelTrackView::KeyDown(
     wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *WXUNUSED(pParent),
     AudacityProject *project)
  {
     double bkpSel0 = viewInfo.selectedRegion.t0(),
        bkpSel1 = viewInfo.selectedRegion.t1();
  
     // Pass keystroke to labeltrack's handler and add to history if any
     // updates were done
     if (DoKeyDown( *project, viewInfo.selectedRegion, event )) {
        ProjectHistory::Get( *project ).PushState(XO("Modified Label"),
           XO("Label Edit"),
           UndoPush::CONSOLIDATE);
     }
  
     // Make sure caret is in view
     int x;
     if (CalcCursorX( *project, &x ))
        ProjectWindow::Get( *project ).ScrollIntoView(x);
  
     // If selection modified, refresh
     // Otherwise, refresh track display if the keystroke was handled
     if (bkpSel0 != viewInfo.selectedRegion.t0() ||
        bkpSel1 != viewInfo.selectedRegion.t1())
        return RefreshCode::RefreshAll;
     else if (!event.GetSkipped())
        return  RefreshCode::RefreshCell;
  
     return RefreshCode::RefreshNone;
  }
  
  unsigned LabelTrackView::Char(
     wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *, AudacityProject *project)
  {
     double bkpSel0 = viewInfo.selectedRegion.t0(),
        bkpSel1 = viewInfo.selectedRegion.t1();
     // Pass keystroke to labeltrack's handler and add to history if any
     // updates were done
  
     if (DoChar( *project, viewInfo.selectedRegion, event ))
        ProjectHistory::Get( *project ).PushState(XO("Modified Label"),
           XO("Label Edit"),
        UndoPush::CONSOLIDATE);
  
     // If selection modified, refresh
     // Otherwise, refresh track display if the keystroke was handled
     if (bkpSel0 != viewInfo.selectedRegion.t0() ||
        bkpSel1 != viewInfo.selectedRegion.t1())
        return RefreshCode::RefreshAll;
     else if (!event.GetSkipped())
        return RefreshCode::RefreshCell;
  
     return RefreshCode::RefreshNone;
  }
  
  /// KeyEvent is called for every keypress when over the label track.
  bool LabelTrackView::DoKeyDown(
     AudacityProject &project, NotifyingSelectedRegion &newSel, wxKeyEvent & event)
  {
     // Only track true changes to the label
     bool updated = false;
  
     // Cache the keycode
     int keyCode = event.GetKeyCode();
     const int mods = event.GetModifiers();
  
     // Check for modifiers and only allow shift
     if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
        event.Skip();
        return updated;
     }
  
     // All editing keys are only active if we're currently editing a label
     const auto pTrack = FindLabelTrack();
     const auto &mLabels = pTrack->GetLabels();
     if ( HasSelection( project ) ) {
        auto labelStruct = mLabels[mSelIndex];
        auto &title = labelStruct.title;
        switch (keyCode) {
  
        case WXK_BACK:
           {
              int len = title.length();
  
              //IF the label is not blank THEN get rid of a letter or letters according to cursor position
              if (len > 0)
              {
                 // IF there are some highlighted letters, THEN DELETE them
                 if (mInitialCursorPos != mCurrentCursorPos)
                    RemoveSelectedText();
                 else
                 {
                    // DELETE one letter
                    if (mCurrentCursorPos > 0) {
                       title.erase(mCurrentCursorPos-1, 1);
                       mCurrentCursorPos--;
                       pTrack->SetLabel(mSelIndex, labelStruct);
                    }
                 }
              }
              else
              {
                 // ELSE no text in text box, so DELETE whole label.
                 pTrack->DeleteLabel( mSelIndex );
              }
              mInitialCursorPos = mCurrentCursorPos;
              updated = true;
           }
           break;
  
        case WXK_DELETE:
        case WXK_NUMPAD_DELETE:
           {
              int len = title.length();
  
              //If the label is not blank get rid of a letter according to cursor position
              if (len > 0)
              {
                 // if there are some highlighted letters, DELETE them
                 if (mInitialCursorPos != mCurrentCursorPos)
                    RemoveSelectedText();
                 else
                 {
                    // DELETE one letter
                    if (mCurrentCursorPos < len) {
                       title.erase(mCurrentCursorPos, 1);
                       pTrack->SetLabel(mSelIndex, labelStruct);
                    }
                 }
              }
              else
              {
                 // DELETE whole label if no text in text box
                 pTrack->DeleteLabel( mSelIndex );
              }
              mInitialCursorPos = mCurrentCursorPos;
              updated = true;
           }
           break;
  
        case WXK_HOME:
        case WXK_NUMPAD_HOME:
           // Move cursor to beginning of label
           mCurrentCursorPos = 0;
           if (mods == wxMOD_SHIFT)
              ;
           else
              mInitialCursorPos = mCurrentCursorPos;
           break;
  
        case WXK_END:
        case WXK_NUMPAD_END:
           // Move cursor to end of label
           mCurrentCursorPos = (int)title.length();
           if (mods == wxMOD_SHIFT)
              ;
           else
              mInitialCursorPos = mCurrentCursorPos;
           break;
  
        case WXK_LEFT:
        case WXK_NUMPAD_LEFT:
           // Moving cursor left
           if (mCurrentCursorPos > 0) {
              mCurrentCursorPos--;
              if (mods == wxMOD_SHIFT)
                 ;
              else
                 mInitialCursorPos = mCurrentCursorPos =
                    std::min(mInitialCursorPos, mCurrentCursorPos);
           }
           break;
  
        case WXK_RIGHT:
        case WXK_NUMPAD_RIGHT:
           // Moving cursor right
           if (mCurrentCursorPos < (int)title.length()) {
              mCurrentCursorPos++;
              if (mods == wxMOD_SHIFT)
                 ;
              else
                 mInitialCursorPos = mCurrentCursorPos =
                    std::max(mInitialCursorPos, mCurrentCursorPos);
           }
           break;
  
        case WXK_RETURN:
        case WXK_NUMPAD_ENTER:
  
        case WXK_ESCAPE:
           if (mRestoreFocus >= 0) {
              auto track = *TrackList::Get( project ).Any()
                 .begin().advance(mRestoreFocus);
              if (track)
                 TrackFocus::Get( project ).Set(track);
              mRestoreFocus = -2;
           }
           mSelIndex = -1;
           break;
  
        case WXK_TAB:
        case WXK_NUMPAD_TAB:
           if (event.ShiftDown()) {
                 mSelIndex--;
           } else {
                 mSelIndex++;
           }
  
           mSelIndex = (mSelIndex + (int)mLabels.size()) % (int)mLabels.size();    // wrap round if necessary
           {
              const auto &newLabel = mLabels[mSelIndex];
              mCurrentCursorPos = newLabel.title.length();
              mInitialCursorPos = mCurrentCursorPos;
              //Set the selection region to be equal to the selection bounds of the tabbed-to label.
              newSel = newLabel.selectedRegion;
           }
           break;
  
        case '\x10':   // OSX
        case WXK_MENU:
        case WXK_WINDOWS_MENU:
           ShowContextMenu( project );
           break;
  
        default:
           if (!IsGoodLabelEditKey(event)) {
              event.Skip();
           }
           break;
        }
     }
     else
     {
        switch (keyCode) {
  
        case WXK_TAB:
        case WXK_NUMPAD_TAB:
           if (!mLabels.empty()) {
              int len = (int) mLabels.size();
              if (event.ShiftDown()) {
                 mSelIndex = len - 1;
                 if (newSel.t0() > mLabels[0].getT0()) {
                    while (mSelIndex >= 0 &&
                           mLabels[mSelIndex].getT0() > newSel.t0()) {
                       mSelIndex--;
                    }
                 }
              } else {
                 mSelIndex = 0;
                 if (newSel.t0() < mLabels[len - 1].getT0()) {
                    while (mSelIndex < len &&
                           mLabels[mSelIndex].getT0() < newSel.t0()) {
                       mSelIndex++;
                    }
                 }
              }
  
              if (mSelIndex >= 0 && mSelIndex < len) {
                 const auto &labelStruct = mLabels[mSelIndex];
                 mCurrentCursorPos = labelStruct.title.length();
                 mInitialCursorPos = mCurrentCursorPos;
                 //Set the selection region to be equal to the selection bounds of the tabbed-to label.
                 newSel = labelStruct.selectedRegion;
              }
              else {
                 mSelIndex = -1;
              }
           }
           break;
  
        default:
           if (!IsGoodLabelFirstKey(event)) {
              event.Skip();
           }
           break;
        }
     }
  
     // Make sure the caret is visible
     mDrawCursor = true;
  
     return updated;
  }
  
  /// OnChar is called for incoming characters -- that's any keypress not handled
  /// by OnKeyDown.
  bool LabelTrackView::DoChar(
     AudacityProject &project, NotifyingSelectedRegion &WXUNUSED(newSel),
     wxKeyEvent & event)
  {
     // Check for modifiers and only allow shift.
     //
     // We still need to check this or we will eat the top level menu accelerators
     // on Windows if our capture or key down handlers skipped the event.
     const int mods = event.GetModifiers();
     if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
        event.Skip();
        return false;
     }
  
     // Only track true changes to the label
     bool updated = false;
  
     // Cache the character
     wxChar charCode = event.GetUnicodeKey();
  
     // Skip if it's not a valid unicode character or a control character
     if (charCode == 0 || wxIscntrl(charCode)) {
        event.Skip();
        return false;
     }
     
     // If we've reached this point and aren't currently editing, add NEW label
     const auto pTrack = FindLabelTrack();
     if ( !HasSelection( project ) ) {
        // Don't create a NEW label for a space
        if (wxIsspace(charCode)) {
           event.Skip();
           return false;
        }
        bool useDialog;
        gPrefs->Read(wxT("/GUI/DialogForNameNewLabel"), &useDialog, false);
        auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
        if (useDialog) {
           wxString title;
           if (DialogForLabelName(
              project, selectedRegion, charCode, title) ==
               wxID_CANCEL) {
              return false;
           }
           pTrack->SetSelected(true);
           pTrack->AddLabel(selectedRegion, title);
           ProjectHistory::Get( project )
              .PushState(XO("Added label"), XO("Label"));
           return false;
        }
        else {
           pTrack->SetSelected(true);
           AddLabel( selectedRegion );
           ProjectHistory::Get( project )
              .PushState(XO("Added label"), XO("Label"));
        }
     }
  
     //
     // Now we are definitely in a label; append the incoming character
     //
  
     const auto &mLabels = pTrack->GetLabels();
     auto labelStruct = mLabels[mSelIndex];
     auto &title = labelStruct.title;
  
     // Test if cursor is in the end of string or not
     if (mInitialCursorPos != mCurrentCursorPos)
        RemoveSelectedText();
  
     if (mCurrentCursorPos < (int)title.length()) {
        // Get substring on the righthand side of cursor
        wxString rightPart = title.Mid(mCurrentCursorPos);
        // Set title to substring on the lefthand side of cursor
        title = title.Left(mCurrentCursorPos);
        //append charcode
        title += charCode;
        //append the right part substring
        title += rightPart;
     }
     else
        //append charCode
        title += charCode;
  
     pTrack->SetLabel( mSelIndex, labelStruct );
  
     //moving cursor position forward
     mInitialCursorPos = ++mCurrentCursorPos;
     updated = true;
  
     // Make sure the caret is visible
     mDrawCursor = true;
  
     return updated;
  }
  
  enum
  {
     OnCutSelectedTextID = 1,      // OSX doesn't like a 0 menu id
     OnCopySelectedTextID,
     OnPasteSelectedTextID,
     OnDeleteSelectedLabelID,
     OnEditSelectedLabelID,
  };
  
  void LabelTrackView::ShowContextMenu( AudacityProject &project )
  {
     wxWindow *parent = wxWindow::FindFocus();
  
     // Bug 2044.  parent can be nullptr after a context switch.
     if( !parent )
        parent = &GetProjectFrame( project );
  
     if( parent )
     {
        wxMenu menu;
        menu.Bind(wxEVT_MENU,
           [this, &project]( wxCommandEvent &event ){
              OnContextMenu( project, event ); }
        );
  
        menu.Append(OnCutSelectedTextID, _("Cu&t"));
        menu.Append(OnCopySelectedTextID, _("&Copy"));
        menu.Append(OnPasteSelectedTextID, _("&Paste"));
        menu.Append(OnDeleteSelectedLabelID, _("&Delete Label"));
        menu.Append(OnEditSelectedLabelID, _("&Edit..."));
  
        menu.Enable(OnCutSelectedTextID, IsTextSelected( project ));
        menu.Enable(OnCopySelectedTextID, IsTextSelected( project ));
        menu.Enable(OnPasteSelectedTextID, IsTextClipSupported());
        menu.Enable(OnDeleteSelectedLabelID, true);
        menu.Enable(OnEditSelectedLabelID, true);
  
        if( !HasSelection( project ) ) {
           wxASSERT( false );
           return;
        }
  
        const auto pTrack = FindLabelTrack();
        const LabelStruct *ls = pTrack->GetLabel(mSelIndex);
  
        wxClientDC dc(parent);
  
        if (msFont.Ok())
        {
           dc.SetFont(msFont);
        }
  
        int x = 0;
        bool success = CalcCursorX( project, &x );
        wxASSERT(success);
        static_cast<void>(success); // Suppress unused variable warning if debug mode is disabled
  
        parent->PopupMenu(&menu, x, ls->y + (mIconHeight / 2) - 1);
     }
  }
  
  void LabelTrackView::OnContextMenu(
     AudacityProject &project, wxCommandEvent & evt )
  {
     auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
  
     switch (evt.GetId())
     {
     /// Cut selected text if cut menu item is selected
     case OnCutSelectedTextID:
        if (CutSelectedText( project ))
        {
           ProjectHistory::Get( project ).PushState(XO("Modified Label"),
                        XO("Label Edit"),
                        UndoPush::CONSOLIDATE);
        }
        break;
  
     /// Copy selected text if copy menu item is selected
     case OnCopySelectedTextID:
        CopySelectedText( project );
        break;
  
     /// paste selected text if paste menu item is selected
     case OnPasteSelectedTextID:
        if (PasteSelectedText(
           project, selectedRegion.t0(), selectedRegion.t1() ))
        {
           ProjectHistory::Get( project ).PushState(XO("Modified Label"),
                        XO("Label Edit"),
                        UndoPush::CONSOLIDATE);
        }
        break;
  
     /// DELETE selected label
     case OnDeleteSelectedLabelID: {
        int ndx = GetLabelIndex(selectedRegion.t0(), selectedRegion.t1());
        if (ndx != -1)
        {
           const auto pTrack = FindLabelTrack();
           pTrack->DeleteLabel(ndx);
           ProjectHistory::Get( project ).PushState(XO("Deleted Label"),
                        XO("Label Edit"),
                        UndoPush::CONSOLIDATE);
        }
     }
        break;
  
     case OnEditSelectedLabelID: {
        int ndx = GetLabelIndex(selectedRegion.t0(), selectedRegion.t1());
        if (ndx != -1)
           DoEditLabels( project, FindLabelTrack().get(), ndx );
     }
        break;
     }
  }
  
  void LabelTrackView::RemoveSelectedText()
  {
     wxString left, right;
  
     int init = mInitialCursorPos;
     int cur = mCurrentCursorPos;
     if (init > cur)
        std::swap(init, cur);
  
     const auto pTrack = FindLabelTrack();
     const auto &mLabels = pTrack->GetLabels();
     auto labelStruct = mLabels[mSelIndex];
     auto &title = labelStruct.title;
  
     if (init > 0)
        left = title.Left(init);
  
     if (cur < (int)title.length())
        right = title.Mid(cur);
  
     title = left + right;
     pTrack->SetLabel( mSelIndex, labelStruct );
     mInitialCursorPos = mCurrentCursorPos = left.length();
  }
  
  bool LabelTrackView::HasSelection( AudacityProject &project ) const
  {
     const auto selIndex = GetSelectedIndex( project );
     return (selIndex >= 0 &&
        selIndex < (int)FindLabelTrack()->GetLabels().size());
  }
  
  int LabelTrackView::GetLabelIndex(double t, double t1)
  {
     //We'd have liked to have times in terms of samples,
     //because then we're doing an intrger comparison.
     //Never mind.  Instead we look for near enough.
     //This level of (in)accuracy is only a problem if we
     //deal with sounds in the MHz range.
     const double delta = 1.0e-7;
     const auto pTrack = FindLabelTrack();
     const auto &mLabels = pTrack->GetLabels();
     { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
        if( fabs( labelStruct.getT0() - t ) > delta )
           continue;
        if( fabs( labelStruct.getT1() - t1 ) > delta )
           continue;
        return i;
     }}
  
     return wxNOT_FOUND;
  }
  
  
  // restoreFocus of -1 is the default, and sets the focus to this label.
  // restoreFocus of -2 or other value leaves the focus unchanged.
  // restoreFocus >= 0 will later cause focus to move to that track.
  int LabelTrackView::AddLabel(const SelectedRegion &selectedRegion,
                           const wxString &title, int restoreFocus)
  {
     const auto pTrack = FindLabelTrack();
     mRestoreFocus = restoreFocus;
     auto pos = pTrack->AddLabel( selectedRegion, title );
     return pos;
  }
  
  void LabelTrackView::OnLabelAdded( LabelTrackEvent &e )
  {
     e.Skip();
     if ( e.mpTrack.lock() != FindTrack() )
        return;
  
     const auto &title = e.mTitle;
     const auto pos = e.mPresentPosition;
  
     mInitialCursorPos = mCurrentCursorPos = title.length();
  
     // restoreFocus is -2 e.g. from Nyquist label creation, when we should not
     // even lose the focus and open the label to edit in the first place.
     // -1 means we don't need to restore it to anywhere.
     // 0 or above is the track to restore to after editing the label is complete.
     if( mRestoreFocus >= -1 )
        mSelIndex = pos;
  
     if( mRestoreFocus < 0 )
        mRestoreFocus = -2;
  
     // Make sure the caret is visible
     //
     // LLL: The cursor will not be drawn when the first label
     //      is added since mDrawCursor will be false.  Presumably,
     //      if the user adds a label, then a cursor should be drawn
     //      to indicate that typing is expected.
     //
     //      If the label is added during actions like import, then the
     //      mDrawCursor flag will be reset once the action is complete.
     mDrawCursor = true;
  }
  
  void LabelTrackView::OnLabelDeleted( LabelTrackEvent &e )
  {
     e.Skip();
     if ( e.mpTrack.lock() != FindTrack() )
        return;
  
     auto index = e.mFormerPosition;
  
     // IF we've deleted the selected label
     // THEN set no label selected.
     if( mSelIndex== index )
     {
        mSelIndex = -1;
        mCurrentCursorPos = 1;
     }
     // IF we removed a label before the selected label
     // THEN the NEW selected label number is one less.
     else if( index < mSelIndex )
     {
        mSelIndex--;
     }
  }
  
  void LabelTrackView::OnLabelPermuted( LabelTrackEvent &e )
  {
     e.Skip();
     if ( e.mpTrack.lock() != FindTrack() )
        return;
  
     auto former = e.mFormerPosition;
     auto present = e.mPresentPosition;
  
     if ( mSelIndex == former )
        mSelIndex = present;
     else if ( former < mSelIndex && mSelIndex <= present )
        -- mSelIndex;
     else if ( former > mSelIndex && mSelIndex >= present )
        ++ mSelIndex;
  }
  
  void LabelTrackView::OnSelectionChange( LabelTrackEvent &e )
  {
     e.Skip();
     if ( e.mpTrack.lock() != FindTrack() )
        return;
  
     if ( !FindTrack()->GetSelected() )
        mSelIndex = -1;
  }
  
  wxBitmap & LabelTrackView::GetGlyph( int i)
  {
     return theTheme.Bitmap( i + bmpLabelGlyph0);
  }
  
  // This one XPM spec is used to generate a number of
  // different wxIcons.
  /* XPM */
  static const char *const GlyphXpmRegionSpec[] = {
  /* columns rows colors chars-per-pixel */
  "15 23 7 1",
  /* Default colors, with first color transparent */
  ". c none",
  "2 c black",
  "3 c black",
  "4 c black",
  "5 c #BEBEF0",
  "6 c #BEBEF0",
  "7 c #BEBEF0",
  /* pixels */
  "...............",
  "...............",
  "...............",
  "....333.444....",
  "...3553.4774...",
  "...3553.4774...",
  "..35553.47774..",
  "..35522222774..",
  ".3552666662774.",
  ".3526666666274.",
  "355266666662774",
  "355266666662774",
  "355266666662774",
  ".3526666666274.",
  ".3552666662774.",
  "..35522222774..",
  "..35553.47774..",
  "...3553.4774...",
  "...3553.4774...",
  "....333.444....",
  "...............",
  "...............",
  "..............."
  };
  
  /// CreateCustomGlyphs() creates the mBoundaryGlyph array.
  /// It's a bit like painting by numbers!
  ///
  /// Schematically the glyphs we want will 'look like':
  ///   <O,  O>   and   <O>
  /// for a left boundary to a label, a right boundary and both.
  /// we're creating all three glyphs using the one Xpm Spec.
  ///
  /// When we hover over a glyph we highlight the
  /// inside of either the '<', the 'O' or the '>' or none,
  /// giving 3 x 4 = 12 combinations.
  ///
  /// Two of those combinations aren't used, but
  /// treating them specially would make other code more
  /// complicated.
  void LabelTrackView::CreateCustomGlyphs()
  {
     int iConfig;
     int iHighlight;
     int index;
     const int nSpecRows =
        sizeof( GlyphXpmRegionSpec )/sizeof( GlyphXpmRegionSpec[0]);
     const char *XmpBmp[nSpecRows];
  
     // The glyphs are declared static wxIcon; so we only need
     // to create them once, no matter how many LabelTracks.
     if( mbGlyphsReady )
        return;
  
     // We're about to tweak the basic color spec to get 12 variations.
     for( iConfig=0;iConfig<NUM_GLYPH_CONFIGS;iConfig++)
     {
        for( iHighlight=0;iHighlight<NUM_GLYPH_HIGHLIGHTS;iHighlight++)
        {
           index = iConfig + NUM_GLYPH_CONFIGS * iHighlight;
           // Copy the basic spec...
           memcpy( XmpBmp, GlyphXpmRegionSpec, sizeof( GlyphXpmRegionSpec ));
           // The higlighted region (if any) is white...
           if( iHighlight==1 ) XmpBmp[5]="5 c #FFFFFF";
           if( iHighlight==2 ) XmpBmp[6]="6 c #FFFFFF";
           if( iHighlight==3 ) XmpBmp[7]="7 c #FFFFFF";
           // For left or right arrow the other side of the glyph
           // is the transparent color.
           if( iConfig==0) { XmpBmp[3]="3 c none"; XmpBmp[5]="5 c none"; }
           if( iConfig==1) { XmpBmp[4]="4 c none"; XmpBmp[7]="7 c none"; }
           // Create the icon from the tweaked spec.
           mBoundaryGlyphs[index] = wxBitmap(XmpBmp);
           // Create the mask
           // SetMask takes ownership
           mBoundaryGlyphs[index].SetMask(safenew wxMask(mBoundaryGlyphs[index], wxColour(192, 192, 192)));
        }
     }
  
     mIconWidth  = mBoundaryGlyphs[0].GetWidth();
     mIconHeight = mBoundaryGlyphs[0].GetHeight();
     mTextHeight = mIconHeight; // until proved otherwise...
     // The icon should have an odd width so that the
     // line goes exactly down the middle.
     wxASSERT( (mIconWidth %2)==1);
  
     mbGlyphsReady=true;
  }
  
  #include "../../../LabelDialog.h"
  
  void LabelTrackView::DoEditLabels
  (AudacityProject &project, LabelTrack *lt, int index)
  {
     const auto &settings = ProjectSettings::Get( project );
     auto format = settings.GetSelectionFormat(),
        freqFormat = settings.GetFrequencySelectionFormatName();
     auto &tracks = TrackList::Get( project );
     auto &trackFactory = TrackFactory::Get( project );
     auto rate = ProjectSettings::Get( project ).GetRate();
     auto &viewInfo = ViewInfo::Get( project );
     auto &window = ProjectWindow::Get( project );
  
     LabelDialog dlg(&window, trackFactory, &tracks,
                     lt, index,
                     viewInfo, rate,
                     format, freqFormat);
  #ifdef __WXGTK__
     dlg.Raise();
  #endif
  
     if (dlg.ShowModal() == wxID_OK) {
        ProjectHistory::Get( project )
           .PushState(XO("Edited labels"), XO("Label"));
     }
  }
  
  int LabelTrackView::DialogForLabelName(
     AudacityProject &project,
     const SelectedRegion& region, const wxString& initialValue, wxString& value)
  {
     auto &trackFocus = TrackFocus::Get( project );
     auto &trackPanel = TrackPanel::Get( project );
     auto &viewInfo = ViewInfo::Get( project );
  
     wxPoint position =
        trackPanel.FindTrackRect( trackFocus.Get() ).GetBottomLeft();
     // The start of the text in the text box will be roughly in line with the label's position
     // if it's a point label, or the start of its region if it's a region label.
     position.x += viewInfo.GetLabelWidth()
        + std::max(0, static_cast<int>(viewInfo.TimeToPosition(region.t0())))
        -40;
     position.y += 2;  // just below the bottom of the track
     position = trackPanel.ClientToScreen(position);
     auto &window = GetProjectFrame( project );
     AudacityTextEntryDialog dialog{ &window,
        XO("Name:"),
        XO("New label"),
        initialValue,
        wxOK | wxCANCEL,
        position };
  
     // keep the dialog within Audacity's window, so that the dialog is always fully visible
     wxRect dialogScreenRect = dialog.GetScreenRect();
     wxRect projScreenRect = window.GetScreenRect();
     wxPoint max = projScreenRect.GetBottomRight() + wxPoint{ -dialogScreenRect.width, -dialogScreenRect.height };
     if (dialogScreenRect.x > max.x) {
        position.x = max.x;
        dialog.Move(position);
     }
     if (dialogScreenRect.y > max.y) {
        position.y = max.y;
        dialog.Move(position);
     }
  
     dialog.SetInsertionPointEnd();      // because, by default, initial text is selected
     int status = dialog.ShowModal();
     if (status != wxID_CANCEL) {
        value = dialog.GetValue();
        value.Trim(true).Trim(false);
     }
  
     return status;
  }
  
  using DoGetLabelTrackView = DoGetView::Override< LabelTrack >;
  template<> template<> auto DoGetLabelTrackView::Implementation() -> Function {
     return [](LabelTrack &track) {
        return std::make_shared<LabelTrackView>( track.SharedPointer() );
     };
  }
  static DoGetLabelTrackView registerDoGetLabelTrackView;
  
  std::shared_ptr<TrackVRulerControls> LabelTrackView::DoGetVRulerControls()
  {
     return
        std::make_shared<LabelTrackVRulerControls>( shared_from_this() );
  }
  
  