1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-09-17 16:50:26 +02:00
audacity/src/tracks/labeltrack/ui/LabelTrackView.cpp
Paul Licameli a5364119eb Eliminate many calls to RedrawProject & TrackPanel::Refresh()...
... Let the window respond to an undo manager event instead, whenever there
is a push or modify

Maybe this makes a few unnecessary redraws that did not happen before.  If
that is important, then we should figure out how to put the logic for eliding
the redraw into ProjectWindow, and the extra information needed for the
decision into the events, but not make intrusions in other code all over the
place.
2019-07-02 08:17:01 -04:00

2117 lines
64 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
LabelTrackView.cpp
Paul Licameli split from TrackPanel.cpp
**********************************************************************/
#include "../../../Audacity.h"
#include "LabelTrackView.h"
#include "../../../Experimental.h"
#include "LabelTrackControls.h"
#include "LabelTrackVRulerControls.h"
#include "LabelGlyphHandle.h"
#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 <wx/clipbrd.h>
#include <wx/dcclient.h>
#include <wx/dcmemory.h>
#include <wx/font.h>
#include <wx/frame.h>
#include <wx/menu.h>
LabelTrackView::LabelTrackView( const std::shared_ptr<Track> &pTrack )
: CommonTrackView{ pTrack }
{
// Label tracks are narrow
// 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<Track> &parent )
{
auto oldParent = FindLabelTrack();
auto newParent = track_cast<LabelTrack*>(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::CopyTo( Track &track ) const
{
TrackView::CopyTo( track );
auto &other = TrackView::Get( track );
if ( const auto pOther = dynamic_cast< const LabelTrackView* >( &other ) ) {
// only one field is important to preserve in undo/redo history
pOther->mSelIndex = mSelIndex;
}
}
LabelTrackView &LabelTrackView::Get( LabelTrack &track )
{
return static_cast< LabelTrackView& >( TrackView::Get( track ) );
}
const LabelTrackView &LabelTrackView::Get( const LabelTrack &track )
{
return static_cast< const LabelTrackView& >( TrackView::Get( track ) );
}
std::shared_ptr<LabelTrack> LabelTrackView::FindLabelTrack()
{
return std::static_pointer_cast<LabelTrack>( FindTrack() );
}
std::shared_ptr<const LabelTrack> LabelTrackView::FindLabelTrack() const
{
return const_cast<LabelTrackView*>(this)->FindLabelTrack();
}
std::vector<UIHandlePtr> LabelTrackView::DetailedHitTest
(const TrackPanelMouseState &st,
const AudacityProject *WXUNUSED(pProject), int, bool)
{
UIHandlePtr result;
std::vector<UIHandlePtr> results;
const wxMouseState &state = st.state;
const auto pTrack = FindLabelTrack();
result = LabelGlyphHandle::HitTest(
mGlyphHandle, state, pTrack, st.rect);
if (result)
results.push_back(result);
result = LabelTextHandle::HitTest(
mTextHandle, state, pTrack);
if (result)
results.push_back(result);
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) && (xUsed[iRow] != x ))
iRow++;
// IF we didn't find one THEN
// find any row that can take a span starting at x.
if( iRow >= nRowsUsed )
{
iRow=0;
while( (iRow<nRows) && (xUsed[iRow] > x ))
iRow++;
}
// IF we found such a row THEN record a valid position.
if( iRow<nRows )
{
// Logic to ameliorate case where first label is under the
// (on track) track name. For later labels it does not matter
// as we can scroll left or right and/or zoom.
// A possible alternative idea would be to (instead) increase the
// translucency of the track name, when the mouse is inside it.
if( (i==0 ) && (iRow==0) && bAvoidName ){
// reserve some space in first row.
// reserve max of 200px or t1, or text box right edge.
const int x2 = zoomInfo.TimeToPosition(0.0, r.x) + 200;
xUsed[iRow]=x+labelStruct.width+xExtra;
if( xUsed[iRow] < x1 ) xUsed[iRow]=x1;
if( xUsed[iRow] < x2 ) xUsed[iRow]=x2;
iRow=1;
}
// Possibly update the number of rows actually used.
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( AudacityProject &project, int * x) const
{
if ( HasSelection( project ) ) {
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 "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<LabelGlyphHandle*>( target.get() );
if (handle)
return &*handle->mpHit;
}
return nullptr;
}
}
#include "../../../TrackPanelDrawingContext.h"
#include "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 = std::static_pointer_cast< const LabelTrack >(
FindTrack()->SubstitutePendingChangedTrack());
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 );
}}
auto &project = *artist->parent->GetProject();
// Draw the label boxes.
{
#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
bool highlightTrack = false;
auto target = dynamic_cast<LabelTextHandle*>(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( project ) == 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( project ) )
{
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( project ) == i )
dc.SetBrush(AColor::labelTextEditBrush);
DrawText( dc, labelStruct, r );
if( GetSelectedIndex( project ) == i )
dc.SetBrush(AColor::labelTextNormalBrush);
}}
// Draw the cursor, if there is one.
if( mDrawCursor && HasSelection( project ) )
{
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::Draw(
TrackPanelDrawingContext &context,
const wxRect &rect, unsigned iPass )
{
if ( iPass == TrackArtist::PassTracks )
Draw( context, rect );
CommonTrackView::Draw( context, rect, iPass );
}
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( AudacityProject &project ) const
{
if ( !HasSelection( project ) )
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( AudacityProject &project )
{
if (!IsTextSelected( project ))
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( AudacityProject &project )
{
if ( !HasSelection( project ) )
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(
AudacityProject &project, double sel0, double sel1 )
{
const auto pTrack = FindLabelTrack();
if ( !HasSelection( project ) )
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( AudacityProject &project ) const
{
// may make delayed update of mutable mSelIndex after track selection change
auto track = FindLabelTrack();
if ( track->GetSelected() ||
TrackPanel::Get(
// unhappy const_cast because because focus may be set if undefined
const_cast<AudacityProject&>( project )
).GetFocusedTrack() == track.get() )
return mSelIndex = std::max( -1,
std::min<int>( track->GetLabels().size() - 1, mSelIndex ) );
else
return mSelIndex = -1;
}
/// TODO: Investigate what happens with large
/// numbers of labels, might need a binary search
/// rather than a linear one.
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) &&
(x<pLabel->xText+pLabel->width+(mIconWidth/2)) &&
(abs(pLabel->y-y)<mIconHeight/2))
{
return true;
}
return false;
}
/// Returns true for keys we capture to start a label.
static bool IsGoodLabelFirstKey(const wxKeyEvent & evt)
{
int keyCode = evt.GetKeyCode();
return (keyCode < WXK_START
&& keyCode != WXK_SPACE && keyCode != WXK_DELETE && keyCode != WXK_RETURN) ||
(keyCode >= 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(
AudacityProject &project, 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( project ) ) {
if (IsGoodLabelEditKey(event)) {
return true;
}
}
else {
bool typeToCreateLabel;
gPrefs->Read(wxT("/GUI/TypeToCreateLabel"), &typeToCreateLabel, false);
if (IsGoodLabelFirstKey(event) && typeToCreateLabel) {
// 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( project ).selectedRegion;
if( GetLabelIndex(selectedRegion.t0(),
selectedRegion.t1()) != wxNOT_FOUND ) {
return false;
}
return true;
}
}
return false;
}
unsigned LabelTrackView::CaptureKey(
wxKeyEvent & event, ViewInfo &, wxWindow *, AudacityProject *project )
{
event.Skip(!DoCaptureKey( *project, event ));
return RefreshCode::RefreshNone;
}
unsigned LabelTrackView::KeyDown(
wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *WXUNUSED(pParent),
AudacityProject *project)
{
double bkpSel0 = viewInfo.selectedRegion.t0(),
bkpSel1 = viewInfo.selectedRegion.t1();
// Pass keystroke to labeltrack's handler and add to history if any
// updates were done
if (DoKeyDown( *project, viewInfo.selectedRegion, event )) {
ProjectHistory::Get( *project ).PushState(_("Modified Label"),
_("Label Edit"),
UndoPush::CONSOLIDATE);
}
// Make sure caret is in view
int x;
if (CalcCursorX( *project, &x ))
TrackPanel::Get( *project ).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 *, AudacityProject *project)
{
double bkpSel0 = viewInfo.selectedRegion.t0(),
bkpSel1 = viewInfo.selectedRegion.t1();
// Pass keystroke to labeltrack's handler and add to history if any
// updates were done
if (DoChar( *project, viewInfo.selectedRegion, event ))
ProjectHistory::Get( *project ).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(
AudacityProject &project, 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( project ) ) {
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( project ).Any()
.begin().advance(mRestoreFocus);
if (track)
TrackPanel::Get( project ).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( project );
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(
AudacityProject &project, 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( project ) ) {
// Don't create a NEW label for a space
if (wxIsspace(charCode)) {
event.Skip();
return false;
}
bool useDialog;
gPrefs->Read(wxT("/GUI/DialogForNameNewLabel"), &useDialog, false);
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
if (useDialog) {
wxString title;
if (DialogForLabelName(
project, selectedRegion, charCode, title) ==
wxID_CANCEL) {
return false;
}
pTrack->SetSelected(true);
pTrack->AddLabel(selectedRegion, title);
ProjectHistory::Get( project ).PushState(_("Added label"), _("Label"));
return false;
}
else {
pTrack->SetSelected(true);
AddLabel( selectedRegion );
ProjectHistory::Get( project ).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( AudacityProject &project )
{
wxWindow *parent = wxWindow::FindFocus();
// Bug 2044. parent can be nullptr after a context switch.
if( !parent )
parent = &GetProjectFrame( project );
if( parent )
{
wxMenu menu;
menu.Bind(wxEVT_MENU,
[this, &project]( wxCommandEvent &event ){
OnContextMenu( project, event ); }
);
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( project ));
menu.Enable(OnCopySelectedTextID, IsTextSelected( project ));
menu.Enable(OnPasteSelectedTextID, IsTextClipSupported());
menu.Enable(OnDeleteSelectedLabelID, true);
menu.Enable(OnEditSelectedLabelID, true);
if( !HasSelection( project ) ) {
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( project, &x );
wxASSERT(success);
static_cast<void>(success); // Suppress unused variable warning if debug mode is disabled
parent->PopupMenu(&menu, x, ls->y + (mIconHeight / 2) - 1);
}
}
void LabelTrackView::OnContextMenu(
AudacityProject &project, wxCommandEvent & evt )
{
auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
switch (evt.GetId())
{
/// Cut selected text if cut menu item is selected
case OnCutSelectedTextID:
if (CutSelectedText( project ))
{
ProjectHistory::Get( project ).PushState(_("Modified Label"),
_("Label Edit"),
UndoPush::CONSOLIDATE);
}
break;
/// Copy selected text if copy menu item is selected
case OnCopySelectedTextID:
CopySelectedText( project );
break;
/// paste selected text if paste menu item is selected
case OnPasteSelectedTextID:
if (PasteSelectedText(
project, selectedRegion.t0(), selectedRegion.t1() ))
{
ProjectHistory::Get( project ).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( project ).PushState(_("Deleted Label"),
_("Label Edit"),
UndoPush::CONSOLIDATE);
}
}
break;
case OnEditSelectedLabelID: {
int ndx = GetLabelIndex(selectedRegion.t0(), selectedRegion.t1());
if (ndx != -1)
DoEditLabels( project, 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( AudacityProject &project ) const
{
const auto selIndex = GetSelectedIndex( project );
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':
/// <O, O> and <O>
/// 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<NUM_GLYPH_CONFIGS;iConfig++)
{
for( iHighlight=0;iHighlight<NUM_GLYPH_HIGHLIGHTS;iHighlight++)
{
index = iConfig + NUM_GLYPH_CONFIGS * iHighlight;
// Copy the basic spec...
memcpy( XmpBmp, GlyphXpmRegionSpec, sizeof( GlyphXpmRegionSpec ));
// The higlighted region (if any) is white...
if( iHighlight==1 ) XmpBmp[5]="5 c #FFFFFF";
if( iHighlight==2 ) XmpBmp[6]="6 c #FFFFFF";
if( iHighlight==3 ) XmpBmp[7]="7 c #FFFFFF";
// For left or right arrow the other side of the glyph
// is the transparent color.
if( iConfig==0) { XmpBmp[3]="3 c none"; XmpBmp[5]="5 c none"; }
if( iConfig==1) { XmpBmp[4]="4 c none"; XmpBmp[7]="7 c none"; }
// Create the icon from the tweaked spec.
mBoundaryGlyphs[index] = wxBitmap(XmpBmp);
// Create the mask
// SetMask takes ownership
mBoundaryGlyphs[index].SetMask(safenew wxMask(mBoundaryGlyphs[index], wxColour(192, 192, 192)));
}
}
mIconWidth = mBoundaryGlyphs[0].GetWidth();
mIconHeight = mBoundaryGlyphs[0].GetHeight();
mTextHeight = mIconHeight; // until proved otherwise...
// The icon should have an odd width so that the
// line goes exactly down the middle.
wxASSERT( (mIconWidth %2)==1);
mbGlyphsReady=true;
}
#include "../../../LabelDialog.h"
void LabelTrackView::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"));
}
}
int LabelTrackView::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 += viewInfo.GetLabelWidth()
+ std::max(0, static_cast<int>(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;
}
using DoGetLabelTrackView = DoGetView::Override< LabelTrack >;
template<> template<> auto DoGetLabelTrackView::Implementation() -> Function {
return [](LabelTrack &track) {
return std::make_shared<LabelTrackView>( track.SharedPointer() );
};
}
static DoGetLabelTrackView registerDoGetLabelTrackView;
std::shared_ptr<TrackVRulerControls> LabelTrackView::DoGetVRulerControls()
{
return
std::make_shared<LabelTrackVRulerControls>( shared_from_this() );
}