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:
commit
d62442827c
2
.gitignore
vendored
2
.gitignore
vendored
@ -108,8 +108,6 @@ mac/tests/
|
||||
*.tlog
|
||||
*.ipch
|
||||
*.opensdf
|
||||
# unsure about the .sal files. Disable for now.
|
||||
*.sal
|
||||
*.vcxproj.user
|
||||
|
||||
# Precompiled Headers
|
||||
|
461
src/AudioIO.cpp
461
src/AudioIO.cpp
@ -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++)
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -730,6 +730,7 @@ public:
|
||||
|
||||
enum class Mode {
|
||||
Off,
|
||||
Refresh,
|
||||
Centered,
|
||||
Right,
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
8347
src/ThemeAsCeeCode.h
8347
src/ThemeAsCeeCode.h
File diff suppressed because it is too large
Load Diff
@ -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(); }
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user