To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / main / MainWindow.cpp

History | View | Annotate | Download (111 KB)

1
/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
2

    
3
/*
4
    Sonic Lineup
5
    Comparative visualisation and alignment of related audio recordings
6
    Centre for Digital Music, Queen Mary, University of London.
7
    
8
    This program is free software; you can redistribute it and/or
9
    modify it under the terms of the GNU General Public License as
10
    published by the Free Software Foundation; either version 2 of the
11
    License, or (at your option) any later version.  See the file
12
    COPYING included with this distribution for more information.
13
*/
14

    
15
#include "../version.h"
16

    
17
#include "MainWindow.h"
18
#include "framework/Document.h"
19
#include "framework/VersionTester.h"
20
#include "align/Align.h"
21

    
22
#include "PreferencesDialog.h"
23
#include "NetworkPermissionTester.h"
24
#include "IntroDialog.h"
25

    
26
#include "view/Pane.h"
27
#include "view/PaneStack.h"
28
#include "data/model/WaveFileModel.h"
29
#include "data/model/SparseOneDimensionalModel.h"
30
#include "data/model/AlignmentModel.h"
31
#include "data/model/SparseOneDimensionalModel.h"
32
#include "base/StorageAdviser.h"
33
#include "base/Exceptions.h"
34
#include "base/TempDirectory.h"
35
#include "base/RecordDirectory.h"
36
#include "view/ViewManager.h"
37
#include "base/Preferences.h"
38
#include "layer/WaveformLayer.h"
39
#include "layer/TimeRulerLayer.h"
40
#include "layer/TimeInstantLayer.h"
41
#include "layer/TimeValueLayer.h"
42
#include "layer/Colour3DPlotLayer.h"
43
#include "layer/SliceLayer.h"
44
#include "layer/SliceableLayer.h"
45
#include "layer/SpectrogramLayer.h"
46
#include "view/Overview.h"
47
#include "widgets/PropertyBox.h"
48
#include "widgets/PropertyStack.h"
49
#include "widgets/AudioDial.h"
50
#include "widgets/LevelPanWidget.h"
51
#include "widgets/LevelPanToolButton.h"
52
#include "widgets/IconLoader.h"
53
#include "widgets/LayerTree.h"
54
#include "widgets/ListInputDialog.h"
55
#include "widgets/SubdividingMenu.h"
56
#include "widgets/NotifyingPushButton.h"
57
#include "widgets/KeyReference.h"
58
#include "widgets/MenuTitle.h"
59
#include "audio/AudioCallbackPlaySource.h"
60
#include "audio/AudioCallbackRecordTarget.h"
61
#include "audio/PlaySpeedRangeMapper.h"
62
#include "data/fileio/DataFileReaderFactory.h"
63
#include "data/fileio/WavFileWriter.h"
64
#include "data/fileio/CSVFileWriter.h"
65
#include "data/fileio/BZipFileDevice.h"
66
#include "data/fileio/FileSource.h"
67
#include "base/RecentFiles.h"
68
#include "transform/TransformFactory.h"
69
#include "transform/ModelTransformerFactory.h"
70
#include "base/PlayParameterRepository.h"
71
#include "base/XmlExportable.h"
72
#include "widgets/CommandHistory.h"
73
#include "base/Profiler.h"
74
#include "base/Clipboard.h"
75
#include "base/UnitDatabase.h"
76
#include "layer/ColourDatabase.h"
77
#include "data/osc/OSCQueue.h"
78

    
79
//!!!
80
#include "data/model/AggregateWaveModel.h"
81

    
82
// For version information
83
#include "vamp/vamp.h"
84
#include "vamp-sdk/PluginBase.h"
85
#include "plugin/api/ladspa.h"
86
#include "plugin/api/dssi.h"
87

    
88
#include <bqaudioio/SystemPlaybackTarget.h>
89
#include <bqaudioio/SystemAudioIO.h>
90

    
91
#include <QApplication>
92
#include <QMessageBox>
93
#include <QGridLayout>
94
#include <QLabel>
95
#include <QAction>
96
#include <QMenuBar>
97
#include <QToolBar>
98
#include <QToolButton>
99
#include <QButtonGroup>
100
#include <QInputDialog>
101
#include <QStatusBar>
102
#include <QTreeView>
103
#include <QFile>
104
#include <QFileInfo>
105
#include <QDir>
106
#include <QTextStream>
107
#include <QProcess>
108
#include <QShortcut>
109
#include <QSettings>
110
#include <QDateTime>
111
#include <QProcess>
112
#include <QCheckBox>
113
#include <QRegExp>
114
#include <QScrollArea>
115
#include <QCloseEvent>
116
#include <QDialogButtonBox>
117
#include <QTextEdit>
118
#include <QFileDialog>
119

    
120
#include <iostream>
121
#include <cstdio>
122
#include <errno.h>
123

    
124
using std::cerr;
125
using std::endl;
126

    
127
using std::vector;
128
using std::map;
129
using std::set;
130
using std::pair;
131

    
132

    
133
MainWindow::MainWindow(AudioMode audioMode) :
134
    MainWindowBase(audioMode,
135
                   MainWindowBase::MIDI_NONE,
136
                   int(PaneStack::Option::NoUserResize) |
137
                   int(PaneStack::Option::NoPropertyStacks) |
138
                   int(PaneStack::Option::ShowAlignmentViews) |
139
                   int(PaneStack::Option::NoCloseOnFirstPane)),
140
    m_mainMenusCreated(false),
141
    m_playbackToolBar(nullptr),
142
    m_recentSessionsMenu(nullptr),
143
    m_deleteSelectedAction(nullptr),
144
    m_ffwdAction(nullptr),
145
    m_rwdAction(nullptr),
146
    m_previousActiveAlignmentType(Align::NoAlignment),
147
    m_previousSubsequence(false),
148
    m_recentSessions("RecentSessions", 20),
149
    m_exiting(false),
150
    m_preferencesDialog(nullptr),
151
    m_layerTreeView(nullptr),
152
    m_keyReference(new KeyReference()),
153
    m_versionTester(nullptr),
154
    m_networkPermission(false),
155
    m_displayMode(OutlineWaveformMode),
156
    m_salientCalculating(false),
157
    m_salientColour(0),
158
    m_sessionState(NoSession),
159
    m_shownAlignmentError(false)
160
{
161
    setWindowTitle(QApplication::applicationName());
162

    
163
    setUnifiedTitleAndToolBarOnMac(true);
164

    
165
    UnitDatabase *udb = UnitDatabase::getInstance();
166
    udb->registerUnit("Hz");
167
    udb->registerUnit("dB");
168
    udb->registerUnit("s");
169

    
170
    ColourDatabase *cdb = ColourDatabase::getInstance();
171
    cdb->setUseDarkBackground(cdb->addColour(Qt::white, tr("White")), true);
172
    cdb->setUseDarkBackground(cdb->addColour(QColor(30, 150, 255), tr("Bright Blue")), true);
173
    cdb->setUseDarkBackground(cdb->addColour(Qt::red, tr("Bright Red")), true);
174
    cdb->setUseDarkBackground(cdb->addColour(Qt::green, tr("Bright Green")), true);
175
    cdb->setUseDarkBackground(cdb->addColour(QColor(255, 188, 80), tr("Bright Orange")), true);
176
    cdb->setUseDarkBackground(cdb->addColour(QColor(225, 74, 255), tr("Bright Purple")), true);
177
    cdb->setUseDarkBackground(cdb->addColour(Qt::yellow, tr("Bright Yellow")), true);
178

    
179
    Preferences::getInstance()->setResampleOnLoad(true);
180

    
181
    Preferences::getInstance()->setSpectrogramSmoothing
182
        (Preferences::SpectrogramInterpolated);
183

    
184
    Preferences::getInstance()->setSpectrogramXSmoothing
185
        (Preferences::SpectrogramXInterpolated);
186

    
187
    QSettings settings;
188

    
189
    settings.beginGroup("LayerDefaults");
190

    
191
    settings.setValue("spectrogram",
192
                      QString("<layer channel=\"-1\" windowSize=\"1024\" colourMap=\"White on Black\" windowHopLevel=\"2\"/>"));
193

    
194
    settings.setValue("melodicrange",
195
                      QString("<layer channel=\"-1\" gain=\"1\" normalizeVisibleArea=\"false\" columnNormalization=\"hybrid\" colourMap=\"Ice\" minFrequency=\"80\" maxFrequency=\"1500\" windowSize=\"8192\" windowOverlap=\"75\" binDisplay=\"0\" />"));
196

    
197
    settings.endGroup();
198

    
199
    settings.beginGroup("MainWindow");
200
    settings.setValue("showstatusbar", false);
201
    settings.endGroup();
202

    
203
    settings.beginGroup("IconLoader");
204
    settings.setValue("invert-icons-on-dark-background", true);
205
    settings.endGroup();
206

    
207
    settings.beginGroup("View");
208
    settings.setValue("showcancelbuttons", false);
209
    settings.endGroup();
210

    
211
    Align::setDefaultAlignmentPreference(Align::MATCHAlignmentWithPitchCompare);
212

    
213
    m_viewManager->setAlignMode(true);
214
    m_viewManager->setPlaySoloMode(true);
215
    m_viewManager->setToolMode(ViewManager::NavigateMode);
216
    m_viewManager->setZoomWheelsEnabled(false);
217
    m_viewManager->setIlluminateLocalFeatures(false);
218
    m_viewManager->setShowWorkTitle(true);
219
    m_viewManager->setOpportunisticEditingEnabled(false);
220

    
221
    loadStyle();
222
    
223
    QFrame *mainFrame = new QFrame;
224
    QGridLayout *mainLayout = new QGridLayout;
225

    
226
    setCentralWidget(mainFrame);
227
    
228
    m_mainScroll = new QScrollArea(mainFrame);
229
    m_mainScroll->setWidgetResizable(true);
230
    m_mainScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
231
    m_mainScroll->setFrameShape(QFrame::NoFrame);
232

    
233
    m_mainScroll->setWidget(m_paneStack);
234

    
235
    QFrame *bottomFrame = new QFrame(mainFrame);
236
    bottomFrame->setObjectName("BottomFrame");
237
    QGridLayout *bottomLayout = new QGridLayout;
238

    
239
    int bottomElementHeight = m_viewManager->scalePixelSize(45);
240
    if (bottomElementHeight < 40) bottomElementHeight = 40;
241
    int bottomButtonHeight = (bottomElementHeight * 3) / 4;
242
    
243
    QButtonGroup *bg = new QButtonGroup;
244
    IconLoader il;
245

    
246
    QFrame *buttonFrame = new QFrame(bottomFrame);
247
    QHBoxLayout *buttonLayout = new QHBoxLayout;
248
    buttonLayout->setSpacing(0);
249
    buttonLayout->setMargin(0);
250
    buttonFrame->setLayout(buttonLayout);
251

    
252
    QPushButton *button = new QPushButton;
253
    button->setIcon(il.load("waveform"));
254
    button->setText(tr("Outline waveform"));
255
    button->setCheckable(true);
256
    button->setChecked(true);
257
    button->setFixedHeight(bottomButtonHeight);
258
    bg->addButton(button);
259
    buttonLayout->addWidget(button);
260
    connect(button, SIGNAL(clicked()), this, SLOT(outlineWaveformModeSelected()));
261
    m_modeButtons[OutlineWaveformMode] = button;
262
    m_modeLayerNames[OutlineWaveformMode] = "Outline Waveform"; // not to be translated
263
    m_modeDisplayOrder.push_back(OutlineWaveformMode);
264

    
265
    button = new QPushButton;
266
    button->setIcon(il.load("waveform"));
267
    button->setText(tr("Waveform"));
268
    button->setCheckable(true);
269
    button->setChecked(false);
270
    button->setFixedHeight(bottomButtonHeight);
271
    bg->addButton(button);
272
    buttonLayout->addWidget(button);
273
    connect(button, SIGNAL(clicked()), this, SLOT(standardWaveformModeSelected()));
274
    m_modeButtons[WaveformMode] = button;
275
    m_modeLayerNames[WaveformMode] = "Waveform"; // not to be translated
276
    m_modeDisplayOrder.push_back(WaveformMode);
277

    
278
    button = new QPushButton;
279
    button->setIcon(il.load("colour3d"));
280
    button->setText(tr("Melodic spectrogram"));
281
    button->setCheckable(true);
282
    button->setChecked(false);
283
    button->setFixedHeight(bottomButtonHeight);
284
    bg->addButton(button);
285
    buttonLayout->addWidget(button);
286
    connect(button, SIGNAL(clicked()), this, SLOT(melodogramModeSelected()));
287
    m_modeButtons[MelodogramMode] = button;
288
    m_modeLayerNames[MelodogramMode] = "Melodogram"; // not to be translated
289
    m_modeDisplayOrder.push_back(MelodogramMode);
290

    
291
    button = new QPushButton;
292
    button->setIcon(il.load("colour3d"));
293
    button->setText(tr("Spectrogram"));
294
    button->setCheckable(true);
295
    button->setChecked(false);
296
    button->setFixedHeight(bottomButtonHeight);
297
    bg->addButton(button);
298
    buttonLayout->addWidget(button);
299
    connect(button, SIGNAL(clicked()), this, SLOT(spectrogramModeSelected()));
300
    m_modeButtons[SpectrogramMode] = button;
301
    m_modeLayerNames[SpectrogramMode] = "Spectrogram"; // not to be translated
302
    m_modeDisplayOrder.push_back(SpectrogramMode);
303

    
304
    button = new QPushButton;
305
    button->setIcon(il.load("values"));
306
    button->setText(tr("Sung pitch"));
307
    button->setCheckable(true);
308
    button->setChecked(false);
309
    button->setFixedHeight(bottomButtonHeight);
310
    bg->addButton(button);
311
    buttonLayout->addWidget(button);
312
    connect(button, SIGNAL(clicked()), this, SLOT(pitchModeSelected()));
313
    m_modeButtons[PitchMode] = button;
314
    m_modeLayerNames[PitchMode] = "Pitch"; // not to be translated
315
    m_modeDisplayOrder.push_back(PitchMode);
316

    
317
    button = new QPushButton;
318
    button->setIcon(il.load("colour3d"));
319
    button->setText(tr("Key"));
320
    button->setCheckable(true);
321
    button->setChecked(false);
322
    button->setFixedHeight(bottomButtonHeight);
323
    bg->addButton(button);
324
    buttonLayout->addWidget(button);
325
    connect(button, SIGNAL(clicked()), this, SLOT(keyModeSelected()));
326
    m_modeButtons[KeyMode] = button;
327
    m_modeLayerNames[KeyMode] = "Key"; // not to be translated
328
    m_modeDisplayOrder.push_back(KeyMode);
329

    
330
    button = new QPushButton;
331
    button->setIcon(il.load("colour3d"));
332
    button->setText(tr("Stereo azimuth"));
333
    button->setCheckable(true);
334
    button->setChecked(false);
335
    button->setFixedHeight(bottomButtonHeight);
336
    bg->addButton(button);
337
    buttonLayout->addWidget(button);
338
    connect(button, SIGNAL(clicked()), this, SLOT(azimuthModeSelected()));
339
    m_modeButtons[AzimuthMode] = button;
340
    m_modeLayerNames[AzimuthMode] = "Azimuth"; // not to be translated
341
    m_modeDisplayOrder.push_back(AzimuthMode);
342

    
343
    m_playSpeed = new AudioDial(bottomFrame);
344
    m_playSpeed->setMinimum(0);
345
    m_playSpeed->setMaximum(120);
346
    m_playSpeed->setValue(60);
347
    m_playSpeed->setFixedWidth(int(bottomElementHeight * 0.9));
348
    m_playSpeed->setFixedHeight(int(bottomElementHeight * 0.9));
349
    m_playSpeed->setNotchesVisible(true);
350
    m_playSpeed->setPageStep(10);
351
    m_playSpeed->setObjectName(tr("Playback Speed"));
352
    m_playSpeed->setRangeMapper(new PlaySpeedRangeMapper);
353
    m_playSpeed->setDefaultValue(60);
354
    m_playSpeed->setShowToolTip(true);
355
    connect(m_playSpeed, SIGNAL(valueChanged(int)),
356
            this, SLOT(playSpeedChanged(int)));
357
    connect(m_playSpeed, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
358
    connect(m_playSpeed, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));
359

    
360
    m_mainLevelPan = new LevelPanToolButton(bottomFrame);
361
    connect(m_mainLevelPan, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
362
    connect(m_mainLevelPan, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));
363
    m_mainLevelPan->setFixedHeight(bottomElementHeight);
364
    m_mainLevelPan->setFixedWidth(bottomElementHeight);
365
    m_mainLevelPan->setImageSize((bottomElementHeight * 3) / 4);
366
    m_mainLevelPan->setBigImageSize(bottomElementHeight * 3);
367

    
368
    int spacing = m_viewManager->scalePixelSize(4);
369

    
370
    bottomLayout->setSpacing(spacing);
371
    bottomLayout->setMargin(spacing);
372
    bottomLayout->addWidget(buttonFrame, 0, 0);
373
    bottomLayout->setColumnStretch(1, 10);
374
    bottomLayout->addWidget(m_playSpeed, 0, 2);
375
    bottomLayout->addWidget(m_mainLevelPan, 0, 3);
376
    bottomFrame->setLayout(bottomLayout);
377
    
378
    mainLayout->setSpacing(spacing);
379
    mainLayout->setMargin(spacing);
380
    mainLayout->addWidget(m_mainScroll, 0, 0);
381
    mainLayout->addWidget(bottomFrame, 1, 0);
382
    mainFrame->setLayout(mainLayout);
383

    
384
    setupMenus();
385

    
386
    statusBar()->hide();
387

    
388
    setIconsVisibleInMenus(false);
389
    finaliseMenus();
390

    
391
    NetworkPermissionTester tester;
392
    m_networkPermission = tester.havePermission();
393
        
394
//    QTimer::singleShot(500, this, SLOT(betaReleaseWarning()));
395
}
396

    
397
MainWindow::~MainWindow()
398
{
399
    delete m_keyReference;
400
    delete m_preferencesDialog;
401
    delete m_layerTreeView;
402
    Profiles::getInstance()->dump();
403
}
404

    
405
void
406
MainWindow::setupMenus()
407
{
408
    if (!m_mainMenusCreated) {
409

    
410
#ifdef Q_OS_LINUX
411
        // In Ubuntu 14.04 the window's menu bar goes missing entirely
412
        // if the user is running any desktop environment other than Unity
413
        // (in which the faux single-menubar appears). The user has a
414
        // workaround, to remove the appmenu-qt5 package, but that is
415
        // awkward and the problem is so severe that it merits disabling
416
        // the system menubar integration altogether. Like this:
417
        menuBar()->setNativeMenuBar(false);
418
#endif
419

    
420
        setupFileMenu();
421
        setupViewMenu();
422
        setupPlaybackMenu();
423
        setupAlignmentMenu();
424
        setupHelpMenu();
425

    
426
        Pane::registerShortcuts(*m_keyReference);
427

    
428
    } else {
429
        setupRecentSessionsMenu();
430
    }
431

    
432
    m_mainMenusCreated = true;
433
}
434

    
435
void
436
MainWindow::goFullScreen()
437
{
438
    if (!m_viewManager) return;
439

    
440
    if (m_viewManager->getZoomWheelsEnabled()) {
441
        // The wheels seem to end up in the wrong place in full-screen mode
442
        toggleZoomWheels();
443
    }
444

    
445
    QWidget *ps = m_mainScroll->takeWidget();
446
    ps->setParent(0);
447

    
448
    QShortcut *sc;
449

    
450
    sc = new QShortcut(QKeySequence("Esc"), ps);
451
    connect(sc, SIGNAL(activated()), this, SLOT(endFullScreen()));
452

    
453
    sc = new QShortcut(QKeySequence("F11"), ps);
454
    connect(sc, SIGNAL(activated()), this, SLOT(endFullScreen()));
455

    
456
    QAction *acts[] = {
457
        m_playAction, m_zoomInAction, m_zoomOutAction, m_zoomFitAction,
458
        m_scrollLeftAction, m_scrollRightAction,
459
        m_selectPreviousPaneAction, m_selectNextPaneAction,
460
        m_selectPreviousDisplayModeAction, m_selectNextDisplayModeAction
461
    };
462

    
463
    for (int i = 0; i < int(sizeof(acts)/sizeof(acts[0])); ++i) {
464
        sc = new QShortcut(acts[i]->shortcut(), ps);
465
        connect(sc, SIGNAL(activated()), acts[i], SLOT(trigger()));
466
    }
467

    
468
    ps->showFullScreen();
469
}
470

    
471
void
472
MainWindow::endFullScreen()
473
{
474
    // these were only created in goFullScreen:
475
    QObjectList cl = m_paneStack->children();
476
    foreach (QObject *o, cl) {
477
        QShortcut *sc = qobject_cast<QShortcut *>(o);
478
        if (sc) delete sc;
479
    }
480

    
481
    m_paneStack->showNormal();
482
    m_mainScroll->setWidget(m_paneStack);
483
}
484

    
485
void
486
MainWindow::setupFileMenu()
487
{
488
    if (m_mainMenusCreated) return;
489

    
490
    QMenu *menu = menuBar()->addMenu(tr("&File"));
491
    menu->setTearOffEnabled(false);
492
    QToolBar *toolbar = addToolBar(tr("File Toolbar"));
493

    
494
    m_keyReference->setCategory(tr("File and Session Management"));
495

    
496
    IconLoader il;
497

    
498
    QIcon icon = il.load("filenew");
499
    QAction *action = new QAction(icon, tr("&Clear Session"), this);
500
    action->setShortcut(tr("Ctrl+N"));
501
    action->setStatusTip(tr("Abandon the current session and start a new one"));
502
    connect(action, SIGNAL(triggered()), this, SLOT(newSession()));
503
    m_keyReference->registerShortcut(action);
504
    menu->addAction(action);
505
    toolbar->addAction(action);
506

    
507
    icon = il.load("fileopen");
508
    action = new QAction(icon, tr("&Add Files..."), this);
509
    action->setShortcut(tr("Ctrl+O"));
510
    action->setStatusTip(tr("Add one or more audio files"));
511
    connect(action, SIGNAL(triggered()), this, SLOT(openFiles()));
512
    m_keyReference->registerShortcut(action);
513
    menu->addAction(action);
514
    toolbar->addAction(action);
515

    
516
    action = new QAction(tr("Add Lo&cation..."), this);
517
    action->setShortcut(tr("Ctrl+Shift+O"));
518
    action->setStatusTip(tr("Add a file from a remote URL"));
519
    connect(action, SIGNAL(triggered()), this, SLOT(openLocation()));
520
    m_keyReference->registerShortcut(action);
521
    menu->addAction(action);
522

    
523
    menu->addSeparator();
524
    
525
    action = new QAction(tr("Browse Recorded Audio"), this);
526
    action->setStatusTip(tr("Open the Recorded Audio folder in the system file browser"));
527
    connect(action, SIGNAL(triggered()), this, SLOT(browseRecordedAudio()));
528
    menu->addAction(action);
529

    
530
    menu->addSeparator();
531

    
532
    m_recentSessionsMenu = menu->addMenu(tr("&Recent Sessions"));
533
    m_recentSessionsMenu->setTearOffEnabled(false);
534
    setupRecentSessionsMenu();
535
    connect(&m_recentSessions, SIGNAL(recentChanged()),
536
            this, SLOT(setupRecentSessionsMenu()));
537
    
538
    /*
539
    menu->addSeparator();
540
    action = new QAction(tr("&Preferences..."), this);
541
    action->setStatusTip(tr("Adjust the application preferences"));
542
    connect(action, SIGNAL(triggered()), this, SLOT(preferences()));
543
    menu->addAction(action);
544
    */
545
    
546
    menu->addSeparator();
547
    action = new QAction(il.load("exit"), tr("&Quit"), this);
548
    action->setShortcut(tr("Ctrl+Q"));
549
    action->setStatusTip(tr("Exit Sonic Lineup"));
550
    connect(action, SIGNAL(triggered()), this, SLOT(close()));
551
    m_keyReference->registerShortcut(action);
552
    menu->addAction(action);
553
}
554

    
555
void
556
MainWindow::setupRecentSessionsMenu()
557
{
558
    m_recentSessionsMenu->clear();
559
    vector<pair<QString, QString>> sessions = m_recentSessions.getRecentEntries();
560
    for (size_t i = 0; i < sessions.size(); ++i) {
561
        QString path = sessions[i].first;
562
        QString label = sessions[i].second;
563
        if (label == "") label = path;
564
        QAction *action = m_recentSessionsMenu->addAction(label);
565
        action->setObjectName(path);
566
        connect(action, SIGNAL(triggered()), this, SLOT(openRecentSession()));
567
        if (i == 0) {
568
            action->setShortcut(tr("Ctrl+R"));
569
            m_keyReference->registerShortcut
570
                (tr("Re-open"),
571
                 action->shortcut().toString(),
572
                 tr("Re-open the current or most recently opened session"));
573
        }
574
    }
575
}
576

    
577
void
578
MainWindow::setupViewMenu()
579
{
580
    if (m_mainMenusCreated) return;
581

    
582
    IconLoader il;
583

    
584
    QAction *action = 0;
585

    
586
    m_keyReference->setCategory(tr("Panning and Navigation"));
587

    
588
    QMenu *menu = menuBar()->addMenu(tr("&View"));
589
    menu->setTearOffEnabled(false);
590
    m_scrollLeftAction = new QAction(tr("Scroll &Left"), this);
591
    m_scrollLeftAction->setShortcut(tr("Left"));
592
    m_scrollLeftAction->setStatusTip(tr("Scroll the current pane to the left"));
593
    connect(m_scrollLeftAction, SIGNAL(triggered()), this, SLOT(scrollLeft()));
594
    connect(this, SIGNAL(canScroll(bool)), m_scrollLeftAction, SLOT(setEnabled(bool)));
595
    m_keyReference->registerShortcut(m_scrollLeftAction);
596
    menu->addAction(m_scrollLeftAction);
597
        
598
    m_scrollRightAction = new QAction(tr("Scroll &Right"), this);
599
    m_scrollRightAction->setShortcut(tr("Right"));
600
    m_scrollRightAction->setStatusTip(tr("Scroll the current pane to the right"));
601
    connect(m_scrollRightAction, SIGNAL(triggered()), this, SLOT(scrollRight()));
602
    connect(this, SIGNAL(canScroll(bool)), m_scrollRightAction, SLOT(setEnabled(bool)));
603
    m_keyReference->registerShortcut(m_scrollRightAction);
604
    menu->addAction(m_scrollRightAction);
605
        
606
    action = new QAction(tr("&Jump Left"), this);
607
    action->setShortcut(tr("Ctrl+Left"));
608
    action->setStatusTip(tr("Scroll the current pane a big step to the left"));
609
    connect(action, SIGNAL(triggered()), this, SLOT(jumpLeft()));
610
    connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool)));
