mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-06 23:02:42 +02:00
... but this brings back some "mutable" members, not in LabelTrack, but in LabelStruct. To make that go away, and still have the Draw function const, figure out how to update the label track layout at the right times, outside of the draw function.
2865 lines
82 KiB
C++
2865 lines
82 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)
|
|
{
|
|
for (auto &original: orig.mLabels) {
|
|
LabelStruct l { original.selectedRegion, original.title };
|
|
mLabels.push_back(l);
|
|
}
|
|
mSelIndex = orig.mSelIndex;
|
|
|
|
// reset flags
|
|
ResetFlags();
|
|
}
|
|
|
|
LabelTrack::~LabelTrack()
|
|
{
|
|
}
|
|
|
|
void LabelTrack::SetOffset(double dOffset)
|
|
{
|
|
for (auto &labelStruct: mLabels)
|
|
labelStruct.selectedRegion.move(dOffset);
|
|
}
|
|
|
|
bool LabelTrack::Clear(double b, double e)
|
|
{
|
|
// May delete labels, so use subscripts to iterate
|
|
for (size_t i = 0; i < mLabels.size(); ++i) {
|
|
auto &labelStruct = mLabels[i];
|
|
LabelStruct::TimeRelations relation =
|
|
labelStruct.RegionRelation(b, e, this);
|
|
if (relation == LabelStruct::BEFORE_LABEL)
|
|
labelStruct.selectedRegion.move(- (e-b));
|
|
else if (relation == LabelStruct::SURROUNDS_LABEL) {
|
|
DeleteLabel( i );
|
|
--i;
|
|
}
|
|
else if (relation == LabelStruct::ENDS_IN_LABEL)
|
|
labelStruct.selectedRegion.setTimes(
|
|
b,
|
|
labelStruct.getT1() - (e - b));
|
|
else if (relation == LabelStruct::BEGINS_IN_LABEL)
|
|
labelStruct.selectedRegion.setT1(b);
|
|
else if (relation == LabelStruct::WITHIN_LABEL)
|
|
labelStruct.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)
|
|
{
|
|
// May delete labels, so use subscripts to iterate
|
|
<<<<<<< HEAD
|
|
for (size_t i = 0, len = mLabels.size(); i < len; ++i) {
|
|
<<<<<<< HEAD
|
|
auto &labelStruct = *mLabels[i];
|
|
=======
|
|
for (size_t i = 0; i < mLabels.size(); ++i) {
|
|
auto &labelStruct = mLabels[i];
|
|
>>>>>>> 1352006... fix
|
|
=======
|
|
auto &labelStruct = mLabels[i];
|
|
>>>>>>> d6970df... Put labels directly in a vector, not pointers to them...
|
|
LabelStruct::TimeRelations relation =
|
|
labelStruct.RegionRelation(b, e, this);
|
|
if (relation == LabelStruct::SURROUNDS_LABEL) {
|
|
DeleteLabel(i);
|
|
--i;
|
|
}
|
|
else if (relation == LabelStruct::WITHIN_LABEL)
|
|
labelStruct.selectedRegion.moveT1( - (e-b));
|
|
else if (relation == LabelStruct::ENDS_IN_LABEL)
|
|
labelStruct.selectedRegion.setT0(e);
|
|
else if (relation == LabelStruct::BEGINS_IN_LABEL)
|
|
labelStruct.selectedRegion.setT1(b);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
void LabelTrack::ShiftLabelsOnInsert(double length, double pt)
|
|
{
|
|
for (auto &labelStruct: mLabels) {
|
|
LabelStruct::TimeRelations relation =
|
|
labelStruct.RegionRelation(pt, pt, this);
|
|
|
|
if (relation == LabelStruct::BEFORE_LABEL)
|
|
labelStruct.selectedRegion.move(length);
|
|
else if (relation == LabelStruct::WITHIN_LABEL)
|
|
labelStruct.selectedRegion.moveT1(length);
|
|
}
|
|
}
|
|
|
|
void LabelTrack::ChangeLabelsOnReverse(double b, double e)
|
|
{
|
|
for (auto &labelStruct: mLabels) {
|
|
if (labelStruct.RegionRelation(b, e, this) ==
|
|
LabelStruct::SURROUNDS_LABEL)
|
|
{
|
|
double aux = b + (e - labelStruct.getT1());
|
|
labelStruct.selectedRegion.setTimes(
|
|
aux,
|
|
e - (labelStruct.getT0() - b));
|
|
}
|
|
}
|
|
SortLabels();
|
|
}
|
|
|
|
void LabelTrack::ScaleLabels(double b, double e, double change)
|
|
{
|
|
for (auto &labelStruct: mLabels) {
|
|
labelStruct.selectedRegion.setTimes(
|
|
AdjustTimeStampOnScale(labelStruct.getT0(), b, e, change),
|
|
AdjustTimeStampOnScale(labelStruct.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 (auto &labelStruct: mLabels) {
|
|
labelStruct.selectedRegion.setTimes(
|
|
warper.Warp(labelStruct.getT0()),
|
|
warper.Warp(labelStruct.getT1()));
|
|
}
|
|
}
|
|
|
|
void LabelTrack::ResetFlags()
|
|
{
|
|
mDragXPos = -1;
|
|
mInitialCursorPos = 1;
|
|
mCurrentCursorPos = 1;
|
|
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
|
|
{
|
|
auto &labelStruct = mLabels[index];
|
|
|
|
// xExtra is extra space
|
|
// between the text and the endpoints.
|
|
const int xExtra=mIconWidth;
|
|
int x = labelStruct.x; // left endpoint
|
|
int x1 = labelStruct.x1; // right endpoint.
|
|
int width = labelStruct.width;
|
|
|
|
int xText; // This is where the text will end up.
|
|
|
|
// Will the text all fit at this zoom?
|
|
bool bTooWideForScreen = width > (r.width-2*xExtra);
|
|
// bool bSimpleCentering = !bTooWideForScreen;
|
|
bool bSimpleCentering = false;
|
|
|
|
//TODO (possibly):
|
|
// Add configurable options as to when to use simple
|
|
// and when complex centering.
|
|
//
|
|
// Simple centering does its best to keep the text
|
|
// centered between the label limits.
|
|
//
|
|
// Complex centering positions the text proportionally
|
|
// to how far we are through the label.
|
|
//
|
|
// If we add preferences for this, we want to be able to
|
|
// choose separately whether:
|
|
// a) Wide text labels centered simple/complex.
|
|
// b) Other text labels centered simple/complex.
|
|
//
|
|
|
|
if( bSimpleCentering )
|
|
{
|
|
// Center text between the two end points.
|
|
xText = (x+x1-width)/2;
|
|
}
|
|
else
|
|
{
|
|
// Calculate xText position to make text line
|
|
// scroll sideways evenly as r moves right.
|
|
|
|
// xText is a linear function of r.x.
|
|
// These variables are used to compute that function.
|
|
int rx0,rx1,xText0,xText1;
|
|
|
|
// Since we will be using a linear function,
|
|
// we should blend smoothly between left and right
|
|
// aligned text as r, the 'viewport' moves.
|
|
if( bTooWideForScreen )
|
|
{
|
|
rx0=x; // when viewport at label start.
|
|
xText0=x+xExtra; // text aligned left.
|
|
rx1=x1-r.width; // when viewport end at label end
|
|
xText1=x1-(width+xExtra); // text aligned right.
|
|
}
|
|
else
|
|
{
|
|
// when label start + width + extra spacing at viewport end..
|
|
rx0=x-r.width+width+2*xExtra;
|
|
// ..text aligned left.
|
|
xText0=x+xExtra;
|
|
// when viewport start + width + extra spacing at label end..
|
|
rx1=x1-(width+2*xExtra);
|
|
// ..text aligned right.
|
|
xText1=x1-(width+xExtra);
|
|
}
|
|
|
|
if( rx1 > rx0 ) // Avoid divide by zero case.
|
|
{
|
|
// Compute the blend between left and right aligned.
|
|
|
|
// Don't use:
|
|
//
|
|
// xText = xText0 + ((xText1-xText0)*(r.x-rx0))/(rx1-rx0);
|
|
//
|
|
// The problem with the above is that it integer-oveflows at
|
|
// high zoom.
|
|
|
|
// Instead use:
|
|
xText = xText0 + (int)((xText1-xText0)*(((float)(r.x-rx0))/(rx1-rx0)));
|
|
}
|
|
else
|
|
{
|
|
// Avoid divide by zero by reverting to
|
|
// simple centering.
|
|
//
|
|
// We could also fall into this case if x and x1
|
|
// are swapped, in which case we'll end up
|
|
// left aligned anyway because code later on
|
|
// will catch that.
|
|
xText = (x+x1-width)/2;
|
|
}
|
|
}
|
|
|
|
// Is the text now appearing partly outside r?
|
|
bool bOffLeft = xText < r.x+xExtra;
|
|
bool bOffRight = xText > r.x+r.width-width-xExtra;
|
|
|
|
// IF any part of the text is offscreen
|
|
// THEN we may bring it back.
|
|
if( bOffLeft == bOffRight )
|
|
{
|
|
//IF both sides on screen, THEN nothing to do.
|
|
//IF both sides off screen THEN don't do
|
|
//anything about it.
|
|
//(because if we did, you'd never get to read
|
|
//all the text by scrolling).
|
|
}
|
|
else if( bOffLeft != bTooWideForScreen)
|
|
{
|
|
// IF we're off on the left, OR we're
|
|
// too wide for the screen and off on the right
|
|
// (only) THEN align left.
|
|
xText = r.x+xExtra;
|
|
}
|
|
else
|
|
{
|
|
// We're off on the right, OR we're
|
|
// too wide and off on the left (only)
|
|
// SO align right.
|
|
xText =r.x+r.width-width-xExtra;
|
|
}
|
|
|
|
// But if we've taken the text out from its endpoints
|
|
// we must move it back so that it's between the endpoints.
|
|
|
|
// We test the left end point last because the
|
|
// text might not even fit between the endpoints (at this
|
|
// zoom factor), and in that case we'd like to position
|
|
// the text at the left end point.
|
|
if( xText > (x1-width-xExtra))
|
|
xText=(x1-width-xExtra);
|
|
if( xText < x+xExtra )
|
|
xText=x+xExtra;
|
|
|
|
labelStruct.xText = xText;
|
|
}
|
|
|
|
/// ComputeLayout determines which row each label
|
|
/// should be placed on, and reserves space for it.
|
|
/// Function assumes that the labels are sorted.
|
|
void LabelTrack::ComputeLayout(const wxRect & r, const ZoomInfo &zoomInfo) const
|
|
{
|
|
int xUsed[MAX_NUM_ROWS];
|
|
|
|
int iRow;
|
|
// Rows are the 'same' height as icons or as the text,
|
|
// whichever is taller.
|
|
const int yRowHeight = wxMax(mTextHeight,mIconHeight)+3;// pixels.
|
|
// Extra space at end of rows.
|
|
// We allow space for one half icon at the start and two
|
|
// half icon widths for extra x for the text frame.
|
|
// [we don't allow half a width space for the end icon since it is
|
|
// allowed to be obscured by the text].
|
|
const int xExtra= (3 * mIconWidth)/2;
|
|
|
|
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 (auto &x : xUsed)
|
|
x = xStart;
|
|
}
|
|
int nRowsUsed=0;
|
|
|
|
{ int i = -1; for (auto &labelStruct : mLabels) { ++i;
|
|
const int x = zoomInfo.TimeToPosition(labelStruct.getT0(), r.x);
|
|
const int x1 = zoomInfo.TimeToPosition(labelStruct.getT1(), r.x);
|
|
int y = r.y;
|
|
|
|
labelStruct.x=x;
|
|
labelStruct.x1=x1;
|
|
labelStruct.y=-1;// -ve indicates nothing doing.
|
|
iRow=0;
|
|
// Our first preference is a row that ends where we start.
|
|
// (This is to encourage merging of adjacent label boundaries).
|
|
while( (iRow<nRowsUsed) && (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;
|
|
labelStruct.y=y;
|
|
// On this row we have used up to max of end marker and width.
|
|
// Plus also allow space to show the start icon and
|
|
// some space for the text frame.
|
|
xUsed[iRow]=x+labelStruct.width+xExtra;
|
|
if( xUsed[iRow] < x1 ) xUsed[iRow]=x1;
|
|
ComputeTextPosition( r, i );
|
|
}
|
|
}}
|
|
}
|
|
|
|
LabelStruct::LabelStruct(const SelectedRegion ®ion,
|
|
const wxString& aTitle)
|
|
: selectedRegion(region)
|
|
, 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) const
|
|
{
|
|
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;
|
|
}
|
|
|
|
void LabelTrack::CalcHighlightXs(int *x1, int *x2) const
|
|
{
|
|
wxMemoryDC dc;
|
|
|
|
if (msFont.Ok()) {
|
|
dc.SetFont(msFont);
|
|
}
|
|
|
|
int pos1 = mInitialCursorPos, pos2 = mCurrentCursorPos;
|
|
if (pos1 > pos2)
|
|
std::swap(pos1, pos2);
|
|
|
|
const auto &labelStruct = mLabels[mSelIndex];
|
|
|
|
// find the left X pos of highlighted area
|
|
labelStruct.getXPos(dc, x1, pos1);
|
|
// find the right X pos of highlighted area
|
|
labelStruct.getXPos(dc, x2, pos2);
|
|
}
|
|
|
|
/// 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);
|
|
|
|
wxCoord textWidth, textHeight;
|
|
|
|
// Get the text widths.
|
|
// TODO: Make more efficient by only re-computing when a
|
|
// text label title changes.
|
|
for (auto &labelStruct : mLabels) {
|
|
dc.GetTextExtent(labelStruct.title, &textWidth, &textHeight);
|
|
labelStruct.width = textWidth;
|
|
}
|
|
|
|
// TODO: And this only needs to be done once, but we
|
|
// do need the dc to do it.
|
|
// We need to set mTextHeight to something sensible,
|
|
// guarding against the case where there are no
|
|
// labels or all are empty strings, which for example
|
|
// happens with a NEW label track.
|
|
dc.GetTextExtent(wxT("Demo Text x^y"), &textWidth, &textHeight);
|
|
mTextHeight = (int)textHeight;
|
|
ComputeLayout( r, zoomInfo );
|
|
dc.SetTextForeground(theTheme.Colour( clrLabelTrackText));
|
|
dc.SetBackgroundMode(wxTRANSPARENT);
|
|
dc.SetBrush(AColor::labelTextNormalBrush);
|
|
dc.SetPen(AColor::labelSurroundPen);
|
|
int GlyphLeft;
|
|
int GlyphRight;
|
|
// Now we draw the various items in this order,
|
|
// so that the correct things overpaint each other.
|
|
|
|
// Draw vertical lines that show where the end positions are.
|
|
for (auto &labelStruct : mLabels)
|
|
labelStruct.DrawLines( dc, r );
|
|
|
|
// Draw the end glyphs.
|
|
{ int i = -1; for (auto &labelStruct : mLabels) { ++i;
|
|
GlyphLeft=0;
|
|
GlyphRight=1;
|
|
if( i==mMouseOverLabelLeft )
|
|
GlyphLeft = mbHitCenter ? 6:9;
|
|
if( i==mMouseOverLabelRight )
|
|
GlyphRight = mbHitCenter ? 7:4;
|
|
labelStruct.DrawGlyphs( dc, r, GlyphLeft, GlyphRight );
|
|
}}
|
|
|
|
// Draw the label boxes.
|
|
{ int i = -1; for (auto &labelStruct : mLabels) { ++i;
|
|
if( mSelIndex==i)
|
|
dc.SetBrush(AColor::labelTextEditBrush);
|
|
labelStruct.DrawTextBox( dc, r );
|
|
if( mSelIndex==i)
|
|
dc.SetBrush(AColor::labelTextNormalBrush);
|
|
}}
|
|
|
|
// Draw highlights
|
|
if ((mDragXPos != -1) && (mSelIndex >= 0 ))
|
|
{
|
|
int xpos1, xpos2;
|
|
CalcHighlightXs(&xpos1, &xpos2);
|
|
mLabels[mSelIndex].DrawHighlight(dc, xpos1, xpos2, mFontHeight);
|
|
}
|
|
|
|
// Draw the text and the label boxes.
|
|
{ int i = -1; for (auto &labelStruct : mLabels) { ++i;
|
|
if( mSelIndex==i)
|
|
dc.SetBrush(AColor::labelTextEditBrush);
|
|
labelStruct.DrawText( dc, r );
|
|
if( mSelIndex==i)
|
|
dc.SetBrush(AColor::labelTextNormalBrush);
|
|
}}
|
|
|
|
// Draw the cursor, if there is one.
|
|
if( mDrawCursor && mSelIndex >=0 )
|
|
{
|
|
const auto &labelStruct = mLabels[mSelIndex];
|
|
int xPos = labelStruct.xText;
|
|
|
|
if( mCurrentCursorPos > 0)
|
|
{
|
|
// Calculate the width of the substring and add it to Xpos
|
|
int partWidth;
|
|
dc.GetTextExtent(labelStruct.title.Left(mCurrentCursorPos), &partWidth, NULL);
|
|
xPos += partWidth;
|
|
}
|
|
|
|
wxPen currentPen = dc.GetPen();
|
|
const int CursorWidth=2;
|
|
currentPen.SetWidth(CursorWidth);
|
|
AColor::Line(dc,
|
|
xPos-1, labelStruct.y - mFontHeight/2 + 1,
|
|
xPos-1, labelStruct.y + mFontHeight/2 - 1);
|
|
currentPen.SetWidth(1);
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
{
|
|
wxMemoryDC dc;
|
|
if(msFont.Ok())
|
|
dc.SetFont(msFont);
|
|
|
|
// A bool indicator to see if set the cursor position or not
|
|
bool finished = false;
|
|
int charIndex = 1;
|
|
int partWidth;
|
|
int oneWidth;
|
|
double bound;
|
|
wxString subString;
|
|
const auto &labelStruct = mLabels[mSelIndex];
|
|
const auto &title = labelStruct.title;
|
|
const int length = title.length();
|
|
while (!finished && (charIndex < length + 1))
|
|
{
|
|
subString = title.Left(charIndex);
|
|
// Get the width of substring
|
|
dc.GetTextExtent(subString, &partWidth, NULL);
|
|
|
|
// Get the width of the last character
|
|
dc.GetTextExtent(subString.Right(1), &oneWidth, NULL);
|
|
bound = labelStruct.xText + partWidth - oneWidth * 0.5;
|
|
|
|
if (xPos <= bound)
|
|
{
|
|
// Found
|
|
mCurrentCursorPos = charIndex - 1;
|
|
finished = true;
|
|
}
|
|
else
|
|
{
|
|
// Advance
|
|
charIndex++;
|
|
}
|
|
}
|
|
if (!finished)
|
|
{
|
|
// Cursor should be in the last position
|
|
mCurrentCursorPos = 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, right;
|
|
auto &labelStruct = mLabels[mSelIndex];
|
|
auto &text = labelStruct.title;
|
|
|
|
int init = mInitialCursorPos;
|
|
int cur = mCurrentCursorPos;
|
|
if (init > cur)
|
|
std::swap(init, cur);
|
|
|
|
// data for cutting
|
|
wxString data = text.Mid(init, cur - init);
|
|
|
|
// get left-remaining text
|
|
if (init > 0)
|
|
left = text.Left(init);
|
|
|
|
// get right-remaining text
|
|
if (cur < (int)text.Length())
|
|
right = text.Mid(cur);
|
|
|
|
// set title to the combination of the two remainders
|
|
text = left + right;
|
|
|
|
// copy data onto clipboard
|
|
if (wxTheClipboard->Open()) {
|
|
// Clipboard owns the data you give it
|
|
wxTheClipboard->SetData(safenew wxTextDataObject(data));
|
|
wxTheClipboard->Close();
|
|
}
|
|
|
|
// set cursor positions
|
|
mInitialCursorPos = mCurrentCursorPos = left.Length();
|
|
return true;
|
|
}
|
|
|
|
/// Copy the selected text in the text box
|
|
/// @return true if text is selected in text box, false otherwise
|
|
bool LabelTrack::CopySelectedText()
|
|
{
|
|
if (mSelIndex == -1)
|
|
return false;
|
|
|
|
const auto &labelStruct = mLabels[mSelIndex];
|
|
if (!labelStruct.highlighted)
|
|
return false;
|
|
|
|
int init = mInitialCursorPos;
|
|
int cur = mCurrentCursorPos;
|
|
if (init > cur)
|
|
std::swap(init, cur);
|
|
|
|
// data for copying
|
|
wxString data = labelStruct.title.Mid(init, cur-init);
|
|
|
|
// copy the data on clipboard
|
|
if (wxTheClipboard->Open()) {
|
|
// Clipboard owns the data you give it
|
|
wxTheClipboard->SetData(safenew wxTextDataObject(data));
|
|
wxTheClipboard->Close();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// PRL: should this set other fields of the label selection?
|
|
/// Paste the text on the clipboard to text box
|
|
/// @return true if mouse is clicked in text box, false otherwise
|
|
bool LabelTrack::PasteSelectedText(double sel0, double sel1)
|
|
{
|
|
if (mSelIndex == -1)
|
|
AddLabel(SelectedRegion(sel0, sel1), wxT(""));
|
|
|
|
wxString text, left, right;
|
|
|
|
// if text data is available
|
|
if (IsTextClipSupported()) {
|
|
if (wxTheClipboard->Open()) {
|
|
wxTextDataObject data;
|
|
wxTheClipboard->GetData(data);
|
|
wxTheClipboard->Close();
|
|
text = data.GetText();
|
|
}
|
|
|
|
// Convert control characters to blanks
|
|
for (int i = 0; i < (int)text.Length(); i++) {
|
|
if (wxIscntrl(text[i])) {
|
|
text[i] = wxT(' ');
|
|
}
|
|
}
|
|
}
|
|
|
|
auto &labelStruct = mLabels[mSelIndex];
|
|
auto &title = labelStruct.title;
|
|
int cur = mCurrentCursorPos, init = cur;
|
|
if (labelStruct.highlighted) {
|
|
init = mInitialCursorPos;
|
|
if (init > cur)
|
|
std::swap(init, cur);
|
|
}
|
|
left = title.Left(init);
|
|
if (cur < (int)title.Length())
|
|
right = title.Mid(cur);
|
|
|
|
title = left + text + right;
|
|
mInitialCursorPos = mCurrentCursorPos = left.Length() + text.Length();
|
|
return true;
|
|
}
|
|
|
|
|
|
/// @return true if the text data is available in the clipboard, false otherwise
|
|
bool LabelTrack::IsTextClipSupported()
|
|
{
|
|
return wxTheClipboard->IsSupported(wxDF_TEXT);
|
|
}
|
|
|
|
|
|
double LabelTrack::GetOffset() const
|
|
{
|
|
return mOffset;
|
|
}
|
|
|
|
double LabelTrack::GetStartTime() const
|
|
{
|
|
if (mLabels.empty())
|
|
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).
|
|
if (mLabels.empty())
|
|
return 0.0;
|
|
|
|
double end = 0.0;
|
|
for (auto &labelStruct: mLabels) {
|
|
const double t1 = labelStruct.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.
|
|
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;
|
|
{ int i = -1; for (auto &labelStruct : mLabels) { ++i;
|
|
//over left or right selection bound
|
|
//Check right bound first, since it is drawn after left bound,
|
|
//so give it precedence for matching/highlighting.
|
|
if( abs(labelStruct.y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 &&
|
|
abs(labelStruct.x1 - d2 -x) < d1)
|
|
{
|
|
mMouseOverLabelRight = i;
|
|
if(abs(labelStruct.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(labelStruct.x1-labelStruct.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(labelStruct.y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 &&
|
|
abs(labelStruct.x + d2 - x) < d1 )
|
|
{
|
|
mMouseOverLabelLeft = i;
|
|
if(abs(labelStruct.x - x) < d2 )
|
|
mbHitCenter = true;
|
|
result |= 1;
|
|
}
|
|
|
|
// give text box better priority for selecting
|
|
if(OverTextBox(&labelStruct, x, y))
|
|
{
|
|
result = 0;
|
|
}
|
|
|
|
}}
|
|
return result;
|
|
}
|
|
|
|
int LabelTrack::OverATextBox(int xx, int yy) const
|
|
{
|
|
for (int nn = (int)mLabels.size(); nn--;) {
|
|
const auto &labelStruct = mLabels[nn];
|
|
if (OverTextBox(&labelStruct, xx, yy))
|
|
return nn;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// return true if the mouse is over text box, false otherwise
|
|
bool LabelTrack::OverTextBox(const LabelStruct *pLabel, int x, int y) const
|
|
{
|
|
if( (pLabel->xText-(mIconWidth/2) < x) &&
|
|
(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 &labelStruct = mLabels[ iLabel ];
|
|
|
|
// Adjust the requested edge.
|
|
bool flipped = labelStruct.AdjustEdge( iEdge, fNewTime );
|
|
// If the edges did not swap, then we are done.
|
|
if( ! flipped )
|
|
return;
|
|
|
|
// If swapping's not allowed we must also move the edge
|
|
// we didn't move. Then we're done.
|
|
if( !bAllowSwapping )
|
|
{
|
|
labelStruct.AdjustEdge( -iEdge, fNewTime );
|
|
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) {
|
|
auto &labelStruct = mLabels[mMouseOverLabelLeft];
|
|
lupd = labelStruct.updated;
|
|
labelStruct.updated = false;
|
|
}
|
|
if(mMouseOverLabelRight>=0) {
|
|
auto &labelStruct = mLabels[mMouseOverLabelRight];
|
|
rupd = labelStruct.updated;
|
|
labelStruct.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;
|
|
|
|
// for preventing dragging glygh from changing current cursor position
|
|
// set end dragging position to current cursor position
|
|
SetCurrentCursorPosition(mDragXPos);
|
|
|
|
// 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;
|
|
|
|
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) {
|
|
auto &labelStruct = mLabels[mSelIndex];
|
|
*newSel = labelStruct.selectedRegion;
|
|
SetCurrentCursorPosition(evt.m_x);
|
|
|
|
// for preventing from resetting by shift+mouse left button
|
|
if (labelStruct.changeInitialMouseXPos)
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
mDrawCursor = true;
|
|
|
|
// reset the highlight indicator
|
|
wxRect highlightedRect;
|
|
{
|
|
int xpos1, xpos2;
|
|
CalcHighlightXs(&xpos1, &xpos2);
|
|
|
|
wxASSERT(mFontHeight >= 0); // should have been set up while drawing
|
|
// the rectangle of highlighted area
|
|
highlightedRect = {
|
|
xpos1, labelStruct.y - mFontHeight / 2,
|
|
(int)(xpos2 - xpos1 + 0.5), mFontHeight
|
|
};
|
|
|
|
// reset when left button is down
|
|
if (evt.LeftDown())
|
|
labelStruct.highlighted = false;
|
|
// reset when right button is down outside text box
|
|
if (evt.RightDown())
|
|
{
|
|
if (!highlightedRect.Contains(evt.m_x, evt.m_y))
|
|
{
|
|
mCurrentCursorPos = mInitialCursorPos = 0;
|
|
labelStruct.highlighted = false;
|
|
}
|
|
}
|
|
// set changeInitialMouseXPos flag
|
|
labelStruct.changeInitialMouseXPos = true;
|
|
}
|
|
|
|
// disable displaying if right button is down outside text box
|
|
if (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 (!OverTextBox(&labelStruct, evt.m_x, evt.m_y))
|
|
mSelIndex = -1;
|
|
double t = zoomInfo.PositionToTime(evt.m_x, r.x);
|
|
*newSel = SelectedRegion(t, t);
|
|
}
|
|
#endif
|
|
|
|
// handle shift+mouse left button
|
|
if (evt.ShiftDown()) {
|
|
// if the mouse is clicked in text box, set flags
|
|
mDragXPos = evt.m_x;
|
|
|
|
// for preventing dragging glygh from changing current cursor position
|
|
// set end dragging position to current cursor position
|
|
SetCurrentCursorPosition(evt.m_x);
|
|
}
|
|
}
|
|
|
|
#if defined(__WXGTK__) && (HAVE_GTK)
|
|
if (evt.MiddleDown()) {
|
|
// Paste text, making a NEW label if none is selected.
|
|
wxTheClipboard->UsePrimarySelection(true);
|
|
PasteSelectedText(newSel->t0(), newSel->t1());
|
|
wxTheClipboard->UsePrimarySelection(false);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
const int mods = event.GetModifiers();
|
|
|
|
// Check for modifiers and only allow shift
|
|
if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
|
|
event.Skip();
|
|
return updated;
|
|
}
|
|
|
|
// All editing keys are only active if we're currently editing a label
|
|
if (mSelIndex >= 0) {
|
|
auto &labelStruct = mLabels[mSelIndex];
|
|
auto &title = labelStruct.title;
|
|
switch (keyCode) {
|
|
|
|
case WXK_BACK:
|
|
{
|
|
int len = title.Length();
|
|
|
|
//IF the label is not blank THEN get rid of a letter or letters according to cursor position
|
|
if (len > 0)
|
|
{
|
|
// IF there are some highlighted letters, THEN DELETE them
|
|
if (labelStruct.highlighted) {
|
|
RemoveSelectedText();
|
|
}
|
|
else
|
|
{
|
|
// DELETE one letter
|
|
if (mCurrentCursorPos > 0) {
|
|
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 = 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 (labelStruct.highlighted) {
|
|
RemoveSelectedText();
|
|
}
|
|
else
|
|
{
|
|
// DELETE one letter
|
|
if (mCurrentCursorPos < len) {
|
|
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
|
|
mCurrentCursorPos = 0;
|
|
if (mods == wxMOD_SHIFT)
|
|
mDragXPos = 0;
|
|
else {
|
|
mDragXPos = -1;
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
}
|
|
break;
|
|
|
|
case WXK_END:
|
|
case WXK_NUMPAD_END:
|
|
// Move cursor to end of label
|
|
mCurrentCursorPos = (int)title.length();
|
|
if (mods == wxMOD_SHIFT)
|
|
mDragXPos = 0;
|
|
else {
|
|
mDragXPos = -1;
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
}
|
|
break;
|
|
|
|
case WXK_LEFT:
|
|
case WXK_NUMPAD_LEFT:
|
|
// Moving cursor left
|
|
if (mCurrentCursorPos > 0) {
|
|
mCurrentCursorPos--;
|
|
if (mods == wxMOD_SHIFT)
|
|
mDragXPos = 0;
|
|
else {
|
|
mDragXPos = -1;
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case WXK_RIGHT:
|
|
case WXK_NUMPAD_RIGHT:
|
|
// Moving cursor right
|
|
if (mCurrentCursorPos < (int)title.length()) {
|
|
mCurrentCursorPos++;
|
|
if (mods == wxMOD_SHIFT)
|
|
mDragXPos = 0;
|
|
else {
|
|
mDragXPos = -1;
|
|
mInitialCursorPos = mCurrentCursorPos;
|
|
}
|
|
}
|
|
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.size()) {
|
|
LabelStruct &newLabel = mLabels[mSelIndex];
|
|
mCurrentCursorPos = newLabel.title.Length();
|
|
//Set the selection region to be equal to the selection bounds of the tabbed-to label.
|
|
newSel = newLabel.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.empty()) {
|
|
int len = (int) mLabels.size();
|
|
if (event.ShiftDown()) {
|
|
mSelIndex = len - 1;
|
|
if (newSel.t0() > mLabels[0].getT0()) {
|
|
while (mSelIndex >= 0 &&
|
|
mLabels[mSelIndex].getT0() >= newSel.t0()) {
|
|
mSelIndex--;
|
|
}
|
|
}
|
|
} else {
|
|
mSelIndex = 0;
|
|
if (newSel.t0() < mLabels[len - 1].getT0()) {
|
|
while (mSelIndex < len &&
|
|
mLabels[mSelIndex].getT0() <= newSel.t0()) {
|
|
mSelIndex++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mSelIndex >= 0 && mSelIndex < len) {
|
|
const auto &labelStruct = mLabels[mSelIndex];
|
|
mCurrentCursorPos = labelStruct.title.Length();
|
|
//Set the selection region to be equal to the selection bounds of the tabbed-to label.
|
|
newSel = labelStruct.selectedRegion;
|
|
}
|
|
else {
|
|
mSelIndex = -1;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (!IsGoodLabelFirstKey(event)) {
|
|
event.Skip();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Make sure the caret is visible
|
|
mDrawCursor = true;
|
|
|
|
return updated;
|
|
}
|
|
|
|
/// OnChar is called for incoming characters -- that's any keypress not handled
|
|
/// by OnKeyDown.
|
|
bool 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.
|
|
const int mods = event.GetModifiers();
|
|
if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
|
|
event.Skip();
|
|
return false;
|
|
}
|
|
|
|
// Only track true changes to the label
|
|
bool updated = false;
|
|
|
|
// Cache the character
|
|
wxChar charCode = event.GetUnicodeKey();
|
|
|
|
// Skip if it's not a valid unicode character or a control character
|
|
if (charCode == 0 || wxIscntrl(charCode)) {
|
|
event.Skip();
|
|
return false;
|
|
}
|
|
|
|
// If we've reached this point and aren't currently editing, add NEW label
|
|
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
|
|
//
|
|
|
|
auto &labelStruct = mLabels[mSelIndex];
|
|
auto &title = labelStruct.title;
|
|
|
|
// Test if cursor is in the end of string or not
|
|
if (labelStruct.highlighted) {
|
|
RemoveSelectedText();
|
|
}
|
|
|
|
if (mCurrentCursorPos < (int)title.length()) {
|
|
// Get substring on the righthand side of cursor
|
|
wxString rightPart = title.Mid(mCurrentCursorPos);
|
|
// Set title to substring on the lefthand side of cursor
|
|
title = title.Left(mCurrentCursorPos);
|
|
//append charcode
|
|
title += charCode;
|
|
//append the right part substring
|
|
title += rightPart;
|
|
}
|
|
else
|
|
//append charCode
|
|
title += charCode;
|
|
|
|
//moving cursor position forward
|
|
mInitialCursorPos = ++mCurrentCursorPos;
|
|
updated = true;
|
|
|
|
// Make sure the caret is visible
|
|
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);
|
|
|
|
wxASSERT(mSelIndex >= 0);
|
|
const LabelStruct *ls = GetLabel(mSelIndex);
|
|
|
|
wxClientDC dc(parent);
|
|
|
|
if (msFont.Ok())
|
|
{
|
|
dc.SetFont(msFont);
|
|
}
|
|
|
|
int x = 0;
|
|
bool success = CalcCursorX(&x);
|
|
wxASSERT(success);
|
|
|
|
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, right;
|
|
|
|
int init = mInitialCursorPos;
|
|
int cur = mCurrentCursorPos;
|
|
if (init > cur)
|
|
std::swap(init, cur);
|
|
|
|
auto &labelStruct = mLabels[mSelIndex];
|
|
auto &title = labelStruct.title;
|
|
|
|
if (init > 0)
|
|
left = title.Left(init);
|
|
|
|
if (cur < (int)title.Length())
|
|
right = title.Mid(cur);
|
|
|
|
title = left + right;
|
|
mInitialCursorPos = mCurrentCursorPos = left.Length();
|
|
labelStruct.highlighted = false;
|
|
mDragXPos = -1;
|
|
}
|
|
|
|
void LabelTrack::Unselect()
|
|
{
|
|
mSelIndex = -1;
|
|
}
|
|
|
|
bool LabelTrack::IsSelected() const
|
|
{
|
|
return (mSelIndex >= 0 && mSelIndex < (int)mLabels.size());
|
|
}
|
|
|
|
/// Export labels including label start and end-times.
|
|
void LabelTrack::Export(wxTextFile & f) const
|
|
{
|
|
// PRL: to do: export other selection fields
|
|
for (auto &labelStruct: mLabels) {
|
|
f.AddLine(wxString::Format(wxT("%f\t%f\t%s"),
|
|
(double)labelStruct.getT0(),
|
|
(double)labelStruct.getT1(),
|
|
labelStruct.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.reserve(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 { SelectedRegion(t0, t1), title };
|
|
mLabels.push_back(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 { selectedRegion, title };
|
|
mLabels.push_back(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.reserve(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.size();
|
|
|
|
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 (auto &labelStruct: mLabels) {
|
|
xmlFile.StartTag(wxT("label"));
|
|
labelStruct.getSelectedRegion()
|
|
.WriteXMLAttributes(xmlFile, wxT("t"), wxT("t1"));
|
|
// PRL: to do: write other selection fields
|
|
xmlFile.WriteAttr(wxT("title"), labelStruct.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;
|
|
|
|
for (auto pLabel : mLabels)
|
|
delete pLabel;
|
|
|
|
mLabels.clear();
|
|
mLabels.reserve(len);
|
|
|
|
for (int 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.size();
|
|
out->AddLine(wxString::Format(wxT("%d"), len));
|
|
|
|
for (auto pLabel : mLabels) {
|
|
const auto &labelStruct = *pLabel;
|
|
out->AddLine(wxString::Format(wxT("%lf"), labelStruct.selectedRegion.mT0));
|
|
out->AddLine(labelStruct.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());
|
|
|
|
for (auto &labelStruct: mLabels) {
|
|
LabelStruct::TimeRelations relation =
|
|
labelStruct.RegionRelation(t0, t1, this);
|
|
if (relation == LabelStruct::SURROUNDS_LABEL) {
|
|
LabelStruct l {
|
|
labelStruct.selectedRegion,
|
|
labelStruct.getT0() - t0,
|
|
labelStruct.getT1() - t0,
|
|
labelStruct.title
|
|
};
|
|
lt->mLabels.push_back(l);
|
|
}
|
|
else if (relation == LabelStruct::WITHIN_LABEL) {
|
|
LabelStruct l {
|
|
labelStruct.selectedRegion,
|
|
0,
|
|
t1-t0,
|
|
labelStruct.title
|
|
};
|
|
lt->mLabels.push_back(l);
|
|
}
|
|
else if (relation == LabelStruct::BEGINS_IN_LABEL) {
|
|
LabelStruct l {
|
|
labelStruct.selectedRegion,
|
|
0,
|
|
labelStruct.getT1() - t0,
|
|
labelStruct.title
|
|
};
|
|
lt->mLabels.push_back(l);
|
|
}
|
|
else if (relation == LabelStruct::ENDS_IN_LABEL) {
|
|
LabelStruct l {
|
|
labelStruct.selectedRegion,
|
|
labelStruct.getT0() - t0,
|
|
t1 - t0,
|
|
labelStruct.title
|
|
};
|
|
lt->mLabels.push_back(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.size();
|
|
int pos = 0;
|
|
|
|
while (pos < len && mLabels[pos].getT0() < t)
|
|
pos++;
|
|
|
|
auto sl = static_cast<const LabelTrack *>(src);
|
|
for (auto &labelStruct: sl->mLabels) {
|
|
LabelStruct l {
|
|
labelStruct.selectedRegion,
|
|
labelStruct.getT0() + t,
|
|
labelStruct.getT1() + t,
|
|
labelStruct.title
|
|
};
|
|
mLabels.insert(mLabels.begin() + pos++, l);
|
|
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);
|
|
|
|
// mLabels may resize as we iterate, so use subscripting
|
|
for (unsigned int i = 0; i < mLabels.size(); ++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 {
|
|
label.selectedRegion,
|
|
label.getT0() + j * tLen,
|
|
label.getT1() + j * tLen,
|
|
label.title
|
|
};
|
|
|
|
// Figure out where to insert
|
|
while (pos < mLabels.size() &&
|
|
mLabels[pos].getT0() < l.getT0())
|
|
pos++;
|
|
mLabels.insert(mLabels.begin() + pos, l);
|
|
}
|
|
}
|
|
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.size();
|
|
|
|
// mLabels may resize as we iterate, so use subscripting
|
|
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 {
|
|
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(mLabels.begin() + i, l);
|
|
}
|
|
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)
|
|
{
|
|
for (auto &labelStruct: mLabels) {
|
|
double t0 = labelStruct.getT0();
|
|
double t1 = labelStruct.getT1();
|
|
if (t0 >= t)
|
|
t0 += len;
|
|
|
|
if (t1 >= t)
|
|
t1 += len;
|
|
labelStruct.selectedRegion.setTimes(t0, t1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int LabelTrack::GetNumLabels() const
|
|
{
|
|
return mLabels.size();
|
|
}
|
|
|
|
const LabelStruct *LabelTrack::GetLabel(int index) const
|
|
{
|
|
return &mLabels[index];
|
|
}
|
|
|
|
int LabelTrack::GetLabelIndex(double t, double t1)
|
|
{
|
|
//We'd have liked to have times in terms of samples,
|
|
//because then we're doing an intrger comparison.
|
|
//Never mind. Instead we look for near enough.
|
|
//This level of (in)accuracy is only a problem if we
|
|
//deal with sounds in the MHz range.
|
|
const double delta = 1.0e-7;
|
|
{ int i = -1; for (auto &labelStruct : mLabels) { ++i;
|
|
if( fabs( labelStruct.getT0() - t ) > delta )
|
|
continue;
|
|
if( fabs( labelStruct.getT1() - t1 ) > delta )
|
|
continue;
|
|
return i;
|
|
}}
|
|
|
|
return wxNOT_FOUND;
|
|
}
|
|
|
|
int LabelTrack::AddLabel(const SelectedRegion &selectedRegion,
|
|
const wxString &title, int restoreFocus)
|
|
{
|
|
LabelStruct l { selectedRegion, title };
|
|
mInitialCursorPos = mCurrentCursorPos = title.length();
|
|
|
|
int len = mLabels.size();
|
|
int pos = 0;
|
|
|
|
while (pos < len && mLabels[pos].getT0() < selectedRegion.t0())
|
|
pos++;
|
|
|
|
mLabels.insert(mLabels.begin() + pos, l);
|
|
|
|
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.size()));
|
|
mLabels.erase(mLabels.begin() + index);
|
|
// IF we've deleted the selected label
|
|
// THEN set no label selected.
|
|
if( mSelIndex== index )
|
|
{
|
|
mSelIndex = -1;
|
|
mCurrentCursorPos = 1;
|
|
}
|
|
// IF we removed a label before the selected label
|
|
// THEN the NEW selected label number is one less.
|
|
else if( index < mSelIndex )
|
|
{
|
|
mSelIndex--;
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
const auto begin = mLabels.begin();
|
|
const auto nn = (int)mLabels.size();
|
|
int i = 1;
|
|
while (true)
|
|
{
|
|
// Find the next disorder
|
|
while (i < nn && mLabels[i - 1].getT0() <= mLabels[i].getT0())
|
|
++i;
|
|
if (i >= nn)
|
|
break;
|
|
|
|
// Where must element i sink to? At most i - 1, maybe less
|
|
int j = i - 2;
|
|
while( (j >= 0) && (mLabels[j].getT0() > mLabels[i].getT0()) )
|
|
--j;
|
|
++j;
|
|
|
|
// Now fix the disorder
|
|
std::rotate(
|
|
begin + j,
|
|
begin + i,
|
|
begin + i + 1
|
|
);
|
|
|
|
// Various indices need to be updated with the moved items...
|
|
auto update = [=](int &index) {
|
|
if( index <= i ) {
|
|
if( index == i )
|
|
index = j;
|
|
else if( index >= j)
|
|
++index;
|
|
}
|
|
};
|
|
update(mMouseOverLabelLeft);
|
|
update(mMouseOverLabelRight);
|
|
update(mSelIndex);
|
|
}
|
|
}
|
|
|
|
wxString LabelTrack::GetTextOfLabels(double t0, double t1) const
|
|
{
|
|
bool firstLabel = true;
|
|
wxString retVal;
|
|
|
|
for (auto &labelStruct: mLabels) {
|
|
if (labelStruct.getT0() >= t0 &&
|
|
labelStruct.getT1() <= t1)
|
|
{
|
|
if (!firstLabel)
|
|
retVal += '\t';
|
|
firstLabel = false;
|
|
retVal += labelStruct.title;
|
|
}
|
|
}
|
|
|
|
return retVal;
|
|
}
|