audacia/src/LabelTrack.cpp

2554 lines
74 KiB
C++
Raw Normal View History

/**********************************************************************
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 <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 "LabelTrack.h"
#include "DirManager.h"
#include "Internat.h"
#include "Prefs.h"
#include "Theme.h"
#include "AllThemeResources.h"
#include "AColor.h"
#include "Project.h"
#include "commands/CommandManager.h"
#include "CaptureEvents.h"
#include "effects/TimeWarper.h"
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 *TrackFactory::NewLabelTrack()
{
return new LabelTrack(mDirManager);
}
LabelTrack::LabelTrack(DirManager * projDirManager):
Track(projDirManager),
mbHitCenter(false),
mOldEdge(-1),
mSelIndex(-1),
mMouseOverLabelLeft(-1),
mMouseOverLabelRight(-1),
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),
mIsAdjustingLabel(false)
{
int len = orig.mLabels.Count();
for (int i = 0; i < len; i++) {
LabelStruct *l = new LabelStruct();
l->t = orig.mLabels[i]->t;
l->t1 = orig.mLabels[i]->t1;
l->title = orig.mLabels[i]->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]->t += dOffset;
mLabels[i]->t1 += dOffset;
}
}
void LabelTrack::ShiftLabelsOnClear(double b, double e)
{
for (size_t i=0;i<mLabels.GetCount();i++){
if (mLabels[i]->t >= e){//label is after deletion region
mLabels[i]->t = mLabels[i]->t - (e-b);
mLabels[i]->t1 = mLabels[i]->t1 - (e-b);
}else if (mLabels[i]->t >= b && mLabels[i]->t1 <= e){//deletion region encloses label
DeleteLabel( i );
i--;
}else if (mLabels[i]->t >= b && mLabels[i]->t1 > e){//deletion region covers start
mLabels[i]->t = b;
mLabels[i]->t1 = mLabels[i]->t1 - (e - mLabels[i]->t);
}else if (mLabels[i]->t < b && mLabels[i]->t1 > b && mLabels[i]->t1 <= e){//deletion regions covers end
mLabels[i]->t1 = b;
}else if (mLabels[i]->t < b && mLabels[i]->t1 > e){//label encloses deletion region
mLabels[i]->t1 = mLabels[i]->t1 - (e-b);
}else if (mLabels[i]->t1 <= b){
//nothing
}
}
}
//used when we want to use clear only on the labels
void LabelTrack::ChangeLabelsOnClear(double b, double e)
{
for (size_t i=0;i<mLabels.GetCount();i++) {
if (mLabels[i]->t >= b && mLabels[i]->t1 <= e){//deletion region encloses label
DeleteLabel(i);
i--;
}else if (mLabels[i]->t < b && mLabels[i]->t1 > e){//label encloses deletion region
mLabels[i]->t1 = mLabels[i]->t1 - (e-b);
}else if (mLabels[i]->t <= e && mLabels[i]->t1 > e){//deletion region covers label start
mLabels[i]->t = e;
}else if (mLabels[i]->t < b && mLabels[i]->t1 >= b){//deletion regions covers label end
mLabels[i]->t1 = b;
}
}
}
void LabelTrack::ShiftLabelsOnInsert(double length, double pt)
{
for (unsigned int i=0;i<mLabels.GetCount();i++){
// label is after the insert point
if (mLabels[i]->t >= pt) {
mLabels[i]->t = mLabels[i]->t + length;
mLabels[i]->t1 = mLabels[i]->t1 + length;
// label is before the insert point
}else if (mLabels[i]->t1 <= pt) {
//nothing
// insert point is inside the label
}else{
mLabels[i]->t1 = mLabels[i]->t1 + length;
}
}
}
void LabelTrack::ChangeLabelsOnReverse(double b, double e)
{
for (size_t i=0; i<mLabels.GetCount(); i++) {
if (mLabels[i]->t >= b && mLabels[i]->t1 <= e) {//deletion region encloses label
double aux = b + (e - mLabels[i]->t1);
mLabels[i]->t1 = e - (mLabels[i]->t - b);
mLabels[i]->t = aux;
}
}
}
void LabelTrack::ScaleLabels(double b, double e, double change)
{
for (unsigned int i=0;i<mLabels.GetCount();i++){
mLabels[i]->t = AdjustTimeStampOnScale(mLabels[i]->t, b, e, change);
mLabels[i]->t1 = AdjustTimeStampOnScale(mLabels[i]->t1, 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) {
double &labelT0 = mLabels[i]->t; labelT0 = warper.Warp(labelT0);
double &labelT1 = mLabels[i]->t1; labelT1 = warper.Warp(labelT1);
}
}
void LabelTrack::ResetFlags()
{
mMouseXPos = -1;
mXPos1 = -1;
mXPos2 = -1;
mDragXPos = -1;
mInitialCursorPos = 1;
mCurrentCursorPos = 1;
mResetCursorPos = false;
mRightDragging = false;
mDrawCursor = false;
}
void LabelTrack::ResetFont()
{
mFontHeight = -1;
wxString facename = gPrefs->Read(wxT("/GUI/LabelFontFacename"), wxT(""));
int size = gPrefs->Read(wxT("/GUI/LabelFontSize"), 12);
if (facename != wxT("")) {
msFont = wxFont(size, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL,
wxFONTWEIGHT_NORMAL, FALSE, facename,
wxFONTENCODING_SYSTEM);
}
else {
msFont = wxFont(size, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL,
wxFONTWEIGHT_NORMAL);
}
}
/// 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)
{
// 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, double h, double pps)
{
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 = r.x -(int)(h*pps) -100;
for(i=0;i<MAX_NUM_ROWS;i++)
xUsed[i]=xStart;
int nRowsUsed=0;
for (i = 0; i < (int)mLabels.Count(); i++)
{
int x = r.x + (int) ((mLabels[i]->t - h) * pps);
int x1 = r.x + (int) ((mLabels[i]->t1 - h) * pps);
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()
{
changeInitialMouseXPos = true;
highlighted = false;
updated = false;
t = 0.0;
t1 = 0.0;
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)
{
// 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 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)
{
//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)
{
//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)
{
*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(wxWindow * parent, int * x)
{
if (mSelIndex >= 0) {
wxClientDC dc(parent);
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, double h, double pps,
double sel0, double sel1)
{
if(msFont.Ok())
dc.SetFont(msFont);
if (mFontHeight == -1)
calculateFontHeight(dc);
double right = h + r.width / pps;
double dsel0 = sel0;
if (dsel0 < h)
dsel0 = h;
if (dsel0 > right)
dsel0 = right;
double dsel1 = sel1;
if (dsel1 < h)
dsel1 = h;
if (dsel1 > right)
dsel1 = right;
wxRect before = r;
before.width = int ((dsel0 - h) * pps);
dc.SetBrush(AColor::labelUnselectedBrush);
dc.SetPen(AColor::labelUnselectedPen);
dc.DrawRectangle(before);
wxRect selr = r;
selr.x += before.width;
selr.width = int ((dsel1 - dsel0) * pps);
dc.SetBrush(AColor::labelSelectedBrush);
dc.SetPen(AColor::labelSelectedPen);
dc.DrawRectangle(selr);
wxRect after = r;
after.x += (before.width + selr.width);
after.width -= (before.width + selr.width);
if (after.x < r.x)
{
after.width -= (r.x - after.x);
after.x = r.x;
}
dc.SetBrush(AColor::labelUnselectedBrush);
dc.SetPen(AColor::labelUnselectedPen);
dc.DrawRectangle(after);
int i;
#ifdef __WXMAC__
long textWidth, textHeight;
#else
int textWidth, textHeight;
#endif
// 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, h , pps );
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(dc, 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(dc, (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(wxDC & dc, int xPos)
{
// 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)
{
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()) {
#if defined(__WXGTK__) && defined(HAVE_GTK)
CaptureEvents capture;
#endif
wxTheClipboard->SetData(new 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()) {
#if defined(__WXGTK__) && defined(HAVE_GTK)
CaptureEvents capture;
#endif
wxTheClipboard->SetData(new wxTextDataObject(data));
wxTheClipboard->Close();
}
return true;
}
/// 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(sel0, sel1, wxT(""));
}
wxString text;
wxString left=wxT("");
wxString right=wxT("");
// if text data is available
if (IsTextClipSupported()) {
if (wxTheClipboard->Open()) {
#if defined(__WXGTK__) && HAVE_GTK
CaptureEvents capture;
#endif
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()
{
#if defined(__WXGTK__) && defined(HAVE_GTK)
CaptureEvents capture;
#endif
return wxTheClipboard->IsSupported(wxDF_TEXT);
}
double LabelTrack::GetStartTime()
{
int len = mLabels.Count();
if (len == 0)
return 0.0;
else
return mLabels[0]->t;
}
double LabelTrack::GetEndTime()
{
//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++)
{
if(mLabels[i]->t1 > end)
end = mLabels[i]->t1;
}
return end;
}
/// 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;
mInBox = false; // to disable the dragging for selecting the text in text box
}
// 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;
mInBox = false; // to disable the dragging for selecting the text in text box
}
// give text box better priority for selecting
if(OverTextBox(pLabel, x, y))
{
result = 0;
}
}
return result;
}
// return true if the mouse is over text box, false otherwise
bool LabelTrack::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;
}
/// HandleMouse gets called with every mouse move or click.
///
bool LabelTrack::HandleMouse(const wxMouseEvent & evt,
wxRect & r, double h, double pps,
double *newSel0, double *newSel1)
{
if(evt.LeftUp())
{
bool lupd = false, rupd = false;
if(mIsAdjustingLabel) {
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 dragging happens in text box
if (mInBox) {
// 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;
}
}
//If we are currently adjusting a label,
//just reset its value and redraw.
if(mIsAdjustingLabel ) // This guard is necessary but hides another bug. && mSelIndex != -1)
{
// LL: Constrain to inside track rectangle for now. Should be changed
// to allow scrolling while dragging labels
int x = evt.m_x + mxMouseDisplacement - r.x;
if (x < 0) {
x = 0;
}
else if (x > r.width - 2) {
x = r.width - 2;
}
double width;
bool bSameLabel = mMouseOverLabelLeft==mMouseOverLabelRight;
//Adjust boundary and make sure that t < t1 on any dragged labels.
//This code pushes both of them in one direction, instead of swapping
//bounds like happens for the selection region.
//If button is down, then we preserve the label width.
if(mMouseOverLabelLeft>=0)
{
width = mLabels[mMouseOverLabelLeft]->t1 - mLabels[mMouseOverLabelLeft]->t;
mLabels[mMouseOverLabelLeft]->t = h + x / pps;
if( evt.ShiftDown() || bSameLabel )
{
mLabels[mMouseOverLabelLeft]->t1 = mLabels[mMouseOverLabelLeft]->t+width;
}
else if( mLabels[mMouseOverLabelLeft]->t > mLabels[mMouseOverLabelLeft]->t1)
{
mLabels[mMouseOverLabelLeft]->t1 = mLabels[mMouseOverLabelLeft]->t;
}
mLabels[mMouseOverLabelLeft]->updated = true;
}
if( (mMouseOverLabelRight>=0) && !bSameLabel )
{
width = mLabels[mMouseOverLabelRight]->t1 - mLabels[mMouseOverLabelRight]->t;
mLabels[mMouseOverLabelRight]->t1 = h + x / pps;
if( evt.ShiftDown() )
{
mLabels[mMouseOverLabelRight]->t = mLabels[mMouseOverLabelRight]->t1-width;
}
else if( mLabels[mMouseOverLabelRight]->t > mLabels[mMouseOverLabelRight]->t1)
{
mLabels[mMouseOverLabelRight]->t = mLabels[mMouseOverLabelRight]->t1;
}
mLabels[mMouseOverLabelRight]->updated = true;
}
if( mSelIndex >=0 )
{
//Set the selection region to be equal to
//the new size of the label.
*newSel0 = mLabels[mSelIndex]->t;
*newSel1 = mLabels[mSelIndex]->t1;
}
SortLabels();
}
return false;
}
if( evt.ButtonDown())
{
//OverGlyph sets mMouseOverLabel to be the chosen label.
mIsAdjustingLabel = OverGlyph(evt.m_x, evt.m_y) != 0;
// reset mouseXPos if the mouse is pressed in the text box
mMouseXPos = -1;
mInBox = false;
bool changeCursor = true;
// reset the highlight indicator
wxRect highlightedRect;
if (mSelIndex != -1) {
// 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;
}
if (mIsAdjustingLabel)
{
float t = 0.0;
// When we start dragging the label(s) we don't want them to jump.
// so we calculate the displacement of the mouse from the drag center
// and use that in subsequent dragging calculations. The mouse stays
// at the same relative displacement throughout dragging.
// However, if two label's edges are being dragged
// then the displacement is relative to the initial average
// position of them, and in that case there can be a jump of at most
// a few pixels to bring the two label boundaries to exactly the same
// position when we start dragging.
// Dragging of three label edges at the same time is not supported (yet).
if( (mMouseOverLabelRight >=0) &&
(mMouseOverLabelLeft >=0)
)
{
t = (mLabels[mMouseOverLabelRight]->t1+mLabels[mMouseOverLabelLeft]->t)/2.0f;
}
else if(mMouseOverLabelRight >=0)
{
t = mLabels[mMouseOverLabelRight]->t1;
}
else if(mMouseOverLabelLeft >=0)
{
t = mLabels[mMouseOverLabelLeft]->t;
}
mxMouseDisplacement = (int)((((t-h) * pps) + r.x )-evt.m_x);
return false;
}
// disable displaying if left button is down
if (evt.LeftDown())
mDragXPos = -1;
// disable displaying if right button is down outside text box
if(mSelIndex != -1)
{
if (evt.RightDown())
{
if (!highlightedRect.Contains(evt.m_x, evt.m_y))
{
mDragXPos = -1;
}
else
// if it's in text box, don't need to reset the current cursor position
changeCursor = false;
}
}
mSelIndex = -1;
LabelStruct * pLabel;
for (int i = 0; i < (int)mLabels.Count(); i++) {
pLabel = mLabels[i];
if(OverTextBox(pLabel, evt.m_x, evt.m_y))
{
mSelIndex = i;
*newSel0 = mLabels[i]->t;
*newSel1 = mLabels[i]->t1;
// set mouseXPos to set current cursor position
if (changeCursor)
mMouseXPos = evt.m_x;
// set mInBox flag
mInBox = true;
return false;
}
}
}
return false;
}
// Check for keys that we will process
bool LabelTrack::CaptureKey(wxKeyEvent & event)
{
// Cache the keycode
int keyCode = event.GetKeyCode();
wxChar charCode = keyCode;
#if wxUSE_UNICODE
charCode = event.GetUnicodeKey();
#endif
if (mSelIndex >= 0) {
switch (keyCode)
{
case WXK_BACK:
case WXK_DELETE:
case WXK_HOME:
case WXK_END:
case WXK_LEFT:
case WXK_RIGHT:
case WXK_RETURN:
case WXK_NUMPAD_ENTER:
case WXK_ESCAPE:
case WXK_TAB:
return true;
break;
}
if( IsGoodLabelCharacter(keyCode, charCode) && !event.CmdDown()) {
return true;
}
}
else
{
if( IsGoodLabelFirstCharacter(keyCode, charCode) && !event.CmdDown() ){
AudacityProject * pProj = GetActiveProject();
CommandManager * cm = pProj->GetCommandManager();
if (pProj->GetAudioIOToken() > 0 && gAudioIO->IsStreamActive(pProj->GetAudioIOToken()) &&
cm && cm->GetKeyFromName(wxT("AddLabelPlaying")) == keyCode)
return false;
// IF Label already there, then don't add a new one on typing.
if( GetLabelIndex( pProj->mViewInfo.sel0, pProj->mViewInfo.sel1) != wxNOT_FOUND )
return false;
if( pProj )
pProj->OnAddLabel();
return true;
}
}
#if 0
if (IsGoodLabelFirstCharacter(keyCode, charCode) && !event.CmdDown()) {
return true;
}
#endif
return false;
}
/// KeyEvent is called for every keypress when over the label track.
bool LabelTrack::OnKeyDown(double & newSel0, double & newSel1, wxKeyEvent & event)
{
// Only track true changes to the label
bool updated = false;
// Cache the keycode
int keyCode = event.GetKeyCode();
wxChar charCode = keyCode;
#if wxUSE_UNICODE
charCode = event.GetUnicodeKey();
#endif
// 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:
{
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:
// Move cursor to beginning of label
mCurrentCursorPos = 0;
if (event.ShiftDown()) {
mDragXPos = 0;
}
else
{
mDragXPos = -1;
mInitialCursorPos = mCurrentCursorPos;
}
break;
case WXK_END:
// Move cursor to end of label
mCurrentCursorPos = (int)mLabels[mSelIndex]->title.length();
if (event.ShiftDown()) {
mDragXPos = 0;
}
else
{
mDragXPos = -1;
mInitialCursorPos = mCurrentCursorPos;
}
break;
case WXK_LEFT:
// Moving cursor left
if (mCurrentCursorPos > 0) {
mCurrentCursorPos--;
if (event.ShiftDown()) {
mDragXPos = 0;
}
else
{
mDragXPos = -1;
mInitialCursorPos = mCurrentCursorPos;
}
}
break;
case WXK_RIGHT:
// Moving cursor right
if (mCurrentCursorPos < (int)mLabels[mSelIndex]->title.length()) {
mCurrentCursorPos++;
if (event.ShiftDown()) {
mDragXPos = 0;
}
else
{
mDragXPos = -1;
mInitialCursorPos = mCurrentCursorPos;
}
}
break;
case WXK_RETURN:
case WXK_NUMPAD_ENTER:
case WXK_ESCAPE:
mSelIndex = -1;
break;
case WXK_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.
newSel0 = mLabels[mSelIndex]->t;
newSel1 = mLabels[mSelIndex]->t1;
}
else {
mSelIndex = -1;
}
break;
default:
event.Skip();
break;
}
}
else
{
switch (keyCode) {
case WXK_TAB:
if (!mLabels.IsEmpty()) {
if (event.ShiftDown()) {
mSelIndex = (int)mLabels.Count() - 1;
} else {
mSelIndex = 0;
}
mCurrentCursorPos = mLabels[mSelIndex]->title.Length();
//Set the selection region to be equal to the selection bounds of the tabbed-to label.
newSel0 = mLabels[mSelIndex]->t;
newSel1 = mLabels[mSelIndex]->t1;
}
break;
default:
event.Skip();
break;
}
}
// Make sure the caret is visible
mDrawCursor = true;
return updated;
}
/// KeyEvent is called for every keypress when over the label track.
bool LabelTrack::OnChar(double & newSel0, double & newSel1, wxKeyEvent & event)
{
// Only track true changes to the label
bool updated = false;
// Cache the keycode
int keyCode = event.GetKeyCode();
wxChar charCode = keyCode;
#if wxUSE_UNICODE
charCode = event.GetUnicodeKey();
#endif
if (mSelIndex >= 0) {
if (IsGoodLabelCharacter(keyCode, charCode)) {
// 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;
}
else
event.Skip();
}
else //if( !IsGoodLabelFirstCharacter(keyCode, charCode))
{
// Don't automatically start a label unless its one of the accepted
// characters.
// We can always create the label and then type the forbidden character
// as our first character.
event.Skip();
}
#if 0
else
{
// Create new label
wxString rightPart;
LabelStruct *l = new LabelStruct();
l->t = newSel0;
l->t1 = newSel1;
l->title += wxChar(charCode);
mCurrentCursorPos = 1;
mInitialCursorPos = mCurrentCursorPos;
int len = mLabels.Count();
int pos = 0;
while (pos < len && l->t > mLabels[pos]->t)
pos++;
mLabels.Insert(l, pos);
mSelIndex = pos;
updated = true;
}
#endif
// Make sure the caret is visible
mDrawCursor = true;
return updated;
}
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)
{
for (int i = 0; i < (int)mLabels.Count(); i++) {
f.AddLine(wxString::Format(wxT("%f\t%f\t%s"),
(double)mLabels[i]->t,
(double)mLabels[i]->t1,
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 t,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, &t))
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 = t; //This is a one-sided label; t1 == t.
//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.
}
LabelStruct *l = new LabelStruct();
l->t = t;
l->t1 = t1;
l->title = title;
mLabels.Add(l);
}
SortLabels();
}
bool LabelTrack::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
{
if (!wxStrcmp(tag, wxT("label"))) {
LabelStruct *l = new LabelStruct();
// loop through attrs, which is a null-terminated list of
// attribute-value pairs
bool has_t1 = false;
double dblValue;
while(*attrs) {
const wxChar *attr = *attrs++;
const wxChar *value = *attrs++;
if (!value)
break;
const wxString strValue = value;
if (!XMLValueChecker::IsGoodString(strValue))
{
delete l;
return false;
}
if (!wxStrcmp(attr, wxT("t")) && Internat::CompatibleToDouble(strValue, &dblValue))
l->t = dblValue;
else if (!wxStrcmp(attr, wxT("t1")) && Internat::CompatibleToDouble(strValue, &dblValue))
{
has_t1 = true;
l->t1 = dblValue;
}
else if (!wxStrcmp(attr, wxT("title")))
l->title = strValue;
} // while
// Handle files created by Audacity 1.1. Labels in Audacity 1.1
// did not have separate start- and end-times.
if (!has_t1)
l->t1 = l->t;
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);
}
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());
for (i = 0; i < len; i++) {
xmlFile.StartTag(wxT("label"));
xmlFile.WriteAttr(wxT("t"), mLabels[i]->t, 8);
xmlFile.WriteAttr(wxT("t1"), mLabels[i]->t1, 8);
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();
if (!Internat::CompatibleToDouble(in->GetNextLine(), &l->t))
return false;
// Legacy file format does not include label end-times.
l->t1 = l->t;
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]->t));
out->AddLine(mLabels[i]->title);
}
out->AddLine(wxT("MLabelsEnd"));
return true;
}
#endif
bool LabelTrack::Cut(double t0, double t1, Track ** dest)
{
*dest = new LabelTrack(GetDirManager());
int len = mLabels.Count();
for (int i = 0; i < len; i++) {
if (t0 <= mLabels[i]->t && mLabels[i]->t <= t1) {
mLabels[i]->t -= t0;
mLabels[i]->t1 -= t0;
((LabelTrack *) (*dest))->mLabels.Add(mLabels[i]);
//Don't use DeleteLabel() since we've only moved it.
mLabels.RemoveAt(i);
// JC: ALWAYS unselect, since RemoveAt renumbers labels.
mSelIndex=-1;
len--;
i--;
}
else if (mLabels[i]->t > t1) {
mLabels[i]->t -= (t1 - t0);
mLabels[i]->t1 -= (t1 - t0);
}
}
((LabelTrack *) (*dest))->mClipLen = (t1 - t0);
return true;
}
bool LabelTrack::Copy(double t0, double t1, Track ** dest)
{
*dest = new LabelTrack(GetDirManager());
int len = mLabels.Count();
for (int i = 0; i < len; i++) {
if (t0 <= mLabels[i]->t && mLabels[i]->t <= t1) {
LabelStruct *l = new LabelStruct();
l->t = mLabels[i]->t - t0;
l->t1 = mLabels[i]->t1 - t0;
l->title = mLabels[i]->title;
((LabelTrack *) (*dest))->mLabels.Add(l);
}
}
((LabelTrack *) (*dest))->mClipLen = (t1 - t0);
return true;
}
bool LabelTrack::Paste(double t, Track * src)
{
if (src->GetKind() != Track::Label)
return false;
int len = mLabels.Count();
int pos = 0;
while (pos < len && mLabels[pos]->t < t)
pos++;
LabelTrack *sl = (LabelTrack *) src;
for (unsigned int j = 0; j < sl->mLabels.Count(); j++) {
LabelStruct *l = new LabelStruct();
l->t = sl->mLabels[j]->t + t;
l->t1 = sl->mLabels[j]->t1 + t;
l->title = sl->mLabels[j]->title;
mLabels.Insert(l, pos++);
len++;
}
return true;
}
// This repeats the labels in a time interval a specified number of times.
// Like Paste(), it does not shift existing labels over
// - It assumes that you've already called ShiftLabelsOnInsert(), because
// sometimes with linking enabled that's hard to avoid.
// - It assumes that you inserted the necessary extra time at t1, not t0.
bool LabelTrack::Repeat(double t0, double t1, int n)
{
// Sanity-check the arguments
if (n < 0 || t1 < t0) return false;
double tLen = t1 - t0;
for (unsigned int i = 0; i < mLabels.GetCount(); i++)
{
if (mLabels[i]->t >= t0 && mLabels[i]->t <= t1 &&
mLabels[i]->t1 >= t0 && mLabels[i]->t1 <= t1)
{
// 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++)
{
LabelStruct *l = new LabelStruct();
l->t = mLabels[i]->t + j * tLen;
l->t1 = mLabels[i]->t1 + j * tLen;
l->title = mLabels[i]->title;
// Figure out where to insert
while (pos < mLabels.Count() && mLabels[pos]->t < l->t)
pos++;
mLabels.Insert(l, pos);
}
}
else if (mLabels[i]->t < t0 &&
mLabels[i]->t1 >= t0 && mLabels[i]->t1 <= t1)
{
// Label ends inside the selection; ShiftLabelsOnInsert() hasn't touched
// it, and we need to extend it through to the last repeat interval
mLabels[i]->t1 += n * tLen;
}
// Other cases have already been handled by ShiftLabelsOnInsert()
}
return true;
}
bool LabelTrack::Clear(double t0, double t1)
{
AudacityProject *p = GetActiveProject();
if (p && p->IsSticky()){
TrackGroupIterator grpIter(p->GetTracks());
Track *t = grpIter.First(this);
// If this track is part of a group, find a Wave track in the group and do
// the clear from there to ensure proper group behavior
while (t) {
if (t->GetKind() == Track::Wave) {
return t->Clear(t0, t1);
}
t = grpIter.Next();
}
// Fallback: shift labels in this track
ShiftLabelsOnClear(t0, t1);
}
else
ChangeLabelsOnClear(t0, t1);
return true;
}
bool LabelTrack::Silence(double t0, double t1)
{
int len = mLabels.Count();
for (int i = 0; i < len; i++) {
// If the label "surrounds" the selection, split it around the selection
if (mLabels[i]->t < t0 && t1 < mLabels[i]->t1)
{
LabelStruct *l = new LabelStruct();
l->t = t1;
l->t1 = mLabels[i]->t1;
l->title = mLabels[i]->title;
mLabels[i]->t1 = t0;
// This might not be the right place to insert, but we sort at the end
++i;
mLabels.Insert(l, i);
}
// If label begins in the selection, move the beginning to selection end
if (t0 <= mLabels[i]->t && mLabels[i]->t <= t1)
{
mLabels[i]->t = t1;
}
// If label ends in the selection, move the end to selection beginning
if (t0 <= mLabels[i]->t1 && mLabels[i]->t1 <= t1)
{
mLabels[i]->t1 = t0;
}
// Delete labels that were totally selected
if (mLabels[i]->t1 < mLabels[i]->t)
{
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++) {
if (mLabels[i]->t >= t)
mLabels[i]->t += len;
if (mLabels[i]->t1 >= t)
mLabels[i]->t1 += len;
}
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->t - t ) > delta )
continue;
if( fabs( l->t1 - t1 ) > delta )
continue;
return i;
}
return wxNOT_FOUND;
}
int LabelTrack::AddLabel(double t, double t1, const wxString &title)
{
LabelStruct *l = new LabelStruct();
l->t = t;
l->t1 = t1;
l->title = title;
mCurrentCursorPos = title.length();
mInitialCursorPos = mCurrentCursorPos;
int len = mLabels.Count();
int pos = 0;
while (pos < len && mLabels[pos]->t < t)
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;
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;
}
/// The 'typing at selection creates label' feature
/// will only accept these charCodes as a first label
/// character.
bool LabelTrack::IsGoodLabelFirstCharacter(int keyCode, wxChar charCode)
{
// The WXK_DELETE is because Windows doesn't convert DELETE to a proper
// unicode value. (Or at least one we can't use...0x46)
return (keyCode < WXK_START && !wxIscntrl(charCode) && (keyCode != WXK_SPACE) && (keyCode != WXK_DELETE)) ||
keyCode > WXK_COMMAND;
}
/// We'll only accept these characters within a label
bool LabelTrack::IsGoodLabelCharacter(int keyCode, wxChar charCode)
{
return (keyCode < WXK_START && !wxIscntrl(charCode)) ||
keyCode > WXK_COMMAND;
}
/// 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]->t > mLabels[i]->t) )
{
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)
{
bool firstLabel = true;
wxString retVal;
for (unsigned int i=0; i < mLabels.GetCount(); ++i)
{
if (mLabels[i]->t >= t0 && mLabels[i]->t1 <= t1)
{
if (!firstLabel)
retVal += '\t';
firstLabel = false;
retVal += mLabels[i]->title;
}
}
return retVal;
}
// Indentation settings for Vim and Emacs and unique identifier for Arch, a
// version control system. Please do not modify past this point.
//
// Local Variables:
// c-basic-offset: 3
// indent-tabs-mode: nil
// End:
//
// vim: et sts=3 sw=3
// arch-tag: f321cf69-6f22-4a1b-a44b-b70d533227e3