611
    m_keyReference->registerShortcut(action);
612
    menu->addAction(action);
613
        
614
    action = new QAction(tr("J&ump Right"), this);
615
    action->setShortcut(tr("Ctrl+Right"));
616
    action->setStatusTip(tr("Scroll the current pane a big step to the right"));
617
    connect(action, SIGNAL(triggered()), this, SLOT(jumpRight()));
618
    connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool)));
619
    m_keyReference->registerShortcut(action);
620
    menu->addAction(action);
621

    
622
    menu->addSeparator();
623

    
624
    action = new QAction(tr("Switch to Previous Pane"), this);
625
    action->setShortcut(tr("["));
626
    action->setStatusTip(tr("Make the next pane up in the pane stack current"));
627
    connect(action, SIGNAL(triggered()), this, SLOT(previousPane()));
628
    connect(this, SIGNAL(canSelectPreviousPane(bool)), action, SLOT(setEnabled(bool)));
629
    m_keyReference->registerShortcut(action);
630
    menu->addAction(action);
631
    m_selectPreviousPaneAction = action;
632

    
633
    action = new QAction(tr("Switch to Next Pane"), this);
634
    action->setShortcut(tr("]"));
635
    action->setStatusTip(tr("Make the next pane down in the pane stack current"));
636
    connect(action, SIGNAL(triggered()), this, SLOT(nextPane()));
637
    connect(this, SIGNAL(canSelectNextPane(bool)), action, SLOT(setEnabled(bool)));
638
    m_keyReference->registerShortcut(action);
639
    menu->addAction(action);
640
    m_selectNextPaneAction = action;
641

    
642
    menu->addSeparator();
643

    
644
    action = new QAction(tr("Switch to Previous View Mode"), this);
645
    action->setShortcut(tr("{"));
646
    action->setStatusTip(tr("Make the next view mode current"));
647
    connect(action, SIGNAL(triggered()), this, SLOT(previousDisplayMode()));
648
    connect(this, SIGNAL(canSelectPreviousDisplayMode(bool)), action, SLOT(setEnabled(bool)));
649
    m_keyReference->registerShortcut(action);
650
    menu->addAction(action);
651
    m_selectPreviousDisplayModeAction = action;
652

    
653
    action = new QAction(tr("Switch to Next View Mode"), this);
654
    action->setShortcut(tr("}"));
655
    action->setStatusTip(tr("Make the next view mode current"));
656
    connect(action, SIGNAL(triggered()), this, SLOT(nextDisplayMode()));
657
    connect(this, SIGNAL(canSelectNextDisplayMode(bool)), action, SLOT(setEnabled(bool)));
658
    m_keyReference->registerShortcut(action);
659
    menu->addAction(action);
660
    m_selectNextDisplayModeAction = action;
661

    
662
    menu->addSeparator();
663

    
664
    m_keyReference->setCategory(tr("Zoom"));
665

    
666
    m_zoomInAction = new QAction(il.load("zoom-in"),
667
                                 tr("Zoom &In"), this);
668
    m_zoomInAction->setShortcut(tr("Up"));
669
    m_zoomInAction->setStatusTip(tr("Increase the zoom level"));
670
    connect(m_zoomInAction, SIGNAL(triggered()), this, SLOT(zoomIn()));
671
    connect(this, SIGNAL(canZoom(bool)), m_zoomInAction, SLOT(setEnabled(bool)));
672
    m_keyReference->registerShortcut(m_zoomInAction);
673
    menu->addAction(m_zoomInAction);
674
        
675
    m_zoomOutAction = new QAction(il.load("zoom-out"),
676
                                  tr("Zoom &Out"), this);
677
    m_zoomOutAction->setShortcut(tr("Down"));
678
    m_zoomOutAction->setStatusTip(tr("Decrease the zoom level"));
679
    connect(m_zoomOutAction, SIGNAL(triggered()), this, SLOT(zoomOut()));
680
    connect(this, SIGNAL(canZoom(bool)), m_zoomOutAction, SLOT(setEnabled(bool)));
681
    m_keyReference->registerShortcut(m_zoomOutAction);
682
    menu->addAction(m_zoomOutAction);
683
        
684
    action = new QAction(tr("Restore &Default Zoom"), this);
685
    action->setStatusTip(tr("Restore the zoom level to the default"));
686
    connect(action, SIGNAL(triggered()), this, SLOT(zoomDefault()));
687
    connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool)));
688
    menu->addAction(action);
689

    
690
    m_zoomFitAction = new QAction(il.load("zoom-fit"),
691
                                  tr("Zoom to &Fit"), this);
692
    m_zoomFitAction->setShortcut(tr("F"));
693
    m_zoomFitAction->setStatusTip(tr("Zoom to show the whole file"));
694
    connect(m_zoomFitAction, SIGNAL(triggered()), this, SLOT(zoomToFit()));
695
    connect(this, SIGNAL(canZoom(bool)), m_zoomFitAction, SLOT(setEnabled(bool)));
696
    m_keyReference->registerShortcut(m_zoomFitAction);
697
    menu->addAction(m_zoomFitAction);
698

    
699
    menu->addSeparator();
700

    
701
    m_keyReference->setCategory(tr("Display Features"));
702

    
703
    action = new QAction(tr("Show &Centre Line"), this);
704
    action->setShortcut(tr("'"));
705
    action->setStatusTip(tr("Show or hide the centre line"));
706
    connect(action, SIGNAL(triggered()), this, SLOT(toggleCentreLine()));
707
    action->setCheckable(true);
708
    action->setChecked(true);
709
    m_keyReference->registerShortcut(action);
710
    menu->addAction(action);
711

    
712
    action = new QAction(tr("Show Salient Feature Layers"), this);
713
    action->setShortcut(tr("#"));
714
    action->setStatusTip(tr("Show or hide all salient-feature layers"));
715
    connect(action, SIGNAL(triggered()), this, SLOT(toggleSalientFeatures()));
716
    action->setCheckable(true);
717
    action->setChecked(true);
718
    m_keyReference->registerShortcut(action);
719
    menu->addAction(action);
720

    
721
    action = new QAction(tr("Show Times and Scales"), this);
722
    action->setShortcut(tr("S"));
723
    action->setStatusTip(tr("Show or hide centre-line timings and all vertical scales"));
724
    connect(action, SIGNAL(triggered()), this, SLOT(toggleVerticalScales()));
725
    action->setCheckable(true);
726
    action->setChecked(false);
727
    m_viewManager->setOverlayMode(ViewManager::NoOverlays);
728
    m_keyReference->registerShortcut(action);
729
    menu->addAction(action);
730

    
731
    // We need this separator even if not adding the full-screen
732
    // option ourselves, as the Mac automatic full-screen entry
733
    // doesn't include a separator first
734
    menu->addSeparator();
735

    
736
#ifndef Q_OS_MAC
737
    // Only on non-Mac platforms -- on the Mac this interacts very
738
    // badly with the "native" full-screen mode
739
    action = new QAction(tr("Go Full-Screen"), this);
740
    action->setShortcut(tr("F11"));
741
    action->setStatusTip(tr("Expand the pane area to the whole screen"));
742
    connect(action, SIGNAL(triggered()), this, SLOT(goFullScreen()));
743
    m_keyReference->registerShortcut(action);
744
    menu->addAction(action);
