1 /**********************************************************************
2 
3 Audacity: A Digital Audio Editor
4 
5 LabelTrackView.cpp
6 
7 Paul Licameli split from TrackPanel.cpp
8 
9 **********************************************************************/
10 
11 
12 #include "LabelTrackView.h"
13 
14 #include "LabelTrackVRulerControls.h"
15 #include "LabelGlyphHandle.h"
16 #include "LabelTextHandle.h"
17 
18 #include "../../../LabelTrack.h"
19 
20 #include "AColor.h"
21 #include "../../../widgets/BasicMenu.h"
22 #include "AllThemeResources.h"
23 #include "../../../HitTestResult.h"
24 #include "Project.h"
25 #include "../../../ProjectHistory.h"
26 #include "ProjectRate.h"
27 #include "../../../ProjectSettings.h"
28 #include "../../../ProjectWindow.h"
29 #include "../../../ProjectWindows.h"
30 #include "../../../RefreshCode.h"
31 #include "Theme.h"
32 #include "../../../TrackArtist.h"
33 #include "../../../TrackPanelAx.h"
34 #include "../../../TrackPanel.h"
35 #include "../../../TrackPanelMouseEvent.h"
36 #include "../../../UndoManager.h"
37 #include "ViewInfo.h"
38 #include "../../../widgets/AudacityTextEntryDialog.h"
39 #include "../../../widgets/wxWidgetsWindowPlacement.h"
40 
41 #include <wx/clipbrd.h>
42 #include <wx/dcclient.h>
43 #include <wx/font.h>
44 #include <wx/frame.h>
45 #include <wx/menu.h>
46 
Index()47 LabelTrackView::Index::Index()
48 :  mIndex(-1),
49    mModified(false)
50 {
51 }
52 
Index(int index)53 LabelTrackView::Index::Index(int index)
54 :  mIndex(index),
55    mModified(false)
56 {
57 }
58 
operator =(int index)59 LabelTrackView::Index &LabelTrackView::Index::operator =(int index)
60 {
61    if (index != mIndex) {
62       mModified = false;
63    }
64    mIndex = index;
65    return *this;
66 }
67 
operator ++()68 LabelTrackView::Index &LabelTrackView::Index::operator ++()
69 {
70    mModified = false;
71    mIndex += 1;
72    return *this;
73 }
74 
operator --()75 LabelTrackView::Index &LabelTrackView::Index::operator --()
76 {
77    mModified = false;
78    mIndex -= 1;
79    return *this;
80 }
81 
operator int() const82 LabelTrackView::Index::operator int() const
83 {
84    return mIndex;
85 }
86 
IsModified() const87 bool LabelTrackView::Index::IsModified() const
88 {
89    return mModified;
90 }
91 
SetModified(bool modified)92 void LabelTrackView::Index::SetModified(bool modified)
93 {
94    mModified = modified;
95 }
96 
LabelTrackView(const std::shared_ptr<Track> & pTrack)97 LabelTrackView::LabelTrackView( const std::shared_ptr<Track> &pTrack )
98    : CommonTrackView{ pTrack }
99 {
100    ResetFont();
101    CreateCustomGlyphs();
102    ResetFlags();
103 
104    // Events will be emitted by the track
105    const auto pLabelTrack = FindLabelTrack();
106    BindTo( pLabelTrack.get() );
107 }
108 
~LabelTrackView()109 LabelTrackView::~LabelTrackView()
110 {
111 }
112 
Reparent(const std::shared_ptr<Track> & parent)113 void LabelTrackView::Reparent( const std::shared_ptr<Track> &parent )
114 {
115    auto oldParent = FindLabelTrack();
116    auto newParent = track_cast<LabelTrack*>(parent.get());
117    if (oldParent.get() != newParent) {
118       UnbindFrom( oldParent.get() );
119       BindTo( newParent );
120    }
121    CommonTrackView::Reparent( parent );
122 }
123 
BindTo(LabelTrack * pParent)124 void LabelTrackView::BindTo( LabelTrack *pParent )
125 {
126    pParent->Bind(
127       EVT_LABELTRACK_ADDITION, &LabelTrackView::OnLabelAdded, this );
128    pParent->Bind(
129       EVT_LABELTRACK_DELETION, &LabelTrackView::OnLabelDeleted, this );
130    pParent->Bind(
131       EVT_LABELTRACK_PERMUTED, &LabelTrackView::OnLabelPermuted, this );
132    pParent->Bind(
133       EVT_LABELTRACK_SELECTION, &LabelTrackView::OnSelectionChange, this );
134 }
135 
UnbindFrom(LabelTrack * pParent)136 void LabelTrackView::UnbindFrom( LabelTrack *pParent )
137 {
138    pParent->Unbind(
139       EVT_LABELTRACK_ADDITION, &LabelTrackView::OnLabelAdded, this );
140    pParent->Unbind(
141       EVT_LABELTRACK_DELETION, &LabelTrackView::OnLabelDeleted, this );
142    pParent->Unbind(
143       EVT_LABELTRACK_PERMUTED, &LabelTrackView::OnLabelPermuted, this );
144    pParent->Unbind(
145       EVT_LABELTRACK_SELECTION, &LabelTrackView::OnSelectionChange, this );
146 }
147 
CopyTo(Track & track) const148 void LabelTrackView::CopyTo( Track &track ) const
149 {
150    TrackView::CopyTo( track );
151    auto &other = TrackView::Get( track );
152 
153    if ( const auto pOther = dynamic_cast< const LabelTrackView* >( &other ) ) {
154       pOther->mNavigationIndex = mNavigationIndex;
155       pOther->mInitialCursorPos = mInitialCursorPos;
156       pOther->mCurrentCursorPos = mCurrentCursorPos;
157       pOther->mTextEditIndex = mTextEditIndex;
158       pOther->mUndoLabel = mUndoLabel;
159    }
160 }
161 
Get(LabelTrack & track)162 LabelTrackView &LabelTrackView::Get( LabelTrack &track )
163 {
164    return static_cast< LabelTrackView& >( TrackView::Get( track ) );
165 }
166 
Get(const LabelTrack & track)167 const LabelTrackView &LabelTrackView::Get( const LabelTrack &track )
168 {
169    return static_cast< const LabelTrackView& >( TrackView::Get( track ) );
170 }
171 
FindLabelTrack()172 std::shared_ptr<LabelTrack> LabelTrackView::FindLabelTrack()
173 {
174    return std::static_pointer_cast<LabelTrack>( FindTrack() );
175 }
176 
FindLabelTrack() const177 std::shared_ptr<const LabelTrack> LabelTrackView::FindLabelTrack() const
178 {
179    return const_cast<LabelTrackView*>(this)->FindLabelTrack();
180 }
181 
DetailedHitTest(const TrackPanelMouseState & st,const AudacityProject * WXUNUSED (pProject),int,bool)182 std::vector<UIHandlePtr> LabelTrackView::DetailedHitTest
183 (const TrackPanelMouseState &st,
184  const AudacityProject *WXUNUSED(pProject), int, bool)
185 {
186    UIHandlePtr result;
187    std::vector<UIHandlePtr> results;
188    const wxMouseState &state = st.state;
189 
190    const auto pTrack = FindLabelTrack();
191    result = LabelGlyphHandle::HitTest(
192       mGlyphHandle, state, pTrack, st.rect);
193    if (result)
194       results.push_back(result);
195 
196    result = LabelTextHandle::HitTest(
197       mTextHandle, state, pTrack);
198    if (result)
199       results.push_back(result);
200 
201    return results;
202 }
203 
204 // static member variables.
205 bool LabelTrackView::mbGlyphsReady=false;
206 
207 wxFont LabelTrackView::msFont;
208 
209 /// We have several variants of the icons (highlighting).
210 /// The icons are draggable, and you can drag one boundary
211 /// or all boundaries at the same timecode depending on whether you
212 /// click the centre (for all) or the arrow part (for one).
213 /// Currently we have twelve variants but we're only using six.
214 wxBitmap LabelTrackView::mBoundaryGlyphs[ NUM_GLYPH_CONFIGS * NUM_GLYPH_HIGHLIGHTS ];
215 int LabelTrackView::mIconHeight;
216 int LabelTrackView::mIconWidth;
217 int LabelTrackView::mTextHeight;
218 
219 int LabelTrackView::mFontHeight=-1;
220 
ResetFlags()221 void LabelTrackView::ResetFlags()
222 {
223    mInitialCursorPos = 1;
224    mCurrentCursorPos = 1;
225    mTextEditIndex = -1;
226    mNavigationIndex = -1;
227 }
228 
SaveFlags() const229 LabelTrackView::Flags LabelTrackView::SaveFlags() const
230 {
231    return {
232       mInitialCursorPos, mCurrentCursorPos, mNavigationIndex,
233       mTextEditIndex, mUndoLabel
234    };
235 }
236 
RestoreFlags(const Flags & flags)237 void LabelTrackView::RestoreFlags( const Flags& flags )
238 {
239    mInitialCursorPos = flags.mInitialCursorPos;
240    mCurrentCursorPos = flags.mCurrentCursorPos;
241    mNavigationIndex = flags.mNavigationIndex;
242    mTextEditIndex = flags.mTextEditIndex;
243 }
244 
GetFont(const wxString & faceName,int size)245 wxFont LabelTrackView::GetFont(const wxString &faceName, int size)
246 {
247    wxFontEncoding encoding;
248    if (faceName.empty())
249       encoding = wxFONTENCODING_DEFAULT;
250    else
251       encoding = wxFONTENCODING_SYSTEM;
252 
253    auto fontInfo = size == 0 ? wxFontInfo() : wxFontInfo(size);
254    fontInfo
255       .Encoding(encoding)
256       .FaceName(faceName);
257 
258    return wxFont(fontInfo);
259 }
260 
ResetFont()261 void LabelTrackView::ResetFont()
262 {
263    mFontHeight = -1;
264    wxString facename = gPrefs->Read(wxT("/GUI/LabelFontFacename"), wxT(""));
265    int size = gPrefs->Read(wxT("/GUI/LabelFontSize"), DefaultFontSize);
266    msFont = GetFont(facename, size);
267 }
268 
269 /// ComputeTextPosition is 'smart' about where to display
270 /// the label text.
271 ///
272 /// The text must be displayed between its endpoints x and x1
273 /// We'd also like it centered between them, and we'd like it on
274 /// screen.  It isn't always possible to achieve all of this,
275 /// so we do the best we can.
276 ///
277 /// This function has a number of tests and adjustments to the
278 /// text start position.  The tests later in the function will
279 /// take priority over the ones earlier, so because centering
280 /// is the first thing we do, it's the first thing we lose if
281 /// we can't do everything we want to.
ComputeTextPosition(const wxRect & r,int index) const282 void LabelTrackView::ComputeTextPosition(const wxRect & r, int index) const
283 {
284    const auto pTrack = FindLabelTrack();
285    const auto &mLabels = pTrack->GetLabels();
286 
287    const auto &labelStruct = mLabels[index];
288 
289    // xExtra is extra space
290    // between the text and the endpoints.
291    const int xExtra=mIconWidth;
292    int x     = labelStruct.x;  // left endpoint
293    int x1    = labelStruct.x1; // right endpoint.
294    int width = labelStruct.width;
295 
296    int xText; // This is where the text will end up.
297 
298    // Will the text all fit at this zoom?
299    bool bTooWideForScreen = width > (r.width-2*xExtra);
300 // bool bSimpleCentering = !bTooWideForScreen;
301    bool bSimpleCentering = false;
302 
303    //TODO (possibly):
304    // Add configurable options as to when to use simple
305    // and when complex centering.
306    //
307    // Simple centering does its best to keep the text
308    // centered between the label limits.
309    //
310    // Complex centering positions the text proportionally
311    // to how far we are through the label.
312    //
313    // If we add preferences for this, we want to be able to
314    // choose separately whether:
315    //   a) Wide text labels centered simple/complex.
316    //   b) Other text labels centered simple/complex.
317    //
318 
319    if( bSimpleCentering )
320    {
321       // Center text between the two end points.
322       xText = (x+x1-width)/2;
323    }
324    else
325    {
326       // Calculate xText position to make text line
327       // scroll sideways evenly as r moves right.
328 
329       // xText is a linear function of r.x.
330       // These variables are used to compute that function.
331       int rx0,rx1,xText0,xText1;
332 
333       // Since we will be using a linear function,
334       // we should blend smoothly between left and right
335       // aligned text as r, the 'viewport' moves.
336       if( bTooWideForScreen )
337       {
338          rx0=x;           // when viewport at label start.
339          xText0=x+xExtra; // text aligned left.
340          rx1=x1-r.width;  // when viewport end at label end
341          xText1=x1-(width+xExtra); // text aligned right.
342       }
343       else
344       {
345          // when label start + width + extra spacing at viewport end..
346          rx0=x-r.width+width+2*xExtra;
347          // ..text aligned left.
348          xText0=x+xExtra;
349          // when viewport start + width + extra spacing at label end..
350          rx1=x1-(width+2*xExtra);
351          // ..text aligned right.
352          xText1=x1-(width+xExtra);
353       }
354 
355       if( rx1 > rx0 ) // Avoid divide by zero case.
356       {
357          // Compute the blend between left and right aligned.
358 
359          // Don't use:
360          //
361          // xText = xText0 + ((xText1-xText0)*(r.x-rx0))/(rx1-rx0);
362          //
363          // The problem with the above is that it integer-oveflows at
364          // high zoom.
365 
366          // Instead use:
367          xText = xText0 + (int)((xText1-xText0)*(((float)(r.x-rx0))/(rx1-rx0)));
368       }
369       else
370       {
371          // Avoid divide by zero by reverting to
372          // simple centering.
373          //
374          // We could also fall into this case if x and x1
375          // are swapped, in which case we'll end up
376          // left aligned anyway because code later on
377          // will catch that.
378          xText = (x+x1-width)/2;
379       }
380    }
381 
382    // Is the text now appearing partly outside r?
383    bool bOffLeft = xText < r.x+xExtra;
384    bool bOffRight = xText > r.x+r.width-width-xExtra;
385 
386    // IF any part of the text is offscreen
387    // THEN we may bring it back.
388    if( bOffLeft == bOffRight )
389    {
390       //IF both sides on screen, THEN nothing to do.
391       //IF both sides off screen THEN don't do
392       //anything about it.
393       //(because if we did, you'd never get to read
394       //all the text by scrolling).
395    }
396    else if( bOffLeft != bTooWideForScreen)
397    {
398       // IF we're off on the left, OR we're
399       // too wide for the screen and off on the right
400       // (only) THEN align left.
401       xText = r.x+xExtra;
402    }
403    else
404    {
405       // We're off on the right, OR we're
406       // too wide and off on the left (only)
407       // SO align right.
408       xText =r.x+r.width-width-xExtra;
409    }
410 
411    // But if we've taken the text out from its endpoints
412    // we must move it back so that it's between the endpoints.
413 
414    // We test the left end point last because the
415    // text might not even fit between the endpoints (at this
416    // zoom factor), and in that case we'd like to position
417    // the text at the left end point.
418    if( xText > (x1-width-xExtra))
419       xText=(x1-width-xExtra);
420    if( xText < x+xExtra )
421       xText=x+xExtra;
422 
423    labelStruct.xText = xText;
424 }
425 
426 /// ComputeLayout determines which row each label
427 /// should be placed on, and reserves space for it.
428 /// Function assumes that the labels are sorted.
ComputeLayout(const wxRect & r,const ZoomInfo & zoomInfo) const429 void LabelTrackView::ComputeLayout(const wxRect & r, const ZoomInfo &zoomInfo) const
430 {
431    int xUsed[MAX_NUM_ROWS];
432 
433    int iRow;
434    // Rows are the 'same' height as icons or as the text,
435    // whichever is taller.
436    const int yRowHeight = wxMax(mTextHeight,mIconHeight)+3;// pixels.
437    // Extra space at end of rows.
438    // We allow space for one half icon at the start and two
439    // half icon widths for extra x for the text frame.
440    // [we don't allow half a width space for the end icon since it is
441    // allowed to be obscured by the text].
442    const int xExtra= (3 * mIconWidth)/2;
443 
444    bool bAvoidName = false;
445    const int nRows = wxMin((r.height / yRowHeight) + 1, MAX_NUM_ROWS);
446    if( nRows > 2 )
447       bAvoidName = gPrefs->ReadBool(wxT("/GUI/ShowTrackNameInWaveform"), false);
448    // Initially none of the rows have been used.
449    // So set a value that is less than any valid value.
450    {
451       // Bug 502: With dragging left of zeros, labels can be in
452       // negative space.  So set least possible value as starting point.
453       const int xStart = INT_MIN;
454       for (auto &x : xUsed)
455          x = xStart;
456    }
457    int nRowsUsed=0;
458 
459    const auto pTrack = FindLabelTrack();
460    const auto &mLabels = pTrack->GetLabels();
461 
462    { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
463       const int x = zoomInfo.TimeToPosition(labelStruct.getT0(), r.x);
464       const int x1 = zoomInfo.TimeToPosition(labelStruct.getT1(), r.x);
465       int y = r.y;
466 
467       labelStruct.x=x;
468       labelStruct.x1=x1;
469       labelStruct.y=-1;// -ve indicates nothing doing.
470       iRow=0;
471       // Our first preference is a row that ends where we start.
472       // (This is to encourage merging of adjacent label boundaries).
473       while( (iRow<nRowsUsed) && (xUsed[iRow] != x ))
474          iRow++;
475 
476       // IF we didn't find one THEN
477       // find any row that can take a span starting at x.
478       if( iRow >= nRowsUsed )
479       {
480          iRow=0;
481          while( (iRow<nRows) && (xUsed[iRow] > x ))
482             iRow++;
483       }
484       // IF we found such a row THEN record a valid position.
485       if( iRow<nRows )
486       {
487          // Logic to ameliorate case where first label is under the
488          // (on track) track name.  For later labels it does not matter
489          // as we can scroll left or right and/or zoom.
490          // A possible alternative idea would be to (instead) increase the
491          // translucency of the track name, when the mouse is inside it.
492          if( (i==0 ) && (iRow==0) && bAvoidName ){
493             // reserve some space in first row.
494             // reserve max of 200px or t1, or text box right edge.
495             const int x2 = zoomInfo.TimeToPosition(0.0, r.x) + 200;
496             xUsed[iRow]=x+labelStruct.width+xExtra;
497             if( xUsed[iRow] < x1 ) xUsed[iRow]=x1;
498             if( xUsed[iRow] < x2 ) xUsed[iRow]=x2;
499             iRow=1;
500          }
501 
502          // Possibly update the number of rows actually used.
503          if( iRow >= nRowsUsed )
504             nRowsUsed=iRow+1;
505          // Record the position for this label
506          y= r.y + iRow * yRowHeight +(yRowHeight/2)+1;
507          labelStruct.y=y;
508          // On this row we have used up to max of end marker and width.
509          // Plus also allow space to show the start icon and
510          // some space for the text frame.
511          xUsed[iRow]=x+labelStruct.width+xExtra;
512          if( xUsed[iRow] < x1 ) xUsed[iRow]=x1;
513          ComputeTextPosition( r, i );
514       }
515    }}
516 }
517 
518 /// Draw vertical lines that go exactly through the position
519 /// of the start or end of a label.
520 ///   @param  dc the device context
521 ///   @param  r  the LabelTrack rectangle.
DrawLines(wxDC & dc,const LabelStruct & ls,const wxRect & r)522 void LabelTrackView::DrawLines(
523    wxDC & dc, const LabelStruct &ls, const wxRect & r)
524 {
525    auto &x = ls.x;
526    auto &x1 = ls.x1;
527    auto &y = ls.y;
528 
529    // Bug 2388 - Point label and range label can appear identical
530    // If the start and end times are not actually the same, but they
531    // would appear so when drawn as lines at current zoom, be sure to draw
532    // two lines - i.e. displace the second line slightly.
533    if (ls.getT0() != ls.getT1()) {
534       if (x == x1)
535          x1++;
536    }
537 
538    // How far out from the centre line should the vertical lines
539    // start, i.e. what is the y position of the icon?
540    // We adjust this so that the line encroaches on the icon
541    // slightly (there is white space in the design).
542    const int yIconStart = y - (mIconHeight /2)+1+(mTextHeight+3)/2;
543    const int yIconEnd   = yIconStart + mIconHeight-2;
544 
545    // If y is positive then it is the center line for the
546    // Label.
547    if( y >= 0 )
548    {
549       if((x  >= r.x) && (x  <= (r.x+r.width)))
550       {
551          // Draw line above and below left dragging widget.
552          AColor::Line(dc, x, r.y,  x, yIconStart - 1);
553          AColor::Line(dc, x, yIconEnd, x, r.y + r.height);
554       }
555       if((x1 >= r.x) && (x1 <= (r.x+r.width)))
556       {
557          // Draw line above and below right dragging widget.
558          AColor::Line(dc, x1, r.y,  x1, yIconStart - 1);
559          AColor::Line(dc, x1, yIconEnd, x1, r.y + r.height);
560       }
561    }
562    else
563    {
564       // Draw the line, even though the widget is off screen
565       AColor::Line(dc, x, r.y,  x, r.y + r.height);
566       AColor::Line(dc, x1, r.y,  x1, r.y + r.height);
567    }
568 }
569 
570 /// DrawGlyphs draws the wxIcons at the start and end of a label.
571 ///   @param  dc the device context
572 ///   @param  r  the LabelTrack rectangle.
DrawGlyphs(wxDC & dc,const LabelStruct & ls,const wxRect & r,int GlyphLeft,int GlyphRight)573 void LabelTrackView::DrawGlyphs(
574    wxDC & dc, const LabelStruct &ls, const wxRect & r,
575    int GlyphLeft, int GlyphRight)
576 {
577    auto &y = ls.y;
578 
579    const int xHalfWidth=mIconWidth/2;
580    const int yStart=y-mIconHeight/2+(mTextHeight+3)/2;
581 
582    // If y == -1, nothing to draw
583    if( y == -1 )
584       return;
585 
586    auto &x = ls.x;
587    auto &x1 = ls.x1;
588 
589    if((x  >= r.x) && (x  <= (r.x+r.width)))
590       dc.DrawBitmap(GetGlyph(GlyphLeft), x-xHalfWidth,yStart, true);
591    // The extra test commented out here would suppress right hand markers
592    // when they overlap the left hand marker (e.g. zoomed out) or to the left.
593    if((x1 >= r.x) && (x1 <= (r.x+r.width)) /*&& (x1>x+mIconWidth)*/)
594       dc.DrawBitmap(GetGlyph(GlyphRight), x1-xHalfWidth,yStart, true);
595 }
596 
GetTextFrameHeight()597 int LabelTrackView::GetTextFrameHeight()
598 {
599     return mTextHeight + TextFramePadding * 2;
600 }
601 
602 /// Draw the text of the label and also draw
603 /// a long thin rectangle for its full extent
604 /// from x to x1 and a rectangular frame
605 /// behind the text itself.
606 ///   @param  dc the device context
607 ///   @param  r  the LabelTrack rectangle.
DrawText(wxDC & dc,const LabelStruct & ls,const wxRect & r)608 void LabelTrackView::DrawText(wxDC & dc, const LabelStruct &ls, const wxRect & r)
609 {
610    const int yFrameHeight = mTextHeight + TextFramePadding * 2;
611    //If y is positive then it is the center line for the
612    //text we are about to draw.
613    //if it isn't, nothing to draw.
614 
615    auto &y = ls.y;
616    if( y == -1 )
617       return;
618 
619    // Draw frame for the text...
620    // We draw it half an icon width left of the text itself.
621    {
622       auto &xText = ls.xText;
623       const int xStart=wxMax(r.x,xText-mIconWidth/2);
624       const int xEnd=wxMin(r.x+r.width,xText+ls.width+mIconWidth/2);
625       const int xWidth = xEnd-xStart;
626 
627       if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
628       {
629          // Now draw the text itself.
630          auto pos = y - LabelBarHeight - yFrameHeight + TextFrameYOffset +
631             (yFrameHeight - mFontHeight) / 2 + dc.GetFontMetrics().ascent;
632          dc.DrawText(ls.title, xText, pos);
633       }
634    }
635 
636 }
637 
DrawTextBox(wxDC & dc,const LabelStruct & ls,const wxRect & r)638 void LabelTrackView::DrawTextBox(
639    wxDC & dc, const LabelStruct &ls, const wxRect & r)
640 {
641    // In drawing the bar and the frame, we compute the clipping
642    // to the viewport ourselves.  Under Win98 the GDI does its
643    // calculations in 16 bit arithmetic, and so gets it completely
644    // wrong at higher zooms where the bar can easily be
645    // more than 65536 pixels wide.
646 
647    // Draw bar for label extent...
648    // We don't quite draw from x to x1 because we allow
649    // half an icon width at each end.
650     const auto textFrameHeight = GetTextFrameHeight();
651     auto& xText = ls.xText;
652     const int xStart = wxMax(r.x, xText - mIconWidth / 2);
653     const int xEnd = wxMin(r.x + r.width, xText + ls.width + mIconWidth / 2);
654     const int xWidth = xEnd - xStart;
655 
656     if ((xStart < (r.x + r.width)) && (xEnd > r.x) && (xWidth > 0))
657     {
658        wxRect frame(
659           xStart, ls.y - (textFrameHeight + LabelBarHeight) / 2 + TextFrameYOffset,
660           xWidth, textFrameHeight);
661        dc.DrawRectangle(frame);
662     }
663 }
664 
DrawBar(wxDC & dc,const LabelStruct & ls,const wxRect & r)665 void LabelTrackView::DrawBar(wxDC& dc, const LabelStruct& ls, const wxRect& r)
666 {
667    //If y is positive then it is the center line for the
668    //text we are about to draw.
669    const int xBarShorten = mIconWidth + 4;
670    auto& y = ls.y;
671    if (y == -1)
672      return;
673 
674    auto& x = ls.x;
675    auto& x1 = ls.x1;
676    const int xStart = wxMax(r.x, x + xBarShorten / 2);
677    const int xEnd = wxMin(r.x + r.width, x1 - xBarShorten / 2);
678    const int xWidth = xEnd - xStart;
679 
680    if ((xStart < (r.x + r.width)) && (xEnd > r.x) && (xWidth > 0))
681    {
682       wxRect bar(xStart, y - (LabelBarHeight - GetTextFrameHeight()) / 2,
683          xWidth, LabelBarHeight);
684       if (x1 > x + xBarShorten)
685          dc.DrawRectangle(bar);
686    }
687 }
688 
689 /// Draws text-selected region within the label
DrawHighlight(wxDC & dc,const LabelStruct & ls,int xPos1,int xPos2,int charHeight)690 void LabelTrackView::DrawHighlight( wxDC & dc, const LabelStruct &ls,
691    int xPos1, int xPos2, int charHeight)
692 {
693    const int yFrameHeight = mTextHeight + TextFramePadding * 2;
694 
695    dc.SetPen(*wxTRANSPARENT_PEN);
696    wxBrush curBrush = dc.GetBrush();
697    curBrush.SetColour(wxString(wxT("BLUE")));
698    auto top = ls.y + TextFrameYOffset - (LabelBarHeight + yFrameHeight) / 2 + (yFrameHeight - charHeight) / 2;
699    if (xPos1 < xPos2)
700       dc.DrawRectangle(xPos1-1, top, xPos2-xPos1+1, charHeight);
701    else
702       dc.DrawRectangle(xPos2-1, top, xPos1-xPos2+1, charHeight);
703 }
704 
705 namespace {
getXPos(const LabelStruct & ls,wxDC & dc,int * xPos1,int cursorPos)706 void getXPos( const LabelStruct &ls, wxDC & dc, int * xPos1, int cursorPos)
707 {
708    *xPos1 = ls.xText;
709    if( cursorPos > 0)
710    {
711       int partWidth;
712       // Calculate the width of the substring and add it to Xpos
713       dc.GetTextExtent(ls.title.Left(cursorPos), &partWidth, NULL);
714       *xPos1 += partWidth;
715    }
716 }
717 }
718 
CalcCursorX(AudacityProject & project,int * x) const719 bool LabelTrackView::CalcCursorX( AudacityProject &project, int * x) const
720 {
721    if (IsValidIndex(mTextEditIndex, project)) {
722       wxMemoryDC dc;
723 
724       if (msFont.Ok()) {
725          dc.SetFont(msFont);
726       }
727 
728       const auto pTrack = FindLabelTrack();
729       const auto &mLabels = pTrack->GetLabels();
730 
731       getXPos(mLabels[mTextEditIndex], dc, x, mCurrentCursorPos);
732       *x += mIconWidth / 2;
733       return true;
734    }
735 
736    return false;
737 }
738 
CalcHighlightXs(int * x1,int * x2) const739 void LabelTrackView::CalcHighlightXs(int *x1, int *x2) const
740 {
741    wxMemoryDC dc;
742 
743    if (msFont.Ok()) {
744       dc.SetFont(msFont);
745    }
746 
747    int pos1 = mInitialCursorPos, pos2 = mCurrentCursorPos;
748    if (pos1 > pos2)
749       std::swap(pos1, pos2);
750 
751    const auto pTrack = FindLabelTrack();
752    const auto &mLabels = pTrack->GetLabels();
753    const auto &labelStruct = mLabels[mTextEditIndex];
754 
755    // find the left X pos of highlighted area
756    getXPos(labelStruct, dc, x1, pos1);
757    // find the right X pos of highlighted area
758    getXPos(labelStruct, dc, x2, pos2);
759 }
760 
761 #include "LabelGlyphHandle.h"
762 namespace {
findHit(TrackPanel * pPanel)763    LabelTrackHit *findHit( TrackPanel *pPanel )
764    {
765       if (! pPanel )
766          return nullptr;
767 
768       // Fetch the highlighting state
769       auto target = pPanel->Target();
770       if (target) {
771          auto handle = dynamic_cast<LabelGlyphHandle*>( target.get() );
772          if (handle)
773             return &*handle->mpHit;
774       }
775       return nullptr;
776    }
777 }
778 
779 #include "../../../TrackPanelDrawingContext.h"
780 #include "LabelTextHandle.h"
781 
782 /// Draw calls other functions to draw the LabelTrack.
783 ///   @param  dc the device context
784 ///   @param  r  the LabelTrack rectangle.
Draw(TrackPanelDrawingContext & context,const wxRect & r) const785 void LabelTrackView::Draw
786 ( TrackPanelDrawingContext &context, const wxRect & r ) const
787 {
788    auto &dc = context.dc;
789    const auto artist = TrackArtist::Get( context );
790    const auto &zoomInfo = *artist->pZoomInfo;
791 
792    auto pHit = findHit( artist->parent );
793 
794    if(msFont.Ok())
795       dc.SetFont(msFont);
796 
797    if (mFontHeight == -1)
798       calculateFontHeight(dc);
799 
800    const auto pTrack = std::static_pointer_cast< const LabelTrack >(
801       FindTrack()->SubstitutePendingChangedTrack());
802    const auto &mLabels = pTrack->GetLabels();
803 
804    TrackArt::DrawBackgroundWithSelection( context, r, pTrack.get(),
805       AColor::labelSelectedBrush, AColor::labelUnselectedBrush,
806       ( pTrack->GetSelected() || pTrack->IsSyncLockSelected() ) );
807 
808    wxCoord textWidth, textHeight;
809 
810    // Get the text widths.
811    // TODO: Make more efficient by only re-computing when a
812    // text label title changes.
813    for (const auto &labelStruct : mLabels) {
814       dc.GetTextExtent(labelStruct.title, &textWidth, &textHeight);
815       labelStruct.width = textWidth;
816    }
817 
818    // TODO: And this only needs to be done once, but we
819    // do need the dc to do it.
820    // We need to set mTextHeight to something sensible,
821    // guarding against the case where there are no
822    // labels or all are empty strings, which for example
823    // happens with a NEW label track.
824    mTextHeight = dc.GetFontMetrics().ascent + dc.GetFontMetrics().descent;
825    const int yFrameHeight = mTextHeight + TextFramePadding * 2;
826 
827    ComputeLayout( r, zoomInfo );
828    dc.SetTextForeground(theTheme.Colour( clrLabelTrackText));
829    dc.SetBackgroundMode(wxTRANSPARENT);
830    dc.SetBrush(AColor::labelTextNormalBrush);
831    dc.SetPen(AColor::labelSurroundPen);
832    int GlyphLeft;
833    int GlyphRight;
834    // Now we draw the various items in this order,
835    // so that the correct things overpaint each other.
836 
837    // Draw vertical lines that show where the end positions are.
838    for (const auto &labelStruct : mLabels)
839       DrawLines( dc, labelStruct, r );
840 
841    // Draw the end glyphs.
842    { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
843       GlyphLeft=0;
844       GlyphRight=1;
845       if( pHit && i == pHit->mMouseOverLabelLeft )
846          GlyphLeft = (pHit->mEdge & 4) ? 6:9;
847       if( pHit && i == pHit->mMouseOverLabelRight )
848          GlyphRight = (pHit->mEdge & 4) ? 7:4;
849       DrawGlyphs( dc, labelStruct, r, GlyphLeft, GlyphRight );
850    }}
851 
852    auto &project = *artist->parent->GetProject();
853 
854    // Draw the label boxes.
855    {
856 #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
857       bool highlightTrack = false;
858       auto target = dynamic_cast<LabelTextHandle*>(context.target.get());
859       highlightTrack = target && target->GetTrack().get() == this;
860 #endif
861       int i = -1; for (const auto &labelStruct : mLabels) { ++i;
862          bool highlight = false;
863 #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
864          highlight = highlightTrack && target->GetLabelNum() == i;
865 #endif
866 
867          dc.SetBrush(mNavigationIndex == i || (pHit && pHit->mMouseOverLabel == i)
868             ? AColor::labelTextEditBrush : AColor::labelTextNormalBrush);
869          DrawBar(dc, labelStruct, r);
870 
871          bool selected = mTextEditIndex == i;
872 
873          if (selected)
874             dc.SetBrush(AColor::labelTextEditBrush);
875          else if (highlight)
876             dc.SetBrush(AColor::uglyBrush);
877          else
878             dc.SetBrush(AColor::labelTextNormalBrush);
879          DrawTextBox(dc, labelStruct, r);
880 
881          dc.SetBrush(AColor::labelTextNormalBrush);
882       }
883    }
884 
885    // Draw highlights
886    if ( (mInitialCursorPos != mCurrentCursorPos) && IsValidIndex(mTextEditIndex, project))
887    {
888       int xpos1, xpos2;
889       CalcHighlightXs(&xpos1, &xpos2);
890       DrawHighlight(dc, mLabels[mTextEditIndex],
891          xpos1, xpos2, dc.GetFontMetrics().ascent + dc.GetFontMetrics().descent);
892    }
893 
894    // Draw the text and the label boxes.
895    { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
896       if(mTextEditIndex == i )
897          dc.SetBrush(AColor::labelTextEditBrush);
898       DrawText( dc, labelStruct, r );
899       if(mTextEditIndex == i )
900          dc.SetBrush(AColor::labelTextNormalBrush);
901    }}
902 
903    // Draw the cursor, if there is one.
904    if(mInitialCursorPos == mCurrentCursorPos && IsValidIndex(mTextEditIndex, project))
905    {
906       const auto &labelStruct = mLabels[mTextEditIndex];
907       int xPos = labelStruct.xText;
908 
909       if( mCurrentCursorPos > 0)
910       {
911          // Calculate the width of the substring and add it to Xpos
912          int partWidth;
913          dc.GetTextExtent(labelStruct.title.Left(mCurrentCursorPos), &partWidth, NULL);
914          xPos += partWidth;
915       }
916 
917       wxPen currentPen = dc.GetPen();
918       const int CursorWidth=2;
919       currentPen.SetWidth(CursorWidth);
920       const auto top = labelStruct.y - (LabelBarHeight + yFrameHeight) / 2 + (yFrameHeight - mFontHeight) / 2 + TextFrameYOffset;
921       AColor::Line(dc,
922                    xPos-1, top,
923                    xPos-1, top + mFontHeight);
924       currentPen.SetWidth(1);
925    }
926 }
927 
Draw(TrackPanelDrawingContext & context,const wxRect & rect,unsigned iPass)928 void LabelTrackView::Draw(
929    TrackPanelDrawingContext &context,
930    const wxRect &rect, unsigned iPass )
931 {
932    if ( iPass == TrackArtist::PassTracks )
933       Draw( context, rect );
934    CommonTrackView::Draw( context, rect, iPass );
935 }
936 
937 /// uses GetTextExtent to find the character position
938 /// corresponding to the x pixel position.
FindCursorPosition(int labelIndex,wxCoord xPos)939 int LabelTrackView::FindCursorPosition(int labelIndex, wxCoord xPos)
940 {
941    int result = -1;
942    wxMemoryDC dc;
943    if(msFont.Ok())
944       dc.SetFont(msFont);
945 
946    // A bool indicator to see if set the cursor position or not
947    bool finished = false;
948    int charIndex = 1;
949    int partWidth;
950    int oneWidth;
951    double bound;
952    wxString subString;
953 
954    const auto pTrack = FindLabelTrack();
955    const auto &mLabels = pTrack->GetLabels();
956    const auto &labelStruct = mLabels[labelIndex];
957    const auto &title = labelStruct.title;
958    const int length = title.length();
959    while (!finished && (charIndex < length + 1))
960    {
961       int unichar = (int)title.at( charIndex-1 );
962       if( (0xDC00 <= unichar) && (unichar <= 0xDFFF)){
963          charIndex++;
964          continue;
965       }
966       subString = title.Left(charIndex);
967       // Get the width of substring
968       dc.GetTextExtent(subString, &partWidth, NULL);
969 
970       // Get the width of the last character
971       dc.GetTextExtent(subString.Right(1), &oneWidth, NULL);
972       bound = labelStruct.xText + partWidth - oneWidth * 0.5;
973 
974       if (xPos <= bound)
975       {
976          // Found
977          result = charIndex - 1;
978          finished = true;
979       }
980       else
981       {
982          // Advance
983          charIndex++;
984       }
985    }
986    if (!finished)
987       // Cursor should be in the last position
988       result = length;
989 
990    return result;
991 }
992 
SetCurrentCursorPosition(int pos)993 void LabelTrackView::SetCurrentCursorPosition(int pos)
994 {
995    mCurrentCursorPos = pos;
996 }
SetTextSelection(int labelIndex,int start,int end)997 void LabelTrackView::SetTextSelection(int labelIndex, int start, int end)
998 {
999     mTextEditIndex = labelIndex;
1000     mInitialCursorPos = start;
1001     mCurrentCursorPos = end;
1002 }
GetTextEditIndex(AudacityProject & project) const1003 int LabelTrackView::GetTextEditIndex(AudacityProject& project) const
1004 {
1005     if (IsValidIndex(mTextEditIndex, project))
1006         return mTextEditIndex;
1007     return -1;
1008 }
ResetTextSelection()1009 void LabelTrackView::ResetTextSelection()
1010 {
1011     mTextEditIndex = -1;
1012     mCurrentCursorPos = 1;
1013     mInitialCursorPos = 1;
1014 }
SetNavigationIndex(int index)1015 void LabelTrackView::SetNavigationIndex(int index)
1016 {
1017     mNavigationIndex = index;
1018 }
GetNavigationIndex(AudacityProject & project) const1019 int LabelTrackView::GetNavigationIndex(AudacityProject& project) const
1020 {
1021     if (IsValidIndex(mNavigationIndex, project))
1022         return mNavigationIndex;
1023     return -1;
1024 }
1025 
calculateFontHeight(wxDC & dc)1026 void LabelTrackView::calculateFontHeight(wxDC & dc)
1027 {
1028    int charDescent;
1029    int charLeading;
1030 
1031    // Calculate the width of the substring and add it to Xpos
1032    dc.GetTextExtent(wxT("(Test String)|[yp]"), NULL, &mFontHeight, &charDescent, &charLeading);
1033 
1034    // The cursor will have height charHeight.  We don't include the descender as
1035    // part of the height because for phonetic fonts this leads to cursors which are
1036    // too tall.  We don't include leading either - it is usually 0.
1037    // To make up for ignoring the descender height, we add one pixel above and below
1038    // using CursorExtraHeight so that the cursor is just a little taller than the
1039    // body of the characters.
1040    const int CursorExtraHeight=2;
1041    mFontHeight += CursorExtraHeight - (charLeading+charDescent);
1042 }
1043 
IsValidIndex(const Index & index,AudacityProject & project) const1044 bool LabelTrackView::IsValidIndex(const Index& index, AudacityProject& project) const
1045 {
1046     if (index == -1)
1047        return false;
1048     // may make delayed update of mutable mSelIndex after track selection change
1049     auto track = FindLabelTrack();
1050     if (track->GetSelected() || (TrackFocus::Get(project).Get() == track.get()))
1051        return index >= 0 && index < static_cast<int>(track->GetLabels().size());
1052     return false;
1053 }
1054 
IsTextSelected(AudacityProject & project) const1055 bool LabelTrackView::IsTextSelected( AudacityProject &project ) const
1056 {
1057    return mCurrentCursorPos != mInitialCursorPos && IsValidIndex(mTextEditIndex, project);
1058 }
1059 
1060 /// Cut the selected text in the text box
1061 ///  @return true if text is selected in text box, false otherwise
CutSelectedText(AudacityProject & project)1062 bool LabelTrackView::CutSelectedText( AudacityProject &project )
1063 {
1064    if (!IsTextSelected( project ))
1065       return false;
1066 
1067    const auto pTrack = FindLabelTrack();
1068    const auto &mLabels = pTrack->GetLabels();
1069 
1070    wxString left, right;
1071    auto labelStruct = mLabels[mTextEditIndex];
1072    auto &text = labelStruct.title;
1073 
1074    if (!mTextEditIndex.IsModified()) {
1075       mUndoLabel = text;
1076    }
1077 
1078    int init = mInitialCursorPos;
1079    int cur = mCurrentCursorPos;
1080    if (init > cur)
1081       std::swap(init, cur);
1082 
1083    // data for cutting
1084    wxString data = text.Mid(init, cur - init);
1085 
1086    // get left-remaining text
1087    if (init > 0)
1088       left = text.Left(init);
1089 
1090    // get right-remaining text
1091    if (cur < (int)text.length())
1092       right = text.Mid(cur);
1093 
1094    // set title to the combination of the two remainders
1095    text = left + right;
1096 
1097    pTrack->SetLabel( mTextEditIndex, labelStruct );
1098 
1099    // copy data onto clipboard
1100    if (wxTheClipboard->Open()) {
1101       // Clipboard owns the data you give it
1102       wxTheClipboard->SetData(safenew wxTextDataObject(data));
1103       wxTheClipboard->Close();
1104    }
1105 
1106    // set cursor positions
1107    mInitialCursorPos = mCurrentCursorPos = left.length();
1108 
1109    mTextEditIndex.SetModified(true);
1110    return true;
1111 }
1112 
1113 /// Copy the selected text in the text box
1114 ///  @return true if text is selected in text box, false otherwise
CopySelectedText(AudacityProject & project)1115 bool LabelTrackView::CopySelectedText( AudacityProject &project )
1116 {
1117    if (!IsTextSelected(project))
1118       return false;
1119 
1120    const auto pTrack = FindLabelTrack();
1121    const auto &mLabels = pTrack->GetLabels();
1122 
1123    const auto &labelStruct = mLabels[mTextEditIndex];
1124 
1125    int init = mInitialCursorPos;
1126    int cur = mCurrentCursorPos;
1127    if (init > cur)
1128       std::swap(init, cur);
1129 
1130    if (init == cur)
1131       return false;
1132 
1133    // data for copying
1134    wxString data = labelStruct.title.Mid(init, cur-init);
1135 
1136    // copy the data on clipboard
1137    if (wxTheClipboard->Open()) {
1138       // Clipboard owns the data you give it
1139       wxTheClipboard->SetData(safenew wxTextDataObject(data));
1140       wxTheClipboard->Close();
1141    }
1142 
1143    return true;
1144 }
1145 
1146 // PRL:  should this set other fields of the label selection?
1147 /// Paste the text on the clipboard to text box
1148 ///  @return true if mouse is clicked in text box, false otherwise
PasteSelectedText(AudacityProject & project,double sel0,double sel1)1149 bool LabelTrackView::PasteSelectedText(
1150    AudacityProject &project, double sel0, double sel1 )
1151 {
1152    const auto pTrack = FindLabelTrack();
1153 
1154    if (!IsValidIndex(mTextEditIndex, project))
1155       SetTextSelection(AddLabel(SelectedRegion(sel0, sel1)));
1156 
1157    wxString text, left, right;
1158 
1159    // if text data is available
1160    if (IsTextClipSupported()) {
1161       if (wxTheClipboard->Open()) {
1162          wxTextDataObject data;
1163          wxTheClipboard->GetData(data);
1164          wxTheClipboard->Close();
1165          text = data.GetText();
1166       }
1167 
1168       if (!mTextEditIndex.IsModified()) {
1169          mUndoLabel = text;
1170       }
1171 
1172       // Convert control characters to blanks
1173       for (int i = 0; i < (int)text.length(); i++) {
1174          if (wxIscntrl(text[i])) {
1175             text[i] = wxT(' ');
1176          }
1177       }
1178    }
1179 
1180    const auto &mLabels = pTrack->GetLabels();
1181    auto labelStruct = mLabels[mTextEditIndex];
1182    auto &title = labelStruct.title;
1183    int cur = mCurrentCursorPos, init = mInitialCursorPos;
1184    if (init > cur)
1185       std::swap(init, cur);
1186    left = title.Left(init);
1187    if (cur < (int)title.length())
1188       right = title.Mid(cur);
1189 
1190    title = left + text + right;
1191 
1192    pTrack->SetLabel(mTextEditIndex, labelStruct );
1193 
1194    mInitialCursorPos =  mCurrentCursorPos = left.length() + text.length();
1195 
1196    mTextEditIndex.SetModified(true);
1197    return true;
1198 }
1199 
SelectAllText(AudacityProject & project)1200 bool LabelTrackView::SelectAllText(AudacityProject& project)
1201 {
1202     if (!IsValidIndex(mTextEditIndex, project))
1203         return false;
1204 
1205     const auto pTrack = FindLabelTrack();
1206 
1207     const auto& mLabels = pTrack->GetLabels();
1208     auto labelStruct = mLabels[mTextEditIndex];
1209     auto& title = labelStruct.title;
1210 
1211     mInitialCursorPos = 0;
1212     mCurrentCursorPos = title.Length();
1213 
1214     return true;
1215 }
1216 
1217 /// @return true if the text data is available in the clipboard, false otherwise
IsTextClipSupported()1218 bool LabelTrackView::IsTextClipSupported()
1219 {
1220    return wxTheClipboard->IsSupported(wxDF_UNICODETEXT);
1221 }
1222 
1223 /// TODO: Investigate what happens with large
1224 /// numbers of labels, might need a binary search
1225 /// rather than a linear one.
OverGlyph(const LabelTrack & track,LabelTrackHit & hit,int x,int y)1226 void LabelTrackView::OverGlyph(
1227    const LabelTrack &track, LabelTrackHit &hit, int x, int y)
1228 {
1229    //Determine the NEW selection.
1230    int result=0;
1231    const int d1=10; //distance in pixels, used for have we hit drag handle.
1232    const int d2=5;  //distance in pixels, used for have we hit drag handle center.
1233 
1234    //If not over a label, reset it
1235    hit.mMouseOverLabelLeft  = -1;
1236    hit.mMouseOverLabelRight = -1;
1237    hit.mMouseOverLabel = -1;
1238    hit.mEdge = 0;
1239 
1240    const auto pTrack = &track;
1241    const auto &mLabels = pTrack->GetLabels();
1242    { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
1243       // give text box better priority for selecting
1244       // reset selection state
1245       if (OverTextBox(&labelStruct, x, y))
1246       {
1247          result = 0;
1248          hit.mMouseOverLabel = -1;
1249          hit.mMouseOverLabelLeft = -1;
1250          hit.mMouseOverLabelRight = -1;
1251          break;
1252       }
1253 
1254       //over left or right selection bound
1255       //Check right bound first, since it is drawn after left bound,
1256       //so give it precedence for matching/highlighting.
1257       if( abs(labelStruct.y - (y - (mTextHeight+3)/2)) < d1 &&
1258                abs(labelStruct.x1 - d2 -x) < d1)
1259       {
1260          hit.mMouseOverLabelRight = i;
1261          if(abs(labelStruct.x1 - x) < d2 )
1262          {
1263             result |= 4;
1264             // If left and right co-incident at this resolution, then we drag both.
1265             // We were more stringent about co-incidence here in the past.
1266             if( abs(labelStruct.x1-labelStruct.x) < 5.0 )
1267             {
1268                result |=1;
1269                hit.mMouseOverLabelLeft = i;
1270             }
1271          }
1272          result |= 2;
1273       }
1274       // Use else-if here rather than else to avoid detecting left and right
1275       // of the same label.
1276       else if(   abs(labelStruct.y - (y - (mTextHeight+3)/2)) < d1 &&
1277             abs(labelStruct.x + d2 - x) < d1 )
1278       {
1279          hit.mMouseOverLabelLeft = i;
1280          if(abs(labelStruct.x - x) < d2 )
1281             result |= 4;
1282          result |= 1;
1283       }
1284       else if (x >= labelStruct.x && x <= labelStruct.x1 &&
1285          abs(y - (labelStruct.y + mTextHeight / 2)) < d1)
1286       {
1287          hit.mMouseOverLabel = i;
1288          result = 3;
1289       }
1290    }}
1291    hit.mEdge = result;
1292 }
1293 
OverATextBox(const LabelTrack & track,int xx,int yy)1294 int LabelTrackView::OverATextBox( const LabelTrack &track, int xx, int yy )
1295 {
1296    const auto pTrack = &track;
1297    const auto &mLabels = pTrack->GetLabels();
1298    for (int nn = (int)mLabels.size(); nn--;) {
1299       const auto &labelStruct = mLabels[nn];
1300       if ( OverTextBox( &labelStruct, xx, yy ) )
1301          return nn;
1302    }
1303 
1304    return -1;
1305 }
1306 
1307 // return true if the mouse is over text box, false otherwise
OverTextBox(const LabelStruct * pLabel,int x,int y)1308 bool LabelTrackView::OverTextBox(const LabelStruct *pLabel, int x, int y)
1309 {
1310    if( (pLabel->xText-(mIconWidth/2) < x) &&
1311             (x<pLabel->xText+pLabel->width+(mIconWidth/2)) &&
1312             (abs(pLabel->y-y)<mIconHeight/2))
1313    {
1314       return true;
1315    }
1316    return false;
1317 }
1318 
1319 /// Returns true for keys we capture to start a label.
IsGoodLabelFirstKey(const wxKeyEvent & evt)1320 static bool IsGoodLabelFirstKey(const wxKeyEvent & evt)
1321 {
1322    int keyCode = evt.GetKeyCode();
1323    return (keyCode < WXK_START
1324                   && keyCode != WXK_SPACE && keyCode != WXK_DELETE && keyCode != WXK_RETURN) ||
1325           (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) ||
1326           (keyCode >= WXK_NUMPAD_EQUAL && keyCode <= WXK_NUMPAD_DIVIDE) ||
1327 #if defined(__WXMAC__)
1328           (keyCode > WXK_RAW_CONTROL) ||
1329 #endif
1330           (keyCode > WXK_WINDOWS_MENU);
1331 }
1332 
1333 /// This returns true for keys we capture for label editing.
IsGoodLabelEditKey(const wxKeyEvent & evt)1334 static bool IsGoodLabelEditKey(const wxKeyEvent & evt)
1335 {
1336    int keyCode = evt.GetKeyCode();
1337 
1338    // Accept everything outside of WXK_START through WXK_COMMAND, plus the keys
1339    // within that range that are usually printable, plus the ones we use for
1340    // keyboard navigation.
1341    return keyCode < WXK_START ||
1342           (keyCode >= WXK_END && keyCode < WXK_UP) ||
1343           (keyCode == WXK_RIGHT) ||
1344           (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) ||
1345           (keyCode >= WXK_NUMPAD_SPACE && keyCode <= WXK_NUMPAD_ENTER) ||
1346           (keyCode >= WXK_NUMPAD_HOME && keyCode <= WXK_NUMPAD_END) ||
1347           (keyCode >= WXK_NUMPAD_DELETE && keyCode <= WXK_NUMPAD_DIVIDE) ||
1348 #if defined(__WXMAC__)
1349           (keyCode > WXK_RAW_CONTROL) ||
1350 #endif
1351           (keyCode > WXK_WINDOWS_MENU);
1352 }
1353 
1354 // Check for keys that we will process
DoCaptureKey(AudacityProject & project,wxKeyEvent & event)1355 bool LabelTrackView::DoCaptureKey(
1356    AudacityProject &project, wxKeyEvent & event )
1357 {
1358    int mods = event.GetModifiers();
1359    auto code = event.GetKeyCode();
1360    const auto pTrack = FindLabelTrack();
1361    const auto& mLabels = pTrack->GetLabels();
1362 
1363    // Allow hardcoded Ctrl+F2 for renaming the selected label,
1364    // if we have any labels
1365    if (code == WXK_F2 && mods == wxMOD_CONTROL && !mLabels.empty()) {
1366       return true;
1367    }
1368 
1369    // Check for modifiers and only allow shift
1370    if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
1371       return false;
1372    }
1373 
1374    // Always capture the navigation keys, if we have any labels
1375    if ((code == WXK_TAB || code == WXK_NUMPAD_TAB) &&
1376        !mLabels.empty())
1377       return true;
1378 
1379    if (IsValidIndex(mTextEditIndex, project)) {
1380       if (IsGoodLabelEditKey(event)) {
1381          return true;
1382       }
1383    }
1384    else {
1385       bool typeToCreateLabel;
1386       gPrefs->Read(wxT("/GUI/TypeToCreateLabel"), &typeToCreateLabel, false);
1387       if (IsGoodLabelFirstKey(event) && typeToCreateLabel) {
1388 
1389 
1390 // The commented out code can prevent label creation, causing bug 1551
1391 // We should only be in DoCaptureKey IF this label track has focus,
1392 // and in that case creating a Label is the expected/intended thing.
1393 #if 0
1394          // If we're playing, don't capture if the selection is the same as the
1395          // playback region (this helps prevent label track creation from
1396          // stealing unmodified kbd. shortcuts)
1397          auto gAudioIO = AudioIOBase::Get();
1398          if (pProj->GetAudioIOToken() > 0 &&
1399                gAudioIO->IsStreamActive(pProj->GetAudioIOToken()))
1400          {
1401             double t0, t1;
1402             pProj->GetPlayRegion(&t0, &t1);
1403             if (pProj->mViewInfo.selectedRegion.t0() == t0 &&
1404                 pProj->mViewInfo.selectedRegion.t1() == t1) {
1405                return false;
1406             }
1407          }
1408 #endif
1409 
1410          // If there's a label there already don't capture
1411          auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
1412          if( GetLabelIndex(selectedRegion.t0(),
1413                            selectedRegion.t1()) != wxNOT_FOUND ) {
1414             return false;
1415          }
1416 
1417          return true;
1418       }
1419    }
1420 
1421    return false;
1422 }
1423 
CaptureKey(wxKeyEvent & event,ViewInfo &,wxWindow *,AudacityProject * project)1424 unsigned LabelTrackView::CaptureKey(
1425    wxKeyEvent & event, ViewInfo &, wxWindow *, AudacityProject *project )
1426 {
1427    event.Skip(!DoCaptureKey( *project, event ));
1428    return RefreshCode::RefreshNone;
1429 }
1430 
KeyDown(wxKeyEvent & event,ViewInfo & viewInfo,wxWindow * WXUNUSED (pParent),AudacityProject * project)1431 unsigned LabelTrackView::KeyDown(
1432    wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *WXUNUSED(pParent),
1433    AudacityProject *project)
1434 {
1435    double bkpSel0 = viewInfo.selectedRegion.t0(),
1436       bkpSel1 = viewInfo.selectedRegion.t1();
1437 
1438    if (IsValidIndex(mTextEditIndex, *project) && !mTextEditIndex.IsModified()) {
1439       const auto pTrack = FindLabelTrack();
1440       const auto &mLabels = pTrack->GetLabels();
1441       auto labelStruct = mLabels[mTextEditIndex];
1442       auto &title = labelStruct.title;
1443       mUndoLabel = title;
1444    }
1445 
1446    // Pass keystroke to labeltrack's handler and add to history if any
1447    // updates were done
1448    if (DoKeyDown( *project, viewInfo.selectedRegion, event )) {
1449       ProjectHistory::Get( *project ).PushState(XO("Modified Label"),
1450          XO("Label Edit"),
1451          mTextEditIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
1452 
1453       mTextEditIndex.SetModified(true);
1454    }
1455 
1456    if (!mTextEditIndex.IsModified()) {
1457       mUndoLabel.clear();
1458    }
1459 
1460    // Make sure caret is in view
1461    int x;
1462    if (CalcCursorX( *project, &x ))
1463       ProjectWindow::Get( *project ).ScrollIntoView(x);
1464 
1465    // If selection modified, refresh
1466    // Otherwise, refresh track display if the keystroke was handled
1467    if (bkpSel0 != viewInfo.selectedRegion.t0() ||
1468       bkpSel1 != viewInfo.selectedRegion.t1())
1469       return RefreshCode::RefreshAll;
1470    else if (!event.GetSkipped())
1471       return  RefreshCode::RefreshCell;
1472 
1473    return RefreshCode::RefreshNone;
1474 }
1475 
Char(wxKeyEvent & event,ViewInfo & viewInfo,wxWindow *,AudacityProject * project)1476 unsigned LabelTrackView::Char(
1477    wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *, AudacityProject *project)
1478 {
1479    double bkpSel0 = viewInfo.selectedRegion.t0(),
1480       bkpSel1 = viewInfo.selectedRegion.t1();
1481    // Pass keystroke to labeltrack's handler and add to history if any
1482    // updates were done
1483 
1484    if (IsValidIndex(mTextEditIndex, *project) && !mTextEditIndex.IsModified()) {
1485       const auto pTrack = FindLabelTrack();
1486       const auto &mLabels = pTrack->GetLabels();
1487       auto labelStruct = mLabels[mTextEditIndex];
1488       auto &title = labelStruct.title;
1489       mUndoLabel = title;
1490    }
1491 
1492    if (DoChar( *project, viewInfo.selectedRegion, event )) {
1493       ProjectHistory::Get( *project ).PushState(XO("Modified Label"),
1494          XO("Label Edit"),
1495           mTextEditIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
1496 
1497       mTextEditIndex.SetModified(true);
1498    }
1499 
1500    if (!mTextEditIndex.IsModified()) {
1501       mUndoLabel.clear();
1502    }
1503 
1504    // If selection modified, refresh
1505    // Otherwise, refresh track display if the keystroke was handled
1506    if (bkpSel0 != viewInfo.selectedRegion.t0() ||
1507       bkpSel1 != viewInfo.selectedRegion.t1())
1508       return RefreshCode::RefreshAll;
1509    else if (!event.GetSkipped())
1510       return RefreshCode::RefreshCell;
1511 
1512    return RefreshCode::RefreshNone;
1513 }
1514 
1515 /// KeyEvent is called for every keypress when over the label track.
DoKeyDown(AudacityProject & project,NotifyingSelectedRegion & newSel,wxKeyEvent & event)1516 bool LabelTrackView::DoKeyDown(
1517    AudacityProject &project, NotifyingSelectedRegion &newSel, wxKeyEvent & event)
1518 {
1519    // Only track true changes to the label
1520    bool updated = false;
1521 
1522    // Cache the keycode
1523    int keyCode = event.GetKeyCode();
1524    const int mods = event.GetModifiers();
1525 
1526    // Check for modifiers and only allow shift
1527    // except in the case of Ctrl + F2, so hardcoded Ctrl+F2 can
1528    // be used for renaming a label
1529    if ((keyCode != WXK_F2 && mods != wxMOD_NONE && mods != wxMOD_SHIFT)
1530       || (keyCode == WXK_F2 && mods != wxMOD_CONTROL)) {
1531       event.Skip();
1532       return updated;
1533    }
1534 
1535    // All editing keys are only active if we're currently editing a label
1536    const auto pTrack = FindLabelTrack();
1537    const auto &mLabels = pTrack->GetLabels();
1538    if (IsValidIndex(mTextEditIndex, project)) {
1539       // Do label text changes
1540       auto labelStruct = mLabels[mTextEditIndex];
1541       auto &title = labelStruct.title;
1542       wxUniChar wchar;
1543       bool more=true;
1544 
1545       switch (keyCode) {
1546 
1547       case WXK_BACK:
1548          {
1549             int len = title.length();
1550 
1551             //IF the label is not blank THEN get rid of a letter or letters according to cursor position
1552             if (len > 0)
1553             {
1554                // IF there are some highlighted letters, THEN DELETE them
1555                if (mInitialCursorPos != mCurrentCursorPos)
1556                   RemoveSelectedText();
1557                else
1558                {
1559                   // DELETE one codepoint leftwards
1560                   while ((mCurrentCursorPos > 0) && more) {
1561                      wchar = title.at( mCurrentCursorPos-1 );
1562                      title.erase(mCurrentCursorPos-1, 1);
1563                      mCurrentCursorPos--;
1564                      if( ((int)wchar > 0xDFFF) || ((int)wchar <0xDC00)){
1565                         pTrack->SetLabel(mTextEditIndex, labelStruct);
1566                         more = false;
1567                      }
1568                   }
1569                }
1570             }
1571             else
1572             {
1573                // ELSE no text in text box, so DELETE whole label.
1574                pTrack->DeleteLabel(mTextEditIndex);
1575                ResetTextSelection();
1576             }
1577             mInitialCursorPos = mCurrentCursorPos;
1578             updated = true;
1579          }
1580          break;
1581 
1582       case WXK_DELETE:
1583       case WXK_NUMPAD_DELETE:
1584          {
1585             int len = title.length();
1586 
1587             //If the label is not blank get rid of a letter according to cursor position
1588             if (len > 0)
1589             {
1590                // if there are some highlighted letters, DELETE them
1591                if (mInitialCursorPos != mCurrentCursorPos)
1592                   RemoveSelectedText();
1593                else
1594                {
1595                   // DELETE one codepoint rightwards
1596                   while ((mCurrentCursorPos < len) && more) {
1597                      wchar = title.at( mCurrentCursorPos );
1598                      title.erase(mCurrentCursorPos, 1);
1599                      if( ((int)wchar > 0xDBFF) || ((int)wchar <0xD800)){
1600                         pTrack->SetLabel(mTextEditIndex, labelStruct);
1601                         more = false;
1602                      }
1603                   }
1604                }
1605             }
1606             else
1607             {
1608                // DELETE whole label if no text in text box
1609                pTrack->DeleteLabel(mTextEditIndex);
1610                ResetTextSelection();
1611             }
1612             mInitialCursorPos = mCurrentCursorPos;
1613             updated = true;
1614          }
1615          break;
1616 
1617       case WXK_HOME:
1618       case WXK_NUMPAD_HOME:
1619          // Move cursor to beginning of label
1620          mCurrentCursorPos = 0;
1621          if (mods == wxMOD_SHIFT)
1622             ;
1623          else
1624             mInitialCursorPos = mCurrentCursorPos;
1625          break;
1626 
1627       case WXK_END:
1628       case WXK_NUMPAD_END:
1629          // Move cursor to end of label
1630          mCurrentCursorPos = (int)title.length();
1631          if (mods == wxMOD_SHIFT)
1632             ;
1633          else
1634             mInitialCursorPos = mCurrentCursorPos;
1635          break;
1636 
1637       case WXK_LEFT:
1638       case WXK_NUMPAD_LEFT:
1639          // Moving cursor left
1640          if (mods != wxMOD_SHIFT && mCurrentCursorPos != mInitialCursorPos)
1641             //put cursor to the left edge of selection
1642             mInitialCursorPos = mCurrentCursorPos =
1643                std::min(mInitialCursorPos, mCurrentCursorPos);
1644          else
1645          {
1646             while ((mCurrentCursorPos > 0) && more) {
1647                wchar = title.at(mCurrentCursorPos - 1);
1648                more = !(((int)wchar > 0xDFFF) || ((int)wchar < 0xDC00));
1649 
1650                --mCurrentCursorPos;
1651             }
1652             if (mods != wxMOD_SHIFT)
1653                mInitialCursorPos = mCurrentCursorPos;
1654          }
1655 
1656          break;
1657 
1658       case WXK_RIGHT:
1659       case WXK_NUMPAD_RIGHT:
1660          // Moving cursor right
1661          if(mods != wxMOD_SHIFT && mCurrentCursorPos != mInitialCursorPos)
1662             //put cursor to the right edge of selection
1663             mInitialCursorPos = mCurrentCursorPos =
1664                std::max(mInitialCursorPos, mCurrentCursorPos);
1665          else
1666          {
1667             while ((mCurrentCursorPos < (int)title.length()) && more) {
1668                wchar = title.at(mCurrentCursorPos);
1669                more = !(((int)wchar > 0xDBFF) || ((int)wchar < 0xD800));
1670 
1671                ++mCurrentCursorPos;
1672             }
1673             if (mods != wxMOD_SHIFT)
1674                mInitialCursorPos = mCurrentCursorPos;
1675          }
1676          break;
1677 
1678       case WXK_ESCAPE:
1679          if (mTextEditIndex.IsModified()) {
1680             title = mUndoLabel;
1681             pTrack->SetLabel(mTextEditIndex, labelStruct);
1682 
1683             ProjectHistory::Get( project ).PushState(XO("Modified Label"),
1684                XO("Label Edit"),
1685                mTextEditIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
1686          }
1687 
1688       case WXK_RETURN:
1689       case WXK_NUMPAD_ENTER:
1690       case WXK_TAB:
1691          if (mRestoreFocus >= 0) {
1692             auto track = *TrackList::Get( project ).Any()
1693                .begin().advance(mRestoreFocus);
1694             if (track)
1695                TrackFocus::Get( project ).Set(track);
1696             mRestoreFocus = -2;
1697          }
1698          SetNavigationIndex(mTextEditIndex);
1699          ResetTextSelection();
1700          break;
1701       case '\x10':   // OSX
1702       case WXK_MENU:
1703       case WXK_WINDOWS_MENU:
1704          ShowContextMenu( project );
1705          break;
1706 
1707       default:
1708          if (!IsGoodLabelEditKey(event)) {
1709             event.Skip();
1710          }
1711          break;
1712       }
1713    }
1714    else
1715    {
1716       // Do navigation
1717       switch (keyCode) {
1718 
1719       case WXK_ESCAPE:
1720           mNavigationIndex = -1;
1721           break;
1722       case WXK_TAB:
1723       case WXK_NUMPAD_TAB:
1724          if (!mLabels.empty()) {
1725             int len = (int) mLabels.size();
1726             // The case where the start of selection is the same as the
1727             // start of a label is handled separately so that if some labels
1728             // have the same start time, all labels are navigated.
1729             if (IsValidIndex(mNavigationIndex, project)
1730                && mLabels[mNavigationIndex].getT0() == newSel.t0())
1731             {
1732                 if (event.ShiftDown()) {
1733                     --mNavigationIndex;
1734                 }
1735                 else {
1736                     ++mNavigationIndex;
1737                 }
1738                 mNavigationIndex = (mNavigationIndex + (int)mLabels.size()) % (int)mLabels.size();    // wrap round if necessary
1739             }
1740             else
1741             {
1742                 if (event.ShiftDown()) {
1743                     //search for the first label starting from the end (and before selection)
1744                     mNavigationIndex = len - 1;
1745                     if (newSel.t0() > mLabels[0].getT0()) {
1746                         while (mNavigationIndex >= 0 &&
1747                             mLabels[mNavigationIndex].getT0() > newSel.t0()) {
1748                             --mNavigationIndex;
1749                         }
1750                     }
1751                 }
1752                 else {
1753                     //search for the first label starting from the beginning (and after selection)
1754                     mNavigationIndex = 0;
1755                     if (newSel.t0() < mLabels[len - 1].getT0()) {
1756                         while (mNavigationIndex < len &&
1757                             mLabels[mNavigationIndex].getT0() < newSel.t0()) {
1758                             ++mNavigationIndex;
1759                         }
1760                     }
1761                 }
1762             }
1763 
1764             if (mNavigationIndex >= 0 && mNavigationIndex < len) {
1765                const auto &labelStruct = mLabels[mNavigationIndex];
1766                mCurrentCursorPos = labelStruct.title.length();
1767                mInitialCursorPos = mCurrentCursorPos;
1768                //Set the selection region to be equal to the selection bounds of the tabbed-to label.
1769                newSel = labelStruct.selectedRegion;
1770                // message for screen reader
1771                /* i18n-hint:
1772                   String is replaced by the name of a label,
1773                   first number gives the position of that label in a sequence
1774                   of labels,
1775                   and the last number is the total number of labels in the sequence.
1776                */
1777                auto message = XO("%s %d of %d")
1778                   .Format(labelStruct.title, mNavigationIndex + 1, pTrack->GetNumLabels());
1779                TrackFocus::Get(project).MessageForScreenReader(message);
1780             }
1781             else {
1782                mNavigationIndex = -1;
1783             }
1784          }
1785          break;
1786       case WXK_F2:     // Must be Ctrl + F2 to have reached here
1787          // Hardcoded Ctrl+F2 activates editing of the label
1788          // pointed to by mNavigationIndex (if valid)
1789          if (IsValidIndex(mNavigationIndex, project)) {
1790              SetTextSelection(mNavigationIndex);
1791          }
1792          break;
1793       default:
1794          if (!IsGoodLabelFirstKey(event)) {
1795             event.Skip();
1796          }
1797          break;
1798       }
1799    }
1800 
1801    return updated;
1802 }
1803 
1804 /// OnChar is called for incoming characters -- that's any keypress not handled
1805 /// by OnKeyDown.
DoChar(AudacityProject & project,NotifyingSelectedRegion & WXUNUSED (newSel),wxKeyEvent & event)1806 bool LabelTrackView::DoChar(
1807    AudacityProject &project, NotifyingSelectedRegion &WXUNUSED(newSel),
1808    wxKeyEvent & event)
1809 {
1810    // Check for modifiers and only allow shift.
1811    //
1812    // We still need to check this or we will eat the top level menu accelerators
1813    // on Windows if our capture or key down handlers skipped the event.
1814    const int mods = event.GetModifiers();
1815    if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
1816       event.Skip();
1817       return false;
1818    }
1819 
1820    // Only track true changes to the label
1821    //bool updated = false;
1822 
1823    // Cache the character
1824    wxChar charCode = event.GetUnicodeKey();
1825 
1826    // Skip if it's not a valid unicode character or a control character
1827    if (charCode == 0 || wxIscntrl(charCode)) {
1828       event.Skip();
1829       return false;
1830    }
1831 
1832    // If we've reached this point and aren't currently editing, add NEW label
1833    const auto pTrack = FindLabelTrack();
1834    if (!IsValidIndex(mTextEditIndex, project)) {
1835       // Don't create a NEW label for a space
1836       if (wxIsspace(charCode)) {
1837          event.Skip();
1838          return false;
1839       }
1840       bool useDialog;
1841       gPrefs->Read(wxT("/GUI/DialogForNameNewLabel"), &useDialog, false);
1842       auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
1843       if (useDialog) {
1844          wxString title;
1845          if (DialogForLabelName(
1846             project, selectedRegion, charCode, title) ==
1847              wxID_CANCEL) {
1848             return false;
1849          }
1850          pTrack->SetSelected(true);
1851          pTrack->AddLabel(selectedRegion, title);
1852          ProjectHistory::Get( project )
1853             .PushState(XO("Added label"), XO("Label"));
1854          return false;
1855       }
1856       else {
1857          pTrack->SetSelected(true);
1858          AddLabel( selectedRegion );
1859          ProjectHistory::Get( project )
1860             .PushState(XO("Added label"), XO("Label"));
1861       }
1862    }
1863 
1864    if (!IsValidIndex(mTextEditIndex, project))
1865       return false;
1866 
1867    //
1868    // Now we are definitely in a label; append the incoming character
1869    //
1870 
1871    // Test if cursor is in the end of string or not
1872    if (mInitialCursorPos != mCurrentCursorPos)
1873       RemoveSelectedText();
1874 
1875    const auto& mLabels = pTrack->GetLabels();
1876    auto labelStruct = mLabels[mTextEditIndex];
1877    auto& title = labelStruct.title;
1878 
1879    if (mCurrentCursorPos < (int)title.length()) {
1880       // Get substring on the righthand side of cursor
1881       wxString rightPart = title.Mid(mCurrentCursorPos);
1882       // Set title to substring on the lefthand side of cursor
1883       title = title.Left(mCurrentCursorPos);
1884       //append charcode
1885       title += charCode;
1886       //append the right part substring
1887       title += rightPart;
1888    }
1889    else
1890       //append charCode
1891       title += charCode;
1892 
1893    pTrack->SetLabel(mTextEditIndex, labelStruct );
1894 
1895    //moving cursor position forward
1896    mInitialCursorPos = ++mCurrentCursorPos;
1897 
1898    return true;
1899 }
1900 
1901 enum
1902 {
1903    OnCutSelectedTextID = 1,      // OSX doesn't like a 0 menu id
1904    OnCopySelectedTextID,
1905    OnPasteSelectedTextID,
1906    OnDeleteSelectedLabelID,
1907    OnEditSelectedLabelID,
1908 };
1909 
ShowContextMenu(AudacityProject & project)1910 void LabelTrackView::ShowContextMenu( AudacityProject &project )
1911 {
1912    wxWindow *parent = wxWindow::FindFocus();
1913 
1914    // Bug 2044.  parent can be nullptr after a context switch.
1915    if( !parent )
1916       parent = &GetProjectFrame( project );
1917 
1918    if( parent )
1919    {
1920       wxMenu menu;
1921       menu.Bind(wxEVT_MENU,
1922          [this, &project]( wxCommandEvent &event ){
1923             OnContextMenu( project, event ); }
1924       );
1925 
1926       menu.Append(OnCutSelectedTextID, _("Cu&t Label text"));
1927       menu.Append(OnCopySelectedTextID, _("&Copy Label text"));
1928       menu.Append(OnPasteSelectedTextID, _("&Paste"));
1929       menu.Append(OnDeleteSelectedLabelID, _("&Delete Label"));
1930       menu.Append(OnEditSelectedLabelID, _("&Edit Label..."));
1931 
1932       menu.Enable(OnCutSelectedTextID, IsTextSelected( project ));
1933       menu.Enable(OnCopySelectedTextID, IsTextSelected( project ));
1934       menu.Enable(OnPasteSelectedTextID, IsTextClipSupported());
1935       menu.Enable(OnDeleteSelectedLabelID, true);
1936       menu.Enable(OnEditSelectedLabelID, true);
1937 
1938       if(!IsValidIndex(mTextEditIndex, project)) {
1939          return;
1940       }
1941 
1942       const auto pTrack = FindLabelTrack();
1943       const LabelStruct *ls = pTrack->GetLabel(mTextEditIndex);
1944 
1945       wxClientDC dc(parent);
1946 
1947       if (msFont.Ok())
1948       {
1949          dc.SetFont(msFont);
1950       }
1951 
1952       int x = 0;
1953       bool success = CalcCursorX( project, &x );
1954       wxASSERT(success);
1955       static_cast<void>(success); // Suppress unused variable warning if debug mode is disabled
1956 
1957       // Bug #2571: Hackage alert! For some reason wxGTK does not like
1958       // displaying the LabelDialog from within the PopupMenu "context".
1959       // So, workaround it by editing the label AFTER the popup menu is
1960       // closed. It's really ugly, but it works.  :-(
1961       mEditIndex = -1;
1962       BasicMenu::Handle{ &menu }.Popup(
1963          wxWidgetsWindowPlacement{ parent },
1964          { x, ls->y + (mIconHeight / 2) - 1 }
1965       );
1966       if (mEditIndex >= 0)
1967       {
1968          DoEditLabels( project, FindLabelTrack().get(), mEditIndex );
1969       }
1970    }
1971 }
1972 
OnContextMenu(AudacityProject & project,wxCommandEvent & evt)1973 void LabelTrackView::OnContextMenu(
1974    AudacityProject &project, wxCommandEvent & evt )
1975 {
1976    auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
1977 
1978    switch (evt.GetId())
1979    {
1980    /// Cut selected text if cut menu item is selected
1981    case OnCutSelectedTextID:
1982       if (CutSelectedText( project ))
1983       {
1984          ProjectHistory::Get( project ).PushState(XO("Modified Label"),
1985                       XO("Label Edit"),
1986                       mTextEditIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
1987       }
1988       break;
1989 
1990    /// Copy selected text if copy menu item is selected
1991    case OnCopySelectedTextID:
1992       CopySelectedText( project );
1993       break;
1994 
1995    /// paste selected text if paste menu item is selected
1996    case OnPasteSelectedTextID:
1997       if (PasteSelectedText(
1998          project, selectedRegion.t0(), selectedRegion.t1() ))
1999       {
2000          ProjectHistory::Get( project ).PushState(XO("Modified Label"),
2001                       XO("Label Edit"),
2002                       mTextEditIndex.IsModified() ? UndoPush::CONSOLIDATE : UndoPush::NONE);
2003       }
2004       break;
2005 
2006    /// DELETE selected label
2007    case OnDeleteSelectedLabelID: {
2008       if (IsValidIndex(mTextEditIndex, project))
2009       {
2010          const auto pTrack = FindLabelTrack();
2011          pTrack->DeleteLabel(mTextEditIndex);
2012          ProjectHistory::Get( project ).PushState(XO("Deleted Label"),
2013                       XO("Label Edit"),
2014                       UndoPush::CONSOLIDATE);
2015       }
2016    }
2017       break;
2018 
2019    case OnEditSelectedLabelID: {
2020       // Bug #2571: See above
2021       if (IsValidIndex(mTextEditIndex, project))
2022          mEditIndex = mTextEditIndex;
2023    }
2024       break;
2025    }
2026 }
2027 
RemoveSelectedText()2028 void LabelTrackView::RemoveSelectedText()
2029 {
2030    wxString left, right;
2031 
2032    int init = mInitialCursorPos;
2033    int cur = mCurrentCursorPos;
2034    if (init > cur)
2035       std::swap(init, cur);
2036 
2037    const auto pTrack = FindLabelTrack();
2038    const auto &mLabels = pTrack->GetLabels();
2039    auto labelStruct = mLabels[mTextEditIndex];
2040    auto &title = labelStruct.title;
2041 
2042    if (init > 0)
2043       left = title.Left(init);
2044 
2045    if (cur < (int)title.length())
2046       right = title.Mid(cur);
2047 
2048    title = left + right;
2049    pTrack->SetLabel( mTextEditIndex, labelStruct );
2050    mInitialCursorPos = mCurrentCursorPos = left.length();
2051 }
2052 /*
2053 bool LabelTrackView::HasSelectedLabel( AudacityProject &project ) const
2054 {
2055    const auto selIndex = GetSelectionIndex( project );
2056    return (selIndex >= 0 &&
2057       selIndex < (int)FindLabelTrack()->GetLabels().size());
2058 }*/
2059 
GetLabelIndex(double t,double t1)2060 int LabelTrackView::GetLabelIndex(double t, double t1)
2061 {
2062    //We'd have liked to have times in terms of samples,
2063    //because then we're doing an intrger comparison.
2064    //Never mind.  Instead we look for near enough.
2065    //This level of (in)accuracy is only a problem if we
2066    //deal with sounds in the MHz range.
2067    const double delta = 1.0e-7;
2068    const auto pTrack = FindLabelTrack();
2069    const auto &mLabels = pTrack->GetLabels();
2070    { int i = -1; for (const auto &labelStruct : mLabels) { ++i;
2071       if( fabs( labelStruct.getT0() - t ) > delta )
2072          continue;
2073       if( fabs( labelStruct.getT1() - t1 ) > delta )
2074          continue;
2075       return i;
2076    }}
2077 
2078    return wxNOT_FOUND;
2079 }
2080 
2081 
2082 // restoreFocus of -1 is the default, and sets the focus to this label.
2083 // restoreFocus of -2 or other value leaves the focus unchanged.
2084 // restoreFocus >= 0 will later cause focus to move to that track.
AddLabel(const SelectedRegion & selectedRegion,const wxString & title,int restoreFocus)2085 int LabelTrackView::AddLabel(const SelectedRegion &selectedRegion,
2086                          const wxString &title, int restoreFocus)
2087 {
2088    const auto pTrack = FindLabelTrack();
2089    mRestoreFocus = restoreFocus;
2090    auto pos = pTrack->AddLabel( selectedRegion, title );
2091    return pos;
2092 }
2093 
OnLabelAdded(LabelTrackEvent & e)2094 void LabelTrackView::OnLabelAdded( LabelTrackEvent &e )
2095 {
2096    e.Skip();
2097    if ( e.mpTrack.lock() != FindTrack() )
2098       return;
2099 
2100    const auto &title = e.mTitle;
2101    const auto pos = e.mPresentPosition;
2102 
2103    mInitialCursorPos = mCurrentCursorPos = title.length();
2104 
2105    // restoreFocus is -2 e.g. from Nyquist label creation, when we should not
2106    // even lose the focus and open the label to edit in the first place.
2107    // -1 means we don't need to restore it to anywhere.
2108    // 0 or above is the track to restore to after editing the label is complete.
2109    if( mRestoreFocus >= -1 )
2110        mTextEditIndex = pos;
2111 
2112    if( mRestoreFocus < 0 )
2113       mRestoreFocus = -2;
2114 }
2115 
OnLabelDeleted(LabelTrackEvent & e)2116 void LabelTrackView::OnLabelDeleted( LabelTrackEvent &e )
2117 {
2118    e.Skip();
2119    if ( e.mpTrack.lock() != FindTrack() )
2120       return;
2121 
2122    auto index = e.mFormerPosition;
2123 
2124    // IF we've deleted the selected label
2125    // THEN set no label selected.
2126    if (mTextEditIndex == index)
2127       ResetTextSelection();
2128 
2129    // IF we removed a label before the selected label
2130    // THEN the NEW selected label number is one less.
2131    else if( index < mTextEditIndex)
2132       --mTextEditIndex;//NB: Keep cursor selection region
2133 }
2134 
OnLabelPermuted(LabelTrackEvent & e)2135 void LabelTrackView::OnLabelPermuted( LabelTrackEvent &e )
2136 {
2137    e.Skip();
2138    if ( e.mpTrack.lock() != FindTrack() )
2139       return;
2140 
2141    auto former = e.mFormerPosition;
2142    auto present = e.mPresentPosition;
2143 
2144    auto fix = [&](Index& index) {
2145        if (index == former)
2146            index = present;
2147        else if (former < index && index <= present)
2148            --index;
2149        else if (former > index && index >= present)
2150            ++index;
2151    };
2152    fix(mNavigationIndex);
2153    fix(mTextEditIndex);
2154 }
2155 
OnSelectionChange(LabelTrackEvent & e)2156 void LabelTrackView::OnSelectionChange( LabelTrackEvent &e )
2157 {
2158    e.Skip();
2159    if ( e.mpTrack.lock() != FindTrack() )
2160       return;
2161 
2162    if (!FindTrack()->GetSelected())
2163    {
2164        SetNavigationIndex(-1);
2165        ResetTextSelection();
2166    }
2167 }
2168 
GetGlyph(int i)2169 wxBitmap & LabelTrackView::GetGlyph( int i)
2170 {
2171    return theTheme.Bitmap( i + bmpLabelGlyph0);
2172 }
2173 
2174 // This one XPM spec is used to generate a number of
2175 // different wxIcons.
2176 /* XPM */
2177 static const char *const GlyphXpmRegionSpec[] = {
2178 /* columns rows colors chars-per-pixel */
2179 "15 23 7 1",
2180 /* Default colors, with first color transparent */
2181 ". c none",
2182 "2 c black",
2183 "3 c black",
2184 "4 c black",
2185 "5 c #BEBEF0",
2186 "6 c #BEBEF0",
2187 "7 c #BEBEF0",
2188 /* pixels */
2189 "...............",
2190 "...............",
2191 "...............",
2192 "....333.444....",
2193 "...3553.4774...",
2194 "...3553.4774...",
2195 "..35553.47774..",
2196 "..35522222774..",
2197 ".3552666662774.",
2198 ".3526666666274.",
2199 "355266666662774",
2200 "355266666662774",
2201 "355266666662774",
2202 ".3526666666274.",
2203 ".3552666662774.",
2204 "..35522222774..",
2205 "..35553.47774..",
2206 "...3553.4774...",
2207 "...3553.4774...",
2208 "....333.444....",
2209 "...............",
2210 "...............",
2211 "..............."
2212 };
2213 
2214 /// CreateCustomGlyphs() creates the mBoundaryGlyph array.
2215 /// It's a bit like painting by numbers!
2216 ///
2217 /// Schematically the glyphs we want will 'look like':
2218 ///   <O,  O>   and   <O>
2219 /// for a left boundary to a label, a right boundary and both.
2220 /// we're creating all three glyphs using the one Xpm Spec.
2221 ///
2222 /// When we hover over a glyph we highlight the
2223 /// inside of either the '<', the 'O' or the '>' or none,
2224 /// giving 3 x 4 = 12 combinations.
2225 ///
2226 /// Two of those combinations aren't used, but
2227 /// treating them specially would make other code more
2228 /// complicated.
CreateCustomGlyphs()2229 void LabelTrackView::CreateCustomGlyphs()
2230 {
2231    int iConfig;
2232    int iHighlight;
2233    int index;
2234    const int nSpecRows =
2235       sizeof( GlyphXpmRegionSpec )/sizeof( GlyphXpmRegionSpec[0]);
2236    const char *XmpBmp[nSpecRows];
2237 
2238    // The glyphs are declared static wxIcon; so we only need
2239    // to create them once, no matter how many LabelTracks.
2240    if( mbGlyphsReady )
2241       return;
2242 
2243    // We're about to tweak the basic color spec to get 12 variations.
2244    for( iConfig=0;iConfig<NUM_GLYPH_CONFIGS;iConfig++)
2245    {
2246       for( iHighlight=0;iHighlight<NUM_GLYPH_HIGHLIGHTS;iHighlight++)
2247       {
2248          index = iConfig + NUM_GLYPH_CONFIGS * iHighlight;
2249          // Copy the basic spec...
2250          memcpy( XmpBmp, GlyphXpmRegionSpec, sizeof( GlyphXpmRegionSpec ));
2251          // The highlighted region (if any) is white...
2252          if( iHighlight==1 ) XmpBmp[5]="5 c #FFFFFF";
2253          if( iHighlight==2 ) XmpBmp[6]="6 c #FFFFFF";
2254          if( iHighlight==3 ) XmpBmp[7]="7 c #FFFFFF";
2255          // For left or right arrow the other side of the glyph
2256          // is the transparent color.
2257          if( iConfig==0) { XmpBmp[3]="3 c none"; XmpBmp[5]="5 c none"; }
2258          if( iConfig==1) { XmpBmp[4]="4 c none"; XmpBmp[7]="7 c none"; }
2259          // Create the icon from the tweaked spec.
2260          mBoundaryGlyphs[index] = wxBitmap(XmpBmp);
2261          // Create the mask
2262          // SetMask takes ownership
2263          mBoundaryGlyphs[index].SetMask(safenew wxMask(mBoundaryGlyphs[index], wxColour(192, 192, 192)));
2264       }
2265    }
2266 
2267    mIconWidth  = mBoundaryGlyphs[0].GetWidth();
2268    mIconHeight = mBoundaryGlyphs[0].GetHeight();
2269    mTextHeight = mIconHeight; // until proved otherwise...
2270    // The icon should have an odd width so that the
2271    // line goes exactly down the middle.
2272    wxASSERT( (mIconWidth %2)==1);
2273 
2274    mbGlyphsReady=true;
2275 }
2276 
2277 #include "../../../LabelDialog.h"
2278 
DoEditLabels(AudacityProject & project,LabelTrack * lt,int index)2279 void LabelTrackView::DoEditLabels
2280 (AudacityProject &project, LabelTrack *lt, int index)
2281 {
2282    const auto &settings = ProjectSettings::Get( project );
2283    auto format = settings.GetSelectionFormat(),
2284       freqFormat = settings.GetFrequencySelectionFormatName();
2285    auto &tracks = TrackList::Get( project );
2286    auto rate = ProjectRate::Get( project ).GetRate();
2287    auto &viewInfo = ViewInfo::Get( project );
2288    auto &window = ProjectWindow::Get( project );
2289 
2290    LabelDialog dlg(&window, project, &tracks,
2291                    lt, index,
2292                    viewInfo, rate,
2293                    format, freqFormat);
2294 #ifdef __WXGTK__
2295    dlg.Raise();
2296 #endif
2297 
2298    if (dlg.ShowModal() == wxID_OK) {
2299       ProjectHistory::Get( project )
2300          .PushState(XO("Edited labels"), XO("Label"));
2301    }
2302 }
2303 
DialogForLabelName(AudacityProject & project,const SelectedRegion & region,const wxString & initialValue,wxString & value)2304 int LabelTrackView::DialogForLabelName(
2305    AudacityProject &project,
2306    const SelectedRegion& region, const wxString& initialValue, wxString& value)
2307 {
2308    auto &trackFocus = TrackFocus::Get( project );
2309    auto &trackPanel = TrackPanel::Get( project );
2310    auto &viewInfo = ViewInfo::Get( project );
2311 
2312    wxPoint position =
2313       trackPanel.FindTrackRect( trackFocus.Get() ).GetBottomLeft();
2314    // The start of the text in the text box will be roughly in line with the label's position
2315    // if it's a point label, or the start of its region if it's a region label.
2316    position.x +=
2317       + std::max(0, static_cast<int>(viewInfo.TimeToPosition(
2318          viewInfo.GetLeftOffset(), region.t0())))
2319       - 39;
2320    position.y += 2;  // just below the bottom of the track
2321    position = trackPanel.ClientToScreen(position);
2322    auto &window = GetProjectFrame( project );
2323    AudacityTextEntryDialog dialog{ &window,
2324       XO("Name:"),
2325       XO("New label"),
2326       initialValue,
2327       wxOK | wxCANCEL,
2328       position };
2329 
2330    // keep the dialog within Audacity's window, so that the dialog is always fully visible
2331    wxRect dialogScreenRect = dialog.GetScreenRect();
2332    wxRect projScreenRect = window.GetScreenRect();
2333    wxPoint max = projScreenRect.GetBottomRight() + wxPoint{ -dialogScreenRect.width, -dialogScreenRect.height };
2334    if (dialogScreenRect.x > max.x) {
2335       position.x = max.x;
2336       dialog.Move(position);
2337    }
2338    if (dialogScreenRect.y > max.y) {
2339       position.y = max.y;
2340       dialog.Move(position);
2341    }
2342 
2343    dialog.SetInsertionPointEnd();      // because, by default, initial text is selected
2344    int status = dialog.ShowModal();
2345    if (status != wxID_CANCEL) {
2346       value = dialog.GetValue();
2347       value.Trim(true).Trim(false);
2348    }
2349 
2350    return status;
2351 }
2352 
2353 using DoGetLabelTrackView = DoGetView::Override< LabelTrack >;
DEFINE_ATTACHED_VIRTUAL_OVERRIDE(DoGetLabelTrackView)2354 DEFINE_ATTACHED_VIRTUAL_OVERRIDE(DoGetLabelTrackView) {
2355    return [](LabelTrack &track) {
2356       return std::make_shared<LabelTrackView>( track.SharedPointer() );
2357    };
2358 }
2359 
DoGetVRulerControls()2360 std::shared_ptr<TrackVRulerControls> LabelTrackView::DoGetVRulerControls()
2361 {
2362    return
2363       std::make_shared<LabelTrackVRulerControls>( shared_from_this() );
2364 }
2365