~tenacity/tenacity

4df39adc47c38546bf1f328028087957f526565a — Panagiotis Vasilopoulos 8 months ago eb774a4 + 2330c51
Merge pull request #515 from akleja/patches-upstream

Backport track affordance and label patches from upstream Audacity.

When the fork was originally started, we ended up carrying over a couple
of features that were half- or unfinished to our fork. This merge is meant
to make up for that.

Signed-off-by: Panagiotis Vasilopoulos <hello@alwayslivid.com>
Reference-to: https://github.com/tenacityteam/tenacity/pull/515
54 files changed, 1109 insertions(+), 776 deletions(-)

M src/CMakeLists.txt
M src/LabelDialog.cpp
M src/LyricsWindow.cpp
M src/LyricsWindow.h
M src/Printing.cpp
M src/ProjectAudioManager.cpp
M src/ProjectFileManager.cpp
M src/Screenshot.cpp
M src/Track.cpp
M src/Track.h
M src/TrackInfo.cpp
M src/TrackPanel.cpp
M src/TrackPanel.h
A src/TrackPanelConstants.h
M src/TrackPanelResizeHandle.cpp
M src/TrackPanelResizeHandle.h
M src/TrackPanelResizerCell.cpp
M src/ViewInfo.h
M src/WaveTrack.cpp
M src/WaveTrack.h
M src/ZoomInfo.h
M src/commands/GetInfoCommand.cpp
M src/commands/GetInfoCommand.h
M src/commands/ScreenshotCommand.cpp
M src/commands/SetLabelCommand.cpp
M src/commands/SetTrackInfoCommand.cpp
M src/effects/Effect.cpp
M src/effects/StereoToMono.cpp
M src/menus/ClipMenus.cpp
M src/menus/EditMenus.cpp
M src/menus/LabelMenus.cpp
M src/menus/TrackMenus.cpp
M src/menus/ViewMenus.cpp
M src/tracks/labeltrack/ui/LabelDefaultClickHandle.cpp
M src/tracks/labeltrack/ui/LabelGlyphHandle.cpp
M src/tracks/labeltrack/ui/LabelGlyphHandle.h
M src/tracks/labeltrack/ui/LabelTextHandle.cpp
M src/tracks/labeltrack/ui/LabelTextHandle.h
M src/tracks/labeltrack/ui/LabelTrackShifter.cpp
M src/tracks/labeltrack/ui/LabelTrackView.cpp
M src/tracks/labeltrack/ui/LabelTrackView.h
M src/tracks/playabletrack/notetrack/ui/NoteTrackView.cpp
M src/tracks/playabletrack/notetrack/ui/NoteTrackView.h
M src/tracks/playabletrack/wavetrack/ui/WaveTrackAffordanceControls.cpp
M src/tracks/playabletrack/wavetrack/ui/WaveTrackAffordanceControls.h
M src/tracks/playabletrack/wavetrack/ui/WaveTrackControls.cpp
M src/tracks/playabletrack/wavetrack/ui/WaveTrackView.cpp
M src/tracks/playabletrack/wavetrack/ui/WaveTrackView.h
M src/tracks/ui/BackgroundCell.cpp
M src/tracks/ui/EnvelopeHandle.cpp
M src/tracks/ui/EnvelopeHandle.h
M src/tracks/ui/TimeShiftHandle.cpp
M src/tracks/ui/TrackView.cpp
M src/tracks/ui/TrackView.h
M src/CMakeLists.txt => src/CMakeLists.txt +1 -0
@@ 298,6 298,7 @@ list( APPEND SOURCES PRIVATE
  TrackPanelAx.h
  TrackPanelCell.cpp
  TrackPanelCell.h
  TrackPanelConstants.h
  TrackPanelDrawable.cpp
  TrackPanelDrawable.h
  TrackPanelDrawingContext.h

M src/LabelDialog.cpp => src/LabelDialog.cpp +1 -1
@@ 426,7 426,7 @@ bool LabelDialog::TransferDataFromWindow()

      // Add the label to it
      lt->AddLabel(rd.selectedRegion, rd.title);
      LabelTrackView::Get( *lt ).SetSelectedIndex( -1 );
      LabelTrackView::Get( *lt ).ResetTextSelection();
   }

   return true;

M src/LyricsWindow.cpp => src/LyricsWindow.cpp +20 -16
@@ 63,7 63,8 @@ LyricsWindow::LyricsWindow(AudacityProject *parent)
   //      // WXMAC doesn't support wxFRAME_FLOAT_ON_PARENT, so we do
   //      SetWindowClass((WindowRef) MacGetWindowRef(), kFloatingWindowClass);
   //   #endif
   mProject = parent;
   auto pProject = parent->shared_from_this();
   mProject = pProject;

   SetWindowTitle();
   auto titleChanged = [&](wxCommandEvent &evt)


@@ 135,7 136,7 @@ LyricsWindow::LyricsWindow(AudacityProject *parent)
   //}

   // Events from the project don't propagate directly to this other frame, so...
   mProject->Bind(EVT_TRACK_PANEL_TIMER,
   pProject->Bind(EVT_TRACK_PANEL_TIMER,
      &LyricsWindow::OnTimer,
      this);
   Center();


@@ 158,16 159,18 @@ void LyricsWindow::OnStyle_Highlight(wxCommandEvent & WXUNUSED(event))

void LyricsWindow::OnTimer(wxCommandEvent &event)
{
   if (ProjectAudioIO::Get( *mProject ).IsAudioActive())
   {
      auto gAudioIO = AudioIO::Get();
      GetLyricsPanel()->Update(gAudioIO->GetStreamTime());
   }
   else
   {
      // Reset lyrics display.
      const auto &selectedRegion = ViewInfo::Get( *mProject ).selectedRegion;
      GetLyricsPanel()->Update(selectedRegion.t0());
   if (auto pProject = mProject.lock()) {
      if (ProjectAudioIO::Get( *pProject ).IsAudioActive())
      {
         auto gAudioIO = AudioIO::Get();
         GetLyricsPanel()->Update(gAudioIO->GetStreamTime());
      }
      else
      {
         // Reset lyrics display.
         const auto &selectedRegion = ViewInfo::Get( *pProject ).selectedRegion;
         GetLyricsPanel()->Update(selectedRegion.t0());
      }
   }

   // Let other listeners get the notification


@@ 176,10 179,11 @@ void LyricsWindow::OnTimer(wxCommandEvent &event)

void LyricsWindow::SetWindowTitle()
{
   wxString name = mProject->GetProjectName();
   if (!name.empty())
   {
      name.Prepend(wxT(" - "));
   wxString name;
   if (auto pProject = mProject.lock()) {
      name = pProject->GetProjectName();
      if (!name.empty())
         name.Prepend(wxT(" - "));
   }

   SetTitle(AudacityKaraokeTitle.Format(name).Translation());

M src/LyricsWindow.h => src/LyricsWindow.h +2 -1
@@ 13,6 13,7 @@
#define __AUDACITY_LYRICS_WINDOW__

#include <wx/frame.h> // to inherit
#include <memory>

#include "Prefs.h"



@@ 40,7 41,7 @@ class LyricsWindow final : public wxFrame,
   // PrefsListener implementation
   void UpdatePrefs() override;

   AudacityProject *mProject;
   std::weak_ptr<AudacityProject> mProject;
   LyricsPanel *mLyricsPanel;

 public:

M src/Printing.cpp => src/Printing.cpp +3 -0
@@ 102,6 102,9 @@ bool AudacityPrintout::OnPrintPage(int WXUNUSED(page))
      r.x = 0;
      r.y = 0;
      r.width = width;
      // Note that the views as printed might not have the same proportional
      // heights as displayed on the screen, because the fixed-sized separators
      // are counted in those heights but not printed
      auto trackHeight = (int)(view.GetHeight() * scale);
      r.height = trackHeight;


M src/ProjectAudioManager.cpp => src/ProjectAudioManager.cpp +1 -1
@@ 722,7 722,7 @@ bool ProjectAudioManager::DoRecord(AudacityProject &project,

            transportTracks.captureTracks.push_back(newTrack);
         }
         TrackList::Get( *p ).GroupChannels(*first, recordingChannels);
         TrackList::Get( *p ).MakeMultiChannelTrack(*first, recordingChannels, true);
         // Bug 1548.  First of new tracks needs the focus.
         TrackFocus::Get(*p).Set(first);
         if (TrackList::Get(*p).back())

M src/ProjectFileManager.cpp => src/ProjectFileManager.cpp +1 -1
@@ 1113,7 1113,7 @@ ProjectFileManager::AddImportedTracks(const FilePath &fileName,
         auto newTrack = tracks.Add( uNewTrack );
         results.push_back(newTrack->SharedPointer());
      }
      tracks.GroupChannels(*first, nChannels);
      tracks.MakeMultiChannelTrack(*first, nChannels, true);
   }
   newTracks.clear();
      

M src/Screenshot.cpp => src/Screenshot.cpp +2 -2
@@ 791,7 791,7 @@ void ScreenshotBigDialog::SizeTracks(int h)
      auto nChannels = channels.size();
      auto height = nChannels == 1 ? 2 * h : h;
      for (auto channel : channels)
         TrackView::Get( *channel ).SetHeight(height);
         TrackView::Get( *channel ).SetExpandedHeight(height);
   }
   ProjectWindow::Get( mContext.project ).RedrawProject();
}


@@ 800,7 800,7 @@ void ScreenshotBigDialog::OnShortTracks(wxCommandEvent & WXUNUSED(event))
{
   for (auto t : TrackList::Get( mContext.project ).Any<WaveTrack>()) {
      auto &view = TrackView::Get( *t );
      view.SetHeight( view.GetMinimizedHeight() );
      view.SetExpandedHeight( view.GetMinimizedHeight() );
   }

   ProjectWindow::Get( mContext.project ).RedrawProject();

M src/Track.cpp => src/Track.cpp +105 -78
@@ 50,7 50,6 @@ Track::Track()
:  vrulerSize(36,0)
{
   mSelected  = false;
   mLinked    = false;

   mIndex = 0;



@@ 76,7 75,7 @@ void Track::Init(const Track &orig)
   mName = orig.mName;

   mSelected = orig.mSelected;
   mLinked = orig.mLinked;
   mLinkType = orig.mLinkType;
   mChannel = orig.mChannel;
}



@@ 172,18 171,18 @@ void Track::SetIndex(int index)
   mIndex = index;
}

void Track::SetLinked(bool l)
void Track::SetLinkType(LinkType linkType)
{
   auto pList = mList.lock();
   if (pList && !pList->mPendingUpdates.empty()) {
      auto orig = pList->FindById( GetId() );
      if (orig && orig != this) {
         orig->SetLinked(l);
         orig->SetLinkType(linkType);
         return;
      }
   }

   DoSetLinked(l);
   DoSetLinkType(linkType);

   if (pList) {
      pList->RecalcPositions(mNode);


@@ 191,19 190,24 @@ void Track::SetLinked(bool l)
   }
}

void Track::DoSetLinked(bool l)
void Track::DoSetLinkType(LinkType linkType) noexcept
{
   mLinked = l;
   mLinkType = linkType;
}

Track *Track::GetLink() const
void Track::SetChannel(ChannelType c) noexcept
{
    mChannel = c;
}

Track *Track::GetLinkedTrack() const
{
   auto pList = mList.lock();
   if (!pList)
      return nullptr;

   if (!pList->isNull(mNode)) {
      if (mLinked) {
      if (HasLinkedTrack()) {
         auto next = pList->getNext( mNode );
         if ( !pList->isNull( next ) )
            return next.first->get();


@@ 213,7 217,7 @@ Track *Track::GetLink() const
         auto prev = pList->getPrev( mNode );
         if ( !pList->isNull( prev ) ) {
            auto track = prev.first->get();
            if (track && track->GetLinked())
            if (track && track->HasLinkedTrack())
               return track;
         }
      }


@@ 222,6 226,11 @@ Track *Track::GetLink() const
   return nullptr;
}

bool Track::HasLinkedTrack() const noexcept
{
    return mLinkType != LinkType::None;
}

namespace {
   inline bool IsSyncLockableNonLabelTrack( const Track *pTrack )
   {


@@ 368,7 377,9 @@ bool Track::IsSelectedOrSyncLockSelected() const
   { return GetSelected() || IsSyncLockSelected(); }

bool Track::IsLeader() const
   { return !GetLink() || GetLinked(); }
{
    return !GetLinkedTrack() || HasLinkedTrack();
}

bool Track::IsSelectedLeader() const
   { return IsSelected() && IsLeader(); }


@@ 378,7 389,7 @@ void Track::FinishCopy
{
   if (dest) {
      dest->SetChannel(n->GetChannel());
      dest->SetLinked(n->GetLinked());
      dest->SetLinkType(n->GetLinkType());
      dest->SetName(n->GetName());
   }
}


@@ 389,32 400,32 @@ bool Track::LinkConsistencyCheck()
   // doesn't fix the problem, but it likely leaves us with orphaned
   // sample blocks instead of much worse problems.
   bool err = false;
   if (GetLinked())
   if (HasLinkedTrack())
   {
      Track *l = GetLink();
      if (l)
      auto link = GetLinkedTrack();
      if (link)
      {
         // A linked track's partner should never itself be linked
         if (l->GetLinked())
         if (link->HasLinkedTrack())
         {
            wxLogWarning(
               wxT("Left track %s had linked right track %s with extra right track link.\n   Removing extra link from right track."),
               GetName(), l->GetName());
               GetName(), link->GetName());
            err = true;
            l->SetLinked(false);
            link->SetLinkType(LinkType::None);
         }

         // Channels should be left and right
         if ( !(  (GetChannel() == Track::LeftChannel &&
                     l->GetChannel() == Track::RightChannel) ||
                     link->GetChannel() == Track::RightChannel) ||
                  (GetChannel() == Track::RightChannel &&
                     l->GetChannel() == Track::LeftChannel) ) )
                     link->GetChannel() == Track::LeftChannel) ) )
         {
            wxLogWarning(
               wxT("Track %s and %s had left/right track links out of order. Setting tracks to not be linked."),
               GetName(), l->GetName());
               GetName(), link->GetName());
            err = true;
            SetLinked(false);
            SetLinkType(LinkType::None);
         }
      }
      else


@@ 423,7 434,7 @@ bool Track::LinkConsistencyCheck()
            wxT("Track %s had link to NULL track. Setting it to not be linked."),
            GetName());
         err = true;
         SetLinked(false);
         SetLinkType(LinkType::None);
      }
   }



@@ 704,57 715,6 @@ Track *TrackList::DoAdd(const std::shared_ptr<Track> &t)
   return back().get();
}

void TrackList::GroupChannels(
   Track &track, size_t groupSize, bool resetChannels )
{
   // If group size is exactly two, group as stereo, else mono (bug 2195).
   auto list = track.mList.lock();
   if ( groupSize > 0 && list.get() == this  ) {
      auto iter = track.mNode.first;
      auto after = iter;
      auto end = this->ListOfTracks::end();
      auto count = groupSize;
      for ( ; after != end && count; ++after, --count )
         ;
      if ( count == 0 ) {
         auto unlink = [&] ( Track &tr ) {
            if ( tr.GetLinked() ) {
               if ( resetChannels ) {
                  auto link = tr.GetLink();
                  if ( link )
                     link->SetChannel( Track::MonoChannel );
               }
               tr.SetLinked( false );
            }
            if ( resetChannels )
               tr.SetChannel( Track::MonoChannel );
         };

         // Disassociate previous tracks -- at most one
         auto pLeader = this->FindLeader( &track );
         if ( *pLeader && *pLeader != &track )
            unlink( **pLeader );

         // First disassociate given and later tracks, then reassociate them
         for ( auto iter2 = iter; iter2 != after; ++iter2 )
             unlink( **iter2 );

         if ( groupSize > 1 ) {
            const auto channel = *iter++;
            channel->SetLinked( groupSize == 2 );
            channel->SetChannel( groupSize == 2? Track::LeftChannel : Track::MonoChannel );
            (*iter++)->SetChannel( groupSize == 2? Track::RightChannel : Track::MonoChannel );
            while (iter != after)
               (*iter++)->SetChannel( Track::MonoChannel );
         }
         return;
      }
   }
   // *this does not contain the track or sufficient following channels
   // or group size is zero
   THROW_INCONSISTENCY_EXCEPTION;
}

auto TrackList::Replace(Track * t, const ListOfTracks::value_type &with) ->
   ListOfTracks::value_type
{


@@ 777,6 737,58 @@ auto TrackList::Replace(Track * t, const ListOfTracks::value_type &with) ->
   return holder;
}

void TrackList::UnlinkChannels(Track& track)
{
   auto list = track.mList.lock();
   if (list.get() == this)
   {
      auto channels = TrackList::Channels(&track);
      for (auto c : channels)
      {
          c->SetLinkType(Track::LinkType::None);
          c->SetChannel(Track::ChannelType::MonoChannel);
      }
   }
   else
      THROW_INCONSISTENCY_EXCEPTION;
}

bool TrackList::MakeMultiChannelTrack(Track& track, int nChannels, bool aligned)
{
   if (nChannels != 2)
      return false;

   auto list = track.mList.lock();
   if (list.get() == this)
   {
      if (*list->FindLeader(&track) != &track)
         return false;

      auto first = list->Find(&track);
      auto canLink = [&]() -> bool {
         int count = nChannels;
         for (auto it = first, end = TrackList::end(); it != end && count; ++it)
         {
            if ((*it)->HasLinkedTrack())
               return false;
            --count;
         }
         return count == 0;
      }();

      if (!canLink)
         return false;

      (*first)->SetLinkType(aligned ? Track::LinkType::Aligned : Track::LinkType::Group);
      (*first)->SetChannel(Track::LeftChannel);
      auto second = std::next(first);
      (*second)->SetChannel(Track::RightChannel);
   }
   else
      THROW_INCONSISTENCY_EXCEPTION;
   return true;
}