745
#endif
746
}
747

    
748
void
749
MainWindow::setupAlignmentMenu()
750
{
751
    m_keyReference->setCategory(tr("Alignment"));
752

    
753
    IconLoader il;
754

    
755
    QMenu *menu = menuBar()->addMenu(tr("&Alignment"));
756
    menu->setTearOffEnabled(false);
757

    
758
    QActionGroup *alignmentGroup = new QActionGroup(this);
759

    
760
//!!! + explanatory status bar texts
761

    
762
    map<Align::AlignmentType, QString> alignmentLabels {
763
        { Align::NoAlignment, tr("No Alignment") },
764
        { Align::LinearAlignment, tr("Linear") },
765
        { Align::TrimmedLinearAlignment, tr("Linear Trimmed") },
766
        { Align::MATCHAlignment, tr("MATCH Aligner") },
767
        { Align::MATCHAlignmentWithPitchCompare, tr("MATCH with Tuning Compensation") },
768
        { Align::SungNoteContourAlignment, tr("Sung Note Contour") },
769
        { Align::ExternalProgramAlignment, tr("External Alignment Program...") }
770
    };
771

    
772
    QAction *action = nullptr;
773
    Align::AlignmentType preference = Align::getAlignmentPreference();
774

    
775
    for (auto al: alignmentLabels) {
776
        action = menu->addAction(al.second);
777
        action->setObjectName(Align::getAlignmentTypeTag(al.first));
778
        action->setActionGroup(alignmentGroup);
779
        action->setCheckable(true);
780
        action->setChecked(al.first == preference);
781
        connect(action, SIGNAL(triggered()), this, SLOT(alignmentTypeChanged()));
782
    }
783

    
784
    menu->addSeparator();
785

    
786
    action = menu->addAction(tr("Align to Section of Reference"));
787
    action->setCheckable(true);
788
    action->setChecked(Align::getUseSubsequenceAlignment());
789
    action->setEnabled(preference != Align::NoAlignment &&
790
                       preference != Align::LinearAlignment &&
791
                       preference != Align::TrimmedLinearAlignment &&
792
                       preference != Align::ExternalProgramAlignment);
793
    connect(action, SIGNAL(triggered()), this, SLOT(alignmentSubsequenceChanged()));
794

    
795
    m_subsequenceAlignmentAction = action;
796
}
797

    
798
void
799
MainWindow::setupPlaybackMenu()
800
{
801
    m_keyReference->setCategory(tr("Playback and Transport Controls"));
802

    
803
    IconLoader il;
804

    
805
    QMenu *menu = menuBar()->addMenu(tr("Play&back"));
806
    menu->setTearOffEnabled(false);
807

    
808
    QToolBar *toolbar = nullptr;
809
    if (m_playbackToolBar) {
810
        toolbar = m_playbackToolBar;
811
    } else {
812
        toolbar = m_playbackToolBar = addToolBar(tr("Playback Toolbar"));
813
    }
814

    
815
    QAction *rwdStartAction = toolbar->addAction(il.load("rewind-start"),
816
                                                 tr("Rewind to Start"));
817
    rwdStartAction->setShortcut(tr("Home"));
818
    rwdStartAction->setStatusTip(tr("Rewind to the start"));
819
    connect(rwdStartAction, SIGNAL(triggered()), this, SLOT(rewindStart()));
820
    connect(this, SIGNAL(canPlay(bool)), rwdStartAction, SLOT(setEnabled(bool)));
821

    
822
    QAction *m_rwdAction = toolbar->addAction(il.load("rewind"),
823
                                              tr("Rewind"));
824
    m_rwdAction->setShortcut(tr("PgUp"));
825
    m_rwdAction->setStatusTip(tr("Rewind to the previous time instant or time ruler notch"));
826
    connect(m_rwdAction, SIGNAL(triggered()), this, SLOT(rewind()));
827
    connect(this, SIGNAL(canRewind(bool)), m_rwdAction, SLOT(setEnabled(bool)));
828

    
829
    m_playAction = toolbar->addAction(il.load("playpause"),
830
                                      tr("Play / Pause"));
831
    m_playAction->setCheckable(true);
832
    m_playAction->setShortcut(tr("Space"));
833
    m_playAction->setStatusTip(tr("Start or stop playback from the current position"));
834
    connect(m_playAction, SIGNAL(triggered()), this, SLOT(play()));
835
    connect(m_playSource, SIGNAL(playStatusChanged(bool)),
836
            m_playAction, SLOT(setChecked(bool)));
837
    connect(this, SIGNAL(canPlay(bool)), m_playAction, SLOT(setEnabled(bool)));
838

    
839
    m_ffwdAction = toolbar->addAction(il.load("ffwd"),
840
                                      tr("Fast Forward"));
841
    m_ffwdAction->setShortcut(tr("PgDown"));
842
    m_ffwdAction->setStatusTip(tr("Fast-forward to the next time instant or time ruler notch"));
843
    connect(m_ffwdAction, SIGNAL(triggered()), this, SLOT(ffwd()));
844
    connect(this, SIGNAL(canFfwd(bool)), m_ffwdAction, SLOT(setEnabled(bool)));
845

    
846
    QAction *ffwdEndAction = toolbar->addAction(il.load("ffwd-end"),
847
                                                tr("Fast Forward to End"));
848
    ffwdEndAction->setShortcut(tr("End"));
849
    ffwdEndAction->setStatusTip(tr("Fast-forward to the end"));
850
    connect(ffwdEndAction, SIGNAL(triggered()), this, SLOT(ffwdEnd()));
851
    connect(this, SIGNAL(canPlay(bool)), ffwdEndAction, SLOT(setEnabled(bool)));
852

    
853
    QAction *recordAction = toolbar->addAction(il.load("record"),
854
                                               tr("Record"));
855
    recordAction->setCheckable(true);
856
    recordAction->setShortcut(tr("Ctrl+Space"));
857
    recordAction->setStatusTip(tr("Record a new audio file"));
858
    connect(recordAction, SIGNAL(triggered()), this, SLOT(record()));
859
    connect(m_recordTarget, SIGNAL(recordStatusChanged(bool)),
860
            recordAction, SLOT(setChecked(bool)));
861
    connect(this, SIGNAL(canRecord(bool)),
862
            recordAction, SLOT(setEnabled(bool)));
863

    
864
    m_keyReference->registerShortcut(m_playAction);
865
    m_keyReference->registerShortcut(m_rwdAction);
866
    m_keyReference->registerShortcut(m_ffwdAction);
867
    m_keyReference->registerShortcut(rwdStartAction);
868
    m_keyReference->registerShortcut(ffwdEndAction);
869
    m_keyReference->registerShortcut(recordAction);
870

    
871
    menu->addAction(m_playAction);
872
    menu->addSeparator();
873
    menu->addAction(m_rwdAction);
874
    menu->addAction(m_ffwdAction);
875
    menu->addSeparator();
876
    menu->addAction(rwdStartAction);
877
    menu->addAction(ffwdEndAction);
878
    menu->addSeparator();
879
    menu->addAction(recordAction);
880
    menu->addSeparator();
881

    
882
    QAction *fastAction = menu->addAction(tr("Speed Up"));
883
    fastAction->setShortcut(tr("Ctrl+PgUp"));
884
    fastAction->setStatusTip(tr("Time-stretch playback to speed it up without changing pitch"));
885
    connect(fastAction, SIGNAL(triggered()), this, SLOT(speedUpPlayback()));
886
    connect(this, SIGNAL(canSpeedUpPlayback(bool)), fastAction, SLOT(setEnabled(bool)));
887
    
888
    QAction *slowAction = menu->addAction(tr("Slow Down"));
889
    slowAction->setShortcut(tr("Ctrl+PgDown"));
890
    slowAction->setStatusTip(tr("Time-stretch playback to slow it down without changing pitch"));
891
    connect(slowAction, SIGNAL(triggered()), this, SLOT(slowDownPlayback()));
892
    connect(this, SIGNAL(canSlowDownPlayback(bool)), slowAction, SLOT(setEnabled(bool)));
893

    
894
    QAction *normalAction = menu->addAction(tr("Restore Normal Speed"));
895
    normalAction->setShortcut(tr("Ctrl+Home"));
896
    normalAction->setStatusTip(tr("Restore non-time-stretched playback"));
897
    connect(normalAction, SIGNAL(triggered()), this, SLOT(restoreNormalPlayback()));
898
    connect(this, SIGNAL(canChangePlaybackSpeed(bool)), normalAction, SLOT(setEnabled(bool)));
899

    
900
    m_keyReference->registerShortcut(fastAction);
901
    m_keyReference->registerShortcut(slowAction);
902
    m_keyReference->registerShortcut(normalAction);
903
}
904

    
905
void
906
MainWindow::setupHelpMenu()
907
{
908
    QMenu *menu = menuBar()->addMenu(tr("&Help"));
909
    menu->setTearOffEnabled(false);
910
    
911
    m_keyReference->setCategory(tr("Help"));
912

    
913
    IconLoader il;
914

    
915
    QAction *action = new QAction(il.load("help"),
916
                                  tr("&Help Reference"), this); 
917
    action->setShortcut(tr("F1"));
918
    action->setStatusTip(tr("Open the reference manual")); 
919
    connect(action, SIGNAL(triggered()), this, SLOT(help()));
920
    m_keyReference->registerShortcut(action);
921
    menu->addAction(action);
922

    
923
    action = new QAction(tr("&Key and Mouse Reference"), this);
924
    action->setShortcut(tr("F2"));
925
    action->setStatusTip(tr("Open a window showing the keystrokes you can use"));
926
    connect(action, SIGNAL(triggered()), this, SLOT(keyReference()));
927
    m_keyReference->registerShortcut(action);
928
    menu->addAction(action);
929

    
930
    QString name = QApplication::applicationName();
931
    
932
    action = new QAction(tr("What's &New In This Release?"), this); 
933
    action->setStatusTip(tr("List the changes in this release (and every previous release) of %1").arg(name)); 
934
    connect(action, SIGNAL(triggered()), this, SLOT(whatsNew()));
935
    menu->addAction(action);
936
    
937
    action = new QAction(tr("&About %1").arg(name), this); 
938
    action->setStatusTip(tr("Show information about %1").arg(name)); 
939
    connect(action, SIGNAL(triggered()), this, SLOT(about()));
940
    menu->addAction(action);
941
}
942

    
943
void
944
MainWindow::updateMenuStates()
945
{
946
    MainWindowBase::updateMenuStates();
947

    
948
    Pane *currentPane = 0;
949
    Layer *currentLayer = 0;
950

    
951
    if (m_paneStack) currentPane = m_paneStack->getCurrentPane();
952
    if (currentPane) currentLayer = currentPane->getSelectedLayer();
953

    
954
    bool haveCurrentPane =
955
        (currentPane != 0);
956
    bool haveCurrentLayer =
957
        (haveCurrentPane &&
958
         (currentLayer != 0));
959
    bool haveCurrentTimeInstantsLayer = 
960
        (haveCurrentLayer &&
961
         dynamic_cast<TimeInstantLayer *>(currentLayer));
962
    bool haveCurrentTimeValueLayer = 
963
        (haveCurrentLayer &&
964
         dynamic_cast<TimeValueLayer *>(currentLayer));
965

    
966
    emit canChangePlaybackSpeed(true);
967
    int v = m_playSpeed->value();
968
    emit canSpeedUpPlayback(v < m_playSpeed->maximum());
969
    emit canSlowDownPlayback(v > m_playSpeed->minimum());
970

    
971
    emit canSelectPreviousDisplayMode
972
        (!m_modeDisplayOrder.empty() &&
973
         (m_displayMode != m_modeDisplayOrder[0]));
974

    
975
    emit canSelectNextDisplayMode
976
        (!m_modeDisplayOrder.empty() &&
977
         (m_displayMode != m_modeDisplayOrder[m_modeDisplayOrder.size()-1]));
978

    
979
    if (m_ffwdAction && m_rwdAction) {
980
        if (haveCurrentTimeInstantsLayer) {
981
            m_ffwdAction->setText(tr("Fast Forward to Next Instant"));
982
            m_ffwdAction->setStatusTip(tr("Fast forward to the next time instant in the current layer"));
983
            m_rwdAction->setText(tr("Rewind to Previous Instant"));
984
            m_rwdAction->setStatusTip(tr("Rewind to the previous time instant in the current layer"));
985
        } else if (haveCurrentTimeValueLayer) {
986
            m_ffwdAction->setText(tr("Fast Forward to Next Point"));
987
            m_ffwdAction->setStatusTip(tr("Fast forward to the next point in the current layer"));
988
            m_rwdAction->setText(tr("Rewind to Previous Point"));
989
            m_rwdAction->setStatusTip(tr("Rewind to the previous point in the current layer"));
990
        } else {
991
            m_ffwdAction->setText(tr("Fast Forward"));
992
            m_ffwdAction->setStatusTip(tr("Fast forward"));
993
            m_rwdAction->setText(tr("Rewind"));
994
            m_rwdAction->setStatusTip(tr("Rewind"));
995
        }
996
    }
997
}
998

    
999
void
1000
MainWindow::updateDescriptionLabel()
1001
{
1002
    // we don't actually have a description label
1003
}
1004

    
1005
void
1006
MainWindow::updateWindowTitle()
1007
{
1008
    QString title;
1009

    
1010
    QString sessionLabel = makeSessionLabel();
1011
    
1012
    if (sessionLabel != "") {
1013
        title = tr("%1: %2")
1014
            .arg(QApplication::applicationName())
1015
            .arg(sessionLabel);
1016
    } else {
1017
        title = QApplication::applicationName();
1018
    }
1019
    
1020
    setWindowTitle(title);
1021
}
1022

    
1023
void
1024
MainWindow::documentModified()
1025
{
1026
    MainWindowBase::documentModified();
1027
}
1028

    
1029
void
1030
MainWindow::documentRestored()
1031
{
1032
    MainWindowBase::documentRestored();
1033
}
1034

    
1035
void
1036
MainWindow::selectMainPane()
1037
{
1038
    if (m_paneStack && m_paneStack->getPaneCount() > 0) {
1039
        m_paneStack->setCurrentPane(m_paneStack->getPane(0));
1040
    }
1041
}
1042

    
1043
void
1044
MainWindow::browseRecordedAudio()
1045
{
1046
    QString path = RecordDirectory::getRecordContainerDirectory();
1047
    if (path == "") path = RecordDirectory::getRecordDirectory();
1048
    if (path == "") return;
1049

    
1050
    openLocalFolder(path);
1051
}
1052

    
1053
void
1054
MainWindow::newSession()
1055
{
1056
    cerr << "MainWindow::newSession" << endl;
1057

    
1058
    closeSession();
1059
    createDocument();
1060
    
1061
    m_displayMode = OutlineWaveformMode;
1062
    for (auto &bp : m_modeButtons) {
1063
        bp.second->setChecked(false);
1064
    }
1065
    m_modeButtons[m_displayMode]->setChecked(true);
1066
    
1067
    // We need a pane, so that we have something to receive drop events
1068
    
1069
    Pane *pane = m_paneStack->addPane();
1070

    
1071
    connect(pane, SIGNAL(contextHelpChanged(const QString &)),
1072
            this, SLOT(contextHelpChanged(const QString &)));
1073

    
1074
    m_document->setAutoAlignment(m_viewManager->getAlignMode());
1075

    
1076
    CommandHistory::getInstance()->clear();
1077
    CommandHistory::getInstance()->documentSaved();
1078
    documentRestored();
1079
    updateMenuStates();
1080
    zoomDefault();
1081

    
1082
    // Record that the last (i.e. current, as of now) session is
1083
    // empty, so that if we exit now and re-start, we get an empty
1084
    // session as is proper instead of loading the last non-empty one
1085
    QSettings settings;
1086
    settings.beginGroup("MainWindow");
1087
    settings.setValue("lastsession", "");
1088
    settings.endGroup();
1089
}
1090

    
1091
void
1092
MainWindow::closeSession()
1093
{
1094
    checkpointSession();
1095
    if (m_sessionState != SessionLoading) {
1096
        m_sessionFile = "";
1097
        m_sessionState = NoSession;
1098
    }
1099

    
1100
    while (m_paneStack->getPaneCount() > 0) {
1101

    
1102
        Pane *pane = m_paneStack->getPane(m_paneStack->getPaneCount() - 1);
1103

    
1104
        while (pane->getLayerCount() > 0) {
1105
            m_document->removeLayerFromView
1106
                (pane, pane->getLayer(pane->getLayerCount() - 1));
1107
        }
1108

    
1109
        m_paneStack->deletePane(pane);
1110
    }
1111

    
1112
    while (m_paneStack->getHiddenPaneCount() > 0) {
1113

    
1114
        Pane *pane = m_paneStack->getHiddenPane
1115
            (m_paneStack->getHiddenPaneCount() - 1);
1116

    
1117
        while (pane->getLayerCount() > 0) {
1118
            m_document->removeLayerFromView
1119
                (pane, pane->getLayer(pane->getLayerCount() - 1));
1120
        }
1121

    
1122
        m_paneStack->deletePane(pane);
1123
    }
1124

    
1125
    delete m_document;
1126
    m_document = 0;
1127
    m_viewManager->clearSelections();
1128
    m_timeRulerLayer = 0; // document owned this
1129

    
1130
    setWindowTitle(tr("Sonic Lineup"));
1131

    
1132
    CommandHistory::getInstance()->clear();
1133
    CommandHistory::getInstance()->documentSaved();
1134
    documentRestored();
1135

    
1136
    m_shownAlignmentError = false;
1137
}
1138

    
1139
void
1140
MainWindow::openFiles()
1141
{
1142
    QString orig = m_audioFile;
1143
    if (orig == "") orig = ".";
1144
    else orig = QFileInfo(orig).absoluteDir().canonicalPath();
1145

    
1146
    FileFinder *ff = FileFinder::getInstance();
1147

    
1148
    QStringList paths = ff->getOpenFileNames(FileFinder::AudioFile,
1149
                                             m_audioFile);
1150

    
1151
    if (paths.empty()) return;
1152
    
1153
    m_sessionState = SessionActive;
1154
    
1155
    for (QString path: paths) {
1156

    
1157
        FileOpenStatus status = FileOpenFailed;
1158
        
1159
        FileSource source(path);
1160
        if (source.isAvailable()) {
1161
            source.waitForData();
1162

    
1163
            try {
1164
                status = openAudio(source, CreateAdditionalModel);
1165
            } catch (const InsufficientDiscSpace &e) {
1166
                SVCERR << "MainWindowBase: Caught InsufficientDiscSpace in file open" << endl;
1167
                QMessageBox::critical
1168
                    (this, tr("Not enough disc space"),
1169
                     tr("<b>Not enough disc space</b><p>There doesn't appear to be enough spare disc space to accommodate any necessary temporary files.</p><p>Please clear some space and try again.</p>").arg(e.what()));
1170
                return;
1171
            }
1172
        }
1173
        
1174
        if (status != FileOpenSucceeded) {
1175
            QMessageBox::critical(this, tr("Failed to open file"),
1176
                                  tr("<b>File open failed</b><p>File \"%1\" could not be opened").arg(path));
1177
        } else {
1178
            configureNewPane(m_paneStack->getCurrentPane());
1179
        }
1180
    }
1181
}
1182

    
1183
void
1184
MainWindow::openLocation()
1185
{
1186
    QSettings settings;
1187
    settings.beginGroup("MainWindow");
1188
    QString lastLocation = settings.value("lastremote", "").toString();
1189

    
1190
    bool ok = false;
1191
    QString text = QInputDialog::getText
1192
        (this, tr("Open Location"),
1193
         tr("Please enter the URL of the location to open:"),
1194
         QLineEdit::Normal, lastLocation, &ok);
1195

    
1196
    if (!ok) return;
1197
    if (text.isEmpty()) return;
1198

    
1199
    m_sessionState = SessionActive;
1200

    
1201
    FileOpenStatus status = FileOpenFailed;
1202
        
1203
    FileSource source(text);
1204
    if (source.isAvailable()) {
1205
        source.waitForData();
1206

    
1207
        try {
1208
            status = openAudio(source, CreateAdditionalModel);
1209
        } catch (const InsufficientDiscSpace &e) {
1210
            SVCERR << "MainWindowBase: Caught InsufficientDiscSpace in file open" << endl;
1211
            QMessageBox::critical
1212
                (this, tr("Not enough disc space"),
1213
                 tr("<b>Not enough disc space</b><p>There doesn't appear to be enough spare disc space to accommodate any necessary temporary files.</p><p>Please clear some space and try again.</p>").arg(e.what()));
1214
            return;
1215
        }
1216
    }
1217
        
1218
    if (status != FileOpenSucceeded) {
1219
        QMessageBox::critical(this, tr("Failed to open location"),
1220
                              tr("<b>Open failed</b><p>URL \"%1\" could not be opened").arg(text));
1221
    } else {
1222
        configureNewPane(m_paneStack->getCurrentPane());
1223
        settings.setValue("lastremote", text);
1224
    }
1225
}
1226

    
1227
void
1228
MainWindow::openRecentSession()
1229
{
1230
    QObject *obj = sender();
1231
    QAction *action = dynamic_cast<QAction *>(obj);
1232
    
1233
    if (!action) {
1234
        cerr << "WARNING: MainWindow::openRecentSession: sender is not an action"
1235
                  << endl;
1236
        return;
1237
    }
1238

    
1239
    QString path = action->objectName();
1240
    
1241
    if (path == "") {
1242
        cerr << "WARNING: MainWindow::openRecentSession: action incorrectly named"
1243
             << endl;
1244
        return;
1245
    }
1246

    
1247
    openSmallSessionFile(path);
1248
}
1249

    
1250
bool
1251
MainWindow::reopenLastSession()
1252
{
1253
    QSettings settings;
1254
    settings.beginGroup("MainWindow");
1255
    QString lastSession = settings.value("lastsession", "").toString();
1256
    settings.endGroup();
1257

    
1258
    if (lastSession != "") {
1259
        openSmallSessionFile(lastSession);
1260
    }
1261

    
1262
    if (m_sessionState == NoSession) {
1263
        newSession(); // to ensure we have a drop target
1264
        return false;
1265
    } else {
1266
        return true;
1267
    }
1268
}
1269

    
1270
void
1271
MainWindow::openSmallSessionFile(QString path)
1272
{
1273
    m_sessionFile = path;
1274
    m_sessionState = SessionLoading;
1275

    
1276
    SVDEBUG << "MainWindow::openSmallSessionFile: m_sessionFile is now "
1277
            << m_sessionFile << endl;
1278

    
1279
    try {
1280
        SmallSession session = SmallSession::load(path);
1281
        openSmallSession(session);
1282
    } catch (const std::runtime_error &e) {
1283
        QMessageBox::critical
1284
            (this, tr("Failed to reload session"),
1285
             tr("<b>Open failed</b>"
1286
                "<p>Session file \"%1\" could not be opened: %2</p>")
1287
             .arg(path).arg(e.what()));
1288
        m_sessionFile = "";
1289
        m_sessionState = NoSession;
1290
    }
1291
}
1292

    
1293
void
1294
MainWindow::openSmallSession(const SmallSession &session)
1295
{
1296
    QString errorText;
1297
    FileOpenStatus status;
1298
    
1299
    closeSession();
1300
    createDocument();
1301

    
1302
    status = openPath(session.mainFile, ReplaceMainModel);
1303

    
1304
    if (status != FileOpenSucceeded) {
1305
        errorText = tr("Unable to open main audio file %1")
1306
            .arg(session.mainFile);
1307
        goto failed;
1308
    }
1309
    
1310
    configureNewPane(m_paneStack->getCurrentPane());
1311
        
1312
    for (QString path: session.additionalFiles) {
1313

    
1314
        QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
1315
        
1316
        status = openPath(path, CreateAdditionalModel);
1317

    
1318
        if (status != FileOpenSucceeded) {
1319
            errorText = tr("Unable to open audio file %1").arg(path);
1320
            goto failed;
1321
        }
1322

    
1323
        configureNewPane(m_paneStack->getCurrentPane());
1324
    }
1325

    
1326
    rewindStart();
1327
    
1328
    m_documentModified = false;
1329
    m_sessionState = SessionActive;
1330
    return;
1331

    
1332
failed:
1333
    QMessageBox::critical(this, tr("Failed to load session"),
1334
                          tr("<b>Open failed</b><p>Session could not be opened: %2</p>").arg(errorText));
1335
    m_sessionFile = "";
1336
    m_sessionState = NoSession;
1337
}
1338

    
1339
bool
1340
MainWindow::selectExistingLayerForMode(Pane *pane,
1341
                                       QString modeName,
1342
                                       ModelId *createFrom)
