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