TrackNodePointer TrackList::Remove(Track *t)
{
   auto result = getEnd();


@@ 826,7 838,7 @@ Track *TrackList::GetNext(Track * t, bool linked) const
   if (t) {
      auto node = t->GetNode();
      if ( !isNull( node ) ) {
         if ( linked && t->GetLinked() )
         if ( linked && t->HasLinkedTrack() )
            node = getNext( node );

         if ( !isNull( node ) )


@@ 850,7 862,7 @@ Track *TrackList::GetPrev(Track * t, bool linked) const
         if (linked) {
            prev = getPrev( node );
            if( !isNull( prev ) &&
                !t->GetLinked() && t->GetLink() )
                !t->HasLinkedTrack() && t->GetLinkedTrack() )
               // Make it the first
               node = prev;
         }


@@ 864,7 876,7 @@ Track *TrackList::GetPrev(Track * t, bool linked) const
            if (linked) {
               prev = getPrev( node );
               if( !isNull( prev ) &&
                   !(*node.first)->GetLinked() && (*node.first)->GetLink() )
                   !(*node.first)->HasLinkedTrack() && (*node.first)->GetLinkedTrack() )
                  node = prev;
            }



@@ 1068,7 1080,7 @@ void TrackList::UpdatePendingTracks()
      if (pendingTrack && src) {
         if (updater)
            updater( *pendingTrack, *src );
         pendingTrack->DoSetLinked(src->GetLinked());
         pendingTrack->DoSetLinkType(src->GetLinkType());
      }
      ++pUpdater;
   }


@@ 1296,3 1308,18 @@ bool TrackList::HasPendingTracks() const
      return true;
   return false;
}

Track::LinkType Track::GetLinkType() const noexcept
{
    return mLinkType;
}

bool Track::IsAlignedWithLeader() const
{
   if (auto owner = GetOwner())
   {
      auto leader = *owner->FindLeader(this);
      return leader != this && leader->GetLinkType() == Track::LinkType::Aligned;
   }
   return false;
}

M src/Track.h => src/Track.h +38 -22
@@ 236,10 236,22 @@ class TENACITY_DLL_API Track /* not final */
   , public AttachedTrackObjects
   , public std::enable_shared_from_this<Track> // see SharedPointer()
{
public:

   //! For two tracks describes the type of the linkage
   enum class LinkType : int {
       None = 0, //< No linkage
       Group = 2, //< Tracks are grouped together
       Aligned, //< Tracks are grouped and changes should be synchronized
   };

private:

   friend class TrackList;

 private:
   TrackId mId; //!< Identifies the track only in-session, not persistently
   LinkType mLinkType{ LinkType::None };

 protected:
   std::weak_ptr<TrackList> mList; //!< Back pointer to owning TrackList


@@ 253,9 265,6 @@ class TENACITY_DLL_API Track /* not final */
 private:
   bool           mSelected;

 protected:
   bool           mLinked;

 public:

   //! Alias for my base type


@@ 360,23 369,28 @@ public:
   static void FinishCopy (const Track *n, Track *dest);

   // For use when loading a file.  Return true if ok, else make repair
   bool LinkConsistencyCheck();
   virtual bool LinkConsistencyCheck();

   bool HasOwner() const { return static_cast<bool>(GetOwner());}

   std::shared_ptr<TrackList> GetOwner() const { return mList.lock(); }

private:
   Track *GetLink() const;
   bool GetLinked  () const { return mLinked; }
   LinkType GetLinkType() const noexcept;
   //! Returns true if the leader track has link type LinkType::Aligned
   bool IsAlignedWithLeader() const;

   friend WaveTrack; // WaveTrack needs to call SetLinked when reloading project
   void SetLinked  (bool l);

   void SetChannel(ChannelType c) { mChannel = c; }
protected:
   
   void SetLinkType(LinkType linkType);
   void DoSetLinkType(LinkType linkType) noexcept;
   void SetChannel(ChannelType c) noexcept;
private:
   // No need yet to make this virtual
   void DoSetLinked(bool l);
   
   Track* GetLinkedTrack() const;
   //! Returns true for leaders of multichannel groups
   bool HasLinkedTrack() const noexcept;

   

   //! Retrieve mNode with debug checks
   TrackNodePointer GetNode() const;


@@ 1485,16 1499,18 @@ public:
   template<typename TrackKind>
      TrackKind *Add( const std::shared_ptr< TrackKind > &t )
         { return static_cast< TrackKind* >( DoAdd( t ) ); }

   /** \brief Define a group of channels starting at the given track
   *
   * @param track and (groupSize - 1) following tracks must be in this
   * list.  They will be disassociated from any groups they already belong to.
   * @param groupSize must be at least 1.
   * @param resetChannels if true, disassociated channels will be marked Mono.
   
   //! Removes linkage if track belongs to a group
   void UnlinkChannels(Track& track);
   /** \brief Converts channels to a multichannel track. 
   * @param first and the following must be in this list. Tracks should
   * not be a part of another group (not linked)
   * @param nChannels number of channels, for now only 2 channels supported
   * @param aligned if true, the link type will be set to Track::LinkType::Aligned,
   * or Track::LinkType::Group otherwise
   * @returns true on success, false if some prerequisites do not met
   */
   void GroupChannels(
      Track &track, size_t groupSize, bool resetChannels = true );
   bool MakeMultiChannelTrack(Track& first, int nChannels, bool aligned);

   /// Replace first track with second track, give back a holder
   /// Give the replacement the same id as the replaced

M src/TrackInfo.cpp => src/TrackInfo.cpp +2 -2
@@ 202,7 202,7 @@ unsigned TrackInfo::MinimumTrackHeight()
      height += commonTrackTCPBottomLines.front().height;
   // + 1 prevents the top item from disappearing for want of enough space,
   // according to the rules in HideTopItem.
   return height + kTopMargin + kBottomMargin + 1;
   return height + kVerticalPadding + 1;
}

bool TrackInfo::HideTopItem( const wxRect &rect, const wxRect &subRect,


@@ 566,7 566,7 @@ void TrackInfo::SetTrackInfoFont(wxDC * dc)
unsigned TrackInfo::DefaultTrackHeight( const TCPLines &topLines )
{
   int needed =
      kTopMargin + kBottomMargin +
      kVerticalPadding +
      totalTCPLines( topLines, true ) +
      totalTCPLines( commonTrackTCPBottomLines, false ) + 1;
   return (unsigned) std::max( needed, (int) TrackView::DefaultHeight );

M src/TrackPanel.cpp => src/TrackPanel.cpp +129 -50
@@ 26,7 26,7 @@
  TrackInfo class to draw the controls area on the left of a track,
  and the TrackArtist class to draw the actual waveforms.

  Note that in some of the older code here, e.g., GetLabelWidth(),
  Note that in some of the older code here,
  "Label" means the TrackInfo plus the vertical ruler.
  Confusing relative to LabelTrack labels.



@@ 45,12 45,14 @@ is time to refresh some aspect of the screen.


#include "TrackPanel.h"
#include "TrackPanelConstants.h"



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

#include "AdornedRulerPanel.h"
#include "tracks/ui/CommonTrackPanelCell.h"
#include "KeyboardCapture.h"
#include "Project.h"
#include "ProjectAudioIO.h"


@@ 91,6 93,9 @@ is time to refresh some aspect of the screen.
#include <wx/dcclient.h>
#include <wx/graphics.h>

static_assert( kVerticalPadding == kTopMargin + kBottomMargin );
static_assert( kTrackInfoBtnSize == kAffordancesAreaHeight, "Drag bar is misaligned with the menu button");

/**

\class TrackPanel


@@ 114,8 119,8 @@ controls, and is a constant.
GetVRulerWidth() is variable -- all tracks have the same ruler width at any
time, but that width may be adjusted when tracks change their vertical scales.

GetLabelWidth() counts columns up to and including the VRuler.
GetLeftOffset() is yet one more -- it counts the "one pixel" column.
GetLeftOffset() counts columns up to and including the VRuler and one more,
the "one pixel" column.

Cell for label has a rectangle that OMITS left, top, and bottom
margins


@@ 752,23 757,30 @@ void TrackPanel::RefreshTrack(Track *trk, bool refreshbacking)
   if (!trk)
      return;

   // Always move to the first channel of the group, and use only
   // the sum of channel heights, not the height of any channel alone!
   trk = *GetTracks()->FindLeader(trk);
   auto &view = TrackView::Get( *trk );
   auto height =
      TrackList::Channels(trk).sum( TrackView::GetTrackHeight )
      - kTopInset - kShadowThickness;
      TrackList::Channels(trk).sum( TrackView::GetTrackHeight );

   // subtract insets and shadows from the rectangle, but not border
   // Set rectangle top according to the scrolling position, `vpos`
   // Subtract the inset (above) and shadow (below) from the height of the
   // rectangle, but not the border
   // This matters because some separators do paint over the border
   wxRect rect(kLeftInset,
            -mViewInfo->vpos + view.GetY() + kTopInset,
            GetRect().GetWidth() - kLeftInset - kRightInset - kShadowThickness,
            height);
   const auto top =
      -mViewInfo->vpos + view.GetCumulativeHeightBefore() + kTopInset;
   height -= (kTopInset + kShadowThickness);

   // Width also subtracts insets (left and right) plus shadow (right)
   const auto left = kLeftInset;
   const auto width = GetRect().GetWidth()
      - (kLeftInset + kRightInset + kShadowThickness);

   wxRect rect(left, top, width, height);

   if( refreshbacking )
   {
      mRefreshBacking = true;
   }

   Refresh( false, &rect );
}


@@ 851,16 863,62 @@ void TrackPanel::DrawTracks(wxDC * dc)
}

void TrackPanel::SetBackgroundCell
(const std::shared_ptr< TrackPanelCell > &pCell)
(const std::shared_ptr< CommonTrackPanelCell > &pCell)
{
   mpBackground = pCell;
}

std::shared_ptr< TrackPanelCell > TrackPanel::GetBackgroundCell()
std::shared_ptr< CommonTrackPanelCell > TrackPanel::GetBackgroundCell()
{
   return mpBackground;
}

namespace {
std::vector<int> FindAdjustedChannelHeights( Track &t )
{
   auto channels = TrackList::Channels(&t);
   wxASSERT(!channels.empty());

   // Collect heights, and count affordances
   int nAffordances = 0;
   int totalHeight = 0;
   std::vector<int> oldHeights;
   for (auto channel : channels) {
      auto &view = TrackView::Get( *channel );
      const auto height = view.GetHeight();
      totalHeight += height;
      oldHeights.push_back( height );
      if (view.GetAffordanceControls())
         ++nAffordances;
   }

   // Allocate results
   auto nChannels = static_cast<int>(oldHeights.size());
   std::vector<int> results;
   results.reserve(nChannels);

   // Now reallocate the channel heights for the presence of affordances
   // and separators
   auto availableHeight = totalHeight
      - nAffordances * kAffordancesAreaHeight
      - (nChannels - 1) * kChannelSeparatorThickness
      - kTrackSeparatorThickness;
   int cumulativeOldHeight = 0;
   int cumulativeNewHeight = 0;
   for (const auto &oldHeight : oldHeights) {
      // Preserve the porportions among the stored heights
      cumulativeOldHeight += oldHeight;
      const auto newHeight =
         cumulativeOldHeight * availableHeight / totalHeight
            - cumulativeNewHeight;
      cumulativeNewHeight += newHeight;
      results.push_back(newHeight);
   }

   return results;
}
}

void TrackPanel::UpdateVRulers()
{
   for (auto t : GetTracks()->Any< WaveTrack >())


@@ 883,15 941,17 @@ void TrackPanel::UpdateTrackVRuler(Track *t)
   if (!t)
      return;

   auto heights = FindAdjustedChannelHeights(*t);

   wxRect rect(mViewInfo->GetVRulerOffset(),
            0,
            mViewInfo->GetVRulerWidth(),
            0);


   auto pHeight = heights.begin();
   for (auto channel : TrackList::Channels(t)) {
      auto &view = TrackView::Get( *channel );
      const auto height = view.GetHeight() - (kTopMargin + kBottomMargin);
      const auto height = *pHeight++;
      rect.SetHeight( height );
      const auto subViews = view.GetSubViews( rect );
      if (subViews.empty())


@@ 1069,7 1129,11 @@ void DrawTrackName(
   // Tracks more than kTranslucentHeight will have maximum translucency for shields.
   const int kOpaqueHeight = 44;
   const int kTranslucentHeight = 124;

   // PRL:  to do:  reexamine this strange use of TrackView::GetHeight,
   // ultimately to compute an opacity
   int h = TrackView::Get( *t ).GetHeight();

   // f codes the opacity as a number between 0.0 and 1.0
   float f = wxClip((h-kOpaqueHeight)/(float)(kTranslucentHeight-kOpaqueHeight),0.0,1.0);
   // kOpaque is the shield's alpha for tracks that are not tall


@@ 1330,7 1394,7 @@ struct HorizontalGroup final : TrackPanelGroup {
};


// optional affordance area, and n channels with vertical rulers,
// optional affordance areas, and n channels with vertical rulers,
// alternating with n - 1 resizers;
// each channel-ruler pair might be divided into multiple views
struct ChannelGroup final : TrackPanelGroup {


@@ 1344,7 1408,9 @@ struct ChannelGroup final : TrackPanelGroup {
      const auto channels = TrackList::Channels( mpTrack.get() );
      const auto pLast = *channels.rbegin();
      wxCoord yy = rect.GetTop();
      for ( auto channel : channels ) 
      auto heights = FindAdjustedChannelHeights(*mpTrack);
      auto pHeight = heights.begin();
      for ( auto channel : channels )
      {
         auto &view = TrackView::Get( *channel );
         if (auto affordance = view.GetAffordanceControls())


@@ 1357,9 1423,9 @@ struct ChannelGroup final : TrackPanelGroup {
            yy += kAffordancesAreaHeight;
         }

         auto height = view.GetHeight();
         auto height = *pHeight++;
         rect.SetTop( yy );
         rect.SetHeight( height - kSeparatorThickness );
         rect.SetHeight( height - kChannelSeparatorThickness );
         refinement.emplace_back( yy,
            std::make_shared< VRulersAndChannels >(
               channel->shared_from_this(),


@@ 1368,7 1434,7 @@ struct ChannelGroup final : TrackPanelGroup {
         if ( channel != pLast ) {
            yy += height;
            refinement.emplace_back(
               yy - kSeparatorThickness,
               yy - kChannelSeparatorThickness,
               TrackPanelResizerCell::Get( *channel ).shared_from_this() );
         }
      }


@@ 1466,7 1532,7 @@ struct ResizingChannelGroup final : TrackPanelGroup {
   { return { Axis::Y, Refinement{
      { rect.GetTop(),
         std::make_shared< LabeledChannelGroup >( mpTrack, mLeftOffset ) },
      { rect.GetTop() + rect.GetHeight() - kSeparatorThickness,
      { rect.GetTop() + rect.GetHeight() - kTrackSeparatorThickness,
         TrackPanelResizerCell::Get(
            **TrackList::Channels( mpTrack.get() ).rbegin() ).shared_from_this()
      }


@@ 1494,9 1560,6 @@ struct Subgroup final : TrackPanelGroup {
         for ( auto channel : TrackList::Channels( leader ) ) {
            auto &view = TrackView::Get( *channel );
            height += view.GetHeight();

            if (view.GetAffordanceControls())
               height += kAffordancesAreaHeight;
         }
         refinement.emplace_back( yy,
            std::make_shared< ResizingChannelGroup >(


@@ 1542,7 1605,7 @@ wxRect TrackPanel::FindTrackRect( const Track * target )
{
   auto leader = *GetTracks()->FindLeader( target );
   if (!leader) {
      return { 0, 0, 0, 0 };
      return {};
   }

   return CellularPanel::FindRect( [&] ( TrackPanelNode &node ) {


@@ 1552,6 1615,46 @@ wxRect TrackPanel::FindTrackRect( const Track * target )
   } );
}

wxRect TrackPanel::FindFocusedTrackRect( const Track * target )
{
   auto rect = FindTrackRect(target);
   if (rect != wxRect{}) {
      // Enlarge horizontally.
      // PRL:  perhaps it's one pixel too much each side, including some gray
      // beyond the yellow?
      rect.x = 0;
      GetClientSize(&rect.width, nullptr);

      // Enlarge vertically, enough to enclose the yellow focus border pixels
      // The the outermost ring of gray pixels is included on three sides
      // but not the top (should that be fixed?)

      // (Note that TrackPanel paints its focus over the "top margin" of the
      // rectangle allotted to the track, according to TrackView::GetY() and
      // TrackView::GetHeight(), but also over the margin of the next track.)

      rect.height += kBottomMargin;
      int dy = kTopMargin - 1;
      rect.Inflate( 0, dy );

      // Note that this rectangle does not coincide with any one of
      // the nodes in the subdivision.
   }
   return rect;
}

std::vector<wxRect> TrackPanel::FindRulerRects( const Track *target )
{
   std::vector<wxRect> results;
   if (target)
      VisitCells( [&]( const wxRect &rect, TrackPanelCell &visited ) {
         if (auto pRuler = dynamic_cast<const TrackVRulerControls*>(&visited);
             pRuler && pRuler->FindTrack().get() == target)
            results.push_back(rect);
      } );
   return results;
}

TrackPanelCell *TrackPanel::GetFocusedCell()
{
   auto pTrack = TrackFocus::Get( *GetProject() ).Get();


@@ 1575,27 1678,3 @@ void TrackPanel::OnTrackFocusChange( wxCommandEvent &event )
      Refresh( false );
   }
}

IsVisibleTrack::IsVisibleTrack(AudacityProject *project)
   : mPanelRect {
        wxPoint{ 0, ViewInfo::Get( *project ).vpos },
        wxSize{
           ViewInfo::Get( *project ).GetTracksUsableWidth(),
           ViewInfo::Get( *project ).GetHeight()
        }
     }
{}

bool IsVisibleTrack::operator () (const Track *pTrack) const
{
   // Need to return true if this track or a later channel intersects
   // the view
   return
   TrackList::Channels(pTrack).StartingWith(pTrack).any_of(
      [this]( const Track *pT ) {
         auto &view = TrackView::Get( *pT );
         wxRect r(0, view.GetY(), 1, view.GetHeight());
         return r.Intersects(mPanelRect);
      }
   );
}

M src/TrackPanel.h => src/TrackPanel.h +25 -5
@@ 31,6 31,9 @@

class wxRect;

// All cells of the TrackPanel are subclasses of this
class CommonTrackPanelCell;

class SpectrumAnalyst;
class Track;
class TrackList;


@@ 135,10 138,27 @@ protected:
public:
   void MakeParentRedrawScrollbars();

   // Rectangle includes track control panel, and the vertical ruler, and
   // the proper track area of all channels, and the separators between them.
   /*!
    @return includes track control panel, and the vertical ruler, and
    the proper track area of all channels, and the separators between them.
    If target is nullptr, returns empty rectangle.
   */
   wxRect FindTrackRect( const Track * target );

   /*!
    @return includes what's in `FindTrackRect(target)` and the focus ring
    area around it.
    If target is nullptr, returns empty rectangle.
   */
   wxRect FindFocusedTrackRect( const Track * target );

   /*!
    @return extents of the vertical rulers of one channel, top to bottom.
    (There may be multiple sub-views, each with a ruler.)
    If target is nullptr, returns an empty vector.
    */
   std::vector<wxRect> FindRulerRects( const Track * target );

protected:
   // Get the root object defining a recursive subdivision of the panel's
   // area into cells


@@ 160,8 180,8 @@ public:
   // Set the object that performs catch-all event handling when the pointer
   // is not in any track or ruler or control panel.
   void SetBackgroundCell
      (const std::shared_ptr< TrackPanelCell > &pCell);
   std::shared_ptr< TrackPanelCell > GetBackgroundCell();
      (const std::shared_ptr< CommonTrackPanelCell > &pCell);
   std::shared_ptr< CommonTrackPanelCell > GetBackgroundCell();

public:



@@ 201,7 221,7 @@ protected:

 protected:

   std::shared_ptr<TrackPanelCell> mpBackground;
   std::shared_ptr<CommonTrackPanelCell> mpBackground;

   DECLARE_EVENT_TABLE()


A src/TrackPanelConstants.h => src/TrackPanelConstants.h +27 -0
@@ 0,0 1,27 @@
/*!********************************************************************
 
 Audacity: A Digital Audio Editor
 
 TrackPanelConstants.h
 
 Paul Licameli split from ViewInfo.h
 
 **********************************************************************/

#ifndef  __AUDACITY_TRACK_PANEL_CONSTANTS__
#define  __AUDACITY_TRACK_PANEL_CONSTANTS__

#include "ZoomInfo.h"

// See big pictorial comment in TrackPanel.cpp for explanation of these numbers
//! constants related to y coordinates in the track panel
enum : int {
   kAffordancesAreaHeight = 18,
   kTopInset = 4,
   kTopMargin = kTopInset + kBorderThickness,
   kBottomMargin = kShadowThickness + kBorderThickness,
   kTrackSeparatorThickness = kBottomMargin + kTopMargin,
   kChannelSeparatorThickness = 1,
};

#endif

M src/TrackPanelResizeHandle.cpp => src/TrackPanelResizeHandle.cpp +14 -14
@@ 69,7 69,7 @@ UIHandle::Result TrackPanelResizeHandle::Click(
         int coord = 0;
         for ( const auto channel : range ) {
            int newCoord = ((double)ii++ /size) * height;
            TrackView::Get(*channel).SetHeight( newCoord - coord );
            TrackView::Get(*channel).SetExpandedHeight( newCoord - coord );
            coord = newCoord;
         }
         ProjectHistory::Get( *pProject ).ModifyState(false);


@@ 92,7 92,7 @@ TrackPanelResizeHandle::TrackPanelResizeHandle
   auto last = *channels.rbegin();
   auto &lastView = TrackView::Get( *last );
   mInitialTrackHeight = lastView.GetHeight();
   mInitialActualHeight = lastView.GetActualHeight();
   mInitialExpandedHeight = lastView.GetExpandedHeight();
   mInitialMinimized = lastView.GetMinimized();

   if (channels.size() > 1) {


@@ 100,7 100,7 @@ TrackPanelResizeHandle::TrackPanelResizeHandle
      auto &firstView = TrackView::Get( *first );

      mInitialUpperTrackHeight = firstView.GetHeight();
      mInitialUpperActualHeight = firstView.GetActualHeight();
      mInitialUpperExpandedHeight = firstView.GetExpandedHeight();

      if (track.get() == *channels.rbegin())
         // capturedTrack is the lowest track


@@ 137,7 137,7 @@ UIHandle::Result TrackPanelResizeHandle::Drag
      auto channels = TrackList::Channels( pTrack.get() );
      for (auto channel : channels) {
         auto &channelView = TrackView::Get( *channel );
         channelView.SetHeight(channelView.GetHeight());
         channelView.SetExpandedHeight(channelView.GetHeight());
         channelView.SetMinimized( false );
      }



@@ 171,8 171,8 @@ UIHandle::Result TrackPanelResizeHandle::Drag
      if (newUpperTrackHeight < prevView.GetMinimizedHeight())
         newUpperTrackHeight = prevView.GetMinimizedHeight();

      view.SetHeight(newTrackHeight);
      prevView.SetHeight(newUpperTrackHeight);
      view.SetExpandedHeight(newTrackHeight);
      prevView.SetExpandedHeight(newUpperTrackHeight);
   };

   auto doResizeBetween = [&] (Track *next, bool WXUNUSED(vStereo)) {


@@ 194,15 194,15 @@ UIHandle::Result TrackPanelResizeHandle::Drag
         mInitialUpperTrackHeight + mInitialTrackHeight - view.GetMinimizedHeight();
      }

      view.SetHeight(newUpperTrackHeight);
      nextView.SetHeight(newTrackHeight);
      view.SetExpandedHeight(newUpperTrackHeight);
      nextView.SetExpandedHeight(newTrackHeight);
   };

   auto doResize = [&] {
      int newTrackHeight = mInitialTrackHeight + delta;
      if (newTrackHeight < view.GetMinimizedHeight())
         newTrackHeight = view.GetMinimizedHeight();
      view.SetHeight(newTrackHeight);
      view.SetExpandedHeight(newTrackHeight);
   };

   //STM: We may be dragging one or two (stereo) tracks.


@@ 268,7 268,7 @@ UIHandle::Result TrackPanelResizeHandle::Cancel(AudacityProject *pProject)
   case IsResizing:
   {
      auto &view = TrackView::Get( *pTrack );
      view.SetHeight(mInitialActualHeight);
      view.SetExpandedHeight(mInitialExpandedHeight);
      view.SetMinimized( mInitialMinimized );
   }
   break;


@@ 277,9 277,9 @@ UIHandle::Result TrackPanelResizeHandle::Cancel(AudacityProject *pProject)
      Track *const next = * ++ tracks.Find(pTrack.get());
      auto
         &view = TrackView::Get( *pTrack ), &nextView = TrackView::Get( *next );
      view.SetHeight(mInitialUpperActualHeight);
      view.SetExpandedHeight(mInitialUpperExpandedHeight);
      view.SetMinimized( mInitialMinimized );
      nextView.SetHeight(mInitialActualHeight);
      nextView.SetExpandedHeight(mInitialExpandedHeight);
      nextView.SetMinimized( mInitialMinimized );
   }
   break;


@@ 288,9 288,9 @@ UIHandle::Result TrackPanelResizeHandle::Cancel(AudacityProject *pProject)
      Track *const prev = * -- tracks.Find(pTrack.get());
      auto
         &view = TrackView::Get( *pTrack ), &prevView = TrackView::Get( *prev );
      view.SetHeight(mInitialActualHeight);
      view.SetExpandedHeight(mInitialExpandedHeight);
      view.SetMinimized( mInitialMinimized );
      prevView.SetHeight(mInitialUpperActualHeight);
      prevView.SetExpandedHeight(mInitialUpperExpandedHeight);
      prevView.SetMinimized(mInitialMinimized);
   }
   break;