1343
{
1344
    // Search the given pane for any layer whose object name matches
1345
    // modeName, showing it if it exists, and hiding all other layers
1346
    // (except for time-instants layers, which are assumed to be used
1347
    // for persistent segment display and are left unmodified).
1348

    
1349
    // In the case where no such layer is found and false is returned,
1350
    // then if the return parameter createFrom is non-null, the value
1351
    // it points to will be set to a pointer to the model from which
1352
    // such a layer should be constructed.
1353

    
1354
    ModelId modelId;
1355

    
1356
    bool have = false;
1357

    
1358
    for (int i = 0; i < pane->getLayerCount(); ++i) {
1359
        
1360
        Layer *layer = pane->getLayer(i);
1361
        if (!layer || qobject_cast<TimeInstantLayer *>(layer)) {
1362
            continue;
1363
        }
1364

    
1365
        modelId = layer->getModel();
1366
        auto sourceId = layer->getSourceModel();
1367
        if (!sourceId.isNone()) modelId = sourceId;
1368
        
1369
        QString ln = layer->objectName();
1370
        if (ln == modeName) {
1371
            layer->showLayer(pane, true);
1372
            have = true;
1373
        } else {
1374
            layer->showLayer(pane, false);
1375
        }
1376
    }
1377
    
1378
    if (have) return true;
1379

    
1380
    if (createFrom) {
1381
        *createFrom = modelId;
1382
    }
1383
    return false;
1384
}
1385

    
1386
void
1387
MainWindow::addSalientFeatureLayer(Pane *pane, ModelId modelId)
1388
{
1389
    //!!! what if there already is one? could have changed the main
1390
    //!!! model for example
1391

    
1392
    auto model = ModelById::getAs<WaveFileModel>(modelId);
1393
    if (!model) {
1394
        cerr << "MainWindow::addSalientFeatureLayer: No model" << endl;
1395
        return;
1396
    }
1397
    
1398
    TransformFactory *tf = TransformFactory::getInstance();
1399
    if (!tf) {
1400
        cerr << "Failed to locate a transform factory!" << endl;
1401
        return;
1402
    }
1403
    
1404
    TransformId id = "vamp:nnls-chroma:chordino:simplechord";
1405
    if (!tf->haveTransform(id)) {
1406
        cerr << "No plugin available for salient feature layer; transform is: "
1407
             << id << endl;
1408
        return;
1409
    }
1410

    
1411
    m_salientCalculating = true;
1412

    
1413
    Transform transform = tf->getDefaultTransformFor
1414
        (id, model->getSampleRate());
1415

    
1416
    ModelTransformer::Input input(modelId, -1);
1417

    
1418
    Layer *newLayer = m_document->createDerivedLayer(transform, modelId);
1419

    
1420
    if (newLayer) {
1421

    
1422
        TimeInstantLayer *til = qobject_cast<TimeInstantLayer *>(newLayer);
1423
        if (til) {
1424
            til->setPlotStyle(TimeInstantLayer::PlotInstants);
1425
            til->setBaseColour(m_salientColour);
1426
        }
1427

    
1428
        auto params = newLayer->getPlayParameters();
1429
        if (params) {
1430
            params->setPlayAudible(false);
1431
        }
1432

    
1433
        connect(til, SIGNAL(modelCompletionChanged(ModelId)),
1434
                this, SLOT(salientLayerCompletionChanged(ModelId)));
1435
        
1436
        m_document->addLayerToView(pane, newLayer);
1437
        m_paneStack->setCurrentLayer(pane, newLayer);
1438
    }
1439
}
1440

    
1441
void
1442
MainWindow::salientLayerCompletionChanged(ModelId)
1443
{
1444
    Layer *layer = qobject_cast<Layer *>(sender());
1445
    if (layer && layer->getCompletion(0) == 100) {
1446
        m_salientCalculating = false;
1447
        for (ModelId am: m_salientPending) {
1448
            mapSalientFeatureLayer(am);
1449
        }
1450
        m_salientPending.clear();
1451
    }
1452
}
1453

    
1454
void
1455
MainWindow::mapAllSalientFeatureLayers()
1456
{
1457
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1458
        Pane *p = m_paneStack->getPane(i);
1459
        for (int j = 0; j < p->getLayerCount(); ++j) {
1460
            auto modelId = p->getLayer(j)->getModel();
1461
            if (auto wfm = ModelById::getAs<WaveFileModel>(modelId)) {
1462
                SVDEBUG << "MainWindow::mapAllSalientFeatureLayers: calling mapSalientFeatureLayer for modelId " << modelId << " in pane " << i << ", layer " << j << endl;
1463
                mapSalientFeatureLayer(modelId);
1464
                break; // but only from inner loop, go on to next pane
1465
            }
1466
        }
1467
    }
1468
}
1469

    
1470
TimeInstantLayer *
1471
MainWindow::findSalientFeatureLayer(Pane *pane)
1472
{
1473
    if (!getMainModel()) return nullptr;
1474
    
1475
    if (!pane) {
1476
        for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1477

    
1478
            Pane *p = m_paneStack->getPane(i);
1479
            bool isAssociatedWithMainModel = false;
1480

    
1481
            for (int j = 0; j < p->getLayerCount(); ++j) {
1482
                Layer *l = p->getLayer(j);
1483
                if (l->getModel() == getMainModelId()) {
1484
                    isAssociatedWithMainModel = true;
1485
                    break;
1486
                }
1487
            }
1488

    
1489
            if (isAssociatedWithMainModel) {
1490
                TimeInstantLayer *layerHere = findSalientFeatureLayer(p);
1491
                if (layerHere) return layerHere;
1492
            }
1493
        }
1494

    
1495
        return nullptr;
1496
    }
1497

    
1498
    for (int i = 0; i < pane->getLayerCount(); ++i) {
1499
        TimeInstantLayer *ll = qobject_cast<TimeInstantLayer *>
1500
            (pane->getLayer(i));
1501
        if (ll) return ll;
1502
    }
1503

    
1504
    return nullptr;
1505
}
1506

    
1507
void
1508
MainWindow::toggleVerticalScales()
1509
{
1510
    if (m_viewManager->getOverlayMode() == ViewManager::NoOverlays) {
1511
        m_viewManager->setOverlayMode(ViewManager::StandardOverlays);
1512
    } else {
1513
        m_viewManager->setOverlayMode(ViewManager::NoOverlays);
1514
    }
1515
}
1516

    
1517
void
1518
MainWindow::toggleSalientFeatures()
1519
{
1520
    bool targetDormantState = false;
1521

    
1522
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1523
        Pane *p = m_paneStack->getPane(i);
1524
        TimeInstantLayer *layer = findSalientFeatureLayer(p);
1525
        if (layer) {
1526
            targetDormantState = !(layer->isLayerDormant(p));
1527
            break;
1528
        }
1529
    }
1530

    
1531
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1532
        Pane *p = m_paneStack->getPane(i);
1533
        TimeInstantLayer *salient = findSalientFeatureLayer(p);
1534
        if (salient) {
1535
            salient->setLayerDormant(p, targetDormantState);
1536
        }
1537
        if (targetDormantState) {
1538
            for (int j = 0; j < p->getLayerCount(); ++j) {
1539
                Layer *l = p->getLayer(j);
1540
                if (l != salient) {
1541
                    p->propertyContainerSelected(p, l);
1542
                    break;
1543
                }
1544
            }
1545
        } else {
1546
            p->propertyContainerSelected(p, salient);
1547
        }
1548
    }
1549
}
1550

    
1551
void
1552
MainWindow::mapSalientFeatureLayer(ModelId modelId)
1553
{
1554
    SVDEBUG << "MainWindow::mapSalientFeatureLayer(" << modelId << ")" << endl;
1555
    
1556
    if (m_salientCalculating) {
1557
        SVDEBUG << "MainWindow::mapSalientFeatureLayer(" << modelId << "): salient still calculating, adding to pending list" << endl;
1558
        m_salientPending.insert(modelId);
1559
        return;
1560
    }
1561

    
1562
    TimeInstantLayer *salient = findSalientFeatureLayer();
1563
    if (!salient) {
1564
        SVCERR << "MainWindow::mapSalientFeatureLayer: No salient layer found"
1565
               << endl;
1566
        m_salientPending.insert(modelId);
1567
        return;
1568
    }
1569
    
1570
    auto model = ModelById::get(modelId);
1571
    if (!model) {
1572
        SVCERR << "MainWindow::mapSalientFeatureLayer: Aligned model is absent" << endl;
1573
        return;
1574
    }
1575

    
1576
    Pane *pane = nullptr;
1577
    Layer *layer = nullptr;
1578
    Pane *firstPane = nullptr;
1579
    
1580
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1581
        Pane *p = m_paneStack->getPane(i);
1582
        if (p && !firstPane) firstPane = p;
1583
        for (int j = 0; j < p->getLayerCount(); ++j) {
1584
            Layer *l = p->getLayer(j);
1585
            if (!l) continue;
1586
            if (l->getModel() == modelId) {
1587
                pane = p;
1588
                layer = l;
1589
                break;
1590
            }
1591
        }
1592
        if (layer) break;
1593
    }
1594

    
1595
    if (!pane || !layer) {
1596
        SVCERR << "MainWindow::mapSalientFeatureLayer: Failed to find model "
1597
               << modelId << " in any layer" << endl;
1598
        return;
1599
    }
1600

    
1601
    QString salientLayerName = tr("Mapped Salient Feature Layer");
1602

    
1603
    // Remove any existing mapped salient layer from this pane (in
1604
    // case we are re-aligning an existing model)
1605
    for (int j = 0; j < pane->getLayerCount(); ++j) {
1606
        Layer *l = pane->getLayer(j);
1607
        if (!l) continue;
1608
        if (l->objectName() == salientLayerName) {
1609
            SVDEBUG << "MainWindow::mapSalientFeatureLayer: "
1610
                    << "Removing existing mapped layer " << l << endl;
1611
            m_document->deleteLayer(l, true); // force flag: remove from views
1612
            break;
1613
        }
1614
    }
1615

    
1616
    pane->setCentreFrame(model->alignFromReference(firstPane->getCentreFrame()));
1617

    
1618
    auto fromId = salient->getModel();
1619
    auto from = ModelById::getAs<SparseOneDimensionalModel>(fromId);
1620
    if (!from) {
1621
        SVCERR << "MainWindow::mapSalientFeatureLayer: "
1622
               << "Salient layer lacks SparseOneDimensionalModel" << endl;
1623
        return;
1624
    }
1625

    
1626
    auto to = std::make_shared<SparseOneDimensionalModel>
1627
        (model->getSampleRate(), from->getResolution(), false);
1628
    auto toId = ModelById::add(to);
1629

    
1630
    EventVector pp = from->getAllEvents();
1631

    
1632
    if (Align::getAlignmentPreference() != Align::NoAlignment) {
1633
        for (const auto &p: pp) {
1634
            Event aligned = p
1635
                .withFrame(model->alignFromReference(p.getFrame()))
1636
                .withLabel(""); // remove label, as the analysis was not
1637
                                // conducted on the audio we're mapping to
1638
            to->add(aligned);
1639
        }
1640
    } else {
1641
        for (const auto &p: pp) {
1642
            to->add(p.withLabel(""));
1643
        }
1644
    }
1645

    
1646
    SVDEBUG << "MainWindow::mapSalientFeatureLayer for model " << modelId
1647
            << ": have " << pp.size() << " events" << endl;
1648
    
1649
    Layer *newLayer = m_document->createImportedLayer(toId);
1650

    
1651
    if (newLayer) {
1652

    
1653
        newLayer->setObjectName(salientLayerName);
1654
        
1655
        TimeInstantLayer *til = qobject_cast<TimeInstantLayer *>(newLayer);
1656
        if (til) {
1657
            til->setPlotStyle(TimeInstantLayer::PlotInstants);
1658
            til->setBaseColour(m_salientColour);
1659
        }
1660
        
1661
        auto params = newLayer->getPlayParameters();
1662
        if (params) {
1663
            params->setPlayAudible(false);
1664
        }
1665

    
1666
        m_document->addLayerToView(pane, newLayer);
1667
        m_paneStack->setCurrentLayer(pane, newLayer);
1668
    }
