mirror of
https://github.com/cookiengineer/audacity
synced 2025-04-29 23:29:41 +02:00
665 lines
19 KiB
C++
665 lines
19 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
Lyrics.cpp
|
|
|
|
Dominic Mazzoni
|
|
Vaughan Johnson
|
|
|
|
**********************************************************************/
|
|
|
|
#include "Lyrics.h"
|
|
|
|
#include <math.h>
|
|
|
|
#include <wx/dcclient.h>
|
|
#include <wx/defs.h>
|
|
#include <wx/dcmemory.h>
|
|
#include <wx/frame.h>
|
|
#include <wx/mimetype.h>
|
|
|
|
#include "AudioIO.h"
|
|
#include "Project.h"
|
|
#include "ProjectWindowBase.h"
|
|
#include "LabelTrack.h"
|
|
#include "commands/CommandManager.h"
|
|
#include "UndoManager.h"
|
|
#include "ViewInfo.h"
|
|
|
|
|
|
BEGIN_EVENT_TABLE(HighlightTextCtrl, wxTextCtrl)
|
|
EVT_MOUSE_EVENTS(HighlightTextCtrl::OnMouseEvent)
|
|
END_EVENT_TABLE()
|
|
|
|
HighlightTextCtrl::HighlightTextCtrl(LyricsPanel* parent,
|
|
wxWindowID id,
|
|
const wxString& value /* = {} */,
|
|
const wxPoint& pos /*= wxDefaultPosition*/,
|
|
const wxSize& size /*= wxDefaultSize*/)
|
|
: wxTextCtrl(parent, id, // wxWindow* parent, wxWindowID id,
|
|
value, // const wxString& value = {},
|
|
pos, // const wxPoint& pos = wxDefaultPosition,
|
|
size, // const wxSize& size = wxDefaultSize,
|
|
wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH | wxTE_RICH2 | wxTE_AUTO_URL | wxTE_NOHIDESEL), //v | wxHSCROLL)
|
|
mLyricsPanel(parent)
|
|
{
|
|
}
|
|
|
|
void HighlightTextCtrl::OnMouseEvent(wxMouseEvent& event)
|
|
{
|
|
if (event.ButtonUp())
|
|
{
|
|
long from, to;
|
|
this->GetSelection(&from, &to);
|
|
|
|
int nCurSyl = mLyricsPanel->GetCurrentSyllableIndex();
|
|
int nNewSyl = mLyricsPanel->FindSyllable(from);
|
|
if (nNewSyl != nCurSyl)
|
|
{
|
|
Syllable* pCurSyl = mLyricsPanel->GetSyllable(nNewSyl);
|
|
auto pProj = FindProjectFromWindow( this );
|
|
auto &selectedRegion = ViewInfo::Get( *pProj ).selectedRegion;
|
|
selectedRegion.setT0( pCurSyl->t );
|
|
|
|
//v Should probably select to end as in
|
|
// SelectUtilities::Handler::OnSelectCursorEnd,
|
|
// but better to generalize that in AudacityProject methods.
|
|
selectedRegion.setT1( pCurSyl->t );
|
|
}
|
|
}
|
|
|
|
event.Skip();
|
|
}
|
|
|
|
|
|
//v static const kHighlightTextCtrlID = 7654;
|
|
|
|
BEGIN_EVENT_TABLE(LyricsPanel, wxPanelWrapper)
|
|
EVT_KEY_DOWN(LyricsPanel::OnKeyEvent)
|
|
EVT_PAINT(LyricsPanel::OnPaint)
|
|
EVT_SIZE(LyricsPanel::OnSize)
|
|
|
|
//v Doesn't seem to be a way to capture a selection event in a read-only wxTextCtrl.
|
|
// EVT_COMMAND_LEFT_CLICK(kHighlightTextCtrlID, LyricsPanel::OnHighlightTextCtrl)
|
|
END_EVENT_TABLE()
|
|
|
|
IMPLEMENT_CLASS(LyricsPanel, wxPanel)
|
|
|
|
LyricsPanel::LyricsPanel(wxWindow* parent, wxWindowID id,
|
|
AudacityProject *project,
|
|
const wxPoint& pos /*= wxDefaultPosition*/,
|
|
const wxSize& size /*= wxDefaultSize*/) :
|
|
wxPanelWrapper(parent, id, pos, size, wxWANTS_CHARS),
|
|
mWidth(size.x), mHeight(size.y)
|
|
, mProject(project)
|
|
{
|
|
mKaraokeHeight = mHeight;
|
|
mLyricsStyle = kBouncingBallLyrics; // default
|
|
mKaraokeFontSize = this->GetDefaultFontSize(); // Call only after mLyricsPanelStyle is set.
|
|
|
|
this->SetBackgroundColour(*wxWHITE);
|
|
|
|
mHighlightTextCtrl =
|
|
safenew HighlightTextCtrl(this, -1, // wxWindow* parent, wxWindowID id,
|
|
wxT(""), // const wxString& value = {},
|
|
wxPoint(0, 0), // const wxPoint& pos = wxDefaultPosition,
|
|
size); // const wxSize& size = wxDefaultSize
|
|
this->SetHighlightFont();
|
|
mHighlightTextCtrl->Show(mLyricsStyle == kHighlightLyrics); // test, in case we conditionalize the default, above
|
|
|
|
|
|
mT = 0.0;
|
|
|
|
Clear();
|
|
Finish(0.0);
|
|
|
|
#ifdef __WXMAC__
|
|
wxSizeEvent dummyEvent;
|
|
OnSize(dummyEvent);
|
|
#endif
|
|
|
|
parent->Bind(wxEVT_SHOW, &LyricsPanel::OnShow, this);
|
|
|
|
project->Bind(EVT_UNDO_PUSHED, &LyricsPanel::UpdateLyrics, this);
|
|
project->Bind(EVT_UNDO_MODIFIED, &LyricsPanel::UpdateLyrics, this);
|
|
project->Bind(EVT_UNDO_OR_REDO, &LyricsPanel::UpdateLyrics, this);
|
|
project->Bind(EVT_UNDO_RESET, &LyricsPanel::UpdateLyrics, this);
|
|
|
|
wxTheApp->Bind(EVT_AUDIOIO_PLAYBACK, &LyricsPanel::OnStartStop, this);
|
|
wxTheApp->Bind(EVT_AUDIOIO_CAPTURE, &LyricsPanel::OnStartStop, this);
|
|
}
|
|
|
|
LyricsPanel::~LyricsPanel()
|
|
{
|
|
}
|
|
|
|
#define I_FIRST_REAL_SYLLABLE 2
|
|
|
|
void LyricsPanel::Clear()
|
|
{
|
|
mSyllables.clear();
|
|
mText = wxT("");
|
|
|
|
// Add two dummy syllables at the beginning
|
|
mSyllables.push_back(Syllable());
|
|
mSyllables[0].t = -2.0;
|
|
mSyllables.push_back(Syllable());
|
|
mSyllables[1].t = -1.0;
|
|
|
|
mHighlightTextCtrl->Clear();
|
|
}
|
|
|
|
void LyricsPanel::AddLabels(const LabelTrack *pLT)
|
|
{
|
|
const size_t numLabels = pLT->GetNumLabels();
|
|
wxString highlightText;
|
|
for (size_t ii = 0; ii < numLabels; ++ii) {
|
|
const LabelStruct *const pLabel = pLT->GetLabel(ii);
|
|
Add(pLabel->getT0(), pLabel->title, highlightText);
|
|
}
|
|
mHighlightTextCtrl->AppendText(highlightText);
|
|
}
|
|
|
|
void LyricsPanel::Add(double t, const wxString &syllable, wxString &highlightText)
|
|
{
|
|
int i = mSyllables.size();
|
|
|
|
{
|
|
Syllable &prevSyllable = mSyllables[i - 1];
|
|
|
|
if (prevSyllable.t == t) {
|
|
// We can't have two syllables with the same time, so append
|
|
// this to the end of the previous one if they're at the
|
|
// same time.
|
|
prevSyllable.text += syllable;
|
|
prevSyllable.textWithSpace += syllable;
|
|
prevSyllable.char1 += syllable.length();
|
|
return;
|
|
}
|
|
}
|
|
|
|
mSyllables.push_back(Syllable());
|
|
Syllable &thisSyllable = mSyllables[i];
|
|
thisSyllable.t = t;
|
|
thisSyllable.text = syllable;
|
|
|
|
thisSyllable.char0 = mText.length();
|
|
|
|
// Put a space between syllables unless the previous one
|
|
// ended in a hyphen
|
|
if (i > 0 &&
|
|
// mSyllables[i-1].text.length() > 0 &&
|
|
mSyllables[i - 1].text.Right(1) != wxT("-"))
|
|
thisSyllable.textWithSpace = wxT(" ") + syllable;
|
|
else
|
|
thisSyllable.textWithSpace = syllable;
|
|
|
|
mText += thisSyllable.textWithSpace;
|
|
thisSyllable.char1 = mText.length();
|
|
|
|
int nTextLen = thisSyllable.textWithSpace.length();
|
|
if ((nTextLen > 0) && (thisSyllable.textWithSpace.Right(1) == wxT("_")))
|
|
highlightText += (thisSyllable.textWithSpace.Left(nTextLen - 1) + wxT("\n"));
|
|
else
|
|
highlightText += thisSyllable.textWithSpace;
|
|
}
|
|
|
|
void LyricsPanel::Finish(double finalT)
|
|
{
|
|
// Add 3 dummy syllables at the end
|
|
int i = mSyllables.size();
|
|
mSyllables.push_back(Syllable());
|
|
mSyllables[i].t = finalT + 1.0;
|
|
mSyllables.push_back(Syllable());
|
|
mSyllables[i+1].t = finalT + 2.0;
|
|
mSyllables.push_back(Syllable());
|
|
mSyllables[i+2].t = finalT + 3.0;
|
|
|
|
// Mark measurements as invalid
|
|
mMeasurementsDone = false; // only for drawn text
|
|
mCurrentSyllable = 0;
|
|
mHighlightTextCtrl->ShowPosition(0);
|
|
}
|
|
|
|
// Binary-search for the syllable whose char0 <= startChar <= char1.
|
|
int LyricsPanel::FindSyllable(long startChar)
|
|
{
|
|
int i1, i2;
|
|
|
|
i1 = 0;
|
|
i2 = mSyllables.size();
|
|
while (i2 > i1+1) {
|
|
int pmid = (i1+i2)/2;
|
|
if (mSyllables[pmid].char0 > startChar)
|
|
i2 = pmid;
|
|
else
|
|
i1 = pmid;
|
|
}
|
|
|
|
if (i1 < 2)
|
|
i1 = 2;
|
|
if (i1 > (int)(mSyllables.size()) - 3)
|
|
i1 = mSyllables.size() - 3;
|
|
|
|
return i1;
|
|
}
|
|
|
|
void LyricsPanel::SetLyricsStyle(const LyricsStyle newLyricsStyle)
|
|
{
|
|
if (mLyricsStyle == newLyricsStyle)
|
|
return;
|
|
|
|
mLyricsStyle = newLyricsStyle;
|
|
mHighlightTextCtrl->Show(mLyricsStyle == kHighlightLyrics);
|
|
|
|
wxSizeEvent ignore;
|
|
this->OnSize(ignore);
|
|
}
|
|
|
|
|
|
unsigned int LyricsPanel::GetDefaultFontSize() const
|
|
{
|
|
return (mLyricsStyle == kBouncingBallLyrics) ? 48 : 10;
|
|
}
|
|
|
|
void LyricsPanel::SetDrawnFont(wxDC *dc)
|
|
{
|
|
dc->SetFont(wxFont(mKaraokeFontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
|
|
}
|
|
|
|
void LyricsPanel::SetHighlightFont() // for kHighlightLyrics
|
|
{
|
|
wxFont newFont(mKaraokeFontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
|
|
mHighlightTextCtrl->SetDefaultStyle(wxTextAttr(wxNullColour, wxNullColour, newFont));
|
|
mHighlightTextCtrl->SetStyle(0, mHighlightTextCtrl->GetLastPosition(),
|
|
wxTextAttr(wxNullColour, wxNullColour, newFont));
|
|
}
|
|
|
|
void LyricsPanel::Measure(wxDC *dc) // only for drawn text
|
|
{
|
|
this->SetDrawnFont(dc);
|
|
int width = 0, height = 0;
|
|
|
|
const int kIndent = 4;
|
|
int x = 2*kIndent;
|
|
|
|
unsigned int i;
|
|
for(i = 0; i < mSyllables.size(); i++) {
|
|
if ((i < I_FIRST_REAL_SYLLABLE) || // Clear() starts the list with I_FIRST_REAL_SYLLABLE dummies.
|
|
(i >= mSyllables.size() - 3)) // Finish() ends with 3 dummies.
|
|
{
|
|
dc->GetTextExtent(wxT("DUMMY"), &width, &height); // Get the correct height even if we're at i=0.
|
|
width = 0;
|
|
}
|
|
else {
|
|
dc->GetTextExtent(mSyllables[i].textWithSpace, &width, &height);
|
|
}
|
|
|
|
// Add some space between words; the space is normally small but
|
|
// when there's a long pause relative to the previous word, insert
|
|
// extra space.
|
|
int extraWidth;
|
|
if (i >= I_FIRST_REAL_SYLLABLE && i < mSyllables.size() - 2)
|
|
{
|
|
double deltaThis = mSyllables[i+1].t - mSyllables[i].t;
|
|
double deltaPrev = mSyllables[i].t - mSyllables[i-1].t;
|
|
|
|
double ratio;
|
|
if (deltaPrev > 0.0)
|
|
ratio = deltaThis / deltaPrev;
|
|
else
|
|
ratio = deltaThis;
|
|
|
|
if (ratio > 2.0)
|
|
extraWidth = 15 + (int)(15.0 * ratio);
|
|
else
|
|
extraWidth = 15;
|
|
}
|
|
else
|
|
extraWidth = 20;
|
|
|
|
mSyllables[i].width = width + extraWidth;
|
|
mSyllables[i].leftX = x;
|
|
mSyllables[i].x = x + width/2;
|
|
x += mSyllables[i].width;
|
|
}
|
|
|
|
mTextHeight = height;
|
|
|
|
mMeasurementsDone = true;
|
|
}
|
|
|
|
// Binary-search for the syllable with the largest time not greater than t
|
|
int LyricsPanel::FindSyllable(double t)
|
|
{
|
|
int i1, i2;
|
|
|
|
i1 = 0;
|
|
i2 = mSyllables.size();
|
|
while (i2 > i1+1) {
|
|
int pmid = (i1+i2)/2;
|
|
if (mSyllables[pmid].t > t)
|
|
i2 = pmid;
|
|
else
|
|
i1 = pmid;
|
|
}
|
|
|
|
if (i1 < 2)
|
|
i1 = 2;
|
|
if (i1 > (int)(mSyllables.size()) - 3)
|
|
i1 = mSyllables.size() - 3;
|
|
|
|
return i1;
|
|
}
|
|
|
|
// Bouncing Ball:
|
|
// Given the current time t, returns the x/y position of the scrolling
|
|
// karaoke display. For some syllable i, when t==mSyllables[i].t,
|
|
// it will return mSyllables[i].x for outX and 0 for outY.
|
|
// In-between words, outX is interpolated using smooth acceleration
|
|
// between the two neighboring words, and y is a positive number indicating
|
|
// the bouncing ball height
|
|
void LyricsPanel::GetKaraokePosition(double t,
|
|
int *outX, double *outY)
|
|
{
|
|
*outX = 0;
|
|
*outY = 0;
|
|
|
|
if (t < mSyllables[I_FIRST_REAL_SYLLABLE].t || t > mSyllables[mSyllables.size() - 3].t)
|
|
return;
|
|
|
|
int i0, i1, i2, i3;
|
|
int x0, x1, x2, x3;
|
|
double t0, t1, t2, t3;
|
|
i1 = FindSyllable(t);
|
|
i2 = i1 + 1;
|
|
|
|
// Because we've padded the syllables with two dummies at the beginning
|
|
// and end, we know that i0...i3 will always exist. Also, we've made
|
|
// sure that we don't ever have two of the same time, so t2>t1 strictly.
|
|
//
|
|
// t
|
|
// \/
|
|
// time: t0 t1 t2 t3
|
|
// pos: x0 x1 x2 x3
|
|
// index: i0 i1 i2 i3
|
|
// vel: vel1 vel2
|
|
|
|
i0 = i1 - 1;
|
|
i3 = i2 + 1;
|
|
|
|
x0 = mSyllables[i0].x;
|
|
x1 = mSyllables[i1].x;
|
|
x2 = mSyllables[i2].x;
|
|
x3 = mSyllables[i3].x;
|
|
|
|
t0 = mSyllables[i0].t;
|
|
t1 = mSyllables[i1].t;
|
|
t2 = mSyllables[i2].t;
|
|
t3 = mSyllables[i3].t;
|
|
|
|
double linear_vel0 = (x1 - x0) / (t1 - t0);
|
|
double linear_vel1 = (x2 - x1) / (t2 - t1);
|
|
double linear_vel2 = (x3 - x2) / (t3 - t2);
|
|
|
|
// average velocities
|
|
double v1 = (linear_vel0 + linear_vel1) / 2;
|
|
double v2 = (linear_vel1 + linear_vel2) / 2;
|
|
|
|
// Solve a cubic equation f(t) = at^3 + bt^2 + ct + d
|
|
// which gives the position x as a function of
|
|
// (t - t1), by constraining f(0), f'(0), f(t2-t1), f'(t2-t1)
|
|
double delta_t = t2 - t1;
|
|
double delta_x = x2 - x1;
|
|
v1 *= delta_t;
|
|
v2 *= delta_t;
|
|
double a = v1 + v2 - 2*delta_x;
|
|
double b = 3*delta_x - 2*v1 - v2;
|
|
double c = v1;
|
|
double d = x1;
|
|
|
|
t = (t - t1) / (t2 - t1);
|
|
double xx = a*t*t*t + b*t*t + c*t + d;
|
|
|
|
// Unfortunately sometimes our cubic goes backwards. This is a quick
|
|
// hack to stop that from happening.
|
|
if (xx < x1)
|
|
xx = x1;
|
|
|
|
*outX = (int)xx;
|
|
|
|
// The y position is a simple cosine curve; the max height is a
|
|
// function of the time.
|
|
double height = t2 - t1 > 4.0? 1.0: sqrt((t2-t1)/4.0);
|
|
*outY = height * sin(M_PI * t);
|
|
}
|
|
|
|
void LyricsPanel::Update(double t)
|
|
{
|
|
if (t < 0.0)
|
|
{
|
|
// TrackPanel::OnTimer passes gAudioIO->GetStreamTime(), which is -DBL_MAX if !IsStreamActive().
|
|
// In that case, use the selection start time.
|
|
auto pProj = FindProjectFromWindow( this );
|
|
const auto &selectedRegion = ViewInfo::Get( *pProj ).selectedRegion;
|
|
mT = selectedRegion.t0();
|
|
}
|
|
else
|
|
mT = t;
|
|
|
|
if (mLyricsStyle == kBouncingBallLyrics)
|
|
{
|
|
wxRect karaokeRect(0, 0, mWidth, mKaraokeHeight);
|
|
this->Refresh(false, &karaokeRect);
|
|
}
|
|
|
|
int i = FindSyllable(mT);
|
|
if (i == mCurrentSyllable)
|
|
return;
|
|
|
|
mCurrentSyllable = i;
|
|
|
|
if (mLyricsStyle == kHighlightLyrics)
|
|
{
|
|
mHighlightTextCtrl->SetSelection(mSyllables[i].char0, mSyllables[i].char1);
|
|
|
|
//v No trail for now.
|
|
//// Leave a trail behind the selection, by highlighting.
|
|
//if (i == I_FIRST_REAL_SYLLABLE)
|
|
// // Reset the trail to zero.
|
|
// mHighlightTextCtrl->SetStyle(0, mHighlightTextCtrl->GetLastPosition(), wxTextAttr(wxNullColour, *wxWHITE));
|
|
//// Mark the trail for mSyllables[i].
|
|
//mHighlightTextCtrl->SetStyle(mSyllables[i].char0, mSyllables[i].char1, wxTextAttr(wxNullColour, *wxLIGHT_GREY));
|
|
|
|
//v Too much flicker: mHighlightTextCtrl->ShowPosition(mSyllables[i].char0);
|
|
}
|
|
}
|
|
|
|
void LyricsPanel::UpdateLyrics(wxEvent &e)
|
|
{
|
|
e.Skip();
|
|
|
|
// It's crucial to not do that repopulating during playback.
|
|
auto gAudioIO = AudioIOBase::Get();
|
|
if (gAudioIO->IsStreamActive()) {
|
|
mDelayedUpdate = true;
|
|
return;
|
|
}
|
|
|
|
Clear();
|
|
|
|
if (!mProject)
|
|
return;
|
|
|
|
// Lyrics come from only the first label track.
|
|
auto pLabelTrack =
|
|
*TrackList::Get( *mProject ).Any< const LabelTrack >().begin();
|
|
if (!pLabelTrack)
|
|
return;
|
|
|
|
// The code that updates the lyrics is rather expensive when there
|
|
// are a lot of labels.
|
|
// So - bail out early if the lyrics window is not visible.
|
|
// We will later force an update when the lyrics window is made visible.
|
|
auto parent = dynamic_cast<wxFrame*>(GetParent());
|
|
if( !(parent && parent->IsVisible()) )
|
|
return;
|
|
|
|
AddLabels(pLabelTrack);
|
|
Finish(pLabelTrack->GetEndTime());
|
|
const auto &selectedRegion = ViewInfo::Get( *mProject ).selectedRegion;
|
|
Update(selectedRegion.t0());
|
|
}
|
|
|
|
void LyricsPanel::OnStartStop(wxCommandEvent &e)
|
|
{
|
|
e.Skip();
|
|
if ( !e.GetInt() && mDelayedUpdate ) {
|
|
mDelayedUpdate = false;
|
|
UpdateLyrics( e );
|
|
}
|
|
}
|
|
|
|
void LyricsPanel::OnShow(wxShowEvent &e)
|
|
{
|
|
e.Skip();
|
|
if (e.IsShown())
|
|
UpdateLyrics(e);
|
|
}
|
|
|
|
void LyricsPanel::OnKeyEvent(wxKeyEvent & event)
|
|
{
|
|
auto project = FindProjectFromWindow( this );
|
|
auto &commandManager = CommandManager::Get( *project );
|
|
commandManager.FilterKeyEvent(project, event, true);
|
|
event.Skip();
|
|
}
|
|
|
|
void LyricsPanel::OnPaint(wxPaintEvent & WXUNUSED(event))
|
|
{
|
|
wxPaintDC dc(this);
|
|
DoPaint(dc);
|
|
}
|
|
|
|
void LyricsPanel::DoPaint(wxDC &dc)
|
|
{
|
|
if (!this->GetParent()->IsShown())
|
|
return;
|
|
|
|
if (mLyricsStyle == kBouncingBallLyrics)
|
|
{
|
|
if (!mMeasurementsDone)
|
|
Measure(&dc);
|
|
|
|
#ifdef __WXMAC__
|
|
// Mac OS X automatically double-buffers the screen for you,
|
|
// so our bitmap is unnecessary
|
|
HandlePaint(dc);
|
|
#else
|
|
wxBitmap bitmap(mWidth, mKaraokeHeight);
|
|
wxMemoryDC memDC;
|
|
memDC.SelectObject(bitmap);
|
|
HandlePaint(memDC);
|
|
dc.Blit(0, 0, mWidth, mKaraokeHeight, &memDC, 0, 0, wxCOPY, FALSE);
|
|
#endif
|
|
}
|
|
else // (mLyricsStyle == kHighlightLyrics)
|
|
{
|
|
//v causes flicker in ported version
|
|
// this->SetHighlightFont();
|
|
}
|
|
}
|
|
|
|
void LyricsPanel::OnSize(wxSizeEvent & WXUNUSED(event))
|
|
{
|
|
GetClientSize(&mWidth, &mHeight);
|
|
|
|
mKaraokeHeight = mHeight;
|
|
|
|
mKaraokeFontSize =
|
|
(int)((float)(this->GetDefaultFontSize() * mHeight) / (float)LYRICS_DEFAULT_HEIGHT);
|
|
// Usually don't get the size window we want, usually less than
|
|
// LYRICS_DEFAULT_HEIGHT, so bump it a little.
|
|
mKaraokeFontSize += 2;
|
|
|
|
if (mLyricsStyle == kBouncingBallLyrics)
|
|
{
|
|
mMeasurementsDone = false;
|
|
wxClientDC dc(this);
|
|
this->DoPaint(dc);
|
|
}
|
|
else // (mLyricsStyle == kHighlightLyrics)
|
|
{
|
|
mHighlightTextCtrl->SetSize(mWidth, mKaraokeHeight);
|
|
this->SetHighlightFont();
|
|
}
|
|
|
|
this->Refresh(false);
|
|
}
|
|
|
|
//v Doesn't seem to be a way to capture a selection event in a read-only wxTextCtrl.
|
|
//void LyricsPanel::OnHighlightTextCtrl(wxCommandEvent & event)
|
|
//{
|
|
// long from, to;
|
|
//
|
|
// mHighlightTextCtrl->GetSelection(&from, &to);
|
|
// // TODO: Find the start time of the corresponding syllable and set playback to start there.
|
|
//}
|
|
|
|
void LyricsPanel::HandlePaint(wxDC &dc)
|
|
{
|
|
wxASSERT(mLyricsStyle == kBouncingBallLyrics);
|
|
dc.SetBrush(*wxWHITE_BRUSH);
|
|
dc.DrawRectangle(0, 0, mWidth, mKaraokeHeight);
|
|
|
|
this->HandlePaint_BouncingBall(dc);
|
|
}
|
|
|
|
void LyricsPanel::HandlePaint_BouncingBall(wxDC &dc)
|
|
{
|
|
int ctr = mWidth / 2;
|
|
int x;
|
|
double y;
|
|
GetKaraokePosition(mT, &x, &y);
|
|
|
|
dc.SetTextForeground(wxColour(238, 0, 102));
|
|
bool changedColor = false;
|
|
|
|
SetDrawnFont(&dc);
|
|
unsigned int i;
|
|
wxCoord yTextTop = mKaraokeHeight - mTextHeight - 4;
|
|
for(i = 0; i < mSyllables.size(); i++) {
|
|
if (mSyllables[i].x + mSyllables[i].width < (x - ctr))
|
|
continue;
|
|
if (mSyllables[i].x > x + ctr)
|
|
continue;
|
|
|
|
if (!changedColor && mSyllables[i].x >= x) {
|
|
dc.SetTextForeground(*wxBLACK);
|
|
changedColor = true;
|
|
}
|
|
|
|
wxString text = mSyllables[i].text;
|
|
if (text.length() > 0 && text.Right(1) == wxT("_")) {
|
|
text = text.Left(text.length() - 1);
|
|
}
|
|
|
|
dc.DrawText(text,
|
|
mSyllables[i].leftX + ctr - x,
|
|
yTextTop);
|
|
}
|
|
|
|
int ballRadius = (int)(mTextHeight / 8.0);
|
|
int bounceTop = ballRadius * 2;
|
|
int bounceHeight = yTextTop - bounceTop;
|
|
int yi = (int)(yTextTop - 4 - (y * bounceHeight));
|
|
|
|
if (mT >= 0.0) {
|
|
wxRect ball(ctr - ballRadius, yi - ballRadius, 2 * ballRadius, 2 * ballRadius);
|
|
dc.SetBrush(wxBrush(wxColour(238, 0, 102), wxBRUSHSTYLE_SOLID));
|
|
dc.DrawEllipse(ball);
|
|
}
|
|
}
|
|
|