M src/TrackPanelResizeHandle.h => src/TrackPanelResizeHandle.h +2 -2
@@ 58,9 58,9 @@ private:

   bool mInitialMinimized{};
   int mInitialTrackHeight{};
   int mInitialActualHeight{};
   int mInitialExpandedHeight{};
   int mInitialUpperTrackHeight{};
   int mInitialUpperActualHeight{};
   int mInitialUpperExpandedHeight{};

   int mMouseClickY{};
};

M src/TrackPanelResizerCell.cpp => src/TrackPanelResizerCell.cpp +1 -1
@@ 74,7 74,7 @@ void TrackPanelResizerCell::Draw(
            
            // Paint the left part of the background
            const auto artist = TrackArtist::Get( context );
            auto labelw = artist->pZoomInfo->GetLabelWidth();
            auto labelw = artist->pZoomInfo->GetLeftOffset() - 1;
            AColor::MediumTrackInfo( dc, pTrack->GetSelected() );
            dc->DrawRectangle(
               rect.GetX(), rect.GetY(), labelw, rect.GetHeight() );

M src/ViewInfo.h => src/ViewInfo.h +1 -7
@@ 98,14 98,8 @@ private:
   SelectedRegion mRegion;
};

// See big pictorial comment in TrackPanel.cpp for explanation of these numbers
enum : int {
   // constants related to y coordinates in the track panel
   kAffordancesAreaHeight = 20,
   kTopInset = 4,
   kTopMargin = kTopInset + kBorderThickness,
   kBottomMargin = kShadowThickness + kBorderThickness,
   kSeparatorThickness = kBottomMargin + kTopMargin,
   kVerticalPadding = 6, /*!< Width of padding below each channel group */
};

