diff --git a/src/LabelDialog.cpp b/src/LabelDialog.cpp index 5121262a0..22a6031a3 100644 --- a/src/LabelDialog.cpp +++ b/src/LabelDialog.cpp @@ -36,6 +36,7 @@ #include "Project.h" #include "ProjectWindow.h" #include "ViewInfo.h" +#include "tracks/labeltrack/ui/LabelTrackView.h" #include "widgets/AudacityMessageBox.h" #include "widgets/ErrorDialog.h" #include "widgets/Grid.h" @@ -420,8 +421,8 @@ bool LabelDialog::TransferDataFromWindow() return false; // Add the label to it - lt->AddLabel(rd.selectedRegion, rd.title, -2); - lt->Unselect(); + lt->AddLabel(rd.selectedRegion, rd.title); + LabelTrackView::Get( *lt ).SetSelectedIndex( -1 ); } return true; @@ -723,7 +724,7 @@ void LabelDialog::OnExport(wxCommandEvent & WXUNUSED(event)) for (i = 0; i < cnt; i++) { RowData &rd = mData[i]; - lt->AddLabel(rd.selectedRegion, rd.title,-2); + lt->AddLabel(rd.selectedRegion, rd.title); } // Export them and clean diff --git a/src/LabelTrack.cpp b/src/LabelTrack.cpp index 16573477e..2741d55ee 100644 --- a/src/LabelTrack.cpp +++ b/src/LabelTrack.cpp @@ -33,74 +33,22 @@ for drawing different aspects of the label and its text box. #include "Experimental.h" -#include "tracks/labeltrack/ui/LabelTrackView.h" - #include #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include "DirManager.h" #include "Prefs.h" -#include "RefreshCode.h" -#include "AllThemeResources.h" -#include "AColor.h" -#include "Project.h" -#include "ProjectHistory.h" -#include "ProjectSettings.h" #include "ProjectFileIORegistry.h" -#include "ProjectWindow.h" -#include "TrackArtist.h" -#include "TrackPanel.h" -#include "UndoManager.h" -#include "ViewInfo.h" -#include "commands/CommandManager.h" #include "effects/TimeWarper.h" #include "widgets/AudacityMessageBox.h" -#include "widgets/ErrorDialog.h" -enum -{ - OnCutSelectedTextID = 1, // OSX doesn't like a 0 menu id - OnCopySelectedTextID, - OnPasteSelectedTextID, - OnDeleteSelectedLabelID, - OnEditSelectedLabelID, -}; - -wxFont LabelTrack::msFont; - -// static member variables. -bool LabelTrack::mbGlyphsReady=false; - -/// We have several variants of the icons (highlighting). -/// The icons are draggable, and you can drag one boundary -/// or all boundaries at the same timecode depending on whether you -/// click the centre (for all) or the arrow part (for one). -/// Currently we have twelve variants but we're only using six. -wxBitmap LabelTrack::mBoundaryGlyphs[ NUM_GLYPH_CONFIGS * NUM_GLYPH_HIGHLIGHTS ]; -int LabelTrack::mIconHeight; -int LabelTrack::mIconWidth; -int LabelTrack::mTextHeight; - -int LabelTrack::mFontHeight=-1; +wxDEFINE_EVENT(EVT_LABELTRACK_ADDITION, LabelTrackEvent); +wxDEFINE_EVENT(EVT_LABELTRACK_DELETION, LabelTrackEvent); +wxDEFINE_EVENT(EVT_LABELTRACK_PERMUTED, LabelTrackEvent); static ProjectFileIORegistry::Entry registerFactory{ wxT( "labeltrack" ), @@ -118,34 +66,30 @@ LabelTrack::Holder TrackFactory::NewLabelTrack() LabelTrack::LabelTrack(const std::shared_ptr &projDirManager): Track(projDirManager), - mSelIndex(-1), - mRestoreFocus(-1), mClipLen(0.0), miLastLabel(-1) { SetDefaultName(_("Label Track")); SetName(GetDefaultName()); - - ResetFont(); - CreateCustomGlyphs(); - - // reset flags - ResetFlags(); } LabelTrack::LabelTrack(const LabelTrack &orig) : Track(orig), - mSelIndex(-1), mClipLen(0.0) { for (auto &original: orig.mLabels) { LabelStruct l { original.selectedRegion, original.title }; mLabels.push_back(l); } - mSelIndex = orig.mSelIndex; +} - // reset flags - ResetFlags(); +void LabelTrack::SetLabel( size_t iLabel, const LabelStruct &newLabel ) +{ + if( iLabel >= mLabels.size() ) { + wxASSERT( false ); + mLabels.resize( iLabel + 1 ); + } + mLabels[ iLabel ] = newLabel; } LabelTrack::~LabelTrack() @@ -275,285 +219,6 @@ void LabelTrack::WarpLabels(const TimeWarper &warper) { SortLabels(); } -void LabelTrack::ResetFlags() -{ - mInitialCursorPos = 1; - mCurrentCursorPos = 1; - mRightDragging = false; - mDrawCursor = false; -} - -void LabelTrack::RestoreFlags( const Flags& flags ) -{ - mInitialCursorPos = flags.mInitialCursorPos; - mCurrentCursorPos = flags.mCurrentCursorPos; - mSelIndex = flags.mSelIndex; - mRightDragging = flags.mRightDragging; - mDrawCursor = flags.mDrawCursor; -} - -wxFont LabelTrack::GetFont(const wxString &faceName, int size) -{ - wxFontEncoding encoding; - if (faceName.empty()) - encoding = wxFONTENCODING_DEFAULT; - else - encoding = wxFONTENCODING_SYSTEM; - return wxFont(size, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, - wxFONTWEIGHT_NORMAL, false, faceName, encoding); -} - -void LabelTrack::ResetFont() -{ - mFontHeight = -1; - wxString facename = gPrefs->Read(wxT("/GUI/LabelFontFacename"), wxT("")); - int size = gPrefs->Read(wxT("/GUI/LabelFontSize"), DefaultFontSize); - msFont = GetFont(facename, size); -} - -/// ComputeTextPosition is 'smart' about where to display -/// the label text. -/// -/// The text must be displayed between its endpoints x and x1 -/// We'd also like it centered between them, and we'd like it on -/// screen. It isn't always possible to achieve all of this, -/// so we do the best we can. -/// -/// This function has a number of tests and adjustments to the -/// text start position. The tests later in the function will -/// take priority over the ones earlier, so because centering -/// is the first thing we do, it's the first thing we lose if -/// we can't do everything we want to. -void LabelTrack::ComputeTextPosition(const wxRect & r, int index) const -{ - auto &labelStruct = mLabels[index]; - - // xExtra is extra space - // between the text and the endpoints. - const int xExtra=mIconWidth; - int x = labelStruct.x; // left endpoint - int x1 = labelStruct.x1; // right endpoint. - int width = labelStruct.width; - - int xText; // This is where the text will end up. - - // Will the text all fit at this zoom? - bool bTooWideForScreen = width > (r.width-2*xExtra); -// bool bSimpleCentering = !bTooWideForScreen; - bool bSimpleCentering = false; - - //TODO (possibly): - // Add configurable options as to when to use simple - // and when complex centering. - // - // Simple centering does its best to keep the text - // centered between the label limits. - // - // Complex centering positions the text proportionally - // to how far we are through the label. - // - // If we add preferences for this, we want to be able to - // choose separately whether: - // a) Wide text labels centered simple/complex. - // b) Other text labels centered simple/complex. - // - - if( bSimpleCentering ) - { - // Center text between the two end points. - xText = (x+x1-width)/2; - } - else - { - // Calculate xText position to make text line - // scroll sideways evenly as r moves right. - - // xText is a linear function of r.x. - // These variables are used to compute that function. - int rx0,rx1,xText0,xText1; - - // Since we will be using a linear function, - // we should blend smoothly between left and right - // aligned text as r, the 'viewport' moves. - if( bTooWideForScreen ) - { - rx0=x; // when viewport at label start. - xText0=x+xExtra; // text aligned left. - rx1=x1-r.width; // when viewport end at label end - xText1=x1-(width+xExtra); // text aligned right. - } - else - { - // when label start + width + extra spacing at viewport end.. - rx0=x-r.width+width+2*xExtra; - // ..text aligned left. - xText0=x+xExtra; - // when viewport start + width + extra spacing at label end.. - rx1=x1-(width+2*xExtra); - // ..text aligned right. - xText1=x1-(width+xExtra); - } - - if( rx1 > rx0 ) // Avoid divide by zero case. - { - // Compute the blend between left and right aligned. - - // Don't use: - // - // xText = xText0 + ((xText1-xText0)*(r.x-rx0))/(rx1-rx0); - // - // The problem with the above is that it integer-oveflows at - // high zoom. - - // Instead use: - xText = xText0 + (int)((xText1-xText0)*(((float)(r.x-rx0))/(rx1-rx0))); - } - else - { - // Avoid divide by zero by reverting to - // simple centering. - // - // We could also fall into this case if x and x1 - // are swapped, in which case we'll end up - // left aligned anyway because code later on - // will catch that. - xText = (x+x1-width)/2; - } - } - - // Is the text now appearing partly outside r? - bool bOffLeft = xText < r.x+xExtra; - bool bOffRight = xText > r.x+r.width-width-xExtra; - - // IF any part of the text is offscreen - // THEN we may bring it back. - if( bOffLeft == bOffRight ) - { - //IF both sides on screen, THEN nothing to do. - //IF both sides off screen THEN don't do - //anything about it. - //(because if we did, you'd never get to read - //all the text by scrolling). - } - else if( bOffLeft != bTooWideForScreen) - { - // IF we're off on the left, OR we're - // too wide for the screen and off on the right - // (only) THEN align left. - xText = r.x+xExtra; - } - else - { - // We're off on the right, OR we're - // too wide and off on the left (only) - // SO align right. - xText =r.x+r.width-width-xExtra; - } - - // But if we've taken the text out from its endpoints - // we must move it back so that it's between the endpoints. - - // We test the left end point last because the - // text might not even fit between the endpoints (at this - // zoom factor), and in that case we'd like to position - // the text at the left end point. - if( xText > (x1-width-xExtra)) - xText=(x1-width-xExtra); - if( xText < x+xExtra ) - xText=x+xExtra; - - labelStruct.xText = xText; -} - -/// ComputeLayout determines which row each label -/// should be placed on, and reserves space for it. -/// Function assumes that the labels are sorted. -void LabelTrack::ComputeLayout(const wxRect & r, const ZoomInfo &zoomInfo) const -{ - int xUsed[MAX_NUM_ROWS]; - - int iRow; - // Rows are the 'same' height as icons or as the text, - // whichever is taller. - const int yRowHeight = wxMax(mTextHeight,mIconHeight)+3;// pixels. - // Extra space at end of rows. - // We allow space for one half icon at the start and two - // half icon widths for extra x for the text frame. - // [we don't allow half a width space for the end icon since it is - // allowed to be obscured by the text]. - const int xExtra= (3 * mIconWidth)/2; - - bool bAvoidName = false; - const int nRows = wxMin((r.height / yRowHeight) + 1, MAX_NUM_ROWS); - if( nRows > 2 ) - bAvoidName = gPrefs->ReadBool(wxT("/GUI/ShowTrackNameInWaveform"), false); - // Initially none of the rows have been used. - // So set a value that is less than any valid value. - { - // Bug 502: With dragging left of zeros, labels can be in - // negative space. So set least possible value as starting point. - const int xStart = INT_MIN; - for (auto &x : xUsed) - x = xStart; - } - int nRowsUsed=0; - - { int i = -1; for (auto &labelStruct : mLabels) { ++i; - const int x = zoomInfo.TimeToPosition(labelStruct.getT0(), r.x); - const int x1 = zoomInfo.TimeToPosition(labelStruct.getT1(), r.x); - int y = r.y; - - labelStruct.x=x; - labelStruct.x1=x1; - labelStruct.y=-1;// -ve indicates nothing doing. - iRow=0; - // Our first preference is a row that ends where we start. - // (This is to encourage merging of adjacent label boundaries). - while( (iRow= nRowsUsed ) - { - iRow=0; - while( (iRow x )) - iRow++; - } - // IF we found such a row THEN record a valid position. - if( iRow= nRowsUsed ) - nRowsUsed=iRow+1; - // Record the position for this label - y= r.y + iRow * yRowHeight +(yRowHeight/2)+1; - labelStruct.y=y; - // On this row we have used up to max of end marker and width. - // Plus also allow space to show the start icon and - // some space for the text frame. - xUsed[iRow]=x+labelStruct.width+xExtra; - if( xUsed[iRow] < x1 ) xUsed[iRow]=x1; - ComputeTextPosition( r, i ); - } - }} -} - LabelStruct::LabelStruct(const SelectedRegion ®ion, const wxString& aTitle) : selectedRegion(region) @@ -584,561 +249,6 @@ LabelStruct::LabelStruct(const SelectedRegion ®ion, y = 0; } -/// Draw vertical lines that go exactly through the position -/// of the start or end of a label. -/// @param dc the device context -/// @param r the LabelTrack rectangle. -void LabelStruct::DrawLines(wxDC & dc, const wxRect & r) const -{ - // How far out from the centre line should the vertical lines - // start, i.e. what is the y position of the icon? - // We adjust this so that the line encroaches on the icon - // slightly (there is white space in the design). - const int yIconStart = y - (LabelTrack::mIconHeight /2)+1+(LabelTrack::mTextHeight+3)/2; - const int yIconEnd = yIconStart + LabelTrack::mIconHeight-2; - - // If y is positive then it is the center line for the - // Label. - if( y >= 0 ) - { - if((x >= r.x) && (x <= (r.x+r.width))) - { - // Draw line above and below left dragging widget. - AColor::Line(dc, x, r.y, x, yIconStart - 1); - AColor::Line(dc, x, yIconEnd, x, r.y + r.height); - } - if((x1 >= r.x) && (x1 <= (r.x+r.width))) - { - // Draw line above and below right dragging widget. - AColor::Line(dc, x1, r.y, x1, yIconStart - 1); - AColor::Line(dc, x1, yIconEnd, x1, r.y + r.height); - } - } - else - { - // Draw the line, even though the widget is off screen - AColor::Line(dc, x, r.y, x, r.y + r.height); - AColor::Line(dc, x1, r.y, x1, r.y + r.height); - } -} - -/// DrawGlyphs draws the wxIcons at the start and end of a label. -/// @param dc the device context -/// @param r the LabelTrack rectangle. -void LabelStruct::DrawGlyphs - (wxDC & dc, const wxRect & r, int GlyphLeft, int GlyphRight) const -{ - const int xHalfWidth=LabelTrack::mIconWidth/2; - const int yStart=y-LabelTrack::mIconHeight/2+(LabelTrack::mTextHeight+3)/2; - - // If y == -1, nothing to draw - if( y == -1 ) - return; - - if((x >= r.x) && (x <= (r.x+r.width))) - dc.DrawBitmap(LabelTrack::GetGlyph(GlyphLeft), x-xHalfWidth,yStart, true); - // The extra test commented out here would suppress right hand markers - // when they overlap the left hand marker (e.g. zoomed out) or to the left. - if((x1 >= r.x) && (x1 <= (r.x+r.width)) /*&& (x1>x+LabelTrack::mIconWidth)*/) - dc.DrawBitmap(LabelTrack::GetGlyph(GlyphRight), x1-xHalfWidth,yStart, true); -} - -/// Draw the text of the label and also draw -/// a long thin rectangle for its full extent -/// from x to x1 and a rectangular frame -/// behind the text itself. -/// @param dc the device context -/// @param r the LabelTrack rectangle. -void LabelStruct::DrawText(wxDC & dc, const wxRect & r) const -{ - //If y is positive then it is the center line for the - //text we are about to draw. - //if it isn't, nothing to draw. - - if( y == -1 ) - return; - - // Draw frame for the text... - // We draw it half an icon width left of the text itself. - { - const int xStart=wxMax(r.x,xText-LabelTrack::mIconWidth/2); - const int xEnd=wxMin(r.x+r.width,xText+width+LabelTrack::mIconWidth/2); - const int xWidth = xEnd-xStart; - - if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0)) - { - // Now draw the text itself. - dc.DrawText(title, xText, y-LabelTrack::mTextHeight/2); - } - } - -} - -void LabelStruct::DrawTextBox(wxDC & dc, const wxRect & r) const -{ - //If y is positive then it is the center line for the - //text we are about to draw. - const int yBarHeight=3; - const int yFrameHeight = LabelTrack::mTextHeight+3; - const int xBarShorten = LabelTrack::mIconWidth+4; - if( y == -1 ) - return; - - { - const int xStart=wxMax(r.x,x+xBarShorten/2); - const int xEnd=wxMin(r.x+r.width,x1-xBarShorten/2); - const int xWidth = xEnd-xStart; - - if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0)) - { - - wxRect bar( xStart,y-yBarHeight/2+yFrameHeight/2, - xWidth,yBarHeight); - if( x1 > x+xBarShorten ) - dc.DrawRectangle(bar); - } - } - - // In drawing the bar and the frame, we compute the clipping - // to the viewport ourselves. Under Win98 the GDI does its - // calculations in 16 bit arithmetic, and so gets it completely - // wrong at higher zooms where the bar can easily be - // more than 65536 pixels wide. - - // Draw bar for label extent... - // We don't quite draw from x to x1 because we allow - // half an icon width at each end. - { - const int xStart=wxMax(r.x,xText-LabelTrack::mIconWidth/2); - const int xEnd=wxMin(r.x+r.width,xText+width+LabelTrack::mIconWidth/2); - const int xWidth = xEnd-xStart; - - if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0)) - { - wxRect frame( - xStart,y-yFrameHeight/2, - xWidth,yFrameHeight ); - dc.DrawRectangle(frame); - } - } -} - -/// Draws text-selected region within the label -void LabelStruct::DrawHighlight - ( wxDC & dc, int xPos1, int xPos2, int charHeight) const -{ - wxPen curPen = dc.GetPen(); - curPen.SetColour(wxString(wxT("BLUE"))); - wxBrush curBrush = dc.GetBrush(); - curBrush.SetColour(wxString(wxT("BLUE"))); - if (xPos1 < xPos2) - dc.DrawRectangle(xPos1-1, y-charHeight/2, xPos2-xPos1+1, charHeight); - else - dc.DrawRectangle(xPos2-1, y-charHeight/2, xPos1-xPos2+1, charHeight); -} - -void LabelStruct::getXPos( wxDC & dc, int * xPos1, int cursorPos) const -{ - *xPos1 = xText; - if( cursorPos > 0) - { - int partWidth; - // Calculate the width of the substring and add it to Xpos - dc.GetTextExtent(title.Left(cursorPos), &partWidth, NULL); - *xPos1 += partWidth; - } -} - -bool LabelTrack::CalcCursorX(int * x) const -{ - if (mSelIndex >= 0) { - wxMemoryDC dc; - - if (msFont.Ok()) { - dc.SetFont(msFont); - } - - mLabels[mSelIndex].getXPos(dc, x, mCurrentCursorPos); - *x += LabelTrack::mIconWidth / 2; - return true; - } - - return false; -} - -void LabelTrack::CalcHighlightXs(int *x1, int *x2) const -{ - wxMemoryDC dc; - - if (msFont.Ok()) { - dc.SetFont(msFont); - } - - int pos1 = mInitialCursorPos, pos2 = mCurrentCursorPos; - if (pos1 > pos2) - std::swap(pos1, pos2); - - const auto &labelStruct = mLabels[mSelIndex]; - - // find the left X pos of highlighted area - labelStruct.getXPos(dc, x1, pos1); - // find the right X pos of highlighted area - labelStruct.getXPos(dc, x2, pos2); -} - -#include "tracks/labeltrack/ui/LabelGlyphHandle.h" -// TODO: don't rely on the global ::GetActiveProject() to find this. -// Rather, give TrackPanelCell a drawing function and pass context into it. -namespace { - LabelTrackHit *findHit() - { - // Fetch the highlighting state - auto target = TrackPanel::Get( *GetActiveProject() ).Target(); - if (target) { - auto handle = dynamic_cast( target.get() ); - if (handle) - return &handle->mHit; - } - return nullptr; - } -} - -#include "TrackPanelDrawingContext.h" -#include "tracks/labeltrack/ui/LabelTextHandle.h" - -/// Draw calls other functions to draw the LabelTrack. -/// @param dc the device context -/// @param r the LabelTrack rectangle. -void LabelTrack::Draw -( TrackPanelDrawingContext &context, const wxRect & r ) const -{ - auto &dc = context.dc; - const auto artist = TrackArtist::Get( context ); - const auto &zoomInfo = *artist->pZoomInfo; - - auto pHit = findHit(); - - if(msFont.Ok()) - dc.SetFont(msFont); - - if (mFontHeight == -1) - calculateFontHeight(dc); - - TrackArt::DrawBackgroundWithSelection( context, r, this, - AColor::labelSelectedBrush, AColor::labelUnselectedBrush, - ( GetSelected() || IsSyncLockSelected() ) ); - - wxCoord textWidth, textHeight; - - // Get the text widths. - // TODO: Make more efficient by only re-computing when a - // text label title changes. - for (auto &labelStruct : mLabels) { - dc.GetTextExtent(labelStruct.title, &textWidth, &textHeight); - labelStruct.width = textWidth; - } - - // TODO: And this only needs to be done once, but we - // do need the dc to do it. - // We need to set mTextHeight to something sensible, - // guarding against the case where there are no - // labels or all are empty strings, which for example - // happens with a NEW label track. - dc.GetTextExtent(wxT("Demo Text x^y"), &textWidth, &textHeight); - mTextHeight = (int)textHeight; - ComputeLayout( r, zoomInfo ); - dc.SetTextForeground(theTheme.Colour( clrLabelTrackText)); - dc.SetBackgroundMode(wxTRANSPARENT); - dc.SetBrush(AColor::labelTextNormalBrush); - dc.SetPen(AColor::labelSurroundPen); - int GlyphLeft; - int GlyphRight; - // Now we draw the various items in this order, - // so that the correct things overpaint each other. - - // Draw vertical lines that show where the end positions are. - for (auto &labelStruct : mLabels) - labelStruct.DrawLines( dc, r ); - - // Draw the end glyphs. - { int i = -1; for (auto &labelStruct : mLabels) { ++i; - GlyphLeft=0; - GlyphRight=1; - if( pHit && i == pHit->mMouseOverLabelLeft ) - GlyphLeft = (pHit->mEdge & 4) ? 6:9; - if( pHit && i == pHit->mMouseOverLabelRight ) - GlyphRight = (pHit->mEdge & 4) ? 7:4; - labelStruct.DrawGlyphs( dc, r, GlyphLeft, GlyphRight ); - }} - - // Draw the label boxes. - { -#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING - bool highlightTrack = false; - auto target = dynamic_cast(context.target.get()); - highlightTrack = target && target->GetTrack().get() == this; -#endif - int i = -1; for (auto &labelStruct : mLabels) { ++i; - bool highlight = false; -#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING - highlight = highlightTrack && target->GetLabelNum() == i; -#endif - bool selected = mSelIndex == i; - - if( selected ) - dc.SetBrush( AColor::labelTextEditBrush ); - else if ( highlight ) - dc.SetBrush( AColor::uglyBrush ); - labelStruct.DrawTextBox( dc, r ); - - if (highlight || selected) - dc.SetBrush(AColor::labelTextNormalBrush); - } - } - - // Draw highlights - if ((mInitialCursorPos != mCurrentCursorPos) && (mSelIndex >= 0 )) - { - int xpos1, xpos2; - CalcHighlightXs(&xpos1, &xpos2); - mLabels[mSelIndex].DrawHighlight(dc, xpos1, xpos2, mFontHeight); - } - - // Draw the text and the label boxes. - { int i = -1; for (auto &labelStruct : mLabels) { ++i; - if( mSelIndex==i) - dc.SetBrush(AColor::labelTextEditBrush); - labelStruct.DrawText( dc, r ); - if( mSelIndex==i) - dc.SetBrush(AColor::labelTextNormalBrush); - }} - - // Draw the cursor, if there is one. - if( mDrawCursor && mSelIndex >=0 ) - { - const auto &labelStruct = mLabels[mSelIndex]; - int xPos = labelStruct.xText; - - if( mCurrentCursorPos > 0) - { - // Calculate the width of the substring and add it to Xpos - int partWidth; - dc.GetTextExtent(labelStruct.title.Left(mCurrentCursorPos), &partWidth, NULL); - xPos += partWidth; - } - - wxPen currentPen = dc.GetPen(); - const int CursorWidth=2; - currentPen.SetWidth(CursorWidth); - AColor::Line(dc, - xPos-1, labelStruct.y - mFontHeight/2 + 1, - xPos-1, labelStruct.y + mFontHeight/2 - 1); - currentPen.SetWidth(1); - } -} - -/// uses GetTextExtent to find the character position -/// corresponding to the x pixel position. -int LabelTrack::FindCurrentCursorPosition(int xPos) -{ - int result = -1; - wxMemoryDC dc; - if(msFont.Ok()) - dc.SetFont(msFont); - - // A bool indicator to see if set the cursor position or not - bool finished = false; - int charIndex = 1; - int partWidth; - int oneWidth; - double bound; - wxString subString; - const auto &labelStruct = mLabels[mSelIndex]; - const auto &title = labelStruct.title; - const int length = title.length(); - while (!finished && (charIndex < length + 1)) - { - subString = title.Left(charIndex); - // Get the width of substring - dc.GetTextExtent(subString, &partWidth, NULL); - - // Get the width of the last character - dc.GetTextExtent(subString.Right(1), &oneWidth, NULL); - bound = labelStruct.xText + partWidth - oneWidth * 0.5; - - if (xPos <= bound) - { - // Found - result = charIndex - 1; - finished = true; - } - else - { - // Advance - charIndex++; - } - } - if (!finished) - // Cursor should be in the last position - result = length; - - return result; -} - -/// Set the cursor position according to x position of mouse -void LabelTrack::SetCurrentCursorPosition(int xPos) -{ - mCurrentCursorPos = FindCurrentCursorPosition(xPos); -} - -void LabelTrack::calculateFontHeight(wxDC & dc) const -{ - int charDescent; - int charLeading; - - // Calculate the width of the substring and add it to Xpos - dc.GetTextExtent(wxT("(Test String)|[yp]"), NULL, &mFontHeight, &charDescent, &charLeading); - - // The cursor will have height charHeight. We don't include the descender as - // part of the height because for phonetic fonts this leads to cursors which are - // too tall. We don't include leading either - it is usually 0. - // To make up for ignoring the descender height, we add one pixel above and below - // using CursorExtraHeight so that the cursor is just a little taller than the - // body of the characters. - const int CursorExtraHeight=2; - mFontHeight += CursorExtraHeight - (charLeading+charDescent); -} - -bool LabelTrack::IsTextSelected() const -{ - if (mSelIndex == -1) - return false; - if (mCurrentCursorPos == mInitialCursorPos) - return false; - return true; -} - -/// Cut the selected text in the text box -/// @return true if text is selected in text box, false otherwise -bool LabelTrack::CutSelectedText() -{ - if (!IsTextSelected()) - return false; - - wxString left, right; - auto &labelStruct = mLabels[mSelIndex]; - auto &text = labelStruct.title; - - int init = mInitialCursorPos; - int cur = mCurrentCursorPos; - if (init > cur) - std::swap(init, cur); - - // data for cutting - wxString data = text.Mid(init, cur - init); - - // get left-remaining text - if (init > 0) - left = text.Left(init); - - // get right-remaining text - if (cur < (int)text.length()) - right = text.Mid(cur); - - // set title to the combination of the two remainders - text = left + right; - - // copy data onto clipboard - if (wxTheClipboard->Open()) { - // Clipboard owns the data you give it - wxTheClipboard->SetData(safenew wxTextDataObject(data)); - wxTheClipboard->Close(); - } - - // set cursor positions - mInitialCursorPos = mCurrentCursorPos = left.length(); - return true; -} - -/// Copy the selected text in the text box -/// @return true if text is selected in text box, false otherwise -bool LabelTrack::CopySelectedText() -{ - if (mSelIndex == -1) - return false; - - const auto &labelStruct = mLabels[mSelIndex]; - - int init = mInitialCursorPos; - int cur = mCurrentCursorPos; - if (init > cur) - std::swap(init, cur); - - if (init == cur) - return false; - - // data for copying - wxString data = labelStruct.title.Mid(init, cur-init); - - // copy the data on clipboard - if (wxTheClipboard->Open()) { - // Clipboard owns the data you give it - wxTheClipboard->SetData(safenew wxTextDataObject(data)); - wxTheClipboard->Close(); - } - - return true; -} - -// PRL: should this set other fields of the label selection? -/// Paste the text on the clipboard to text box -/// @return true if mouse is clicked in text box, false otherwise -bool LabelTrack::PasteSelectedText(double sel0, double sel1) -{ - if (mSelIndex == -1) - AddLabel(SelectedRegion(sel0, sel1), wxT("")); - - wxString text, left, right; - - // if text data is available - if (IsTextClipSupported()) { - if (wxTheClipboard->Open()) { - wxTextDataObject data; - wxTheClipboard->GetData(data); - wxTheClipboard->Close(); - text = data.GetText(); - } - - // Convert control characters to blanks - for (int i = 0; i < (int)text.length(); i++) { - if (wxIscntrl(text[i])) { - text[i] = wxT(' '); - } - } - } - - auto &labelStruct = mLabels[mSelIndex]; - auto &title = labelStruct.title; - int cur = mCurrentCursorPos, init = mInitialCursorPos; - if (init > cur) - std::swap(init, cur); - left = title.Left(init); - if (cur < (int)title.length()) - right = title.Mid(cur); - - title = left + text + right; - mInitialCursorPos = mCurrentCursorPos = left.length() + text.length(); - return true; -} - - -/// @return true if the text data is available in the clipboard, false otherwise -bool LabelTrack::IsTextClipSupported() -{ - return wxTheClipboard->IsSupported(wxDF_TEXT); -} - - double LabelTrack::GetOffset() const { return mOffset; @@ -1173,92 +283,6 @@ Track::Holder LabelTrack::Clone() const return std::make_shared( *this ); } -void LabelTrack::SetSelected(bool s) -{ - Track::SetSelected(s); - if (!s) - Unselect(); -} - -/// TODO: Investigate what happens with large -/// numbers of labels, might need a binary search -/// rather than a linear one. -void LabelTrack::OverGlyph(LabelTrackHit &hit, int x, int y) const -{ - //Determine the NEW selection. - int result=0; - const int d1=10; //distance in pixels, used for have we hit drag handle. - const int d2=5; //distance in pixels, used for have we hit drag handle center. - - //If not over a label, reset it - hit.mMouseOverLabelLeft = -1; - hit.mMouseOverLabelRight = -1; - hit.mEdge = 0; - { int i = -1; for (auto &labelStruct : mLabels) { ++i; - //over left or right selection bound - //Check right bound first, since it is drawn after left bound, - //so give it precedence for matching/highlighting. - if( abs(labelStruct.y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 && - abs(labelStruct.x1 - d2 -x) < d1) - { - hit.mMouseOverLabelRight = i; - if(abs(labelStruct.x1 - x) < d2 ) - { - result |= 4; - // If left and right co-incident at this resolution, then we drag both. - // We could be a little less stringent about co-incidence here if we liked. - if( abs(labelStruct.x1-labelStruct.x) < 1.0 ) - { - result |=1; - hit.mMouseOverLabelLeft = i; - } - } - result |= 2; - } - // Use else-if here rather than else to avoid detecting left and right - // of the same label. - else if( abs(labelStruct.y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 && - abs(labelStruct.x + d2 - x) < d1 ) - { - hit.mMouseOverLabelLeft = i; - if(abs(labelStruct.x - x) < d2 ) - result |= 4; - result |= 1; - } - - // give text box better priority for selecting - if(OverTextBox(&labelStruct, x, y)) - { - result = 0; - } - - }} - hit.mEdge = result; -} - -int LabelTrack::OverATextBox(int xx, int yy) const -{ - for (int nn = (int)mLabels.size(); nn--;) { - const auto &labelStruct = mLabels[nn]; - if (OverTextBox(&labelStruct, xx, yy)) - return nn; - } - - return -1; -} - -// return true if the mouse is over text box, false otherwise -bool LabelTrack::OverTextBox(const LabelStruct *pLabel, int x, int y) const -{ - if( (pLabel->xText-(mIconWidth/2) < x) && - (xxText+pLabel->width+(mIconWidth/2)) && - (abs(pLabel->y-y) max ) - result=max; - return result; -} - -bool LabelTrack::HandleGlyphDragRelease -(LabelTrackHit &hit, const wxMouseEvent & evt, - wxRect & r, const ZoomInfo &zoomInfo, - SelectedRegion *newSel) -{ - if(evt.LeftUp()) - { - bool lupd = false, rupd = false; - if( hit.mMouseOverLabelLeft >= 0 ) { - auto &labelStruct = mLabels[ hit.mMouseOverLabelLeft ]; - lupd = labelStruct.updated; - labelStruct.updated = false; - } - if( hit.mMouseOverLabelRight >= 0 ) { - auto &labelStruct = mLabels[ hit.mMouseOverLabelRight ]; - rupd = labelStruct.updated; - labelStruct.updated = false; - } - - hit.mIsAdjustingLabel = false; - hit.mMouseOverLabelLeft = -1; - hit.mMouseOverLabelRight = -1; - return lupd || rupd; - } - - if(evt.Dragging()) - { - //If we are currently adjusting a label, - //just reset its value and redraw. - // LL: Constrain to inside track rectangle for now. Should be changed - // to allow scrolling while dragging labels - int x = Constrain( evt.m_x + mxMouseDisplacement - r.x, 0, r.width); - - // If exactly one edge is selected we allow swapping - bool bAllowSwapping = - ( hit.mMouseOverLabelLeft >=0 ) != - ( hit.mMouseOverLabelRight >= 0); - // If we're on the 'dot' and nowe're moving, - // Though shift-down inverts that. - // and if both edges the same, then we're always moving the label. - bool bLabelMoving = hit.mbIsMoving; - bLabelMoving ^= evt.ShiftDown(); - bLabelMoving |= ( hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight ); - double fNewX = zoomInfo.PositionToTime(x, 0); - if( bLabelMoving ) - { - MayMoveLabel( hit.mMouseOverLabelLeft, -1, fNewX ); - MayMoveLabel( hit.mMouseOverLabelRight, +1, fNewX ); - } - else - { - MayAdjustLabel( hit, hit.mMouseOverLabelLeft, -1, bAllowSwapping, fNewX ); - MayAdjustLabel( hit, hit.mMouseOverLabelRight, +1, bAllowSwapping, fNewX ); - } - - if( mSelIndex >=0 ) - { - //Set the selection region to be equal to - //the NEW size of the label. - *newSel = mLabels[mSelIndex].selectedRegion; - } - SortLabels( &hit ); - } - - return false; -} - -void LabelTrack::HandleTextDragRelease(const wxMouseEvent & evt) -{ - if(evt.LeftUp()) - { -#if 0 - // AWD: Due to wxWidgets bug #7491 (fix not ported to 2.8 branch) we - // should never write the primary selection. We can enable this block - // when we move to the 3.0 branch (or if a fixed 2.8 version is released - // and we can do a runtime version check) -#if defined (__WXGTK__) && defined (HAVE_GTK) - // On GTK, if we just dragged out a text selection, set the primary - // selection - if (mInitialCursorPos != mCurrentCursorPos) { - wxTheClipboard->UsePrimarySelection(true); - CopySelectedText(); - wxTheClipboard->UsePrimarySelection(false); - } -#endif -#endif - - return; - } - - if(evt.Dragging()) - { - if (!mRightDragging) - // Update drag end - SetCurrentCursorPosition(evt.m_x); - - return; - } - - if (evt.RightUp()) { - if ((mSelIndex != -1) && OverTextBox(GetLabel(mSelIndex), evt.m_x, evt.m_y)) { - // popup menu for editing - ShowContextMenu(); - } - } - - return; -} - -void LabelTrack::HandleGlyphClick -(LabelTrackHit &hit, const wxMouseEvent & evt, - const wxRect & r, const ZoomInfo &zoomInfo, - SelectedRegion *WXUNUSED(newSel)) -{ - if (evt.ButtonDown()) - { - //OverGlyph sets mMouseOverLabel to be the chosen label. - OverGlyph(hit, evt.m_x, evt.m_y); - hit.mIsAdjustingLabel = evt.Button(wxMOUSE_BTN_LEFT) && - ( hit.mEdge & 3 ) != 0; - - if (hit.mIsAdjustingLabel) - { - float t = 0.0; - // We move if we hit the centre, we adjust one edge if we hit a chevron. - // This is if we are moving just one edge. - hit.mbIsMoving = (hit.mEdge & 4)!=0; - // When we start dragging the label(s) we don't want them to jump. - // so we calculate the displacement of the mouse from the drag center - // and use that in subsequent dragging calculations. The mouse stays - // at the same relative displacement throughout dragging. - - // However, if two label's edges are being dragged - // then the displacement is relative to the initial average - // position of them, and in that case there can be a jump of at most - // a few pixels to bring the two label boundaries to exactly the same - // position when we start dragging. - - // Dragging of three label edges at the same time is not supported (yet). - if( ( hit.mMouseOverLabelRight >= 0 ) && - ( hit.mMouseOverLabelLeft >= 0 ) - ) - { - t = (mLabels[ hit.mMouseOverLabelRight ].getT1() + - mLabels[ hit.mMouseOverLabelLeft ].getT0()) / 2.0f; - // If we're moving two edges, then it's a move (label size preserved) - // if both edges are the same label, and it's an adjust (label sizes change) - // if we're on a boundary between two different labels. - hit.mbIsMoving = - ( hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight ); - } - else if( hit.mMouseOverLabelRight >=0) - { - t = mLabels[ hit.mMouseOverLabelRight ].getT1(); - } - else if( hit.mMouseOverLabelLeft >=0) - { - t = mLabels[ hit.mMouseOverLabelLeft ].getT0(); - } - mxMouseDisplacement = zoomInfo.TimeToPosition(t, r.x) - evt.m_x; - } - } -} - -void LabelTrack::HandleTextClick(const wxMouseEvent & evt, - const wxRect & r, const ZoomInfo &zoomInfo, - SelectedRegion *newSel) -{ - static_cast(r);//compiler food. - static_cast(zoomInfo);//compiler food. - if (evt.ButtonDown()) - { - - mSelIndex = OverATextBox(evt.m_x, evt.m_y); - if (mSelIndex != -1) { - auto &labelStruct = mLabels[mSelIndex]; - *newSel = labelStruct.selectedRegion; - - if (evt.LeftDown()) { - // Find the NEW drag end - auto position = FindCurrentCursorPosition(evt.m_x); - - // Anchor shift-drag at the farther end of the previous highlight - // that is farther from the click, on Mac, for consistency with - // its text editors, but on the others, re-use the previous - // anchor. - if (evt.ShiftDown()) { -#ifdef __WXMAC__ - // Set the drag anchor at the end of the previous selection - // that is farther from the NEW drag end - if (abs(position - mCurrentCursorPos) > - abs(position - mInitialCursorPos)) - mInitialCursorPos = mCurrentCursorPos; -#else - // mInitialCursorPos remains as before -#endif - } - else - mInitialCursorPos = position; - - mCurrentCursorPos = position; - - mDrawCursor = true; - mRightDragging = false; - } - else - // Actually this might be right or middle down - mRightDragging = true; - - // Middle click on GTK: paste from primary selection -#if defined(__WXGTK__) && (HAVE_GTK) - if (evt.MiddleDown()) { - // Check for a click outside of the selected label's text box; in this - // case PasteSelectedText() will start a NEW label at the click - // location - if (!OverTextBox(&labelStruct, evt.m_x, evt.m_y)) - mSelIndex = -1; - double t = zoomInfo.PositionToTime(evt.m_x, r.x); - *newSel = SelectedRegion(t, t); - } -#endif - } -#if defined(__WXGTK__) && (HAVE_GTK) - if (evt.MiddleDown()) { - // Paste text, making a NEW label if none is selected. - wxTheClipboard->UsePrimarySelection(true); - PasteSelectedText(newSel->t0(), newSel->t1()); - wxTheClipboard->UsePrimarySelection(false); - } -#endif - } -} - -static bool IsGoodLabelFirstKey(const wxKeyEvent & evt); -static bool IsGoodLabelEditKey(const wxKeyEvent & evt); - -// Check for keys that we will process -bool LabelTrackView::DoCaptureKey(wxKeyEvent & event) -{ - // Check for modifiers and only allow shift - int mods = event.GetModifiers(); - if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) { - return false; - } - - // Always capture the navigation keys, if we have any labels - auto code = event.GetKeyCode(); - const auto pTrack = FindLabelTrack(); - const auto &mLabels = pTrack->GetLabels(); - if ((code == WXK_TAB || code == WXK_NUMPAD_TAB) && - !mLabels.empty()) - return true; - - if ( pTrack->mSelIndex >= 0 ) { - if (IsGoodLabelEditKey(event)) { - return true; - } - } - else { - bool typeToCreateLabel; - gPrefs->Read(wxT("/GUI/TypeToCreateLabel"), &typeToCreateLabel, false); - if (IsGoodLabelFirstKey(event) && typeToCreateLabel) { - AudacityProject * pProj = GetActiveProject(); - - -// The commented out code can prevent label creation, causing bug 1551 -// We should only be in DoCaptureKey IF this label track has focus, -// and in that case creating a Label is the expected/intended thing. -#if 0 - // If we're playing, don't capture if the selection is the same as the - // playback region (this helps prevent label track creation from - // stealing unmodified kbd. shortcuts) - auto gAudioIO = AudioIOBase::Get(); - if (pProj->GetAudioIOToken() > 0 && - gAudioIO->IsStreamActive(pProj->GetAudioIOToken())) - { - double t0, t1; - pProj->GetPlayRegion(&t0, &t1); - if (pProj->mViewInfo.selectedRegion.t0() == t0 && - pProj->mViewInfo.selectedRegion.t1() == t1) { - return false; - } - } -#endif - - // If there's a label there already don't capture - auto &selectedRegion = ViewInfo::Get( *pProj ).selectedRegion; - if( pTrack->GetLabelIndex(selectedRegion.t0(), - selectedRegion.t1()) != wxNOT_FOUND ) { - return false; - } - - return true; - } - } - - return false; -} - -unsigned LabelTrackView::CaptureKey(wxKeyEvent & event, ViewInfo &, wxWindow *) -{ - event.Skip(!DoCaptureKey(event)); - return RefreshCode::RefreshNone; -} - -unsigned LabelTrackView::KeyDown( - wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *WXUNUSED(pParent)) -{ - double bkpSel0 = viewInfo.selectedRegion.t0(), - bkpSel1 = viewInfo.selectedRegion.t1(); - - AudacityProject *const pProj = GetActiveProject(); - - // Pass keystroke to labeltrack's handler and add to history if any - // updates were done - if (DoKeyDown(viewInfo.selectedRegion, event)) { - ProjectHistory::Get( *pProj ).PushState(_("Modified Label"), - _("Label Edit"), - UndoPush::CONSOLIDATE); - } - - // Make sure caret is in view - int x; - const auto pTrack = FindLabelTrack(); - if (pTrack->CalcCursorX(&x)) { - TrackPanel::Get( *pProj ).ScrollIntoView(x); - } - - // If selection modified, refresh - // Otherwise, refresh track display if the keystroke was handled - if (bkpSel0 != viewInfo.selectedRegion.t0() || - bkpSel1 != viewInfo.selectedRegion.t1()) - return RefreshCode::RefreshAll; - else if (!event.GetSkipped()) - return RefreshCode::RefreshCell; - - return RefreshCode::RefreshNone; -} - -unsigned LabelTrackView::Char( - wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *) -{ - double bkpSel0 = viewInfo.selectedRegion.t0(), - bkpSel1 = viewInfo.selectedRegion.t1(); - // Pass keystroke to labeltrack's handler and add to history if any - // updates were done - - AudacityProject *const pProj = GetActiveProject(); - - if (DoChar(viewInfo.selectedRegion, event)) - ProjectHistory::Get( *pProj ).PushState(_("Modified Label"), - _("Label Edit"), - UndoPush::CONSOLIDATE); - - // If selection modified, refresh - // Otherwise, refresh track display if the keystroke was handled - if (bkpSel0 != viewInfo.selectedRegion.t0() || - bkpSel1 != viewInfo.selectedRegion.t1()) - return RefreshCode::RefreshAll; - else if (!event.GetSkipped()) - return RefreshCode::RefreshCell; - - return RefreshCode::RefreshNone; -} - -/// KeyEvent is called for every keypress when over the label track. -bool LabelTrackView::DoKeyDown(SelectedRegion &newSel, wxKeyEvent & event) -{ - // Only track true changes to the label - bool updated = false; - - // Cache the keycode - int keyCode = event.GetKeyCode(); - const int mods = event.GetModifiers(); - - // Check for modifiers and only allow shift - if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) { - event.Skip(); - return updated; - } - - // All editing keys are only active if we're currently editing a label - const auto pTrack = FindLabelTrack(); - auto &mLabels = pTrack->mLabels; - auto &mSelIndex = pTrack->mSelIndex; - auto &mInitialCursorPos = pTrack->mInitialCursorPos; - auto &mCurrentCursorPos = pTrack->mCurrentCursorPos; - auto &mRestoreFocus = pTrack->mRestoreFocus; - if ( pTrack->mSelIndex >= 0 ) { - auto &labelStruct = mLabels[mSelIndex]; - auto &title = labelStruct.title; - switch (keyCode) { - - case WXK_BACK: - { - int len = title.length(); - - //IF the label is not blank THEN get rid of a letter or letters according to cursor position - if (len > 0) - { - // IF there are some highlighted letters, THEN DELETE them - if (mInitialCursorPos != mCurrentCursorPos) - pTrack->RemoveSelectedText(); - else - { - // DELETE one letter - if (mCurrentCursorPos > 0) { - title.erase(mCurrentCursorPos-1, 1); - mCurrentCursorPos--; - } - } - } - else - { - // ELSE no text in text box, so DELETE whole label. - pTrack->DeleteLabel( mSelIndex ); - } - mInitialCursorPos = mCurrentCursorPos; - updated = true; - } - break; - - case WXK_DELETE: - case WXK_NUMPAD_DELETE: - { - int len = title.length(); - - //If the label is not blank get rid of a letter according to cursor position - if (len > 0) - { - // if there are some highlighted letters, DELETE them - if (mInitialCursorPos != mCurrentCursorPos) - pTrack->RemoveSelectedText(); - else - { - // DELETE one letter - if (mCurrentCursorPos < len) { - title.erase(mCurrentCursorPos, 1); - } - } - } - else - { - // DELETE whole label if no text in text box - pTrack->DeleteLabel( mSelIndex ); - } - mInitialCursorPos = mCurrentCursorPos; - updated = true; - } - break; - - case WXK_HOME: - case WXK_NUMPAD_HOME: - // Move cursor to beginning of label - mCurrentCursorPos = 0; - if (mods == wxMOD_SHIFT) - ; - else - mInitialCursorPos = mCurrentCursorPos; - break; - - case WXK_END: - case WXK_NUMPAD_END: - // Move cursor to end of label - mCurrentCursorPos = (int)title.length(); - if (mods == wxMOD_SHIFT) - ; - else - mInitialCursorPos = mCurrentCursorPos; - break; - - case WXK_LEFT: - case WXK_NUMPAD_LEFT: - // Moving cursor left - if (mCurrentCursorPos > 0) { - mCurrentCursorPos--; - if (mods == wxMOD_SHIFT) - ; - else - mInitialCursorPos = mCurrentCursorPos = - std::min(mInitialCursorPos, mCurrentCursorPos); - } - break; - - case WXK_RIGHT: - case WXK_NUMPAD_RIGHT: - // Moving cursor right - if (mCurrentCursorPos < (int)title.length()) { - mCurrentCursorPos++; - if (mods == wxMOD_SHIFT) - ; - else - mInitialCursorPos = mCurrentCursorPos = - std::max(mInitialCursorPos, mCurrentCursorPos); - } - break; - - case WXK_RETURN: - case WXK_NUMPAD_ENTER: - - case WXK_ESCAPE: - if (mRestoreFocus >= 0) { - auto track = *TrackList::Get( *GetActiveProject() ).Any() - .begin().advance(mRestoreFocus); - if (track) - TrackPanel::Get( *GetActiveProject() ).SetFocusedTrack(track); - mRestoreFocus = -1; - } - mSelIndex = -1; - break; - - case WXK_TAB: - case WXK_NUMPAD_TAB: - if (event.ShiftDown()) { - mSelIndex--; - } else { - mSelIndex++; - } - - mSelIndex = (mSelIndex + (int)mLabels.size()) % (int)mLabels.size(); // wrap round if necessary - { - LabelStruct &newLabel = mLabels[mSelIndex]; - mCurrentCursorPos = newLabel.title.length(); - mInitialCursorPos = mCurrentCursorPos; - //Set the selection region to be equal to the selection bounds of the tabbed-to label. - newSel = newLabel.selectedRegion; - } - break; - - case '\x10': // OSX - case WXK_MENU: - case WXK_WINDOWS_MENU: - pTrack->ShowContextMenu(); - break; - - default: - if (!IsGoodLabelEditKey(event)) { - event.Skip(); - } - break; - } - } - else - { - switch (keyCode) { - - case WXK_TAB: - case WXK_NUMPAD_TAB: - if (!mLabels.empty()) { - int len = (int) mLabels.size(); - if (event.ShiftDown()) { - mSelIndex = len - 1; - if (newSel.t0() > mLabels[0].getT0()) { - while (mSelIndex >= 0 && - mLabels[mSelIndex].getT0() > newSel.t0()) { - mSelIndex--; - } - } - } else { - mSelIndex = 0; - if (newSel.t0() < mLabels[len - 1].getT0()) { - while (mSelIndex < len && - mLabels[mSelIndex].getT0() < newSel.t0()) { - mSelIndex++; - } - } - } - - if (mSelIndex >= 0 && mSelIndex < len) { - const auto &labelStruct = mLabels[mSelIndex]; - mCurrentCursorPos = labelStruct.title.length(); - mInitialCursorPos = mCurrentCursorPos; - //Set the selection region to be equal to the selection bounds of the tabbed-to label. - newSel = labelStruct.selectedRegion; - } - else { - mSelIndex = -1; - } - } - break; - - default: - if (!IsGoodLabelFirstKey(event)) { - event.Skip(); - } - break; - } - } - - // Make sure the caret is visible - pTrack->mDrawCursor = true; - - return updated; -} - -/// OnChar is called for incoming characters -- that's any keypress not handled -/// by OnKeyDown. -bool LabelTrackView::DoChar(SelectedRegion &WXUNUSED(newSel), wxKeyEvent & event) -{ - // Check for modifiers and only allow shift. - // - // We still need to check this or we will eat the top level menu accelerators - // on Windows if our capture or key down handlers skipped the event. - const int mods = event.GetModifiers(); - if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) { - event.Skip(); - return false; - } - - // Only track true changes to the label - bool updated = false; - - // Cache the character - wxChar charCode = event.GetUnicodeKey(); - - // Skip if it's not a valid unicode character or a control character - if (charCode == 0 || wxIscntrl(charCode)) { - event.Skip(); - return false; - } - - // If we've reached this point and aren't currently editing, add NEW label - const auto pTrack = FindLabelTrack(); - if ( pTrack->mSelIndex < 0 ) { - // Don't create a NEW label for a space - if (wxIsspace(charCode)) { - event.Skip(); - return false; - } - bool useDialog; - AudacityProject *p = GetActiveProject(); - gPrefs->Read(wxT("/GUI/DialogForNameNewLabel"), &useDialog, false); - auto &selectedRegion = ViewInfo::Get( *p ).selectedRegion; - if (useDialog) { - wxString title; - if (pTrack->DialogForLabelName( - *p, selectedRegion, charCode, title) == - wxID_CANCEL) { - return false; - } - pTrack->SetSelected(true); - pTrack->AddLabel(selectedRegion, title, -2); - ProjectHistory::Get( *p ).PushState(_("Added label"), _("Label")); - return false; - } - else { - pTrack->SetSelected(true); - pTrack->AddLabel(selectedRegion); - ProjectHistory::Get( *p ).PushState(_("Added label"), _("Label")); - } - } - - // - // Now we are definitely in a label; append the incoming character - // - - auto &mLabels = pTrack->mLabels; - auto &mSelIndex = pTrack->mSelIndex; - auto &mInitialCursorPos = pTrack->mInitialCursorPos; - auto &mCurrentCursorPos = pTrack->mCurrentCursorPos; - auto &labelStruct = mLabels[mSelIndex]; - auto &title = labelStruct.title; - - // Test if cursor is in the end of string or not - if (mInitialCursorPos != mCurrentCursorPos) - pTrack->RemoveSelectedText(); - - if (mCurrentCursorPos < (int)title.length()) { - // Get substring on the righthand side of cursor - wxString rightPart = title.Mid(mCurrentCursorPos); - // Set title to substring on the lefthand side of cursor - title = title.Left(mCurrentCursorPos); - //append charcode - title += charCode; - //append the right part substring - title += rightPart; - } - else - //append charCode - title += charCode; - - //moving cursor position forward - mInitialCursorPos = ++mCurrentCursorPos; - updated = true; - - // Make sure the caret is visible - pTrack->mDrawCursor = true; - - return updated; -} - -void LabelTrack::ShowContextMenu() -{ - wxWindow *parent = wxWindow::FindFocus(); - - // Bug 2044. parent can be nullptr after a context switch. - if( !parent ) - parent = FindProjectFrame( GetActiveProject() ); - - if( parent ) - { - wxMenu menu; - menu.Bind(wxEVT_MENU, &LabelTrack::OnContextMenu, this); - - menu.Append(OnCutSelectedTextID, _("Cu&t")); - menu.Append(OnCopySelectedTextID, _("&Copy")); - menu.Append(OnPasteSelectedTextID, _("&Paste")); - menu.Append(OnDeleteSelectedLabelID, _("&Delete Label")); - menu.Append(OnEditSelectedLabelID, _("&Edit...")); - - menu.Enable(OnCutSelectedTextID, IsTextSelected()); - menu.Enable(OnCopySelectedTextID, IsTextSelected()); - menu.Enable(OnPasteSelectedTextID, IsTextClipSupported()); - menu.Enable(OnDeleteSelectedLabelID, true); - menu.Enable(OnEditSelectedLabelID, true); - - wxASSERT(mSelIndex >= 0); - const LabelStruct *ls = GetLabel(mSelIndex); - - wxClientDC dc(parent); - - if (msFont.Ok()) - { - dc.SetFont(msFont); - } - - int x = 0; - bool success = CalcCursorX(&x); - wxASSERT(success); - static_cast(success); // Suppress unused variable warning if debug mode is disabled - - parent->PopupMenu(&menu, x, ls->y + (mIconHeight / 2) - 1); - } -} - -void LabelTrack::OnContextMenu(wxCommandEvent & evt) -{ - AudacityProject *p = GetActiveProject(); - auto &selectedRegion = ViewInfo::Get( *p ).selectedRegion; - - switch (evt.GetId()) - { - /// Cut selected text if cut menu item is selected - case OnCutSelectedTextID: - if (CutSelectedText()) - { - ProjectHistory::Get( *p ).PushState(_("Modified Label"), - _("Label Edit"), - UndoPush::CONSOLIDATE); - } - break; - - /// Copy selected text if copy menu item is selected - case OnCopySelectedTextID: - CopySelectedText(); - break; - - /// paste selected text if paste menu item is selected - case OnPasteSelectedTextID: - if (PasteSelectedText(selectedRegion.t0(), selectedRegion.t1())) - { - ProjectHistory::Get( *p ).PushState(_("Modified Label"), - _("Label Edit"), - UndoPush::CONSOLIDATE); - } - break; - - /// DELETE selected label - case OnDeleteSelectedLabelID: { - int ndx = GetLabelIndex(selectedRegion.t0(), selectedRegion.t1()); - if (ndx != -1) - { - DeleteLabel(ndx); - ProjectHistory::Get( *p ).PushState(_("Deleted Label"), - _("Label Edit"), - UndoPush::CONSOLIDATE); - } - } - break; - - case OnEditSelectedLabelID: { - int ndx = GetLabelIndex(selectedRegion.t0(), selectedRegion.t1()); - if (ndx != -1) - DoEditLabels(*p, this, ndx); - } - break; - } -} - -void LabelTrack::RemoveSelectedText() -{ - wxString left, right; - - int init = mInitialCursorPos; - int cur = mCurrentCursorPos; - if (init > cur) - std::swap(init, cur); - - auto &labelStruct = mLabels[mSelIndex]; - auto &title = labelStruct.title; - - if (init > 0) - left = title.Left(init); - - if (cur < (int)title.length()) - right = title.Mid(cur); - - title = left + right; - mInitialCursorPos = mCurrentCursorPos = left.length(); -} - -void LabelTrack::Unselect() -{ - mSelIndex = -1; -} - -bool LabelTrack::HasSelection() const -{ - return (mSelIndex >= 0 && mSelIndex < (int)mLabels.size()); -} - /// Export labels including label start and end-times. void LabelTrack::Export(wxTextFile & f) const { @@ -2745,34 +897,10 @@ const LabelStruct *LabelTrack::GetLabel(int index) const return &mLabels[index]; } -int LabelTrack::GetLabelIndex(double t, double t1) -{ - //We'd have liked to have times in terms of samples, - //because then we're doing an intrger comparison. - //Never mind. Instead we look for near enough. - //This level of (in)accuracy is only a problem if we - //deal with sounds in the MHz range. - const double delta = 1.0e-7; - { int i = -1; for (auto &labelStruct : mLabels) { ++i; - if( fabs( labelStruct.getT0() - t ) > delta ) - continue; - if( fabs( labelStruct.getT1() - t1 ) > delta ) - continue; - return i; - }} - - return wxNOT_FOUND; -} - - -// restoreFocus of -1 is the default, and sets the focus to this label. -// restoreFocus of -2 or other value leaves the focus unchanged. -// restoreFocus >= 0 will later cause focus to move to that track. int LabelTrack::AddLabel(const SelectedRegion &selectedRegion, - const wxString &title, int restoreFocus) + const wxString &title) { LabelStruct l { selectedRegion, title }; - mInitialCursorPos = mCurrentCursorPos = title.length(); int len = mLabels.size(); int pos = 0; @@ -2782,25 +910,10 @@ int LabelTrack::AddLabel(const SelectedRegion &selectedRegion, mLabels.insert(mLabels.begin() + pos, l); - // restoreFocus is -2 e.g. from Nyquist label creation, when we should not - // even lose the focus and open the label to edit in the first place. - // -1 means we don't need to restore it to anywhere. - // 0 or above is the track to restore to afetr editing the label is complete. - if( restoreFocus >= -1 ) - mSelIndex = pos; - - // Make sure the caret is visible - // - // LLL: The cursor will not be drawn when the first label - // is added since mDrawCursor will be false. Presumably, - // if the user adds a label, then a cursor should be drawn - // to indicate that typing is expected. - // - // If the label is added during actions like import, then the - // mDrawCursor flag will be reset once the action is complete. - mDrawCursor = true; - - mRestoreFocus = restoreFocus; + // wxWidgets will own the event object + QueueEvent( safenew LabelTrackEvent{ + EVT_LABELTRACK_ADDITION, SharedPointer(), title, -1, pos + } ); return pos; } @@ -2808,170 +921,21 @@ int LabelTrack::AddLabel(const SelectedRegion &selectedRegion, void LabelTrack::DeleteLabel(int index) { wxASSERT((index < (int)mLabels.size())); - mLabels.erase(mLabels.begin() + index); - // IF we've deleted the selected label - // THEN set no label selected. - if( mSelIndex== index ) - { - mSelIndex = -1; - mCurrentCursorPos = 1; - } - // IF we removed a label before the selected label - // THEN the NEW selected label number is one less. - else if( index < mSelIndex ) - { - mSelIndex--; - } -} + auto iter = mLabels.begin() + index; + const auto title = iter->title; + mLabels.erase(iter); -wxBitmap & LabelTrack::GetGlyph( int i) -{ - return theTheme.Bitmap( i + bmpLabelGlyph0); -} - -// This one XPM spec is used to generate a number of -// different wxIcons. -/* XPM */ -static const char *const GlyphXpmRegionSpec[] = { -/* columns rows colors chars-per-pixel */ -"15 23 7 1", -/* Default colors, with first color transparent */ -". c none", -"2 c black", -"3 c black", -"4 c black", -"5 c #BEBEF0", -"6 c #BEBEF0", -"7 c #BEBEF0", -/* pixels */ -"...............", -"...............", -"...............", -"....333.444....", -"...3553.4774...", -"...3553.4774...", -"..35553.47774..", -"..35522222774..", -".3552666662774.", -".3526666666274.", -"355266666662774", -"355266666662774", -"355266666662774", -".3526666666274.", -".3552666662774.", -"..35522222774..", -"..35553.47774..", -"...3553.4774...", -"...3553.4774...", -"....333.444....", -"...............", -"...............", -"..............." -}; - -/// CreateCustomGlyphs() creates the mBoundaryGlyph array. -/// It's a bit like painting by numbers! -/// -/// Schematically the glyphs we want will 'look like': -/// and -/// for a left boundary to a label, a right boundary and both. -/// we're creating all three glyphs using the one Xpm Spec. -/// -/// When we hover over a glyph we highlight the -/// inside of either the '<', the 'O' or the '>' or none, -/// giving 3 x 4 = 12 combinations. -/// -/// Two of those combinations aren't used, but -/// treating them specially would make other code more -/// complicated. -void LabelTrack::CreateCustomGlyphs() -{ - int iConfig; - int iHighlight; - int index; - const int nSpecRows = - sizeof( GlyphXpmRegionSpec )/sizeof( GlyphXpmRegionSpec[0]); - const char *XmpBmp[nSpecRows]; - - // The glyphs are declared static wxIcon; so we only need - // to create them once, no matter how many LabelTracks. - if( mbGlyphsReady ) - return; - - // We're about to tweak the basic color spec to get 12 variations. - for( iConfig=0;iConfig= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) || - (keyCode >= WXK_NUMPAD_EQUAL && keyCode <= WXK_NUMPAD_DIVIDE) || -#if defined(__WXMAC__) - (keyCode > WXK_RAW_CONTROL) || -#endif - (keyCode > WXK_WINDOWS_MENU); -} - -/// This returns true for keys we capture for label editing. -static bool IsGoodLabelEditKey(const wxKeyEvent & evt) -{ - int keyCode = evt.GetKeyCode(); - - // Accept everything outside of WXK_START through WXK_COMMAND, plus the keys - // within that range that are usually printable, plus the ones we use for - // keyboard navigation. - return keyCode < WXK_START || - (keyCode >= WXK_END && keyCode < WXK_UP) || - (keyCode == WXK_RIGHT) || - (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) || - (keyCode >= WXK_NUMPAD_SPACE && keyCode <= WXK_NUMPAD_ENTER) || - (keyCode >= WXK_NUMPAD_HOME && keyCode <= WXK_NUMPAD_END) || - (keyCode >= WXK_NUMPAD_DELETE && keyCode <= WXK_NUMPAD_DIVIDE) || -#if defined(__WXMAC__) - (keyCode > WXK_RAW_CONTROL) || -#endif - (keyCode > WXK_WINDOWS_MENU); + // wxWidgets will own the event object + QueueEvent( safenew LabelTrackEvent{ + EVT_LABELTRACK_DELETION, SharedPointer(), title, index, -1 + } ); } /// Sorts the labels in order of their starting times. /// This function is called often (whilst dragging a label) /// We expect them to be very nearly in order, so insertion /// sort (with a linear search) is a reasonable choice. -void LabelTrack::SortLabels( LabelTrackHit *pHit ) +void LabelTrack::SortLabels() { const auto begin = mLabels.begin(); const auto nn = (int)mLabels.size(); @@ -2997,20 +961,12 @@ void LabelTrack::SortLabels( LabelTrackHit *pHit ) begin + i + 1 ); - // Various indices need to be updated with the moved items... - auto update = [=](int &index) { - if( index <= i ) { - if( index == i ) - index = j; - else if( index >= j) - ++index; - } - }; - if ( pHit ) { - update( pHit->mMouseOverLabelLeft ); - update( pHit->mMouseOverLabelRight ); - } - update(mSelIndex); + // Let listeners update their stored indices + // wxWidgets will own the event object + QueueEvent( safenew LabelTrackEvent{ + EVT_LABELTRACK_PERMUTED, SharedPointer(), + mLabels[j].title, i, j + } ); } } @@ -3084,78 +1040,3 @@ int LabelTrack::FindNextLabel(const SelectedRegion& currentRegion) miLastLabel = i; return i; } - -#include "LabelDialog.h" - -void LabelTrack::DoEditLabels -(AudacityProject &project, LabelTrack *lt, int index) -{ - const auto &settings = ProjectSettings::Get( project ); - auto format = settings.GetSelectionFormat(), - freqFormat = settings.GetFrequencySelectionFormatName(); - auto &tracks = TrackList::Get( project ); - auto &trackFactory = TrackFactory::Get( project ); - auto rate = ProjectSettings::Get( project ).GetRate(); - auto &viewInfo = ViewInfo::Get( project ); - auto &window = ProjectWindow::Get( project ); - - LabelDialog dlg(&window, trackFactory, &tracks, - lt, index, - viewInfo, rate, - format, freqFormat); -#ifdef __WXGTK__ - dlg.Raise(); -#endif - - if (dlg.ShowModal() == wxID_OK) { - ProjectHistory::Get( project ) - .PushState(_("Edited labels"), _("Label")); - window.RedrawProject(); - } -} - -int LabelTrack::DialogForLabelName( - AudacityProject &project, - const SelectedRegion& region, const wxString& initialValue, wxString& value) -{ - auto &trackPanel = TrackPanel::Get( project ); - auto &viewInfo = ViewInfo::Get( project ); - - wxPoint position = trackPanel.FindTrackRect(trackPanel.GetFocusedTrack()).GetBottomLeft(); - // The start of the text in the text box will be roughly in line with the label's position - // if it's a point label, or the start of its region if it's a region label. - position.x += trackPanel.GetLabelWidth() - + std::max(0, static_cast(viewInfo.TimeToPosition(region.t0()))) - -40; - position.y += 2; // just below the bottom of the track - position = trackPanel.ClientToScreen(position); - auto &window = GetProjectFrame( project ); - AudacityTextEntryDialog dialog{ &window, - _("Name:"), - _("New label"), - initialValue, - wxOK | wxCANCEL, - position }; - - // keep the dialog within Audacity's window, so that the dialog is always fully visible - wxRect dialogScreenRect = dialog.GetScreenRect(); - wxRect projScreenRect = window.GetScreenRect(); - wxPoint max = projScreenRect.GetBottomRight() + wxPoint{ -dialogScreenRect.width, -dialogScreenRect.height }; - if (dialogScreenRect.x > max.x) { - position.x = max.x; - dialog.Move(position); - } - if (dialogScreenRect.y > max.y) { - position.y = max.y; - dialog.Move(position); - } - - dialog.SetInsertionPointEnd(); // because, by default, initial text is selected - int status = dialog.ShowModal(); - if (status != wxID_CANCEL) { - value = dialog.GetValue(); - value.Trim(true).Trim(false); - } - - return status; -} diff --git a/src/LabelTrack.h b/src/LabelTrack.h index e8976c6ab..47f9c9a36 100644 --- a/src/LabelTrack.h +++ b/src/LabelTrack.h @@ -17,20 +17,11 @@ #include "Track.h" -class wxFont; -class wxKeyEvent; -class wxMouseEvent; class wxTextFile; -class wxWindow; -class wxIcon; -class wxBitmap; -class TrackList; class AudacityProject; class DirManager; class TimeWarper; -class ZoomInfo; - struct LabelTrackHit; struct TrackPanelDrawingContext; @@ -38,18 +29,12 @@ struct TrackPanelDrawingContext; class LabelStruct { public: + LabelStruct() = default; // Copies region LabelStruct(const SelectedRegion& region, const wxString &aTitle); // Copies region but then overwrites other times LabelStruct(const SelectedRegion& region, double t0, double t1, const wxString &aTitle); - void DrawLines( wxDC & dc, const wxRect & r) const; - void DrawGlyphs - ( wxDC & dc, const wxRect & r, int GlyphLeft, int GlyphRight) const; - void DrawText( wxDC & dc, const wxRect & r) const; - void DrawTextBox( wxDC & dc, const wxRect & r) const; - void DrawHighlight( wxDC & dc, int xPos1, int xPos2, int charHeight) const; - void getXPos( wxDC & dc, int * xPos1, int cursorPos) const; const SelectedRegion &getSelectedRegion() const { return selectedRegion; } double getDuration() const { return selectedRegion.duration(); } double getT0() const { return selectedRegion.t0(); } @@ -84,54 +69,33 @@ public: public: SelectedRegion selectedRegion; wxString title; /// Text of the label. - mutable int width; /// width of the text in pixels. + mutable int width{}; /// width of the text in pixels. // Working storage for on-screen layout. - mutable int x; /// Pixel position of left hand glyph - mutable int x1; /// Pixel position of right hand glyph - mutable int xText; /// Pixel position of left hand side of text box - mutable int y; /// Pixel position of label. + mutable int x{}; /// Pixel position of left hand glyph + mutable int x1{}; /// Pixel position of right hand glyph + mutable int xText{}; /// Pixel position of left hand side of text box + mutable int y{}; /// Pixel position of label. - bool updated; /// flag to tell if the label times were updated + bool updated{}; /// flag to tell if the label times were updated }; using LabelArray = std::vector; -const int NUM_GLYPH_CONFIGS = 3; -const int NUM_GLYPH_HIGHLIGHTS = 4; -const int MAX_NUM_ROWS =80; - -class AUDACITY_DLL_API LabelTrack final : public Track +class AUDACITY_DLL_API LabelTrack final + : public Track + , public wxEvtHandler { - friend class LabelTrackView; - friend class LabelStruct; - public: - static void DoEditLabels( - AudacityProject &project, LabelTrack *lt = nullptr, int index = -1); - static int DialogForLabelName( - AudacityProject &project, const SelectedRegion& region, - const wxString& initialValue, wxString& value); - - bool IsTextSelected() const; - - void CreateCustomGlyphs(); LabelTrack(const std::shared_ptr &projDirManager); LabelTrack(const LabelTrack &orig); virtual ~ LabelTrack(); + void SetLabel( size_t iLabel, const LabelStruct &newLabel ); + void SetOffset(double dOffset) override; - static const int DefaultFontSize = 12; - - static wxFont GetFont(const wxString &faceName, int size = DefaultFontSize); - static void ResetFont(); - - void Draw( TrackPanelDrawingContext &context, const wxRect & r ) const; - - int getSelectedIndex() const { return mSelIndex; } - double GetOffset() const override; double GetStartTime() const override; double GetEndTime() const override; @@ -142,8 +106,6 @@ private: Track::Holder Clone() const override; public: - void SetSelected(bool s) override; - bool HandleXMLTag(const wxChar *tag, const wxChar **attrs) override; XMLTagHandler *HandleXMLChild(const wxChar *tag) override; void WriteXML(XMLWriter &xmlFile) const override; @@ -161,76 +123,21 @@ public: void Silence(double t0, double t1) override; void InsertSilence(double t, double len) override; - void OverGlyph(LabelTrackHit &hit, int x, int y) const; - static wxBitmap & GetGlyph( int i); - - - struct Flags { - int mInitialCursorPos, mCurrentCursorPos, mSelIndex; - bool mRightDragging, mDrawCursor; - }; - void ResetFlags(); - Flags SaveFlags() const - { - return { - mInitialCursorPos, mCurrentCursorPos, mSelIndex, - mRightDragging, mDrawCursor - }; - } - void RestoreFlags( const Flags& flags ); - - int OverATextBox(int xx, int yy) const; - bool OverTextBox(const LabelStruct *pLabel, int x, int y) const; - bool CutSelectedText(); - bool CopySelectedText(); - bool PasteSelectedText(double sel0, double sel1); - static bool IsTextClipSupported(); - - void HandleGlyphClick - (LabelTrackHit &hit, - const wxMouseEvent & evt, const wxRect & r, const ZoomInfo &zoomInfo, - SelectedRegion *newSel); - void HandleTextClick - (const wxMouseEvent & evt, const wxRect & r, const ZoomInfo &zoomInfo, - SelectedRegion *newSel); - bool HandleGlyphDragRelease - (LabelTrackHit &hit, - const wxMouseEvent & evt, wxRect & r, const ZoomInfo &zoomInfo, - SelectedRegion *newSel); - void HandleTextDragRelease(const wxMouseEvent & evt); void Import(wxTextFile & f); void Export(wxTextFile & f) const; - void Unselect(); - - // Whether any label box is selected -- not, whether the track is selected. - bool HasSelection() const; - int GetNumLabels() const; const LabelStruct *GetLabel(int index) const; const LabelArray &GetLabels() const { return mLabels; } + void OnLabelAdded( const wxString &title, int pos ); //This returns the index of the label we just added. - int AddLabel(const SelectedRegion ®ion, const wxString &title = {}, - int restoreFocus = -1); - //And this tells us the index, if there is a label already there. - int GetLabelIndex(double t, double t1); + int AddLabel(const SelectedRegion ®ion, const wxString &title); //This deletes the label at given index. void DeleteLabel(int index); - //get current cursor position, - // relative to the left edge of the track panel - bool CalcCursorX(int * x) const; - - void CalcHighlightXs(int *x1, int *x2) const; - - void MayAdjustLabel - ( LabelTrackHit &hit, - int iLabel, int iEdge, bool bAllowSwapping, double fNewTime); - void MayMoveLabel( int iLabel, int iEdge, double fNewTime); - // This pastes labels without shifting existing ones bool PasteOver(double t, const Track *src); @@ -251,57 +158,61 @@ public: int FindPrevLabel(const SelectedRegion& currentSelection); public: - void SortLabels(LabelTrackHit *pHit = nullptr); + void SortLabels(); + private: TrackKind GetKind() const override { return TrackKind::Label; } - void ShowContextMenu(); - void OnContextMenu(wxCommandEvent & evt); - - int mSelIndex; /// Keeps track of the currently selected label - int mxMouseDisplacement; /// Displacement of mouse cursor from the centre being dragged. LabelArray mLabels; - static int mIconHeight; - static int mIconWidth; - static int mTextHeight; - static bool mbGlyphsReady; - static wxBitmap mBoundaryGlyphs[NUM_GLYPH_CONFIGS * NUM_GLYPH_HIGHLIGHTS]; - - static int mFontHeight; - int mCurrentCursorPos; /// current cursor position - int mInitialCursorPos; /// initial cursor position - - bool mRightDragging; /// flag to tell if it's a valid dragging - bool mDrawCursor; /// flag to tell if drawing the - /// cursor or not - int mRestoreFocus; /// Restore focus to this track - /// when done editing - // Set in copied label tracks double mClipLen; int miLastLabel; // used by FindNextLabel and FindPrevLabel - void ComputeLayout(const wxRect & r, const ZoomInfo &zoomInfo) const; - void ComputeTextPosition(const wxRect & r, int index) const; - -public: - int FindCurrentCursorPosition(int xPos); - void SetCurrentCursorPosition(int xPos); - private: - void calculateFontHeight(wxDC & dc) const; - void RemoveSelectedText(); - - static wxFont msFont; - -protected: std::shared_ptr DoGetView() override; std::shared_ptr DoGetControls() override; - - friend class GetInfoCommand; // to get labels. - friend class SetLabelCommand; // to set labels. }; +struct LabelTrackEvent : TrackListEvent +{ + explicit + LabelTrackEvent( + wxEventType commandType, const std::shared_ptr &pTrack, + const wxString &title, + int formerPosition, + int presentPosition + ) + : TrackListEvent{ commandType, pTrack } + , mTitle{ title } + , mFormerPosition{ formerPosition } + , mPresentPosition{ presentPosition } + {} + + LabelTrackEvent( const LabelTrackEvent& ) = default; + wxEvent *Clone() const override { + // wxWidgets will own the event object + return safenew LabelTrackEvent(*this); } + + wxString mTitle; + + // invalid for addition event + int mFormerPosition{ -1 }; + + // invalid for deletion event + int mPresentPosition{ -1 }; +}; + +// Posted when a label is added. +wxDECLARE_EXPORTED_EVENT(AUDACITY_DLL_API, + EVT_LABELTRACK_ADDITION, LabelTrackEvent); + +// Posted when a label is deleted. +wxDECLARE_EXPORTED_EVENT(AUDACITY_DLL_API, + EVT_LABELTRACK_DELETION, LabelTrackEvent); + +// Posted when a label is repositioned in the sequence of labels. +wxDECLARE_EXPORTED_EVENT(AUDACITY_DLL_API, + EVT_LABELTRACK_PERMUTED, LabelTrackEvent); #endif diff --git a/src/ProjectAudioManager.cpp b/src/ProjectAudioManager.cpp index 7a79a969f..dbdc7c3db 100644 --- a/src/ProjectAudioManager.cpp +++ b/src/ProjectAudioManager.cpp @@ -114,8 +114,7 @@ void ProjectAudioManager::OnAudioIOStopRecording() pTrack->AddLabel( SelectedRegion{ interval.first, interval.first + interval.second }, - wxString::Format(wxT("%ld"), counter++), - -2 ); + wxString::Format(wxT("%ld"), counter++)); ShowWarningDialog(&window, wxT("DropoutDetected"), _("\ Recorded audio was lost at the labeled locations. Possible causes:\n\ \n\ diff --git a/src/Track.h b/src/Track.h index 22904d87b..a37238707 100644 --- a/src/Track.h +++ b/src/Track.h @@ -334,7 +334,7 @@ private: bool GetSelected() const { return mSelected; } - virtual void SetSelected(bool s); + void SetSelected(bool s); public: @@ -1081,7 +1081,9 @@ struct TrackListEvent : public wxCommandEvent TrackListEvent( const TrackListEvent& ) = default; - wxEvent *Clone() const override { return new TrackListEvent(*this); } + wxEvent *Clone() const override { + // wxWidgets will own the event object + return safenew TrackListEvent(*this); } std::weak_ptr mpTrack; int mCode; diff --git a/src/TrackArtist.cpp b/src/TrackArtist.cpp index e55f0978c..3d627afa4 100644 --- a/src/TrackArtist.cpp +++ b/src/TrackArtist.cpp @@ -88,7 +88,7 @@ audio tracks. #include "widgets/Ruler.h" #include "AllThemeResources.h" #include "TrackPanelDrawingContext.h" -#include "tracks/ui/TrackView.h" +#include "tracks/labeltrack/ui/LabelTrackView.h" #undef PROFILE_WAVEFORM @@ -414,7 +414,7 @@ void TrackArt::DrawTrack(TrackPanelDrawingContext &context, }, #endif // USE_MIDI [&](const LabelTrack *lt) { - lt->Draw( context, rect ); + LabelTrackView::Get( *lt ).Draw( context, rect ); }, [&](const TimeTrack *tt) { DrawTimeTrack( context, tt, rect ); diff --git a/src/commands/GetInfoCommand.cpp b/src/commands/GetInfoCommand.cpp index c05aad4ea..36eaa5513 100644 --- a/src/commands/GetInfoCommand.cpp +++ b/src/commands/GetInfoCommand.cpp @@ -610,8 +610,7 @@ bool GetInfoCommand::SendLabels(const CommandContext &context) context.StartArray(); context.AddItem( (double)i ); // Track number. context.StartArray(); - for (int nn = 0; nn< (int)labelTrack->mLabels.size(); nn++) { - const auto &label = labelTrack->mLabels[nn]; + for ( const auto &label : labelTrack->GetLabels() ) { context.StartArray(); context.AddItem( label.getT0() ); // start context.AddItem( label.getT1() ); // end diff --git a/src/commands/SetLabelCommand.cpp b/src/commands/SetLabelCommand.cpp index 9641f9eb4..f3c973e1c 100644 --- a/src/commands/SetLabelCommand.cpp +++ b/src/commands/SetLabelCommand.cpp @@ -25,6 +25,7 @@ #include "../Shuttle.h" #include "../ShuttleGui.h" #include "CommandContext.h" +#include "../tracks/labeltrack/ui/LabelTrackView.h" SetLabelCommand::SetLabelCommand() { @@ -68,49 +69,51 @@ bool SetLabelCommand::Apply(const CommandContext & context) AudacityProject * p = &context.project; auto &tracks = TrackList::Get( *p ); auto &selectedRegion = ViewInfo::Get( *p ).selectedRegion; - LabelStruct * pLabel = NULL; - int i=0; - int nn=0; - - LabelTrack *labelTrack {}; - for (auto lt : tracks.Any()) { - if( i > mLabelIndex ) - break; - labelTrack = lt; - for (nn = 0; - (nn< (int)labelTrack->mLabels.size()) && i<=mLabelIndex; - nn++) { - i++; - pLabel = &labelTrack->mLabels[nn]; + const LabelStruct * pLabel = nullptr; + LabelTrack *labelTrack = nullptr; + auto ii = mLabelIndex; + if ( mLabelIndex >= 0 ) { + for (auto lt : tracks.Any()) { + const auto &labels = lt->GetLabels(); + const auto nLabels = labels.size(); + if( ii >= nLabels ) + ii -= nLabels; + else { + labelTrack = lt; + pLabel = &labels[ ii ]; + break; + } } } - if ( (i< mLabelIndex) || (pLabel == NULL)) + if ( !pLabel ) { context.Error(wxT("LabelIndex was invalid.")); return false; } + auto newLabel = *pLabel; if( bHasText ) - pLabel->title = mText; + newLabel.title = mText; if( bHasT0 ) - pLabel->selectedRegion.setT0(mT0, false); + newLabel.selectedRegion.setT0(mT0, false); if( bHasT1 ) - pLabel->selectedRegion.setT1(mT1, false); + newLabel.selectedRegion.setT1(mT1, false); if( bHasT0 || bHasT1 ) - pLabel->selectedRegion.ensureOrdering(); - pLabel->updated = true; + newLabel.selectedRegion.ensureOrdering(); + labelTrack->SetLabel( ii, newLabel ); // Only one label can be selected. - if( bHasSelected ){ + if( bHasSelected ) { + auto &view = LabelTrackView::Get( *labelTrack ); if( mbSelected ) { - labelTrack->mSelIndex = nn-1; + view.SetSelectedIndex( ii ); double t0 = pLabel->selectedRegion.t0(); double t1 = pLabel->selectedRegion.t1(); selectedRegion.setTimes( t0, t1); } - else if( labelTrack->mSelIndex == (nn-1) ) - labelTrack->mSelIndex = -1; + else if( view.GetSelectedIndex() == ii ) + view.SetSelectedIndex( -1 ); } labelTrack->SortLabels(); diff --git a/src/commands/SetLabelCommand.h b/src/commands/SetLabelCommand.h index 8f9cebdab..4ade1b35f 100644 --- a/src/commands/SetLabelCommand.h +++ b/src/commands/SetLabelCommand.h @@ -37,6 +37,8 @@ public: bool Apply(const CommandContext & context) override; public: + // zero-based index of the desired label, within the concatenation of the + // arrays of labels of all label tracks int mLabelIndex; wxString mText; double mT0; diff --git a/src/effects/FindClipping.cpp b/src/effects/FindClipping.cpp index 4cdf4b358..2813de22c 100644 --- a/src/effects/FindClipping.cpp +++ b/src/effects/FindClipping.cpp @@ -216,8 +216,7 @@ bool EffectFindClipping::ProcessOne(LabelTrack * lt, if (stoprun >= mStop) { lt->AddLabel(SelectedRegion(startTime, wt->LongSamplesToTime(start + s - mStop)), - wxString::Format(wxT("%lld of %lld"), startrun.as_long_long(), (samps - mStop).as_long_long()), - -2); + wxString::Format(wxT("%lld of %lld"), startrun.as_long_long(), (samps - mStop).as_long_long())); startrun = 0; stoprun = 0; samps = 0; diff --git a/src/effects/nyquist/Nyquist.cpp b/src/effects/nyquist/Nyquist.cpp index 2b7008d4b..83fc3113e 100644 --- a/src/effects/nyquist/Nyquist.cpp +++ b/src/effects/nyquist/Nyquist.cpp @@ -1406,7 +1406,7 @@ bool NyquistEffect::ProcessOne() // let Nyquist analyzers define more complicated selections nyx_get_label(l, &t0, &t1, &str); - ltrack->AddLabel(SelectedRegion(t0 + mT0, t1 + mT0), UTF8CTOWX(str), -2); + ltrack->AddLabel(SelectedRegion(t0 + mT0, t1 + mT0), UTF8CTOWX(str)); } return (GetType() != EffectTypeProcess || mIsPrompt); } diff --git a/src/effects/vamp/VampEffect.cpp b/src/effects/vamp/VampEffect.cpp index 7e460e1d0..fc8e543e8 100644 --- a/src/effects/vamp/VampEffect.cpp +++ b/src/effects/vamp/VampEffect.cpp @@ -759,7 +759,7 @@ void VampEffect::AddFeatures(LabelTrack *ltrack, } } - ltrack->AddLabel(SelectedRegion(ltime0, ltime1), label, -2); + ltrack->AddLabel(SelectedRegion(ltime0, ltime1), label); } } diff --git a/src/menus/EditMenus.cpp b/src/menus/EditMenus.cpp index a807361eb..8134a8ca9 100644 --- a/src/menus/EditMenus.cpp +++ b/src/menus/EditMenus.cpp @@ -22,6 +22,7 @@ #include "../prefs/PrefsDialog.h" #include "../prefs/SpectrogramSettings.h" #include "../prefs/WaveformSettings.h" +#include "../tracks/labeltrack/ui/LabelTrackView.h" #include "../widgets/AudacityMessageBox.h" // private helper classes and functions @@ -46,10 +47,11 @@ bool DoPasteText(AudacityProject &project) for (auto pLabelTrack : tracks.Any()) { // Does this track have an active label? - if (pLabelTrack->HasSelection()) { + if (LabelTrackView::Get( *pLabelTrack ).HasSelection()) { // Yes, so try pasting into it - if (pLabelTrack->PasteSelectedText(selectedRegion.t0(), + auto &view = LabelTrackView::Get( *pLabelTrack ); + if (view.PasteSelectedText(selectedRegion.t0(), selectedRegion.t1())) { ProjectHistory::Get( project ) @@ -57,7 +59,7 @@ bool DoPasteText(AudacityProject &project) // Make sure caret is in view int x; - if (pLabelTrack->CalcCursorX(&x)) { + if (view.CalcCursorX(&x)) { trackPanel.ScrollIntoView(x); } @@ -301,7 +303,8 @@ void OnCut(const CommandContext &context) // in the middle of editing the label text and select "Cut". for (auto lt : tracks.Selected< LabelTrack >()) { - if (lt->CutSelectedText()) { + auto &view = LabelTrackView::Get( *lt ); + if (view.CutSelectedText()) { trackPanel.Refresh(false); return; } @@ -408,7 +411,8 @@ void OnCopy(const CommandContext &context) auto &selectedRegion = ViewInfo::Get( project ).selectedRegion; for (auto lt : tracks.Selected< LabelTrack >()) { - if (lt->CopySelectedText()) { + auto &view = LabelTrackView::Get( *lt ); + if (view.CopySelectedText()) { //trackPanel.Refresh(false); return; } @@ -1095,7 +1099,9 @@ const ReservedCommandFlag CutCopyAvailableFlag{ [](const AudacityProject &project){ auto range = TrackList::Get( project ).Any() - + &LabelTrack::IsTextSelected; + + [](const LabelTrack *pTrack){ + return LabelTrackView::Get( *pTrack ).IsTextSelected(); + }; if ( !range.empty() ) return true; diff --git a/src/menus/LabelMenus.cpp b/src/menus/LabelMenus.cpp index 26b300810..b5942f266 100644 --- a/src/menus/LabelMenus.cpp +++ b/src/menus/LabelMenus.cpp @@ -12,6 +12,7 @@ #include "../WaveTrack.h" #include "../commands/CommandContext.h" #include "../commands/CommandManager.h" +#include "../tracks/labeltrack/ui/LabelTrackView.h" // private helper classes and functions namespace { @@ -31,7 +32,7 @@ int DoAddLabel( bool useDialog; gPrefs->Read(wxT("/GUI/DialogForNameNewLabel"), &useDialog, false); if (useDialog) { - if (LabelTrack::DialogForLabelName( + if (LabelTrackView::DialogForLabelName( project, region, wxEmptyString, title) == wxID_CANCEL) return -1; // index } @@ -57,22 +58,22 @@ int DoAddLabel( // SelectNone(); lt->SetSelected(true); - int focusTrackNumber; + int index; if (useDialog) { - focusTrackNumber = -2; + index = lt->AddLabel(region, title); } else { - focusTrackNumber = -1; + int focusTrackNumber = -1; if (pFocusedTrack && preserveFocus) { // Must remember the track to re-focus after finishing a label edit. // do NOT identify it by a pointer, which might dangle! Identify // by position. focusTrackNumber = pFocusedTrack->GetIndex(); } + index = + LabelTrackView::Get( *lt ).AddLabel(region, title, focusTrackNumber); } - int index = lt->AddLabel(region, title, focusTrackNumber); - ProjectHistory::Get( project ).PushState(_("Added label"), _("Label")); window.RedrawProject(); @@ -256,7 +257,7 @@ struct Handler : CommandHandlerObject { void OnEditLabels(const CommandContext &context) { auto &project = context.project; - LabelTrack::DoEditLabels(project); + LabelTrackView::DoEditLabels(project); } void OnAddLabel(const CommandContext &context) @@ -318,12 +319,13 @@ void OnPasteNewLabel(const CommandContext &context) // Unselect the last label, so we'll have just one active label when // we're done if (plt) - plt->Unselect(); + LabelTrackView::Get( *plt ).SetSelectedIndex( -1 ); // Add a NEW label, paste into it // Paul L: copy whatever defines the selected region, not just times - lt->AddLabel(selectedRegion); - if (lt->PasteSelectedText(selectedRegion.t0(), + auto &view = LabelTrackView::Get( *lt ); + view.AddLabel(selectedRegion); + if (view.PasteSelectedText(selectedRegion.t0(), selectedRegion.t1())) bPastedSomething = true; diff --git a/src/tracks/labeltrack/ui/LabelDefaultClickHandle.cpp b/src/tracks/labeltrack/ui/LabelDefaultClickHandle.cpp index 68f55f185..3f21f426a 100644 --- a/src/tracks/labeltrack/ui/LabelDefaultClickHandle.cpp +++ b/src/tracks/labeltrack/ui/LabelDefaultClickHandle.cpp @@ -11,6 +11,7 @@ Paul Licameli split from TrackPanel.cpp #include "../../../Audacity.h" #include "LabelDefaultClickHandle.h" +#include "LabelTrackView.h" #include "../../ui/TrackView.h" #include "../../../HitTestResult.h" #include "../../../LabelTrack.h" @@ -26,7 +27,9 @@ LabelDefaultClickHandle::~LabelDefaultClickHandle() } struct LabelDefaultClickHandle::LabelState { - std::vector< std::pair< std::weak_ptr, LabelTrack::Flags > > mPairs; + std::vector< + std::pair< std::weak_ptr, LabelTrackView::Flags > + > mPairs; }; void LabelDefaultClickHandle::SaveState( AudacityProject *pProject ) @@ -35,17 +38,21 @@ void LabelDefaultClickHandle::SaveState( AudacityProject *pProject ) auto &pairs = mLabelState->mPairs; auto &tracks = TrackList::Get( *pProject ); - for (auto lt : tracks.Any()) + for (auto lt : tracks.Any()) { + auto &view = LabelTrackView::Get( *lt ); pairs.push_back( std::make_pair( - lt->SharedPointer(), lt->SaveFlags() ) ); + lt->SharedPointer(), view.SaveFlags() ) ); + } } void LabelDefaultClickHandle::RestoreState( AudacityProject *pProject ) { if ( mLabelState ) { for ( const auto &pair : mLabelState->mPairs ) - if (auto pLt = TrackList::Get( *pProject ).Lock(pair.first)) - pLt->RestoreFlags( pair.second ); + if (auto pLt = TrackList::Get( *pProject ).Lock(pair.first)) { + auto &view = LabelTrackView::Get( *pLt ); + view.RestoreFlags( pair.second ); + } mLabelState.reset(); } } @@ -64,8 +71,9 @@ UIHandle::Result LabelDefaultClickHandle::Click const auto pLT = evt.pCell.get(); for (auto lt : TrackList::Get( *pProject ).Any()) { if (pLT != &TrackView::Get( *lt )) { - lt->ResetFlags(); - lt->Unselect(); + auto &view = LabelTrackView::Get( *lt ); + view.ResetFlags(); + view.SetSelectedIndex( -1 ); } } } diff --git a/src/tracks/labeltrack/ui/LabelGlyphHandle.cpp b/src/tracks/labeltrack/ui/LabelGlyphHandle.cpp index cdb7a2d0e..cfb5cdc8e 100644 --- a/src/tracks/labeltrack/ui/LabelGlyphHandle.cpp +++ b/src/tracks/labeltrack/ui/LabelGlyphHandle.cpp @@ -11,6 +11,7 @@ Paul Licameli split from TrackPanel.cpp #include "../../../Audacity.h" #include "LabelGlyphHandle.h" +#include "LabelTrackView.h" #include "../../../HitTestResult.h" #include "../../../LabelTrack.h" #include "../../../ProjectHistory.h" @@ -22,10 +23,46 @@ Paul Licameli split from TrackPanel.cpp #include #include +LabelTrackHit::LabelTrackHit( const std::shared_ptr &pLT ) + : mpLT{ pLT } +{ + pLT->Bind( + EVT_LABELTRACK_PERMUTED, &LabelTrackHit::OnLabelPermuted, this ); +} + +LabelTrackHit::~LabelTrackHit() +{ + // Must do this because this sink isn't wxEvtHandler + mpLT->Unbind( + EVT_LABELTRACK_PERMUTED, &LabelTrackHit::OnLabelPermuted, this ); +} + +void LabelTrackHit::OnLabelPermuted( LabelTrackEvent &e ) +{ + e.Skip(); + if ( e.mpTrack.lock() != mpLT ) + return; + + auto former = e.mFormerPosition; + auto present = e.mPresentPosition; + + auto update = [=]( int &index ){ + if ( index == former ) + index = present; + else if ( former < index && index <= present ) + -- index; + else if ( former > index && index >= present ) + ++ index; + }; + + update( mMouseOverLabelLeft ); + update( mMouseOverLabelRight ); +} + LabelGlyphHandle::LabelGlyphHandle (const std::shared_ptr &pLT, - const wxRect &rect, const LabelTrackHit &hit) - : mHit{ hit } + const wxRect &rect, const std::shared_ptr &pHit) + : mpHit{ pHit } , mpLT{ pLT } , mRect{ rect } { @@ -39,7 +76,7 @@ void LabelGlyphHandle::Enter(bool) UIHandle::Result LabelGlyphHandle::NeedChangeHighlight (const LabelGlyphHandle &oldState, const LabelGlyphHandle &newState) { - if (oldState.mHit.mEdge != newState.mHit.mEdge) + if (oldState.mpHit->mEdge != newState.mpHit->mEdge) // pointer moves between the circle and the chevron return RefreshCode::RefreshCell; return 0; @@ -61,14 +98,18 @@ UIHandlePtr LabelGlyphHandle::HitTest const wxMouseState &state, const std::shared_ptr &pLT, const wxRect &rect) { - LabelTrackHit hit{}; - pLT->OverGlyph(hit, state.m_x, state.m_y); + // Allocate on heap because there are pointers to it when it is bound as + // an event sink, therefore it's not copyable; make it shared so + // LabelGlyphHandle can be copyable: + auto pHit = std::make_shared( pLT ); + + LabelTrackView::OverGlyph(*pLT, *pHit, state.m_x, state.m_y); // IF edge!=0 THEN we've set the cursor and we're done. // signal this by setting the tip. - if ( hit.mEdge & 3 ) + if ( pHit->mEdge & 3 ) { - auto result = std::make_shared( pLT, rect, hit ); + auto result = std::make_shared( pLT, rect, pHit ); result = AssignUIHandlePtr(holder, result); return result; } @@ -80,6 +121,64 @@ LabelGlyphHandle::~LabelGlyphHandle() { } +void LabelGlyphHandle::HandleGlyphClick +(LabelTrackHit &hit, const wxMouseEvent & evt, + const wxRect & r, const ZoomInfo &zoomInfo, + SelectedRegion *WXUNUSED(newSel)) +{ + if (evt.ButtonDown()) + { + //OverGlyph sets mMouseOverLabel to be the chosen label. + const auto pTrack = mpLT; + LabelTrackView::OverGlyph(*pTrack, hit, evt.m_x, evt.m_y); + hit.mIsAdjustingLabel = evt.Button(wxMOUSE_BTN_LEFT) && + ( hit.mEdge & 3 ) != 0; + + if (hit.mIsAdjustingLabel) + { + float t = 0.0; + // We move if we hit the centre, we adjust one edge if we hit a chevron. + // This is if we are moving just one edge. + hit.mbIsMoving = (hit.mEdge & 4)!=0; + // When we start dragging the label(s) we don't want them to jump. + // so we calculate the displacement of the mouse from the drag center + // and use that in subsequent dragging calculations. The mouse stays + // at the same relative displacement throughout dragging. + + // However, if two label's edges are being dragged + // then the displacement is relative to the initial average + // position of them, and in that case there can be a jump of at most + // a few pixels to bring the two label boundaries to exactly the same + // position when we start dragging. + + // Dragging of three label edges at the same time is not supported (yet). + + const auto &mLabels = pTrack->GetLabels(); + if( ( hit.mMouseOverLabelRight >= 0 ) && + ( hit.mMouseOverLabelLeft >= 0 ) + ) + { + t = (mLabels[ hit.mMouseOverLabelRight ].getT1() + + mLabels[ hit.mMouseOverLabelLeft ].getT0()) / 2.0f; + // If we're moving two edges, then it's a move (label size preserved) + // if both edges are the same label, and it's an adjust (label sizes change) + // if we're on a boundary between two different labels. + hit.mbIsMoving = + ( hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight ); + } + else if( hit.mMouseOverLabelRight >=0) + { + t = mLabels[ hit.mMouseOverLabelRight ].getT1(); + } + else if( hit.mMouseOverLabelLeft >=0) + { + t = mLabels[ hit.mMouseOverLabelLeft ].getT0(); + } + mxMouseDisplacement = zoomInfo.TimeToPosition(t, r.x) - evt.m_x; + } + } +} + UIHandle::Result LabelGlyphHandle::Click (const TrackPanelMouseEvent &evt, AudacityProject *pProject) { @@ -88,10 +187,10 @@ UIHandle::Result LabelGlyphHandle::Click const wxMouseEvent &event = evt.event; auto &viewInfo = ViewInfo::Get( *pProject ); - mpLT->HandleGlyphClick - (mHit, event, mRect, viewInfo, &viewInfo.selectedRegion); + HandleGlyphClick( + *mpHit, event, mRect, viewInfo, &viewInfo.selectedRegion); - if (! mHit.mIsAdjustingLabel ) + if (! mpHit->mIsAdjustingLabel ) { // The positive hit test should have ensured otherwise //wxASSERT(false); @@ -111,6 +210,143 @@ UIHandle::Result LabelGlyphHandle::Click return result; } +/// If the index is for a real label, adjust its left or right boundary. +/// @iLabel - index of label, -1 for none. +/// @iEdge - which edge is requested to move, -1 for left +1 for right. +/// @bAllowSwapping - if we can switch which edge is being dragged. +/// fNewTime - the NEW time for this edge of the label. +void LabelGlyphHandle::MayAdjustLabel +( LabelTrackHit &hit, int iLabel, int iEdge, bool bAllowSwapping, double fNewTime) +{ + if( iLabel < 0 ) + return; + + const auto pTrack = mpLT; + const auto &mLabels = pTrack->GetLabels(); + auto labelStruct = mLabels[ iLabel ]; + + // Adjust the requested edge. + bool flipped = labelStruct.AdjustEdge( iEdge, fNewTime ); + // If the edges did not swap, then we are done. + if( ! flipped ) { + pTrack->SetLabel( iLabel, labelStruct ); + return; + } + + // If swapping's not allowed we must also move the edge + // we didn't move. Then we're done. + if( !bAllowSwapping ) + { + labelStruct.AdjustEdge( -iEdge, fNewTime ); + pTrack->SetLabel( iLabel, labelStruct ); + return; + } + + pTrack->SetLabel( iLabel, labelStruct ); + + // Swap our record of what we are dragging. + std::swap( hit.mMouseOverLabelLeft, hit.mMouseOverLabelRight ); +} + +// If the index is for a real label, adjust its left and right boundary. +void LabelGlyphHandle::MayMoveLabel( int iLabel, int iEdge, double fNewTime) +{ + if( iLabel < 0 ) + return; + + const auto pTrack = mpLT; + const auto &mLabels = pTrack->GetLabels(); + auto labelStruct = mLabels[ iLabel ]; + labelStruct.MoveLabel( iEdge, fNewTime ); + pTrack->SetLabel( iLabel, labelStruct ); +} + +// Constrain function, as in processing/arduino. +// returned value will be between min and max (inclusive). +static int Constrain( int value, int min, int max ) +{ + wxASSERT( min <= max ); + int result=value; + if( result < min ) + result=min; + if( result > max ) + result=max; + return result; +} + +bool LabelGlyphHandle::HandleGlyphDragRelease +(LabelTrackHit &hit, const wxMouseEvent & evt, + wxRect & r, const ZoomInfo &zoomInfo, + SelectedRegion *newSel) +{ + const auto pTrack = mpLT; + const auto &mLabels = pTrack->GetLabels(); + if(evt.LeftUp()) + { + bool lupd = false, rupd = false; + if( hit.mMouseOverLabelLeft >= 0 ) { + auto labelStruct = mLabels[ hit.mMouseOverLabelLeft ]; + lupd = labelStruct.updated; + labelStruct.updated = false; + pTrack->SetLabel( hit.mMouseOverLabelLeft, labelStruct ); + } + if( hit.mMouseOverLabelRight >= 0 ) { + auto labelStruct = mLabels[ hit.mMouseOverLabelRight ]; + rupd = labelStruct.updated; + labelStruct.updated = false; + pTrack->SetLabel( hit.mMouseOverLabelRight, labelStruct ); + } + + hit.mIsAdjustingLabel = false; + hit.mMouseOverLabelLeft = -1; + hit.mMouseOverLabelRight = -1; + return lupd || rupd; + } + + if(evt.Dragging()) + { + //If we are currently adjusting a label, + //just reset its value and redraw. + // LL: Constrain to inside track rectangle for now. Should be changed + // to allow scrolling while dragging labels + int x = Constrain( evt.m_x + mxMouseDisplacement - r.x, 0, r.width); + + // If exactly one edge is selected we allow swapping + bool bAllowSwapping = + ( hit.mMouseOverLabelLeft >=0 ) != + ( hit.mMouseOverLabelRight >= 0); + // If we're on the 'dot' and nowe're moving, + // Though shift-down inverts that. + // and if both edges the same, then we're always moving the label. + bool bLabelMoving = hit.mbIsMoving; + bLabelMoving ^= evt.ShiftDown(); + bLabelMoving |= ( hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight ); + double fNewX = zoomInfo.PositionToTime(x, 0); + if( bLabelMoving ) + { + MayMoveLabel( hit.mMouseOverLabelLeft, -1, fNewX ); + MayMoveLabel( hit.mMouseOverLabelRight, +1, fNewX ); + } + else + { + MayAdjustLabel( hit, hit.mMouseOverLabelLeft, -1, bAllowSwapping, fNewX ); + MayAdjustLabel( hit, hit.mMouseOverLabelRight, +1, bAllowSwapping, fNewX ); + } + + const auto &view = LabelTrackView::Get( *pTrack ); + if( view.HasSelection() ) + { + auto selIndex = view.GetSelectedIndex(); + //Set the selection region to be equal to + //the NEW size of the label. + *newSel = mLabels[ selIndex ].selectedRegion; + } + pTrack->SortLabels(); + } + + return false; +} + UIHandle::Result LabelGlyphHandle::Drag (const TrackPanelMouseEvent &evt, AudacityProject *pProject) { @@ -118,8 +354,8 @@ UIHandle::Result LabelGlyphHandle::Drag const wxMouseEvent &event = evt.event; auto &viewInfo = ViewInfo::Get( *pProject ); - mpLT->HandleGlyphDragRelease - (mHit, event, mRect, viewInfo, &viewInfo.selectedRegion); + HandleGlyphDragRelease( + *mpHit, event, mRect, viewInfo, &viewInfo.selectedRegion); // Refresh all so that the change of selection is redrawn in all tracks return result | RefreshCode::RefreshAll | RefreshCode::DrawOverlays; @@ -128,7 +364,7 @@ UIHandle::Result LabelGlyphHandle::Drag HitTestPreview LabelGlyphHandle::Preview (const TrackPanelMouseState &, const AudacityProject *) { - return HitPreview( (mHit.mEdge & 4 )!=0); + return HitPreview( (mpHit->mEdge & 4 )!=0); } UIHandle::Result LabelGlyphHandle::Release @@ -139,8 +375,8 @@ UIHandle::Result LabelGlyphHandle::Release const wxMouseEvent &event = evt.event; auto &viewInfo = ViewInfo::Get( *pProject ); - if (mpLT->HandleGlyphDragRelease - (mHit, event, mRect, viewInfo, &viewInfo.selectedRegion)) { + if (HandleGlyphDragRelease( + *mpHit, event, mRect, viewInfo, &viewInfo.selectedRegion)) { ProjectHistory::Get( *pProject ).PushState(_("Modified Label"), _("Label Edit"), UndoPush::CONSOLIDATE); diff --git a/src/tracks/labeltrack/ui/LabelGlyphHandle.h b/src/tracks/labeltrack/ui/LabelGlyphHandle.h index a45655931..276ea9ca7 100644 --- a/src/tracks/labeltrack/ui/LabelGlyphHandle.h +++ b/src/tracks/labeltrack/ui/LabelGlyphHandle.h @@ -15,6 +15,9 @@ Paul Licameli split from TrackPanel.cpp class wxMouseState; class LabelTrack; +class LabelTrackEvent; +class SelectedRegion; +class ZoomInfo; /// mEdge: /// 0 if not over a glyph, @@ -26,12 +29,20 @@ class LabelTrack; /// mMouseLabelLeft - index of any left label hit /// mMouseLabelRight - index of any right label hit /// -struct LabelTrackHit { +struct LabelTrackHit +{ + LabelTrackHit( const std::shared_ptr &pLT ); + ~LabelTrackHit(); + int mEdge{}; int mMouseOverLabelLeft{ -1 }; /// Keeps track of which left label the mouse is currently over. int mMouseOverLabelRight{ -1 }; /// Keeps track of which right label the mouse is currently over. bool mbIsMoving {}; bool mIsAdjustingLabel {}; + + std::shared_ptr mpLT {}; + + void OnLabelPermuted( LabelTrackEvent &e ); }; class LabelGlyphHandle final : public LabelDefaultClickHandle @@ -41,7 +52,7 @@ class LabelGlyphHandle final : public LabelDefaultClickHandle public: explicit LabelGlyphHandle (const std::shared_ptr &pLT, - const wxRect &rect, const LabelTrackHit &hit); + const wxRect &rect, const std::shared_ptr &pHit); LabelGlyphHandle &operator=(const LabelGlyphHandle&) = default; @@ -72,14 +83,31 @@ public: bool StopsOnKeystroke() override { return true; } - LabelTrackHit mHit{}; + std::shared_ptr mpHit{}; static UIHandle::Result NeedChangeHighlight (const LabelGlyphHandle &oldState, const LabelGlyphHandle &newState); private: + void HandleGlyphClick + (LabelTrackHit &hit, + const wxMouseEvent & evt, const wxRect & r, const ZoomInfo &zoomInfo, + SelectedRegion *newSel); + bool HandleGlyphDragRelease + (LabelTrackHit &hit, + const wxMouseEvent & evt, wxRect & r, const ZoomInfo &zoomInfo, + SelectedRegion *newSel); + + void MayAdjustLabel + ( LabelTrackHit &hit, + int iLabel, int iEdge, bool bAllowSwapping, double fNewTime); + void MayMoveLabel( int iLabel, int iEdge, double fNewTime); + std::shared_ptr mpLT {}; wxRect mRect {}; + + /// Displacement of mouse cursor from the centre being dragged. + int mxMouseDisplacement; }; #endif diff --git a/src/tracks/labeltrack/ui/LabelTextHandle.cpp b/src/tracks/labeltrack/ui/LabelTextHandle.cpp index 5b7b7f2fa..e290fda8e 100644 --- a/src/tracks/labeltrack/ui/LabelTextHandle.cpp +++ b/src/tracks/labeltrack/ui/LabelTextHandle.cpp @@ -11,6 +11,7 @@ Paul Licameli split from TrackPanel.cpp #include "../../../Audacity.h" #include "LabelTextHandle.h" +#include "LabelTrackView.h" #include "../../../Experimental.h" #include "../../../HitTestResult.h" @@ -23,6 +24,8 @@ Paul Licameli split from TrackPanel.cpp #include "../../../ViewInfo.h" #include "../../../images/Cursors.h" +#include + LabelTextHandle::LabelTextHandle ( const std::shared_ptr &pLT, int labelNum ) : mpLT{ pLT } @@ -54,7 +57,8 @@ UIHandlePtr LabelTextHandle::HitTest // If Control is down, let the select handle be hit instead int labelNum; if (!state.ControlDown() && - (labelNum = pLT->OverATextBox(state.m_x, state.m_y) ) >= 0) { + (labelNum = + LabelTrackView::OverATextBox(*pLT, state.m_x, state.m_y) ) >= 0) { auto result = std::make_shared( pLT, labelNum ); result = AssignUIHandlePtr(holder, result); return result; @@ -67,6 +71,80 @@ LabelTextHandle::~LabelTextHandle() { } +void LabelTextHandle::HandleTextClick(const wxMouseEvent & evt, + const wxRect & r, const ZoomInfo &zoomInfo, + SelectedRegion *newSel) +{ + auto pTrack = mpLT.lock(); + if (!pTrack) + return; + + auto &view = LabelTrackView::Get( *pTrack ); + static_cast(r);//compiler food. + static_cast(zoomInfo);//compiler food. + if (evt.ButtonDown()) + { + const auto selIndex = LabelTrackView::OverATextBox( *pTrack, evt.m_x, evt.m_y ); + view.SetSelectedIndex( selIndex ); + if ( selIndex != -1 ) { + const auto &mLabels = pTrack->GetLabels(); + const auto &labelStruct = mLabels[ selIndex ]; + *newSel = labelStruct.selectedRegion; + + if (evt.LeftDown()) { + // Find the NEW drag end + auto position = view.FindCursorPosition( evt.m_x ); + + // Anchor shift-drag at the farther end of the previous highlight + // that is farther from the click, on Mac, for consistency with + // its text editors, but on the others, re-use the previous + // anchor. + auto initial = view.GetInitialCursorPosition(); + if (evt.ShiftDown()) { +#ifdef __WXMAC__ + // Set the drag anchor at the end of the previous selection + // that is farther from the NEW drag end + const auto current = view.GetCurrentCursorPosition(); + if ( abs( position - current ) > abs( position - initial ) ) + initial = current; +#else + // initial position remains as before +#endif + } + else + initial = position; + + view.SetTextHighlight( initial, position ); + mRightDragging = false; + } + else + // Actually this might be right or middle down + mRightDragging = true; + + // Middle click on GTK: paste from primary selection +#if defined(__WXGTK__) && (HAVE_GTK) + if (evt.MiddleDown()) { + // Check for a click outside of the selected label's text box; in this + // case PasteSelectedText() will start a NEW label at the click + // location + if (!LabelTrackView::OverTextBox(&labelStruct, evt.m_x, evt.m_y)) + view.SetSelectedIndex( -1 ); + double t = zoomInfo.PositionToTime(evt.m_x, r.x); + *newSel = SelectedRegion(t, t); + } +#endif + } +#if defined(__WXGTK__) && (HAVE_GTK) + if (evt.MiddleDown()) { + // Paste text, making a NEW label if none is selected. + wxTheClipboard->UsePrimarySelection(true); + view.PasteSelectedText(newSel->t0(), newSel->t1()); + wxTheClipboard->UsePrimarySelection(false); + } +#endif + } +} + UIHandle::Result LabelTextHandle::Click (const TrackPanelMouseEvent &evt, AudacityProject *pProject) { @@ -85,8 +163,7 @@ UIHandle::Result LabelTextHandle::Click auto &viewInfo = ViewInfo::Get( *pProject ); mSelectedRegion = viewInfo.selectedRegion; - pLT->HandleTextClick( event, evt.rect, viewInfo, &viewInfo.selectedRegion ); - wxASSERT(pLT->HasSelection()); + HandleTextClick( event, evt.rect, viewInfo, &viewInfo.selectedRegion ); { // IF the user clicked a label, THEN select all other tracks by Label @@ -115,6 +192,58 @@ UIHandle::Result LabelTextHandle::Click return result | RefreshCode::RefreshCell | RefreshCode::UpdateSelection; } +void LabelTextHandle::HandleTextDragRelease(const wxMouseEvent & evt) +{ + auto pTrack = mpLT.lock(); + if (!pTrack) + return; + auto &view = LabelTrackView::Get( *pTrack ); + + if(evt.LeftUp()) + { +#if 0 + // AWD: Due to wxWidgets bug #7491 (fix not ported to 2.8 branch) we + // should never write the primary selection. We can enable this block + // when we move to the 3.0 branch (or if a fixed 2.8 version is released + // and we can do a runtime version check) +#if defined (__WXGTK__) && defined (HAVE_GTK) + // On GTK, if we just dragged out a text selection, set the primary + // selection + if (mInitialCursorPos != mCurrentCursorPos) { + wxTheClipboard->UsePrimarySelection(true); + CopySelectedText(); + wxTheClipboard->UsePrimarySelection(false); + } +#endif +#endif + + return; + } + + if(evt.Dragging()) + { + if (!mRightDragging) + // Update drag end + view.SetCurrentCursorPosition( + view.FindCursorPosition( evt.m_x ) ); + + return; + } + + if (evt.RightUp()) { + const auto selIndex = view.GetSelectedIndex(); + if ( selIndex != -1 && + LabelTrackView::OverTextBox( + pTrack->GetLabel( selIndex ), evt.m_x, evt.m_y ) ) { + // popup menu for editing + // TODO: handle context menus via CellularPanel? + view.ShowContextMenu(); + } + } + + return; +} + UIHandle::Result LabelTextHandle::Drag (const TrackPanelMouseEvent &evt, AudacityProject *pProject) { @@ -124,7 +253,7 @@ UIHandle::Result LabelTextHandle::Drag const wxMouseEvent &event = evt.event; auto pLT = TrackList::Get( *pProject ).Lock(mpLT); if(pLT) - pLT->HandleTextDragRelease(event); + HandleTextDragRelease(event); // locate the initial mouse position if (event.LeftIsDown()) { @@ -132,10 +261,11 @@ UIHandle::Result LabelTextHandle::Drag mLabelTrackStartXPos = event.m_x; mLabelTrackStartYPos = event.m_y; + auto pView = pLT ? &LabelTrackView::Get( *pLT ) : nullptr; if (pLT && - (pLT->getSelectedIndex() != -1) && - pLT->OverTextBox( - pLT->GetLabel(pLT->getSelectedIndex()), + (pView->GetSelectedIndex() != -1) && + LabelTrackView::OverTextBox( + pLT->GetLabel(pView->GetSelectedIndex()), mLabelTrackStartXPos, mLabelTrackStartYPos)) mLabelTrackStartYPos = -1; @@ -172,7 +302,7 @@ UIHandle::Result LabelTextHandle::Release const wxMouseEvent &event = evt.event; auto pLT = TrackList::Get( *pProject ).Lock(mpLT); if (pLT) - pLT->HandleTextDragRelease(event); + HandleTextDragRelease(event); // handle mouse left button up if (event.LeftUp()) diff --git a/src/tracks/labeltrack/ui/LabelTextHandle.h b/src/tracks/labeltrack/ui/LabelTextHandle.h index db568ce0c..9477ced37 100644 --- a/src/tracks/labeltrack/ui/LabelTextHandle.h +++ b/src/tracks/labeltrack/ui/LabelTextHandle.h @@ -17,6 +17,7 @@ Paul Licameli split from TrackPanel.cpp class wxMouseState; class LabelTrack; class SelectionStateChanger; +class ZoomInfo; class LabelTextHandle final : public LabelDefaultClickHandle { @@ -55,12 +56,20 @@ public: Result Cancel(AudacityProject *pProject) override; private: + void HandleTextClick + (const wxMouseEvent & evt, const wxRect & r, const ZoomInfo &zoomInfo, + SelectedRegion *newSel); + void HandleTextDragRelease(const wxMouseEvent & evt); + std::weak_ptr mpLT {}; int mLabelNum{ -1 }; int mLabelTrackStartXPos { -1 }; int mLabelTrackStartYPos { -1 }; SelectedRegion mSelectedRegion{}; std::shared_ptr mChanger; + + /// flag to tell if it's a valid dragging + bool mRightDragging{ false }; }; #endif diff --git a/src/tracks/labeltrack/ui/LabelTrackControls.cpp b/src/tracks/labeltrack/ui/LabelTrackControls.cpp index f17410d81..0054dbdf9 100644 --- a/src/tracks/labeltrack/ui/LabelTrackControls.cpp +++ b/src/tracks/labeltrack/ui/LabelTrackControls.cpp @@ -11,6 +11,7 @@ Paul Licameli split from TrackPanel.cpp #include "../../../Audacity.h" #include "LabelTrackControls.h" +#include "LabelTrackView.h" #include "../../../HitTestResult.h" #include "../../../LabelTrack.h" #include "../../../widgets/PopupMenuTable.h" @@ -103,10 +104,10 @@ void LabelTrackMenuTable::OnSetFont(wxCommandEvent &) // Correct for empty facename, or bad preference file: // get the name of a really existing font, to highlight by default // in the list box - facename = LabelTrack::GetFont(facename).GetFaceName(); + facename = LabelTrackView::GetFont(facename).GetFaceName(); long fontsize = gPrefs->Read(wxT("/GUI/LabelFontSize"), - LabelTrack::DefaultFontSize); + LabelTrackView::DefaultFontSize); /* i18n-hint: (noun) This is the font for the label track.*/ wxDialogWrapper dlg(mpData->pParent, wxID_ANY, wxString(_("Label Track Font"))); @@ -160,7 +161,7 @@ void LabelTrackMenuTable::OnSetFont(wxCommandEvent &) gPrefs->Write(wxT("/GUI/LabelFontSize"), sc->GetValue()); gPrefs->Flush(); - LabelTrack::ResetFont(); + LabelTrackView::ResetFont(); mpData->result = RefreshCode::RefreshAll; } diff --git a/src/tracks/labeltrack/ui/LabelTrackView.cpp b/src/tracks/labeltrack/ui/LabelTrackView.cpp index 2f97e9a94..4a217bfec 100644 --- a/src/tracks/labeltrack/ui/LabelTrackView.cpp +++ b/src/tracks/labeltrack/ui/LabelTrackView.cpp @@ -8,8 +8,10 @@ Paul Licameli split from TrackPanel.cpp **********************************************************************/ +#include "../../../Audacity.h" #include "LabelTrackView.h" -#include "../../../LabelTrack.h" + +#include "Experimental.h" #include "LabelTrackControls.h" #include "LabelTrackVRulerControls.h" @@ -17,8 +19,31 @@ Paul Licameli split from TrackPanel.cpp #include "LabelTextHandle.h" #include "LabelTrackVRulerControls.h" +#include "../../../LabelTrack.h" + +#include "../../../AColor.h" +#include "../../../AllThemeResources.h" +#include "../../../Clipboard.h" #include "../../../HitTestResult.h" +#include "../../../Project.h" +#include "../../../ProjectHistory.h" +#include "../../../ProjectSettings.h" +#include "../../../ProjectWindow.h" +#include "../../../RefreshCode.h" +#include "../../../Theme.h" +#include "../../../TrackArtist.h" +#include "../../../TrackPanel.h" #include "../../../TrackPanelMouseEvent.h" +#include "../../../UndoManager.h" +#include "../../../ViewInfo.h" +#include "../../../widgets/ErrorDialog.h" + +#include +#include +#include +#include +#include +#include LabelTrackView::LabelTrackView( const std::shared_ptr &pTrack ) : CommonTrackView{ pTrack } @@ -27,12 +52,61 @@ LabelTrackView::LabelTrackView( const std::shared_ptr &pTrack ) // Default is to allow two rows so that NEW users get the // idea that labels can 'stack' when they would overlap. DoSetHeight(73); + + ResetFont(); + CreateCustomGlyphs(); + ResetFlags(); + + // Events will be emitted by the track + const auto pLabelTrack = FindLabelTrack(); + BindTo( pLabelTrack.get() ); } LabelTrackView::~LabelTrackView() { } +void LabelTrackView::Reparent( const std::shared_ptr &parent ) +{ + auto oldParent = FindLabelTrack(); + auto newParent = track_cast(parent.get()); + if (oldParent.get() != newParent) { + UnbindFrom( oldParent.get() ); + BindTo( newParent ); + } + CommonTrackView::Reparent( parent ); +} + +void LabelTrackView::BindTo( LabelTrack *pParent ) +{ + pParent->Bind( + EVT_LABELTRACK_ADDITION, &LabelTrackView::OnLabelAdded, this ); + pParent->Bind( + EVT_LABELTRACK_DELETION, &LabelTrackView::OnLabelDeleted, this ); + pParent->Bind( + EVT_LABELTRACK_PERMUTED, &LabelTrackView::OnLabelPermuted, this ); +} + +void LabelTrackView::UnbindFrom( LabelTrack *pParent ) +{ + pParent->Unbind( + EVT_LABELTRACK_ADDITION, &LabelTrackView::OnLabelAdded, this ); + pParent->Unbind( + EVT_LABELTRACK_DELETION, &LabelTrackView::OnLabelDeleted, this ); + pParent->Unbind( + EVT_LABELTRACK_PERMUTED, &LabelTrackView::OnLabelPermuted, this ); +} + +void LabelTrackView::Copy( const TrackView &other ) +{ + TrackView::Copy( other ); + + if ( const auto pOther = dynamic_cast< const LabelTrackView* >( &other ) ) { + // only one field is important to preserve in undo/redo history + mSelIndex = pOther->mSelIndex; + } +} + LabelTrackView &LabelTrackView::Get( LabelTrack &track ) { return static_cast< LabelTrackView& >( TrackView::Get( track ) ); @@ -75,6 +149,1937 @@ std::vector LabelTrackView::DetailedHitTest return results; } +// static member variables. +bool LabelTrackView::mbGlyphsReady=false; + +wxFont LabelTrackView::msFont; + +/// We have several variants of the icons (highlighting). +/// The icons are draggable, and you can drag one boundary +/// or all boundaries at the same timecode depending on whether you +/// click the centre (for all) or the arrow part (for one). +/// Currently we have twelve variants but we're only using six. +wxBitmap LabelTrackView::mBoundaryGlyphs[ NUM_GLYPH_CONFIGS * NUM_GLYPH_HIGHLIGHTS ]; +int LabelTrackView::mIconHeight; +int LabelTrackView::mIconWidth; +int LabelTrackView::mTextHeight; + +int LabelTrackView::mFontHeight=-1; + +void LabelTrackView::ResetFlags() +{ + mInitialCursorPos = 1; + mCurrentCursorPos = 1; + mDrawCursor = false; +} + +void LabelTrackView::RestoreFlags( const Flags& flags ) +{ + mInitialCursorPos = flags.mInitialCursorPos; + mCurrentCursorPos = flags.mCurrentCursorPos; + mSelIndex = flags.mSelIndex; + mDrawCursor = flags.mDrawCursor; +} + +wxFont LabelTrackView::GetFont(const wxString &faceName, int size) +{ + wxFontEncoding encoding; + if (faceName.empty()) + encoding = wxFONTENCODING_DEFAULT; + else + encoding = wxFONTENCODING_SYSTEM; + return wxFont(size, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, + wxFONTWEIGHT_NORMAL, false, faceName, encoding); +} + +void LabelTrackView::ResetFont() +{ + mFontHeight = -1; + wxString facename = gPrefs->Read(wxT("/GUI/LabelFontFacename"), wxT("")); + int size = gPrefs->Read(wxT("/GUI/LabelFontSize"), DefaultFontSize); + msFont = GetFont(facename, size); +} + +/// ComputeTextPosition is 'smart' about where to display +/// the label text. +/// +/// The text must be displayed between its endpoints x and x1 +/// We'd also like it centered between them, and we'd like it on +/// screen. It isn't always possible to achieve all of this, +/// so we do the best we can. +/// +/// This function has a number of tests and adjustments to the +/// text start position. The tests later in the function will +/// take priority over the ones earlier, so because centering +/// is the first thing we do, it's the first thing we lose if +/// we can't do everything we want to. +void LabelTrackView::ComputeTextPosition(const wxRect & r, int index) const +{ + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + + const auto &labelStruct = mLabels[index]; + + // xExtra is extra space + // between the text and the endpoints. + const int xExtra=mIconWidth; + int x = labelStruct.x; // left endpoint + int x1 = labelStruct.x1; // right endpoint. + int width = labelStruct.width; + + int xText; // This is where the text will end up. + + // Will the text all fit at this zoom? + bool bTooWideForScreen = width > (r.width-2*xExtra); +// bool bSimpleCentering = !bTooWideForScreen; + bool bSimpleCentering = false; + + //TODO (possibly): + // Add configurable options as to when to use simple + // and when complex centering. + // + // Simple centering does its best to keep the text + // centered between the label limits. + // + // Complex centering positions the text proportionally + // to how far we are through the label. + // + // If we add preferences for this, we want to be able to + // choose separately whether: + // a) Wide text labels centered simple/complex. + // b) Other text labels centered simple/complex. + // + + if( bSimpleCentering ) + { + // Center text between the two end points. + xText = (x+x1-width)/2; + } + else + { + // Calculate xText position to make text line + // scroll sideways evenly as r moves right. + + // xText is a linear function of r.x. + // These variables are used to compute that function. + int rx0,rx1,xText0,xText1; + + // Since we will be using a linear function, + // we should blend smoothly between left and right + // aligned text as r, the 'viewport' moves. + if( bTooWideForScreen ) + { + rx0=x; // when viewport at label start. + xText0=x+xExtra; // text aligned left. + rx1=x1-r.width; // when viewport end at label end + xText1=x1-(width+xExtra); // text aligned right. + } + else + { + // when label start + width + extra spacing at viewport end.. + rx0=x-r.width+width+2*xExtra; + // ..text aligned left. + xText0=x+xExtra; + // when viewport start + width + extra spacing at label end.. + rx1=x1-(width+2*xExtra); + // ..text aligned right. + xText1=x1-(width+xExtra); + } + + if( rx1 > rx0 ) // Avoid divide by zero case. + { + // Compute the blend between left and right aligned. + + // Don't use: + // + // xText = xText0 + ((xText1-xText0)*(r.x-rx0))/(rx1-rx0); + // + // The problem with the above is that it integer-oveflows at + // high zoom. + + // Instead use: + xText = xText0 + (int)((xText1-xText0)*(((float)(r.x-rx0))/(rx1-rx0))); + } + else + { + // Avoid divide by zero by reverting to + // simple centering. + // + // We could also fall into this case if x and x1 + // are swapped, in which case we'll end up + // left aligned anyway because code later on + // will catch that. + xText = (x+x1-width)/2; + } + } + + // Is the text now appearing partly outside r? + bool bOffLeft = xText < r.x+xExtra; + bool bOffRight = xText > r.x+r.width-width-xExtra; + + // IF any part of the text is offscreen + // THEN we may bring it back. + if( bOffLeft == bOffRight ) + { + //IF both sides on screen, THEN nothing to do. + //IF both sides off screen THEN don't do + //anything about it. + //(because if we did, you'd never get to read + //all the text by scrolling). + } + else if( bOffLeft != bTooWideForScreen) + { + // IF we're off on the left, OR we're + // too wide for the screen and off on the right + // (only) THEN align left. + xText = r.x+xExtra; + } + else + { + // We're off on the right, OR we're + // too wide and off on the left (only) + // SO align right. + xText =r.x+r.width-width-xExtra; + } + + // But if we've taken the text out from its endpoints + // we must move it back so that it's between the endpoints. + + // We test the left end point last because the + // text might not even fit between the endpoints (at this + // zoom factor), and in that case we'd like to position + // the text at the left end point. + if( xText > (x1-width-xExtra)) + xText=(x1-width-xExtra); + if( xText < x+xExtra ) + xText=x+xExtra; + + labelStruct.xText = xText; +} + +/// ComputeLayout determines which row each label +/// should be placed on, and reserves space for it. +/// Function assumes that the labels are sorted. +void LabelTrackView::ComputeLayout(const wxRect & r, const ZoomInfo &zoomInfo) const +{ + int xUsed[MAX_NUM_ROWS]; + + int iRow; + // Rows are the 'same' height as icons or as the text, + // whichever is taller. + const int yRowHeight = wxMax(mTextHeight,mIconHeight)+3;// pixels. + // Extra space at end of rows. + // We allow space for one half icon at the start and two + // half icon widths for extra x for the text frame. + // [we don't allow half a width space for the end icon since it is + // allowed to be obscured by the text]. + const int xExtra= (3 * mIconWidth)/2; + + bool bAvoidName = false; + const int nRows = wxMin((r.height / yRowHeight) + 1, MAX_NUM_ROWS); + if( nRows > 2 ) + bAvoidName = gPrefs->ReadBool(wxT("/GUI/ShowTrackNameInWaveform"), false); + // Initially none of the rows have been used. + // So set a value that is less than any valid value. + { + // Bug 502: With dragging left of zeros, labels can be in + // negative space. So set least possible value as starting point. + const int xStart = INT_MIN; + for (auto &x : xUsed) + x = xStart; + } + int nRowsUsed=0; + + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + + { int i = -1; for (const auto &labelStruct : mLabels) { ++i; + const int x = zoomInfo.TimeToPosition(labelStruct.getT0(), r.x); + const int x1 = zoomInfo.TimeToPosition(labelStruct.getT1(), r.x); + int y = r.y; + + labelStruct.x=x; + labelStruct.x1=x1; + labelStruct.y=-1;// -ve indicates nothing doing. + iRow=0; + // Our first preference is a row that ends where we start. + // (This is to encourage merging of adjacent label boundaries). + while( (iRow= nRowsUsed ) + { + iRow=0; + while( (iRow x )) + iRow++; + } + // IF we found such a row THEN record a valid position. + if( iRow= nRowsUsed ) + nRowsUsed=iRow+1; + // Record the position for this label + y= r.y + iRow * yRowHeight +(yRowHeight/2)+1; + labelStruct.y=y; + // On this row we have used up to max of end marker and width. + // Plus also allow space to show the start icon and + // some space for the text frame. + xUsed[iRow]=x+labelStruct.width+xExtra; + if( xUsed[iRow] < x1 ) xUsed[iRow]=x1; + ComputeTextPosition( r, i ); + } + }} +} + +/// Draw vertical lines that go exactly through the position +/// of the start or end of a label. +/// @param dc the device context +/// @param r the LabelTrack rectangle. +void LabelTrackView::DrawLines( + wxDC & dc, const LabelStruct &ls, const wxRect & r) +{ + auto &x = ls.x; + auto &x1 = ls.x1; + auto &y = ls.y; + + // How far out from the centre line should the vertical lines + // start, i.e. what is the y position of the icon? + // We adjust this so that the line encroaches on the icon + // slightly (there is white space in the design). + const int yIconStart = y - (mIconHeight /2)+1+(mTextHeight+3)/2; + const int yIconEnd = yIconStart + mIconHeight-2; + + // If y is positive then it is the center line for the + // Label. + if( y >= 0 ) + { + if((x >= r.x) && (x <= (r.x+r.width))) + { + // Draw line above and below left dragging widget. + AColor::Line(dc, x, r.y, x, yIconStart - 1); + AColor::Line(dc, x, yIconEnd, x, r.y + r.height); + } + if((x1 >= r.x) && (x1 <= (r.x+r.width))) + { + // Draw line above and below right dragging widget. + AColor::Line(dc, x1, r.y, x1, yIconStart - 1); + AColor::Line(dc, x1, yIconEnd, x1, r.y + r.height); + } + } + else + { + // Draw the line, even though the widget is off screen + AColor::Line(dc, x, r.y, x, r.y + r.height); + AColor::Line(dc, x1, r.y, x1, r.y + r.height); + } +} + +/// DrawGlyphs draws the wxIcons at the start and end of a label. +/// @param dc the device context +/// @param r the LabelTrack rectangle. +void LabelTrackView::DrawGlyphs( + wxDC & dc, const LabelStruct &ls, const wxRect & r, + int GlyphLeft, int GlyphRight) +{ + auto &y = ls.y; + + const int xHalfWidth=mIconWidth/2; + const int yStart=y-mIconHeight/2+(mTextHeight+3)/2; + + // If y == -1, nothing to draw + if( y == -1 ) + return; + + auto &x = ls.x; + auto &x1 = ls.x1; + + if((x >= r.x) && (x <= (r.x+r.width))) + dc.DrawBitmap(GetGlyph(GlyphLeft), x-xHalfWidth,yStart, true); + // The extra test commented out here would suppress right hand markers + // when they overlap the left hand marker (e.g. zoomed out) or to the left. + if((x1 >= r.x) && (x1 <= (r.x+r.width)) /*&& (x1>x+mIconWidth)*/) + dc.DrawBitmap(GetGlyph(GlyphRight), x1-xHalfWidth,yStart, true); +} + +/// Draw the text of the label and also draw +/// a long thin rectangle for its full extent +/// from x to x1 and a rectangular frame +/// behind the text itself. +/// @param dc the device context +/// @param r the LabelTrack rectangle. +void LabelTrackView::DrawText(wxDC & dc, const LabelStruct &ls, const wxRect & r) +{ + //If y is positive then it is the center line for the + //text we are about to draw. + //if it isn't, nothing to draw. + + auto &y = ls.y; + if( y == -1 ) + return; + + // Draw frame for the text... + // We draw it half an icon width left of the text itself. + { + auto &xText = ls.xText; + const int xStart=wxMax(r.x,xText-mIconWidth/2); + const int xEnd=wxMin(r.x+r.width,xText+ls.width+mIconWidth/2); + const int xWidth = xEnd-xStart; + + if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0)) + { + // Now draw the text itself. + dc.DrawText(ls.title, xText, y-mTextHeight/2); + } + } + +} + +void LabelTrackView::DrawTextBox( + wxDC & dc, const LabelStruct &ls, const wxRect & r) +{ + //If y is positive then it is the center line for the + //text we are about to draw. + const int yBarHeight=3; + const int yFrameHeight = mTextHeight+3; + const int xBarShorten = mIconWidth+4; + auto &y = ls.y; + if( y == -1 ) + return; + + { + auto &x = ls.x; + auto &x1 = ls.x1; + const int xStart=wxMax(r.x,x+xBarShorten/2); + const int xEnd=wxMin(r.x+r.width,x1-xBarShorten/2); + const int xWidth = xEnd-xStart; + + if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0)) + { + + wxRect bar( xStart,y-yBarHeight/2+yFrameHeight/2, + xWidth,yBarHeight); + if( x1 > x+xBarShorten ) + dc.DrawRectangle(bar); + } + } + + // In drawing the bar and the frame, we compute the clipping + // to the viewport ourselves. Under Win98 the GDI does its + // calculations in 16 bit arithmetic, and so gets it completely + // wrong at higher zooms where the bar can easily be + // more than 65536 pixels wide. + + // Draw bar for label extent... + // We don't quite draw from x to x1 because we allow + // half an icon width at each end. + { + auto &xText = ls.xText; + const int xStart=wxMax(r.x,xText-mIconWidth/2); + const int xEnd=wxMin(r.x+r.width,xText+ls.width+mIconWidth/2); + const int xWidth = xEnd-xStart; + + if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0)) + { + wxRect frame( + xStart,y-yFrameHeight/2, + xWidth,yFrameHeight ); + dc.DrawRectangle(frame); + } + } +} + +/// Draws text-selected region within the label +void LabelTrackView::DrawHighlight( wxDC & dc, const LabelStruct &ls, + int xPos1, int xPos2, int charHeight) +{ + wxPen curPen = dc.GetPen(); + curPen.SetColour(wxString(wxT("BLUE"))); + wxBrush curBrush = dc.GetBrush(); + curBrush.SetColour(wxString(wxT("BLUE"))); + auto &y = ls.y; + if (xPos1 < xPos2) + dc.DrawRectangle(xPos1-1, y-charHeight/2, xPos2-xPos1+1, charHeight); + else + dc.DrawRectangle(xPos2-1, y-charHeight/2, xPos1-xPos2+1, charHeight); +} + +namespace { +void getXPos( const LabelStruct &ls, wxDC & dc, int * xPos1, int cursorPos) +{ + *xPos1 = ls.xText; + if( cursorPos > 0) + { + int partWidth; + // Calculate the width of the substring and add it to Xpos + dc.GetTextExtent(ls.title.Left(cursorPos), &partWidth, NULL); + *xPos1 += partWidth; + } +} +} + +bool LabelTrackView::CalcCursorX(int * x) const +{ + if ( HasSelection() ) { + wxMemoryDC dc; + + if (msFont.Ok()) { + dc.SetFont(msFont); + } + + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + + getXPos(mLabels[mSelIndex], dc, x, mCurrentCursorPos); + *x += mIconWidth / 2; + return true; + } + + return false; +} + +void LabelTrackView::CalcHighlightXs(int *x1, int *x2) const +{ + wxMemoryDC dc; + + if (msFont.Ok()) { + dc.SetFont(msFont); + } + + int pos1 = mInitialCursorPos, pos2 = mCurrentCursorPos; + if (pos1 > pos2) + std::swap(pos1, pos2); + + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + const auto &labelStruct = mLabels[mSelIndex]; + + // find the left X pos of highlighted area + getXPos(labelStruct, dc, x1, pos1); + // find the right X pos of highlighted area + getXPos(labelStruct, dc, x2, pos2); +} + +#include "tracks/labeltrack/ui/LabelGlyphHandle.h" +// TODO: don't rely on the global ::GetActiveProject() to find this. +// Rather, give TrackPanelCell a drawing function and pass context into it. +namespace { + LabelTrackHit *findHit() + { + // Fetch the highlighting state + auto target = TrackPanel::Get( *GetActiveProject() ).Target(); + if (target) { + auto handle = dynamic_cast( target.get() ); + if (handle) + return &*handle->mpHit; + } + return nullptr; + } +} + +#include "TrackPanelDrawingContext.h" +#include "tracks/labeltrack/ui/LabelTextHandle.h" + +/// Draw calls other functions to draw the LabelTrack. +/// @param dc the device context +/// @param r the LabelTrack rectangle. +void LabelTrackView::Draw +( TrackPanelDrawingContext &context, const wxRect & r ) const +{ + auto &dc = context.dc; + const auto artist = TrackArtist::Get( context ); + const auto &zoomInfo = *artist->pZoomInfo; + + auto pHit = findHit(); + + if(msFont.Ok()) + dc.SetFont(msFont); + + if (mFontHeight == -1) + calculateFontHeight(dc); + + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + + TrackArt::DrawBackgroundWithSelection( context, r, pTrack.get(), + AColor::labelSelectedBrush, AColor::labelUnselectedBrush, + ( pTrack->GetSelected() || pTrack->IsSyncLockSelected() ) ); + + wxCoord textWidth, textHeight; + + // Get the text widths. + // TODO: Make more efficient by only re-computing when a + // text label title changes. + for (const auto &labelStruct : mLabels) { + dc.GetTextExtent(labelStruct.title, &textWidth, &textHeight); + labelStruct.width = textWidth; + } + + // TODO: And this only needs to be done once, but we + // do need the dc to do it. + // We need to set mTextHeight to something sensible, + // guarding against the case where there are no + // labels or all are empty strings, which for example + // happens with a NEW label track. + dc.GetTextExtent(wxT("Demo Text x^y"), &textWidth, &textHeight); + mTextHeight = (int)textHeight; + ComputeLayout( r, zoomInfo ); + dc.SetTextForeground(theTheme.Colour( clrLabelTrackText)); + dc.SetBackgroundMode(wxTRANSPARENT); + dc.SetBrush(AColor::labelTextNormalBrush); + dc.SetPen(AColor::labelSurroundPen); + int GlyphLeft; + int GlyphRight; + // Now we draw the various items in this order, + // so that the correct things overpaint each other. + + // Draw vertical lines that show where the end positions are. + for (const auto &labelStruct : mLabels) + DrawLines( dc, labelStruct, r ); + + // Draw the end glyphs. + { int i = -1; for (const auto &labelStruct : mLabels) { ++i; + GlyphLeft=0; + GlyphRight=1; + if( pHit && i == pHit->mMouseOverLabelLeft ) + GlyphLeft = (pHit->mEdge & 4) ? 6:9; + if( pHit && i == pHit->mMouseOverLabelRight ) + GlyphRight = (pHit->mEdge & 4) ? 7:4; + DrawGlyphs( dc, labelStruct, r, GlyphLeft, GlyphRight ); + }} + + // Draw the label boxes. + { +#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING + bool highlightTrack = false; + auto target = dynamic_cast(context.target.get()); + highlightTrack = target && target->GetTrack().get() == this; +#endif + int i = -1; for (const auto &labelStruct : mLabels) { ++i; + bool highlight = false; +#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING + highlight = highlightTrack && target->GetLabelNum() == i; +#endif + bool selected = GetSelectedIndex() == i; + + if( selected ) + dc.SetBrush( AColor::labelTextEditBrush ); + else if ( highlight ) + dc.SetBrush( AColor::uglyBrush ); + DrawTextBox( dc, labelStruct, r ); + + if (highlight || selected) + dc.SetBrush(AColor::labelTextNormalBrush); + } + } + + // Draw highlights + if ( (mInitialCursorPos != mCurrentCursorPos) && HasSelection() ) + { + int xpos1, xpos2; + CalcHighlightXs(&xpos1, &xpos2); + DrawHighlight(dc, mLabels[mSelIndex], + xpos1, xpos2, mFontHeight); + } + + // Draw the text and the label boxes. + { int i = -1; for (const auto &labelStruct : mLabels) { ++i; + if( GetSelectedIndex() == i ) + dc.SetBrush(AColor::labelTextEditBrush); + DrawText( dc, labelStruct, r ); + if( GetSelectedIndex() == i ) + dc.SetBrush(AColor::labelTextNormalBrush); + }} + + // Draw the cursor, if there is one. + if( mDrawCursor && HasSelection() ) + { + const auto &labelStruct = mLabels[mSelIndex]; + int xPos = labelStruct.xText; + + if( mCurrentCursorPos > 0) + { + // Calculate the width of the substring and add it to Xpos + int partWidth; + dc.GetTextExtent(labelStruct.title.Left(mCurrentCursorPos), &partWidth, NULL); + xPos += partWidth; + } + + wxPen currentPen = dc.GetPen(); + const int CursorWidth=2; + currentPen.SetWidth(CursorWidth); + AColor::Line(dc, + xPos-1, labelStruct.y - mFontHeight/2 + 1, + xPos-1, labelStruct.y + mFontHeight/2 - 1); + currentPen.SetWidth(1); + } +} + +void LabelTrackView::SetSelectedIndex( int index ) +{ + if ( index >= 0 && index < FindLabelTrack()->GetLabels().size() ) + mSelIndex = index; + else + mSelIndex = -1; +} + +/// uses GetTextExtent to find the character position +/// corresponding to the x pixel position. +int LabelTrackView::FindCursorPosition(wxCoord xPos) +{ + int result = -1; + wxMemoryDC dc; + if(msFont.Ok()) + dc.SetFont(msFont); + + // A bool indicator to see if set the cursor position or not + bool finished = false; + int charIndex = 1; + int partWidth; + int oneWidth; + double bound; + wxString subString; + + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + const auto &labelStruct = mLabels[mSelIndex]; + const auto &title = labelStruct.title; + const int length = title.length(); + while (!finished && (charIndex < length + 1)) + { + subString = title.Left(charIndex); + // Get the width of substring + dc.GetTextExtent(subString, &partWidth, NULL); + + // Get the width of the last character + dc.GetTextExtent(subString.Right(1), &oneWidth, NULL); + bound = labelStruct.xText + partWidth - oneWidth * 0.5; + + if (xPos <= bound) + { + // Found + result = charIndex - 1; + finished = true; + } + else + { + // Advance + charIndex++; + } + } + if (!finished) + // Cursor should be in the last position + result = length; + + return result; +} + +void LabelTrackView::SetCurrentCursorPosition(int pos) +{ + mCurrentCursorPos = pos; +} + +void LabelTrackView::SetTextHighlight( + int initialPosition, int currentPosition ) +{ + mInitialCursorPos = initialPosition; + mCurrentCursorPos = currentPosition; + mDrawCursor = true; +} + +void LabelTrackView::calculateFontHeight(wxDC & dc) +{ + int charDescent; + int charLeading; + + // Calculate the width of the substring and add it to Xpos + dc.GetTextExtent(wxT("(Test String)|[yp]"), NULL, &mFontHeight, &charDescent, &charLeading); + + // The cursor will have height charHeight. We don't include the descender as + // part of the height because for phonetic fonts this leads to cursors which are + // too tall. We don't include leading either - it is usually 0. + // To make up for ignoring the descender height, we add one pixel above and below + // using CursorExtraHeight so that the cursor is just a little taller than the + // body of the characters. + const int CursorExtraHeight=2; + mFontHeight += CursorExtraHeight - (charLeading+charDescent); +} + +bool LabelTrackView::IsTextSelected() const +{ + if ( !HasSelection() ) + return false; + if (mCurrentCursorPos == mInitialCursorPos) + return false; + return true; +} + +/// Cut the selected text in the text box +/// @return true if text is selected in text box, false otherwise +bool LabelTrackView::CutSelectedText() +{ + if (!IsTextSelected()) + return false; + + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + + wxString left, right; + auto labelStruct = mLabels[mSelIndex]; + auto &text = labelStruct.title; + + int init = mInitialCursorPos; + int cur = mCurrentCursorPos; + if (init > cur) + std::swap(init, cur); + + // data for cutting + wxString data = text.Mid(init, cur - init); + + // get left-remaining text + if (init > 0) + left = text.Left(init); + + // get right-remaining text + if (cur < (int)text.length()) + right = text.Mid(cur); + + // set title to the combination of the two remainders + text = left + right; + + pTrack->SetLabel( mSelIndex, labelStruct ); + + // copy data onto clipboard + if (wxTheClipboard->Open()) { + // Clipboard owns the data you give it + wxTheClipboard->SetData(safenew wxTextDataObject(data)); + wxTheClipboard->Close(); + } + + // set cursor positions + mInitialCursorPos = mCurrentCursorPos = left.length(); + return true; +} + +/// Copy the selected text in the text box +/// @return true if text is selected in text box, false otherwise +bool LabelTrackView::CopySelectedText() +{ + if ( !HasSelection() ) + return false; + + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + + const auto &labelStruct = mLabels[mSelIndex]; + + int init = mInitialCursorPos; + int cur = mCurrentCursorPos; + if (init > cur) + std::swap(init, cur); + + if (init == cur) + return false; + + // data for copying + wxString data = labelStruct.title.Mid(init, cur-init); + + // copy the data on clipboard + if (wxTheClipboard->Open()) { + // Clipboard owns the data you give it + wxTheClipboard->SetData(safenew wxTextDataObject(data)); + wxTheClipboard->Close(); + } + + return true; +} + +// PRL: should this set other fields of the label selection? +/// Paste the text on the clipboard to text box +/// @return true if mouse is clicked in text box, false otherwise +bool LabelTrackView::PasteSelectedText(double sel0, double sel1) +{ + const auto pTrack = FindLabelTrack(); + + if ( !HasSelection() ) + AddLabel(SelectedRegion(sel0, sel1)); + + wxString text, left, right; + + // if text data is available + if (IsTextClipSupported()) { + if (wxTheClipboard->Open()) { + wxTextDataObject data; + wxTheClipboard->GetData(data); + wxTheClipboard->Close(); + text = data.GetText(); + } + + // Convert control characters to blanks + for (int i = 0; i < (int)text.length(); i++) { + if (wxIscntrl(text[i])) { + text[i] = wxT(' '); + } + } + } + + const auto &mLabels = pTrack->GetLabels(); + auto labelStruct = mLabels[mSelIndex]; + auto &title = labelStruct.title; + int cur = mCurrentCursorPos, init = mInitialCursorPos; + if (init > cur) + std::swap(init, cur); + left = title.Left(init); + if (cur < (int)title.length()) + right = title.Mid(cur); + + title = left + text + right; + + pTrack->SetLabel( mSelIndex, labelStruct ); + + mInitialCursorPos = mCurrentCursorPos = left.length() + text.length(); + return true; +} + + +/// @return true if the text data is available in the clipboard, false otherwise +bool LabelTrackView::IsTextClipSupported() +{ + return wxTheClipboard->IsSupported(wxDF_TEXT); +} + + +int LabelTrackView::GetSelectedIndex() const +{ + // may make delayed update of mutable mSelIndex after track selection change + auto track = FindLabelTrack(); + if ( track->GetSelected() ) + return mSelIndex = std::max( -1, + std::min( track->GetLabels().size() - 1, mSelIndex ) ); + else + return mSelIndex = -1; +} + +/// TODO: Investigate what happens with large +/// numbers of labels, might need a binary search +/// rather than a linear one. +void LabelTrackView::OverGlyph( + const LabelTrack &track, LabelTrackHit &hit, int x, int y) +{ + //Determine the NEW selection. + int result=0; + const int d1=10; //distance in pixels, used for have we hit drag handle. + const int d2=5; //distance in pixels, used for have we hit drag handle center. + + //If not over a label, reset it + hit.mMouseOverLabelLeft = -1; + hit.mMouseOverLabelRight = -1; + hit.mEdge = 0; + + const auto pTrack = &track; + const auto &mLabels = pTrack->GetLabels(); + { int i = -1; for (const auto &labelStruct : mLabels) { ++i; + //over left or right selection bound + //Check right bound first, since it is drawn after left bound, + //so give it precedence for matching/highlighting. + if( abs(labelStruct.y - (y - (mTextHeight+3)/2)) < d1 && + abs(labelStruct.x1 - d2 -x) < d1) + { + hit.mMouseOverLabelRight = i; + if(abs(labelStruct.x1 - x) < d2 ) + { + result |= 4; + // If left and right co-incident at this resolution, then we drag both. + // We could be a little less stringent about co-incidence here if we liked. + if( abs(labelStruct.x1-labelStruct.x) < 1.0 ) + { + result |=1; + hit.mMouseOverLabelLeft = i; + } + } + result |= 2; + } + // Use else-if here rather than else to avoid detecting left and right + // of the same label. + else if( abs(labelStruct.y - (y - (mTextHeight+3)/2)) < d1 && + abs(labelStruct.x + d2 - x) < d1 ) + { + hit.mMouseOverLabelLeft = i; + if(abs(labelStruct.x - x) < d2 ) + result |= 4; + result |= 1; + } + + // give text box better priority for selecting + if(OverTextBox(&labelStruct, x, y)) + { + result = 0; + } + + }} + hit.mEdge = result; +} + +int LabelTrackView::OverATextBox( const LabelTrack &track, int xx, int yy ) +{ + const auto pTrack = &track; + const auto &mLabels = pTrack->GetLabels(); + for (int nn = (int)mLabels.size(); nn--;) { + const auto &labelStruct = mLabels[nn]; + if ( OverTextBox( &labelStruct, xx, yy ) ) + return nn; + } + + return -1; +} + +// return true if the mouse is over text box, false otherwise +bool LabelTrackView::OverTextBox(const LabelStruct *pLabel, int x, int y) +{ + if( (pLabel->xText-(mIconWidth/2) < x) && + (xxText+pLabel->width+(mIconWidth/2)) && + (abs(pLabel->y-y)= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) || + (keyCode >= WXK_NUMPAD_EQUAL && keyCode <= WXK_NUMPAD_DIVIDE) || +#if defined(__WXMAC__) + (keyCode > WXK_RAW_CONTROL) || +#endif + (keyCode > WXK_WINDOWS_MENU); +} + +/// This returns true for keys we capture for label editing. +static bool IsGoodLabelEditKey(const wxKeyEvent & evt) +{ + int keyCode = evt.GetKeyCode(); + + // Accept everything outside of WXK_START through WXK_COMMAND, plus the keys + // within that range that are usually printable, plus the ones we use for + // keyboard navigation. + return keyCode < WXK_START || + (keyCode >= WXK_END && keyCode < WXK_UP) || + (keyCode == WXK_RIGHT) || + (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) || + (keyCode >= WXK_NUMPAD_SPACE && keyCode <= WXK_NUMPAD_ENTER) || + (keyCode >= WXK_NUMPAD_HOME && keyCode <= WXK_NUMPAD_END) || + (keyCode >= WXK_NUMPAD_DELETE && keyCode <= WXK_NUMPAD_DIVIDE) || +#if defined(__WXMAC__) + (keyCode > WXK_RAW_CONTROL) || +#endif + (keyCode > WXK_WINDOWS_MENU); +} + +// Check for keys that we will process +bool LabelTrackView::DoCaptureKey(wxKeyEvent & event) +{ + // Check for modifiers and only allow shift + int mods = event.GetModifiers(); + if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) { + return false; + } + + // Always capture the navigation keys, if we have any labels + auto code = event.GetKeyCode(); + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + if ((code == WXK_TAB || code == WXK_NUMPAD_TAB) && + !mLabels.empty()) + return true; + + if ( HasSelection() ) { + if (IsGoodLabelEditKey(event)) { + return true; + } + } + else { + bool typeToCreateLabel; + gPrefs->Read(wxT("/GUI/TypeToCreateLabel"), &typeToCreateLabel, false); + if (IsGoodLabelFirstKey(event) && typeToCreateLabel) { + AudacityProject * pProj = GetActiveProject(); + + +// The commented out code can prevent label creation, causing bug 1551 +// We should only be in DoCaptureKey IF this label track has focus, +// and in that case creating a Label is the expected/intended thing. +#if 0 + // If we're playing, don't capture if the selection is the same as the + // playback region (this helps prevent label track creation from + // stealing unmodified kbd. shortcuts) + auto gAudioIO = AudioIOBase::Get(); + if (pProj->GetAudioIOToken() > 0 && + gAudioIO->IsStreamActive(pProj->GetAudioIOToken())) + { + double t0, t1; + pProj->GetPlayRegion(&t0, &t1); + if (pProj->mViewInfo.selectedRegion.t0() == t0 && + pProj->mViewInfo.selectedRegion.t1() == t1) { + return false; + } + } +#endif + + // If there's a label there already don't capture + auto &selectedRegion = ViewInfo::Get( *pProj ).selectedRegion; + if( GetLabelIndex(selectedRegion.t0(), + selectedRegion.t1()) != wxNOT_FOUND ) { + return false; + } + + return true; + } + } + + return false; +} + +unsigned LabelTrackView::CaptureKey(wxKeyEvent & event, ViewInfo &, wxWindow *) +{ + event.Skip(!DoCaptureKey(event)); + return RefreshCode::RefreshNone; +} + +unsigned LabelTrackView::KeyDown( + wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *WXUNUSED(pParent)) +{ + double bkpSel0 = viewInfo.selectedRegion.t0(), + bkpSel1 = viewInfo.selectedRegion.t1(); + + AudacityProject *const pProj = GetActiveProject(); + + // Pass keystroke to labeltrack's handler and add to history if any + // updates were done + if (DoKeyDown(viewInfo.selectedRegion, event)) { + ProjectHistory::Get( *pProj ).PushState(_("Modified Label"), + _("Label Edit"), + UndoPush::CONSOLIDATE); + } + + // Make sure caret is in view + int x; + if (CalcCursorX(&x)) + TrackPanel::Get( *pProj ).ScrollIntoView(x); + + // If selection modified, refresh + // Otherwise, refresh track display if the keystroke was handled + if (bkpSel0 != viewInfo.selectedRegion.t0() || + bkpSel1 != viewInfo.selectedRegion.t1()) + return RefreshCode::RefreshAll; + else if (!event.GetSkipped()) + return RefreshCode::RefreshCell; + + return RefreshCode::RefreshNone; +} + +unsigned LabelTrackView::Char( + wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *) +{ + double bkpSel0 = viewInfo.selectedRegion.t0(), + bkpSel1 = viewInfo.selectedRegion.t1(); + // Pass keystroke to labeltrack's handler and add to history if any + // updates were done + + AudacityProject *const pProj = GetActiveProject(); + + if (DoChar(viewInfo.selectedRegion, event)) + ProjectHistory::Get( *pProj ).PushState(_("Modified Label"), + _("Label Edit"), + UndoPush::CONSOLIDATE); + + // If selection modified, refresh + // Otherwise, refresh track display if the keystroke was handled + if (bkpSel0 != viewInfo.selectedRegion.t0() || + bkpSel1 != viewInfo.selectedRegion.t1()) + return RefreshCode::RefreshAll; + else if (!event.GetSkipped()) + return RefreshCode::RefreshCell; + + return RefreshCode::RefreshNone; +} + +/// KeyEvent is called for every keypress when over the label track. +bool LabelTrackView::DoKeyDown(SelectedRegion &newSel, wxKeyEvent & event) +{ + // Only track true changes to the label + bool updated = false; + + // Cache the keycode + int keyCode = event.GetKeyCode(); + const int mods = event.GetModifiers(); + + // Check for modifiers and only allow shift + if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) { + event.Skip(); + return updated; + } + + // All editing keys are only active if we're currently editing a label + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + if ( HasSelection() ) { + auto labelStruct = mLabels[mSelIndex]; + auto &title = labelStruct.title; + switch (keyCode) { + + case WXK_BACK: + { + int len = title.length(); + + //IF the label is not blank THEN get rid of a letter or letters according to cursor position + if (len > 0) + { + // IF there are some highlighted letters, THEN DELETE them + if (mInitialCursorPos != mCurrentCursorPos) + RemoveSelectedText(); + else + { + // DELETE one letter + if (mCurrentCursorPos > 0) { + title.erase(mCurrentCursorPos-1, 1); + mCurrentCursorPos--; + } + } + } + else + { + // ELSE no text in text box, so DELETE whole label. + pTrack->DeleteLabel( mSelIndex ); + } + mInitialCursorPos = mCurrentCursorPos; + updated = true; + } + break; + + case WXK_DELETE: + case WXK_NUMPAD_DELETE: + { + int len = title.length(); + + //If the label is not blank get rid of a letter according to cursor position + if (len > 0) + { + // if there are some highlighted letters, DELETE them + if (mInitialCursorPos != mCurrentCursorPos) + RemoveSelectedText(); + else + { + // DELETE one letter + if (mCurrentCursorPos < len) { + title.erase(mCurrentCursorPos, 1); + } + } + } + else + { + // DELETE whole label if no text in text box + pTrack->DeleteLabel( mSelIndex ); + } + mInitialCursorPos = mCurrentCursorPos; + updated = true; + } + break; + + case WXK_HOME: + case WXK_NUMPAD_HOME: + // Move cursor to beginning of label + mCurrentCursorPos = 0; + if (mods == wxMOD_SHIFT) + ; + else + mInitialCursorPos = mCurrentCursorPos; + break; + + case WXK_END: + case WXK_NUMPAD_END: + // Move cursor to end of label + mCurrentCursorPos = (int)title.length(); + if (mods == wxMOD_SHIFT) + ; + else + mInitialCursorPos = mCurrentCursorPos; + break; + + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + // Moving cursor left + if (mCurrentCursorPos > 0) { + mCurrentCursorPos--; + if (mods == wxMOD_SHIFT) + ; + else + mInitialCursorPos = mCurrentCursorPos = + std::min(mInitialCursorPos, mCurrentCursorPos); + } + break; + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + // Moving cursor right + if (mCurrentCursorPos < (int)title.length()) { + mCurrentCursorPos++; + if (mods == wxMOD_SHIFT) + ; + else + mInitialCursorPos = mCurrentCursorPos = + std::max(mInitialCursorPos, mCurrentCursorPos); + } + break; + + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + + case WXK_ESCAPE: + if (mRestoreFocus >= 0) { + auto track = *TrackList::Get( *GetActiveProject() ).Any() + .begin().advance(mRestoreFocus); + if (track) + TrackPanel::Get( *GetActiveProject() ).SetFocusedTrack(track); + mRestoreFocus = -2; + } + mSelIndex = -1; + break; + + case WXK_TAB: + case WXK_NUMPAD_TAB: + if (event.ShiftDown()) { + mSelIndex--; + } else { + mSelIndex++; + } + + mSelIndex = (mSelIndex + (int)mLabels.size()) % (int)mLabels.size(); // wrap round if necessary + { + const auto &newLabel = mLabels[mSelIndex]; + mCurrentCursorPos = newLabel.title.length(); + mInitialCursorPos = mCurrentCursorPos; + //Set the selection region to be equal to the selection bounds of the tabbed-to label. + newSel = newLabel.selectedRegion; + } + break; + + case '\x10': // OSX + case WXK_MENU: + case WXK_WINDOWS_MENU: + ShowContextMenu(); + break; + + default: + if (!IsGoodLabelEditKey(event)) { + event.Skip(); + } + break; + } + } + else + { + switch (keyCode) { + + case WXK_TAB: + case WXK_NUMPAD_TAB: + if (!mLabels.empty()) { + int len = (int) mLabels.size(); + if (event.ShiftDown()) { + mSelIndex = len - 1; + if (newSel.t0() > mLabels[0].getT0()) { + while (mSelIndex >= 0 && + mLabels[mSelIndex].getT0() > newSel.t0()) { + mSelIndex--; + } + } + } else { + mSelIndex = 0; + if (newSel.t0() < mLabels[len - 1].getT0()) { + while (mSelIndex < len && + mLabels[mSelIndex].getT0() < newSel.t0()) { + mSelIndex++; + } + } + } + + if (mSelIndex >= 0 && mSelIndex < len) { + const auto &labelStruct = mLabels[mSelIndex]; + mCurrentCursorPos = labelStruct.title.length(); + mInitialCursorPos = mCurrentCursorPos; + //Set the selection region to be equal to the selection bounds of the tabbed-to label. + newSel = labelStruct.selectedRegion; + } + else { + mSelIndex = -1; + } + } + break; + + default: + if (!IsGoodLabelFirstKey(event)) { + event.Skip(); + } + break; + } + } + + // Make sure the caret is visible + mDrawCursor = true; + + return updated; +} + +/// OnChar is called for incoming characters -- that's any keypress not handled +/// by OnKeyDown. +bool LabelTrackView::DoChar(SelectedRegion &WXUNUSED(newSel), wxKeyEvent & event) +{ + // Check for modifiers and only allow shift. + // + // We still need to check this or we will eat the top level menu accelerators + // on Windows if our capture or key down handlers skipped the event. + const int mods = event.GetModifiers(); + if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) { + event.Skip(); + return false; + } + + // Only track true changes to the label + bool updated = false; + + // Cache the character + wxChar charCode = event.GetUnicodeKey(); + + // Skip if it's not a valid unicode character or a control character + if (charCode == 0 || wxIscntrl(charCode)) { + event.Skip(); + return false; + } + + // If we've reached this point and aren't currently editing, add NEW label + const auto pTrack = FindLabelTrack(); + if ( !HasSelection() ) { + // Don't create a NEW label for a space + if (wxIsspace(charCode)) { + event.Skip(); + return false; + } + bool useDialog; + AudacityProject *p = GetActiveProject(); + gPrefs->Read(wxT("/GUI/DialogForNameNewLabel"), &useDialog, false); + auto &selectedRegion = ViewInfo::Get( *p ).selectedRegion; + if (useDialog) { + wxString title; + if (DialogForLabelName( + *p, selectedRegion, charCode, title) == + wxID_CANCEL) { + return false; + } + pTrack->SetSelected(true); + pTrack->AddLabel(selectedRegion, title); + ProjectHistory::Get( *p ).PushState(_("Added label"), _("Label")); + return false; + } + else { + pTrack->SetSelected(true); + AddLabel( selectedRegion ); + ProjectHistory::Get( *p ).PushState(_("Added label"), _("Label")); + } + } + + // + // Now we are definitely in a label; append the incoming character + // + + const auto &mLabels = pTrack->GetLabels(); + auto labelStruct = mLabels[mSelIndex]; + auto &title = labelStruct.title; + + // Test if cursor is in the end of string or not + if (mInitialCursorPos != mCurrentCursorPos) + RemoveSelectedText(); + + if (mCurrentCursorPos < (int)title.length()) { + // Get substring on the righthand side of cursor + wxString rightPart = title.Mid(mCurrentCursorPos); + // Set title to substring on the lefthand side of cursor + title = title.Left(mCurrentCursorPos); + //append charcode + title += charCode; + //append the right part substring + title += rightPart; + } + else + //append charCode + title += charCode; + + pTrack->SetLabel( mSelIndex, labelStruct ); + + //moving cursor position forward + mInitialCursorPos = ++mCurrentCursorPos; + updated = true; + + // Make sure the caret is visible + mDrawCursor = true; + + return updated; +} + +enum +{ + OnCutSelectedTextID = 1, // OSX doesn't like a 0 menu id + OnCopySelectedTextID, + OnPasteSelectedTextID, + OnDeleteSelectedLabelID, + OnEditSelectedLabelID, +}; + +void LabelTrackView::ShowContextMenu() +{ + wxWindow *parent = wxWindow::FindFocus(); + + // Bug 2044. parent can be nullptr after a context switch. + if( !parent ) + parent = FindProjectFrame( GetActiveProject() ); + + if( parent ) + { + wxMenu menu; + menu.Bind(wxEVT_MENU, &LabelTrackView::OnContextMenu, this); + + menu.Append(OnCutSelectedTextID, _("Cu&t")); + menu.Append(OnCopySelectedTextID, _("&Copy")); + menu.Append(OnPasteSelectedTextID, _("&Paste")); + menu.Append(OnDeleteSelectedLabelID, _("&Delete Label")); + menu.Append(OnEditSelectedLabelID, _("&Edit...")); + + menu.Enable(OnCutSelectedTextID, IsTextSelected()); + menu.Enable(OnCopySelectedTextID, IsTextSelected()); + menu.Enable(OnPasteSelectedTextID, IsTextClipSupported()); + menu.Enable(OnDeleteSelectedLabelID, true); + menu.Enable(OnEditSelectedLabelID, true); + + if( !HasSelection() ) { + wxASSERT( false ); + return; + } + + const auto pTrack = FindLabelTrack(); + const LabelStruct *ls = pTrack->GetLabel(mSelIndex); + + wxClientDC dc(parent); + + if (msFont.Ok()) + { + dc.SetFont(msFont); + } + + int x = 0; + bool success = CalcCursorX(&x); + wxASSERT(success); + static_cast(success); // Suppress unused variable warning if debug mode is disabled + + parent->PopupMenu(&menu, x, ls->y + (mIconHeight / 2) - 1); + } +} + +void LabelTrackView::OnContextMenu(wxCommandEvent & evt) +{ + AudacityProject *p = GetActiveProject(); + auto &selectedRegion = ViewInfo::Get( *p ).selectedRegion; + + switch (evt.GetId()) + { + /// Cut selected text if cut menu item is selected + case OnCutSelectedTextID: + if (CutSelectedText()) + { + ProjectHistory::Get( *p ).PushState(_("Modified Label"), + _("Label Edit"), + UndoPush::CONSOLIDATE); + } + break; + + /// Copy selected text if copy menu item is selected + case OnCopySelectedTextID: + CopySelectedText(); + break; + + /// paste selected text if paste menu item is selected + case OnPasteSelectedTextID: + if (PasteSelectedText(selectedRegion.t0(), selectedRegion.t1())) + { + ProjectHistory::Get( *p ).PushState(_("Modified Label"), + _("Label Edit"), + UndoPush::CONSOLIDATE); + } + break; + + /// DELETE selected label + case OnDeleteSelectedLabelID: { + int ndx = GetLabelIndex(selectedRegion.t0(), selectedRegion.t1()); + if (ndx != -1) + { + const auto pTrack = FindLabelTrack(); + pTrack->DeleteLabel(ndx); + ProjectHistory::Get( *p ).PushState(_("Deleted Label"), + _("Label Edit"), + UndoPush::CONSOLIDATE); + } + } + break; + + case OnEditSelectedLabelID: { + int ndx = GetLabelIndex(selectedRegion.t0(), selectedRegion.t1()); + if (ndx != -1) + DoEditLabels(*p, FindLabelTrack().get(), ndx); + } + break; + } +} + +void LabelTrackView::RemoveSelectedText() +{ + wxString left, right; + + int init = mInitialCursorPos; + int cur = mCurrentCursorPos; + if (init > cur) + std::swap(init, cur); + + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + auto labelStruct = mLabels[mSelIndex]; + auto &title = labelStruct.title; + + if (init > 0) + left = title.Left(init); + + if (cur < (int)title.length()) + right = title.Mid(cur); + + title = left + right; + pTrack->SetLabel( mSelIndex, labelStruct ); + mInitialCursorPos = mCurrentCursorPos = left.length(); +} + +bool LabelTrackView::HasSelection() const +{ + const auto selIndex = GetSelectedIndex(); + return (selIndex >= 0 && + selIndex < (int)FindLabelTrack()->GetLabels().size()); +} + +int LabelTrackView::GetLabelIndex(double t, double t1) +{ + //We'd have liked to have times in terms of samples, + //because then we're doing an intrger comparison. + //Never mind. Instead we look for near enough. + //This level of (in)accuracy is only a problem if we + //deal with sounds in the MHz range. + const double delta = 1.0e-7; + const auto pTrack = FindLabelTrack(); + const auto &mLabels = pTrack->GetLabels(); + { int i = -1; for (const auto &labelStruct : mLabels) { ++i; + if( fabs( labelStruct.getT0() - t ) > delta ) + continue; + if( fabs( labelStruct.getT1() - t1 ) > delta ) + continue; + return i; + }} + + return wxNOT_FOUND; +} + + +// restoreFocus of -1 is the default, and sets the focus to this label. +// restoreFocus of -2 or other value leaves the focus unchanged. +// restoreFocus >= 0 will later cause focus to move to that track. +int LabelTrackView::AddLabel(const SelectedRegion &selectedRegion, + const wxString &title, int restoreFocus) +{ + const auto pTrack = FindLabelTrack(); + mRestoreFocus = restoreFocus; + auto pos = pTrack->AddLabel( selectedRegion, title ); + return pos; +} + +void LabelTrackView::OnLabelAdded( LabelTrackEvent &e ) +{ + e.Skip(); + if ( e.mpTrack.lock() != FindTrack() ) + return; + + const auto &title = e.mTitle; + const auto pos = e.mPresentPosition; + + mInitialCursorPos = mCurrentCursorPos = title.length(); + + // restoreFocus is -2 e.g. from Nyquist label creation, when we should not + // even lose the focus and open the label to edit in the first place. + // -1 means we don't need to restore it to anywhere. + // 0 or above is the track to restore to after editing the label is complete. + if( mRestoreFocus >= -1 ) + mSelIndex = pos; + + if( mRestoreFocus < 0 ) + mRestoreFocus = -2; + + // Make sure the caret is visible + // + // LLL: The cursor will not be drawn when the first label + // is added since mDrawCursor will be false. Presumably, + // if the user adds a label, then a cursor should be drawn + // to indicate that typing is expected. + // + // If the label is added during actions like import, then the + // mDrawCursor flag will be reset once the action is complete. + mDrawCursor = true; +} + +void LabelTrackView::OnLabelDeleted( LabelTrackEvent &e ) +{ + e.Skip(); + if ( e.mpTrack.lock() != FindTrack() ) + return; + + auto index = e.mFormerPosition; + + // IF we've deleted the selected label + // THEN set no label selected. + if( mSelIndex== index ) + { + mSelIndex = -1; + mCurrentCursorPos = 1; + } + // IF we removed a label before the selected label + // THEN the NEW selected label number is one less. + else if( index < mSelIndex ) + { + mSelIndex--; + } +} + +void LabelTrackView::OnLabelPermuted( LabelTrackEvent &e ) +{ + e.Skip(); + if ( e.mpTrack.lock() != FindTrack() ) + return; + + auto former = e.mFormerPosition; + auto present = e.mPresentPosition; + + if ( mSelIndex == former ) + mSelIndex = present; + else if ( former < mSelIndex && mSelIndex <= present ) + -- mSelIndex; + else if ( former > mSelIndex && mSelIndex >= present ) + ++ mSelIndex; +} + +wxBitmap & LabelTrackView::GetGlyph( int i) +{ + return theTheme.Bitmap( i + bmpLabelGlyph0); +} + +// This one XPM spec is used to generate a number of +// different wxIcons. +/* XPM */ +static const char *const GlyphXpmRegionSpec[] = { +/* columns rows colors chars-per-pixel */ +"15 23 7 1", +/* Default colors, with first color transparent */ +". c none", +"2 c black", +"3 c black", +"4 c black", +"5 c #BEBEF0", +"6 c #BEBEF0", +"7 c #BEBEF0", +/* pixels */ +"...............", +"...............", +"...............", +"....333.444....", +"...3553.4774...", +"...3553.4774...", +"..35553.47774..", +"..35522222774..", +".3552666662774.", +".3526666666274.", +"355266666662774", +"355266666662774", +"355266666662774", +".3526666666274.", +".3552666662774.", +"..35522222774..", +"..35553.47774..", +"...3553.4774...", +"...3553.4774...", +"....333.444....", +"...............", +"...............", +"..............." +}; + +/// CreateCustomGlyphs() creates the mBoundaryGlyph array. +/// It's a bit like painting by numbers! +/// +/// Schematically the glyphs we want will 'look like': +/// and +/// for a left boundary to a label, a right boundary and both. +/// we're creating all three glyphs using the one Xpm Spec. +/// +/// When we hover over a glyph we highlight the +/// inside of either the '<', the 'O' or the '>' or none, +/// giving 3 x 4 = 12 combinations. +/// +/// Two of those combinations aren't used, but +/// treating them specially would make other code more +/// complicated. +void LabelTrackView::CreateCustomGlyphs() +{ + int iConfig; + int iHighlight; + int index; + const int nSpecRows = + sizeof( GlyphXpmRegionSpec )/sizeof( GlyphXpmRegionSpec[0]); + const char *XmpBmp[nSpecRows]; + + // The glyphs are declared static wxIcon; so we only need + // to create them once, no matter how many LabelTracks. + if( mbGlyphsReady ) + return; + + // We're about to tweak the basic color spec to get 12 variations. + for( iConfig=0;iConfig(viewInfo.TimeToPosition(region.t0()))) + -40; + position.y += 2; // just below the bottom of the track + position = trackPanel.ClientToScreen(position); + auto &window = GetProjectFrame( project ); + AudacityTextEntryDialog dialog{ &window, + _("Name:"), + _("New label"), + initialValue, + wxOK | wxCANCEL, + position }; + + // keep the dialog within Audacity's window, so that the dialog is always fully visible + wxRect dialogScreenRect = dialog.GetScreenRect(); + wxRect projScreenRect = window.GetScreenRect(); + wxPoint max = projScreenRect.GetBottomRight() + wxPoint{ -dialogScreenRect.width, -dialogScreenRect.height }; + if (dialogScreenRect.x > max.x) { + position.x = max.x; + dialog.Move(position); + } + if (dialogScreenRect.y > max.y) { + position.y = max.y; + dialog.Move(position); + } + + dialog.SetInsertionPointEnd(); // because, by default, initial text is selected + int status = dialog.ShowModal(); + if (status != wxID_CANCEL) { + value = dialog.GetValue(); + value.Trim(true).Trim(false); + } + + return status; +} + std::shared_ptr LabelTrack::DoGetView() { return std::make_shared( SharedPointer() ); diff --git a/src/tracks/labeltrack/ui/LabelTrackView.h b/src/tracks/labeltrack/ui/LabelTrackView.h index 4a2515a81..4a25aabb7 100644 --- a/src/tracks/labeltrack/ui/LabelTrackView.h +++ b/src/tracks/labeltrack/ui/LabelTrackView.h @@ -15,8 +15,23 @@ Paul Licameli split from class LabelTrack class LabelGlyphHandle; class LabelTextHandle; +class LabelDefaultClickHandle; +class LabelStruct; class LabelTrack; +struct LabelTrackEvent; +struct LabelTrackHit; class SelectedRegion; +struct TrackPanelDrawingContext; +class ZoomInfo; + +class wxBitmap; +class wxCommandEvent; +class wxDC; +class wxMouseEvent; + +constexpr int NUM_GLYPH_CONFIGS = 3; +constexpr int NUM_GLYPH_HIGHLIGHTS = 4; +constexpr int MAX_NUM_ROWS =80; class wxKeyEvent; @@ -25,7 +40,11 @@ class LabelTrackView final : public CommonTrackView LabelTrackView( const LabelTrackView& ) = delete; LabelTrackView &operator=( const LabelTrackView& ) = delete; + void Reparent( const std::shared_ptr &parent ) override; + public: + enum : int { DefaultFontSize = 12 }; + explicit LabelTrackView( const std::shared_ptr &pTrack ); ~LabelTrackView() override; @@ -37,7 +56,15 @@ public: bool DoKeyDown(SelectedRegion &sel, wxKeyEvent & event); bool DoChar(SelectedRegion &sel, wxKeyEvent & event); + //This returns the index of the label we just added. + int AddLabel(const SelectedRegion ®ion, + const wxString &title = {}, + int restoreFocus = -1); + private: + void BindTo( LabelTrack *pParent ); + void UnbindFrom( LabelTrack *pParent ); + std::vector DetailedHitTest (const TrackPanelMouseState &state, const AudacityProject *pProject, int currentTool, bool bMultiTool) @@ -54,11 +81,137 @@ private: std::shared_ptr DoGetVRulerControls() override; + // Preserve some view state too for undo/redo purposes + void Copy( const TrackView &other ) override; + +public: + static void DoEditLabels( + AudacityProject &project, LabelTrack *lt = nullptr, int index = -1); + + static int DialogForLabelName( + AudacityProject &project, const SelectedRegion& region, + const wxString& initialValue, wxString& value); + + bool IsTextSelected() const; + +private: + void CreateCustomGlyphs(); + +public: + static wxFont GetFont(const wxString &faceName, int size = DefaultFontSize); + static void ResetFont(); + + void Draw( TrackPanelDrawingContext &context, const wxRect & r ) const; + + int GetSelectedIndex() const; + void SetSelectedIndex( int index ); + + bool CutSelectedText(); + bool CopySelectedText(); + bool PasteSelectedText(double sel0, double sel1); + + static void OverGlyph( + const LabelTrack &track, LabelTrackHit &hit, int x, int y ); + +private: + static wxBitmap & GetGlyph( int i); + +public: + struct Flags { + int mInitialCursorPos, mCurrentCursorPos, mSelIndex; + bool mDrawCursor; + }; + + void ResetFlags(); + Flags SaveFlags() const + { + return { + mInitialCursorPos, mCurrentCursorPos, mSelIndex, + mDrawCursor + }; + } + void RestoreFlags( const Flags& flags ); + + static int OverATextBox( const LabelTrack &track, int xx, int yy ); + + static bool OverTextBox( const LabelStruct *pLabel, int x, int y ); + +private: + static bool IsTextClipSupported(); + +public: + void AddedLabel( const wxString &title, int pos ); + void DeletedLabel( int index ); + +private: + //And this tells us the index, if there is a label already there. + int GetLabelIndex(double t, double t1); + +public: + //get current cursor position, + // relative to the left edge of the track panel + bool CalcCursorX(int * x) const; + +private: + void CalcHighlightXs(int *x1, int *x2) const; + +public: + void ShowContextMenu(); + +private: + void OnContextMenu(wxCommandEvent & evt); + + mutable int mSelIndex{-1}; /// Keeps track of the currently selected label + + static int mIconHeight; + static int mIconWidth; + static int mTextHeight; + + static bool mbGlyphsReady; + static wxBitmap mBoundaryGlyphs[NUM_GLYPH_CONFIGS * NUM_GLYPH_HIGHLIGHTS]; + + static int mFontHeight; + int mCurrentCursorPos; /// current cursor position + int mInitialCursorPos; /// initial cursor position + + bool mDrawCursor; /// flag to tell if drawing the + /// cursor or not + int mRestoreFocus{-2}; /// Restore focus to this track + /// when done editing + + void ComputeTextPosition(const wxRect & r, int index) const; + void ComputeLayout(const wxRect & r, const ZoomInfo &zoomInfo) const; + static void DrawLines( wxDC & dc, const LabelStruct &ls, const wxRect & r); + static void DrawGlyphs( wxDC & dc, const LabelStruct &ls, const wxRect & r, + int GlyphLeft, int GlyphRight); + static void DrawText( wxDC & dc, const LabelStruct &ls, const wxRect & r); + static void DrawTextBox( wxDC & dc, const LabelStruct &ls, const wxRect & r); + static void DrawHighlight( + wxDC & dc, const LabelStruct &ls, int xPos1, int xPos2, int charHeight); + +public: + /// convert pixel coordinate to character position in text box + int FindCursorPosition(wxCoord xPos); + int GetCurrentCursorPosition() const { return mCurrentCursorPos; } + void SetCurrentCursorPosition(int pos); + int GetInitialCursorPosition() const { return mInitialCursorPos; } + void SetTextHighlight( int initialPosition, int currentPosition ); + + static void calculateFontHeight(wxDC & dc); + bool HasSelection() const; + void RemoveSelectedText(); + + void OnLabelAdded( LabelTrackEvent& ); + void OnLabelDeleted( LabelTrackEvent& ); + void OnLabelPermuted( LabelTrackEvent& ); + std::shared_ptr FindLabelTrack(); std::shared_ptr FindLabelTrack() const; std::weak_ptr mGlyphHandle; std::weak_ptr mTextHandle; + + static wxFont msFont; }; #endif diff --git a/src/tracks/ui/CommonTrackPanelCell.h b/src/tracks/ui/CommonTrackPanelCell.h index c0ccfec9a..a571624b5 100644 --- a/src/tracks/ui/CommonTrackPanelCell.h +++ b/src/tracks/ui/CommonTrackPanelCell.h @@ -63,7 +63,7 @@ public: std::shared_ptr DoFindTrack() override; - void Reparent( const std::shared_ptr &parent ); + virtual void Reparent( const std::shared_ptr &parent ); private: std::weak_ptr< Track > mwTrack;