1669
}
1670

    
1671
void
1672
MainWindow::outlineWaveformModeSelected()
1673
{
1674
    QString name = m_modeLayerNames[OutlineWaveformMode];
1675

    
1676
    Pane *currentPane = m_paneStack->getCurrentPane();
1677
    
1678
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1679

    
1680
        Pane *pane = m_paneStack->getPane(i);
1681
        if (!pane) continue;
1682

    
1683
        ModelId createFrom;
1684
        if (!selectExistingLayerForMode(pane, name, &createFrom) &&
1685
            !createFrom.isNone()) {
1686

    
1687
            Layer *newLayer = m_document->createLayer(LayerFactory::Waveform);
1688
            newLayer->setObjectName(name);
1689

    
1690
            QString layerPropertyXml =
1691
                QString("<layer scale=\"%1\" channelMode=\"%2\" gain=\"0.95\"/>")
1692
                .arg(int(WaveformLayer::MeterScale))
1693
                .arg(int(WaveformLayer::MergeChannels));
1694
            LayerFactory::getInstance()->setLayerProperties
1695
                (newLayer, layerPropertyXml);
1696

    
1697
            SingleColourLayer *scl =
1698
                qobject_cast<SingleColourLayer *>(newLayer);
1699
            if (scl) {
1700
                scl->setBaseColour
1701
                    (i % ColourDatabase::getInstance()->getColourCount());
1702
            }
1703
            
1704
            m_document->setModel(newLayer, createFrom);
1705
            m_document->addLayerToView(pane, newLayer);
1706
            m_paneStack->setCurrentLayer(pane, newLayer);
1707
        }
1708

    
1709
        TimeInstantLayer *salient = findSalientFeatureLayer(pane);
1710
        if (salient) {
1711
            pane->propertyContainerSelected(pane, salient);
1712
        }
1713
    }
1714

    
1715
    if (currentPane) {
1716
        m_paneStack->setCurrentPane(currentPane);
1717
    }
1718
    
1719
    m_displayMode = OutlineWaveformMode;
1720
    checkpointSession();
1721
}
1722

    
1723
void
1724
MainWindow::standardWaveformModeSelected()
1725
{
1726
    QString name = m_modeLayerNames[WaveformMode];
1727

    
1728
    Pane *currentPane = m_paneStack->getCurrentPane();
1729
    
1730
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1731

    
1732
        Pane *pane = m_paneStack->getPane(i);
1733
        if (!pane) continue;
1734

    
1735
        ModelId createFrom;
1736
        if (!selectExistingLayerForMode(pane, name, &createFrom) &&
1737
            !createFrom.isNone()) {
1738

    
1739
            Layer *newLayer = m_document->createLayer(LayerFactory::Waveform);
1740
            newLayer->setObjectName(name);
1741

    
1742
            QString layerPropertyXml =
1743
                QString("<layer scale=\"%1\" channelMode=\"%2\"/>")
1744
                .arg(int(WaveformLayer::LinearScale))
1745
                .arg(int(WaveformLayer::SeparateChannels));
1746
            LayerFactory::getInstance()->setLayerProperties
1747
                (newLayer, layerPropertyXml);
1748

    
1749
            SingleColourLayer *scl =
1750
                qobject_cast<SingleColourLayer *>(newLayer);
1751
            if (scl) {
1752
                scl->setBaseColour
1753
                    (i % ColourDatabase::getInstance()->getColourCount());
1754
            }
1755
            
1756
            m_document->setModel(newLayer, createFrom);
1757
            m_document->addLayerToView(pane, newLayer);
1758
            m_paneStack->setCurrentLayer(pane, newLayer);
1759
        }
1760

    
1761
        TimeInstantLayer *salient = findSalientFeatureLayer(pane);
1762
        if (salient) {
1763
            pane->propertyContainerSelected(pane, salient);
1764
        }
1765
    }
1766

    
1767
    if (currentPane) {
1768
        m_paneStack->setCurrentPane(currentPane);
1769
    }
1770

    
1771
    m_displayMode = WaveformMode;
1772
    checkpointSession();
1773
}
1774

    
1775
void
1776
MainWindow::spectrogramModeSelected()
1777
{
1778
    QString name = m_modeLayerNames[SpectrogramMode];
1779

    
1780
    Pane *currentPane = m_paneStack->getCurrentPane();
1781
    
1782
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1783

    
1784
        Pane *pane = m_paneStack->getPane(i);
1785
        if (!pane) continue;
1786

    
1787
        ModelId createFrom;
1788
        if (!selectExistingLayerForMode(pane, name, &createFrom) &&
1789
            !createFrom.isNone()) {
1790
            Layer *newLayer = m_document->createLayer(LayerFactory::Spectrogram);
1791
            newLayer->setObjectName(name);
1792
            m_document->setModel(newLayer, createFrom);
1793
            m_document->addLayerToView(pane, newLayer);
1794
            m_paneStack->setCurrentLayer(pane, newLayer);
1795
        }
1796

    
1797
        TimeInstantLayer *salient = findSalientFeatureLayer(pane);
1798
        if (salient) {
1799
            pane->propertyContainerSelected(pane, salient);
1800
        }
1801
    }
1802

    
1803
    if (currentPane) {
1804
        m_paneStack->setCurrentPane(currentPane);
1805
    }
1806

    
1807
    m_displayMode = SpectrogramMode;
1808
    checkpointSession();
1809
}
1810

    
1811
void
1812
MainWindow::melodogramModeSelected()
1813
{
1814
    QString name = m_modeLayerNames[MelodogramMode];
1815

    
1816
    Pane *currentPane = m_paneStack->getCurrentPane();
1817

    
1818
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1819

    
1820
        Pane *pane = m_paneStack->getPane(i);
1821
        if (!pane) continue;
1822

    
1823
        ModelId createFrom;
1824
        if (!selectExistingLayerForMode(pane, name, &createFrom) &&
1825
            !createFrom.isNone()) {
1826
            Layer *newLayer = m_document->createLayer
1827
                (LayerFactory::MelodicRangeSpectrogram);
1828
            SpectrogramLayer *spectrogram = qobject_cast<SpectrogramLayer *>
1829
                (newLayer);
1830
            spectrogram->setVerticallyFixed();
1831
            newLayer->setObjectName(name);
1832
            m_document->setModel(newLayer, createFrom);
1833
            m_document->addLayerToView(pane, newLayer);
1834
            m_paneStack->setCurrentLayer(pane, newLayer);
1835
        }
1836

    
1837
        TimeInstantLayer *salient = findSalientFeatureLayer(pane);
1838
        if (salient) {
1839
            pane->propertyContainerSelected(pane, salient);
1840
        }
1841
    }
1842

    
1843
    if (currentPane) {
1844
        m_paneStack->setCurrentPane(currentPane);
1845
    }
1846

    
1847
    m_displayMode = MelodogramMode;
1848
    checkpointSession();
1849
}
1850

    
1851
void
1852
MainWindow::selectTransformDrivenMode(DisplayMode mode,
1853
                                      QString transformId,
1854
                                      Transform::ParameterMap parameters,
1855
                                      QString layerPropertyXml,
1856
                                      bool includeGhostReference)
1857
{
1858
    QString name = m_modeLayerNames[mode];
1859

    
1860
    // Bring forth any existing layers of the appropriate name; for
1861
    // each pane that lacks one, make a note of the model from which
1862
    // we should create it
1863

    
1864
    map<Pane *, ModelId> sourceModels;
1865
    
1866
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1867
        Pane *pane = m_paneStack->getPane(i);
1868
        ModelId createFrom;
1869
        if (!selectExistingLayerForMode(pane, name, &createFrom)) {
1870
            if (!createFrom.isNone()) {
1871
                sourceModels[pane] = createFrom;
1872
            }
1873
        }
1874
    }
1875

    
1876
    Layer *ghostReference = nullptr;
1877

    
1878
    if (includeGhostReference && !sourceModels.empty()) {
1879

    
1880
        // Look up the layer of this type in the first pane -- this is
1881
        // the reference that we must include as a ghost in the pane
1882
        // that we're adding the new layer to.
1883

    
1884
        // NB it won't exist if this is the first time into this mode
1885
        // and we haven't created the layer for the reference pane yet
1886
        // - we have to handle that in the creation loop below.
1887
        
1888
        Pane *pane = m_paneStack->getPane(0);
1889
        
1890
        for (int i = 0; i < pane->getLayerCount(); ++i) {
1891
            Layer *layer = pane->getLayer(i);
1892
            if (!layer || qobject_cast<TimeInstantLayer *>(layer)) {
1893
                continue;
1894
            }
1895
            if (layer->objectName() == name) {
1896
                ghostReference = layer;
1897
                break;
1898
            }
1899
        }
1900
    }
1901

    
1902
    Pane *currentPane = m_paneStack->getCurrentPane();
1903

    
1904
    TransformFactory *tf = TransformFactory::getInstance();
1905

    
1906
    if (tf->haveTransform(transformId)) {
1907

    
1908
        for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
1909

    
1910
            Pane *pane = m_paneStack->getPane(i);
1911

    
1912
            if (sourceModels.find(pane) == sourceModels.end()) {
1913
                // no need to create, this one exists already
1914
                continue;
1915
            }
1916

    
1917
            ModelId source = sourceModels[pane];
1918

    
1919
            if (ghostReference) {
1920
                m_document->addLayerToView(pane, ghostReference);
1921
                pane->setUseAligningProxy(true);
1922
            }
1923
            
1924
            Transform transform = tf->getDefaultTransformFor(transformId);
1925

    
1926
            if (!parameters.empty()) {
1927
                transform.setParameters(parameters);
1928
            }
1929

    
1930
            ModelTransformer::Input input(source, -1);
1931

    
1932
            Layer *layer = m_document->createDerivedLayer(transform, source);
1933

    
1934
            if (layer) {
1935

    
1936
                layer->setObjectName(name);
1937
                LayerFactory::getInstance()->setLayerProperties
1938
                    (layer, layerPropertyXml);
1939

    
1940
                SingleColourLayer *scl =
1941
                    qobject_cast<SingleColourLayer *>(layer);
1942
                if (scl) {
1943
                    int colourIndex = 
1944
                        (i % ColourDatabase::getInstance()->getColourCount());
1945
                    scl->setBaseColour(colourIndex);
1946
                }
1947

    
1948
                m_document->addLayerToView(pane, layer);
1949
                m_paneStack->setCurrentLayer(pane, layer);
1950

    
1951
                if (!ghostReference && includeGhostReference && i == 0) {
1952
                    ghostReference = layer;
1953
                }
1954
                
1955
            } else {
1956
                SVCERR << "ERROR: Failed to create derived layer" << endl;
1957
            }
1958

    
1959
            TimeInstantLayer *salient = findSalientFeatureLayer(pane);
1960
            if (salient) {
1961
                pane->propertyContainerSelected(pane, salient);
1962
            }
1963
        }
1964
    } else {
1965
        SVCERR << "ERROR: No plugin available for mode: " << name << endl;
1966
    }
1967

    
1968
    if (currentPane) {
1969
        m_paneStack->setCurrentPane(currentPane);
1970
    }
1971

    
1972
    m_displayMode = mode;
1973
    checkpointSession();
1974
}
1975

    
1976
void
1977
MainWindow::pitchModeSelected()
1978
{
1979
    QString propertyXml =
1980
        QString("<layer plotStyle=\"%1\" verticalScale=\"%2\" scaleMinimum=\"%3\" scaleMaximum=\"%4\"/>")
1981
        .arg(int(TimeValueLayer::PlotDiscreteCurves))
1982
        .arg(int(TimeValueLayer::LogScale))
1983
        .arg(40)
1984
        .arg(510);
1985
    
1986
    selectTransformDrivenMode
1987
        (PitchMode,
1988
         "vamp:pyin:pyin:smoothedpitchtrack",
1989
         {},
1990
         propertyXml,
1991
         true); // ghost reference
1992
}
1993

    
1994
void
1995
MainWindow::keyModeSelected()
1996
{
1997
    QString propertyXml =
1998
        QString("<layer colourMap=\"Sunset\" opaque=\"true\" smooth=\"false\" "
1999
                "binScale=\"%1\" columnNormalization=\"none\"/>")
2000
        .arg(int(BinScale::Linear));
2001
    
2002
    selectTransformDrivenMode
2003
        (KeyMode,
2004
         "vamp:qm-vamp-plugins:qm-keydetector:mergedkeystrength",
2005
         {},
2006
         propertyXml,
2007
         false);
2008
}
2009

    
2010
void
2011
MainWindow::azimuthModeSelected()
2012
{
2013
    QString propertyXml =
2014
        QString("<layer colourMap=\"Ice\" opaque=\"true\" smooth=\"true\" "
2015
                "binScale=\"%1\" columnNormalization=\"hybrid\"/>")
2016
        .arg(int(BinScale::Linear));
2017

    
2018
    selectTransformDrivenMode
2019
        (AzimuthMode,
2020
         "vamp:azi:azi:plan",
2021
         {},
2022
         propertyXml,
2023
         false);
2024
}
2025

    
2026
void
2027
MainWindow::previousDisplayMode()
2028
{
2029
    for (int i = 0; in_range_for(m_modeDisplayOrder, i); ++i) {
2030
        if (m_displayMode == m_modeDisplayOrder[i]) {
2031
            if (i > 0) {
2032
                m_modeButtons[m_modeDisplayOrder[i-1]]->click();
2033
            }
2034
            break;
2035
        }
2036
    }
2037
}
2038

    
2039
void
2040
MainWindow::nextDisplayMode()
2041
{
2042
    for (int i = 0; in_range_for(m_modeDisplayOrder, i); ++i) {
2043
        if (m_displayMode == m_modeDisplayOrder[i]) {
2044
            if (in_range_for(m_modeDisplayOrder, i+1)) {
2045
                m_modeButtons[m_modeDisplayOrder[i+1]]->click();
2046
            }
2047
            break;
2048
        }
2049
    }
2050
}
2051

    
2052
void
2053
MainWindow::updateModeFromLayers()
2054
{
2055
    for (auto &bp : m_modeButtons) {
2056
        bp.second->setChecked(false);
2057
    }
2058

    
2059
    SVCERR << "MainWindow::updateModeFromLayers" << endl;
2060
    
2061
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
2062

    
2063
        Pane *pane = m_paneStack->getPane(i);
2064
        if (!pane) continue;
2065

    
2066
        SVCERR << "MainWindow::updateModeFromLayers: pane " << i << "..." << endl;
2067
        
2068
        bool found = false;
2069
        
2070
        for (int j = 0; j < pane->getLayerCount(); ++j) {
2071

    
2072
            Layer *layer = pane->getLayer(j);
2073
            if (!layer || qobject_cast<TimeInstantLayer *>(layer)) {
2074
                continue;
2075
            }
2076
            if (layer->isLayerDormant(pane)) {
2077
                continue;
2078
            }
2079

    
2080
            QString ln = layer->objectName();
2081

    
2082
            SVCERR << "MainWindow::updateModeFromLayers: layer " << j << " has name " << ln << endl;
2083

    
2084
            for (const auto &mp: m_modeLayerNames) {
2085
                if (ln == mp.second) {
2086
                    m_displayMode = mp.first;
2087
                    found = true;
2088
                    break;
2089
                }
2090
            }
2091
        }
2092

    
2093
        if (found) break;
2094
    }
2095

    
2096
    m_modeButtons[m_displayMode]->setChecked(true);