enum : int {

M src/WaveTrack.cpp => src/WaveTrack.cpp +63 -3
@@ 35,6 35,7 @@ from the project that will own the track.
#include <wx/defs.h>
#include <wx/intl.h>
#include <wx/debug.h>
#include <wx/log.h>

#include <float.h>
#include <math.h>


@@ 64,6 65,33 @@ from the project that will own the track.

using std::max;

namespace {

bool AreAligned(const WaveClipPointers& a, const WaveClipPointers& b)
{
   if (a.size() != b.size())
      return false;

   const auto compare = [](const WaveClip* a, const WaveClip* b) {
      return a->GetStartTime() == b->GetStartTime() &&
         a->GetNumSamples() == b->GetNumSamples();
   };

   return std::mismatch(a.begin(), a.end(), b.begin(), compare).first == a.end();
}

//Handles possible future file values
Track::LinkType ToLinkType(int value)
{
   if (value < 0)
      return Track::LinkType::None;
   else if (value > 3)
      return Track::LinkType::Group;
   return static_cast<Track::LinkType>(value);
}

}

static ProjectFileIORegistry::Entry registerFactory{
   wxT( "wavetrack" ),
   []( AudacityProject &project ){


@@ 240,7 268,39 @@ void WaveTrack::SetPanFromChannelType()
      SetPan( -1.0f );
   else if( mChannel == Track::RightChannel )
      SetPan( 1.0f );
};
}

bool WaveTrack::LinkConsistencyCheck()
{
   auto err = PlayableTrack::LinkConsistencyCheck();

   auto linkType = GetLinkType();
   if (static_cast<int>(linkType) == 1 || //Comes from old audacity version
       linkType == LinkType::Aligned) 
   {
      auto next = dynamic_cast<WaveTrack*>(*std::next(GetOwner()->Find(this)));
      if (next == nullptr)
      {
         //next track is not a wave track, fix and report error
          wxLogWarning(
             wxT("Right track %s is expected to be a WaveTrack.\n Removing link from left wave track %s."),
             next->GetName(), GetName());
         SetLinkType(LinkType::None);
         SetChannel(MonoChannel);
         err = true;
      }
      else
      {
         auto newLinkType = AreAligned(SortedClipArray(), next->SortedClipArray())
            ? LinkType::Aligned : LinkType::Group;
         //not an error
         if (newLinkType != linkType)
            SetLinkType(newLinkType);
      }
   }
   return !err;
}


void WaveTrack::SetLastScaleType() const
{


@@ 1667,7 1727,7 @@ bool WaveTrack::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
         }
         else if (!wxStrcmp(attr, wxT("linked")) &&
                  XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
            SetLinked(nValue != 0);
            SetLinkType(ToLinkType(nValue));
         else if (!wxStrcmp(attr, wxT("colorindex")) &&
                  XMLValueChecker::IsGoodString(strValue) &&
                  strValue.ToLong(&nValue))


@@ 1734,7 1794,7 @@ void WaveTrack::WriteXML(XMLWriter &xmlFile) const
   xmlFile.StartTag(wxT("wavetrack"));
   this->Track::WriteCommonXMLAttributes( xmlFile );
   xmlFile.WriteAttr(wxT("channel"), mChannel);
   xmlFile.WriteAttr(wxT("linked"), mLinked);
   xmlFile.WriteAttr(wxT("linked"), static_cast<int>(GetLinkType()));
   this->PlayableTrack::WriteXMLAttributes(xmlFile);
   xmlFile.WriteAttr(wxT("rate"), mRate);
   xmlFile.WriteAttr(wxT("gain"), (double)mGain);

M src/WaveTrack.h => src/WaveTrack.h +2 -0
@@ 100,6 100,8 @@ private:
   ChannelType GetChannel() const override;
   virtual void SetPanFromChannelType() override;

   bool LinkConsistencyCheck() override;

   /** @brief Get the time at which the first clip in the track starts
    *
    * @return time in seconds, or zero if there are no clips in the track

M src/ZoomInfo.h => src/ZoomInfo.h +4 -2
@@ 98,9 98,11 @@ public:
   int GetVRulerWidth() const { return mVRulerWidth; }
   void SetVRulerWidth( int width ) { mVRulerWidth = width; }
   int GetVRulerOffset() const { return kTrackInfoWidth + kLeftMargin; }
   int GetLabelWidth() const { return GetVRulerOffset() + GetVRulerWidth(); }
   int GetLeftOffset() const { return GetLabelWidth() + 1;}

   // The x-coordinate of the start of the displayed track data
   int GetLeftOffset() const
      { return GetVRulerOffset() + GetVRulerWidth() + 1; }
   // The number of pixel columns for display of track data
   int GetTracksUsableWidth() const
   {
      return

M src/commands/GetInfoCommand.cpp => src/commands/GetInfoCommand.cpp +9 -69
@@ 694,80 694,20 @@ void GetInfoCommand::ExploreAdornments( const CommandContext &context,
}

void GetInfoCommand::ExploreTrackPanel( const CommandContext &context,
   wxPoint P, wxWindow * pWin, int WXUNUSED(Id), int depth )
   wxPoint P, int depth )
{
   AudacityProject * pProj = &context.project;
   auto &tp = TrackPanel::Get( *pProj );
   auto &viewInfo = ViewInfo::Get( *pProj );

   wxRect trackRect = pWin->GetRect();

   for ( auto t : TrackList::Get( *pProj ).Any() + IsVisibleTrack{ pProj } ) {
      auto &view = TrackView::Get( *t );
      trackRect.y = view.GetY() - viewInfo.vpos;
      trackRect.height = view.GetHeight();

#if 0
      // Work in progress on getting the TCP button positions and sizes.
      wxRect rect = trackRect;
      Track *l = t->GetLink();

      if (t->GetLinked()) {
         rect.height += l->GetHeight();
      }

      switch (t->GetKind()) {
         case Track::Wave:
         {
            break;
         }
#ifdef USE_MIDI
         case Track::Note:
         {
            break;
         }
#endif // USE_MIDI
         case Track::Label:
            break;
         case Track::Time:
            break;
      }
      {
         // Start with whole track rect
         wxRect R = trackRect;

         // Now exclude left, right, and top insets
         R.x += kLeftInset;
         R.y += kTopInset;
         R.width -= kLeftInset * 2;
         R.height -= kTopInset;

         int labelw = viewInfo.GetLabelWidth();
         //int vrul = viewInfo.GetVRulerOffset();
         bool bIsWave = true;
         //mTrackInfo.DrawBackground(dc, R, t->GetSelected(), bIsWave, labelw, vrul);


         for (Overlay * pOverlay : pTP->mOverlays) {
            auto R2(pOverlay->GetRectangle(trackRect.GetSize()).first);
            context.Status( wxString::Format("  [ %2i, %3i, %3i, %3i, %3i, \"%s\" ],", 
               depth, R2.GetLeft(), R2.GetTop(), R2.GetRight(), R2.GetBottom(), "Otherthing" )); 
         }
      }
#endif

      // The VRuler.
      {  
         wxRect R = trackRect;
         R.x += viewInfo.GetVRulerOffset();
         R.y += kTopMargin;
         R.width = viewInfo.GetVRulerWidth();
         R.height -= (kTopMargin + kBottomMargin);
   wxRect panelRect{ {}, tp.GetSize() };
   for ( auto t : TrackList::Get( *pProj ).Any() ) {
      auto rulers = tp.FindRulerRects(t);
      for (auto &R : rulers) {
         if (!R.Intersects(panelRect))
            continue;
         R.SetPosition( R.GetPosition() + P );

         context.StartStruct();
         context.AddItem( depth, "depth" );
         context.AddItem( "VRuler", "label" ); 
         context.AddItem( "VRuler", "label" );
         context.StartField("box");
         context.StartArray();
         context.AddItem( R.GetLeft() );


@@ 790,7 730,7 @@ void GetInfoCommand::ExploreWindows( const CommandContext &context,
   if( pWin->GetName() == "Track Panel" )
   {
      wxRect R = pWin->GetScreenRect();
      ExploreTrackPanel(  context, R.GetPosition()-P, pWin, Id, depth );
      ExploreTrackPanel(  context, R.GetPosition()-P, depth );
      return;
   }
   wxWindowList list = pWin->GetChildren();

M src/commands/GetInfoCommand.h => src/commands/GetInfoCommand.h +1 -1
@@ 58,7 58,7 @@ private:

   void ExploreMenu( const CommandContext &context, wxMenu * pMenu, int Id, int depth );
   void ExploreTrackPanel( const CommandContext & context,
      wxPoint P, wxWindow * pWin, int Id, int depth );
      wxPoint P, int depth );
   void ExploreAdornments( const CommandContext & context,
      wxPoint P, wxWindow * pWin, int Id, int depth );
   void ExploreWindows( const CommandContext & context,

M src/commands/ScreenshotCommand.cpp => src/commands/ScreenshotCommand.cpp +1 -21
@@ 708,27 708,7 @@ wxRect ScreenshotCommand::GetTracksRect(TrackPanel * panel){
wxRect ScreenshotCommand::GetTrackRect( AudacityProject * pProj, TrackPanel * panel, int n){
   auto FindRectangle = []( TrackPanel &panel, Track &t )
   {
      // This rectangle omits the focus ring about the track, and
      // also within that, a narrow black border with a "shadow" below and
      // to the right
      wxRect rect = panel.FindTrackRect( &t );

      // Enlarge horizontally.
      // PRL:  perhaps it's one pixel too much each side, including some gray
      // beyond the yellow?
      rect.x = 0;
      panel.GetClientSize(&rect.width, nullptr);

      // Enlarge vertically, enough to enclose the yellow focus border pixels
      // Omit the outermost ring of gray pixels

      // (Note that TrackPanel paints its focus over the "top margin" of the
      // rectangle allotted to the track, according to TrackView::GetY() and
      // TrackView::GetHeight(), but also over the margin of the next track.)

      rect.height += kBottomMargin;
      int dy = kTopMargin - 1;
      rect.Inflate( 0, dy );
      wxRect rect = panel.FindFocusedTrackRect( &t );

      // Reposition it relative to parent of panel
      rect.SetPosition(

M src/commands/SetLabelCommand.cpp => src/commands/SetLabelCommand.cpp +3 -3
@@ 114,13 114,13 @@ bool SetLabelCommand::Apply(const CommandContext & context)
      auto &view = LabelTrackView::Get( *labelTrack );
      if( mbSelected )
      {
         view.SetSelectedIndex( ii );
         view.SetNavigationIndex( ii );
         double t0 = pLabel->selectedRegion.t0();
         double t1 = pLabel->selectedRegion.t1();
         selectedRegion.setTimes( t0, t1);
      }
      else if( view.GetSelectedIndex( context.project ) == ii )
         view.SetSelectedIndex( -1 );
      else if( view.GetNavigationIndex( context.project ) == ii )
         view.SetNavigationIndex( -1 );
   }

   labelTrack->SortLabels();

M src/commands/SetTrackInfoCommand.cpp => src/commands/SetTrackInfoCommand.cpp +1 -1
@@ 383,7 383,7 @@ bool SetTrackVisualsCommand::ApplyInner(const CommandContext & context, Track * 
      wt->SetWaveColorIndex( mColour );

   if( t && bHasHeight )
      TrackView::Get( *t ).SetHeight( mHeight );
      TrackView::Get( *t ).SetExpandedHeight( mHeight );

   if( wt && bHasDisplayType  ) {
      auto &view = WaveTrackView::Get( *wt );

M src/effects/Effect.cpp => src/effects/Effect.cpp +1 -1
@@ 2395,8 2395,8 @@ void Effect::Preview(bool dryOnly)
         mixRight->Offset(-mixRight->GetStartTime());
         mixRight->SetSelected(true);
         pRight = mTracks->Add( mixRight );
         mTracks->MakeMultiChannelTrack(*pLeft, 2, true);
      }
      mTracks->GroupChannels(*pLeft, pRight ? 2 : 1);
   }
   else {
      for (auto src : saveTracks->Any< const WaveTrack >()) {

M src/effects/StereoToMono.cpp => src/effects/StereoToMono.cpp +1 -1
@@ 213,7 213,7 @@ bool EffectStereoToMono::ProcessOne(sampleCount & curTime, sampleCount totalTime
   double minStart = wxMin(left->GetStartTime(), right->GetStartTime());
   left->Clear(left->GetStartTime(), left->GetEndTime());
   left->Paste(minStart, outTrack.get());
   mOutputTracks->GroupChannels(*left,  1);
   mOutputTracks->UnlinkChannels(*left);
   mOutputTracks->Remove(right);

   return bResult;

M src/menus/ClipMenus.cpp => src/menus/ClipMenus.cpp +2 -2
@@ 643,7 643,7 @@ double DoClipMove( AudacityProject &project, Track *track,
      // Find the first channel that has a clip at time t0
      auto hitTestResult = TrackShifter::HitTestResult::Track;
      for (auto channel : TrackList::Channels(track) ) {
         uShifter = MakeTrackShifter::Call( *track, project );
         uShifter = MakeTrackShifter::Call( *channel, project );
         if ( (hitTestResult = uShifter->HitTest( t0, viewInfo )) ==
             TrackShifter::HitTestResult::Miss )
            uShifter.reset();


@@ 657,7 657,7 @@ double DoClipMove( AudacityProject &project, Track *track,
      auto desiredT0 = viewInfo.OffsetTimeByPixels( t0, ( right ? 1 : -1 ) );
      auto desiredSlideAmount = pShifter->HintOffsetLarger( desiredT0 - t0 );

      state.Init( project, *track, hitTestResult, std::move( uShifter ),
      state.Init( project, pShifter->GetTrack(), hitTestResult, std::move( uShifter ),
         t0, viewInfo, trackList, syncLocked );

      auto hSlideAmount = state.DoSlideHorizontal( desiredSlideAmount );

M src/menus/EditMenus.cpp => src/menus/EditMenus.cpp +2 -1
@@ 46,7 46,7 @@ bool DoPasteText(AudacityProject &project)
   for (auto pLabelTrack : tracks.Any<LabelTrack>())
   {
      // Does this track have an active label?
      if (LabelTrackView::Get( *pLabelTrack ).HasSelection( project )) {
      if (LabelTrackView::Get( *pLabelTrack ).GetTextEditIndex(project) != -1) {

         // Yes, so try pasting into it
         auto &view = LabelTrackView::Get( *pLabelTrack );


@@ 607,6 607,7 @@ void OnPaste(const CommandContext &context)
      if (ff) {
         TrackFocus::Get(project).Set(ff);
         ff->EnsureVisible();
         ff->LinkConsistencyCheck();
      }
   }
}

M src/menus/LabelMenus.cpp => src/menus/LabelMenus.cpp +1 -1
@@ 346,7 346,7 @@ void OnPasteNewLabel(const CommandContext &context)
      // Unselect the last label, so we'll have just one active label when
      // we're done
      if (plt)
         LabelTrackView::Get( *plt ).SetSelectedIndex( -1 );
         LabelTrackView::Get( *plt ).ResetTextSelection();

      // Add a NEW label, paste into it
      // Paul L:  copy whatever defines the selected region, not just times

M src/menus/TrackMenus.cpp => src/menus/TrackMenus.cpp +5 -5
@@ 87,10 87,10 @@ void DoMixAndRender
      auto pNewLeft = tracks.Add( uNewLeft );
      decltype(pNewLeft) pNewRight{};
      if (uNewRight)
         pNewRight = tracks.Add( uNewRight );

      // Do this only after adding tracks to the list
      tracks.GroupChannels(*pNewLeft, pNewRight ? 2 : 1);
      {
         pNewRight = tracks.Add(uNewRight);
         tracks.MakeMultiChannelTrack(*pNewLeft, 2, true);
      }

      // If we're just rendering (not mixing), keep the track name the same
      if (selectedCount==1) {


@@ 644,7 644,7 @@ void OnNewStereoTrack(const CommandContext &context)
   auto right = tracks.Add( trackFactory.NewWaveTrack( defaultFormat, rate ) );
   right->SetSelected(true);

   tracks.GroupChannels(*left, 2);
   tracks.MakeMultiChannelTrack(*left, 2, true);

   ProjectHistory::Get( project )
      .PushState(XO("Created new stereo audio track"), XO("New Track"));

M src/menus/ViewMenus.cpp => src/menus/ViewMenus.cpp +1 -1
@@ 154,7 154,7 @@ void DoZoomFitV(AudacityProject &project)
   height = std::max( (int)TrackInfo::MinimumTrackHeight(), height );

   for (auto t : range)
      TrackView::Get( *t ).SetHeight(height);
      TrackView::Get( *t ).SetExpandedHeight(height);
}
}


M src/tracks/labeltrack/ui/LabelDefaultClickHandle.cpp => src/tracks/labeltrack/ui/LabelDefaultClickHandle.cpp +0 -1
@@ 72,7 72,6 @@ UIHandle::Result LabelDefaultClickHandle::Click
         if (pLT != &TrackView::Get( *lt )) {
            auto &view = LabelTrackView::Get( *lt );
            view.ResetFlags();
            view.SetSelectedIndex( -1 );
         }
      }
   }

M src/tracks/labeltrack/ui/LabelGlyphHandle.cpp => src/tracks/labeltrack/ui/LabelGlyphHandle.cpp +85 -34
@@ 19,6 19,9 @@ Paul Licameli split from TrackPanel.cpp
#include "../../../TrackPanelMouseEvent.h"
#include "../../../UndoManager.h"
#include "../../../ViewInfo.h"
#include "../../../SelectionState.h"
#include "../../../ProjectAudioIO.h"
#include "../../../tracks/ui/TimeShiftHandle.h"

#include <wx/cursor.h>
#include <wx/translation.h>


@@ 57,6 60,7 @@ void LabelTrackHit::OnLabelPermuted( LabelTrackEvent &e )
   
   update( mMouseOverLabelLeft );
   update( mMouseOverLabelRight );
   update( mMouseOverLabel );
}

LabelGlyphHandle::LabelGlyphHandle


@@ 124,26 128,24 @@ LabelGlyphHandle::~LabelGlyphHandle()
void LabelGlyphHandle::HandleGlyphClick
(LabelTrackHit &hit, const wxMouseEvent & evt,
 const wxRect & r, const ZoomInfo &zoomInfo,
 NotifyingSelectedRegion &WXUNUSED(newSel))
 NotifyingSelectedRegion &newSel)
{
   if (evt.ButtonDown())
   {
      //OverGlyph sets mMouseOverLabel to be the chosen label.
      const auto pTrack = mpLT;
      LabelTrackView::OverGlyph(*pTrack, hit, evt.m_x, evt.m_y);

      hit.mIsAdjustingLabel = evt.Button(wxMOUSE_BTN_LEFT) &&
         ( hit.mEdge & 3 ) != 0;

      if (hit.mIsAdjustingLabel)
      {
         double t = 0.0;
         // We move if we hit the centre, we adjust one edge if we hit a chevron.
         // This is if we are moving just one edge.
         hit.mbIsMoving = (hit.mEdge & 4)!=0;

         // No to the above!  We initially expect to be moving just one edge.
         hit.mbIsMoving = false;
         auto& view = LabelTrackView::Get(*pTrack);
         view.ResetTextSelection();

         double t = 0.0;
         
         // When we start dragging the label(s) we don't want them to jump.
         // so we calculate the displacement of the mouse from the drag center
         // and use that in subsequent dragging calculations.  The mouse stays


@@ 170,11 172,14 @@ void LabelGlyphHandle::HandleGlyphClick
            // If we're on a boundary between two different labels, 
            // then it's an adjust.
            // In both cases the two points coalesce.
            hit.mbIsMoving = (hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight);

            // 
            // NOTE: seems that it's not neccessary that hitting the both
            // left and right handles mean that we're dealing with a point, 
            // but the range will be turned into a point on click
            bool isPointLabel = hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight;
            // Except!  We don't coalesce if both ends are from the same label and
            // we have deliberately chosen to preserve length, by holding shift down.
            if (!(hit.mbIsMoving && evt.ShiftDown()))
            if (!(isPointLabel && evt.ShiftDown()))
            {
               MayAdjustLabel(hit, hit.mMouseOverLabelLeft, -1, false, t);
               MayAdjustLabel(hit, hit.mMouseOverLabelRight, 1, false, t);


@@ 190,6 195,10 @@ void LabelGlyphHandle::HandleGlyphClick
         {
            t = mLabels[ hit.mMouseOverLabelLeft ].getT0();
         }
         else if (hit.mMouseOverLabel >= 0)
         {
            t = mLabels[hit.mMouseOverLabel].getT0();
         }
         mxMouseDisplacement = zoomInfo.TimeToPosition(t, r.x) - evt.m_x;
      }
   }


@@ 201,6 210,8 @@ UIHandle::Result LabelGlyphHandle::Click
   auto result = LabelDefaultClickHandle::Click( evt, pProject );

   const wxMouseEvent &event = evt.event;
   auto& selectionState = SelectionState::Get(*pProject);
   auto& tracks = TrackList::Get(*pProject);

   auto &viewInfo = ViewInfo::Get( *pProject );
   HandleGlyphClick(


@@ 216,13 227,6 @@ UIHandle::Result LabelGlyphHandle::Click
      // redraw the track.
      result |= RefreshCode::RefreshCell;

   // handle shift+ctrl down
   /*if (event.ShiftDown()) { // && event.ControlDown()) {
      lTrack->SetHighlightedByKey(true);
      Refresh(false);
      return;
   }*/

   return result;
}



@@ 300,24 304,66 @@ bool LabelGlyphHandle::HandleGlyphDragRelease
   const auto &mLabels = pTrack->GetLabels();
   if(evt.LeftUp())
   {
      bool lupd = false, rupd = false;
      bool updated = false;
      if( hit.mMouseOverLabelLeft >= 0 ) {
         auto labelStruct = mLabels[ hit.mMouseOverLabelLeft ];
         lupd = labelStruct.updated;
         updated |= labelStruct.updated;
         labelStruct.updated = false;
         pTrack->SetLabel( hit.mMouseOverLabelLeft, labelStruct );
      }
      if( hit.mMouseOverLabelRight >= 0 ) {
         auto labelStruct = mLabels[ hit.mMouseOverLabelRight ];
         rupd = labelStruct.updated;
         updated |= labelStruct.updated;
         labelStruct.updated = false;
         pTrack->SetLabel( hit.mMouseOverLabelRight, labelStruct );
      }

      if (hit.mMouseOverLabel >= 0)
      {
          auto labelStruct = mLabels[hit.mMouseOverLabel];
          if (!labelStruct.updated)
          {
              //happens on click over bar between handles (without moving a cursor)
              newSel = labelStruct.selectedRegion;

              // IF the user clicked a label, THEN select all other tracks by Label
              // do nothing if at least one other track is selected
              auto& selectionState = SelectionState::Get(project);
              auto& tracks = TrackList::Get(project);

              bool done = tracks.Selected().any_of(
                  [&](const Track* track) { return track != static_cast<Track*>(pTrack.get()); }
              );

              if (!done) {
                  //otherwise, select all tracks
                  for (auto t : tracks.Any())
                      selectionState.SelectTrack(*t, true, true);
              }

              // Do this after, for its effect on TrackPanel's memory of last selected
              // track (which affects shift-click actions)
              selectionState.SelectTrack(*pTrack.get(), true, true);

              // PRL: bug1659 -- make selection change undo correctly
              updated = !ProjectAudioIO::Get(project).IsAudioActive();
              
              auto& view = LabelTrackView::Get(*pTrack);
              view.SetNavigationIndex(hit.mMouseOverLabel);
          }
          else
          {
              labelStruct.updated = false;
              pTrack->SetLabel(hit.mMouseOverLabel, labelStruct);
              updated = true;
          }
      }

      hit.mIsAdjustingLabel = false;
      hit.mMouseOverLabelLeft  = -1;
      hit.mMouseOverLabelRight = -1;
      return lupd || rupd;
      hit.mMouseOverLabel = -1;
      return updated;
   }

   if(evt.Dragging())


@@ 328,35 374,40 @@ bool LabelGlyphHandle::HandleGlyphDragRelease
      //      to allow scrolling while dragging labels
      int x = Constrain( evt.m_x + mxMouseDisplacement - r.x, 0, r.width);

      // If exactly one edge is selected we allow swapping
      bool bAllowSwapping =
         ( hit.mMouseOverLabelLeft >=0 ) !=
         ( hit.mMouseOverLabelRight >= 0);
      double fNewX = zoomInfo.PositionToTime(x, 0);
      // Moving the whole ranged label
      if (hit.mMouseOverLabel != -1)
      {
         MayMoveLabel(hit.mMouseOverLabel, -1, fNewX);
      }
      // If we're on the 'dot' and nowe're moving,
      // Though shift-down inverts that.
      // and if both edges the same, then we're always moving the label.
      bool bLabelMoving = hit.mbIsMoving;
      bLabelMoving ^= evt.ShiftDown();
      bLabelMoving |= ( hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight );
      double fNewX = zoomInfo.PositionToTime(x, 0);
      if( bLabelMoving )
      else if((hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight) || evt.ShiftDown())
      {
         MayMoveLabel( hit.mMouseOverLabelLeft,  -1, fNewX );
         MayMoveLabel( hit.mMouseOverLabelRight, +1, fNewX );
      }
      else
      {
         // If exactly one edge is selected we allow swapping
         bool bAllowSwapping =
            (hit.mMouseOverLabelLeft >= 0) !=
            (hit.mMouseOverLabelRight >= 0);
         MayAdjustLabel( hit, hit.mMouseOverLabelLeft,  -1, bAllowSwapping, fNewX );
         MayAdjustLabel( hit, hit.mMouseOverLabelRight, +1, bAllowSwapping, fNewX );
      }

      const auto &view = LabelTrackView::Get( *pTrack );
      if( view.HasSelection( project ) )
      auto navigationIndex = view.GetNavigationIndex(project);
      if(navigationIndex != -1 &&
          (navigationIndex == hit.mMouseOverLabel ||
              navigationIndex == hit.mMouseOverLabelLeft ||
              navigationIndex == hit.mMouseOverLabelRight))
      {
         auto selIndex = view.GetSelectedIndex( project );
         //Set the selection region to be equal to
         //the NEW size of the label.
         newSel = mLabels[ selIndex ].selectedRegion;
         newSel = mLabels[navigationIndex].selectedRegion;
      }
      pTrack->SortLabels();
   }

