/* * Copyright (c) 2011-2022 Meltytech, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mainwindow.h" #include "ui_mainwindow.h" #include "scrubbar.h" #include "openotherdialog.h" #include "player.h" #include "defaultlayouts.h" #include "widgets/alsawidget.h" #include "widgets/colorbarswidget.h" #include "widgets/colorproducerwidget.h" #include "widgets/countproducerwidget.h" #include "widgets/decklinkproducerwidget.h" #include "widgets/directshowvideowidget.h" #include "widgets/glaxnimateproducerwidget.h" #include "widgets/isingwidget.h" #include "widgets/jackproducerwidget.h" #include "widgets/toneproducerwidget.h" #include "widgets/lissajouswidget.h" #include "widgets/networkproducerwidget.h" #include "widgets/noisewidget.h" #include "widgets/plasmawidget.h" #include "widgets/pulseaudiowidget.h" #include "widgets/video4linuxwidget.h" #include "widgets/x11grabwidget.h" #include "widgets/avformatproducerwidget.h" #include "widgets/imageproducerwidget.h" #include "widgets/blipproducerwidget.h" #include "widgets/newprojectfolder.h" #include "docks/recentdock.h" #include "docks/encodedock.h" #include "docks/jobsdock.h" #include "jobqueue.h" #include "docks/playlistdock.h" #include "glwidget.h" #include "controllers/filtercontroller.h" #include "controllers/scopecontroller.h" #include "docks/filtersdock.h" #include "dialogs/customprofiledialog.h" #include "dialogs/saveimagedialog.h" #include "settings.h" #include "leapnetworklistener.h" #include "database.h" #include "widgets/gltestwidget.h" #include "docks/timelinedock.h" #include "widgets/lumamixtransition.h" #include "qmltypes/qmlutilities.h" #include "qmltypes/qmlapplication.h" #include "autosavefile.h" #include "commands/playlistcommands.h" #include "shotcut_mlt_properties.h" #include "widgets/avfoundationproducerwidget.h" #include "dialogs/textviewerdialog.h" #include "widgets/gdigrabwidget.h" #include "models/audiolevelstask.h" #include "widgets/trackpropertieswidget.h" #include "widgets/timelinepropertieswidget.h" #include "dialogs/unlinkedfilesdialog.h" #include "docks/keyframesdock.h" #include "docks/markersdock.h" #include "docks/notesdock.h" #include "util.h" #include "models/keyframesmodel.h" #include "dialogs/listselectiondialog.h" #include "widgets/textproducerwidget.h" #include "qmltypes/qmlprofile.h" #include "dialogs/longuitask.h" #include "dialogs/systemsyncdialog.h" #include "proxymanager.h" #ifdef Q_OS_WIN #include "windowstools.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool eventDebugCallback(void **data) { QEvent *event = reinterpret_cast(data[1]); if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) { QObject *receiver = reinterpret_cast(data[0]); LOG_DEBUG() << event << "->" << receiver; } else if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease) { QObject *receiver = reinterpret_cast(data[0]); LOG_DEBUG() << event << "->" << receiver; } return false; } static const int AUTOSAVE_TIMEOUT_MS = 60000; static const char *kReservedLayoutPrefix = "__%1"; static const char *kLayoutSwitcherName("layoutSwitcherGrid"); MainWindow::MainWindow() : QMainWindow(0) , ui(new Ui::MainWindow) , m_isKKeyPressed(false) , m_keyerGroup(0) , m_previewScaleGroup(0) , m_keyerMenu(0) , m_isPlaylistLoaded(false) , m_exitCode(EXIT_SUCCESS) , m_navigationPosition(0) , m_upgradeUrl("https://www.shotcut.org/download/") , m_keyframesDock(0) { #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) QLibrary libJack("libjack.so.0"); if (!libJack.load()) { QMessageBox::critical(this, qApp->applicationName(), tr("Error: This program requires the JACK 1 library.\n\nPlease install it using your package manager. It may be named libjack0, jack-audio-connection-kit, jack, or similar.")); ::exit(EXIT_FAILURE); } else { libJack.unload(); } QLibrary libSDL("libSDL2-2.0.so.0"); if (!libSDL.load()) { QMessageBox::critical(this, qApp->applicationName(), tr("Error: This program requires the SDL 2 library.\n\nPlease install it using your package manager. It may be named libsdl2-2.0-0, SDL2, or similar.")); ::exit(EXIT_FAILURE); } else { libSDL.unload(); } #endif connectFocusSignals(); registerDebugCallback(); LOG_DEBUG() << "begin"; LOG_INFO() << "device pixel ratio =" << devicePixelRatioF(); #ifndef Q_OS_WIN new GLTestWidget(this); #endif connect(&m_autosaveTimer, SIGNAL(timeout()), this, SLOT(onAutosaveTimeout())); m_autosaveTimer.start(AUTOSAVE_TIMEOUT_MS); // Initialize all QML types QmlUtilities::registerCommonTypes(); // Create the UI. ui->setupUi(this); #ifdef Q_OS_MAC // Qt 5 on OS X supports the standard Full Screen window widget. ui->actionEnter_Full_Screen->setVisible(false); // OS X has a standard Full Screen shortcut we should use. ui->actionEnter_Full_Screen->setShortcut(QKeySequence((Qt::CTRL + Qt::META + Qt::Key_F))); #endif setDockNestingEnabled(true); ui->statusBar->hide(); connectUISignals(); // Accept drag-n-drop of files. this->setAcceptDrops(true); setupAndConnectUndoStack(); // Add the player widget. setupAndConnectPlayerWidget(); setupSettingsMenu(); setupOpenOtherMenu(); readPlayerSettings(); configureVideoWidget(); setupLayoutSwitcher(); centerLayoutInRemainingToolbarSpace(); #ifndef SHOTCUT_NOUPGRADE if (Settings.noUpgrade() || qApp->property("noupgrade").toBool()) #endif delete ui->actionUpgrade; setupAndConnectDocks(); setupMenuView(); connectVideoWidgetSignals(); readWindowSettings(); setFocus(); setCurrentFile(""); setupAndConnectLeapNetworkListener(); connect(&m_network, SIGNAL(finished(QNetworkReply *)), SLOT(onUpgradeCheckFinished(QNetworkReply *))); resetSourceUpdated(); m_clipboardUpdatedAt.setSecsSinceEpoch(0); onClipboardChanged(); connect(QGuiApplication::clipboard(), SIGNAL(dataChanged()), this, SLOT(onClipboardChanged())); QThreadPool::globalInstance()->setMaxThreadCount(qMin(4, QThreadPool::globalInstance()->maxThreadCount())); ProxyManager::removePending(); LOG_DEBUG() << "end"; } void MainWindow::connectFocusSignals() { if (!qgetenv("OBSERVE_FOCUS").isEmpty()) { connect(qApp, &QApplication::focusChanged, this, &MainWindow::onFocusChanged); connect(qApp, &QGuiApplication::focusObjectChanged, this, &MainWindow::onFocusObjectChanged); connect(qApp, &QGuiApplication::focusWindowChanged, this, &MainWindow::onFocusWindowChanged); } } void MainWindow::registerDebugCallback() { if (!qgetenv("EVENT_DEBUG").isEmpty()) QInternal::registerCallback(QInternal::EventNotifyCallback, eventDebugCallback); } void MainWindow::connectUISignals() { connect(ui->actionOpen, SIGNAL(triggered()), this, SLOT(openVideo())); connect(ui->actionAbout_Qt, SIGNAL(triggered()), qApp, SLOT(aboutQt())); connect(this, &MainWindow::producerOpened, this, &MainWindow::onProducerOpened); connect(ui->mainToolBar, SIGNAL(visibilityChanged(bool)), SLOT(onToolbarVisibilityChanged(bool))); ui->actionSave->setEnabled(false); } void MainWindow::setupAndConnectUndoStack() { m_undoStack = new QUndoStack(this); m_undoStack->setUndoLimit(Settings.undoLimit()); QAction *undoAction = m_undoStack->createUndoAction(this); QAction *redoAction = m_undoStack->createRedoAction(this); undoAction->setIcon(QIcon::fromTheme("edit-undo", QIcon(":/icons/oxygen/32x32/actions/edit-undo.png"))); redoAction->setIcon(QIcon::fromTheme("edit-redo", QIcon(":/icons/oxygen/32x32/actions/edit-redo.png"))); undoAction->setShortcut(QString::fromLatin1("Ctrl+Z")); #ifdef Q_OS_WIN redoAction->setShortcut(QString::fromLatin1("Ctrl+Y")); #else redoAction->setShortcut(QString::fromLatin1("Ctrl+Shift+Z")); #endif ui->menuEdit->insertAction(ui->actionCut, undoAction); ui->menuEdit->insertAction(ui->actionCut, redoAction); ui->menuEdit->insertSeparator(ui->actionCut); ui->actionUndo->setIcon(undoAction->icon()); ui->actionRedo->setIcon(redoAction->icon()); ui->actionUndo->setToolTip(undoAction->toolTip()); ui->actionRedo->setToolTip(redoAction->toolTip()); connect(m_undoStack, SIGNAL(canUndoChanged(bool)), ui->actionUndo, SLOT(setEnabled(bool))); connect(m_undoStack, SIGNAL(canRedoChanged(bool)), ui->actionRedo, SLOT(setEnabled(bool))); } void MainWindow::setupAndConnectPlayerWidget() { m_player = new Player; MLT.videoWidget()->installEventFilter(this); ui->centralWidget->layout()->addWidget(m_player); connect(this, &MainWindow::producerOpened, m_player, &Player::onProducerOpened); connect(m_player, SIGNAL(showStatusMessage(QString)), this, SLOT(showStatusMessage(QString))); connect(m_player, SIGNAL(inChanged(int)), this, SLOT(onCutModified())); connect(m_player, SIGNAL(outChanged(int)), this, SLOT(onCutModified())); connect(m_player, SIGNAL(tabIndexChanged(int)), SLOT(onPlayerTabIndexChanged(int))); connect(MLT.videoWidget(), SIGNAL(started()), SLOT(processMultipleFiles())); connect(MLT.videoWidget(), SIGNAL(paused()), m_player, SLOT(showPaused())); connect(MLT.videoWidget(), SIGNAL(playing()), m_player, SLOT(showPlaying())); connect(MLT.videoWidget(), SIGNAL(toggleZoom(bool)), m_player, SLOT(toggleZoom(bool))); } void MainWindow::setupLayoutSwitcher() { auto group = new QActionGroup(this); group->addAction(ui->actionLayoutLogging); group->addAction(ui->actionLayoutEditing); group->addAction(ui->actionLayoutEffects); group->addAction(ui->actionLayoutAudio); group->addAction(ui->actionLayoutColor); group->addAction(ui->actionLayoutPlayer); switch (Settings.layoutMode()) { case LayoutMode::Custom: break; case LayoutMode::Logging: ui->actionLayoutLogging->setChecked(true); break; case LayoutMode::Editing: ui->actionLayoutEditing->setChecked(true); break; case LayoutMode::Effects: ui->actionLayoutEffects->setChecked(true); break; case LayoutMode::Color: ui->actionLayoutColor->setChecked(true); break; case LayoutMode::Audio: ui->actionLayoutAudio->setChecked(true); break; case LayoutMode::PlayerOnly: ui->actionLayoutPlayer->setChecked(true); break; default: ui->actionLayoutEditing->setChecked(true); break; } } void MainWindow::centerLayoutInRemainingToolbarSpace() { auto spacer = new QWidget; spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); ui->mainToolBar->insertWidget(ui->dummyAction, spacer); spacer = new QWidget; spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); ui->mainToolBar->addWidget(spacer); updateLayoutSwitcher(); } void MainWindow::setupAndConnectDocks() { m_scopeController = new ScopeController(this, ui->menuView); QDockWidget *audioMeterDock = findChild("AudioPeakMeterDock"); if (audioMeterDock) { audioMeterDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_1)); connect(ui->actionAudioMeter, SIGNAL(triggered()), audioMeterDock->toggleViewAction(), SLOT(trigger())); } m_propertiesDock = new QDockWidget(tr("Properties"), this); m_propertiesDock->hide(); m_propertiesDock->setObjectName("propertiesDock"); m_propertiesDock->setWindowIcon(ui->actionProperties->icon()); m_propertiesDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_2)); m_propertiesDock->toggleViewAction()->setIcon(ui->actionProperties->icon()); m_propertiesDock->setMinimumWidth(300); QScrollArea *scroll = new QScrollArea; scroll->setWidgetResizable(true); m_propertiesDock->setWidget(scroll); ui->menuView->addAction(m_propertiesDock->toggleViewAction()); connect(m_propertiesDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onPropertiesDockTriggered(bool))); connect(ui->actionProperties, SIGNAL(triggered()), this, SLOT(onPropertiesDockTriggered())); m_recentDock = new RecentDock(this); m_recentDock->hide(); m_recentDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_3)); ui->menuView->addAction(m_recentDock->toggleViewAction()); connect(m_recentDock, SIGNAL(itemActivated(QString)), this, SLOT(open(QString))); connect(m_recentDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onRecentDockTriggered(bool))); connect(ui->actionRecent, SIGNAL(triggered()), this, SLOT(onRecentDockTriggered())); connect(this, SIGNAL(openFailed(QString)), m_recentDock, SLOT(remove(QString))); connect(m_recentDock, &RecentDock::deleted, m_player->projectWidget(), &NewProjectFolder::updateRecentProjects); m_playlistDock = new PlaylistDock(this); m_playlistDock->hide(); m_playlistDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_4)); ui->menuView->addAction(m_playlistDock->toggleViewAction()); connect(m_playlistDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onPlaylistDockTriggered(bool))); connect(ui->actionPlaylist, SIGNAL(triggered()), this, SLOT(onPlaylistDockTriggered())); connect(m_playlistDock, SIGNAL(clipOpened(Mlt::Producer *, bool)), this, SLOT(openCut(Mlt::Producer *, bool))); connect(m_playlistDock, SIGNAL(itemActivated(int)), this, SLOT(seekPlaylist(int))); connect(m_playlistDock, SIGNAL(showStatusMessage(QString)), this, SLOT(showStatusMessage(QString))); connect(m_playlistDock->model(), SIGNAL(created()), this, SLOT(onPlaylistCreated())); connect(m_playlistDock->model(), SIGNAL(cleared()), this, SLOT(onPlaylistCleared())); connect(m_playlistDock->model(), SIGNAL(closed()), this, SLOT(onPlaylistClosed())); connect(m_playlistDock->model(), SIGNAL(modified()), this, SLOT(onPlaylistModified())); connect(m_playlistDock->model(), SIGNAL(loaded()), this, SLOT(onPlaylistLoaded())); connect(this, SIGNAL(producerOpened()), m_playlistDock, SLOT(onProducerOpened())); if (!Settings.playerGPU()) connect(m_playlistDock->model(), SIGNAL(loaded()), this, SLOT(updateThumbnails())); connect(m_player, &Player::inChanged, m_playlistDock, &PlaylistDock::onInChanged); connect(m_player, &Player::outChanged, m_playlistDock, &PlaylistDock::onOutChanged); connect(m_playlistDock->model(), &PlaylistModel::inChanged, this, &MainWindow::onPlaylistInChanged); connect(m_playlistDock->model(), &PlaylistModel::outChanged, this, &MainWindow::onPlaylistOutChanged); m_timelineDock = new TimelineDock(this); m_timelineDock->hide(); m_timelineDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_5)); ui->menuView->addAction(m_timelineDock->toggleViewAction()); connect(m_timelineDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onTimelineDockTriggered(bool))); connect(ui->actionTimeline, SIGNAL(triggered()), SLOT(onTimelineDockTriggered())); connect(m_player, SIGNAL(seeked(int)), m_timelineDock, SLOT(onSeeked(int))); connect(m_timelineDock, SIGNAL(seeked(int)), SLOT(seekTimeline(int))); connect(m_timelineDock, SIGNAL(clipClicked()), SLOT(onTimelineClipSelected())); connect(m_timelineDock, SIGNAL(showStatusMessage(QString)), this, SLOT(showStatusMessage(QString))); connect(m_timelineDock->model(), SIGNAL(showStatusMessage(QString)), this, SLOT(showStatusMessage(QString))); connect(m_timelineDock->model(), SIGNAL(created()), SLOT(onMultitrackCreated())); connect(m_timelineDock->model(), SIGNAL(closed()), SLOT(onMultitrackClosed())); connect(m_timelineDock->model(), SIGNAL(modified()), SLOT(onMultitrackModified())); connect(m_timelineDock->model(), SIGNAL(durationChanged()), SLOT(onMultitrackDurationChanged())); connect(m_timelineDock, SIGNAL(clipOpened(Mlt::Producer *)), SLOT(openCut(Mlt::Producer *))); connect(m_timelineDock->model(), &MultitrackModel::seeked, this, &MainWindow::seekTimeline); connect(m_timelineDock->model(), SIGNAL(scaleFactorChanged()), m_player, SLOT(pause())); connect(m_timelineDock->markersModel(), SIGNAL(modified()), SLOT(onMultitrackModified())); connect(m_timelineDock, SIGNAL(selected(Mlt::Producer *)), SLOT(loadProducerWidget(Mlt::Producer *))); connect(m_timelineDock, SIGNAL(selectionChanged()), SLOT(onTimelineSelectionChanged())); connect(m_timelineDock, SIGNAL(clipCopied()), SLOT(onClipCopied())); connect(m_timelineDock, SIGNAL(filteredClicked()), SLOT(onFiltersDockTriggered())); connect(m_playlistDock, SIGNAL(addAllTimeline(Mlt::Playlist *)), SLOT(onTimelineDockTriggered())); connect(m_playlistDock, SIGNAL(addAllTimeline(Mlt::Playlist *, bool)), SLOT(onAddAllToTimeline(Mlt::Playlist *, bool))); connect(m_player, SIGNAL(previousSought()), m_timelineDock, SLOT(seekPreviousEdit())); connect(m_player, SIGNAL(nextSought()), m_timelineDock, SLOT(seekNextEdit())); connect(m_timelineDock, SIGNAL(isRecordingChanged(bool)), m_player, SLOT(onMuteButtonToggled(bool))); m_filterController = new FilterController(this); m_filtersDock = new FiltersDock(m_filterController->metadataModel(), m_filterController->attachedModel(), this); m_filtersDock->setMinimumSize(400, 300); m_filtersDock->hide(); m_filtersDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_6)); ui->menuView->addAction(m_filtersDock->toggleViewAction()); connect(m_filtersDock, SIGNAL(currentFilterRequested(int)), m_filterController, SLOT(setCurrentFilter(int)), Qt::QueuedConnection); connect(m_filtersDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onFiltersDockTriggered(bool))); connect(ui->actionFilters, SIGNAL(triggered()), this, SLOT(onFiltersDockTriggered())); connect(m_filterController, SIGNAL(currentFilterChanged(QmlFilter *, QmlMetadata *, int)), m_filtersDock, SLOT(setCurrentFilter(QmlFilter *, QmlMetadata *, int))); connect(this, SIGNAL(producerOpened()), m_filterController, SLOT(setProducer())); connect(m_filterController->attachedModel(), SIGNAL(changed()), SLOT(onFilterModelChanged())); connect(m_filtersDock, SIGNAL(changed()), SLOT(onFilterModelChanged())); connect(m_filterController, SIGNAL(filterChanged(Mlt::Service *)), m_timelineDock->model(), SLOT(onFilterChanged(Mlt::Service *))); connect(m_filterController->attachedModel(), SIGNAL(addedOrRemoved(Mlt::Producer *)), m_timelineDock->model(), SLOT(filterAddedOrRemoved(Mlt::Producer *))); connect(&QmlApplication::singleton(), SIGNAL(filtersPasted(Mlt::Producer *)), m_timelineDock->model(), SLOT(filterAddedOrRemoved(Mlt::Producer *))); connect(&QmlApplication::singleton(), &QmlApplication::filtersPasted, this, &MainWindow::onProducerModified); connect(m_filterController, SIGNAL(statusChanged(QString)), this, SLOT(showStatusMessage(QString))); connect(m_timelineDock, SIGNAL(fadeInChanged(int)), m_filterController, SLOT(onFadeInChanged())); connect(m_timelineDock, SIGNAL(fadeOutChanged(int)), m_filterController, SLOT(onFadeOutChanged())); connect(m_timelineDock, SIGNAL(selected(Mlt::Producer *)), m_filterController, SLOT(setProducer(Mlt::Producer *))); connect(m_player, SIGNAL(seeked(int)), m_filtersDock, SLOT(onSeeked(int)), Qt::QueuedConnection); connect(m_filtersDock, SIGNAL(seeked(int)), SLOT(seekKeyframes(int))); connect(MLT.videoWidget(), SIGNAL(frameDisplayed(const SharedFrame &)), m_filtersDock, SLOT(onShowFrame(const SharedFrame &))); connect(m_player, SIGNAL(inChanged(int)), m_filtersDock, SIGNAL(producerInChanged(int))); connect(m_player, SIGNAL(outChanged(int)), m_filtersDock, SIGNAL(producerOutChanged(int))); connect(m_player, SIGNAL(inChanged(int)), m_filterController, SLOT(onServiceInChanged(int))); connect(m_player, SIGNAL(outChanged(int)), m_filterController, SLOT(onServiceOutChanged(int))); connect(this, SIGNAL(serviceInChanged(int, Mlt::Service *)), m_filterController, SLOT(onServiceInChanged(int, Mlt::Service *))); connect(this, SIGNAL(serviceOutChanged(int, Mlt::Service *)), m_filterController, SLOT(onServiceOutChanged(int, Mlt::Service *))); m_markersDock = new MarkersDock(this); m_markersDock->hide(); m_markersDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_6)); m_markersDock->setModel(m_timelineDock->markersModel()); ui->menuView->addAction(m_markersDock->toggleViewAction()); connect(m_markersDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onMarkersDockTriggered(bool))); connect(ui->actionMarkers, SIGNAL(triggered()), this, SLOT(onMarkersDockTriggered())); connect(m_markersDock, SIGNAL(seekRequested(int)), SLOT(seekTimeline(int))); connect(m_markersDock, SIGNAL(addRequested()), m_timelineDock, SLOT(createMarker())); connect(m_markersDock, SIGNAL(addAroundSelectionRequested()), m_timelineDock, SLOT(createOrEditSelectionMarker())); connect(m_timelineDock, SIGNAL(markerSeeked(int)), m_markersDock, SLOT(onMarkerSelectionRequest(int))); m_keyframesDock = new KeyframesDock(m_filtersDock->qmlProducer(), this); m_keyframesDock->hide(); m_keyframesDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_7)); ui->menuView->addAction(m_keyframesDock->toggleViewAction()); connect(m_keyframesDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onKeyframesDockTriggered(bool))); connect(ui->actionKeyframes, SIGNAL(triggered()), this, SLOT(onKeyframesDockTriggered())); connect(m_filterController, SIGNAL(currentFilterChanged(QmlFilter *, QmlMetadata *, int)), m_keyframesDock, SLOT(setCurrentFilter(QmlFilter *, QmlMetadata *))); connect(m_keyframesDock, SIGNAL(visibilityChanged(bool)), m_filtersDock->qmlProducer(), SLOT(remakeAudioLevels(bool))); m_historyDock = new QDockWidget(tr("History"), this); m_historyDock->hide(); m_historyDock->setObjectName("historyDock"); m_historyDock->setWindowIcon(ui->actionHistory->icon()); m_historyDock->toggleViewAction()->setIcon(ui->actionHistory->icon()); m_historyDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_8)); m_historyDock->setMinimumWidth(150); ui->menuView->addAction(m_historyDock->toggleViewAction()); connect(m_historyDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onHistoryDockTriggered(bool))); connect(ui->actionHistory, SIGNAL(triggered()), this, SLOT(onHistoryDockTriggered())); QUndoView *undoView = new QUndoView(m_undoStack, m_historyDock); undoView->setObjectName("historyView"); undoView->setAlternatingRowColors(true); undoView->setSpacing(2); m_historyDock->setWidget(undoView); ui->actionUndo->setDisabled(true); ui->actionRedo->setDisabled(true); m_encodeDock = new EncodeDock(this); m_encodeDock->hide(); m_encodeDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_9)); ui->menuView->addAction(m_encodeDock->toggleViewAction()); connect(this, SIGNAL(producerOpened()), m_encodeDock, SLOT(onProducerOpened())); connect(ui->actionEncode, SIGNAL(triggered()), this, SLOT(onEncodeTriggered())); connect(ui->actionExportVideo, SIGNAL(triggered()), this, SLOT(onEncodeTriggered())); connect(m_encodeDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onEncodeTriggered(bool))); connect(m_encodeDock, SIGNAL(captureStateChanged(bool)), m_player, SLOT(onCaptureStateChanged(bool))); connect(m_encodeDock, SIGNAL(captureStateChanged(bool)), m_propertiesDock, SLOT(setDisabled(bool))); connect(m_encodeDock, SIGNAL(captureStateChanged(bool)), m_recentDock, SLOT(setDisabled(bool))); connect(m_encodeDock, SIGNAL(captureStateChanged(bool)), m_filtersDock, SLOT(setDisabled(bool))); connect(m_encodeDock, SIGNAL(captureStateChanged(bool)), m_keyframesDock, SLOT(setDisabled(bool))); connect(m_encodeDock, SIGNAL(captureStateChanged(bool)), ui->actionOpen, SLOT(setDisabled(bool))); connect(m_encodeDock, SIGNAL(captureStateChanged(bool)), ui->actionOpenOther, SLOT(setDisabled(bool))); connect(m_encodeDock, SIGNAL(captureStateChanged(bool)), ui->actionExit, SLOT(setDisabled(bool))); connect(m_encodeDock, SIGNAL(captureStateChanged(bool)), this, SLOT(onCaptureStateChanged(bool))); connect(m_encodeDock, SIGNAL(captureStateChanged(bool)), m_historyDock, SLOT(setDisabled(bool))); connect(this, SIGNAL(profileChanged()), m_encodeDock, SLOT(onProfileChanged())); connect(this, SIGNAL(profileChanged()), SLOT(onProfileChanged())); connect(this, SIGNAL(profileChanged()), &QmlProfile::singleton(), SIGNAL(profileChanged())); connect(this, SIGNAL(audioChannelsChanged()), m_encodeDock, SLOT(onAudioChannelsChanged())); connect(m_playlistDock->model(), SIGNAL(modified()), m_encodeDock, SLOT(onProducerOpened())); connect(m_timelineDock, SIGNAL(clipCopied()), m_encodeDock, SLOT(onProducerOpened())); connect(m_timelineDock, SIGNAL(markerRangesChanged()), m_encodeDock, SLOT(onProducerOpened())); m_encodeDock->onProfileChanged(); m_jobsDock = new JobsDock(this); m_jobsDock->hide(); m_jobsDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_0)); ui->menuView->addAction(m_jobsDock->toggleViewAction()); connect(&JOBS, SIGNAL(jobAdded()), m_jobsDock, SLOT(onJobAdded())); connect(m_jobsDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onJobsDockTriggered(bool))); connect(ui->actionJobs, SIGNAL(triggered()), this, SLOT(onJobsDockTriggered())); m_notesDock = new NotesDock(this); m_notesDock->hide(); m_notesDock->toggleViewAction()->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_3)); ui->menuView->insertAction(m_playlistDock->toggleViewAction(), m_notesDock->toggleViewAction()); connect(m_notesDock->toggleViewAction(), SIGNAL(triggered(bool)), this, SLOT(onNotesDockTriggered(bool))); connect(ui->actionNotes, SIGNAL(triggered()), this, SLOT(onNotesDockTriggered())); connect(m_notesDock, SIGNAL(modified()), this, SLOT(onNoteModified())); addDockWidget(Qt::LeftDockWidgetArea, m_propertiesDock); addDockWidget(Qt::RightDockWidgetArea, m_recentDock); addDockWidget(Qt::LeftDockWidgetArea, m_playlistDock); addDockWidget(Qt::BottomDockWidgetArea, m_timelineDock); addDockWidget(Qt::LeftDockWidgetArea, m_filtersDock); addDockWidget(Qt::BottomDockWidgetArea, m_keyframesDock); addDockWidget(Qt::RightDockWidgetArea, m_historyDock); addDockWidget(Qt::LeftDockWidgetArea, m_encodeDock); addDockWidget(Qt::RightDockWidgetArea, m_jobsDock); addDockWidget(Qt::LeftDockWidgetArea, m_notesDock); splitDockWidget(m_timelineDock, m_markersDock, Qt::Horizontal); tabifyDockWidget(m_propertiesDock, m_playlistDock); tabifyDockWidget(m_playlistDock, m_filtersDock); tabifyDockWidget(m_filtersDock, m_encodeDock); tabifyDockWidget(m_encodeDock, m_notesDock); splitDockWidget(m_recentDock, findChild("AudioWaveformDock"), Qt::Vertical); splitDockWidget(audioMeterDock, m_recentDock, Qt::Horizontal); tabifyDockWidget(m_recentDock, m_historyDock); tabifyDockWidget(m_historyDock, m_jobsDock); tabifyDockWidget(m_keyframesDock, m_timelineDock); m_recentDock->raise(); resetDockCorners(); } void MainWindow::setupMenuView() { ui->menuView->addSeparator(); ui->menuView->addAction(ui->actionApplicationLog); } void MainWindow::connectVideoWidgetSignals() { Mlt::GLWidget *videoWidget = (Mlt::GLWidget *) & (MLT); connect(videoWidget, SIGNAL(dragStarted()), m_playlistDock, SLOT(onPlayerDragStarted())); connect(videoWidget, SIGNAL(seekTo(int)), m_player, SLOT(seek(int))); connect(videoWidget, SIGNAL(gpuNotSupported()), this, SLOT(onGpuNotSupported())); connect(videoWidget->quickWindow(), SIGNAL(sceneGraphInitialized()), SLOT(onSceneGraphInitialized()), Qt::QueuedConnection); connect(videoWidget, SIGNAL(frameDisplayed(const SharedFrame &)), m_scopeController, SIGNAL(newFrame(const SharedFrame &))); connect(m_filterController, SIGNAL(currentFilterChanged(QmlFilter *, QmlMetadata *, int)), videoWidget, SLOT(setCurrentFilter(QmlFilter *, QmlMetadata *))); } void MainWindow::setupAndConnectLeapNetworkListener() { LeapNetworkListener *leap = new LeapNetworkListener(this); connect(leap, SIGNAL(shuttle(float)), SLOT(onShuttle(float))); connect(leap, SIGNAL(jogRightFrame()), SLOT(stepRightOneFrame())); connect(leap, SIGNAL(jogRightSecond()), SLOT(stepRightOneSecond())); connect(leap, SIGNAL(jogLeftFrame()), SLOT(stepLeftOneFrame())); connect(leap, SIGNAL(jogLeftSecond()), SLOT(stepLeftOneSecond())); } void MainWindow::onFocusWindowChanged(QWindow *) const { LOG_DEBUG() << "Focuswindow changed"; LOG_DEBUG() << "Current focusWidget:" << QApplication::focusWidget(); LOG_DEBUG() << "Current focusObject:" << QApplication::focusObject(); LOG_DEBUG() << "Current focusWindow:" << QApplication::focusWindow(); } void MainWindow::onFocusObjectChanged(QObject *) const { LOG_DEBUG() << "Focusobject changed"; LOG_DEBUG() << "Current focusWidget:" << QApplication::focusWidget(); LOG_DEBUG() << "Current focusObject:" << QApplication::focusObject(); LOG_DEBUG() << "Current focusWindow:" << QApplication::focusWindow(); } void MainWindow::onTimelineClipSelected() { // Synchronize navigation position with timeline selection. TimelineDock *t = m_timelineDock; if (t->selection().isEmpty()) return; m_navigationPosition = t->centerOfClip(t->selection().first().y(), t->selection().first().x()); // Switch to Project player. if (m_player->tabIndex() != Player::ProjectTabIndex) { t->saveAndClearSelection(); m_player->onTabBarClicked(Player::ProjectTabIndex); } } void MainWindow::onAddAllToTimeline(Mlt::Playlist *playlist, bool skipProxy) { // We stop the player because of a bug on Windows that results in some // strange memory leak when using Add All To Timeline, more noticeable // with (high res?) still image files. if (MLT.isSeekable()) m_player->pause(); else m_player->stop(); m_timelineDock->appendFromPlaylist(playlist, skipProxy); } MainWindow &MainWindow::singleton() { static MainWindow *instance = new MainWindow; return *instance; } MainWindow::~MainWindow() { delete ui; Mlt::Controller::destroy(); } void MainWindow::setupSettingsMenu() { LOG_DEBUG() << "begin"; QActionGroup *group = new QActionGroup(this); group->addAction(ui->actionChannels1); group->addAction(ui->actionChannels2); group->addAction(ui->actionChannels6); group = new QActionGroup(this); group->addAction(ui->actionOneField); group->addAction(ui->actionLinearBlend); m_previewScaleGroup = new QActionGroup(this); m_previewScaleGroup->addAction(ui->actionPreviewNone); m_previewScaleGroup->addAction(ui->actionPreview360); m_previewScaleGroup->addAction(ui->actionPreview540); m_previewScaleGroup->addAction(ui->actionPreview720); //XXX workaround yadif crashing with mlt_transition // group->addAction(ui->actionYadifTemporal); // group->addAction(ui->actionYadifSpatial); ui->actionYadifTemporal->setVisible(false); ui->actionYadifSpatial->setVisible(false); group = new QActionGroup(this); group->addAction(ui->actionNearest); group->addAction(ui->actionBilinear); group->addAction(ui->actionBicubic); group->addAction(ui->actionHyper); if (Settings.playerGPU()) { group = new QActionGroup(this); group->addAction(ui->actionGammaRec709); group->addAction(ui->actionGammaSRGB); } else { delete ui->menuGamma; } m_profileGroup = new QActionGroup(this); m_profileGroup->addAction(ui->actionProfileAutomatic); ui->actionProfileAutomatic->setData(QString()); buildVideoModeMenu(ui->menuProfile, m_customProfileMenu, m_profileGroup, ui->actionAddCustomProfile, ui->actionProfileRemove); // Add the SDI and HDMI devices to the Settings menu. m_externalGroup = new QActionGroup(this); ui->actionExternalNone->setData(QString()); m_externalGroup->addAction(ui->actionExternalNone); QList screens = QGuiApplication::screens(); int n = screens.size(); for (int i = 0; n > 1 && i < n; i++) { QAction *action = new QAction(tr("Screen %1 (%2 x %3 @ %4 Hz)").arg(i) .arg(screens[i]->size().width() * screens[i]->devicePixelRatio()) .arg(screens[i]->size().height() * screens[i]->devicePixelRatio()) .arg(screens[i]->refreshRate()), this); action->setCheckable(true); action->setData(i); m_externalGroup->addAction(action); } Mlt::Profile profile; Mlt::Consumer decklink(profile, "decklink:"); if (decklink.is_valid()) { decklink.set("list_devices", 1); int n = decklink.get_int("devices"); for (int i = 0; i < n; ++i) { QString device(decklink.get(QString("device.%1").arg(i).toLatin1().constData())); if (!device.isEmpty()) { QAction *action = new QAction(device, this); action->setCheckable(true); action->setData(QString("decklink:%1").arg(i)); m_externalGroup->addAction(action); if (!m_keyerGroup) { m_keyerGroup = new QActionGroup(this); action = new QAction(tr("Off"), m_keyerGroup); action->setData(QVariant(0)); action->setCheckable(true); action = new QAction(tr("Internal"), m_keyerGroup); action->setData(QVariant(1)); action->setCheckable(true); action = new QAction(tr("External"), m_keyerGroup); action->setData(QVariant(2)); action->setCheckable(true); } } } } if (m_externalGroup->actions().count() > 1) ui->menuExternal->addActions(m_externalGroup->actions()); else { delete ui->menuExternal; ui->menuExternal = 0; } if (m_keyerGroup) { m_keyerMenu = ui->menuExternal->addMenu(tr("DeckLink Keyer")); m_keyerMenu->addActions(m_keyerGroup->actions()); m_keyerMenu->setDisabled(true); connect(m_keyerGroup, SIGNAL(triggered(QAction *)), this, SLOT(onKeyerTriggered(QAction *))); } connect(m_externalGroup, SIGNAL(triggered(QAction *)), this, SLOT(onExternalTriggered(QAction *))); connect(m_profileGroup, SIGNAL(triggered(QAction *)), this, SLOT(onProfileTriggered(QAction *))); // Setup the language menu actions m_languagesGroup = new QActionGroup(this); QAction *a; a = new QAction(QLocale::languageToString(QLocale::Arabic), m_languagesGroup); a->setCheckable(true); a->setData("ar"); a = new QAction(QLocale::languageToString(QLocale::Catalan), m_languagesGroup); a->setCheckable(true); a->setData("ca"); a = new QAction(QLocale::languageToString(QLocale::Chinese).append(" (China)"), m_languagesGroup); a->setCheckable(true); a->setData("zh_CN"); a = new QAction(QLocale::languageToString(QLocale::Chinese).append(" (Taiwan)"), m_languagesGroup); a->setCheckable(true); a->setData("zh_TW"); a = new QAction(QLocale::languageToString(QLocale::Czech), m_languagesGroup); a->setCheckable(true); a->setData("cs"); a = new QAction(QLocale::languageToString(QLocale::Danish), m_languagesGroup); a->setCheckable(true); a->setData("da"); a = new QAction(QLocale::languageToString(QLocale::Dutch), m_languagesGroup); a->setCheckable(true); a->setData("nl"); a = new QAction(QLocale::languageToString(QLocale::English).append(" (Great Britain)"), m_languagesGroup); a->setCheckable(true); a->setData("en_GB"); a = new QAction(QLocale::languageToString(QLocale::English).append(" (United States)"), m_languagesGroup); a->setCheckable(true); a->setData("en_US"); a = new QAction(QLocale::languageToString(QLocale::Estonian), m_languagesGroup); a->setCheckable(true); a->setData("et"); a = new QAction(QLocale::languageToString(QLocale::Finnish), m_languagesGroup); a->setCheckable(true); a->setData("fi"); a = new QAction(QLocale::languageToString(QLocale::French), m_languagesGroup); a->setCheckable(true); a->setData("fr"); a = new QAction(QLocale::languageToString(QLocale::Gaelic), m_languagesGroup); a->setCheckable(true); a->setData("gd"); a = new QAction(QLocale::languageToString(QLocale::Galician), m_languagesGroup); a->setCheckable(true); a->setData("gl"); a = new QAction(QLocale::languageToString(QLocale::German), m_languagesGroup); a->setCheckable(true); a->setData("de"); a = new QAction(QLocale::languageToString(QLocale::Greek), m_languagesGroup); a->setCheckable(true); a->setData("el"); a = new QAction(QLocale::languageToString(QLocale::Hungarian), m_languagesGroup); a->setCheckable(true); a->setData("hu"); a = new QAction(QLocale::languageToString(QLocale::Italian), m_languagesGroup); a->setCheckable(true); a->setData("it"); a = new QAction(QLocale::languageToString(QLocale::Japanese), m_languagesGroup); a->setCheckable(true); a->setData("ja"); a = new QAction(QLocale::languageToString(QLocale::Korean), m_languagesGroup); a->setCheckable(true); a->setData("ko"); a = new QAction(QLocale::languageToString(QLocale::Nepali), m_languagesGroup); a->setCheckable(true); a->setData("ne"); a = new QAction(QLocale::languageToString(QLocale::NorwegianBokmal), m_languagesGroup); a->setCheckable(true); a->setData("nb"); a = new QAction(QLocale::languageToString(QLocale::NorwegianNynorsk), m_languagesGroup); a->setCheckable(true); a->setData("nn"); a = new QAction(QLocale::languageToString(QLocale::Occitan), m_languagesGroup); a->setCheckable(true); a->setData("oc"); a = new QAction(QLocale::languageToString(QLocale::Polish), m_languagesGroup); a->setCheckable(true); a->setData("pl"); a = new QAction(QLocale::languageToString(QLocale::Portuguese).append(" (Brazil)"), m_languagesGroup); a->setCheckable(true); a->setData("pt_BR"); a = new QAction(QLocale::languageToString(QLocale::Portuguese).append(" (Portugal)"), m_languagesGroup); a->setCheckable(true); a->setData("pt_PT"); a = new QAction(QLocale::languageToString(QLocale::Romanian), m_languagesGroup); a->setCheckable(true); a->setData("ro"); a = new QAction(QLocale::languageToString(QLocale::Russian), m_languagesGroup); a->setCheckable(true); a->setData("ru"); a = new QAction(QLocale::languageToString(QLocale::Slovak), m_languagesGroup); a->setCheckable(true); a->setData("sk"); a = new QAction(QLocale::languageToString(QLocale::Slovenian), m_languagesGroup); a->setCheckable(true); a->setData("sl"); a = new QAction(QLocale::languageToString(QLocale::Spanish), m_languagesGroup); a->setCheckable(true); a->setData("es"); a = new QAction(QLocale::languageToString(QLocale::Swedish), m_languagesGroup); a->setCheckable(true); a->setData("sv"); a = new QAction(QLocale::languageToString(QLocale::Thai), m_languagesGroup); a->setCheckable(true); a->setData("th"); a = new QAction(QLocale::languageToString(QLocale::Turkish), m_languagesGroup); a->setCheckable(true); a->setData("tr"); a = new QAction(QLocale::languageToString(QLocale::Ukrainian), m_languagesGroup); a->setCheckable(true); a->setData("uk"); ui->menuLanguage->addActions(m_languagesGroup->actions()); const QString locale = Settings.language(); foreach (QAction *action, m_languagesGroup->actions()) { if (action->data().toString().startsWith(locale)) { action->setChecked(true); break; } } connect(m_languagesGroup, SIGNAL(triggered(QAction *)), this, SLOT(onLanguageTriggered(QAction *))); // Setup the themes actions group = new QActionGroup(this); group->addAction(ui->actionSystemTheme); group->addAction(ui->actionFusionDark); group->addAction(ui->actionFusionLight); if (Settings.theme() == "dark") ui->actionFusionDark->setChecked(true); else if (Settings.theme() == "light") ui->actionFusionLight->setChecked(true); else ui->actionSystemTheme->setChecked(true); #if defined(Q_OS_WIN) || (defined(Q_OS_MAC) && defined(Q_PROCESSOR_ARM)) // On Windows, if there is no JACK or it is not running // then Shotcut crashes inside MLT's call to jack_client_open(). // Therefore, the JACK option for Shotcut is banned on Windows. delete ui->actionJack; ui->actionJack = 0; #endif #if !defined(Q_OS_MAC) // Setup the display method actions. if (!Settings.playerGPU()) { group = new QActionGroup(this); #if defined(Q_OS_WIN) ui->actionDrawingAutomatic->setData(0); group->addAction(ui->actionDrawingAutomatic); ui->actionDrawingDirectX->setData(Qt::AA_UseOpenGLES); group->addAction(ui->actionDrawingDirectX); #else delete ui->actionDrawingAutomatic; delete ui->actionDrawingDirectX; #endif ui->actionDrawingOpenGL->setData(Qt::AA_UseDesktopOpenGL); group->addAction(ui->actionDrawingOpenGL); ui->actionDrawingSoftware->setData(Qt::AA_UseSoftwareOpenGL); group->addAction(ui->actionDrawingSoftware); connect(group, SIGNAL(triggered(QAction *)), this, SLOT(onDrawingMethodTriggered(QAction *))); switch (Settings.drawMethod()) { case Qt::AA_UseDesktopOpenGL: ui->actionDrawingOpenGL->setChecked(true); break; #if defined(Q_OS_WIN) case Qt::AA_UseOpenGLES: ui->actionDrawingDirectX->setChecked(true); break; #endif case Qt::AA_UseSoftwareOpenGL: ui->actionDrawingSoftware->setChecked(true); break; #if defined(Q_OS_WIN) default: ui->actionDrawingAutomatic->setChecked(true); break; #else default: ui->actionDrawingOpenGL->setChecked(true); break; #endif } } else { // GPU mode only works with OpenGL. delete ui->menuDrawingMethod; ui->menuDrawingMethod = 0; } #else // Q_OS_MAC delete ui->menuDrawingMethod; ui->menuDrawingMethod = 0; #endif // Setup the job priority actions group = new QActionGroup(this); group->addAction(ui->actionJobPriorityLow); group->addAction(ui->actionJobPriorityNormal); if (Settings.jobPriority() == QThread::LowPriority) ui->actionJobPriorityLow->setChecked(true); else ui->actionJobPriorityNormal->setChecked(true); // Add custom layouts to View > Layout submenu. m_layoutGroup = new QActionGroup(this); connect(m_layoutGroup, SIGNAL(triggered(QAction *)), SLOT(onLayoutTriggered(QAction *))); if (Settings.layouts().size() > 0) { ui->menuLayout->addAction(ui->actionLayoutRemove); ui->menuLayout->addSeparator(); } foreach (QString name, Settings.layouts()) ui->menuLayout->addAction(addLayout(m_layoutGroup, name)); if (qApp->property("clearRecent").toBool()) { ui->actionClearRecentOnExit->setVisible(false); Settings.setRecent(QStringList()); Settings.setClearRecent(true); } else { ui->actionClearRecentOnExit->setChecked(Settings.clearRecent()); } // Initialize the proxy submenu ui->actionUseProxy->setChecked(Settings.proxyEnabled()); ui->actionProxyUseProjectFolder->setChecked(Settings.proxyUseProjectFolder()); ui->actionProxyUseHardware->setChecked(Settings.proxyUseHardware()); LOG_DEBUG() << "end"; } void MainWindow::setupOpenOtherMenu() { // Open Other toolbar menu button QScopedPointer mltProducers(MLT.repository()->producers()); QScopedPointer mltFilters(MLT.repository()->filters()); QMenu *otherMenu = new QMenu(this); ui->actionOpenOther2->setMenu(otherMenu); // populate the generators if (mltProducers->get_data("color")) { otherMenu->addAction(tr("Color"), this, SLOT(onOpenOtherTriggered()))->setObjectName("color"); if (!Settings.playerGPU() && mltProducers->get_data("qtext") && mltFilters->get_data("dynamictext")) otherMenu->addAction(tr("Text"), this, SLOT(onOpenOtherTriggered()))->setObjectName("text"); } if (mltProducers->get_data("glaxnimate")) otherMenu->addAction(tr("Animation"), this, SLOT(onOpenOtherTriggered()))->setObjectName("glaxnimate"); if (mltProducers->get_data("noise")) otherMenu->addAction(tr("Noise"), this, SLOT(onOpenOtherTriggered()))->setObjectName("noise"); if (mltProducers->get_data("frei0r.ising0r")) otherMenu->addAction(tr("Ising"), this, SLOT(onOpenOtherTriggered()))->setObjectName("ising0r"); if (mltProducers->get_data("frei0r.lissajous0r")) otherMenu->addAction(tr("Lissajous"), this, SLOT(onOpenOtherTriggered()))->setObjectName("lissajous0r"); if (mltProducers->get_data("frei0r.plasma")) otherMenu->addAction(tr("Plasma"), this, SLOT(onOpenOtherTriggered()))->setObjectName("plasma"); if (mltProducers->get_data("frei0r.test_pat_B")) otherMenu->addAction(tr("Color Bars"), this, SLOT(onOpenOtherTriggered()))->setObjectName("test_pat_B"); if (mltProducers->get_data("tone")) otherMenu->addAction(tr("Audio Tone"), this, SLOT(onOpenOtherTriggered()))->setObjectName("tone"); if (mltProducers->get_data("count")) otherMenu->addAction(tr("Count"), this, SLOT(onOpenOtherTriggered()))->setObjectName("count"); if (mltProducers->get_data("blipflash")) otherMenu->addAction(tr("Blip Flash"), this, SLOT(onOpenOtherTriggered()))->setObjectName("blipflash"); #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) otherMenu->addAction(tr("Video4Linux"), this, SLOT(onOpenOtherTriggered()))->setObjectName("v4l2"); otherMenu->addAction(tr("PulseAudio"), this, SLOT(onOpenOtherTriggered()))->setObjectName("pulse"); otherMenu->addAction(tr("JACK Audio"), this, SLOT(onOpenOtherTriggered()))->setObjectName("jack"); otherMenu->addAction(tr("ALSA Audio"), this, SLOT(onOpenOtherTriggered()))->setObjectName("alsa"); #elif defined(Q_OS_WIN) || defined(Q_OS_MAC) otherMenu->addAction(tr("Audio/Video Device"), this, SLOT(onOpenOtherTriggered()))->setObjectName("device"); #endif if (mltProducers->get_data("decklink")) otherMenu->addAction(tr("SDI/HDMI"), this, SLOT(onOpenOtherTriggered()))->setObjectName("decklink"); } QAction *MainWindow::addProfile(QActionGroup *actionGroup, const QString &desc, const QString &name) { QAction *action = new QAction(desc, this); action->setCheckable(true); action->setData(name); actionGroup->addAction(action); return action; } QAction *MainWindow::addLayout(QActionGroup *actionGroup, const QString &name) { QAction *action = new QAction(name, this); actionGroup->addAction(action); return action; } void MainWindow::open(Mlt::Producer *producer) { if (!producer->is_valid()) showStatusMessage(tr("Failed to open ")); else if (producer->get_int("error")) showStatusMessage(tr("Failed to open ") + producer->get("resource")); bool ok = false; int screen = Settings.playerExternal().toInt(&ok); if (ok && screen != QApplication::desktop()->screenNumber(this)) m_player->moveVideoToScreen(screen); // no else here because open() will delete the producer if open fails if (!MLT.setProducer(producer)) { emit producerOpened(); if (!MLT.profile().is_explicit() || MLT.URL().endsWith(".mlt") || MLT.URL().endsWith(".xml")) emit profileChanged(); } m_player->setFocus(); m_playlistDock->setUpdateButtonEnabled(false); // Needed on Windows. Upon first file open, window is deactivated, perhaps OpenGL-related. activateWindow(); } bool MainWindow::isCompatibleWithGpuMode(MltXmlChecker &checker) { if (checker.needsGPU() && !Settings.playerGPU()) { LOG_INFO() << "file uses GPU but GPU not enabled"; QMessageBox dialog(QMessageBox::Warning, qApp->applicationName(), tr("The file you opened uses GPU effects, but GPU effects are not enabled.\n\n" "GPU effects are EXPERIMENTAL, UNSTABLE and UNSUPPORTED! Unsupported means do not report bugs about it."), QMessageBox::Ok, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::Ok); dialog.setEscapeButton(QMessageBox::Ok); dialog.exec(); return false; } else if (checker.needsCPU() && Settings.playerGPU()) { LOG_INFO() << "file uses GPU incompatible filters but GPU is enabled"; QMessageBox dialog(QMessageBox::Question, qApp->applicationName(), tr("The file you opened uses CPU effects that are incompatible with GPU effects, but GPU effects are enabled.\n" "Do you want to disable GPU effects and restart?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); int r = dialog.exec(); if (r == QMessageBox::Yes) { ui->actionGPU->setChecked(false); m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } return false; } return true; } bool MainWindow::saveRepairedXmlFile(MltXmlChecker &checker, QString &fileName) { QFileInfo fi(fileName); auto filename = QString("%1/%2 - %3.%4").arg(fi.path()) .arg(fi.completeBaseName()).arg(tr("Repaired")).arg(fi.suffix()); auto caption = tr("Save Repaired XML"); filename = QFileDialog::getSaveFileName(this, caption, filename, tr("MLT XML (*.mlt)"), nullptr, Util::getFileDialogOptions()); if (!filename.isEmpty()) { QFile repaired(filename); repaired.open(QIODevice::WriteOnly); LOG_INFO() << "repaired MLT XML file name" << repaired.fileName(); if (checker.tempFile().exists()) { checker.tempFile().open(); QByteArray xml = checker.tempFile().readAll(); checker.tempFile().close(); qint64 n = repaired.write(xml); while (n > 0 && n < xml.size()) { qint64 x = repaired.write(xml.right(xml.size() - n)); if (x > 0) n += x; else n = x; } repaired.close(); if (n == xml.size()) { fileName = repaired.fileName(); return true; } } QMessageBox::warning(this, qApp->applicationName(), tr("Repairing the project failed.")); LOG_WARNING() << "repairing failed"; } return false; } bool MainWindow::isXmlRepaired(MltXmlChecker &checker, QString &fileName) { bool result = true; if (checker.isCorrected()) { LOG_WARNING() << fileName; QMessageBox dialog(QMessageBox::Question, qApp->applicationName(), tr("Shotcut noticed some problems in your project.\n" "Do you want Shotcut to try to repair it?\n\n" "If you choose Yes, Shotcut will create a copy of your project\n" "with \"- Repaired\" in the file name and open it."), QMessageBox::No | QMessageBox::Yes, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); int r = dialog.exec(); if (r == QMessageBox::Yes) result = saveRepairedXmlFile(checker, fileName); } else if (checker.unlinkedFilesModel().rowCount() > 0) { UnlinkedFilesDialog dialog(this); dialog.setModel(checker.unlinkedFilesModel()); dialog.setWindowModality(QmlApplication::dialogModality()); if (dialog.exec() == QDialog::Accepted) { if (checker.check(fileName) == QXmlStreamReader::NoError && checker.isCorrected()) result = saveRepairedXmlFile(checker, fileName); } else { result = false; } } return result; } bool MainWindow::checkAutoSave(QString &url) { QMutexLocker locker(&m_autosaveMutex); // check whether autosave files exist: QSharedPointer stale(AutoSaveFile::getFile(url)); if (stale) { QMessageBox dialog(QMessageBox::Question, qApp->applicationName(), tr("Auto-saved files exist. Do you want to recover them now?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); int r = dialog.exec(); if (r == QMessageBox::Yes) { if (!stale->open(QIODevice::ReadWrite)) { LOG_WARNING() << "failed to recover autosave file" << url; } else { m_autosaveFile = stale; url = stale->fileName(); return true; } } } // create new autosave object m_autosaveFile.reset(new AutoSaveFile(url)); return false; } void MainWindow::stepLeftBySeconds(int sec) { m_player->seek(m_player->position() + sec * qRound(MLT.profile().fps())); } void MainWindow::doAutosave() { QMutexLocker locker(&m_autosaveMutex); if (m_autosaveFile) { bool success = false; if (m_autosaveFile->isOpen() || m_autosaveFile->open(QIODevice::ReadWrite)) { m_autosaveFile->close(); success = saveXML(m_autosaveFile->fileName(), false /* without relative paths */); m_autosaveFile->open(QIODevice::ReadWrite); } if (!success) { LOG_ERROR() << "failed to open autosave file for writing" << m_autosaveFile->fileName(); } } } void MainWindow::setFullScreen(bool isFullScreen) { if (isFullScreen) { #ifdef Q_OS_WIN showMaximized(); #else showFullScreen(); #endif ui->actionEnter_Full_Screen->setVisible(false); } } QString MainWindow::untitledFileName() const { QDir dir = Settings.appDataLocation(); if (!dir.exists()) dir.mkpath(dir.path()); return dir.filePath("__untitled__.mlt"); } void MainWindow::setProfile(const QString &profile_name) { LOG_DEBUG() << profile_name; MLT.setProfile(profile_name); emit profileChanged(); } bool MainWindow::isSourceClipMyProject(QString resource, bool withDialog) { if (m_player->tabIndex() == Player::ProjectTabIndex && MLT.savedProducer() && MLT.savedProducer()->is_valid()) resource = QString::fromUtf8(MLT.savedProducer()->get("resource")); if (!resource.isEmpty() && QDir(resource) == QDir(fileName())) { if (withDialog) { QMessageBox dialog(QMessageBox::Information, qApp->applicationName(), tr("You cannot add a project to itself!"), QMessageBox::Ok, this); dialog.setDefaultButton(QMessageBox::Ok); dialog.setEscapeButton(QMessageBox::Ok); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.exec(); } return true; } return false; } bool MainWindow::keyframesDockIsVisible() const { return m_keyframesDock && m_keyframesDock->isVisible(); } void MainWindow::setAudioChannels(int channels) { LOG_DEBUG() << channels; MLT.videoWidget()->setProperty("audio_channels", channels); MLT.setAudioChannels(channels); if (channels == 1) ui->actionChannels1->setChecked(true); else if (channels == 2) ui->actionChannels2->setChecked(true); else if (channels == 6) ui->actionChannels6->setChecked(true); emit audioChannelsChanged(); } void MainWindow::showSaveError() { QMessageBox dialog(QMessageBox::Critical, qApp->applicationName(), tr("There was an error saving. Please try again."), QMessageBox::Ok, this); dialog.setDefaultButton(QMessageBox::Ok); dialog.setEscapeButton(QMessageBox::Ok); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.exec(); } void MainWindow::setPreviewScale(int scale) { LOG_DEBUG() << scale; switch (scale) { case 360: ui->actionPreview360->setChecked(true); break; case 540: ui->actionPreview540->setChecked(true); break; case 720: ui->actionPreview720->setChecked(true); break; default: ui->actionPreviewNone->setChecked(true); break; } MLT.setPreviewScale(scale); MLT.refreshConsumer(); } void MainWindow::setVideoModeMenu() { // Find a matching video mode for (const auto action : m_profileGroup->actions()) { auto s = action->data().toString(); Mlt::Profile profile(s.toUtf8().constData()); if (MLT.profile().width() == profile.width() && MLT.profile().height() == profile.height() && MLT.profile().sample_aspect_num() == profile.sample_aspect_num() && MLT.profile().sample_aspect_den() == profile.sample_aspect_den() && MLT.profile().frame_rate_num() == profile.frame_rate_num() && MLT.profile().frame_rate_den() == profile.frame_rate_den() && MLT.profile().colorspace() == profile.colorspace() && MLT.profile().progressive() == profile.progressive()) { // Select it action->setChecked(true); return; } } // Choose Automatic if nothing found m_profileGroup->actions().first()->setChecked(true); } void MainWindow::resetVideoModeMenu() { // Change selected Video Mode back to Settings for (const auto action : m_profileGroup->actions()) { if (action->data().toString() == Settings.playerProfile()) { action->setChecked(true); break; } } } void MainWindow::resetDockCorners() { setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); } void MainWindow::showIncompatibleProjectMessage(const QString &shotcutVersion) { LOG_INFO() << shotcutVersion; QMessageBox dialog(QMessageBox::Information, qApp->applicationName(), tr("This project file requires a newer version!\n\n" "It was made with version ") + shotcutVersion, QMessageBox::Ok, this); dialog.setDefaultButton(QMessageBox::Ok); dialog.setEscapeButton(QMessageBox::Ok); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.exec(); } static void autosaveTask(MainWindow *p) { LOG_DEBUG_TIME(); p->doAutosave(); } void MainWindow::onAutosaveTimeout() { if (isWindowModified()) { QtConcurrent::run(autosaveTask, this); } if (Util::isMemoryLow()) { MLT.pause(); QMessageBox dialog(QMessageBox::Critical, qApp->applicationName(), tr("You are running low on available memory!\n\n" "Please close other applications or web browser tabs and retry.\n" "Or save and restart Shotcut."), QMessageBox::Retry | QMessageBox::Save | QMessageBox::Ignore, this); dialog.setDefaultButton(QMessageBox::Retry); dialog.setEscapeButton(QMessageBox::Ignore); dialog.setWindowModality(QmlApplication::dialogModality()); switch (dialog.exec()) { case QMessageBox::Save: on_actionSave_triggered(); m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); break; case QMessageBox::Retry: onAutosaveTimeout(); break; default: break; } } } void MainWindow::open(QString url, const Mlt::Properties *properties, bool play) { LOG_DEBUG() << url; bool modified = false; MltXmlChecker checker; QFileInfo info(url); if (info.isRelative()) { QDir pwd(QDir::currentPath()); url = pwd.filePath(url); } if (url.endsWith(".mlt") || url.endsWith(".xml")) { if (url != untitledFileName()) { showStatusMessage(tr("Opening %1").arg(url)); QCoreApplication::processEvents(); } switch (checker.check(url)) { case QXmlStreamReader::NoError: if (!isCompatibleWithGpuMode(checker)) { showStatusMessage(tr("Failed to open ").append(url)); return; } break; case QXmlStreamReader::CustomError: showIncompatibleProjectMessage(checker.shotcutVersion()); return; default: showStatusMessage(tr("Failed to open ").append(url)); return; } // only check for a modified project when loading a project, not a simple producer if (!continueModified()) return; QCoreApplication::processEvents(); // close existing project if (playlist()) m_playlistDock->model()->close(); if (multitrack()) m_timelineDock->model()->close(); MLT.purgeMemoryPool(); if (!isXmlRepaired(checker, url)) return; modified = checkAutoSave(url); if (modified) { if (checker.check(url) == QXmlStreamReader::NoError) { if (!isCompatibleWithGpuMode(checker)) return; } else { showStatusMessage(tr("Failed to open ").append(url)); showIncompatibleProjectMessage(checker.shotcutVersion()); return; } if (!isXmlRepaired(checker, url)) return; } // let the new project change the profile if (modified || QFile::exists(url)) { MLT.profile().set_explicit(false); setWindowModified(modified); resetSourceUpdated(); } } if (!playlist() && !multitrack()) { if (!modified && !continueModified()) return; setCurrentFile(""); setWindowModified(modified); sourceUpdated(); MLT.resetURL(); // Return to automatic video mode if selected. if (m_profileGroup->checkedAction() && m_profileGroup->checkedAction()->data().toString().isEmpty()) MLT.profile().set_explicit(false); } if (url.endsWith(".mlt") || url.endsWith(".xml")) { checker.setLocale(); LOG_INFO() << "decimal point" << MLT.decimalPoint(); } QString urlToOpen = checker.isUpdated() ? checker.tempFile().fileName() : url; if (!MLT.open(QDir::fromNativeSeparators(urlToOpen), QDir::fromNativeSeparators(url)) && MLT.producer() && MLT.producer()->is_valid()) { Mlt::Properties *props = const_cast(properties); if (props && props->is_valid()) mlt_properties_inherit(MLT.producer()->get_properties(), props->get_properties()); m_player->setPauseAfterOpen(!play || !MLT.isClip()); setAudioChannels(MLT.audioChannels()); if (url.endsWith(".mlt") || url.endsWith(".xml")) { setVideoModeMenu(); m_notesDock->setText(MLT.producer()->get(kShotcutProjectNote)); } open(MLT.producer()); if (url.startsWith(AutoSaveFile::path())) { QMutexLocker locker(&m_autosaveMutex); if (m_autosaveFile && m_autosaveFile->managedFileName() != untitledFileName()) { m_recentDock->add(m_autosaveFile->managedFileName()); LOG_INFO() << m_autosaveFile->managedFileName(); } } else { m_recentDock->add(url); LOG_INFO() << url; } } else if (url != untitledFileName()) { showStatusMessage(tr("Failed to open ") + url); emit openFailed(url); } } void MainWindow::openMultiple(const QStringList &paths) { if (paths.size() > 1) { QList urls; foreach (const QString &s, paths) urls << s; openMultiple(urls); } else if (!paths.isEmpty()) { open(paths.first()); } } void MainWindow::openMultiple(const QList &urls) { if (urls.size() > 1) { m_multipleFiles = Util::sortedFileList(Util::expandDirectories(urls)); open(m_multipleFiles.first()); } else { QUrl url = urls.first(); open(Util::removeFileScheme(url)); } } void MainWindow::openVideo() { QString path = Settings.openPath(); #ifdef Q_OS_MAC path.append("/*"); #endif LOG_DEBUG() << Util::getFileDialogOptions(); QStringList filenames = QFileDialog::getOpenFileNames(this, tr("Open File"), path, tr("All Files (*);;MLT XML (*.mlt)"), nullptr, Util::getFileDialogOptions()); if (filenames.length() > 0) { Settings.setOpenPath(QFileInfo(filenames.first()).path()); activateWindow(); if (filenames.length() > 1) m_multipleFiles = filenames; open(filenames.first()); } else { // If file invalid, then on some platforms the dialog messes up SDL. MLT.onWindowResize(); activateWindow(); } } void MainWindow::openCut(Mlt::Producer *producer, bool play) { m_player->setPauseAfterOpen(!play); open(producer); MLT.seek(producer->get_in()); } void MainWindow::hideProducer() { // This is a hack to release references to the old producer, but it // probably leaves a reference to the new color producer somewhere not // yet identified (root cause). openCut(new Mlt::Producer(MLT.profile(), "color:_hide")); QCoreApplication::processEvents(); openCut(new Mlt::Producer(MLT.profile(), "color:_hide")); QCoreApplication::processEvents(); QScrollArea *scrollArea = (QScrollArea *) m_propertiesDock->widget(); delete scrollArea->widget(); scrollArea->setWidget(nullptr); m_player->reset(); QCoreApplication::processEvents(); } void MainWindow::closeProducer() { hideProducer(); MLT.stop(); MLT.close(); MLT.setSavedProducer(0); } void MainWindow::showStatusMessage(QAction *action, int timeoutSeconds) { // This object takes ownership of the passed action. // This version does not currently log its message. m_statusBarAction.reset(action); action->setParent(nullptr); m_player->setStatusLabel(action->text(), timeoutSeconds, action); } void MainWindow::showStatusMessage(const QString &message, int timeoutSeconds, QPalette::ColorRole role) { LOG_INFO() << message; auto action = new QAction; connect(action, SIGNAL(triggered()), this, SLOT(onStatusMessageClicked())); m_statusBarAction.reset(action); m_player->setStatusLabel(message, timeoutSeconds, action, role); } void MainWindow::onStatusMessageClicked() { showStatusMessage(QString(), 0); } void MainWindow::seekPlaylist(int start) { if (!playlist()) return; // we bypass this->open() to prevent sending producerOpened signal to self, which causes to reload playlist if (!MLT.producer() || (void *) MLT.producer()->get_producer() != (void *) playlist()->get_playlist()) MLT.setProducer(new Mlt::Producer(*playlist())); m_player->setIn(-1); m_player->setOut(-1); // since we do not emit producerOpened, these components need updating on_actionJack_triggered(ui->actionJack && ui->actionJack->isChecked()); m_player->onProducerOpened(false); m_encodeDock->onProducerOpened(); m_filterController->setProducer(); updateMarkers(); MLT.seek(start); m_player->setFocus(); m_player->switchToTab(Player::ProjectTabIndex); } void MainWindow::seekTimeline(int position, bool seekPlayer) { if (!multitrack()) return; // we bypass this->open() to prevent sending producerOpened signal to self, which causes to reload playlist if (MLT.producer() && (void *) MLT.producer()->get_producer() != (void *) multitrack()->get_producer()) { MLT.setProducer(new Mlt::Producer(*multitrack())); m_player->setIn(-1); m_player->setOut(-1); // since we do not emit producerOpened, these components need updating on_actionJack_triggered(ui->actionJack && ui->actionJack->isChecked()); m_player->onProducerOpened(false); m_encodeDock->onProducerOpened(); m_filterController->setProducer(); updateMarkers(); m_player->setFocus(); m_player->switchToTab(Player::ProjectTabIndex); m_timelineDock->emitSelectedFromSelection(); } if (seekPlayer) m_player->seek(position); else m_player->pause(); } void MainWindow::seekKeyframes(int position) { m_player->seek(position); } void MainWindow::readPlayerSettings() { LOG_DEBUG() << "begin"; ui->actionRealtime->setChecked(Settings.playerRealtime()); ui->actionProgressive->setChecked(Settings.playerProgressive()); ui->actionScrubAudio->setChecked(Settings.playerScrubAudio()); if (ui->actionJack) ui->actionJack->setChecked(Settings.playerJACK()); if (ui->actionGPU) { MLT.videoWidget()->setProperty("gpu", ui->actionGPU->isChecked()); ui->actionGPU->setChecked(Settings.playerGPU()); } QString external = Settings.playerExternal(); bool ok = false; external.toInt(&ok); auto isExternalPeripheral = !external.isEmpty() && !ok; setAudioChannels(Settings.playerAudioChannels()); if (isExternalPeripheral) { setPreviewScale(0); m_previewScaleGroup->setEnabled(false); } else { setPreviewScale(Settings.playerPreviewScale()); m_previewScaleGroup->setEnabled(true); } QString deinterlacer = Settings.playerDeinterlacer(); QString interpolation = Settings.playerInterpolation(); if (deinterlacer == "onefield") ui->actionOneField->setChecked(true); else if (deinterlacer == "linearblend") ui->actionLinearBlend->setChecked(true); else if (deinterlacer == "yadif-nospatial") ui->actionYadifTemporal->setChecked(true); else ui->actionYadifSpatial->setChecked(true); if (interpolation == "nearest") ui->actionNearest->setChecked(true); else if (interpolation == "bilinear") ui->actionBilinear->setChecked(true); else if (interpolation == "bicubic") ui->actionBicubic->setChecked(true); else ui->actionHyper->setChecked(true); foreach (QAction *a, m_externalGroup->actions()) { if (a->data() == external) { a->setChecked(true); if (a->data().toString().startsWith("decklink") && m_keyerMenu) m_keyerMenu->setEnabled(true); break; } } if (m_keyerGroup) { int keyer = Settings.playerKeyerMode(); foreach (QAction *a, m_keyerGroup->actions()) { if (a->data() == keyer) { a->setChecked(true); break; } } } QString profile = Settings.playerProfile(); // Automatic not permitted for SDI/HDMI if (isExternalPeripheral && profile.isEmpty()) profile = "atsc_720p_50"; foreach (QAction *a, m_profileGroup->actions()) { // Automatic not permitted for SDI/HDMI if (a->data().toString().isEmpty() && !external.isEmpty() && !ok) a->setDisabled(true); if (a->data().toString() == profile) { a->setChecked(true); break; } } QString gamma = Settings.playerGamma(); if (gamma == "bt709") ui->actionGammaRec709->setChecked(true); else ui->actionGammaSRGB->setChecked(true); LOG_DEBUG() << "end"; } void MainWindow::readWindowSettings() { LOG_DEBUG() << "begin"; Settings.setWindowGeometryDefault(saveGeometry()); Settings.setWindowStateDefault(saveState()); Settings.sync(); if (!Settings.windowGeometry().isEmpty()) { restoreGeometry(Settings.windowGeometry()); restoreState(Settings.windowState()); #ifdef Q_OS_MAC m_filtersDock->setFloating(false); #endif } else { restoreState(kLayoutEditingDefault); } #ifdef Q_OS_WIN if (isMaximized()) { ui->actionEnter_Full_Screen->setText(tr("Exit Full Screen")); } else { ui->actionEnter_Full_Screen->setText(tr("Enter Full Screen")); } #endif LOG_DEBUG() << "end"; } void MainWindow::writeSettings() { #ifndef Q_OS_MAC if (isFullScreen()) showNormal(); #endif Settings.setPlayerGPU(ui->actionGPU->isChecked()); Settings.setWindowGeometry(saveGeometry()); Settings.setWindowState(saveState()); Settings.sync(); } void MainWindow::configureVideoWidget() { LOG_DEBUG() << "begin"; if (m_profileGroup->checkedAction()) setProfile(m_profileGroup->checkedAction()->data().toString()); MLT.videoWidget()->setProperty("realtime", ui->actionRealtime->isChecked()); bool ok = false; m_externalGroup->checkedAction()->data().toInt(&ok); if (!ui->menuExternal || m_externalGroup->checkedAction()->data().toString().isEmpty() || ok) { MLT.videoWidget()->setProperty("progressive", ui->actionProgressive->isChecked()); } else { MLT.videoWidget()->setProperty("mlt_service", m_externalGroup->checkedAction()->data()); MLT.videoWidget()->setProperty("progressive", MLT.profile().progressive()); ui->actionProgressive->setEnabled(false); } if (ui->actionChannels1->isChecked()) setAudioChannels(1); else if (ui->actionChannels2->isChecked()) setAudioChannels(2); else setAudioChannels(6); if (ui->actionOneField->isChecked()) MLT.videoWidget()->setProperty("deinterlace_method", "onefield"); else if (ui->actionLinearBlend->isChecked()) MLT.videoWidget()->setProperty("deinterlace_method", "linearblend"); else if (ui->actionYadifTemporal->isChecked()) MLT.videoWidget()->setProperty("deinterlace_method", "yadif-nospatial"); else MLT.videoWidget()->setProperty("deinterlace_method", "yadif"); if (ui->actionNearest->isChecked()) MLT.videoWidget()->setProperty("rescale", "nearest"); else if (ui->actionBilinear->isChecked()) MLT.videoWidget()->setProperty("rescale", "bilinear"); else if (ui->actionBicubic->isChecked()) MLT.videoWidget()->setProperty("rescale", "bicubic"); else MLT.videoWidget()->setProperty("rescale", "hyper"); if (m_keyerGroup) MLT.videoWidget()->setProperty("keyer", m_keyerGroup->checkedAction()->data()); LOG_DEBUG() << "end"; } void MainWindow::setCurrentFile(const QString &filename) { QString shownName = tr("Untitled"); if (filename == untitledFileName()) m_currentFile.clear(); else m_currentFile = filename; if (!m_currentFile.isEmpty()) shownName = QFileInfo(m_currentFile).fileName(); #ifdef Q_OS_MAC setWindowTitle(QString("%1 - %2").arg(shownName).arg(qApp->applicationName())); #else setWindowTitle(QString("%1[*] - %2").arg(shownName).arg(qApp->applicationName())); #endif } void MainWindow::on_actionAbout_Shotcut_triggered() { const auto copyright = QStringLiteral("Copyright © 2011-2022 Meltytech, LLC"); const auto license = QStringLiteral("GNU General Public License v3.0"); const auto url = QStringLiteral("https://www.shotcut.org/"); QMessageBox::about(this, tr("About %1").arg(qApp->applicationName()), tr("

Shotcut version %2

" "

%1 is a free, open source, cross platform video editor.

" "

%4

" "

Licensed under the %5

" "

This program proudly uses the following projects:

" "

The source code used to build this program can be downloaded from " "%3.

" "This program is distributed in the hope that it will be useful, " "but WITHOUT ANY WARRANTY; without even the implied warranty of " "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
" ).arg(qApp->applicationName(), qApp->applicationVersion(), url, copyright, license)); } void MainWindow::keyPressEvent(QKeyEvent *event) { if (event->isAccepted() && event->key() != Qt::Key_F12) return; bool handled = true; switch (event->key()) { case Qt::Key_Home: m_player->seek(0); break; case Qt::Key_End: if (MLT.producer()) m_player->seek(MLT.producer()->get_length()); break; case Qt::Key_Left: if ((event->modifiers() & Qt::ControlModifier) && m_timelineDock->isVisible()) { if (m_timelineDock->selection().isEmpty()) { m_timelineDock->selectClipUnderPlayhead(); } else if (m_timelineDock->selection().size() == 1) { int newIndex = m_timelineDock->selection().first().x() - 1; if (newIndex < 0) break; m_timelineDock->setSelection(QList() << QPoint(newIndex, m_timelineDock->selection().first().y())); m_navigationPosition = m_timelineDock->centerOfClip(m_timelineDock->currentTrack(), newIndex); } } else { stepLeftOneFrame(); } break; case Qt::Key_Right: if ((event->modifiers() & Qt::ControlModifier) && m_timelineDock->isVisible()) { if (m_timelineDock->selection().isEmpty()) { m_timelineDock->selectClipUnderPlayhead(); } else if (m_timelineDock->selection().size() == 1) { int newIndex = m_timelineDock->selection().first().x() + 1; if (newIndex >= m_timelineDock->clipCount(-1)) break; m_timelineDock->setSelection(QList() << QPoint(newIndex, m_timelineDock->selection().first().y())); m_navigationPosition = m_timelineDock->centerOfClip(m_timelineDock->currentTrack(), newIndex); } } else { stepRightOneFrame(); } break; case Qt::Key_PageUp: case Qt::Key_PageDown: { int directionMultiplier = event->key() == Qt::Key_PageUp ? -1 : 1; int seconds = 1; if (event->modifiers() & Qt::ControlModifier) seconds *= 5; if (event->modifiers() & Qt::ShiftModifier) seconds *= 2; stepLeftBySeconds(seconds * directionMultiplier); } break; case Qt::Key_Space: #ifdef Q_OS_MAC // Spotlight defaults to Cmd+Space, so also accept Ctrl+Space. if ((event->modifiers() == Qt::MetaModifier || (event->modifiers() & Qt::ControlModifier)) && m_timelineDock->isVisible()) #else if (event->modifiers() == Qt::ControlModifier && m_timelineDock->isVisible()) #endif m_timelineDock->selectClipUnderPlayhead(); else handled = false; break; case Qt::Key_A: if (event->modifiers() == Qt::ShiftModifier) { m_playlistDock->show(); m_playlistDock->raise(); m_playlistDock->on_actionAppendCut_triggered(); } else if ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::ShiftModifier)) { m_playlistDock->show(); m_playlistDock->raise(); m_playlistDock->on_actionSelectAll_triggered(); } else if (event->modifiers() == Qt::ControlModifier) { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->selectAll(); } else if (event->modifiers() == Qt::NoModifier) { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->append(-1); } break; case Qt::Key_C: if (event->modifiers() == Qt::ShiftModifier && m_playlistDock->model()->rowCount() > 0) { m_playlistDock->show(); m_playlistDock->raise(); m_playlistDock->on_actionCopy_triggered(); } else if (isMultitrackValid()) { m_timelineDock->show(); m_timelineDock->raise(); if (m_timelineDock->selection().isEmpty()) { m_timelineDock->copy(-1, -1); } else { auto &selected = m_timelineDock->selection().first(); m_timelineDock->copy(selected.y(), selected.x()); } } break; case Qt::Key_D: if (event->modifiers() == Qt::ControlModifier) { m_timelineDock->setSelection(); m_timelineDock->model()->reload(); } else if ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::ShiftModifier)) { m_playlistDock->show(); m_playlistDock->raise(); m_playlistDock->on_actionSelectNone_triggered(); } else { handled = false; } break; case Qt::Key_F: if (event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::ControlModifier) { m_filtersDock->show(); m_filtersDock->raise(); m_filtersDock->widget()->setFocus(); m_filtersDock->openFilterMenu(); } else if (event->modifiers() == Qt::ShiftModifier) { filterController()->removeCurrent(); #ifdef Q_OS_MAC } else if ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::MetaModifier)) { on_actionEnter_Full_Screen_triggered(); #else } else if ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::ShiftModifier)) { on_actionEnter_Full_Screen_triggered(); #endif } else { handled = false; } break; case Qt::Key_H: #ifdef Q_OS_MAC // OS X uses Cmd+H to hide an app. if (event->modifiers() & Qt::MetaModifier && isMultitrackValid()) #else if (event->modifiers() & Qt::ControlModifier && isMultitrackValid()) #endif m_timelineDock->toggleTrackHidden(m_timelineDock->currentTrack()); break; case Qt::Key_J: if (m_isKKeyPressed) m_player->seek(m_player->position() - 1); else m_player->rewind(false); break; case Qt::Key_K: m_player->pause(); m_isKKeyPressed = true; break; case Qt::Key_L: #ifdef Q_OS_MAC // OS X uses Cmd+H to hide an app and Cmd+M to minimize. Therefore, we force // it to be the apple keyboard control key aka meta. Therefore, to be // consistent with all track header toggles, we make the lock toggle also use // meta. if (event->modifiers() & Qt::MetaModifier && isMultitrackValid()) #else if (event->modifiers() & Qt::ControlModifier && isMultitrackValid()) #endif m_timelineDock->setTrackLock(m_timelineDock->currentTrack(), !m_timelineDock->isTrackLocked(m_timelineDock->currentTrack())); else if (m_isKKeyPressed) m_player->seek(m_player->position() + 1); else m_player->fastForward(false); break; case Qt::Key_M: #ifdef Q_OS_MAC // OS X uses Cmd+M to minimize an app. if (event->modifiers() & Qt::MetaModifier && isMultitrackValid()) { #else if (event->modifiers() & Qt::ControlModifier && isMultitrackValid()) { #endif if (event->modifiers() & Qt::ShiftModifier) { m_timelineDock->deleteMarker(); } else { m_timelineDock->toggleTrackMute(m_timelineDock->currentTrack()); } } else if (event->modifiers() == Qt::NoModifier && isMultitrackValid()) { m_timelineDock->createOrEditMarker(); } else if (event->modifiers() == Qt::AltModifier && isMultitrackValid()) { m_timelineDock->createOrEditSelectionMarker(); } break; case Qt::Key_I: if (event->modifiers() == Qt::ControlModifier) { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->addVideoTrack(); } else if ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::AltModifier)) { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->insertTrack(); } else { setInToCurrent(event->modifiers() & Qt::ShiftModifier); } break; case Qt::Key_O: setOutToCurrent(event->modifiers() & Qt::ShiftModifier); break; case Qt::Key_P: if (event->modifiers() == Qt::ControlModifier) { Settings.setTimelineSnap(!Settings.timelineSnap()); } else if (event->modifiers() & Qt::ControlModifier) { if (event->modifiers() & Qt::AltModifier) { Settings.setTimelineScrollZoom(!Settings.timelineScrollZoom()); } if (event->modifiers() & Qt::ShiftModifier) { Settings.setTimelineCenterPlayhead(!Settings.timelineCenterPlayhead()); } } break; case Qt::Key_R: if (event->modifiers() & Qt::AltModifier && event->modifiers() & Qt::ShiftModifier) { Settings.setTimelineRippleAllTracks(!Settings.timelineRipple()); Settings.setTimelineRipple(!Settings.timelineRipple()); Settings.setTimelineRippleMarkers(!Settings.timelineRippleMarkers()); } else if (event->modifiers() & Qt::AltModifier && !(event->modifiers() & Qt::ControlModifier) && !(event->modifiers() & Qt::ShiftModifier)) { Settings.setTimelineRippleMarkers(!Settings.timelineRippleMarkers()); } else if (event->modifiers() & Qt::ControlModifier) { if (event->modifiers() & Qt::AltModifier) { Settings.setTimelineRippleAllTracks(!Settings.timelineRippleAllTracks()); } else if (event->modifiers() & Qt::ShiftModifier) { Settings.setTimelineRippleAllTracks(!Settings.timelineRipple()); Settings.setTimelineRipple(!Settings.timelineRipple()); } else { Settings.setTimelineRipple(!Settings.timelineRipple()); } } else if (isMultitrackValid()) { m_timelineDock->show(); m_timelineDock->raise(); if (MLT.isClip() || m_timelineDock->selection().isEmpty()) { m_timelineDock->replace(-1, -1); } else { auto &selected = m_timelineDock->selection().first(); m_timelineDock->replace(selected.y(), selected.x()); } } break; case Qt::Key_S: if (isMultitrackValid()) m_timelineDock->splitClip(); break; case Qt::Key_T: m_player->focusPositionSpinner(); break; case Qt::Key_U: if (event->modifiers() == Qt::ControlModifier) { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->addAudioTrack(); } else if ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::AltModifier)) { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->removeTrack(); } break; case Qt::Key_V: // Avid Splice In if (event->modifiers() == Qt::ShiftModifier) { m_playlistDock->show(); m_playlistDock->raise(); m_playlistDock->on_actionInsertCut_triggered(); } else { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->insert(-1); } break; case Qt::Key_B: if (event->modifiers() & Qt::ControlModifier && event->modifiers() & Qt::AltModifier) { // Toggle track blending. int trackIndex = m_timelineDock->currentTrack(); bool isBottomVideo = m_timelineDock->model()->data(m_timelineDock->model()->index(trackIndex), MultitrackModel::IsBottomVideoRole).toBool(); if (!isBottomVideo) { bool isComposite = m_timelineDock->model()->data(m_timelineDock->model()->index(trackIndex), MultitrackModel::IsCompositeRole).toBool(); m_timelineDock->setTrackComposite(trackIndex, !isComposite); } } else if (event->modifiers() == Qt::ShiftModifier) { if (m_playlistDock->model()->rowCount() > 0) { // Update playlist item. m_playlistDock->show(); m_playlistDock->raise(); m_playlistDock->on_actionUpdate_triggered(); } } else { // Overwrite on timeline. m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->overwrite(-1); } break; case Qt::Key_Escape: // Avid Toggle Active Monitor if (MLT.isPlaylist()) { if (isMultitrackValid()) m_player->onTabBarClicked(Player::ProjectTabIndex); else if (MLT.savedProducer()) m_player->onTabBarClicked(Player::SourceTabIndex); else m_playlistDock->on_actionOpen_triggered(); } else if (MLT.isMultitrack()) { if (MLT.savedProducer()) m_player->onTabBarClicked(Player::SourceTabIndex); // TODO else open clip under playhead of current track if available } else { if (isMultitrackValid() || (playlist() && playlist()->count() > 0)) m_player->onTabBarClicked(Player::ProjectTabIndex); } break; case Qt::Key_Up: if (isMultitrackValid() && (event->modifiers() & Qt::AltModifier) && (event->modifiers() & Qt::AltModifier) && m_timelineDock->isVisible()) { m_timelineDock->moveTrackUp(); } else if (m_playlistDock->isVisible() && event->modifiers() & Qt::AltModifier && m_playlistDock->model()->rowCount() > 0) { m_playlistDock->raise(); m_playlistDock->decrementIndex(); m_playlistDock->on_actionOpen_triggered(); } else if ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::ShiftModifier)) { if (m_playlistDock->model()->rowCount() > 0) { m_playlistDock->raise(); m_playlistDock->moveClipUp(); m_playlistDock->decrementIndex(); } } else if (isMultitrackValid()) { int newClipIndex = -1; int trackIndex = m_timelineDock->currentTrack() - 1; if ((event->modifiers() & Qt::ControlModifier) && !m_timelineDock->selection().isEmpty() && trackIndex > -1) { newClipIndex = m_timelineDock->clipIndexAtPosition(trackIndex, m_navigationPosition); } m_timelineDock->incrementCurrentTrack(-1); if (newClipIndex >= 0) { newClipIndex = qMin(newClipIndex, m_timelineDock->clipCount(trackIndex) - 1); m_timelineDock->setSelection(QList() << QPoint(newClipIndex, trackIndex)); } } break; case Qt::Key_Down: if (isMultitrackValid() && (event->modifiers() & Qt::AltModifier) && (event->modifiers() & Qt::ShiftModifier) && m_timelineDock->isVisible()) { m_timelineDock->moveTrackDown(); } else if (m_playlistDock->isVisible() && event->modifiers() & Qt::AltModifier && m_playlistDock->model()->rowCount() > 0) { m_playlistDock->raise(); m_playlistDock->incrementIndex(); m_playlistDock->on_actionOpen_triggered(); } else if ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::ShiftModifier)) { if (m_playlistDock->model()->rowCount() > 0) { m_playlistDock->raise(); m_playlistDock->moveClipDown(); m_playlistDock->incrementIndex(); } } else if (isMultitrackValid()) { int newClipIndex = -1; int trackIndex = m_timelineDock->currentTrack() + 1; if ((event->modifiers() & Qt::ControlModifier) && !m_timelineDock->selection().isEmpty() && trackIndex < m_timelineDock->model()->trackList().count()) { newClipIndex = m_timelineDock->clipIndexAtPosition(trackIndex, m_navigationPosition); } m_timelineDock->incrementCurrentTrack(1); if (newClipIndex >= 0) { newClipIndex = qMin(newClipIndex, m_timelineDock->clipCount(trackIndex) - 1); m_timelineDock->setSelection(QList() << QPoint(newClipIndex, trackIndex)); } } break; case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: case Qt::Key_8: case Qt::Key_9: if ((event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::KeypadModifier) && m_playlistDock->isVisible() && m_playlistDock->model()->rowCount() > 0) { m_playlistDock->raise(); m_playlistDock->setIndex(event->key() - Qt::Key_1); } break; case Qt::Key_0: if ((event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::KeypadModifier)) { if (m_timelineDock->isVisible()) { emit m_timelineDock->zoomToFit(); } else if (m_playlistDock->isVisible() && m_playlistDock->model()->rowCount() > 0) { m_playlistDock->raise(); m_playlistDock->setIndex(9); } } if (m_keyframesDock->isVisible() && (event->modifiers() & Qt::AltModifier)) { emit m_keyframesDock->zoomToFit(); } break; case Qt::Key_X: // Avid Extract if (event->modifiers() == Qt::ShiftModifier && m_playlistDock->model()->rowCount() > 0) { m_playlistDock->show(); m_playlistDock->raise(); m_playlistDock->on_removeButton_clicked(); } else if (isMultitrackValid()) { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->removeSelection(); } break; case Qt::Key_Backspace: case Qt::Key_Delete: if (isMultitrackValid()) { m_timelineDock->show(); m_timelineDock->raise(); if (event->modifiers() == Qt::ShiftModifier) m_timelineDock->removeSelection(); else m_timelineDock->liftSelection(); } else if (m_playlistDock->model()->rowCount() > 0) { m_playlistDock->show(); m_playlistDock->raise(); m_playlistDock->on_removeButton_clicked(); } break; case Qt::Key_Z: // Avid Lift if (event->modifiers() == Qt::ShiftModifier && m_playlistDock->model()->rowCount() > 0) { m_playlistDock->show(); m_playlistDock->raise(); m_playlistDock->on_removeButton_clicked(); } else if (isMultitrackValid() && event->modifiers() == Qt::NoModifier) { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->liftSelection(); } break; case Qt::Key_Minus: if (m_timelineDock->isVisible() && !(event->modifiers() & Qt::AltModifier)) { if (event->modifiers() & Qt::ControlModifier) emit m_timelineDock->makeTracksShorter(); else emit m_timelineDock->zoomOut(); } if (m_keyframesDock->isVisible() && (event->modifiers() & Qt::AltModifier)) { emit m_keyframesDock->zoomOut(); } break; case Qt::Key_Equal: case Qt::Key_Plus: if (m_timelineDock->isVisible() && !(event->modifiers() & Qt::AltModifier)) { if (event->modifiers() & Qt::ControlModifier) emit m_timelineDock->makeTracksTaller(); else emit m_timelineDock->zoomIn(); } if (m_keyframesDock->isVisible() && (event->modifiers() & Qt::AltModifier)) { emit m_keyframesDock->zoomIn(); } break; case Qt::Key_Enter: // Seek to current playlist item case Qt::Key_Return: if (m_playlistDock->isVisible() && m_playlistDock->position() >= 0) { if (event->modifiers() == Qt::ShiftModifier) m_playlistDock->on_actionGoto_triggered(); else if (event->modifiers() == Qt::ControlModifier) m_playlistDock->on_actionOpen_triggered(); } break; case Qt::Key_F2: onPropertiesDockTriggered(true); emit renameRequested(); break; case Qt::Key_F3: onRecentDockTriggered(true); m_recentDock->find(); break; case Qt::Key_F5: m_timelineDock->model()->reload(); m_keyframesDock->model().reload(); break; case Qt::Key_F11: on_actionEnter_Full_Screen_triggered(); break; case Qt::Key_F12: LOG_DEBUG() << "event isAccepted:" << event->isAccepted(); LOG_DEBUG() << "Current focusWidget:" << QApplication::focusWidget(); LOG_DEBUG() << "Current focusObject:" << QApplication::focusObject(); LOG_DEBUG() << "Current focusWindow:" << QApplication::focusWindow(); break; case Qt::Key_BracketLeft: if (filterController()->currentFilter() && m_filtersDock->qmlProducer()) { if (event->modifiers() == Qt::AltModifier) { emit m_keyframesDock->seekPreviousSimple(); } else { int i = m_filtersDock->qmlProducer()->position() + m_filtersDock->qmlProducer()->in(); if (filterController()->currentFilter()->allowTrim()) { m_keyframesDock->model().trimFilterIn(i); } } } break; case Qt::Key_BracketRight: if (filterController()->currentFilter() && m_filtersDock->qmlProducer()) { if (event->modifiers() == Qt::AltModifier) { emit m_keyframesDock->seekNextSimple(); } else { int i = m_filtersDock->qmlProducer()->position() + m_filtersDock->qmlProducer()->in(); if (filterController()->currentFilter()->allowTrim()) { m_keyframesDock->model().trimFilterOut(i); } } } break; case Qt::Key_BraceLeft: if (filterController()->currentFilter() && m_filtersDock->qmlProducer()) { int i = m_filtersDock->qmlProducer()->position() + m_filtersDock->qmlProducer()->in() - filterController()->currentFilter()->in(); if (filterController()->currentFilter()->allowAnimateIn()) { filterController()->currentFilter()->setAnimateIn(i); } } break; case Qt::Key_BraceRight: if (filterController()->currentFilter() && m_filtersDock->qmlProducer()) { int i = filterController()->currentFilter()->out() - (m_filtersDock->qmlProducer()->position() + m_filtersDock->qmlProducer()->in()); if (filterController()->currentFilter()->allowAnimateOut()) { filterController()->currentFilter()->setAnimateOut(i); } } break; case Qt::Key_Semicolon: if (filterController()->currentFilter() && m_filtersDock->qmlProducer() && m_keyframesDock->currentParameter() >= 0) { auto position = m_filtersDock->qmlProducer()->position() - (filterController()->currentFilter()->in() - m_filtersDock->qmlProducer()->in()); auto parameterIndex = m_keyframesDock->currentParameter(); if (m_keyframesDock->model().isKeyframe(parameterIndex, position)) { auto keyframeIndex = m_keyframesDock->model().keyframeIndex(parameterIndex, position); m_keyframesDock->model().remove(parameterIndex, keyframeIndex); } else { m_keyframesDock->model().addKeyframe(parameterIndex, position); } } break; case Qt::Key_Less: m_timelineDock->seekPrevMarker(); break; case Qt::Key_Greater: m_timelineDock->seekNextMarker(); break; default: handled = false; break; } if (handled) event->setAccepted(handled); else QMainWindow::keyPressEvent(event); } void MainWindow::keyReleaseEvent(QKeyEvent *event) { if (event->key() == Qt::Key_K) { m_isKKeyPressed = false; event->setAccepted(true); } else { QMainWindow::keyReleaseEvent(event); } } void MainWindow::hideSetDataDirectory() { delete ui->actionAppDataSet; } QAction *MainWindow::actionAddCustomProfile() const { return ui->actionAddCustomProfile; } QAction *MainWindow::actionProfileRemove() const { return ui->actionProfileRemove; } void MainWindow::buildVideoModeMenu(QMenu *topMenu, QMenu *&customMenu, QActionGroup *group, QAction *addAction, QAction *removeAction) { topMenu->addAction(addProfile(group, "HD 720p 50 fps", "atsc_720p_50")); topMenu->addAction(addProfile(group, "HD 720p 59.94 fps", "atsc_720p_5994")); topMenu->addAction(addProfile(group, "HD 720p 60 fps", "atsc_720p_60")); topMenu->addAction(addProfile(group, "HD 1080i 25 fps", "atsc_1080i_50")); topMenu->addAction(addProfile(group, "HD 1080i 29.97 fps", "atsc_1080i_5994")); topMenu->addAction(addProfile(group, "HD 1080p 23.98 fps", "atsc_1080p_2398")); topMenu->addAction(addProfile(group, "HD 1080p 24 fps", "atsc_1080p_24")); topMenu->addAction(addProfile(group, "HD 1080p 25 fps", "atsc_1080p_25")); topMenu->addAction(addProfile(group, "HD 1080p 29.97 fps", "atsc_1080p_2997")); topMenu->addAction(addProfile(group, "HD 1080p 30 fps", "atsc_1080p_30")); topMenu->addAction(addProfile(group, "HD 1080p 50 fps", "atsc_1080p_50")); topMenu->addAction(addProfile(group, "HD 1080p 59.94 fps", "atsc_1080p_5994")); topMenu->addAction(addProfile(group, "HD 1080p 60 fps", "atsc_1080p_60")); topMenu->addAction(addProfile(group, "SD NTSC", "dv_ntsc")); topMenu->addAction(addProfile(group, "SD PAL", "dv_pal")); topMenu->addAction(addProfile(group, "UHD 2160p 23.98 fps", "uhd_2160p_2398")); topMenu->addAction(addProfile(group, "UHD 2160p 24 fps", "uhd_2160p_24")); topMenu->addAction(addProfile(group, "UHD 2160p 25 fps", "uhd_2160p_25")); topMenu->addAction(addProfile(group, "UHD 2160p 29.97 fps", "uhd_2160p_2997")); topMenu->addAction(addProfile(group, "UHD 2160p 30 fps", "uhd_2160p_30")); topMenu->addAction(addProfile(group, "UHD 2160p 50 fps", "uhd_2160p_50")); topMenu->addAction(addProfile(group, "UHD 2160p 59.94 fps", "uhd_2160p_5994")); topMenu->addAction(addProfile(group, "UHD 2160p 60 fps", "uhd_2160p_60")); QMenu *menu = topMenu->addMenu(tr("Non-Broadcast")); menu->addAction(addProfile(group, "640x480 4:3 NTSC", "square_ntsc")); menu->addAction(addProfile(group, "768x576 4:3 PAL", "square_pal")); menu->addAction(addProfile(group, "854x480 16:9 NTSC", "square_ntsc_wide")); menu->addAction(addProfile(group, "1024x576 16:9 PAL", "square_pal_wide")); menu->addAction(addProfile(group, tr("DVD Widescreen NTSC"), "dv_ntsc_wide")); menu->addAction(addProfile(group, tr("DVD Widescreen PAL"), "dv_pal_wide")); menu->addAction(addProfile(group, "HD 720p 23.98 fps", "atsc_720p_2398")); menu->addAction(addProfile(group, "HD 720p 24 fps", "atsc_720p_24")); menu->addAction(addProfile(group, "HD 720p 25 fps", "atsc_720p_25")); menu->addAction(addProfile(group, "HD 720p 29.97 fps", "atsc_720p_2997")); menu->addAction(addProfile(group, "HD 720p 30 fps", "atsc_720p_30")); menu->addAction(addProfile(group, "HD 1080i 60 fps", "atsc_1080i_60")); menu->addAction(addProfile(group, "HDV 1080i 25 fps", "hdv_1080_50i")); menu->addAction(addProfile(group, "HDV 1080i 29.97 fps", "hdv_1080_60i")); menu->addAction(addProfile(group, "HDV 1080p 25 fps", "hdv_1080_25p")); menu->addAction(addProfile(group, "HDV 1080p 29.97 fps", "hdv_1080_30p")); menu->addAction(addProfile(group, tr("Square 1080p 30 fps"), "square_1080p_30")); menu->addAction(addProfile(group, tr("Square 1080p 60 fps"), "square_1080p_60")); menu->addAction(addProfile(group, tr("Vertical HD 30 fps"), "vertical_hd_30")); menu->addAction(addProfile(group, tr("Vertical HD 60 fps"), "vertical_hd_60")); customMenu = topMenu->addMenu(tr("Custom")); customMenu->addAction(addAction); // Load custom profiles QDir dir(Settings.appDataLocation()); if (dir.cd("profiles")) { QStringList profiles = dir.entryList(QDir::Files | QDir::NoDotAndDotDot | QDir::Readable); if (profiles.length() > 0) { customMenu->addAction(removeAction); customMenu->addSeparator(); } foreach (QString name, profiles) customMenu->addAction(addProfile(group, name, dir.filePath(name))); } } void MainWindow::newProject(const QString &filename, bool isProjectFolder) { if (isProjectFolder) { QFileInfo info(filename); MLT.setProjectFolder(info.absolutePath()); } if (saveXML(filename)) { QMutexLocker locker(&m_autosaveMutex); if (m_autosaveFile) m_autosaveFile->changeManagedFile(filename); else m_autosaveFile.reset(new AutoSaveFile(filename)); setCurrentFile(filename); setWindowModified(false); resetSourceUpdated(); if (MLT.producer()) showStatusMessage(tr("Saved %1").arg(m_currentFile)); m_undoStack->setClean(); m_recentDock->add(filename); } else { showSaveError(); } } void MainWindow::addCustomProfile(const QString &name, QMenu *menu, QAction *action, QActionGroup *group) { // Add new profile to the menu. QDir dir(Settings.appDataLocation()); if (dir.cd("profiles")) { QStringList profiles = dir.entryList(QDir::Files | QDir::NoDotAndDotDot | QDir::Readable); if (profiles.length() == 1) { menu->addAction(action); menu->addSeparator(); } action = addProfile(group, name, dir.filePath(name)); action->setChecked(true); menu->addAction(action); Settings.setPlayerProfile(dir.filePath(name)); Settings.sync(); } } void MainWindow::removeCustomProfiles(const QStringList &profiles, QDir &dir, QMenu *menu, QAction *action) { foreach (const QString &profile, profiles) { // Remove the file. dir.remove(profile); // Locate the menu item. foreach (QAction *a, menu->actions()) { if (a->text() == profile) { // Remove the menu item. delete a; break; } } } // If no more custom video modes. if (menu->actions().size() == 3) { // Remove the Remove action and separator. menu->removeAction(action); foreach (QAction *a, menu->actions()) { if (a->isSeparator()) { delete a; break; } } } } // Drag-n-drop events bool MainWindow::eventFilter(QObject *target, QEvent *event) { if (event->type() == QEvent::DragEnter && target == MLT.videoWidget()) { dragEnterEvent(static_cast(event)); return true; } else if (event->type() == QEvent::Drop && target == MLT.videoWidget()) { dropEvent(static_cast(event)); return true; } else if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) { if (QEvent::KeyPress == event->type()) { // Let Shift+Escape be a global hook to defocus a widget (assign global player focus). auto keyEvent = static_cast(event); if (Qt::Key_Escape == keyEvent->key() && Qt::ShiftModifier == keyEvent->modifiers()) { m_player->setFocus(); return true; } } QQuickWidget *focusedQuickWidget = qobject_cast(qApp->focusWidget()); if (focusedQuickWidget && focusedQuickWidget->quickWindow()->activeFocusItem()) { event->accept(); qApp->sendEvent(focusedQuickWidget->quickWindow()->activeFocusItem(), event); QWidget *w = focusedQuickWidget->parentWidget(); if (!event->isAccepted()) qApp->sendEvent(w, event); return true; } } return QMainWindow::eventFilter(target, event); } void MainWindow::dragEnterEvent(QDragEnterEvent *event) { // Simulate the player firing a dragStarted even to make the playlist close // its help text view. This lets one drop a clip directly into the playlist // from a fresh start. Mlt::GLWidget *videoWidget = (Mlt::GLWidget *) &Mlt::Controller::singleton(); emit videoWidget->dragStarted(); event->acceptProposedAction(); } void MainWindow::dropEvent(QDropEvent *event) { const QMimeData *mimeData = event->mimeData(); if (mimeData->hasFormat("application/x-qabstractitemmodeldatalist")) { QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist"); QDataStream stream(&encoded, QIODevice::ReadOnly); QMap roleDataMap; while (!stream.atEnd()) { int row, col; stream >> row >> col >> roleDataMap; } if (roleDataMap.contains(Qt::ToolTipRole)) { // DisplayRole is just basename, ToolTipRole contains full path open(roleDataMap[Qt::ToolTipRole].toString()); event->acceptProposedAction(); } } else if (mimeData->hasUrls()) { openMultiple(mimeData->urls()); event->acceptProposedAction(); } else if (mimeData->hasFormat(Mlt::XmlMimeType )) { m_playlistDock->on_actionOpen_triggered(); event->acceptProposedAction(); } } void MainWindow::closeEvent(QCloseEvent *event) { m_timelineDock->stopRecording(); if (continueJobsRunning() && continueModified()) { LOG_DEBUG() << "begin"; JOBS.cleanup(); writeSettings(); if (m_exitCode == EXIT_SUCCESS) { MLT.stop(); } else { if (multitrack()) m_timelineDock->model()->close(); if (playlist()) m_playlistDock->model()->close(); else onMultitrackClosed(); } QThreadPool::globalInstance()->clear(); AudioLevelsTask::closeAll(); event->accept(); emit aboutToShutDown(); if (m_exitCode == EXIT_SUCCESS) { QApplication::quit(); LOG_DEBUG() << "end"; ::_Exit(0); } else { QApplication::exit(m_exitCode); LOG_DEBUG() << "end"; } return; } event->ignore(); } void MainWindow::showEvent(QShowEvent *event) { // This is needed to prevent a crash on windows on startup when timeline // is visible and dock title bars are hidden. Q_UNUSED(event) ui->actionShowTitleBars->setChecked(Settings.showTitleBars()); on_actionShowTitleBars_triggered(Settings.showTitleBars()); ui->actionShowToolbar->setChecked(Settings.showToolBar()); on_actionShowToolbar_triggered(Settings.showToolBar()); ui->actionShowTextUnderIcons->setChecked(Settings.textUnderIcons()); on_actionShowTextUnderIcons_toggled(Settings.textUnderIcons()); ui->actionShowSmallIcons->setChecked(Settings.smallIcons()); on_actionShowSmallIcons_toggled(Settings.smallIcons()); windowHandle()->installEventFilter(this); #ifndef SHOTCUT_NOUPGRADE if (!Settings.noUpgrade() && !qApp->property("noupgrade").toBool()) QTimer::singleShot(0, this, SLOT(showUpgradePrompt())); #endif #ifdef Q_OS_WIN WindowsTaskbarButton::getInstance().setParentWindow(this); #endif onAutosaveTimeout(); QTimer::singleShot(100, this, [ = ]() { Database::singleton(this); #ifdef Q_OS_WIN this->setProperty("windowOpacity", 1.0); #endif }); } void MainWindow::hideEvent(QHideEvent *event) { Q_UNUSED(event) #ifdef Q_OS_WIN setProperty("windowOpacity", 0.0); #endif } void MainWindow::on_actionOpenOther_triggered() { // these static are used to open dialog with previous configuration OpenOtherDialog dialog(this); if (MLT.producer()) dialog.load(MLT.producer()); if (dialog.exec() == QDialog::Accepted) { closeProducer(); auto &profile = MLT.profile(); auto producer = dialog.newProducer(profile); if (!profile.is_explicit()) { profile.from_producer(*producer); profile.set_width(Util::coerceMultiple(profile.width())); profile.set_height(Util::coerceMultiple(profile.height())); } MLT.updatePreviewProfile(); setPreviewScale(Settings.playerPreviewScale()); open(producer); } } void MainWindow::onProducerOpened(bool withReopen) { QWidget *w = loadProducerWidget(MLT.producer()); if (withReopen && w && !MLT.producer()->get(kMultitrackItemProperty)) { if (-1 != w->metaObject()->indexOfSignal("producerReopened(bool)")) connect(w, SIGNAL(producerReopened(bool)), m_player, SLOT(onProducerOpened(bool))); } else if (MLT.isPlaylist()) { m_playlistDock->model()->load(); if (playlist()) { m_isPlaylistLoaded = true; m_player->setIn(-1); m_player->setOut(-1); m_playlistDock->setVisible(true); m_playlistDock->raise(); m_player->enableTab(Player::ProjectTabIndex); m_player->switchToTab(Player::ProjectTabIndex); } } else if (MLT.isMultitrack()) { m_timelineDock->blockSelection(true); m_timelineDock->model()->load(); m_timelineDock->blockSelection(false); if (isMultitrackValid()) { m_player->setIn(-1); m_player->setOut(-1); m_timelineDock->setVisible(true); m_timelineDock->raise(); m_player->enableTab(Player::ProjectTabIndex); m_player->switchToTab(Player::ProjectTabIndex); m_timelineDock->selectMultitrack(); QTimer::singleShot(0, this, [ = ]() { m_timelineDock->setSelection(); }); } } if (MLT.isClip()) { m_player->enableTab(Player::SourceTabIndex); m_player->switchToTab(Player::SourceTabIndex); Util::getHash(*MLT.producer()); ui->actionPaste->setEnabled(true); } ui->actionSave->setEnabled(true); QMutexLocker locker(&m_autosaveMutex); if (m_autosaveFile) setCurrentFile(m_autosaveFile->managedFileName()); else if (!MLT.URL().isEmpty()) setCurrentFile(MLT.URL()); on_actionJack_triggered(ui->actionJack && ui->actionJack->isChecked()); } void MainWindow::onProducerChanged() { MLT.refreshConsumer(); if (playlist() && MLT.producer() && MLT.producer()->is_valid() && MLT.producer()->get_int(kPlaylistIndexProperty)) m_playlistDock->setUpdateButtonEnabled(true); sourceUpdated(); } bool MainWindow::on_actionSave_triggered() { m_timelineDock->stopRecording(); if (m_currentFile.isEmpty()) { return on_actionSave_As_triggered(); } else { if (Util::warnIfNotWritable(m_currentFile, this, tr("Save XML"))) return false; bool success = saveXML(m_currentFile); QMutexLocker locker(&m_autosaveMutex); m_autosaveFile.reset(new AutoSaveFile(m_currentFile)); setCurrentFile(m_currentFile); setWindowModified(false); if (success) { showStatusMessage(tr("Saved %1").arg(m_currentFile)); } else { showSaveError(); } m_undoStack->setClean(); return true; } } bool MainWindow::on_actionSave_As_triggered() { QString path = Settings.savePath(); if (!m_currentFile.isEmpty()) path = m_currentFile; QString caption = tr("Save XML"); QString filename = QFileDialog::getSaveFileName(this, caption, path, tr("MLT XML (*.mlt)"), nullptr, Util::getFileDialogOptions()); if (!filename.isEmpty()) { QFileInfo fi(filename); Settings.setSavePath(fi.path()); if (fi.suffix() != "mlt") filename += ".mlt"; if (Util::warnIfNotWritable(filename, this, caption)) return false; newProject(filename); } return !filename.isEmpty(); } bool MainWindow::continueModified() { if (isWindowModified()) { QMessageBox dialog(QMessageBox::Warning, qApp->applicationName(), tr("The project has been modified.\n" "Do you want to save your changes?"), QMessageBox::No | QMessageBox::Cancel | QMessageBox::Yes, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::Cancel); int r = dialog.exec(); if (r == QMessageBox::Yes || r == QMessageBox::No) { if (r == QMessageBox::Yes) { return on_actionSave_triggered(); } else { QMutexLocker locker(&m_autosaveMutex); m_autosaveFile.reset(); } } else if (r == QMessageBox::Cancel) { return false; } } return true; } bool MainWindow::continueJobsRunning() { if (JOBS.hasIncomplete()) { QMessageBox dialog(QMessageBox::Warning, qApp->applicationName(), tr("There are incomplete jobs.\n" "Do you want to still want to exit?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); return (dialog.exec() == QMessageBox::Yes); } if (m_encodeDock->isExportInProgress()) { QMessageBox dialog(QMessageBox::Warning, qApp->applicationName(), tr("An export is in progress.\n" "Do you want to still want to exit?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); return (dialog.exec() == QMessageBox::Yes); } return true; } QUndoStack *MainWindow::undoStack() const { return m_undoStack; } void MainWindow::onEncodeTriggered(bool checked) { if (checked) { m_encodeDock->show(); m_encodeDock->raise(); } } void MainWindow::onCaptureStateChanged(bool started) { if (started && (MLT.resource().startsWith("x11grab:") || MLT.resource().startsWith("gdigrab:") || MLT.resource().startsWith("avfoundation")) && !MLT.producer()->get_int(kBackgroundCaptureProperty)) showMinimized(); } void MainWindow::onJobsDockTriggered(bool checked) { if (checked) { m_jobsDock->show(); m_jobsDock->raise(); } } void MainWindow::onRecentDockTriggered(bool checked) { if (checked) { m_recentDock->show(); m_recentDock->raise(); } } void MainWindow::onPropertiesDockTriggered(bool checked) { if (checked) { m_propertiesDock->show(); m_propertiesDock->raise(); } } void MainWindow::onPlaylistDockTriggered(bool checked) { if (checked) { m_playlistDock->show(); m_playlistDock->raise(); } } void MainWindow::onTimelineDockTriggered(bool checked) { if (checked) { m_timelineDock->show(); m_timelineDock->raise(); } } void MainWindow::onHistoryDockTriggered(bool checked) { if (checked) { m_historyDock->show(); m_historyDock->raise(); } } void MainWindow::onFiltersDockTriggered(bool checked) { if (checked) { m_filtersDock->show(); m_filtersDock->raise(); } } void MainWindow::onKeyframesDockTriggered(bool checked) { if (checked) { m_keyframesDock->show(); m_keyframesDock->raise(); } } void MainWindow::onMarkersDockTriggered(bool checked) { if (checked) { m_markersDock->show(); m_markersDock->raise(); } } void MainWindow::onNotesDockTriggered(bool checked) { if (checked) { m_notesDock->show(); m_notesDock->raise(); } } void MainWindow::onPlaylistCreated() { if (!playlist() || playlist()->count() == 0) return; m_player->enableTab(Player::ProjectTabIndex, true); } void MainWindow::onPlaylistLoaded() { updateMarkers(); m_player->enableTab(Player::ProjectTabIndex, true); } void MainWindow::onPlaylistCleared() { m_player->onTabBarClicked(Player::SourceTabIndex); setWindowModified(true); } void MainWindow::onPlaylistClosed() { closeProducer(); setProfile(Settings.playerProfile()); resetVideoModeMenu(); setAudioChannels(Settings.playerAudioChannels()); setCurrentFile(""); setWindowModified(false); resetSourceUpdated(); m_undoStack->clear(); MLT.resetURL(); QMutexLocker locker(&m_autosaveMutex); m_autosaveFile.reset(new AutoSaveFile(untitledFileName())); if (!isMultitrackValid()) m_player->enableTab(Player::ProjectTabIndex, false); } void MainWindow::onPlaylistModified() { setWindowModified(true); if (MLT.producer() && playlist() && (void *) MLT.producer()->get_producer() == (void *) playlist()->get_playlist()) m_player->onDurationChanged(); updateMarkers(); m_player->enableTab(Player::ProjectTabIndex, true); } void MainWindow::onMultitrackCreated() { m_player->enableTab(Player::ProjectTabIndex, true); } void MainWindow::onMultitrackClosed() { setAudioChannels(Settings.playerAudioChannels()); closeProducer(); setProfile(Settings.playerProfile()); resetVideoModeMenu(); setCurrentFile(""); setWindowModified(false); resetSourceUpdated(); m_undoStack->clear(); MLT.resetURL(); QMutexLocker locker(&m_autosaveMutex); m_autosaveFile.reset(new AutoSaveFile(untitledFileName())); if (!playlist() || playlist()->count() == 0) m_player->enableTab(Player::ProjectTabIndex, false); } void MainWindow::onMultitrackModified() { setWindowModified(true); // Reflect this playlist info onto the producer for keyframes dock. if (!m_timelineDock->selection().isEmpty()) { int trackIndex = m_timelineDock->selection().first().y(); int clipIndex = m_timelineDock->selection().first().x(); QScopedPointer info(m_timelineDock->getClipInfo(trackIndex, clipIndex)); if (info && info->producer && info->producer->is_valid()) { int expected = info->frame_in; QScopedPointer info2(m_timelineDock->getClipInfo(trackIndex, clipIndex - 1)); if (info2 && info2->producer && info2->producer->is_valid() && info2->producer->get(kShotcutTransitionProperty)) { // Factor in a transition left of the clip. expected -= info2->frame_count; info->producer->set(kPlaylistStartProperty, info2->start); } else { info->producer->set(kPlaylistStartProperty, info->start); } if (expected != info->producer->get_int(kFilterInProperty)) { int delta = expected - info->producer->get_int(kFilterInProperty); info->producer->set(kFilterInProperty, expected); emit m_filtersDock->producerInChanged(delta); } expected = info->frame_out; info2.reset(m_timelineDock->getClipInfo(trackIndex, clipIndex + 1)); if (info2 && info2->producer && info2->producer->is_valid() && info2->producer->get(kShotcutTransitionProperty)) { // Factor in a transition right of the clip. expected += info2->frame_count; } if (expected != info->producer->get_int(kFilterOutProperty)) { int delta = expected - info->producer->get_int(kFilterOutProperty); info->producer->set(kFilterOutProperty, expected); emit m_filtersDock->producerOutChanged(delta); } } } MLT.refreshConsumer(); } void MainWindow::onMultitrackDurationChanged() { if (MLT.producer() && (void *) MLT.producer()->get_producer() == (void *) multitrack()->get_producer()) m_player->onDurationChanged(); } void MainWindow::onNoteModified() { setWindowModified(true); } void MainWindow::onCutModified() { if (!playlist() && !multitrack()) { setWindowModified(true); } if (playlist()) m_playlistDock->setUpdateButtonEnabled(true); sourceUpdated(); } void MainWindow::onProducerModified() { setWindowModified(true); sourceUpdated(); } void MainWindow::onFilterModelChanged() { MLT.refreshConsumer(); setWindowModified(true); sourceUpdated(); if (playlist()) m_playlistDock->setUpdateButtonEnabled(true); } void MainWindow::updateMarkers() { if (playlist() && MLT.isPlaylist()) { QList markers; int n = playlist()->count(); for (int i = 0; i < n; i++) markers.append(playlist()->clip_start(i)); m_player->setMarkers(markers); } } void MainWindow::updateThumbnails() { if (Settings.playlistThumbnails() != "hidden") m_playlistDock->model()->refreshThumbnails(); } void MainWindow::on_actionUndo_triggered() { TimelineSelectionBlocker blocker(*m_timelineDock); m_undoStack->undo(); } void MainWindow::on_actionRedo_triggered() { TimelineSelectionBlocker blocker(*m_timelineDock); m_undoStack->redo(); } void MainWindow::on_actionFAQ_triggered() { QDesktopServices::openUrl(QUrl("https://www.shotcut.org/FAQ/")); } void MainWindow::on_actionForum_triggered() { QDesktopServices::openUrl(QUrl("https://forum.shotcut.org/")); } bool MainWindow::saveXML(const QString &filename, bool withRelativePaths) { bool result; QString notes = m_notesDock->getText(); if (m_timelineDock->model()->rowCount() > 0) { result = MLT.saveXML(filename, multitrack(), withRelativePaths, nullptr, false, notes); } else if (m_playlistDock->model()->rowCount() > 0) { int in = MLT.producer()->get_in(); int out = MLT.producer()->get_out(); MLT.producer()->set_in_and_out(0, MLT.producer()->get_length() - 1); result = MLT.saveXML(filename, playlist(), withRelativePaths, nullptr, false, notes); MLT.producer()->set_in_and_out(in, out); } else if (MLT.producer()) { result = MLT.saveXML(filename, (MLT.isMultitrack() || MLT.isPlaylist()) ? MLT.savedProducer() : 0, withRelativePaths, nullptr, false, notes); } else { // Save an empty playlist, which is accepted by both MLT and Shotcut. Mlt::Playlist playlist(MLT.profile()); result = MLT.saveXML(filename, &playlist, withRelativePaths, nullptr, false, notes); } return result; } void MainWindow::changeTheme(const QString &theme) { LOG_DEBUG() << "begin"; if (theme == "dark") { QApplication::setStyle("Fusion"); QPalette palette; palette.setColor(QPalette::Window, QColor(50, 50, 50)); palette.setColor(QPalette::WindowText, QColor(220, 220, 220)); palette.setColor(QPalette::Base, QColor(30, 30, 30)); palette.setColor(QPalette::AlternateBase, QColor(40, 40, 40)); palette.setColor(QPalette::Highlight, QColor(23, 92, 118)); palette.setColor(QPalette::HighlightedText, Qt::white); palette.setColor(QPalette::ToolTipBase, palette.color(QPalette::Highlight)); palette.setColor(QPalette::ToolTipText, palette.color(QPalette::WindowText)); palette.setColor(QPalette::Text, palette.color(QPalette::WindowText)); palette.setColor(QPalette::BrightText, Qt::red); palette.setColor(QPalette::Button, palette.color(QPalette::Window)); palette.setColor(QPalette::ButtonText, palette.color(QPalette::WindowText)); palette.setColor(QPalette::Link, palette.color(QPalette::Highlight).lighter()); palette.setColor(QPalette::LinkVisited, palette.color(QPalette::Highlight)); palette.setColor(QPalette::Disabled, QPalette::Text, Qt::darkGray); palette.setColor(QPalette::Disabled, QPalette::ButtonText, Qt::darkGray); QApplication::setPalette(palette); QIcon::setThemeName("dark"); QMetaObject::invokeMethod(&MAIN, "on_actionShowTextUnderIcons_toggled", Qt::QueuedConnection, Q_ARG(bool, Settings.textUnderIcons())); } else if (theme == "light") { QStyle *style = QStyleFactory::create("Fusion"); QApplication::setStyle(style); QApplication::setPalette(style->standardPalette()); QIcon::setThemeName("light"); QMetaObject::invokeMethod(&MAIN, "on_actionShowTextUnderIcons_toggled", Qt::QueuedConnection, Q_ARG(bool, Settings.textUnderIcons())); } else { QApplication::setStyle(qApp->property("system-style").toString()); QIcon::setThemeName("oxygen"); } emit QmlApplication::singleton().paletteChanged(); LOG_DEBUG() << "end"; } Mlt::Playlist *MainWindow::playlist() const { return m_playlistDock->model()->playlist(); } bool MainWindow::isPlaylistValid() const { return m_playlistDock->model()->playlist() && m_playlistDock->model()->rowCount() > 0; } Mlt::Producer *MainWindow::multitrack() const { return m_timelineDock->model()->tractor(); } bool MainWindow::isMultitrackValid() const { return m_timelineDock->model()->tractor() && !m_timelineDock->model()->trackList().empty(); } QWidget *MainWindow::loadProducerWidget(Mlt::Producer *producer) { QWidget *w = 0; QScrollArea *scrollArea = (QScrollArea *) m_propertiesDock->widget(); if (!producer || !producer->is_valid()) { if (scrollArea->widget()) scrollArea->widget()->deleteLater(); return w; } else { scrollArea->show(); } QString service(producer->get("mlt_service")); QString resource = QString::fromUtf8(producer->get("resource")); QString shotcutProducer(producer->get(kShotcutProducerProperty)); if (resource.startsWith("video4linux2:") || QString::fromUtf8(producer->get("resource1")).startsWith("video4linux2:")) w = new Video4LinuxWidget(this); else if (resource.startsWith("pulse:")) w = new PulseAudioWidget(this); else if (resource.startsWith("jack:")) w = new JackProducerWidget(this); else if (resource.startsWith("alsa:")) w = new AlsaWidget(this); else if (resource.startsWith("dshow:") || QString::fromUtf8(producer->get("resource1")).startsWith("dshow:")) w = new DirectShowVideoWidget(this); else if (resource.startsWith("avfoundation:")) w = new AvfoundationProducerWidget(this); else if (resource.startsWith("x11grab:")) w = new X11grabWidget(this); else if (resource.startsWith("gdigrab:")) w = new GDIgrabWidget(this); else if (service.startsWith("avformat") || shotcutProducer == "avformat") w = new AvformatProducerWidget(this); else if (MLT.isImageProducer(producer)) { w = new ImageProducerWidget(this); connect(m_player, SIGNAL(outChanged(int)), w, SLOT(updateDuration())); } else if (service == "decklink" || resource.contains("decklink")) w = new DecklinkProducerWidget(this); else if (service == "color") w = new ColorProducerWidget(this); else if (service == "glaxnimate") w = new GlaxnimateProducerWidget(this); else if (service == "noise") w = new NoiseWidget(this); else if (service == "frei0r.ising0r") w = new IsingWidget(this); else if (service == "frei0r.lissajous0r") w = new LissajousWidget(this); else if (service == "frei0r.plasma") w = new PlasmaWidget(this); else if (service == "frei0r.test_pat_B") w = new ColorBarsWidget(this); else if (service == "tone") w = new ToneProducerWidget(this); else if (service == "count") w = new CountProducerWidget(this); else if (service == "blipflash") w = new BlipProducerWidget(this); else if (producer->parent().get(kShotcutTransitionProperty)) { w = new LumaMixTransition(producer->parent(), this); scrollArea->setWidget(w); if (-1 != w->metaObject()->indexOfSignal("modified()")) connect(w, SIGNAL(modified()), SLOT(onProducerModified())); return w; } else if (mlt_service_playlist_type == producer->type()) { int trackIndex = m_timelineDock->currentTrack(); bool isBottomVideo = m_timelineDock->model()->data(m_timelineDock->model()->index(trackIndex), MultitrackModel::IsBottomVideoRole).toBool(); if (!isBottomVideo) { w = new TrackPropertiesWidget(*producer, this); scrollArea->setWidget(w); return w; } } else if (mlt_service_tractor_type == producer->type()) { w = new TimelinePropertiesWidget(*producer, this); scrollArea->setWidget(w); connect(w, SIGNAL(editProfile()), SLOT(on_actionAddCustomProfile_triggered())); return w; } if (w) { dynamic_cast(w)->setProducer(producer); if (-1 != w->metaObject()->indexOfSignal("producerChanged(Mlt::Producer*)")) { connect(w, SIGNAL(producerChanged(Mlt::Producer *)), SLOT(onProducerChanged())); connect(w, SIGNAL(producerChanged(Mlt::Producer *)), m_filterController, SLOT(setProducer(Mlt::Producer *))); connect(w, SIGNAL(producerChanged(Mlt::Producer *)), m_playlistDock, SLOT(onProducerChanged(Mlt::Producer *))); if (producer->get(kMultitrackItemProperty)) connect(w, SIGNAL(producerChanged(Mlt::Producer *)), m_timelineDock, SLOT(onProducerChanged(Mlt::Producer *))); } if (-1 != w->metaObject()->indexOfSignal("modified()")) { connect(w, SIGNAL(modified()), SLOT(onProducerModified())); connect(w, SIGNAL(modified()), m_playlistDock, SLOT(onProducerModified())); connect(w, SIGNAL(modified()), m_timelineDock, SLOT(onProducerModified())); connect(w, SIGNAL(modified()), m_keyframesDock, SLOT(onProducerModified())); connect(w, SIGNAL(modified()), m_filterController, SLOT(onProducerChanged())); } if (-1 != w->metaObject()->indexOfSlot("updateDuration()")) { connect(m_timelineDock, SIGNAL(durationChanged()), w, SLOT(updateDuration())); } if (-1 != w->metaObject()->indexOfSlot("rename()")) { connect(this, SIGNAL(renameRequested()), w, SLOT(rename())); } if (-1 != w->metaObject()->indexOfSlot("offerConvert(QString)")) { connect(m_filterController->attachedModel(), SIGNAL(requestConvert(QString, bool, bool)), w, SLOT(offerConvert(QString, bool, bool))); } scrollArea->setWidget(w); onProducerChanged(); } else if (scrollArea->widget()) { scrollArea->widget()->deleteLater(); } return w; } void MainWindow::on_actionEnter_Full_Screen_triggered() { #ifdef Q_OS_WIN bool isFull = isMaximized(); #else bool isFull = isFullScreen(); #endif if (isFull) { showNormal(); ui->actionEnter_Full_Screen->setText(tr("Enter Full Screen")); } else { #ifdef Q_OS_WIN showMaximized(); #else showFullScreen(); #endif ui->actionEnter_Full_Screen->setText(tr("Exit Full Screen")); } } void MainWindow::onGpuNotSupported() { Settings.setPlayerGPU(false); if (ui->actionGPU) { ui->actionGPU->setChecked(false); ui->actionGPU->setDisabled(true); } LOG_WARNING() << ""; QMessageBox::critical(this, qApp->applicationName(), tr("GPU effects are not supported")); } void MainWindow::stepLeftOneFrame() { m_player->seek(m_player->position() - 1); } void MainWindow::stepRightOneFrame() { m_player->seek(m_player->position() + 1); } void MainWindow::stepLeftOneSecond() { stepLeftBySeconds(-1); } void MainWindow::stepRightOneSecond() { stepLeftBySeconds(1); } void MainWindow::setInToCurrent(bool ripple) { if (m_player->tabIndex() == Player::ProjectTabIndex && isMultitrackValid()) { m_timelineDock->trimClipAtPlayhead(TimelineDock::TrimInPoint, ripple); } else if (MLT.isSeekableClip()) { m_player->setIn(m_player->position()); int delta = m_player->position() - MLT.producer()->get_in(); emit m_player->inChanged(delta); } } void MainWindow::setOutToCurrent(bool ripple) { if (m_player->tabIndex() == Player::ProjectTabIndex && isMultitrackValid()) { m_timelineDock->trimClipAtPlayhead(TimelineDock::TrimOutPoint, ripple); } else if (MLT.isSeekableClip()) { m_player->setOut(m_player->position()); int delta = m_player->position() - MLT.producer()->get_out(); emit m_player->outChanged(delta); } } void MainWindow::onShuttle(float x) { if (x == 0) { m_player->pause(); } else if (x > 0) { m_player->play(10.0 * x); } else { m_player->play(20.0 * x); } } void MainWindow::showUpgradePrompt() { if (Settings.checkUpgradeAutomatic()) { showStatusMessage("Checking for upgrade..."); m_network.get(QNetworkRequest(QUrl("https://check.shotcut.org/version.json"))); } else { QAction *action = new QAction(tr("Click here to check for a new version of Shotcut."), 0); connect(action, SIGNAL(triggered(bool)), SLOT(on_actionUpgrade_triggered())); showStatusMessage(action, 15 /* seconds */); } } void MainWindow::on_actionRealtime_triggered(bool checked) { Settings.setPlayerRealtime(checked); if (Settings.playerGPU()) MLT.pause(); if (MLT.consumer()) { MLT.restart(); } } void MainWindow::on_actionProgressive_triggered(bool checked) { MLT.videoWidget()->setProperty("progressive", checked); if (Settings.playerGPU()) MLT.pause(); if (MLT.consumer()) { MLT.profile().set_progressive(checked); MLT.updatePreviewProfile(); MLT.restart(); } Settings.setPlayerProgressive(checked); } void MainWindow::changeAudioChannels(bool checked, int channels) { if ( checked ) { Settings.setPlayerAudioChannels(channels); setAudioChannels(Settings.playerAudioChannels()); } } void MainWindow::on_actionChannels1_triggered(bool checked) { changeAudioChannels(checked, 1); } void MainWindow::on_actionChannels2_triggered(bool checked) { changeAudioChannels(checked, 2); } void MainWindow::on_actionChannels6_triggered(bool checked) { changeAudioChannels(checked, 6); } void MainWindow::changeDeinterlacer(bool checked, const char *method) { if (checked) { MLT.videoWidget()->setProperty("deinterlace_method", method); if (MLT.consumer()) { MLT.consumer()->set("deinterlace_method", method); MLT.refreshConsumer(); } } Settings.setPlayerDeinterlacer(method); } void MainWindow::on_actionOneField_triggered(bool checked) { changeDeinterlacer(checked, "onefield"); } void MainWindow::on_actionLinearBlend_triggered(bool checked) { changeDeinterlacer(checked, "linearblend"); } void MainWindow::on_actionYadifTemporal_triggered(bool checked) { changeDeinterlacer(checked, "yadif-nospatial"); } void MainWindow::on_actionYadifSpatial_triggered(bool checked) { changeDeinterlacer(checked, "yadif"); } void MainWindow::changeInterpolation(bool checked, const char *method) { if (checked) { MLT.videoWidget()->setProperty("rescale", method); if (MLT.consumer()) { MLT.consumer()->set("rescale", method); MLT.refreshConsumer(); } } Settings.setPlayerInterpolation(method); } void MainWindow::processMultipleFiles() { if (m_multipleFiles.length() <= 0) return; QStringList multipleFiles = m_multipleFiles; m_multipleFiles.clear(); int count = multipleFiles.length(); if (count > 1) { LongUiTask longTask(tr("Open Files")); m_playlistDock->show(); m_playlistDock->raise(); for (int i = 0; i < count; i++) { QString filename = multipleFiles.takeFirst(); LOG_DEBUG() << filename; longTask.reportProgress(QFileInfo(filename).fileName(), i, count); Mlt::Producer p(MLT.profile(), filename.toUtf8().constData()); if (p.is_valid()) { if (QDir::toNativeSeparators(filename) == QDir::toNativeSeparators(MAIN.fileName())) { MAIN.showStatusMessage(QObject::tr("You cannot add a project to itself!")); continue; } Util::getHash(p); Mlt::Producer *producer = MLT.setupNewProducer(&p); ProxyManager::generateIfNotExists(*producer); undoStack()->push(new Playlist::AppendCommand(*m_playlistDock->model(), MLT.XML(producer), false)); m_recentDock->add(filename.toUtf8().constData()); delete producer; } } emit m_playlistDock->model()->modified(); } if (m_isPlaylistLoaded && Settings.playerGPU()) { updateThumbnails(); m_isPlaylistLoaded = false; } } void MainWindow::onLanguageTriggered(QAction *action) { Settings.setLanguage(action->data().toString()); QMessageBox dialog(QMessageBox::Information, qApp->applicationName(), tr("You must restart Shotcut to switch to the new language.\n" "Do you want to restart now?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); dialog.setWindowModality(QmlApplication::dialogModality()); if (dialog.exec() == QMessageBox::Yes) { m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } } void MainWindow::on_actionNearest_triggered(bool checked) { changeInterpolation(checked, "nearest"); } void MainWindow::on_actionBilinear_triggered(bool checked) { changeInterpolation(checked, "bilinear"); } void MainWindow::on_actionBicubic_triggered(bool checked) { changeInterpolation(checked, "bicubic"); } void MainWindow::on_actionHyper_triggered(bool checked) { changeInterpolation(checked, "hyper"); } void MainWindow::on_actionJack_triggered(bool checked) { Settings.setPlayerJACK(checked); if (!MLT.enableJack(checked)) { if (ui->actionJack) ui->actionJack->setChecked(false); Settings.setPlayerJACK(false); QMessageBox::warning(this, qApp->applicationName(), tr("Failed to connect to JACK.\nPlease verify that JACK is installed and running.")); } } void MainWindow::on_actionGPU_triggered(bool checked) { if (checked) { QMessageBox dialog(QMessageBox::Warning, qApp->applicationName(), tr("GPU effects are experimental and may cause instability on some systems. " "Some CPU effects are incompatible with GPU effects and will be disabled. " "A project created with GPU effects can not be converted to a CPU only project later." "\n\n" "Do you want to enable GPU effects and restart Shotcut?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); dialog.setWindowModality(QmlApplication::dialogModality()); if (dialog.exec() == QMessageBox::Yes) { m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } else { ui->actionGPU->setChecked(false); } } else { QMessageBox dialog(QMessageBox::Information, qApp->applicationName(), tr("Shotcut must restart to disable GPU effects." "\n\n" "Disable GPU effects and restart?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); dialog.setWindowModality(QmlApplication::dialogModality()); if (dialog.exec() == QMessageBox::Yes) { m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } else { ui->actionGPU->setChecked(true); } } } void MainWindow::onExternalTriggered(QAction *action) { LOG_DEBUG() << action->data().toString(); bool isExternal = !action->data().toString().isEmpty(); Settings.setPlayerExternal(action->data().toString()); MLT.stop(); bool ok = false; int screen = action->data().toInt(&ok); if (ok || action->data().toString().isEmpty()) { m_player->moveVideoToScreen(ok ? screen : -2); isExternal = false; MLT.videoWidget()->setProperty("mlt_service", QVariant()); } else { m_player->moveVideoToScreen(-2); MLT.videoWidget()->setProperty("mlt_service", action->data()); } QString profile = Settings.playerProfile(); // Automatic not permitted for SDI/HDMI if (isExternal && profile.isEmpty()) { profile = "atsc_720p_50"; Settings.setPlayerProfile(profile); setProfile(profile); MLT.restart(); foreach (QAction *a, m_profileGroup->actions()) { if (a->data() == profile) { a->setChecked(true); break; } } } else { MLT.consumerChanged(); } // Automatic not permitted for SDI/HDMI m_profileGroup->actions().at(0)->setEnabled(!isExternal); // Disable progressive option when SDI/HDMI ui->actionProgressive->setEnabled(!isExternal); bool isProgressive = isExternal ? MLT.profile().progressive() : ui->actionProgressive->isChecked(); MLT.videoWidget()->setProperty("progressive", isProgressive); if (MLT.consumer()) { MLT.consumer()->set("progressive", isProgressive); MLT.restart(); } if (m_keyerMenu) m_keyerMenu->setEnabled(action->data().toString().startsWith("decklink")); // Preview scaling not permitted for SDI/HDMI if (isExternal) { setPreviewScale(0); m_previewScaleGroup->setEnabled(false); } else { setPreviewScale(Settings.playerPreviewScale()); m_previewScaleGroup->setEnabled(true); } } void MainWindow::onKeyerTriggered(QAction *action) { LOG_DEBUG() << action->data().toString(); MLT.videoWidget()->setProperty("keyer", action->data()); MLT.consumerChanged(); Settings.setPlayerKeyerMode(action->data().toInt()); } void MainWindow::onProfileTriggered(QAction *action) { Settings.setPlayerProfile(action->data().toString()); if (MLT.producer() && MLT.producer()->is_valid()) { // Save the XML to get correct in/out points before profile is changed. QString xml = MLT.XML(); setProfile(action->data().toString()); MLT.restart(xml); emit producerOpened(false); } else { setProfile(action->data().toString()); } } void MainWindow::onProfileChanged() { if (multitrack() && MLT.isMultitrack() && (m_timelineDock->selection().isEmpty() || m_timelineDock->currentTrack() == -1)) { emit m_timelineDock->selected(multitrack()); } } void MainWindow::on_actionAddCustomProfile_triggered() { QString xml; if (MLT.producer() && MLT.producer()->is_valid()) { // Save the XML to get correct in/out points before profile is changed. xml = MLT.XML(); } CustomProfileDialog dialog(this); dialog.setWindowModality(QmlApplication::dialogModality()); if (dialog.exec() == QDialog::Accepted) { QString name = dialog.profileName(); if (!name.isEmpty()) { addCustomProfile(name, customProfileMenu(), actionProfileRemove(), profileGroup()); } else if (m_profileGroup->checkedAction()) { m_profileGroup->checkedAction()->setChecked(false); } // Use the new profile. emit profileChanged(); if (!xml.isEmpty()) { MLT.restart(xml); emit producerOpened(false); } } } void MainWindow::restartAfterChangeTheme() { QMessageBox dialog(QMessageBox::Information, qApp->applicationName(), tr("You must restart %1 to switch to the new theme.\n" "Do you want to restart now?").arg(qApp->applicationName()), QMessageBox::No | QMessageBox::Yes, this); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); dialog.setWindowModality(QmlApplication::dialogModality()); if (dialog.exec() == QMessageBox::Yes) { m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } } void MainWindow::on_actionSystemTheme_triggered() { Settings.setTheme("system"); restartAfterChangeTheme(); } void MainWindow::on_actionFusionDark_triggered() { Settings.setTheme("dark"); restartAfterChangeTheme(); } void MainWindow::on_actionFusionLight_triggered() { Settings.setTheme("light"); restartAfterChangeTheme(); } void MainWindow::on_actionJobPriorityLow_triggered() { Settings.setJobPriority("low"); } void MainWindow::on_actionJobPriorityNormal_triggered() { Settings.setJobPriority("normal"); } void MainWindow::on_actionTutorials_triggered() { QDesktopServices::openUrl(QUrl("https://www.shotcut.org/tutorials/")); } void MainWindow::on_actionRestoreLayout_triggered() { auto mode = Settings.layoutMode(); if (mode != LayoutMode::Custom) { // Clear the saved layout for this mode Settings.setLayout(QString(kReservedLayoutPrefix).arg(mode), QByteArray(), QByteArray()); // Reset the layout mode so the current layout is saved as custom when trigger action Settings.setLayoutMode(); } switch (mode) { case LayoutMode::Custom: ui->actionLayoutEditing->setChecked(true); Q_FALLTHROUGH(); case LayoutMode::Editing: on_actionLayoutEditing_triggered(); break; case LayoutMode::Logging: on_actionLayoutLogging_triggered(); break; case LayoutMode::Effects: on_actionLayoutEffects_triggered(); break; case LayoutMode::Color: on_actionLayoutColor_triggered(); break; case LayoutMode::Audio: on_actionLayoutAudio_triggered(); break; case LayoutMode::PlayerOnly: on_actionLayoutPlayer_triggered(); break; } } void MainWindow::on_actionShowTitleBars_triggered(bool checked) { QList docks = findChildren(); for (int i = 0; i < docks.count(); i++) { QDockWidget *dock = docks.at(i); if (checked) { dock->setTitleBarWidget(0); } else { if (!dock->isFloating()) { dock->setTitleBarWidget(new QWidget); } } } Settings.setShowTitleBars(checked); } void MainWindow::on_actionShowToolbar_triggered(bool checked) { ui->mainToolBar->setVisible(checked); } void MainWindow::onToolbarVisibilityChanged(bool visible) { ui->actionShowToolbar->setChecked(visible); Settings.setShowToolBar(visible); } void MainWindow::on_menuExternal_aboutToShow() { foreach (QAction *action, m_externalGroup->actions()) { bool ok = false; int i = action->data().toInt(&ok); if (ok) { if (i == QApplication::desktop()->screenNumber(this)) { if (action->isChecked()) { m_externalGroup->actions().first()->setChecked(true); Settings.setPlayerExternal(QString()); } action->setDisabled(true); } else { action->setEnabled(true); } } } } void MainWindow::on_actionUpgrade_triggered() { if (Settings.askUpgradeAutomatic()) { QMessageBox dialog(QMessageBox::Question, qApp->applicationName(), tr("Do you want to automatically check for updates in the future?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); dialog.setCheckBox(new QCheckBox(tr("Do not show this anymore.", "Automatic upgrade check dialog"))); Settings.setCheckUpgradeAutomatic(dialog.exec() == QMessageBox::Yes); if (dialog.checkBox()->isChecked()) Settings.setAskUpgradeAutomatic(false); } showStatusMessage("Checking for upgrade..."); m_network.get(QNetworkRequest(QUrl("https://check.shotcut.org/version.json"))); } void MainWindow::on_actionOpenXML_triggered() { QString path = Settings.openPath(); #ifdef Q_OS_MAC path.append("/*"); #endif QStringList filenames = QFileDialog::getOpenFileNames(this, tr("Open File"), path, tr("MLT XML (*.mlt);;All Files (*)"), nullptr, Util::getFileDialogOptions()); if (filenames.length() > 0) { QString url = filenames.first(); MltXmlChecker checker; if (checker.check(url) == QXmlStreamReader::NoError) { if (!isCompatibleWithGpuMode(checker)) return; isXmlRepaired(checker, url); // Check if the locale usage differs. // Get current locale. QString localeName = QString(::setlocale(MLT_LC_CATEGORY, nullptr)).toUpper(); // Test if it is C or POSIX. bool currentlyUsingLocale = (localeName != "" && localeName != "C" && localeName != "POSIX"); if (currentlyUsingLocale != checker.usesLocale()) { // Show a warning dialog and cancel if requested. QMessageBox dialog(QMessageBox::Question, qApp->applicationName(), tr("The decimal point of the MLT XML file\nyou want to open is incompatible.\n\n" "Do you want to continue to open this MLT XML file?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::No); dialog.setEscapeButton(QMessageBox::No); if (dialog.exec() != QMessageBox::Yes) return; } } else { showStatusMessage(tr("Failed to open ").append(url)); showIncompatibleProjectMessage(checker.shotcutVersion()); return; } Settings.setOpenPath(QFileInfo(url).path()); activateWindow(); if (filenames.length() > 1) m_multipleFiles = filenames; if (!MLT.openXML(url)) { open(MLT.producer()); m_recentDock->add(url); LOG_INFO() << url; } else { showStatusMessage(tr("Failed to open ") + url); emit openFailed(url); } } } void MainWindow::on_actionGammaSRGB_triggered(bool checked) { Q_UNUSED(checked) Settings.setPlayerGamma("iec61966_2_1"); MLT.restart(); MLT.refreshConsumer(); } void MainWindow::on_actionGammaRec709_triggered(bool checked) { Q_UNUSED(checked) Settings.setPlayerGamma("bt709"); MLT.restart(); MLT.refreshConsumer(); } void MainWindow::onFocusChanged(QWidget *, QWidget * ) const { LOG_DEBUG() << "Focuswidget changed"; LOG_DEBUG() << "Current focusWidget:" << QApplication::focusWidget(); LOG_DEBUG() << "Current focusObject:" << QApplication::focusObject(); LOG_DEBUG() << "Current focusWindow:" << QApplication::focusWindow(); } void MainWindow::on_actionScrubAudio_triggered(bool checked) { Settings.setPlayerScrubAudio(checked); } #if !defined(Q_OS_MAC) void MainWindow::onDrawingMethodTriggered(QAction *action) { Settings.setDrawMethod(action->data().toInt()); QMessageBox dialog(QMessageBox::Information, qApp->applicationName(), tr("You must restart Shotcut to change the display method.\n" "Do you want to restart now?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); dialog.setWindowModality(QmlApplication::dialogModality()); if (dialog.exec() == QMessageBox::Yes) { m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } } #endif void MainWindow::on_actionApplicationLog_triggered() { TextViewerDialog dialog(this); QDir dir = Settings.appDataLocation(); QFile logFile(dir.filePath("shotcut-log.txt")); logFile.open(QIODevice::ReadOnly | QIODevice::Text); dialog.setText(logFile.readAll()); logFile.close(); dialog.setWindowTitle(tr("Application Log")); dialog.exec(); } void MainWindow::on_actionClose_triggered() { m_timelineDock->stopRecording(); if (continueModified()) { LOG_DEBUG() << ""; QMutexLocker locker(&m_autosaveMutex); m_autosaveFile.reset(); locker.unlock(); setCurrentFile(""); MLT.resetURL(); MLT.setProjectFolder(QString()); ui->actionSave->setEnabled(false); MLT.stop(); if (multitrack()) m_timelineDock->model()->close(); if (playlist()) m_playlistDock->model()->close(); else onMultitrackClosed(); m_notesDock->setText(""); m_player->enableTab(Player::SourceTabIndex, false); MLT.purgeMemoryPool(); MLT.resetLocale(); } } void MainWindow::onPlayerTabIndexChanged(int index) { if (Player::SourceTabIndex == index) m_timelineDock->saveAndClearSelection(); else m_timelineDock->restoreSelection(); } void MainWindow::onUpgradeCheckFinished(QNetworkReply *reply) { if (!reply->error()) { QByteArray response = reply->readAll(); LOG_DEBUG() << "response: " << response; QJsonDocument json = QJsonDocument::fromJson(response); QString current = qApp->applicationVersion(); if (!json.isNull() && json.object().value("version_string").type() == QJsonValue::String) { QString latest = json.object().value("version_string").toString(); if (current != "adhoc" && QVersionNumber::fromString(current) < QVersionNumber::fromString(latest)) { QAction *action = new QAction(tr("Shotcut version %1 is available! Click here to get it.").arg( latest), 0); connect(action, SIGNAL(triggered(bool)), SLOT(onUpgradeTriggered())); if (!json.object().value("url").isUndefined()) m_upgradeUrl = json.object().value("url").toString(); showStatusMessage(action, 15 /* seconds */); } else { showStatusMessage(tr("You are running the latest version of Shotcut.")); } reply->deleteLater(); return; } else { LOG_WARNING() << "failed to parse version.json"; } } else { LOG_WARNING() << reply->errorString(); } QAction *action = new QAction( tr("Failed to read version.json when checking. Click here to go to the Web site."), 0); connect(action, SIGNAL(triggered(bool)), SLOT(onUpgradeTriggered())); showStatusMessage(action); reply->deleteLater(); } void MainWindow::onUpgradeTriggered() { QDesktopServices::openUrl(QUrl(m_upgradeUrl)); } void MainWindow::onTimelineSelectionChanged() { bool enable = (m_timelineDock->selection().size() > 0); ui->actionCut->setEnabled(enable); ui->actionCopy->setEnabled(enable); } void MainWindow::on_actionCut_triggered() { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->removeSelection(true); } void MainWindow::on_actionCopy_triggered() { m_timelineDock->show(); m_timelineDock->raise(); if (!m_timelineDock->selection().isEmpty()) m_timelineDock->copy(m_timelineDock->selection().first().y(), m_timelineDock->selection().first().x()); } void MainWindow::on_actionPaste_triggered() { m_timelineDock->show(); m_timelineDock->raise(); m_timelineDock->insert(-1); } void MainWindow::onClipCopied() { m_player->enableTab(Player::SourceTabIndex); } void MainWindow::on_actionExportEDL_triggered() { // Dialog to get export file name. QString path = Settings.savePath(); QString caption = tr("Export EDL"); QString saveFileName = QFileDialog::getSaveFileName(this, caption, path, tr("EDL (*.edl);;All Files (*)"), nullptr, Util::getFileDialogOptions()); if (!saveFileName.isEmpty()) { QFileInfo fi(saveFileName); if (fi.suffix() != "edl") saveFileName += ".edl"; if (Util::warnIfNotWritable(saveFileName, this, caption)) return; // Locate the JavaScript file in the filesystem. QDir qmlDir = QmlUtilities::qmlDir(); qmlDir.cd("export-edl"); QString jsFileName = qmlDir.absoluteFilePath("export-edl.js"); QFile scriptFile(jsFileName); if (scriptFile.open(QIODevice::ReadOnly)) { // Read JavaScript into a string. QTextStream stream(&scriptFile); stream.setCodec("UTF-8"); stream.setAutoDetectUnicode(true); QString contents = stream.readAll(); scriptFile.close(); // Evaluate JavaScript. QJSEngine jsEngine; QJSValue result = jsEngine.evaluate(contents, jsFileName); if (!result.isError()) { // Call the JavaScript main function. QJSValue options = jsEngine.newObject(); options.setProperty("useBaseNameForReelName", true); options.setProperty("useBaseNameForClipComment", true); options.setProperty("channelsAV", "AA/V"); QJSValueList args; args << MLT.XML(0, true, true) << options; result = result.call(args); if (!result.isError()) { // Save the result with the export file name. QFile f(saveFileName); f.open(QIODevice::WriteOnly | QIODevice::Text); f.write(result.toString().toLatin1()); f.close(); } } if (result.isError()) { LOG_ERROR() << "Uncaught exception at line" << result.property("lineNumber").toInt() << ":" << result.toString(); showStatusMessage(tr("A JavaScript error occurred during export.")); } } else { showStatusMessage(tr("Failed to open export-edl.js")); } } } void MainWindow::on_actionExportFrame_triggered() { if (!MLT.producer() || !MLT.producer()->is_valid()) return; filterController()->setCurrentFilter(QmlFilter::DeselectCurrentFilter); Mlt::GLWidget *glw = qobject_cast(MLT.videoWidget()); connect(glw, SIGNAL(imageReady()), SLOT(onGLWidgetImageReady())); MLT.setPreviewScale(0); glw->requestImage(); MLT.refreshConsumer(); } void MainWindow::onGLWidgetImageReady() { Mlt::GLWidget *glw = qobject_cast(MLT.videoWidget()); QImage image = glw->image(); disconnect(glw, SIGNAL(imageReady()), this, nullptr); if (Settings.playerGPU() || Settings.playerPreviewScale()) { MLT.setPreviewScale(Settings.playerPreviewScale()); } if (!image.isNull() && (glw->imageIsProxy() || (MLT.isMultitrack() && Settings.proxyEnabled())) ) { QMessageBox dialog(QMessageBox::Question, tr("Export frame from proxy?"), tr("This frame may be from a lower resolution proxy instead of the original source.\n\n" "Do you still want to continue?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); dialog.setWindowModality(QmlApplication::dialogModality()); if (dialog.exec() != QMessageBox::Yes) { return; } } if (!image.isNull()) { SaveImageDialog dialog(this, tr("Export Frame"), image); dialog.exec(); if ( !dialog.saveFile().isEmpty() ) { m_recentDock->add(dialog.saveFile()); } } else { showStatusMessage(tr("Unable to export frame.")); } } void MainWindow::on_actionAppDataSet_triggered() { QMessageBox dialog(QMessageBox::Information, qApp->applicationName(), tr("You must restart Shotcut to change the data directory.\n" "Do you want to continue?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); dialog.setWindowModality(QmlApplication::dialogModality()); if (dialog.exec() != QMessageBox::Yes) return; QString dirName = QFileDialog::getExistingDirectory(this, tr("Data Directory"), Settings.appDataLocation(), Util::getFileDialogOptions()); if (!dirName.isEmpty()) { // Move the data files. QDirIterator it(Settings.appDataLocation()); while (it.hasNext()) { if (!it.filePath().isEmpty() && it.fileName() != "." && it.fileName() != "..") { if (!QFile::exists(dirName + "/" + it.fileName())) { if (it.fileInfo().isDir()) { if (!QFile::rename(it.filePath(), dirName + "/" + it.fileName())) LOG_WARNING() << "Failed to move" << it.filePath() << "to" << dirName + "/" + it.fileName(); } else { if (!QFile::copy(it.filePath(), dirName + "/" + it.fileName())) LOG_WARNING() << "Failed to copy" << it.filePath() << "to" << dirName + "/" + it.fileName(); } } } it.next(); } writeSettings(); Settings.setAppDataLocally(dirName); m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } } void MainWindow::on_actionAppDataShow_triggered() { Util::showInFolder(Settings.appDataLocation()); } void MainWindow::on_actionNew_triggered() { on_actionClose_triggered(); } void MainWindow::on_actionKeyboardShortcuts_triggered() { QDesktopServices::openUrl(QUrl("https://www.shotcut.org/howtos/keyboard-shortcuts/")); } void MainWindow::on_actionLayoutLogging_triggered() { Settings.setLayout(QString(kReservedLayoutPrefix).arg(Settings.layoutMode()), QByteArray(), saveState()); Settings.setLayoutMode(LayoutMode::Logging); auto state = Settings.layoutState(QString(kReservedLayoutPrefix).arg(LayoutMode::Logging)); if (state.isEmpty()) { restoreState(kLayoutLoggingDefault); // setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); // setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); // setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); // setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); // resizeDocks({m_playlistDock, m_propertiesDock}, // {qFloor(width() * 0.25), qFloor(width() * 0.25)}, Qt::Horizontal); } else { // LOG_DEBUG() << state.toBase64(); restoreState(state); } Settings.setWindowState(saveState()); } void MainWindow::on_actionLayoutEditing_triggered() { Settings.setLayout(QString(kReservedLayoutPrefix).arg(Settings.layoutMode()), QByteArray(), saveState()); Settings.setLayoutMode(LayoutMode::Editing); auto state = Settings.layoutState(QString(kReservedLayoutPrefix).arg(LayoutMode::Editing)); if (state.isEmpty()) { restoreState(kLayoutEditingDefault); // resetDockCorners(); } else { // LOG_DEBUG() << state.toBase64(); restoreState(state); } Settings.setWindowState(saveState()); } void MainWindow::on_actionLayoutEffects_triggered() { Settings.setLayout(QString(kReservedLayoutPrefix).arg(Settings.layoutMode()), QByteArray(), saveState()); Settings.setLayoutMode(LayoutMode::Effects); auto state = Settings.layoutState(QString(kReservedLayoutPrefix).arg(LayoutMode::Effects)); if (state.isEmpty()) { restoreState(kLayoutEffectsDefault); // resetDockCorners(); } else { // LOG_DEBUG() << state.toBase64(); restoreState(state); } Settings.setWindowState(saveState()); } void MainWindow::on_actionLayoutColor_triggered() { Settings.setLayout(QString(kReservedLayoutPrefix).arg(Settings.layoutMode()), QByteArray(), saveState()); Settings.setLayoutMode(LayoutMode::Color); auto state = Settings.layoutState(QString(kReservedLayoutPrefix).arg(LayoutMode::Color)); if (state.isEmpty()) { restoreState(kLayoutColorDefault); // setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); // setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); // setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); // setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); } else { // LOG_DEBUG() << state.toBase64(); restoreState(state); } Settings.setWindowState(saveState()); } void MainWindow::on_actionLayoutAudio_triggered() { Settings.setLayout(QString(kReservedLayoutPrefix).arg(Settings.layoutMode()), QByteArray(), saveState()); Settings.setLayoutMode(LayoutMode::Audio); auto state = Settings.layoutState(QString(kReservedLayoutPrefix).arg(LayoutMode::Audio)); if (state.isEmpty()) { restoreState(kLayoutAudioDefault); // setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); // setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); // setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); // setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); } else { // LOG_DEBUG() << state.toBase64(); restoreState(state); } Settings.setWindowState(saveState()); } void MainWindow::on_actionLayoutPlayer_triggered() { Settings.setLayout(QString(kReservedLayoutPrefix).arg(Settings.layoutMode()), QByteArray(), saveState()); Settings.setLayoutMode(LayoutMode::PlayerOnly); auto state = Settings.layoutState(QString(kReservedLayoutPrefix).arg(LayoutMode::PlayerOnly)); if (state.isEmpty()) { restoreState(kLayoutPlayerDefault); // resetDockCorners(); } else { // LOG_DEBUG() << state.toBase64(); restoreState(state); } Settings.setWindowState(saveState()); } void MainWindow::on_actionLayoutPlaylist_triggered() { if (Settings.layoutMode() != LayoutMode::Custom) { Settings.setLayout(QString(kReservedLayoutPrefix).arg(Settings.layoutMode()), QByteArray(), saveState()); Settings.setLayoutMode(LayoutMode::Custom); } clearCurrentLayout(); restoreState(Settings.windowStateDefault()); m_recentDock->show(); m_recentDock->raise(); m_playlistDock->show(); m_playlistDock->raise(); Settings.setWindowState(saveState()); } void MainWindow::on_actionLayoutClip_triggered() { if (Settings.layoutMode() != LayoutMode::Custom) { Settings.setLayout(QString(kReservedLayoutPrefix).arg(Settings.layoutMode()), QByteArray(), saveState()); Settings.setLayoutMode(LayoutMode::Custom); } clearCurrentLayout(); restoreState(Settings.windowStateDefault()); m_recentDock->show(); m_recentDock->raise(); m_filtersDock->show(); m_filtersDock->raise(); Settings.setWindowState(saveState()); } void MainWindow::on_actionLayoutAdd_triggered() { QInputDialog dialog(this); dialog.setInputMode(QInputDialog::TextInput); dialog.setWindowTitle(tr("Add Custom Layout")); dialog.setLabelText(tr("Name")); dialog.setWindowModality(QmlApplication::dialogModality()); auto result = dialog.exec(); auto name = dialog.textValue(); if (result == QDialog::Accepted && !name.isEmpty()) { if (Settings.setLayout(name, saveGeometry(), saveState())) { Settings.setLayoutMode(); clearCurrentLayout(); Settings.sync(); if (Settings.layouts().size() == 1) { ui->menuLayout->addAction(ui->actionLayoutRemove); ui->menuLayout->addSeparator(); } ui->menuLayout->addAction(addLayout(m_layoutGroup, name)); } } } void MainWindow::onLayoutTriggered(QAction *action) { if (Settings.layoutMode() != LayoutMode::Custom) { Settings.setLayout(QString(kReservedLayoutPrefix).arg(Settings.layoutMode()), QByteArray(), saveState()); Settings.setLayoutMode(LayoutMode::Custom); } clearCurrentLayout(); restoreState(Settings.layoutState(action->text())); Settings.setWindowState(saveState()); } void MainWindow::on_actionProfileRemove_triggered() { QDir dir(Settings.appDataLocation()); if (dir.cd("profiles")) { // Setup the dialog. QStringList profiles = dir.entryList(QDir::Files | QDir::NoDotAndDotDot | QDir::Readable); ListSelectionDialog dialog(profiles, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setWindowTitle(tr("Remove Video Mode")); // Show the dialog. if (dialog.exec() == QDialog::Accepted) { removeCustomProfiles(dialog.selection(), dir, customProfileMenu(), actionProfileRemove()); } } } void MainWindow::on_actionLayoutRemove_triggered() { // Setup the dialog. ListSelectionDialog dialog(Settings.layouts(), this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setWindowTitle(tr("Remove Layout")); // Show the dialog. if (dialog.exec() == QDialog::Accepted) { foreach (const QString &layout, dialog.selection()) { // Update the configuration. if (Settings.removeLayout(layout)) Settings.sync(); // Locate the menu item. foreach (QAction *action, ui->menuLayout->actions()) { if (action->text() == layout) { // Remove the menu item. delete action; break; } } } // If no more custom layouts. if (Settings.layouts().size() == 0) { // Remove the Remove action and separator. ui->menuLayout->removeAction(ui->actionLayoutRemove); bool isSecondSeparator = false; foreach (QAction *action, ui->menuLayout->actions()) { if (action->isSeparator()) { if (isSecondSeparator) { delete action; break; } else { isSecondSeparator = true; } } } } } } void MainWindow::on_actionOpenOther2_triggered() { ui->actionOpenOther2->menu()->popup(mapToGlobal(ui->mainToolBar->geometry().bottomLeft()) + QPoint( 64, 0)); } void MainWindow::onOpenOtherTriggered(QWidget *widget) { QDialog dialog(this); dialog.resize(426, 288); QVBoxLayout vlayout(&dialog); vlayout.addWidget(widget); QDialogButtonBox buttonBox(&dialog); buttonBox.setOrientation(Qt::Horizontal); buttonBox.setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); vlayout.addWidget(&buttonBox); connect(&buttonBox, SIGNAL(accepted()), &dialog, SLOT(accept())); connect(&buttonBox, SIGNAL(rejected()), &dialog, SLOT(reject())); QString name = widget->objectName(); if (name == "NoiseWidget" || dialog.exec() == QDialog::Accepted) { closeProducer(); auto &profile = MLT.profile(); auto producer = dynamic_cast(widget)->newProducer(profile); if (!profile.is_explicit()) { profile.from_producer(*producer); profile.set_width(Util::coerceMultiple(profile.width())); profile.set_height(Util::coerceMultiple(profile.height())); } MLT.updatePreviewProfile(); setPreviewScale(Settings.playerPreviewScale()); open(producer); if (name == "TextProducerWidget") { m_filtersDock->show(); m_filtersDock->raise(); } else { m_propertiesDock->show(); m_propertiesDock->raise(); } } delete widget; } void MainWindow::onOpenOtherTriggered() { if (sender()->objectName() == "color") onOpenOtherTriggered(new ColorProducerWidget(this)); else if (sender()->objectName() == "text") onOpenOtherTriggered(new TextProducerWidget(this)); else if (sender()->objectName() == "glaxnimate") onOpenOtherTriggered(new GlaxnimateProducerWidget(this)); else if (sender()->objectName() == "noise") onOpenOtherTriggered(new NoiseWidget(this)); else if (sender()->objectName() == "ising0r") onOpenOtherTriggered(new IsingWidget(this)); else if (sender()->objectName() == "lissajous0r") onOpenOtherTriggered(new LissajousWidget(this)); else if (sender()->objectName() == "plasma") onOpenOtherTriggered(new PlasmaWidget(this)); else if (sender()->objectName() == "test_pat_B") onOpenOtherTriggered(new ColorBarsWidget(this)); else if (sender()->objectName() == "tone") onOpenOtherTriggered(new ToneProducerWidget(this)); else if (sender()->objectName() == "count") onOpenOtherTriggered(new CountProducerWidget(this)); else if (sender()->objectName() == "blipflash") onOpenOtherTriggered(new BlipProducerWidget(this)); else if (sender()->objectName() == "v4l2") onOpenOtherTriggered(new Video4LinuxWidget(this)); else if (sender()->objectName() == "pulse") onOpenOtherTriggered(new PulseAudioWidget(this)); else if (sender()->objectName() == "jack") onOpenOtherTriggered(new JackProducerWidget(this)); else if (sender()->objectName() == "alsa") onOpenOtherTriggered(new AlsaWidget(this)); #if defined(Q_OS_MAC) else if (sender()->objectName() == "device") onOpenOtherTriggered(new AvfoundationProducerWidget(this)); #elif defined(Q_OS_WIN) else if (sender()->objectName() == "device") onOpenOtherTriggered(new DirectShowVideoWidget(this)); #endif else if (sender()->objectName() == "decklink") onOpenOtherTriggered(new DecklinkProducerWidget(this)); } void MainWindow::on_actionClearRecentOnExit_toggled(bool arg1) { Settings.setClearRecent(arg1); if (arg1) Settings.setRecent(QStringList()); } void MainWindow::onSceneGraphInitialized() { if (Settings.playerGPU() && Settings.playerWarnGPU()) { QMessageBox dialog(QMessageBox::Warning, qApp->applicationName(), tr("GPU effects are EXPERIMENTAL, UNSTABLE and UNSUPPORTED! Unsupported means do not report bugs about it.\n\n" "Do you want to disable GPU effects and restart Shotcut?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); dialog.setWindowModality(QmlApplication::dialogModality()); if (dialog.exec() == QMessageBox::Yes) { ui->actionGPU->setChecked(false); m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } else { ui->actionGPU->setVisible(true); } } else if (Settings.playerGPU()) { ui->actionGPU->setVisible(true); } } void MainWindow::on_actionShowTextUnderIcons_toggled(bool b) { ui->mainToolBar->setToolButtonStyle(b ? Qt::ToolButtonTextUnderIcon : Qt::ToolButtonIconOnly); Settings.setTextUnderIcons(b); updateLayoutSwitcher(); } void MainWindow::on_actionShowSmallIcons_toggled(bool b) { ui->mainToolBar->setIconSize(b ? QSize(16, 16) : QSize()); Settings.setSmallIcons(b); updateLayoutSwitcher(); } void MainWindow::onPlaylistInChanged(int in) { m_player->blockSignals(true); m_player->setIn(in); m_player->blockSignals(false); } void MainWindow::onPlaylistOutChanged(int out) { m_player->blockSignals(true); m_player->setOut(out); m_player->blockSignals(false); } void MainWindow::on_actionPreviewNone_triggered(bool checked) { if (checked) { Settings.setPlayerPreviewScale(0); setPreviewScale(0); m_player->showIdleStatus(); } } void MainWindow::on_actionPreview360_triggered(bool checked) { if (checked) { Settings.setPlayerPreviewScale(360); setPreviewScale(360); m_player->showIdleStatus(); } } void MainWindow::on_actionPreview540_triggered(bool checked) { if (checked) { Settings.setPlayerPreviewScale(540); setPreviewScale(540); m_player->showIdleStatus(); } } void MainWindow::on_actionPreview720_triggered(bool checked) { if (checked) { Settings.setPlayerPreviewScale(720); setPreviewScale(720); m_player->showIdleStatus(); } } QUuid MainWindow::timelineClipUuid(int trackIndex, int clipIndex) { QScopedPointer info(m_timelineDock->getClipInfo(trackIndex, clipIndex)); if (info && info->cut && info->cut->is_valid()) return MLT.ensureHasUuid(*info->cut); return QUuid(); } void MainWindow::replaceInTimeline(const QUuid &uuid, Mlt::Producer &producer) { int trackIndex = -1; int clipIndex = -1; // lookup the current track and clip index by UUID QScopedPointer info(MAIN.timelineClipInfoByUuid(uuid, trackIndex, clipIndex)); if (trackIndex >= 0 && clipIndex >= 0) { Util::getHash(producer); Util::applyCustomProperties(producer, *info->producer, producer.get_in(), producer.get_out()); m_timelineDock->replace(trackIndex, clipIndex, MLT.XML(&producer)); } } Mlt::ClipInfo *MainWindow::timelineClipInfoByUuid(const QUuid &uuid, int &trackIndex, int &clipIndex) { return m_timelineDock->model()->findClipByUuid(uuid, trackIndex, clipIndex); } void MainWindow::replaceAllByHash(const QString &hash, Mlt::Producer &producer, bool isProxy) { Util::getHash(producer); if (!isProxy) m_recentDock->add(producer.get("resource")); if (MLT.isClip() && Util::getHash(*MLT.producer()) == hash) { Util::applyCustomProperties(producer, *MLT.producer(), MLT.producer()->get_in(), MLT.producer()->get_out()); MLT.copyFilters(*MLT.producer(), producer); MLT.close(); m_player->setPauseAfterOpen(true); open(new Mlt::Producer(MLT.profile(), "xml-string", MLT.XML(&producer).toUtf8().constData())); } else if (MLT.savedProducer() && Util::getHash(*MLT.savedProducer()) == hash) { Util::applyCustomProperties(producer, *MLT.savedProducer(), MLT.savedProducer()->get_in(), MLT.savedProducer()->get_out()); MLT.copyFilters(*MLT.savedProducer(), producer); MLT.setSavedProducer(&producer); } if (playlist()) { if (isProxy) { m_playlistDock->replaceClipsWithHash(hash, producer); } else { // Append to playlist producer.set(kPlaylistIndexProperty, playlist()->count()); MAIN.undoStack()->push( new Playlist::AppendCommand(*m_playlistDock->model(), MLT.XML(&producer))); } } if (isMultitrackValid()) { m_timelineDock->replaceClipsWithHash(hash, producer); } } void MainWindow::on_actionTopics_triggered() { QDesktopServices::openUrl(QUrl("https://www.shotcut.org/howtos/")); } void MainWindow::on_actionSync_triggered() { auto dialog = new SystemSyncDialog(this); dialog->show(); dialog->raise(); dialog->activateWindow(); } void MainWindow::on_actionUseProxy_triggered(bool checked) { if (MLT.producer()) { QDir dir(m_currentFile.isEmpty() ? QDir::tempPath() : QFileInfo(m_currentFile).dir()); QScopedPointer tmp(new QTemporaryFile(dir.filePath("shotcut-XXXXXX.mlt"))); tmp->open(); tmp->close(); QString fileName = tmp->fileName(); tmp->remove(); tmp.reset(); LOG_DEBUG() << fileName; if (saveXML(fileName)) { MltXmlChecker checker; Settings.setProxyEnabled(checked); checker.check(fileName); if (!isXmlRepaired(checker, fileName)) { QFile::remove(fileName); return; } if (checker.isUpdated()) { QFile::remove(fileName); fileName = checker.tempFile().fileName(); } // Open the temporary file int result = 0; { LongUiTask longTask(checked ? tr("Turn Proxy On") : tr("Turn Proxy Off")); QFuture future = QtConcurrent::run([ = ]() { return MLT.open(QDir::fromNativeSeparators(fileName), QDir::fromNativeSeparators(m_currentFile)); }); result = longTask.wait(tr("Converting"), future); } if (!result) { auto position = m_player->position(); m_undoStack->clear(); m_player->stop(); m_player->setPauseAfterOpen(true); open(MLT.producer()); MLT.seek(m_player->position()); m_player->seek(position); if (checked && (isPlaylistValid() || isMultitrackValid())) { // Prompt user if they want to create missing proxies QMessageBox dialog(QMessageBox::Question, qApp->applicationName(), tr("Do you want to create missing proxies for every file in this project?\n\n" "You must reopen your project after all proxy jobs are finished."), QMessageBox::No | QMessageBox::Yes, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); if (dialog.exec() == QMessageBox::Yes) { Mlt::Producer producer(playlist()); if (producer.is_valid()) { ProxyManager::generateIfNotExistsAll(producer); } producer = multitrack(); if (producer.is_valid()) { ProxyManager::generateIfNotExistsAll(producer); } } } } else if (fileName != untitledFileName()) { showStatusMessage(tr("Failed to open ") + fileName); emit openFailed(fileName); } } else { ui->actionUseProxy->setChecked(!checked); showSaveError(); } QFile::remove(fileName); } else { Settings.setProxyEnabled(checked); } m_player->showIdleStatus(); } void MainWindow::on_actionProxyStorageSet_triggered() { // Present folder dialog just like App Data Directory QString dirName = QFileDialog::getExistingDirectory(this, tr("Proxy Folder"), Settings.proxyFolder(), Util::getFileDialogOptions()); if (!dirName.isEmpty() && dirName != Settings.proxyFolder()) { auto oldFolder = Settings.proxyFolder(); Settings.setProxyFolder(dirName); Settings.sync(); // Get a count for the progress dialog auto oldDir = QDir(oldFolder); auto dirList = oldDir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); auto count = dirList.size(); if (count > 0) { // Prompt user if they want to create missing proxies QMessageBox dialog(QMessageBox::Question, qApp->applicationName(), tr("Do you want to move all files from the old folder to the new folder?"), QMessageBox::No | QMessageBox::Yes, this); dialog.setWindowModality(QmlApplication::dialogModality()); dialog.setDefaultButton(QMessageBox::Yes); dialog.setEscapeButton(QMessageBox::No); if (dialog.exec() == QMessageBox::Yes) { // Move the existing files LongUiTask longTask(tr("Moving Files")); int i = 0; for (const auto &fileName : dirList) { if (!fileName.isEmpty() && !QFile::exists(dirName + "/" + fileName)) { LOG_DEBUG() << "moving" << oldDir.filePath(fileName) << "to" << dirName + "/" + fileName; longTask.reportProgress(fileName, i++, count); if (!QFile::rename(oldDir.filePath(fileName), dirName + "/" + fileName)) LOG_WARNING() << "Failed to move" << oldDir.filePath(fileName); } } } } } } void MainWindow::on_actionProxyStorageShow_triggered() { Util::showInFolder(ProxyManager::dir().path()); } void MainWindow::on_actionProxyUseProjectFolder_triggered(bool checked) { Settings.setProxyUseProjectFolder(checked); } void MainWindow::on_actionProxyUseHardware_triggered(bool checked) { if (checked && Settings.encodeHardware().isEmpty()) { if (!m_encodeDock->detectHardwareEncoders()) ui->actionProxyUseHardware->setChecked(false); } Settings.setProxyUseHardware(ui->actionProxyUseHardware->isChecked()); } void MainWindow::on_actionProxyConfigureHardware_triggered() { m_encodeDock->on_hwencodeButton_clicked(); if (Settings.encodeHardware().isEmpty()) { ui->actionProxyUseHardware->setChecked(false); Settings.setProxyUseHardware(false); } } void MainWindow::updateLayoutSwitcher() { if (Settings.textUnderIcons() && !Settings.smallIcons()) { auto layoutSwitcher = findChild(kLayoutSwitcherName); if (layoutSwitcher) { layoutSwitcher->show(); for (const auto &child : layoutSwitcher->findChildren()) { child->show(); } } else { layoutSwitcher = new QWidget; layoutSwitcher->setObjectName(kLayoutSwitcherName); auto layoutGrid = new QGridLayout(layoutSwitcher); layoutGrid->setContentsMargins(0, 0, 0, 0); ui->mainToolBar->insertWidget(ui->dummyAction, layoutSwitcher); auto button = new QToolButton; button->setAutoRaise(true); button->setDefaultAction(ui->actionLayoutLogging); layoutGrid->addWidget(button, 0, 0, Qt::AlignCenter); button = new QToolButton; button->setAutoRaise(true); button->setDefaultAction(ui->actionLayoutEditing); layoutGrid->addWidget(button, 0, 1, Qt::AlignCenter); button = new QToolButton; button->setAutoRaise(true); button->setDefaultAction(ui->actionLayoutEffects); layoutGrid->addWidget(button, 0, 2, Qt::AlignCenter); button = new QToolButton; button->setAutoRaise(true); button->setDefaultAction(ui->actionLayoutColor); layoutGrid->addWidget(button, 1, 0, Qt::AlignCenter); button = new QToolButton; button->setAutoRaise(true); button->setDefaultAction(ui->actionLayoutAudio); layoutGrid->addWidget(button, 1, 1, Qt::AlignCenter); button = new QToolButton; button->setAutoRaise(true); button->setDefaultAction(ui->actionLayoutPlayer); layoutGrid->addWidget(button, 1, 2, Qt::AlignCenter); } ui->mainToolBar->removeAction(ui->actionLayoutLogging); ui->mainToolBar->removeAction(ui->actionLayoutEditing); ui->mainToolBar->removeAction(ui->actionLayoutEffects); ui->mainToolBar->removeAction(ui->actionLayoutColor); ui->mainToolBar->removeAction(ui->actionLayoutAudio); ui->mainToolBar->removeAction(ui->actionLayoutPlayer); } else { auto layoutSwitcher = findChild(kLayoutSwitcherName); if (layoutSwitcher) { layoutSwitcher->hide(); for (const auto &child : layoutSwitcher->findChildren()) { child->hide(); } ui->mainToolBar->insertAction(ui->dummyAction, ui->actionLayoutLogging); ui->mainToolBar->insertAction(ui->dummyAction, ui->actionLayoutEditing); ui->mainToolBar->insertAction(ui->dummyAction, ui->actionLayoutEffects); ui->mainToolBar->insertAction(ui->dummyAction, ui->actionLayoutColor); ui->mainToolBar->insertAction(ui->dummyAction, ui->actionLayoutAudio); ui->mainToolBar->insertAction(ui->dummyAction, ui->actionLayoutPlayer); } } } void MainWindow::clearCurrentLayout() { auto currentLayout = ui->actionLayoutLogging->actionGroup()->checkedAction(); if (currentLayout) { currentLayout->setChecked(false); } } void MainWindow::onClipboardChanged() { auto s = QGuiApplication::clipboard()->text(); if (MLT.isMltXml(s) && !s.contains(kShotcutFiltersClipboard)) { m_clipboardUpdatedAt = QDateTime::currentDateTime(); LOG_DEBUG() << m_clipboardUpdatedAt; } } void MainWindow::sourceUpdated() { if (MLT.isClip()) { m_sourceUpdatedAt = QDateTime::currentDateTime(); } } void MainWindow::resetSourceUpdated() { m_sourceUpdatedAt.setSecsSinceEpoch(0); } void MainWindow::on_actionExportChapters_triggered() { // Dialog to get export file name. QString path = Settings.savePath(); QString caption = tr("Export Chapters"); QString saveFileName = QFileDialog::getSaveFileName(this, caption, path, tr("Text (*.txt);;All Files (*)"), nullptr, Util::getFileDialogOptions()); if (!saveFileName.isEmpty()) { QFileInfo fi(saveFileName); if (fi.suffix() != "txt") saveFileName += ".txt"; if (Util::warnIfNotWritable(saveFileName, this, caption)) return; // Locate the JavaScript file in the filesystem. QDir qmlDir = QmlUtilities::qmlDir(); qmlDir.cd("export-chapters"); QString jsFileName = qmlDir.absoluteFilePath("export-chapters.js"); QFile scriptFile(jsFileName); if (scriptFile.open(QIODevice::ReadOnly)) { // Read JavaScript into a string. QTextStream stream(&scriptFile); stream.setCodec("UTF-8"); stream.setAutoDetectUnicode(true); QString contents = stream.readAll(); scriptFile.close(); // Evaluate JavaScript. QJSEngine jsEngine; QJSValue result = jsEngine.evaluate(contents, jsFileName); if (!result.isError()) { // Call the JavaScript main function. QJSValueList args; args << MLT.XML(0, true, true); result = result.call(args); if (!result.isError()) { // Save the result with the export file name. QFile f(saveFileName); f.open(QIODevice::WriteOnly | QIODevice::Text); f.write(result.toString().toLatin1()); f.close(); } } if (result.isError()) { LOG_ERROR() << "Uncaught exception at line" << result.property("lineNumber").toInt() << ":" << result.toString(); showStatusMessage(tr("A JavaScript error occurred during export.")); } } else { showStatusMessage(tr("Failed to open export-chapters.js")); } } } void MainWindow::on_actionAudioVideoDevice_triggered() { QDialog dialog(this); dialog.setWindowModality(QmlApplication::dialogModality()); auto layout = new QVBoxLayout(&dialog); QWidget *widget = nullptr; #if defined(Q_OS_LINUX) widget = new Video4LinuxWidget; dialog.resize(500, 400); dialog.setSizeGripEnabled(true); #elif defined(Q_OS_MAC) widget = new AvfoundationProducerWidget; dialog.resize(1, 1); #elif defined(Q_OS_WIN) widget = new DirectShowVideoWidget; dialog.resize(1, 1); #endif layout->addWidget(widget); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); layout->addWidget(buttonBox); if (dialog.exec() == QDialog::Accepted) { Mlt::Profile profile; profile.set_explicit(1); delete dynamic_cast(widget)->newProducer(profile); } }