2097
}
2098

    
2099
void
2100
MainWindow::reselectMode()
2101
{
2102
    switch (m_displayMode) {
2103
    case OutlineWaveformMode: outlineWaveformModeSelected(); break;
2104
    case WaveformMode: standardWaveformModeSelected(); break;
2105
    case SpectrogramMode: spectrogramModeSelected(); break;
2106
    case MelodogramMode: melodogramModeSelected(); break;
2107
    case AzimuthMode: azimuthModeSelected(); break;
2108
    case PitchMode: pitchModeSelected(); break;
2109
    case KeyMode: keyModeSelected(); break;
2110
    }
2111
}
2112

    
2113
void
2114
MainWindow::paneAdded(Pane *pane)
2115
{
2116
    pane->setPlaybackFollow(PlaybackScrollContinuous);
2117
    m_paneStack->sizePanesEqually();
2118
    checkpointSession();
2119
}    
2120

    
2121
void
2122
MainWindow::paneHidden(Pane *)
2123
{
2124
    checkpointSession();
2125
}    
2126

    
2127
void
2128
MainWindow::paneAboutToBeDeleted(Pane *)
2129
{
2130
}    
2131

    
2132
void
2133
MainWindow::paneDropAccepted(Pane * /* pane */, QStringList uriList)
2134
{
2135
    if (uriList.empty()) return;
2136

    
2137
    QUrl first(uriList[0]);
2138

    
2139
    cerr << "uriList.size() == " << uriList.size() << endl;
2140
    cerr << "first.isLocalFile() == " << first.isLocalFile() << endl;
2141
    cerr << "QFileInfo(first.path()).isDir() == " << QFileInfo(first.path()).isDir() << endl;
2142

    
2143
    m_sessionState = SessionActive;
2144
    
2145
    if (uriList.size() == 1 &&
2146
        first.isLocalFile() &&
2147
        QFileInfo(first.path()).isDir()) {
2148

    
2149
        FileOpenStatus status = openDirOfAudio(first.path());
2150

    
2151
        if (status != FileOpenSucceeded) {
2152
            QMessageBox::critical(this, tr("Failed to open dropped URL"),
2153
                                  tr("<b>Open failed</b><p>Dropped URL \"%1\" could not be opened").arg(uriList[0]));
2154
        }
2155

    
2156
        return;
2157
    }
2158
    
2159
    for (QString uri: uriList) {
2160

    
2161
        FileOpenStatus status = FileOpenFailed;
2162
        
2163
        FileSource source(uri);
2164
        if (source.isAvailable()) {
2165
            source.waitForData();
2166

    
2167
            try {
2168
                status = openAudio(source, CreateAdditionalModel);
2169
            } catch (const InsufficientDiscSpace &e) {
2170
                SVCERR << "MainWindowBase: Caught InsufficientDiscSpace in file open" << endl;
2171
                QMessageBox::critical
2172
                    (this, tr("Not enough disc space"),
2173
                     tr("<b>Not enough disc space</b><p>There doesn't appear to be enough spare disc space to accommodate any necessary temporary files.</p><p>Please clear some space and try again.</p>").arg(e.what()));
2174
                return;
2175
            }
2176
        }
2177
            
2178
        if (status != FileOpenSucceeded) {
2179
            QMessageBox::critical(this, tr("Failed to open dropped URL"),
2180
                                  tr("<b>Open failed</b><p>Dropped audio file location \"%1\" could not be opened").arg(uri));
2181
        } else {
2182
            configureNewPane(m_paneStack->getCurrentPane());
2183
        }
2184
    }
2185
}
2186

    
2187
void
2188
MainWindow::paneDropAccepted(Pane *pane, QString text)
2189
{
2190
    if (pane) m_paneStack->setCurrentPane(pane);
2191

    
2192
    QUrl testUrl(text);
2193
    if (testUrl.scheme() == "file" || 
2194
        testUrl.scheme() == "http" || 
2195
        testUrl.scheme() == "ftp") {
2196
        QStringList list;
2197
        list.push_back(text);
2198
        paneDropAccepted(pane, list);
2199
        return;
2200
    }
2201

    
2202
    //!!! open as text -- but by importing as if a CSV, or just adding
2203
    //to a text layer?
2204
}
2205

    
2206
void
2207
MainWindow::configureNewPane(Pane *pane)
2208
{
2209
    SVCERR << "MainWindow::configureNewPane(" << pane << ")" << endl;
2210

    
2211
    if (!pane) return;
2212

    
2213
    // MainWindowBase::addOpenedAudioModel adds a waveform layer for
2214
    // each additional model besides the main one (assuming that the
2215
    // main one gets it, if needed, from the session template). We
2216
    // don't actually want to use those - we'll be adding our own with
2217
    // specific parameters - but they don't cost much, so rather than
2218
    // remove them, just rename them to something that won't cause
2219
    // confusion with the name-based mode layer handling. NB we have
2220
    // to do this before calling reselectMode(), as that will add
2221
    // another competing waveform layer
2222
    
2223
    Layer *waveformLayer = 0;
2224

    
2225
    for (int i = 0; i < pane->getLayerCount(); ++i) {
2226
        Layer *layer = pane->getLayer(i);
2227
        if (!layer) {
2228
            continue;
2229
        }
2230
        if (dynamic_cast<WaveformLayer *>(layer)) {
2231
            waveformLayer = layer;
2232
        }
2233
        if (dynamic_cast<TimeValueLayer *>(layer)) {
2234
            break;
2235
        }
2236
    }
2237

    
2238
    if (waveformLayer) {
2239
        waveformLayer->setObjectName("Automatically Created - Unused"); // not to be translated
2240
    }
2241

    
2242
    zoomToFit();
2243
    reselectMode();
2244

    
2245
    if (Align::getAlignmentPreference() == Align::NoAlignment) {
2246
        mapAllSalientFeatureLayers();
2247
    }
2248
}
2249

    
2250
void
2251
MainWindow::record()
2252
{
2253
    MainWindowBase::record();
2254
    configureNewPane(m_paneStack->getCurrentPane());
2255
    zoomDefault();
2256
}
2257

    
2258
void
2259
MainWindow::closeEvent(QCloseEvent *e)
2260
{
2261
    if (m_exiting) {
2262
        e->accept();
2263
        return;
2264
    }
2265

    
2266
//    cerr << "MainWindow::closeEvent" << endl;
2267

    
2268
    if (m_openingAudioFile) {
2269
//        cerr << "Busy - ignoring close event" << endl;
2270
        e->ignore();
2271
        return;
2272
    }
2273

    
2274
    closeSession();
2275

    
2276
    QSettings settings;
2277
    settings.beginGroup("MainWindow");
2278
    settings.setValue("maximised", isMaximized());
2279
    if (!isMaximized()) {
2280
        settings.setValue("size", size());
2281
        settings.setValue("position", pos());
2282
    }
2283
    settings.endGroup();
2284

    
2285
    delete m_keyReference;
2286
    m_keyReference = 0;
2287

    
2288
    if (m_preferencesDialog &&
2289
        m_preferencesDialog->isVisible()) {
2290
        closeSession(); // otherwise we'll have to wait for prefs changes
2291
        m_preferencesDialog->applicationClosing(false);
2292
    }
2293

    
2294
    if (m_layerTreeView &&
2295
        m_layerTreeView->isVisible()) {
2296
        delete m_layerTreeView;
2297
    }
2298

    
2299
    e->accept();
2300

    
2301
    m_exiting = true;
2302
    qApp->closeAllWindows();
2303
    
2304
    return;
2305
}
2306

    
2307
bool
2308
MainWindow::checkSaveModified()
2309
{
2310
    // It should always be OK to save, with our active-session paradigm
2311
    return true;
2312
}
2313

    
2314
bool
2315
MainWindow::commitData(bool /* mayAskUser */)
2316
{
2317
    if (m_preferencesDialog &&
2318
        m_preferencesDialog->isVisible()) {
2319
        m_preferencesDialog->applicationClosing(true);
2320
    }
2321
    checkpointSession();
2322
    return true;
2323
}
2324

    
2325
void
2326
MainWindow::preferenceChanged(PropertyContainer::PropertyName name)
2327
{
2328
    MainWindowBase::preferenceChanged(name);
2329
}
2330

    
2331
void
2332
MainWindow::renameCurrentLayer()
2333
{
2334
    Pane *pane = m_paneStack->getCurrentPane();
2335
    if (pane) {
2336
        Layer *layer = pane->getSelectedLayer();
2337
        if (layer) {
2338
            bool ok = false;
2339
            QString newName = QInputDialog::getText
2340
                (this, tr("Rename Layer"),
2341
                 tr("New name for this layer:"),
2342
                 QLineEdit::Normal, layer->objectName(), &ok);
2343
            if (ok) {
2344
                layer->setObjectName(newName);
2345
            }
2346
        }
2347
    }
2348
}
2349

    
2350
void
2351
MainWindow::alignmentTypeChanged()
2352
{
2353
    QAction *action = dynamic_cast<QAction *>(sender());
2354
    
2355
    if (!action || !m_viewManager) return;
2356

    
2357
    Align::AlignmentType alignmentType =
2358
        Align::getAlignmentTypeForTag(action->objectName());
2359

    
2360
    if (alignmentType == Align::ExternalProgramAlignment) {
2361
        if (!approveAlignmentProgram()) {
2362
            return;
2363
        }
2364
    }
2365
        
2366
    m_subsequenceAlignmentAction->setEnabled
2367
        (alignmentType != Align::NoAlignment &&
2368
         alignmentType != Align::LinearAlignment &&
2369
         alignmentType != Align::TrimmedLinearAlignment &&
2370
         alignmentType != Align::ExternalProgramAlignment);
2371

    
2372
    updateAlignmentPreferences(alignmentType,
2373
                               Align::getUseSubsequenceAlignment());
2374
}
2375

    
2376
void
2377
MainWindow::alignmentSubsequenceChanged()
2378
{
2379
    QAction *action = dynamic_cast<QAction *>(sender());
2380
    
2381
    if (!action || !m_viewManager) return;
2382

    
2383
    updateAlignmentPreferences(Align::getAlignmentPreference(),
2384
                               action->isChecked());
2385
}
2386

    
2387
void
2388
MainWindow::updateAlignmentPreferences(Align::AlignmentType alignmentType,
2389
                                       bool subsequence)
2390
{
2391
    m_shownAlignmentError = false;
2392
    
2393
    Align::setAlignmentPreference(alignmentType);
2394
    Align::setUseSubsequenceAlignment(subsequence);
2395
    
2396
    if (alignmentType == Align::NoAlignment) {
2397

    
2398
        m_viewManager->setAlignMode(false);
2399
        m_document->setAutoAlignment(false);
2400

    
2401
        SVDEBUG << "MainWindow::alignmentTypeChanged: type is now NoAlignment, so salient feature layer won't be automatically mapped - doing it by hand" << endl;
2402

    
2403
        mapAllSalientFeatureLayers();
2404
        checkpointSession();
2405

    
2406
    } else {
2407

    
2408
        m_viewManager->setAlignMode(true);
2409
        
2410
        if (alignmentType == m_previousActiveAlignmentType &&
2411
            subsequence == m_previousSubsequence) {
2412
            m_document->alignModels();
2413
        } else {
2414
            m_document->realignModels();
2415
        }
2416

    
2417
        m_document->setAutoAlignment(true);
2418
        m_previousActiveAlignmentType = alignmentType;
2419
        m_previousSubsequence = subsequence;
2420
    }
2421

    
2422
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
2423
        Pane *pane = m_paneStack->getPane(i);
2424
        if (!pane) continue;
2425
        pane->update();
2426
    }
2427
}
2428

    
2429
bool
2430
MainWindow::approveAlignmentProgram()
2431
{
2432
    bool finished = false;
2433
    bool accepted = false;
2434

    
2435
    while (!finished) {
2436
    
2437
        QString currentProgram = Align::getPreferredAlignmentProgram();
2438

    
2439
        QDialog *d = new QDialog(this);
2440
        d->setWindowTitle(tr("External Alignment Program"));
2441

    
2442
        QGridLayout *layout = new QGridLayout;
2443
        d->setLayout(layout);
2444

    
2445
        int row = 0;
2446

    
2447
        if (currentProgram == "") {
2448
            layout->addWidget
2449
                (new QLabel(tr("<p>The following external program will be run in order to calculate time alignments:</p><p><i>No program is currently set.</i></p>")),
2450
                 row++, 0, 1, 3);
2451
        } else {
2452
            layout->addWidget
2453
                (new QLabel(tr("<p>The following external program will be run in order to calculate time alignments:</p><p><small><code>%1</code></small></p>")
2454
                            .arg(XmlExportable::encodeEntities(currentProgram))),
2455
                 row++, 0, 1, 3);
2456
        }
2457

    
2458
        QPushButton *choose = new QPushButton;
2459
        if (currentProgram == "") {
2460
            choose->setText(tr("Choose program..."));
2461
        } else {
2462
            choose->setText(tr("Change..."));
2463
        }
2464
        connect(choose, SIGNAL(clicked()),
2465
                this, SLOT(selectAlignmentProgram()));
2466
        layout->addWidget(choose, row, 0);
2467

    
2468
        if (currentProgram != "") {
2469
            QPushButton *forget = new QPushButton;
2470
            forget->setText(tr("Clear"));
2471
            layout->addWidget(forget, row, 1);
2472
            connect(forget, SIGNAL(clicked()),
2473
                    this, SLOT(clearAlignmentProgram()));
2474
        }
2475

    
2476
        row++;
2477
        layout->setColumnStretch(2, 100);
2478

    
2479
        layout->addWidget
2480
            (new QLabel(tr("<p>Please refer to the documentation for more details.</p>")),
2481
             row++, 0, 1, 3);
2482
    
2483
        QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Ok);
2484
        layout->addWidget(bb, row++, 0, 1, 3);
2485
        connect(bb, SIGNAL(accepted()), d, SLOT(accept()));
2486
        connect(bb, SIGNAL(rejected()), d, SLOT(reject()));
2487
        connect(this, SIGNAL(externalAlignmentProgramChanged()),
2488
                d, SLOT(reject()));
2489

    
2490
        if (d->exec() == QDialog::Accepted) {
2491
            finished = true;
2492
            accepted = true;
2493
        } else {
2494
            if (Align::getPreferredAlignmentProgram() == currentProgram) {
2495
                // unchanged, not accepted - must be explicit cancel
2496
                finished = true;
2497
            }
2498
        }
2499
        
2500
        delete d;
2501
    }
2502

    
2503
    return accepted;
2504
}
2505

    
2506
void
2507
MainWindow::selectAlignmentProgram()
2508
{
2509
    QString currentProgram = Align::getPreferredAlignmentProgram();
2510
    QString newProgram =
2511
        QFileDialog::getOpenFileName(this,
2512
                                     tr("External Alignment Program"),
2513
                                     currentProgram);
2514
    if (newProgram != "") {
2515
        SVCERR << "Setting alignment preference to ExternalProgramAlignment "
2516
               << "with program " << newProgram << endl;
2517
        Align::setAlignmentPreference(Align::ExternalProgramAlignment);
2518
        Align::setPreferredAlignmentProgram(newProgram);
2519
        emit externalAlignmentProgramChanged();
2520
    }
2521
}
2522

    
2523
void
2524
MainWindow::clearAlignmentProgram()
2525
{
2526
    Align::setAlignmentPreference(Align::ExternalProgramAlignment);
2527
    Align::setPreferredAlignmentProgram("");
2528
    emit externalAlignmentProgramChanged();
2529
}
2530
    
2531
void
2532
MainWindow::playSpeedChanged(int position)
2533
{
2534
    PlaySpeedRangeMapper mapper;
2535

    
2536
    double percent = m_playSpeed->mappedValue();
2537
    double factor = mapper.getFactorForValue(percent);
2538

    
2539
//    cerr << "play speed position = " << position << " (range 0-120) percent = " << percent << " factor = " << factor << endl;
2540

    
2541
    int centre = m_playSpeed->defaultValue();
2542

    
2543
    // Percentage is shown to 0dp if >100, to 1dp if <100; factor is
2544
    // shown to 3sf
2545

    
2546
    char pcbuf[30];
2547
    char facbuf[30];
2548
    
2549
    if (position == centre) {
2550
        contextHelpChanged(tr("Playback speed: Normal"));
2551
    } else if (position < centre) {
2552
        sprintf(pcbuf, "%.1f", percent);
2553
        sprintf(facbuf, "%.3g", 1.0 / factor);
2554
        contextHelpChanged(tr("Playback speed: %1% (%2x slower)")
2555
                           .arg(pcbuf)
2556
                           .arg(facbuf));
2557
    } else {
2558
        sprintf(pcbuf, "%.0f", percent);
2559
        sprintf(facbuf, "%.3g", factor);
2560
        contextHelpChanged(tr("Playback speed: %1% (%2x faster)")
2561
                           .arg(pcbuf)
2562
                           .arg(facbuf));
2563
    }
2564

    
2565
    m_playSource->setTimeStretch(1.0 / factor); // factor is a speedup
2566

    
2567
    updateMenuStates();
2568
}
2569

    
2570
void
2571
MainWindow::speedUpPlayback()
2572
{
2573
    int value = m_playSpeed->value();
2574
    value = value + m_playSpeed->pageStep();
2575
    if (value > m_playSpeed->maximum()) value = m_playSpeed->maximum();
2576
    m_playSpeed->setValue(value);
2577
}
2578

    
2579
void
2580
MainWindow::slowDownPlayback()
2581
{
2582
    int value = m_playSpeed->value();
2583
    value = value - m_playSpeed->pageStep();
2584
    if (value < m_playSpeed->minimum()) value = m_playSpeed->minimum();
2585
    m_playSpeed->setValue(value);
2586
}
2587

    
2588
void
2589
MainWindow::restoreNormalPlayback()
2590
{
2591
    m_playSpeed->setValue(m_playSpeed->defaultValue());
2592
}
2593

    
2594
void
2595
MainWindow::updateVisibleRangeDisplay(Pane *p) const
2596
{
2597
    if (!getMainModel() || !p) {
2598
        return;
2599
    }
2600

    
2601
    bool haveSelection = false;
2602
    sv_frame_t startFrame = 0, endFrame = 0;
2603

    
2604
    if (m_viewManager && m_viewManager->haveInProgressSelection()) {
2605

    
2606
        bool exclusive = false;
2607
        Selection s = m_viewManager->getInProgressSelection(exclusive);
2608

    
2609
        if (!s.isEmpty()) {
2610
            haveSelection = true;
2611
            startFrame = s.getStartFrame();
2612
            endFrame = s.getEndFrame();
2613
        }
2614
    }
2615

    
2616
    if (!haveSelection) {
2617
        startFrame = p->getFirstVisibleFrame();
2618
        endFrame = p->getLastVisibleFrame();
2619
    }
2620

    
2621
    RealTime start = RealTime::frame2RealTime
2622
        (startFrame, getMainModel()->getSampleRate());
2623

    
2624
    RealTime end = RealTime::frame2RealTime
2625
        (endFrame, getMainModel()->getSampleRate());
2626

    
2627
    RealTime duration = end - start;
2628

    
2629
    QString startStr, endStr, durationStr;
2630
    startStr = start.toText(true).c_str();
2631
    endStr = end.toText(true).c_str();
2632
    durationStr = duration.toText(true).c_str();
2633

    
2634
    if (haveSelection) {
2635
        m_myStatusMessage = tr("Selection: %1 to %2 (duration %3)")
2636
            .arg(startStr).arg(endStr).arg(durationStr);
2637
    } else {
2638
        m_myStatusMessage = tr("Visible: %1 to %2 (duration %3)")
2639
            .arg(startStr).arg(endStr).arg(durationStr);
2640
    }
2641

    
2642
    statusBar()->showMessage(m_myStatusMessage);
2643
}
2644

    
2645
void
2646
MainWindow::updatePositionStatusDisplays() const
2647
{
2648
    if (!statusBar()->isVisible()) return;
2649

    
2650
}
2651

    
2652
void
2653
MainWindow::monitoringLevelsChanged(float left, float right)
2654
{
2655
    m_mainLevelPan->setMonitoringLevels(left, right);
2656
}
2657

    
2658
void
2659
MainWindow::sampleRateMismatch(sv_samplerate_t requested,
2660
                               sv_samplerate_t actual,
2661
                               bool willResample)