M src/tracks/labeltrack/ui/LabelGlyphHandle.h => src/tracks/labeltrack/ui/LabelGlyphHandle.h +2 -1
@@ 35,9 35,10 @@ struct LabelTrackHit
   ~LabelTrackHit();

   int mEdge{};
   //This one is to distinguish ranged label from point label
   int mMouseOverLabel{ -1 };        /// Keeps track of which (ranged) label the mouse is currently over.
   int mMouseOverLabelLeft{ -1 };    /// Keeps track of which left label the mouse is currently over.
   int mMouseOverLabelRight{ -1 };   /// Keeps track of which right label the mouse is currently over.
   bool mbIsMoving {};
   bool mIsAdjustingLabel {};

   std::shared_ptr<LabelTrack> mpLT {};

M src/tracks/labeltrack/ui/LabelTextHandle.cpp => src/tracks/labeltrack/ui/LabelTextHandle.cpp +24 -74
@@ 70,34 70,21 @@ LabelTextHandle::~LabelTextHandle()
{
}

void LabelTextHandle::HandleTextClick(AudacityProject &
#if defined(__WXGTK__) && (HAVE_GTK)
                                                       project
#endif
                                                              ,
   const wxMouseEvent & evt,
   const wxRect & r, const ZoomInfo &zoomInfo,
   NotifyingSelectedRegion &newSel)
