1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-05-03 17:19:43 +02:00

Merge branch 'master' into scrollplay

This commit is contained in:
Paul Licameli 2016-06-01 13:24:29 -04:00
commit d62442827c
29 changed files with 5082 additions and 5230 deletions

2
.gitignore vendored
View File

@ -108,8 +108,6 @@ mac/tests/
*.tlog
*.ipch
*.opensdf
# unsure about the .sal files. Disable for now.
*.sal
*.vcxproj.user
# Precompiled Headers

View File

@ -344,6 +344,8 @@ double AudioIO::mCachedBestRateOut;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
#include "tracks/ui/Scrubbing.h"
/*
This work queue class, with the aid of the playback ring
buffers, coordinates three threads during scrub play:
@ -370,33 +372,35 @@ So a small, fixed queue size should be adequate.
struct AudioIO::ScrubQueue
{
ScrubQueue(double t0, double t1, wxLongLong startClockMillis,
double minTime, double maxTime,
double rate, double maxSpeed, double minStutter)
double rate, long maxDebt,
const ScrubbingOptions &options)
: mTrailingIdx(0)
, mMiddleIdx(1)
, mLeadingIdx(2)
, mMinSample(minTime * rate)
, mMaxSample(maxTime * rate)
, mLeadingIdx(1)
, mRate(rate)
, mMinStutter(lrint(std::max(0.0, minStutter) * mRate))
, mLastScrubTimeMillis(startClockMillis)
, mUpdating()
, mMaxDebt { maxDebt }
{
bool success = InitEntry(mEntries[mMiddleIdx],
t0, t1, maxSpeed, false, NULL, false);
if (!success)
{
// StartClock equals now? Really?
--mLastScrubTimeMillis;
success = InitEntry(mEntries[mMiddleIdx],
t0, t1, maxSpeed, false, NULL, false);
const long s0 = std::max(options.minSample, std::min(options.maxSample,
lrint(t0 * mRate)
));
const long s1 = lrint(t1 * mRate);
Duration dd { *this };
long actualDuration = std::max(1L, dd.duration);
auto success = mEntries[mMiddleIdx].Init(nullptr,
s0, s1, actualDuration, options);
if (success)
++mLeadingIdx;
else {
// If not, we can wait to enqueue again later
dd.Cancel();
}
wxASSERT(success);
// So the play indicator starts out unconfused:
{
Entry &entry = mEntries[mTrailingIdx];
entry.mS0 = entry.mS1 = mEntries[mMiddleIdx].mS0;
entry.mS0 = entry.mS1 = s0;
entry.mPlayed = entry.mDuration = 1;
}
}
@ -419,30 +423,57 @@ struct AudioIO::ScrubQueue
mAvailable.Signal();
}
bool Producer(double end, double maxSpeed, bool bySpeed, bool maySkip)
bool Producer(double end, const ScrubbingOptions &options)
{
// Main thread indicates a scrubbing interval
// MAY ADVANCE mLeadingIdx, BUT IT NEVER CATCHES UP TO mTrailingIdx.
wxMutexLocker locker(mUpdating);
const unsigned next = (mLeadingIdx + 1) % Size;
bool result = true;
unsigned next = (mLeadingIdx + 1) % Size;
if (next != mTrailingIdx)
{
Entry &previous = mEntries[(mLeadingIdx + Size - 1) % Size];
auto current = &mEntries[mLeadingIdx];
auto previous = &mEntries[(mLeadingIdx + Size - 1) % Size];
// Use the previous end as NEW start.
const double startTime = previous.mS1 / mRate;
// Might reject the request because of zero duration,
// or a too-short "stutter"
const bool success =
(InitEntry(mEntries[mLeadingIdx], startTime, end, maxSpeed,
bySpeed, &previous, maySkip));
if (success) {
const long s0 = previous->mS1;
Duration dd { *this };
const auto &origDuration = dd.duration;
if (origDuration <= 0)
return false;
auto actualDuration = origDuration;
const long s1 = options.enqueueBySpeed
? s0 + lrint(origDuration * end) // end is a speed
: lrint(end * mRate); // end is a time
auto success =
current->Init(previous, s0, s1, actualDuration, options);
if (success)
mLeadingIdx = next;
mAvailable.Signal();
else {
dd.Cancel();
return false;
}
return success;
// Fill up the queue with some silence if there was trimming
wxASSERT(actualDuration <= origDuration);
if (actualDuration < origDuration) {
next = (mLeadingIdx + 1) % Size;
if (next != mTrailingIdx) {
previous = &mEntries[(mLeadingIdx + Size - 1) % Size];
current = &mEntries[mLeadingIdx];
current->InitSilent(*previous, origDuration - actualDuration);
mLeadingIdx = next;
}
else
// Oops, can't enqueue the silence -- so do what?
;
}
mAvailable.Signal();
return result;
}
else
{
@ -455,33 +486,85 @@ struct AudioIO::ScrubQueue
}
}
void Transformer(long &startSample, long &endSample, long &duration)
void Transformer(long &startSample, long &endSample, long &duration,
Maybe<wxMutexLocker> &cleanup)
{
// Audio thread is ready for the next interval.
// MAY ADVANCE mMiddleIdx, WHICH MAY EQUAL mLeadingIdx, BUT DOES NOT PASS IT.
wxMutexLocker locker(mUpdating);
bool checkDebt = false;
if (!cleanup) {
cleanup.create(mUpdating);
// Check for cancellation of work only when re-enetering the cricial section
checkDebt = true;
}
while(!mNudged && mMiddleIdx == mLeadingIdx)
mAvailable.Wait();
mNudged = false;
if (mMiddleIdx != mLeadingIdx)
{
// There is work in the queue
auto now = ::wxGetLocalTimeMillis();
if (checkDebt &&
mLastTransformerTimeMillis >= 0 && // Not the first time for this scrub
mMiddleIdx != mLeadingIdx) {
// There is work in the queue, but if Producer is outrunning us, discard some,
// which may make a skip yet keep playback better synchronized with user gestures.
const auto interval = (now - mLastTransformerTimeMillis).ToDouble() / 1000.0;
const Entry &previous = mEntries[(mMiddleIdx + Size - 1) % Size];
const auto deficit =
static_cast<long>(interval * mRate) - // Samples needed in the last time interval
mCredit; // Samples done in the last time interval
mCredit = 0;
mDebt += deficit;
auto toDiscard = mDebt - mMaxDebt;
while (toDiscard > 0 && mMiddleIdx != mLeadingIdx) {
// Cancel some debt (discard some new work)
auto &entry = mEntries[mMiddleIdx];
auto &dur = entry.mDuration;
if (toDiscard >= dur) {
// Discard entire queue entry
mDebt -= dur;
toDiscard -= dur;
dur = 0; // So Consumer() will handle abandoned entry correctly
mMiddleIdx = (mMiddleIdx + 1) % Size;
}
else {
// Adjust the start time
auto &start = entry.mS0;
const auto end = entry.mS1;
const auto ratio = static_cast<double>(toDiscard) / static_cast<double>(dur);
const auto adjustment = static_cast<long>(std::abs(end - start) * ratio);
if (start <= end)
start += adjustment;
else
start -= adjustment;
mDebt -= toDiscard;
dur -= toDiscard;
toDiscard = 0;
}
}
}
if (mMiddleIdx != mLeadingIdx) {
// There is still work in the queue, after cancelling debt
Entry &entry = mEntries[mMiddleIdx];
startSample = entry.mS0;
endSample = entry.mS1;
duration = entry.mDuration;
const unsigned next = (mMiddleIdx + 1) % Size;
mMiddleIdx = next;
mMiddleIdx = (mMiddleIdx + 1) % Size;
mCredit += duration;
}
else
{
// We got the shut-down signal, or we got nudged
else {
// We got the shut-down signal, or we got nudged, or we discarded all the work.
startSample = endSample = duration = -1L;
}
if (checkDebt)
mLastTransformerTimeMillis = now;
}
double Consumer(unsigned long frames)
@ -530,21 +613,26 @@ private:
, mPlayed(0)
{}
bool Init(long s0, long s1, long duration, Entry *previous,
double maxSpeed, long minStutter, long minSample, long maxSample,
bool adjustStart)
bool Init(Entry *previous, long s0, long s1,
long &duration /* in/out */,
const ScrubbingOptions &options)
{
if (duration <= 0)
return false;
double speed = double(abs(s1 - s0)) / duration;
bool maxed = false;
const bool &adjustStart = options.adjustStart;
// May change the requested speed (or reject)
if (!adjustStart && speed > maxSpeed)
wxASSERT(duration > 0);
double speed = static_cast<double>(std::abs(s1 - s0)) / duration;
bool adjustedSpeed = false;
auto minSpeed = std::min(options.minSpeed, options.maxSpeed);
wxASSERT(minSpeed == options.minSpeed);
// May change the requested speed and duration
if (!adjustStart && speed > options.maxSpeed)
{
// Reduce speed to the maximum selected in the user interface.
speed = maxSpeed;
maxed = true;
speed = options.maxSpeed;
mGoal = s1;
adjustedSpeed = true;
}
else if (!adjustStart &&
previous &&
@ -556,86 +644,76 @@ private:
// continue at no less than maximum. (Without this
// the final catch-up can make a slow scrub interval
// that drops the pitch and sounds wrong.)
duration = lrint(speed * duration / maxSpeed);
if (duration <= 0)
{
previous->mGoal = -1;
return false;
}
speed = maxSpeed;
maxed = true;
}
if (speed < GetMinScrubSpeed())
// Mixers were set up to go only so slowly, not slower.
// This will put a request for some silence in the work queue.
speed = 0.0;
// Before we change s1:
mGoal = maxed ? s1 : -1;
// May change s1 or s0 to match speed change:
if (adjustStart)
{
bool silent = false;
// Adjust s1 first, and duration, if s1 is out of bounds.
// (Assume s0 is in bounds, because it is the last scrub's s1 which was checked.)
if (s1 != s0)
{
const long newS1 = std::max(minSample, std::min(maxSample, s1));
if (s1 != newS1)
{
long newDuration = long(duration * double(newS1 - s0) / (s1 - s0));
s1 = newS1;
if (newDuration == 0)
// Enqueue a silent scrub with s0 == s1
silent = true;
else
// Shorten
duration = newDuration;
}
}
if (!silent)
{
// When playback follows a fast mouse movement by "stuttering"
// at maximum playback, don't make stutters too short to be useful.
if (duration < minStutter)
return false;
// Limit diff because this is seeking.
const long diff = lrint(std::min(1.0, speed) * duration);
if (s0 < s1)
s0 = s1 - diff;
else
s0 = s1 + diff;
}
minSpeed = options.maxSpeed;
mGoal = s1;
adjustedSpeed = true;
}
else
mGoal = -1;
if (speed < minSpeed) {
// Trim the duration.
duration = std::max(0L, lrint(speed * duration / minSpeed));
speed = minSpeed;
adjustedSpeed = true;
}
if (speed < ScrubbingOptions::MinAllowedScrubSpeed()) {
// Mixers were set up to go only so slowly, not slower.
// This will put a request for some silence in the work queue.
adjustedSpeed = true;
speed = 0.0;
}
// May change s1 or s0 to match speed change or stay in bounds of the project
if (adjustedSpeed && !adjustStart)
{
// adjust end
// adjust s1
const long diff = lrint(speed * duration);
if (s0 < s1)
s1 = s0 + diff;
else
s1 = s0 - diff;
}
// Adjust s1 again, and duration, if s1 is out of bounds. (Assume s0 is in bounds.)
if (s1 != s0)
{
const long newS1 = std::max(minSample, std::min(maxSample, s1));
if (s1 != newS1)
{
long newDuration = long(duration * double(newS1 - s0) / (s1 - s0));
s1 = newS1;
if (newDuration == 0)
// Enqueue a silent scrub with s0 == s1
;
else
// Shorten
duration = newDuration;
}
bool silent = false;
// Adjust s1 (again), and duration, if s1 is out of bounds,
// or abandon if a stutter is too short.
// (Assume s0 is in bounds, because it equals the last scrub's s1 which was checked.)
if (s1 != s0)
{
long newDuration = duration;
const long newS1 = std::max(options.minSample, std::min(options.maxSample, s1));
if(s1 != newS1)
newDuration = std::max(0L,
static_cast<long>(duration * static_cast<double>(newS1 - s0) / (s1 - s0))
);
// When playback follows a fast mouse movement by "stuttering"
// at maximum playback, don't make stutters too short to be useful.
if (options.adjustStart && newDuration < options.minStutter)
return false;
else if (newDuration == 0) {
// Enqueue a silent scrub with s0 == s1
silent = true;
s1 = s0;
}
else if (s1 != newS1) {
// Shorten
duration = newDuration;
s1 = newS1;
}
}
if (adjustStart && !silent)
{
// Limit diff because this is seeking.
const long diff = lrint(std::min(options.maxSpeed, speed) * duration);
if (s0 < s1)
s0 = s1 - diff;
else
s0 = s1 + diff;
}
mS0 = s0;
@ -645,9 +723,20 @@ private:
return true;
}
void InitSilent(const Entry &previous, long duration)
{
mGoal = previous.mGoal;
mS0 = mS1 = previous.mS1;
mPlayed = 0;
mDuration = duration;
}
double GetTime(double rate) const
{
return (mS0 + ((mS1 - mS0) * mPlayed) / double(mDuration)) / rate;
return
(mS0 +
(mS1 - mS0) * static_cast<double>(mPlayed) / static_cast<double>(mDuration))
/ rate;
}
// These sample counts are initialized in the UI, producer, thread:
@ -667,33 +756,37 @@ private:
long mPlayed;
};
bool InitEntry(Entry &entry, double t0, double end, double maxSpeed,
bool bySpeed, Entry *previous, bool maySkip)
{
const wxLongLong clockTime(::wxGetLocalTimeMillis());
const long duration =
mRate * (clockTime - mLastScrubTimeMillis).ToDouble() / 1000.0;
const long s0 = t0 * mRate;
const long s1 = bySpeed
? s0 + lrint(duration * end) // end is a speed
: lrint(end * mRate); // end is a time
const bool success =
entry.Init(s0, s1, duration, previous, maxSpeed, mMinStutter,
mMinSample, mMaxSample, maySkip);
if (success)
mLastScrubTimeMillis = clockTime;
return success;
}
struct Duration {
Duration (ScrubQueue &queue_) : queue(queue_) {}
~Duration ()
{
if(!cancelled)
queue.mLastScrubTimeMillis = clockTime;
}
void Cancel() { cancelled = true; }
ScrubQueue &queue;
const wxLongLong clockTime { ::wxGetLocalTimeMillis() };
const long duration { static_cast<long>
(queue.mRate * (clockTime - queue.mLastScrubTimeMillis).ToDouble() / 1000.0)
};
bool cancelled { false };
};
enum { Size = 10 };
Entry mEntries[Size];
unsigned mTrailingIdx;
unsigned mMiddleIdx;
unsigned mLeadingIdx;
const long mMinSample, mMaxSample;
const double mRate;
const long mMinStutter;
wxLongLong mLastScrubTimeMillis;
wxLongLong mLastTransformerTimeMillis { -1LL };
long mCredit { 0L };
long mDebt { 0L };
const long mMaxDebt;
mutable wxMutex mUpdating;
mutable wxCondition mAvailable { mUpdating };
bool mNudged { false };
@ -1354,10 +1447,6 @@ bool AudioIO::StartPortAudioStream(double sampleRate,
// pick a rate to do the audio I/O at, from those available. The project
// rate is suggested, but we may get something else if it isn't supported
mRate = GetBestRate(numCaptureChannels > 0, numPlaybackChannels > 0, sampleRate);
if (mListener) {
// advertise the chosen I/O sample rate to the UI
mListener->OnAudioIORate((int)mRate);
}
// Special case: Our 24-bit sample format is different from PortAudio's
// 3-byte packed format. So just make PortAudio return float samples,
@ -1511,6 +1600,12 @@ void AudioIO::StartMonitoring(double sampleRate)
// Now start the PortAudio stream!
mLastPaError = Pa_StartStream( mPortStreamV19 );
// Update UI display only now, after all possibilities for error are past.
if ((mLastPaError == paNoError) && mListener) {
// advertise the chosen I/O sample rate to the UI
mListener->OnAudioIORate((int)mRate);
}
}
int AudioIO::StartStream(const WaveTrackArray &playbackTracks,
@ -1518,12 +1613,14 @@ int AudioIO::StartStream(const WaveTrackArray &playbackTracks,
#ifdef EXPERIMENTAL_MIDI_OUT
const NoteTrackArray &midiPlaybackTracks,
#endif
double sampleRate, double t0, double t1,
double t0, double t1,
const AudioIOStartStreamOptions &options)
{
if( IsBusy() )
return 0;
const auto &sampleRate = options.rate;
// We just want to set mStreamToken to -1 - this way avoids
// an extremely rare but possible race condition, if two functions
// somehow called StartStream at the same time...
@ -1580,26 +1677,27 @@ int AudioIO::StartStream(const WaveTrackArray &playbackTracks,
mCaptureBuffers = NULL;
mResample = NULL;
double playbackTime = 4.0;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
bool scrubbing = (options.pScrubbingOptions != nullptr);
// Scrubbing is not compatible with looping or recording or a time track!
const double scrubDelay = lrint(options.scrubDelay * sampleRate) / sampleRate;
bool scrubbing = (scrubDelay > 0);
double maxScrubSpeed = options.maxScrubSpeed;
double minScrubStutter = options.minScrubStutter;
if (scrubbing)
{
const auto &scrubOptions = *options.pScrubbingOptions;
if (mCaptureTracks->size() > 0 ||
mPlayMode == PLAY_LOOPED ||
mTimeTrack != NULL ||
options.maxScrubSpeed < GetMinScrubSpeed())
{
scrubOptions.maxSpeed < ScrubbingOptions::MinAllowedScrubSpeed()) {
wxASSERT(false);
scrubbing = false;
}
}
if (scrubbing)
{
mPlayMode = PLAY_SCRUB;
else {
playbackTime = lrint(scrubOptions.delay * sampleRate) / sampleRate;
mPlayMode = PLAY_SCRUB;
}
}
#endif
@ -1640,11 +1738,6 @@ int AudioIO::StartStream(const WaveTrackArray &playbackTracks,
// mouse input, so make fillings more and shorter.
// What Audio thread produces for playback is then consumed by the PortAudio
// thread, in many smaller pieces.
double playbackTime = 4.0;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
if (scrubbing)
playbackTime = scrubDelay;
#endif
mPlaybackSamplesToCopy = playbackTime * mRate;
// Capacity of the playback buffer.
@ -1737,9 +1830,13 @@ int AudioIO::StartStream(const WaveTrackArray &playbackTracks,
const Mixer::WarpOptions &warpOptions =
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
scrubbing ? Mixer::WarpOptions(GetMinScrubSpeed(), GetMaxScrubSpeed()) :
scrubbing
? Mixer::WarpOptions
(ScrubbingOptions::MinAllowedScrubSpeed(),
ScrubbingOptions::MaxAllowedScrubSpeed())
:
#endif
Mixer::WarpOptions(mTimeTrack);
Mixer::WarpOptions(mTimeTrack);
for (unsigned int i = 0; i < mPlaybackTracks->size(); i++)
{
@ -1857,10 +1954,11 @@ int AudioIO::StartStream(const WaveTrackArray &playbackTracks,
delete mScrubQueue;
if (scrubbing)
{
const auto &scrubOptions = *options.pScrubbingOptions;
mScrubQueue =
new ScrubQueue(mT0, mT1, options.scrubStartClockTimeMillis,
0.0, options.maxScrubTime,
sampleRate, maxScrubSpeed, minScrubStutter);
new ScrubQueue(mT0, mT1, scrubOptions.startClockTimeMillis,
sampleRate, 2 * scrubOptions.minStutter,
scrubOptions);
mScrubDuration = 0;
mSilentScrub = false;
}
@ -1904,6 +2002,12 @@ int AudioIO::StartStream(const WaveTrackArray &playbackTracks,
}
}
// Update UI display only now, after all possibilities for error are past.
if (mListener) {
// advertise the chosen I/O sample rate to the UI
mListener->OnAudioIORate((int)mRate);
}
if (mNumPlaybackChannels > 0)
{
wxCommandEvent e(EVT_AUDIOIO_PLAYBACK);
@ -2293,9 +2397,7 @@ void AudioIO::StopStream()
while( mAudioThreadShouldCallFillBuffersOnce == true )
{
// LLL: Experienced recursive yield here...once.
// PRL: Made it safe yield to avoid a certain recursive event processing in the
// time ruler when switching from scrub to quick play.
wxGetApp().SafeYield(nullptr, true); // Pass true for onlyIfNeeded to avoid recursive call error.
wxGetApp().Yield(true); // Pass true for onlyIfNeeded to avoid recursive call error.
if (mScrubQueue)
mScrubQueue->Nudge();
wxMilliSleep( 50 );
@ -2423,6 +2525,11 @@ void AudioIO::StopStream()
mScrubQueue = 0;
}
#endif
if (mListener) {
// Tell UI to hide sample rate
mListener->OnAudioIORate(0);
}
}
void AudioIO::SetPaused(bool state)
@ -2448,18 +2555,11 @@ bool AudioIO::IsPaused()
}
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
bool AudioIO::EnqueueScrubByPosition(double endTime, double maxSpeed, bool maySkip)
bool AudioIO::EnqueueScrub
(double endTimeOrSpeed, const ScrubbingOptions &options)
{
if (mScrubQueue)
return mScrubQueue->Producer(endTime, maxSpeed, false, maySkip);
else
return false;
}
bool AudioIO::EnqueueScrubBySignedSpeed(double speed, double maxSpeed, bool maySkip)
{
if (mScrubQueue)
return mScrubQueue->Producer(speed, maxSpeed, true, maySkip);
return mScrubQueue->Producer(endTimeOrSpeed, options);
else
return false;
}
@ -3361,6 +3461,7 @@ void AudioIO::FillBuffers()
// PRL: or, when scrubbing, we may get work repeatedly from the
// scrub queue.
bool done = false;
Maybe<wxMutexLocker> cleanup;
do {
// How many samples to produce for each channel.
long frames = available;
@ -3436,7 +3537,7 @@ void AudioIO::FillBuffers()
if (!done && mScrubDuration <= 0)
{
long startSample, endSample;
mScrubQueue->Transformer(startSample, endSample, mScrubDuration);
mScrubQueue->Transformer(startSample, endSample, mScrubDuration, cleanup);
if (mScrubDuration < 0)
{
// Can't play anything
@ -3452,7 +3553,7 @@ void AudioIO::FillBuffers()
double startTime, endTime, speed;
startTime = startSample / mRate;
endTime = endSample / mRate;
speed = double(abs(endSample - startSample)) / mScrubDuration;
speed = double(std::abs(endSample - startSample)) / mScrubDuration;
for (i = 0; i < mPlaybackTracks->size(); i++)
mPlaybackMixers[i]->SetTimesAndSpeed(startTime, endTime, speed);
}
@ -4173,7 +4274,7 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
(gAudioIO->mT0, gAudioIO->mTime);
else
gAudioIO->mWarpedTime = gAudioIO->mTime - gAudioIO->mT0;
gAudioIO->mWarpedTime = abs(gAudioIO->mWarpedTime);
gAudioIO->mWarpedTime = std::abs(gAudioIO->mWarpedTime);
// Reset mixer positions and flush buffers for all tracks
for (i = 0; i < (unsigned int)numPlaybackTracks; i++)

View File

@ -85,52 +85,35 @@ DECLARE_EXPORTED_EVENT_TYPE(AUDACITY_DLL_API, EVT_AUDIOIO_PLAYBACK, -1);
DECLARE_EXPORTED_EVENT_TYPE(AUDACITY_DLL_API, EVT_AUDIOIO_CAPTURE, -1);
DECLARE_EXPORTED_EVENT_TYPE(AUDACITY_DLL_API, EVT_AUDIOIO_MONITOR, -1);
struct ScrubbingOptions;
// To avoid growing the argument list of StartStream, add fields here
struct AudioIOStartStreamOptions
{
AudioIOStartStreamOptions()
explicit
AudioIOStartStreamOptions(double rate_)
: timeTrack(NULL)
, listener(NULL)
, rate(rate_)
, playLooped(false)
, cutPreviewGapStart(0.0)
, cutPreviewGapLen(0.0)
, pStartTime(NULL)
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
, scrubDelay(0.0)
, maxScrubSpeed(1.0)
, minScrubStutter(0.0)
, scrubStartClockTimeMillis(-1)
, maxScrubTime(0.0)
#endif
{}
TimeTrack *timeTrack;
AudioIOListener* listener;
double rate;
bool playLooped;
double cutPreviewGapStart;
double cutPreviewGapLen;
double * pStartTime;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
// Positive value indicates that scrubbing will happen
// Non-null value indicates that scrubbing will happen
// (do not specify a time track, looping, or recording, which
// are all incompatible with scrubbing):
double scrubDelay;
// We need a limiting value for the speed of the first scrub
// interval:
double maxScrubSpeed;
// When maximum speed scrubbing skips to follow the mouse,
// this is the minimum amount of playback at the maximum speed:
double minScrubStutter;
// Scrubbing needs the time of start of the mouse movement that began
// the scrub:
wxLongLong scrubStartClockTimeMillis;
// usually from TrackList::GetEndTime()
double maxScrubTime;
ScrubbingOptions *pScrubbingOptions {};
#endif
};
@ -163,9 +146,8 @@ class AUDACITY_DLL_API AudioIO final {
#ifdef EXPERIMENTAL_MIDI_OUT
const NoteTrackArray &midiTracks,
#endif
double sampleRate, double t0, double t1,
const AudioIOStartStreamOptions &options =
AudioIOStartStreamOptions());
double t0, double t1,
const AudioIOStartStreamOptions &options);
/** \brief Stop recording, playback or input monitoring.
*
@ -180,34 +162,17 @@ class AUDACITY_DLL_API AudioIO final {
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
bool IsScrubbing() { return IsBusy() && mScrubQueue != 0; }
static double GetMaxScrubSpeed() { return 32.0; } // Is five octaves enough for your amusement?
static double GetMinScrubSpeed() { return 0.01; }
/** \brief enqueue a NEW end time, using the last end as the new start,
/** \brief enqueue a NEW scrub play interval, using the last end as the new start,
* to be played over the same duration, as between this and the last
* enqueuing (or the starting of the stream). Except, we do not exceed maximum
* scrub speed, so may need to adjust either the start or the end.
* If maySkip is true, then when mouse movement exceeds maximum scrub speed,
* If options.adjustStart is true, then when mouse movement exceeds maximum scrub speed,
* adjust the beginning of the scrub interval rather than the end, so that
* the scrub skips or "stutters" to stay near the cursor.
* But if the "stutter" is too short for the minimum, then there is no effect
* on the work queue.
* Return true if some work was really enqueued.
* Return true if some sound was really enqueued.
* But if the "stutter" is too short for the minimum, enqueue nothing and return false.
*/
bool EnqueueScrubByPosition(double endTime, double maxSpeed, bool maySkip);
/** \brief enqueue a NEW positive or negative scrubbing speed,
* using the last end as the NEW start,
* to be played over the same duration, as between this and the last
* enqueueing (or the starting of the stream). Except, we do not exceed maximum
* scrub speed, so may need to adjust either the start or the end.
* If maySkip is true, then when mouse movement exceeds maximum scrub speed,
* adjust the beginning of the scrub interval rather than the end, so that
* the scrub skips or "stutters" to stay near the cursor.
* But if the "stutter" is too short for the minimum, then there is no effect
* on the work queue.
* Return true if some work was really enqueued.
*/
bool EnqueueScrubBySignedSpeed(double speed, double maxSpeed, bool maySkip);
bool EnqueueScrub(double endTimeOrSpeed, const ScrubbingOptions &options);
/** \brief return the ending time of the last enqueued scrub interval.
*/

View File

@ -22,7 +22,9 @@ public:
AudioIOListener() {}
virtual ~AudioIOListener() {}
// Pass 0 when audio stops, positive when it starts:
virtual void OnAudioIORate(int rate) = 0;
virtual void OnAudioIOStartRecording() = 0;
virtual void OnAudioIOStopRecording() = 0;
virtual void OnAudioIONewBlockFiles(const AutoSaveFile & blockFileLog) = 0;

View File

@ -1131,7 +1131,7 @@ AudacityProject::~AudacityProject()
AudioIOStartStreamOptions AudacityProject::GetDefaultPlayOptions()
{
AudioIOStartStreamOptions options;
AudioIOStartStreamOptions options { GetRate() };
options.timeTrack = GetTracks()->GetTimeTrack();
options.listener = this;
return options;
@ -4788,7 +4788,10 @@ void AudacityProject::TP_HandleResize()
void AudacityProject::GetPlayRegion(double* playRegionStart,
double *playRegionEnd)
{
mRuler->GetPlayRegion(playRegionStart, playRegionEnd);
if (mRuler)
mRuler->GetPlayRegion(playRegionStart, playRegionEnd);
else
*playRegionEnd = *playRegionStart = 0;
}
void AudacityProject::AutoSave()
@ -4886,7 +4889,13 @@ void AudacityProject::MayStartMonitoring()
void AudacityProject::OnAudioIORate(int rate)
{
wxString display;
display = wxString::Format(_("Actual Rate: %d"), rate);
if (rate > 0) {
display = wxString::Format(_("Actual Rate: %d"), rate);
}
else
// clear the status field
;
int x, y;
mStatusBar->GetTextExtent(display, &x, &y);
int widths[] = {0, GetControlToolBar()->WidthForStatusBar(mStatusBar), -1, x+50};
@ -5369,8 +5378,18 @@ void AudacityProject::PlaybackScroller::OnTimer(wxCommandEvent &event)
// Let other listeners get the notification
event.Skip();
if (mMode != Mode::Off && mProject->IsAudioActive())
{
if(!mProject->IsAudioActive())
return;
else if (mMode == Mode::Refresh) {
// PRL: see comments in Scrubbing.cpp for why this is sometimes needed.
// These unnecessary refreshes cause wheel rotation events to be delivered more uniformly
// to the application, so scrub speed control is smoother.
// (So I see at least with OS 10.10 and wxWidgets 3.0.2.)
// Is there another way to ensure that than by refreshing?
const auto trackPanel = mProject->GetTrackPanel();
trackPanel->Refresh(false);
}
else if (mMode != Mode::Off) {
// Pan the view, so that we center the play indicator.
ViewInfo &viewInfo = mProject->GetViewInfo();

View File

@ -730,6 +730,7 @@ public:
enum class Mode {
Off,
Refresh,
Centered,
Right,
};

View File

@ -256,6 +256,19 @@ private:
}
#endif
friend inline bool operator ==
(const SelectedRegion &lhs, const SelectedRegion &rhs)
{
return
lhs.mT0 == rhs.mT0
&& lhs.mT1 == rhs.mT1
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
&& lhs.mF0 == rhs.mF0
&& lhs.mF1 == rhs.mF1
#endif
;
}
double mT0;
double mT1;
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
@ -265,4 +278,9 @@ private:
};
inline bool operator != (const SelectedRegion &lhs, const SelectedRegion &rhs)
{
return !(lhs == rhs);
}
#endif

View File

@ -65,6 +65,7 @@ and use it for toolbar and window layouts too.
#include "Project.h"
#include "toolbars/ToolBar.h"
#include "toolbars/ToolManager.h"
#include "widgets/Ruler.h"
#include "ImageManipulation.h"
#include "Theme.h"
#include "Experimental.h"
@ -230,6 +231,7 @@ void Theme::ApplyUpdatedImages()
if( pToolBar )
pToolBar->ReCreateButtons();
}
p->GetRulerPanel()->ReCreateButtons();
}
void Theme::RegisterImages()
@ -958,27 +960,41 @@ void ThemeBase::SaveComponents()
if( (mBitmapFlags[i] & resFlagInternal)==0)
{
FileName = FileNames::ThemeComponent( mBitmapNames[i] );
if( !wxFileExists( FileName ))
if( wxFileExists( FileName ))
{
if( !mImages[i].SaveFile( FileName, wxBITMAP_TYPE_PNG ))
{
wxMessageBox(
wxString::Format(
_("Audacity could not save file:\n %s"),
FileName.c_str() ));
return;
}
n++;
++n;
break;
}
}
}
if( n==0 )
if (n > 0)
{
wxMessageBox(
wxString::Format(
_("All required files in:\n %s\nwere already present."),
FileNames::ThemeComponentsDir().c_str() ));
return;
auto result =
wxMessageBox(
wxString::Format(
_("Some required files in:\n %s\nwere already present. Overwrite?"),
FileNames::ThemeComponentsDir().c_str()),
wxMessageBoxCaptionStr,
wxYES_NO | wxNO_DEFAULT);
if(result == wxNO)
return;
}
for(i=0;i<(int)mImages.GetCount();i++)
{
if( (mBitmapFlags[i] & resFlagInternal)==0)
{
FileName = FileNames::ThemeComponent( mBitmapNames[i] );
if( !mImages[i].SaveFile( FileName, wxBITMAP_TYPE_PNG ))
{
wxMessageBox(
wxString::Format(
_("Audacity could not save file:\n %s"),
FileName.c_str() ));
return;
}
}
}
wxMessageBox(
wxString::Format(

File diff suppressed because it is too large Load Diff

View File

@ -1167,7 +1167,7 @@ bool TrackList::MoveDown(Track * t)
return false;
}
bool TrackList::Contains(Track * t) const
bool TrackList::Contains(const Track * t) const
{
return std::find_if(begin(), end(),
[=](const value_type &track) { return t == track.get(); }

View File

@ -492,7 +492,7 @@ class TrackList final : public wxEvtHandler, public ListOfTracks
#endif
/// Mainly a test function. Uses a linear search, so could be slow.
bool Contains(Track * t) const;
bool Contains(const Track * t) const;
bool IsEmpty() const;
int GetCount() const;

View File

@ -659,6 +659,7 @@ void TrackPanel::BuildMenus(void)
mTimeTrackMenu->Append(OnSetTimeTrackRangeID, _("&Range..."));
mTimeTrackMenu->AppendCheckItem(OnTimeTrackLogIntID, _("Logarithmic &Interpolation"));
/*
mRulerWaveformMenu = new wxMenu();
BuildVRulerMenuItems
(mRulerWaveformMenu, OnFirstWaveformScaleID,
@ -668,6 +669,7 @@ void TrackPanel::BuildMenus(void)
BuildVRulerMenuItems
(mRulerSpectrumMenu, OnFirstSpectrumScaleID,
SpectrogramSettings::GetScaleNames());
*/
}
void TrackPanel::BuildCommonDropMenuItems(wxMenu * menu)
@ -686,6 +688,8 @@ void TrackPanel::BuildCommonDropMenuItems(wxMenu * menu)
}
/*
// left over from PRL's vertical ruler context menu experiment in 2.1.2
// static
void TrackPanel::BuildVRulerMenuItems
(wxMenu * menu, int firstId, const wxArrayString &names)
@ -698,6 +702,7 @@ void TrackPanel::BuildVRulerMenuItems
menu->Append(OnZoomOutVerticalID, _("Zoom Out\tShift-Left-Click"));
menu->Append(OnZoomFitVerticalID, _("Zoom to Fit\tShift-Right-Click"));
}
*/
void TrackPanel::DeleteMenus(void)
{
@ -944,6 +949,9 @@ void TrackPanel::OnTimer(wxTimerEvent& )
//ANSWER-ME: Was DisplaySelection added to solve a repaint problem?
DisplaySelection();
}
if (mLastDrawnSelectedRegion != mViewInfo->selectedRegion) {
UpdateSelectionDisplay();
}
// Notify listeners for timer ticks
{
@ -1038,13 +1046,15 @@ double TrackPanel::GetScreenEndTime() const
{
int width;
GetTracksUsableArea(&width, NULL);
return mViewInfo->PositionToTime(width, true);
return mViewInfo->PositionToTime(width, 0, true);
}
/// AS: OnPaint( ) is called during the normal course of
/// completing a repaint operation.
void TrackPanel::OnPaint(wxPaintEvent & /* event */)
{
mLastDrawnSelectedRegion = mViewInfo->selectedRegion;
#if DEBUG_DRAW_TIMING
wxStopWatch sw;
#endif
@ -1114,6 +1124,52 @@ void TrackPanel::MakeParentRedrawScrollbars()
mListener->TP_RedrawScrollbars();
}
void TrackPanel::HandleInterruptedDrag()
{
// Certain drags need to complete their effects before handling keystroke shortcut
// commands: those that have undoable editing effects. For others, keystrokes are
// harmless and we do nothing.
switch (mMouseCapture)
{
case IsUncaptured:
case IsVZooming:
case IsSelecting:
case IsSelectingLabelText:
case IsResizing:
case IsResizingBetweenLinkedTracks:
case IsResizingBelowLinkedTracks:
case IsMuting:
case IsSoloing:
case IsMinimizing:
case IsPopping:
case IsZooming:
return;
default:
;
}
/*
So this includes the cases:
IsClosing,
IsAdjustingLabel,
IsAdjustingSample,
IsRearranging,
IsSliding,
IsEnveloping,
IsGainSliding,
IsPanSliding,
WasOverCutLine,
IsStretching
*/
wxMouseEvent evt { wxEVT_LEFT_UP };
evt.SetPosition(this->ScreenToClient(::wxGetMousePosition()));
this->ProcessEvent(evt);
}
bool TrackPanel::HandleEscapeKey(bool down)
{
if (!down)
@ -2718,7 +2774,12 @@ void TrackPanel::SelectionHandleDrag(wxMouseEvent & event, Track *clickedTrack)
#endif
ExtendSelection(x, rect.x, clickedTrack);
UpdateSelectionDisplay();
// If scrubbing does not use the helper poller thread, then
// don't do this at every mouse event, because it slows down seek-scrub.
// Instead, let OnTimer do it, which is often enough.
// And even if scrubbing does use the thread, then skipping this does not
// bring that advantage, but it is probably still a good idea anyway.
// UpdateSelectionDisplay();
}
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
@ -4756,6 +4817,24 @@ void TrackPanel::OnTrackListUpdated(wxCommandEvent & e)
SetFocusedTrack(NULL);
}
if (!mTracks->Contains(mCapturedTrack)) {
SetCapturedTrack(nullptr);
if (HasCapture())
ReleaseMouse();
}
if (!mTracks->Contains(mFreqSelTrack)) {
mFreqSelTrack = nullptr;
if (HasCapture())
ReleaseMouse();
}
if (!mTracks->Contains(mPopupMenuTarget)) {
mPopupMenuTarget = nullptr;
if (HasCapture())
ReleaseMouse();
}
if (e.GetClientData()) {
OnTrackListResized(e);
return;
@ -5442,6 +5521,14 @@ void TrackPanel::HandleResize(wxMouseEvent & event)
/// Handle mouse wheel rotation (for zoom in/out, vertical and horizontal scrolling)
void TrackPanel::HandleWheelRotation(wxMouseEvent & event)
{
if(event.GetWheelAxis() == wxMOUSE_WHEEL_HORIZONTAL) {
// Two-fingered horizontal swipe on mac is treated like shift-mousewheel
event.SetShiftDown(true);
// This makes the wave move in the same direction as the fingers, and the scrollbar
// thumb moves oppositely
event.m_wheelRotation *= -1;
}
if(!event.HasAnyModifiers()) {
// We will later un-skip if we do anything, but if we don't,
// propagate the event up for the sake of the scrubber
@ -5539,6 +5626,7 @@ void TrackPanel::HandleWheelRotation(wxMouseEvent & event)
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
if (GetProject()->GetScrubber().IsScrubbing()) {
GetProject()->GetScrubber().HandleScrollWheel(steps);
event.Skip(false);
}
else
#endif
@ -5674,6 +5762,8 @@ void TrackPanel::HandleWheelRotationInVRuler
/// Filter captured keys typed into LabelTracks.
void TrackPanel::OnCaptureKey(wxCommandEvent & event)
{
HandleInterruptedDrag();
// Only deal with LabelTracks
Track *t = GetFocusedTrack();
if (!t || t->GetKind() != Track::Label) {
@ -8617,7 +8707,7 @@ void TrackPanel::SetFocusedTrack( Track *t )
AudacityProject::ReleaseKeyboard(this);
}
if (t && t->GetKind() == Track::Label) {
if (t) {
AudacityProject::CaptureKeyboard(this);
}

View File

@ -73,7 +73,6 @@ DECLARE_EXPORTED_EVENT_TYPE(AUDACITY_DLL_API, EVT_TRACK_PANEL_TIMER, -1);
enum {
kTimerInterval = 50, // milliseconds
kOneSecondCountdown = 1000 / kTimerInterval,
};
class AUDACITY_DLL_API TrackInfo
@ -193,6 +192,7 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel {
//virtual void SetSelectionFormat(int iformat)
//virtual void SetSnapTo(int snapto)
virtual void HandleInterruptedDrag();
virtual bool HandleEscapeKey(bool down);
virtual void HandleAltKey(bool down);
virtual void HandleShiftKey(bool down);
@ -241,7 +241,10 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel {
* @param menu the menu to add the commands to.
*/
virtual void BuildCommonDropMenuItems(wxMenu * menu);
static void BuildVRulerMenuItems(wxMenu * menu, int firstId, const wxArrayString &names);
// left over from PRL's vertical ruler context menu experiment in 2.1.2
// static void BuildVRulerMenuItems(wxMenu * menu, int firstId, const wxArrayString &names);
virtual bool IsAudioActive();
virtual bool IsUnsafe();
virtual bool HandleLabelTrackClick(LabelTrack * lTrack, wxRect &rect, wxMouseEvent & event);
@ -789,6 +792,8 @@ protected:
// The screenshot class needs to access internals
friend class ScreenshotCommand;
SelectedRegion mLastDrawnSelectedRegion {};
public:
wxSize vrulerSize;

View File

@ -1155,14 +1155,15 @@ bool WaveTrack::SyncLockAdjust(double oldT1, double newT1)
// If track is empty at oldT1 insert whitespace; otherwise, silence
if (IsEmpty(oldT1, oldT1))
{
bool ret = false;
bool ret = true;
// Check if clips can move
bool clipsCanMove = true;
gPrefs->Read(wxT("/GUI/EditClipCanMove"), &clipsCanMove);
if (clipsCanMove) {
auto tmp = Cut (oldT1, GetEndTime() + 1.0/GetRate());
if (!ret) return false;
if (!tmp)
return false;
ret = Paste(newT1, tmp.get());
wxASSERT(ret);

View File

@ -2534,7 +2534,7 @@ void Effect::Preview(bool dryOnly)
double previewLen;
gPrefs->Read(wxT("/AudioIO/EffectsPreviewLen"), &previewLen, 6.0);
double rate = mProjectRate;
const double rate = mProjectRate;
if (isNyquist && isGenerator) {
previewDuration = CalcPreviewInputLength(previewLen);
@ -2637,12 +2637,13 @@ void Effect::Preview(bool dryOnly)
NoteTrackArray empty;
#endif
// Start audio playing
AudioIOStartStreamOptions options { rate };
int token =
gAudioIO->StartStream(playbackTracks, recordingTracks,
#ifdef EXPERIMENTAL_MIDI_OUT
empty,
#endif
rate, mT0, t1);
mT0, t1, options);
if (token) {
int previewing = eProgressSuccess;

View File

@ -41,7 +41,7 @@ public:
mParent = parent;
mLink = link;
if (!wxControl::Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNO_BORDER, wxDefaultValidator, wxEmptyString))
if (!wxControl::Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNO_BORDER | wxTAB_TRAVERSAL, wxDefaultValidator, wxEmptyString))
{
return false;
}

View File

@ -129,7 +129,7 @@ AButton *ControlToolBar::MakeButton(teBmps eEnabledUp, teBmps eEnabledDown, teBm
bool processdownevents,
const wxChar *label)
{
AButton *r = ToolBar::MakeButton(
AButton *r = ToolBar::MakeButton(this,
bmpRecoloredUpLarge, bmpRecoloredDownLarge, bmpRecoloredHiliteLarge,
eEnabledUp, eEnabledDown, eDisabled,
wxWindowID(id),
@ -634,7 +634,7 @@ int ControlToolBar::PlayPlayRegion(const SelectedRegion &selectedRegion,
#ifdef EXPERIMENTAL_MIDI_OUT
NoteTrackArray(),
#endif
p->GetRate(), tcp0, tcp1, myOptions);
tcp0, tcp1, myOptions);
} else
{
// Cannot create cut preview tracks, clean up and exit
@ -655,7 +655,7 @@ int ControlToolBar::PlayPlayRegion(const SelectedRegion &selectedRegion,
#ifdef EXPERIMENTAL_MIDI_OUT
t->GetNoteTrackArray(false),
#endif
p->GetRate(), t0, t1, options);
t0, t1, options);
}
if (token != 0) {
success = true;
@ -1085,7 +1085,7 @@ void ControlToolBar::OnRecord(wxCommandEvent &evt)
#ifdef EXPERIMENTAL_MIDI_OUT
midiTracks,
#endif
p->GetRate(), t0, t1, options);
t0, t1, options);
bool success = (token != 0);
@ -1278,4 +1278,3 @@ void ControlToolBar::UpdateStatusBar(AudacityProject *pProject)
{
pProject->GetStatusBar()->SetStatusText(StateForStatusBar(), stateStatusBarField);
}

View File

@ -105,7 +105,7 @@ AButton *EditToolBar::AddButton(
{
AButton *&r = mButtons[id];
r = ToolBar::MakeButton(
r = ToolBar::MakeButton(this,
bmpRecoloredUpSmall, bmpRecoloredDownSmall, bmpRecoloredHiliteSmall,
eEnabledUp, eEnabledDown, eDisabled,
wxWindowID(id),

View File

@ -700,6 +700,7 @@ void ToolBar::MakeButtonBackgroundsSmall()
}
/// Makes a button and its four different state bitmaps
/// @param parent Parent window for the button.
/// @param eUp Background for when button is Up.
/// @param eDown Background for when button is Down.
/// @param eHilite Background for when button is Hilit.
@ -710,7 +711,8 @@ void ToolBar::MakeButtonBackgroundsSmall()
/// @param placement Placement position
/// @param processdownevents true iff button handles down events.
/// @param size Size of the background.
AButton * ToolBar::MakeButton(teBmps eUp,
AButton * ToolBar::MakeButton(wxWindow *parent,
teBmps eUp,
teBmps eDown,
teBmps eHilite,
teBmps eStandardUp,
@ -731,7 +733,7 @@ AButton * ToolBar::MakeButton(teBmps eUp,
wxImagePtr disable2 (OverlayImage(eUp, eDisabled, xoff, yoff));
AButton * button =
new AButton(this, id, placement, size, *up2, *hilite2, *down2,
new AButton(parent, id, placement, size, *up2, *hilite2, *down2,
*disable2, processdownevents);
return button;

View File

@ -121,9 +121,11 @@ class ToolBar /* not final */ : public wxPanel
virtual int GetInitialWidth() { return -1; }
virtual int GetMinToolbarWidth() { return GetInitialWidth(); }
virtual wxSize GetDockedSize() { return GetMinSize(); }
protected:
AButton *MakeButton(teBmps eUp,
public:
static
AButton *MakeButton(wxWindow *parent,
teBmps eUp,
teBmps eDown,
teBmps eHilite,
teBmps eStandardUp,
@ -144,6 +146,7 @@ class ToolBar /* not final */ : public wxPanel
teBmps eDisabled,
wxSize size);
protected:
void SetButton(bool down, AButton *button);
void MakeMacRecoloredImage(teBmps eBmpOut, teBmps eBmpIn);

View File

@ -156,7 +156,7 @@ void ToolsToolBar::UpdatePrefs()
AButton * ToolsToolBar::MakeTool( teBmps eTool,
int id, const wxChar *label)
{
AButton *button = ToolBar::MakeButton(
AButton *button = ToolBar::MakeButton(this,
bmpRecoloredUpSmall, bmpRecoloredDownSmall, bmpRecoloredHiliteSmall,
eTool, eTool, eTool,
wxWindowID(id),

View File

@ -147,7 +147,7 @@ AButton *TranscriptionToolBar::AddButton(
{
AButton *&r = mButtons[id];
r = ToolBar::MakeButton(
r = ToolBar::MakeButton(this,
bmpRecoloredUpSmall, bmpRecoloredDownSmall, bmpRecoloredHiliteSmall,
eFore, eFore, eDisabled,
wxWindowID(id),

View File

@ -142,12 +142,20 @@ void PlayIndicatorOverlay::OnTimer(wxCommandEvent &event)
}
}
auto trackPanel = mProject->GetTrackPanel();
if (!mProject->IsAudioActive()) {
mNewIndicatorX = -1;
const auto &scrubber = mProject->GetScrubber();
if (scrubber.HasStartedScrubbing())
mNewIndicatorX = scrubber.GetScrubStartPosition();
else
mNewIndicatorX = -1;
if (scrubber.HasStartedScrubbing()) {
auto position = scrubber.GetScrubStartPosition();
int width;
trackPanel->GetTracksUsableArea(&width, nullptr);
const auto offset = trackPanel->GetLeftOffset();
if(position >= trackPanel->GetLeftOffset() &&
position < offset + width)
mNewIndicatorX = position;
}
}
else {
ViewInfo &viewInfo = mProject->GetViewInfo();
@ -155,7 +163,7 @@ void PlayIndicatorOverlay::OnTimer(wxCommandEvent &event)
// Calculate the horizontal position of the indicator
const double playPos = viewInfo.mRecentStreamTime;
const bool onScreen = playPos >= 0.0 &&
bool onScreen = playPos >= 0.0 &&
between_incexc(viewInfo.h,
playPos,
mProject->GetScreenEndTime());
@ -176,6 +184,11 @@ void PlayIndicatorOverlay::OnTimer(wxCommandEvent &event)
!gAudioIO->IsPaused())
{
mProject->TP_ScrollWindow(playPos);
// Might yet be off screen, check it
onScreen = playPos >= 0.0 &&
between_incexc(viewInfo.h,
playPos,
mProject->GetScreenEndTime());
}
// Always update scrollbars even if not scrolling the window. This is
@ -183,7 +196,10 @@ void PlayIndicatorOverlay::OnTimer(wxCommandEvent &event)
// length of the project and therefore the appearance of the scrollbar.
mProject->TP_RedrawScrollbars();
mNewIndicatorX = viewInfo.TimeToPosition(playPos, mProject->GetTrackPanel()->GetLeftOffset());
if (onScreen)
mNewIndicatorX = viewInfo.TimeToPosition(playPos, trackPanel->GetLeftOffset());
else
mNewIndicatorX = -1;
}
if(mPartner)

View File

@ -20,6 +20,12 @@ Paul Licameli split from TrackPanel.cpp
#include "../../TrackPanelCellIterator.h"
#include "../../commands/CommandFunctors.h"
#include "../../toolbars/ControlToolBar.h"
#undef USE_TRANSCRIPTION_TOOLBAR
#ifdef USE_TRANSCRIPTION_TOOLBAR
#include "../../toolbars/TranscriptionToolBar.h"
#endif
#include "../../widgets/Ruler.h"
#include <algorithm>
@ -40,8 +46,13 @@ enum {
#endif
ScrubPollInterval_ms = 50,
kOneSecondCountdown = 1000 / ScrubPollInterval_ms,
};
static const double MinStutter = 0.2;
static const double MaxDragSpeed = 1.0;
namespace {
double FindScrubbingSpeed(const ViewInfo &viewInfo, double maxScrubSpeed, double screen, double timeAtMouse)
{
@ -114,6 +125,32 @@ namespace {
}
}
#ifdef USE_SCRUB_THREAD
class Scrubber::ScrubPollerThread final : public wxThread {
public:
ScrubPollerThread(Scrubber &scrubber)
: wxThread { }
, mScrubber(scrubber)
{}
ExitCode Entry() override;
private:
Scrubber &mScrubber;
};
auto Scrubber::ScrubPollerThread::Entry() -> ExitCode
{
while( !TestDestroy() )
{
wxThread::Sleep(ScrubPollInterval_ms);
mScrubber.ContinueScrubbingPoll();
}
return 0;
}
#endif
class Scrubber::ScrubPoller : public wxTimer
{
public:
@ -131,16 +168,20 @@ void Scrubber::ScrubPoller::Notify()
// rather than in SelectionHandleDrag()
// so that even without drag events, we can instruct the play head to
// keep approaching the mouse cursor, when its maximum speed is limited.
mScrubber.ContinueScrubbing();
#ifndef USE_SCRUB_THREAD
// If there is no helper thread, this main thread timer is responsible
// for playback and for UI
mScrubber.ContinueScrubbingPoll();
#endif
mScrubber.ContinueScrubbingUI();
}
Scrubber::Scrubber(AudacityProject *project)
: mScrubToken(-1)
, mScrubStartClockTimeMillis(-1)
, mScrubHasFocus(false)
, mPaused(true)
, mScrubSpeedDisplayCountdown(0)
, mScrubStartPosition(-1)
, mMaxScrubSpeed(-1.0)
, mScrubSeekPress(false)
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
, mSmoothScrollingScrub(false)
@ -149,6 +190,7 @@ Scrubber::Scrubber(AudacityProject *project)
, mProject(project)
, mPoller { std::make_unique<ScrubPoller>(*this) }
, mOptions {}
{
if (wxTheApp)
wxTheApp->Connect
@ -159,6 +201,11 @@ Scrubber::Scrubber(AudacityProject *project)
Scrubber::~Scrubber()
{
#ifdef USE_SCRUB_THREAD
if (mpThread)
mpThread->Delete();
#endif
mProject->PopEventHandler();
if (wxTheApp)
wxTheApp->Disconnect
@ -236,7 +283,7 @@ void Scrubber::MarkScrubStart(
ctb->UpdateStatusBar(mProject);
mScrubStartPosition = xx;
mScrubStartClockTimeMillis = ::wxGetLocalTimeMillis();
mOptions.startClockTimeMillis = ::wxGetLocalTimeMillis();
CheckMenuItem();
}
@ -291,24 +338,28 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
}
AudioIOStartStreamOptions options(mProject->GetDefaultPlayOptions());
options.pScrubbingOptions = &mOptions;
options.timeTrack = NULL;
options.scrubDelay = (ScrubPollInterval_ms / 1000.0);
options.scrubStartClockTimeMillis = mScrubStartClockTimeMillis;
options.minScrubStutter = 0.2;
#if 0
mOptions.delay = (ScrubPollInterval_ms * 0.9 / 1000.0);
mOptions.minSpeed = 0.0;
#ifdef USE_TRANSCRIPTION_TOOLBAR
if (!mAlwaysSeeking) {
// Take the starting speed limit from the transcription toolbar,
// but it may be varied during the scrub.
mMaxScrubSpeed = options.maxScrubSpeed =
p->GetTranscriptionToolBar()->GetPlaySpeed();
mMaxSpeed = mOptions.maxSpeed =
mProject->GetTranscriptionToolBar()->GetPlaySpeed();
}
#else
// That idea seems unpopular... just make it one for move-scrub,
// but big for drag-scrub
mMaxScrubSpeed = options.maxScrubSpeed =
mDragging ? AudioIO::GetMaxScrubSpeed() : 1.0;
mMaxSpeed = mOptions.maxSpeed = mDragging ? MaxDragSpeed : 1.0;
#endif
options.maxScrubTime = mProject->GetTracks()->GetEndTime();
mOptions.minSample = 0;
mOptions.maxSample =
lrint(std::max(0.0, mProject->GetTracks()->GetEndTime()) * options.rate);
mOptions.minStutter =
mDragging ? 0.0 : lrint(std::max(0.0, MinStutter) * options.rate);
ControlToolBar::PlayAppearance appearance =
ControlToolBar::PlayAppearance::Scrub;
const bool cutPreview = false;
@ -317,7 +368,7 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
static const double maxScrubSpeedBase =
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
mLogMaxScrubSpeed = floor(0.5 +
log(mMaxScrubSpeed) / log(maxScrubSpeedBase)
log(mMaxSpeed) / log(maxScrubSpeedBase)
);
#endif
mScrubSpeedDisplayCountdown = 0;
@ -328,15 +379,20 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
}
else
// Wait to test again
mScrubStartClockTimeMillis = ::wxGetLocalTimeMillis();
mOptions.startClockTimeMillis = ::wxGetLocalTimeMillis();
if (IsScrubbing()) {
using Mode = AudacityProject::PlaybackScroller::Mode;
mProject->GetPlaybackScroller().Activate
(mSmoothScrollingScrub ? Mode::Centered : Mode::Off);
mScrubHasFocus = true;
ActivateScroller();
mPaused = false;
mLastScrubPosition = xx;
#ifdef USE_SCRUB_THREAD
// Detached thread is self-deleting, after it receives the Delete() message
mpThread = safenew ScrubPollerThread{ *this };
mpThread->Create(4096);
mpThread->Run();
#endif
mPoller->Start(ScrubPollInterval_ms);
}
@ -345,7 +401,67 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
}
}
void Scrubber::ContinueScrubbing()
void Scrubber::ContinueScrubbingPoll()
{
// Thus scrubbing relies mostly on periodic polling of mouse and keys,
// not event notifications. But there are a few event handlers that
// leave messages for this routine, in mScrubSeekPress and in mPaused.
// Decide whether to skip play, because either mouse is down now,
// or there was a left click event. (This is then a delayed reaction, in a
// timer callback, to a left click event detected elsewhere.)
const bool seek = PollIsSeeking() || mScrubSeekPress;
bool result = false;
if (mPaused) {
// When paused, enqueue silent scrubs.
mOptions.minSpeed = 0.0;
mOptions.maxSpeed = mMaxSpeed;
mOptions.adjustStart = false;
mOptions.enqueueBySpeed = true;
result = gAudioIO->EnqueueScrub(0, mOptions);
}
else {
const wxMouseState state(::wxGetMouseState());
const auto trackPanel = mProject->GetTrackPanel();
const wxPoint position = trackPanel->ScreenToClient(state.GetPosition());
const auto &viewInfo = mProject->GetViewInfo();
if (mDragging && mSmoothScrollingScrub) {
const auto lastTime = gAudioIO->GetLastTimeInScrubQueue();
const auto delta = mLastScrubPosition - position.x;
const double time = viewInfo.OffsetTimeByPixels(lastTime, delta);
mOptions.minSpeed = 0.0;
mOptions.maxSpeed = mMaxSpeed;
mOptions.adjustStart = true;
mOptions.enqueueBySpeed = false;
result = gAudioIO->EnqueueScrub(time, mOptions);
mLastScrubPosition = position.x;
}
else {
const double time = viewInfo.PositionToTime(position.x, trackPanel->GetLeftOffset());
mOptions.adjustStart = seek;
mOptions.minSpeed = (mDragging || !seek) ? 0.0 : 1.0;
mOptions.maxSpeed = (mDragging || !seek) ? mMaxSpeed : 1.0;
if (mSmoothScrollingScrub) {
const double speed = FindScrubSpeed(seek, time);
mOptions.enqueueBySpeed = true;
result = gAudioIO->EnqueueScrub(speed, mOptions);
}
else {
mOptions.enqueueBySpeed = false;
result = gAudioIO->EnqueueScrub(time, mOptions);
}
}
}
if (result)
mScrubSeekPress = false;
// else, if seek requested, try again at a later time when we might
// enqueue a long enough stutter
}
void Scrubber::ContinueScrubbingUI()
{
const wxMouseState state(::wxGetMouseState());
@ -355,60 +471,20 @@ void Scrubber::ContinueScrubbing()
return;
}
// Thus scrubbing relies mostly on periodic polling of mouse and keys,
// not event notifications. But there are a few event handlers that
// leave messages for this routine, in mScrubSeekPress and in mScrubHasFocus.
// Seek only when the pointer is in the panel. Else, scrub.
TrackPanel *const trackPanel = mProject->GetTrackPanel();
// Decide whether to skip play, because either mouse is down now,
// or there was a left click event. (This is then a delayed reaction, in a
// timer callback, to a left click event detected elsewhere.)
const bool seek = PollIsSeeking() || mScrubSeekPress;
const bool seek = PollIsSeeking();
{
// Show the correct status for seeking.
bool backup = mAlwaysSeeking;
mAlwaysSeeking = seek;
const auto ctb = mProject->GetControlToolBar();
ctb->UpdateStatusBar(mProject);
if (ctb)
ctb->UpdateStatusBar(mProject);
mAlwaysSeeking = backup;
}
const wxPoint position = trackPanel->ScreenToClient(state.GetPosition());
const auto &viewInfo = mProject->GetViewInfo();
bool result = false;
if (!mScrubHasFocus)
// When we don't have focus, enqueue silent scrubs until we regain focus.
result = gAudioIO->EnqueueScrubBySignedSpeed(0, mMaxScrubSpeed, false);
else if (mDragging && mSmoothScrollingScrub) {
const auto lastTime = gAudioIO->GetLastTimeInScrubQueue();
const auto delta = mLastScrubPosition - position.x;
const double time = viewInfo.OffsetTimeByPixels(lastTime, delta);
result = gAudioIO->EnqueueScrubByPosition(time, mMaxScrubSpeed, true);
mLastScrubPosition = position.x;
}
else {
const double time = viewInfo.PositionToTime(position.x, trackPanel->GetLeftOffset());
if (seek)
// Cause OnTimer() to suppress the speed display
mScrubSpeedDisplayCountdown = 1;
if (mSmoothScrollingScrub) {
const double speed = FindScrubSpeed(seek, time);
result = gAudioIO->EnqueueScrubBySignedSpeed(speed, mMaxScrubSpeed, seek);
}
else
result = gAudioIO->EnqueueScrubByPosition
(time, seek ? 1.0 : mMaxScrubSpeed, seek);
}
if (result)
mScrubSeekPress = false;
// else, if seek requested, try again at a later time when we might
// enqueue a long enough stutter
if (seek)
mScrubSpeedDisplayCountdown = 0;
if (mSmoothScrollingScrub)
;
@ -420,6 +496,13 @@ void Scrubber::ContinueScrubbing()
void Scrubber::StopScrubbing()
{
#ifdef USE_SCRUB_THREAD
if (mpThread) {
mpThread->Delete();
mpThread = nullptr;
}
#endif
mPoller->Stop();
UncheckAllMenuItems();
@ -460,11 +543,11 @@ bool Scrubber::ShouldDrawScrubSpeed()
return false;
return IsScrubbing() &&
mScrubHasFocus && (
// Draw for (non-scroll) scrub, sometimes, but never for seek
(!PollIsSeeking() && mScrubSpeedDisplayCountdown > 0)
// Draw always for scroll-scrub and for scroll-seek
|| mSmoothScrollingScrub
!mPaused && (
// Draw for (non-scroll) scrub, sometimes, but never for seek
(!PollIsSeeking() && mScrubSpeedDisplayCountdown > 0)
// Draw always for scroll-scrub and for scroll-seek
|| mSmoothScrollingScrub
);
}
@ -473,7 +556,7 @@ double Scrubber::FindScrubSpeed(bool seeking, double time) const
ViewInfo &viewInfo = mProject->GetViewInfo();
const double screen = mProject->GetScreenEndTime() - viewInfo.h;
return (seeking ? FindSeekSpeed : FindScrubbingSpeed)
(viewInfo, mMaxScrubSpeed, screen, time);
(viewInfo, mMaxSpeed, screen, time);
}
void Scrubber::HandleScrollWheel(int steps)
@ -482,14 +565,17 @@ void Scrubber::HandleScrollWheel(int steps)
// Not likely you would spin it with the left button down, but...
return;
if (steps == 0)
return;
const int newLogMaxScrubSpeed = mLogMaxScrubSpeed + steps;
static const double maxScrubSpeedBase =
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
double newSpeed = pow(maxScrubSpeedBase, newLogMaxScrubSpeed);
if (newSpeed >= AudioIO::GetMinScrubSpeed() &&
newSpeed <= AudioIO::GetMaxScrubSpeed()) {
if (newSpeed >= ScrubbingOptions::MinAllowedScrubSpeed() &&
newSpeed <= ScrubbingOptions::MaxAllowedScrubSpeed()) {
mLogMaxScrubSpeed = newLogMaxScrubSpeed;
mMaxScrubSpeed = newSpeed;
mMaxSpeed = newSpeed;
if (!mSmoothScrollingScrub)
// Show the speed for one second
mScrubSpeedDisplayCountdown = kOneSecondCountdown + 1;
@ -498,7 +584,12 @@ void Scrubber::HandleScrollWheel(int steps)
void Scrubber::Pause( bool paused )
{
mScrubHasFocus = !paused;
mPaused = paused;
}
bool Scrubber::IsPaused() const
{
return mPaused;
}
void Scrubber::OnActivateOrDeactivateApp(wxActivateEvent &event)
@ -691,6 +782,24 @@ bool Scrubber::PollIsSeeking()
return mDragging || (mAlwaysSeeking || ::wxGetMouseState().LeftIsDown());
}
void Scrubber::ActivateScroller()
{
using Mode = AudacityProject::PlaybackScroller::Mode;
mProject->GetPlaybackScroller().Activate(mSmoothScrollingScrub
? Mode::Centered
:
#ifdef __WXMAC__
// PRL: cause many "unnecessary" refreshes. For reasons I don't understand,
// doing this causes wheel rotation events (mapped from the double finger vertical
// swipe) to be delivered more uniformly to the application, so that spped control
// works better.
Mode::Refresh
#else
Mode::Off
#endif
);
}
void Scrubber::DoScrub(bool scroll, bool seek)
{
const bool wasScrubbing = IsScrubbing();
@ -709,9 +818,7 @@ void Scrubber::DoScrub(bool scroll, bool seek)
}
else if(!match) {
mSmoothScrollingScrub = scroll;
using Mode = AudacityProject::PlaybackScroller::Mode;
mProject->GetPlaybackScroller().Activate
(scroll ? Mode::Centered : Mode::Off);
ActivateScroller();
mAlwaysSeeking = seek;
UncheckAllMenuItems();
CheckMenuItem();

View File

@ -21,6 +21,48 @@ Paul Licameli split from TrackPanel.cpp
class AudacityProject;
// Conditionally compile either a separate thead, or else use a timer in the main
// thread, to poll the mouse and update scrubbing speed and direction. The advantage of
// a thread may be immunity to choppy scrubbing in case redrawing takes too much time.
#ifdef __WXGTK__
// Unfortunately some things the thread needs to do are not thread safe
#else
#define USE_SCRUB_THREAD
#endif
// For putting an increment of work in the scrubbing queue
struct ScrubbingOptions {
ScrubbingOptions() {}
bool adjustStart {};
// usually from TrackList::GetEndTime()
long maxSample {};
long minSample {};
bool enqueueBySpeed {};
double delay {};
// Limiting values for the speed of a scrub interval:
double minSpeed { 0.0 };
double maxSpeed { 1.0 };
// When maximum speed scrubbing skips to follow the mouse,
// this is the minimum amount of playback allowed at the maximum speed:
long minStutter {};
// Scrubbing needs the time of start of the mouse movement that began
// the scrub:
wxLongLong startClockTimeMillis { -1 };
static double MaxAllowedScrubSpeed()
{ return 32.0; } // Is five octaves enough for your amusement?
static double MinAllowedScrubSpeed()
{ return 0.01; } // Mixer needs a lower bound speed. Scrub no slower than this.
};
// Scrub state object
class Scrubber : public wxEvtHandler
{
@ -39,7 +81,8 @@ public:
// Assume xx is relative to the left edge of TrackPanel!
bool MaybeStartScrubbing(wxCoord xx);
void ContinueScrubbing();
void ContinueScrubbingUI();
void ContinueScrubbingPoll();
// This is meant to be called only from ControlToolBar
void StopScrubbing();
@ -61,7 +104,7 @@ public:
bool ShouldDrawScrubSpeed();
double FindScrubSpeed(bool seeking, double time) const;
double GetMaxScrubSpeed() const { return mMaxScrubSpeed; }
double GetMaxScrubSpeed() const { return mOptions.maxSpeed; }
void HandleScrollWheel(int steps);
@ -87,8 +130,10 @@ public:
static std::vector<wxString> GetAllUntranslatedStatusStrings();
void Pause(bool paused);
bool IsPaused() const;
private:
void ActivateScroller();
void DoScrub(bool scroll, bool seek);
void OnActivateOrDeactivateApp(wxActivateEvent & event);
void UncheckAllMenuItems();
@ -108,12 +153,10 @@ private:
private:
int mScrubToken;
wxLongLong mScrubStartClockTimeMillis;
bool mScrubHasFocus;
bool mPaused;
int mScrubSpeedDisplayCountdown;
wxCoord mScrubStartPosition;
wxCoord mLastScrubPosition {};
double mMaxScrubSpeed;
bool mScrubSeekPress;
bool mSmoothScrollingScrub;
bool mAlwaysSeeking {};
@ -127,8 +170,20 @@ private:
DECLARE_EVENT_TABLE()
#ifdef USE_SCRUB_THREAD
// Course corrections in playback are done in a helper thread, unhindered by
// the complications of the main event dispatch loop
class ScrubPollerThread;
ScrubPollerThread *mpThread {};
#endif
// Other periodic update of the UI must be done in the main thread,
// by this object which is driven by timer events.
class ScrubPoller;
std::unique_ptr<ScrubPoller> mPoller;
ScrubbingOptions mOptions;
double mMaxSpeed { 1.0 };
};
// Specialist in drawing the scrub speed, and listening for certain events

View File

@ -392,18 +392,6 @@ void AButton::OnMouseEvent(wxMouseEvent & event)
(event.m_x >= 0 && event.m_y >= 0 &&
event.m_x < clientSize.x && event.m_y < clientSize.y);
if (!mButtonIsDown)
{
// Note that CMD (or CTRL) takes precedence over Shift if both are down
// see also AButton::Listener::OnKeyUp()
if (event.CmdDown() && HasAlternateImages(2))
mAlternateIdx = 2;
else if (event.ShiftDown() && HasAlternateImages(1))
mAlternateIdx = 1;
else
mAlternateIdx = 0;
}
if (mEnabled && event.IsButton()) {
if (event.ButtonIsDown(wxMOUSE_BTN_ANY)) {
mIsClicking = true;

View File

@ -36,13 +36,14 @@
#include "ErrorDialog.h"
#include "HelpSystem.h"
const wxString HelpSystem::HelpHostname = wxT("manual.audacityteam.org");
#if IS_ALPHA
const wxString HelpSystem::HelpHostname = wxT("alphamanual.audacityteam.org");
const wxString HelpSystem::HelpServerHomeDir = wxT("/man/");
const wxString HelpSystem::HelpServerManDir = wxT("/man/");
#else
const wxString HelpSystem::HelpServerHomeDir = wxT("/o/");
const wxString HelpSystem::HelpServerManDir = wxT("/o/man/");
const wxString HelpSystem::HelpHostname = wxT("manual.audacityteam.org");
const wxString HelpSystem::HelpServerHomeDir = wxT("/");
const wxString HelpSystem::HelpServerManDir = wxT("/man/");
#endif
const wxString HelpSystem::LocalHelpManDir = wxT("/man/");
const wxString HelpSystem::ReleaseSuffix = wxT(".html");

View File

@ -1814,8 +1814,9 @@ void QuickPlayRulerOverlay::Draw(OverlayPanel &panel, wxDC &dc)
if (mOldQPIndicatorPos >= 0) {
auto ruler = GetRuler();
auto scrub =
ruler->mPrevZone == AdornedRulerPanel::StatusChoice::EnteringScrubZone ||
mPartner.mProject->GetScrubber().HasStartedScrubbing();
ruler->mMouseEventState == AdornedRulerPanel::mesNone &&
(ruler->mPrevZone == AdornedRulerPanel::StatusChoice::EnteringScrubZone ||
(mPartner.mProject->GetScrubber().HasStartedScrubbing()));
auto width = scrub ? IndicatorBigWidth() : IndicatorSmallWidth;
ruler->DoDrawIndicator(&dc, mOldQPIndicatorPos, true, width, scrub);
}
@ -1934,14 +1935,6 @@ BEGIN_EVENT_TABLE(AdornedRulerPanel, OverlayPanel)
// Scrub bar menu commands
EVT_MENU(OnShowHideScrubbingID, AdornedRulerPanel::OnToggleScrubbing)
// Key events, to navigate buttons
EVT_COMMAND(wxID_ANY, EVT_CAPTURE_KEY, AdornedRulerPanel::OnCaptureKey)
EVT_KEY_DOWN(AdornedRulerPanel::OnKeyDown)
// Correct management of track focus
EVT_SET_FOCUS(AdornedRulerPanel::OnSetFocus)
EVT_KILL_FOCUS(AdornedRulerPanel::OnKillFocus)
// Pop up menus on Windows
EVT_CONTEXT_MENU(AdornedRulerPanel::OnContextMenu)
@ -1956,6 +1949,8 @@ AdornedRulerPanel::AdornedRulerPanel(AudacityProject* parent,
, mProject(parent)
, mViewInfo(viewinfo)
{
ReCreateButtons();
SetLabel( _("Timeline") );
SetName(GetLabel());
SetBackgroundStyle(wxBG_STYLE_PAINT);
@ -1993,8 +1988,6 @@ AdornedRulerPanel::AdornedRulerPanel(AudacityProject* parent,
mPlayRegionDragsSelection = (gPrefs->Read(wxT("/QuickPlay/DragSelection"), 0L) == 1)? true : false;
mQuickPlayEnabled = !!gPrefs->Read(wxT("/QuickPlay/QuickPlayEnabled"), 1L);
mButtonFont.Create(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
UpdatePrefs();
#if wxUSE_TOOLTIPS
@ -2057,95 +2050,10 @@ void AdornedRulerPanel::UpdatePrefs()
UpdateRects();
RegenerateTooltips(mPrevZone);
mButtonFontSize = -1;
}
namespace {
enum { ArrowWidth = 8, ArrowSpacing = 1, ArrowHeight = ArrowWidth / 2 };
// Find the part of the button rectangle in which you can click the arrow.
// It includes the lower right corner.
wxRect GetArrowRect(const wxRect &buttonRect)
{
// Change the following lines to change the size of the hot zone.
// Make the hot zone as tall as the button
auto width = std::min(
std::max(1, buttonRect.GetWidth()) - 1,
ArrowWidth + 2 * ArrowSpacing
+ 2 // bevel around arrow
+ 2 // outline around the bevel
);
auto height = buttonRect.GetHeight();
return wxRect {
buttonRect.GetRight() + 1 - width,
buttonRect.GetBottom() + 1 - height,
width, height
};
}
wxRect GetTextRect(const wxRect &buttonRect)
{
auto result = buttonRect;
result.width -= GetArrowRect(buttonRect).width;
return result;
}
// Compensate for off-by-one problem in the bevel-drawing functions
struct Deflator {
Deflator(wxRect &rect) : mRect(rect) {
--mRect.width;
--mRect.height;
}
~Deflator() {
++mRect.width;
++mRect.height;
}
wxRect &mRect;
};
}
wxFont &AdornedRulerPanel::GetButtonFont() const
void AdornedRulerPanel::ReCreateButtons()
{
if (mButtonFontSize < 0) {
mButtonFontSize = 10;
bool done;
do {
done = true;
mButtonFont.SetPointSize(mButtonFontSize);
wxCoord width, height;
for (auto button = StatusChoice::FirstButton; done && IsButton(button); ++button) {
auto rect = GetTextRect(GetButtonRect(button));
auto availableWidth = rect.GetWidth();
auto availableHeight = rect.GetHeight();
// Deduct for outlines, and room to move text
// I might deduct 2 more for bevel, but that made the text too small.
#ifdef __WXMSW__
// Deduct less for MSW, because GetTextExtent appears to overstate width, and
// I don't know why. Not really happy with this arbitrary fix.
availableWidth -= 1;
availableHeight -= 1;
#else
availableWidth -= 2 + 1;
availableHeight -= 2 + 1;
#endif
GetParent()->GetTextExtent(
wxGetTranslation(GetPushButtonStrings(button)->label),
&width, &height, NULL, NULL, &mButtonFont);
// Yes, < not <= ! Leave at least some room.
done = width < availableWidth && height < availableHeight;
}
mButtonFontSize--;
} while (mButtonFontSize > 0 && !done);
}
return mButtonFont;
}
void AdornedRulerPanel::InvalidateRuler()
@ -2162,7 +2070,6 @@ void AdornedRulerPanel::RegenerateTooltips(StatusChoice choice)
}
else {
switch(choice) {
case StatusChoice::QuickPlayButton :
case StatusChoice::EnteringQP :
if (!mQuickPlayEnabled) {
this->SetToolTip(_("Quick-Play disabled"));
@ -2171,14 +2078,6 @@ void AdornedRulerPanel::RegenerateTooltips(StatusChoice choice)
this->SetToolTip(_("Quick-Play enabled"));
}
break;
case StatusChoice::ScrubBarButton :
if (!mShowScrubbing) {
this->SetToolTip(_("Scrub bar hidden"));
}
else {
this->SetToolTip(_("Scrub bar shown"));
}
break;
case StatusChoice::EnteringScrubZone :
this->SetToolTip(_("Scrub Bar"));
break;
@ -2232,8 +2131,6 @@ void AdornedRulerPanel::OnPaint(wxPaintEvent & WXUNUSED(evt))
DoDrawPlayRegion(&backDC);
DoDrawPushbuttons(&backDC);
DoDrawEdge(&backDC);
DisplayBitmap(dc);
@ -2340,25 +2237,8 @@ void AdornedRulerPanel::OnMouseEvents(wxMouseEvent &evt)
}
const auto position = evt.GetPosition();
const bool overButtons = GetButtonAreaRect(true).Contains(position);
StatusChoice button;
{
auto mouseState = FindButton(evt);
button = mouseState.button;
if (IsButton(button)) {
TabState newState{ button, mouseState.state == PointerState::InArrow };
if (mTabState != newState) {
// Change the button highlight
mTabState = newState;
Refresh(false);
}
}
else if(evt.Leaving() && !HasFocus())
// erase the button highlight
Refresh(false);
}
const bool inScrubZone = !overButtons &&
const bool inScrubZone =
// only if scrubbing is allowed now
mProject->GetScrubber().CanScrub() &&
mShowScrubbing &&
@ -2366,13 +2246,11 @@ void AdornedRulerPanel::OnMouseEvents(wxMouseEvent &evt)
const StatusChoice zone =
evt.Leaving()
? StatusChoice::Leaving
: overButtons
? button
: inScrubZone
? StatusChoice::EnteringScrubZone
: mInner.Contains(position)
? StatusChoice::EnteringQP
: StatusChoice::NoChange;
: inScrubZone
? StatusChoice::EnteringScrubZone
: mInner.Contains(position)
? StatusChoice::EnteringQP
: StatusChoice::NoChange;
const bool changeInZone = (zone != mPrevZone);
const bool changing = evt.Leaving() || evt.Entering() || changeInZone;
@ -2390,51 +2268,48 @@ void AdornedRulerPanel::OnMouseEvents(wxMouseEvent &evt)
// Handle status bar messages
UpdateStatusBarAndTooltips (changing ? zone : StatusChoice::NoChange);
if ((IsButton(zone) || IsButton(mPrevZone)) &&
(changing || evt.Moving() || evt.Dragging()))
// So that the highlights in pushbuttons can update
Refresh(false);
mPrevZone = zone;
auto &scrubber = mProject->GetScrubber();
if (scrubber.HasStartedScrubbing()) {
if (IsButton(zone) || evt.RightDown())
// Fall through to pushbutton handling
if (evt.RightDown())
// Fall through to context menu handling
;
else if (zone == StatusChoice::EnteringQP &&
mQuickPlayEnabled &&
evt.LeftDown()) {
// Stop scrubbing
if (HasCapture())
ReleaseMouse();
mProject->OnStop();
// Continue to quick play event handling
}
else {
// If already clicked for scrub, preempt the usual event handling,
// no matter what the y coordinate.
bool switchToQP = (zone == StatusChoice::EnteringQP && mQuickPlayEnabled);
if (switchToQP && evt.LeftDown()) {
// We can't stop scrubbing yet (see comments in Bug 1391), but we can pause it.
mProject->OnPause();
// Don't return, fall through
}
else if (scrubber.IsPaused())
// Just fall through
;
else {
// If already clicked for scrub, preempt the usual event handling,
// no matter what the y coordinate.
// Do this hack so scrubber can detect mouse drags anywhere
evt.ResumePropagation(wxEVENT_PROPAGATE_MAX);
// Do this hack so scrubber can detect mouse drags anywhere
evt.ResumePropagation(wxEVENT_PROPAGATE_MAX);
if (scrubber.IsScrubbing())
evt.Skip();
else if (evt.LeftDClick())
// On the second button down, switch the pending scrub to scrolling
scrubber.MarkScrubStart(evt.m_x, true, false);
else
evt.Skip();
if (scrubber.IsScrubbing())
evt.Skip();
else if (evt.LeftDClick())
// On the second button down, switch the pending scrub to scrolling
scrubber.MarkScrubStart(evt.m_x, true, false);
else
evt.Skip();
// Don't do this, it slows down drag-scrub on Mac.
// Timer updates of display elsewhere make it unnecessary.
// Done here, it's too frequent.
// ShowQuickPlayIndicator();
// Don't do this, it slows down drag-scrub on Mac.
// Timer updates of display elsewhere make it unnecessary.
// Done here, it's too frequent.
// ShowQuickPlayIndicator();
if (HasCapture())
ReleaseMouse();
return;
if (HasCapture())
ReleaseMouse();
return;
}
}
}
@ -2471,14 +2346,10 @@ void AdornedRulerPanel::OnMouseEvents(wxMouseEvent &evt)
return;
}
if (HasCapture() && mCaptureState.button != StatusChoice::NoButton)
HandlePushbuttonEvent(evt);
else if (!HasCapture() && overButtons)
HandlePushbuttonClick(evt);
// Handle popup menus
else if (!HasCapture() && evt.RightDown() && !(evt.LeftIsDown())) {
ShowButtonMenu
(inScrubZone ? StatusChoice::ScrubBarButton : StatusChoice::QuickPlayButton,
if (!HasCapture() && evt.RightDown() && !(evt.LeftIsDown())) {
ShowContextMenu
(inScrubZone ? MenuChoice::Scrub : MenuChoice::QuickPlay,
&position);
return;
}
@ -2680,8 +2551,6 @@ void AdornedRulerPanel::HandleQPRelease(wxMouseEvent &evt)
HideQuickPlayIndicator();
mCaptureState = CaptureState{};
if (mPlayRegionEnd < mPlayRegionStart) {
// Swap values to ensure mPlayRegionStart < mPlayRegionEnd
double tmp = mPlayRegionStart;
@ -2797,39 +2666,32 @@ void AdornedRulerPanel::UpdateStatusBarAndTooltips(StatusChoice choice)
wxString message {};
if (IsButton(choice)) {
bool state = GetButtonState(choice);
const auto &strings = *GetPushButtonStrings(choice);
message = wxGetTranslation(state ? strings.disable : strings.enable);
}
else {
const auto &scrubber = mProject->GetScrubber();
const bool scrubbing = scrubber.HasStartedScrubbing();
if (scrubbing && choice != StatusChoice::Leaving)
// Don't distinguish zones
choice = StatusChoice::EnteringScrubZone;
const auto &scrubber = mProject->GetScrubber();
const bool scrubbing = scrubber.HasStartedScrubbing();
if (scrubbing && choice != StatusChoice::Leaving)
// Don't distinguish zones
choice = StatusChoice::EnteringScrubZone;
switch (choice) {
case StatusChoice::EnteringQP:
{
// message = Insert timeline status bar message here
}
break;
case StatusChoice::EnteringScrubZone:
{
if (scrubbing) {
if(!scrubber.IsAlwaysSeeking())
message = _("Click or drag to seek");
}
else
message = _("Click to scrub, Double-Click to scroll, Drag to seek");
}
break;
default:
break;
switch (choice) {
case StatusChoice::EnteringQP:
{
// message = Insert timeline status bar message here
}
break;
case StatusChoice::EnteringScrubZone:
{
if (scrubbing) {
if(!scrubber.IsAlwaysSeeking())
message = _("Click or drag to seek");
}
else
message = _("Click to scrub, Double-Click to scroll, Drag to seek");
}
break;
default:
break;
}
// Display a message, or empty message
@ -2849,99 +2711,9 @@ void AdornedRulerPanel::OnToggleScrubbing(wxCommandEvent&)
PostSizeEventToParent();
}
void AdornedRulerPanel::OnCaptureKey(wxCommandEvent &event)
{
wxKeyEvent *kevent = (wxKeyEvent *)event.GetEventObject();
int keyCode = kevent->GetKeyCode();
switch (keyCode)
{
case WXK_DOWN:
case WXK_NUMPAD_DOWN:
case WXK_UP:
case WXK_NUMPAD_UP:
case WXK_TAB:
case WXK_NUMPAD_TAB:
case WXK_RIGHT:
case WXK_NUMPAD_RIGHT:
case WXK_LEFT:
case WXK_NUMPAD_LEFT:
case WXK_RETURN:
case WXK_NUMPAD_ENTER:
return;
}
event.Skip();
}
void AdornedRulerPanel::OnKeyDown(wxKeyEvent &event)
{
switch (event.GetKeyCode())
{
case WXK_DOWN:
case WXK_NUMPAD_DOWN:
// Always takes our focus away, so redraw.
mProject->GetTrackPanel()->OnNextTrack();
break;
case WXK_UP:
case WXK_NUMPAD_UP:
mProject->GetTrackPanel()->OnPrevTrack();
break;
case WXK_TAB:
case WXK_NUMPAD_TAB:
if (event.ShiftDown())
goto prev;
else
goto next;
case WXK_RIGHT:
case WXK_NUMPAD_RIGHT:
next:
++mTabState;
Refresh();
break;
case WXK_LEFT:
case WXK_NUMPAD_LEFT:
prev:
--mTabState;
Refresh();
break;
case WXK_RETURN:
case WXK_NUMPAD_ENTER:
if(mTabState.mMenu)
ShowButtonMenu(mTabState.mButton, nullptr);
else {
ToggleButtonState(mTabState.mButton);
Refresh();
}
break;
default:
event.Skip();
break;
}
}
void AdornedRulerPanel::OnSetFocus(wxFocusEvent & WXUNUSED(event))
{
AudacityProject::CaptureKeyboard(this);
mTabState = TabState{};
Refresh( false );
}
void AdornedRulerPanel::OnKillFocus(wxFocusEvent & WXUNUSED(event))
{
AudacityProject::ReleaseKeyboard(this);
Refresh(false);
}
void AdornedRulerPanel::OnContextMenu(wxContextMenuEvent & WXUNUSED(event))
{
ShowButtonMenu(mTabState.mButton, nullptr);
ShowContextMenu(MenuChoice::QuickPlay, nullptr);
}
void AdornedRulerPanel::OnCaptureLost(wxMouseCaptureLostEvent & WXUNUSED(evt))
@ -3013,11 +2785,8 @@ void AdornedRulerPanel::ShowScrubMenu(const wxPoint & pos)
auto cleanup = finally([this]{ PopEventHandler(); });
wxMenu rulerMenu;
auto label = wxGetTranslation(
AdornedRulerPanel::PushbuttonLabels
[static_cast<int>(StatusChoice::ScrubBarButton)].label);
rulerMenu.AppendCheckItem(OnShowHideScrubbingID, _("Scrub Bar"));
if(GetButtonState(StatusChoice::ScrubBarButton))
if(mShowScrubbing)
rulerMenu.FindItem(OnShowHideScrubbingID)->Check();
rulerMenu.AppendSeparator();
@ -3153,329 +2922,31 @@ void AdornedRulerPanel::DoDrawPlayRegion(wxDC * dc)
}
}
wxRect AdornedRulerPanel::GetButtonAreaRect(bool includeBorder) const
void AdornedRulerPanel::ShowContextMenu( MenuChoice choice, const wxPoint *pPosition)
{
int x, y, bottomMargin;
if(includeBorder)
x = 0, y = 0, bottomMargin = 0;
else {
x = std::max(LeftMargin, FocusBorderLeft);
y = std::max(TopMargin, FocusBorderTop);
bottomMargin = std::max(BottomMargin, FocusBorderBottom);
}
wxRect rect {
x, y,
mProject->GetTrackPanel()->GetLeftOffset() - x,
GetRulerHeight() - y - bottomMargin
};
// Leave room for one digit on the ruler, so "0.0" is not obscured if you go to start.
// But the digit string at the left end may be longer if you are not at the start.
// Perhaps there should be room for more than one digit.
wxScreenDC dc;
dc.SetFont(*mRuler.GetFonts().major);
rect.width -= dc.GetTextExtent(wxT("0")).GetWidth();
return rect;
}
wxRect AdornedRulerPanel::GetButtonRect( StatusChoice button ) const
{
if (!IsButton(button))
return wxRect {};
wxRect rect { GetButtonAreaRect() };
// Reduce the height
rect.height -= (GetRulerHeight() - ProperRulerHeight);
auto num = static_cast<unsigned>(button);
auto denom = static_cast<unsigned>(StatusChoice::NumButtons);
rect.x += (num * rect.width) / denom;
rect.width = (((1 + num) * rect.width) / denom) - rect.x;
return rect;
}
auto AdornedRulerPanel::InButtonRect( StatusChoice button, wxMouseEvent *pEvent ) const
-> PointerState
{
auto rect = GetButtonRect(button);
auto state = pEvent ? *pEvent : ::wxGetMouseState();
auto point = pEvent ? pEvent->GetPosition() : ScreenToClient(state.GetPosition());
if(!rect.Contains(point))
return PointerState::Out;
else {
auto rightDown = state.RightIsDown()
#ifdef __WXMAC__
// make drag with Mac Control down act like right drag
|| (state.RawControlDown() && state.ButtonIsDown(wxMOUSE_BTN_ANY))
#endif
;
if(rightDown ||
(pEvent && pEvent->RightUp()) ||
GetArrowRect(rect).Contains(point))
return PointerState::InArrow;
else
return PointerState::In;
}
}
auto AdornedRulerPanel::FindButton( wxMouseEvent &mouseEvent ) const
-> CaptureState
{
for (auto button = StatusChoice::FirstButton; IsButton(button); ++button) {
auto state = InButtonRect( button, &mouseEvent );
if (state != PointerState::Out)
return CaptureState{ button, state };
}
return { StatusChoice::NoButton, PointerState::Out };
}
bool AdornedRulerPanel::GetButtonState( StatusChoice button ) const
{
switch(button) {
case StatusChoice::QuickPlayButton:
return mQuickPlayEnabled;
case StatusChoice::ScrubBarButton:
return mShowScrubbing;
default:
wxASSERT(false);
return false;
}
}
void AdornedRulerPanel::ToggleButtonState( StatusChoice button )
{
wxCommandEvent dummy;
switch(button) {
case StatusChoice::QuickPlayButton:
OnToggleQuickPlay(dummy);
break;
case StatusChoice::ScrubBarButton:
OnToggleScrubbing(dummy);
break;
default:
wxASSERT(false);
}
UpdateStatusBarAndTooltips(mCaptureState.button);
}
void AdornedRulerPanel::ShowButtonMenu( StatusChoice button, const wxPoint *pPosition)
{
if (!IsButton(button))
return;
wxPoint position;
if(pPosition)
position = *pPosition;
else
{
auto rect = GetArrowRect(GetButtonRect(button));
auto rect = GetRect();
position = { rect.GetLeft() + 1, rect.GetBottom() + 1 };
}
// Be sure the arrow button appears pressed
mTabState = { button, true };
mShowingMenu = true;
Refresh();
// Do the rest after Refresh() takes effect
CallAfter([=]{
switch (button) {
case StatusChoice::QuickPlayButton:
ShowMenu(position); break;
case StatusChoice::ScrubBarButton:
ShowScrubMenu(position); break;
default:
return;
}
// dismiss and clear Quick-Play indicator
HideQuickPlayIndicator();
if (HasCapture())
ReleaseMouse();
mShowingMenu = false;
Refresh();
});
}
const AdornedRulerPanel::ButtonStrings AdornedRulerPanel::PushbuttonLabels
[static_cast<size_t>(StatusChoice::NumButtons)]
{
{ XO("Quick-Play"), XO("Enable Quick-Play"), XO("Disable Quick-Play") },
/* i18n-hint: A long screen area (bar) controlling variable speed play (scrubbing) */
{ XO("Scrub Bar"), XO("Show Scrub Bar"), XO("Hide Scrub Bar") },
};
namespace {
void DrawButtonBackground(wxDC *dc, const wxRect &rect, bool down, bool highlight) {
// Choose the pen
if (highlight)
AColor::Light(dc, false);
else
// This color choice corresponds to part of TrackInfo::DrawBordersWithin() :
AColor::Dark(dc, false);
auto pen = dc->GetPen();
// pen.SetWidth(2);
// Choose the brush
if (down)
AColor::Solo(dc, true, false);
else
AColor::MediumTrackInfo(dc, false);
dc->SetPen(pen);
dc->DrawRectangle(rect);
// Draw the bevel
auto rect2 = rect.Deflate(1, 1);
Deflator def(rect2);
AColor::BevelTrackInfo(*dc, !down, rect2);
}
}
void AdornedRulerPanel::DoDrawPushbutton
(wxDC *dc, StatusChoice button, bool buttonState, bool arrowState) const
{
// Adapted from TrackInfo::DrawMuteSolo()
ADCChanger changer(dc);
const auto rect = GetButtonRect( button );
const auto arrowRect = GetArrowRect(rect);
auto arrowBev = arrowRect.Deflate(1, 1);
const auto textRect = GetTextRect(rect);
auto textBev = textRect.Deflate(1, 1);
// Draw borders, bevels, and backgrounds of the split sections
const bool tabHighlight =
mTabState.mButton == button &&
(HasFocus() || rect.Contains( ScreenToClient(::wxGetMousePosition()) ));
if (tabHighlight)
arrowState = arrowState || mShowingMenu;
if (tabHighlight && mTabState.mMenu) {
// Draw highlighted arrow after
DrawButtonBackground(dc, textRect, buttonState, false);
DrawButtonBackground(dc, arrowRect, arrowState, true);
}
else {
// Draw maybe highlighted text after
DrawButtonBackground(dc, arrowRect, arrowState, false);
DrawButtonBackground(dc, textRect, buttonState, (tabHighlight && !mTabState.mMenu));
switch (choice) {
case MenuChoice::QuickPlay:
ShowMenu(position); break;
case MenuChoice::Scrub:
ShowScrubMenu(position); break;
default:
return;
}
// Draw the menu triangle
{
auto x = arrowBev.GetX() + ArrowSpacing;
auto y = arrowBev.GetY() + (arrowBev.GetHeight() - ArrowHeight) / 2;
// dismiss and clear Quick-Play indicator
HideQuickPlayIndicator();
// Color it as in TrackInfo::DrawTitleBar
#ifdef EXPERIMENTAL_THEMING
wxColour c = theTheme.Colour( clrTrackPanelText );
#else
wxColour c = *wxBLACK;
#endif
//if (pointerState == PointerState::InArrow)
dc->SetBrush( wxBrush{ c } );
//else
//dc->SetBrush( wxBrush{ *wxTRANSPARENT_BRUSH } ); // Make outlined arrow only
dc->SetPen( wxPen{ c } );
// This function draws an arrow half as tall as wide:
AColor::Arrow(*dc, x, y, ArrowWidth);
}
// Draw the text
{
dc->SetTextForeground(theTheme.Colour(clrTrackPanelText));
wxCoord textWidth, textHeight;
wxString str = wxGetTranslation(GetPushButtonStrings(button)->label);
dc->SetFont(GetButtonFont());
dc->GetTextExtent(str, &textWidth, &textHeight);
auto xx = textBev.x + (textBev.width - textWidth) / 2;
auto yy = textBev.y + (textBev.height - textHeight) / 2;
if (buttonState)
// Shift the text a bit for "down" appearance
++xx, ++yy;
dc->DrawText(str, xx, yy);
}
}
void AdornedRulerPanel::HandlePushbuttonClick(wxMouseEvent &evt)
{
auto pair = FindButton(evt);
auto button = pair.button;
if (IsButton(button) && evt.ButtonDown()) {
CaptureMouse();
mCaptureState = pair;
Refresh();
}
}
void AdornedRulerPanel::HandlePushbuttonEvent(wxMouseEvent &evt)
{
if(evt.ButtonUp()) {
if(HasCapture())
ReleaseMouse();
auto button = mCaptureState.button;
auto capturedIn = mCaptureState.state;
auto in = InButtonRect(button, &evt);
if (in != capturedIn)
;
else if (in == PointerState::In)
ToggleButtonState(button);
else
ShowButtonMenu(button, nullptr);
mCaptureState = CaptureState{};
}
Refresh();
}
void AdornedRulerPanel::DoDrawPushbuttons(wxDC *dc) const
{
// Paint the area behind the buttons
wxRect background = GetButtonAreaRect();
#ifndef SCRUB_ABOVE
// Reduce the height
background.y = mInner.y;
background.height = mInner.height;
#endif
AColor::MediumTrackInfo(dc, false);
dc->DrawRectangle(background);
for (auto button = StatusChoice::FirstButton; IsButton(button); ++button) {
bool buttonState = GetButtonState(button);
bool arrowState = false;
if (button == mCaptureState.button) {
auto in = InButtonRect(button, nullptr);
if (in == mCaptureState.state) {
if (in == PointerState::In) {
// Toggle button's apparent state for mouseover
buttonState = !buttonState;
}
else if (in == PointerState::InArrow) {
// Menu arrow is not sticky
arrowState = true;
}
}
}
DoDrawPushbutton(dc, button, buttonState, arrowState);
}
if (HasCapture())
ReleaseMouse();
}
void AdornedRulerPanel::DoDrawBackground(wxDC * dc)

View File

@ -315,39 +315,14 @@ public:
void InvalidateRuler();
void UpdatePrefs();
void ReCreateButtons();
enum class StatusChoice {
FirstButton = 0,
QuickPlayButton = FirstButton,
ScrubBarButton,
NumButtons,
LastButton = NumButtons - 1,
NoButton = -1,
EnteringQP = NumButtons,
EnteringQP,
EnteringScrubZone,
Leaving,
NoChange
};
enum class PointerState {
Out = 0, In, InArrow
};
struct CaptureState {
CaptureState() {}
CaptureState(StatusChoice s, PointerState p) : button(s), state(p) {}
StatusChoice button { StatusChoice::NoButton };
PointerState state { PointerState::Out };
};
friend inline StatusChoice &operator++ (StatusChoice &choice) {
choice = static_cast<StatusChoice>(1 + static_cast<int>(choice));
return choice;
}
friend inline StatusChoice &operator-- (StatusChoice &choice) {
choice = static_cast<StatusChoice>(-1 + static_cast<int>(choice));
return choice;
}
void RegenerateTooltips(StatusChoice choice);
@ -367,15 +342,6 @@ private:
void HandleQPRelease(wxMouseEvent &event);
void StartQPPlay(bool looped, bool cutPreview);
static inline bool IsButton(StatusChoice choice)
{
auto integer = static_cast<int>(choice);
return integer >= 0 &&
integer < static_cast<int>(StatusChoice::NumButtons);
}
static inline bool IsButton(int choice)
{ return IsButton(static_cast<StatusChoice>(choice)); }
void UpdateStatusBarAndTooltips(StatusChoice choice);
void OnCaptureLost(wxMouseCaptureLostEvent &evt);
@ -392,32 +358,8 @@ private:
void ShowOrHideQuickPlayIndicator(bool show);
void DoDrawPlayRegion(wxDC * dc);
wxRect GetButtonAreaRect(bool includeBorder = false) const;
struct ButtonStrings {
wxString label, enable, disable;
};
static const ButtonStrings PushbuttonLabels[];
static const ButtonStrings *GetPushButtonStrings(StatusChoice choice)
{
if(IsButton(choice))
return &PushbuttonLabels[static_cast<size_t>(choice)];
return nullptr;
}
wxRect GetButtonRect( StatusChoice button ) const;
PointerState InButtonRect( StatusChoice button, wxMouseEvent *pEvent ) const;
CaptureState FindButton( wxMouseEvent &mouseEvent ) const;
bool GetButtonState( StatusChoice button ) const;
void ToggleButtonState( StatusChoice button );
void ShowButtonMenu( StatusChoice button, const wxPoint *pPosition);
void DoDrawPushbutton
(wxDC *dc, StatusChoice button, bool buttonState, bool arrowState) const;
void DoDrawPushbuttons(wxDC *dc) const;
void HandlePushbuttonClick(wxMouseEvent &evt);
void HandlePushbuttonEvent(wxMouseEvent &evt);
wxFont &GetButtonFont() const;
enum class MenuChoice { QuickPlay, Scrub };
void ShowContextMenu( MenuChoice choice, const wxPoint *pPosition);
double Pos2Time(int p, bool ignoreFisheye = false);
int Time2Pos(double t, bool ignoreFisheye = false);
@ -472,18 +414,12 @@ private:
void OnToggleScrubbing(wxCommandEvent&);
void OnCaptureKey(wxCommandEvent &event);
void OnKeyDown(wxKeyEvent &event);
void OnSetFocus(wxFocusEvent &);
void OnKillFocus(wxFocusEvent &);
void OnContextMenu(wxContextMenuEvent & WXUNUSED(event));
bool mPlayRegionDragsSelection;
bool mTimelineToolTip;
bool mQuickPlayEnabled;
CaptureState mCaptureState {};
enum MouseEventState {
mesNone,
mesDraggingPlayRegionStart,
@ -501,49 +437,9 @@ private:
StatusChoice mPrevZone { StatusChoice::NoChange };
struct TabState {
StatusChoice mButton { StatusChoice::FirstButton };
bool mMenu { false };
TabState() {}
TabState(StatusChoice button, bool menu)
: mButton{ button }, mMenu{ menu } {}
bool operator == (const TabState &rhs) const
{ return mButton == rhs.mButton && mMenu == rhs.mMenu; }
bool operator != (const TabState &rhs) const { return !(*this == rhs); }
TabState &operator ++ () {
if (!mMenu)
mMenu = true;
else {
mMenu = false;
if (!IsButton (++mButton))
mButton = StatusChoice::FirstButton;
}
return *this;
}
TabState &operator -- () {
if (mMenu)
mMenu = false;
else {
mMenu = true;
if (!IsButton (--mButton))
mButton = StatusChoice::LastButton;
}
return *this;
}
};
TabState mTabState;
bool mShowScrubbing { true };
mutable int mButtonFontSize { -1 };
mutable wxFont mButtonFont;
bool mDoubleClick {};
bool mShowingMenu {};
DECLARE_EVENT_TABLE()