mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-06 14:52:34 +02:00
2937 lines
85 KiB
C++
2937 lines
85 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
LabelTrack.cpp
|
|
|
|
Dominic Mazzoni
|
|
James Crook
|
|
Jun Wan
|
|
|
|
*******************************************************************//**
|
|
|
|
\class LabelTrack
|
|
\brief A LabelTrack is a Track that holds labels (LabelStruct).
|
|
|
|
These are used to annotate a waveform.
|
|
Each label has a start time and an end time.
|
|
The text of the labels is editable and the
|
|
positions of the end points are draggable.
|
|
|
|
*//****************************************************************//**
|
|
|
|
\class LabelStruct
|
|
\brief A LabelStruct holds information for ONE label in a LabelTrack
|
|
|
|
LabelStruct also has label specific functions, mostly functions
|
|
for drawing different aspects of the label and its text box.
|
|
|
|
*//*******************************************************************/
|
|
|
|
#include "Audacity.h"
|
|
#include "LabelTrack.h"
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <wx/bitmap.h>
|
|
#include <wx/brush.h>
|
|
#include <wx/clipbrd.h>
|
|
#include <wx/dataobj.h>
|
|
#include <wx/dc.h>
|
|
#include <wx/dcclient.h>
|
|
#include <wx/event.h>
|
|
#include <wx/intl.h>
|
|
#include <wx/log.h>
|
|
#include <wx/msgdlg.h>
|
|
#include <wx/pen.h>
|
|
#include <wx/string.h>
|
|
#include <wx/textfile.h>
|
|
#include <wx/utils.h>
|
|
|
|
#include "AudioIO.h"
|
|
#include "DirManager.h"
|
|
#include "Internat.h"
|
|
#include "Prefs.h"
|
|
#include "Theme.h"
|
|
#include "AllThemeResources.h"
|
|
#include "AColor.h"
|
|
#include "Project.h"
|
|
#include "TrackArtist.h"
|
|
#include "TrackPanel.h"
|
|
#include "UndoManager.h"
|
|
#include "commands/CommandManager.h"
|
|
|
|
#include "effects/TimeWarper.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;
|
|
|
|
LabelTrack::Holder TrackFactory::NewLabelTrack()
|
|
{
|
|
return std::make_unique<LabelTrack>(mDirManager);
|
|
}
|
|
|
|
LabelTrack::LabelTrack(DirManager * projDirManager):
|
|
Track(projDirManager),
|
|
mbHitCenter(false),
|
|
mOldEdge(-1),
|
|
mSelIndex(-1),
|
|
mMouseOverLabelLeft(-1),
|
|
mMouseOverLabelRight(-1),
|
|
mRestoreFocus(-1),
|
|
mClipLen(0.0),
|
|
mIsAdjustingLabel(false)
|
|
{
|
|
SetDefaultName(_("Label Track"));
|
|
SetName(GetDefaultName());
|
|
|
|
// 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.
|
|
SetHeight(73);
|
|
|
|
ResetFont();
|
|
CreateCustomGlyphs();
|
|
|
|
// reset flags
|
|
ResetFlags();
|
|
}
|
|
|
|
LabelTrack::LabelTrack(const LabelTrack &orig) :
|
|
Track(orig),
|
|
mbHitCenter(false),
|
|
mOldEdge(-1),
|
|
mSelIndex(-1),
|
|
mMouseOverLabelLeft(-1),
|
|
mMouseOverLabelRight(-1),
|
|
mClipLen(0.0),
|
|
mIsAdjustingLabel(false)
|
|
{
|
|
int len = orig.mLabels.Count();
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
const LabelStruct& original = *orig.mLabels[i];
|
|
LabelStruct *l =
|
|
new LabelStruct(original.selectedRegion, original.title);
|
|
mLabels.Add(l);
|
|
}
|
|
mSelIndex = orig.mSelIndex;
|
|
|
|
// reset flags
|
|
ResetFlags();
|
|
}
|
|
|
|
LabelTrack::~LabelTrack()
|
|
{
|
|
int len = mLabels.Count();
|
|
|
|
for (int i = 0; i < len; i++)
|
|
delete mLabels[i];
|
|
}
|
|
|
|
void LabelTrack::SetOffset(double dOffset)
|
|
{
|
|
int len = mLabels.Count();
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
mLabels[i]->selectedRegion.move(dOffset);
|
|
}
|
|
}
|
|
|
|
bool LabelTrack::Clear(double b, double e)
|
|
{
|
|
for (size_t i=0;i<mLabels.GetCount();i++){
|
|
LabelStruct::TimeRelations relation =
|
|
mLabels[i]->RegionRelation(b, e, this);
|
|
if (relation == LabelStruct::BEFORE_LABEL) {
|
|
mLabels[i]->selectedRegion.move(- (e-b));
|
|
} else if (relation == LabelStruct::SURROUNDS_LABEL) {
|
|
DeleteLabel( i );
|
|
i--;
|
|
} else if (relation == LabelStruct::ENDS_IN_LABEL) {
|
|
mLabels[i]->selectedRegion.setTimes(
|
|
b,
|
|
mLabels[i]->getT1() - (e - b));
|
|
} else if (relation == LabelStruct::BEGINS_IN_LABEL) {
|
|
mLabels[i]->selectedRegion.setT1(b);
|
|
} else if (relation == LabelStruct::WITHIN_LABEL) {
|
|
mLabels[i]->selectedRegion.moveT1( - (e-b));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#if 0
|
|
//used when we want to use clear only on the labels
|
|
bool LabelTrack::SplitDelete(double b, double e)
|
|
{
|
|
for (size_t i=0;i<mLabels.GetCount();i++) {
|
|
LabelStruct::TimeRelations relation =
|
|
mLabels[i]->RegionRelation(b, e, this);
|
|
if (relation == LabelStruct::SURROUNDS_LABEL) {
|
|
DeleteLabel(i);
|
|
i--;
|
|
} else if (relation == LabelStruct::WITHIN_LABEL) {
|
|
mLabels[i]->selectedRegion.moveT1( - (e-b));
|
|
} else if (relation == LabelStruct::ENDS_IN_LABEL) {
|
|
mLabels[i]->selectedRegion.setT0(e);
|
|
} else if (relation == LabelStruct::BEGINS_IN_LABEL) {
|
|
mLabels[i]->selectedRegion.setT1(b);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
void LabelTrack::ShiftLabelsOnInsert(double length, double pt)
|
|
{
|
|
for (unsigned int i=0;i<mLabels.GetCount();i++) {
|
|
LabelStruct::TimeRelations relation =
|
|
mLabels[i]->RegionRelation(pt, pt, this);
|
|
|
|
if (relation == LabelStruct::BEFORE_LABEL) {
|
|
mLabels[i]->selectedRegion.move(length);
|
|
}
|
|
else if (relation == LabelStruct::WITHIN_LABEL) {
|
|
mLabels[i]->selectedRegion.moveT1(length);
|
|
}
|
|
}
|
|
}
|
|
|
|
void LabelTrack::ChangeLabelsOnReverse(double b, double e)
|
|
{
|
|
for (size_t i=0; i<mLabels.GetCount(); i++) {
|
|
if (mLabels[i]->RegionRelation(b, e, this) ==
|
|
LabelStruct::SURROUNDS_LABEL)
|
|
{
|
|
double aux = b + (e - mLabels[i]->getT1());
|
|
mLabels[i]->selectedRegion.setTimes(
|
|
aux,
|
|
e - (mLabels[i]->getT0() - b));
|
|
}
|
|
}
|
|
SortLabels();
|
|
}
|
|
|
|
void LabelTrack::ScaleLabels(double b, double e, double change)
|
|
{
|
|
for (unsigned int i=0;i<mLabels.GetCount();i++){
|
|
mLabels[i]->selectedRegion.setTimes(
|
|
AdjustTimeStampOnScale(mLabels[i]->getT0(), b, e, change),
|
|
AdjustTimeStampOnScale(mLabels[i]->getT1(), b, e, change));
|
|
}
|
|
}
|
|
|
|
double LabelTrack::AdjustTimeStampOnScale(double t, double b, double e, double change)
|
|
{
|
|
//t is the time stamp we'll be changing
|
|
//b and e are the selection start and end
|
|
|
|
if (t < b){
|
|
return t;
|
|
}else if (t > e){
|
|
double shift = (e-b)*change - (e-b);
|
|
return t + shift;
|
|
}else{
|
|
double shift = (t-b)*change - (t-b);
|
|
return t + shift;
|
|
}
|
|
}
|
|
|
|
// Move the labels in the track according to the given TimeWarper.
|
|
// (If necessary this could be optimised by ignoring labels that occur before a
|
|
// specified time, as in most cases they don't need to move.)
|
|
void LabelTrack::WarpLabels(const TimeWarper &warper) {
|
|
for (int i = 0; i < (int)mLabels.GetCount(); ++i) {
|
|
mLabels[i]->selectedRegion.setTimes(
|
|
warper.Warp(mLabels[i]->getT0()),
|
|
warper.Warp(mLabels[i]->getT1()));
|
|
}
|
|
}
|
|
|
|
void LabelTrack::ResetFlags()
|
|
{
|
|
mMouseXPos = -1;
|
|
mXPos1 = -1;
|
|
mXPos2 = -1;
|
|
mDragXPos = -1;
|
|
mInitialCursorPos = 1;
|
|
mCurrentCursorPos = 1;
|
|
mResetCursorPos = false;
|
|
mRightDragging = false;
|
|
mDrawCursor = false;
|
|
}
|
|
|
|
wxFont LabelTrack::GetFont(const wxString &faceName, int size)
|
|
{
|
|
wxFontEncoding encoding;
|
|
if (faceName == wxT(""))
|
|
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
|
|
{
|
|
// xExtra is extra space
|
|
// between the text and the endpoints.
|
|
const int xExtra=mIconWidth;
|
|
int x = mLabels[index]->x; // left endpoint
|
|
int x1 = mLabels[index]->x1; // right endpoint.
|
|
int width = mLabels[index]->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;
|
|
|
|
mLabels[index]->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 i;
|
|
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;
|
|
|
|
const int nRows = wxMin((r.height / yRowHeight) + 1, MAX_NUM_ROWS);
|
|
// Initially none of the rows have been used.
|
|
// So set a value that is less than any valid value.
|
|
{
|
|
const int xStart = zoomInfo.TimeToPosition(0.0, r.x) - 100;
|
|
for(i=0;i<MAX_NUM_ROWS;i++)
|
|
xUsed[i]=xStart;
|
|
}
|
|
int nRowsUsed=0;
|
|
|
|
for (i = 0; i < (int)mLabels.Count(); i++)
|
|
{
|
|
const int x = zoomInfo.TimeToPosition(mLabels[i]->getT0(), r.x);
|
|
const int x1 = zoomInfo.TimeToPosition(mLabels[i]->getT1(), r.x);
|
|
int y = r.y;
|
|
|
|
mLabels[i]->x=x;
|
|
mLabels[i]->x1=x1;
|
|
mLabels[i]->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 )
|
|
{
|
|
// 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;
|
|
mLabels[i]->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+mLabels[i]->width+xExtra;
|
|
if( xUsed[iRow] < x1 ) xUsed[iRow]=x1;
|
|
ComputeTextPosition( r, i );
|
|
}
|
|
}
|
|
}
|
|
|
|
LabelStruct::LabelStruct(const SelectedRegion ®ion,
|
|
const wxString& aTitle)
|
|
: selectedRegion(region)
|
|
, title(aTitle)
|
|
{
|
|
changeInitialMouseXPos = true;
|
|
highlighted = false;
|
|
updated = false;
|
|
width = 0;
|
|
x = 0;
|
|
x1 = 0;
|
|
xText = 0;
|
|
y = 0;
|
|
}
|
|
|
|
LabelStruct::LabelStruct(const SelectedRegion ®ion,
|
|
double t0, double t1,
|
|
const wxString& aTitle)
|
|
: selectedRegion(region)
|
|
, title(aTitle)
|
|
{
|
|
// Overwrite the times
|
|
selectedRegion.setTimes(t0, t1);
|
|
|
|
changeInitialMouseXPos = true;
|
|
highlighted = false;
|
|
updated = false;
|
|
width = 0;
|
|
x = 0;
|
|
x1 = 0;
|
|
xText = 0;
|
|
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)
|
|
{
|
|
highlighted = true;
|
|
changeInitialMouseXPos = false;
|
|
|
|
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;
|
|
}
|
|
|
|
/// Draw calls other functions to draw the LabelTrack.
|
|
/// @param dc the device context
|
|
/// @param r the LabelTrack rectangle.
|
|
void LabelTrack::Draw(wxDC & dc, const wxRect & r,
|
|
const SelectedRegion &selectedRegion,
|
|
const ZoomInfo &zoomInfo) const
|
|
{
|
|
if(msFont.Ok())
|
|
dc.SetFont(msFont);
|
|
|
|
if (mFontHeight == -1)
|
|
calculateFontHeight(dc);
|
|
|
|
TrackArtist::DrawBackgroundWithSelection(&dc, r, this,
|
|
AColor::labelSelectedBrush, AColor::labelUnselectedBrush,
|
|
selectedRegion, zoomInfo);
|
|
|
|
int i;
|
|
|
|
wxCoord textWidth, textHeight;
|
|
|
|
// Get the text widths.
|
|
// TODO: Make more efficient by only re-computing when a
|
|
// text label title changes.
|
|
for (i = 0; i < (int)mLabels.Count(); i++)
|
|
{
|
|
dc.GetTextExtent(mLabels[i]->title, &textWidth, &textHeight);
|
|
mLabels[i]->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);
|
|
const int nLabels = (int)mLabels.Count();
|
|
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 (i = 0; i < nLabels; i++)
|
|
{
|
|
mLabels[i]->DrawLines( dc, r );
|
|
}
|
|
|
|
// Draw the end glyphs.
|
|
for (i = 0; i < nLabels; i++)
|
|
{
|
|
GlyphLeft=0;
|
|
GlyphRight=1;
|
|
if( i==mMouseOverLabelLeft )
|
|
GlyphLeft = mbHitCenter ? 6:9;
|
|
if( i==mMouseOverLabelRight )
|
|
GlyphRight = mbHitCenter ? 7:4;
|
|
mLabels[i]->DrawGlyphs( dc, r, GlyphLeft, GlyphRight );
|
|
}
|
|
|
|
// Draw the label boxes.
|
|
for (i = 0; i < nLabels; i++)
|
|
{
|
|
if( mSelIndex==i) dc.SetBrush(AColor::labelTextEditBrush);
|
|
mLabels[i]->DrawTextBox( dc, r );
|
|
if( mSelIndex==i) dc.SetBrush(AColor::labelTextNormalBrush);
|
|
}
|
|
|
|
// Draw highlights
|
|
if ((mDragXPos != -1) && (mSelIndex >= 0 ))
|
|
{
|
|
// find the left X pos of highlighted area
|
|
mLabels[mSelIndex]->getXPos(dc, &mXPos1, mInitialCursorPos);
|
|
// for preventing dragging glygh from changing current cursor position
|
|
if (mResetCursorPos) {
|
|
// set end dragging position to current cursor position
|
|
SetCurrentCursorPosition(mDragXPos);
|
|
mResetCursorPos = false;
|
|
}
|
|
// find the right X pos of highlighted area
|
|
mLabels[mSelIndex]->getXPos(dc, &mXPos2, mCurrentCursorPos);
|
|
mLabels[mSelIndex]->DrawHighlight(dc, mXPos1, mXPos2, mFontHeight);
|
|
}
|
|
|
|
// Draw the text and the label boxes.
|
|
for (i = 0; i < nLabels; i++)
|
|
{
|
|
if( mSelIndex==i) dc.SetBrush(AColor::labelTextEditBrush);
|
|
mLabels[i]->DrawText( dc, r );
|
|
if( mSelIndex==i) dc.SetBrush(AColor::labelTextNormalBrush);
|
|
}
|
|
|
|
// Draw the cursor, if there is one.
|
|
if( mSelIndex >=0 )
|
|
{
|
|
i = mSelIndex;
|
|
int xPos = mLabels[i]->xText;
|
|
|
|
// if mouse is clicked in text box
|
|
if (mMouseXPos != -1)
|
|
{
|
|
// set current cursor position
|
|
SetCurrentCursorPosition((int) mMouseXPos);
|
|
// for preventing from resetting by shift+mouse left button
|
|
// set initialCursorPos equal to currentCursorPos
|
|
if (mLabels[mSelIndex]->changeInitialMouseXPos)
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
mDrawCursor = true;
|
|
mMouseXPos = -1;
|
|
}
|
|
|
|
if( mCurrentCursorPos > 0)
|
|
{
|
|
// Calculate the width of the substring and add it to Xpos
|
|
int partWidth;
|
|
dc.GetTextExtent((mLabels[i]->title).Left(mCurrentCursorPos), &partWidth, NULL);
|
|
xPos += partWidth;
|
|
}
|
|
|
|
// Draw the cursor
|
|
wxPen currentPen = dc.GetPen();
|
|
const int CursorWidth=2;
|
|
if (mDrawCursor) {
|
|
currentPen.SetWidth(CursorWidth);
|
|
AColor::Line(dc,
|
|
xPos-1, mLabels[i]->y - mFontHeight/2 + 1,
|
|
xPos-1, mLabels[i]->y + mFontHeight/2 - 1);
|
|
currentPen.SetWidth(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Set the cursor position according to x position of mouse
|
|
/// uses GetTextExtent to find the character position
|
|
/// corresponding to the x pixel position.
|
|
void LabelTrack::SetCurrentCursorPosition(int xPos) const
|
|
{
|
|
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;
|
|
while (!finished && (charIndex < (int)mLabels[mSelIndex]->title.length() + 1))
|
|
{
|
|
subString = (mLabels[mSelIndex]->title).Left(charIndex);
|
|
// Get the width of substring
|
|
dc.GetTextExtent(subString, &partWidth, NULL);
|
|
if (charIndex > 1)
|
|
{
|
|
// Get the width of the last character
|
|
dc.GetTextExtent(subString.Right(1), &oneWidth, NULL);
|
|
bound = mLabels[mSelIndex]->xText + partWidth - oneWidth * 0.5;
|
|
}
|
|
else
|
|
{
|
|
bound = mLabels[mSelIndex]->xText + partWidth * 0.5;
|
|
}
|
|
|
|
if (xPos <= bound)
|
|
{
|
|
// Found
|
|
mCurrentCursorPos = charIndex - 1;
|
|
finished = true;
|
|
}
|
|
else
|
|
{
|
|
// Advance
|
|
charIndex++;
|
|
}
|
|
}
|
|
if (!finished)
|
|
{
|
|
// Cursor should be in the last position
|
|
mCurrentCursorPos = mLabels[mSelIndex]->title.length();
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
if (mSelIndex == -1)
|
|
return false;
|
|
if (!mLabels[mSelIndex]->highlighted)
|
|
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=wxT("");
|
|
wxString right=wxT("");
|
|
wxString text = mLabels[mSelIndex]->title;
|
|
|
|
// swapping to make sure currentCursorPos > initialCursorPos always
|
|
if (mInitialCursorPos > mCurrentCursorPos) {
|
|
int temp = mCurrentCursorPos;
|
|
mCurrentCursorPos = mInitialCursorPos;
|
|
mInitialCursorPos = temp;
|
|
}
|
|
|
|
// data for cutting
|
|
wxString data = text.Mid(mInitialCursorPos, mCurrentCursorPos-mInitialCursorPos);
|
|
|
|
// get left-remaining text
|
|
if (mInitialCursorPos > 0) {
|
|
left = text.Mid(0, mInitialCursorPos);
|
|
}
|
|
|
|
// get right-remaining text
|
|
if (mCurrentCursorPos < (int)text.Length()) {
|
|
right = text.Mid(mCurrentCursorPos, text.Length()-mCurrentCursorPos);
|
|
}
|
|
|
|
// set title to the combination of the two remainders
|
|
mLabels[mSelIndex]->title = 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
|
|
mCurrentCursorPos = left.Length();
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
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;
|
|
if (!mLabels[mSelIndex]->highlighted)
|
|
return false;
|
|
|
|
// swapping to make sure currentCursorPos > mInitialCursorPos always
|
|
int init = mInitialCursorPos;
|
|
int cur = mCurrentCursorPos;
|
|
if (init > cur) {
|
|
cur = mInitialCursorPos;
|
|
init = mCurrentCursorPos;
|
|
}
|
|
|
|
// data for copying
|
|
wxString data = mLabels[mSelIndex]->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;
|
|
wxString left=wxT("");
|
|
wxString right=wxT("");
|
|
|
|
// 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
|
|
int i;
|
|
for (i = 0; i < (int)text.Length(); i++) {
|
|
if (wxIscntrl(text[i])) {
|
|
text[i] = wxT(' ');
|
|
}
|
|
}
|
|
}
|
|
|
|
// if there is some highlighted text
|
|
if (mLabels[mSelIndex]->highlighted) {
|
|
// swapping to make sure currentCursorPos > mInitialCursorPos always
|
|
if (mInitialCursorPos > mCurrentCursorPos) {
|
|
int temp = mCurrentCursorPos;
|
|
mCurrentCursorPos = mInitialCursorPos;
|
|
mInitialCursorPos = temp;
|
|
}
|
|
|
|
// same as cutting
|
|
if (mInitialCursorPos > 0) {
|
|
left = (mLabels[mSelIndex]->title).Mid(0, mInitialCursorPos);
|
|
}
|
|
if (mCurrentCursorPos < (int)(mLabels[mSelIndex]->title).Length()) {
|
|
right = (mLabels[mSelIndex]->title).Mid(mCurrentCursorPos, (mLabels[mSelIndex]->title).Length()-mCurrentCursorPos);
|
|
}
|
|
mLabels[mSelIndex]->title = left + text + right;
|
|
mCurrentCursorPos = left.Length() + text.Length();
|
|
}
|
|
else
|
|
{
|
|
// insert the data on the clipboard from the cursor position
|
|
if (mCurrentCursorPos < (int)(mLabels[mSelIndex]->title).Length()) {
|
|
right = (mLabels[mSelIndex]->title).Mid(mCurrentCursorPos);
|
|
}
|
|
mLabels[mSelIndex]->title = (mLabels[mSelIndex]->title).Left(mCurrentCursorPos);
|
|
mLabels[mSelIndex]->title += text;
|
|
mLabels[mSelIndex]->title += right;
|
|
mCurrentCursorPos += text.Length();
|
|
}
|
|
// set mInitialCursorPos equal to currentCursorPos
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
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;
|
|
}
|
|
|
|
double LabelTrack::GetStartTime() const
|
|
{
|
|
int len = mLabels.Count();
|
|
|
|
if (len == 0)
|
|
return 0.0;
|
|
else
|
|
return mLabels[0]->getT0();
|
|
}
|
|
|
|
double LabelTrack::GetEndTime() const
|
|
{
|
|
//we need to scan through all the labels, because the last
|
|
//label might not have the right-most end (if there is overlap).
|
|
int len = mLabels.Count();
|
|
if (len == 0)
|
|
return 0.0;
|
|
|
|
double end = 0.0;
|
|
for(int i = 0; i < len; i++)
|
|
{
|
|
const double t1 = mLabels[i]->getT1();
|
|
if(t1 > end)
|
|
end = t1;
|
|
}
|
|
return end;
|
|
}
|
|
|
|
Track::Holder LabelTrack::Duplicate() const
|
|
{
|
|
return std::make_unique<LabelTrack>( *this );
|
|
}
|
|
|
|
void LabelTrack::SetSelected(bool s)
|
|
{
|
|
Track::SetSelected(s);
|
|
if (!s)
|
|
Unselect();
|
|
}
|
|
|
|
/// OverGlyph returns 0 if not over a glyph,
|
|
/// 1 if over the left-hand glyph, and
|
|
/// 2 if over the right-hand glyph on a label.
|
|
/// 3 if over both right and left.
|
|
///
|
|
/// It also sets up member variables:
|
|
/// mMouseLabelLeft - index of any left label hit
|
|
/// mMouseLabelRight - index of any right label hit
|
|
/// mbHitCenter - if (x,y) 'hits the spot'.
|
|
///
|
|
/// TODO: Investigate what happens with large
|
|
/// numbers of labels, might need a binary search
|
|
/// rather than a linear one.
|
|
int LabelTrack::OverGlyph(int x, int y)
|
|
{
|
|
//Determine the NEW selection.
|
|
LabelStruct * pLabel;
|
|
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
|
|
mMouseOverLabelLeft = -1;
|
|
mMouseOverLabelRight = -1;
|
|
mbHitCenter = false;
|
|
for (int i = 0; i < (int)mLabels.Count(); i++)
|
|
{
|
|
pLabel = 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(pLabel->y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 &&
|
|
abs(pLabel->x1 - d2 -x) < d1)
|
|
{
|
|
mMouseOverLabelRight = i;
|
|
if(abs(pLabel->x1 - x) < d2 )
|
|
{
|
|
mbHitCenter = true;
|
|
// 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(pLabel->x1-pLabel->x) < 1.0 )
|
|
{
|
|
result |=1;
|
|
mMouseOverLabelLeft = i;
|
|
}
|
|
}
|
|
result |= 2;
|
|
}
|
|
// Use else-if here rather than else to avoid detecting left and right
|
|
// of the same label.
|
|
else if( abs(pLabel->y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 &&
|
|
abs(pLabel->x + d2 - x) < d1 )
|
|
{
|
|
mMouseOverLabelLeft = i;
|
|
if(abs(pLabel->x - x) < d2 )
|
|
mbHitCenter = true;
|
|
result |= 1;
|
|
}
|
|
|
|
// give text box better priority for selecting
|
|
if(OverTextBox(pLabel, x, y))
|
|
{
|
|
result = 0;
|
|
}
|
|
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int LabelTrack::OverATextBox(int xx, int yy) const
|
|
{
|
|
for (int nn = (int)mLabels.Count(); nn--;) {
|
|
if (OverTextBox(mLabels[nn], 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) &&
|
|
(x<pLabel->xText+pLabel->width+(mIconWidth/2)) &&
|
|
(abs(pLabel->y-y)<mIconHeight/2))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Adjust label's left or right boundary, depending which is requested.
|
|
// Return true iff the label flipped.
|
|
bool LabelStruct::AdjustEdge( int iEdge, double fNewTime)
|
|
{
|
|
updated = true;
|
|
if( iEdge < 0 )
|
|
return selectedRegion.setT0(fNewTime);
|
|
else
|
|
return selectedRegion.setT1(fNewTime);
|
|
}
|
|
|
|
// We're moving the label. Adjust both left and right edge.
|
|
void LabelStruct::MoveLabel( int iEdge, double fNewTime)
|
|
{
|
|
double fTimeSpan = getDuration();
|
|
|
|
if( iEdge < 0 )
|
|
{
|
|
selectedRegion.setTimes(fNewTime, fNewTime+fTimeSpan);
|
|
}
|
|
else
|
|
{
|
|
selectedRegion.setTimes(fNewTime-fTimeSpan, fNewTime);
|
|
}
|
|
updated = true;
|
|
}
|
|
|
|
auto LabelStruct::RegionRelation(
|
|
double reg_t0, double reg_t1, const LabelTrack * WXUNUSED(parent)) const
|
|
-> TimeRelations
|
|
{
|
|
bool retainLabels = false;
|
|
|
|
wxASSERT(reg_t0 <= reg_t1);
|
|
gPrefs->Read(wxT("/GUI/RetainLabels"), &retainLabels);
|
|
|
|
if(retainLabels) {
|
|
|
|
// Desired behavior for edge cases: The length of the selection is smaller
|
|
// than the length of the label if the selection is within the label or
|
|
// matching exactly a (region) label.
|
|
|
|
if (reg_t0 < getT0() && reg_t1 > getT1())
|
|
return SURROUNDS_LABEL;
|
|
else if (reg_t1 < getT0())
|
|
return BEFORE_LABEL;
|
|
else if (reg_t0 > getT1())
|
|
return AFTER_LABEL;
|
|
|
|
else if (reg_t0 >= getT0() && reg_t0 <= getT1() &&
|
|
reg_t1 >= getT0() && reg_t1 <= getT1())
|
|
return WITHIN_LABEL;
|
|
|
|
else if (reg_t0 >= getT0() && reg_t0 <= getT1())
|
|
return BEGINS_IN_LABEL;
|
|
else
|
|
return ENDS_IN_LABEL;
|
|
|
|
} else {
|
|
|
|
// AWD: Desired behavior for edge cases: point labels bordered by the
|
|
// selection are included within it. Region labels are included in the
|
|
// selection to the extent that the selection covers them; specifically,
|
|
// they're not included at all if the selection borders them, and they're
|
|
// fully included if the selection covers them fully, even if it just
|
|
// borders their endpoints. This is just one of many possible schemes.
|
|
|
|
// The first test catches bordered point-labels and selected-through
|
|
// region-labels; move it to third and selection edges become inclusive
|
|
// WRT point-labels.
|
|
if (reg_t0 <= getT0() && reg_t1 >= getT1())
|
|
return SURROUNDS_LABEL;
|
|
else if (reg_t1 <= getT0())
|
|
return BEFORE_LABEL;
|
|
else if (reg_t0 >= getT1())
|
|
return AFTER_LABEL;
|
|
|
|
// At this point, all point labels should have returned.
|
|
|
|
else if (reg_t0 > getT0() && reg_t0 < getT1() &&
|
|
reg_t1 > getT0() && reg_t1 < getT1())
|
|
return WITHIN_LABEL;
|
|
|
|
// Knowing that none of the other relations match simplifies remaining
|
|
// tests
|
|
else if (reg_t0 > getT0() && reg_t0 < getT1())
|
|
return BEGINS_IN_LABEL;
|
|
else
|
|
return ENDS_IN_LABEL;
|
|
|
|
}
|
|
}
|
|
|
|
/// 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 LabelTrack::MayAdjustLabel( int iLabel, int iEdge, bool bAllowSwapping, double fNewTime)
|
|
{
|
|
if( iLabel < 0 )
|
|
return;
|
|
LabelStruct * pLabel = mLabels[ iLabel ];
|
|
|
|
// Adjust the requested edge.
|
|
bool flipped = pLabel->AdjustEdge( iEdge, fNewTime );
|
|
// If the edges did not swap, then we are done.
|
|
if( ! flipped )
|
|
return;
|
|
|
|
// If swapping's not allowed we must also move the edge
|
|
// we didn't move. Then we're done.
|
|
if( !bAllowSwapping )
|
|
{
|
|
pLabel->AdjustEdge( -iEdge, fNewTime );
|
|
return;
|
|
}
|
|
|
|
// Swap our record of what we are dragging.
|
|
int Temp = mMouseOverLabelLeft;
|
|
mMouseOverLabelLeft = mMouseOverLabelRight;
|
|
mMouseOverLabelRight = Temp;
|
|
}
|
|
|
|
// If the index is for a real label, adjust its left and right boundary.
|
|
void LabelTrack::MayMoveLabel( int iLabel, int iEdge, double fNewTime)
|
|
{
|
|
if( iLabel < 0 )
|
|
return;
|
|
mLabels[ iLabel ]->MoveLabel( iEdge, fNewTime );
|
|
}
|
|
|
|
// 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 LabelTrack::HandleGlyphDragRelease(const wxMouseEvent & evt,
|
|
wxRect & r, const ZoomInfo &zoomInfo,
|
|
SelectedRegion *newSel)
|
|
{
|
|
if(evt.LeftUp())
|
|
{
|
|
bool lupd = false, rupd = false;
|
|
if(mMouseOverLabelLeft>=0) {
|
|
lupd = mLabels[mMouseOverLabelLeft]->updated;
|
|
mLabels[mMouseOverLabelLeft]->updated = false;
|
|
}
|
|
if(mMouseOverLabelRight>=0) {
|
|
rupd = mLabels[mMouseOverLabelRight]->updated;
|
|
mLabels[mMouseOverLabelRight]->updated = false;
|
|
}
|
|
|
|
mIsAdjustingLabel = false;
|
|
mMouseOverLabelLeft = -1;
|
|
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 = (mMouseOverLabelLeft >=0 ) ^ ( 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 = mbIsMoving;
|
|
bLabelMoving ^= evt.ShiftDown();
|
|
bLabelMoving |= mMouseOverLabelLeft==mMouseOverLabelRight;
|
|
double fNewX = zoomInfo.PositionToTime(x, 0);
|
|
if( bLabelMoving )
|
|
{
|
|
MayMoveLabel( mMouseOverLabelLeft, -1, fNewX );
|
|
MayMoveLabel( mMouseOverLabelRight, +1, fNewX );
|
|
}
|
|
else
|
|
{
|
|
MayAdjustLabel( mMouseOverLabelLeft, -1, bAllowSwapping, fNewX );
|
|
MayAdjustLabel( 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();
|
|
}
|
|
|
|
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 dragging happens in text box
|
|
// end dragging x position in pixels
|
|
// set flag to update current cursor position
|
|
mDragXPos = evt.m_x;
|
|
mResetCursorPos = true;
|
|
|
|
// if it's an invalid dragging, disable displaying
|
|
if (mRightDragging) {
|
|
mDragXPos = -1;
|
|
mRightDragging = false;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (evt.RightUp()) {
|
|
if ((mSelIndex != -1) && OverTextBox(GetLabel(mSelIndex), evt.m_x, evt.m_y)) {
|
|
// popup menu for editing
|
|
ShowContextMenu();
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void LabelTrack::HandleClick(const wxMouseEvent & evt,
|
|
const wxRect & r, const ZoomInfo &zoomInfo,
|
|
SelectedRegion *newSel)
|
|
{
|
|
if (evt.ButtonDown())
|
|
{
|
|
//OverGlyph sets mMouseOverLabel to be the chosen label.
|
|
int iGlyph = OverGlyph(evt.m_x, evt.m_y);
|
|
mIsAdjustingLabel = evt.Button(wxMOUSE_BTN_LEFT) &&
|
|
iGlyph != 0;
|
|
|
|
// reset mouseXPos if the mouse is pressed in the text box
|
|
mMouseXPos = -1;
|
|
|
|
if (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.
|
|
mbIsMoving = mbHitCenter;
|
|
// 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( (mMouseOverLabelRight >=0) &&
|
|
(mMouseOverLabelLeft >=0)
|
|
)
|
|
{
|
|
t = (mLabels[mMouseOverLabelRight]->getT1() +
|
|
mLabels[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.
|
|
mbIsMoving = (mMouseOverLabelLeft == mMouseOverLabelRight);
|
|
}
|
|
else if(mMouseOverLabelRight >=0)
|
|
{
|
|
t = mLabels[mMouseOverLabelRight]->getT1();
|
|
}
|
|
else if(mMouseOverLabelLeft >=0)
|
|
{
|
|
t = mLabels[mMouseOverLabelLeft]->getT0();
|
|
}
|
|
mxMouseDisplacement = zoomInfo.TimeToPosition(t, r.x) - evt.m_x;
|
|
return;
|
|
}
|
|
|
|
// disable displaying if left button is down
|
|
if (evt.LeftDown())
|
|
mDragXPos = -1;
|
|
|
|
mSelIndex = OverATextBox(evt.m_x, evt.m_y);
|
|
if (mSelIndex != -1) {
|
|
*newSel = mLabels[mSelIndex]->selectedRegion;
|
|
// set mouseXPos to set current cursor position
|
|
mMouseXPos = evt.m_x;
|
|
}
|
|
|
|
// reset the highlight indicator
|
|
wxRect highlightedRect;
|
|
if (mSelIndex != -1) {
|
|
wxASSERT(mFontHeight >= 0); // should have been set up while drawing
|
|
// the rectangle of highlighted area
|
|
if (mXPos1 < mXPos2)
|
|
highlightedRect = wxRect(mXPos1, mLabels[mSelIndex]->y - mFontHeight / 2, (int)(mXPos2 - mXPos1 + 0.5), mFontHeight);
|
|
else
|
|
highlightedRect = wxRect(mXPos2, mLabels[mSelIndex]->y - mFontHeight / 2, (int)(mXPos1 - mXPos2 + 0.5), mFontHeight);
|
|
|
|
// reset when left button is down
|
|
if (evt.LeftDown())
|
|
mLabels[mSelIndex]->highlighted = false;
|
|
// reset when right button is down outside text box
|
|
if (evt.RightDown())
|
|
{
|
|
if (!highlightedRect.Contains(evt.m_x, evt.m_y))
|
|
{
|
|
mCurrentCursorPos = 0;
|
|
mInitialCursorPos = 0;
|
|
mLabels[mSelIndex]->highlighted = false;
|
|
}
|
|
}
|
|
// set changeInitialMouseXPos flag
|
|
mLabels[mSelIndex]->changeInitialMouseXPos = true;
|
|
}
|
|
|
|
// disable displaying if right button is down outside text box
|
|
if (mSelIndex != -1 && evt.RightDown()
|
|
&& !highlightedRect.Contains(evt.m_x, evt.m_y))
|
|
mDragXPos = -1;
|
|
|
|
// 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 (mSelIndex != -1) {
|
|
if (!OverTextBox(mLabels[mSelIndex], evt.m_x, evt.m_y))
|
|
mSelIndex = -1;
|
|
double t = zoomInfo.PositionToTime(evt.m_x, r.x);
|
|
*newSel = SelectedRegion(t, t);
|
|
}
|
|
|
|
wxTheClipboard->UsePrimarySelection(true);
|
|
PasteSelectedText(newSel->t0(), newSel->t1());
|
|
wxTheClipboard->UsePrimarySelection(false);
|
|
|
|
return;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// Check for keys that we will process
|
|
bool LabelTrack::CaptureKey(wxKeyEvent & event)
|
|
{
|
|
// Check for modifiers and only allow shift
|
|
int mods = event.GetModifiers();
|
|
if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
|
|
return false;
|
|
}
|
|
|
|
if (mSelIndex >= 0) {
|
|
if (IsGoodLabelEditKey(event)) {
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
if (IsGoodLabelFirstKey(event)) {
|
|
AudacityProject * pProj = GetActiveProject();
|
|
|
|
// 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)
|
|
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;
|
|
}
|
|
}
|
|
|
|
// If there's a label there already don't capture
|
|
if( GetLabelIndex(pProj->mViewInfo.selectedRegion.t0(),
|
|
pProj->mViewInfo.selectedRegion.t1()) != wxNOT_FOUND ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// KeyEvent is called for every keypress when over the label track.
|
|
bool LabelTrack::OnKeyDown(SelectedRegion &newSel, wxKeyEvent & event)
|
|
{
|
|
// Only track true changes to the label
|
|
bool updated = false;
|
|
|
|
// Cache the keycode
|
|
int keyCode = event.GetKeyCode();
|
|
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
|
|
if (mSelIndex >= 0) {
|
|
switch (keyCode) {
|
|
|
|
case WXK_BACK:
|
|
{
|
|
int len = mLabels[mSelIndex]->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 (mLabels[mSelIndex]->highlighted) {
|
|
RemoveSelectedText();
|
|
}
|
|
else
|
|
{
|
|
// DELETE one letter
|
|
if (mCurrentCursorPos > 0) {
|
|
mLabels[mSelIndex]->title.Remove(mCurrentCursorPos-1, 1);
|
|
mCurrentCursorPos--;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ELSE no text in text box, so DELETE whole label.
|
|
DeleteLabel( mSelIndex );
|
|
}
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
updated = true;
|
|
}
|
|
break;
|
|
|
|
case WXK_DELETE:
|
|
case WXK_NUMPAD_DELETE:
|
|
{
|
|
int len = mLabels[mSelIndex]->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 (mLabels[mSelIndex]->highlighted) {
|
|
RemoveSelectedText();
|
|
}
|
|
else
|
|
{
|
|
// DELETE one letter
|
|
if (mCurrentCursorPos < len) {
|
|
mLabels[mSelIndex]->title.Remove(mCurrentCursorPos, 1);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// DELETE whole label if no text in text box
|
|
DeleteLabel( mSelIndex );
|
|
}
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
updated = true;
|
|
}
|
|
break;
|
|
|
|
case WXK_HOME:
|
|
case WXK_NUMPAD_HOME:
|
|
// Move cursor to beginning of label
|
|
if (mods == wxMOD_SHIFT) {
|
|
mCurrentCursorPos = 0;
|
|
mDragXPos = 0;
|
|
}
|
|
else if (mods == wxMOD_NONE)
|
|
{
|
|
mCurrentCursorPos = 0;
|
|
mDragXPos = -1;
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
}
|
|
else {
|
|
// Not handled
|
|
event.Skip();
|
|
}
|
|
break;
|
|
|
|
case WXK_END:
|
|
case WXK_NUMPAD_END:
|
|
// Move cursor to end of label
|
|
if (mods == wxMOD_SHIFT) {
|
|
mCurrentCursorPos = (int)mLabels[mSelIndex]->title.length();
|
|
mDragXPos = 0;
|
|
}
|
|
else if (mods == wxMOD_NONE)
|
|
{
|
|
mCurrentCursorPos = (int)mLabels[mSelIndex]->title.length();
|
|
mDragXPos = -1;
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
}
|
|
else {
|
|
// Not handled
|
|
event.Skip();
|
|
}
|
|
break;
|
|
|
|
case WXK_LEFT:
|
|
case WXK_NUMPAD_LEFT:
|
|
// Moving cursor left
|
|
if (mods == wxMOD_SHIFT) {
|
|
if (mCurrentCursorPos > 0) {
|
|
mCurrentCursorPos--;
|
|
mDragXPos = 0;
|
|
}
|
|
}
|
|
else if (mods == wxMOD_NONE) {
|
|
if (mCurrentCursorPos > 0) {
|
|
mCurrentCursorPos--;
|
|
mDragXPos = -1;
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
}
|
|
}
|
|
else {
|
|
// Not handled
|
|
event.Skip();
|
|
}
|
|
break;
|
|
|
|
case WXK_RIGHT:
|
|
case WXK_NUMPAD_RIGHT:
|
|
// Moving cursor right
|
|
if (mods == wxMOD_SHIFT) {
|
|
if (mCurrentCursorPos < (int)mLabels[mSelIndex]->title.length()) {
|
|
mCurrentCursorPos++;
|
|
mDragXPos = 0;
|
|
}
|
|
}
|
|
else if (mods == wxMOD_NONE)
|
|
{
|
|
if (mCurrentCursorPos < (int)mLabels[mSelIndex]->title.length()) {
|
|
mCurrentCursorPos++;
|
|
mDragXPos = -1;
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
}
|
|
}
|
|
else {
|
|
// Not handled
|
|
event.Skip();
|
|
}
|
|
break;
|
|
|
|
case WXK_RETURN:
|
|
case WXK_NUMPAD_ENTER:
|
|
|
|
case WXK_ESCAPE:
|
|
if (mRestoreFocus >= 0) {
|
|
TrackListIterator iter(GetActiveProject()->GetTracks());
|
|
Track *track = iter.First();
|
|
while (track && mRestoreFocus--)
|
|
track = iter.Next();
|
|
if (track)
|
|
GetActiveProject()->GetTrackPanel()->SetFocusedTrack(track);
|
|
mRestoreFocus = -1;
|
|
}
|
|
mSelIndex = -1;
|
|
break;
|
|
|
|
case WXK_TAB:
|
|
case WXK_NUMPAD_TAB:
|
|
if (event.ShiftDown()) {
|
|
mSelIndex--;
|
|
} else {
|
|
mSelIndex++;
|
|
}
|
|
|
|
if (mSelIndex >= 0 && mSelIndex < (int)mLabels.Count()) {
|
|
mCurrentCursorPos = mLabels[mSelIndex]->title.Length();
|
|
//Set the selection region to be equal to the selection bounds of the tabbed-to label.
|
|
newSel = mLabels[mSelIndex]->selectedRegion;
|
|
}
|
|
else {
|
|
mSelIndex = -1;
|
|
}
|
|
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.IsEmpty()) {
|
|
int len = (int) mLabels.Count();
|
|
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) {
|
|
mCurrentCursorPos = mLabels[mSelIndex]->title.Length();
|
|
//Set the selection region to be equal to the selection bounds of the tabbed-to label.
|
|
newSel = mLabels[mSelIndex]->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 LabelTrack::OnChar(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.
|
|
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
|
|
if (mSelIndex < 0) {
|
|
// Don't create a NEW label for a space
|
|
if (wxIsspace(charCode)) {
|
|
event.Skip();
|
|
return false;
|
|
}
|
|
SetSelected(true);
|
|
AudacityProject *p = GetActiveProject();
|
|
AddLabel(p->mViewInfo.selectedRegion);
|
|
p->PushState(_("Added label"), _("Label"));
|
|
}
|
|
|
|
//
|
|
// Now we are definitely in a label; append the incoming character
|
|
//
|
|
|
|
// Test if cursor is in the end of string or not
|
|
if (mLabels[mSelIndex]->highlighted) {
|
|
RemoveSelectedText();
|
|
}
|
|
|
|
if (mCurrentCursorPos < (int)mLabels[mSelIndex]->title.length()) {
|
|
// Get substring on the righthand side of cursor
|
|
wxString rightPart = mLabels[mSelIndex]->title.Mid(mCurrentCursorPos);
|
|
// Set title to substring on the lefthand side of cursor
|
|
mLabels[mSelIndex]->title = mLabels[mSelIndex]->title.Left(mCurrentCursorPos);
|
|
//append charcode
|
|
mLabels[mSelIndex]->title += charCode;
|
|
//append the right part substring
|
|
mLabels[mSelIndex]->title += rightPart;
|
|
}
|
|
else
|
|
{
|
|
//append charCode
|
|
mLabels[mSelIndex]->title += charCode;
|
|
}
|
|
//moving cursor position forward
|
|
mCurrentCursorPos++;
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
updated = true;
|
|
|
|
// Make sure the caret is visible
|
|
mDrawCursor = true;
|
|
|
|
return updated;
|
|
}
|
|
|
|
void LabelTrack::ShowContextMenu()
|
|
{
|
|
wxWindow *parent = wxWindow::FindFocus();
|
|
|
|
{
|
|
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);
|
|
|
|
const LabelStruct *ls = GetLabel(mSelIndex);
|
|
|
|
wxClientDC dc(parent);
|
|
|
|
if (msFont.Ok())
|
|
{
|
|
dc.SetFont(msFont);
|
|
}
|
|
|
|
int x;
|
|
if (mMouseXPos != -1)
|
|
{
|
|
x = mMouseXPos;
|
|
}
|
|
else
|
|
{
|
|
dc.GetTextExtent(ls->title.Left(mCurrentCursorPos), &x, NULL);
|
|
x += ls->xText;
|
|
}
|
|
|
|
parent->PopupMenu(&menu, x, ls->y + (mIconHeight / 2) - 1);
|
|
}
|
|
|
|
// it's an invalid dragging event
|
|
SetWrongDragging(true);
|
|
}
|
|
|
|
void LabelTrack::OnContextMenu(wxCommandEvent & evt)
|
|
{
|
|
AudacityProject *p = GetActiveProject();
|
|
|
|
switch (evt.GetId())
|
|
{
|
|
/// Cut selected text if cut menu item is selected
|
|
case OnCutSelectedTextID:
|
|
if (CutSelectedText())
|
|
{
|
|
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(p->GetSel0(), p->GetSel1()))
|
|
{
|
|
p->PushState(_("Modified Label"),
|
|
_("Label Edit"),
|
|
UndoPush::CONSOLIDATE);
|
|
}
|
|
break;
|
|
|
|
/// DELETE selected label
|
|
case OnDeleteSelectedLabelID: {
|
|
int ndx = GetLabelIndex(p->GetSel0(), p->GetSel1());
|
|
if (ndx != -1)
|
|
{
|
|
DeleteLabel(ndx);
|
|
p->PushState(_("Deleted Label"),
|
|
_("Label Edit"),
|
|
UndoPush::CONSOLIDATE);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case OnEditSelectedLabelID: {
|
|
int ndx = GetLabelIndex(p->GetSel0(), p->GetSel1());
|
|
if (ndx != -1)
|
|
p->DoEditLabels(this, ndx);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void LabelTrack::RemoveSelectedText()
|
|
{
|
|
wxString left = wxT("");
|
|
wxString right = wxT("");
|
|
|
|
if (mInitialCursorPos > mCurrentCursorPos) {
|
|
int temp = mCurrentCursorPos;
|
|
mCurrentCursorPos = mInitialCursorPos;
|
|
mInitialCursorPos = temp;
|
|
}
|
|
|
|
if (mInitialCursorPos > 0) {
|
|
left = (mLabels[mSelIndex]->title).Mid(0, mInitialCursorPos);
|
|
}
|
|
if (mCurrentCursorPos < (int)(mLabels[mSelIndex]->title).Length()) {
|
|
right = (mLabels[mSelIndex]->title).Mid(mCurrentCursorPos, (mLabels[mSelIndex]->title).Length()-mCurrentCursorPos);
|
|
}
|
|
mLabels[mSelIndex]->title = left + right;
|
|
mCurrentCursorPos = left.Length();
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
mLabels[mSelIndex]->highlighted = false;
|
|
mDragXPos = -1;
|
|
}
|
|
|
|
void LabelTrack::Unselect()
|
|
{
|
|
mSelIndex = -1;
|
|
}
|
|
|
|
bool LabelTrack::IsSelected() const
|
|
{
|
|
return (mSelIndex >= 0 && mSelIndex < (int)mLabels.Count());
|
|
}
|
|
|
|
/// Export labels including label start and end-times.
|
|
void LabelTrack::Export(wxTextFile & f) const
|
|
{
|
|
// PRL: to do: export other selection fields
|
|
for (int i = 0; i < (int)mLabels.Count(); i++) {
|
|
f.AddLine(wxString::Format(wxT("%f\t%f\t%s"),
|
|
(double)mLabels[i]->getT0(),
|
|
(double)mLabels[i]->getT1(),
|
|
mLabels[i]->title.c_str()));
|
|
}
|
|
}
|
|
|
|
/// Import labels, handling files with or without end-times.
|
|
void LabelTrack::Import(wxTextFile & in)
|
|
{
|
|
wxString currentLine;
|
|
int i, i2,len;
|
|
int index, lines;
|
|
wxString s,s1;
|
|
wxString title;
|
|
double t0,t1;
|
|
|
|
lines = in.GetLineCount();
|
|
|
|
mLabels.Clear();
|
|
mLabels.Alloc(lines);
|
|
|
|
//Currently, we expect a tag file to have two values and a label
|
|
//on each line. If the second token is not a number, we treat
|
|
//it as a single-value label.
|
|
for (index = 0; index < lines; index++) {
|
|
currentLine = in.GetLine(index);
|
|
|
|
len = currentLine.Length();
|
|
if (len == 0)
|
|
return;
|
|
|
|
//get the timepoint of the left edge of the label.
|
|
i = 0;
|
|
while (i < len && currentLine.GetChar(i) != wxT(' ')
|
|
&& currentLine.GetChar(i) != wxT('\t'))
|
|
{
|
|
i++;
|
|
}
|
|
s = currentLine.Left(i);
|
|
|
|
if (!Internat::CompatibleToDouble(s, &t0))
|
|
return;
|
|
|
|
//Increment one letter.
|
|
i++;
|
|
|
|
//Now, go until we find the start of the get the next token
|
|
while (i < len
|
|
&& (currentLine.GetChar(i) == wxT(' ')
|
|
|| currentLine.GetChar(i) == wxT('\t')))
|
|
{
|
|
i++;
|
|
}
|
|
//Keep track of the start of the second token
|
|
i2=i;
|
|
|
|
//Now, go to the end of the second token.
|
|
while (i < len && currentLine.GetChar(i) != wxT(' ')
|
|
&& currentLine.GetChar(i) != wxT('\t'))
|
|
{
|
|
i++;
|
|
}
|
|
|
|
//We are at the end of the second token.
|
|
s1 = currentLine.Mid(i2,i-i2+1).Strip(wxString::stripType(0x3));
|
|
if (!Internat::CompatibleToDouble(s1, &t1))
|
|
{
|
|
//s1 is not a number.
|
|
t1 = t0; //This is a one-sided label; t1 == t0.
|
|
|
|
//Because s1 is not a number, the label should be
|
|
//The rest of the line, starting at i2;
|
|
title = currentLine.Right(len - i2).Strip(wxString::stripType(0x3)); //0x3 indicates both
|
|
}
|
|
else
|
|
{
|
|
//s1 is a number, and it is stored correctly in t1.
|
|
//The title should be the remainder of the line,
|
|
//After we eat
|
|
|
|
//Get rid of spaces at either end
|
|
title = currentLine.Right(len - i).Strip(wxString::stripType(0x3)); //0x3 indicates both.
|
|
|
|
}
|
|
// PRL: to do: import other selection fields
|
|
LabelStruct *l = new LabelStruct(SelectedRegion(t0, t1), title);
|
|
mLabels.Add(l);
|
|
}
|
|
SortLabels();
|
|
}
|
|
|
|
bool LabelTrack::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
|
|
{
|
|
if (!wxStrcmp(tag, wxT("label"))) {
|
|
|
|
SelectedRegion selectedRegion;
|
|
wxString title;
|
|
|
|
// loop through attrs, which is a null-terminated list of
|
|
// attribute-value pairs
|
|
while(*attrs) {
|
|
const wxChar *attr = *attrs++;
|
|
const wxChar *value = *attrs++;
|
|
|
|
if (!value)
|
|
break;
|
|
|
|
const wxString strValue = value;
|
|
if (!XMLValueChecker::IsGoodString(strValue))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (selectedRegion.HandleXMLAttribute(attr, value, wxT("t"), wxT("t1")))
|
|
;
|
|
else if (!wxStrcmp(attr, wxT("title")))
|
|
title = strValue;
|
|
|
|
} // while
|
|
|
|
// Handle files created by Audacity 1.1. Labels in Audacity 1.1
|
|
// did not have separate start- and end-times.
|
|
// PRL: this superfluous now, given class SelectedRegion's internal
|
|
// consistency guarantees
|
|
//if (selectedRegion.t1() < 0)
|
|
// selectedRegion.collapseToT0();
|
|
|
|
LabelStruct *l = new LabelStruct(selectedRegion, title);
|
|
mLabels.Add(l);
|
|
|
|
return true;
|
|
}
|
|
else if (!wxStrcmp(tag, wxT("labeltrack"))) {
|
|
long nValue = -1;
|
|
while (*attrs) {
|
|
const wxChar *attr = *attrs++;
|
|
const wxChar *value = *attrs++;
|
|
|
|
if (!value)
|
|
return true;
|
|
|
|
const wxString strValue = value;
|
|
if (!wxStrcmp(attr, wxT("name")) && XMLValueChecker::IsGoodString(strValue))
|
|
mName = strValue;
|
|
else if (!wxStrcmp(attr, wxT("numlabels")) &&
|
|
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
|
|
{
|
|
if (nValue < 0)
|
|
{
|
|
wxLogWarning(wxT("Project shows negative number of labels: %d"), nValue);
|
|
return false;
|
|
}
|
|
mLabels.Clear();
|
|
mLabels.Alloc(nValue);
|
|
}
|
|
else if (!wxStrcmp(attr, wxT("height")) &&
|
|
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
|
|
SetHeight(nValue);
|
|
else if (!wxStrcmp(attr, wxT("minimized")) &&
|
|
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
|
|
SetMinimized(nValue != 0);
|
|
else if (!wxStrcmp(attr, wxT("isSelected")) &&
|
|
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
|
|
this->SetSelected(nValue != 0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
XMLTagHandler *LabelTrack::HandleXMLChild(const wxChar *tag)
|
|
{
|
|
if (!wxStrcmp(tag, wxT("label")))
|
|
return this;
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
void LabelTrack::WriteXML(XMLWriter &xmlFile)
|
|
{
|
|
int len = mLabels.Count();
|
|
int i;
|
|
|
|
xmlFile.StartTag(wxT("labeltrack"));
|
|
xmlFile.WriteAttr(wxT("name"), mName);
|
|
xmlFile.WriteAttr(wxT("numlabels"), len);
|
|
xmlFile.WriteAttr(wxT("height"), this->GetActualHeight());
|
|
xmlFile.WriteAttr(wxT("minimized"), this->GetMinimized());
|
|
xmlFile.WriteAttr(wxT("isSelected"), this->GetSelected());
|
|
|
|
for (i = 0; i < len; i++) {
|
|
xmlFile.StartTag(wxT("label"));
|
|
mLabels[i]->getSelectedRegion()
|
|
.WriteXMLAttributes(xmlFile, wxT("t"), wxT("t1"));
|
|
// PRL: to do: write other selection fields
|
|
xmlFile.WriteAttr(wxT("title"), mLabels[i]->title);
|
|
xmlFile.EndTag(wxT("label"));
|
|
}
|
|
|
|
xmlFile.EndTag(wxT("labeltrack"));
|
|
}
|
|
|
|
#if LEGACY_PROJECT_FILE_SUPPORT
|
|
bool LabelTrack::Load(wxTextFile * in, DirManager * dirManager)
|
|
{
|
|
if (in->GetNextLine() != wxT("NumMLabels"))
|
|
return false;
|
|
|
|
unsigned long len;
|
|
if (!(in->GetNextLine().ToULong(&len)))
|
|
return false;
|
|
|
|
unsigned int i;
|
|
for (i = 0; i < mLabels.Count(); i++)
|
|
delete mLabels[i];
|
|
mLabels.Clear();
|
|
mLabels.Alloc(len);
|
|
|
|
for (i = 0; i < len; i++) {
|
|
LabelStruct *l = new LabelStruct();
|
|
double t0;
|
|
if (!Internat::CompatibleToDouble(in->GetNextLine(), &t0))
|
|
return false;
|
|
l->selectedRegion.setT0(t0, false);
|
|
// Legacy file format does not include label end-times.
|
|
l->selectedRegion.collapseToT0();
|
|
// PRL: nothing NEW to do, legacy file support
|
|
l->title = in->GetNextLine();
|
|
mLabels.Add(l);
|
|
}
|
|
|
|
if (in->GetNextLine() != wxT("MLabelsEnd"))
|
|
return false;
|
|
SortLabels();
|
|
return true;
|
|
}
|
|
|
|
bool LabelTrack::Save(wxTextFile * out, bool overwrite)
|
|
{
|
|
out->AddLine(wxT("NumMLabels"));
|
|
int len = mLabels.Count();
|
|
out->AddLine(wxString::Format(wxT("%d"), len));
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
out->AddLine(wxString::Format(wxT("%lf"), mLabels[i]->selectedRegion.mT0));
|
|
out->AddLine(mLabels[i]->title);
|
|
}
|
|
out->AddLine(wxT("MLabelsEnd"));
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
Track::Holder LabelTrack::Cut(double t0, double t1)
|
|
{
|
|
auto tmp = Copy(t0, t1);
|
|
if (!tmp)
|
|
return{};
|
|
if (!Clear(t0, t1))
|
|
return{};
|
|
|
|
return std::move(tmp);
|
|
}
|
|
|
|
#if 0
|
|
Track::Holder LabelTrack::SplitCut(double t0, double t1)
|
|
{
|
|
// SplitCut() == Copy() + SplitDelete()
|
|
|
|
Track::Holder tmp = Copy(t0, t1);
|
|
if (!tmp)
|
|
return {};
|
|
if (!SplitDelete(t0, t1))
|
|
return {};
|
|
|
|
return std::move(tmp);
|
|
}
|
|
#endif
|
|
|
|
Track::Holder LabelTrack::Copy(double t0, double t1) const
|
|
{
|
|
auto tmp = std::make_unique<LabelTrack>(GetDirManager());
|
|
const auto lt = static_cast<LabelTrack*>(tmp.get());
|
|
int len = mLabels.Count();
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
LabelStruct::TimeRelations relation =
|
|
mLabels[i]->RegionRelation(t0, t1, this);
|
|
if (relation == LabelStruct::SURROUNDS_LABEL) {
|
|
const LabelStruct &label = *mLabels[i];
|
|
LabelStruct *l =
|
|
new LabelStruct(label.selectedRegion,
|
|
label.getT0() - t0,
|
|
label.getT1() - t0,
|
|
label.title);
|
|
lt->mLabels.Add(l);
|
|
}
|
|
else if (relation == LabelStruct::WITHIN_LABEL) {
|
|
const LabelStruct &label = *mLabels[i];
|
|
LabelStruct *l =
|
|
new LabelStruct(label.selectedRegion, 0, t1-t0,
|
|
label.title);
|
|
lt->mLabels.Add(l);
|
|
}
|
|
else if (relation == LabelStruct::BEGINS_IN_LABEL) {
|
|
const LabelStruct &label = *mLabels[i];
|
|
LabelStruct *l =
|
|
new LabelStruct(label.selectedRegion,
|
|
0,
|
|
label.getT1() - t0,
|
|
label.title);
|
|
lt->mLabels.Add(l);
|
|
}
|
|
else if (relation == LabelStruct::ENDS_IN_LABEL) {
|
|
const LabelStruct &label = *mLabels[i];
|
|
LabelStruct *l =
|
|
new LabelStruct(label.selectedRegion,
|
|
label.getT0() - t0,
|
|
t1 - t0,
|
|
label.title);
|
|
lt->mLabels.Add(l);
|
|
}
|
|
}
|
|
lt->mClipLen = (t1 - t0);
|
|
|
|
return std::move(tmp);
|
|
}
|
|
|
|
|
|
bool LabelTrack::PasteOver(double t, const Track * src)
|
|
{
|
|
if (src->GetKind() != Track::Label)
|
|
return false;
|
|
|
|
int len = mLabels.Count();
|
|
int pos = 0;
|
|
|
|
while (pos < len && mLabels[pos]->getT0() < t)
|
|
pos++;
|
|
|
|
LabelTrack *sl = (LabelTrack *) src;
|
|
for (unsigned int j = 0; j < sl->mLabels.Count(); j++) {
|
|
const LabelStruct &label = *sl->mLabels[j];
|
|
LabelStruct *l =
|
|
new LabelStruct(label.selectedRegion,
|
|
label.getT0() + t,
|
|
label.getT1() + t,
|
|
label.title);
|
|
mLabels.Insert(l, pos++);
|
|
len++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool LabelTrack::Paste(double t, const Track *src)
|
|
{
|
|
if (src->GetKind() != Track::Label)
|
|
return false;
|
|
|
|
LabelTrack *lt = (LabelTrack *)src;
|
|
|
|
double shiftAmt = lt->mClipLen > 0.0 ? lt->mClipLen : lt->GetEndTime();
|
|
|
|
ShiftLabelsOnInsert(shiftAmt, t);
|
|
return PasteOver(t, src);
|
|
}
|
|
|
|
// This repeats the labels in a time interval a specified number of times.
|
|
bool LabelTrack::Repeat(double t0, double t1, int n)
|
|
{
|
|
// Sanity-check the arguments
|
|
if (n < 0 || t1 < t0)
|
|
return false;
|
|
|
|
double tLen = t1 - t0;
|
|
|
|
// Insert space for the repetitions
|
|
ShiftLabelsOnInsert(tLen * n, t1);
|
|
|
|
for (unsigned int i = 0; i < mLabels.GetCount(); i++)
|
|
{
|
|
LabelStruct::TimeRelations relation =
|
|
mLabels[i]->RegionRelation(t0, t1, this);
|
|
if (relation == LabelStruct::SURROUNDS_LABEL)
|
|
{
|
|
// Label is completely inside the selection; duplicate it in each
|
|
// repeat interval
|
|
unsigned int pos = i; // running label insertion position in mLabels
|
|
|
|
for (int j = 1; j <= n; j++)
|
|
{
|
|
const LabelStruct &label = *mLabels[i];
|
|
LabelStruct *l =
|
|
new LabelStruct(label.selectedRegion,
|
|
label.getT0() + j * tLen,
|
|
label.getT1() + j * tLen,
|
|
label.title);
|
|
|
|
// Figure out where to insert
|
|
while (pos < mLabels.Count() &&
|
|
mLabels[pos]->getT0() < l->getT0())
|
|
pos++;
|
|
mLabels.Insert(l, pos);
|
|
}
|
|
}
|
|
else if (relation == LabelStruct::BEGINS_IN_LABEL)
|
|
{
|
|
// Label ends inside the selection; ShiftLabelsOnInsert() hasn't touched
|
|
// it, and we need to extend it through to the last repeat interval
|
|
mLabels[i]->selectedRegion.moveT1(n * tLen);
|
|
}
|
|
|
|
// Other cases have already been handled by ShiftLabelsOnInsert()
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool LabelTrack::Silence(double t0, double t1)
|
|
{
|
|
int len = mLabels.Count();
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
LabelStruct::TimeRelations relation =
|
|
mLabels[i]->RegionRelation(t0, t1, this);
|
|
if (relation == LabelStruct::WITHIN_LABEL)
|
|
{
|
|
// Split label around the selection
|
|
const LabelStruct &label = *mLabels[i];
|
|
LabelStruct *l =
|
|
new LabelStruct(label.selectedRegion, t1, label.getT1(),
|
|
label.title);
|
|
|
|
mLabels[i]->selectedRegion.setT1(t0);
|
|
|
|
// This might not be the right place to insert, but we sort at the end
|
|
++i;
|
|
mLabels.Insert(l, i);
|
|
}
|
|
else if (relation == LabelStruct::ENDS_IN_LABEL)
|
|
{
|
|
// Beginning of label to selection end
|
|
mLabels[i]->selectedRegion.setT0(t1);
|
|
}
|
|
else if (relation == LabelStruct::BEGINS_IN_LABEL)
|
|
{
|
|
// End of label to selection beginning
|
|
mLabels[i]->selectedRegion.setT1(t0);
|
|
}
|
|
else if (relation == LabelStruct::SURROUNDS_LABEL)
|
|
{
|
|
DeleteLabel( i );
|
|
len--;
|
|
i--;
|
|
}
|
|
}
|
|
|
|
SortLabels();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool LabelTrack::InsertSilence(double t, double len)
|
|
{
|
|
int numLabels = mLabels.Count();
|
|
|
|
for (int i = 0; i < numLabels; i++) {
|
|
double t0 = mLabels[i]->getT0();
|
|
double t1 = mLabels[i]->getT1();
|
|
if (t0 >= t)
|
|
t0 += len;
|
|
|
|
if (t1 >= t)
|
|
t1 += len;
|
|
mLabels[i]->selectedRegion.setTimes(t0, t1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int LabelTrack::GetNumLabels() const
|
|
{
|
|
return mLabels.Count();
|
|
}
|
|
|
|
const LabelStruct *LabelTrack::GetLabel(int index) const
|
|
{
|
|
return mLabels[index];
|
|
}
|
|
|
|
int LabelTrack::GetLabelIndex(double t, double t1)
|
|
{
|
|
LabelStruct *l;
|
|
|
|
int len = mLabels.Count();
|
|
int i;
|
|
//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;
|
|
for( i=0;i<len;i++)
|
|
{
|
|
l = mLabels[i];
|
|
if( fabs( l->getT0() - t ) > delta )
|
|
continue;
|
|
if( fabs( l->getT1() - t1 ) > delta )
|
|
continue;
|
|
return i;
|
|
}
|
|
|
|
return wxNOT_FOUND;
|
|
}
|
|
|
|
int LabelTrack::AddLabel(const SelectedRegion &selectedRegion,
|
|
const wxString &title, int restoreFocus)
|
|
{
|
|
LabelStruct *l = new LabelStruct(selectedRegion, title);
|
|
mCurrentCursorPos = title.length();
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
|
|
int len = mLabels.Count();
|
|
int pos = 0;
|
|
|
|
while (pos < len && mLabels[pos]->getT0() < selectedRegion.t0())
|
|
pos++;
|
|
|
|
mLabels.Insert(l, pos);
|
|
|
|
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;
|
|
|
|
return pos;
|
|
}
|
|
|
|
void LabelTrack::DeleteLabel(int index)
|
|
{
|
|
wxASSERT((index < (int)mLabels.GetCount()));
|
|
delete mLabels[index];
|
|
mLabels.RemoveAt(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--;
|
|
}
|
|
}
|
|
|
|
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':
|
|
/// <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 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<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
|
|
mBoundaryGlyphs[index].SetMask(new 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;
|
|
}
|
|
|
|
/// Returns true for keys we capture to start a label.
|
|
bool LabelTrack::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.
|
|
bool LabelTrack::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);
|
|
}
|
|
|
|
/// 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()
|
|
{
|
|
int i,j;
|
|
LabelStruct * pTemp;
|
|
for (i = 1; i < (int)mLabels.Count(); i++)
|
|
{
|
|
j=i-1;
|
|
while( (j>=0) && (mLabels[j]->getT0() >
|
|
mLabels[i]->getT0()) )
|
|
{
|
|
j--;
|
|
}
|
|
j++;
|
|
if( j<i)
|
|
{
|
|
// Remove at i and insert at j.
|
|
// Don't use DeleteLabel() since just moving it.
|
|
pTemp = mLabels[i];
|
|
mLabels.RemoveAt( i );
|
|
mLabels.Insert(pTemp, j);
|
|
|
|
// Various indecese need to be updated with the moved items...
|
|
if( mMouseOverLabelLeft <=i )
|
|
{
|
|
if( mMouseOverLabelLeft == i )
|
|
mMouseOverLabelLeft=j;
|
|
else if( mMouseOverLabelLeft >= j)
|
|
mMouseOverLabelLeft++;
|
|
}
|
|
if( mMouseOverLabelRight <=i )
|
|
{
|
|
if( mMouseOverLabelRight == i )
|
|
mMouseOverLabelRight=j;
|
|
else if( mMouseOverLabelRight >= j)
|
|
mMouseOverLabelRight++;
|
|
}
|
|
if( mSelIndex <=i )
|
|
{
|
|
if( mSelIndex == i )
|
|
mSelIndex=j;
|
|
else if( mSelIndex >= j)
|
|
mSelIndex++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
wxString LabelTrack::GetTextOfLabels(double t0, double t1) const
|
|
{
|
|
bool firstLabel = true;
|
|
wxString retVal;
|
|
|
|
for (unsigned int i=0; i < mLabels.GetCount(); ++i)
|
|
{
|
|
if (mLabels[i]->getT0() >= t0 &&
|
|
mLabels[i]->getT1() <= t1)
|
|
{
|
|
if (!firstLabel)
|
|
retVal += '\t';
|
|
firstLabel = false;
|
|
retVal += mLabels[i]->title;
|
|
}
|
|
}
|
|
|
|
return retVal;
|
|
}
|