void LabelTextHandle::HandleTextClick(AudacityProject &project, const wxMouseEvent & evt)
{
   auto pTrack = mpLT.lock();
   if (!pTrack)
      return;

   auto &view = LabelTrackView::Get( *pTrack );
   static_cast<void>(r);//compiler food.
   static_cast<void>(zoomInfo);//compiler food.
   if (evt.ButtonDown())
   {
      const auto selIndex = LabelTrackView::OverATextBox( *pTrack, evt.m_x, evt.m_y );
      view.SetSelectedIndex( selIndex );
      if ( selIndex != -1 ) {
         const auto &mLabels = pTrack->GetLabels();
         const auto &labelStruct = mLabels[ selIndex ];
         newSel = labelStruct.selectedRegion;

         if (evt.LeftDown()) {
            mRightDragging = false;
            // Find the NEW drag end
            auto position = view.FindCursorPosition( evt.m_x );
            auto position = view.FindCursorPosition(selIndex, evt.m_x );

            // Anchor shift-drag at the farther end of the previous highlight
            // that is farther from the click, on Mac, for consistency with


@@ 118,13 105,18 @@ void LabelTextHandle::HandleTextClick(AudacityProject &
            else
               initial = position;

            view.SetTextHighlight( initial, position );
            mRightDragging = false;
            view.SetTextSelection(selIndex, initial, position );
         }
         else
         {
            if (!view.IsTextSelected(project))
            {
               auto position = view.FindCursorPosition(selIndex, evt.m_x);
               view.SetTextSelection(selIndex, position, position);
            }
            // Actually this might be right or middle down
            mRightDragging = true;

         }
         // Middle click on GTK: paste from primary selection
#if defined(__WXGTK__) && (HAVE_GTK)
         if (evt.MiddleDown()) {


@@ 132,7 124,7 @@ void LabelTextHandle::HandleTextClick(AudacityProject &
            // case PasteSelectedText() will start a NEW label at the click
            // location
            if (!LabelTrackView::OverTextBox(&labelStruct, evt.m_x, evt.m_y))
               view.SetSelectedIndex( -1 );
               view.ResetTextSelection();
            double t = zoomInfo.PositionToTime(evt.m_x, r.x);
            newSel = SelectedRegion(t, t);
         }


@@ 158,41 150,10 @@ UIHandle::Result LabelTextHandle::Click

   auto result = LabelDefaultClickHandle::Click( evt, pProject );

   auto &selectionState = SelectionState::Get( *pProject );
   auto &tracks = TrackList::Get( *pProject );
   mChanger =
      std::make_shared< SelectionStateChanger >( selectionState, tracks );

   const wxMouseEvent &event = evt.event;
   auto &viewInfo = ViewInfo::Get( *pProject );

   mSelectedRegion = viewInfo.selectedRegion;
   HandleTextClick( *pProject,
      event, evt.rect, viewInfo, viewInfo.selectedRegion );

   {
      // IF the user clicked a label, THEN select all other tracks by Label

      //do nothing if at least one other track is selected
      bool done = tracks.Selected().any_of(
         [&](const Track *pTrack){ return pTrack != pLT.get(); }
      );

      if (!done) {
         //otherwise, select all tracks
         for (auto t : tracks.Any())
            selectionState.SelectTrack( *t, true, true );
      }

      // Do this after, for its effect on TrackPanel's memory of last selected
      // track (which affects shift-click actions)
      selectionState.SelectTrack( *pLT, true, true );
   }

   // PRL: bug1659 -- make selection change undo correctly
   const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
   if (!unsafe)
      ProjectHistory::Get( *pProject ).ModifyState(false);
   HandleTextClick(*pProject, event);

   return result | RefreshCode::RefreshCell;
}


@@ 228,19 189,19 @@ void LabelTextHandle::HandleTextDragRelease(

   if(evt.Dragging())
   {
      if (!mRightDragging)
      auto index = view.GetTextEditIndex(project);
      if (!mRightDragging && index != -1)
         // Update drag end
         view.SetCurrentCursorPosition(
            view.FindCursorPosition( evt.m_x ) );

         view.SetCurrentCursorPosition(view.FindCursorPosition(index, evt.m_x ));
      return;
   }

   if (evt.RightUp()) {
      const auto selIndex = view.GetSelectedIndex( project );
      if ( selIndex != -1 &&
         LabelTrackView::OverTextBox(
            pTrack->GetLabel( selIndex ), evt.m_x, evt.m_y ) ) {
   if (evt.RightUp())
   {
      auto index = view.GetTextEditIndex(project);
      if(index != -1 &&
         LabelTrackView::OverTextBox(pTrack->GetLabel(index), evt.m_x, evt.m_y)) 
      {
         // popup menu for editing
         // TODO: handle context menus via CellularPanel?
         view.ShowContextMenu( project );


@@ 269,10 230,9 @@ UIHandle::Result LabelTextHandle::Drag
         mLabelTrackStartYPos = event.m_y;

         auto pView = pLT ? &LabelTrackView::Get( *pLT ) : nullptr;
         if (pLT &&
            (pView->GetSelectedIndex( project ) != -1) &&
         if (pLT && (pView->GetTextEditIndex( project ) != -1) &&
             LabelTrackView::OverTextBox(
               pLT->GetLabel(pView->GetSelectedIndex( project )),
               pLT->GetLabel(pView->GetTextEditIndex( project )),
               mLabelTrackStartXPos,
               mLabelTrackStartYPos))
            mLabelTrackStartYPos = -1;


@@ 301,11 261,6 @@ UIHandle::Result LabelTextHandle::Release
   // Only selected a part of a text string and changed track selectedness.
   // No undoable effects.

   if (mChanger) {
      mChanger->Commit();
      mChanger.reset();
   }

   const wxMouseEvent &event = evt.event;
   auto pLT = TrackList::Get( *pProject ).Lock(mpLT);
   if (pLT)


@@ 320,11 275,6 @@ UIHandle::Result LabelTextHandle::Release

UIHandle::Result LabelTextHandle::Cancel( AudacityProject *pProject )
{
   // Restore the selection states of tracks
   // Note that we are also relying on LabelDefaultClickHandle::Cancel
   // to restore the selection state of the labels in the tracks.
   auto &viewInfo = ViewInfo::Get( *pProject );
   viewInfo.selectedRegion = mSelectedRegion;
   auto result = LabelDefaultClickHandle::Cancel( pProject );
   return result | RefreshCode::RefreshAll;
}

M src/tracks/labeltrack/ui/LabelTextHandle.h => src/tracks/labeltrack/ui/LabelTextHandle.h +1 -5
@@ 58,9 58,7 @@ public:

private:
   void HandleTextClick
      (AudacityProject &project,
       const wxMouseEvent & evt, const wxRect & r, const ZoomInfo &zoomInfo,
       NotifyingSelectedRegion &newSel);
      (AudacityProject &project, const wxMouseEvent & evt);
   void HandleTextDragRelease(
      AudacityProject &project, const wxMouseEvent & evt);



@@ 68,8 66,6 @@ private:
   int mLabelNum{ -1 };
   int mLabelTrackStartXPos { -1 };
   int mLabelTrackStartYPos { -1 };
   SelectedRegion mSelectedRegion{};
   std::shared_ptr<SelectionStateChanger> mChanger;

   /// flag to tell if it's a valid dragging
   bool mRightDragging{ false };

M src/tracks/labeltrack/ui/LabelTrackShifter.cpp => src/tracks/labeltrack/ui/LabelTrackShifter.cpp +1 -1
@@ 60,7 60,7 @@ public:
         iLabel =
            LabelTrackView::OverATextBox(*mpTrack, pParams->xx, pParams->yy);
      if (iLabel == -1)
         iLabel = LabelTrackView::Get(*mpTrack).GetSelectedIndex(mProject);
         iLabel = LabelTrackView::Get(*mpTrack).GetNavigationIndex(mProject);
      if (iLabel != -1) {
         UnfixIntervals([&](const auto &myInterval){
            return GetIndex( myInterval ) == iLabel;

M src/tracks/labeltrack/ui/LabelTrackView.cpp => src/tracks/labeltrack/ui/LabelTrackView.cpp +285 -244
@@ 147,10 147,10 @@ void LabelTrackView::CopyTo( Track &track ) const
   auto &other = TrackView::Get( track );

   if ( const auto pOther = dynamic_cast< const LabelTrackView* >( &other ) ) {
      pOther->mSelIndex = mSelIndex;
      pOther->mNavigationIndex = mNavigationIndex;
      pOther->mInitialCursorPos = mInitialCursorPos;
      pOther->mCurrentCursorPos = mCurrentCursorPos;
      pOther->mDrawCursor = mDrawCursor;
      pOther->mTextEditIndex = mTextEditIndex;
      pOther->mUndoLabel = mUndoLabel;
   }
}


@@ 218,15 218,24 @@ void LabelTrackView::ResetFlags()
{
   mInitialCursorPos = 1;
   mCurrentCursorPos = 1;
   mDrawCursor = false;
   mTextEditIndex = -1;
   mNavigationIndex = -1;
}

LabelTrackView::Flags LabelTrackView::SaveFlags() const
{
   return {
      mInitialCursorPos, mCurrentCursorPos, mNavigationIndex,
      mTextEditIndex, mUndoLabel
   };
}

void LabelTrackView::RestoreFlags( const Flags& flags )
{
   mInitialCursorPos = flags.mInitialCursorPos;
   mCurrentCursorPos = flags.mCurrentCursorPos;
   mSelIndex = flags.mSelIndex;
   mDrawCursor = flags.mDrawCursor;
   mNavigationIndex = flags.mNavigationIndex;
   mTextEditIndex = flags.mTextEditIndex;
}

wxFont LabelTrackView::GetFont(const wxString &faceName, int size)


@@ 236,8 245,13 @@ wxFont LabelTrackView::GetFont(const wxString &faceName, int size)
      encoding = wxFONTENCODING_DEFAULT;
   else
      encoding = wxFONTENCODING_SYSTEM;
   return wxFont(size, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL,
                 wxFONTWEIGHT_NORMAL, false, faceName, encoding);
   
   auto fontInfo = size == 0 ? wxFontInfo() : wxFontInfo(size);
   fontInfo
      .Encoding(encoding)
      .FaceName(faceName);

   return wxFont(fontInfo);
}

void LabelTrackView::ResetFont()


@@ 576,6 590,11 @@ void LabelTrackView::DrawGlyphs(
      dc.DrawBitmap(GetGlyph(GlyphRight), x1-xHalfWidth,yStart, true);
}

int LabelTrackView::GetTextFrameHeight()
{
    return mTextHeight + TextFramePadding * 2;
}

/// 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


@@ 584,6 603,7 @@ void LabelTrackView::DrawGlyphs(
///   @param  r  the LabelTrack rectangle.
void LabelTrackView::DrawText(wxDC & dc, const LabelStruct &ls, const wxRect & r)
{
   const int yFrameHeight = mTextHeight + TextFramePadding * 2;
   //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.


@@ 603,7 623,9 @@ void LabelTrackView::DrawText(wxDC & dc, const LabelStruct &ls, const wxRect & r
      if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
      {
         // Now draw the text itself.
         dc.DrawText(ls.title, xText, y-mTextHeight/2);
         auto pos = y - LabelBarHeight - yFrameHeight + TextFrameYOffset +
            (yFrameHeight - mFontHeight) / 2 + dc.GetFontMetrics().ascent;
         dc.DrawText(ls.title, xText, pos);
      }
   }



@@ 612,32 634,6 @@ void LabelTrackView::DrawText(wxDC & dc, const LabelStruct &ls, const wxRect & r
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


@@ 647,19 643,42 @@ void LabelTrackView::DrawTextBox(
   // 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;
    const auto textFrameHeight = GetTextFrameHeight();
    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);
      }
    if ((xStart < (r.x + r.width)) && (xEnd > r.x) && (xWidth > 0))
    {
       wxRect frame(
          xStart, ls.y - (textFrameHeight + LabelBarHeight) / 2 + TextFrameYOffset,
          xWidth, textFrameHeight);
       dc.DrawRectangle(frame);
    }
}

void LabelTrackView::DrawBar(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 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 - (LabelBarHeight - GetTextFrameHeight()) / 2,
         xWidth, LabelBarHeight);
      if (x1 > x + xBarShorten)
         dc.DrawRectangle(bar);
   }
}



@@ 667,15 686,16 @@ void LabelTrackView::DrawTextBox(
void LabelTrackView::DrawHighlight( wxDC & dc, const LabelStruct &ls,
   int xPos1, int xPos2, int charHeight)
{
   wxPen curPen = dc.GetPen();
   curPen.SetColour(wxString(wxT("BLUE")));
   const int yFrameHeight = mTextHeight + TextFramePadding * 2;
   
   dc.SetPen(*wxTRANSPARENT_PEN);
   wxBrush curBrush = dc.GetBrush();
   curBrush.SetColour(wxString(wxT("BLUE")));
   auto &y = ls.y;
   auto top = ls.y + TextFrameYOffset - (LabelBarHeight + yFrameHeight) / 2 + (yFrameHeight - charHeight) / 2;
   if (xPos1 < xPos2)
      dc.DrawRectangle(xPos1-1, y-charHeight/2, xPos2-xPos1+1, charHeight);
      dc.DrawRectangle(xPos1-1, top, xPos2-xPos1+1, charHeight);
   else
      dc.DrawRectangle(xPos2-1, y-charHeight/2, xPos1-xPos2+1, charHeight);
      dc.DrawRectangle(xPos2-1, top, xPos1-xPos2+1, charHeight);
}

namespace {


@@ 694,7 714,7 @@ void getXPos( const LabelStruct &ls, wxDC & dc, int * xPos1, int cursorPos)

bool LabelTrackView::CalcCursorX( AudacityProject &project, int * x) const
{
   if ( HasSelection( project ) ) {
   if (IsValidIndex(mTextEditIndex, project)) {
      wxMemoryDC dc;

      if (msFont.Ok()) {


@@ 704,7 724,7 @@ bool LabelTrackView::CalcCursorX( AudacityProject &project, int * x) const
      const auto pTrack = FindLabelTrack();
      const auto &mLabels = pTrack->GetLabels();

      getXPos(mLabels[mSelIndex], dc, x, mCurrentCursorPos);
      getXPos(mLabels[mTextEditIndex], dc, x, mCurrentCursorPos);
      *x += mIconWidth / 2;
      return true;
   }


@@ 726,7 746,7 @@ void LabelTrackView::CalcHighlightXs(int *x1, int *x2) const

   const auto pTrack = FindLabelTrack();
   const auto &mLabels = pTrack->GetLabels();
   const auto &labelStruct = mLabels[mSelIndex];
   const auto &labelStruct = mLabels[mTextEditIndex];

   // find the left X pos of highlighted area
   getXPos(labelStruct, dc, x1, pos1);


@@ 797,8 817,9 @@ void LabelTrackView::Draw
   // 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;
   mTextHeight = dc.GetFontMetrics().ascent + dc.GetFontMetrics().descent;
   const int yFrameHeight = mTextHeight + TextFramePadding * 2;

   ComputeLayout( r, zoomInfo );
   dc.SetTextForeground(theTheme.Colour( clrLabelTrackText));
   dc.SetBackgroundMode(wxTRANSPARENT);


@@ 838,41 859,47 @@ void LabelTrackView::Draw
#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
         highlight = highlightTrack && target->GetLabelNum() == i;
#endif
         bool selected = GetSelectedIndex( project ) == i;
         
         dc.SetBrush(mNavigationIndex == i || (pHit && pHit->mMouseOverLabel == i) 
            ? AColor::labelTextEditBrush : AColor::labelTextNormalBrush);
         DrawBar(dc, labelStruct, r);

         if( selected )
            dc.SetBrush( AColor::labelTextEditBrush );
         else if ( highlight )
            dc.SetBrush( AColor::uglyBrush );
         DrawTextBox( dc, labelStruct, r );
         bool selected = mTextEditIndex == i;

         if (highlight || selected)
         if (selected)
            dc.SetBrush(AColor::labelTextEditBrush);
         else if (highlight)
            dc.SetBrush(AColor::uglyBrush);
         else
            dc.SetBrush(AColor::labelTextNormalBrush);
         DrawTextBox(dc, labelStruct, r);

         dc.SetBrush(AColor::labelTextNormalBrush);
      }
   }

   // Draw highlights
   if ( (mInitialCursorPos != mCurrentCursorPos) && HasSelection( project ) )
   if ( (mInitialCursorPos != mCurrentCursorPos) && IsValidIndex(mTextEditIndex, project))
   {
      int xpos1, xpos2;
      CalcHighlightXs(&xpos1, &xpos2);
      DrawHighlight(dc, mLabels[mSelIndex],
         xpos1, xpos2, mFontHeight);
      DrawHighlight(dc, mLabels[mTextEditIndex],
         xpos1, xpos2, dc.GetFontMetrics().ascent + dc.GetFontMetrics().descent);
   }

   // Draw the text and the label boxes.
   { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
      if( GetSelectedIndex( project ) == i )
      if(mTextEditIndex == i )
         dc.SetBrush(AColor::labelTextEditBrush);
      DrawText( dc, labelStruct, r );
      if( GetSelectedIndex( project ) == i )
      if(mTextEditIndex == i )
         dc.SetBrush(AColor::labelTextNormalBrush);
   }}

   // Draw the cursor, if there is one.
   if( mDrawCursor && HasSelection( project ) )
   if(mInitialCursorPos == mCurrentCursorPos && IsValidIndex(mTextEditIndex, project))
   {
      const auto &labelStruct = mLabels[mSelIndex];
      const auto &labelStruct = mLabels[mTextEditIndex];
      int xPos = labelStruct.xText;

      if( mCurrentCursorPos > 0)


@@ 886,9 913,10 @@ void LabelTrackView::Draw
      wxPen currentPen = dc.GetPen();
      const int CursorWidth=2;
      currentPen.SetWidth(CursorWidth);
      const auto top = labelStruct.y - (LabelBarHeight + yFrameHeight) / 2 + (yFrameHeight - mFontHeight) / 2 + TextFrameYOffset;
      AColor::Line(dc,
                   xPos-1, labelStruct.y - mFontHeight/2 + 1,
                   xPos-1, labelStruct.y + mFontHeight/2 - 1);
                   xPos-1, top,
                   xPos-1, top + mFontHeight);
      currentPen.SetWidth(1);
   }
}


@@ 902,17 930,9 @@ void LabelTrackView::Draw(
   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 LabelTrackView::FindCursorPosition(int labelIndex, wxCoord xPos)
{
   int result = -1;
   wxMemoryDC dc;


@@ 929,7 949,7 @@ int LabelTrackView::FindCursorPosition(wxCoord xPos)

   const auto pTrack = FindLabelTrack();
   const auto &mLabels = pTrack->GetLabels();
   const auto &labelStruct = mLabels[mSelIndex];
   const auto &labelStruct = mLabels[labelIndex];
   const auto &title = labelStruct.title;
   const int length = title.length();
   while (!finished && (charIndex < length + 1))


@@ 970,13 990,33 @@ void LabelTrackView::SetCurrentCursorPosition(int pos)
{
   mCurrentCursorPos = pos;
}

void LabelTrackView::SetTextHighlight(
   int initialPosition, int currentPosition )
void LabelTrackView::SetTextSelection(int labelIndex, int start, int end)
{
    mTextEditIndex = labelIndex;
    mInitialCursorPos = start;
    mCurrentCursorPos = end;
}
int LabelTrackView::GetTextEditIndex(AudacityProject& project) const
{
    if (IsValidIndex(mTextEditIndex, project))
        return mTextEditIndex;
    return -1;
}
void LabelTrackView::ResetTextSelection()
{
    mTextEditIndex = -1;
    mCurrentCursorPos = 1;
    mInitialCursorPos = 1;
}
void LabelTrackView::SetNavigationIndex(int index)
{
   mInitialCursorPos = initialPosition;
   mCurrentCursorPos = currentPosition;
   mDrawCursor = true;
    mNavigationIndex = index;
}
int LabelTrackView::GetNavigationIndex(AudacityProject& project) const
{
    if (IsValidIndex(mNavigationIndex, project))
        return mNavigationIndex;
    return -1;
}

void LabelTrackView::calculateFontHeight(wxDC & dc)


@@ 997,13 1037,20 @@ void LabelTrackView::calculateFontHeight(wxDC & dc)
   mFontHeight += CursorExtraHeight - (charLeading+charDescent);
}

bool LabelTrackView::IsValidIndex(const Index& index, AudacityProject& project) const
{
    if (index == -1)
       return false;
    // may make delayed update of mutable mSelIndex after track selection change
    auto track = FindLabelTrack();
    if (track->GetSelected() || (TrackFocus::Get(project).Get() == track.get()))
       return index >= 0 && index < static_cast<int>(track->GetLabels().size());
    return false;
}

bool LabelTrackView::IsTextSelected( AudacityProject &project ) const
{
   if ( !HasSelection( project ) )
      return false;
   if (mCurrentCursorPos == mInitialCursorPos)
      return false;
   return true;
   return mCurrentCursorPos != mInitialCursorPos && IsValidIndex(mTextEditIndex, project);
}

/// Cut the selected text in the text box


@@ 1017,10 1064,10 @@ bool LabelTrackView::CutSelectedText( AudacityProject &project )
   const auto &mLabels = pTrack->GetLabels();

   wxString left, right;
   auto labelStruct = mLabels[mSelIndex];
   auto labelStruct = mLabels[mTextEditIndex];
   auto &text = labelStruct.title;

   if (!mSelIndex.IsModified()) {
   if (!mTextEditIndex.IsModified()) {
      mUndoLabel = text;
   }



@@ 1043,7 1090,7 @@ bool LabelTrackView::CutSelectedText( AudacityProject &project )
   // set title to the combination of the two remainders
   text = left + right;

   pTrack->SetLabel( mSelIndex, labelStruct );
   pTrack->SetLabel( mTextEditIndex, labelStruct );

   // copy data onto clipboard
   if (wxTheClipboard->Open()) {


@@ 1055,7 1102,7 @@ bool LabelTrackView::CutSelectedText( AudacityProject &project )
   // set cursor positions
   mInitialCursorPos = mCurrentCursorPos = left.length();

   mSelIndex.SetModified(true);
   mTextEditIndex.SetModified(true);
   return true;
}



@@ 1063,13 1110,13 @@ bool LabelTrackView::CutSelectedText( AudacityProject &project )
///  @return true if text is selected in text box, false otherwise
bool LabelTrackView::CopySelectedText( AudacityProject &project )
{
   if ( !HasSelection( project ) )
   if (!IsTextSelected(project))
      return false;

   const auto pTrack = FindLabelTrack();
   const auto &mLabels = pTrack->GetLabels();

   const auto &labelStruct = mLabels[mSelIndex];
   const auto &labelStruct = mLabels[mTextEditIndex];

   int init = mInitialCursorPos;
   int cur = mCurrentCursorPos;


@@ 1100,8 1147,8 @@ bool LabelTrackView::PasteSelectedText(
{
   const auto pTrack = FindLabelTrack();

   if ( !HasSelection( project ) )
      AddLabel(SelectedRegion(sel0, sel1));
   if (!IsValidIndex(mTextEditIndex, project))
      SetTextSelection(AddLabel(SelectedRegion(sel0, sel1)));

   wxString text, left, right;



@@ 1114,7 1161,7 @@ bool LabelTrackView::PasteSelectedText(
         text = data.GetText();
      }

      if (!mSelIndex.IsModified()) {
      if (!mTextEditIndex.IsModified()) {
         mUndoLabel = text;
      }



@@ 1127,7 1174,7 @@ bool LabelTrackView::PasteSelectedText(
   }

   const auto &mLabels = pTrack->GetLabels();
   auto labelStruct = mLabels[mSelIndex];
   auto labelStruct = mLabels[mTextEditIndex];
   auto &title = labelStruct.title;
   int cur = mCurrentCursorPos, init = mInitialCursorPos;
   if (init > cur)


@@ 1138,11 1185,11 @@ bool LabelTrackView::PasteSelectedText(

   title = left + text + right;

   pTrack->SetLabel( mSelIndex, labelStruct );
   pTrack->SetLabel(mTextEditIndex, labelStruct );

   mInitialCursorPos =  mCurrentCursorPos = left.length() + text.length();

   mSelIndex.SetModified(true);
   mTextEditIndex.SetModified(true);
   return true;
}



@@ 1152,20 1199,6 @@ bool LabelTrackView::IsTextClipSupported()
   return wxTheClipboard->IsSupported(wxDF_UNICODETEXT);
}


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( 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.


@@ 1180,11 1213,23 @@ void LabelTrackView::OverGlyph(
   //If not over a label, reset it
   hit.mMouseOverLabelLeft  = -1;
   hit.mMouseOverLabelRight = -1;
   hit.mMouseOverLabel = -1;
   hit.mEdge = 0;

   const auto pTrack = &track;
   const auto &mLabels = pTrack->GetLabels();
   { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
      // give text box better priority for selecting
      // reset selection state
      if (OverTextBox(&labelStruct, x, y))
      {
         result = 0;
         hit.mMouseOverLabel = -1;
         hit.mMouseOverLabelLeft = -1;
         hit.mMouseOverLabelRight = -1;
         break;
      }
   
      //over left or right selection bound
      //Check right bound first, since it is drawn after left bound,
      //so give it precedence for matching/highlighting.


@@ 1215,13 1260,12 @@ void LabelTrackView::OverGlyph(
            result |= 4;
         result |= 1;
      }

      // give text box better priority for selecting
      if(OverTextBox(&labelStruct, x, y))
      else if (x >= labelStruct.x && x <= labelStruct.x1 &&
         abs(y - (labelStruct.y + mTextHeight / 2)) < d1)
      {
         result = 0;
         hit.mMouseOverLabel = i;
         result = 3;
      }

   }}
   hit.mEdge = result;
}


@@ 1304,7 1348,7 @@ bool LabelTrackView::DoCaptureKey(
       !mLabels.empty())
      return true;

   if ( HasSelection( project ) ) {
   if (IsValidIndex(mTextEditIndex, project) || IsValidIndex(mNavigationIndex, project)) {
      if (IsGoodLabelEditKey(event)) {
         return true;
      }


@@ 1363,10 1407,10 @@ unsigned LabelTrackView::KeyDown(
   double bkpSel0 = viewInfo.selectedRegion.t0(),
      bkpSel1 = viewInfo.selectedRegion.t1();

   if (!mSelIndex.IsModified() && HasSelection( *project )) {
   if (IsValidIndex(mTextEditIndex, *project) && !mTextEditIndex.IsModified()) {
      const auto pTrack = FindLabelTrack();
      const auto &mLabels = pTrack->GetLabels();
      auto labelStruct = mLabels[mSelIndex];
      auto labelStruct = mLabels[mTextEditIndex];
      auto &title = labelStruct.title;
      mUndoLabel = title;
   }


@@ 1376,12 1420,12 @@ unsigned LabelTrackView::KeyDown(
   if (DoKeyDown( *project, viewInfo.selectedRegion, event )) {
      ProjectHistory::Get( *project ).PushState(XO("Modified Label"),
         XO("Label Edit"),
         mSelIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
         mTextEditIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);

      mSelIndex.SetModified(true);
      mTextEditIndex.SetModified(true);
   }

   if (!mSelIndex.IsModified()) {
   if (!mTextEditIndex.IsModified()) {
      mUndoLabel.clear();
   }



@@ 1409,10 1453,10 @@ unsigned LabelTrackView::Char(
   // Pass keystroke to labeltrack's handler and add to history if any
   // updates were done

   if (!mSelIndex.IsModified() && HasSelection( *project )) {
   if (IsValidIndex(mTextEditIndex, *project) && !mTextEditIndex.IsModified()) {
      const auto pTrack = FindLabelTrack();
      const auto &mLabels = pTrack->GetLabels();
      auto labelStruct = mLabels[mSelIndex];
      auto labelStruct = mLabels[mTextEditIndex];
      auto &title = labelStruct.title;
      mUndoLabel = title;
   }


@@ 1420,12 1464,12 @@ unsigned LabelTrackView::Char(
   if (DoChar( *project, viewInfo.selectedRegion, event )) {
      ProjectHistory::Get( *project ).PushState(XO("Modified Label"),
         XO("Label Edit"),
         mSelIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
          mTextEditIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);

      mSelIndex.SetModified(true);
      mTextEditIndex.SetModified(true);
   }

   if (!mSelIndex.IsModified()) {
   if (!mTextEditIndex.IsModified()) {
      mUndoLabel.clear();
   }



@@ 1460,8 1504,8 @@ bool LabelTrackView::DoKeyDown(
   // 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];
   if (IsValidIndex(mTextEditIndex, project)) {
      auto labelStruct = mLabels[mTextEditIndex];
      auto &title = labelStruct.title;
      wxUniChar wchar;
      bool more=true;


@@ 1486,7 1530,7 @@ bool LabelTrackView::DoKeyDown(
                     title.erase(mCurrentCursorPos-1, 1);
                     mCurrentCursorPos--;
                     if( ((int)wchar > 0xDFFF) || ((int)wchar <0xDC00)){
                        pTrack->SetLabel(mSelIndex, labelStruct);
                        pTrack->SetLabel(mTextEditIndex, labelStruct);
                        more = false;
                     }
                  }


@@ 1495,7 1539,8 @@ bool LabelTrackView::DoKeyDown(
            else
            {
               // ELSE no text in text box, so DELETE whole label.
               pTrack->DeleteLabel( mSelIndex );
               pTrack->DeleteLabel(mTextEditIndex);
               ResetTextSelection();
            }
            mInitialCursorPos = mCurrentCursorPos;
            updated = true;


@@ 1520,7 1565,7 @@ bool LabelTrackView::DoKeyDown(
                     wchar = title.at( mCurrentCursorPos );
                     title.erase(mCurrentCursorPos, 1);
                     if( ((int)wchar > 0xDBFF) || ((int)wchar <0xD800)){
                        pTrack->SetLabel(mSelIndex, labelStruct);
                        pTrack->SetLabel(mTextEditIndex, labelStruct);
                        more = false;
                     }
                  }


@@ 1529,7 1574,8 @@ bool LabelTrackView::DoKeyDown(
            else
            {
               // DELETE whole label if no text in text box
               pTrack->DeleteLabel( mSelIndex );
               pTrack->DeleteLabel(mTextEditIndex);
               ResetTextSelection();
            }
            mInitialCursorPos = mCurrentCursorPos;
            updated = true;


@@ 1589,17 1635,18 @@ bool LabelTrackView::DoKeyDown(
         break;

      case WXK_ESCAPE:
         if (mSelIndex.IsModified()) {
         if (mTextEditIndex.IsModified()) {
            title = mUndoLabel;
            pTrack->SetLabel(mSelIndex, labelStruct);
            pTrack->SetLabel(mTextEditIndex, labelStruct);

            ProjectHistory::Get( project ).PushState(XO("Modified Label"),
               XO("Label Edit"),
               mSelIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
               mTextEditIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
         }

      case WXK_RETURN:
      case WXK_NUMPAD_ENTER:
      case WXK_TAB:
         if (mRestoreFocus >= 0) {
            auto track = *TrackList::Get( project ).Any()
               .begin().advance(mRestoreFocus);


@@ 1607,27 1654,9 @@ bool LabelTrackView::DoKeyDown(
               TrackFocus::Get( project ).Set(track);
            mRestoreFocus = -2;
         }
         mSelIndex = -1;
         SetNavigationIndex(mTextEditIndex);
         ResetTextSelection();
         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:


@@ 1649,37 1678,61 @@ bool LabelTrackView::DoKeyDown(
      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 (IsValidIndex(mNavigationIndex, project))
            {
                if (event.ShiftDown()) {
                    --mNavigationIndex;
                }
                else {
                    ++mNavigationIndex;
                }
                mNavigationIndex = (mNavigationIndex + (int)mLabels.size()) % (int)mLabels.size();    // wrap round if necessary
            }
            else
            {
                // no valid navigation index, then
                if (event.ShiftDown()) {
                    //search for the first label starting from the end (and before selection)
                    mNavigationIndex = len - 1;
                    if (newSel.t0() > mLabels[0].getT0()) {
                        while (mNavigationIndex >= 0 &&
                            mLabels[mNavigationIndex].getT0() > newSel.t0()) {
                            --mNavigationIndex;
                        }
                    }
                }
                else {
                    //search for the first label starting from the beginning (and after selection)
                    mNavigationIndex = 0;
                    if (newSel.t0() < mLabels[len - 1].getT0()) {
                        while (mNavigationIndex < len &&
                            mLabels[mNavigationIndex].getT0() < newSel.t0()) {
                            ++mNavigationIndex;
                        }
                    }
                }
            }

            if (mSelIndex >= 0 && mSelIndex < len) {
               const auto &labelStruct = mLabels[mSelIndex];
            if (mNavigationIndex >= 0 && mNavigationIndex < len) {
               const auto &labelStruct = mLabels[mNavigationIndex];
               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;
               mNavigationIndex = -1;
            }
         }
         break;

      case WXK_RETURN:
      case WXK_NUMPAD_ENTER:
         //pressing Enter key activates editing of the label
         //pointed to by mNavigationIndex (if valid)
         if (IsValidIndex(mNavigationIndex, project)) {
             SetTextSelection(mNavigationIndex);
         }
         break;
      default:
         if (!IsGoodLabelFirstKey(event)) {
            event.Skip();


@@ 1688,9 1741,6 @@ bool LabelTrackView::DoKeyDown(
      }
   }

   // Make sure the caret is visible
   mDrawCursor = true;

   return updated;
}



@@ 1711,7 1761,7 @@ bool LabelTrackView::DoChar(
   }

   // Only track true changes to the label
   bool updated = false;
   //bool updated = false;

   // Cache the character
   wxChar charCode = event.GetUnicodeKey();


@@ 1724,7 1774,7 @@ bool LabelTrackView::DoChar(
   
   // If we've reached this point and aren't currently editing, add NEW label
   const auto pTrack = FindLabelTrack();
   if ( !HasSelection( project ) ) {
   if (!IsValidIndex(mTextEditIndex, project)) {
      // Don't create a NEW label for a space
      if (wxIsspace(charCode)) {
         event.Skip();


@@ 1754,6 1804,9 @@ bool LabelTrackView::DoChar(
      }
   }

   if (!IsValidIndex(mTextEditIndex, project))
      return false;

   //
   // Now we are definitely in a label; append the incoming character
   //


@@ 1763,7 1816,7 @@ bool LabelTrackView::DoChar(
      RemoveSelectedText();

   const auto& mLabels = pTrack->GetLabels();
   auto labelStruct = mLabels[mSelIndex];
   auto labelStruct = mLabels[mTextEditIndex];
   auto& title = labelStruct.title;

   if (mCurrentCursorPos < (int)title.length()) {


@@ 1780,16 1833,12 @@ bool LabelTrackView::DoChar(
      //append charCode
      title += charCode;

   pTrack->SetLabel( mSelIndex, labelStruct );
   pTrack->SetLabel(mTextEditIndex, labelStruct );

   //moving cursor position forward
   mInitialCursorPos = ++mCurrentCursorPos;
   updated = true;

   // Make sure the caret is visible
   mDrawCursor = true;

   return updated;
   
   return true;
}

enum


@@ 1829,13 1878,12 @@ void LabelTrackView::ShowContextMenu( AudacityProject &project )
      menu.Enable(OnDeleteSelectedLabelID, true);
      menu.Enable(OnEditSelectedLabelID, true);

      if( !HasSelection( project ) ) {
         wxASSERT( false );
      if(!IsValidIndex(mTextEditIndex, project)) {
         return;
      }

      const auto pTrack = FindLabelTrack();
      const LabelStruct *ls = pTrack->GetLabel(mSelIndex);
      const LabelStruct *ls = pTrack->GetLabel(mTextEditIndex);

      wxClientDC dc(parent);



@@ 1875,7 1923,7 @@ void LabelTrackView::OnContextMenu(
      {
         ProjectHistory::Get( project ).PushState(XO("Modified Label"),
                      XO("Label Edit"),
                      mSelIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
                      mTextEditIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
      }
      break;



@@ 1891,17 1939,16 @@ void LabelTrackView::OnContextMenu(
      {
         ProjectHistory::Get( project ).PushState(XO("Modified Label"),
                      XO("Label Edit"),
                      mSelIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
                      mTextEditIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
      }
      break;

   /// DELETE selected label
   case OnDeleteSelectedLabelID: {
      int ndx = GetLabelIndex(selectedRegion.t0(), selectedRegion.t1());
      if (ndx != -1)
      if (IsValidIndex(mTextEditIndex, project))
      {
         const auto pTrack = FindLabelTrack();
         pTrack->DeleteLabel(ndx);
         pTrack->DeleteLabel(mTextEditIndex);
         ProjectHistory::Get( project ).PushState(XO("Deleted Label"),
                      XO("Label Edit"),
                      UndoPush::CONSOLIDATE);


@@ 1911,7 1958,8 @@ void LabelTrackView::OnContextMenu(

   case OnEditSelectedLabelID: {
      // Bug #2571: See above
      mEditIndex = GetLabelIndex(selectedRegion.t0(), selectedRegion.t1());
      if (IsValidIndex(mTextEditIndex, project))
         mEditIndex = mTextEditIndex;
   }
      break;
   }


@@ 1928,7 1976,7 @@ void LabelTrackView::RemoveSelectedText()

   const auto pTrack = FindLabelTrack();
   const auto &mLabels = pTrack->GetLabels();
   auto labelStruct = mLabels[mSelIndex];
   auto labelStruct = mLabels[mTextEditIndex];
   auto &title = labelStruct.title;

   if (init > 0)


@@ 1938,16 1986,16 @@ void LabelTrackView::RemoveSelectedText()
      right = title.Mid(cur);

   title = left + right;
   pTrack->SetLabel( mSelIndex, labelStruct );
   pTrack->SetLabel( mTextEditIndex, labelStruct );
   mInitialCursorPos = mCurrentCursorPos = left.length();
}

bool LabelTrackView::HasSelection( AudacityProject &project ) const
/*
bool LabelTrackView::HasSelectedLabel( AudacityProject &project ) const
{
   const auto selIndex = GetSelectedIndex( project );
   const auto selIndex = GetSelectionIndex( project );
   return (selIndex >= 0 &&
      selIndex < (int)FindLabelTrack()->GetLabels().size());
}
}*/

int LabelTrackView::GetLabelIndex(double t, double t1)
{


@@ 1999,21 2047,10 @@ void LabelTrackView::OnLabelAdded( LabelTrackEvent &e )
   // -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;
       mTextEditIndex = 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 )


@@ 2026,17 2063,13 @@ void LabelTrackView::OnLabelDeleted( LabelTrackEvent &e )

   // IF we've deleted the selected label
   // THEN set no label selected.
   if( mSelIndex== index )
   {
      mSelIndex = -1;
      mCurrentCursorPos = 1;
   }
   if (mTextEditIndex == index)
      ResetTextSelection();
   
   // IF we removed a label before the selected label
   // THEN the NEW selected label number is one less.
   else if( index < mSelIndex )
   {
      --mSelIndex;
   }
   else if( index < mTextEditIndex)
      --mTextEditIndex;//NB: Keep cursor selection region
}

void LabelTrackView::OnLabelPermuted( LabelTrackEvent &e )


@@ 2048,12 2081,16 @@ void LabelTrackView::OnLabelPermuted( LabelTrackEvent &e )
   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;
   auto fix = [&](Index& index) {
       if (index == former)
           index = present;
       else if (former < index && index <= present)
           --index;
       else if (former > index && index >= present)
           ++index;
   };
   fix(mNavigationIndex);
   fix(mTextEditIndex);
}

void LabelTrackView::OnSelectionChange( LabelTrackEvent &e )


@@ 2062,8 2099,11 @@ void LabelTrackView::OnSelectionChange( LabelTrackEvent &e )
   if ( e.mpTrack.lock() != FindTrack() )
      return;

   if ( !FindTrack()->GetSelected() )
      mSelIndex = -1;
   if (!FindTrack()->GetSelected())
   {
       SetNavigationIndex(-1);
       ResetTextSelection();
   }
}

wxBitmap & LabelTrackView::GetGlyph( int i)


@@ 2213,9 2253,10 @@ int LabelTrackView::DialogForLabelName(
      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.x +=
      + std::max(0, static_cast<int>(viewInfo.TimeToPosition(
         viewInfo.GetLeftOffset(), region.t0())))
      - 39;
   position.y += 2;  // just below the bottom of the track
   position = trackPanel.ClientToScreen(position);
   auto &window = GetProjectFrame( project );

M src/tracks/labeltrack/ui/LabelTrackView.h => src/tracks/labeltrack/ui/LabelTrackView.h +27 -21
@@ 44,8 44,11 @@ class TENACITY_DLL_API LabelTrackView final : public CommonTrackView
   void Reparent( const std::shared_ptr<Track> &parent ) override;

public:
   enum : int { DefaultFontSize = 12 };
   
   enum : int { DefaultFontSize = 0 }; //system preferred
   static constexpr int TextFramePadding { 2 };
   static constexpr int TextFrameYOffset { -1 };
   static constexpr int LabelBarHeight { 6 }; 

   explicit
   LabelTrackView( const std::shared_ptr<Track> &pTrack );
   ~LabelTrackView() override;


@@ 109,9 112,6 @@ public:

   void Draw( TrackPanelDrawingContext &context, const wxRect & r ) const;

   int GetSelectedIndex( AudacityProject &project ) const;
   void SetSelectedIndex( int index );

   bool CutSelectedText( AudacityProject &project );
   bool CopySelectedText( AudacityProject &project );
   bool PasteSelectedText(


@@ 143,19 143,13 @@ private:
public:
   struct Flags {
      int mInitialCursorPos, mCurrentCursorPos;
      Index mSelIndex{-1};
      bool mDrawCursor;
      Index mNavigationIndex;
      Index mTextEditIndex;
      wxString mUndoLabel;
   };

   void ResetFlags();
   Flags SaveFlags() const
   {
      return {
         mInitialCursorPos, mCurrentCursorPos, mSelIndex,
         mDrawCursor, mUndoLabel
      };
   }
   Flags SaveFlags() const;
   void RestoreFlags( const Flags& flags );

   static int OverATextBox( const LabelTrack &track, int xx, int yy );


@@ 187,7 181,11 @@ public:
private:
   void OnContextMenu( AudacityProject &project, wxCommandEvent & evt);

   mutable Index mSelIndex{-1};  /// Keeps track of the currently selected label
   /// Keeps track of the currently selected label (not same as selection region)
   /// used for navigation between labels
   mutable Index mNavigationIndex{ -1 };
   /// Index of the current label text beeing edited
   mutable Index mTextEditIndex{ -1 };

   mutable wxString mUndoLabel;



@@ 202,8 200,7 @@ private:
   mutable int mCurrentCursorPos;                  /// current cursor position
   mutable int mInitialCursorPos;                  /// initial cursor position

   mutable bool mDrawCursor;                       /// flag to tell if drawing the
                                                   /// cursor or not
   
   int mRestoreFocus{-2};                          /// Restore focus to this track
                                                   /// when done editing



@@ 212,18 209,28 @@ private:
   static void DrawLines( wxDC & dc, const LabelStruct &ls, const wxRect & r);
   static void DrawGlyphs( wxDC & dc, const LabelStruct &ls, const wxRect & r,
      int GlyphLeft, int GlyphRight);
   static int GetTextFrameHeight();
   static void DrawText( wxDC & dc, const LabelStruct &ls, const wxRect & r);
   static void DrawTextBox( wxDC & dc, const LabelStruct &ls, const wxRect & r);
   static void DrawBar(wxDC& dc, const LabelStruct& ls, const wxRect& r);
   static void DrawHighlight(
      wxDC & dc, const LabelStruct &ls, int xPos1, int xPos2, int charHeight);

public:
   /// convert pixel coordinate to character position in text box
   int FindCursorPosition(wxCoord xPos);
   int FindCursorPosition(int labelIndex, wxCoord xPos);
   int GetCurrentCursorPosition() const { return mCurrentCursorPos; }
   void SetCurrentCursorPosition(int pos);
   int GetInitialCursorPosition() const { return mInitialCursorPos; }
   void SetTextHighlight( int initialPosition, int currentPosition );

   /// Sets the label with specified index for editing,
   /// optionaly selection may be specified with [start, end]
   void SetTextSelection(int labelIndex, int start = 1, int end = 1);
   int GetTextEditIndex(AudacityProject& project) const;
   void ResetTextSelection();

   void SetNavigationIndex(int index);
   int GetNavigationIndex(AudacityProject& project) const;

private:



@@ 234,8 241,7 @@ private:

   static void calculateFontHeight(wxDC & dc);

public:
   bool HasSelection( AudacityProject &project ) const;
   bool IsValidIndex(const Index& index, AudacityProject& project) const;

private:
   void RemoveSelectedText();

M src/tracks/playabletrack/notetrack/ui/NoteTrackView.cpp => src/tracks/playabletrack/notetrack/ui/NoteTrackView.cpp +6 -2
@@ 76,9 76,13 @@ std::shared_ptr<TrackVRulerControls> NoteTrackView::DoGetVRulerControls()
#define TIME_TO_X(t) (zoomInfo.TimeToPosition((t), rect.x))
#define X_TO_TIME(xx) (zoomInfo.PositionToTime((xx), rect.x))

std::shared_ptr<CommonTrackCell> NoteTrackView::DoGetAffordanceControls()
std::shared_ptr<CommonTrackCell> NoteTrackView::GetAffordanceControls()
{
   return std::make_shared<NoteTrackAffordanceControls>(DoFindTrack());
   if (mpAffordanceCellControl == nullptr)
   {
      mpAffordanceCellControl = std::make_shared<NoteTrackAffordanceControls>(DoFindTrack());
   }
   return mpAffordanceCellControl;
}

namespace {

M src/tracks/playabletrack/notetrack/ui/NoteTrackView.h => src/tracks/playabletrack/notetrack/ui/NoteTrackView.h +3 -1
@@ 25,7 25,7 @@ public:

private:
   std::shared_ptr<TrackVRulerControls> DoGetVRulerControls() override;
   std::shared_ptr<CommonTrackCell> DoGetAffordanceControls() override;
   std::shared_ptr<CommonTrackCell> GetAffordanceControls() override;

   std::vector<UIHandlePtr> DetailedHitTest
      (const TrackPanelMouseState &state,


@@ 36,5 36,7 @@ private:
   void Draw(
      TrackPanelDrawingContext &context,
      const wxRect &rect, unsigned iPass ) override;

   std::shared_ptr<CommonTrackCell> mpAffordanceCellControl;
};
#endif

M src/tracks/playabletrack/wavetrack/ui/WaveTrackAffordanceControls.cpp => src/tracks/playabletrack/wavetrack/ui/WaveTrackAffordanceControls.cpp +21 -5
@@ 16,6 16,7 @@
#include "../../../../TrackPanelMouseEvent.h"
#include "../../../../TrackArtist.h"
#include "../../../../TrackPanelDrawingContext.h"
#include "../../../../TrackPanelResizeHandle.h"
#include "../../../../ViewInfo.h"
#include "../../../../WaveTrack.h"
#include "../../../../WaveClip.h"


@@ 34,14 35,29 @@ std::vector<UIHandlePtr> WaveTrackAffordanceControls::HitTest(const TrackPanelMo

    std::vector<UIHandlePtr> results;

    const auto track = FindTrack();

    const auto waveTrack = std::static_pointer_cast<WaveTrack>(track->SubstitutePendingChangedTrack());
    auto px = state.state.m_x;
    auto py = state.state.m_y;

    const auto rect = state.rect;

    auto px = state.state.m_x;
    auto py = state.state.m_y;
    const auto track = FindTrack();

    auto trackList = track->GetOwner();
    if ((std::abs(rect.GetTop() - py) <= WaveTrackView::kChannelSeparatorThickness / 2) 
        && trackList
        && !track->IsLeader())
    {
        //given that track is not a leader there always should be
        //another track before this one
        auto prev = --trackList->Find(track.get());
        auto result = std::static_pointer_cast<UIHandle>(
            std::make_shared<TrackPanelResizeHandle>((*prev)->shared_from_this(), py)
        );
        result = AssignUIHandlePtr(mResizeHandle, result);
        results.push_back(result);
    }

    const auto waveTrack = std::static_pointer_cast<WaveTrack>(track->SubstitutePendingChangedTrack());

    auto& zoomInfo = ViewInfo::Get(*pProject);
    for (const auto& clip : waveTrack->GetClips())

M src/tracks/playabletrack/wavetrack/ui/WaveTrackAffordanceControls.h => src/tracks/playabletrack/wavetrack/ui/WaveTrackAffordanceControls.h +1 -0
@@ 19,6 19,7 @@ class TENACITY_DLL_API WaveTrackAffordanceControls : public CommonTrackCell
{
    std::weak_ptr<WaveClip> mFocusClip;
    std::weak_ptr<AffordanceHandle> mAffordanceHandle;
    std::weak_ptr<UIHandle> mResizeHandle;
public:
    WaveTrackAffordanceControls(const std::shared_ptr<Track>& pTrack);


M src/tracks/playabletrack/wavetrack/ui/WaveTrackControls.cpp => src/tracks/playabletrack/wavetrack/ui/WaveTrackControls.cpp +9 -9
@@ 828,7 828,7 @@ void WaveTrackMenuTable::OnMergeStereo(wxCommandEvent &)
      ((TrackView::Get( *pTrack ).GetMinimized()) &&
       (TrackView::Get( *partner ).GetMinimized()));

   tracks.GroupChannels( *pTrack, 2 );
   tracks.MakeMultiChannelTrack( *pTrack, 2, false );

   // Set partner's parameters to match target.
   partner->Merge(*pTrack);


@@ 843,8 843,8 @@ void WaveTrackMenuTable::OnMergeStereo(wxCommandEvent &)
   view.SetMinimized(false);
   partnerView.SetMinimized(false);
   int AverageHeight = (view.GetHeight() + partnerView.GetHeight()) / 2;
   view.SetHeight(AverageHeight);
   partnerView.SetHeight(AverageHeight);
   view.SetExpandedHeight(AverageHeight);
   partnerView.SetExpandedHeight(AverageHeight);
   view.SetMinimized(bBothMinimizedp);
   partnerView.SetMinimized(bBothMinimizedp);



@@ 879,17 879,17 @@ void WaveTrackMenuTable::SplitStereo(bool stereo)

      //make sure no channel is smaller than its minimum height
      if (view.GetHeight() < view.GetMinimizedHeight())
         view.SetHeight(view.GetMinimizedHeight());
         view.SetExpandedHeight(view.GetMinimizedHeight());
      totalHeight += view.GetHeight();
      ++nChannels;
   }

   TrackList::Get( *project ).GroupChannels( *pTrack, 1 );
   TrackList::Get( *project ).UnlinkChannels( *pTrack );
   int averageHeight = totalHeight / nChannels;

   for (auto channel : channels)
      // Make tracks the same height
      TrackView::Get( *channel ).SetHeight( averageHeight );
      TrackView::Get( *channel ).SetExpandedHeight( averageHeight );
}

/// Swap the left and right channels of a stero track...


@@ 898,6 898,7 @@ void WaveTrackMenuTable::OnSwapChannels(wxCommandEvent &)
   AudacityProject *const project = &mpData->project;

   WaveTrack *const pTrack = static_cast<WaveTrack*>(mpData->pTrack);
   const auto linkType = pTrack->GetLinkType();
   auto channels = TrackList::Channels( pTrack );
   if (channels.size() != 2)
      return;


@@ 911,9 912,8 @@ void WaveTrackMenuTable::OnSwapChannels(wxCommandEvent &)
   SplitStereo(false);

   auto &tracks = TrackList::Get( *project );
   tracks.MoveUp( partner );
   tracks.GroupChannels( *partner, 2 );

   tracks.MoveUp(partner);
   tracks.MakeMultiChannelTrack(*partner, 2, linkType == Track::LinkType::Aligned);
   if (hasFocus)
      trackFocus.Set(partner);


M src/tracks/playabletrack/wavetrack/ui/WaveTrackView.cpp => src/tracks/playabletrack/wavetrack/ui/WaveTrackView.cpp +97 -26
@@ 31,6 31,7 @@ Paul Licameli split from TrackPanel.cpp
#include "../../../../TrackArtist.h"
#include "../../../../TrackPanelDrawingContext.h"
#include "../../../../TrackPanelMouseEvent.h"
#include "../../../../TrackPanelResizeHandle.h"
#include "../../../../ViewInfo.h"
#include "../../../../prefs/TracksPrefs.h"



@@ 703,6 704,49 @@ std::pair<
         mCloseHandle,
         *pWaveTrackView, *this, state ) )
         results.second.push_back( pHandle );

      auto channels = TrackList::Channels(wt.get());
      if(channels.size() > 1) {
         // Only one cell is tested and we need to know
         // which one and it's relative location to the border.
         auto subviews = pWaveTrackView->GetSubViews();
         auto currentSubview = std::find_if(subviews.begin(), subviews.end(), 
            [self = shared_from_this()](const auto& p){
               return self == p.second;
         });
         if (currentSubview != subviews.end())
         {
            auto currentSubviewIndex = std::distance(subviews.begin(), currentSubview);
            
            const auto py = state.state.GetY();
            const auto topBorderHit = std::abs(py - state.rect.GetTop())
               <= WaveTrackView::kChannelSeparatorThickness / 2;
            const auto bottomBorderHit = std::abs(py - state.rect.GetBottom())
               <= WaveTrackView::kChannelSeparatorThickness / 2;

            auto currentChannel = channels.find(wt.get());
            auto currentChannelIndex = std::distance(channels.begin(), currentChannel);

            if (//for not-last-view check the bottom border hit
               ((currentChannelIndex != channels.size() - 1)
                  && (currentSubviewIndex == static_cast<int>(subviews.size()) - 1)
                  && bottomBorderHit)
               ||
               //or for not-first-view check the top border hit
               ((currentChannelIndex != 0) && currentSubviewIndex == 0 && topBorderHit))
            {
               //depending on which border hit test succeeded on we
               //need to choose a proper target for resizing
               auto it = bottomBorderHit ? currentChannel : currentChannel.advance(-1);
               auto result = std::static_pointer_cast<UIHandle>(
                  std::make_shared<TrackPanelResizeHandle>((*it)->shared_from_this(), py)
               );
               result = AssignUIHandlePtr(mResizeHandle, result);
               results.second.push_back(result);
            }
         }
      }

      if ( auto pHandle = SubViewAdjustHandle::HitTest(
         mAdjustHandle,
         *pWaveTrackView, *this, state ) )


@@ 965,6 1009,11 @@ void WaveTrackView::DoSetDisplay(Display display, bool exclusive)

auto WaveTrackView::GetSubViews( const wxRect &rect ) -> Refinement
{
   return GetSubViews(&rect);
}

auto WaveTrackView::GetSubViews(const wxRect* rect) -> Refinement
{
   BuildSubViews();

   // Collect the visible views in the right sequence


@@ 974,41 1023,51 @@ auto WaveTrackView::GetSubViews( const wxRect &rect ) -> Refinement
   std::vector< Item > items;
   size_t ii = 0;
   float total = 0;
   WaveTrackSubViews::ForEach( [&]( WaveTrackSubView &subView ){
      auto &placement = mPlacements[ii];
   WaveTrackSubViews::ForEach([&](WaveTrackSubView& subView) {
      auto& placement = mPlacements[ii];
      auto index = placement.index;
      auto fraction = placement.fraction;
      if ( index >= 0 && fraction > 0.0 )
      if (index >= 0 && fraction > 0.0)
         total += fraction,
         items.push_back( { index, fraction, subView.shared_from_this() } );
         items.push_back({ index, fraction, subView.shared_from_this() });
      ++ii;
   } );
   std::sort( items.begin(), items.end(), [](const Item &a, const Item &b){
   });
   std::sort(items.begin(), items.end(), [](const Item& a, const Item& b) {
      return a.index < b.index;
   } );
   });

   // Remove views we don't need
   auto begin = items.begin(), end = items.end(),
     newEnd = std::remove_if( begin, end,
        []( const Item &item ){ return !item.pView; } );
   items.erase( newEnd, end );
        newEnd = std::remove_if(begin, end,
            [](const Item& item) { return !item.pView; });
   items.erase(newEnd, end);

   // Assign coordinates, redenominating to the total height,
   // storing integer values
   Refinement results;
   results.reserve( items.size() );
   const auto top = rect.GetTop();
   const auto height = rect.GetHeight();
   float partial = 0;
   wxCoord lastCoord = 0;
   for ( const auto &item : items ) {
      wxCoord newCoord = top + (partial / total) * height;
      results.emplace_back( newCoord, item.pView );
      partial += item.fraction;
   }

   // Cache for the use of sub-view dragging
   mLastHeight = height;
   if (rect != nullptr)
   {
      // Assign coordinates, redenominating to the total height,
      // storing integer values
      results.reserve(items.size());
      const auto top = rect->GetTop();
      const auto height = rect->GetHeight();
      float partial = 0;
      wxCoord lastCoord = 0;
      for (const auto& item : items) {
         wxCoord newCoord = top + (partial / total) * height;
         results.emplace_back(newCoord, item.pView);
         partial += item.fraction;
      }

      // Cache for the use of sub-view dragging
      mLastHeight = height;
   }
   else
   {
      std::transform(items.begin(), items.end(), std::back_inserter(results), [](const auto& item) {
         return std::make_pair(0, item.pView);
      });
   }

   return results;
}


@@ 1026,9 1085,14 @@ WaveTrackView::GetAllSubViews()
   return results;
}

std::shared_ptr<CommonTrackCell> WaveTrackView::DoGetAffordanceControls()
std::shared_ptr<CommonTrackCell> WaveTrackView::GetAffordanceControls()
{
   return std::make_shared<WaveTrackAffordanceControls>(FindTrack());
    auto track = FindTrack();
    if (!track->IsAlignedWithLeader())
    {
        return DoGetAffordance(track);
    }
    return {};
}

void WaveTrackView::DoSetMinimized( bool minimized )


@@ 1042,6 1106,13 @@ void WaveTrackView::DoSetMinimized( bool minimized )
   } );
}

std::shared_ptr<CommonTrackCell> WaveTrackView::DoGetAffordance(const std::shared_ptr<Track>& track)
{
    if (mpAffordanceCellControl == nullptr)
        mpAffordanceCellControl = std::make_shared<WaveTrackAffordanceControls>(track);
    return mpAffordanceCellControl;
}

using DoGetWaveTrackView = DoGetView::Override< WaveTrack >;
template<> template<> auto DoGetWaveTrackView::Implementation() -> Function {
   return [](WaveTrack &track) {

M src/tracks/playabletrack/wavetrack/ui/WaveTrackView.h => src/tracks/playabletrack/wavetrack/ui/WaveTrackView.h +19 -3
@@ 52,6 52,7 @@ protected:

private:
   std::weak_ptr<UIHandle> mCloseHandle;
   std::weak_ptr<UIHandle> mResizeHandle;
   std::weak_ptr<UIHandle> mAdjustHandle;
   std::weak_ptr<UIHandle> mRearrangeHandle;
   std::weak_ptr<CutlineHandle> mCutlineHandle;


@@ 77,6 78,8 @@ class TENACITY_DLL_API WaveTrackView final
   WaveTrackView &operator=( const WaveTrackView& ) = delete;

public:
   static constexpr int kChannelSeparatorThickness{ 8 };

   using Display = WaveTrackViewConstants::Display;

   static WaveTrackView &Get( WaveTrack &track );


@@ 128,6 131,15 @@ public:

   std::weak_ptr<WaveClip> GetSelectedClip();

   // Returns a visible subset of subviews, sorted in the same 
   // order as they are supposed to be displayed
   

   // Get the visible sub-views,
   // if rect is provided then result will contain
   // y coordinate for each subview within this rect
   Refinement GetSubViews(const wxRect* rect = nullptr);

private:
   void BuildSubViews() const;
   void DoSetDisplay(Display display, bool exclusive = true);


@@ 143,11 155,10 @@ private:
      override;

   // TrackView implementation
   // Get the visible sub-views with top y coordinates
   Refinement GetSubViews( const wxRect &rect ) override;
   Refinement GetSubViews(const wxRect& rect) override;

protected:
   std::shared_ptr<CommonTrackCell> DoGetAffordanceControls() override;
   std::shared_ptr<CommonTrackCell> GetAffordanceControls() override;

   void DoSetMinimized( bool minimized ) override;



@@ 158,6 169,11 @@ protected:
   mutable wxCoord mLastHeight{};

   bool mMultiView{ false };

private:
   std::shared_ptr<CommonTrackCell> DoGetAffordance(const std::shared_ptr<Track>& track);

   std::shared_ptr<CommonTrackCell> mpAffordanceCellControl;
};

// Helper for drawing routines

M src/tracks/ui/BackgroundCell.cpp => src/tracks/ui/BackgroundCell.cpp +6 -6
@@ 19,6 19,7 @@ Paul Licameli split from TrackPanel.cpp
#include "../../Track.h"
#include "../../TrackArtist.h"
#include "../../TrackPanel.h"
#include "../../TrackPanelConstants.h"
#include "../../TrackPanelDrawingContext.h"
#include "../../TrackPanelMouseEvent.h"
#include "../../UIHandle.h"


@@ 37,6 38,9 @@ class BackgroundHandle : public UIHandle
public:
   BackgroundHandle() {}

   BackgroundHandle(BackgroundHandle&&) = default;
   BackgroundHandle& operator=(BackgroundHandle&&) = default;

   static HitTestPreview HitPreview()
   {
      static wxCursor arrowCursor{ wxCURSOR_ARROW };


@@ 108,12 112,8 @@ std::vector<UIHandlePtr> BackgroundCell::HitTest
(const TrackPanelMouseState &,
 const AudacityProject *)
{
   std::vector<UIHandlePtr> results;
   auto result = mHandle.lock();
   if (!result)
      result = std::make_shared<BackgroundHandle>();
   results.push_back(result);
   return results;
   auto result = AssignUIHandlePtr(mHandle, std::make_shared<BackgroundHandle>());
   return { result };
}

std::shared_ptr<Track> BackgroundCell::DoFindTrack()

M src/tracks/ui/EnvelopeHandle.cpp => src/tracks/ui/EnvelopeHandle.cpp +2 -2
@@ 43,9 43,9 @@ EnvelopeHandle::~EnvelopeHandle()
{}

UIHandlePtr EnvelopeHandle::HitAnywhere
(std::weak_ptr<EnvelopeHandle> & WXUNUSED(holder), Envelope *envelope, bool timeTrack)
(std::weak_ptr<EnvelopeHandle> &holder, Envelope *envelope, bool timeTrack)
{
   auto result = std::make_shared<EnvelopeHandle>( envelope );
   auto result = AssignUIHandlePtr(holder, std::make_shared<EnvelopeHandle>(envelope));
   result->mTimeTrack = timeTrack;
   return result;
}

M src/tracks/ui/EnvelopeHandle.h => src/tracks/ui/EnvelopeHandle.h +3 -0
@@ 38,6 38,9 @@ class TENACITY_DLL_API EnvelopeHandle final : public UIHandle

public:
   explicit EnvelopeHandle( Envelope *pEnvelope );
   
   EnvelopeHandle(EnvelopeHandle&&) = default;
   EnvelopeHandle& operator=(EnvelopeHandle&&) = default;

   virtual ~EnvelopeHandle();


M src/tracks/ui/TimeShiftHandle.cpp => src/tracks/ui/TimeShiftHandle.cpp +2 -0
@@ 961,6 961,8 @@ UIHandle::Result TimeShiftHandle::Release
   if (mDidSlideVertically) {
      msg = XO("Moved clips to another track");
      consolidate = false;
      for (auto& pair : mClipMoveState.shifters)
         pair.first->LinkConsistencyCheck();
   }
   else {
      msg = ( mClipMoveState.hSlideAmount > 0

M src/tracks/ui/TrackView.cpp => src/tracks/ui/TrackView.cpp +10 -15
@@ 41,7 41,7 @@ int TrackView::GetCumulativeHeight( const Track *pTrack )
   if ( !pTrack )
      return 0;
   auto &view = Get( *pTrack );
   return view.GetY() + view.GetHeight();
   return view.GetCumulativeHeightBefore() + view.GetHeight();
}

int TrackView::GetTotalHeight( const TrackList &list )


@@ 87,7 87,7 @@ void TrackView::SetMinimized(bool isMinimized)

void TrackView::WriteXMLAttributes( XMLWriter &xmlFile ) const
{
   xmlFile.WriteAttr(wxT("height"), GetActualHeight());
   xmlFile.WriteAttr(wxT("height"), GetExpandedHeight());
   xmlFile.WriteAttr(wxT("minimized"), GetMinimized());
}



@@ 101,7 101,7 @@ bool TrackView::HandleXMLAttribute( const wxChar *attr, const wxChar *value )
      // will stall Audacity as it tries to create an enormous vertical ruler.
      // So clamp to reasonable values.
      nValue = std::max( 40l, std::min( nValue, 1000l ));
      SetHeight(nValue);
      SetExpandedHeight(nValue);
      return true;
   }
   else if (!wxStrcmp(attr, wxT("minimized")) &&


@@ 141,13 141,6 @@ std::shared_ptr<const TrackVRulerControls> TrackView::GetVRulerControls() const
   return const_cast< TrackView* >( this )->GetVRulerControls();
}

std::shared_ptr<CommonTrackCell> TrackView::GetAffordanceControls()
{
   if (!mpAffordanceCellControl)
      mpAffordanceCellControl = DoGetAffordanceControls();
   return mpAffordanceCellControl;
}

void TrackView::DoSetY(int y)
{
   mY = y;


@@ 161,7 154,7 @@ int TrackView::GetHeight() const
   return mHeight;
}

void TrackView::SetHeight(int h)
void TrackView::SetExpandedHeight(int h)
{
   DoSetHeight(h);
   FindTrack()->AdjustPositions();


@@ 172,15 165,17 @@ void TrackView::DoSetHeight(int h)
   mHeight = h;
}

std::shared_ptr<CommonTrackCell> TrackView::DoGetAffordanceControls()
std::shared_ptr<CommonTrackCell> TrackView::GetAffordanceControls()
{
   return {};
}

namespace {

// Attach an object to each project.  It receives track list events and updates
// track Y coordinates
/*!
 Attached to each project, it receives track list events and maintains the
 cache of cumulative track view heights for use by TrackPanel.
 */
struct TrackPositioner final : ClientData::Base, wxEvtHandler
{
   AudacityProject &mProject;


@@ 214,7 209,7 @@ struct TrackPositioner final : ClientData::Base, wxEvtHandler

      while( auto pTrack = *iter ) {
         auto &view = TrackView::Get( *pTrack );
         view.SetY( yy );
         view.SetCumulativeHeightBefore( yy );
         yy += view.GetHeight();
         ++iter;
      }

M src/tracks/ui/TrackView.h => src/tracks/ui/TrackView.h +33 -11
@@ 49,22 49,45 @@ public:
   bool GetMinimized() const { return mMinimized; }
   void SetMinimized( bool minimized );

   int GetY() const { return mY; }
   int GetActualHeight() const { return mHeight; }
   //! @return cached sum of `GetHeight()` of all preceding tracks
   int GetCumulativeHeightBefore() const { return mY; }

   //! @return height of the track when expanded
   /*! See other comments for GetHeight */
   int GetExpandedHeight() const { return mHeight; }

   //! @return height of the track when collapsed
   /*! See other comments for GetHeight */
   virtual int GetMinimizedHeight() const = 0;

   //! @return height of the track as it now appears, expanded or collapsed
   /*!
    Total "height" of channels of a track includes padding areas above and
    below it, and is pixel-accurate for the channel group.
    The "heights" of channels within a group determine the proportions of
    heights of the track data shown -- but the actual total pixel heights
    may differ when other fixed-height adornments and paddings are added,
    according to other rules for allocation of height.
   */
   int GetHeight() const;

   void SetY(int y) { DoSetY( y ); }
   void SetHeight(int height);
   //! Set cached value dependent on position within the track list
   void SetCumulativeHeightBefore(int y) { DoSetY( y ); }

   /*! Sets height for expanded state.
    Does not expand a track if it is now collapsed.
    See other comments for GetHeight
    */
   void SetExpandedHeight(int height);

   // Return another, associated TrackPanelCell object that implements the
   // mouse actions for the vertical ruler
   std::shared_ptr<TrackVRulerControls> GetVRulerControls();
   std::shared_ptr<const TrackVRulerControls> GetVRulerControls() const;

   // by default returns nullptr, meaning that track has no drag controls area
   std::shared_ptr<CommonTrackCell> GetAffordanceControls();

   // Returns cell that would be used at affordance area, by default returns nullptr,
   // meaning that track has no such area.
   virtual std::shared_ptr<CommonTrackCell> GetAffordanceControls();

   void WriteXMLAttributes( XMLWriter & ) const override;
   bool HandleXMLAttribute( const wxChar *attr, const wxChar *value ) override;


@@ 81,21 104,20 @@ public:

   virtual void DoSetMinimized( bool isMinimized );

protected:
private:

   // No need yet to make this virtual
   void DoSetY(int y);

   void DoSetHeight(int h);

protected:

   // Private factory to make appropriate object; class TrackView handles
   // memory management thereafter
   virtual std::shared_ptr<TrackVRulerControls> DoGetVRulerControls() = 0;
   // May return nullptr (which is default) if track does not need affordance area
   virtual std::shared_ptr<CommonTrackCell> DoGetAffordanceControls();

   std::shared_ptr<TrackVRulerControls> mpVRulerControls;
   std::shared_ptr<CommonTrackCell> mpAffordanceCellControl;

private:
   bool           mMinimized{ false };