2662
{
2663
    if (!willResample) {
2664
        //!!! more helpful message needed
2665
        QMessageBox::information
2666
            (this, tr("Sample rate mismatch"),
2667
             tr("The sample rate of this audio file (%1 Hz) does not match\nthe current playback rate (%2 Hz).\n\nThe file will play at the wrong speed and pitch.")
2668
             .arg(requested).arg(actual));
2669
    }        
2670

    
2671
    updateDescriptionLabel();
2672
}
2673

    
2674
void
2675
MainWindow::audioOverloadPluginDisabled()
2676
{
2677
    QMessageBox::information
2678
        (this, tr("Audio processing overload"),
2679
         tr("<b>Overloaded</b><p>Audio effects plugin auditioning has been disabled due to a processing overload."));
2680
}
2681

    
2682
void
2683
MainWindow::introDialog()
2684
{
2685
    IntroDialog::show(this);
2686
    
2687
    // and now that's out of the way
2688
    checkForNewerVersion();
2689
}
2690

    
2691
void
2692
MainWindow::checkForNewerVersion()
2693
{
2694
    if (m_networkPermission) {
2695
        SVDEBUG << "Network permission granted: checking for updates" << endl;
2696
        m_versionTester = new VersionTester
2697
            ("sonicvisualiser.org", "latest-vect-version.txt", VECT_VERSION);
2698
        connect(m_versionTester, SIGNAL(newerVersionAvailable(QString)),
2699
                this, SLOT(newerVersionAvailable(QString)));
2700
    } else {
2701
        SVDEBUG << "Network permission not granted: not checking for updates"
2702
                << endl;
2703
    }
2704
}
2705

    
2706
void
2707
MainWindow::betaReleaseWarning()
2708
{
2709
    QMessageBox::information
2710
        (this, tr("Beta release"),
2711
         tr("<b>This is a beta release of %1</b><p>Please see the \"What's New\" option in the Help menu for a list of changes since the last proper release.</p>").arg(QApplication::applicationName()));
2712
}
2713

    
2714
void
2715
MainWindow::layerRemoved(Layer *layer)
2716
{
2717
    MainWindowBase::layerRemoved(layer);
2718
}
2719

    
2720
void
2721
MainWindow::layerInAView(Layer *layer, bool inAView)
2722
{
2723
    MainWindowBase::layerInAView(layer, inAView);
2724
}
2725

    
2726
void
2727
MainWindow::modelAdded(ModelId model)
2728
{
2729
    MainWindowBase::modelAdded(model);
2730
}
2731

    
2732
QString
2733
MainWindow::makeSessionFilename()
2734
{
2735
    auto mainModel = getMainModel();
2736
    if (!mainModel) {
2737
        SVDEBUG << "MainWindow::makeSessionFilename: No main model, returning empty filename" << endl;
2738
        return {};
2739
    }
2740
    
2741
    QDir parentDir(TempDirectory::getInstance()->getContainingPath());
2742
    QString sessionDirName("session");
2743

    
2744
    if (!parentDir.mkpath(sessionDirName)) {
2745
        SVCERR << "ERROR: makeSessionFilename: Failed to create session dir in \"" << parentDir.canonicalPath() << "\"" << endl;
2746
        QMessageBox::critical(this, tr("Failed to create session directory"),
2747
                              tr("<p>Failed to create directory \"%1\" for session files</p>")
2748
                              .arg(parentDir.filePath(sessionDirName)));
2749
        return {};
2750
    }
2751

    
2752
    QDir sessionDir(parentDir.filePath(sessionDirName));
2753
    
2754
    QDateTime now = QDateTime::currentDateTime();
2755
    QString dateDirName = QString("%1").arg(now.toString("yyyyMMdd"));
2756

    
2757
    if (!sessionDir.mkpath(dateDirName)) {
2758
        SVCERR << "ERROR: makeSessionFilename: Failed to create datestamped session dir in \"" << sessionDir.canonicalPath() << "\"" << endl;
2759
        QMessageBox::critical(this, tr("Failed to create session directory"),
2760
                              tr("<p>Failed to create date directory \"%1\" for session files</p>")
2761
                              .arg(sessionDir.filePath(dateDirName)));
2762
        return {};
2763
    }
2764

    
2765
    QDir dateDir(sessionDir.filePath(dateDirName));
2766

    
2767
    QString sessionName = mainModel->getTitle();
2768
    if (sessionName == "") {
2769
        sessionName = mainModel->getLocation();
2770
    }
2771
    sessionName = QFileInfo(sessionName).baseName();
2772
    sessionName.replace(QRegExp("[<>:\"/\\\\|?*\\0000-\\0039()\\[\\]$]"), "_");
2773

    
2774
    QString sessionExt = 
2775
        InteractiveFileFinder::getInstance()->getApplicationSessionExtension();
2776

    
2777
    QString filePath = dateDir.filePath(QString("%1.%2")
2778
                                        .arg(sessionName)
2779
                                        .arg(sessionExt));
2780
    int suffix = 0;
2781
    while (QFile(filePath).exists()) {
2782
        if (++suffix == 100) {
2783
            SVCERR << "ERROR: makeSessionFilename: Failed to come up with unique session filename for " << sessionName << endl;
2784
            QMessageBox::critical(this, tr("Failed to obtain unique filename"),
2785
                                  tr("<p>Failed to obtain a unique filename for session file</p>"));
2786
            return {};
2787
        }
2788
        filePath = dateDir.filePath(QString("%1-%2.%3")
2789
                                    .arg(sessionName)
2790
                                    .arg(suffix)
2791
                                    .arg(sessionExt));
2792
    }
2793

    
2794
    SVDEBUG << "MainWindow::makeSessionFilename: returning "
2795
            << filePath << endl;
2796

    
2797
    return filePath;
2798
}
2799

    
2800
QString
2801
MainWindow::makeSessionLabel()
2802
{
2803
    auto mainModel = getMainModel();
2804
    if (!mainModel) {
2805
        SVDEBUG << "MainWindow::makeSessionFilename: No main model, returning empty filename" << endl;
2806
        return {};
2807
    }
2808

    
2809
    QString sessionName = mainModel->getTitle();
2810
    if (sessionName == "") {
2811
        sessionName = mainModel->getLocation();
2812
        sessionName = QFileInfo(sessionName).baseName();
2813
    }
2814

    
2815
    int paneCount = 1;
2816
    if (m_paneStack) paneCount = m_paneStack->getPaneCount();
2817
    QString label = tr("%1: %n file(s)", "", paneCount).arg(sessionName);
2818
    
2819
    SVDEBUG << "MainWindow::makeSessionLabel: returning "
2820
            << label << endl;
2821

    
2822
    return label;
2823
}
2824

    
2825
void
2826
MainWindow::checkpointSession()
2827
{
2828
    if (m_sessionState == NoSession) {
2829
        SVCERR << "MainWindow::checkpointSession: no current session" << endl;
2830
        return;
2831
    }
2832

    
2833
    if (m_sessionState == SessionLoading) {
2834
        SVCERR << "MainWindow::checkpointSession: session is loading" << endl;
2835
        return;
2836
    }
2837
    
2838
    // This test is necessary, so that we don't get into a nasty loop
2839
    // when checkpointing on closeSession called when opening a new
2840
    // session file
2841
    if (!m_documentModified) {
2842
        SVCERR << "MainWindow::checkpointSession: nothing to save" << endl;
2843
        return;
2844
    }
2845
    
2846
    if (m_sessionFile == "") {
2847
        SVCERR << "MainWindow::checkpointSession: no current session file" << endl;
2848
        return;
2849
    }
2850

    
2851
    QString sessionExt = 
2852
        InteractiveFileFinder::getInstance()->getApplicationSessionExtension();
2853

    
2854
    if (!m_sessionFile.endsWith("." + sessionExt)) {
2855
        // At one point in development we had a nasty situation where
2856
        // we loaded an audio file from the recent files list, then
2857
        // immediately saved the session over the top of it! This is
2858
        // just an additional guard against that kind of thing
2859
        SVCERR << "MainWindow::checkpointSession: suspicious session filename "
2860
               << m_sessionFile << ", not saving to it" << endl;
2861
        return;
2862

    
2863
        //!!! + we should also check that it is actually in our
2864
        //!!! auto-save session directory
2865
    }
2866
    
2867
    SVCERR << "MainWindow::checkpointSession: saving to session file: "
2868
           << m_sessionFile << endl;
2869

    
2870
    SmallSession session(makeSmallSession());
2871

    
2872
    try {
2873
        SmallSession::save(session, m_sessionFile);
2874
        m_recentSessions.addFile(m_sessionFile, makeSessionLabel());
2875
        CommandHistory::getInstance()->documentSaved();
2876
        documentRestored();
2877

    
2878
        QSettings settings;
2879
        settings.beginGroup("MainWindow");
2880
        settings.setValue("lastsession", m_sessionFile);
2881
        settings.endGroup();
2882

    
2883
        SVCERR << "MainWindow::checkpointSession complete" << endl;
2884
        
2885
    } catch (const std::runtime_error &e) {
2886
        SVCERR << "MainWindow::checkpointSession: save failed: "
2887
               << e.what() << endl;
2888
        QMessageBox::critical
2889
            (this, tr("Failed to checkpoint session"),
2890
             tr("<b>Checkpoint failed</b>"
2891
                "<p>Session checkpoint file could not be saved: %1</p>")
2892
             .arg(e.what()));
2893
    }
2894
}
2895

    
2896
SmallSession
2897
MainWindow::makeSmallSession()
2898
{
2899
    SmallSession session;
2900
    if (!m_paneStack) return session;
2901

    
2902
    auto mainModel = getMainModel();
2903
    if (!mainModel) return session;
2904

    
2905
    session.mainFile = mainModel->getLocation();
2906

    
2907
    std::set<QString> alreadyRecorded;
2908
    alreadyRecorded.insert(session.mainFile);
2909
    
2910
    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
2911
        Pane *p = m_paneStack->getPane(i);
2912
        for (int j = 0; j < p->getLayerCount(); ++j) {
2913
            Layer *l = p->getLayer(j);
2914
            auto modelId = l->getModel();
2915
            auto sourceId = l->getSourceModel();
2916
            if (!sourceId.isNone()) {
2917
                modelId = sourceId;
2918
            }
2919
            if (auto wfm = ModelById::getAs<WaveFileModel>(modelId)) {
2920
                QString location = wfm->getLocation();
2921
                if (alreadyRecorded.find(location) == alreadyRecorded.end()) {
2922
                    session.additionalFiles.push_back(location);
2923
                    alreadyRecorded.insert(location);
2924
                }
2925
            }
2926
        }
2927
    }
2928

    
2929
    SVCERR << "MainWindow::makeSmallSession: have " << session.additionalFiles.size() << " non-main model(s)" << endl;
2930
    
2931
    return session;
2932
}
2933

    
2934
void
2935
MainWindow::mainModelChanged(ModelId modelId)
2936
{
2937
    SVDEBUG << "MainWindow::mainModelChanged(" << modelId << ")" << endl;
2938

    
2939
    if (m_sessionState == SessionLoading) {
2940
        SVDEBUG << "MainWindow::mainModelChanged: Session is loading, not (re)making session filename" << endl;
2941
    } else if (modelId.isNone()) {
2942
        SVDEBUG << "MainWindow::mainModelChanged: Null model, not (re)making session filename" << endl;
2943
    } else {
2944
        if (m_sessionState == NoSession) {
2945
            SVDEBUG << "MainWindow::mainModelChanged: Marking session as active" << endl;
2946
            m_sessionState = SessionActive;
2947
        } else {
2948
            SVDEBUG << "MainWindow::mainModelChanged: Session is active" << endl;
2949
        }
2950
        if (m_sessionFile == "") {
2951
            SVDEBUG << "MainWindow::mainModelChanged: No session file set, calling makeSessionFilename" << endl;
2952
            m_sessionFile = makeSessionFilename();
2953
        }
2954
    }
2955
    
2956
    m_salientPending.clear();
2957
    m_salientCalculating = false;
2958

    
2959
    MainWindowBase::mainModelChanged(modelId);
2960

    
2961
    if (m_playTarget || m_audioIO) {
2962
        connect(m_mainLevelPan, SIGNAL(levelChanged(float)),
2963
                this, SLOT(mainModelGainChanged(float)));
2964
        connect(m_mainLevelPan, SIGNAL(panChanged(float)),
2965
                this, SLOT(mainModelPanChanged(float)));
2966
    }
2967

    
2968
    SVDEBUG << "Pane stack pane count = " << m_paneStack->getPaneCount() << endl;
2969

    
2970
    auto model = ModelById::getAs<WaveFileModel>(modelId);
2971
    if (model &&
2972
        m_paneStack &&
2973
        (m_paneStack->getPaneCount() == 0)) {
2974
        
2975
        AddPaneCommand *command = new AddPaneCommand(this);
2976
        CommandHistory::getInstance()->addCommand(command);
2977
        Pane *pane = command->getPane();
2978
        Layer *newLayer =
2979
            m_document->createMainModelLayer(LayerFactory::Waveform);
2980
        newLayer->setObjectName(tr("Outline Waveform"));
2981
        
2982
        bool mono = (model->getChannelCount() == 1);
2983
        
2984
        QString layerPropertyXml =
2985
            QString("<layer scale=\"%1\" channelMode=\"%2\"/>")
2986
            .arg(int(WaveformLayer::MeterScale))
2987
            .arg(int(mono ?
2988
                     WaveformLayer::SeparateChannels :
2989
                     WaveformLayer::MergeChannels));
2990
        LayerFactory::getInstance()->setLayerProperties
2991
            (newLayer, layerPropertyXml);
2992
            
2993
        m_document->addLayerToView(pane, newLayer);
2994

    
2995
        addSalientFeatureLayer(pane, modelId);
2996
    }
2997

    
2998
    m_document->setAutoAlignment(m_viewManager->getAlignMode());
2999
}
3000

    
3001
void
3002
MainWindow::mainModelGainChanged(float gain)
3003
{
3004
    if (m_playTarget) {
3005
        m_playTarget->setOutputGain(gain);
3006
    } else if (m_audioIO) {
3007
        m_audioIO->setOutputGain(gain);
3008
    }
3009
}
3010

    
3011
void
3012
MainWindow::mainModelPanChanged(float balance)
3013
{
3014
    // this is indeed stereo balance rather than pan
3015
    if (m_playTarget) {
3016
        m_playTarget->setOutputBalance(balance);
3017
    } else if (m_audioIO) {
3018
        m_audioIO->setOutputBalance(balance);
3019
    }
3020
}
3021

    
3022
void
3023
MainWindow::modelGenerationFailed(QString transformName, QString message)
3024
{
3025
    if (message != "") {
3026

    
3027
        QMessageBox::warning
3028
            (this,
3029
             tr("Failed to generate layer"),
3030
             tr("<b>Layer generation failed</b><p>Failed to generate derived layer.<p>The layer transform \"%1\" failed:<p>%2")
3031
             .arg(transformName).arg(message),
3032
             QMessageBox::Ok);
3033
    } else {
3034
        QMessageBox::warning
3035
            (this,
3036
             tr("Failed to generate layer"),
3037
             tr("<b>Layer generation failed</b><p>Failed to generate a derived layer.<p>The layer transform \"%1\" failed.<p>No error information is available.")
3038
             .arg(transformName),
3039
             QMessageBox::Ok);
3040
    }
3041
}
3042

    
3043
void
3044
MainWindow::modelGenerationWarning(QString /* transformName */, QString message)
3045
{
3046
    QMessageBox::warning
3047
        (this, tr("Warning"), message, QMessageBox::Ok);
3048
}
3049

    
3050
void
3051
MainWindow::modelRegenerationFailed(QString layerName,
3052
                                    QString transformName, QString message)
3053
{
3054
    if (message != "") {
3055

    
3056
        QMessageBox::warning
3057
            (this,
3058
             tr("Failed to regenerate layer"),
3059
             tr("<b>Layer generation failed</b><p>Failed to regenerate derived layer \"%1\" using new data model as input.<p>The layer transform \"%2\" failed:<p>%3")
3060
             .arg(layerName).arg(transformName).arg(message),
3061
             QMessageBox::Ok);
3062
    } else {
3063
        QMessageBox::warning
3064
            (this,
3065
             tr("Failed to regenerate layer"),
3066
             tr("<b>Layer generation failed</b><p>Failed to regenerate derived layer \"%1\" using new data model as input.<p>The layer transform \"%2\" failed.<p>No error information is available.")
3067
             .arg(layerName).arg(transformName),
3068
             QMessageBox::Ok);
3069
    }
3070
}
3071

    
3072
void
3073
MainWindow::modelRegenerationWarning(QString layerName,
3074
                                     QString /* transformName */,
3075
                                     QString message)
