1863 lines
59 KiB
C++
1863 lines
59 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
ProjectWindow.cpp
|
|
|
|
Paul Licameli split from AudacityProject.cpp
|
|
|
|
**********************************************************************/
|
|
|
|
#include "ProjectWindow.h"
|
|
|
|
|
|
|
|
#include "AllThemeResources.h"
|
|
#include "AudioIO.h"
|
|
#include "Menus.h"
|
|
#include "Project.h"
|
|
#include "ProjectAudioIO.h"
|
|
#include "ProjectStatus.h"
|
|
#include "RefreshCode.h"
|
|
#include "TrackPanelMouseEvent.h"
|
|
#include "TrackPanelAx.h"
|
|
#include "UndoManager.h"
|
|
#include "ViewInfo.h"
|
|
#include "WaveClip.h"
|
|
#include "WaveTrack.h"
|
|
#include "prefs/ThemePrefs.h"
|
|
#include "prefs/TracksPrefs.h"
|
|
#include "toolbars/ToolManager.h"
|
|
#include "tracks/ui/Scrubbing.h"
|
|
#include "tracks/ui/TrackView.h"
|
|
#include "widgets/wxPanelWrapper.h"
|
|
#include "widgets/WindowAccessible.h"
|
|
|
|
#include <wx/display.h>
|
|
#include <wx/scrolbar.h>
|
|
#include <wx/sizer.h>
|
|
|
|
// Returns the screen containing a rectangle, or -1 if none does.
|
|
int ScreenContaining( wxRect & r ){
|
|
unsigned int n = wxDisplay::GetCount();
|
|
for(unsigned int i = 0;i<n;i++){
|
|
wxDisplay d(i);
|
|
wxRect scr = d.GetClientArea();
|
|
if( scr.Contains( r ) )
|
|
return (int)i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// true IFF TL and BR corners are on a connected display.
|
|
// Does not need to check all four. We just need to check that
|
|
// the window probably is straddling screens in a sensible way.
|
|
// If the user wants to use mixed landscape and portrait, they can.
|
|
bool CornersOnScreen( wxRect & r ){
|
|
if( wxDisplay::GetFromPoint( r.GetTopLeft() ) == wxNOT_FOUND) return false;
|
|
if( wxDisplay::GetFromPoint( r.GetBottomRight() ) == wxNOT_FOUND) return false;
|
|
return true;
|
|
}
|
|
|
|
// true iff we have enough of the top bar to be able to reposition the window.
|
|
bool IsWindowAccessible(wxRect *requestedRect)
|
|
{
|
|
wxDisplay display;
|
|
wxRect targetTitleRect(requestedRect->GetLeftTop(), requestedRect->GetBottomRight());
|
|
// Hackery to approximate a window top bar size from a window size.
|
|
// and exclude the open/close and borders.
|
|
targetTitleRect.x += 15;
|
|
targetTitleRect.width -= 100;
|
|
if (targetTitleRect.width < 165) targetTitleRect.width = 165;
|
|
targetTitleRect.height = 15;
|
|
int targetBottom = targetTitleRect.GetBottom();
|
|
int targetRight = targetTitleRect.GetRight();
|
|
// This looks like overkill to check each and every pixel in the ranges.
|
|
// and decide that if any is visible on screen we are OK.
|
|
for (int i = targetTitleRect.GetLeft(); i < targetRight; i++) {
|
|
for (int j = targetTitleRect.GetTop(); j < targetBottom; j++) {
|
|
int monitor = display.GetFromPoint(wxPoint(i, j));
|
|
if (monitor != wxNOT_FOUND) {
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
// BG: The default size and position of the first window
|
|
void GetDefaultWindowRect(wxRect *defRect)
|
|
{
|
|
*defRect = wxGetClientDisplayRect();
|
|
|
|
int width = 940;
|
|
int height = 674;
|
|
|
|
//These conditional values assist in improving placement and size
|
|
//of NEW windows on different platforms.
|
|
#ifdef __WXGTK__
|
|
height += 20;
|
|
#endif
|
|
|
|
#ifdef __WXMSW__
|
|
height += 40;
|
|
#endif
|
|
|
|
#ifdef __WXMAC__
|
|
height += 55;
|
|
#endif
|
|
|
|
// Use screen size where it is smaller than the values we would like.
|
|
// Otherwise use the values we would like, and centred.
|
|
if (width < defRect->width)
|
|
{
|
|
defRect->x = (defRect->width - width)/2;
|
|
defRect->width = width;
|
|
}
|
|
|
|
if (height < defRect->height)
|
|
{
|
|
defRect->y = (defRect->height - height)/2;
|
|
// Bug 1119 workaround
|
|
// Small adjustment for very small Mac screens.
|
|
// If there is only a tiny space at the top
|
|
// then instead of vertical centre, align to bottom.
|
|
const int pixelsFormenu = 60;
|
|
if( defRect->y < pixelsFormenu )
|
|
defRect->y *=2;
|
|
defRect->height = height;
|
|
}
|
|
}
|
|
|
|
// BG: Calculate where to place the next window (could be the first window)
|
|
// BG: Does not store X and Y in prefs. This is intentional.
|
|
//
|
|
// LL: This should NOT need to be this complicated...FIXME
|
|
void GetNextWindowPlacement(wxRect *nextRect, bool *pMaximized, bool *pIconized)
|
|
{
|
|
int inc = 25;
|
|
|
|
wxRect defaultRect;
|
|
GetDefaultWindowRect(&defaultRect);
|
|
|
|
gPrefs->Read(wxT("/Window/Maximized"), pMaximized, false);
|
|
gPrefs->Read(wxT("/Window/Iconized"), pIconized, false);
|
|
|
|
wxRect windowRect;
|
|
gPrefs->Read(wxT("/Window/X"), &windowRect.x, defaultRect.x);
|
|
gPrefs->Read(wxT("/Window/Y"), &windowRect.y, defaultRect.y);
|
|
gPrefs->Read(wxT("/Window/Width"), &windowRect.width, defaultRect.width);
|
|
gPrefs->Read(wxT("/Window/Height"), &windowRect.height, defaultRect.height);
|
|
|
|
wxRect normalRect;
|
|
gPrefs->Read(wxT("/Window/Normal_X"), &normalRect.x, defaultRect.x);
|
|
gPrefs->Read(wxT("/Window/Normal_Y"), &normalRect.y, defaultRect.y);
|
|
gPrefs->Read(wxT("/Window/Normal_Width"), &normalRect.width, defaultRect.width);
|
|
gPrefs->Read(wxT("/Window/Normal_Height"), &normalRect.height, defaultRect.height);
|
|
|
|
// Workaround 2.1.1 and earlier bug on OSX...affects only normalRect, but let's just
|
|
// validate for all rects and plats
|
|
if (normalRect.width == 0 || normalRect.height == 0) {
|
|
normalRect = defaultRect;
|
|
}
|
|
if (windowRect.width == 0 || windowRect.height == 0) {
|
|
windowRect = defaultRect;
|
|
}
|
|
|
|
|
|
wxRect screenRect( wxGetClientDisplayRect());
|
|
#if defined(__WXMAC__)
|
|
|
|
// On OSX, the top of the window should never be less than the menu height,
|
|
// so something is amiss if it is
|
|
if (normalRect.y < screenRect.y) {
|
|
normalRect = defaultRect;
|
|
}
|
|
if (windowRect.y < screenRect.y) {
|
|
windowRect = defaultRect;
|
|
}
|
|
#endif
|
|
|
|
// IF projects empty, THEN it's the first window.
|
|
// It lands where the config says it should, and can straddle screen.
|
|
if (AllProjects{}.empty()) {
|
|
if (*pMaximized || *pIconized) {
|
|
*nextRect = normalRect;
|
|
}
|
|
else {
|
|
*nextRect = windowRect;
|
|
}
|
|
// Resize, for example if one monitor that was on is now off.
|
|
if (!CornersOnScreen( wxRect(*nextRect).Deflate( 32, 32 ))) {
|
|
*nextRect = defaultRect;
|
|
}
|
|
if (!IsWindowAccessible(nextRect)) {
|
|
*nextRect = defaultRect;
|
|
}
|
|
// Do not trim the first project window down.
|
|
// All corners are on screen (or almost so), and
|
|
// the rect may straddle screens.
|
|
return;
|
|
}
|
|
|
|
|
|
// ELSE a subsequent NEW window. It will NOT straddle screens.
|
|
|
|
// We don't mind being 32 pixels off the screen in any direction.
|
|
// Make sure initial sizes (pretty much) fit within the display bounds
|
|
// We used to trim the sizes which could result in ridiculously small windows.
|
|
// contributing to bug 1243.
|
|
// Now instead if the window significantly doesn't fit the screen, we use the default
|
|
// window instead, which we know does.
|
|
if (ScreenContaining( wxRect(normalRect).Deflate( 32, 32 ))<0) {
|
|
normalRect = defaultRect;
|
|
}
|
|
if (ScreenContaining( wxRect(windowRect).Deflate( 32, 32 ) )<0) {
|
|
windowRect = defaultRect;
|
|
}
|
|
|
|
bool validWindowSize = false;
|
|
ProjectWindow * validProject = NULL;
|
|
for ( auto iter = AllProjects{}.rbegin(), end = AllProjects{}.rend();
|
|
iter != end; ++iter
|
|
) {
|
|
auto pProject = *iter;
|
|
if (!GetProjectFrame( *pProject ).IsIconized()) {
|
|
validWindowSize = true;
|
|
validProject = &ProjectWindow::Get( *pProject );
|
|
break;
|
|
}
|
|
}
|
|
if (validWindowSize) {
|
|
*nextRect = validProject->GetRect();
|
|
*pMaximized = validProject->IsMaximized();
|
|
*pIconized = validProject->IsIconized();
|
|
// Do not straddle screens.
|
|
if (ScreenContaining( wxRect(*nextRect).Deflate( 32, 32 ) )<0) {
|
|
*nextRect = defaultRect;
|
|
}
|
|
}
|
|
else {
|
|
*nextRect = normalRect;
|
|
}
|
|
|
|
//Placement depends on the increments
|
|
nextRect->x += inc;
|
|
nextRect->y += inc;
|
|
|
|
// defaultrect is a rectangle on the first screen. It's the right fallback to
|
|
// use most of the time if things are not working out right with sizing.
|
|
// windowRect is a saved rectangle size.
|
|
// normalRect seems to be a substitute for windowRect when iconized or maximised.
|
|
|
|
// Windows can say that we are off screen when actually we are not.
|
|
// On Windows 10 I am seeing miscalculation by about 6 pixels.
|
|
// To fix this we allow some sloppiness on the edge being counted as off screen.
|
|
// This matters most when restoring very carefully sized windows that are maximised
|
|
// in one dimension (height or width) but not both.
|
|
const int edgeSlop = 10;
|
|
|
|
// Next four lines are getting the rectangle for the screen that contains the
|
|
// top left corner of nextRect (and defaulting to rect of screen 0 otherwise).
|
|
wxPoint p = nextRect->GetLeftTop();
|
|
int scr = std::max( 0, wxDisplay::GetFromPoint( p ));
|
|
wxDisplay d( scr );
|
|
screenRect = d.GetClientArea();
|
|
|
|
// Now we (possibly) start trimming our rectangle down.
|
|
// Have we hit the right side of the screen?
|
|
wxPoint bottomRight = nextRect->GetBottomRight();
|
|
if (bottomRight.x > (screenRect.GetRight()+edgeSlop)) {
|
|
int newWidth = screenRect.GetWidth() - nextRect->GetLeft();
|
|
if (newWidth < defaultRect.GetWidth()) {
|
|
nextRect->x = windowRect.x;
|
|
nextRect->y = windowRect.y;
|
|
nextRect->width = windowRect.width;
|
|
}
|
|
else {
|
|
nextRect->width = newWidth;
|
|
}
|
|
}
|
|
|
|
// Have we hit the bottom of the screen?
|
|
bottomRight = nextRect->GetBottomRight();
|
|
if (bottomRight.y > (screenRect.GetBottom()+edgeSlop)) {
|
|
nextRect->y -= inc;
|
|
bottomRight = nextRect->GetBottomRight();
|
|
if (bottomRight.y > (screenRect.GetBottom()+edgeSlop)) {
|
|
nextRect->SetBottom(screenRect.GetBottom());
|
|
}
|
|
}
|
|
|
|
// After all that we could have a window that does not have a visible
|
|
// top bar. [It is unlikely, but something might have gone wrong]
|
|
// If so, use the safe fallback size.
|
|
if (!IsWindowAccessible(nextRect)) {
|
|
*nextRect = defaultRect;
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
// This wrapper prevents the scrollbars from retaining focus after being
|
|
// used. Otherwise, the only way back to the track panel is to click it
|
|
// and that causes your original location to be lost.
|
|
class ScrollBar final : public wxScrollBar
|
|
{
|
|
public:
|
|
ScrollBar(wxWindow* parent, wxWindowID id, long style)
|
|
: wxScrollBar(parent, id, wxDefaultPosition, wxDefaultSize, style)
|
|
{
|
|
}
|
|
|
|
void OnSetFocus(wxFocusEvent & e)
|
|
{
|
|
wxWindow *w = e.GetWindow();
|
|
if (w != NULL) {
|
|
w->SetFocus();
|
|
}
|
|
}
|
|
|
|
void SetScrollbar(int position, int thumbSize,
|
|
int range, int pageSize,
|
|
bool refresh = true) override;
|
|
|
|
private:
|
|
DECLARE_EVENT_TABLE()
|
|
};
|
|
|
|
void ScrollBar::SetScrollbar(int position, int thumbSize,
|
|
int range, int pageSize,
|
|
bool refresh)
|
|
{
|
|
// Mitigate flashing of scrollbars by refreshing only when something really changes.
|
|
|
|
// PRL: This may have been made unnecessary by other fixes for flashing, see
|
|
// commit ac05b190bee7dd0000bce56edb0e5e26185c972f
|
|
|
|
auto changed =
|
|
position != GetThumbPosition() ||
|
|
thumbSize != GetThumbSize() ||
|
|
range != GetRange() ||
|
|
pageSize != GetPageSize();
|
|
if (!changed)
|
|
return;
|
|
|
|
wxScrollBar::SetScrollbar(position, thumbSize, range, pageSize, refresh);
|
|
}
|
|
|
|
BEGIN_EVENT_TABLE(ScrollBar, wxScrollBar)
|
|
EVT_SET_FOCUS(ScrollBar::OnSetFocus)
|
|
END_EVENT_TABLE()
|
|
|
|
// Common mouse wheel handling in track panel cells, moved here to avoid
|
|
// compilation dependencies on Track, TrackPanel, and Scrubbing at low levels
|
|
// which made cycles
|
|
static struct MouseWheelHandler {
|
|
|
|
MouseWheelHandler()
|
|
{
|
|
CommonTrackPanelCell::InstallMouseWheelHook( *this );
|
|
}
|
|
|
|
// Need a bit of memory from one call to the next
|
|
mutable double mVertScrollRemainder = 0.0;
|
|
|
|
unsigned operator()
|
|
( const TrackPanelMouseEvent &evt, AudacityProject *pProject ) const
|
|
{
|
|
using namespace RefreshCode;
|
|
|
|
if ( TrackList::Get( *pProject ).empty() )
|
|
// Scrolling and Zoom in and out commands are disabled when there are no tracks.
|
|
// This should be disabled too for consistency. Otherwise
|
|
// you do see changes in the time ruler.
|
|
return Cancelled;
|
|
|
|
unsigned result = RefreshAll;
|
|
const wxMouseEvent &event = evt.event;
|
|
auto &viewInfo = ViewInfo::Get( *pProject );
|
|
Scrubber &scrubber = Scrubber::Get( *pProject );
|
|
auto &window = ProjectWindow::Get( *pProject );
|
|
const auto steps = evt.steps;
|
|
|
|
if (event.ShiftDown()
|
|
// Don't pan during smooth scrolling. That would conflict with keeping
|
|
// the play indicator centered.
|
|
&& !scrubber.IsScrollScrubbing()
|
|
)
|
|
{
|
|
// MM: Scroll left/right when used with Shift key down
|
|
window.TP_ScrollWindow(
|
|
viewInfo.OffsetTimeByPixels(
|
|
viewInfo.PositionToTime(0), 50.0 * -steps));
|
|
}
|
|
else if (event.CmdDown())
|
|
{
|
|
#if 0
|
|
// JKC: Alternative scroll wheel zooming code
|
|
// using AudacityProject zooming, which is smarter,
|
|
// it keeps selections on screen and centred if it can,
|
|
// also this ensures mousewheel and zoom buttons give same result.
|
|
double ZoomFactor = pow(2.0, steps);
|
|
AudacityProject *p = GetProject();
|
|
if( steps > 0 )
|
|
// PRL: Track panel refresh may be needed if you reenable this
|
|
// code, but we don't want this file dependent on TrackPanel.cpp
|
|
p->ZoomInByFactor( ZoomFactor );
|
|
else
|
|
p->ZoomOutByFactor( ZoomFactor );
|
|
#endif
|
|
// MM: Zoom in/out when used with Control key down
|
|
// We're converting pixel positions to times,
|
|
// counting pixels from the left edge of the track.
|
|
int trackLeftEdge = viewInfo.GetLeftOffset();
|
|
|
|
// Time corresponding to mouse position
|
|
wxCoord xx;
|
|
double center_h;
|
|
double mouse_h = viewInfo.PositionToTime(event.m_x, trackLeftEdge);
|
|
|
|
// Scrubbing? Expand or contract about the center, ignoring mouse position
|
|
if (scrubber.IsScrollScrubbing())
|
|
center_h = viewInfo.h +
|
|
(viewInfo.GetScreenEndTime() - viewInfo.h) / 2.0;
|
|
// Zooming out? Focus on mouse.
|
|
else if( steps <= 0 )
|
|
center_h = mouse_h;
|
|
// No Selection? Focus on mouse.
|
|
else if((viewInfo.selectedRegion.t1() - viewInfo.selectedRegion.t0() ) < 0.00001 )
|
|
center_h = mouse_h;
|
|
// Before Selection? Focus on left
|
|
else if( mouse_h < viewInfo.selectedRegion.t0() )
|
|
center_h = viewInfo.selectedRegion.t0();
|
|
// After Selection? Focus on right
|
|
else if( mouse_h > viewInfo.selectedRegion.t1() )
|
|
center_h = viewInfo.selectedRegion.t1();
|
|
// Inside Selection? Focus on mouse
|
|
else
|
|
center_h = mouse_h;
|
|
|
|
xx = viewInfo.TimeToPosition(center_h, trackLeftEdge);
|
|
|
|
// Time corresponding to last (most far right) audio.
|
|
double audioEndTime = TrackList::Get( *pProject ).GetEndTime();
|
|
|
|
// Disabled this code to fix Bug 1923 (tricky to wheel-zoom right of waveform).
|
|
#if 0
|
|
// When zooming in empty space, it's easy to 'lose' the waveform.
|
|
// This prevents it.
|
|
// IF zooming in
|
|
if (steps > 0)
|
|
{
|
|
// IF mouse is to right of audio
|
|
if (center_h > audioEndTime)
|
|
// Zooming brings far right of audio to mouse.
|
|
center_h = audioEndTime;
|
|
}
|
|
#endif
|
|
|
|
wxCoord xTrackEnd = viewInfo.TimeToPosition( audioEndTime );
|
|
viewInfo.ZoomBy(pow(2.0, steps));
|
|
|
|
double new_center_h = viewInfo.PositionToTime(xx, trackLeftEdge);
|
|
viewInfo.h += (center_h - new_center_h);
|
|
|
|
// If wave has gone off screen, bring it back.
|
|
// This means that the end of the track stays where it was.
|
|
if( viewInfo.h > audioEndTime )
|
|
viewInfo.h += audioEndTime - viewInfo.PositionToTime( xTrackEnd );
|
|
|
|
|
|
result |= FixScrollbars;
|
|
}
|
|
else
|
|
{
|
|
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
|
|
if (scrubber.IsScrubbing()) {
|
|
scrubber.HandleScrollWheel(steps);
|
|
evt.event.Skip(false);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
// MM: Scroll up/down when used without modifier keys
|
|
double lines = steps * 4 + mVertScrollRemainder;
|
|
mVertScrollRemainder = lines - floor(lines);
|
|
lines = floor(lines);
|
|
auto didSomething = window.TP_ScrollUpDown((int)-lines);
|
|
if (!didSomething)
|
|
result |= Cancelled;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
} sMouseWheelHandler;
|
|
|
|
AudacityProject::AttachedWindows::RegisteredFactory sProjectWindowKey{
|
|
[]( AudacityProject &parent ) -> wxWeakRef< wxWindow > {
|
|
wxRect wndRect;
|
|
bool bMaximized = false;
|
|
bool bIconized = false;
|
|
GetNextWindowPlacement(&wndRect, &bMaximized, &bIconized);
|
|
|
|
auto pWindow = safenew ProjectWindow(
|
|
nullptr, -1,
|
|
wxDefaultPosition,
|
|
wxSize(wndRect.width, wndRect.height),
|
|
parent
|
|
);
|
|
|
|
auto &window = *pWindow;
|
|
// wxGTK3 seems to need to require creating the window using default position
|
|
// and then manually positioning it.
|
|
window.SetPosition(wndRect.GetPosition());
|
|
|
|
if(bMaximized) {
|
|
window.Maximize(true);
|
|
}
|
|
else if (bIconized) {
|
|
// if the user close down and iconized state we could start back up and iconized state
|
|
// window.Iconize(TRUE);
|
|
}
|
|
|
|
return pWindow;
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
ProjectWindow &ProjectWindow::Get( AudacityProject &project )
|
|
{
|
|
return project.AttachedWindows::Get< ProjectWindow >( sProjectWindowKey );
|
|
}
|
|
|
|
const ProjectWindow &ProjectWindow::Get( const AudacityProject &project )
|
|
{
|
|
return Get( const_cast< AudacityProject & >( project ) );
|
|
}
|
|
|
|
ProjectWindow *ProjectWindow::Find( AudacityProject *pProject )
|
|
{
|
|
return pProject
|
|
? pProject->AttachedWindows::Find< ProjectWindow >( sProjectWindowKey )
|
|
: nullptr;
|
|
}
|
|
|
|
const ProjectWindow *ProjectWindow::Find( const AudacityProject *pProject )
|
|
{
|
|
return Find( const_cast< AudacityProject * >( pProject ) );
|
|
}
|
|
|
|
int ProjectWindow::NextWindowID()
|
|
{
|
|
return mNextWindowID++;
|
|
}
|
|
|
|
enum {
|
|
FirstID = 1000,
|
|
|
|
// Window controls
|
|
|
|
HSBarID,
|
|
VSBarID,
|
|
|
|
NextID,
|
|
};
|
|
|
|
//If you want any of these files, ask JKC. They are not
|
|
//yet checked in to Audacity SVN as of 12-Feb-2010
|
|
#ifdef EXPERIMENTAL_NOTEBOOK
|
|
#include "GuiFactory.h"
|
|
#include "APanel.h"
|
|
#endif
|
|
|
|
ProjectWindow::ProjectWindow(wxWindow * parent, wxWindowID id,
|
|
const wxPoint & pos,
|
|
const wxSize & size, AudacityProject &project)
|
|
: ProjectWindowBase{ parent, id, pos, size, project }
|
|
{
|
|
mNextWindowID = NextID;
|
|
|
|
// Two sub-windows need to be made before Init(),
|
|
// so that this constructor can complete, and then TrackPanel and
|
|
// AdornedRulerPanel can retrieve those windows from this in their
|
|
// factory functions
|
|
|
|
// PRL: this panel groups the top tool dock and the ruler into one
|
|
// tab cycle.
|
|
// Must create it with non-default width equal to the main window width,
|
|
// or else the device toolbar doesn't make initial widths of the choice
|
|
// controls correct.
|
|
mTopPanel = safenew wxPanelWrapper {
|
|
this, wxID_ANY, wxDefaultPosition,
|
|
wxSize{ this->GetSize().GetWidth(), -1 }
|
|
};
|
|
mTopPanel->SetLabel( "Top Panel" );// Not localised
|
|
mTopPanel->SetLayoutDirection(wxLayout_LeftToRight);
|
|
mTopPanel->SetAutoLayout(true);
|
|
#ifdef EXPERIMENTAL_DA2
|
|
mTopPanel->SetBackgroundColour(theTheme.Colour( clrMedium ));
|
|
#endif
|
|
|
|
wxWindow * pPage;
|
|
|
|
#ifdef EXPERIMENTAL_NOTEBOOK
|
|
// We are using a notebook (tabbed panel), so we create the notebook and add pages.
|
|
GuiFactory Factory;
|
|
wxNotebook * pNotebook;
|
|
mMainPanel = Factory.AddPanel(
|
|
this, wxPoint( left, top ), wxSize( width, height ) );
|
|
pNotebook = Factory.AddNotebook( mMainPanel );
|
|
/* i18n-hint: This is an experimental feature where the main panel in
|
|
Audacity is put on a notebook tab, and this is the name on that tab.
|
|
Other tabs in that notebook may have instruments, patch panels etc.*/
|
|
pPage = Factory.AddPage( pNotebook, _("Main Mix"));
|
|
#else
|
|
// Not using a notebook, so we place the track panel inside another panel,
|
|
// this keeps the notebook code and normal code consistent and also
|
|
// paves the way for adding additional windows inside the track panel.
|
|
mMainPanel = safenew wxPanelWrapper(this, -1,
|
|
wxDefaultPosition,
|
|
wxDefaultSize,
|
|
wxNO_BORDER);
|
|
mMainPanel->SetSizer( safenew wxBoxSizer(wxVERTICAL) );
|
|
mMainPanel->SetLabel("Main Panel");// Not localised.
|
|
pPage = mMainPanel;
|
|
// Set the colour here to the track panel background to avoid
|
|
// flicker when Audacity starts up.
|
|
// However, that leads to areas next to the horizontal scroller
|
|
// being painted in background colour and not scroller background
|
|
// colour, so suppress this for now.
|
|
//pPage->SetBackgroundColour( theTheme.Colour( clrDark ));
|
|
#endif
|
|
pPage->SetLayoutDirection(wxLayout_LeftToRight);
|
|
|
|
#ifdef EXPERIMENTAL_DA2
|
|
pPage->SetBackgroundColour(theTheme.Colour( clrMedium ));
|
|
#endif
|
|
|
|
mMainPage = pPage;
|
|
|
|
mPlaybackScroller = std::make_unique<PlaybackScroller>( &project );
|
|
|
|
// PRL: Old comments below. No longer observing the ordering that it
|
|
// recommends. ProjectWindow::OnActivate puts the focus directly into
|
|
// the TrackPanel, which avoids the problems.
|
|
// LLL: When Audacity starts or becomes active after returning from
|
|
// another application, the first window that can accept focus
|
|
// will be given the focus even if we try to SetFocus(). By
|
|
// creating the scrollbars after the TrackPanel, we resolve
|
|
// several focus problems.
|
|
|
|
mHsbar = safenew ScrollBar(pPage, HSBarID, wxSB_HORIZONTAL);
|
|
mVsbar = safenew ScrollBar(pPage, VSBarID, wxSB_VERTICAL);
|
|
#if wxUSE_ACCESSIBILITY
|
|
// so that name can be set on a standard control
|
|
mHsbar->SetAccessible(safenew WindowAccessible(mHsbar));
|
|
mVsbar->SetAccessible(safenew WindowAccessible(mVsbar));
|
|
#endif
|
|
mHsbar->SetLayoutDirection(wxLayout_LeftToRight);
|
|
mHsbar->SetName(_("Horizontal Scrollbar"));
|
|
mVsbar->SetName(_("Vertical Scrollbar"));
|
|
|
|
project.Bind( EVT_UNDO_MODIFIED, &ProjectWindow::OnUndoPushedModified, this );
|
|
project.Bind( EVT_UNDO_PUSHED, &ProjectWindow::OnUndoPushedModified, this );
|
|
project.Bind( EVT_UNDO_OR_REDO, &ProjectWindow::OnUndoRedo, this );
|
|
project.Bind( EVT_UNDO_RESET, &ProjectWindow::OnUndoReset, this );
|
|
|
|
wxTheApp->Bind(EVT_THEME_CHANGE, &ProjectWindow::OnThemeChange, this);
|
|
}
|
|
|
|
ProjectWindow::~ProjectWindow()
|
|
{
|
|
// Tool manager gives us capture sometimes
|
|
if(HasCapture())
|
|
ReleaseMouse();
|
|
}
|
|
|
|
BEGIN_EVENT_TABLE(ProjectWindow, wxFrame)
|
|
EVT_MENU(wxID_ANY, ProjectWindow::OnMenu)
|
|
EVT_MOUSE_EVENTS(ProjectWindow::OnMouseEvent)
|
|
EVT_CLOSE(ProjectWindow::OnCloseWindow)
|
|
EVT_SIZE(ProjectWindow::OnSize)
|
|
EVT_SHOW(ProjectWindow::OnShow)
|
|
EVT_ICONIZE(ProjectWindow::OnIconize)
|
|
EVT_MOVE(ProjectWindow::OnMove)
|
|
EVT_ACTIVATE(ProjectWindow::OnActivate)
|
|
EVT_COMMAND_SCROLL_LINEUP(HSBarID, ProjectWindow::OnScrollLeftButton)
|
|
EVT_COMMAND_SCROLL_LINEDOWN(HSBarID, ProjectWindow::OnScrollRightButton)
|
|
EVT_COMMAND_SCROLL(HSBarID, ProjectWindow::OnScroll)
|
|
EVT_COMMAND_SCROLL(VSBarID, ProjectWindow::OnScroll)
|
|
// Fires for menu with ID #1...first menu defined
|
|
EVT_UPDATE_UI(1, ProjectWindow::OnUpdateUI)
|
|
EVT_COMMAND(wxID_ANY, EVT_TOOLBAR_UPDATED, ProjectWindow::OnToolBarUpdate)
|
|
//mchinen:multithreaded calls - may not be threadsafe with CommandEvent: may have to change.
|
|
END_EVENT_TABLE()
|
|
|
|
void ProjectWindow::ApplyUpdatedTheme()
|
|
{
|
|
auto &project = mProject;
|
|
SetBackgroundColour(theTheme.Colour( clrMedium ));
|
|
ClearBackground();// For wxGTK.
|
|
}
|
|
|
|
void ProjectWindow::RedrawProject(const bool bForceWaveTracks /*= false*/)
|
|
{
|
|
auto pThis = wxWeakRef<ProjectWindow>(this);
|
|
CallAfter( [pThis, bForceWaveTracks]{
|
|
|
|
if (!pThis)
|
|
return;
|
|
if (pThis->IsBeingDeleted())
|
|
return;
|
|
|
|
auto &project = pThis->mProject ;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackPanel = GetProjectPanel( project );
|
|
pThis->FixScrollbars();
|
|
if (bForceWaveTracks)
|
|
{
|
|
for ( auto pWaveTrack : tracks.Any< WaveTrack >() )
|
|
for (const auto &clip: pWaveTrack->GetClips())
|
|
clip->MarkChanged();
|
|
}
|
|
trackPanel.Refresh(false);
|
|
|
|
});
|
|
}
|
|
|
|
void ProjectWindow::OnThemeChange(wxCommandEvent& evt)
|
|
{
|
|
evt.Skip();
|
|
auto &project = mProject;
|
|
this->ApplyUpdatedTheme();
|
|
auto &toolManager = ToolManager::Get( project );
|
|
for( int ii = 0; ii < ToolBarCount; ++ii )
|
|
{
|
|
ToolBar *pToolBar = toolManager.GetToolBar(ii);
|
|
if( pToolBar )
|
|
pToolBar->ReCreateButtons();
|
|
}
|
|
}
|
|
|
|
void ProjectWindow::UpdatePrefs()
|
|
{
|
|
// Update status bar widths in case of language change
|
|
UpdateStatusWidths();
|
|
}
|
|
|
|
void ProjectWindow::FinishAutoScroll()
|
|
{
|
|
// Set a flag so we don't have to generate two update events
|
|
mAutoScrolling = true;
|
|
|
|
// Call our Scroll method which updates our ViewInfo variables
|
|
// to reflect the positions of the scrollbars
|
|
DoScroll();
|
|
|
|
mAutoScrolling = false;
|
|
}
|
|
|
|
#if defined(__WXMAC__)
|
|
// const int sbarSpaceWidth = 15;
|
|
// const int sbarControlWidth = 16;
|
|
// const int sbarExtraLen = 1;
|
|
const int sbarHjump = 30; //STM: This is how far the thumb jumps when the l/r buttons are pressed, or auto-scrolling occurs -- in pixels
|
|
#elif defined(__WXMSW__)
|
|
const int sbarSpaceWidth = 16;
|
|
const int sbarControlWidth = 16;
|
|
const int sbarExtraLen = 0;
|
|
const int sbarHjump = 30; //STM: This is how far the thumb jumps when the l/r buttons are pressed, or auto-scrolling occurs -- in pixels
|
|
#else // wxGTK, wxMOTIF, wxX11
|
|
const int sbarSpaceWidth = 15;
|
|
const int sbarControlWidth = 15;
|
|
const int sbarExtraLen = 0;
|
|
const int sbarHjump = 30; //STM: This is how far the thumb jumps when the l/r buttons are pressed, or auto-scrolling occurs -- in pixels
|
|
#include "AllThemeResources.h"
|
|
#endif
|
|
|
|
// Make sure selection edge is in view
|
|
void ProjectWindow::ScrollIntoView(double pos)
|
|
{
|
|
auto &trackPanel = GetProjectPanel( mProject );
|
|
auto &viewInfo = ViewInfo::Get( mProject );
|
|
auto w = viewInfo.GetTracksUsableWidth();
|
|
|
|
int pixel = viewInfo.TimeToPosition(pos);
|
|
if (pixel < 0 || pixel >= w)
|
|
{
|
|
TP_ScrollWindow
|
|
(viewInfo.OffsetTimeByPixels(pos, -(w / 2)));
|
|
trackPanel.Refresh(false);
|
|
}
|
|
}
|
|
|
|
void ProjectWindow::ScrollIntoView(int x)
|
|
{
|
|
auto &viewInfo = ViewInfo::Get( mProject );
|
|
ScrollIntoView(viewInfo.PositionToTime(x, viewInfo.GetLeftOffset()));
|
|
}
|
|
|
|
///
|
|
/// This method handles general left-scrolling, either for drag-scrolling
|
|
/// or when the scrollbar is clicked to the left of the thumb
|
|
///
|
|
void ProjectWindow::OnScrollLeft()
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
wxInt64 pos = mHsbar->GetThumbPosition();
|
|
// move at least one scroll increment
|
|
pos -= wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1);
|
|
pos = wxMax(pos, 0);
|
|
viewInfo.sbarH -= sbarHjump;
|
|
viewInfo.sbarH = std::max(viewInfo.sbarH,
|
|
-(wxInt64) PixelWidthBeforeTime(0.0));
|
|
|
|
|
|
if (pos != mHsbar->GetThumbPosition()) {
|
|
mHsbar->SetThumbPosition((int)pos);
|
|
FinishAutoScroll();
|
|
}
|
|
}
|
|
///
|
|
/// This method handles general right-scrolling, either for drag-scrolling
|
|
/// or when the scrollbar is clicked to the right of the thumb
|
|
///
|
|
|
|
void ProjectWindow::OnScrollRight()
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
wxInt64 pos = mHsbar->GetThumbPosition();
|
|
// move at least one scroll increment
|
|
// use wxInt64 for calculation to prevent temporary overflow
|
|
pos += wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1);
|
|
wxInt64 max = mHsbar->GetRange() - mHsbar->GetThumbSize();
|
|
pos = wxMin(pos, max);
|
|
viewInfo.sbarH += sbarHjump;
|
|
viewInfo.sbarH = std::min(viewInfo.sbarH,
|
|
viewInfo.sbarTotal
|
|
- (wxInt64) PixelWidthBeforeTime(0.0) - viewInfo.sbarScreen);
|
|
|
|
if (pos != mHsbar->GetThumbPosition()) {
|
|
mHsbar->SetThumbPosition((int)pos);
|
|
FinishAutoScroll();
|
|
}
|
|
}
|
|
|
|
|
|
///
|
|
/// This handles the event when the left direction button on the scrollbar is depressed
|
|
///
|
|
void ProjectWindow::OnScrollLeftButton(wxScrollEvent & /*event*/)
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
wxInt64 pos = mHsbar->GetThumbPosition();
|
|
// move at least one scroll increment
|
|
pos -= wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1);
|
|
pos = wxMax(pos, 0);
|
|
viewInfo.sbarH -= sbarHjump;
|
|
viewInfo.sbarH = std::max(viewInfo.sbarH,
|
|
- (wxInt64) PixelWidthBeforeTime(0.0));
|
|
|
|
if (pos != mHsbar->GetThumbPosition()) {
|
|
mHsbar->SetThumbPosition((int)pos);
|
|
DoScroll();
|
|
}
|
|
}
|
|
|
|
///
|
|
/// This handles the event when the right direction button on the scrollbar is depressed
|
|
///
|
|
void ProjectWindow::OnScrollRightButton(wxScrollEvent & /*event*/)
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
wxInt64 pos = mHsbar->GetThumbPosition();
|
|
// move at least one scroll increment
|
|
// use wxInt64 for calculation to prevent temporary overflow
|
|
pos += wxMax((wxInt64)(sbarHjump * viewInfo.sbarScale), 1);
|
|
wxInt64 max = mHsbar->GetRange() - mHsbar->GetThumbSize();
|
|
pos = wxMin(pos, max);
|
|
viewInfo.sbarH += sbarHjump;
|
|
viewInfo.sbarH = std::min(viewInfo.sbarH,
|
|
viewInfo.sbarTotal
|
|
- (wxInt64) PixelWidthBeforeTime(0.0) - viewInfo.sbarScreen);
|
|
|
|
if (pos != mHsbar->GetThumbPosition()) {
|
|
mHsbar->SetThumbPosition((int)pos);
|
|
DoScroll();
|
|
}
|
|
}
|
|
|
|
|
|
bool ProjectWindow::MayScrollBeyondZero() const
|
|
{
|
|
auto &project = mProject;
|
|
auto &scrubber = Scrubber::Get( project );
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
if (viewInfo.bScrollBeyondZero)
|
|
return true;
|
|
|
|
if (scrubber.HasMark() ||
|
|
ProjectAudioIO::Get( project ).IsAudioActive()) {
|
|
if (mPlaybackScroller) {
|
|
auto mode = mPlaybackScroller->GetMode();
|
|
if (mode == PlaybackScroller::Mode::Pinned ||
|
|
mode == PlaybackScroller::Mode::Right)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
double ProjectWindow::ScrollingLowerBoundTime() const
|
|
{
|
|
auto &project = mProject;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
if (!MayScrollBeyondZero())
|
|
return 0;
|
|
const double screen = viewInfo.GetScreenEndTime() - viewInfo.h;
|
|
return std::min(tracks.GetStartTime(), -screen);
|
|
}
|
|
|
|
// PRL: Bug1197: we seem to need to compute all in double, to avoid differing results on Mac
|
|
// That's why ViewInfo::TimeRangeToPixelWidth was defined, with some regret.
|
|
double ProjectWindow::PixelWidthBeforeTime(double scrollto) const
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
const double lowerBound = ScrollingLowerBoundTime();
|
|
return
|
|
// Ignoring fisheye is correct here
|
|
viewInfo.TimeRangeToPixelWidth(scrollto - lowerBound);
|
|
}
|
|
|
|
void ProjectWindow::SetHorizontalThumb(double scrollto)
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
const auto unscaled = PixelWidthBeforeTime(scrollto);
|
|
const int max = mHsbar->GetRange() - mHsbar->GetThumbSize();
|
|
const int pos =
|
|
std::min(max,
|
|
std::max(0,
|
|
(int)(floor(0.5 + unscaled * viewInfo.sbarScale))));
|
|
mHsbar->SetThumbPosition(pos);
|
|
viewInfo.sbarH = floor(0.5 + unscaled - PixelWidthBeforeTime(0.0));
|
|
viewInfo.sbarH = std::max(viewInfo.sbarH,
|
|
- (wxInt64) PixelWidthBeforeTime(0.0));
|
|
viewInfo.sbarH = std::min(viewInfo.sbarH,
|
|
viewInfo.sbarTotal
|
|
- (wxInt64) PixelWidthBeforeTime(0.0) - viewInfo.sbarScreen);
|
|
}
|
|
|
|
//
|
|
// This method, like the other methods prefaced with TP, handles TrackPanel
|
|
// 'callback'.
|
|
//
|
|
void ProjectWindow::TP_ScrollWindow(double scrollto)
|
|
{
|
|
SetHorizontalThumb(scrollto);
|
|
|
|
// Call our Scroll method which updates our ViewInfo variables
|
|
// to reflect the positions of the scrollbars
|
|
DoScroll();
|
|
}
|
|
|
|
//
|
|
// Scroll vertically. This is called for example by the mouse wheel
|
|
// handler in Track Panel. A positive argument makes the window
|
|
// scroll down, while a negative argument scrolls up.
|
|
//
|
|
bool ProjectWindow::TP_ScrollUpDown(int delta)
|
|
{
|
|
int oldPos = mVsbar->GetThumbPosition();
|
|
int pos = oldPos + delta;
|
|
int max = mVsbar->GetRange() - mVsbar->GetThumbSize();
|
|
|
|
// Can be negative in case of only one track
|
|
if (max < 0)
|
|
max = 0;
|
|
|
|
if (pos > max)
|
|
pos = max;
|
|
else if (pos < 0)
|
|
pos = 0;
|
|
|
|
if (pos != oldPos)
|
|
{
|
|
mVsbar->SetThumbPosition(pos);
|
|
|
|
DoScroll();
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
void ProjectWindow::FixScrollbars()
|
|
{
|
|
auto &project = mProject;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackPanel = GetProjectPanel( project );
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
|
|
bool refresh = false;
|
|
bool rescroll = false;
|
|
|
|
int totalHeight = TrackView::GetTotalHeight( tracks ) + 32;
|
|
|
|
auto panelWidth = viewInfo.GetTracksUsableWidth();
|
|
auto panelHeight = viewInfo.GetHeight();
|
|
|
|
// (From Debian...at least I think this is the change corresponding
|
|
// to this comment)
|
|
//
|
|
// (2.) GTK critical warning "IA__gtk_range_set_range: assertion
|
|
// 'min < max' failed" because of negative numbers as result of window
|
|
// size checking. Added a sanity check that straightens up the numbers
|
|
// in edge cases.
|
|
if (panelWidth < 0) {
|
|
panelWidth = 0;
|
|
}
|
|
if (panelHeight < 0) {
|
|
panelHeight = 0;
|
|
}
|
|
|
|
auto LastTime = std::numeric_limits<double>::lowest();
|
|
for (const Track *track : tracks) {
|
|
// Iterate over pending changed tracks if present.
|
|
track = track->SubstitutePendingChangedTrack().get();
|
|
LastTime = std::max( LastTime, track->GetEndTime() );
|
|
}
|
|
LastTime =
|
|
std::max(LastTime, viewInfo.selectedRegion.t1());
|
|
|
|
const double screen =
|
|
viewInfo.GetScreenEndTime() - viewInfo.h;
|
|
const double halfScreen = screen / 2.0;
|
|
|
|
// If we can scroll beyond zero,
|
|
// Add 1/2 of a screen of blank space to the end
|
|
// and another 1/2 screen before the beginning
|
|
// so that any point within the union of the selection and the track duration
|
|
// may be scrolled to the midline.
|
|
// May add even more to the end, so that you can always scroll the starting time to zero.
|
|
const double lowerBound = ScrollingLowerBoundTime();
|
|
const double additional = MayScrollBeyondZero()
|
|
? -lowerBound + std::max(halfScreen, screen - LastTime)
|
|
: screen / 4.0;
|
|
|
|
viewInfo.total = LastTime + additional;
|
|
|
|
// Don't remove time from total that's still on the screen
|
|
viewInfo.total = std::max(viewInfo.total, viewInfo.h + screen);
|
|
|
|
if (viewInfo.h < lowerBound) {
|
|
viewInfo.h = lowerBound;
|
|
rescroll = true;
|
|
}
|
|
|
|
viewInfo.sbarTotal = (wxInt64) (viewInfo.GetTotalWidth());
|
|
viewInfo.sbarScreen = (wxInt64)(panelWidth);
|
|
viewInfo.sbarH = (wxInt64) (viewInfo.GetBeforeScreenWidth());
|
|
|
|
// PRL: Can someone else find a more elegant solution to bug 812, than
|
|
// introducing this boolean member variable?
|
|
// Setting mVSbar earlier, int HandlXMLTag, didn't succeed in restoring
|
|
// the vertical scrollbar to its saved position. So defer that till now.
|
|
// mbInitializingScrollbar should be true only at the start of the life
|
|
// of an AudacityProject reopened from disk.
|
|
if (!mbInitializingScrollbar) {
|
|
viewInfo.vpos = mVsbar->GetThumbPosition() * viewInfo.scrollStep;
|
|
}
|
|
mbInitializingScrollbar = false;
|
|
|
|
if (viewInfo.vpos >= totalHeight)
|
|
viewInfo.vpos = totalHeight - 1;
|
|
if (viewInfo.vpos < 0)
|
|
viewInfo.vpos = 0;
|
|
|
|
bool oldhstate;
|
|
bool oldvstate;
|
|
bool newhstate =
|
|
(viewInfo.GetScreenEndTime() - viewInfo.h) < viewInfo.total;
|
|
bool newvstate = panelHeight < totalHeight;
|
|
|
|
#ifdef __WXGTK__
|
|
oldhstate = mHsbar->IsShown();
|
|
oldvstate = mVsbar->IsShown();
|
|
mHsbar->Show(newhstate);
|
|
mVsbar->Show(panelHeight < totalHeight);
|
|
#else
|
|
oldhstate = mHsbar->IsEnabled();
|
|
oldvstate = mVsbar->IsEnabled();
|
|
mHsbar->Enable(newhstate);
|
|
mVsbar->Enable(panelHeight < totalHeight);
|
|
#endif
|
|
|
|
if (panelHeight >= totalHeight && viewInfo.vpos != 0) {
|
|
viewInfo.vpos = 0;
|
|
|
|
refresh = true;
|
|
rescroll = false;
|
|
}
|
|
if (!newhstate && viewInfo.sbarH != 0) {
|
|
viewInfo.sbarH = 0;
|
|
|
|
refresh = true;
|
|
rescroll = false;
|
|
}
|
|
|
|
// wxScrollbar only supports int values but we need a greater range, so
|
|
// we scale the scrollbar coordinates on demand. We only do this if we
|
|
// would exceed the int range, so we can always use the maximum resolution
|
|
// available.
|
|
|
|
// Don't use the full 2^31 max int range but a bit less, so rounding
|
|
// errors in calculations do not overflow max int
|
|
wxInt64 maxScrollbarRange = (wxInt64)(2147483647 * 0.999);
|
|
if (viewInfo.sbarTotal > maxScrollbarRange)
|
|
viewInfo.sbarScale = ((double)maxScrollbarRange) / viewInfo.sbarTotal;
|
|
else
|
|
viewInfo.sbarScale = 1.0; // use maximum resolution
|
|
|
|
{
|
|
int scaledSbarH = (int)(viewInfo.sbarH * viewInfo.sbarScale);
|
|
int scaledSbarScreen = (int)(viewInfo.sbarScreen * viewInfo.sbarScale);
|
|
int scaledSbarTotal = (int)(viewInfo.sbarTotal * viewInfo.sbarScale);
|
|
const int offset =
|
|
(int)(floor(0.5 + viewInfo.sbarScale * PixelWidthBeforeTime(0.0)));
|
|
|
|
mHsbar->SetScrollbar(scaledSbarH + offset, scaledSbarScreen, scaledSbarTotal,
|
|
scaledSbarScreen, TRUE);
|
|
}
|
|
|
|
// Vertical scrollbar
|
|
mVsbar->SetScrollbar(viewInfo.vpos / viewInfo.scrollStep,
|
|
panelHeight / viewInfo.scrollStep,
|
|
totalHeight / viewInfo.scrollStep,
|
|
panelHeight / viewInfo.scrollStep, TRUE);
|
|
|
|
if (refresh || (rescroll &&
|
|
(viewInfo.GetScreenEndTime() - viewInfo.h) < viewInfo.total)) {
|
|
trackPanel.Refresh(false);
|
|
}
|
|
|
|
MenuManager::Get( project ).UpdateMenus();
|
|
|
|
if (oldhstate != newhstate || oldvstate != newvstate) {
|
|
UpdateLayout();
|
|
}
|
|
}
|
|
|
|
void ProjectWindow::UpdateLayout()
|
|
{
|
|
auto &project = mProject;
|
|
auto &trackPanel = GetProjectPanel( project );
|
|
auto &toolManager = ToolManager::Get( project );
|
|
|
|
// 1. Layout panel, to get widths of the docks.
|
|
Layout();
|
|
// 2. Layout toolbars to pack the toolbars correctly in docks which
|
|
// are now the correct width.
|
|
toolManager.LayoutToolBars();
|
|
// 3. Layout panel, to resize docks, in particular reducing the height
|
|
// of any empty docks, or increasing the height of docks that need it.
|
|
Layout();
|
|
|
|
// Bug 2455
|
|
// The commented out code below is to calculate a nice minimum size for
|
|
// the window. However on Ubuntu when the window is minimised it leads to
|
|
// an insanely tall window.
|
|
// Using a fixed min size fixes that.
|
|
// However there is still something strange when minimised, as once
|
|
// UpdateLayout is called once, when minimised, it gets called repeatedly.
|
|
#if 0
|
|
// Retrieve size of this projects window
|
|
wxSize mainsz = GetSize();
|
|
|
|
// Retrieve position of the track panel to use as the size of the top
|
|
// third of the window
|
|
wxPoint tppos = ClientToScreen(trackPanel.GetParent()->GetPosition());
|
|
|
|
// Retrieve position of bottom dock to use as the size of the bottom
|
|
// third of the window
|
|
wxPoint sbpos = ClientToScreen(toolManager.GetBotDock()->GetPosition());
|
|
|
|
// The "+ 50" is the minimum height of the TrackPanel
|
|
SetMinSize( wxSize(250, (mainsz.y - sbpos.y) + tppos.y + 50));
|
|
#endif
|
|
SetMinSize( wxSize(250, 250));
|
|
SetMaxSize( wxSize(20000, 20000));
|
|
}
|
|
|
|
void ProjectWindow::HandleResize()
|
|
{
|
|
// Activate events can fire during window teardown, so just
|
|
// ignore them.
|
|
if (mIsDeleting) {
|
|
return;
|
|
}
|
|
|
|
CallAfter( [this]{
|
|
|
|
if (mIsDeleting)
|
|
return;
|
|
|
|
FixScrollbars();
|
|
UpdateLayout();
|
|
|
|
});
|
|
}
|
|
|
|
|
|
bool ProjectWindow::IsIconized() const
|
|
{
|
|
return mIconized;
|
|
}
|
|
|
|
void ProjectWindow::UpdateStatusWidths()
|
|
{
|
|
enum { nWidths = nStatusBarFields + 1 };
|
|
int widths[ nWidths ]{ 0 };
|
|
widths[ rateStatusBarField ] = 150;
|
|
const auto statusBar = GetStatusBar();
|
|
const auto &functions = ProjectStatus::GetStatusWidthFunctions();
|
|
// Start from 1 not 0
|
|
// Specifying a first column always of width 0 was needed for reasons
|
|
// I forget now
|
|
for ( int ii = 1; ii <= nStatusBarFields; ++ii ) {
|
|
int &width = widths[ ii ];
|
|
for ( const auto &function : functions ) {
|
|
auto results =
|
|
function( mProject, static_cast< StatusBarField >( ii ) );
|
|
for ( const auto &string : results.first ) {
|
|
int w;
|
|
statusBar->GetTextExtent(string.Translation(), &w, nullptr);
|
|
width = std::max<int>( width, w + results.second );
|
|
}
|
|
}
|
|
}
|
|
// The main status field is not fixed width
|
|
widths[ mainStatusBarField ] = -1;
|
|
statusBar->SetStatusWidths( nWidths, widths );
|
|
}
|
|
|
|
void ProjectWindow::MacShowUndockedToolbars(bool show)
|
|
{
|
|
(void)show;//compiler food
|
|
#ifdef __WXMAC__
|
|
// Save the focus so we can restore it to whatever had it before since
|
|
// showing a previously hidden toolbar will cause the focus to be set to
|
|
// its frame. If this is not done it will appear that activation events
|
|
// aren't being sent to the project window since they are actually being
|
|
// delivered to the last tool frame shown.
|
|
wxWindow *focused = FindFocus();
|
|
|
|
// Find all the floating toolbars, and show or hide them
|
|
const auto &children = GetChildren();
|
|
for(const auto &child : children) {
|
|
if (auto frame = dynamic_cast<ToolFrame*>(child)) {
|
|
if (!show) {
|
|
frame->Hide();
|
|
}
|
|
else if (frame->GetBar() &&
|
|
frame->GetBar()->IsVisible() ) {
|
|
frame->Show();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restore the focus if needed
|
|
if (focused) {
|
|
focused->SetFocus();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void ProjectWindow::OnIconize(wxIconizeEvent &event)
|
|
{
|
|
//JKC: On Iconizing we get called twice. Don't know
|
|
// why but it does no harm.
|
|
// Should we be returning true/false rather than
|
|
// void return? I don't know.
|
|
mIconized = event.IsIconized();
|
|
|
|
#if defined(__WXMAC__)
|
|
// Readdresses bug 1431 since a crash could occur when restoring iconized
|
|
// floating toolbars due to recursion (bug 2411).
|
|
MacShowUndockedToolbars(!mIconized);
|
|
if( !mIconized )
|
|
{
|
|
Raise();
|
|
}
|
|
#endif
|
|
|
|
// VisibileProjectCount seems to be just a counter for debugging.
|
|
// It's not used outside this function.
|
|
auto VisibleProjectCount = std::count_if(
|
|
AllProjects{}.begin(), AllProjects{}.end(),
|
|
[]( const AllProjects::value_type &ptr ){
|
|
return !GetProjectFrame( *ptr ).IsIconized();
|
|
}
|
|
);
|
|
event.Skip();
|
|
|
|
// This step is to fix part of Bug 2040, where the BackingPanel
|
|
// size was not restored after we leave Iconized state.
|
|
|
|
// Queue up a resize event using OnShow so that we
|
|
// refresh the track panel. But skip this, if we're iconized.
|
|
if( mIconized )
|
|
return;
|
|
wxShowEvent Evt;
|
|
OnShow( Evt );
|
|
}
|
|
|
|
void ProjectWindow::OnMove(wxMoveEvent & event)
|
|
{
|
|
if (!this->IsMaximized() && !this->IsIconized())
|
|
SetNormalizedWindowState(this->GetRect());
|
|
event.Skip();
|
|
}
|
|
|
|
void ProjectWindow::OnSize(wxSizeEvent & event)
|
|
{
|
|
// (From Debian)
|
|
//
|
|
// (3.) GTK critical warning "IA__gdk_window_get_origin: assertion
|
|
// 'GDK_IS_WINDOW (window)' failed": Received events of type wxSizeEvent
|
|
// on the main project window cause calls to "ClientToScreen" - which is
|
|
// not available until the window is first shown. So the class has to
|
|
// keep track of wxShowEvent events and inhibit those actions until the
|
|
// window is first shown.
|
|
if (mShownOnce) {
|
|
HandleResize();
|
|
if (!this->IsMaximized() && !this->IsIconized())
|
|
SetNormalizedWindowState(this->GetRect());
|
|
}
|
|
event.Skip();
|
|
}
|
|
|
|
void ProjectWindow::OnShow(wxShowEvent & event)
|
|
{
|
|
// Remember that the window has been shown at least once
|
|
mShownOnce = true;
|
|
|
|
// (From Debian...see also TrackPanel::OnTimer and AudacityTimer::Notify)
|
|
//
|
|
// Description: Workaround for wxWidgets bug: Reentry in clipboard
|
|
// The wxWidgets bug http://trac.wxwidgets.org/ticket/16636 prevents
|
|
// us from doing clipboard operations in wxShowEvent and wxTimerEvent
|
|
// processing because those event could possibly be processed during
|
|
// the (not sufficiently protected) Yield() of a first clipboard
|
|
// operation, causing reentry. Audacity had a workaround in place
|
|
// for this problem (the class "CaptureEvents"), which however isn't
|
|
// applicable with wxWidgets 3.0 because it's based on changing the
|
|
// gdk event handler, a change that would be overridden by wxWidgets's
|
|
// own gdk event handler change.
|
|
// Instead, as a NEW workaround, specifically protect those processings
|
|
// of wxShowEvent and wxTimerEvent that try to do clipboard operations
|
|
// from being executed within Yield(). This is done by delaying their
|
|
// execution by posting pure wxWidgets events - which are never executed
|
|
// during Yield().
|
|
// Author: Martin Stegh fer <martin@steghoefer.eu>
|
|
// Bug-Debian: https://bugs.debian.org/765341
|
|
|
|
// the actual creation/showing of the window).
|
|
// Post the event instead of calling OnSize(..) directly. This ensures that
|
|
// this is a pure wxWidgets event (no GDK event behind it) and that it
|
|
// therefore isn't processed within the YieldFor(..) of the clipboard
|
|
// operations (workaround for Debian bug #765341).
|
|
// QueueEvent() will take ownership of the event
|
|
GetEventHandler()->QueueEvent(safenew wxSizeEvent(GetSize()));
|
|
|
|
// Further processing by default handlers
|
|
event.Skip();
|
|
}
|
|
|
|
///
|
|
/// A toolbar has been updated, so handle it like a sizing event.
|
|
///
|
|
void ProjectWindow::OnToolBarUpdate(wxCommandEvent & event)
|
|
{
|
|
HandleResize();
|
|
|
|
event.Skip(false); /* No need to propagate any further */
|
|
}
|
|
|
|
void ProjectWindow::OnUndoPushedModified( wxCommandEvent &evt )
|
|
{
|
|
evt.Skip();
|
|
RedrawProject();
|
|
}
|
|
|
|
void ProjectWindow::OnUndoRedo( wxCommandEvent &evt )
|
|
{
|
|
evt.Skip();
|
|
HandleResize();
|
|
RedrawProject();
|
|
}
|
|
|
|
void ProjectWindow::OnUndoReset( wxCommandEvent &evt )
|
|
{
|
|
evt.Skip();
|
|
HandleResize();
|
|
// RedrawProject(); // Should we do this here too?
|
|
}
|
|
|
|
void ProjectWindow::OnScroll(wxScrollEvent & WXUNUSED(event))
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
const wxInt64 offset = PixelWidthBeforeTime(0.0);
|
|
viewInfo.sbarH =
|
|
(wxInt64)(mHsbar->GetThumbPosition() / viewInfo.sbarScale) - offset;
|
|
DoScroll();
|
|
|
|
#ifndef __WXMAC__
|
|
// Bug2179
|
|
// This keeps the time ruler in sync with horizontal scrolling, without
|
|
// making an undesirable compilation dependency of this source file on
|
|
// the ruler
|
|
wxTheApp->ProcessIdle();
|
|
#endif
|
|
}
|
|
|
|
void ProjectWindow::DoScroll()
|
|
{
|
|
auto &project = mProject;
|
|
auto &trackPanel = GetProjectPanel( project );
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
const double lowerBound = ScrollingLowerBoundTime();
|
|
|
|
auto width = viewInfo.GetTracksUsableWidth();
|
|
viewInfo.SetBeforeScreenWidth(viewInfo.sbarH, width, lowerBound);
|
|
|
|
|
|
if (MayScrollBeyondZero()) {
|
|
enum { SCROLL_PIXEL_TOLERANCE = 10 };
|
|
if (std::abs(viewInfo.TimeToPosition(0.0, 0
|
|
)) < SCROLL_PIXEL_TOLERANCE) {
|
|
// Snap the scrollbar to 0
|
|
viewInfo.h = 0;
|
|
SetHorizontalThumb(0.0);
|
|
}
|
|
}
|
|
|
|
viewInfo.vpos = mVsbar->GetThumbPosition() * viewInfo.scrollStep;
|
|
|
|
//mchinen: do not always set this project to be the active one.
|
|
//a project may autoscroll while playing in the background
|
|
//I think this is okay since OnMouseEvent has one of these.
|
|
//SetActiveProject(this);
|
|
|
|
if (!mAutoScrolling) {
|
|
trackPanel.Refresh(false);
|
|
}
|
|
}
|
|
|
|
void ProjectWindow::OnMenu(wxCommandEvent & event)
|
|
{
|
|
#ifdef __WXMSW__
|
|
// Bug 1642: We can arrive here with bogus menu IDs, which we
|
|
// proceed to process. So if bogus, don't.
|
|
// The bogus menu IDs are probably generated by controls on the TrackPanel,
|
|
// such as the Project Rate.
|
|
// 17000 is the magic number at which we start our menu.
|
|
// This code would probably NOT be OK on Mac, since we assign
|
|
// some specific ID numbers.
|
|
if( event.GetId() < 17000){
|
|
event.Skip();
|
|
return;
|
|
}
|
|
#endif
|
|
auto &project = mProject;
|
|
auto &commandManager = CommandManager::Get( project );
|
|
bool handled = commandManager.HandleMenuID( GetProject(),
|
|
event.GetId(), MenuManager::Get( project ).GetUpdateFlags(),
|
|
false);
|
|
|
|
if (handled)
|
|
event.Skip(false);
|
|
else{
|
|
event.ResumePropagation( 999 );
|
|
event.Skip(true);
|
|
}
|
|
}
|
|
|
|
void ProjectWindow::OnUpdateUI(wxUpdateUIEvent & WXUNUSED(event))
|
|
{
|
|
auto &project = mProject;
|
|
MenuManager::Get( project ).UpdateMenus();
|
|
}
|
|
|
|
void ProjectWindow::OnActivate(wxActivateEvent & event)
|
|
{
|
|
// Activate events can fire during window teardown, so just
|
|
// ignore them.
|
|
if (IsBeingDeleted()) {
|
|
return;
|
|
}
|
|
|
|
auto &project = mProject;
|
|
|
|
mActive = event.GetActive();
|
|
|
|
// Under Windows, focus can be "lost" when returning to
|
|
// Audacity from a different application.
|
|
//
|
|
// This was observed by minimizing all windows using WINDOWS+M and
|
|
// then ALT+TAB to return to Audacity. Focus will be given to the
|
|
// project window frame which is not at all useful.
|
|
//
|
|
// So, we use ToolManager's observation of focus changes in a wxEventFilter.
|
|
// Then, when we receive the
|
|
// activate event, we restore that focus to the child or the track
|
|
// panel if no child had the focus (which probably should never happen).
|
|
if (mActive) {
|
|
auto &toolManager = ToolManager::Get( project );
|
|
SetActiveProject( &project );
|
|
if ( ! toolManager.RestoreFocus() )
|
|
GetProjectPanel( project ).SetFocus();
|
|
}
|
|
event.Skip();
|
|
}
|
|
|
|
bool ProjectWindow::IsActive()
|
|
{
|
|
return mActive;
|
|
}
|
|
|
|
void ProjectWindow::OnMouseEvent(wxMouseEvent & event)
|
|
{
|
|
auto &project = mProject;
|
|
if (event.ButtonDown())
|
|
SetActiveProject( &project );
|
|
}
|
|
|
|
void ProjectWindow::ZoomAfterImport(Track *pTrack)
|
|
{
|
|
auto &project = mProject;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &trackPanel = GetProjectPanel( project );
|
|
|
|
DoZoomFit();
|
|
|
|
trackPanel.SetFocus();
|
|
if (!pTrack)
|
|
pTrack = *tracks.Selected().begin();
|
|
if (!pTrack)
|
|
pTrack = *tracks.Any().begin();
|
|
if (pTrack) {
|
|
TrackFocus::Get(project).Set(pTrack);
|
|
pTrack->EnsureVisible();
|
|
}
|
|
}
|
|
|
|
// Utility function called by other zoom methods
|
|
void ProjectWindow::Zoom(double level)
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
viewInfo.SetZoom(level);
|
|
FixScrollbars();
|
|
// See if we can center the selection on screen, and have it actually fit.
|
|
// tOnLeft is the amount of time we would need before the selection left edge to center it.
|
|
float t0 = viewInfo.selectedRegion.t0();
|
|
float t1 = viewInfo.selectedRegion.t1();
|
|
float tAvailable = viewInfo.GetScreenEndTime() - viewInfo.h;
|
|
float tOnLeft = (tAvailable - t0 + t1)/2.0;
|
|
// Bug 1292 (Enh) is effectively a request to do this scrolling of the selection into view.
|
|
// If tOnLeft is positive, then we have room for the selection, so scroll to it.
|
|
if( tOnLeft >=0 )
|
|
TP_ScrollWindow( t0-tOnLeft);
|
|
}
|
|
|
|
// Utility function called by other zoom methods
|
|
void ProjectWindow::ZoomBy(double multiplier)
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
viewInfo.ZoomBy(multiplier);
|
|
FixScrollbars();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// This method 'rewinds' the track, by setting the cursor to 0 and
|
|
// scrolling the window to fit 0 on the left side of it
|
|
// (maintaining current zoom).
|
|
// If shift is held down, it will extend the left edge of the
|
|
// selection to 0 (holding right edge constant), otherwise it will
|
|
// move both left and right edge of selection to 0
|
|
///////////////////////////////////////////////////////////////////
|
|
void ProjectWindow::Rewind(bool shift)
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
viewInfo.selectedRegion.setT0(0, false);
|
|
if (!shift)
|
|
viewInfo.selectedRegion.setT1(0);
|
|
|
|
TP_ScrollWindow(0);
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// This method 'fast-forwards' the track, by setting the cursor to
|
|
// the end of the samples on the selected track and scrolling the
|
|
// window to fit the end on its right side (maintaining current zoom).
|
|
// If shift is held down, it will extend the right edge of the
|
|
// selection to the end (holding left edge constant), otherwise it will
|
|
// move both left and right edge of selection to the end
|
|
///////////////////////////////////////////////////////////////////
|
|
void ProjectWindow::SkipEnd(bool shift)
|
|
{
|
|
auto &project = mProject;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
double len = tracks.GetEndTime();
|
|
|
|
viewInfo.selectedRegion.setT1(len, false);
|
|
if (!shift)
|
|
viewInfo.selectedRegion.setT0(len);
|
|
|
|
// Make sure the end of the track is visible
|
|
ScrollIntoView(len);
|
|
}
|
|
|
|
// TrackPanel callback method
|
|
void ProjectWindow::TP_ScrollLeft()
|
|
{
|
|
OnScrollLeft();
|
|
}
|
|
|
|
// TrackPanel callback method
|
|
void ProjectWindow::TP_ScrollRight()
|
|
{
|
|
OnScrollRight();
|
|
}
|
|
|
|
// TrackPanel callback method
|
|
void ProjectWindow::TP_RedrawScrollbars()
|
|
{
|
|
FixScrollbars();
|
|
}
|
|
|
|
void ProjectWindow::TP_HandleResize()
|
|
{
|
|
HandleResize();
|
|
}
|
|
|
|
ProjectWindow::PlaybackScroller::PlaybackScroller(AudacityProject *project)
|
|
: mProject(project)
|
|
{
|
|
mProject->Bind(EVT_TRACK_PANEL_TIMER,
|
|
&PlaybackScroller::OnTimer,
|
|
this);
|
|
}
|
|
|
|
void ProjectWindow::PlaybackScroller::OnTimer(wxCommandEvent &event)
|
|
{
|
|
// Let other listeners get the notification
|
|
event.Skip();
|
|
|
|
auto gAudioIO = AudioIO::Get();
|
|
mRecentStreamTime = gAudioIO->GetStreamTime();
|
|
|
|
auto cleanup = finally([&]{
|
|
// Propagate the message to other listeners bound to this
|
|
this->SafelyProcessEvent( event );
|
|
});
|
|
|
|
if(!ProjectAudioIO::Get( *mProject ).IsAudioActive())
|
|
return;
|
|
else if (mMode == Mode::Refresh) {
|
|
// PRL: see comments in Scrubbing.cpp for why this is sometimes needed.
|
|
// These unnecessary refreshes cause wheel rotation events to be delivered more uniformly
|
|
// to the application, so scrub speed control is smoother.
|
|
// (So I see at least with OS 10.10 and wxWidgets 3.0.2.)
|
|
// Is there another way to ensure that than by refreshing?
|
|
auto &trackPanel = GetProjectPanel( *mProject );
|
|
trackPanel.Refresh(false);
|
|
}
|
|
else if (mMode != Mode::Off) {
|
|
// Pan the view, so that we put the play indicator at some fixed
|
|
// fraction of the window width.
|
|
|
|
auto &viewInfo = ViewInfo::Get( *mProject );
|
|
auto &trackPanel = GetProjectPanel( *mProject );
|
|
const int posX = viewInfo.TimeToPosition(mRecentStreamTime);
|
|
auto width = viewInfo.GetTracksUsableWidth();
|
|
int deltaX;
|
|
switch (mMode)
|
|
{
|
|
default:
|
|
wxASSERT(false);
|
|
/* fallthru */
|
|
case Mode::Pinned:
|
|
deltaX =
|
|
posX - width * TracksPrefs::GetPinnedHeadPositionPreference();
|
|
break;
|
|
case Mode::Right:
|
|
deltaX = posX - width; break;
|
|
}
|
|
viewInfo.h =
|
|
viewInfo.OffsetTimeByPixels(viewInfo.h, deltaX, true);
|
|
if (!ProjectWindow::Get( *mProject ).MayScrollBeyondZero())
|
|
// Can't scroll too far left
|
|
viewInfo.h = std::max(0.0, viewInfo.h);
|
|
trackPanel.Refresh(false);
|
|
}
|
|
}
|
|
|
|
void ProjectWindow::ZoomInByFactor( double ZoomFactor )
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
|
|
auto gAudioIO = AudioIO::Get();
|
|
// LLL: Handling positioning differently when audio is
|
|
// actively playing. Don't do this if paused.
|
|
if (gAudioIO->IsStreamActive(
|
|
ProjectAudioIO::Get( project ).GetAudioIOToken()) &&
|
|
!gAudioIO->IsPaused()){
|
|
ZoomBy(ZoomFactor);
|
|
ScrollIntoView(gAudioIO->GetStreamTime());
|
|
return;
|
|
}
|
|
|
|
// DMM: Here's my attempt to get logical zooming behavior
|
|
// when there's a selection that's currently at least
|
|
// partially on-screen
|
|
|
|
const double endTime = viewInfo.GetScreenEndTime();
|
|
const double duration = endTime - viewInfo.h;
|
|
|
|
bool selectionIsOnscreen =
|
|
(viewInfo.selectedRegion.t0() < endTime) &&
|
|
(viewInfo.selectedRegion.t1() >= viewInfo.h);
|
|
|
|
bool selectionFillsScreen =
|
|
(viewInfo.selectedRegion.t0() < viewInfo.h) &&
|
|
(viewInfo.selectedRegion.t1() > endTime);
|
|
|
|
if (selectionIsOnscreen && !selectionFillsScreen) {
|
|
// Start with the center of the selection
|
|
double selCenter = (viewInfo.selectedRegion.t0() +
|
|
viewInfo.selectedRegion.t1()) / 2;
|
|
|
|
// If the selection center is off-screen, pick the
|
|
// center of the part that is on-screen.
|
|
if (selCenter < viewInfo.h)
|
|
selCenter = viewInfo.h +
|
|
(viewInfo.selectedRegion.t1() - viewInfo.h) / 2;
|
|
if (selCenter > endTime)
|
|
selCenter = endTime -
|
|
(endTime - viewInfo.selectedRegion.t0()) / 2;
|
|
|
|
// Zoom in
|
|
ZoomBy(ZoomFactor);
|
|
const double newDuration =
|
|
viewInfo.GetScreenEndTime() - viewInfo.h;
|
|
|
|
// Recenter on selCenter
|
|
TP_ScrollWindow(selCenter - newDuration / 2);
|
|
return;
|
|
}
|
|
|
|
|
|
double origLeft = viewInfo.h;
|
|
double origWidth = duration;
|
|
ZoomBy(ZoomFactor);
|
|
|
|
const double newDuration =
|
|
viewInfo.GetScreenEndTime() - viewInfo.h;
|
|
double newh = origLeft + (origWidth - newDuration) / 2;
|
|
|
|
// MM: Commented this out because it was confusing users
|
|
/*
|
|
// make sure that the *right-hand* end of the selection is
|
|
// no further *left* than 1/3 of the way across the screen
|
|
if (viewInfo.selectedRegion.t1() < newh + viewInfo.screen / 3)
|
|
newh = viewInfo.selectedRegion.t1() - viewInfo.screen / 3;
|
|
|
|
// make sure that the *left-hand* end of the selection is
|
|
// no further *right* than 2/3 of the way across the screen
|
|
if (viewInfo.selectedRegion.t0() > newh + viewInfo.screen * 2 / 3)
|
|
newh = viewInfo.selectedRegion.t0() - viewInfo.screen * 2 / 3;
|
|
*/
|
|
|
|
TP_ScrollWindow(newh);
|
|
}
|
|
|
|
void ProjectWindow::ZoomOutByFactor( double ZoomFactor )
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
|
|
//Zoom() may change these, so record original values:
|
|
const double origLeft = viewInfo.h;
|
|
const double origWidth = viewInfo.GetScreenEndTime() - origLeft;
|
|
|
|
ZoomBy(ZoomFactor);
|
|
const double newWidth = viewInfo.GetScreenEndTime() - viewInfo.h;
|
|
|
|
const double newh = origLeft + (origWidth - newWidth) / 2;
|
|
// newh = (newh > 0) ? newh : 0;
|
|
TP_ScrollWindow(newh);
|
|
}
|
|
|
|
double ProjectWindow::GetZoomOfToFit() const
|
|
{
|
|
auto &project = mProject;
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
|
|
const double end = tracks.GetEndTime();
|
|
const double start = viewInfo.bScrollBeyondZero
|
|
? std::min( tracks.GetStartTime(), 0.0)
|
|
: 0;
|
|
const double len = end - start;
|
|
|
|
if (len <= 0.0)
|
|
return viewInfo.GetZoom();
|
|
|
|
auto w = viewInfo.GetTracksUsableWidth();
|
|
w -= 10;
|
|
return w/len;
|
|
}
|
|
|
|
void ProjectWindow::DoZoomFit()
|
|
{
|
|
auto &project = mProject;
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
auto &tracks = TrackList::Get( project );
|
|
auto &window = *this;
|
|
|
|
const double start = viewInfo.bScrollBeyondZero
|
|
? std::min(tracks.GetStartTime(), 0.0)
|
|
: 0;
|
|
|
|
window.Zoom( window.GetZoomOfToFit() );
|
|
window.TP_ScrollWindow(start);
|
|
}
|
|
|
|
static struct InstallTopPanelHook{ InstallTopPanelHook() {
|
|
ToolManager::SetGetTopPanelHook(
|
|
[]( wxWindow &window ){
|
|
auto pProjectWindow = dynamic_cast< ProjectWindow* >( &window );
|
|
return pProjectWindow ? pProjectWindow->GetTopPanel() : nullptr;
|
|
}
|
|
);
|
|
}} installTopPanelHook;
|