mirror of
https://github.com/cookiengineer/audacity
synced 2025-08-05 14:49:25 +02:00
Problem: Currently calling Track::EnsureVisible() also sets the track as focus. In Audacity 2.3.3 the timing of the code to set the focus was changed. Rather than a direct call, an event is queued, and then the focus is set. This has changed the timing of the focus event which is sent with respect to other focus and name change events. In particular in the case of toggling the selectness of the focused track, this moved the focus event to be after the name change event. These changes only had an effect on NVDA - Jaws and Narrator were unaffected. The introduction of this bug has highlighted an existing problem. 1. There are a small number of existing cases where a track needs to be visible, but where it is already the focus, and so setting the focus is unnecessary. For example, pressing Enter to toggle whether a track is selected. 2. Some of the Audacity code which calls EnsureVisible() is written with the assumption that EnsureVisible() doesn't set the focus, and so there are unnecessary focus events. Whilst other code which calls EnsureVisible() assumes that it also sets the focus. Confusion. The Fix: Remove the setting of focus from within Track::EnsureVisible(), and so remove the unnecessary focus events. Calls to set the focus were added before calls to EnsureVisible where the code was relying on EnsureVisible to set the focus. In TrackPanel::ProcessUIHandleResult, and TrackPanel::OnMouseEvent, I wasn't sure if the focus needed to be set, so called it anyway to ensure that the behaviour did not change. So I would like to remove the setting of focus from within Track::EnsureVisible(), and add explicit calls to set the focus where necessary. I think this would make the code clearer, remove unnecessary calls to set the focus, and make it easier to keep NVDA happy.
251 lines
7.2 KiB
C++
251 lines
7.2 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
TrackUtilities.cpp
|
|
|
|
Paul Licameli split from TrackMenus.cpp
|
|
|
|
**********************************************************************/
|
|
|
|
#include "TrackUtilities.h"
|
|
|
|
#include "ProjectHistory.h"
|
|
#include "ProjectSettings.h"
|
|
#include "ProjectWindow.h"
|
|
#include "Track.h"
|
|
#include "TrackPanelAx.h"
|
|
#include "TrackPanel.h"
|
|
|
|
namespace TrackUtilities {
|
|
|
|
void DoRemoveTracks( AudacityProject &project )
|
|
{
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackPanel = TrackPanel::Get( project );
|
|
|
|
std::vector<Track*> toRemove;
|
|
for (auto track : tracks.Selected())
|
|
toRemove.push_back(track);
|
|
|
|
// Capture the track preceding the first removed track
|
|
Track *f{};
|
|
if (!toRemove.empty()) {
|
|
auto found = tracks.Find(toRemove[0]);
|
|
f = *--found;
|
|
}
|
|
|
|
for (auto track : toRemove)
|
|
tracks.Remove(track);
|
|
|
|
if (!f)
|
|
// try to use the last track
|
|
f = *tracks.Any().rbegin();
|
|
if (f) {
|
|
// Try to use the first track after the removal
|
|
auto found = tracks.FindLeader(f);
|
|
auto t = *++found;
|
|
if (t)
|
|
f = t;
|
|
}
|
|
|
|
// If we actually have something left, then set focus and make sure it's seen
|
|
if (f) {
|
|
TrackFocus::Get(project).Set(f);
|
|
f->EnsureVisible();
|
|
}
|
|
|
|
ProjectHistory::Get( project )
|
|
.PushState(_("Removed audio track(s)"), _("Remove Track"));
|
|
|
|
trackPanel.UpdateViewIfNoTracks();
|
|
}
|
|
|
|
void DoTrackMute(AudacityProject &project, Track *t, bool exclusive)
|
|
{
|
|
const auto &settings = ProjectSettings::Get( project );
|
|
auto &tracks = TrackList::Get( project );
|
|
|
|
// Whatever t is, replace with lead channel
|
|
t = *tracks.FindLeader(t);
|
|
|
|
// "exclusive" mute means mute the chosen track and unmute all others.
|
|
if (exclusive) {
|
|
for (auto leader : tracks.Leaders<PlayableTrack>()) {
|
|
const auto group = TrackList::Channels(leader);
|
|
bool chosen = (t == leader);
|
|
for (auto channel : group)
|
|
channel->SetMute( chosen ),
|
|
channel->SetSolo( false );
|
|
}
|
|
}
|
|
else {
|
|
// Normal click toggles this track.
|
|
auto pt = dynamic_cast<PlayableTrack *>( t );
|
|
if (!pt)
|
|
return;
|
|
|
|
bool wasMute = pt->GetMute();
|
|
for (auto channel : TrackList::Channels(pt))
|
|
channel->SetMute( !wasMute );
|
|
|
|
if (settings.IsSoloSimple() || settings.IsSoloNone())
|
|
{
|
|
// We also set a solo indicator if we have just one track / stereo pair playing.
|
|
// in a group of more than one playable tracks.
|
|
// otherwise clear solo on everything.
|
|
|
|
auto range = tracks.Leaders<PlayableTrack>();
|
|
auto nPlayableTracks = range.size();
|
|
auto nPlaying = (range - &PlayableTrack::GetMute).size();
|
|
|
|
for (auto track : tracks.Any<PlayableTrack>())
|
|
// will set both of a stereo pair
|
|
track->SetSolo( (nPlaying==1) && (nPlayableTracks > 1 ) && !track->GetMute() );
|
|
}
|
|
}
|
|
ProjectHistory::Get( project ).ModifyState(true);
|
|
|
|
TrackFocus::Get( project ).UpdateAccessibility();
|
|
}
|
|
|
|
void DoTrackSolo(AudacityProject &project, Track *t, bool exclusive)
|
|
{
|
|
const auto &settings = ProjectSettings::Get( project );
|
|
auto &tracks = TrackList::Get( project );
|
|
|
|
// Whatever t is, replace with lead channel
|
|
t = *tracks.FindLeader(t);
|
|
|
|
const auto pt = dynamic_cast<PlayableTrack *>( t );
|
|
if (!pt)
|
|
return;
|
|
bool bWasSolo = pt->GetSolo();
|
|
|
|
bool bSoloMultiple = !settings.IsSoloSimple() ^ exclusive;
|
|
|
|
// Standard and Simple solo have opposite defaults:
|
|
// Standard - Behaves as individual buttons, shift=radio buttons
|
|
// Simple - Behaves as radio buttons, shift=individual
|
|
// In addition, Simple solo will mute/unmute tracks
|
|
// when in standard radio button mode.
|
|
if ( bSoloMultiple )
|
|
{
|
|
for (auto channel : TrackList::Channels(pt))
|
|
channel->SetSolo( !bWasSolo );
|
|
}
|
|
else
|
|
{
|
|
// Normal click solo this track only, mute everything else.
|
|
// OR unmute and unsolo everything.
|
|
for (auto leader : tracks.Leaders<PlayableTrack>()) {
|
|
const auto group = TrackList::Channels(leader);
|
|
bool chosen = (t == leader);
|
|
for (auto channel : group) {
|
|
if (chosen) {
|
|
channel->SetSolo( !bWasSolo );
|
|
if( settings.IsSoloSimple() )
|
|
channel->SetMute( false );
|
|
}
|
|
else {
|
|
channel->SetSolo( false );
|
|
if( settings.IsSoloSimple() )
|
|
channel->SetMute( !bWasSolo );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ProjectHistory::Get( project ).ModifyState(true);
|
|
|
|
TrackFocus::Get( project ).UpdateAccessibility();
|
|
}
|
|
|
|
void DoRemoveTrack(AudacityProject &project, Track * toRemove)
|
|
{
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackFocus = TrackFocus::Get( project );
|
|
auto &window = ProjectWindow::Get( project );
|
|
|
|
// If it was focused, then NEW focus is the next or, if
|
|
// unavailable, the previous track. (The NEW focus is set
|
|
// after the track has been removed.)
|
|
bool toRemoveWasFocused = trackFocus.Get() == toRemove;
|
|
Track* newFocus{};
|
|
if (toRemoveWasFocused) {
|
|
auto iterNext = tracks.FindLeader(toRemove), iterPrev = iterNext;
|
|
newFocus = *++iterNext;
|
|
if (!newFocus) {
|
|
newFocus = *--iterPrev;
|
|
}
|
|
}
|
|
|
|
wxString name = toRemove->GetName();
|
|
|
|
auto channels = TrackList::Channels(toRemove);
|
|
// Be careful to post-increment over positions that get erased!
|
|
auto &iter = channels.first;
|
|
while (iter != channels.end())
|
|
tracks.Remove( * iter++ );
|
|
|
|
if (toRemoveWasFocused)
|
|
trackFocus.Set( newFocus );
|
|
|
|
ProjectHistory::Get( project ).PushState(
|
|
wxString::Format(_("Removed track '%s.'"),
|
|
name),
|
|
_("Track Remove"));
|
|
}
|
|
|
|
void DoMoveTrack
|
|
(AudacityProject &project, Track* target, MoveChoice choice)
|
|
{
|
|
auto &tracks = TrackList::Get( project );
|
|
|
|
wxString longDesc, shortDesc;
|
|
|
|
switch (choice)
|
|
{
|
|
case OnMoveTopID:
|
|
/* i18n-hint: Past tense of 'to move', as in 'moved audio track up'.*/
|
|
longDesc = _("Moved '%s' to Top");
|
|
shortDesc = _("Move Track to Top");
|
|
|
|
// TODO: write TrackList::Rotate to do this in one step and avoid emitting
|
|
// an event for each swap
|
|
while (tracks.CanMoveUp(target))
|
|
tracks.Move(target, true);
|
|
|
|
break;
|
|
case OnMoveBottomID:
|
|
/* i18n-hint: Past tense of 'to move', as in 'moved audio track up'.*/
|
|
longDesc = _("Moved '%s' to Bottom");
|
|
shortDesc = _("Move Track to Bottom");
|
|
|
|
// TODO: write TrackList::Rotate to do this in one step and avoid emitting
|
|
// an event for each swap
|
|
while (tracks.CanMoveDown(target))
|
|
tracks.Move(target, false);
|
|
|
|
break;
|
|
default:
|
|
bool bUp = (OnMoveUpID == choice);
|
|
|
|
tracks.Move(target, bUp);
|
|
longDesc =
|
|
/* i18n-hint: Past tense of 'to move', as in 'moved audio track up'.*/
|
|
bUp? _("Moved '%s' Up")
|
|
: _("Moved '%s' Down");
|
|
shortDesc =
|
|
/* i18n-hint: Past tense of 'to move', as in 'moved audio track up'.*/
|
|
bUp? _("Move Track Up")
|
|
: _("Move Track Down");
|
|
|
|
}
|
|
|
|
longDesc = longDesc.Format(target->GetName());
|
|
|
|
ProjectHistory::Get( project ).PushState(longDesc, shortDesc);
|
|
}
|
|
|
|
}
|