2865 lines
85 KiB
C++
2865 lines
85 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
TrackPanel.cpp
|
|
|
|
Dominic Mazzoni
|
|
and lots of other contributors
|
|
|
|
Implements TrackPanel and TrackInfo.
|
|
|
|
********************************************************************//*!
|
|
|
|
\file TrackPanel.cpp
|
|
\brief
|
|
Implements TrackPanel and TrackInfo.
|
|
|
|
*//***************************************************************//**
|
|
|
|
\class TrackPanel
|
|
\brief
|
|
The TrackPanel class coordinates updates and operations on the
|
|
main part of the screen which contains multiple tracks.
|
|
|
|
It uses many other classes, but in particular it uses the
|
|
TrackInfo class to draw the controls area on the left of a track,
|
|
and the TrackArtist class to draw the actual waveforms.
|
|
|
|
Note that in some of the older code here, e.g., GetLabelWidth(),
|
|
"Label" means the TrackInfo plus the vertical ruler.
|
|
Confusing relative to LabelTrack labels.
|
|
|
|
The TrackPanel manages multiple tracks and their TrackInfos.
|
|
|
|
Note that with stereo tracks there will be one TrackInfo
|
|
being used by two wavetracks.
|
|
|
|
*//*****************************************************************//**
|
|
|
|
\namespace TrackInfo
|
|
\brief
|
|
Functions for drawing the track control panel, which is shown to the side
|
|
of a track
|
|
It has the menus, pan and gain controls displayed in it.
|
|
So "Info" is somewhat a misnomer. Should possibly be "TrackControls".
|
|
|
|
It maintains global slider widget instances that are reparented and
|
|
repositioned as needed for drawing and interaction with the user,
|
|
interoperating with the custom panel subdivision implemented in CellularPanel
|
|
and avoiding wxWidgets sizers
|
|
|
|
If we'd instead coded it as a wxWindow, we would have an instance
|
|
of this class for each track displayed.
|
|
|
|
*//**************************************************************//**
|
|
|
|
\class TrackPanel::AudacityTimer
|
|
\brief Timer class dedicated to informing the TrackPanel that it
|
|
is time to refresh some aspect of the screen.
|
|
|
|
*//*****************************************************************/
|
|
|
|
#include "Audacity.h" // for USE_* macros
|
|
#include "TrackPanel.h"
|
|
|
|
#include <wx/setup.h> // for wxUSE_* macros
|
|
|
|
#include "Experimental.h"
|
|
|
|
#include "AdornedRulerPanel.h"
|
|
#include "KeyboardCapture.h"
|
|
#include "Project.h"
|
|
#include "TrackPanelMouseEvent.h"
|
|
#include "TrackPanelResizeHandle.h"
|
|
//#define DEBUG_DRAW_TIMING 1
|
|
|
|
#include "AColor.h"
|
|
#include "AllThemeResources.h"
|
|
#include "AudioIO.h"
|
|
#include "float_cast.h"
|
|
|
|
#include "Prefs.h"
|
|
#include "RefreshCode.h"
|
|
#include "TrackArtist.h"
|
|
#include "TrackPanelAx.h"
|
|
#include "ViewInfo.h"
|
|
#include "WaveTrack.h"
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
#include "NoteTrack.h"
|
|
#endif
|
|
|
|
#include "ondemand/ODManager.h"
|
|
#include "ondemand/ODTask.h"
|
|
|
|
#include "toolbars/ControlToolBar.h"
|
|
#include "toolbars/ToolsToolBar.h"
|
|
|
|
#include "tracks/ui/TrackControls.h" // for inheritance relation
|
|
#include "tracks/ui/TrackVRulerControls.h" // for inheritance relation
|
|
|
|
//This loads the appropriate set of cursors, depending on platform.
|
|
#include "../images/Cursors.h"
|
|
|
|
#include "widgets/ASlider.h"
|
|
#include <algorithm>
|
|
|
|
#include <wx/dcclient.h>
|
|
|
|
wxDEFINE_EVENT(EVT_TRACK_PANEL_TIMER, wxCommandEvent);
|
|
|
|
/**
|
|
|
|
\class TrackPanel
|
|
|
|
This is a diagram of TrackPanel's division of one (non-stereo) track rectangle.
|
|
Total height equals Track::GetHeight()'s value. Total width is the wxWindow's
|
|
width. Each charater that is not . represents one pixel.
|
|
|
|
Inset space of this track, and top inset of the next track, are used to draw the
|
|
focus highlight.
|
|
|
|
Top inset of the right channel of a stereo track, and bottom shadow line of the
|
|
left channel, are used for the channel separator.
|
|
|
|
"Margin" is a term used for inset plus border (top and left) or inset plus
|
|
shadow plus border (right and bottom).
|
|
|
|
GetVRulerOffset() counts columns from the left edge up to and including
|
|
controls, and is a constant.
|
|
|
|
GetVRulerWidth() is variable -- all tracks have the same ruler width at any
|
|
time, but that width may be adjusted when tracks change their vertical scales.
|
|
|
|
GetLabelWidth() counts columns up to and including the VRuler.
|
|
GetLeftOffset() is yet one more -- it counts the "one pixel" column.
|
|
|
|
Cell for label has a rectangle that OMITS left, top, and bottom
|
|
margins
|
|
|
|
Cell for vruler has a rectangle right of the label,
|
|
up to and including the One Pixel column, and OMITS top and bottom margins
|
|
|
|
Cell() for track returns a rectangle with x == GetLeftOffset(), and OMITS
|
|
right, top, and bottom margins
|
|
|
|
+--------------- ... ------ ... --------------------- ... ... -------------+
|
|
| Top Inset |
|
|
| |
|
|
| +------------ ... ------ ... --------------------- ... ... ----------+ |
|
|
| L|+-Border---- ... ------ ... --------------------- ... ... -Border-+ |R |
|
|
| e||+---------- ... -++--- ... -+++----------------- ... ... -------+| |i |
|
|
| f|B| || ||| |BS|g |
|
|
| t|o| Controls || V |O| The good stuff |oh|h |
|
|
| |r| || R |n| |ra|t |
|
|
| I|d| || u |e| |dd| |
|
|
| n|e| || l | | |eo|I |
|
|
| s|r| || e |P| |rw|n |
|
|
| e||| || r |i| ||||s |
|
|
| t||| || |x| ||||e |
|
|
| ||| || |e| ||||t |
|
|
| ||| || |l| |||| |
|
|
| ||| || ||| |||| |
|
|
|
|
. ... .. ... .... .
|
|
. ... .. ... .... .
|
|
. ... .. ... .... .
|
|
|
|
| ||| || ||| |||| |
|
|
| ||+---------- -++-- ... -+++----------------- ... ... -------+||| |
|
|
| |+-Border---- ... ----- ... --------------------- ... ... -Border-+|| |
|
|
| | Shadow---- ... ----- ... --------------------- ... ... --Shadow-+| |
|
|
*/
|
|
|
|
// Is the distance between A and B less than D?
|
|
template < class A, class B, class DIST > bool within(A a, B b, DIST d)
|
|
{
|
|
return (a > b - d) && (a < b + d);
|
|
}
|
|
|
|
BEGIN_EVENT_TABLE(TrackPanel, CellularPanel)
|
|
EVT_MOUSE_EVENTS(TrackPanel::OnMouseEvent)
|
|
EVT_KEY_DOWN(TrackPanel::OnKeyDown)
|
|
|
|
EVT_PAINT(TrackPanel::OnPaint)
|
|
|
|
EVT_TIMER(wxID_ANY, TrackPanel::OnTimer)
|
|
|
|
EVT_COMMAND(wxID_ANY, EVT_ODTASK_UPDATE, TrackPanel::OnODTask)
|
|
EVT_COMMAND(wxID_ANY, EVT_ODTASK_COMPLETE, TrackPanel::OnODTask)
|
|
END_EVENT_TABLE()
|
|
|
|
/// Makes a cursor from an XPM, uses CursorId as a fallback.
|
|
/// TODO: Move this function to some other source file for reuse elsewhere.
|
|
std::unique_ptr<wxCursor> MakeCursor( int WXUNUSED(CursorId), const char * const pXpm[36], int HotX, int HotY )
|
|
{
|
|
#ifdef CURSORS_SIZE32
|
|
const int HotAdjust =0;
|
|
#else
|
|
const int HotAdjust =8;
|
|
#endif
|
|
|
|
wxImage Image = wxImage(wxBitmap(pXpm).ConvertToImage());
|
|
Image.SetMaskColour(255,0,0);
|
|
Image.SetMask();// Enable mask.
|
|
|
|
Image.SetOption( wxIMAGE_OPTION_CUR_HOTSPOT_X, HotX-HotAdjust );
|
|
Image.SetOption( wxIMAGE_OPTION_CUR_HOTSPOT_Y, HotY-HotAdjust );
|
|
return std::make_unique<wxCursor>( Image );
|
|
}
|
|
|
|
|
|
|
|
// Don't warn us about using 'this' in the base member initializer list.
|
|
#ifndef __WXGTK__ //Get rid if this pragma for gtk
|
|
#pragma warning( disable: 4355 )
|
|
#endif
|
|
TrackPanel::TrackPanel(wxWindow * parent, wxWindowID id,
|
|
const wxPoint & pos,
|
|
const wxSize & size,
|
|
const std::shared_ptr<TrackList> &tracks,
|
|
ViewInfo * viewInfo,
|
|
TrackPanelListener * listener,
|
|
AdornedRulerPanel * ruler)
|
|
: CellularPanel(parent, id, pos, size, viewInfo,
|
|
wxWANTS_CHARS | wxNO_BORDER),
|
|
mListener(listener),
|
|
mTracks(tracks),
|
|
mRuler(ruler),
|
|
mTrackArtist(nullptr),
|
|
mRefreshBacking(false),
|
|
vrulerSize(36,0)
|
|
#ifndef __WXGTK__ //Get rid if this pragma for gtk
|
|
#pragma warning( default: 4355 )
|
|
#endif
|
|
{
|
|
TrackInfo::ReCreateSliders( this );
|
|
TrackInfo::UpdatePrefs( this );
|
|
|
|
SetLayoutDirection(wxLayout_LeftToRight);
|
|
SetLabel(_("Track Panel"));
|
|
SetName(_("Track Panel"));
|
|
SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
|
|
{
|
|
auto pAx = std::make_unique <TrackPanelAx>( this );
|
|
#if wxUSE_ACCESSIBILITY
|
|
// wxWidgets owns the accessible object
|
|
SetAccessible(mAx = pAx.release());
|
|
#else
|
|
// wxWidgets does not own the object, but we need to retain it
|
|
mAx = std::move(pAx);
|
|
#endif
|
|
}
|
|
|
|
mRedrawAfterStop = false;
|
|
|
|
mTrackArtist = std::make_unique<TrackArtist>( this );
|
|
|
|
mTimeCount = 0;
|
|
mTimer.parent = this;
|
|
// Timer is started after the window is visible
|
|
GetProject()->Bind(wxEVT_IDLE, &TrackPanel::OnIdle, this);
|
|
|
|
// Register for tracklist updates
|
|
mTracks->Bind(EVT_TRACKLIST_RESIZING,
|
|
&TrackPanel::OnTrackListResizing,
|
|
this);
|
|
mTracks->Bind(EVT_TRACKLIST_ADDITION,
|
|
&TrackPanel::OnTrackListResizing,
|
|
this);
|
|
mTracks->Bind(EVT_TRACKLIST_DELETION,
|
|
&TrackPanel::OnTrackListDeletion,
|
|
this);
|
|
wxTheApp->Bind(EVT_AUDIOIO_PLAYBACK,
|
|
&TrackPanel::OnPlayback,
|
|
this);
|
|
|
|
UpdatePrefs();
|
|
}
|
|
|
|
|
|
TrackPanel::~TrackPanel()
|
|
{
|
|
mTimer.Stop();
|
|
|
|
// This can happen if a label is being edited and the user presses
|
|
// ALT+F4 or Command+Q
|
|
if (HasCapture())
|
|
ReleaseMouse();
|
|
}
|
|
|
|
LWSlider *TrackPanel::GainSlider( const WaveTrack *wt )
|
|
{
|
|
auto pControls = wt->GetTrackControl();
|
|
auto rect = FindRect( *pControls );
|
|
wxRect sliderRect;
|
|
TrackInfo::GetGainRect( rect.GetTopLeft(), sliderRect );
|
|
return TrackInfo::GainSlider(sliderRect, wt, false, this);
|
|
}
|
|
|
|
LWSlider *TrackPanel::PanSlider( const WaveTrack *wt )
|
|
{
|
|
auto pControls = wt->GetTrackControl();
|
|
auto rect = FindRect( *pControls );
|
|
wxRect sliderRect;
|
|
TrackInfo::GetPanRect( rect.GetTopLeft(), sliderRect );
|
|
return TrackInfo::PanSlider(sliderRect, wt, false, this);
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
LWSlider *TrackPanel::VelocitySlider( const NoteTrack *nt )
|
|
{
|
|
auto pControls = nt->GetTrackControl();
|
|
auto rect = FindRect( *pControls );
|
|
wxRect sliderRect;
|
|
TrackInfo::GetVelocityRect( rect.GetTopLeft(), sliderRect );
|
|
return TrackInfo::VelocitySlider(sliderRect, nt, false, this);
|
|
}
|
|
#endif
|
|
|
|
wxString TrackPanel::gSoloPref;
|
|
|
|
void TrackPanel::UpdatePrefs()
|
|
{
|
|
gPrefs->Read(wxT("/GUI/Solo"), &gSoloPref, wxT("Simple"));
|
|
|
|
// All vertical rulers must be recalculated since the minimum and maximum
|
|
// frequences may have been changed.
|
|
UpdateVRulers();
|
|
|
|
TrackInfo::UpdatePrefs( this );
|
|
|
|
Refresh();
|
|
}
|
|
|
|
void TrackPanel::ApplyUpdatedTheme()
|
|
{
|
|
TrackInfo::ReCreateSliders( this );
|
|
}
|
|
|
|
|
|
wxSize TrackPanel::GetTracksUsableArea() const
|
|
{
|
|
auto size = GetSize();
|
|
return {
|
|
std::max( 0, size.GetWidth() - ( GetLeftOffset() + kRightMargin ) ),
|
|
size.GetHeight()
|
|
};
|
|
}
|
|
|
|
void TrackPanel::GetTracksUsableArea(int *width, int *height) const
|
|
{
|
|
auto size = GetTracksUsableArea();
|
|
if (width)
|
|
*width = size.GetWidth();
|
|
if (height)
|
|
*height = size.GetHeight();
|
|
}
|
|
|
|
/// Gets the pointer to the AudacityProject that
|
|
/// goes with this track panel.
|
|
AudacityProject * TrackPanel::GetProject() const
|
|
{
|
|
//JKC casting away constness here.
|
|
//Do it in two stages in case 'this' is not a wxWindow.
|
|
//when the compiler will flag the error.
|
|
wxWindow const * const pConstWind = this;
|
|
wxWindow * pWind=(wxWindow*)pConstWind;
|
|
#ifdef EXPERIMENTAL_NOTEBOOK
|
|
pWind = pWind->GetParent(); //Page
|
|
wxASSERT( pWind );
|
|
pWind = pWind->GetParent(); //Notebook
|
|
wxASSERT( pWind );
|
|
#endif
|
|
pWind = pWind->GetParent(); //MainPanel
|
|
wxASSERT( pWind );
|
|
pWind = pWind->GetParent(); //Project
|
|
wxASSERT( pWind );
|
|
return (AudacityProject*)pWind;
|
|
}
|
|
|
|
void TrackPanel::OnIdle(wxIdleEvent& event)
|
|
{
|
|
// The window must be ready when the timer fires (#1401)
|
|
if (IsShownOnScreen())
|
|
{
|
|
mTimer.Start(kTimerInterval, FALSE);
|
|
|
|
// Timer is started, we don't need the event anymore
|
|
GetProject()->Unbind(wxEVT_IDLE, &TrackPanel::OnIdle, this);
|
|
}
|
|
else
|
|
{
|
|
// Get another idle event, wx only guarantees we get one
|
|
// event after "some other normal events occur"
|
|
event.RequestMore();
|
|
}
|
|
}
|
|
|
|
/// AS: This gets called on our wx timer events.
|
|
void TrackPanel::OnTimer(wxTimerEvent& )
|
|
{
|
|
#ifdef __WXMAC__
|
|
// Unfortunate part of fix for bug 1431
|
|
// Without this, the toolbars hide only every other time that you press
|
|
// the yellow title bar button. For some reason, not every press sends
|
|
// us a deactivate event for the application.
|
|
{
|
|
auto project = GetProject();
|
|
if (project->IsIconized())
|
|
project->MacShowUndockedToolbars(false);
|
|
}
|
|
#endif
|
|
|
|
mTimeCount++;
|
|
|
|
AudacityProject *const p = GetProject();
|
|
|
|
// Check whether we were playing or recording, but the stream has stopped.
|
|
if (p->GetAudioIOToken()>0 && !IsAudioActive())
|
|
{
|
|
//the stream may have been started up after this one finished (by some other project)
|
|
//in that case reset the buttons don't stop the stream
|
|
p->GetControlToolBar()->StopPlaying(!gAudioIO->IsStreamActive());
|
|
}
|
|
|
|
// Next, check to see if we were playing or recording
|
|
// audio, but now Audio I/O is completely finished.
|
|
if (p->GetAudioIOToken()>0 &&
|
|
!gAudioIO->IsAudioTokenActive(p->GetAudioIOToken()))
|
|
{
|
|
p->FixScrollbars();
|
|
p->SetAudioIOToken(0);
|
|
p->RedrawProject();
|
|
|
|
mRedrawAfterStop = false;
|
|
|
|
//ANSWER-ME: Was DisplaySelection added to solve a repaint problem?
|
|
DisplaySelection();
|
|
}
|
|
if (mLastDrawnSelectedRegion != mViewInfo->selectedRegion) {
|
|
UpdateSelectionDisplay();
|
|
}
|
|
|
|
// Notify listeners for timer ticks
|
|
{
|
|
wxCommandEvent e(EVT_TRACK_PANEL_TIMER);
|
|
p->GetEventHandler()->ProcessEvent(e);
|
|
}
|
|
|
|
DrawOverlays(false);
|
|
mRuler->DrawOverlays(false);
|
|
|
|
if(IsAudioActive() && gAudioIO->GetNumCaptureChannels()) {
|
|
|
|
// Periodically update the display while recording
|
|
|
|
if (!mRedrawAfterStop) {
|
|
mRedrawAfterStop = true;
|
|
MakeParentRedrawScrollbars();
|
|
mListener->TP_ScrollUpDown( 99999999 );
|
|
Refresh( false );
|
|
}
|
|
else {
|
|
if ((mTimeCount % 5) == 0) {
|
|
// Must tell OnPaint() to recreate the backing bitmap
|
|
// since we've not done a full refresh.
|
|
mRefreshBacking = true;
|
|
Refresh( false );
|
|
}
|
|
}
|
|
}
|
|
if(mTimeCount > 1000)
|
|
mTimeCount = 0;
|
|
}
|
|
|
|
///Handles the redrawing necessary for tasks as they partially update in the
|
|
///background, or finish.
|
|
void TrackPanel::OnODTask(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
//todo: add track data to the event - check to see if the project contains it before redrawing.
|
|
Refresh(false);
|
|
}
|
|
|
|
double TrackPanel::GetScreenEndTime() const
|
|
{
|
|
int width;
|
|
GetTracksUsableArea(&width, NULL);
|
|
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
|
|
|
|
{
|
|
wxPaintDC dc(this);
|
|
|
|
// Retrieve the damage rectangle
|
|
wxRect box = GetUpdateRegion().GetBox();
|
|
|
|
// Recreate the backing bitmap if we have a full refresh
|
|
// (See TrackPanel::Refresh())
|
|
if (mRefreshBacking || (box == GetRect()))
|
|
{
|
|
// Reset (should a mutex be used???)
|
|
mRefreshBacking = false;
|
|
|
|
// Redraw the backing bitmap
|
|
DrawTracks(&GetBackingDCForRepaint());
|
|
|
|
// Copy it to the display
|
|
DisplayBitmap(dc);
|
|
}
|
|
else
|
|
{
|
|
// Copy full, possibly clipped, damage rectangle
|
|
RepairBitmap(dc, box.x, box.y, box.width, box.height);
|
|
}
|
|
|
|
// Done with the clipped DC
|
|
|
|
// Drawing now goes directly to the client area.
|
|
// DrawOverlays() may need to draw outside the clipped region.
|
|
// (Used to make a NEW, separate wxClientDC, but that risks flashing
|
|
// problems on Mac.)
|
|
dc.DestroyClippingRegion();
|
|
DrawOverlays(true, &dc);
|
|
}
|
|
|
|
#if DEBUG_DRAW_TIMING
|
|
sw.Pause();
|
|
wxLogDebug(wxT("Total: %ld milliseconds"), sw.Time());
|
|
wxPrintf(wxT("Total: %ld milliseconds\n"), sw.Time());
|
|
#endif
|
|
}
|
|
|
|
void TrackPanel::MakeParentModifyState(bool bWantsAutoSave)
|
|
{
|
|
GetProject()->ModifyState(bWantsAutoSave);
|
|
}
|
|
|
|
void TrackPanel::MakeParentRedrawScrollbars()
|
|
{
|
|
mListener->TP_RedrawScrollbars();
|
|
}
|
|
|
|
namespace {
|
|
std::shared_ptr<Track> FindTrack(TrackPanelCell *pCell )
|
|
{
|
|
if (pCell)
|
|
return static_cast<CommonTrackPanelCell*>( pCell )->FindTrack();
|
|
return {};
|
|
}
|
|
}
|
|
|
|
void TrackPanel::ProcessUIHandleResult
|
|
(TrackPanelCell *pClickedCell, TrackPanelCell *pLatestCell,
|
|
UIHandle::Result refreshResult)
|
|
{
|
|
const auto panel = this;
|
|
auto pLatestTrack = FindTrack( pLatestCell ).get();
|
|
|
|
// This precaution checks that the track is not only nonnull, but also
|
|
// really owned by the track list
|
|
auto pClickedTrack = GetTracks()->Lock(
|
|
std::weak_ptr<Track>{ FindTrack( pClickedCell ) }
|
|
).get();
|
|
|
|
// TODO: make a finer distinction between refreshing the track control area,
|
|
// and the waveform area. As it is, redraw both whenever you must redraw either.
|
|
|
|
// Copy data from the underlying tracks to the pending tracks that are
|
|
// really displayed
|
|
TrackList::Get( *panel->GetProject() ).UpdatePendingTracks();
|
|
|
|
using namespace RefreshCode;
|
|
|
|
if (refreshResult & DestroyedCell) {
|
|
panel->UpdateViewIfNoTracks();
|
|
// Beware stale pointer!
|
|
if (pLatestTrack == pClickedTrack)
|
|
pLatestTrack = NULL;
|
|
pClickedTrack = NULL;
|
|
}
|
|
|
|
if (pClickedTrack && (refreshResult & RefreshCode::UpdateVRuler))
|
|
panel->UpdateVRuler(pClickedTrack);
|
|
|
|
if (refreshResult & RefreshCode::DrawOverlays) {
|
|
panel->DrawOverlays(false);
|
|
mRuler->DrawOverlays(false);
|
|
}
|
|
|
|
// Refresh all if told to do so, or if told to refresh a track that
|
|
// is not known.
|
|
const bool refreshAll =
|
|
( (refreshResult & RefreshAll)
|
|
|| ((refreshResult & RefreshCell) && !pClickedTrack)
|
|
|| ((refreshResult & RefreshLatestCell) && !pLatestTrack));
|
|
|
|
if (refreshAll)
|
|
panel->Refresh(false);
|
|
else {
|
|
if (refreshResult & RefreshCell)
|
|
panel->RefreshTrack(pClickedTrack);
|
|
if (refreshResult & RefreshLatestCell)
|
|
panel->RefreshTrack(pLatestTrack);
|
|
}
|
|
|
|
if (refreshResult & FixScrollbars)
|
|
panel->MakeParentRedrawScrollbars();
|
|
|
|
if (refreshResult & Resize)
|
|
panel->GetListener()->TP_HandleResize();
|
|
|
|
// This flag is superfluous if you do full refresh,
|
|
// because TrackPanel::Refresh() does this too
|
|
if (refreshResult & UpdateSelection) {
|
|
panel->DisplaySelection();
|
|
|
|
{
|
|
// Formerly in TrackPanel::UpdateSelectionDisplay():
|
|
|
|
// Make sure the ruler follows suit.
|
|
// mRuler->DrawSelection();
|
|
|
|
// ... but that too is superfluous it does nothing but refresh
|
|
// the ruler, while DisplaySelection calls TP_DisplaySelection which
|
|
// also always refreshes the ruler.
|
|
}
|
|
}
|
|
|
|
if ((refreshResult & RefreshCode::EnsureVisible) && pClickedTrack)
|
|
panel->EnsureVisible(pClickedTrack);
|
|
}
|
|
|
|
void TrackPanel::HandlePageUpKey()
|
|
{
|
|
mListener->TP_ScrollWindow(2 * mViewInfo->h - GetScreenEndTime());
|
|
}
|
|
|
|
void TrackPanel::HandlePageDownKey()
|
|
{
|
|
mListener->TP_ScrollWindow(GetScreenEndTime());
|
|
}
|
|
|
|
bool TrackPanel::IsAudioActive()
|
|
{
|
|
AudacityProject *p = GetProject();
|
|
return p->IsAudioActive();
|
|
}
|
|
|
|
void TrackPanel::UpdateStatusMessage( const wxString &st )
|
|
{
|
|
auto status = st;
|
|
if (HasEscape())
|
|
/* i18n-hint Esc is a key on the keyboard */
|
|
status += wxT(" "), status += _("(Esc to cancel)");
|
|
mListener->TP_DisplayStatusMessage(status);
|
|
}
|
|
|
|
void TrackPanel::UpdateSelectionDisplay()
|
|
{
|
|
// Full refresh since the label area may need to indicate
|
|
// newly selected tracks.
|
|
Refresh(false);
|
|
|
|
// Make sure the ruler follows suit.
|
|
mRuler->DrawSelection();
|
|
|
|
// As well as the SelectionBar.
|
|
DisplaySelection();
|
|
}
|
|
|
|
void TrackPanel::UpdateAccessibility()
|
|
{
|
|
if (mAx)
|
|
mAx->Updated();
|
|
}
|
|
|
|
// Counts tracks, counting stereo tracks as one track.
|
|
size_t TrackPanel::GetTrackCount() const
|
|
{
|
|
return GetTracks()->Leaders().size();
|
|
}
|
|
|
|
// Counts selected tracks, counting stereo tracks as one track.
|
|
size_t TrackPanel::GetSelectedTrackCount() const
|
|
{
|
|
return GetTracks()->SelectedLeaders().size();
|
|
}
|
|
|
|
void TrackPanel::MessageForScreenReader(const wxString& message)
|
|
{
|
|
if (mAx)
|
|
mAx->MessageForScreenReader(message);
|
|
}
|
|
|
|
void TrackPanel::UpdateViewIfNoTracks()
|
|
{
|
|
if (mTracks->empty())
|
|
{
|
|
// BG: There are no more tracks on screen
|
|
//BG: Set zoom to normal
|
|
mViewInfo->SetZoom(ZoomInfo::GetDefaultZoom());
|
|
|
|
//STM: Set selection to 0,0
|
|
//PRL: and default the rest of the selection information
|
|
mViewInfo->selectedRegion = SelectedRegion();
|
|
|
|
// PRL: Following causes the time ruler to align 0 with left edge.
|
|
// Bug 972
|
|
mViewInfo->h = 0;
|
|
|
|
mListener->TP_RedrawScrollbars();
|
|
mListener->TP_HandleResize();
|
|
mListener->TP_DisplayStatusMessage(wxT("")); //STM: Clear message if all tracks are removed
|
|
}
|
|
}
|
|
|
|
void TrackPanel::OnPlayback(wxEvent &e)
|
|
{
|
|
e.Skip();
|
|
// Starting or stopping of play or record affects some cursors.
|
|
// Start or stop is in progress now, not completed; so delay the cursor
|
|
// change until next idle time.
|
|
CallAfter( [this] { HandleCursorForPresentMouseState(); } );
|
|
}
|
|
|
|
// The tracks positions within the list have changed, so update the vertical
|
|
// ruler size for the track that triggered the event.
|
|
void TrackPanel::OnTrackListResizing(TrackListEvent & e)
|
|
{
|
|
auto t = e.mpTrack.lock();
|
|
// A deleted track can trigger the event. In which case do nothing here.
|
|
// A deleted track can have a valid pointer but no owner, bug 2060
|
|
if( t && t->HasOwner() )
|
|
UpdateVRuler(t.get());
|
|
e.Skip();
|
|
}
|
|
|
|
// Tracks have been removed from the list.
|
|
void TrackPanel::OnTrackListDeletion(wxEvent & e)
|
|
{
|
|
// copy shared_ptr for safety, as in HandleClick
|
|
auto handle = Target();
|
|
if (handle) {
|
|
handle->OnProjectChange(GetProject());
|
|
}
|
|
|
|
// If the focused track disappeared but there are still other tracks,
|
|
// this reassigns focus.
|
|
GetFocusedTrack();
|
|
|
|
UpdateVRulerSize();
|
|
|
|
e.Skip();
|
|
}
|
|
|
|
struct TrackInfo::TCPLine {
|
|
using DrawFunction = void (*)(
|
|
TrackPanelDrawingContext &context,
|
|
const wxRect &rect,
|
|
const Track *maybeNULL
|
|
);
|
|
|
|
unsigned items; // a bitwise OR of values of the enum above
|
|
int height;
|
|
int extraSpace;
|
|
DrawFunction drawFunction;
|
|
};
|
|
|
|
namespace {
|
|
|
|
#define RANGE(array) (array), (array) + sizeof(array)/sizeof(*(array))
|
|
using TCPLines = std::vector< TrackInfo::TCPLine >;
|
|
|
|
enum : unsigned {
|
|
// The sequence is not significant, just keep bits distinct
|
|
kItemBarButtons = 1 << 0,
|
|
kItemStatusInfo1 = 1 << 1,
|
|
kItemMute = 1 << 2,
|
|
kItemSolo = 1 << 3,
|
|
kItemGain = 1 << 4,
|
|
kItemPan = 1 << 5,
|
|
kItemVelocity = 1 << 6,
|
|
kItemMidiControlsRect = 1 << 7,
|
|
kItemMinimize = 1 << 8,
|
|
kItemSyncLock = 1 << 9,
|
|
kItemStatusInfo2 = 1 << 10,
|
|
|
|
kHighestBottomItem = kItemMinimize,
|
|
};
|
|
|
|
|
|
#ifdef EXPERIMENTAL_DA
|
|
|
|
#define TITLE_ITEMS \
|
|
{ kItemBarButtons, kTrackInfoBtnSize, 4, \
|
|
&TrackInfo::CloseTitleDrawFunction },
|
|
// DA: Has Mute and Solo on separate lines.
|
|
#define MUTE_SOLO_ITEMS(extra) \
|
|
{ kItemMute, kTrackInfoBtnSize + 1, 1, \
|
|
&TrackInfo::WideMuteDrawFunction }, \
|
|
{ kItemSolo, kTrackInfoBtnSize + 1, extra, \
|
|
&TrackInfo::WideSoloDrawFunction },
|
|
// DA: Does not have status information for a track.
|
|
#define STATUS_ITEMS
|
|
|
|
#else
|
|
|
|
#define TITLE_ITEMS \
|
|
{ kItemBarButtons, kTrackInfoBtnSize, 0, \
|
|
&TrackInfo::CloseTitleDrawFunction },
|
|
#define MUTE_SOLO_ITEMS(extra) \
|
|
{ kItemMute | kItemSolo, kTrackInfoBtnSize + 1, extra, \
|
|
&TrackInfo::MuteAndSoloDrawFunction },
|
|
#define STATUS_ITEMS \
|
|
{ kItemStatusInfo1, 12, 0, \
|
|
&TrackInfo::Status1DrawFunction }, \
|
|
{ kItemStatusInfo2, 12, 0, \
|
|
&TrackInfo::Status2DrawFunction },
|
|
|
|
#endif
|
|
|
|
#define COMMON_ITEMS \
|
|
TITLE_ITEMS
|
|
|
|
const TrackInfo::TCPLine defaultCommonTrackTCPLines[] = {
|
|
COMMON_ITEMS
|
|
};
|
|
TCPLines commonTrackTCPLines{ RANGE(defaultCommonTrackTCPLines) };
|
|
|
|
const TrackInfo::TCPLine defaultWaveTrackTCPLines[] = {
|
|
COMMON_ITEMS
|
|
MUTE_SOLO_ITEMS(2)
|
|
{ kItemGain, kTrackInfoSliderHeight, kTrackInfoSliderExtra,
|
|
&TrackInfo::GainSliderDrawFunction },
|
|
{ kItemPan, kTrackInfoSliderHeight, kTrackInfoSliderExtra,
|
|
&TrackInfo::PanSliderDrawFunction },
|
|
STATUS_ITEMS
|
|
};
|
|
TCPLines waveTrackTCPLines{ RANGE(defaultWaveTrackTCPLines) };
|
|
|
|
const TrackInfo::TCPLine defaultNoteTrackTCPLines[] = {
|
|
COMMON_ITEMS
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
MUTE_SOLO_ITEMS(0)
|
|
{ kItemMidiControlsRect, kMidiCellHeight * 4, 0,
|
|
&TrackInfo::MidiControlsDrawFunction },
|
|
{ kItemVelocity, kTrackInfoSliderHeight, kTrackInfoSliderExtra,
|
|
&TrackInfo::VelocitySliderDrawFunction },
|
|
#endif
|
|
};
|
|
TCPLines noteTrackTCPLines{ RANGE(defaultNoteTrackTCPLines) };
|
|
|
|
int totalTCPLines( const TCPLines &lines, bool omitLastExtra )
|
|
{
|
|
int total = 0;
|
|
int lastExtra = 0;
|
|
for ( const auto line : lines ) {
|
|
lastExtra = line.extraSpace;
|
|
total += line.height + lastExtra;
|
|
}
|
|
if (omitLastExtra)
|
|
total -= lastExtra;
|
|
return total;
|
|
}
|
|
|
|
const TCPLines &getTCPLines( const Track &track )
|
|
{
|
|
auto lines = track.TypeSwitch< TCPLines * >(
|
|
#ifdef USE_MIDI
|
|
[](const NoteTrack*){
|
|
return ¬eTrackTCPLines;
|
|
},
|
|
#endif
|
|
[](const WaveTrack*){
|
|
return &waveTrackTCPLines;
|
|
},
|
|
[](const Track*){
|
|
return &commonTrackTCPLines;
|
|
}
|
|
);
|
|
|
|
if (lines)
|
|
return *lines;
|
|
|
|
return commonTrackTCPLines;
|
|
}
|
|
|
|
// return y value and height
|
|
std::pair< int, int > CalcItemY( const TCPLines &lines, unsigned iItem )
|
|
{
|
|
int y = 0;
|
|
auto pLines = lines.begin();
|
|
while ( pLines != lines.end() &&
|
|
0 == (pLines->items & iItem) ) {
|
|
y += pLines->height + pLines->extraSpace;
|
|
++pLines;
|
|
}
|
|
int height = 0;
|
|
if ( pLines != lines.end() )
|
|
height = pLines->height;
|
|
return { y, height };
|
|
}
|
|
|
|
// Items for the bottom of the panel, listed bottom-upwards
|
|
// As also with the top items, the extra space is below the item
|
|
const TrackInfo::TCPLine defaultCommonTrackTCPBottomLines[] = {
|
|
// The '0' avoids impinging on bottom line of TCP
|
|
// Use -1 if you do want to do so.
|
|
{ kItemSyncLock | kItemMinimize, kTrackInfoBtnSize, 0,
|
|
&TrackInfo::MinimizeSyncLockDrawFunction },
|
|
};
|
|
TCPLines commonTrackTCPBottomLines{ RANGE(defaultCommonTrackTCPBottomLines) };
|
|
|
|
// return y value and height
|
|
std::pair< int, int > CalcBottomItemY
|
|
( const TCPLines &lines, unsigned iItem, int height )
|
|
{
|
|
int y = height;
|
|
auto pLines = lines.begin();
|
|
while ( pLines != lines.end() &&
|
|
0 == (pLines->items & iItem) ) {
|
|
y -= pLines->height + pLines->extraSpace;
|
|
++pLines;
|
|
}
|
|
if (pLines != lines.end())
|
|
y -= (pLines->height + pLines->extraSpace );
|
|
return { y, pLines->height };
|
|
}
|
|
|
|
}
|
|
|
|
unsigned TrackInfo::MinimumTrackHeight()
|
|
{
|
|
unsigned height = 0;
|
|
if (!commonTrackTCPLines.empty())
|
|
height += commonTrackTCPLines.front().height;
|
|
if (!commonTrackTCPBottomLines.empty())
|
|
height += commonTrackTCPBottomLines.front().height;
|
|
// + 1 prevents the top item from disappearing for want of enough space,
|
|
// according to the rules in HideTopItem.
|
|
return height + kTopMargin + kBottomMargin + 1;
|
|
}
|
|
|
|
bool TrackInfo::HideTopItem( const wxRect &rect, const wxRect &subRect,
|
|
int allowance ) {
|
|
auto limit = CalcBottomItemY
|
|
( commonTrackTCPBottomLines, kHighestBottomItem, rect.height).first;
|
|
// Return true if the rectangle is even touching the limit
|
|
// without an overlap. That was the behavior as of 2.1.3.
|
|
return subRect.y + subRect.height - allowance >= rect.y + limit;
|
|
}
|
|
|
|
void TrackPanel::OnKeyDown(wxKeyEvent & event)
|
|
{
|
|
switch (event.GetKeyCode())
|
|
{
|
|
// Allow PageUp and PageDown keys to
|
|
//scroll the Track Panel left and right
|
|
case WXK_PAGEUP:
|
|
HandlePageUpKey();
|
|
return;
|
|
|
|
case WXK_PAGEDOWN:
|
|
HandlePageDownKey();
|
|
return;
|
|
|
|
default:
|
|
// fall through to base class handler
|
|
event.Skip();
|
|
}
|
|
}
|
|
|
|
void TrackPanel::OnMouseEvent(wxMouseEvent & event)
|
|
{
|
|
if (event.LeftDown()) {
|
|
// wxTimers seem to be a little unreliable, so this
|
|
// "primes" it to make sure it keeps going for a while...
|
|
|
|
// When this timer fires, we call TrackPanel::OnTimer and
|
|
// possibly update the screen for offscreen scrolling.
|
|
mTimer.Stop();
|
|
mTimer.Start(kTimerInterval, FALSE);
|
|
}
|
|
|
|
|
|
if (event.ButtonUp()) {
|
|
//EnsureVisible should be called after processing the up-click.
|
|
this->CallAfter( [this, event]{
|
|
const auto foundCell = FindCell(event.m_x, event.m_y);
|
|
const auto t = FindTrack( foundCell.pCell.get() );
|
|
if ( t )
|
|
EnsureVisible(t.get());
|
|
} );
|
|
}
|
|
|
|
// Must also fall through to base class handler
|
|
event.Skip();
|
|
}
|
|
|
|
double TrackPanel::GetMostRecentXPos()
|
|
{
|
|
return mViewInfo->PositionToTime(MostRecentXCoord(), GetLabelWidth());
|
|
}
|
|
|
|
void TrackPanel::RefreshTrack(Track *trk, bool refreshbacking)
|
|
{
|
|
if (!trk)
|
|
return;
|
|
|
|
trk = *GetTracks()->FindLeader(trk);
|
|
auto height =
|
|
TrackList::Channels(trk).sum( &Track::GetHeight )
|
|
- kTopInset - kShadowThickness;
|
|
|
|
// subtract insets and shadows from the rectangle, but not border
|
|
// This matters because some separators do paint over the border
|
|
wxRect rect(kLeftInset,
|
|
-mViewInfo->vpos + trk->GetY() + kTopInset,
|
|
GetRect().GetWidth() - kLeftInset - kRightInset - kShadowThickness,
|
|
height);
|
|
|
|
if( refreshbacking )
|
|
{
|
|
mRefreshBacking = true;
|
|
}
|
|
|
|
Refresh( false, &rect );
|
|
}
|
|
|
|
|
|
/// This method overrides Refresh() of wxWindow so that the
|
|
/// boolean play indictaor can be set to false, so that an old play indicator that is
|
|
/// no longer there won't get XORed (to erase it), thus redrawing it on the
|
|
/// TrackPanel
|
|
void TrackPanel::Refresh(bool eraseBackground /* = TRUE */,
|
|
const wxRect *rect /* = NULL */)
|
|
{
|
|
// Tell OnPaint() to refresh the backing bitmap.
|
|
//
|
|
// Originally I had the check within the OnPaint() routine and it
|
|
// was working fine. That was until I found that, even though a full
|
|
// refresh was requested, Windows only set the onscreen portion of a
|
|
// window as damaged.
|
|
//
|
|
// So, if any part of the trackpanel was off the screen, full refreshes
|
|
// didn't work and the display got corrupted.
|
|
if( !rect || ( *rect == GetRect() ) )
|
|
{
|
|
mRefreshBacking = true;
|
|
}
|
|
wxWindow::Refresh(eraseBackground, rect);
|
|
DisplaySelection();
|
|
}
|
|
|
|
#include "TrackPanelDrawingContext.h"
|
|
|
|
/// Draw the actual track areas. We only draw the borders
|
|
/// and the little buttons and menues and whatnot here, the
|
|
/// actual contents of each track are drawn by the TrackArtist.
|
|
void TrackPanel::DrawTracks(wxDC * dc)
|
|
{
|
|
wxRegion region = GetUpdateRegion();
|
|
|
|
const wxRect clip = GetRect();
|
|
|
|
mTrackArtist->pSelectedRegion = &mViewInfo->selectedRegion;
|
|
mTrackArtist->pZoomInfo = mViewInfo;
|
|
TrackPanelDrawingContext context {
|
|
*dc, Target(), mLastMouseState, mTrackArtist.get()
|
|
};
|
|
|
|
// Draw margins on two or three sides.
|
|
ClearLeftAndRightMargins(context, clip);
|
|
if ( GetTracks()->Any() )
|
|
// This margin may may scrolled up out of view
|
|
ClearTopMargin( context, clip );
|
|
|
|
// Don't draw a bottom margin here.
|
|
|
|
ToolsToolBar *pTtb = GetProject()->GetToolsToolBar();
|
|
bool bMultiToolDown = pTtb->IsDown(multiTool);
|
|
bool envelopeFlag = pTtb->IsDown(envelopeTool) || bMultiToolDown;
|
|
bool bigPointsFlag = pTtb->IsDown(drawTool) || bMultiToolDown;
|
|
bool sliderFlag = bMultiToolDown;
|
|
|
|
const bool hasSolo = GetTracks()->Any< PlayableTrack >()
|
|
.any_of( []( const PlayableTrack *pt ) {
|
|
pt = static_cast< const PlayableTrack * >(
|
|
pt->SubstitutePendingChangedTrack().get() );
|
|
return (pt && pt->GetSolo());
|
|
} );
|
|
|
|
mTrackArtist->leftOffset = GetLeftOffset();
|
|
mTrackArtist->drawEnvelope = envelopeFlag;
|
|
mTrackArtist->bigPoints = bigPointsFlag;
|
|
mTrackArtist->drawSliders = sliderFlag;
|
|
mTrackArtist->hasSolo = hasSolo;
|
|
TrackArt::DrawTracks( context, GetTracks(), region, clip );
|
|
|
|
// Draw the rest, including the click-to-deselect blank area below all
|
|
// tracks
|
|
DrawEverythingElse(context, region, clip);
|
|
TrackArt::DrawTrackNames( context, GetTracks(), region, clip );
|
|
}
|
|
|
|
/// Draws 'Everything else'. In particular it draws:
|
|
/// - Drop shadow for tracks and vertical rulers.
|
|
/// - Zooming Indicators.
|
|
/// - Fills in space below the tracks.
|
|
void TrackPanel::DrawEverythingElse(TrackPanelDrawingContext &context,
|
|
const wxRegion ®ion,
|
|
const wxRect & clip)
|
|
{
|
|
// We draw everything else
|
|
auto dc = &context.dc;
|
|
|
|
// Fix the horizontal extent, will vary the vertical:
|
|
wxRect trackRect{
|
|
kLeftMargin, 0, clip.width - (kLeftMargin + kRightMargin), 0
|
|
};
|
|
wxRect focusRect{};
|
|
|
|
// The loop below now groups each track with the margin BELOW it, to
|
|
// correspond better with the subdivision of panel area used in hit testing.
|
|
|
|
for ( auto leaderTrack : GetTracks()->Leaders< const Track >()
|
|
// Predicate is true iff any channel in the group is wholly or partly
|
|
// visible:
|
|
+ IsVisibleTrack{ GetProject() } ) {
|
|
const auto channels = TrackList::Channels(leaderTrack);
|
|
bool focused = false;
|
|
wxRect teamRect = trackRect;
|
|
teamRect.height = 0;
|
|
bool first = true;
|
|
for (auto channel : channels) {
|
|
focused = focused || mAx->IsFocused(channel);
|
|
channel = channel->SubstitutePendingChangedTrack().get();
|
|
if (first)
|
|
first = false,
|
|
teamRect.y = channel->GetY() - mViewInfo->vpos + kTopMargin;
|
|
teamRect.height += channel->GetHeight();
|
|
}
|
|
|
|
if (focused) {
|
|
focusRect = teamRect;
|
|
focusRect.height -= kSeparatorThickness;
|
|
}
|
|
DrawOutside(context, leaderTrack, teamRect);
|
|
|
|
// Believe it or not, we can speed up redrawing if we don't
|
|
// redraw the vertical ruler when only the waveform data has
|
|
// changed. An example is during recording.
|
|
|
|
#if DEBUG_DRAW_TIMING
|
|
// wxRect rbox = region.GetBox();
|
|
// wxPrintf(wxT("Update Region: %d %d %d %d\n"),
|
|
// rbox.x, rbox.y, rbox.width, rbox.height);
|
|
#endif
|
|
|
|
for (auto channel : channels) {
|
|
bool bSelected = channel->GetSelected();
|
|
channel = channel->SubstitutePendingChangedTrack().get();
|
|
trackRect.y = channel->GetY() - mViewInfo->vpos + kTopMargin;
|
|
trackRect.height = channel->GetHeight();
|
|
if (region.Contains(
|
|
0, trackRect.y, GetLeftOffset(), trackRect.height)) {
|
|
wxRect rect{
|
|
GetVRulerOffset(),
|
|
trackRect.y,
|
|
GetVRulerWidth() + 1,
|
|
trackRect.height - kSeparatorThickness
|
|
};
|
|
TrackArt::DrawVRuler(context, channel, rect, bSelected);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto target = Target();
|
|
if (target)
|
|
target->DrawExtras(UIHandle::Cells, dc, region, clip);
|
|
|
|
// Paint over the part below the tracks
|
|
trackRect.y += trackRect.height;
|
|
if (trackRect.y < clip.GetBottom()) {
|
|
AColor::TrackPanelBackground(dc, false);
|
|
dc->DrawRectangle(trackRect.x,
|
|
trackRect.y,
|
|
trackRect.width,
|
|
clip.height - trackRect.y);
|
|
}
|
|
|
|
// Sometimes highlight is not drawn on backing bitmap. I thought
|
|
// it was because FindFocus did not return "this" on Mac, but
|
|
// when I removed that test, yielding this condition:
|
|
// if (GetFocusedTrack() != NULL) {
|
|
// the highlight was reportedly drawn even when something else
|
|
// was the focus and no highlight should be drawn. -RBD
|
|
if (GetFocusedTrack() != NULL &&
|
|
wxWindow::FindFocus() == this ) {
|
|
HighlightFocusedTrack(dc, focusRect);
|
|
}
|
|
|
|
if (target)
|
|
target->DrawExtras(UIHandle::Panel, dc, region, clip);
|
|
}
|
|
|
|
// Make this #include go away!
|
|
#include "tracks/ui/TrackControls.h"
|
|
|
|
void TrackInfo::DrawItems
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track &track )
|
|
{
|
|
const auto topLines = getTCPLines( track );
|
|
const auto bottomLines = commonTrackTCPBottomLines;
|
|
DrawItems
|
|
( context, rect, &track, topLines, bottomLines );
|
|
}
|
|
|
|
void TrackInfo::DrawItems
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack,
|
|
const std::vector<TCPLine> &topLines, const std::vector<TCPLine> &bottomLines )
|
|
{
|
|
auto dc = &context.dc;
|
|
TrackInfo::SetTrackInfoFont(dc);
|
|
dc->SetTextForeground(theTheme.Colour(clrTrackPanelText));
|
|
|
|
{
|
|
int yy = 0;
|
|
for ( const auto &line : topLines ) {
|
|
wxRect itemRect{
|
|
rect.x, rect.y + yy,
|
|
rect.width, line.height
|
|
};
|
|
if ( !TrackInfo::HideTopItem( rect, itemRect ) &&
|
|
line.drawFunction )
|
|
line.drawFunction( context, itemRect, pTrack );
|
|
yy += line.height + line.extraSpace;
|
|
}
|
|
}
|
|
{
|
|
int yy = rect.height;
|
|
for ( const auto &line : bottomLines ) {
|
|
yy -= line.height + line.extraSpace;
|
|
if ( line.drawFunction ) {
|
|
wxRect itemRect{
|
|
rect.x, rect.y + yy,
|
|
rect.width, line.height
|
|
};
|
|
line.drawFunction( context, itemRect, pTrack );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#include "tracks/ui/TrackButtonHandles.h"
|
|
void TrackInfo::CloseTitleDrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
auto dc = &context.dc;
|
|
bool selected = pTrack ? pTrack->GetSelected() : true;
|
|
{
|
|
wxRect bev = rect;
|
|
GetCloseBoxHorizontalBounds( rect, bev );
|
|
auto target = dynamic_cast<CloseButtonHandle*>( context.target.get() );
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
bool captured = hit && target->IsClicked();
|
|
bool down = captured && bev.Contains( context.lastState.GetPosition());
|
|
AColor::Bevel2(*dc, !down, bev, selected, hit );
|
|
|
|
#ifdef EXPERIMENTAL_THEMING
|
|
wxPen pen( theTheme.Colour( clrTrackPanelText ));
|
|
dc->SetPen( pen );
|
|
#else
|
|
dc->SetPen(*wxBLACK_PEN);
|
|
#endif
|
|
bev.Inflate( -1, -1 );
|
|
// Draw the "X"
|
|
const int s = 6;
|
|
|
|
int ls = bev.x + ((bev.width - s) / 2);
|
|
int ts = bev.y + ((bev.height - s) / 2);
|
|
int rs = ls + s;
|
|
int bs = ts + s;
|
|
|
|
AColor::Line(*dc, ls, ts, rs, bs);
|
|
AColor::Line(*dc, ls + 1, ts, rs + 1, bs);
|
|
AColor::Line(*dc, rs, ts, ls, bs);
|
|
AColor::Line(*dc, rs + 1, ts, ls + 1, bs);
|
|
|
|
// bev.Inflate(-1, -1);
|
|
}
|
|
|
|
{
|
|
wxRect bev = rect;
|
|
GetTitleBarHorizontalBounds( rect, bev );
|
|
auto target = dynamic_cast<MenuButtonHandle*>( context.target.get() );
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
bool captured = hit && target->IsClicked();
|
|
bool down = captured && bev.Contains( context.lastState.GetPosition());
|
|
wxString titleStr =
|
|
pTrack ? pTrack->GetName() : _("Name");
|
|
|
|
//bev.Inflate(-1, -1);
|
|
AColor::Bevel2(*dc, !down, bev, selected, hit);
|
|
|
|
// Draw title text
|
|
SetTrackInfoFont(dc);
|
|
|
|
// Bug 1660 The 'k' of 'Audio Track' was being truncated.
|
|
// Constant of 32 found by counting pixels on a windows machine.
|
|
// I believe it's the size of the X close button + the size of the
|
|
// drop down arrow.
|
|
int allowableWidth = rect.width - 32;
|
|
|
|
wxCoord textWidth, textHeight;
|
|
dc->GetTextExtent(titleStr, &textWidth, &textHeight);
|
|
while (textWidth > allowableWidth) {
|
|
titleStr = titleStr.Left(titleStr.length() - 1);
|
|
dc->GetTextExtent(titleStr, &textWidth, &textHeight);
|
|
}
|
|
|
|
// Pop-up triangle
|
|
#ifdef EXPERIMENTAL_THEMING
|
|
wxColour c = theTheme.Colour( clrTrackPanelText );
|
|
#else
|
|
wxColour c = *wxBLACK;
|
|
#endif
|
|
|
|
// wxGTK leaves little scraps (antialiasing?) of the
|
|
// characters if they are repeatedly drawn. This
|
|
// happens when holding down mouse button and moving
|
|
// in and out of the title bar. So clear it first.
|
|
// AColor::MediumTrackInfo(dc, t->GetSelected());
|
|
// dc->DrawRectangle(bev);
|
|
|
|
dc->SetTextForeground( c );
|
|
dc->SetTextBackground( wxTRANSPARENT );
|
|
dc->DrawText(titleStr, bev.x + 2, bev.y + (bev.height - textHeight) / 2);
|
|
|
|
|
|
|
|
dc->SetPen(c);
|
|
dc->SetBrush(c);
|
|
|
|
int s = 10; // Width of dropdown arrow...height is half of width
|
|
AColor::Arrow(*dc,
|
|
bev.GetRight() - s - 3, // 3 to offset from right border
|
|
bev.y + ((bev.height - (s / 2)) / 2),
|
|
s);
|
|
|
|
}
|
|
}
|
|
|
|
void TrackInfo::MinimizeSyncLockDrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
auto dc = &context.dc;
|
|
bool selected = pTrack ? pTrack->GetSelected() : true;
|
|
bool syncLockSelected = pTrack ? pTrack->IsSyncLockSelected() : true;
|
|
bool minimized = pTrack ? pTrack->GetMinimized() : false;
|
|
{
|
|
wxRect bev = rect;
|
|
GetMinimizeHorizontalBounds(rect, bev);
|
|
auto target = dynamic_cast<MinimizeButtonHandle*>( context.target.get() );
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
bool captured = hit && target->IsClicked();
|
|
bool down = captured && bev.Contains( context.lastState.GetPosition());
|
|
|
|
// Clear background to get rid of previous arrow
|
|
//AColor::MediumTrackInfo(dc, t->GetSelected());
|
|
//dc->DrawRectangle(bev);
|
|
|
|
AColor::Bevel2(*dc, !down, bev, selected, hit);
|
|
|
|
#ifdef EXPERIMENTAL_THEMING
|
|
wxColour c = theTheme.Colour(clrTrackPanelText);
|
|
dc->SetBrush(c);
|
|
dc->SetPen(c);
|
|
#else
|
|
AColor::Dark(dc, selected);
|
|
#endif
|
|
|
|
AColor::Arrow(*dc,
|
|
bev.x - 5 + bev.width / 2,
|
|
bev.y - 2 + bev.height / 2,
|
|
10,
|
|
minimized);
|
|
}
|
|
|
|
{
|
|
wxRect bev = rect;
|
|
GetSelectButtonHorizontalBounds(rect, bev);
|
|
auto target = dynamic_cast<SelectButtonHandle*>( context.target.get() );
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
bool captured = hit && target->IsClicked();
|
|
bool down = captured && bev.Contains( context.lastState.GetPosition());
|
|
|
|
AColor::Bevel2(*dc, !down, bev, selected, hit);
|
|
|
|
#ifdef EXPERIMENTAL_THEMING
|
|
wxColour c = theTheme.Colour(clrTrackPanelText);
|
|
dc->SetBrush(c);
|
|
dc->SetPen(c);
|
|
#else
|
|
AColor::Dark(dc, selected);
|
|
#endif
|
|
|
|
wxString str = _("Select");
|
|
wxCoord textWidth;
|
|
wxCoord textHeight;
|
|
SetTrackInfoFont(dc);
|
|
dc->GetTextExtent(str, &textWidth, &textHeight);
|
|
|
|
dc->SetTextForeground( c );
|
|
dc->SetTextBackground( wxTRANSPARENT );
|
|
dc->DrawText(str, bev.x + 2 + (bev.width-textWidth)/2, bev.y + (bev.height - textHeight) / 2);
|
|
}
|
|
|
|
|
|
// Draw the sync-lock indicator if this track is in a sync-lock selected group.
|
|
if (syncLockSelected)
|
|
{
|
|
wxRect syncLockIconRect = rect;
|
|
|
|
GetSyncLockHorizontalBounds( rect, syncLockIconRect );
|
|
wxBitmap syncLockBitmap(theTheme.Image(bmpSyncLockIcon));
|
|
// Icon is 12x12 and syncLockIconRect is 16x16.
|
|
dc->DrawBitmap(syncLockBitmap,
|
|
syncLockIconRect.x + 3,
|
|
syncLockIconRect.y + 2,
|
|
true);
|
|
}
|
|
}
|
|
|
|
#include "tracks/playabletrack/notetrack/ui/NoteTrackButtonHandle.h"
|
|
void TrackInfo::MidiControlsDrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
auto target = dynamic_cast<NoteTrackButtonHandle*>( context.target.get() );
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
auto channel = hit ? target->GetChannel() : -1;
|
|
auto &dc = context.dc;
|
|
wxRect midiRect = rect;
|
|
GetMidiControlsHorizontalBounds(rect, midiRect);
|
|
NoteTrack::DrawLabelControls
|
|
( static_cast<const NoteTrack *>(pTrack), dc, midiRect, channel );
|
|
#endif // EXPERIMENTAL_MIDI_OUT
|
|
}
|
|
|
|
template<typename TrackClass>
|
|
void TrackInfo::SliderDrawFunction
|
|
( LWSlider *(*Selector)
|
|
(const wxRect &sliderRect, const TrackClass *t, bool captured, wxWindow*),
|
|
wxDC *dc, const wxRect &rect, const Track *pTrack,
|
|
bool captured, bool highlight )
|
|
{
|
|
wxRect sliderRect = rect;
|
|
TrackInfo::GetSliderHorizontalBounds( rect.GetTopLeft(), sliderRect );
|
|
auto wt = static_cast<const TrackClass*>( pTrack );
|
|
Selector( sliderRect, wt, captured, nullptr )->OnPaint(*dc, highlight);
|
|
}
|
|
|
|
#include "tracks/playabletrack/wavetrack/ui/WaveTrackSliderHandles.h"
|
|
void TrackInfo::PanSliderDrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
auto target = dynamic_cast<PanSliderHandle*>( context.target.get() );
|
|
auto dc = &context.dc;
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
bool captured = hit && target->IsClicked();
|
|
SliderDrawFunction<WaveTrack>
|
|
( &TrackInfo::PanSlider, dc, rect, pTrack, captured, hit);
|
|
}
|
|
|
|
void TrackInfo::GainSliderDrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
auto target = dynamic_cast<GainSliderHandle*>( context.target.get() );
|
|
auto dc = &context.dc;
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
if( hit )
|
|
hit=hit;
|
|
bool captured = hit && target->IsClicked();
|
|
SliderDrawFunction<WaveTrack>
|
|
( &TrackInfo::GainSlider, dc, rect, pTrack, captured, hit);
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
#include "tracks/playabletrack/notetrack/ui/NoteTrackSliderHandles.h"
|
|
void TrackInfo::VelocitySliderDrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
auto dc = &context.dc;
|
|
auto target = dynamic_cast<VelocitySliderHandle*>( context.target.get() );
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
bool captured = hit && target->IsClicked();
|
|
SliderDrawFunction<NoteTrack>
|
|
( &TrackInfo::VelocitySlider, dc, rect, pTrack, captured, hit);
|
|
}
|
|
#endif
|
|
|
|
void TrackInfo::MuteOrSoloDrawFunction
|
|
( wxDC *dc, const wxRect &bev, const Track *pTrack, bool down,
|
|
bool WXUNUSED(captured),
|
|
bool solo, bool hit )
|
|
{
|
|
//bev.Inflate(-1, -1);
|
|
bool selected = pTrack ? pTrack->GetSelected() : true;
|
|
auto pt = dynamic_cast<const PlayableTrack *>(pTrack);
|
|
bool value = pt ? (solo ? pt->GetSolo() : pt->GetMute()) : false;
|
|
|
|
#if 0
|
|
AColor::MediumTrackInfo( dc, t->GetSelected());
|
|
if( solo )
|
|
{
|
|
if( pt && pt->GetSolo() )
|
|
{
|
|
AColor::Solo(dc, pt->GetSolo(), t->GetSelected());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( pt && pt->GetMute() )
|
|
{
|
|
AColor::Mute(dc, pt->GetMute(), t->GetSelected(), pt->GetSolo());
|
|
}
|
|
}
|
|
//(solo) ? AColor::Solo(dc, t->GetSolo(), t->GetSelected()) :
|
|
// AColor::Mute(dc, t->GetMute(), t->GetSelected(), t->GetSolo());
|
|
dc->SetPen( *wxTRANSPARENT_PEN );//No border!
|
|
dc->DrawRectangle(bev);
|
|
#endif
|
|
|
|
wxCoord textWidth, textHeight;
|
|
wxString str = (solo) ?
|
|
/* i18n-hint: This is on a button that will silence all the other tracks.*/
|
|
_("Solo") :
|
|
/* i18n-hint: This is on a button that will silence this track.*/
|
|
_("Mute");
|
|
|
|
AColor::Bevel2(
|
|
*dc,
|
|
value == down,
|
|
bev,
|
|
selected, hit
|
|
);
|
|
|
|
SetTrackInfoFont(dc);
|
|
dc->GetTextExtent(str, &textWidth, &textHeight);
|
|
dc->DrawText(str, bev.x + (bev.width - textWidth) / 2, bev.y + (bev.height - textHeight) / 2);
|
|
}
|
|
|
|
#include "tracks/playabletrack/ui/PlayableTrackButtonHandles.h"
|
|
void TrackInfo::WideMuteDrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
auto dc = &context.dc;
|
|
wxRect bev = rect;
|
|
GetWideMuteSoloHorizontalBounds( rect, bev );
|
|
auto target = dynamic_cast<MuteButtonHandle*>( context.target.get() );
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
bool captured = hit && target->IsClicked();
|
|
bool down = captured && bev.Contains( context.lastState.GetPosition());
|
|
MuteOrSoloDrawFunction( dc, bev, pTrack, down, captured, false, hit );
|
|
}
|
|
|
|
void TrackInfo::WideSoloDrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
auto dc = &context.dc;
|
|
wxRect bev = rect;
|
|
GetWideMuteSoloHorizontalBounds( rect, bev );
|
|
auto target = dynamic_cast<SoloButtonHandle*>( context.target.get() );
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
bool captured = hit && target->IsClicked();
|
|
bool down = captured && bev.Contains( context.lastState.GetPosition());
|
|
MuteOrSoloDrawFunction( dc, bev, pTrack, down, captured, true, hit );
|
|
}
|
|
|
|
void TrackInfo::MuteAndSoloDrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
auto dc = &context.dc;
|
|
bool bHasSoloButton = TrackPanel::HasSoloButton();
|
|
|
|
wxRect bev = rect;
|
|
if ( bHasSoloButton )
|
|
GetNarrowMuteHorizontalBounds( rect, bev );
|
|
else
|
|
GetWideMuteSoloHorizontalBounds( rect, bev );
|
|
{
|
|
auto target = dynamic_cast<MuteButtonHandle*>( context.target.get() );
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
bool captured = hit && target->IsClicked();
|
|
bool down = captured && bev.Contains( context.lastState.GetPosition());
|
|
MuteOrSoloDrawFunction( dc, bev, pTrack, down, captured, false, hit );
|
|
}
|
|
|
|
if( !bHasSoloButton )
|
|
return;
|
|
|
|
GetNarrowSoloHorizontalBounds( rect, bev );
|
|
{
|
|
auto target = dynamic_cast<SoloButtonHandle*>( context.target.get() );
|
|
bool hit = target && target->GetTrack().get() == pTrack;
|
|
bool captured = hit && target->IsClicked();
|
|
bool down = captured && bev.Contains( context.lastState.GetPosition());
|
|
MuteOrSoloDrawFunction( dc, bev, pTrack, down, captured, true, hit );
|
|
}
|
|
}
|
|
|
|
void TrackInfo::StatusDrawFunction
|
|
( const wxString &string, wxDC *dc, const wxRect &rect )
|
|
{
|
|
static const int offset = 3;
|
|
dc->DrawText(string, rect.x + offset, rect.y);
|
|
}
|
|
|
|
void TrackInfo::Status1DrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
auto dc = &context.dc;
|
|
auto wt = static_cast<const WaveTrack*>(pTrack);
|
|
|
|
/// Returns the string to be displayed in the track label
|
|
/// indicating whether the track is mono, left, right, or
|
|
/// stereo and what sample rate it's using.
|
|
auto rate = wt ? wt->GetRate() : 44100.0;
|
|
wxString s;
|
|
if (!pTrack || TrackList::Channels(pTrack).size() > 1)
|
|
// TODO: more-than-two-channels-message
|
|
// more appropriate strings
|
|
s = _("Stereo, %dHz");
|
|
else {
|
|
if (wt->GetChannel() == Track::MonoChannel)
|
|
s = _("Mono, %dHz");
|
|
else if (wt->GetChannel() == Track::LeftChannel)
|
|
s = _("Left, %dHz");
|
|
else if (wt->GetChannel() == Track::RightChannel)
|
|
s = _("Right, %dHz");
|
|
}
|
|
s = wxString::Format( s, (int) (rate + 0.5) );
|
|
|
|
StatusDrawFunction( s, dc, rect );
|
|
}
|
|
|
|
void TrackInfo::Status2DrawFunction
|
|
( TrackPanelDrawingContext &context,
|
|
const wxRect &rect, const Track *pTrack )
|
|
{
|
|
auto dc = &context.dc;
|
|
auto wt = static_cast<const WaveTrack*>(pTrack);
|
|
auto format = wt ? wt->GetSampleFormat() : floatSample;
|
|
auto s = GetSampleFormatStr(format);
|
|
StatusDrawFunction( s, dc, rect );
|
|
}
|
|
|
|
void TrackPanel::DrawOutside
|
|
(TrackPanelDrawingContext &context,
|
|
const Track * t, const wxRect & rec)
|
|
{
|
|
// Given rectangle excludes left and right margins, and encompasses a
|
|
// channel group of tracks, plus the resizer area below
|
|
|
|
auto dc = &context.dc;
|
|
|
|
// Start with whole track rect
|
|
wxRect rect = rec;
|
|
|
|
{
|
|
ClearSeparator(context, rect);
|
|
|
|
// Now exclude the resizer below
|
|
rect.height -= kSeparatorThickness;
|
|
|
|
int labelw = GetLabelWidth();
|
|
int vrul = GetVRulerOffset();
|
|
|
|
TrackInfo::DrawBackground( dc, rect, t->GetSelected(), vrul );
|
|
|
|
// Vaughan, 2010-08-24: No longer doing this.
|
|
// Draw sync-lock tiles in ruler area.
|
|
//if (t->IsSyncLockSelected()) {
|
|
// wxRect tileFill = rect;
|
|
// tileFill.x = GetVRulerOffset();
|
|
// tileFill.width = GetVRulerWidth();
|
|
// TrackArt::DrawSyncLockTiles(dc, tileFill);
|
|
//}
|
|
|
|
DrawBordersAroundTrack( dc, rect );
|
|
{
|
|
auto channels = TrackList::Channels(t);
|
|
// omit last (perhaps, only) channel
|
|
--channels.second;
|
|
for (auto channel : channels) {
|
|
// draw the sash below this channel
|
|
channel = channel->SubstitutePendingChangedTrack().get();
|
|
auto yy =
|
|
channel->GetY() - mViewInfo->vpos + channel->GetHeight()
|
|
- kBottomMargin;
|
|
wxRect sashRect{
|
|
vrul, yy, rect.GetRight() - vrul, kSeparatorThickness
|
|
};
|
|
DrawSash( dc, sashRect, labelw, t->GetSelected() );
|
|
}
|
|
}
|
|
|
|
DrawShadow( dc, rect );
|
|
}
|
|
|
|
// Draw things within the track control panel
|
|
rect.width = kTrackInfoWidth;
|
|
TrackInfo::DrawItems( context, rect, *t );
|
|
|
|
//mTrackInfo.DrawBordersWithin( dc, rect, *t );
|
|
}
|
|
|
|
void TrackPanel::ClearTopMargin
|
|
(TrackPanelDrawingContext &context, const wxRect &clip)
|
|
{
|
|
auto dc = &context.dc;
|
|
|
|
// Area above the first track if there is one
|
|
AColor::TrackPanelBackground(dc, false);
|
|
wxRect side{
|
|
clip.x + kLeftMargin,
|
|
-mViewInfo->vpos,
|
|
clip.width - ( kLeftMargin + kRightMargin ),
|
|
kTopMargin
|
|
};
|
|
|
|
if (side.Intersects(clip))
|
|
dc->DrawRectangle(side);
|
|
}
|
|
|
|
// Paint the inset areas of the whole panel, left and right, in a background
|
|
// color
|
|
void TrackPanel::ClearLeftAndRightMargins
|
|
(TrackPanelDrawingContext &context, const wxRect & clip)
|
|
{
|
|
auto dc = &context.dc;
|
|
|
|
// Fill in area outside of tracks
|
|
AColor::TrackPanelBackground(dc, false);
|
|
wxRect side;
|
|
|
|
// Area between panel border and left track border
|
|
side = clip;
|
|
side.width = kLeftMargin;
|
|
dc->DrawRectangle(side);
|
|
|
|
// Area between panel border and right track border
|
|
side = clip;
|
|
side.x += side.width - kRightMargin;
|
|
side.width = kRightMargin;
|
|
dc->DrawRectangle(side);
|
|
}
|
|
|
|
// Given rectangle should be the whole track rectangle
|
|
// Paint the separator area below in a background color
|
|
void TrackPanel::ClearSeparator
|
|
(TrackPanelDrawingContext &context, const wxRect & rect)
|
|
{
|
|
auto dc = &context.dc;
|
|
|
|
// Fill in area outside of the track
|
|
AColor::TrackPanelBackground(dc, false);
|
|
|
|
// Area below the track, where the resizer will be
|
|
auto height = kSeparatorThickness;
|
|
wxRect side{
|
|
rect.x,
|
|
rect.y + rect.height - height,
|
|
rect.width,
|
|
height
|
|
};
|
|
dc->DrawRectangle(side);
|
|
}
|
|
|
|
void TrackPanel::DrawSash(
|
|
wxDC * dc, const wxRect & rect, int labelw, bool bSelected )
|
|
{
|
|
// Area between channels of a group
|
|
// Paint the channel separator over (what would be) the lower border of this
|
|
// channel, down to and including the upper border of the next channel
|
|
|
|
ADCChanger cleanup{ dc };
|
|
|
|
// Paint the left part of the background
|
|
AColor::MediumTrackInfo(dc, bSelected);
|
|
dc->DrawRectangle( rect.GetX(), rect.GetY(), labelw, rect.GetHeight() );
|
|
|
|
// Stroke the left border
|
|
dc->SetPen(*wxBLACK_PEN);
|
|
{
|
|
const auto left = rect.GetLeft();
|
|
AColor::Line( *dc, left, rect.GetTop(), left, rect.GetBottom() );
|
|
}
|
|
|
|
AColor::TrackPanelBackground(dc, false);
|
|
|
|
wxRect rec{ rect };
|
|
rec.width -= labelw - rec.x;
|
|
rec.x = labelw;
|
|
|
|
dc->DrawRectangle( wxRect( rec ).Inflate( 0, -kBorderThickness ) );
|
|
|
|
// These lines stroke over what is otherwise "border" of each channel
|
|
dc->SetBrush(*wxTRANSPARENT_BRUSH);
|
|
dc->SetPen(*wxBLACK_PEN);
|
|
const auto left = rec.GetLeft();
|
|
const auto right = rec.GetRight();
|
|
const auto top = rec.GetTop();
|
|
const auto bottom = rec.GetBottom();
|
|
AColor::Line( *dc, left, top, right, top );
|
|
AColor::Line( *dc, left, bottom, right, bottom );
|
|
}
|
|
|
|
|
|
void TrackPanel::SetBackgroundCell
|
|
(const std::shared_ptr< TrackPanelCell > &pCell)
|
|
{
|
|
mpBackground = pCell;
|
|
}
|
|
|
|
std::shared_ptr< TrackPanelCell > TrackPanel::GetBackgroundCell()
|
|
{
|
|
return mpBackground;
|
|
}
|
|
|
|
/// Draw a three-level highlight gradient around the focused track.
|
|
void TrackPanel::HighlightFocusedTrack(wxDC * dc, const wxRect & rect)
|
|
{
|
|
wxRect theRect = rect;
|
|
theRect.Inflate( kBorderThickness );
|
|
theRect.width += kShadowThickness;
|
|
theRect.height += kShadowThickness;
|
|
dc->SetBrush(*wxTRANSPARENT_BRUSH);
|
|
|
|
AColor::TrackFocusPen(dc, 0);
|
|
theRect.Inflate(1);
|
|
dc->DrawRectangle(theRect);
|
|
|
|
AColor::TrackFocusPen(dc, 1);
|
|
theRect.Inflate(1);
|
|
dc->DrawRectangle(theRect);
|
|
|
|
AColor::TrackFocusPen(dc, 2);
|
|
theRect.Inflate(1);
|
|
dc->DrawRectangle(theRect);
|
|
}
|
|
|
|
void TrackPanel::UpdateVRulers()
|
|
{
|
|
for (auto t : GetTracks()->Any< const WaveTrack >())
|
|
UpdateTrackVRuler(t);
|
|
|
|
UpdateVRulerSize();
|
|
}
|
|
|
|
void TrackPanel::UpdateVRuler(Track *t)
|
|
{
|
|
if (t)
|
|
UpdateTrackVRuler(t);
|
|
|
|
UpdateVRulerSize();
|
|
}
|
|
|
|
void TrackPanel::UpdateTrackVRuler(const Track *t)
|
|
{
|
|
wxASSERT(t);
|
|
if (!t)
|
|
return;
|
|
|
|
wxRect rect(GetVRulerOffset(),
|
|
kTopMargin,
|
|
GetVRulerWidth(),
|
|
0);
|
|
|
|
|
|
for (auto channel : TrackList::Channels(t)) {
|
|
rect.height = channel->GetHeight() - (kTopMargin + kBottomMargin);
|
|
mTrackArtist->UpdateVRuler(channel, rect);
|
|
}
|
|
}
|
|
|
|
void TrackPanel::UpdateVRulerSize()
|
|
{
|
|
auto trackRange = GetTracks()->Any();
|
|
if (trackRange) {
|
|
wxSize s { 0, 0 };
|
|
for (auto t : trackRange)
|
|
s.IncTo(t->vrulerSize);
|
|
|
|
if (vrulerSize != s) {
|
|
vrulerSize = s;
|
|
mRuler->SetLeftOffset(GetLeftOffset()); // bevel on AdornedRuler
|
|
mRuler->Refresh();
|
|
}
|
|
}
|
|
Refresh(false);
|
|
}
|
|
|
|
// Make sure selection edge is in view
|
|
void TrackPanel::ScrollIntoView(double pos)
|
|
{
|
|
int w;
|
|
GetTracksUsableArea( &w, NULL );
|
|
|
|
int pixel = mViewInfo->TimeToPosition(pos);
|
|
if (pixel < 0 || pixel >= w)
|
|
{
|
|
mListener->TP_ScrollWindow
|
|
(mViewInfo->OffsetTimeByPixels(pos, -(w / 2)));
|
|
Refresh(false);
|
|
}
|
|
}
|
|
|
|
void TrackPanel::ScrollIntoView(int x)
|
|
{
|
|
ScrollIntoView(mViewInfo->PositionToTime(x, GetLeftOffset()));
|
|
}
|
|
|
|
void TrackPanel::OnTrackMenu(Track *t)
|
|
{
|
|
CellularPanel::DoContextMenu( t );
|
|
}
|
|
|
|
Track * TrackPanel::GetFirstSelectedTrack()
|
|
{
|
|
auto t = *GetTracks()->Selected().begin();
|
|
if (t)
|
|
return t;
|
|
else
|
|
//if nothing is selected, return the first track
|
|
return *GetTracks()->Any().begin();
|
|
}
|
|
|
|
void TrackPanel::EnsureVisible(Track * t)
|
|
{
|
|
SetFocusedTrack(t);
|
|
|
|
int trackTop = 0;
|
|
int trackHeight =0;
|
|
|
|
for (auto it : GetTracks()->Leaders()) {
|
|
trackTop += trackHeight;
|
|
|
|
auto channels = TrackList::Channels(it);
|
|
trackHeight = channels.sum( &Track::GetHeight );
|
|
|
|
//We have found the track we want to ensure is visible.
|
|
if (channels.contains(t)) {
|
|
|
|
//Get the size of the trackpanel.
|
|
int width, height;
|
|
GetSize(&width, &height);
|
|
|
|
if (trackTop < mViewInfo->vpos) {
|
|
height = mViewInfo->vpos - trackTop + mViewInfo->scrollStep;
|
|
height /= mViewInfo->scrollStep;
|
|
mListener->TP_ScrollUpDown(-height);
|
|
}
|
|
else if (trackTop + trackHeight > mViewInfo->vpos + height) {
|
|
height = (trackTop + trackHeight) - (mViewInfo->vpos + height);
|
|
height = (height + mViewInfo->scrollStep + 1) / mViewInfo->scrollStep;
|
|
mListener->TP_ScrollUpDown(height);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
Refresh(false);
|
|
}
|
|
|
|
// 0.0 scrolls to top
|
|
// 1.0 scrolls to bottom.
|
|
void TrackPanel::VerticalScroll( float fracPosition){
|
|
|
|
int trackTop = 0;
|
|
int trackHeight = 0;
|
|
|
|
auto tracks = GetTracks();
|
|
auto GetHeight =
|
|
[&]( const Track *t ){ return tracks->GetGroupHeight(t); };
|
|
|
|
auto range = tracks->Leaders();
|
|
if (!range.empty()) {
|
|
trackHeight = GetHeight( *range.rbegin() );
|
|
--range.second;
|
|
}
|
|
trackTop = range.sum( GetHeight );
|
|
|
|
int delta;
|
|
|
|
//Get the size of the trackpanel.
|
|
int width, height;
|
|
GetSize(&width, &height);
|
|
|
|
delta = (fracPosition * (trackTop + trackHeight - height)) - mViewInfo->vpos + mViewInfo->scrollStep;
|
|
//wxLogDebug( "Scroll down by %i pixels", delta );
|
|
delta /= mViewInfo->scrollStep;
|
|
mListener->TP_ScrollUpDown(delta);
|
|
Refresh(false);
|
|
}
|
|
|
|
|
|
// Draw a rectangular border
|
|
void TrackPanel::DrawBordersAroundTrack( wxDC * dc, const wxRect & rect )
|
|
{
|
|
// Border around track and label area
|
|
// leaving room for the shadow
|
|
dc->SetBrush(*wxTRANSPARENT_BRUSH);
|
|
dc->SetPen(*wxBLACK_PEN);
|
|
dc->DrawRectangle( rect.Inflate( kBorderThickness, kBorderThickness ) );
|
|
}
|
|
|
|
// Given rectangle is the track rectangle excluding the border
|
|
// Stroke lines along bottom and right, which are slightly short at
|
|
// bottom-left and top-right
|
|
void TrackPanel::DrawShadow( wxDC * dc, const wxRect & rect )
|
|
{
|
|
int right = rect.GetRight() + kBorderThickness + kShadowThickness;
|
|
int bottom = rect.GetBottom() + kBorderThickness + kShadowThickness;
|
|
|
|
// shadow color for lines
|
|
dc->SetPen(*wxBLACK_PEN);
|
|
|
|
// bottom
|
|
AColor::Line(*dc, rect.x, bottom, right, bottom);
|
|
// right
|
|
AColor::Line(*dc, right, rect.y, right, bottom);
|
|
|
|
// background color erases small parts of those lines
|
|
AColor::TrackPanelBackground(dc, false);
|
|
|
|
// bottom-left
|
|
AColor::Line(*dc, rect.x, bottom, rect.x + 1, bottom);
|
|
// top-right
|
|
AColor::Line(*dc, right, rect.y, right, rect.y + 1);
|
|
}
|
|
|
|
namespace {
|
|
|
|
/*
|
|
|
|
The following classes define the subdivision of the area of the TrackPanel
|
|
into cells with differing reponses to mouse, keyboard, and scroll wheel
|
|
events.
|
|
|
|
The classes defining the less inclusive areas are earlier, while those
|
|
defining ever larger hierarchical groupings of cells are later.
|
|
|
|
To describe that subdivision again, informally, and top-down:
|
|
|
|
Firstly subtract margin areas, on the left and right, that do not interact.
|
|
|
|
Secondly subtract a noninterative margin above the first track, and an area
|
|
below all tracks that causes deselection of all tracks if you click it.
|
|
(One or both of those areas might be vertically scrolled off-screen, though.)
|
|
Divide what remains into areas corresponding to the several tracks.
|
|
|
|
Thirdly, for each track, subtract an area below, which you can click and drag
|
|
to resize the track vertically.
|
|
|
|
Fourthly, subtract an area at the left, which contains the track controls,
|
|
such as the menu and delete and minimize buttons, and others appropriate
|
|
to the track subtype.
|
|
|
|
Fifthly, divide what remains into the vertically stacked channels, if there
|
|
are more than one, alternating with separators, which can be clicked to
|
|
resize the channel views.
|
|
|
|
Lastly, split the area for each channel into a vertical ruler, and an area
|
|
that displays the channel's own contents.
|
|
|
|
*/
|
|
|
|
struct EmptyCell final : CommonTrackPanelCell {
|
|
std::vector< UIHandlePtr > HitTest(
|
|
const TrackPanelMouseState &, const AudacityProject *) override
|
|
{ return {}; }
|
|
virtual std::shared_ptr< Track > DoFindTrack() override { return {}; }
|
|
static std::shared_ptr<EmptyCell> Instance()
|
|
{
|
|
static auto instance = std::make_shared< EmptyCell >();
|
|
return instance;
|
|
}
|
|
};
|
|
|
|
// A vertical ruler left of a channel
|
|
struct VRulerAndChannel final : TrackPanelGroup {
|
|
VRulerAndChannel(
|
|
const std::shared_ptr< Track > &pChannel, wxCoord leftOffset )
|
|
: mpChannel{ pChannel }, mLeftOffset{ leftOffset } {}
|
|
Subdivision Children( const wxRect &rect ) override
|
|
{
|
|
return { Axis::X, Refinement{
|
|
{ rect.GetLeft(), mpChannel->GetVRulerControl() },
|
|
{ mLeftOffset, mpChannel }
|
|
} };
|
|
}
|
|
std::shared_ptr< Track > mpChannel;
|
|
wxCoord mLeftOffset;
|
|
};
|
|
|
|
// n channels with vertical rulers, alternating with n - 1 resizers
|
|
struct ChannelGroup final : TrackPanelGroup {
|
|
ChannelGroup( const std::shared_ptr< Track > &pTrack, wxCoord leftOffset )
|
|
: mpTrack{ pTrack }, mLeftOffset{ leftOffset } {}
|
|
Subdivision Children( const wxRect &rect ) override
|
|
{
|
|
Refinement refinement;
|
|
|
|
const auto channels = TrackList::Channels( mpTrack.get() );
|
|
const auto pLast = *channels.rbegin();
|
|
wxCoord yy = rect.GetTop();
|
|
for ( auto channel : channels ) {
|
|
refinement.emplace_back( yy,
|
|
std::make_shared< VRulerAndChannel >(
|
|
channel->SharedPointer(), mLeftOffset ) );
|
|
if ( channel != pLast ) {
|
|
const auto substitute =
|
|
channel->SubstitutePendingChangedTrack();
|
|
yy += substitute->GetHeight();
|
|
refinement.emplace_back(
|
|
yy - kSeparatorThickness, channel->GetResizer() );
|
|
}
|
|
}
|
|
|
|
return { Axis::Y, std::move( refinement ) };
|
|
}
|
|
std::shared_ptr< Track > mpTrack;
|
|
wxCoord mLeftOffset;
|
|
};
|
|
|
|
// A track control panel, left of n vertical rulers and n channels
|
|
// alternating with n - 1 resizers
|
|
struct LabeledChannelGroup final : TrackPanelGroup {
|
|
LabeledChannelGroup(
|
|
const std::shared_ptr< Track > &pTrack, wxCoord leftOffset )
|
|
: mpTrack{ pTrack }, mLeftOffset{ leftOffset } {}
|
|
Subdivision Children( const wxRect &rect ) override
|
|
{ return { Axis::X, Refinement{
|
|
{ rect.GetLeft(), mpTrack->GetTrackControl() },
|
|
{ rect.GetLeft() + kTrackInfoWidth,
|
|
std::make_shared< ChannelGroup >( mpTrack, mLeftOffset ) }
|
|
} }; }
|
|
std::shared_ptr< Track > mpTrack;
|
|
wxCoord mLeftOffset;
|
|
};
|
|
|
|
// Stacks a label and a single or multi-channel track on a resizer below,
|
|
// which is associated with the last channel
|
|
struct ResizingChannelGroup final : TrackPanelGroup {
|
|
ResizingChannelGroup(
|
|
const std::shared_ptr< Track > &pTrack, wxCoord leftOffset )
|
|
: mpTrack{ pTrack }, mLeftOffset{ leftOffset } {}
|
|
Subdivision Children( const wxRect &rect ) override
|
|
{ return { Axis::Y, Refinement{
|
|
{ rect.GetTop(),
|
|
std::make_shared< LabeledChannelGroup >( mpTrack, mLeftOffset ) },
|
|
{ rect.GetTop() + rect.GetHeight() - kSeparatorThickness,
|
|
( *TrackList::Channels( mpTrack.get() ).rbegin() )->GetResizer() }
|
|
} }; }
|
|
std::shared_ptr< Track > mpTrack;
|
|
wxCoord mLeftOffset;
|
|
};
|
|
|
|
// Stacks a dead area at top, the tracks, and the click-to-deselect area below
|
|
struct Subgroup final : TrackPanelGroup {
|
|
explicit Subgroup( TrackPanel &panel ) : mPanel{ panel } {}
|
|
Subdivision Children( const wxRect &rect ) override
|
|
{
|
|
wxCoord yy = -mPanel.GetViewInfo()->vpos;
|
|
Refinement refinement;
|
|
|
|
auto &tracks = *mPanel.GetTracks();
|
|
if ( tracks.Any() )
|
|
refinement.emplace_back( yy, EmptyCell::Instance() ),
|
|
yy += kTopMargin;
|
|
|
|
for ( const auto leader : tracks.Leaders() ) {
|
|
wxCoord height = 0;
|
|
for ( auto channel : TrackList::Channels( leader ) ) {
|
|
auto substitute =
|
|
channel->SubstitutePendingChangedTrack();
|
|
height += substitute->GetHeight();
|
|
}
|
|
refinement.emplace_back( yy,
|
|
std::make_shared< ResizingChannelGroup >(
|
|
leader->SharedPointer(), mPanel.GetLeftOffset() )
|
|
);
|
|
yy += height;
|
|
}
|
|
|
|
refinement.emplace_back( std::max( 0, yy ), mPanel.GetBackgroundCell() );
|
|
|
|
return { Axis::Y, std::move( refinement ) };
|
|
}
|
|
TrackPanel &mPanel;
|
|
};
|
|
|
|
// Main group shaves off the left and right margins
|
|
struct MainGroup final : TrackPanelGroup {
|
|
explicit MainGroup( TrackPanel &panel ) : mPanel{ panel } {}
|
|
Subdivision Children( const wxRect &rect ) override
|
|
{ return { Axis::X, Refinement{
|
|
{ 0, EmptyCell::Instance() },
|
|
{ kLeftMargin, std::make_shared< Subgroup >( mPanel ) },
|
|
{ rect.GetRight() + 1 - kRightMargin, EmptyCell::Instance() }
|
|
} }; }
|
|
TrackPanel &mPanel;
|
|
};
|
|
|
|
}
|
|
|
|
std::shared_ptr<TrackPanelNode> TrackPanel::Root()
|
|
{
|
|
// Root and other subgroup objects are throwaways.
|
|
// They might instead be cached to avoid repeated allocation.
|
|
// That cache would need invalidation when there is addition, deletion, or
|
|
// permutation of tracks, or change of width of the vertical rulers.
|
|
return std::make_shared< MainGroup >( *this );
|
|
}
|
|
|
|
// This finds the rectangle of a given track (including all channels),
|
|
// either that of the label 'adornment' or the track itself
|
|
// The given track is assumed to be the first channel
|
|
wxRect TrackPanel::FindTrackRect( const Track * target )
|
|
{
|
|
auto leader = *GetTracks()->FindLeader( target );
|
|
if (!leader) {
|
|
return { 0, 0, 0, 0 };
|
|
}
|
|
|
|
return CellularPanel::FindRect( [&] ( TrackPanelNode &node ) {
|
|
if (auto pGroup = dynamic_cast<const LabeledChannelGroup*>( &node ))
|
|
return pGroup->mpTrack.get() == leader;
|
|
return false;
|
|
} );
|
|
}
|
|
|
|
int TrackPanel::GetVRulerWidth() const
|
|
{
|
|
return vrulerSize.x;
|
|
}
|
|
|
|
/// Displays the bounds of the selection in the status bar.
|
|
void TrackPanel::DisplaySelection()
|
|
{
|
|
if (!mListener)
|
|
return;
|
|
|
|
// DM: Note that the Selection Bar can actually MODIFY the selection
|
|
// if snap-to mode is on!!!
|
|
mListener->TP_DisplaySelection();
|
|
}
|
|
|
|
TrackPanelCell *TrackPanel::GetFocusedCell()
|
|
{
|
|
return mAx->GetFocus().get();
|
|
}
|
|
|
|
Track *TrackPanel::GetFocusedTrack()
|
|
{
|
|
return static_cast<Track*>( GetFocusedCell() );
|
|
}
|
|
|
|
void TrackPanel::SetFocusedCell()
|
|
{
|
|
SetFocusedTrack( GetFocusedTrack() );
|
|
}
|
|
|
|
void TrackPanel::SetFocusedTrack( Track *t )
|
|
{
|
|
// Make sure we always have the first linked track of a stereo track
|
|
t = *GetTracks()->FindLeader(t);
|
|
|
|
auto cell = mAx->SetFocus( Track::SharedPointer( t ) ).get();
|
|
|
|
if (cell) {
|
|
KeyboardCapture::Capture(this);
|
|
Refresh( false );
|
|
}
|
|
}
|
|
|
|
/**********************************************************************
|
|
|
|
TrackInfo code is destined to move out of this file.
|
|
|
|
**********************************************************************/
|
|
|
|
namespace {
|
|
|
|
wxFont gFont;
|
|
|
|
std::unique_ptr<LWSlider>
|
|
gGainCaptured
|
|
, gPanCaptured
|
|
, gGain
|
|
, gPan
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
, gVelocityCaptured
|
|
, gVelocity
|
|
#endif
|
|
;
|
|
|
|
}
|
|
|
|
void TrackInfo::ReCreateSliders( wxWindow *pParent ){
|
|
const wxPoint point{ 0, 0 };
|
|
wxRect sliderRect;
|
|
GetGainRect(point, sliderRect);
|
|
|
|
float defPos = 1.0;
|
|
/* i18n-hint: Title of the Gain slider, used to adjust the volume */
|
|
gGain = std::make_unique<LWSlider>(pParent, _("Gain"),
|
|
wxPoint(sliderRect.x, sliderRect.y),
|
|
wxSize(sliderRect.width, sliderRect.height),
|
|
DB_SLIDER);
|
|
gGain->SetDefaultValue(defPos);
|
|
|
|
gGainCaptured = std::make_unique<LWSlider>(pParent, _("Gain"),
|
|
wxPoint(sliderRect.x, sliderRect.y),
|
|
wxSize(sliderRect.width, sliderRect.height),
|
|
DB_SLIDER);
|
|
gGainCaptured->SetDefaultValue(defPos);
|
|
|
|
GetPanRect(point, sliderRect);
|
|
|
|
defPos = 0.0;
|
|
/* i18n-hint: Title of the Pan slider, used to move the sound left or right */
|
|
gPan = std::make_unique<LWSlider>(pParent, _("Pan"),
|
|
wxPoint(sliderRect.x, sliderRect.y),
|
|
wxSize(sliderRect.width, sliderRect.height),
|
|
PAN_SLIDER);
|
|
gPan->SetDefaultValue(defPos);
|
|
|
|
gPanCaptured = std::make_unique<LWSlider>(pParent, _("Pan"),
|
|
wxPoint(sliderRect.x, sliderRect.y),
|
|
wxSize(sliderRect.width, sliderRect.height),
|
|
PAN_SLIDER);
|
|
gPanCaptured->SetDefaultValue(defPos);
|
|
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
GetVelocityRect(point, sliderRect);
|
|
|
|
/* i18n-hint: Title of the Velocity slider, used to adjust the volume of note tracks */
|
|
gVelocity = std::make_unique<LWSlider>(pParent, _("Velocity"),
|
|
wxPoint(sliderRect.x, sliderRect.y),
|
|
wxSize(sliderRect.width, sliderRect.height),
|
|
VEL_SLIDER);
|
|
gVelocity->SetDefaultValue(0.0);
|
|
gVelocityCaptured = std::make_unique<LWSlider>(pParent, _("Velocity"),
|
|
wxPoint(sliderRect.x, sliderRect.y),
|
|
wxSize(sliderRect.width, sliderRect.height),
|
|
VEL_SLIDER);
|
|
gVelocityCaptured->SetDefaultValue(0.0);
|
|
#endif
|
|
|
|
}
|
|
|
|
void TrackInfo::GetCloseBoxHorizontalBounds( const wxRect & rect, wxRect &dest )
|
|
{
|
|
dest.x = rect.x;
|
|
dest.width = kTrackInfoBtnSize;
|
|
}
|
|
|
|
void TrackInfo::GetCloseBoxRect(const wxRect & rect, wxRect & dest)
|
|
{
|
|
GetCloseBoxHorizontalBounds( rect, dest );
|
|
auto results = CalcItemY( commonTrackTCPLines, kItemBarButtons );
|
|
dest.y = rect.y + results.first;
|
|
dest.height = results.second;
|
|
}
|
|
|
|
static const int TitleSoloBorderOverlap = 1;
|
|
|
|
void TrackInfo::GetTitleBarHorizontalBounds( const wxRect & rect, wxRect &dest )
|
|
{
|
|
// to right of CloseBoxRect, plus a little more
|
|
wxRect closeRect;
|
|
GetCloseBoxHorizontalBounds( rect, closeRect );
|
|
dest.x = rect.x + closeRect.width + 1;
|
|
dest.width = rect.x + rect.width - dest.x + TitleSoloBorderOverlap;
|
|
}
|
|
|
|
void TrackInfo::GetTitleBarRect(const wxRect & rect, wxRect & dest)
|
|
{
|
|
GetTitleBarHorizontalBounds( rect, dest );
|
|
auto results = CalcItemY( commonTrackTCPLines, kItemBarButtons );
|
|
dest.y = rect.y + results.first;
|
|
dest.height = results.second;
|
|
}
|
|
|
|
void TrackInfo::GetNarrowMuteHorizontalBounds( const wxRect & rect, wxRect &dest )
|
|
{
|
|
dest.x = rect.x;
|
|
dest.width = rect.width / 2 + 1;
|
|
}
|
|
|
|
void TrackInfo::GetNarrowSoloHorizontalBounds( const wxRect & rect, wxRect &dest )
|
|
{
|
|
wxRect muteRect;
|
|
GetNarrowMuteHorizontalBounds( rect, muteRect );
|
|
dest.x = rect.x + muteRect.width;
|
|
dest.width = rect.width - muteRect.width + TitleSoloBorderOverlap;
|
|
}
|
|
|
|
void TrackInfo::GetWideMuteSoloHorizontalBounds( const wxRect & rect, wxRect &dest )
|
|
{
|
|
// Larger button, symmetrically placed intended.
|
|
// On windows this gives 15 pixels each side.
|
|
dest.width = rect.width - 2 * kTrackInfoBtnSize + 6;
|
|
dest.x = rect.x + kTrackInfoBtnSize -3;
|
|
}
|
|
|
|
void TrackInfo::GetMuteSoloRect
|
|
(const wxRect & rect, wxRect & dest, bool solo, bool bHasSoloButton,
|
|
const Track *pTrack)
|
|
{
|
|
|
|
auto resultsM = CalcItemY( getTCPLines( *pTrack ), kItemMute );
|
|
auto resultsS = CalcItemY( getTCPLines( *pTrack ), kItemSolo );
|
|
dest.height = resultsS.second;
|
|
|
|
int yMute = resultsM.first;
|
|
int ySolo = resultsS.first;
|
|
|
|
bool bSameRow = ( yMute == ySolo );
|
|
bool bNarrow = bSameRow && bHasSoloButton;
|
|
|
|
if( bNarrow )
|
|
{
|
|
if( solo )
|
|
GetNarrowSoloHorizontalBounds( rect, dest );
|
|
else
|
|
GetNarrowMuteHorizontalBounds( rect, dest );
|
|
}
|
|
else
|
|
GetWideMuteSoloHorizontalBounds( rect, dest );
|
|
|
|
if( bSameRow || !solo )
|
|
dest.y = rect.y + yMute;
|
|
else
|
|
dest.y = rect.y + ySolo;
|
|
|
|
}
|
|
|
|
void TrackInfo::GetSliderHorizontalBounds( const wxPoint &topleft, wxRect &dest )
|
|
{
|
|
dest.x = topleft.x + 6;
|
|
dest.width = kTrackInfoSliderWidth;
|
|
}
|
|
|
|
void TrackInfo::GetGainRect(const wxPoint &topleft, wxRect & dest)
|
|
{
|
|
GetSliderHorizontalBounds( topleft, dest );
|
|
auto results = CalcItemY( waveTrackTCPLines, kItemGain );
|
|
dest.y = topleft.y + results.first;
|
|
dest.height = results.second;
|
|
}
|
|
|
|
void TrackInfo::GetPanRect(const wxPoint &topleft, wxRect & dest)
|
|
{
|
|
GetGainRect( topleft, dest );
|
|
auto results = CalcItemY( waveTrackTCPLines, kItemPan );
|
|
dest.y = topleft.y + results.first;
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
void TrackInfo::GetVelocityRect(const wxPoint &topleft, wxRect & dest)
|
|
{
|
|
GetSliderHorizontalBounds( topleft, dest );
|
|
auto results = CalcItemY( noteTrackTCPLines, kItemVelocity );
|
|
dest.y = topleft.y + results.first;
|
|
dest.height = results.second;
|
|
}
|
|
#endif
|
|
|
|
void TrackInfo::GetMinimizeHorizontalBounds( const wxRect &rect, wxRect &dest )
|
|
{
|
|
const int space = 0;// was 3.
|
|
dest.x = rect.x + space;
|
|
|
|
wxRect syncLockRect;
|
|
GetSyncLockHorizontalBounds( rect, syncLockRect );
|
|
|
|
// Width is rect.width less space on left for track select
|
|
// and on right for sync-lock icon.
|
|
dest.width = kTrackInfoBtnSize;
|
|
// rect.width - (space + syncLockRect.width);
|
|
}
|
|
|
|
void TrackInfo::GetMinimizeRect(const wxRect & rect, wxRect &dest)
|
|
{
|
|
GetMinimizeHorizontalBounds( rect, dest );
|
|
auto results = CalcBottomItemY
|
|
( commonTrackTCPBottomLines, kItemMinimize, rect.height);
|
|
dest.y = rect.y + results.first;
|
|
dest.height = results.second;
|
|
}
|
|
|
|
void TrackInfo::GetSelectButtonHorizontalBounds( const wxRect &rect, wxRect &dest )
|
|
{
|
|
const int space = 0;// was 3.
|
|
dest.x = rect.x + space;
|
|
|
|
wxRect syncLockRect;
|
|
GetSyncLockHorizontalBounds( rect, syncLockRect );
|
|
wxRect minimizeRect;
|
|
GetMinimizeHorizontalBounds( rect, minimizeRect );
|
|
|
|
dest.x = dest.x + space + minimizeRect.width;
|
|
// Width is rect.width less space on left for track select
|
|
// and on right for sync-lock icon.
|
|
dest.width = rect.width - (space + syncLockRect.width) - (space + minimizeRect.width);
|
|
}
|
|
|
|
|
|
void TrackInfo::GetSelectButtonRect(const wxRect & rect, wxRect &dest)
|
|
{
|
|
GetSelectButtonHorizontalBounds( rect, dest );
|
|
auto results = CalcBottomItemY
|
|
( commonTrackTCPBottomLines, kItemMinimize, rect.height);
|
|
dest.y = rect.y + results.first;
|
|
dest.height = results.second;
|
|
}
|
|
|
|
void TrackInfo::GetSyncLockHorizontalBounds( const wxRect &rect, wxRect &dest )
|
|
{
|
|
dest.width = kTrackInfoBtnSize;
|
|
dest.x = rect.x + rect.width - dest.width;
|
|
}
|
|
|
|
void TrackInfo::GetSyncLockIconRect(const wxRect & rect, wxRect &dest)
|
|
{
|
|
GetSyncLockHorizontalBounds( rect, dest );
|
|
auto results = CalcBottomItemY
|
|
( commonTrackTCPBottomLines, kItemSyncLock, rect.height);
|
|
dest.y = rect.y + results.first;
|
|
dest.height = results.second;
|
|
}
|
|
|
|
#ifdef USE_MIDI
|
|
void TrackInfo::GetMidiControlsHorizontalBounds
|
|
( const wxRect &rect, wxRect &dest )
|
|
{
|
|
dest.x = rect.x + 1; // To center slightly
|
|
// PRL: TODO: kMidiCellWidth is defined in terms of the other constant
|
|
// kTrackInfoWidth but I am trying to avoid use of that constant.
|
|
// Can cell width be computed from dest.width instead?
|
|
dest.width = kMidiCellWidth * 4;
|
|
}
|
|
|
|
void TrackInfo::GetMidiControlsRect(const wxRect & rect, wxRect & dest)
|
|
{
|
|
GetMidiControlsHorizontalBounds( rect, dest );
|
|
auto results = CalcItemY( noteTrackTCPLines, kItemMidiControlsRect );
|
|
dest.y = rect.y + results.first;
|
|
dest.height = results.second;
|
|
}
|
|
#endif
|
|
|
|
/// \todo Probably should move to 'Utils.cpp'.
|
|
void TrackInfo::SetTrackInfoFont(wxDC * dc)
|
|
{
|
|
dc->SetFont(gFont);
|
|
}
|
|
|
|
#if 0
|
|
void TrackInfo::DrawBordersWithin
|
|
( wxDC* dc, const wxRect & rect, const Track &track ) const
|
|
{
|
|
AColor::Dark(dc, false); // same color as border of toolbars (ToolBar::OnPaint())
|
|
|
|
// below close box and title bar
|
|
wxRect buttonRect;
|
|
GetTitleBarRect( rect, buttonRect );
|
|
AColor::Line
|
|
(*dc, rect.x, buttonRect.y + buttonRect.height,
|
|
rect.width - 1, buttonRect.y + buttonRect.height);
|
|
|
|
// between close box and title bar
|
|
AColor::Line
|
|
(*dc, buttonRect.x, buttonRect.y,
|
|
buttonRect.x, buttonRect.y + buttonRect.height - 1);
|
|
|
|
GetMuteSoloRect( rect, buttonRect, false, true, &track );
|
|
|
|
bool bHasMuteSolo = dynamic_cast<const PlayableTrack*>( &track ) != NULL;
|
|
if( bHasMuteSolo && !TrackInfo::HideTopItem( rect, buttonRect ) )
|
|
{
|
|
// above mute/solo
|
|
AColor::Line
|
|
(*dc, rect.x, buttonRect.y,
|
|
rect.width - 1, buttonRect.y);
|
|
|
|
// between mute/solo
|
|
// Draw this little line; if there is no solo, wide mute button will
|
|
// overpaint it later:
|
|
AColor::Line
|
|
(*dc, buttonRect.x + buttonRect.width, buttonRect.y,
|
|
buttonRect.x + buttonRect.width, buttonRect.y + buttonRect.height - 1);
|
|
|
|
// below mute/solo
|
|
AColor::Line
|
|
(*dc, rect.x, buttonRect.y + buttonRect.height,
|
|
rect.width - 1, buttonRect.y + buttonRect.height);
|
|
}
|
|
|
|
// left of and above minimize button
|
|
wxRect minimizeRect;
|
|
this->GetMinimizeRect(rect, minimizeRect);
|
|
AColor::Line
|
|
(*dc, minimizeRect.x - 1, minimizeRect.y,
|
|
minimizeRect.x - 1, minimizeRect.y + minimizeRect.height - 1);
|
|
AColor::Line
|
|
(*dc, minimizeRect.x, minimizeRect.y - 1,
|
|
minimizeRect.x + minimizeRect.width - 1, minimizeRect.y - 1);
|
|
}
|
|
#endif
|
|
|
|
//#define USE_BEVELS
|
|
|
|
// Paint the whole given rectangle some fill color
|
|
void TrackInfo::DrawBackground(
|
|
wxDC * dc, const wxRect & rect, bool bSelected, const int vrul)
|
|
{
|
|
// fill in label
|
|
wxRect fill = rect;
|
|
fill.width = vrul - kLeftMargin;
|
|
AColor::MediumTrackInfo(dc, bSelected);
|
|
dc->DrawRectangle(fill);
|
|
|
|
#ifdef USE_BEVELS
|
|
// This branch is not now used
|
|
// PRL: todo: banish magic numbers.
|
|
// PRL: vrul was the x coordinate of left edge of the vertical ruler.
|
|
// PRL: bHasMuteSolo was true iff the track was WaveTrack.
|
|
if( bHasMuteSolo )
|
|
{
|
|
int ylast = rect.height-20;
|
|
int ybutton = wxMin(32,ylast-17);
|
|
int ybuttonEnd = 67;
|
|
|
|
fill=wxRect( rect.x+1, rect.y+17, vrul-6, ybutton);
|
|
AColor::BevelTrackInfo( *dc, true, fill );
|
|
|
|
if( ybuttonEnd < ylast ){
|
|
fill=wxRect( rect.x+1, rect.y+ybuttonEnd, fill.width, ylast - ybuttonEnd);
|
|
AColor::BevelTrackInfo( *dc, true, fill );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fill=wxRect( rect.x+1, rect.y+17, vrul-6, rect.height-37);
|
|
AColor::BevelTrackInfo( *dc, true, fill );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
namespace {
|
|
unsigned DefaultTrackHeight( const TCPLines &topLines )
|
|
{
|
|
int needed =
|
|
kTopMargin + kBottomMargin +
|
|
totalTCPLines( topLines, true ) +
|
|
totalTCPLines( commonTrackTCPBottomLines, false ) + 1;
|
|
return (unsigned) std::max( needed, (int) Track::DefaultHeight );
|
|
}
|
|
}
|
|
|
|
unsigned TrackInfo::DefaultNoteTrackHeight()
|
|
{
|
|
return DefaultTrackHeight( noteTrackTCPLines );
|
|
}
|
|
|
|
unsigned TrackInfo::DefaultWaveTrackHeight()
|
|
{
|
|
return DefaultTrackHeight( waveTrackTCPLines );
|
|
}
|
|
|
|
LWSlider * TrackInfo::GainSlider
|
|
(const wxRect &sliderRect, const WaveTrack *t, bool captured, wxWindow *pParent)
|
|
{
|
|
wxPoint pos = sliderRect.GetPosition();
|
|
float gain = t ? t->GetGain() : 1.0;
|
|
|
|
gGain->Move(pos);
|
|
gGain->Set(gain);
|
|
gGainCaptured->Move(pos);
|
|
gGainCaptured->Set(gain);
|
|
|
|
auto slider = (captured ? gGainCaptured : gGain).get();
|
|
slider->SetParent( pParent ? pParent : ::GetActiveProject() );
|
|
return slider;
|
|
}
|
|
|
|
LWSlider * TrackInfo::PanSlider
|
|
(const wxRect &sliderRect, const WaveTrack *t, bool captured, wxWindow *pParent)
|
|
{
|
|
wxPoint pos = sliderRect.GetPosition();
|
|
float pan = t ? t->GetPan() : 0.0;
|
|
|
|
gPan->Move(pos);
|
|
gPan->Set(pan);
|
|
gPanCaptured->Move(pos);
|
|
gPanCaptured->Set(pan);
|
|
|
|
auto slider = (captured ? gPanCaptured : gPan).get();
|
|
slider->SetParent( pParent ? pParent : ::GetActiveProject() );
|
|
return slider;
|
|
}
|
|
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
LWSlider * TrackInfo::VelocitySlider
|
|
(const wxRect &sliderRect, const NoteTrack *t, bool captured, wxWindow *pParent)
|
|
{
|
|
wxPoint pos = sliderRect.GetPosition();
|
|
float velocity = t ? t->GetVelocity() : 0.0;
|
|
|
|
gVelocity->Move(pos);
|
|
gVelocity->Set(velocity);
|
|
gVelocityCaptured->Move(pos);
|
|
gVelocityCaptured->Set(velocity);
|
|
|
|
auto slider = (captured ? gVelocityCaptured : gVelocity).get();
|
|
slider->SetParent( pParent ? pParent : ::GetActiveProject() );
|
|
return slider;
|
|
}
|
|
#endif
|
|
|
|
void TrackInfo::UpdatePrefs( wxWindow *pParent )
|
|
{
|
|
// Calculation of best font size depends on language, so it should be redone in case
|
|
// the language preference changed.
|
|
|
|
int fontSize = 10;
|
|
gFont.Create(fontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
|
|
|
|
int allowableWidth =
|
|
// PRL: was it correct to include the margin?
|
|
( kTrackInfoWidth + kLeftMargin )
|
|
- 2; // 2 to allow for left/right borders
|
|
int textWidth, textHeight;
|
|
do {
|
|
gFont.SetPointSize(fontSize);
|
|
pParent->GetTextExtent(_("Stereo, 999999Hz"),
|
|
&textWidth,
|
|
&textHeight,
|
|
NULL,
|
|
NULL,
|
|
&gFont);
|
|
fontSize--;
|
|
} while (textWidth >= allowableWidth);
|
|
}
|
|
|
|
TrackPanelNode::TrackPanelNode()
|
|
{
|
|
}
|
|
|
|
TrackPanelNode::~TrackPanelNode()
|
|
{
|
|
}
|
|
|
|
TrackPanelGroup::TrackPanelGroup()
|
|
{
|
|
}
|
|
|
|
TrackPanelGroup::~TrackPanelGroup()
|
|
{
|
|
}
|
|
|
|
TrackPanelCell::~TrackPanelCell()
|
|
{
|
|
}
|
|
|
|
HitTestPreview TrackPanelCell::DefaultPreview
|
|
(const TrackPanelMouseState &, const AudacityProject *)
|
|
{
|
|
return {};
|
|
}
|
|
|
|
unsigned TrackPanelCell::HandleWheelRotation
|
|
(const TrackPanelMouseEvent &, AudacityProject *)
|
|
{
|
|
return RefreshCode::Cancelled;
|
|
}
|
|
|
|
unsigned TrackPanelCell::DoContextMenu
|
|
(const wxRect &, wxWindow*, wxPoint *)
|
|
{
|
|
return RefreshCode::RefreshNone;
|
|
}
|
|
|
|
unsigned TrackPanelCell::CaptureKey(wxKeyEvent &event, ViewInfo &, wxWindow *)
|
|
{
|
|
event.Skip();
|
|
return RefreshCode::RefreshNone;
|
|
}
|
|
|
|
unsigned TrackPanelCell::KeyDown(wxKeyEvent &event, ViewInfo &, wxWindow *)
|
|
{
|
|
event.Skip();
|
|
return RefreshCode::RefreshNone;
|
|
}
|
|
|
|
unsigned TrackPanelCell::KeyUp(wxKeyEvent &event, ViewInfo &, wxWindow *)
|
|
{
|
|
event.Skip();
|
|
return RefreshCode::RefreshNone;
|
|
}
|
|
|
|
unsigned TrackPanelCell::Char(wxKeyEvent &event, ViewInfo &, wxWindow *)
|
|
{
|
|
event.Skip();
|
|
return RefreshCode::RefreshNone;
|
|
}
|
|
|
|
IsVisibleTrack::IsVisibleTrack(AudacityProject *project)
|
|
: mPanelRect {
|
|
wxPoint{ 0, ViewInfo::Get( *project ).vpos },
|
|
project->GetTrackPanel()->GetTracksUsableArea()
|
|
}
|
|
{}
|
|
|
|
bool IsVisibleTrack::operator () (const Track *pTrack) const
|
|
{
|
|
// Need to return true if this track or a later channel intersects
|
|
// the view
|
|
return
|
|
TrackList::Channels(pTrack).StartingWith(pTrack).any_of(
|
|
[this]( const Track *pT ) {
|
|
wxRect r(0, pT->GetY(), 1, pT->GetHeight());
|
|
return r.Intersects(mPanelRect);
|
|
}
|
|
);
|
|
}
|