Audacity  2.3.1
LabelTrack.cpp
Go to the documentation of this file.
1 /**********************************************************************
2 
3  Audacity: A Digital Audio Editor
4 
5  LabelTrack.cpp
6 
7  Dominic Mazzoni
8  James Crook
9  Jun Wan
10 
11 *******************************************************************//****************************************************************//*******************************************************************/
30 
31 #include "Audacity.h"
32 #include "LabelTrack.h"
33 #include "Experimental.h"
34 #include "TrackPanel.h"
35 
36 #include <stdio.h>
37 #include <algorithm>
38 #include <limits.h>
39 #include <float.h>
40 
41 #include <wx/bitmap.h>
42 #include <wx/brush.h>
43 #include <wx/clipbrd.h>
44 #include <wx/dataobj.h>
45 #include <wx/dc.h>
46 #include <wx/dcclient.h>
47 #include <wx/event.h>
48 #include <wx/intl.h>
49 #include <wx/log.h>
50 #include <wx/pen.h>
51 #include <wx/string.h>
52 #include <wx/textfile.h>
53 #include <wx/tokenzr.h>
54 #include <wx/utils.h>
55 
56 #include "AudioIO.h"
57 #include "DirManager.h"
58 #include "Internat.h"
59 #include "Menus.h"
60 #include "Prefs.h"
61 #include "RefreshCode.h"
62 #include "Theme.h"
63 #include "AllThemeResources.h"
64 #include "AColor.h"
65 #include "Project.h"
66 #include "TrackArtist.h"
67 #include "TrackPanel.h"
68 #include "UndoManager.h"
70 
71 #include "effects/TimeWarper.h"
72 #include "widgets/ErrorDialog.h"
73 
74 enum
75 {
76  OnCutSelectedTextID = 1, // OSX doesn't like a 0 menu id
81 };
82 
83 wxFont LabelTrack::msFont;
84 
85 // static member variables.
86 bool LabelTrack::mbGlyphsReady=false;
87 
97 
99 
101 {
102  return std::make_unique<LabelTrack>(mDirManager);
103 }
104 
105 LabelTrack::LabelTrack(const std::shared_ptr<DirManager> &projDirManager):
106  Track(projDirManager),
107  mSelIndex(-1),
108  mRestoreFocus(-1),
109  mClipLen(0.0),
110  miLastLabel(-1)
111 {
112  SetDefaultName(_("Label Track"));
114 
115  // Label tracks are narrow
116  // Default is to allow two rows so that NEW users get the
117  // idea that labels can 'stack' when they would overlap.
118  SetHeight(73);
119 
120  ResetFont();
122 
123  // reset flags
124  ResetFlags();
125 }
126 
128  Track(orig),
129  mSelIndex(-1),
130  mClipLen(0.0)
131 {
132  for (auto &original: orig.mLabels) {
133  LabelStruct l { original.selectedRegion, original.title };
134  mLabels.push_back(l);
135  }
136  mSelIndex = orig.mSelIndex;
137 
138  // reset flags
139  ResetFlags();
140 }
141 
143 {
144 }
145 
146 void LabelTrack::SetOffset(double dOffset)
147 {
148  for (auto &labelStruct: mLabels)
149  labelStruct.selectedRegion.move(dOffset);
150 }
151 
152 void LabelTrack::Clear(double b, double e)
153 {
154  // May DELETE labels, so use subscripts to iterate
155  for (size_t i = 0; i < mLabels.size(); ++i) {
156  auto &labelStruct = mLabels[i];
157  LabelStruct::TimeRelations relation =
158  labelStruct.RegionRelation(b, e, this);
159  if (relation == LabelStruct::BEFORE_LABEL)
160  labelStruct.selectedRegion.move(- (e-b));
161  else if (relation == LabelStruct::SURROUNDS_LABEL) {
162  DeleteLabel( i );
163  --i;
164  }
165  else if (relation == LabelStruct::ENDS_IN_LABEL)
166  labelStruct.selectedRegion.setTimes(
167  b,
168  labelStruct.getT1() - (e - b));
169  else if (relation == LabelStruct::BEGINS_IN_LABEL)
170  labelStruct.selectedRegion.setT1(b);
171  else if (relation == LabelStruct::WITHIN_LABEL)
172  labelStruct.selectedRegion.moveT1( - (e-b));
173  }
174 }
175 
176 #if 0
177 //used when we want to use clear only on the labels
178 bool LabelTrack::SplitDelete(double b, double e)
179 {
180  // May DELETE labels, so use subscripts to iterate
181  for (size_t i = 0, len = mLabels.size(); i < len; ++i) {
182  auto &labelStruct = mLabels[i];
183  LabelStruct::TimeRelations relation =
184  labelStruct.RegionRelation(b, e, this);
185  if (relation == LabelStruct::SURROUNDS_LABEL) {
186  DeleteLabel(i);
187  --i;
188  }
189  else if (relation == LabelStruct::WITHIN_LABEL)
190  labelStruct.selectedRegion.moveT1( - (e-b));
191  else if (relation == LabelStruct::ENDS_IN_LABEL)
192  labelStruct.selectedRegion.setT0(e);
193  else if (relation == LabelStruct::BEGINS_IN_LABEL)
194  labelStruct.selectedRegion.setT1(b);
195  }
196 
197  return true;
198 }
199 #endif
200 
201 void LabelTrack::ShiftLabelsOnInsert(double length, double pt)
202 {
203  for (auto &labelStruct: mLabels) {
204  LabelStruct::TimeRelations relation =
205  labelStruct.RegionRelation(pt, pt, this);
206 
207  if (relation == LabelStruct::BEFORE_LABEL)
208  labelStruct.selectedRegion.move(length);
209  else if (relation == LabelStruct::WITHIN_LABEL)
210  labelStruct.selectedRegion.moveT1(length);
211  }
212 }
213 
214 void LabelTrack::ChangeLabelsOnReverse(double b, double e)
215 {
216  for (auto &labelStruct: mLabels) {
217  if (labelStruct.RegionRelation(b, e, this) ==
219  {
220  double aux = b + (e - labelStruct.getT1());
221  labelStruct.selectedRegion.setTimes(
222  aux,
223  e - (labelStruct.getT0() - b));
224  }
225  }
226  SortLabels();
227 }
228 
229 void LabelTrack::ScaleLabels(double b, double e, double change)
230 {
231  for (auto &labelStruct: mLabels) {
232  labelStruct.selectedRegion.setTimes(
233  AdjustTimeStampOnScale(labelStruct.getT0(), b, e, change),
234  AdjustTimeStampOnScale(labelStruct.getT1(), b, e, change));
235  }
236 }
237 
238 double LabelTrack::AdjustTimeStampOnScale(double t, double b, double e, double change)
239 {
240 //t is the time stamp we'll be changing
241 //b and e are the selection start and end
242 
243  if (t < b){
244  return t;
245  }else if (t > e){
246  double shift = (e-b)*change - (e-b);
247  return t + shift;
248  }else{
249  double shift = (t-b)*change - (t-b);
250  return t + shift;
251  }
252 }
253 
254 // Move the labels in the track according to the given TimeWarper.
255 // (If necessary this could be optimised by ignoring labels that occur before a
256 // specified time, as in most cases they don't need to move.)
257 void LabelTrack::WarpLabels(const TimeWarper &warper) {
258  for (auto &labelStruct: mLabels) {
259  labelStruct.selectedRegion.setTimes(
260  warper.Warp(labelStruct.getT0()),
261  warper.Warp(labelStruct.getT1()));
262  }
263 
264  // This should not be needed, assuming the warper is nondecreasing, but
265  // let's not assume too much.
266  SortLabels();
267 }
268 
270 {
271  mInitialCursorPos = 1;
272  mCurrentCursorPos = 1;
273  mRightDragging = false;
274  mDrawCursor = false;
275 }
276 
277 void LabelTrack::RestoreFlags( const Flags& flags )
278 {
281  mSelIndex = flags.mSelIndex;
283  mDrawCursor = flags.mDrawCursor;
284 }
285 
286 wxFont LabelTrack::GetFont(const wxString &faceName, int size)
287 {
288  wxFontEncoding encoding;
289  if (faceName == wxT(""))
290  encoding = wxFONTENCODING_DEFAULT;
291  else
292  encoding = wxFONTENCODING_SYSTEM;
293  return wxFont(size, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL,
294  wxFONTWEIGHT_NORMAL, false, faceName, encoding);
295 }
296 
298 {
299  mFontHeight = -1;
300  wxString facename = gPrefs->Read(wxT("/GUI/LabelFontFacename"), wxT(""));
301  int size = gPrefs->Read(wxT("/GUI/LabelFontSize"), DefaultFontSize);
302  msFont = GetFont(facename, size);
303 }
304 
318 void LabelTrack::ComputeTextPosition(const wxRect & r, int index) const
319 {
320  auto &labelStruct = mLabels[index];
321 
322  // xExtra is extra space
323  // between the text and the endpoints.
324  const int xExtra=mIconWidth;
325  int x = labelStruct.x; // left endpoint
326  int x1 = labelStruct.x1; // right endpoint.
327  int width = labelStruct.width;
328 
329  int xText; // This is where the text will end up.
330 
331  // Will the text all fit at this zoom?
332  bool bTooWideForScreen = width > (r.width-2*xExtra);
333 // bool bSimpleCentering = !bTooWideForScreen;
334  bool bSimpleCentering = false;
335 
336  //TODO (possibly):
337  // Add configurable options as to when to use simple
338  // and when complex centering.
339  //
340  // Simple centering does its best to keep the text
341  // centered between the label limits.
342  //
343  // Complex centering positions the text proportionally
344  // to how far we are through the label.
345  //
346  // If we add preferences for this, we want to be able to
347  // choose separately whether:
348  // a) Wide text labels centered simple/complex.
349  // b) Other text labels centered simple/complex.
350  //
351 
352  if( bSimpleCentering )
353  {
354  // Center text between the two end points.
355  xText = (x+x1-width)/2;
356  }
357  else
358  {
359  // Calculate xText position to make text line
360  // scroll sideways evenly as r moves right.
361 
362  // xText is a linear function of r.x.
363  // These variables are used to compute that function.
364  int rx0,rx1,xText0,xText1;
365 
366  // Since we will be using a linear function,
367  // we should blend smoothly between left and right
368  // aligned text as r, the 'viewport' moves.
369  if( bTooWideForScreen )
370  {
371  rx0=x; // when viewport at label start.
372  xText0=x+xExtra; // text aligned left.
373  rx1=x1-r.width; // when viewport end at label end
374  xText1=x1-(width+xExtra); // text aligned right.
375  }
376  else
377  {
378  // when label start + width + extra spacing at viewport end..
379  rx0=x-r.width+width+2*xExtra;
380  // ..text aligned left.
381  xText0=x+xExtra;
382  // when viewport start + width + extra spacing at label end..
383  rx1=x1-(width+2*xExtra);
384  // ..text aligned right.
385  xText1=x1-(width+xExtra);
386  }
387 
388  if( rx1 > rx0 ) // Avoid divide by zero case.
389  {
390  // Compute the blend between left and right aligned.
391 
392  // Don't use:
393  //
394  // xText = xText0 + ((xText1-xText0)*(r.x-rx0))/(rx1-rx0);
395  //
396  // The problem with the above is that it integer-oveflows at
397  // high zoom.
398 
399  // Instead use:
400  xText = xText0 + (int)((xText1-xText0)*(((float)(r.x-rx0))/(rx1-rx0)));
401  }
402  else
403  {
404  // Avoid divide by zero by reverting to
405  // simple centering.
406  //
407  // We could also fall into this case if x and x1
408  // are swapped, in which case we'll end up
409  // left aligned anyway because code later on
410  // will catch that.
411  xText = (x+x1-width)/2;
412  }
413  }
414 
415  // Is the text now appearing partly outside r?
416  bool bOffLeft = xText < r.x+xExtra;
417  bool bOffRight = xText > r.x+r.width-width-xExtra;
418 
419  // IF any part of the text is offscreen
420  // THEN we may bring it back.
421  if( bOffLeft == bOffRight )
422  {
423  //IF both sides on screen, THEN nothing to do.
424  //IF both sides off screen THEN don't do
425  //anything about it.
426  //(because if we did, you'd never get to read
427  //all the text by scrolling).
428  }
429  else if( bOffLeft != bTooWideForScreen)
430  {
431  // IF we're off on the left, OR we're
432  // too wide for the screen and off on the right
433  // (only) THEN align left.
434  xText = r.x+xExtra;
435  }
436  else
437  {
438  // We're off on the right, OR we're
439  // too wide and off on the left (only)
440  // SO align right.
441  xText =r.x+r.width-width-xExtra;
442  }
443 
444  // But if we've taken the text out from its endpoints
445  // we must move it back so that it's between the endpoints.
446 
447  // We test the left end point last because the
448  // text might not even fit between the endpoints (at this
449  // zoom factor), and in that case we'd like to position
450  // the text at the left end point.
451  if( xText > (x1-width-xExtra))
452  xText=(x1-width-xExtra);
453  if( xText < x+xExtra )
454  xText=x+xExtra;
455 
456  labelStruct.xText = xText;
457 }
458 
462 void LabelTrack::ComputeLayout(const wxRect & r, const ZoomInfo &zoomInfo) const
463 {
464  int xUsed[MAX_NUM_ROWS];
465 
466  int iRow;
467  // Rows are the 'same' height as icons or as the text,
468  // whichever is taller.
469  const int yRowHeight = wxMax(mTextHeight,mIconHeight)+3;// pixels.
470  // Extra space at end of rows.
471  // We allow space for one half icon at the start and two
472  // half icon widths for extra x for the text frame.
473  // [we don't allow half a width space for the end icon since it is
474  // allowed to be obscured by the text].
475  const int xExtra= (3 * mIconWidth)/2;
476 
477  const int nRows = wxMin((r.height / yRowHeight) + 1, MAX_NUM_ROWS);
478  // Initially none of the rows have been used.
479  // So set a value that is less than any valid value.
480  {
481  // Bug 502: With dragging left of zeros, labels can be in
482  // negative space. So set least possible value as starting point.
483  const int xStart = INT_MIN;
484  for (auto &x : xUsed)
485  x = xStart;
486  }
487  int nRowsUsed=0;
488 
489  { int i = -1; for (auto &labelStruct : mLabels) { ++i;
490  const int x = zoomInfo.TimeToPosition(labelStruct.getT0(), r.x);
491  const int x1 = zoomInfo.TimeToPosition(labelStruct.getT1(), r.x);
492  int y = r.y;
493 
494  labelStruct.x=x;
495  labelStruct.x1=x1;
496  labelStruct.y=-1;// -ve indicates nothing doing.
497  iRow=0;
498  // Our first preference is a row that ends where we start.
499  // (This is to encourage merging of adjacent label boundaries).
500  while( (iRow<nRowsUsed) && (xUsed[iRow] != x ))
501  iRow++;
502  // IF we didn't find one THEN
503  // find any row that can take a span starting at x.
504  if( iRow >= nRowsUsed )
505  {
506  iRow=0;
507  while( (iRow<nRows) && (xUsed[iRow] > x ))
508  iRow++;
509  }
510  // IF we found such a row THEN record a valid position.
511  if( iRow<nRows )
512  {
513  // Possibly update the number of rows actually used.
514  if( iRow >= nRowsUsed )
515  nRowsUsed=iRow+1;
516  // Record the position for this label
517  y= r.y + iRow * yRowHeight +(yRowHeight/2)+1;
518  labelStruct.y=y;
519  // On this row we have used up to max of end marker and width.
520  // Plus also allow space to show the start icon and
521  // some space for the text frame.
522  xUsed[iRow]=x+labelStruct.width+xExtra;
523  if( xUsed[iRow] < x1 ) xUsed[iRow]=x1;
524  ComputeTextPosition( r, i );
525  }
526  }}
527 }
528 
530  const wxString& aTitle)
531 : selectedRegion(region)
532 , title(aTitle)
533 {
534  updated = false;
535  width = 0;
536  x = 0;
537  x1 = 0;
538  xText = 0;
539  y = 0;
540 }
541 
543  double t0, double t1,
544  const wxString& aTitle)
545 : selectedRegion(region)
546 , title(aTitle)
547 {
548  // Overwrite the times
549  selectedRegion.setTimes(t0, t1);
550 
551  updated = false;
552  width = 0;
553  x = 0;
554  x1 = 0;
555  xText = 0;
556  y = 0;
557 }
558 
563 void LabelStruct::DrawLines(wxDC & dc, const wxRect & r) const
564 {
565  // How far out from the centre line should the vertical lines
566  // start, i.e. what is the y position of the icon?
567  // We adjust this so that the line encroaches on the icon
568  // slightly (there is white space in the design).
569  const int yIconStart = y - (LabelTrack::mIconHeight /2)+1+(LabelTrack::mTextHeight+3)/2;
570  const int yIconEnd = yIconStart + LabelTrack::mIconHeight-2;
571 
572  // If y is positive then it is the center line for the
573  // Label.
574  if( y >= 0 )
575  {
576  if((x >= r.x) && (x <= (r.x+r.width)))
577  {
578  // Draw line above and below left dragging widget.
579  AColor::Line(dc, x, r.y, x, yIconStart - 1);
580  AColor::Line(dc, x, yIconEnd, x, r.y + r.height);
581  }
582  if((x1 >= r.x) && (x1 <= (r.x+r.width)))
583  {
584  // Draw line above and below right dragging widget.
585  AColor::Line(dc, x1, r.y, x1, yIconStart - 1);
586  AColor::Line(dc, x1, yIconEnd, x1, r.y + r.height);
587  }
588  }
589  else
590  {
591  // Draw the line, even though the widget is off screen
592  AColor::Line(dc, x, r.y, x, r.y + r.height);
593  AColor::Line(dc, x1, r.y, x1, r.y + r.height);
594  }
595 }
596 
601  (wxDC & dc, const wxRect & r, int GlyphLeft, int GlyphRight) const
602 {
603  const int xHalfWidth=LabelTrack::mIconWidth/2;
604  const int yStart=y-LabelTrack::mIconHeight/2+(LabelTrack::mTextHeight+3)/2;
605 
606  // If y == -1, nothing to draw
607  if( y == -1 )
608  return;
609 
610  if((x >= r.x) && (x <= (r.x+r.width)))
611  dc.DrawBitmap(LabelTrack::GetGlyph(GlyphLeft), x-xHalfWidth,yStart, true);
612  // The extra test commented out here would suppress right hand markers
613  // when they overlap the left hand marker (e.g. zoomed out) or to the left.
614  if((x1 >= r.x) && (x1 <= (r.x+r.width)) /*&& (x1>x+LabelTrack::mIconWidth)*/)
615  dc.DrawBitmap(LabelTrack::GetGlyph(GlyphRight), x1-xHalfWidth,yStart, true);
616 }
617 
624 void LabelStruct::DrawText(wxDC & dc, const wxRect & r) const
625 {
626  //If y is positive then it is the center line for the
627  //text we are about to draw.
628  //if it isn't, nothing to draw.
629 
630  if( y == -1 )
631  return;
632 
633  // Draw frame for the text...
634  // We draw it half an icon width left of the text itself.
635  {
636  const int xStart=wxMax(r.x,xText-LabelTrack::mIconWidth/2);
637  const int xEnd=wxMin(r.x+r.width,xText+width+LabelTrack::mIconWidth/2);
638  const int xWidth = xEnd-xStart;
639 
640  if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
641  {
642  // Now draw the text itself.
643  dc.DrawText(title, xText, y-LabelTrack::mTextHeight/2);
644  }
645  }
646 
647 }
648 
649 void LabelStruct::DrawTextBox(wxDC & dc, const wxRect & r) const
650 {
651  //If y is positive then it is the center line for the
652  //text we are about to draw.
653  const int yBarHeight=3;
654  const int yFrameHeight = LabelTrack::mTextHeight+3;
655  const int xBarShorten = LabelTrack::mIconWidth+4;
656  if( y == -1 )
657  return;
658 
659  {
660  const int xStart=wxMax(r.x,x+xBarShorten/2);
661  const int xEnd=wxMin(r.x+r.width,x1-xBarShorten/2);
662  const int xWidth = xEnd-xStart;
663 
664  if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
665  {
666 
667  wxRect bar( xStart,y-yBarHeight/2+yFrameHeight/2,
668  xWidth,yBarHeight);
669  if( x1 > x+xBarShorten )
670  dc.DrawRectangle(bar);
671  }
672  }
673 
674  // In drawing the bar and the frame, we compute the clipping
675  // to the viewport ourselves. Under Win98 the GDI does its
676  // calculations in 16 bit arithmetic, and so gets it completely
677  // wrong at higher zooms where the bar can easily be
678  // more than 65536 pixels wide.
679 
680  // Draw bar for label extent...
681  // We don't quite draw from x to x1 because we allow
682  // half an icon width at each end.
683  {
684  const int xStart=wxMax(r.x,xText-LabelTrack::mIconWidth/2);
685  const int xEnd=wxMin(r.x+r.width,xText+width+LabelTrack::mIconWidth/2);
686  const int xWidth = xEnd-xStart;
687 
688  if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
689  {
690  wxRect frame(
691  xStart,y-yFrameHeight/2,
692  xWidth,yFrameHeight );
693  dc.DrawRectangle(frame);
694  }
695  }
696 }
697 
700  ( wxDC & dc, int xPos1, int xPos2, int charHeight) const
701 {
702  wxPen curPen = dc.GetPen();
703  curPen.SetColour(wxString(wxT("BLUE")));
704  wxBrush curBrush = dc.GetBrush();
705  curBrush.SetColour(wxString(wxT("BLUE")));
706  if (xPos1 < xPos2)
707  dc.DrawRectangle(xPos1-1, y-charHeight/2, xPos2-xPos1+1, charHeight);
708  else
709  dc.DrawRectangle(xPos2-1, y-charHeight/2, xPos1-xPos2+1, charHeight);
710 }
711 
712 void LabelStruct::getXPos( wxDC & dc, int * xPos1, int cursorPos) const
713 {
714  *xPos1 = xText;
715  if( cursorPos > 0)
716  {
717  int partWidth;
718  // Calculate the width of the substring and add it to Xpos
719  dc.GetTextExtent(title.Left(cursorPos), &partWidth, NULL);
720  *xPos1 += partWidth;
721  }
722 }
723 
724 bool LabelTrack::CalcCursorX(int * x) const
725 {
726  if (mSelIndex >= 0) {
727  wxMemoryDC dc;
728 
729  if (msFont.Ok()) {
730  dc.SetFont(msFont);
731  }
732 
733  mLabels[mSelIndex].getXPos(dc, x, mCurrentCursorPos);
734  *x += LabelTrack::mIconWidth / 2;
735  return true;
736  }
737 
738  return false;
739 }
740 
741 void LabelTrack::CalcHighlightXs(int *x1, int *x2) const
742 {
743  wxMemoryDC dc;
744 
745  if (msFont.Ok()) {
746  dc.SetFont(msFont);
747  }
748 
749  int pos1 = mInitialCursorPos, pos2 = mCurrentCursorPos;
750  if (pos1 > pos2)
751  std::swap(pos1, pos2);
752 
753  const auto &labelStruct = mLabels[mSelIndex];
754 
755  // find the left X pos of highlighted area
756  labelStruct.getXPos(dc, x1, pos1);
757  // find the right X pos of highlighted area
758  labelStruct.getXPos(dc, x2, pos2);
759 }
760 
762 // TODO: don't rely on the global ::GetActiveProject() to find this.
763 // Rather, give TrackPanelCell a drawing function and pass context into it.
764 namespace {
765  LabelTrackHit *findHit()
766  {
767  // Fetch the highlighting state
768  auto target = GetActiveProject()->GetTrackPanel()->Target();
769  if (target) {
770  auto handle = dynamic_cast<LabelGlyphHandle*>( target.get() );
771  if (handle)
772  return &handle->mHit;
773  }
774  return nullptr;
775  }
776 }
777 
780 
784 void LabelTrack::Draw
785 ( TrackPanelDrawingContext &context, const wxRect & r ) const
786 {
787  auto &dc = context.dc;
788  const auto artist = TrackArtist::Get( context );
789  const auto &zoomInfo = *artist->pZoomInfo;
790 
791  auto pHit = findHit();
792 
793  if(msFont.Ok())
794  dc.SetFont(msFont);
795 
796  if (mFontHeight == -1)
797  calculateFontHeight(dc);
798 
799  TrackArt::DrawBackgroundWithSelection( context, r, this,
801  ( GetSelected() || IsSyncLockSelected() ) );
802 
803  wxCoord textWidth, textHeight;
804 
805  // Get the text widths.
806  // TODO: Make more efficient by only re-computing when a
807  // text label title changes.
808  for (auto &labelStruct : mLabels) {
809  dc.GetTextExtent(labelStruct.title, &textWidth, &textHeight);
810  labelStruct.width = textWidth;
811  }
812 
813  // TODO: And this only needs to be done once, but we
814  // do need the dc to do it.
815  // We need to set mTextHeight to something sensible,
816  // guarding against the case where there are no
817  // labels or all are empty strings, which for example
818  // happens with a NEW label track.
819  dc.GetTextExtent(wxT("Demo Text x^y"), &textWidth, &textHeight);
820  mTextHeight = (int)textHeight;
821  ComputeLayout( r, zoomInfo );
822  dc.SetTextForeground(theTheme.Colour( clrLabelTrackText));
823  dc.SetBackgroundMode(wxTRANSPARENT);
824  dc.SetBrush(AColor::labelTextNormalBrush);
825  dc.SetPen(AColor::labelSurroundPen);
826  int GlyphLeft;
827  int GlyphRight;
828  // Now we draw the various items in this order,
829  // so that the correct things overpaint each other.
830 
831  // Draw vertical lines that show where the end positions are.
832  for (auto &labelStruct : mLabels)
833  labelStruct.DrawLines( dc, r );
834 
835  // Draw the end glyphs.
836  { int i = -1; for (auto &labelStruct : mLabels) { ++i;
837  GlyphLeft=0;
838  GlyphRight=1;
839  if( pHit && i == pHit->mMouseOverLabelLeft )
840  GlyphLeft = (pHit->mEdge & 4) ? 6:9;
841  if( pHit && i == pHit->mMouseOverLabelRight )
842  GlyphRight = (pHit->mEdge & 4) ? 7:4;
843  labelStruct.DrawGlyphs( dc, r, GlyphLeft, GlyphRight );
844  }}
845 
846  // Draw the label boxes.
847  {
848 #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
849  bool highlightTrack = false;
850  auto target = dynamic_cast<LabelTextHandle*>(context.target.get());
851  highlightTrack = target && target->GetTrack().get() == this;
852 #endif
853  int i = -1; for (auto &labelStruct : mLabels) { ++i;
854  bool highlight = false;
855 #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
856  highlight = highlightTrack && target->GetLabelNum() == i;
857 #endif
858  bool selected = mSelIndex == i;
859 
860  if( selected )
861  dc.SetBrush( AColor::labelTextEditBrush );
862  else if ( highlight )
863  dc.SetBrush( AColor::uglyBrush );
864  labelStruct.DrawTextBox( dc, r );
865 
866  if (highlight || selected)
867  dc.SetBrush(AColor::labelTextNormalBrush);
868  }
869  }
870 
871  // Draw highlights
872  if ((mInitialCursorPos != mCurrentCursorPos) && (mSelIndex >= 0 ))
873  {
874  int xpos1, xpos2;
875  CalcHighlightXs(&xpos1, &xpos2);
876  mLabels[mSelIndex].DrawHighlight(dc, xpos1, xpos2, mFontHeight);
877  }
878 
879  // Draw the text and the label boxes.
880  { int i = -1; for (auto &labelStruct : mLabels) { ++i;
881  if( mSelIndex==i)
882  dc.SetBrush(AColor::labelTextEditBrush);
883  labelStruct.DrawText( dc, r );
884  if( mSelIndex==i)
885  dc.SetBrush(AColor::labelTextNormalBrush);
886  }}
887 
888  // Draw the cursor, if there is one.
889  if( mDrawCursor && mSelIndex >=0 )
890  {
891  const auto &labelStruct = mLabels[mSelIndex];
892  int xPos = labelStruct.xText;
893 
894  if( mCurrentCursorPos > 0)
895  {
896  // Calculate the width of the substring and add it to Xpos
897  int partWidth;
898  dc.GetTextExtent(labelStruct.title.Left(mCurrentCursorPos), &partWidth, NULL);
899  xPos += partWidth;
900  }
901 
902  wxPen currentPen = dc.GetPen();
903  const int CursorWidth=2;
904  currentPen.SetWidth(CursorWidth);
905  AColor::Line(dc,
906  xPos-1, labelStruct.y - mFontHeight/2 + 1,
907  xPos-1, labelStruct.y + mFontHeight/2 - 1);
908  currentPen.SetWidth(1);
909  }
910 }
911 
915 {
916  int result = -1;
917  wxMemoryDC dc;
918  if(msFont.Ok())
919  dc.SetFont(msFont);
920 
921  // A bool indicator to see if set the cursor position or not
922  bool finished = false;
923  int charIndex = 1;
924  int partWidth;
925  int oneWidth;
926  double bound;
927  wxString subString;
928  const auto &labelStruct = mLabels[mSelIndex];
929  const auto &title = labelStruct.title;
930  const int length = title.length();
931  while (!finished && (charIndex < length + 1))
932  {
933  subString = title.Left(charIndex);
934  // Get the width of substring
935  dc.GetTextExtent(subString, &partWidth, NULL);
936 
937  // Get the width of the last character
938  dc.GetTextExtent(subString.Right(1), &oneWidth, NULL);
939  bound = labelStruct.xText + partWidth - oneWidth * 0.5;
940 
941  if (xPos <= bound)
942  {
943  // Found
944  result = charIndex - 1;
945  finished = true;
946  }
947  else
948  {
949  // Advance
950  charIndex++;
951  }
952  }
953  if (!finished)
954  // Cursor should be in the last position
955  result = length;
956 
957  return result;
958 }
959 
962 {
964 }
965 
966 void LabelTrack::calculateFontHeight(wxDC & dc) const
967 {
968  int charDescent;
969  int charLeading;
970 
971  // Calculate the width of the substring and add it to Xpos
972  dc.GetTextExtent(wxT("(Test String)|[yp]"), NULL, &mFontHeight, &charDescent, &charLeading);
973 
974  // The cursor will have height charHeight. We don't include the descender as
975  // part of the height because for phonetic fonts this leads to cursors which are
976  // too tall. We don't include leading either - it is usually 0.
977  // To make up for ignoring the descender height, we add one pixel above and below
978  // using CursorExtraHeight so that the cursor is just a little taller than the
979  // body of the characters.
980  const int CursorExtraHeight=2;
981  mFontHeight += CursorExtraHeight - (charLeading+charDescent);
982 }
983 
985 {
986  if (mSelIndex == -1)
987  return false;
989  return false;
990  return true;
991 }
992 
996 {
997  if (!IsTextSelected())
998  return false;
999 
1000  wxString left, right;
1001  auto &labelStruct = mLabels[mSelIndex];
1002  auto &text = labelStruct.title;
1003 
1004  int init = mInitialCursorPos;
1005  int cur = mCurrentCursorPos;
1006  if (init > cur)
1007  std::swap(init, cur);
1008 
1009  // data for cutting
1010  wxString data = text.Mid(init, cur - init);
1011 
1012  // get left-remaining text
1013  if (init > 0)
1014  left = text.Left(init);
1015 
1016  // get right-remaining text
1017  if (cur < (int)text.Length())
1018  right = text.Mid(cur);
1019 
1020  // set title to the combination of the two remainders
1021  text = left + right;
1022 
1023  // copy data onto clipboard
1024  if (wxTheClipboard->Open()) {
1025  // Clipboard owns the data you give it
1026  wxTheClipboard->SetData(safenew wxTextDataObject(data));
1027  wxTheClipboard->Close();
1028  }
1029 
1030  // set cursor positions
1031  mInitialCursorPos = mCurrentCursorPos = left.Length();
1032  return true;
1033 }
1034 
1038 {
1039  if (mSelIndex == -1)
1040  return false;
1041 
1042  const auto &labelStruct = mLabels[mSelIndex];
1043 
1044  int init = mInitialCursorPos;
1045  int cur = mCurrentCursorPos;
1046  if (init > cur)
1047  std::swap(init, cur);
1048 
1049  if (init == cur)
1050  return false;
1051 
1052  // data for copying
1053  wxString data = labelStruct.title.Mid(init, cur-init);
1054 
1055  // copy the data on clipboard
1056  if (wxTheClipboard->Open()) {
1057  // Clipboard owns the data you give it
1058  wxTheClipboard->SetData(safenew wxTextDataObject(data));
1059  wxTheClipboard->Close();
1060  }
1061 
1062  return true;
1063 }
1064 
1065 // PRL: should this set other fields of the label selection?
1068 bool LabelTrack::PasteSelectedText(double sel0, double sel1)
1069 {
1070  if (mSelIndex == -1)
1071  AddLabel(SelectedRegion(sel0, sel1), wxT(""));
1072 
1073  wxString text, left, right;
1074 
1075  // if text data is available
1076  if (IsTextClipSupported()) {
1077  if (wxTheClipboard->Open()) {
1078  wxTextDataObject data;
1079  wxTheClipboard->GetData(data);
1080  wxTheClipboard->Close();
1081  text = data.GetText();
1082  }
1083 
1084  // Convert control characters to blanks
1085  for (int i = 0; i < (int)text.Length(); i++) {
1086  if (wxIscntrl(text[i])) {
1087  text[i] = wxT(' ');
1088  }
1089  }
1090  }
1091 
1092  auto &labelStruct = mLabels[mSelIndex];
1093  auto &title = labelStruct.title;
1094  int cur = mCurrentCursorPos, init = mInitialCursorPos;
1095  if (init > cur)
1096  std::swap(init, cur);
1097  left = title.Left(init);
1098  if (cur < (int)title.Length())
1099  right = title.Mid(cur);
1100 
1101  title = left + text + right;
1102  mInitialCursorPos = mCurrentCursorPos = left.Length() + text.Length();
1103  return true;
1104 }
1105 
1106 
1109 {
1110  return wxTheClipboard->IsSupported(wxDF_TEXT);
1111 }
1112 
1113 
1115 {
1116  return mOffset;
1117 }
1118 
1120 {
1121  if (mLabels.empty())
1122  return 0.0;
1123  else
1124  return mLabels[0].getT0();
1125 }
1126 
1128 {
1129  //we need to scan through all the labels, because the last
1130  //label might not have the right-most end (if there is overlap).
1131  if (mLabels.empty())
1132  return 0.0;
1133 
1134  double end = 0.0;
1135  for (auto &labelStruct: mLabels) {
1136  const double t1 = labelStruct.getT1();
1137  if(t1 > end)
1138  end = t1;
1139  }
1140  return end;
1141 }
1142 
1144 {
1145  return std::make_unique<LabelTrack>( *this );
1146 }
1147 
1149 {
1150  Track::SetSelected(s);
1151  if (!s)
1152  Unselect();
1153 }
1154 
1158 void LabelTrack::OverGlyph(LabelTrackHit &hit, int x, int y) const
1159 {
1160  //Determine the NEW selection.
1161  int result=0;
1162  const int d1=10; //distance in pixels, used for have we hit drag handle.
1163  const int d2=5; //distance in pixels, used for have we hit drag handle center.
1164 
1165  //If not over a label, reset it
1166  hit.mMouseOverLabelLeft = -1;
1167  hit.mMouseOverLabelRight = -1;
1168  hit.mEdge = 0;
1169  { int i = -1; for (auto &labelStruct : mLabels) { ++i;
1170  //over left or right selection bound
1171  //Check right bound first, since it is drawn after left bound,
1172  //so give it precedence for matching/highlighting.
1173  if( abs(labelStruct.y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 &&
1174  abs(labelStruct.x1 - d2 -x) < d1)
1175  {
1176  hit.mMouseOverLabelRight = i;
1177  if(abs(labelStruct.x1 - x) < d2 )
1178  {
1179  result |= 4;
1180  // If left and right co-incident at this resolution, then we drag both.
1181  // We could be a little less stringent about co-incidence here if we liked.
1182  if( abs(labelStruct.x1-labelStruct.x) < 1.0 )
1183  {
1184  result |=1;
1185  hit.mMouseOverLabelLeft = i;
1186  }
1187  }
1188  result |= 2;
1189  }
1190  // Use else-if here rather than else to avoid detecting left and right
1191  // of the same label.
1192  else if( abs(labelStruct.y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 &&
1193  abs(labelStruct.x + d2 - x) < d1 )
1194  {
1195  hit.mMouseOverLabelLeft = i;
1196  if(abs(labelStruct.x - x) < d2 )
1197  result |= 4;
1198  result |= 1;
1199  }
1200 
1201  // give text box better priority for selecting
1202  if(OverTextBox(&labelStruct, x, y))
1203  {
1204  result = 0;
1205  }
1206 
1207  }}
1208  hit.mEdge = result;
1209 }
1210 
1211 int LabelTrack::OverATextBox(int xx, int yy) const
1212 {
1213  for (int nn = (int)mLabels.size(); nn--;) {
1214  const auto &labelStruct = mLabels[nn];
1215  if (OverTextBox(&labelStruct, xx, yy))
1216  return nn;
1217  }
1218 
1219  return -1;
1220 }
1221 
1222 // return true if the mouse is over text box, false otherwise
1223 bool LabelTrack::OverTextBox(const LabelStruct *pLabel, int x, int y) const
1224 {
1225  if( (pLabel->xText-(mIconWidth/2) < x) &&
1226  (x<pLabel->xText+pLabel->width+(mIconWidth/2)) &&
1227  (abs(pLabel->y-y)<mIconHeight/2))
1228  {
1229  return true;
1230  }
1231  return false;
1232 }
1233 
1234 // Adjust label's left or right boundary, depending which is requested.
1235 // Return true iff the label flipped.
1236 bool LabelStruct::AdjustEdge( int iEdge, double fNewTime)
1237 {
1238  updated = true;
1239  if( iEdge < 0 )
1240  return selectedRegion.setT0(fNewTime);
1241  else
1242  return selectedRegion.setT1(fNewTime);
1243 }
1244 
1245 // We're moving the label. Adjust both left and right edge.
1246 void LabelStruct::MoveLabel( int iEdge, double fNewTime)
1247 {
1248  double fTimeSpan = getDuration();
1249 
1250  if( iEdge < 0 )
1251  {
1252  selectedRegion.setTimes(fNewTime, fNewTime+fTimeSpan);
1253  }
1254  else
1255  {
1256  selectedRegion.setTimes(fNewTime-fTimeSpan, fNewTime);
1257  }
1258  updated = true;
1259 }
1260 
1261 LabelStruct LabelStruct::Import(wxTextFile &file, int &index)
1262 {
1263  SelectedRegion sr;
1264  wxString title;
1265  static const wxString continuation{ wxT("\\") };
1266 
1267  wxString firstLine = file.GetLine(index++);
1268 
1269  {
1270  // Assume tab is an impossible character within the exported text
1271  // of the label, so can be only a delimiter. But other white space may
1272  // be part of the label text.
1273  wxStringTokenizer toker { firstLine, wxT("\t") };
1274 
1275  //get the timepoint of the left edge of the label.
1276  auto token = toker.GetNextToken();
1277 
1278  double t0;
1279  if (!Internat::CompatibleToDouble(token, &t0))
1280  throw BadFormatException{};
1281 
1282  token = toker.GetNextToken();
1283 
1284  double t1;
1285  if (!Internat::CompatibleToDouble(token, &t1))
1286  //s1 is not a number.
1287  t1 = t0; //This is a one-sided label; t1 == t0.
1288  else
1289  token = toker.GetNextToken();
1290 
1291  sr.setTimes( t0, t1 );
1292 
1293  title = token;
1294  }
1295 
1296  // Newer selection fields are written on additional lines beginning with
1297  // '\' which is an impossible numerical character that older versions of
1298  // audacity will ignore. Test for the presence of such a line and then
1299  // parse it if we can.
1300 
1301  // There may also be additional continuation lines from future formats that
1302  // we ignore.
1303 
1304  // Advance index over all continuation lines first, before we might throw
1305  // any exceptions.
1306  int index2 = index;
1307  while (index < (int)file.GetLineCount() &&
1308  file.GetLine(index).StartsWith(continuation))
1309  ++index;
1310 
1311  if (index2 < index) {
1312  wxStringTokenizer toker { file.GetLine(index2++), wxT("\t") };
1313  auto token = toker.GetNextToken();
1314  if (token != continuation)
1315  throw BadFormatException{};
1316 
1317  token = toker.GetNextToken();
1318  double f0;
1319  if (!Internat::CompatibleToDouble(token, &f0))
1320  throw BadFormatException{};
1321 
1322  token = toker.GetNextToken();
1323  double f1;
1324  if (!Internat::CompatibleToDouble(token, &f1))
1325  throw BadFormatException{};
1326 
1327  sr.setFrequencies(f0, f1);
1328  }
1329 
1330  return LabelStruct{ sr, title };
1331 }
1332 
1333 void LabelStruct::Export(wxTextFile &file) const
1334 {
1335  file.AddLine(wxString::Format(wxT("%s\t%s\t%s"),
1336  Internat::ToString(getT0(), FLT_DIG),
1337  Internat::ToString(getT1(), FLT_DIG),
1338  title
1339  ));
1340 
1341  // Do we need more lines?
1342  auto f0 = selectedRegion.f0();
1343  auto f1 = selectedRegion.f1();
1346  return;
1347 
1348  // Write a \ character at the start of a second line,
1349  // so that earlier versions of Audacity ignore it.
1350  file.AddLine(wxString::Format(wxT("\\\t%s\t%s"),
1351  Internat::ToString(f0, FLT_DIG),
1352  Internat::ToString(f1, FLT_DIG)
1353  ));
1354 
1355  // Additional lines in future formats should also start with '\'.
1356 }
1357 
1359  double reg_t0, double reg_t1, const LabelTrack * WXUNUSED(parent)) const
1360 -> TimeRelations
1361 {
1362  bool retainLabels = false;
1363 
1364  wxASSERT(reg_t0 <= reg_t1);
1365  gPrefs->Read(wxT("/GUI/RetainLabels"), &retainLabels);
1366 
1367  if(retainLabels) {
1368 
1369  // Desired behavior for edge cases: The length of the selection is smaller
1370  // than the length of the label if the selection is within the label or
1371  // matching exactly a (region) label.
1372 
1373  if (reg_t0 < getT0() && reg_t1 > getT1())
1374  return SURROUNDS_LABEL;
1375  else if (reg_t1 < getT0())
1376  return BEFORE_LABEL;
1377  else if (reg_t0 > getT1())
1378  return AFTER_LABEL;
1379 
1380  else if (reg_t0 >= getT0() && reg_t0 <= getT1() &&
1381  reg_t1 >= getT0() && reg_t1 <= getT1())
1382  return WITHIN_LABEL;
1383 
1384  else if (reg_t0 >= getT0() && reg_t0 <= getT1())
1385  return BEGINS_IN_LABEL;
1386  else
1387  return ENDS_IN_LABEL;
1388 
1389  } else {
1390 
1391  // AWD: Desired behavior for edge cases: point labels bordered by the
1392  // selection are included within it. Region labels are included in the
1393  // selection to the extent that the selection covers them; specifically,
1394  // they're not included at all if the selection borders them, and they're
1395  // fully included if the selection covers them fully, even if it just
1396  // borders their endpoints. This is just one of many possible schemes.
1397 
1398  // The first test catches bordered point-labels and selected-through
1399  // region-labels; move it to third and selection edges become inclusive
1400  // WRT point-labels.
1401  if (reg_t0 <= getT0() && reg_t1 >= getT1())
1402  return SURROUNDS_LABEL;
1403  else if (reg_t1 <= getT0())
1404  return BEFORE_LABEL;
1405  else if (reg_t0 >= getT1())
1406  return AFTER_LABEL;
1407 
1408  // At this point, all point labels should have returned.
1409 
1410  else if (reg_t0 > getT0() && reg_t0 < getT1() &&
1411  reg_t1 > getT0() && reg_t1 < getT1())
1412  return WITHIN_LABEL;
1413 
1414  // Knowing that none of the other relations match simplifies remaining
1415  // tests
1416  else if (reg_t0 > getT0() && reg_t0 < getT1())
1417  return BEGINS_IN_LABEL;
1418  else
1419  return ENDS_IN_LABEL;
1420 
1421  }
1422 }
1423 
1430 ( LabelTrackHit &hit, int iLabel, int iEdge, bool bAllowSwapping, double fNewTime)
1431 {
1432  if( iLabel < 0 )
1433  return;
1434  LabelStruct &labelStruct = mLabels[ iLabel ];
1435 
1436  // Adjust the requested edge.
1437  bool flipped = labelStruct.AdjustEdge( iEdge, fNewTime );
1438  // If the edges did not swap, then we are done.
1439  if( ! flipped )
1440  return;
1441 
1442  // If swapping's not allowed we must also move the edge
1443  // we didn't move. Then we're done.
1444  if( !bAllowSwapping )
1445  {
1446  labelStruct.AdjustEdge( -iEdge, fNewTime );
1447  return;
1448  }
1449 
1450  // Swap our record of what we are dragging.
1451  std::swap( hit.mMouseOverLabelLeft, hit.mMouseOverLabelRight );
1452 }
1453 
1454 // If the index is for a real label, adjust its left and right boundary.
1455 void LabelTrack::MayMoveLabel( int iLabel, int iEdge, double fNewTime)
1456 {
1457  if( iLabel < 0 )
1458  return;
1459  mLabels[ iLabel ].MoveLabel( iEdge, fNewTime );
1460 }
1461 
1462 // Constrain function, as in processing/arduino.
1463 // returned value will be between min and max (inclusive).
1464 static int Constrain( int value, int min, int max )
1465 {
1466  wxASSERT( min <= max );
1467  int result=value;
1468  if( result < min )
1469  result=min;
1470  if( result > max )
1471  result=max;
1472  return result;
1473 }
1474 
1476 (LabelTrackHit &hit, const wxMouseEvent & evt,
1477  wxRect & r, const ZoomInfo &zoomInfo,
1478  SelectedRegion *newSel)
1479 {
1480  if(evt.LeftUp())
1481  {
1482  bool lupd = false, rupd = false;
1483  if( hit.mMouseOverLabelLeft >= 0 ) {
1484  auto &labelStruct = mLabels[ hit.mMouseOverLabelLeft ];
1485  lupd = labelStruct.updated;
1486  labelStruct.updated = false;
1487  }
1488  if( hit.mMouseOverLabelRight >= 0 ) {
1489  auto &labelStruct = mLabels[ hit.mMouseOverLabelRight ];
1490  rupd = labelStruct.updated;
1491  labelStruct.updated = false;
1492  }
1493 
1494  hit.mIsAdjustingLabel = false;
1495  hit.mMouseOverLabelLeft = -1;
1496  hit.mMouseOverLabelRight = -1;
1497  return lupd || rupd;
1498  }
1499 
1500  if(evt.Dragging())
1501  {
1502  //If we are currently adjusting a label,
1503  //just reset its value and redraw.
1504  // LL: Constrain to inside track rectangle for now. Should be changed
1505  // to allow scrolling while dragging labels
1506  int x = Constrain( evt.m_x + mxMouseDisplacement - r.x, 0, r.width);
1507 
1508  // If exactly one edge is selected we allow swapping
1509  bool bAllowSwapping =
1510  ( hit.mMouseOverLabelLeft >=0 ) !=
1511  ( hit.mMouseOverLabelRight >= 0);
1512  // If we're on the 'dot' and nowe're moving,
1513  // Though shift-down inverts that.
1514  // and if both edges the same, then we're always moving the label.
1515  bool bLabelMoving = hit.mbIsMoving;
1516  bLabelMoving ^= evt.ShiftDown();
1517  bLabelMoving |= ( hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight );
1518  double fNewX = zoomInfo.PositionToTime(x, 0);
1519  if( bLabelMoving )
1520  {
1521  MayMoveLabel( hit.mMouseOverLabelLeft, -1, fNewX );
1522  MayMoveLabel( hit.mMouseOverLabelRight, +1, fNewX );
1523  }
1524  else
1525  {
1526  MayAdjustLabel( hit, hit.mMouseOverLabelLeft, -1, bAllowSwapping, fNewX );
1527  MayAdjustLabel( hit, hit.mMouseOverLabelRight, +1, bAllowSwapping, fNewX );
1528  }
1529 
1530  if( mSelIndex >=0 )
1531  {
1532  //Set the selection region to be equal to
1533  //the NEW size of the label.
1534  *newSel = mLabels[mSelIndex].selectedRegion;
1535  }
1536  SortLabels( &hit );
1537  }
1538 
1539  return false;
1540 }
1541 
1542 void LabelTrack::HandleTextDragRelease(const wxMouseEvent & evt)
1543 {
1544  if(evt.LeftUp())
1545  {
1546 #if 0
1547  // AWD: Due to wxWidgets bug #7491 (fix not ported to 2.8 branch) we
1548  // should never write the primary selection. We can enable this block
1549  // when we move to the 3.0 branch (or if a fixed 2.8 version is released
1550  // and we can do a runtime version check)
1551 #if defined (__WXGTK__) && defined (HAVE_GTK)
1552  // On GTK, if we just dragged out a text selection, set the primary
1553  // selection
1555  wxTheClipboard->UsePrimarySelection(true);
1556  CopySelectedText();
1557  wxTheClipboard->UsePrimarySelection(false);
1558  }
1559 #endif
1560 #endif
1561 
1562  return;
1563  }
1564 
1565  if(evt.Dragging())
1566  {
1567  if (!mRightDragging)
1568  // Update drag end
1569  SetCurrentCursorPosition(evt.m_x);
1570 
1571  return;
1572  }
1573 
1574  if (evt.RightUp()) {
1575  if ((mSelIndex != -1) && OverTextBox(GetLabel(mSelIndex), evt.m_x, evt.m_y)) {
1576  // popup menu for editing
1577  ShowContextMenu();
1578  }
1579  }
1580 
1581  return;
1582 }
1583 
1585 (LabelTrackHit &hit, const wxMouseEvent & evt,
1586  const wxRect & r, const ZoomInfo &zoomInfo,
1587  SelectedRegion *WXUNUSED(newSel))
1588 {
1589  if (evt.ButtonDown())
1590  {
1591  //OverGlyph sets mMouseOverLabel to be the chosen label.
1592  OverGlyph(hit, evt.m_x, evt.m_y);
1593  hit.mIsAdjustingLabel = evt.Button(wxMOUSE_BTN_LEFT) &&
1594  ( hit.mEdge & 3 ) != 0;
1595 
1596  if (hit.mIsAdjustingLabel)
1597  {
1598  float t = 0.0;
1599  // We move if we hit the centre, we adjust one edge if we hit a chevron.
1600  // This is if we are moving just one edge.
1601  hit.mbIsMoving = (hit.mEdge & 4)!=0;
1602  // When we start dragging the label(s) we don't want them to jump.
1603  // so we calculate the displacement of the mouse from the drag center
1604  // and use that in subsequent dragging calculations. The mouse stays
1605  // at the same relative displacement throughout dragging.
1606 
1607  // However, if two label's edges are being dragged
1608  // then the displacement is relative to the initial average
1609  // position of them, and in that case there can be a jump of at most
1610  // a few pixels to bring the two label boundaries to exactly the same
1611  // position when we start dragging.
1612 
1613  // Dragging of three label edges at the same time is not supported (yet).
1614  if( ( hit.mMouseOverLabelRight >= 0 ) &&
1615  ( hit.mMouseOverLabelLeft >= 0 )
1616  )
1617  {
1618  t = (mLabels[ hit.mMouseOverLabelRight ].getT1() +
1619  mLabels[ hit.mMouseOverLabelLeft ].getT0()) / 2.0f;
1620  // If we're moving two edges, then it's a move (label size preserved)
1621  // if both edges are the same label, and it's an adjust (label sizes change)
1622  // if we're on a boundary between two different labels.
1623  hit.mbIsMoving =
1625  }
1626  else if( hit.mMouseOverLabelRight >=0)
1627  {
1628  t = mLabels[ hit.mMouseOverLabelRight ].getT1();
1629  }
1630  else if( hit.mMouseOverLabelLeft >=0)
1631  {
1632  t = mLabels[ hit.mMouseOverLabelLeft ].getT0();
1633  }
1634  mxMouseDisplacement = zoomInfo.TimeToPosition(t, r.x) - evt.m_x;
1635  }
1636  }
1637 }
1638 
1639 void LabelTrack::HandleTextClick(const wxMouseEvent & evt,
1640  const wxRect & r, const ZoomInfo &zoomInfo,
1641  SelectedRegion *newSel)
1642 {
1643  static_cast<void>(r);//compiler food.
1644  static_cast<void>(zoomInfo);//compiler food.
1645  if (evt.ButtonDown())
1646  {
1647 
1648  mSelIndex = OverATextBox(evt.m_x, evt.m_y);
1649  if (mSelIndex != -1) {
1650  auto &labelStruct = mLabels[mSelIndex];
1651  *newSel = labelStruct.selectedRegion;
1652 
1653  if (evt.LeftDown()) {
1654  // Find the NEW drag end
1655  auto position = FindCurrentCursorPosition(evt.m_x);
1656 
1657  // Anchor shift-drag at the farther end of the previous highlight
1658  // that is farther from the click, on Mac, for consistency with
1659  // its text editors, but on the others, re-use the previous
1660  // anchor.
1661  if (evt.ShiftDown()) {
1662 #ifdef __WXMAC__
1663  // Set the drag anchor at the end of the previous selection
1664  // that is farther from the NEW drag end
1665  if (abs(position - mCurrentCursorPos) >
1666  abs(position - mInitialCursorPos))
1668 #else
1669  // mInitialCursorPos remains as before
1670 #endif
1671  }
1672  else
1673  mInitialCursorPos = position;
1674 
1675  mCurrentCursorPos = position;
1676 
1677  mDrawCursor = true;
1678  mRightDragging = false;
1679  }
1680  else
1681  // Actually this might be right or middle down
1682  mRightDragging = true;
1683 
1684  // reset the highlight indicator
1685  wxRect highlightedRect;
1686  {
1687  int xpos1, xpos2;
1688  CalcHighlightXs(&xpos1, &xpos2);
1689 
1690  wxASSERT(mFontHeight >= 0); // should have been set up while drawing
1691  // the rectangle of highlighted area
1692  highlightedRect = {
1693  xpos1, labelStruct.y - mFontHeight / 2,
1694  (int)(xpos2 - xpos1 + 0.5), mFontHeight
1695  };
1696  }
1697 
1698  // Middle click on GTK: paste from primary selection
1699 #if defined(__WXGTK__) && (HAVE_GTK)
1700  if (evt.MiddleDown()) {
1701  // Check for a click outside of the selected label's text box; in this
1702  // case PasteSelectedText() will start a NEW label at the click
1703  // location
1704  if (!OverTextBox(&labelStruct, evt.m_x, evt.m_y))
1705  mSelIndex = -1;
1706  double t = zoomInfo.PositionToTime(evt.m_x, r.x);
1707  *newSel = SelectedRegion(t, t);
1708  }
1709 #endif
1710  }
1711 #if defined(__WXGTK__) && (HAVE_GTK)
1712  if (evt.MiddleDown()) {
1713  // Paste text, making a NEW label if none is selected.
1714  wxTheClipboard->UsePrimarySelection(true);
1715  PasteSelectedText(newSel->t0(), newSel->t1());
1716  wxTheClipboard->UsePrimarySelection(false);
1717  }
1718 #endif
1719  }
1720 }
1721 
1722 // Check for keys that we will process
1723 bool LabelTrack::DoCaptureKey(wxKeyEvent & event)
1724 {
1725  // Check for modifiers and only allow shift
1726  int mods = event.GetModifiers();
1727  if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
1728  return false;
1729  }
1730 
1731  // Always capture the navigation keys, if we have any labels
1732  auto code = event.GetKeyCode();
1733  if ((code == WXK_TAB || code == WXK_NUMPAD_TAB) &&
1734  !mLabels.empty())
1735  return true;
1736 
1737  if (mSelIndex >= 0) {
1738  if (IsGoodLabelEditKey(event)) {
1739  return true;
1740  }
1741  }
1742  else {
1743  bool typeToCreateLabel;
1744  gPrefs->Read(wxT("/GUI/TypeToCreateLabel"), &typeToCreateLabel, true);
1745  if (IsGoodLabelFirstKey(event) && typeToCreateLabel) {
1746  AudacityProject * pProj = GetActiveProject();
1747 
1748 
1749 // The commented out code can prevent label creation, causing bug 1551
1750 // We should only be in DoCaptureKey IF this label track has focus,
1751 // and in that case creating a Label is the expected/intended thing.
1752 #if 0
1753  // If we're playing, don't capture if the selection is the same as the
1754  // playback region (this helps prevent label track creation from
1755  // stealing unmodified kbd. shortcuts)
1756  if (pProj->GetAudioIOToken() > 0 &&
1758  {
1759  double t0, t1;
1760  pProj->GetPlayRegion(&t0, &t1);
1761  if (pProj->mViewInfo.selectedRegion.t0() == t0 &&
1762  pProj->mViewInfo.selectedRegion.t1() == t1) {
1763  return false;
1764  }
1765  }
1766 #endif
1767 
1768  // If there's a label there already don't capture
1770  pProj->mViewInfo.selectedRegion.t1()) != wxNOT_FOUND ) {
1771  return false;
1772  }
1773 
1774  return true;
1775  }
1776  }
1777 
1778  return false;
1779 }
1780 
1781 unsigned LabelTrack::CaptureKey(wxKeyEvent & event, ViewInfo &, wxWindow *)
1782 {
1783  event.Skip(!DoCaptureKey(event));
1784  return RefreshCode::RefreshNone;
1785 }
1786 
1787 unsigned LabelTrack::KeyDown(wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *WXUNUSED(pParent))
1788 {
1789  double bkpSel0 = viewInfo.selectedRegion.t0(),
1790  bkpSel1 = viewInfo.selectedRegion.t1();
1791 
1792  AudacityProject *const pProj = GetActiveProject();
1793 
1794  // Pass keystroke to labeltrack's handler and add to history if any
1795  // updates were done
1796  if (OnKeyDown(viewInfo.selectedRegion, event)) {
1797  pProj->PushState(_("Modified Label"),
1798  _("Label Edit"),
1800  }
1801 
1802  // Make sure caret is in view
1803  int x;
1804  if (CalcCursorX(&x)) {
1805  pProj->GetTrackPanel()->ScrollIntoView(x);
1806  }
1807 
1808  // If selection modified, refresh
1809  // Otherwise, refresh track display if the keystroke was handled
1810  if (bkpSel0 != viewInfo.selectedRegion.t0() ||
1811  bkpSel1 != viewInfo.selectedRegion.t1())
1812  return RefreshCode::RefreshAll;
1813  else if (!event.GetSkipped())
1814  return RefreshCode::RefreshCell;
1815 
1816  return RefreshCode::RefreshNone;
1817 }
1818 
1819 unsigned LabelTrack::Char(wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *)
1820 {
1821  double bkpSel0 = viewInfo.selectedRegion.t0(),
1822  bkpSel1 = viewInfo.selectedRegion.t1();
1823  // Pass keystroke to labeltrack's handler and add to history if any
1824  // updates were done
1825 
1826  AudacityProject *const pProj = GetActiveProject();
1827 
1828  if (OnChar(viewInfo.selectedRegion, event))
1829  pProj->PushState(_("Modified Label"),
1830  _("Label Edit"),
1832 
1833  // If selection modified, refresh
1834  // Otherwise, refresh track display if the keystroke was handled
1835  if (bkpSel0 != viewInfo.selectedRegion.t0() ||
1836  bkpSel1 != viewInfo.selectedRegion.t1())
1837  return RefreshCode::RefreshAll;
1838  else if (!event.GetSkipped())
1839  return RefreshCode::RefreshCell;
1840 
1841  return RefreshCode::RefreshNone;
1842 }
1843 
1845 bool LabelTrack::OnKeyDown(SelectedRegion &newSel, wxKeyEvent & event)
1846 {
1847  // Only track true changes to the label
1848  bool updated = false;
1849 
1850  // Cache the keycode
1851  int keyCode = event.GetKeyCode();
1852  const int mods = event.GetModifiers();
1853 
1854  // Check for modifiers and only allow shift
1855  if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
1856  event.Skip();
1857  return updated;
1858  }
1859 
1860  // All editing keys are only active if we're currently editing a label
1861  if (mSelIndex >= 0) {
1862  auto &labelStruct = mLabels[mSelIndex];
1863  auto &title = labelStruct.title;
1864  switch (keyCode) {
1865 
1866  case WXK_BACK:
1867  {
1868  int len = title.Length();
1869 
1870  //IF the label is not blank THEN get rid of a letter or letters according to cursor position
1871  if (len > 0)
1872  {
1873  // IF there are some highlighted letters, THEN DELETE them
1876  else
1877  {
1878  // DELETE one letter
1879  if (mCurrentCursorPos > 0) {
1880  title.Remove(mCurrentCursorPos-1, 1);
1882  }
1883  }
1884  }
1885  else
1886  {
1887  // ELSE no text in text box, so DELETE whole label.
1889  }
1891  updated = true;
1892  }
1893  break;
1894 
1895  case WXK_DELETE:
1896  case WXK_NUMPAD_DELETE:
1897  {
1898  int len = title.Length();
1899 
1900  //If the label is not blank get rid of a letter according to cursor position
1901  if (len > 0)
1902  {
1903  // if there are some highlighted letters, DELETE them
1906  else
1907  {
1908  // DELETE one letter
1909  if (mCurrentCursorPos < len) {
1910  title.Remove(mCurrentCursorPos, 1);
1911  }
1912  }
1913  }
1914  else
1915  {
1916  // DELETE whole label if no text in text box
1918  }
1920  updated = true;
1921  }
1922  break;
1923 
1924  case WXK_HOME:
1925  case WXK_NUMPAD_HOME:
1926  // Move cursor to beginning of label
1927  mCurrentCursorPos = 0;
1928  if (mods == wxMOD_SHIFT)
1929  ;
1930  else
1932  break;
1933 
1934  case WXK_END:
1935  case WXK_NUMPAD_END:
1936  // Move cursor to end of label
1937  mCurrentCursorPos = (int)title.length();
1938  if (mods == wxMOD_SHIFT)
1939  ;
1940  else
1942  break;
1943 
1944  case WXK_LEFT:
1945  case WXK_NUMPAD_LEFT:
1946  // Moving cursor left
1947  if (mCurrentCursorPos > 0) {
1949  if (mods == wxMOD_SHIFT)
1950  ;
1951  else
1954  }
1955  break;
1956 
1957  case WXK_RIGHT:
1958  case WXK_NUMPAD_RIGHT:
1959  // Moving cursor right
1960  if (mCurrentCursorPos < (int)title.length()) {
1962  if (mods == wxMOD_SHIFT)
1963  ;
1964  else
1967  }
1968  break;
1969 
1970  case WXK_RETURN:
1971  case WXK_NUMPAD_ENTER:
1972 
1973  case WXK_ESCAPE:
1974  if (mRestoreFocus >= 0) {
1975  auto track = *GetActiveProject()->GetTracks()->Any()
1976  .begin().advance(mRestoreFocus);
1977  if (track)
1979  mRestoreFocus = -1;
1980  }
1981  mSelIndex = -1;
1982  break;
1983 
1984  case WXK_TAB:
1985  case WXK_NUMPAD_TAB:
1986  if (event.ShiftDown()) {
1987  mSelIndex--;
1988  } else {
1989  mSelIndex++;
1990  }
1991 
1992  mSelIndex = (mSelIndex + (int)mLabels.size()) % (int)mLabels.size(); // wrap round if necessary
1993  {
1994  LabelStruct &newLabel = mLabels[mSelIndex];
1995  mCurrentCursorPos = newLabel.title.Length();
1997  //Set the selection region to be equal to the selection bounds of the tabbed-to label.
1998  newSel = newLabel.selectedRegion;
1999  }
2000  break;
2001 
2002  case '\x10': // OSX
2003  case WXK_MENU:
2004  case WXK_WINDOWS_MENU:
2005  ShowContextMenu();
2006  break;
2007 
2008  default:
2009  if (!IsGoodLabelEditKey(event)) {
2010  event.Skip();
2011  }
2012  break;
2013  }
2014  }
2015  else
2016  {
2017  switch (keyCode) {
2018 
2019  case WXK_TAB:
2020  case WXK_NUMPAD_TAB:
2021  if (!mLabels.empty()) {
2022  int len = (int) mLabels.size();
2023  if (event.ShiftDown()) {
2024  mSelIndex = len - 1;
2025  if (newSel.t0() > mLabels[0].getT0()) {
2026  while (mSelIndex >= 0 &&
2027  mLabels[mSelIndex].getT0() > newSel.t0()) {
2028  mSelIndex--;
2029  }
2030  }
2031  } else {
2032  mSelIndex = 0;
2033  if (newSel.t0() < mLabels[len - 1].getT0()) {
2034  while (mSelIndex < len &&
2035  mLabels[mSelIndex].getT0() < newSel.t0()) {
2036  mSelIndex++;
2037  }
2038  }
2039  }
2040 
2041  if (mSelIndex >= 0 && mSelIndex < len) {
2042  const auto &labelStruct = mLabels[mSelIndex];
2043  mCurrentCursorPos = labelStruct.title.Length();
2045  //Set the selection region to be equal to the selection bounds of the tabbed-to label.
2046  newSel = labelStruct.selectedRegion;
2047  }
2048  else {
2049  mSelIndex = -1;
2050  }
2051  }
2052  break;
2053 
2054  default:
2055  if (!IsGoodLabelFirstKey(event)) {
2056  event.Skip();
2057  }
2058  break;
2059  }
2060  }
2061 
2062  // Make sure the caret is visible
2063  mDrawCursor = true;
2064 
2065  return updated;
2066 }
2067 
2070 bool LabelTrack::OnChar(SelectedRegion &WXUNUSED(newSel), wxKeyEvent & event)
2071 {
2072  // Check for modifiers and only allow shift.
2073  //
2074  // We still need to check this or we will eat the top level menu accelerators
2075  // on Windows if our capture or key down handlers skipped the event.
2076  const int mods = event.GetModifiers();
2077  if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
2078  event.Skip();
2079  return false;
2080  }
2081 
2082  // Only track true changes to the label
2083  bool updated = false;
2084 
2085  // Cache the character
2086  wxChar charCode = event.GetUnicodeKey();
2087 
2088  // Skip if it's not a valid unicode character or a control character
2089  if (charCode == 0 || wxIscntrl(charCode)) {
2090  event.Skip();
2091  return false;
2092  }
2093 
2094  // If we've reached this point and aren't currently editing, add NEW label
2095  if (mSelIndex < 0) {
2096  // Don't create a NEW label for a space
2097  if (wxIsspace(charCode)) {
2098  event.Skip();
2099  return false;
2100  }
2101  bool useDialog;
2103  gPrefs->Read(wxT("/Gui/DialogForNameNewLabel"), &useDialog, false);
2104  if (useDialog) {
2105  wxString title;
2106  if (DialogForLabelName(
2107  *p, p->mViewInfo.selectedRegion, charCode, title) ==
2108  wxID_CANCEL) {
2109  return false;
2110  }
2111  SetSelected(true);
2112  AddLabel(p->mViewInfo.selectedRegion, title, -2);
2113  p->PushState(_("Added label"), _("Label"));
2114  return false;
2115  }
2116  else {
2117  SetSelected(true);
2119  p->PushState(_("Added label"), _("Label"));
2120  }
2121  }
2122 
2123  //
2124  // Now we are definitely in a label; append the incoming character
2125  //
2126 
2127  auto &labelStruct = mLabels[mSelIndex];
2128  auto &title = labelStruct.title;
2129 
2130  // Test if cursor is in the end of string or not
2133 
2134  if (mCurrentCursorPos < (int)title.length()) {
2135  // Get substring on the righthand side of cursor
2136  wxString rightPart = title.Mid(mCurrentCursorPos);
2137  // Set title to substring on the lefthand side of cursor
2138  title = title.Left(mCurrentCursorPos);
2139  //append charcode
2140  title += charCode;
2141  //append the right part substring
2142  title += rightPart;
2143  }
2144  else
2145  //append charCode
2146  title += charCode;
2147 
2148  //moving cursor position forward
2150  updated = true;
2151 
2152  // Make sure the caret is visible
2153  mDrawCursor = true;
2154 
2155  return updated;
2156 }
2157 
2159 {
2160  wxWindow *parent = wxWindow::FindFocus();
2161 
2162  // Bug 2044. parent can be nullptr after a context switch.
2163  if( !parent )
2164  parent = GetActiveProject();
2165 
2166  if( parent )
2167  {
2168  wxMenu menu;
2169  menu.Bind(wxEVT_MENU, &LabelTrack::OnContextMenu, this);
2170 
2171  menu.Append(OnCutSelectedTextID, _("Cu&t"));
2172  menu.Append(OnCopySelectedTextID, _("&Copy"));
2173  menu.Append(OnPasteSelectedTextID, _("&Paste"));
2174  menu.Append(OnDeleteSelectedLabelID, _("&Delete Label"));
2175  menu.Append(OnEditSelectedLabelID, _("&Edit..."));
2176 
2177  menu.Enable(OnCutSelectedTextID, IsTextSelected());
2178  menu.Enable(OnCopySelectedTextID, IsTextSelected());
2180  menu.Enable(OnDeleteSelectedLabelID, true);
2181  menu.Enable(OnEditSelectedLabelID, true);
2182 
2183  wxASSERT(mSelIndex >= 0);
2184  const LabelStruct *ls = GetLabel(mSelIndex);
2185 
2186  wxClientDC dc(parent);
2187 
2188  if (msFont.Ok())
2189  {
2190  dc.SetFont(msFont);
2191  }
2192 
2193  int x = 0;
2194  bool success = CalcCursorX(&x);
2195  wxASSERT(success);
2196  static_cast<void>(success); // Suppress unused variable warning if debug mode is disabled
2197 
2198  parent->PopupMenu(&menu, x, ls->y + (mIconHeight / 2) - 1);
2199  }
2200 }
2201 
2202 void LabelTrack::OnContextMenu(wxCommandEvent & evt)
2203 {
2205 
2206  switch (evt.GetId())
2207  {
2209  case OnCutSelectedTextID:
2210  if (CutSelectedText())
2211  {
2212  p->PushState(_("Modified Label"),
2213  _("Label Edit"),
2215  }
2216  break;
2217 
2219  case OnCopySelectedTextID:
2220  CopySelectedText();
2221  break;
2222 
2224  case OnPasteSelectedTextID:
2225  if (PasteSelectedText(p->GetSel0(), p->GetSel1()))
2226  {
2227  p->PushState(_("Modified Label"),
2228  _("Label Edit"),
2230  }
2231  break;
2232 
2234  case OnDeleteSelectedLabelID: {
2235  int ndx = GetLabelIndex(p->GetSel0(), p->GetSel1());
2236  if (ndx != -1)
2237  {
2238  DeleteLabel(ndx);
2239  p->PushState(_("Deleted Label"),
2240  _("Label Edit"),
2242  }
2243  }
2244  break;
2245 
2246  case OnEditSelectedLabelID: {
2247  int ndx = GetLabelIndex(p->GetSel0(), p->GetSel1());
2248  if (ndx != -1)
2249  DoEditLabels(*p, this, ndx);
2250  }
2251  break;
2252  }
2253 }
2254 
2256 {
2257  wxString left, right;
2258 
2259  int init = mInitialCursorPos;
2260  int cur = mCurrentCursorPos;
2261  if (init > cur)
2262  std::swap(init, cur);
2263 
2264  auto &labelStruct = mLabels[mSelIndex];
2265  auto &title = labelStruct.title;
2266 
2267  if (init > 0)
2268  left = title.Left(init);
2269 
2270  if (cur < (int)title.Length())
2271  right = title.Mid(cur);
2272 
2273  title = left + right;
2274  mInitialCursorPos = mCurrentCursorPos = left.Length();
2275 }
2276 
2278 {
2279  mSelIndex = -1;
2280 }
2281 
2283 {
2284  return (mSelIndex >= 0 && mSelIndex < (int)mLabels.size());
2285 }
2286 
2288 void LabelTrack::Export(wxTextFile & f) const
2289 {
2290  // PRL: to do: export other selection fields
2291  for (auto &labelStruct: mLabels)
2292  labelStruct.Export(f);
2293 }
2294 
2296 void LabelTrack::Import(wxTextFile & in)
2297 {
2298  int lines = in.GetLineCount();
2299 
2300  mLabels.clear();
2301  mLabels.reserve(lines);
2302 
2303  //Currently, we expect a tag file to have two values and a label
2304  //on each line. If the second token is not a number, we treat
2305  //it as a single-value label.
2306  bool error = false;
2307  for (int index = 0; index < lines;) {
2308  try {
2309  // Let LabelStruct::Import advance index
2310  LabelStruct l { LabelStruct::Import(in, index) };
2311  mLabels.push_back(l);
2312  }
2313  catch(const LabelStruct::BadFormatException&) { error = true; }
2314  }
2315  if (error)
2316  ::AudacityMessageBox( _("One or more saved labels could not be read.") );
2317  SortLabels();
2318 }
2319 
2320 bool LabelTrack::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
2321 {
2322  if (!wxStrcmp(tag, wxT("label"))) {
2323 
2324  SelectedRegion selectedRegion;
2325  wxString title;
2326 
2327  // loop through attrs, which is a null-terminated list of
2328  // attribute-value pairs
2329  while(*attrs) {
2330  const wxChar *attr = *attrs++;
2331  const wxChar *value = *attrs++;
2332 
2333  if (!value)
2334  break;
2335 
2336  const wxString strValue = value;
2337  // Bug 1905 was about long label strings.
2338  if (!XMLValueChecker::IsGoodLongString(strValue))
2339  {
2340  return false;
2341  }
2342 
2343  if (selectedRegion.HandleXMLAttribute(attr, value, wxT("t"), wxT("t1")))
2344  ;
2345  else if (!wxStrcmp(attr, wxT("title")))
2346  title = strValue;
2347 
2348  } // while
2349 
2350  // Handle files created by Audacity 1.1. Labels in Audacity 1.1
2351  // did not have separate start- and end-times.
2352  // PRL: this superfluous now, given class SelectedRegion's internal
2353  // consistency guarantees
2354  //if (selectedRegion.t1() < 0)
2355  // selectedRegion.collapseToT0();
2356 
2357  LabelStruct l { selectedRegion, title };
2358  mLabels.push_back(l);
2359 
2360  return true;
2361  }
2362  else if (!wxStrcmp(tag, wxT("labeltrack"))) {
2363  long nValue = -1;
2364  while (*attrs) {
2365  const wxChar *attr = *attrs++;
2366  const wxChar *value = *attrs++;
2367 
2368  if (!value)
2369  return true;
2370 
2371  const wxString strValue = value;
2372  if (!wxStrcmp(attr, wxT("name")) && XMLValueChecker::IsGoodString(strValue))
2373  mName = strValue;
2374  else if (!wxStrcmp(attr, wxT("numlabels")) &&
2375  XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
2376  {
2377  if (nValue < 0)
2378  {
2379  wxLogWarning(wxT("Project shows negative number of labels: %d"), nValue);
2380  return false;
2381  }
2382  mLabels.clear();
2383  mLabels.reserve(nValue);
2384  }
2385  else if (!wxStrcmp(attr, wxT("height")) &&
2386  XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
2387  SetHeight(nValue);
2388  else if (!wxStrcmp(attr, wxT("minimized")) &&
2389  XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
2390  SetMinimized(nValue != 0);
2391  else if (!wxStrcmp(attr, wxT("isSelected")) &&
2392  XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
2393  this->SetSelected(nValue != 0);
2394  }
2395 
2396  return true;
2397  }
2398 
2399  return false;
2400 }
2401 
2403 {
2404  if (!wxStrcmp(tag, wxT("label")))
2405  return this;
2406  else
2407  return NULL;
2408 }
2409 
2410 void LabelTrack::WriteXML(XMLWriter &xmlFile) const
2411 // may throw
2412 {
2413  int len = mLabels.size();
2414 
2415  xmlFile.StartTag(wxT("labeltrack"));
2416  xmlFile.WriteAttr(wxT("name"), mName);
2417  xmlFile.WriteAttr(wxT("numlabels"), len);
2418  xmlFile.WriteAttr(wxT("height"), this->GetActualHeight());
2419  xmlFile.WriteAttr(wxT("minimized"), this->GetMinimized());
2420  xmlFile.WriteAttr(wxT("isSelected"), this->GetSelected());
2421 
2422  for (auto &labelStruct: mLabels) {
2423  xmlFile.StartTag(wxT("label"));
2424  labelStruct.getSelectedRegion()
2425  .WriteXMLAttributes(xmlFile, wxT("t"), wxT("t1"));
2426  // PRL: to do: write other selection fields
2427  xmlFile.WriteAttr(wxT("title"), labelStruct.title);
2428  xmlFile.EndTag(wxT("label"));
2429  }
2430 
2431  xmlFile.EndTag(wxT("labeltrack"));
2432 }
2433 
2434 #if LEGACY_PROJECT_FILE_SUPPORT
2435 bool LabelTrack::Load(wxTextFile * in, DirManager * dirManager)
2436 {
2437  if (in->GetNextLine() != wxT("NumMLabels"))
2438  return false;
2439 
2440  unsigned long len;
2441  if (!(in->GetNextLine().ToULong(&len)))
2442  return false;
2443 
2444  mLabels.clear();
2445  mLabels.reserve(len);
2446 
2447  for (int i = 0; i < len; i++) {
2448  double t0;
2449  if (!Internat::CompatibleToDouble(in->GetNextLine(), &t0))
2450  return false;
2451  // Legacy file format does not include label end-times.
2452  // PRL: nothing NEW to do, legacy file support
2453  mLabels.push_back(LabelStruct {
2454  SelectedRegion{ t0, t0 }, in->GetNextLine()
2455  });
2456  }
2457 
2458  if (in->GetNextLine() != wxT("MLabelsEnd"))
2459  return false;
2460  SortLabels();
2461  return true;
2462 }
2463 
2464 bool LabelTrack::Save(wxTextFile * out, bool overwrite)
2465 {
2466  out->AddLine(wxT("NumMLabels"));
2467  int len = mLabels.size();
2468  out->AddLine(wxString::Format(wxT("%d"), len));
2469 
2470  for (auto pLabel : mLabels) {
2471  const auto &labelStruct = *pLabel;
2472  out->AddLine(wxString::Format(wxT("%lf"), labelStruct.selectedRegion.mT0));
2473  out->AddLine(labelStruct.title);
2474  }
2475  out->AddLine(wxT("MLabelsEnd"));
2476 
2477  return true;
2478 }
2479 #endif
2480 
2481 Track::Holder LabelTrack::Cut(double t0, double t1)
2482 {
2483  auto tmp = Copy(t0, t1);
2484 
2485  Clear(t0, t1);
2486 
2487  return tmp;
2488 }
2489 
2490 #if 0
2491 Track::Holder LabelTrack::SplitCut(double t0, double t1)
2492 {
2493  // SplitCut() == Copy() + SplitDelete()
2494 
2495  Track::Holder tmp = Copy(t0, t1);
2496 
2497  if (!SplitDelete(t0, t1))
2498  return {};
2499 
2500  return tmp;
2501 }
2502 #endif
2503 
2504 Track::Holder LabelTrack::Copy(double t0, double t1, bool) const
2505 {
2506  auto tmp = std::make_unique<LabelTrack>(GetDirManager());
2507  const auto lt = static_cast<LabelTrack*>(tmp.get());
2508 
2509  for (auto &labelStruct: mLabels) {
2510  LabelStruct::TimeRelations relation =
2511  labelStruct.RegionRelation(t0, t1, this);
2512  if (relation == LabelStruct::SURROUNDS_LABEL) {
2513  LabelStruct l {
2514  labelStruct.selectedRegion,
2515  labelStruct.getT0() - t0,
2516  labelStruct.getT1() - t0,
2517  labelStruct.title
2518  };
2519  lt->mLabels.push_back(l);
2520  }
2521  else if (relation == LabelStruct::WITHIN_LABEL) {
2522  LabelStruct l {
2523  labelStruct.selectedRegion,
2524  0,
2525  t1-t0,
2526  labelStruct.title
2527  };
2528  lt->mLabels.push_back(l);
2529  }
2530  else if (relation == LabelStruct::BEGINS_IN_LABEL) {
2531  LabelStruct l {
2532  labelStruct.selectedRegion,
2533  0,
2534  labelStruct.getT1() - t0,
2535  labelStruct.title
2536  };
2537  lt->mLabels.push_back(l);
2538  }
2539  else if (relation == LabelStruct::ENDS_IN_LABEL) {
2540  LabelStruct l {
2541  labelStruct.selectedRegion,
2542  labelStruct.getT0() - t0,
2543  t1 - t0,
2544  labelStruct.title
2545  };
2546  lt->mLabels.push_back(l);
2547  }
2548  }
2549  lt->mClipLen = (t1 - t0);
2550 
2551  // This std::move is needed to "upcast" the pointer type
2552  return std::move(tmp);
2553 }
2554 
2555 
2556 bool LabelTrack::PasteOver(double t, const Track * src)
2557 {
2558  auto result = src->TypeSwitch< bool >( [&](const LabelTrack *sl) {
2559  int len = mLabels.size();
2560  int pos = 0;
2561 
2562  while (pos < len && mLabels[pos].getT0() < t)
2563  pos++;
2564 
2565  for (auto &labelStruct: sl->mLabels) {
2566  LabelStruct l {
2567  labelStruct.selectedRegion,
2568  labelStruct.getT0() + t,
2569  labelStruct.getT1() + t,
2570  labelStruct.title
2571  };
2572  mLabels.insert(mLabels.begin() + pos++, l);
2573  }
2574 
2575  return true;
2576  } );
2577 
2578  if (! result )
2579  // THROW_INCONSISTENCY_EXCEPTION; // ?
2580  (void)0;// intentionally do nothing
2581 
2582  return result;
2583 }
2584 
2585 void LabelTrack::Paste(double t, const Track *src)
2586 {
2587  bool bOk = src->TypeSwitch< bool >( [&](const LabelTrack *lt) {
2588  double shiftAmt = lt->mClipLen > 0.0 ? lt->mClipLen : lt->GetEndTime();
2589 
2590  ShiftLabelsOnInsert(shiftAmt, t);
2591  PasteOver(t, src);
2592 
2593  return true;
2594  } );
2595 
2596  if ( !bOk )
2597  // THROW_INCONSISTENCY_EXCEPTION; // ?
2598  (void)0;// intentionally do nothing
2599 }
2600 
2601 // This repeats the labels in a time interval a specified number of times.
2602 bool LabelTrack::Repeat(double t0, double t1, int n)
2603 {
2604  // Sanity-check the arguments
2605  if (n < 0 || t1 < t0)
2606  return false;
2607 
2608  double tLen = t1 - t0;
2609 
2610  // Insert space for the repetitions
2611  ShiftLabelsOnInsert(tLen * n, t1);
2612 
2613  // mLabels may resize as we iterate, so use subscripting
2614  for (unsigned int i = 0; i < mLabels.size(); ++i)
2615  {
2616  LabelStruct::TimeRelations relation =
2617  mLabels[i].RegionRelation(t0, t1, this);
2618  if (relation == LabelStruct::SURROUNDS_LABEL)
2619  {
2620  // Label is completely inside the selection; duplicate it in each
2621  // repeat interval
2622  unsigned int pos = i; // running label insertion position in mLabels
2623 
2624  for (int j = 1; j <= n; j++)
2625  {
2626  const LabelStruct &label = mLabels[i];
2627  LabelStruct l {
2628  label.selectedRegion,
2629  label.getT0() + j * tLen,
2630  label.getT1() + j * tLen,
2631  label.title
2632  };
2633 
2634  // Figure out where to insert
2635  while (pos < mLabels.size() &&
2636  mLabels[pos].getT0() < l.getT0())
2637  pos++;
2638  mLabels.insert(mLabels.begin() + pos, l);
2639  }
2640  }
2641  else if (relation == LabelStruct::BEGINS_IN_LABEL)
2642  {
2643  // Label ends inside the selection; ShiftLabelsOnInsert() hasn't touched
2644  // it, and we need to extend it through to the last repeat interval
2645  mLabels[i].selectedRegion.moveT1(n * tLen);
2646  }
2647 
2648  // Other cases have already been handled by ShiftLabelsOnInsert()
2649  }
2650 
2651  return true;
2652 }
2653 
2654 void LabelTrack::Silence(double t0, double t1)
2655 {
2656  int len = mLabels.size();
2657 
2658  // mLabels may resize as we iterate, so use subscripting
2659  for (int i = 0; i < len; ++i) {
2660  LabelStruct::TimeRelations relation =
2661  mLabels[i].RegionRelation(t0, t1, this);
2662  if (relation == LabelStruct::WITHIN_LABEL)
2663  {
2664  // Split label around the selection
2665  const LabelStruct &label = mLabels[i];
2666  LabelStruct l {
2667  label.selectedRegion,
2668  t1,
2669  label.getT1(),
2670  label.title
2671  };
2672 
2673  mLabels[i].selectedRegion.setT1(t0);
2674 
2675  // This might not be the right place to insert, but we sort at the end
2676  ++i;
2677  mLabels.insert(mLabels.begin() + i, l);
2678  }
2679  else if (relation == LabelStruct::ENDS_IN_LABEL)
2680  {
2681  // Beginning of label to selection end
2682  mLabels[i].selectedRegion.setT0(t1);
2683  }
2684  else if (relation == LabelStruct::BEGINS_IN_LABEL)
2685  {
2686  // End of label to selection beginning
2687  mLabels[i].selectedRegion.setT1(t0);
2688  }
2689  else if (relation == LabelStruct::SURROUNDS_LABEL)
2690  {
2691  DeleteLabel( i );
2692  len--;
2693  i--;
2694  }
2695  }
2696 
2697  SortLabels();
2698 }
2699 
2700 void LabelTrack::InsertSilence(double t, double len)
2701 {
2702  for (auto &labelStruct: mLabels) {
2703  double t0 = labelStruct.getT0();
2704  double t1 = labelStruct.getT1();
2705  if (t0 >= t)
2706  t0 += len;
2707 
2708  if (t1 >= t)
2709  t1 += len;
2710  labelStruct.selectedRegion.setTimes(t0, t1);
2711  }
2712 }
2713 
2715 {
2716  return mLabels.size();
2717 }
2718 
2719 const LabelStruct *LabelTrack::GetLabel(int index) const
2720 {
2721  return &mLabels[index];
2722 }
2723 
2724 int LabelTrack::GetLabelIndex(double t, double t1)
2725 {
2726  //We'd have liked to have times in terms of samples,
2727  //because then we're doing an intrger comparison.
2728  //Never mind. Instead we look for near enough.
2729  //This level of (in)accuracy is only a problem if we
2730  //deal with sounds in the MHz range.
2731  const double delta = 1.0e-7;
2732  { int i = -1; for (auto &labelStruct : mLabels) { ++i;
2733  if( fabs( labelStruct.getT0() - t ) > delta )
2734  continue;
2735  if( fabs( labelStruct.getT1() - t1 ) > delta )
2736  continue;
2737  return i;
2738  }}
2739 
2740  return wxNOT_FOUND;
2741 }
2742 
2743 
2744 // restoreFocus of -1 is the default, and sets the focus to this label.
2745 // restoreFocus of -2 or other value leaves the focus unchanged.
2746 // restoreFocus >= 0 will later cause focus to move to that track.
2747 int LabelTrack::AddLabel(const SelectedRegion &selectedRegion,
2748  const wxString &title, int restoreFocus)
2749 {
2750  LabelStruct l { selectedRegion, title };
2751  mInitialCursorPos = mCurrentCursorPos = title.length();
2752 
2753  int len = mLabels.size();
2754  int pos = 0;
2755 
2756  while (pos < len && mLabels[pos].getT0() < selectedRegion.t0())
2757  pos++;
2758 
2759  mLabels.insert(mLabels.begin() + pos, l);
2760 
2761  // restoreFocus is -2 e.g. from Nyquist label creation, when we should not
2762  // even lose the focus and open the label to edit in the first place.
2763  // -1 means we don't need to restore it to anywhere.
2764  // 0 or above is the track to restore to afetr editing the label is complete.
2765  if( restoreFocus >= -1 )
2766  mSelIndex = pos;
2767 
2768  // Make sure the caret is visible
2769  //
2770  // LLL: The cursor will not be drawn when the first label
2771  // is added since mDrawCursor will be false. Presumably,
2772  // if the user adds a label, then a cursor should be drawn
2773  // to indicate that typing is expected.
2774  //
2775  // If the label is added during actions like import, then the
2776  // mDrawCursor flag will be reset once the action is complete.
2777  mDrawCursor = true;
2778 
2779  mRestoreFocus = restoreFocus;
2780 
2781  return pos;
2782 }
2783 
2785 {
2786  wxASSERT((index < (int)mLabels.size()));
2787  mLabels.erase(mLabels.begin() + index);
2788  // IF we've deleted the selected label
2789  // THEN set no label selected.
2790  if( mSelIndex== index )
2791  {
2792  mSelIndex = -1;
2793  mCurrentCursorPos = 1;
2794  }
2795  // IF we removed a label before the selected label
2796  // THEN the NEW selected label number is one less.
2797  else if( index < mSelIndex )
2798  {
2799  mSelIndex--;
2800  }
2801 }
2802 
2803 wxBitmap & LabelTrack::GetGlyph( int i)
2804 {
2805  return theTheme.Bitmap( i + bmpLabelGlyph0);
2806 }
2807 
2808 // This one XPM spec is used to generate a number of
2809 // different wxIcons.
2810 /* XPM */
2811 static const char *const GlyphXpmRegionSpec[] = {
2812 /* columns rows colors chars-per-pixel */
2813 "15 23 7 1",
2814 /* Default colors, with first color transparent */
2815 ". c none",
2816 "2 c black",
2817 "3 c black",
2818 "4 c black",
2819 "5 c #BEBEF0",
2820 "6 c #BEBEF0",
2821 "7 c #BEBEF0",
2822 /* pixels */
2823 "...............",
2824 "...............",
2825 "...............",
2826 "....333.444....",
2827 "...3553.4774...",
2828 "...3553.4774...",
2829 "..35553.47774..",
2830 "..35522222774..",
2831 ".3552666662774.",
2832 ".3526666666274.",
2833 "355266666662774",
2834 "355266666662774",
2835 "355266666662774",
2836 ".3526666666274.",
2837 ".3552666662774.",
2838 "..35522222774..",
2839 "..35553.47774..",
2840 "...3553.4774...",
2841 "...3553.4774...",
2842 "....333.444....",
2843 "...............",
2844 "...............",
2845 "..............."
2846 };
2847 
2864 {
2865  int iConfig;
2866  int iHighlight;
2867  int index;
2868  const int nSpecRows =
2869  sizeof( GlyphXpmRegionSpec )/sizeof( GlyphXpmRegionSpec[0]);
2870  const char *XmpBmp[nSpecRows];
2871 
2872  // The glyphs are declared static wxIcon; so we only need
2873  // to create them once, no matter how many LabelTracks.
2874  if( mbGlyphsReady )
2875  return;
2876 
2877  // We're about to tweak the basic color spec to get 12 variations.
2878  for( iConfig=0;iConfig<NUM_GLYPH_CONFIGS;iConfig++)
2879  {
2880  for( iHighlight=0;iHighlight<NUM_GLYPH_HIGHLIGHTS;iHighlight++)
2881  {
2882  index = iConfig + NUM_GLYPH_CONFIGS * iHighlight;
2883  // Copy the basic spec...
2884  memcpy( XmpBmp, GlyphXpmRegionSpec, sizeof( GlyphXpmRegionSpec ));
2885  // The higlighted region (if any) is white...
2886  if( iHighlight==1 ) XmpBmp[5]="5 c #FFFFFF";
2887  if( iHighlight==2 ) XmpBmp[6]="6 c #FFFFFF";
2888  if( iHighlight==3 ) XmpBmp[7]="7 c #FFFFFF";
2889  // For left or right arrow the other side of the glyph
2890  // is the transparent color.
2891  if( iConfig==0) { XmpBmp[3]="3 c none"; XmpBmp[5]="5 c none"; }
2892  if( iConfig==1) { XmpBmp[4]="4 c none"; XmpBmp[7]="7 c none"; }
2893  // Create the icon from the tweaked spec.
2894  mBoundaryGlyphs[index] = wxBitmap(XmpBmp);
2895  // Create the mask
2896  // SetMask takes ownership
2897  mBoundaryGlyphs[index].SetMask(safenew wxMask(mBoundaryGlyphs[index], wxColour(192, 192, 192)));
2898  }
2899  }
2900 
2901  mIconWidth = mBoundaryGlyphs[0].GetWidth();
2902  mIconHeight = mBoundaryGlyphs[0].GetHeight();
2903  mTextHeight = mIconHeight; // until proved otherwise...
2904  // The icon should have an odd width so that the
2905  // line goes exactly down the middle.
2906  wxASSERT( (mIconWidth %2)==1);
2907 
2908  mbGlyphsReady=true;
2909 }
2910 
2912 bool LabelTrack::IsGoodLabelFirstKey(const wxKeyEvent & evt)
2913 {
2914  int keyCode = evt.GetKeyCode();
2915  return (keyCode < WXK_START
2916  && keyCode != WXK_SPACE && keyCode != WXK_DELETE && keyCode != WXK_RETURN) ||
2917  (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) ||
2918  (keyCode >= WXK_NUMPAD_EQUAL && keyCode <= WXK_NUMPAD_DIVIDE) ||
2919 #if defined(__WXMAC__)
2920  (keyCode > WXK_RAW_CONTROL) ||
2921 #endif
2922  (keyCode > WXK_WINDOWS_MENU);
2923 }
2924 
2926 bool LabelTrack::IsGoodLabelEditKey(const wxKeyEvent & evt)
2927 {
2928  int keyCode = evt.GetKeyCode();
2929 
2930  // Accept everything outside of WXK_START through WXK_COMMAND, plus the keys
2931  // within that range that are usually printable, plus the ones we use for
2932  // keyboard navigation.
2933  return keyCode < WXK_START ||
2934  (keyCode >= WXK_END && keyCode < WXK_UP) ||
2935  (keyCode == WXK_RIGHT) ||
2936  (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) ||
2937  (keyCode >= WXK_NUMPAD_SPACE && keyCode <= WXK_NUMPAD_ENTER) ||
2938  (keyCode >= WXK_NUMPAD_HOME && keyCode <= WXK_NUMPAD_END) ||
2939  (keyCode >= WXK_NUMPAD_DELETE && keyCode <= WXK_NUMPAD_DIVIDE) ||
2940 #if defined(__WXMAC__)
2941  (keyCode > WXK_RAW_CONTROL) ||
2942 #endif
2943  (keyCode > WXK_WINDOWS_MENU);
2944 }
2945 
2951 {
2952  const auto begin = mLabels.begin();
2953  const auto nn = (int)mLabels.size();
2954  int i = 1;
2955  while (true)
2956  {
2957  // Find the next disorder
2958  while (i < nn && mLabels[i - 1].getT0() <= mLabels[i].getT0())
2959  ++i;
2960  if (i >= nn)
2961  break;
2962 
2963  // Where must element i sink to? At most i - 1, maybe less
2964  int j = i - 2;
2965  while( (j >= 0) && (mLabels[j].getT0() > mLabels[i].getT0()) )
2966  --j;
2967  ++j;
2968 
2969  // Now fix the disorder
2970  std::rotate(
2971  begin + j,
2972  begin + i,
2973  begin + i + 1
2974  );
2975 
2976  // Various indices need to be updated with the moved items...
2977  auto update = [=](int &index) {
2978  if( index <= i ) {
2979  if( index == i )
2980  index = j;
2981  else if( index >= j)
2982  ++index;
2983  }
2984  };
2985  if ( pHit ) {
2986  update( pHit->mMouseOverLabelLeft );
2987  update( pHit->mMouseOverLabelRight );
2988  }
2989  update(mSelIndex);
2990  }
2991 }
2992 
2993 wxString LabelTrack::GetTextOfLabels(double t0, double t1) const
2994 {
2995  bool firstLabel = true;
2996  wxString retVal;
2997 
2998  for (auto &labelStruct: mLabels) {
2999  if (labelStruct.getT0() >= t0 &&
3000  labelStruct.getT1() <= t1)
3001  {
3002  if (!firstLabel)
3003  retVal += '\t';
3004  firstLabel = false;
3005  retVal += labelStruct.title;
3006  }
3007  }
3008 
3009  return retVal;
3010 }
3011 
3013 {
3014  int i = -1;
3015 
3016  if (!mLabels.empty()) {
3017  int len = (int) mLabels.size();
3018  if (miLastLabel >= 0 && miLastLabel + 1 < len
3019  && currentRegion.t0() == mLabels[miLastLabel].getT0()
3020  && currentRegion.t0() == mLabels[miLastLabel + 1].getT0() ) {
3021  i = miLastLabel + 1;
3022  }
3023  else {
3024  i = 0;
3025  if (currentRegion.t0() < mLabels[len - 1].getT0()) {
3026  while (i < len &&
3027  mLabels[i].getT0() <= currentRegion.t0()) {
3028  i++;
3029  }
3030  }
3031  }
3032  }
3033 
3034  miLastLabel = i;
3035  return i;
3036 }
3037 
3038  int LabelTrack::FindPrevLabel(const SelectedRegion& currentRegion)
3039 {
3040  int i = -1;
3041 
3042  if (!mLabels.empty()) {
3043  int len = (int) mLabels.size();
3044  if (miLastLabel > 0 && miLastLabel < len
3045  && currentRegion.t0() == mLabels[miLastLabel].getT0()
3046  && currentRegion.t0() == mLabels[miLastLabel - 1].getT0() ) {
3047  i = miLastLabel - 1;
3048  }
3049  else {
3050  i = len - 1;
3051  if (currentRegion.t0() > mLabels[0].getT0()) {
3052  while (i >=0 &&
3053  mLabels[i].getT0() >= currentRegion.t0()) {
3054  i--;
3055  }
3056  }
3057  }
3058  }
3059 
3060  miLastLabel = i;
3061  return i;
3062 }
3063 
3064 #include "LabelDialog.h"
3065 
3067 (AudacityProject &project, LabelTrack *lt, int index)
3068 {
3069  auto format = project.GetSelectionFormat(),
3070  freqFormat = project.GetFrequencySelectionFormatName();
3071  auto tracks = project.GetTracks();
3072  auto trackFactory = project.GetTrackFactory();
3073  auto rate = project.GetRate();
3074  auto &viewInfo = project.GetViewInfo();
3075 
3076  LabelDialog dlg(&project, *trackFactory, tracks,
3077  lt, index,
3078  viewInfo, rate,
3079  format, freqFormat);
3080 #ifdef __WXGTK__
3081  dlg.Raise();
3082 #endif
3083 
3084  if (dlg.ShowModal() == wxID_OK) {
3085  project.PushState(_("Edited labels"), _("Label"));
3086  project.RedrawProject();
3087  }
3088 }
3089 
3091  AudacityProject &project,
3092  const SelectedRegion& region, const wxString& initialValue, wxString& value)
3093 {
3094  auto trackPanel = project.GetTrackPanel();
3095  auto &viewInfo = project.GetViewInfo();
3096 
3097  wxPoint position = trackPanel->FindTrackRect(trackPanel->GetFocusedTrack(), false).GetBottomLeft();
3098  // The start of the text in the text box will be roughly in line with the label's position
3099  // if it's a point label, or the start of its region if it's a region label.
3100  position.x += trackPanel->GetLabelWidth()
3101  + std::max(0, static_cast<int>(viewInfo.TimeToPosition(region.t0())))
3102  -40;
3103  position.y += 2; // just below the bottom of the track
3104  position = trackPanel->ClientToScreen(position);
3105  AudacityTextEntryDialog dialog{ &project,
3106  _("Name:"),
3107  _("New label"),
3108  initialValue,
3109  wxOK | wxCANCEL,
3110  position };
3111 
3112  // keep the dialog within Audacity's window, so that the dialog is always fully visible
3113  wxRect dialogScreenRect = dialog.GetScreenRect();
3114  wxRect projScreenRect = project.GetScreenRect();
3115  wxPoint max = projScreenRect.GetBottomRight() + wxPoint{ -dialogScreenRect.width, -dialogScreenRect.height };
3116  if (dialogScreenRect.x > max.x) {
3117  position.x = max.x;
3118  dialog.Move(position);
3119  }
3120  if (dialogScreenRect.y > max.y) {
3121  position.y = max.y;
3122  dialog.Move(position);
3123  }
3124 
3125  dialog.SetInsertionPointEnd(); // because, by default, initial text is selected
3126  int status = dialog.ShowModal();
3127  if (status != wxID_CANCEL) {
3128  value = dialog.GetValue();
3129  value.Trim(true).Trim(false);
3130  }
3131 
3132  return status;
3133 }
static int mIconHeight
Definition: LabelTrack.h:289
bool IsTextSelected()
Definition: LabelTrack.cpp:984
void getXPos(wxDC &dc, int *xPos1, int cursorPos) const
Definition: LabelTrack.cpp:712
double GetOffset() const override
static int Constrain(int value, int min, int max)
bool OnChar(SelectedRegion &sel, wxKeyEvent &event)
void RedrawProject(const bool bForceWaveTracks=false)
Definition: Project.cpp:1466
std::shared_ptr< LabelTrack > GetTrack() const
AudacityPrefs * gPrefs
Definition: Prefs.cpp:73
void WriteXML(XMLWriter &xmlFile) const override
void Unselect()
static wxBrush labelSelectedBrush
Definition: AColor.h:129
int mCurrentCursorPos
Definition: LabelTrack.h:296
AUDACITY_DLL_API Theme theTheme
Definition: Theme.cpp:209
std::unique_ptr< LabelTrack > Holder
Definition: LabelTrack.h:164
void ChangeLabelsOnReverse(double b, double e)
Definition: LabelTrack.cpp:214
Creates and manages BlockFile objects.
Definition: DirManager.h:52
static void DoEditLabels(AudacityProject &project, LabelTrack *lt=nullptr, int index=-1)
void SetOffset(double dOffset) override
Definition: LabelTrack.cpp:146
double t0() const
static bool IsTextClipSupported()
ViewInfo is used mainly to hold the zooming, selection and scroll information. It also has some statu...
Definition: ViewInfo.h:143
double GetSel0() const
Definition: Project.h:221
bool DoCaptureKey(wxKeyEvent &event)
void InsertSilence(double t, double len) override
SelectedRegion selectedRegion
Definition: ViewInfo.h:162
void HandleTextDragRelease(const wxMouseEvent &evt)
R TypeSwitch(const Functions &...functions)
Definition: Track.h:630
unsigned CaptureKey(wxKeyEvent &event, ViewInfo &viewInfo, wxWindow *pParent) override
void Draw(TrackPanelDrawingContext &context, const wxRect &r) const
Definition: LabelTrack.cpp:785
bool IsGoodLabelFirstKey(const wxKeyEvent &evt)
Returns true for keys we capture to start a label.
int FindCurrentCursorPosition(int xPos)
Definition: LabelTrack.cpp:914
void SetCurrentCursorPosition(int xPos)
Set the cursor position according to x position of mouse.
Definition: LabelTrack.cpp:961
TimeRelations RegionRelation(double reg_t0, double reg_t1, const LabelTrack *parent=NULL) const
void MayMoveLabel(int iLabel, int iEdge, double fNewTime)
const NumericFormatId & GetFrequencySelectionFormatName() const
Definition: Project.cpp:1724
static wxBrush uglyBrush
Definition: AColor.h:148
Track::Holder Duplicate() const override
bool Repeat(double t0, double t1, int n)
void DeleteLabel(int index)
bool CalcCursorX(int *x) const
Definition: LabelTrack.cpp:724
void calculateFontHeight(wxDC &dc) const
Definition: LabelTrack.cpp:966
static wxBrush labelTextNormalBrush
Definition: AColor.h:126
void SetHeight(int h)
Definition: Track.cpp:192
wxString label
Definition: Tags.cpp:733
bool CopySelectedText()
wxString title
Definition: LabelTrack.h:92
UIHandlePtr Target()
void ComputeLayout(const wxRect &r, const ZoomInfo &zoomInfo) const
Definition: LabelTrack.cpp:462
double PositionToTime(wxInt64 position, wxInt64 origin=0, bool ignoreFisheye=false) const
Definition: ViewInfo.cpp:49
int mInitialCursorPos
current cursor position
Definition: LabelTrack.h:297
int xText
Pixel position of right hand glyph.
Definition: LabelTrack.h:98
auto Any() -> TrackIterRange< TrackType >
Definition: Track.h:1246
bool updated
Pixel position of label.
Definition: LabelTrack.h:101
const std::shared_ptr< DirManager > mDirManager
Definition: Track.h:1626
const int MAX_NUM_ROWS
Definition: LabelTrack.h:108
bool OverTextBox(const LabelStruct *pLabel, int x, int y) const
int GetAudioIOToken() const
Definition: Project.cpp:1523
int AudacityMessageBox(const wxString &message, const wxString &caption=AudacityMessageBoxCaptionStr(), long style=wxOK|wxCENTRE, wxWindow *parent=NULL, int x=wxDefaultCoord, int y=wxDefaultCoord)
Definition: ErrorDialog.h:92
bool IsStreamActive() const
Returns true if the audio i/o is running at all, but not during cleanup.
Definition: AudioIO.cpp:2749
double GetStartTime() const override
void Clear(double t0, double t1) override
Definition: LabelTrack.cpp:152
TimeRelations
Relationships between selection region and labels.
Definition: LabelTrack.h:73
int GetNumLabels() const
int mMouseOverLabelRight
Keeps track of which left label the mouse is currently over.
void ScaleLabels(double b, double e, double change)
Definition: LabelTrack.cpp:229
Wrap wxTextEntryDialog so that caption IS translatable.
Definition: ErrorDialog.h:108
void SetMinimized(bool isMinimized)
Definition: Track.cpp:222
void DrawHighlight(wxDC &dc, int xPos1, int xPos2, int charHeight) const
Draws text-selected region within the label.
Definition: LabelTrack.cpp:700
double t1() const
void MayAdjustLabel(LabelTrackHit &hit, int iLabel, int iEdge, bool bAllowSwapping, double fNewTime)
double GetEndTime() const override
int x1
Pixel position of left hand glyph.
Definition: LabelTrack.h:97
double getDuration() const
Definition: LabelTrack.h:60
int miLastLabel
Definition: LabelTrack.h:308
void ShowContextMenu()
void Export(wxTextFile &file) const
const std::shared_ptr< DirManager > & GetDirManager() const
Definition: Track.h:400
static int mIconWidth
Definition: LabelTrack.h:290
static void ResetFont()
Definition: LabelTrack.cpp:297
LabelArray mLabels
Displacement of mouse cursor from the centre being dragged.
Definition: LabelTrack.h:287
void ScrollIntoView(double pos)
void OverGlyph(LabelTrackHit &hit, int x, int y) const
#define safenew
Definition: Audacity.h:230
static bool IsGoodInt(const wxString &strInt)
Check that the supplied string can be converted to a long (32bit) integer.
virtual void SetSelected(bool s)
Definition: Track.cpp:102
A LabelTrack is a Track that holds labels (LabelStruct).
Definition: LabelTrack.h:113
void Export(wxTextFile &f) const
Export labels including label start and end-times.
static const char *const GlyphXpmRegionSpec[]
void SetFocusedTrack(Track *t)
wxString GetDefaultName() const
Definition: Track.h:378
double f0() const
wxBitmap & Bitmap(int iIndex)
Definition: Theme.cpp:1244
double AdjustTimeStampOnScale(double t, double b, double e, double change)
Definition: LabelTrack.cpp:238
Contains declarations for TimeWarper, IdentityTimeWarper, ShiftTimeWarper, LinearTimeWarper, LinearInputRateSlideTimeWarper, LinearOutputRateSlideTimeWarper, LinearInputInverseRateTimeWarper, GeometricInputRateTimeWarper, GeometricOutputRateTimeWarper classes.
int x
width of the text in pixels.
Definition: LabelTrack.h:96
AudacityProject provides the main window, with tools and tracks contained within it.
Definition: Project.h:174
A LabelStruct holds information for ONE label in a LabelTrack.
Definition: LabelTrack.h:44
bool mRightDragging
initial cursor position
Definition: LabelTrack.h:299
void SetSelected(bool s) override
void DrawBackgroundWithSelection(TrackPanelDrawingContext &contex, const wxRect &rect, const Track *track, const wxBrush &selBrush, const wxBrush &unselBrush, bool useSelection=true)
static wxString ToString(double numberToConvert, int digitsAfterDecimalPoint=-1)
Convert a number to a string, always uses the dot as decimal separator.
Definition: Internat.cpp:138
int mSelIndex
Definition: LabelTrack.h:285
static wxFont GetFont(const wxString &faceName, int size=DefaultFontSize)
Definition: LabelTrack.cpp:286
void DrawTextBox(wxDC &dc, const wxRect &r) const
Definition: LabelTrack.cpp:649
int format
Definition: ExportPCM.cpp:56
static bool IsGoodString(const wxString &str)
void ComputeTextPosition(const wxRect &r, int index) const
Definition: LabelTrack.cpp:318
static bool CompatibleToDouble(const wxString &stringToConvert, double *result)
Convert a string to a number.
Definition: Internat.cpp:122
Defines a selected portion of a project.
wxString GetTextOfLabels(double t0, double t1) const
const int NUM_GLYPH_CONFIGS
Definition: LabelTrack.h:106
bool HandleXMLAttribute(const wxChar *attr, const wxChar *value, const wxChar *legacyT0Name=sDefaultT0Name, const wxChar *legacyT1Name=sDefaultT1Name)
bool setT1(double t, bool maySwap=true)
static int mFontHeight
Definition: LabelTrack.h:295
static wxBitmap & GetGlyph(int i)
static wxBrush labelTextEditBrush
Definition: AColor.h:127
int FindPrevLabel(const SelectedRegion &currentSelection)
const LabelStruct * GetLabel(int index) const
Dialog for editing labels.
Definition: LabelDialog.h:34
void WarpLabels(const TimeWarper &warper)
Definition: LabelTrack.cpp:257
LabelStruct(const SelectedRegion &region, const wxString &aTitle)
Definition: LabelTrack.cpp:529
static LabelStruct Import(wxTextFile &file, int &index)
static bool IsGoodLongString(const wxString &str)
void Silence(double t0, double t1) override
Fundamental data object of Audacity, placed in the TrackPanel. Classes derived form it include the Wa...
Definition: Track.h:191
bool mDrawCursor
flag to tell if it's a valid dragging
Definition: LabelTrack.h:300
Track::Holder Copy(double t0, double t1, bool forClipboard=true) const override
static int DialogForLabelName(AudacityProject &project, const SelectedRegion &region, const wxString &initialValue, wxString &value)
This class is an interface which should be implemented by classes which wish to be able to load and s...
Definition: XMLTagHandler.h:72
ViewInfo mViewInfo
Definition: Project.h:555
wxInt64 TimeToPosition(double time, wxInt64 origin=0, bool ignoreFisheye=false) const
STM: Converts a project time to screen x position.
Definition: ViewInfo.cpp:59
int width
Text of the label.
Definition: LabelTrack.h:93
int min(int a, int b)
bool PasteOver(double t, const Track *src)
void HandleTextClick(const wxMouseEvent &evt, const wxRect &r, const ZoomInfo &zoomInfo, SelectedRegion *newSel)
unsigned KeyDown(wxKeyEvent &event, ViewInfo &viewInfo, wxWindow *pParent) override
void RemoveSelectedText()
static void Line(wxDC &dc, wxCoord x1, wxCoord y1, wxCoord x2, wxCoord y2)
Definition: AColor.cpp:122
bool PasteSelectedText(double sel0, double sel1)
static wxBitmap mBoundaryGlyphs[NUM_GLYPH_CONFIGS *NUM_GLYPH_HIGHLIGHTS]
Definition: LabelTrack.h:293
Track::Holder Cut(double t0, double t1) override
int mRestoreFocus
cursor or not
Definition: LabelTrack.h:302
double mOffset
Definition: Track.h:354
bool HandleXMLTag(const wxChar *tag, const wxChar **attrs) override
LabelTrack(const std::shared_ptr< DirManager > &projDirManager)
Definition: LabelTrack.cpp:105
virtual ~LabelTrack()
Definition: LabelTrack.cpp:142
void SetDefaultName(const wxString &n)
Definition: Track.h:379
void OnContextMenu(wxCommandEvent &evt)
bool HandleGlyphDragRelease(LabelTrackHit &hit, const wxMouseEvent &evt, wxRect &r, const ZoomInfo &zoomInfo, SelectedRegion *newSel)
bool setTimes(double t0, double t1)
int GetLabelIndex(double t, double t1)
_("Move Track &Down")+wxT("\t")+(GetActiveProject() -> GetCommandManager() ->GetKeyFromName(wxT("TrackMoveDown")).Raw()), OnMoveTrack) POPUP_MENU_ITEM(OnMoveTopID, _("Move Track to &Top")+wxT("\t")+(GetActiveProject() ->GetCommandManager() ->GetKeyFromName(wxT("TrackMoveTop")).Raw()), OnMoveTrack) POPUP_MENU_ITEM(OnMoveBottomID, _("Move Track to &Bottom")+wxT("\t")+(GetActiveProject() ->GetCommandManager() ->GetKeyFromName(wxT("TrackMoveBottom")).Raw()), OnMoveTrack)#define SET_TRACK_NAME_PLUGIN_SYMBOLclass SetTrackNameCommand:public AudacityCommand
AudioIO * gAudioIO
Definition: AudioIO.cpp:491
static bool mbGlyphsReady
Definition: LabelTrack.h:292
void CalcHighlightXs(int *x1, int *x2) const
Definition: LabelTrack.cpp:741
void PushState(const wxString &desc, const wxString &shortDesc)
Definition: Project.cpp:4625
static TrackArtist * Get(TrackPanelDrawingContext &)
void SetName(const wxString &n)
Definition: Track.cpp:94
unsigned Char(wxKeyEvent &event, ViewInfo &viewInfo, wxWindow *pParent) override
double getT1() const
Definition: LabelTrack.h:62
void SortLabels(LabelTrackHit *pHit=nullptr)
AUDACITY_DLL_API AudacityProject * GetActiveProject()
Definition: Project.cpp:311
void MoveLabel(int iEdge, double fNewTime)
static wxFont msFont
Definition: LabelTrack.h:321
SelectedRegion selectedRegion
Definition: LabelTrack.h:91
void CreateCustomGlyphs()
void Paste(double t, const Track *src) override
const int NUM_GLYPH_HIGHLIGHTS
Definition: LabelTrack.h:107
int FindNextLabel(const SelectedRegion &currentSelection)
virtual double Warp(double originalTime) const =0
Transforms one point in time to another point. For example, a time stretching effect might use one to...
Definition: TimeWarper.h:61
const NumericFormatId & GetSelectionFormat() const
Definition: Project.cpp:1762
void HandleGlyphClick(LabelTrackHit &hit, const wxMouseEvent &evt, const wxRect &r, const ZoomInfo &zoomInfo, SelectedRegion *newSel)
void Import(wxTextFile &f)
Import labels, handling files with or without end-times.
double f1() const
TrackPanel * GetTrackPanel()
Definition: Project.h:324
bool setFrequencies(double f0, double f1)
void ShiftLabelsOnInsert(double length, double pt)
Definition: LabelTrack.cpp:201
bool OnKeyDown(SelectedRegion &sel, wxKeyEvent &event)
KeyEvent is called for every keypress when over the label track.
wxColour & Colour(int iIndex)
Definition: Theme.cpp:1225
double GetRate() const
Definition: Project.h:216
bool setT0(double t, bool maySwap=true)
std::unique_ptr< Track > Holder
Definition: Track.h:369
static wxPen labelSurroundPen
Definition: AColor.h:134
double GetSel1() const
Definition: Project.h:222
XMLTagHandler * HandleXMLChild(const wxChar *tag) override
static int mTextHeight
Definition: LabelTrack.h:291
TrackList * GetTracks()
Definition: Project.h:209
double getT0() const
Definition: LabelTrack.h:61
void DrawText(wxDC &dc, const wxRect &r) const
Definition: LabelTrack.cpp:624
Base class for XMLFileWriter and XMLStringWriter that provides the general functionality for creating...
Definition: XMLWriter.h:22
int y
Pixel position of left hand side of text box.
Definition: LabelTrack.h:99
void GetPlayRegion(double *playRegionStart, double *playRegionEnd)
Definition: Project.cpp:5101
int OverATextBox(int xx, int yy) const
void ResetFlags()
Definition: LabelTrack.cpp:269
std::unique_ptr< LabelTrack > NewLabelTrack()
Definition: LabelTrack.cpp:100
bool HasSelection() const
int AddLabel(const SelectedRegion &region, const wxString &title=wxT(""), int restoreFocus=-1)
void DrawLines(wxDC &dc, const wxRect &r) const
Definition: LabelTrack.cpp:563
const ViewInfo & GetViewInfo() const
Definition: Project.h:224
void DrawGlyphs(wxDC &dc, const wxRect &r, int GlyphLeft, int GlyphRight) const
Definition: LabelTrack.cpp:601
void RestoreFlags(const Flags &flags)
Definition: LabelTrack.cpp:277
static const int UndefinedFrequency
bool CutSelectedText()
Definition: LabelTrack.cpp:995
TrackFactory * GetTrackFactory()
Definition: Project.cpp:1513
LabelTrackHit mHit
static const int DefaultFontSize
Definition: LabelTrack.h:151
bool AdjustEdge(int iEdge, double fNewTime)
bool mbIsMoving
Keeps track of which right label the mouse is currently over.
bool IsGoodLabelEditKey(const wxKeyEvent &evt)
This returns true for keys we capture for label editing.
static wxBrush labelUnselectedBrush
Definition: AColor.h:128
wxString mName
Definition: Track.h:206