3076
{
3077
    QMessageBox::warning
3078
        (this, tr("Warning"), tr("<b>Warning when regenerating layer</b><p>When regenerating the derived layer \"%1\" using new data model as input:<p>%2").arg(layerName).arg(message), QMessageBox::Ok);
3079
}
3080

    
3081
void
3082
MainWindow::alignmentComplete(ModelId amId)
3083
{
3084
    SVCERR << "MainWindow::alignmentComplete(" << amId << ")" << endl;
3085
    auto am = ModelById::getAs<AlignmentModel>(amId);
3086
    if (!am) {
3087
        SVCERR << "MainWindow::alignmentComplete: AlignmentModel is absent!"
3088
               << endl;
3089
        return;
3090
    }
3091
    
3092
    ModelId modelId = am->getAlignedModel();
3093
    mapSalientFeatureLayer(modelId);
3094
    checkpointSession();
3095
}
3096

    
3097
void
3098
MainWindow::alignmentFailed(ModelId, QString message)
3099
{
3100
    if (m_shownAlignmentError) {
3101
        return;
3102
    }
3103

    
3104
    // Set this before showing the dialog, in case we have more than
3105
    // one pending. It will be reset when e.g. alignment type changes
3106
    // or new session started.
3107
    m_shownAlignmentError = true;
3108
        
3109
    QMessageBox::warning
3110
        (this,
3111
         tr("Failed to calculate alignment"),
3112
         tr("<b>Alignment calculation failed</b><p>Failed to calculate an audio alignment:<p>%1")
3113
         .arg(message),
3114
         QMessageBox::Ok);
3115
}
3116

    
3117
void
3118
MainWindow::showLayerTree()
3119
{
3120
    if (!m_layerTreeView.isNull()) {
3121
        m_layerTreeView->show();
3122
        m_layerTreeView->raise();
3123
        return;
3124
    }
3125

    
3126
    //!!! should use an actual dialog class
3127
        
3128
    m_layerTreeView = new QTreeView();
3129
    LayerTreeModel *tree = new LayerTreeModel(m_paneStack);
3130
    m_layerTreeView->resize(500, 300); //!!!
3131
    m_layerTreeView->setModel(tree);
3132
    m_layerTreeView->expandAll();
3133
    m_layerTreeView->show();
3134
}
3135

    
3136
void
3137
MainWindow::handleOSCMessage(const OSCMessage & /* message */)
3138
{
3139
    cerr << "MainWindow::handleOSCMessage: Not implemented" << endl;
3140
}
3141

    
3142
void
3143
MainWindow::preferences()
3144
{
3145
    if (!m_preferencesDialog.isNull()) {
3146
        m_preferencesDialog->show();
3147
        m_preferencesDialog->raise();
3148
        return;
3149
    }
3150

    
3151
    m_preferencesDialog = new PreferencesDialog(this);
3152

    
3153
    // DeleteOnClose is safe here, because m_preferencesDialog is a
3154
    // QPointer that will be zeroed when the dialog is deleted.  We
3155
    // use it in preference to leaving the dialog lying around because
3156
    // if you Cancel the dialog, it resets the preferences state
3157
    // without resetting its own widgets, so its state will be
3158
    // incorrect when next shown unless we construct it afresh
3159
    m_preferencesDialog->setAttribute(Qt::WA_DeleteOnClose);
3160

    
3161
    m_preferencesDialog->show();
3162
}
3163

    
3164
void
3165
MainWindow::mouseEnteredWidget()
3166
{
3167
    QWidget *w = dynamic_cast<QWidget *>(sender());
3168
    if (!w) return;
3169

    
3170
    if (w == m_mainLevelPan) {
3171
        contextHelpChanged(tr("Adjust the master playback level"));
3172
    } else if (w == m_playSpeed) {
3173
        contextHelpChanged(tr("Adjust the master playback speed"));
3174
    }
3175
}
3176

    
3177
void
3178
MainWindow::mouseLeftWidget()
3179
{
3180
    contextHelpChanged("");
3181
}
3182

    
3183
void
3184
MainWindow::website()
3185
{
3186
    openHelpUrl(tr("http://www.sonicvisualiser.org/sonic-lineup/"));
3187
}
3188

    
3189
void
3190
MainWindow::help()
3191
{
3192
    openHelpUrl(tr("http://www.sonicvisualiser.org/sonic-lineup/doc/reference/%1/en/").arg(VECT_VERSION));
3193
}
3194

    
3195
void
3196
MainWindow::whatsNew()
3197
{
3198
    QFile changelog(":CHANGELOG");
3199
    changelog.open(QFile::ReadOnly);
3200
    QByteArray content = changelog.readAll();
3201
    QString text = QString::fromUtf8(content);
3202

    
3203
    QDialog *d = new QDialog(this);
3204
    d->setWindowTitle(tr("What's New"));
3205
        
3206
    QGridLayout *layout = new QGridLayout;
3207
    d->setLayout(layout);
3208

    
3209
    int row = 0;
3210
    
3211
    QLabel *iconLabel = new QLabel;
3212
    iconLabel->setPixmap(QApplication::windowIcon().pixmap(64, 64));
3213
    layout->addWidget(iconLabel, row, 0);
3214
    
3215
    layout->addWidget
3216
        (new QLabel(tr("<h3>What's New in %1</h3>")
3217
                    .arg(QApplication::applicationName())),
3218
         row++, 1);
3219
    layout->setColumnStretch(2, 10);
3220

    
3221
    QTextEdit *textEdit = new QTextEdit;
3222
    layout->addWidget(textEdit, row++, 1, 1, 2);
3223

    
3224
    if (m_newerVersionIs != "") {
3225
        layout->addWidget(new QLabel(tr("<b>Note:</b> A newer version of %1 is available.<br>(Version %2 is available; you are using version %3)").arg(QApplication::applicationName()).arg(m_newerVersionIs).arg(VECT_VERSION)), row++, 1, 1, 2);
3226
    }
3227
    
3228
    QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Ok);
3229
    layout->addWidget(bb, row++, 0, 1, 3);
3230
    connect(bb, SIGNAL(accepted()), d, SLOT(accept()));
3231

    
3232
    text.replace('\r', "");
3233
    text.replace(QRegExp("(.)\n +(.)"), "\\1 \\2");
3234
    text.replace(QRegExp("\n - ([^\n]+)"), "\n<li>\\1</li>");
3235
    text.replace(QRegExp(": *\n"), ":\n<ul>\n");
3236
    text.replace(QRegExp("</li>\n\\s*\n"), "</li>\n</ul>\n\n");
3237
    text.replace(QRegExp("\n(\\w[^:\n]+:)"), "\n<p><b>\\1</b></p>");
3238
//    text.replace(QRegExp("<li>([^,.\n]+)([,.] +\\w)"), "<li><b>\\1</b>\\2");
3239
    
3240
    textEdit->setHtml(text);
3241
    textEdit->setReadOnly(true);
3242

    
3243
    d->setMinimumSize(m_viewManager->scalePixelSize(520),
3244
                      m_viewManager->scalePixelSize(450));
3245
    
3246
    d->exec();
3247

    
3248
    delete d;
3249
}
3250

    
3251
QString
3252
MainWindow::getReleaseText() const
3253
{
3254
    bool debug = false;
3255
    QString version = "(unknown version)";
3256

    
3257
#ifdef BUILD_DEBUG
3258
    debug = true;
3259
#endif // BUILD_DEBUG
3260
#ifdef VECT_VERSION
3261
#ifdef SVNREV
3262
    version = tr("Release %1 : Revision %2").arg(VECT_VERSION).arg(SVNREV);
3263
#else // !SVNREV
3264
    version = tr("Release %1").arg(VECT_VERSION);
3265
#endif // SVNREV
3266
#else // !VECT_VERSION
3267
#ifdef SVNREV
3268
    version = tr("Unreleased : Revision %1").arg(SVNREV);
3269
#endif // SVNREV
3270
#endif // VECT_VERSION
3271

    
3272
    return tr("%1 : %2 configuration, %3-bit build")
3273
        .arg(version)
3274
        .arg(debug ? tr("Debug") : tr("Release"))
3275
        .arg(sizeof(void *) * 8);
3276
}
3277

    
3278
void
3279
MainWindow::about()
3280
{
3281
    QString aboutText;
3282

    
3283
    aboutText += tr("<h3>About Sonic Lineup</h3>");
3284
    aboutText += tr("<p>Sonic Lineup is an application for comparative visualisation and alignment of related audio recordings.<br><a style=\"color: #c1e9f3\" href=\"http://www.sonicvisualiser.org/sonic-lineup/\">http://www.sonicvisualiser.org/sonic-lineup/</a></p>");
3285
    aboutText += QString("<p><small>%1</small></p>").arg(getReleaseText());
3286

    
3287
    aboutText += 
3288
        tr("<p><small>Sonic Lineup and Sonic Visualiser application code<br>Copyright &copy; 2005&ndash;2020 Chris Cannam"
3289
           " and Queen Mary, University of London.</small></p>");
3290

    
3291
    aboutText += 
3292
        tr("<p><small>MATCH Audio Alignment plugin<br>Copyright &copy; "
3293
           "2007&ndash;2020 Simon Dixon, Chris Cannam, and Queen Mary "
3294
           "University of London;<br>Copyright &copy; 2014&ndash;2015 Tido "
3295
           "GmbH.</small></p>");
3296

    
3297
    aboutText += 
3298
        tr("<p><small>NNLS Chroma and Chordino plugin<br>Copyright &copy; "
3299
           "2008&ndash;2019 Matthias Mauch and Queen Mary "
3300
           "University of London.</small></p>");
3301

    
3302
    aboutText += 
3303
        tr("<p><small>pYIN plugin<br>Copyright &copy; "
3304
           "2012&ndash;2019 Matthias Mauch and Queen Mary "
3305
           "University of London.</small></p>");
3306

    
3307
    aboutText += 
3308
        tr("<p><small>QM Key Detector plugin<br>Copyright &copy; "
3309
           "2006&ndash;2019 Katy Noland, Christian Landone, and Queen Mary "
3310
           "University of London.</small></p>");
3311

    
3312
    aboutText += "<p><small>";
3313
    
3314
    aboutText += tr("With Qt v%1 &copy; The Qt Company").arg(QT_VERSION_STR);
3315

    
3316
    aboutText += "</small><small>";
3317

    
3318
#ifdef HAVE_JACK
3319
#ifdef JACK_VERSION
3320
    aboutText += tr("<br>With JACK audio output library v%1 &copy; Paul Davis and Jack O'Quin").arg(JACK_VERSION);
3321
#else // !JACK_VERSION
3322
    aboutText += tr("<br>With JACK audio output library &copy; Paul Davis and Jack O'Quin");
3323
#endif // JACK_VERSION
3324
#endif // HAVE_JACK
3325
#ifdef HAVE_PORTAUDIO
3326
    aboutText += tr("<br>With PortAudio audio output library &copy; Ross Bencina and Phil Burk");
3327
#endif // HAVE_PORTAUDIO
3328
#ifdef HAVE_LIBPULSE
3329
#ifdef LIBPULSE_VERSION
3330
    aboutText += tr("<br>With PulseAudio audio output library v%1 &copy; Lennart Poettering and Pierre Ossman").arg(LIBPULSE_VERSION);
3331
#else // !LIBPULSE_VERSION
3332
    aboutText += tr("<br>With PulseAudio audio output library &copy; Lennart Poettering and Pierre Ossman");
3333
#endif // LIBPULSE_VERSION
3334
#endif // HAVE_LIBPULSE
3335
#ifdef HAVE_OGGZ
3336
#ifdef OGGZ_VERSION
3337
    aboutText += tr("<br>With Ogg file decoder (oggz v%1, fishsound v%2) &copy; CSIRO Australia").arg(OGGZ_VERSION).arg(FISHSOUND_VERSION);
3338
#else // !OGGZ_VERSION
3339
    aboutText += tr("<br>With Ogg file decoder &copy; CSIRO Australia");
3340
#endif // OGGZ_VERSION
3341
#endif // HAVE_OGGZ
3342
#ifdef HAVE_OPUS
3343
    aboutText += tr("<br>With Opus decoder &copy; Xiph.Org Foundation");
3344
#endif // HAVE_OPUS
3345
#ifdef HAVE_MAD
3346
#ifdef MAD_VERSION
3347
    aboutText += tr("<br>With MAD mp3 decoder v%1 &copy; Underbit Technologies Inc").arg(MAD_VERSION);
3348
#else // !MAD_VERSION
3349
    aboutText += tr("<br>With MAD mp3 decoder &copy; Underbit Technologies Inc");
3350
#endif // MAD_VERSION
3351
#endif // HAVE_MAD
3352
#ifdef HAVE_SAMPLERATE
3353
#ifdef SAMPLERATE_VERSION
3354
    aboutText += tr("<br>With libsamplerate v%1 &copy; Erik de Castro Lopo").arg(SAMPLERATE_VERSION);
3355
#else // !SAMPLERATE_VERSION
3356
    aboutText += tr("<br>With libsamplerate &copy; Erik de Castro Lopo");
3357
#endif // SAMPLERATE_VERSION
3358
#endif // HAVE_SAMPLERATE
3359
#ifdef HAVE_SNDFILE
3360
#ifdef SNDFILE_VERSION
3361
    aboutText += tr("<br>With libsndfile v%1 &copy; Erik de Castro Lopo").arg(SNDFILE_VERSION);
3362
#else // !SNDFILE_VERSION
3363
    aboutText += tr("<br>With libsndfile &copy; Erik de Castro Lopo");
3364
#endif // SNDFILE_VERSION
3365
#endif // HAVE_SNDFILE
3366
#ifdef HAVE_FFTW3F
3367
#ifdef FFTW3_VERSION
3368
    aboutText += tr("<br>With FFTW3 v%1 &copy; Matteo Frigo and MIT").arg(FFTW3_VERSION);
3369
#else // !FFTW3_VERSION
3370
    aboutText += tr("<br>With FFTW3 &copy; Matteo Frigo and MIT");
3371
#endif // FFTW3_VERSION
3372
#endif // HAVE_FFTW3F
3373
#ifdef HAVE_RUBBERBAND
3374
#ifdef RUBBERBAND_VERSION
3375
    aboutText += tr("<br>With Rubber Band Library v%1 &copy; Particular Programs Ltd").arg(RUBBERBAND_VERSION);
3376
#else // !RUBBERBAND_VERSION
3377
    aboutText += tr("<br>With Rubber Band Library &copy; Particular Programs Ltd");
3378
#endif // RUBBERBAND_VERSION
3379
#endif // HAVE_RUBBERBAND
3380
    aboutText += tr("<br>With Vamp plugin support (API v%1, host SDK v%2) &copy; Chris Cannam and QMUL").arg(VAMP_API_VERSION).arg(VAMP_SDK_VERSION);
3381
#ifdef REDLAND_VERSION
3382
    aboutText += tr("<br>With Redland RDF datastore v%1 &copy; Dave Beckett and the University of Bristol").arg(REDLAND_VERSION);
3383
#else // !REDLAND_VERSION
3384
    aboutText += tr("<br>With Redland RDF datastore &copy; Dave Beckett and the University of Bristol");
3385
#endif // REDLAND_VERSION
3386
    aboutText += tr("<br>With Serd and Sord RDF parser and store &copy; David Robillard");
3387
    aboutText += "</small></p>";
3388

    
3389
    aboutText += "<p><small>";
3390
    aboutText += tr("Russian UI translation contributed by Alexandre Prokoudine.");
3391
    aboutText += "<br>";
3392
    aboutText += tr("Czech UI translation contributed by Pavel Fric.");
3393
    aboutText += "</small></p>";
3394
    
3395
    aboutText +=
3396
        "<p><small>This program is free software; you can redistribute it and/or "
3397
        "modify it under the terms of the GNU General Public License as "
3398
        "published by the Free Software Foundation; either version 2 of the "
3399
        "License, or (at your option) any later version.<br>See the file "
3400
        "COPYING included with this distribution for more information.</small></p>";
3401

    
3402
    // use our own dialog so we can influence the size
3403

    
3404
    QDialog *d = new QDialog(this);
3405

    
3406
    d->setWindowTitle(tr("About %1").arg(QApplication::applicationName()));
3407
        
3408
    QGridLayout *layout = new QGridLayout;
3409
    d->setLayout(layout);
3410

    
3411
    int row = 0;
3412
    
3413
    QLabel *iconLabel = new QLabel;
3414
    iconLabel->setPixmap(QApplication::windowIcon().pixmap(64, 64));
3415
    layout->addWidget(iconLabel, row, 0, Qt::AlignTop);
3416

    
3417
    QLabel *mainText = new QLabel();
3418
    layout->addWidget(mainText, row, 1, 1, 2);
3419

    
3420
    layout->setRowStretch(row, 10);
3421
    layout->setColumnStretch(1, 10);
3422

    
3423
    ++row;
3424

    
3425
    QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Ok);
3426
    layout->addWidget(bb, row++, 0, 1, 3);
3427
    connect(bb, SIGNAL(accepted()), d, SLOT(accept()));
3428

    
3429
    mainText->setWordWrap(true);
3430
    mainText->setOpenExternalLinks(true);
3431
    mainText->setText(aboutText);
3432

    
3433
    d->setMinimumSize(m_viewManager->scalePixelSize(420),
3434
                      m_viewManager->scalePixelSize(200));
3435
    
3436
    d->exec();
3437

    
3438
    delete d;
3439
}
3440

    
3441
void
3442
MainWindow::keyReference()
3443
{
3444
    m_keyReference->show();
3445
}
3446

    
3447
void
3448
MainWindow::newerVersionAvailable(QString version)
3449
{
3450
    m_newerVersionIs = version;
3451
    
3452
    //!!! nicer URL would be nicer
3453
    QSettings settings;
3454
    settings.beginGroup("NewerVersionWarning");
3455
    QString tag = QString("version-%1-available-show").arg(version);
3456
    if (settings.value(tag, true).toBool()) {
3457
        QString title(tr("Newer version available"));
3458
        QString text(tr("<h3>Newer version available</h3><p>You are using version %1 of %2, but version %3 is now available.</p><p>Please see the <a style=\"color: #c1e9f3\" href=\"http://www.sonicvisualiser.org/sonic-lineup/\">%4 website</a> for more information.</p>").arg(VECT_VERSION).arg(QApplication::applicationName()).arg(version).arg(QApplication::applicationName()));
3459
        QMessageBox::information(this, title, text);
3460
        settings.setValue(tag, false);
3461
    }
3462
    settings.endGroup();
3463
}
3464

    
3465
void
3466
MainWindow::loadStyle()
3467
{
3468
    m_viewManager->setGlobalDarkBackground(true);
3469

    
3470
#ifdef Q_OS_MAC    
3471
    QString stylepath = ":vect-mac.qss";
3472
#else
3473
    QString stylepath = ":vect.qss";
3474
#endif
3475

    
3476
    QFile file(stylepath);
3477
    if (!file.open(QFile::ReadOnly)) {
3478
        SVCERR << "WARNING: Failed to open style file " << stylepath << endl;
3479
    } else {
3480
        QString styleSheet = QLatin1String(file.readAll());
3481
        qApp->setStyleSheet(styleSheet);
3482
        QPalette pal(Qt::white, Qt::gray, Qt::white, Qt::black, Qt::gray, Qt::white, Qt::white, Qt::black, Qt::black);
3483
        qApp->setPalette(pal);
3484
    }
3485
}
3486

    
3487

    
3488