übernahme Code Shortcut

This commit is contained in:
georg0480
2026-01-31 15:28:10 +01:00
parent 6f4d6b9301
commit ef46c21291
1787 changed files with 1126465 additions and 0 deletions

395
src/CMakeLists.txt Normal file
View File

@@ -0,0 +1,395 @@
find_package(Qt6 REQUIRED COMPONENTS Core)
add_executable(shotcut WIN32 MACOSX_BUNDLE
abstractproducerwidget.cpp abstractproducerwidget.h
actions.cpp actions.h
autosavefile.cpp autosavefile.h
commands/filtercommands.cpp commands/filtercommands.h
commands/markercommands.cpp commands/markercommands.h
commands/playlistcommands.cpp commands/playlistcommands.h
commands/subtitlecommands.cpp commands/subtitlecommands.h
commands/timelinecommands.cpp commands/timelinecommands.h
commands/undohelper.cpp commands/undohelper.h
controllers/filtercontroller.cpp controllers/filtercontroller.h
controllers/scopecontroller.cpp controllers/scopecontroller.h
database.cpp database.h
dialogs/actionsdialog.cpp dialogs/actionsdialog.h
dialogs/addencodepresetdialog.cpp dialogs/addencodepresetdialog.h
dialogs/addencodepresetdialog.ui
dialogs/alignaudiodialog.cpp dialogs/alignaudiodialog.h
dialogs/alignmentarray.cpp dialogs/alignmentarray.h
dialogs/bitratedialog.h dialogs/bitratedialog.cpp
dialogs/customprofiledialog.cpp dialogs/customprofiledialog.h
dialogs/customprofiledialog.ui
dialogs/durationdialog.cpp dialogs/durationdialog.h
dialogs/durationdialog.ui
dialogs/editmarkerdialog.cpp dialogs/editmarkerdialog.h
dialogs/filedatedialog.cpp dialogs/filedatedialog.h
dialogs/filedownloaddialog.cpp dialogs/filedownloaddialog.h
dialogs/listselectiondialog.cpp dialogs/listselectiondialog.h
dialogs/listselectiondialog.ui
dialogs/longuitask.cpp dialogs/longuitask.h
dialogs/multifileexportdialog.cpp dialogs/multifileexportdialog.h
dialogs/resourcedialog.cpp dialogs/resourcedialog.h
dialogs/saveimagedialog.cpp dialogs/saveimagedialog.h
dialogs/slideshowgeneratordialog.cpp dialogs/slideshowgeneratordialog.h
dialogs/speechdialog.h dialogs/speechdialog.cpp
dialogs/subtitletrackdialog.cpp dialogs/subtitletrackdialog.h
dialogs/systemsyncdialog.cpp dialogs/systemsyncdialog.h
dialogs/systemsyncdialog.ui
dialogs/textviewerdialog.cpp dialogs/textviewerdialog.h
dialogs/textviewerdialog.ui
dialogs/transcodedialog.cpp dialogs/transcodedialog.h
dialogs/transcodedialog.ui
dialogs/transcribeaudiodialog.cpp dialogs/transcribeaudiodialog.h
dialogs/unlinkedfilesdialog.cpp dialogs/unlinkedfilesdialog.h
dialogs/unlinkedfilesdialog.ui
docks/encodedock.cpp docks/encodedock.h
docks/encodedock.ui
docks/filesdock.cpp docks/filesdock.h
docks/filesdock.ui
docks/filtersdock.cpp docks/filtersdock.h
docks/jobsdock.cpp docks/jobsdock.h
docks/jobsdock.ui
docks/keyframesdock.cpp docks/keyframesdock.h
docks/markersdock.cpp docks/markersdock.h
docks/notesdock.cpp docks/notesdock.h
docks/playlistdock.cpp docks/playlistdock.h
docks/playlistdock.ui
docks/recentdock.cpp docks/recentdock.h
docks/recentdock.ui
docks/scopedock.cpp docks/scopedock.h
docks/subtitlesdock.cpp docks/subtitlesdock.h
docks/timelinedock.cpp docks/timelinedock.h
FlatpakWrapperGenerator.cpp FlatpakWrapperGenerator.h
htmlgenerator.h htmlgenerator.cpp
jobqueue.cpp jobqueue.h
jobs/abstractjob.cpp jobs/abstractjob.h
jobs/bitrateviewerjob.h jobs/bitrateviewerjob.cpp
jobs/dockerpulljob.h jobs/dockerpulljob.cpp
jobs/encodejob.cpp jobs/encodejob.h
jobs/ffmpegjob.cpp jobs/ffmpegjob.h
jobs/ffprobejob.cpp jobs/ffprobejob.h
jobs/gopro2gpxjob.cpp jobs/gopro2gpxjob.h
jobs/htmlgeneratorjob.cpp jobs/htmlgeneratorjob.h
jobs/kokorodokijob.cpp jobs/kokorodokijob.h
jobs/meltjob.cpp jobs/meltjob.h
jobs/postjobaction.cpp jobs/postjobaction.h
jobs/qimagejob.cpp jobs/qimagejob.h
jobs/screencapturejob.cpp jobs/screencapturejob.h
jobs/videoqualityjob.cpp jobs/videoqualityjob.h
jobs/whisperjob.cpp jobs/whisperjob.h
main.cpp
mainwindow.cpp mainwindow.h
mainwindow.ui
mltcontroller.cpp mltcontroller.h
mltxmlchecker.cpp mltxmlchecker.h
models/actionsmodel.cpp models/actionsmodel.h
models/alignclipsmodel.cpp models/alignclipsmodel.h
models/attachedfiltersmodel.cpp models/attachedfiltersmodel.h
models/audiolevelstask.cpp models/audiolevelstask.h
models/extensionmodel.cpp models/extensionmodel.h
models/keyframesmodel.cpp models/keyframesmodel.h
models/markersmodel.cpp models/markersmodel.h
models/metadatamodel.cpp models/metadatamodel.h
models/motiontrackermodel.h models/motiontrackermodel.cpp
models/multitrackmodel.cpp models/multitrackmodel.h
models/playlistmodel.cpp models/playlistmodel.h
models/resourcemodel.cpp models/resourcemodel.h
models/subtitles.cpp models/subtitles.h
models/subtitlesmodel.cpp models/subtitlesmodel.h
models/subtitlesselectionmodel.cpp models/subtitlesselectionmodel.h
openotherdialog.cpp openotherdialog.h
openotherdialog.ui
player.cpp player.h
proxymanager.cpp proxymanager.h
qmltypes/colordialog.h qmltypes/colordialog.cpp
qmltypes/colorpickeritem.cpp qmltypes/colorpickeritem.h
qmltypes/colorwheelitem.cpp qmltypes/colorwheelitem.h
qmltypes/filedialog.h qmltypes/filedialog.cpp
qmltypes/fontdialog.h qmltypes/fontdialog.cpp
qmltypes/messagedialog.h qmltypes/messagedialog.cpp
qmltypes/qmlapplication.cpp qmltypes/qmlapplication.h
qmltypes/qmleditmenu.cpp qmltypes/qmleditmenu.h
qmltypes/qmlextension.cpp qmltypes/qmlextension.h
qmltypes/qmlfile.cpp qmltypes/qmlfile.h
qmltypes/qmlfilter.cpp qmltypes/qmlfilter.h
qmltypes/qmlmarkermenu.cpp qmltypes/qmlmarkermenu.h
qmltypes/qmlmetadata.cpp qmltypes/qmlmetadata.h
qmltypes/qmlproducer.cpp qmltypes/qmlproducer.h
qmltypes/qmlprofile.cpp qmltypes/qmlprofile.h
qmltypes/qmlrichtext.cpp qmltypes/qmlrichtext.h
qmltypes/qmlrichtextmenu.cpp qmltypes/qmlrichtextmenu.h
qmltypes/qmlutilities.cpp qmltypes/qmlutilities.h
qmltypes/qmlview.cpp qmltypes/qmlview.h
qmltypes/thumbnailprovider.cpp qmltypes/thumbnailprovider.h
qmltypes/timelineitems.cpp qmltypes/timelineitems.h
resources.qrc
scrubbar.cpp scrubbar.h
settings.cpp settings.h
sharedframe.cpp sharedframe.h
shotcut_mlt_properties.h
transcoder.cpp transcoder.h
screencapture/rectangleselector.cpp
screencapture/rectangleselector.h
screencapture/screencapture.cpp
screencapture/screencapture.h
screencapture/toolbarwidget.cpp
screencapture/toolbarwidget.h
screencapture/windowpicker.cpp
screencapture/windowpicker.h
spatialmedia/box.cpp spatialmedia/box.h
spatialmedia/container.cpp spatialmedia/container.h
spatialmedia/mpeg4_container.cpp spatialmedia/mpeg4_container.h
spatialmedia/sa3d.cpp spatialmedia/sa3d.h
spatialmedia/spatialmedia.cpp spatialmedia/spatialmedia.h
transportcontrol.h
util.cpp util.h
videowidget.cpp videowidget.h
widgets/alsawidget.cpp widgets/alsawidget.h
widgets/alsawidget.ui
widgets/audiometerwidget.cpp widgets/audiometerwidget.h
widgets/audioscale.cpp widgets/audioscale.h
widgets/avformatproducerwidget.cpp widgets/avformatproducerwidget.h
widgets/avformatproducerwidget.ui
widgets/avfoundationproducerwidget.cpp widgets/avfoundationproducerwidget.h
widgets/avfoundationproducerwidget.ui
widgets/blipproducerwidget.cpp widgets/blipproducerwidget.h
widgets/blipproducerwidget.ui
widgets/colorbarswidget.cpp widgets/colorbarswidget.h
widgets/colorbarswidget.ui
widgets/colorproducerwidget.cpp widgets/colorproducerwidget.h
widgets/colorproducerwidget.ui
widgets/colorwheel.cpp widgets/colorwheel.h
widgets/countproducerwidget.cpp widgets/countproducerwidget.h
widgets/countproducerwidget.ui
widgets/decklinkproducerwidget.cpp widgets/decklinkproducerwidget.h
widgets/decklinkproducerwidget.ui
widgets/directshowvideowidget.cpp widgets/directshowvideowidget.h
widgets/directshowvideowidget.ui
widgets/docktoolbar.cpp widgets/docktoolbar.h
widgets/editmarkerwidget.cpp widgets/editmarkerwidget.h
widgets/exportpresetstreeview.cpp widgets/exportpresetstreeview.h
widgets/frameratewidget.cpp widgets/frameratewidget.h
widgets/glaxnimateproducerwidget.cpp widgets/glaxnimateproducerwidget.h
widgets/glaxnimateproducerwidget.ui
widgets/htmlgeneratorwidget.cpp widgets/htmlgeneratorwidget.h
widgets/htmlgeneratorwidget.ui
widgets/imageproducerwidget.cpp widgets/imageproducerwidget.h
widgets/imageproducerwidget.ui
widgets/isingwidget.cpp widgets/isingwidget.h
widgets/isingwidget.ui
widgets/lineeditclear.cpp widgets/lineeditclear.h
widgets/lissajouswidget.cpp widgets/lissajouswidget.h
widgets/lissajouswidget.ui
widgets/lumamixtransition.cpp widgets/lumamixtransition.h
widgets/lumamixtransition.ui
widgets/mltclipproducerwidget.cpp widgets/mltclipproducerwidget.h
widgets/networkproducerwidget.cpp widgets/networkproducerwidget.h
widgets/networkproducerwidget.ui
widgets/newprojectfolder.cpp widgets/newprojectfolder.h
widgets/newprojectfolder.ui
widgets/noisewidget.cpp widgets/noisewidget.h
widgets/noisewidget.ui
widgets/plasmawidget.cpp widgets/plasmawidget.h
widgets/plasmawidget.ui
widgets/playlisticonview.cpp widgets/playlisticonview.h
widgets/playlistlistview.cpp widgets/playlistlistview.h
widgets/playlisttable.cpp widgets/playlisttable.h
widgets/producerpreviewwidget.cpp widgets/producerpreviewwidget.h
widgets/pulseaudiowidget.cpp widgets/pulseaudiowidget.h
widgets/pulseaudiowidget.ui
widgets/resourcewidget.cpp widgets/resourcewidget.h
widgets/scopes/audioloudnessscopewidget.cpp widgets/scopes/audioloudnessscopewidget.h
widgets/scopes/audiopeakmeterscopewidget.cpp widgets/scopes/audiopeakmeterscopewidget.h
widgets/scopes/audiospectrumscopewidget.cpp widgets/scopes/audiospectrumscopewidget.h
widgets/scopes/audiosurroundscopewidget.cpp widgets/scopes/audiosurroundscopewidget.h
widgets/scopes/audiovectorscopewidget.cpp widgets/scopes/audiovectorscopewidget.h
widgets/scopes/audiowaveformscopewidget.cpp widgets/scopes/audiowaveformscopewidget.h
widgets/scopes/scopewidget.cpp widgets/scopes/scopewidget.h
widgets/scopes/videohistogramscopewidget.cpp widgets/scopes/videohistogramscopewidget.h
widgets/scopes/videorgbparadescopewidget.cpp widgets/scopes/videorgbparadescopewidget.h
widgets/scopes/videorgbwaveformscopewidget.cpp widgets/scopes/videorgbwaveformscopewidget.h
widgets/scopes/videovectorscopewidget.cpp widgets/scopes/videovectorscopewidget.h
widgets/scopes/videowaveformscopewidget.cpp widgets/scopes/videowaveformscopewidget.h
widgets/scopes/videozoomscopewidget.cpp widgets/scopes/videozoomscopewidget.h
widgets/scopes/videozoomwidget.cpp widgets/scopes/videozoomwidget.h
widgets/screenselector.cpp widgets/screenselector.h
widgets/servicepresetwidget.cpp widgets/servicepresetwidget.h
widgets/servicepresetwidget.ui
widgets/slideshowgeneratorwidget.cpp widgets/slideshowgeneratorwidget.h
widgets/statuslabelwidget.cpp widgets/statuslabelwidget.h
widgets/textproducerwidget.cpp widgets/textproducerwidget.h
widgets/textproducerwidget.ui
widgets/timelinepropertieswidget.cpp widgets/timelinepropertieswidget.h
widgets/timelinepropertieswidget.ui
widgets/timespinbox.cpp widgets/timespinbox.h
widgets/toneproducerwidget.cpp widgets/toneproducerwidget.h
widgets/toneproducerwidget.ui
widgets/trackpropertieswidget.cpp widgets/trackpropertieswidget.h
widgets/trackpropertieswidget.ui
widgets/video4linuxwidget.cpp widgets/video4linuxwidget.h
widgets/video4linuxwidget.ui
../icons/resources.qrc
)
add_custom_target(OTHER_FILES
SOURCES
../.github/ISSUE_TEMPLATE.md
../.github/workflows/build-linux-unstable.yml
../.github/workflows/build-linux.yml
../.github/workflows/build-macos-unstable.yml
../.github/workflows/build-macos.yml
../.github/workflows/build-sdk-windows-unstable.yml
../.github/workflows/build-sdk-windows.yml
../.github/workflows/build-windows-unstable.yml
../.github/workflows/build-windows.yml
../COPYING
../icons/dark/index.theme
../icons/light/index.theme
../packaging/linux/appimage/appimage.yml
../packaging/linux/Makefile
../packaging/linux/org.shotcut.Shotcut.desktop
../packaging/linux/org.shotcut.Shotcut.metainfo.xml.in
../packaging/linux/org.shotcut.Shotcut.xml
../packaging/linux/shotcut.1
../packaging/linux/snapcraft.yaml.in
../packaging/macos/Info.plist.in
../packaging/macos/shotcut.icns
../packaging/windows/shotcut.iss
../packaging/windows/shotcut.rc.in
../README.md
../scripts/build-shotcut-msys2.sh
../scripts/build-shotcut.sh
../scripts/codesign_and_notarize.sh
../scripts/notarize.sh
../scripts/staple.sh
)
target_link_libraries(shotcut
PRIVATE
CuteLogger
PkgConfig::mlt++
PkgConfig::FFTW
Qt6::Charts
Qt6::Multimedia
Qt6::Network
Qt6::OpenGL
Qt6::OpenGLWidgets
Qt6::QuickControls2
Qt6::QuickWidgets
Qt6::Sql
Qt6::WebSockets
Qt6::Widgets
Qt6::Xml
)
if(UNIX AND NOT APPLE)
target_link_libraries(shotcut PRIVATE Qt6::DBus X11::X11)
endif()
file(GLOB_RECURSE QML_SRC "qml/*")
target_sources(shotcut PRIVATE ${QML_SRC})
target_include_directories(shotcut PRIVATE ${CMAKE_SOURCE_DIR}/CuteLogger/include)
target_compile_definitions(shotcut PRIVATE SHOTCUT_VERSION="${SHOTCUT_VERSION}")
# Add compile definitions when certain custom cache variables are ON
if(EXTERNAL_LAUNCHERS)
target_compile_definitions(shotcut PRIVATE EXTERNAL_LAUNCHERS)
endif()
if(USE_VULKAN)
target_compile_definitions(shotcut PRIVATE USE_VULKAN)
endif()
if(WIN32)
# Windows resource
string(REPLACE . , PRODUCT_VERSION ${PROJECT_VERSION})
configure_file(${CMAKE_SOURCE_DIR}/packaging/windows/shotcut.rc.in ${CMAKE_SOURCE_DIR}/packaging/windows/shotcut.rc)
target_sources(shotcut PRIVATE ${CMAKE_SOURCE_DIR}/packaging/windows/shotcut.rc)
# Windows integration features
# These are not yet available in Qt 6. They plan to make a cross-platform API planned but not yet implemented.
# find_package(Qt 6.2 REQUIRED COMPONENTS WinExtras)
# target_sources(shotcut PRIVATE windowstools.cpp windowstools.h)
# target_link_libraries(shotcut PRIVATE Qt6::WinExtras)
target_sources(shotcut PRIVATE widgets/d3dvideowidget.h widgets/d3dvideowidget.cpp)
target_sources(shotcut PRIVATE widgets/openglvideowidget.h widgets/openglvideowidget.cpp)
target_link_libraries(shotcut PRIVATE d3d11 d3dcompiler)
# Runtime exception handler for debug only
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64")
target_include_directories(shotcut PRIVATE ${CMAKE_SOURCE_DIR}/drmingw/include)
target_link_directories(shotcut PRIVATE ${CMAKE_SOURCE_DIR}/drmingw/x64/lib)
target_link_libraries(shotcut PRIVATE debug exchndl)
endif()
if(WINDOWS_DEPLOY)
install(TARGETS shotcut RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX})
else()
target_compile_definitions(shotcut PRIVATE NODEPLOY)
install(TARGETS shotcut RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()
install(DIRECTORY qml DESTINATION ${CMAKE_INSTALL_PREFIX}/share/shotcut/)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/filter-sets DESTINATION ${CMAKE_INSTALL_PREFIX}/share/shotcut/)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/voices DESTINATION ${CMAKE_INSTALL_PREFIX}/share/shotcut/)
else()
target_sources(shotcut PRIVATE widgets/openglvideowidget.h widgets/openglvideowidget.cpp)
endif()
if(APPLE)
target_sources(shotcut PRIVATE macos.mm macos.h
widgets/metalvideowidget.h widgets/metalvideowidget.mm)
set_target_properties(shotcut PROPERTIES
OUTPUT_NAME "Shotcut"
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/packaging/macos/Info.plist.in)
find_library(FOUNDATION Foundation)
find_library(COCOA Cocoa)
target_link_libraries(shotcut PRIVATE ${FOUNDATION} ${COCOA})
set(APP_ICON ${CMAKE_SOURCE_DIR}/packaging/macos/shotcut.icns)
target_sources(shotcut PRIVATE ${APP_ICON})
set_source_files_properties(${APP_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
# Create a symlink to the qml folder after building
# These are skipped for release because it breaks the app bundling and is only for development.
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Shotcut.app/Contents/Resources/shotcut)
file(CREATE_LINK ${CMAKE_SOURCE_DIR}/src/qml ${CMAKE_CURRENT_BINARY_DIR}/Shotcut.app/Contents/Resources/shotcut/qml SYMBOLIC)
file(CREATE_LINK ${CMAKE_SOURCE_DIR}/filter-sets ${CMAKE_CURRENT_BINARY_DIR}/Shotcut.app/Contents/Resources/shotcut/filter-sets SYMBOLIC)
file(CREATE_LINK ${CMAKE_SOURCE_DIR}/voices ${CMAKE_CURRENT_BINARY_DIR}/Shotcut.app/Contents/Resources/shotcut/voices SYMBOLIC)
endif()
install(TARGETS shotcut BUNDLE DESTINATION ${CMAKE_INSTALL_PREFIX})
install(DIRECTORY qml DESTINATION ${CMAKE_INSTALL_PREFIX}/Shotcut.app/Contents/Resources/shotcut/)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/filter-sets DESTINATION ${CMAKE_INSTALL_PREFIX}/Shotcut.app/Contents/Resources/shotcut/)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/voices DESTINATION ${CMAKE_INSTALL_PREFIX}/Shotcut.app/Contents/Resources/shotcut/)
endif()
if(UNIX AND NOT APPLE)
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/share/shotcut)
file(CREATE_LINK ${CMAKE_SOURCE_DIR}/src/qml ${CMAKE_BINARY_DIR}/share/shotcut/qml SYMBOLIC)
file(CREATE_LINK ${CMAKE_SOURCE_DIR}/filter-sets ${CMAKE_BINARY_DIR}/share/shotcut/filter-sets SYMBOLIC)
file(CREATE_LINK ${CMAKE_SOURCE_DIR}/voices ${CMAKE_BINARY_DIR}/share/shotcut/voices SYMBOLIC)
include(GNUInstallDirs)
install(TARGETS shotcut RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(DIRECTORY qml DESTINATION ${CMAKE_INSTALL_DATADIR}/shotcut/)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/filter-sets DESTINATION ${CMAKE_INSTALL_DATADIR}/shotcut/)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/voices DESTINATION ${CMAKE_INSTALL_DATADIR}/shotcut/)
string(REPLACE . - SHOTCUT_DATE ${SHOTCUT_VERSION})
string(PREPEND SHOTCUT_DATE "20")
configure_file(${CMAKE_SOURCE_DIR}/packaging/linux/org.shotcut.Shotcut.metainfo.xml.in
${CMAKE_BINARY_DIR}/packaging/linux/org.shotcut.Shotcut.metainfo.xml)
install(FILES ${CMAKE_BINARY_DIR}/packaging/linux/org.shotcut.Shotcut.metainfo.xml
DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo/)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/linux/org.shotcut.Shotcut.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications/)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/linux/org.shotcut.Shotcut.xml
DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages/)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/linux/icons/64x64/org.shotcut.Shotcut.png
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps/)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/linux/icons/128x128/org.shotcut.Shotcut.png
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps/)
install(FILES ${CMAKE_SOURCE_DIR}/packaging/linux/shotcut.1
DESTINATION ${CMAKE_INSTALL_DATADIR}/man/man1/)
endif()

View File

@@ -0,0 +1,189 @@
/*
* Copyright (c) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "FlatpakWrapperGenerator.h"
#include "Logger.h"
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QRegularExpression>
#include <QTextStream>
static QString trim(const QString &s)
{
QString t = s;
return t.trimmed();
}
QList<AppEntry> FlatpakWrapperGenerator::listInstalledApps() const
{
QProcess p;
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
auto env = QProcessEnvironment::systemEnvironment();
env.remove("LD_LIBRARY_PATH");
LOG_DEBUG() << env.toStringList();
p.setProcessEnvironment(env);
#endif
p.start("/usr/bin/flatpak", {"list", "--app", "--columns=application,branch"});
if (!p.waitForStarted(5000)) {
LOG_WARNING() << "Failed to start flatpak";
return {};
}
p.waitForFinished(10000);
const QString out = QString::fromUtf8(p.readAllStandardOutput());
QList<AppEntry> apps;
for (const QString &line : out.split('\n', Qt::SkipEmptyParts)) {
const QString s = trim(line);
if (s.isEmpty())
continue;
// Expect lines like: "org.gimp.GIMP\tstable" or space-separated
QStringList parts = s.split(QRegularExpression("\t+| +"), Qt::SkipEmptyParts);
if (parts.size() >= 2) {
const QString appId = parts.at(0);
const QString branch = parts.at(1);
// Skip potential header line if present
if (!appId.contains('.'))
continue;
apps.push_back(AppEntry{appId, branch});
} else if (parts.size() == 1) {
const QString appId = parts.at(0);
if (!appId.contains('.'))
continue;
apps.push_back(AppEntry{appId, QStringLiteral("stable")});
}
}
return apps;
}
bool FlatpakWrapperGenerator::ensureOutputDir() const
{
if (m_outputDir.isEmpty())
return false;
QDir dir(m_outputDir);
if (dir.exists())
return true;
if (m_dryRun)
return true;
return dir.mkpath(".");
}
bool FlatpakWrapperGenerator::generateAllInstalled()
{
const QList<AppEntry> apps = listInstalledApps();
if (apps.isEmpty()) {
LOG_WARNING() << "No Flatpak apps found or flatpak not available.";
return false;
}
if (!ensureOutputDir()) {
LOG_WARNING() << "Failed to ensure output directory" << m_outputDir;
return false;
}
bool allOk = true;
for (const AppEntry &app : apps) {
if (!writeWrapper(app))
allOk = false;
}
return allOk;
}
bool FlatpakWrapperGenerator::generateFor(const QStringList &appIds)
{
if (!ensureOutputDir()) {
LOG_WARNING() << "Failed to ensure output directory" << m_outputDir;
return false;
}
bool allOk = true;
for (const QString &id : appIds) {
const QString branch = getBranchForAppId(id);
if (!writeWrapper(AppEntry{id, branch}))
allOk = false;
}
return allOk;
}
QString FlatpakWrapperGenerator::getBranchForAppId(const QString &appId) const
{
// Query branch for specific app-id
QProcess p;
p.start("flatpak", {"info", appId, "--show-branch"});
if (!p.waitForStarted(5000)) {
LOG_WARNING() << "Failed to start flatpak for branch query";
return QStringLiteral("stable");
}
p.waitForFinished(10000);
QString out = QString::fromUtf8(p.readAllStandardOutput()).trimmed();
if (out.isEmpty())
out = QStringLiteral("stable");
return out;
}
QString FlatpakWrapperGenerator::buildWrapperScript(const AppEntry &app) const
{
QString script;
script += "#!/usr/bin/env sh\n";
script += "# Auto-generated by Shotcut\n";
script += "exec /usr/bin/flatpak run --branch=" + app.branch + " " + app.appId + " \"$@\"\n";
return script;
}
bool FlatpakWrapperGenerator::writeWrapper(const AppEntry &app) const
{
// Use the 3rd dotted segment (index 2) as the name...
QStringList segs = app.appId.split('.');
QString baseName = segs.size() >= 3 ? segs.at(2) : app.appId.section('.', -1);
if (app.appId == QStringLiteral("fr.handbrake.ghb")) {
baseName = QStringLiteral("Handbrake");
}
// ...followed by the branch if not stable
if (!app.branch.isEmpty() && app.branch != QLatin1String("stable")) {
baseName += '-' + app.branch;
}
baseName = m_prefix + baseName;
const QString path = QDir(m_outputDir).filePath(baseName);
const QString script = buildWrapperScript(app);
if (QFileInfo::exists(path) && !m_force) {
LOG_INFO() << "Skip (exists):" << path;
return true;
}
LOG_INFO() << (m_dryRun ? "Would write:" : "Writing:") << path;
if (m_dryRun)
return true;
QFile f(path);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
LOG_WARNING() << "Failed to open for write:" << path;
return false;
}
QTextStream ts(&f);
ts << script;
f.close();
// Make it executable
QFile::Permissions perms = f.permissions();
perms |= QFile::ExeOwner | QFile::ExeGroup | QFile::ExeOther;
f.setPermissions(perms);
return true;
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2025 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QList>
#include <QString>
#include <QStringList>
struct AppEntry
{
QString appId;
QString branch; // e.g., "stable"
};
class FlatpakWrapperGenerator
{
public:
void setOutputDir(const QString &dir) { m_outputDir = dir; }
void setForce(bool v) { m_force = v; }
void setDryRun(bool v) { m_dryRun = v; }
void setPrefix(const QString &p) { m_prefix = p; }
bool generateAllInstalled();
bool generateFor(const QStringList &appIds);
private:
bool ensureOutputDir() const;
QList<AppEntry> listInstalledApps() const;
QString getBranchForAppId(const QString &appId) const;
bool writeWrapper(const AppEntry &app) const;
QString buildWrapperScript(const AppEntry &app) const;
QString m_outputDir;
bool m_force = false;
bool m_dryRun = false;
QString m_prefix;
};

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2012-2025 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 <http://www.gnu.org/licenses/>.
*/
#include "abstractproducerwidget.h"
#include <QWidget>
AbstractProducerWidget::AbstractProducerWidget() {}
AbstractProducerWidget::~AbstractProducerWidget() {}
void AbstractProducerWidget::setProducer(Mlt::Producer *producer)
{
if (producer) {
loadPreset(*producer);
m_producer.reset(new Mlt::Producer(producer));
} else {
m_producer.reset();
}
}
bool AbstractProducerWidget::isDevice(const QWidget *widget)
{
auto name = widget->objectName();
return "AlsaWidget" == name || "alsaWidget" == name || "AvfoundationProducerWidget" == name
|| "avfoundationWidget" == name || "DecklinkProducerWidget" == name
|| "decklinkWidget" == name || "DirectShowVideoWidget" == name
|| "dshowVideoWidget" == name || "PulseAudioWidget" == name || "pulseWidget" == name
|| "Video4LinuxWidget" == name || "v4lWidget" == name;
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2012-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 <http://www.gnu.org/licenses/>.
*/
#ifndef ABSTRACTPRODUCERWIDGET_H
#define ABSTRACTPRODUCERWIDGET_H
#include <MltProducer.h>
#include <QScopedPointer>
class QWidget;
class AbstractProducerWidget
{
public:
AbstractProducerWidget();
virtual ~AbstractProducerWidget();
virtual Mlt::Producer *newProducer(Mlt::Profile &) = 0;
virtual void setProducer(Mlt::Producer *);
virtual Mlt::Properties getPreset() const
{
Mlt::Properties p;
return p;
}
virtual void loadPreset(Mlt::Properties &) {}
Mlt::Producer *producer() const { return m_producer.data(); }
static bool isDevice(const QWidget *widget);
protected:
QScopedPointer<Mlt::Producer> m_producer;
};
#endif // ABSTRACTPRODUCERWIDGET_H

169
src/actions.cpp Normal file
View File

@@ -0,0 +1,169 @@
/*
* Copyright (c) 2013-2023 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 <http://www.gnu.org/licenses/>.
*/
#include "actions.h"
#include "Logger.h"
#include "settings.h"
#include <QAction>
#include <QMenu>
const char *ShotcutActions::hardKeyProperty = "_hardkey";
const char *ShotcutActions::displayProperty = "_display";
const char *ShotcutActions::defaultKey1Property = "_defaultKey1";
const char *ShotcutActions::defaultKey2Property = "_defaultKey2";
const char *ShotcutActions::defaultToolTipProperty = "_defaultToolTip";
static QScopedPointer<ShotcutActions> instance;
ShotcutActions &ShotcutActions::singleton()
{
if (!instance) {
instance.reset(new ShotcutActions());
}
return *instance;
}
void ShotcutActions::add(const QString &key, QAction *action, QString group)
{
auto iterator = m_actions.find(key);
if (iterator != m_actions.end() && iterator.value() != action) {
LOG_ERROR() << "Action already exists with this key" << key;
return;
}
action->setObjectName(key);
if (group.isEmpty()) {
group = tr("Other");
}
action->setProperty(displayProperty, group + " > " + action->iconText());
QList<QKeySequence> sequences = action->shortcuts();
if (sequences.size() > 0)
action->setProperty(defaultKey1Property, sequences[0].toString());
if (sequences.size() > 1)
action->setProperty(defaultKey2Property, sequences[1].toString());
action->setProperty(defaultToolTipProperty, action->toolTip());
m_actions[key] = action;
}
void ShotcutActions::loadFromMenu(QMenu *menu, QString group)
{
if (!menu->title().isEmpty()) {
if (!group.isEmpty())
group = group + " > " + menu->menuAction()->iconText();
else
group = menu->menuAction()->iconText();
}
for (QAction *action : menu->actions()) {
if (action->isSeparator() || action->objectName() == "dummyAction")
continue;
QMenu *submenu = action->menu();
if (submenu) {
loadFromMenu(submenu, group);
} else {
if (action->objectName().isEmpty()) {
// Each action must have a unique object name
QString newObjectName = group + action->iconText();
newObjectName = newObjectName.replace(" ", "");
newObjectName = newObjectName.replace(">", "");
action->setObjectName(newObjectName);
}
add(action->objectName(), action, group);
}
}
}
QAction *ShotcutActions::operator[](const QString &key)
{
auto iterator = m_actions.find(key);
if (iterator != m_actions.end()) {
return iterator.value();
}
return nullptr;
}
QList<QString> ShotcutActions::keys()
{
return m_actions.keys();
}
void ShotcutActions::overrideShortcuts(const QString &key, QList<QKeySequence> shortcuts)
{
QAction *action = m_actions[key];
if (!action) {
LOG_ERROR() << "Invalid action" << key;
return;
}
QList<QKeySequence> defaultShortcuts;
QVariant seq = action->property(defaultKey1Property);
if (seq.isValid())
defaultShortcuts << QKeySequence::fromString(seq.toString());
seq = action->property(defaultKey2Property);
if (seq.isValid())
defaultShortcuts << QKeySequence::fromString(seq.toString());
// Make the lists the same size for easy comparison
while (shortcuts.size() < 2)
shortcuts << QKeySequence();
while (defaultShortcuts.size() < 2)
defaultShortcuts << QKeySequence();
if (shortcuts == defaultShortcuts) {
// Shortcuts are set to default - delete all settings
Settings.clearShortcuts(action->objectName());
} else {
Settings.setShortcuts(action->objectName(), shortcuts);
}
action->setShortcuts(shortcuts);
addShortcutToToolTip(action);
}
void ShotcutActions::initializeShortcuts()
{
// Call this function exactly once after all the actions have been
// added to the ShotcutActions object.
for (auto action : m_actions) {
QList<QKeySequence> shortcutSettings = Settings.shortcuts(action->objectName());
if (!shortcutSettings.isEmpty())
action->setShortcuts(shortcutSettings);
addShortcutToToolTip(action);
}
}
void ShotcutActions::addShortcutToToolTip(QAction *action)
{
QString tooltip = action->property(defaultToolTipProperty).toString();
QString shortcut = action->shortcut().toString(QKeySequence::NativeText);
if (shortcut.isEmpty())
shortcut = action->property(hardKeyProperty).toString();
if (!shortcut.isEmpty()) {
if (!tooltip.isEmpty())
tooltip += " ";
tooltip = tooltip + "(" + shortcut + ")";
}
action->setToolTip(tooltip);
}

57
src/actions.h Normal file
View File

@@ -0,0 +1,57 @@
/*
* Copyright (c) 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 <http://www.gnu.org/licenses/>.
*/
#ifndef ACTIONS_H
#define ACTIONS_H
#include <QHash>
#include <QObject>
class QAction;
class QMenu;
class ShotcutActions : public QObject
{
Q_OBJECT
public:
static const char *hardKeyProperty;
static const char *displayProperty;
static const char *defaultKey1Property;
static const char *defaultKey2Property;
static const char *defaultToolTipProperty;
static ShotcutActions &singleton();
explicit ShotcutActions()
: QObject()
{}
void add(const QString &name, QAction *action, QString group = "");
void loadFromMenu(QMenu *menu, const QString group = "");
QAction *operator[](const QString &key);
QList<QString> keys();
void overrideShortcuts(const QString &key, QList<QKeySequence> shortcuts);
void initializeShortcuts();
private:
void addShortcutToToolTip(QAction *action);
QHash<QString, QAction *> m_actions;
};
#define Actions ShotcutActions::singleton()
#endif // ACTIONS_H

95
src/autosavefile.cpp Normal file
View File

@@ -0,0 +1,95 @@
/*
* Copyright (c) 2011-2016 Meltytech, LLC
* Author: Dan Dennedy <dan@dennedy.org>
* Loosely based on ideas from KAutoSaveFile by Jacob R Rideout <kde@jacobrideout.net>
* and Kdenlive by Jean-Baptiste Mardelle.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "autosavefile.h"
#include "settings.h"
#include <QtCore/QCryptographicHash>
#include <QtCore/QDir>
static const QLatin1String subdir("/autosave");
static const QLatin1String extension(".mlt");
static QString hashName(const QString &name)
{
return QString::fromLatin1(
QCryptographicHash::hash(name.toUtf8(), QCryptographicHash::Md5).toHex());
}
AutoSaveFile::AutoSaveFile(const QString &filename, QObject *parent)
: QFile(parent)
, m_managedFileNameChanged(false)
{
changeManagedFile(filename);
}
AutoSaveFile::~AutoSaveFile()
{
if (!fileName().isEmpty())
remove();
}
void AutoSaveFile::changeManagedFile(const QString &filename)
{
if (!fileName().isEmpty())
remove();
m_managedFile = filename;
m_managedFileNameChanged = true;
}
bool AutoSaveFile::open(OpenMode openmode)
{
QString tempFile;
if (m_managedFileNameChanged) {
QString staleFilesDir = path();
if (!QDir().mkpath(staleFilesDir)) {
return false;
}
tempFile = staleFilesDir + QChar::fromLatin1('/') + hashName(m_managedFile) + extension;
} else {
tempFile = fileName();
}
m_managedFileNameChanged = false;
setFileName(tempFile);
return QFile::open(openmode);
}
AutoSaveFile *AutoSaveFile::getFile(const QString &filename)
{
AutoSaveFile *result = 0;
QDir appDir(path());
QFileInfo info(appDir.absolutePath(), hashName(filename) + extension);
if (info.exists()) {
result = new AutoSaveFile(filename);
result->setFileName(info.filePath());
result->m_managedFileNameChanged = false;
}
return result;
}
QString AutoSaveFile::path()
{
return Settings.appDataLocation() + subdir;
}

47
src/autosavefile.h Normal file
View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2011-2015 Meltytech, LLC
* Author: Dan Dennedy <dan@dennedy.org>
* Loosely based on ideas from KAutoSaveFile by Jacob R Rideout <kde@jacobrideout.net>
* and Kdenlive by Jean-Baptiste Mardelle.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef AUTOSAVEFILE_H
#define AUTOSAVEFILE_H
#include <QtCore/QFile>
#include <QtCore/QString>
class AutoSaveFile : public QFile
{
Q_OBJECT
public:
explicit AutoSaveFile(const QString &filename, QObject *parent = 0);
~AutoSaveFile();
QString managedFileName() const { return m_managedFile; }
void changeManagedFile(const QString &filename);
virtual bool open(OpenMode openmode);
static AutoSaveFile *getFile(const QString &filename);
static QString path();
private:
Q_DISABLE_COPY(AutoSaveFile)
QString m_managedFile;
bool m_managedFileNameChanged;
};
#endif // AUTOSAVEFILE_H

View File

@@ -0,0 +1,415 @@
/*
* Copyright (c) 2021-2025 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 <http://www.gnu.org/licenses/>.
*/
#include "filtercommands.h"
#include "Logger.h"
#include "controllers/filtercontroller.h"
#include "mainwindow.h"
#include "mltcontroller.h"
#include "qmltypes/qmlapplication.h"
class FindProducerParser : public Mlt::Parser
{
private:
QUuid m_uuid;
Mlt::Producer m_producer;
public:
FindProducerParser(QUuid uuid)
: Mlt::Parser()
, m_uuid(uuid)
{}
Mlt::Producer producer() { return m_producer; }
int on_start_filter(Mlt::Filter *) { return 0; }
int on_start_producer(Mlt::Producer *producer)
{
if (MLT.uuid(*producer) == m_uuid) {
m_producer = producer;
return 1;
}
return 0;
}
int on_end_producer(Mlt::Producer *) { return 0; }
int on_start_playlist(Mlt::Playlist *playlist) { return on_start_producer(playlist); }
int on_end_playlist(Mlt::Playlist *) { return 0; }
int on_start_tractor(Mlt::Tractor *tractor) { return on_start_producer(tractor); }
int on_end_tractor(Mlt::Tractor *) { return 0; }
int on_start_multitrack(Mlt::Multitrack *) { return 0; }
int on_end_multitrack(Mlt::Multitrack *) { return 0; }
int on_start_track() { return 0; }
int on_end_track() { return 0; }
int on_end_filter(Mlt::Filter *) { return 0; }
int on_start_transition(Mlt::Transition *) { return 0; }
int on_end_transition(Mlt::Transition *) { return 0; }
int on_start_chain(Mlt::Chain *chain) { return on_start_producer(chain); }
int on_end_chain(Mlt::Chain *) { return 0; }
int on_start_link(Mlt::Link *) { return 0; }
int on_end_link(Mlt::Link *) { return 0; }
};
static Mlt::Producer findProducer(const QUuid &uuid)
{
FindProducerParser graphParser(uuid);
if (MAIN.isMultitrackValid()) {
graphParser.start(*MAIN.multitrack());
if (graphParser.producer().is_valid()) {
return graphParser.producer();
}
}
if (MAIN.playlist() && MAIN.playlist()->count() > 0) {
graphParser.start(*MAIN.playlist());
if (graphParser.producer().is_valid()) {
return graphParser.producer();
}
}
Mlt::Producer producer(MLT.isClip() ? MLT.producer() : MLT.savedProducer());
if (producer.is_valid()) {
graphParser.start(producer);
if (graphParser.producer().is_valid()) {
return graphParser.producer();
}
}
return Mlt::Producer();
}
namespace Filter {
AddCommand::AddCommand(AttachedFiltersModel &model,
const QString &name,
Mlt::Service &service,
int row,
AddCommand::AddType type,
QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_producer(*model.producer())
, m_producerUuid(MLT.ensureHasUuid(m_producer))
, m_type(type)
{
if (m_type == AddCommand::AddSingle) {
setText(QObject::tr("Add %1 filter").arg(name));
} else {
setText(QObject::tr("Add %1 filter set").arg(name));
}
m_rows.push_back(row);
m_services.push_back(service);
}
void AddCommand::redo()
{
LOG_DEBUG() << text() << m_rows[0];
Mlt::Producer producer = m_producer;
if (!producer.is_valid()) {
producer = findProducer(m_producerUuid);
}
Q_ASSERT(producer.is_valid());
int adjustFrom = producer.filter_count();
for (int i = 0; i < m_rows.size(); i++) {
m_model.doAddService(producer, m_services[i], m_rows[i]);
}
if (AddSetLast == m_type)
MLT.adjustFilters(producer, adjustFrom);
// Only hold the producer reference for the first redo and lookup by UUID thereafter.
m_producer = Mlt::Producer();
}
void AddCommand::undo()
{
LOG_DEBUG() << text() << m_rows[0];
Mlt::Producer producer(findProducer(m_producerUuid));
Q_ASSERT(producer.is_valid());
// Remove the services in reverse order
for (int i = m_rows.size() - 1; i >= 0; i--) {
m_model.doRemoveService(producer, m_rows[i]);
}
}
bool AddCommand::mergeWith(const QUndoCommand *other)
{
AddCommand *that = const_cast<AddCommand *>(static_cast<const AddCommand *>(other));
if (!that || that->id() != id()) {
LOG_ERROR() << "Invalid merge";
return false;
}
if (m_type != AddSet || !(that->m_type == AddSet || that->m_type == AddSetLast)) {
// Only merge services from the same filter set
return false;
}
m_type = that->m_type;
m_rows.push_back(that->m_rows.front());
m_services.push_back(that->m_services.front());
return true;
}
RemoveCommand::RemoveCommand(AttachedFiltersModel &model,
const QString &name,
Mlt::Service &service,
int row,
QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_row(row)
, m_producer(*model.producer())
, m_producerUuid(MLT.ensureHasUuid(m_producer))
, m_service(service)
{
setText(QObject::tr("Remove %1 filter").arg(name));
}
void RemoveCommand::redo()
{
LOG_DEBUG() << text() << m_row;
Mlt::Producer producer = m_producer;
if (!producer.is_valid()) {
producer = findProducer(m_producerUuid);
}
Q_ASSERT(producer.is_valid());
m_model.doRemoveService(producer, m_row);
// Only hold the producer reference for the first redo and lookup by UUID thereafter.
m_producer = Mlt::Producer();
}
void RemoveCommand::undo()
{
Q_ASSERT(m_service.is_valid());
LOG_DEBUG() << text() << m_row;
Mlt::Producer producer(findProducer(m_producerUuid));
Q_ASSERT(producer.is_valid());
m_model.doAddService(producer, m_service, m_row);
}
MoveCommand::MoveCommand(
AttachedFiltersModel &model, const QString &name, int fromRow, int toRow, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_fromRow(fromRow)
, m_toRow(toRow)
, m_producer(*model.producer())
, m_producerUuid(MLT.ensureHasUuid(m_producer))
{
setText(QObject::tr("Move %1 filter").arg(name));
}
void MoveCommand::redo()
{
LOG_DEBUG() << text() << "from" << m_fromRow << "to" << m_toRow;
Mlt::Producer producer = m_producer;
if (!producer.is_valid()) {
producer = findProducer(m_producerUuid);
}
Q_ASSERT(producer.is_valid());
if (producer.is_valid()) {
m_model.doMoveService(producer, m_fromRow, m_toRow);
}
if (m_producer.is_valid()) {
// Only hold the producer reference for the first redo and lookup by UUID thereafter.
m_producer = Mlt::Producer();
}
}
void MoveCommand::undo()
{
LOG_DEBUG() << text() << "from" << m_toRow << "to" << m_fromRow;
Mlt::Producer producer(findProducer(m_producerUuid));
Q_ASSERT(producer.is_valid());
if (producer.is_valid()) {
m_model.doMoveService(producer, m_toRow, m_fromRow);
}
}
DisableCommand::DisableCommand(
AttachedFiltersModel &model, const QString &name, int row, bool disabled, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_row(row)
, m_producer(*model.producer())
, m_producerUuid(MLT.ensureHasUuid(m_producer))
, m_disabled(disabled)
{
if (disabled) {
setText(QObject::tr("Disable %1 filter").arg(name));
} else {
setText(QObject::tr("Enable %1 filter").arg(name));
}
}
void DisableCommand::redo()
{
LOG_DEBUG() << text() << m_row;
Mlt::Producer producer = m_producer;
if (!producer.is_valid()) {
producer = findProducer(m_producerUuid);
}
Q_ASSERT(producer.is_valid());
if (producer.is_valid()) {
m_model.doSetDisabled(producer, m_row, m_disabled);
}
if (m_producer.is_valid()) {
// Only hold the producer reference for the first redo and lookup by UUID thereafter.
m_producer = Mlt::Producer();
}
}
void DisableCommand::undo()
{
LOG_DEBUG() << text() << m_row;
Mlt::Producer producer(findProducer(m_producerUuid));
Q_ASSERT(producer.is_valid());
if (producer.is_valid()) {
m_model.doSetDisabled(producer, m_row, !m_disabled);
}
}
bool DisableCommand::mergeWith(const QUndoCommand *other)
{
// TODO: This doesn't always provide expected results.
// If you toggle twice and then undo, you get the opposite of the original state.
// It would make sense to merge three toggles in a row, but not two.
// Do not implement for now.
return false;
/*
DisableCommand *that = const_cast<DisableCommand *>(static_cast<const DisableCommand *>(other));
if (!that || that->id() != id())
return false;
m_disabled = that->m_disabled;
setText(that->text());
return true;
*/
}
PasteCommand::PasteCommand(AttachedFiltersModel &model,
const QString &filterProducerXml,
QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_xml(filterProducerXml)
, m_producerUuid(MLT.ensureHasUuid(*model.producer()))
{
setText(QObject::tr("Paste filters"));
m_beforeXml = MLT.XML(model.producer());
}
void PasteCommand::redo()
{
LOG_DEBUG() << text();
Mlt::Producer producer = findProducer(m_producerUuid);
Q_ASSERT(producer.is_valid());
Mlt::Profile profile(kDefaultMltProfile);
Mlt::Producer filtersProducer(profile, "xml-string", m_xml.toUtf8().constData());
if (filtersProducer.is_valid() && filtersProducer.filter_count() > 0) {
MLT.pasteFilters(&producer, &filtersProducer);
}
emit QmlApplication::singleton().filtersPasted(&producer);
}
void PasteCommand::undo()
{
LOG_DEBUG() << text();
Mlt::Producer producer = findProducer(m_producerUuid);
Q_ASSERT(producer.is_valid());
// Remove all filters
for (int i = 0; i < producer.filter_count(); i++) {
Mlt::Filter *filter = producer.filter(i);
if (filter && filter->is_valid() && !filter->get_int("_loader")
&& !filter->get_int(kShotcutHiddenProperty)) {
producer.detach(*filter);
i--;
}
delete filter;
}
// Restore the "before" filters
Mlt::Profile profile(kDefaultMltProfile);
Mlt::Producer filtersProducer(profile, "xml-string", m_beforeXml.toUtf8().constData());
if (filtersProducer.is_valid() && filtersProducer.filter_count() > 0) {
MLT.pasteFilters(&producer, &filtersProducer);
}
emit QmlApplication::singleton().filtersPasted(&producer);
}
UndoParameterCommand::UndoParameterCommand(const QString &name,
FilterController *controller,
int row,
Mlt::Properties &before,
const QString &desc,
QUndoCommand *parent)
: QUndoCommand(parent)
, m_filterController(controller)
, m_row(row)
, m_producerUuid(MLT.ensureHasUuid(*controller->attachedModel()->producer()))
, m_firstRedo(true)
{
if (desc.isEmpty()) {
setText(QObject::tr("Change %1 filter").arg(name));
} else {
setText(QObject::tr("Change %1 filter: %2").arg(name, desc));
}
m_before.inherit(before);
Mlt::Service *service = controller->attachedModel()->getService(m_row);
m_after.inherit(*service);
}
void UndoParameterCommand::update(const QString &propertyName)
{
Mlt::Service *service = m_filterController->attachedModel()->getService(m_row);
m_after.pass_property(*service, propertyName.toUtf8().constData());
}
void UndoParameterCommand::redo()
{
LOG_DEBUG() << text();
if (m_firstRedo) {
m_firstRedo = false;
} else {
Mlt::Producer producer = findProducer(m_producerUuid);
Q_ASSERT(producer.is_valid());
if (producer.is_valid() && m_filterController) {
Mlt::Service service = m_filterController->attachedModel()->doGetService(producer,
m_row);
service.inherit(m_after);
m_filterController->onUndoOrRedo(service);
}
}
}
void UndoParameterCommand::undo()
{
LOG_DEBUG() << text();
Mlt::Producer producer = findProducer(m_producerUuid);
Q_ASSERT(producer.is_valid());
if (producer.is_valid() && m_filterController) {
Mlt::Service service = m_filterController->attachedModel()->doGetService(producer, m_row);
service.inherit(m_before);
m_filterController->onUndoOrRedo(service);
}
}
bool UndoParameterCommand::mergeWith(const QUndoCommand *other)
{
UndoParameterCommand *that = const_cast<UndoParameterCommand *>(
static_cast<const UndoParameterCommand *>(other));
LOG_DEBUG() << "this filter" << m_row << "that filter" << that->m_row;
if (that->id() != id() || that->m_row != m_row || that->m_producerUuid != m_producerUuid
|| that->text() != text())
return false;
m_after = that->m_after;
return true;
}
} // namespace Filter

View File

@@ -0,0 +1,243 @@
/*
* Copyright (c) 2021-2024 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 <http://www.gnu.org/licenses/>.
*/
#ifndef FILTERCOMMANDS_H
#define FILTERCOMMANDS_H
#include "models/attachedfiltersmodel.h"
#include <MltProducer.h>
#include <MltService.h>
#include <QString>
#include <QUndoCommand>
#include <QUuid>
class QmlMetadata;
class FilterController;
namespace Filter {
enum {
UndoIdAdd = 300,
UndoIdMove,
UndoIdDisable,
UndoIdChangeParameter,
UndoIdChangeAddKeyframe,
UndoIdChangeRemoveKeyframe,
UndoIdChangeKeyframe,
};
class AddCommand : public QUndoCommand
{
public:
typedef enum {
AddSingle,
AddSet,
AddSetLast,
} AddType;
AddCommand(AttachedFiltersModel &model,
const QString &name,
Mlt::Service &service,
int row,
AddCommand::AddType type = AddCommand::AddSingle,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdAdd; }
bool mergeWith(const QUndoCommand *other);
private:
AttachedFiltersModel &m_model;
std::vector<int> m_rows;
std::vector<Mlt::Service> m_services;
Mlt::Producer m_producer;
QUuid m_producerUuid;
AddType m_type;
};
class RemoveCommand : public QUndoCommand
{
public:
RemoveCommand(AttachedFiltersModel &model,
const QString &name,
Mlt::Service &service,
int row,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
AttachedFiltersModel &m_model;
int m_index;
int m_row;
Mlt::Producer m_producer;
QUuid m_producerUuid;
Mlt::Service m_service;
};
class MoveCommand : public QUndoCommand
{
public:
MoveCommand(AttachedFiltersModel &model,
const QString &name,
int fromRow,
int toRow,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdMove; }
private:
AttachedFiltersModel &m_model;
int m_fromRow;
int m_toRow;
Mlt::Producer m_producer;
QUuid m_producerUuid;
};
class DisableCommand : public QUndoCommand
{
public:
DisableCommand(AttachedFiltersModel &model,
const QString &name,
int row,
bool disabled,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdDisable; }
bool mergeWith(const QUndoCommand *other);
private:
AttachedFiltersModel &m_model;
int m_row;
Mlt::Producer m_producer;
QUuid m_producerUuid;
bool m_disabled;
};
class PasteCommand : public QUndoCommand
{
public:
PasteCommand(AttachedFiltersModel &model,
const QString &filterProducerXml,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
AttachedFiltersModel &m_model;
QString m_xml;
QString m_beforeXml;
QUuid m_producerUuid;
};
class UndoParameterCommand : public QUndoCommand
{
public:
UndoParameterCommand(const QString &name,
FilterController *controller,
int row,
Mlt::Properties &before,
const QString &desc = QString(),
QUndoCommand *parent = 0);
void update(const QString &propertyName);
void redo();
void undo();
protected:
int id() const { return UndoIdChangeParameter; }
bool mergeWith(const QUndoCommand *other);
private:
int m_row;
QUuid m_producerUuid;
Mlt::Properties m_before;
Mlt::Properties m_after;
FilterController *m_filterController;
bool m_firstRedo;
};
class UndoAddKeyframeCommand : public UndoParameterCommand
{
public:
UndoAddKeyframeCommand(const QString &name,
FilterController *controller,
int row,
Mlt::Properties &before)
: UndoParameterCommand(name, controller, row, before, QObject::tr("add keyframe"))
{}
protected:
int id() const { return UndoIdChangeAddKeyframe; }
bool mergeWith(const QUndoCommand *other) { return false; }
};
class UndoRemoveKeyframeCommand : public UndoParameterCommand
{
public:
UndoRemoveKeyframeCommand(const QString &name,
FilterController *controller,
int row,
Mlt::Properties &before)
: UndoParameterCommand(name, controller, row, before, QObject::tr("remove keyframe"))
{}
protected:
int id() const { return UndoIdChangeRemoveKeyframe; }
bool mergeWith(const QUndoCommand *other) { return false; }
};
class UndoModifyKeyframeCommand : public UndoParameterCommand
{
public:
UndoModifyKeyframeCommand(const QString &name,
FilterController *controller,
int row,
Mlt::Properties &before,
int paramIndex,
int keyframeIndex)
: UndoParameterCommand(name, controller, row, before, QObject::tr("modify keyframe"))
, m_paramIndex(paramIndex)
, m_keyframeIndex(keyframeIndex)
{}
protected:
int id() const { return UndoIdChangeRemoveKeyframe; }
bool mergeWith(const QUndoCommand *other)
{
auto *that = dynamic_cast<const UndoModifyKeyframeCommand *>(other);
if (!that || m_paramIndex != that->m_paramIndex || m_keyframeIndex != that->m_keyframeIndex)
return false;
else
return UndoParameterCommand::mergeWith(other);
}
private:
int m_paramIndex;
int m_keyframeIndex;
};
} // namespace Filter
#endif // FILTERCOMMANDS_H

View File

@@ -0,0 +1,128 @@
/*
* Copyright (c) 2021 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 <http://www.gnu.org/licenses/>.
*/
#include "markercommands.h"
#include "Logger.h"
namespace Markers {
DeleteCommand::DeleteCommand(MarkersModel &model, const Marker &delMarker, int index)
: QUndoCommand(0)
, m_model(model)
, m_delMarker(delMarker)
, m_index(index)
{
setText(QObject::tr("Delete marker: %1").arg(m_delMarker.text));
}
void DeleteCommand::redo()
{
m_model.doRemove(m_index);
}
void DeleteCommand::undo()
{
m_model.doInsert(m_index, m_delMarker);
}
AppendCommand::AppendCommand(MarkersModel &model, const Marker &newMarker, int index)
: QUndoCommand(0)
, m_model(model)
, m_newMarker(newMarker)
, m_index(index)
{
setText(QObject::tr("Add marker: %1").arg(m_newMarker.text));
}
void AppendCommand::redo()
{
m_model.doAppend(m_newMarker);
}
void AppendCommand::undo()
{
m_model.doRemove(m_index);
}
UpdateCommand::UpdateCommand(MarkersModel &model,
const Marker &newMarker,
const Marker &oldMarker,
int index)
: QUndoCommand(0)
, m_model(model)
, m_newMarker(newMarker)
, m_oldMarker(oldMarker)
, m_index(index)
{
if (m_newMarker.text == m_oldMarker.text && m_newMarker.color == m_oldMarker.color) {
setText(QObject::tr("Move marker: %1").arg(m_oldMarker.text));
} else {
setText(QObject::tr("Edit marker: %1").arg(m_oldMarker.text));
}
}
void UpdateCommand::redo()
{
m_model.doUpdate(m_index, m_newMarker);
}
void UpdateCommand::undo()
{
m_model.doUpdate(m_index, m_oldMarker);
}
bool UpdateCommand::mergeWith(const QUndoCommand *other)
{
const UpdateCommand *that = static_cast<const UpdateCommand *>(other);
LOG_DEBUG() << "this index" << m_index << "that index" << that->m_index;
if (that->id() != id() || that->m_index != m_index)
return false;
bool merge = false;
if (that->m_newMarker.text == m_oldMarker.text && that->m_newMarker.color == m_oldMarker.color) {
// Only start/end change. Merge with previous move command.
merge = true;
} else if (that->m_newMarker.end == m_oldMarker.end
&& that->m_newMarker.start == m_oldMarker.start) {
// Only text/color change. Merge with previous edit command.
merge = true;
}
if (!merge)
return false;
m_newMarker = that->m_newMarker;
return true;
}
ClearCommand::ClearCommand(MarkersModel &model, QList<Marker> &clearMarkers)
: QUndoCommand(0)
, m_model(model)
, m_clearMarkers(clearMarkers)
{
setText(QObject::tr("Clear markers"));
}
void ClearCommand::redo()
{
m_model.doClear();
}
void ClearCommand::undo()
{
m_model.doReplace(m_clearMarkers);
}
} // namespace Markers

View File

@@ -0,0 +1,89 @@
/*
* Copyright (c) 2021-2023 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 <http://www.gnu.org/licenses/>.
*/
#ifndef MARKERCOMMANDS_H
#define MARKERCOMMANDS_H
#include "models/markersmodel.h"
#include <QUndoCommand>
namespace Markers {
enum {
UndoIdUpdate = 200,
};
class DeleteCommand : public QUndoCommand
{
public:
DeleteCommand(MarkersModel &model, const Marker &delMarker, int index);
void redo();
void undo();
private:
MarkersModel &m_model;
Marker m_delMarker;
int m_index;
};
class AppendCommand : public QUndoCommand
{
public:
AppendCommand(MarkersModel &model, const Marker &newMarker, int index);
void redo();
void undo();
private:
MarkersModel &m_model;
Marker m_newMarker;
int m_index;
};
class UpdateCommand : public QUndoCommand
{
public:
UpdateCommand(MarkersModel &model, const Marker &newMarker, const Marker &oldMarker, int index);
void redo();
void undo();
protected:
int id() const { return UndoIdUpdate; }
bool mergeWith(const QUndoCommand *other);
private:
MarkersModel &m_model;
Marker m_newMarker;
Marker m_oldMarker;
int m_index;
};
class ClearCommand : public QUndoCommand
{
public:
ClearCommand(MarkersModel &model, QList<Marker> &clearMarkers);
void redo();
void undo();
private:
MarkersModel &m_model;
QList<Marker> m_clearMarkers;
};
} // namespace Markers
#endif // MARKERCOMMANDS_H

View File

@@ -0,0 +1,555 @@
/*
* Copyright (c) 2013-2024 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 <http://www.gnu.org/licenses/>.
*/
#include "playlistcommands.h"
#include "Logger.h"
#include "docks/playlistdock.h"
#include "mainwindow.h"
#include "mltcontroller.h"
#include "shotcut_mlt_properties.h"
#include <QTreeWidget>
namespace Playlist {
AppendCommand::AppendCommand(PlaylistModel &model,
const QString &xml,
bool emitModified,
QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_xml(xml)
, m_emitModified(emitModified)
{
setText(QObject::tr("Append playlist item %1").arg(m_model.rowCount() + 1));
}
void AppendCommand::redo()
{
LOG_DEBUG() << "";
Mlt::Producer producer(MLT.profile(), "xml-string", m_xml.toUtf8().constData());
m_model.append(producer, m_emitModified);
if (m_uuid.isNull()) {
m_uuid = MLT.ensureHasUuid(producer);
} else {
MLT.setUuid(producer, m_uuid);
}
}
void AppendCommand::undo()
{
LOG_DEBUG() << "";
m_model.remove(m_model.rowCount() - 1);
}
InsertCommand::InsertCommand(PlaylistModel &model, const QString &xml, int row, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_xml(xml)
, m_row(row)
{
setText(QObject::tr("Insert playist item %1").arg(row + 1));
}
void InsertCommand::redo()
{
LOG_DEBUG() << "row" << m_row;
Mlt::Producer producer(MLT.profile(), "xml-string", m_xml.toUtf8().constData());
m_model.insert(producer, m_row);
if (m_uuid.isNull()) {
m_uuid = MLT.ensureHasUuid(producer);
} else {
MLT.setUuid(producer, m_uuid);
}
}
void InsertCommand::undo()
{
LOG_DEBUG() << "row" << m_row;
m_model.remove(m_row);
}
UpdateCommand::UpdateCommand(PlaylistModel &model, const QString &xml, int row, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_newXml(xml)
, m_row(row)
{
setText(QObject::tr("Update playlist item %1").arg(row + 1));
QScopedPointer<Mlt::ClipInfo> info(m_model.playlist()->clip_info(row));
info->producer->set_in_and_out(info->frame_in, info->frame_out);
m_oldXml = MLT.XML(info->producer);
}
void UpdateCommand::redo()
{
LOG_DEBUG() << "row" << m_row;
Mlt::Producer producer(MLT.profile(), "xml-string", m_newXml.toUtf8().constData());
m_model.update(m_row, producer);
if (m_uuid.isNull()) {
m_uuid = MLT.ensureHasUuid(producer);
} else {
MLT.setUuid(producer, m_uuid);
}
}
void UpdateCommand::undo()
{
LOG_DEBUG() << "row" << m_row;
Mlt::Producer producer(MLT.profile(), "xml-string", m_oldXml.toUtf8().constData());
m_model.update(m_row, producer);
}
bool UpdateCommand::mergeWith(const QUndoCommand *other)
{
const UpdateCommand *that = static_cast<const UpdateCommand *>(other);
LOG_DEBUG() << "this row" << m_row << "that row" << that->m_row;
if (that->id() != id() || that->m_row != m_row)
return false;
m_newXml = that->m_newXml;
return true;
}
RemoveCommand::RemoveCommand(PlaylistModel &model, int row, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_row(row)
{
QScopedPointer<Mlt::ClipInfo> info(m_model.playlist()->clip_info(row));
info->producer->set_in_and_out(info->frame_in, info->frame_out);
m_xml = MLT.XML(info->producer);
setText(QObject::tr("Remove playlist item %1").arg(row + 1));
m_uuid = MLT.ensureHasUuid(*info->producer);
}
void RemoveCommand::redo()
{
LOG_DEBUG() << "row" << m_row;
m_model.remove(m_row);
}
void RemoveCommand::undo()
{
LOG_DEBUG() << "row" << m_row;
Mlt::Producer producer(MLT.profile(), "xml-string", m_xml.toUtf8().constData());
m_model.insert(producer, m_row);
MLT.setUuid(producer, m_uuid);
}
ClearCommand::ClearCommand(PlaylistModel &model, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
{
m_xml = MLT.XML(m_model.playlist());
setText(QObject::tr("Clear playlist"));
for (int i = 0; i < m_model.playlist()->count(); i++) {
Mlt::Producer clip(m_model.playlist()->get_clip(i));
if (clip.is_valid()) {
m_uuids << MLT.ensureHasUuid(clip.parent());
}
}
}
void ClearCommand::redo()
{
LOG_DEBUG() << "";
m_model.clear();
}
void ClearCommand::undo()
{
LOG_DEBUG() << "";
Mlt::Producer *producer = new Mlt::Producer(MLT.profile(),
"xml-string",
m_xml.toUtf8().constData());
if (producer->is_valid()) {
producer->set("resource", "<playlist>");
if (!MLT.setProducer(producer)) {
m_model.load();
for (int i = 0; i < m_model.playlist()->count(); i++) {
Mlt::Producer clip(m_model.playlist()->get_clip(i));
if (clip.is_valid() && i < m_uuids.size()) {
MLT.setUuid(clip.parent(), m_uuids[i]);
}
}
MLT.pause();
MAIN.seekPlaylist(0);
}
} else {
LOG_ERROR() << "failed to restore playlist from XML";
}
}
MoveCommand::MoveCommand(PlaylistModel &model, int from, int to, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_from(from)
, m_to(to)
{
setText(QObject::tr("Move item from %1 to %2").arg(from + 1).arg(to + 1));
}
void MoveCommand::redo()
{
LOG_DEBUG() << "from" << m_from << "to" << m_to;
m_model.move(m_from, m_to);
}
void MoveCommand::undo()
{
LOG_DEBUG() << "from" << m_from << "to" << m_to;
m_model.move(m_to, m_from);
}
SortCommand::SortCommand(PlaylistModel &model, int column, Qt::SortOrder order, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_column(column)
, m_order(order)
{
m_xml = MLT.XML(m_model.playlist());
QString columnName = m_model.headerData(m_column, Qt::Horizontal, Qt::DisplayRole).toString();
setText(QObject::tr("Sort playlist by %1").arg(columnName));
for (int i = 0; i < m_model.playlist()->count(); i++) {
Mlt::Producer clip(m_model.playlist()->get_clip(i));
if (clip.is_valid()) {
m_uuids << MLT.ensureHasUuid(clip.parent());
}
}
}
void SortCommand::redo()
{
LOG_DEBUG() << m_column;
m_model.sort(m_column, m_order);
}
void SortCommand::undo()
{
LOG_DEBUG() << "";
Mlt::Producer *producer = new Mlt::Producer(MLT.profile(),
"xml-string",
m_xml.toUtf8().constData());
if (producer->is_valid()) {
producer->set("resource", "<playlist>");
if (!MLT.setProducer(producer)) {
m_model.load();
for (int i = 0; i < m_model.playlist()->count(); i++) {
Mlt::Producer clip(m_model.playlist()->get_clip(i));
if (clip.is_valid() && i < m_uuids.size()) {
MLT.setUuid(clip.parent(), m_uuids[i]);
}
}
MLT.pause();
MAIN.seekPlaylist(0);
}
} else {
LOG_ERROR() << "failed to restore playlist from XML";
}
}
TrimClipInCommand::TrimClipInCommand(PlaylistModel &model, int row, int in, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_row(row)
, m_oldIn(in)
, m_newIn(in)
, m_out(-1)
{
setText(QObject::tr("Trim playlist item %1 in").arg(row + 1));
QScopedPointer<Mlt::ClipInfo> info(m_model.playlist()->clip_info(row));
if (info) {
m_oldIn = info->frame_in;
m_out = info->frame_out;
}
}
void TrimClipInCommand::redo()
{
LOG_DEBUG() << "row" << m_row << "in" << m_newIn;
m_model.setInOut(m_row, m_newIn, m_out);
}
void TrimClipInCommand::undo()
{
LOG_DEBUG() << "row" << m_row << "in" << m_oldIn;
m_model.setInOut(m_row, m_oldIn, m_out);
}
bool TrimClipInCommand::mergeWith(const QUndoCommand *other)
{
const TrimClipInCommand *that = static_cast<const TrimClipInCommand *>(other);
LOG_DEBUG() << "this row" << m_row << "that row" << that->m_row;
if (that->id() != id() || that->m_row != m_row)
return false;
m_newIn = that->m_newIn;
return true;
}
TrimClipOutCommand::TrimClipOutCommand(PlaylistModel &model, int row, int out, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_row(row)
, m_in(-1)
, m_oldOut(out)
, m_newOut(out)
{
setText(QObject::tr("Trim playlist item %1 out").arg(row + 1));
QScopedPointer<Mlt::ClipInfo> info(m_model.playlist()->clip_info(row));
if (info) {
m_in = info->frame_in;
m_oldOut = info->frame_out;
}
}
void TrimClipOutCommand::redo()
{
LOG_DEBUG() << "row" << m_row << "out" << m_newOut;
m_model.setInOut(m_row, m_in, m_newOut);
}
void TrimClipOutCommand::undo()
{
LOG_DEBUG() << "row" << m_row << "out" << m_oldOut;
m_model.setInOut(m_row, m_in, m_oldOut);
}
bool TrimClipOutCommand::mergeWith(const QUndoCommand *other)
{
const TrimClipOutCommand *that = static_cast<const TrimClipOutCommand *>(other);
LOG_DEBUG() << "this row" << m_row << "that row" << that->m_row;
if (that->id() != id() || that->m_row != m_row)
return false;
m_newOut = that->m_newOut;
return true;
}
ReplaceCommand::ReplaceCommand(PlaylistModel &model,
const QString &xml,
int row,
QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_newXml(xml)
, m_row(row)
{
setText(QObject::tr("Replace playlist item %1").arg(row + 1));
QScopedPointer<Mlt::ClipInfo> info(m_model.playlist()->clip_info(row));
info->producer->set_in_and_out(info->frame_in, info->frame_out);
m_uuid = MLT.ensureHasUuid(*info->producer);
m_oldXml = MLT.XML(info->producer);
}
void ReplaceCommand::redo()
{
LOG_DEBUG() << "row" << m_row;
Mlt::Producer producer(MLT.profile(), "xml-string", m_newXml.toUtf8().constData());
m_model.update(m_row, producer, true);
}
void ReplaceCommand::undo()
{
LOG_DEBUG() << "row" << m_row;
Mlt::Producer producer(MLT.profile(), "xml-string", m_oldXml.toUtf8().constData());
m_model.update(m_row, producer, true);
MLT.setUuid(producer, m_uuid);
}
NewBinCommand::NewBinCommand(PlaylistModel &model,
QTreeWidget *tree,
const QString &bin,
QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_binTree(tree)
, m_bin(bin)
{
setText(QObject::tr("Add new bin: %1").arg(bin));
auto props = m_model.playlist()->get_props(kShotcutBinsProperty);
if (props && props->is_valid()) {
m_oldBins.copy(*props, "");
}
}
void NewBinCommand::redo()
{
auto item = new QTreeWidgetItem(m_binTree, {m_bin});
auto icon = QIcon::fromTheme("folder", QIcon(":/icons/oxygen/32x32/places/folder.png"));
item->setIcon(0, icon);
PlaylistDock::sortBins(m_binTree);
emit m_binTree->itemSelectionChanged();
std::unique_ptr<Mlt::Properties> props(m_model.playlist()->get_props(kShotcutBinsProperty));
if (!props || !props->is_valid()) {
props.reset(new Mlt::Properties);
m_model.playlist()->set(kShotcutBinsProperty, *props);
}
for (int i = PlaylistDock::SmartBinCount; i < m_binTree->topLevelItemCount(); ++i) {
auto name = m_binTree->topLevelItem(i)->text(0);
props->set(QString::number(i).toLatin1().constData(), name.toUtf8().constData());
}
}
void NewBinCommand::undo()
{
m_model.playlist()->set(kShotcutBinsProperty, m_oldBins);
auto items = m_binTree->findItems(m_bin, Qt::MatchExactly);
if (!items.isEmpty())
delete items.first();
RenameBinCommand::rebuildBinList(m_model, m_binTree);
}
MoveToBinCommand::MoveToBinCommand(PlaylistModel &model,
QTreeWidget *tree,
const QString &bin,
const QList<int> &rows,
QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_binTree(tree)
, m_bin(bin)
{
setText(QObject::tr("Move %n item(s) to bin: %1", "", rows.size()).arg(bin));
for (const auto row : rows) {
auto clip = m_model.playlist()->get_clip(row);
if (clip && clip->is_valid() && clip->parent().is_valid()) {
m_oldData.append({row, clip->parent().get(kShotcutBinsProperty)});
}
}
}
void MoveToBinCommand::redo()
{
for (auto &old : m_oldData) {
m_model.setBin(old.row, m_bin);
}
}
void MoveToBinCommand::undo()
{
for (auto &old : m_oldData) {
m_model.setBin(old.row, old.bin);
}
}
RenameBinCommand::RenameBinCommand(PlaylistModel &model,
QTreeWidget *tree,
const QString &bin,
const QString &newName,
QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_binTree(tree)
, m_bin(bin)
, m_newName(newName)
{
if (newName.isEmpty()) {
setText(QObject::tr("Remove bin: %1").arg(bin));
} else {
setText(QObject::tr("Rename bin: %1").arg(newName));
}
}
void RenameBinCommand::redo()
{
auto items = m_binTree->findItems(m_bin, Qt::MatchExactly);
if (!items.isEmpty()) {
m_binTree->blockSignals(true);
items.first()->setSelected(false);
m_binTree->blockSignals(false);
if (m_newName.isEmpty()) {
// Remove
delete items.first();
// Remove bin property from playlist items
for (int i = 0; i < m_model.playlist()->count(); ++i) {
auto clip = m_model.playlist()->get_clip(i);
if (clip && clip->is_valid() && m_bin == clip->parent().get(kShotcutBinsProperty)) {
clip->parent().Mlt::Properties::clear(kShotcutBinsProperty);
m_removedRows << i;
}
}
m_model.renameBin(m_bin);
rebuildBinList(m_model, m_binTree);
// Select ALL bin
m_binTree->clearSelection();
} else {
// Rename
items.first()->setText(0, m_newName);
items.first()->setSelected(true);
m_model.renameBin(m_bin, m_newName);
rebuildBinList(m_model, m_binTree);
// Reselect bin
PlaylistDock::sortBins(m_binTree);
emit m_binTree->itemSelectionChanged();
}
}
}
void RenameBinCommand::undo()
{
auto items = m_binTree->findItems(m_newName, Qt::MatchExactly);
if (m_newName.isEmpty()) {
// Undo remove
auto item = new QTreeWidgetItem(m_binTree, {m_bin});
auto icon = QIcon::fromTheme("folder", QIcon(":/icons/oxygen/32x32/places/folder.png"));
item->setIcon(0, icon);
PlaylistDock::sortBins(m_binTree);
// Restore bin property on playlist items
for (auto row : m_removedRows) {
m_model.playlist()->get_clip(row)->parent().set(kShotcutBinsProperty,
m_bin.toUtf8().constData());
}
m_model.renameBin(m_bin);
rebuildBinList(m_model, m_binTree);
} else if (!items.isEmpty()) {
// Undo rename
m_binTree->blockSignals(true);
m_binTree->clearSelection();
m_binTree->blockSignals(false);
items.first()->setText(0, m_bin);
items.first()->setSelected(true);
m_model.renameBin(m_newName, m_bin);
rebuildBinList(m_model, m_binTree);
// Reselect bin
PlaylistDock::sortBins(m_binTree);
}
emit m_binTree->itemSelectionChanged();
}
void RenameBinCommand::rebuildBinList(PlaylistModel &model, QTreeWidget *binTree)
{
// Rebuild list of bins
std::unique_ptr<Mlt::Properties> props(model.playlist()->get_props(kShotcutBinsProperty));
for (int i = 0; i < props->count(); ++i) {
const auto name = QString::fromLatin1(props->get_name(i));
if (!name.startsWith('_') && !name.startsWith('.'))
props->clear(name.toLatin1().constData());
}
for (int i = PlaylistDock::SmartBinCount; i < binTree->topLevelItemCount(); ++i) {
auto name = binTree->topLevelItem(i)->text(0);
props->set(QString::number(i).toLatin1().constData(), name.toUtf8().constData());
}
}
} // namespace Playlist

View File

@@ -0,0 +1,254 @@
/*
* Copyright (c) 2013-2024 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 <http://www.gnu.org/licenses/>.
*/
#ifndef PLAYLISTCOMMANDS_H
#define PLAYLISTCOMMANDS_H
#include "models/playlistmodel.h"
#include <QString>
#include <QUndoCommand>
#include <QUuid>
class QTreeWidget;
namespace Playlist {
enum { UndoIdTrimClipIn = 0, UndoIdTrimClipOut, UndoIdUpdate };
class AppendCommand : public QUndoCommand
{
public:
AppendCommand(PlaylistModel &model,
const QString &xml,
bool emitModified = true,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
PlaylistModel &m_model;
QString m_xml;
bool m_emitModified;
QUuid m_uuid;
};
class InsertCommand : public QUndoCommand
{
public:
InsertCommand(PlaylistModel &model, const QString &xml, int row, QUndoCommand *parent = 0);
void redo();
void undo();
private:
PlaylistModel &m_model;
QString m_xml;
int m_row;
QUuid m_uuid;
};
class UpdateCommand : public QUndoCommand
{
public:
UpdateCommand(PlaylistModel &model, const QString &xml, int row, QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdUpdate; }
bool mergeWith(const QUndoCommand *other);
private:
PlaylistModel &m_model;
QString m_newXml;
QString m_oldXml;
int m_row;
QUuid m_uuid;
};
class RemoveCommand : public QUndoCommand
{
public:
RemoveCommand(PlaylistModel &model, int row, QUndoCommand *parent = 0);
void redo();
void undo();
private:
PlaylistModel &m_model;
QString m_xml;
int m_row;
QUuid m_uuid;
};
class MoveCommand : public QUndoCommand
{
public:
MoveCommand(PlaylistModel &model, int from, int to, QUndoCommand *parent = 0);
void redo();
void undo();
private:
PlaylistModel &m_model;
int m_from;
int m_to;
};
class ClearCommand : public QUndoCommand
{
public:
ClearCommand(PlaylistModel &model, QUndoCommand *parent = 0);
void redo();
void undo();
private:
PlaylistModel &m_model;
QString m_xml;
QVector<QUuid> m_uuids;
};
class SortCommand : public QUndoCommand
{
public:
SortCommand(PlaylistModel &model, int column, Qt::SortOrder order, QUndoCommand *parent = 0);
void redo();
void undo();
private:
PlaylistModel &m_model;
int m_column;
Qt::SortOrder m_order;
QString m_xml;
QVector<QUuid> m_uuids;
};
class TrimClipInCommand : public QUndoCommand
{
public:
TrimClipInCommand(PlaylistModel &model, int row, int in, QUndoCommand *parent = nullptr);
void redo();
void undo();
protected:
int id() const { return UndoIdTrimClipIn; }
bool mergeWith(const QUndoCommand *other);
private:
PlaylistModel &m_model;
int m_row;
int m_oldIn;
int m_newIn;
int m_out;
};
class TrimClipOutCommand : public QUndoCommand
{
public:
TrimClipOutCommand(PlaylistModel &model, int row, int out, QUndoCommand *parent = nullptr);
void redo();
void undo();
protected:
int id() const { return UndoIdTrimClipOut; }
bool mergeWith(const QUndoCommand *other);
private:
PlaylistModel &m_model;
int m_row;
int m_in;
int m_oldOut;
int m_newOut;
};
class ReplaceCommand : public QUndoCommand
{
public:
ReplaceCommand(PlaylistModel &model, const QString &xml, int row, QUndoCommand *parent = 0);
void redo();
void undo();
private:
PlaylistModel &m_model;
QString m_newXml;
QString m_oldXml;
int m_row;
QUuid m_uuid;
};
class NewBinCommand : public QUndoCommand
{
public:
NewBinCommand(PlaylistModel &model,
QTreeWidget *tree,
const QString &bin,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
PlaylistModel &m_model;
QTreeWidget *m_binTree;
QString m_bin;
Mlt::Properties m_oldBins;
};
class MoveToBinCommand : public QUndoCommand
{
public:
MoveToBinCommand(PlaylistModel &model,
QTreeWidget *tree,
const QString &bin,
const QList<int> &rows,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
PlaylistModel &m_model;
QTreeWidget *m_binTree;
QString m_bin;
typedef struct
{
int row;
QString bin;
} oldData;
QList<oldData> m_oldData;
};
class RenameBinCommand : public QUndoCommand
{
public:
RenameBinCommand(PlaylistModel &model,
QTreeWidget *tree,
const QString &bin,
const QString &newName = QString(),
QUndoCommand *parent = 0);
void redo();
void undo();
static void rebuildBinList(PlaylistModel &model, QTreeWidget *binTree);
private:
PlaylistModel &m_model;
QTreeWidget *m_binTree;
QString m_bin;
QString m_newName;
QList<int> m_removedRows;
};
} // namespace Playlist
#endif // PLAYLISTCOMMANDS_H

View File

@@ -0,0 +1,343 @@
/*
* Copyright (c) 2024 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 <http://www.gnu.org/licenses/>.
*/
#include "subtitlecommands.h"
#include "Logger.h"
#include "mainwindow.h"
#include <QFileInfo>
namespace Subtitles {
InsertTrackCommand::InsertTrackCommand(SubtitlesModel &model,
const SubtitlesModel::SubtitleTrack &track,
int index)
: QUndoCommand(0)
, m_model(model)
, m_track(track)
, m_index(index)
{
setText(QObject::tr("Add subtitle track: %1").arg(m_track.name));
}
void InsertTrackCommand::redo()
{
LOG_DEBUG() << m_track.name;
m_model.doInsertTrack(m_track, m_index);
}
void InsertTrackCommand::undo()
{
m_model.doRemoveTrack(m_index);
}
RemoveTrackCommand::RemoveTrackCommand(SubtitlesModel &model, int trackIndex)
: QUndoCommand(0)
, m_model(model)
, m_trackIndex(trackIndex)
, m_saveTrack(m_model.getTrack(trackIndex))
{
setText(QObject::tr("Remove subtitle track: %1").arg(m_saveTrack.name));
int count = m_model.itemCount(m_trackIndex);
m_saveSubtitles.reserve(count);
for (int i = 0; i < count; i++) {
m_saveSubtitles.push_back(m_model.getItem(m_trackIndex, i));
}
}
void RemoveTrackCommand::redo()
{
m_model.doRemoveTrack(m_trackIndex);
}
void RemoveTrackCommand::undo()
{
m_model.doInsertTrack(m_saveTrack, m_trackIndex);
m_model.doInsertSubtitleItems(m_trackIndex, m_saveSubtitles);
}
EditTrackCommand::EditTrackCommand(SubtitlesModel &model,
const SubtitlesModel::SubtitleTrack &track,
int index)
: QUndoCommand(0)
, m_model(model)
, m_newTrack(track)
, m_index(index)
{
m_oldTrack = m_model.getTrack(index);
setText(QObject::tr("Edit subtitle track: %1").arg(m_newTrack.name));
}
void EditTrackCommand::redo()
{
LOG_DEBUG() << m_oldTrack.name;
m_model.doEditTrack(m_newTrack, m_index);
}
void EditTrackCommand::undo()
{
m_model.doEditTrack(m_oldTrack, m_index);
}
OverwriteSubtitlesCommand::OverwriteSubtitlesCommand(SubtitlesModel &model,
int trackIndex,
const QList<Subtitles::SubtitleItem> &items)
: QUndoCommand(0)
, m_model(model)
, m_trackIndex(trackIndex)
, m_newSubtitles(items)
{
if (m_newSubtitles.size() == 1) {
setText(QObject::tr("Add subtitle"));
} else {
setText(QObject::tr("Add %n subtitles", nullptr, m_newSubtitles.size()));
}
if (m_newSubtitles.size() <= 0) {
return;
}
// Save anything that will be removed
int64_t startPosition = m_newSubtitles[0].start;
int64_t endPosition = m_newSubtitles[m_newSubtitles.size() - 1].end;
int count = m_model.itemCount(m_trackIndex);
for (int i = 0; i < count; i++) {
auto item = m_model.getItem(m_trackIndex, i);
if ((item.start >= startPosition && item.start < endPosition)
|| (item.end > startPosition && item.end < endPosition)) {
m_saveSubtitles.push_back(item);
}
}
}
void OverwriteSubtitlesCommand::redo()
{
LOG_DEBUG() << m_newSubtitles.size();
if (m_newSubtitles.size() > 0) {
if (m_saveSubtitles.size() > 0) {
m_model.doRemoveSubtitleItems(m_trackIndex, m_saveSubtitles);
}
m_model.doInsertSubtitleItems(m_trackIndex, m_newSubtitles);
}
}
void OverwriteSubtitlesCommand::undo()
{
LOG_DEBUG() << m_newSubtitles.size();
if (m_newSubtitles.size() > 0) {
m_model.doRemoveSubtitleItems(m_trackIndex, m_newSubtitles);
if (m_saveSubtitles.size() > 0) {
m_model.doInsertSubtitleItems(m_trackIndex, m_saveSubtitles);
}
}
}
RemoveSubtitlesCommand::RemoveSubtitlesCommand(SubtitlesModel &model,
int trackIndex,
const QList<Subtitles::SubtitleItem> &items)
: QUndoCommand(0)
, m_model(model)
, m_trackIndex(trackIndex)
, m_items(items)
{
if (m_items.size() == 1) {
setText(QObject::tr("Remove subtitle"));
} else {
setText(QObject::tr("Remove %n subtitles", nullptr, m_items.size()));
}
}
void RemoveSubtitlesCommand::redo()
{
LOG_DEBUG() << m_items.size();
m_model.doRemoveSubtitleItems(m_trackIndex, m_items);
}
void RemoveSubtitlesCommand::undo()
{
LOG_DEBUG() << m_items.size();
if (m_items.size() > 0) {
m_model.doInsertSubtitleItems(m_trackIndex, m_items);
}
}
SetTextCommand::SetTextCommand(SubtitlesModel &model,
int trackIndex,
int itemIndex,
const QString &text)
: QUndoCommand(0)
, m_model(model)
, m_trackIndex(trackIndex)
, m_itemIndex(itemIndex)
, m_newText(text)
{
setText(QObject::tr("Edit subtitle text"));
m_oldText = QString::fromStdString(m_model.getItem(trackIndex, itemIndex).text);
}
void SetTextCommand::redo()
{
m_model.doSetText(m_trackIndex, m_itemIndex, m_newText);
}
void SetTextCommand::undo()
{
m_model.doSetText(m_trackIndex, m_itemIndex, m_oldText);
}
bool SetTextCommand::mergeWith(const QUndoCommand *other)
{
const SetTextCommand *that = static_cast<const SetTextCommand *>(other);
if (m_trackIndex != that->m_trackIndex || m_itemIndex != that->m_itemIndex) {
return false;
}
LOG_DEBUG() << "track" << m_trackIndex << "item" << m_itemIndex;
m_newText = that->m_newText;
return true;
}
SetStartCommand::SetStartCommand(SubtitlesModel &model,
int trackIndex,
int itemIndex,
int64_t msTime)
: QUndoCommand(0)
, m_model(model)
, m_trackIndex(trackIndex)
, m_itemIndex(itemIndex)
, m_newStart(msTime)
{
setText(QObject::tr("Change subtitle start"));
m_oldStart = m_model.getItem(trackIndex, itemIndex).start;
}
void SetStartCommand::redo()
{
int64_t endTime = m_model.getItem(m_trackIndex, m_itemIndex).end;
m_model.doSetTime(m_trackIndex, m_itemIndex, m_newStart, endTime);
}
void SetStartCommand::undo()
{
int64_t endTime = m_model.getItem(m_trackIndex, m_itemIndex).end;
m_model.doSetTime(m_trackIndex, m_itemIndex, m_oldStart, endTime);
}
bool SetStartCommand::mergeWith(const QUndoCommand *other)
{
const SetStartCommand *that = static_cast<const SetStartCommand *>(other);
if (m_trackIndex != that->m_trackIndex || m_itemIndex != that->m_itemIndex) {
return false;
}
LOG_DEBUG() << "track" << m_trackIndex << "item" << m_itemIndex;
m_newStart = that->m_newStart;
return true;
}
SetEndCommand::SetEndCommand(SubtitlesModel &model, int trackIndex, int itemIndex, int64_t msTime)
: QUndoCommand(0)
, m_model(model)
, m_trackIndex(trackIndex)
, m_itemIndex(itemIndex)
, m_newEnd(msTime)
{
setText(QObject::tr("Change subtitle end"));
m_oldEnd = m_model.getItem(trackIndex, itemIndex).end;
}
void SetEndCommand::redo()
{
int64_t startTime = m_model.getItem(m_trackIndex, m_itemIndex).start;
m_model.doSetTime(m_trackIndex, m_itemIndex, startTime, m_newEnd);
}
void SetEndCommand::undo()
{
int64_t startTime = m_model.getItem(m_trackIndex, m_itemIndex).start;
m_model.doSetTime(m_trackIndex, m_itemIndex, startTime, m_oldEnd);
}
bool SetEndCommand::mergeWith(const QUndoCommand *other)
{
const SetEndCommand *that = static_cast<const SetEndCommand *>(other);
if (m_trackIndex != that->m_trackIndex || m_itemIndex != that->m_itemIndex) {
return false;
}
LOG_DEBUG() << "track" << m_trackIndex << "item" << m_itemIndex;
m_newEnd = that->m_newEnd;
return true;
}
MoveSubtitlesCommand::MoveSubtitlesCommand(SubtitlesModel &model,
int trackIndex,
const QList<Subtitles::SubtitleItem> &items,
int64_t msTime)
: QUndoCommand(0)
, m_model(model)
, m_trackIndex(trackIndex)
, m_oldSubtitles(items)
{
if (m_oldSubtitles.size() <= 0) {
return;
}
if (m_oldSubtitles.size() == 1) {
setText(QObject::tr("Move subtitle"));
} else {
setText(QObject::tr("Move %n subtitles", nullptr, m_oldSubtitles.size()));
}
// Create a list of subtitles with the new times
int64_t delta = msTime - m_oldSubtitles[0].start;
for (int i = 0; i < m_oldSubtitles.size(); i++) {
m_newSubtitles.push_back(m_oldSubtitles[i]);
m_newSubtitles[i].start += delta;
m_newSubtitles[i].end += delta;
}
}
void MoveSubtitlesCommand::redo()
{
LOG_DEBUG() << m_oldSubtitles.size();
m_model.doRemoveSubtitleItems(m_trackIndex, m_oldSubtitles);
m_model.doInsertSubtitleItems(m_trackIndex, m_newSubtitles);
}
void MoveSubtitlesCommand::undo()
{
LOG_DEBUG() << m_oldSubtitles.size();
m_model.doRemoveSubtitleItems(m_trackIndex, m_newSubtitles);
m_model.doInsertSubtitleItems(m_trackIndex, m_oldSubtitles);
}
bool MoveSubtitlesCommand::mergeWith(const QUndoCommand *other)
{
const MoveSubtitlesCommand *that = static_cast<const MoveSubtitlesCommand *>(other);
if (m_trackIndex != that->m_trackIndex) {
return false;
}
if (m_oldSubtitles.size() != that->m_oldSubtitles.size()) {
return false;
}
if (m_newSubtitles[0].start != that->m_oldSubtitles[0].start) {
return false;
}
LOG_DEBUG() << "track" << m_trackIndex;
m_newSubtitles = that->m_newSubtitles;
return true;
}
} // namespace Subtitles

View File

@@ -0,0 +1,188 @@
/*
* Copyright (c) 2024 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 <http://www.gnu.org/licenses/>.
*/
#ifndef SUBTITLECOMMANDS_H
#define SUBTITLECOMMANDS_H
#include "models/subtitlesmodel.h"
#include <QUndoCommand>
namespace Subtitles {
enum {
UndoIdSubText = 400,
UndoIdSubStart,
UndoIdSubEnd,
UndoIdSubMove,
};
class InsertTrackCommand : public QUndoCommand
{
public:
InsertTrackCommand(SubtitlesModel &model, const SubtitlesModel::SubtitleTrack &track, int index);
void redo();
void undo();
private:
SubtitlesModel &m_model;
SubtitlesModel::SubtitleTrack m_track;
int m_index;
};
class RemoveTrackCommand : public QUndoCommand
{
public:
RemoveTrackCommand(SubtitlesModel &model, int trackIndex);
void redo();
void undo();
private:
SubtitlesModel &m_model;
int m_trackIndex;
SubtitlesModel::SubtitleTrack m_saveTrack;
QList<Subtitles::SubtitleItem> m_saveSubtitles;
};
class EditTrackCommand : public QUndoCommand
{
public:
EditTrackCommand(SubtitlesModel &model, const SubtitlesModel::SubtitleTrack &track, int index);
void redo();
void undo();
private:
SubtitlesModel &m_model;
SubtitlesModel::SubtitleTrack m_oldTrack;
SubtitlesModel::SubtitleTrack m_newTrack;
int m_index;
};
class OverwriteSubtitlesCommand : public QUndoCommand
{
public:
OverwriteSubtitlesCommand(SubtitlesModel &model,
int trackIndex,
const QList<Subtitles::SubtitleItem> &items);
void redo();
void undo();
protected:
QList<Subtitles::SubtitleItem> m_newSubtitles;
private:
SubtitlesModel &m_model;
int m_trackIndex;
QList<Subtitles::SubtitleItem> m_saveSubtitles;
};
class RemoveSubtitlesCommand : public QUndoCommand
{
public:
RemoveSubtitlesCommand(SubtitlesModel &model,
int trackIndex,
const QList<Subtitles::SubtitleItem> &items);
void redo();
void undo();
private:
SubtitlesModel &m_model;
int m_trackIndex;
QList<Subtitles::SubtitleItem> m_items;
};
class SetTextCommand : public QUndoCommand
{
public:
SetTextCommand(SubtitlesModel &model, int trackIndex, int itemIndex, const QString &text);
void redo();
void undo();
protected:
int id() const { return UndoIdSubText; }
bool mergeWith(const QUndoCommand *other);
private:
SubtitlesModel &m_model;
int m_trackIndex;
int m_itemIndex;
QString m_newText;
QString m_oldText;
};
class SetStartCommand : public QUndoCommand
{
public:
SetStartCommand(SubtitlesModel &model, int trackIndex, int itemIndex, int64_t msTime);
void redo();
void undo();
protected:
int id() const { return UndoIdSubStart; }
bool mergeWith(const QUndoCommand *other);
private:
SubtitlesModel &m_model;
int m_trackIndex;
int m_itemIndex;
int64_t m_newStart;
int64_t m_oldStart;
};
class SetEndCommand : public QUndoCommand
{
public:
SetEndCommand(SubtitlesModel &model, int trackIndex, int itemIndex, int64_t msTime);
void redo();
void undo();
protected:
int id() const { return UndoIdSubEnd; }
bool mergeWith(const QUndoCommand *other);
private:
SubtitlesModel &m_model;
int m_trackIndex;
int m_itemIndex;
int64_t m_newEnd;
int64_t m_oldEnd;
};
class MoveSubtitlesCommand : public QUndoCommand
{
public:
MoveSubtitlesCommand(SubtitlesModel &model,
int trackIndex,
const QList<Subtitles::SubtitleItem> &items,
int64_t msTime);
void redo();
void undo();
protected:
int id() const { return UndoIdSubMove; }
bool mergeWith(const QUndoCommand *other);
private:
SubtitlesModel &m_model;
int m_trackIndex;
QList<Subtitles::SubtitleItem> m_oldSubtitles;
QList<Subtitles::SubtitleItem> m_newSubtitles;
};
} // namespace Subtitles
#endif // SUBTITLECOMMANDS_H

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,892 @@
/*
* Copyright (c) 2013-2025 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 <http://www.gnu.org/licenses/>.
*/
#ifndef COMMANDS_H
#define COMMANDS_H
#include "docks/timelinedock.h"
#include "models/markersmodel.h"
#include "models/multitrackmodel.h"
#include "undohelper.h"
#include <MltProducer.h>
#include <MltTransition.h>
#include <QObject>
#include <QString>
#include <QUndoCommand>
#include <QUuid>
#include <vector>
namespace Timeline {
enum {
UndoIdTrimClipIn = 100,
UndoIdTrimClipOut,
UndoIdFadeIn,
UndoIdFadeOut,
UndoIdTrimTransitionIn,
UndoIdTrimTransitionOut,
UndoIdAddTransitionByTrimIn,
UndoIdAddTransitionByTrimOut,
UndoIdUpdate,
UndoIdMoveClip,
UndoIdChangeGain,
};
struct ClipPosition
{
ClipPosition(int track, int clip)
{
trackIndex = track;
clipIndex = clip;
}
bool operator<(const ClipPosition &rhs) const
{
if (trackIndex == rhs.trackIndex) {
return clipIndex < rhs.clipIndex;
} else {
return trackIndex < rhs.trackIndex;
}
}
int trackIndex;
int clipIndex;
};
class AppendCommand : public QUndoCommand
{
public:
AppendCommand(MultitrackModel &model,
int trackIndex,
const QString &xml,
bool skipProxy = false,
bool seek = true,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
QString m_xml;
UndoHelper m_undoHelper;
bool m_skipProxy;
bool m_seek;
QVector<QUuid> m_uuids;
};
class InsertCommand : public QUndoCommand
{
public:
InsertCommand(MultitrackModel &model,
MarkersModel &markersModel,
int trackIndex,
int position,
const QString &xml,
bool seek = true,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
MarkersModel &m_markersModel;
int m_trackIndex;
int m_position;
QString m_xml;
QStringList m_oldTracks;
UndoHelper m_undoHelper;
bool m_seek;
bool m_rippleAllTracks;
bool m_rippleMarkers;
int m_markersShift;
QVector<QUuid> m_uuids;
};
class OverwriteCommand : public QUndoCommand
{
public:
OverwriteCommand(MultitrackModel &model,
int trackIndex,
int position,
const QString &xml,
bool seek = true,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_position;
QString m_xml;
UndoHelper m_undoHelper;
bool m_seek;
QVector<QUuid> m_uuids;
};
class LiftCommand : public QUndoCommand
{
public:
LiftCommand(MultitrackModel &model, int trackIndex, int clipIndex, QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
UndoHelper m_undoHelper;
};
class RemoveCommand : public QUndoCommand
{
public:
RemoveCommand(MultitrackModel &model,
MarkersModel &markersModel,
int trackIndex,
int clipIndex,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
MarkersModel &m_markersModel;
int m_trackIndex;
int m_clipIndex;
UndoHelper m_undoHelper;
bool m_rippleAllTracks;
bool m_rippleMarkers;
int m_markerRemoveStart;
int m_markerRemoveEnd;
QList<Markers::Marker> m_markers;
};
class GroupCommand : public QUndoCommand
{
public:
GroupCommand(MultitrackModel &model, QUndoCommand *parent = 0);
void addToGroup(int trackIndex, int clipIndex);
void redo();
void undo();
private:
MultitrackModel &m_model;
QList<ClipPosition> m_clips;
QMap<ClipPosition, int> m_prevGroups;
};
class UngroupCommand : public QUndoCommand
{
public:
UngroupCommand(MultitrackModel &model, QUndoCommand *parent = 0);
void removeFromGroup(int trackIndex, int clipIndex);
void redo();
void undo();
private:
MultitrackModel &m_model;
QMap<ClipPosition, int> m_prevGroups;
};
class NameTrackCommand : public QUndoCommand
{
public:
NameTrackCommand(MultitrackModel &model,
int trackIndex,
const QString &name,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
QString m_name;
QString m_oldName;
};
class MergeCommand : public QUndoCommand
{
public:
MergeCommand(MultitrackModel &model, int trackIndex, int clipIndex, QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
UndoHelper m_undoHelper;
};
class MuteTrackCommand : public QUndoCommand
{
public:
MuteTrackCommand(MultitrackModel &model, int trackIndex, QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
bool m_oldValue;
};
class HideTrackCommand : public QUndoCommand
{
public:
HideTrackCommand(MultitrackModel &model, int trackIndex, QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
bool m_oldValue;
};
class CompositeTrackCommand : public QUndoCommand
{
public:
CompositeTrackCommand(MultitrackModel &model,
int trackIndex,
bool value,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
bool m_value;
bool m_oldValue;
};
class LockTrackCommand : public QUndoCommand
{
public:
LockTrackCommand(MultitrackModel &model, int trackIndex, bool value, QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
bool m_value;
bool m_oldValue;
};
class MoveClipCommand : public QUndoCommand
{
public:
MoveClipCommand(TimelineDock &timeline,
int trackDelta,
int positionDelta,
bool ripple,
QUndoCommand *parent = 0);
void addClip(int trackIndex, int clipIndex);
void redo();
void undo();
protected:
int id() const { return UndoIdMoveClip; }
bool mergeWith(const QUndoCommand *other);
private:
void redoMarkers();
TimelineDock &m_timeline;
MultitrackModel &m_model;
MarkersModel &m_markersModel;
struct Info
{
int trackIndex;
int clipIndex;
int frame_in;
int frame_out;
int start;
int group;
QUuid uuid;
Info()
: trackIndex(-1)
, clipIndex(-1)
, frame_in(-1)
, frame_out(-1)
, start(0)
, group(-1)
{}
};
int m_trackDelta;
int m_positionDelta;
bool m_ripple;
bool m_rippleAllTracks;
bool m_rippleMarkers;
UndoHelper m_undoHelper;
QMultiMap<int, Info> m_clips; // ordered by position
bool m_redo;
int m_earliestStart;
QList<Markers::Marker> m_markers;
};
class TrimCommand : public QUndoCommand
{
public:
explicit TrimCommand(QUndoCommand *parent = 0)
: QUndoCommand(parent)
{}
void setUndoHelper(UndoHelper *helper) { m_undoHelper.reset(helper); }
protected:
QScopedPointer<UndoHelper> m_undoHelper;
};
class TrimClipInCommand : public TrimCommand
{
public:
TrimClipInCommand(MultitrackModel &model,
MarkersModel &markersModel,
int trackIndex,
int clipIndex,
int delta,
bool ripple,
bool redo = true,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdTrimClipIn; }
bool mergeWith(const QUndoCommand *other);
private:
MultitrackModel &m_model;
MarkersModel &m_markersModel;
int m_trackIndex;
int m_clipIndex;
int m_delta;
bool m_ripple;
bool m_rippleAllTracks;
bool m_rippleMarkers;
bool m_redo;
int m_markerRemoveStart;
int m_markerRemoveEnd;
QList<Markers::Marker> m_markers;
};
class TrimClipOutCommand : public TrimCommand
{
public:
TrimClipOutCommand(MultitrackModel &model,
MarkersModel &markersModel,
int trackIndex,
int clipIndex,
int delta,
bool ripple,
bool redo = true,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdTrimClipOut; }
bool mergeWith(const QUndoCommand *other);
private:
MultitrackModel &m_model;
MarkersModel &m_markersModel;
int m_trackIndex;
int m_clipIndex;
int m_delta;
bool m_ripple;
bool m_rippleAllTracks;
bool m_rippleMarkers;
bool m_redo;
int m_markerRemoveStart;
int m_markerRemoveEnd;
QList<Markers::Marker> m_markers;
};
class SplitCommand : public QUndoCommand
{
public:
SplitCommand(MultitrackModel &model,
const std::vector<int> &trackIndex,
const std::vector<int> &clipIndex,
int position,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
std::vector<int> m_trackIndex;
std::vector<int> m_clipIndex;
int m_position;
UndoHelper m_undoHelper;
};
class FadeInCommand : public QUndoCommand
{
public:
FadeInCommand(MultitrackModel &model,
int trackIndex,
int clipIndex,
int duration,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdFadeIn; }
bool mergeWith(const QUndoCommand *other);
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
int m_duration;
int m_previous;
};
class FadeOutCommand : public QUndoCommand
{
public:
FadeOutCommand(MultitrackModel &model,
int trackIndex,
int clipIndex,
int duration,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdFadeOut; }
bool mergeWith(const QUndoCommand *other);
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
int m_duration;
int m_previous;
};
class AddTransitionCommand : public QUndoCommand
{
public:
AddTransitionCommand(TimelineDock &timeline,
int trackIndex,
int clipIndex,
int position,
bool ripple,
QUndoCommand *parent = 0);
void redo();
void undo();
int getTransitionIndex() const { return m_transitionIndex; }
private:
TimelineDock &m_timeline;
MultitrackModel &m_model;
MarkersModel &m_markersModel;
int m_trackIndex;
int m_clipIndex;
int m_position;
int m_transitionIndex;
bool m_ripple;
UndoHelper m_undoHelper;
bool m_rippleAllTracks;
bool m_rippleMarkers;
int m_markerOldStart;
int m_markerNewStart;
QList<Markers::Marker> m_markers;
};
class TrimTransitionInCommand : public TrimCommand
{
public:
TrimTransitionInCommand(MultitrackModel &model,
int trackIndex,
int clipIndex,
int delta,
bool redo = true,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdTrimTransitionIn; }
bool mergeWith(const QUndoCommand *other);
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
int m_delta;
bool m_notify;
bool m_redo;
};
class TrimTransitionOutCommand : public TrimCommand
{
public:
TrimTransitionOutCommand(MultitrackModel &model,
int trackIndex,
int clipIndex,
int delta,
bool redo = true,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdTrimTransitionOut; }
bool mergeWith(const QUndoCommand *other);
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
int m_delta;
bool m_notify;
bool m_redo;
};
class AddTransitionByTrimInCommand : public TrimCommand
{
public:
AddTransitionByTrimInCommand(TimelineDock &timeline,
int trackIndex,
int clipIndex,
int duration,
int trimDelta,
bool redo = true,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdAddTransitionByTrimIn; }
bool mergeWith(const QUndoCommand *other);
private:
TimelineDock &m_timeline;
int m_trackIndex;
int m_clipIndex;
int m_duration;
int m_trimDelta;
bool m_notify;
bool m_redo;
};
class RemoveTransitionByTrimInCommand : public TrimCommand
{
public:
RemoveTransitionByTrimInCommand(MultitrackModel &model,
int trackIndex,
int clipIndex,
int delta,
QString xml,
bool redo = true,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
int m_delta;
QString m_xml;
bool m_redo;
};
class RemoveTransitionByTrimOutCommand : public TrimCommand
{
public:
RemoveTransitionByTrimOutCommand(MultitrackModel &model,
int trackIndex,
int clipIndex,
int delta,
QString xml,
bool redo = true,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
int m_delta;
QString m_xml;
bool m_redo;
};
class AddTransitionByTrimOutCommand : public TrimCommand
{
public:
AddTransitionByTrimOutCommand(MultitrackModel &model,
int trackIndex,
int clipIndex,
int duration,
int trimDelta,
bool redo = true,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdAddTransitionByTrimOut; }
bool mergeWith(const QUndoCommand *other);
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
int m_duration;
int m_trimDelta;
bool m_notify;
bool m_redo;
};
class AddTrackCommand : public QUndoCommand
{
public:
AddTrackCommand(MultitrackModel &model, bool isVideo, QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
bool m_isVideo;
QUuid m_uuid;
};
class InsertTrackCommand : public QUndoCommand
{
public:
InsertTrackCommand(MultitrackModel &model,
int trackIndex,
TrackType trackType = PlaylistTrackType,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
TrackType m_trackType;
QUuid m_uuid;
};
class RemoveTrackCommand : public QUndoCommand
{
public:
RemoveTrackCommand(MultitrackModel &model, int trackIndex, QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
TrackType m_trackType;
QString m_trackName;
UndoHelper m_undoHelper;
QScopedPointer<Mlt::Producer> m_filtersProducer;
QUuid m_uuid;
};
class MoveTrackCommand : public QUndoCommand
{
public:
MoveTrackCommand(MultitrackModel &model,
int fromTrackIndex,
int toTrackIndex,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_fromTrackIndex;
int m_toTrackIndex;
};
class ChangeBlendModeCommand : public QObject, public QUndoCommand
{
Q_OBJECT
public:
ChangeBlendModeCommand(Mlt::Transition &transition,
const QString &propertyName,
const QString &mode,
QUndoCommand *parent = 0);
void redo();
void undo();
signals:
void modeChanged(QString &mode);
private:
Mlt::Transition m_transition;
QString m_propertyName;
QString m_newMode;
QString m_oldMode;
};
class UpdateCommand : public QUndoCommand
{
public:
UpdateCommand(TimelineDock &timeline,
int trackIndex,
int clipIndex,
int position,
QUndoCommand *parent = 0);
void setXmlAfter(const QString &xml);
void setPosition(int trackIndex, int clipIndex, int position);
void setRippleAllTracks(bool);
int trackIndex() const { return m_trackIndex; }
int clipIndex() const { return m_clipIndex; }
int position() const { return m_position; }
void redo();
void undo();
private:
TimelineDock &m_timeline;
int m_trackIndex;
int m_clipIndex;
int m_position;
QString m_xmlAfter;
bool m_isFirstRedo;
UndoHelper m_undoHelper;
bool m_ripple;
bool m_rippleAllTracks;
};
class DetachAudioCommand : public QUndoCommand
{
public:
DetachAudioCommand(TimelineDock &timeline,
int trackIndex,
int clipIndex,
int position,
const QString &xml,
QUndoCommand *parent = 0);
void redo();
void undo();
private:
TimelineDock &m_timeline;
int m_trackIndex;
int m_clipIndex;
int m_position;
int m_targetTrackIndex;
QString m_xml;
UndoHelper m_undoHelper;
bool m_trackAdded;
QUuid m_uuid;
};
class ReplaceCommand : public QUndoCommand
{
public:
ReplaceCommand(MultitrackModel &model,
int trackIndex,
int clipIndex,
const QString &xml,
QUndoCommand *parent = nullptr);
void redo();
void undo();
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
QString m_xml;
bool m_isFirstRedo;
UndoHelper m_undoHelper;
};
class AlignClipsCommand : public QUndoCommand
{
public:
AlignClipsCommand(MultitrackModel &model, QUndoCommand *parent = 0);
void addAlignment(QUuid uuid, int offset, double speedCompensation);
void redo();
void undo();
private:
MultitrackModel &m_model;
UndoHelper m_undoHelper;
bool m_redo;
struct Alignment
{
QUuid uuid;
int offset;
double speed;
};
QVector<Alignment> m_alignments;
};
class ApplyFiltersCommand : public QUndoCommand
{
public:
ApplyFiltersCommand(MultitrackModel &model,
const QString &filterProducerXml,
QUndoCommand *parent = 0);
void addClip(int trackIndex, int clipIndex);
void redo();
void undo();
private:
MultitrackModel &m_model;
QString m_xml;
QMap<ClipPosition, QString> m_prevFilters;
};
class ChangeGainCommand : public QUndoCommand
{
public:
ChangeGainCommand(MultitrackModel &model,
int trackIndex,
int clipIndex,
double gain,
QUndoCommand *parent = 0);
void redo();
void undo();
protected:
int id() const { return UndoIdChangeGain; }
bool mergeWith(const QUndoCommand *other);
private:
MultitrackModel &m_model;
int m_trackIndex;
int m_clipIndex;
double m_gain;
double m_previous;
};
} // namespace Timeline
#endif

454
src/commands/undohelper.cpp Normal file
View File

@@ -0,0 +1,454 @@
/*
* Copyright (c) 2015-2024 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 <http://www.gnu.org/licenses/>.
*/
#include "undohelper.h"
#include "Logger.h"
#include "mltcontroller.h"
#include "models/audiolevelstask.h"
#include "shotcut_mlt_properties.h"
#include <QScopedPointer>
#include <QUuid>
#ifdef UNDOHELPER_DEBUG
#define UNDOLOG LOG_DEBUG()
#else
#define UNDOLOG \
if (false) \
LOG_DEBUG()
#endif
UndoHelper::UndoHelper(MultitrackModel &model)
: m_model(model)
, m_hints(NoHints)
{}
void UndoHelper::recordBeforeState()
{
#ifdef UNDOHELPER_DEBUG
debugPrintState("Before state");
#endif
m_state.clear();
m_clipsAdded.clear();
m_insertedOrder.clear();
for (int i = 0; i < m_model.trackList().count(); ++i) {
int mltIndex = m_model.trackList()[i].mlt_index;
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(mltIndex));
Mlt::Playlist playlist(*trackProducer);
for (int j = 0; j < playlist.count(); ++j) {
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(j));
QUuid uid = MLT.ensureHasUuid(clip->parent());
if (clip->is_blank()) {
uid = MLT.ensureHasUuid(*clip);
}
m_insertedOrder << uid;
Info &info = m_state[uid];
if (!(m_hints & SkipXML))
info.xml = MLT.XML(&clip->parent());
Mlt::ClipInfo clipInfo;
playlist.clip_info(j, &clipInfo);
info.frame_in = clipInfo.frame_in;
info.frame_out = clipInfo.frame_out;
info.oldTrackIndex = i;
info.oldClipIndex = j;
info.isBlank = playlist.is_blank(j);
if (clipInfo.cut && clipInfo.cut->property_exists(kShotcutGroupProperty)) {
info.group = clipInfo.cut->get_int(kShotcutGroupProperty);
}
}
}
}
void UndoHelper::recordAfterState()
{
#ifdef UNDOHELPER_DEBUG
debugPrintState("After state");
#endif
QList<QUuid> clipsRemoved = m_state.keys();
m_clipsAdded.clear();
for (int i = 0; i < m_model.trackList().count(); ++i) {
int mltIndex = m_model.trackList()[i].mlt_index;
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(mltIndex));
Mlt::Playlist playlist(*trackProducer);
for (int j = 0; j < playlist.count(); ++j) {
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(j));
QUuid uid = MLT.ensureHasUuid(clip->parent());
if (clip->is_blank()) {
uid = MLT.ensureHasUuid(*clip);
}
/* Clips not previously in m_state are new */
if (!m_state.contains(uid)) {
UNDOLOG << "New clip at" << i << j;
m_clipsAdded << uid;
m_affectedTracks << i;
} else {
Info &info = m_state[uid];
info.changes = 0;
info.newTrackIndex = i;
info.newClipIndex = j;
/* Indices have changed; these are moved */
if (info.oldTrackIndex != info.newTrackIndex
|| info.oldClipIndex != info.newClipIndex) {
UNDOLOG << "Clip" << uid << "moved from" << info.oldTrackIndex
<< info.oldClipIndex << "to" << info.newTrackIndex << info.newClipIndex;
info.changes |= Moved;
m_affectedTracks << info.oldTrackIndex;
m_affectedTracks << info.newTrackIndex;
}
if (!(m_hints & SkipXML) && !info.isBlank) {
QString newXml = MLT.XML(&clip->parent());
if (info.xml != newXml) {
UNDOLOG << "Modified xml:" << uid;
info.changes |= XMLModified;
m_affectedTracks << i;
}
}
Mlt::ClipInfo newInfo;
playlist.clip_info(j, &newInfo);
/* Only in/out point changes are handled at this time. */
if (info.frame_in != newInfo.frame_in || info.frame_out != newInfo.frame_out) {
UNDOLOG << "In/out changed:" << uid;
info.changes |= ClipInfoModified;
info.in_delta = info.frame_in - newInfo.frame_in;
info.out_delta = newInfo.frame_out - info.frame_out;
m_affectedTracks << i;
}
}
clipsRemoved.removeOne(uid);
}
}
/* Clips that did not show up are removed from the timeline */
foreach (QUuid uid, clipsRemoved) {
UNDOLOG << "Clip removed:" << uid;
auto &info = m_state[uid];
info.changes = Removed;
m_affectedTracks << info.oldTrackIndex;
}
}
void UndoHelper::undoChanges()
{
#ifdef UNDOHELPER_DEBUG
debugPrintState("Before undo");
#endif
if (m_hints & RestoreTracks) {
restoreAffectedTracks();
emit m_model.modified();
#ifdef UNDOHELPER_DEBUG
debugPrintState("After undo");
#endif
return;
}
QMap<int, int> indexAdjustment;
/* We're walking through the list in the order of uids, which is the order in which the
* clips were laid out originally. As we go through the clips we make sure the clips behind
* the current index are as they were originally before we move on to the next one */
foreach (QUuid uid, m_insertedOrder) {
const Info &info = m_state[uid];
UNDOLOG << "Handling uid" << uid << "on track" << info.oldTrackIndex << "index"
<< info.oldClipIndex;
int trackIndex = m_model.trackList()[info.oldTrackIndex].mlt_index;
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(trackIndex));
Mlt::Playlist playlist(*trackProducer);
/* This is the index in the track we're currently restoring */
int currentIndex = qMin(info.oldClipIndex + indexAdjustment[trackIndex],
playlist.count() - 1);
/* Clips that were moved are simply searched for using the uid, and moved in place. We
* do not use the indices directly because they become invalid once the playlist is
* modified. */
if (info.changes & Moved) {
Q_ASSERT(info.newTrackIndex == info.oldTrackIndex
&& "cross-track moves are unsupported so far");
int clipCurrentlyAt = -1;
for (int i = 0; i < playlist.count(); ++i) {
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(i));
if (MLT.uuid(clip->parent()) == uid || MLT.uuid(*clip) == uid) {
clipCurrentlyAt = i;
break;
}
}
Q_ASSERT(clipCurrentlyAt != -1 && "Moved clip could not be found");
UNDOLOG << "Found clip with uid" << uid << "at index" << clipCurrentlyAt;
if (clipCurrentlyAt != info.oldClipIndex
&& (currentIndex < clipCurrentlyAt || currentIndex > clipCurrentlyAt + 1)) {
UNDOLOG << "moving from" << clipCurrentlyAt << "to" << currentIndex;
QModelIndex modelIndex = m_model.createIndex(clipCurrentlyAt, 0, info.oldTrackIndex);
m_model.beginMoveRows(modelIndex.parent(),
clipCurrentlyAt,
clipCurrentlyAt,
modelIndex.parent(),
currentIndex);
playlist.move(clipCurrentlyAt, currentIndex);
m_model.endMoveRows();
}
}
/* Removed clips are reinserted using their stored XML */
if (info.changes & Removed) {
QModelIndex modelIndex = m_model.createIndex(currentIndex, 0, info.oldTrackIndex);
m_model.beginInsertRows(modelIndex.parent(), currentIndex, currentIndex);
if (info.isBlank) {
playlist.insert_blank(currentIndex, info.frame_out - info.frame_in);
UNDOLOG << "inserting isBlank at " << currentIndex;
} else {
UNDOLOG << "inserting clip at " << currentIndex << uid;
Q_ASSERT(!(m_hints & SkipXML) && "Cannot restore clip without stored XML");
Q_ASSERT(!info.xml.isEmpty());
Mlt::Producer restoredClip(MLT.profile(),
"xml-string",
info.xml.toUtf8().constData());
if (restoredClip.type() == mlt_service_tractor_type) { // transition
restoredClip.set("mlt_type", "mlt_producer");
} else {
fixTransitions(playlist, currentIndex, restoredClip);
}
playlist.insert(restoredClip, currentIndex, info.frame_in, info.frame_out);
}
m_model.endInsertRows();
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(currentIndex));
Q_ASSERT(currentIndex < playlist.count());
Q_ASSERT(!clip.isNull());
if (info.isBlank) {
MLT.setUuid(*clip, uid);
} else {
MLT.setUuid(clip->parent(), uid);
}
if (info.group >= 0) {
clip->set(kShotcutGroupProperty, info.group);
}
AudioLevelsTask::start(clip->parent(), &m_model, modelIndex);
indexAdjustment[trackIndex]++;
}
/* Only in/out points handled so far */
if (info.changes & ClipInfoModified) {
int filterIn = MLT.filterIn(playlist, currentIndex);
int filterOut = MLT.filterOut(playlist, currentIndex);
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(currentIndex));
if (clip && clip->is_valid()) {
UNDOLOG << "resizing clip at" << currentIndex << "in" << info.frame_in << "out"
<< info.frame_out;
if (clip->parent().get_data("mlt_mix"))
clip->parent().set("mlt_mix", nullptr, 0);
if (clip->get_data("mix_in"))
clip->set("mix_in", nullptr, 0);
if (clip->get_data("mix_out"))
clip->set("mix_out", nullptr, 0);
playlist.resize_clip(currentIndex, info.frame_in, info.frame_out);
MLT.adjustClipFilters(clip->parent(),
filterIn,
filterOut,
info.in_delta,
info.out_delta,
info.in_delta);
}
QModelIndex modelIndex = m_model.createIndex(currentIndex, 0, info.oldTrackIndex);
QVector<int> roles;
roles << MultitrackModel::InPointRole;
roles << MultitrackModel::OutPointRole;
roles << MultitrackModel::DurationRole;
emit m_model.dataChanged(modelIndex, modelIndex, roles);
if (clip && clip->is_valid())
AudioLevelsTask::start(clip->parent(), &m_model, modelIndex);
}
}
/* Finally we walk through the tracks once more, removing clips that
* were added, and clearing the temporarily used uid property */
int trackIndex = 0;
foreach (const Track &track, m_model.trackList()) {
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(track.mlt_index));
Mlt::Playlist playlist(*trackProducer);
for (int i = playlist.count() - 1; i >= 0; --i) {
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(i));
QUuid uid = MLT.uuid(clip->parent());
if (clip->is_blank()) {
uid = MLT.uuid(*clip);
}
if (m_clipsAdded.removeOne(uid)) {
UNDOLOG << "Removing clip at" << i;
m_model.beginRemoveRows(m_model.index(trackIndex), i, i);
if (clip->parent().get_data("mlt_mix"))
clip->parent().set("mlt_mix", NULL, 0);
if (clip->get_data("mix_in"))
clip->set("mix_in", NULL, 0);
if (clip->get_data("mix_out"))
clip->set("mix_out", NULL, 0);
playlist.remove(i);
m_model.endRemoveRows();
}
}
trackIndex++;
}
emit m_model.modified();
#ifdef UNDOHELPER_DEBUG
debugPrintState("After undo");
#endif
}
void UndoHelper::setHints(OptimizationHints hints)
{
m_hints = hints;
}
void UndoHelper::debugPrintState(const QString &title)
{
LOG_DEBUG() << "timeline state:" << title << "{";
for (int i = 0; i < m_model.trackList().count(); ++i) {
int mltIndex = m_model.trackList()[i].mlt_index;
QString trackStr = QStringLiteral(" track %1 (mlt-idx %2):").arg(i).arg(mltIndex);
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(mltIndex));
Mlt::Playlist playlist(*trackProducer);
for (int j = 0; j < playlist.count(); ++j) {
Mlt::ClipInfo info;
playlist.clip_info(j, &info);
QUuid uid = MLT.uuid(*info.producer);
if (info.producer->is_blank() && info.cut) {
uid = MLT.uuid(*info.cut);
}
trackStr += QStringLiteral(" [ %5 %1 -> %2 (%3 frames) %4]")
.arg(info.frame_in)
.arg(info.frame_out)
.arg(info.frame_count)
.arg(info.cut->is_blank() ? "blank " : "clip")
.arg(uid.toString());
}
LOG_DEBUG() << qPrintable(trackStr);
}
LOG_DEBUG() << "}";
}
void UndoHelper::restoreAffectedTracks()
{
// Remove everything in the affected tracks.
for (const auto &trackIndex : std::as_const(m_affectedTracks)) {
if (trackIndex >= 0 && trackIndex < m_model.trackList().size()) {
auto mlt_index = m_model.trackList().at(trackIndex).mlt_index;
QScopedPointer<Mlt::Producer> producer(m_model.tractor()->track(mlt_index));
if (producer->is_valid()) {
Mlt::Playlist playlist(*producer.data());
m_model.beginRemoveRows(m_model.index(trackIndex), 0, playlist.count() - 1);
UNDOLOG << "clearing track" << trackIndex;
playlist.clear();
m_model.endRemoveRows();
}
}
}
for (const auto &uid : std::as_const(m_insertedOrder)) {
const Info &info = m_state[uid];
if (m_affectedTracks.contains(info.oldTrackIndex)) {
UNDOLOG << "Handling uid" << uid << "on track" << info.oldTrackIndex << "index"
<< info.oldClipIndex;
// Clips are restored using their stored XML.
int mltIndex = m_model.trackList()[info.oldTrackIndex].mlt_index;
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(mltIndex));
Mlt::Playlist playlist(*trackProducer);
auto currentIndex = playlist.count();
QModelIndex modelIndex = m_model.createIndex(currentIndex, 0, info.oldTrackIndex);
m_model.beginInsertRows(modelIndex.parent(), currentIndex, currentIndex);
if (info.isBlank) {
playlist.blank(info.frame_out - info.frame_in);
UNDOLOG << "appending blank at" << currentIndex << info.frame_out << info.frame_in;
} else {
UNDOLOG << "appending clip at" << currentIndex;
Q_ASSERT(!(m_hints & SkipXML) && "Cannot restore clip without stored XML");
Q_ASSERT(!info.xml.isEmpty());
Mlt::Producer restoredClip(MLT.profile(),
"xml-string",
info.xml.toUtf8().constData());
if (restoredClip.type() == mlt_service_tractor_type) { // transition
restoredClip.set("mlt_type", "mlt_producer");
}
playlist.append(restoredClip, info.frame_in, info.frame_out);
if (info.group >= 0) {
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(currentIndex));
clip->set(kShotcutGroupProperty, info.group);
}
}
m_model.endInsertRows();
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(currentIndex));
Q_ASSERT(currentIndex < playlist.count());
Q_ASSERT(!clip.isNull());
if (info.isBlank) {
MLT.setUuid(*clip, uid);
} else {
MLT.setUuid(clip->parent(), uid);
}
AudioLevelsTask::start(clip->parent(), &m_model, modelIndex);
}
}
for (const auto &trackIndex : std::as_const(m_affectedTracks)) {
if (trackIndex >= 0 && trackIndex < m_model.trackList().size()) {
auto mlt_index = m_model.trackList().at(trackIndex).mlt_index;
QScopedPointer<Mlt::Producer> producer(m_model.tractor()->track(mlt_index));
if (producer->is_valid()) {
Mlt::Playlist playlist(*producer.data());
for (auto currentIndex = 0; currentIndex < playlist.count(); currentIndex++) {
Mlt::Producer clip = playlist.get_clip(currentIndex);
fixTransitions(playlist, currentIndex, clip);
}
}
}
}
}
void UndoHelper::fixTransitions(Mlt::Playlist playlist, int clipIndex, Mlt::Producer clip)
{
if (clip.is_blank()) {
return;
}
int transitionIndex = 0;
for (auto currentIndex : {clipIndex + 1, clipIndex - 1}) {
// Connect a transition on the right/left to the new producer.
Mlt::Producer producer(playlist.get_clip(currentIndex));
if (producer.is_valid() && producer.parent().get(kShotcutTransitionProperty)) {
Mlt::Tractor transition(producer.parent());
if (transition.is_valid()) {
QScopedPointer<Mlt::Producer> transitionClip(transition.track(transitionIndex));
if (transitionClip->is_valid()
&& transitionClip->parent().get_service() != clip.parent().get_service()) {
UNDOLOG << "Fixing transition at clip index" << currentIndex
<< "transition index" << transitionIndex;
transitionClip.reset(
clip.cut(transitionClip->get_in(), transitionClip->get_out()));
transition.set_track(*transitionClip.data(), transitionIndex);
}
}
}
transitionIndex++;
}
}

91
src/commands/undohelper.h Normal file
View File

@@ -0,0 +1,91 @@
/*
* Copyright (c) 2015-2020 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 <http://www.gnu.org/licenses/>.
*/
#ifndef UNDOHELPER_H
#define UNDOHELPER_H
#include "models/multitrackmodel.h"
#include <MltPlaylist.h>
#include <QList>
#include <QMap>
#include <QSet>
#include <QString>
class UndoHelper
{
public:
enum OptimizationHints { NoHints, SkipXML, RestoreTracks };
UndoHelper(MultitrackModel &model);
void recordBeforeState();
void recordAfterState();
void undoChanges();
void setHints(OptimizationHints hints);
QSet<int> affectedTracks() const { return m_affectedTracks; }
private:
void debugPrintState(const QString &title);
void restoreAffectedTracks();
void fixTransitions(Mlt::Playlist playlist, int clipIndex, Mlt::Producer clip);
enum ChangeFlags {
NoChange = 0x0,
ClipInfoModified = 0x1,
XMLModified = 0x2,
Moved = 0x4,
Removed = 0x8
};
struct Info
{
int oldTrackIndex;
int oldClipIndex;
int newTrackIndex;
int newClipIndex;
bool isBlank;
QString xml;
int frame_in;
int frame_out;
int in_delta;
int out_delta;
int group;
int changes;
Info()
: oldTrackIndex(-1)
, oldClipIndex(-1)
, newTrackIndex(-1)
, newClipIndex(-1)
, isBlank(false)
, frame_in(-1)
, frame_out(-1)
, in_delta(0)
, out_delta(0)
, changes(NoChange)
, group(-1)
{}
};
QMap<QUuid, Info> m_state;
QList<QUuid> m_clipsAdded;
QList<QUuid> m_insertedOrder;
QSet<int> m_affectedTracks;
MultitrackModel &m_model;
OptimizationHints m_hints;
};
#endif // UNDOHELPER_H

View File

@@ -0,0 +1,455 @@
/*
* Copyright (c) 2014-2025 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 <http://www.gnu.org/licenses/>.
*/
#include "filtercontroller.h"
#include "Logger.h"
#include "mltcontroller.h"
#include "qmltypes/qmlapplication.h"
#include "qmltypes/qmlfilter.h"
#include "qmltypes/qmlmetadata.h"
#include "qmltypes/qmlutilities.h"
#include "settings.h"
#include "shotcut_mlt_properties.h"
#include <MltLink.h>
#include <QDir>
#include <QQmlComponent>
#include <QQmlEngine>
#include <QTimerEvent>
FilterController::FilterController(QObject *parent)
: QObject(parent)
, m_metadataModel(this)
, m_attachedModel(this)
, m_currentFilterIndex(QmlFilter::NoCurrentFilter)
{
startTimer(0);
connect(&m_attachedModel, SIGNAL(changed()), this, SLOT(handleAttachedModelChange()));
connect(&m_attachedModel,
SIGNAL(modelAboutToBeReset()),
this,
SLOT(handleAttachedModelAboutToReset()));
connect(&m_attachedModel,
SIGNAL(rowsAboutToBeRemoved(const QModelIndex &, int, int)),
this,
SLOT(handleAttachedRowsAboutToBeRemoved(const QModelIndex &, int, int)));
connect(&m_attachedModel,
SIGNAL(rowsRemoved(const QModelIndex &, int, int)),
this,
SLOT(handleAttachedRowsRemoved(const QModelIndex &, int, int)));
connect(&m_attachedModel,
SIGNAL(rowsInserted(const QModelIndex &, int, int)),
this,
SLOT(handleAttachedRowsInserted(const QModelIndex &, int, int)));
connect(&m_attachedModel,
SIGNAL(duplicateAddFailed(int)),
this,
SLOT(handleAttachDuplicateFailed(int)));
}
void FilterController::loadFilterMetadata()
{
QScopedPointer<Mlt::Properties> mltFilters(MLT.repository()->filters());
QScopedPointer<Mlt::Properties> mltLinks(MLT.repository()->links());
QScopedPointer<Mlt::Properties> mltProducers(MLT.repository()->producers());
QDir dir = QmlUtilities::qmlDir();
dir.cd("filters");
foreach (QString dirName,
dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Executable)) {
QDir subdir = dir;
subdir.cd(dirName);
subdir.setFilter(QDir::Files | QDir::NoDotAndDotDot | QDir::Readable);
subdir.setNameFilters(QStringList("meta*.qml"));
foreach (QString fileName, subdir.entryList()) {
LOG_DEBUG() << "reading filter metadata" << dirName << fileName;
QQmlComponent component(QmlUtilities::sharedEngine(), subdir.absoluteFilePath(fileName));
QmlMetadata *meta = qobject_cast<QmlMetadata *>(component.create());
if (meta) {
QScopedPointer<Mlt::Properties> mltMetadata(
MLT.repository()->metadata(mlt_service_filter_type,
meta->mlt_service().toLatin1().constData()));
QString version;
if (mltMetadata && mltMetadata->is_valid() && mltMetadata->get("version")) {
version = QString::fromLatin1(mltMetadata->get("version"));
if (version.startsWith("lavfi"))
version.remove(0, 5);
}
// Check if mlt_service is available.
if (mltFilters->get_data(meta->mlt_service().toLatin1().constData()) &&
// Check if MLT glaxnimate producer is available if needed
("maskGlaxnimate" != meta->objectName() || mltProducers->get_data("glaxnimate"))
&& (version.isEmpty() || meta->isMltVersion(version))) {
LOG_DEBUG() << "added filter" << meta->name();
meta->loadSettings();
meta->setPath(subdir);
meta->setParent(0);
addMetadata(meta);
// Check if a keyframes minimum version is required.
if (!version.isEmpty() && meta->keyframes()) {
meta->setProperty("version", version);
meta->keyframes()->checkVersion(version);
}
} else if (meta->type() == QmlMetadata::Link
&& mltLinks->get_data(meta->mlt_service().toLatin1().constData())) {
LOG_DEBUG() << "added link" << meta->name();
meta->loadSettings();
meta->setPath(subdir);
meta->setParent(0);
addMetadata(meta);
}
if (meta->isDeprecated())
meta->setName(meta->name() + " " + tr("(DEPRECATED)"));
} else if (!meta) {
LOG_WARNING() << component.errorString();
}
}
};
}
QmlMetadata *FilterController::metadata(const QString &id)
{
QmlMetadata *meta = 0;
int rowCount = m_metadataModel.sourceRowCount();
for (int i = 0; i < rowCount; i++) {
QmlMetadata *tmpMeta = m_metadataModel.getFromSource(i);
if (tmpMeta->uniqueId() == id) {
meta = tmpMeta;
break;
}
}
return meta;
}
QmlMetadata *FilterController::metadataForService(Mlt::Service *service)
{
QString uniqueId = service->get(kShotcutFilterProperty);
// Fallback to mlt_service for legacy filters
if (uniqueId.isEmpty()) {
uniqueId = service->get("mlt_service");
}
return metadata(uniqueId);
}
bool FilterController::isOutputTrackSelected() const
{
return m_attachedModel.producer() && m_attachedModel.producer()->is_valid()
&& mlt_service_tractor_type == m_attachedModel.producer()->type()
&& !m_attachedModel.producer()->get(kShotcutTransitionProperty)
&& m_attachedModel.rowCount() == 0;
}
void FilterController::loadFilterSets()
{
auto dir = QmlApplication::dataDir();
if (dir.cd("shotcut") && dir.cd("filter-sets")) {
QStringList entries = dir.entryList(QDir::Files | QDir::Readable);
for (const auto &s : entries) {
auto meta = new QmlMetadata;
meta->setType(QmlMetadata::FilterSet);
if (s == QUrl::toPercentEncoding(QUrl::fromPercentEncoding(s.toUtf8())))
meta->setName(QUrl::fromPercentEncoding(s.toUtf8()));
else
meta->setName(s);
meta->set_mlt_service("stock");
meta->loadSettings();
addMetadata(meta);
}
}
dir = Settings.appDataLocation();
if (dir.cd("filter-sets")) {
QStringList entries = dir.entryList(QDir::Files | QDir::Readable);
for (const auto &s : entries) {
auto meta = new QmlMetadata;
meta->setType(QmlMetadata::FilterSet);
if (s == QUrl::toPercentEncoding(QUrl::fromPercentEncoding(s.toUtf8())))
meta->setName(QUrl::fromPercentEncoding(s.toUtf8()));
else
meta->setName(s);
meta->loadSettings();
addMetadata(meta);
}
}
}
void FilterController::onUndoOrRedo(Mlt::Service &service)
{
MLT.refreshConsumer();
if (m_currentFilter && m_mltService.is_valid()
&& service.get_service() == m_mltService.get_service()) {
emit undoOrRedo();
QMetaObject::invokeMethod(this,
"setCurrentFilter",
Qt::QueuedConnection,
Q_ARG(int, m_currentFilterIndex));
}
}
void FilterController::timerEvent(QTimerEvent *event)
{
loadFilterMetadata();
loadFilterSets();
killTimer(event->timerId());
}
MetadataModel *FilterController::metadataModel()
{
return &m_metadataModel;
}
AttachedFiltersModel *FilterController::attachedModel()
{
return &m_attachedModel;
}
void FilterController::setProducer(Mlt::Producer *producer)
{
m_attachedModel.setProducer(producer);
if (producer && producer->is_valid()) {
m_metadataModel.updateFilterMask(!MLT.isTrackProducer(*producer),
producer->type() == mlt_service_chain_type,
producer->type() == mlt_service_playlist_type,
producer->type() == mlt_service_tractor_type,
producer->get("mlt_service") != QString("xml-clip"));
} else {
setCurrentFilter(QmlFilter::DeselectCurrentFilter);
}
}
void FilterController::setCurrentFilter(int attachedIndex)
{
if (attachedIndex == m_currentFilterIndex) {
return;
}
m_currentFilterIndex = attachedIndex;
// VUIs may instruct MLT filters to not render if they are doing the rendering
// theirself, for example, Text: Rich. Component.onDestruction is not firing.
if (m_mltService.is_valid()) {
if (m_mltService.get_int("_hide")) {
m_mltService.clear("_hide");
MLT.refreshConsumer();
}
}
QmlMetadata *meta = m_attachedModel.getMetadata(m_currentFilterIndex);
QmlFilter *filter = nullptr;
if (meta) {
emit currentFilterChanged(nullptr, nullptr, QmlFilter::NoCurrentFilter);
std::unique_ptr<Mlt::Service> service(m_attachedModel.getService(m_currentFilterIndex));
if (!service || !service->is_valid())
return;
m_mltService = Mlt::Service(service->get_service());
filter = new QmlFilter(m_mltService, meta);
filter->setIsNew(m_mltService.get_int(kNewFilterProperty));
m_mltService.clear(kNewFilterProperty);
connect(filter, SIGNAL(changed(QString)), SLOT(onQmlFilterChanged(const QString &)));
}
emit currentFilterChanged(filter, meta, m_currentFilterIndex);
m_currentFilter.reset(filter);
if (filter && !m_attachedModel.isSourceClip()) {
filter->startUndoTracking();
}
}
void FilterController::onGainChanged()
{
if (m_currentFilter) {
QString name = m_currentFilter->objectNameOrService();
if (name == QStringLiteral("audioGain")) {
emit m_currentFilter->changed();
}
}
}
void FilterController::onFadeInChanged()
{
if (m_currentFilter) {
QString name = m_currentFilter->objectNameOrService();
if (name.startsWith("fadeIn")) {
emit m_currentFilter->changed();
emit m_currentFilter->animateInChanged();
}
}
}
void FilterController::onFadeOutChanged()
{
if (m_currentFilter) {
QString name = m_currentFilter->objectNameOrService();
if (name.startsWith("fadeOut")) {
emit m_currentFilter->changed();
emit m_currentFilter->animateOutChanged();
}
}
}
void FilterController::onServiceInChanged(int delta, Mlt::Service *service)
{
if (delta && m_currentFilter
&& (!service || m_currentFilter->service().get_service() == service->get_service())) {
emit m_currentFilter->inChanged(delta);
}
}
void FilterController::onServiceOutChanged(int delta, Mlt::Service *service)
{
if (delta && m_currentFilter
&& (!service || m_currentFilter->service().get_service() == service->get_service())) {
emit m_currentFilter->outChanged(delta);
}
}
void FilterController::handleAttachedModelChange()
{
if (m_currentFilter) {
emit m_currentFilter->changed("disable");
}
}
void FilterController::handleAttachedModelAboutToReset()
{
setCurrentFilter(QmlFilter::NoCurrentFilter);
}
void FilterController::handleAttachedRowsRemoved(const QModelIndex &, int first, int)
{
m_currentFilterIndex = QmlFilter::DeselectCurrentFilter; // Force update
setCurrentFilter(qBound(0, first, qMax(m_attachedModel.rowCount() - 1, 0)));
}
void FilterController::handleAttachedRowsInserted(const QModelIndex &, int first, int)
{
m_currentFilterIndex = QmlFilter::DeselectCurrentFilter; // Force update
setCurrentFilter(qBound(0, first, qMax(m_attachedModel.rowCount() - 1, 0)));
}
void FilterController::handleAttachDuplicateFailed(int index)
{
const QmlMetadata *meta = m_attachedModel.getMetadata(index);
emit statusChanged(tr("Only one %1 filter is allowed.").arg(meta->name()));
setCurrentFilter(index);
}
void FilterController::onQmlFilterChanged(const QString &name)
{
if (name == "disable") {
QModelIndex index = m_attachedModel.index(m_currentFilterIndex);
emit m_attachedModel.dataChanged(index, index, QVector<int>() << Qt::CheckStateRole);
}
emit filterChanged(&m_mltService);
}
void FilterController::removeCurrent()
{
if (m_currentFilterIndex > QmlFilter::NoCurrentFilter)
m_attachedModel.remove(m_currentFilterIndex);
}
void FilterController::onProducerChanged()
{
emit m_attachedModel.trackTitleChanged();
}
void FilterController::pauseUndoTracking()
{
if (m_currentFilter && !m_attachedModel.isSourceClip()) {
m_currentFilter->stopUndoTracking();
}
}
void FilterController::resumeUndoTracking()
{
if (m_currentFilter && !m_attachedModel.isSourceClip()) {
m_currentFilter->startUndoTracking();
}
}
void FilterController::addMetadata(QmlMetadata *meta)
{
m_metadataModel.add(meta);
}
void FilterController::handleAttachedRowsAboutToBeRemoved(const QModelIndex &parent,
int first,
int last)
{
auto filter = m_attachedModel.getService(first);
m_motionTrackerModel.remove(m_motionTrackerModel.keyForFilter(filter));
}
void FilterController::addOrEditFilter(Mlt::Filter *filter, const QStringList &key_properties)
{
int rows = m_attachedModel.rowCount();
int serviceIndex = -1;
for (int i = 0; i < rows; i++) {
QScopedPointer<Mlt::Service> service(m_attachedModel.getService(i));
bool servicesMatch = true;
if (metadataForService(service.data())->uniqueId()
!= metadataForService(filter)->uniqueId()) {
continue;
}
for (auto &k : key_properties) {
const auto keyByteArray = k.toUtf8();
const char *key = keyByteArray.constData();
if (!service->property_exists(key) || !service->property_exists(key)) {
servicesMatch = false;
break;
} else if (QString(service->get(key)) != QString(filter->get(key))) {
servicesMatch = false;
break;
}
}
if (servicesMatch) {
serviceIndex = i;
break;
}
}
if (serviceIndex < 0) {
serviceIndex = m_attachedModel.addService(filter);
}
setCurrentFilter(serviceIndex);
}
void FilterController::setTrackTransitionService(const QString &service)
{
if (service == QStringLiteral("qtblend")) {
m_metadataModel.setHidden("qtBlendMode", false);
m_metadataModel.setHidden("blendMode", true);
m_metadataModel.setHidden("movitBlendMode", true);
} else if (service == QStringLiteral("frei0r.cairoblend")) {
m_metadataModel.setHidden("qtBlendMode", true);
m_metadataModel.setHidden("blendMode", false);
m_metadataModel.setHidden("movitBlendMode", true);
} else if (service == QStringLiteral("movit.overlay")) {
m_metadataModel.setHidden("qtBlendMode", true);
m_metadataModel.setHidden("blendMode", true);
m_metadataModel.setHidden("movitBlendMode", false);
} else {
m_metadataModel.setHidden("qtBlendMode", true);
m_metadataModel.setHidden("blendMode", true);
m_metadataModel.setHidden("movitBlendMode", true);
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (c) 2014-2025 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 <http://www.gnu.org/licenses/>.
*/
#ifndef FILTERCONTROLLER_H
#define FILTERCONTROLLER_H
#include "models/attachedfiltersmodel.h"
#include "models/metadatamodel.h"
#include "models/motiontrackermodel.h"
#include "qmltypes/qmlfilter.h"
#include "qmltypes/qmlmetadata.h"
#include <QFuture>
#include <QObject>
#include <QScopedPointer>
class QTimerEvent;
class FilterController : public QObject
{
Q_OBJECT
public:
explicit FilterController(QObject *parent = 0);
MetadataModel *metadataModel();
MotionTrackerModel *motionTrackerModel() { return &m_motionTrackerModel; }
AttachedFiltersModel *attachedModel();
QmlMetadata *metadata(const QString &id);
QmlMetadata *metadataForService(Mlt::Service *service);
QmlFilter *currentFilter() const { return m_currentFilter.data(); }
bool isOutputTrackSelected() const;
void onUndoOrRedo(Mlt::Service &service);
int currentIndex() const { return m_currentFilterIndex; }
void addOrEditFilter(Mlt::Filter *filter, const QStringList &key_properties);
void setTrackTransitionService(const QString &service);
protected:
void timerEvent(QTimerEvent *);
signals:
void currentFilterChanged(QmlFilter *filter, QmlMetadata *meta, int index);
void statusChanged(QString);
void filterChanged(Mlt::Service *);
void undoOrRedo();
public slots:
void setProducer(Mlt::Producer *producer = 0);
void setCurrentFilter(int attachedIndex);
void onFadeInChanged();
void onFadeOutChanged();
void onGainChanged();
void onServiceInChanged(int delta, Mlt::Service *service = 0);
void onServiceOutChanged(int delta, Mlt::Service *service = 0);
void removeCurrent();
void onProducerChanged();
void pauseUndoTracking();
void resumeUndoTracking();
private slots:
void handleAttachedModelChange();
void handleAttachedModelAboutToReset();
void addMetadata(QmlMetadata *);
void handleAttachedRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last);
void handleAttachedRowsRemoved(const QModelIndex &parent, int first, int last);
void handleAttachedRowsInserted(const QModelIndex &parent, int first, int last);
void handleAttachDuplicateFailed(int index);
void onQmlFilterChanged(const QString &name);
private:
void loadFilterSets();
void loadFilterMetadata();
QFuture<void> m_future;
QScopedPointer<QmlFilter> m_currentFilter;
Mlt::Service m_mltService;
MetadataModel m_metadataModel;
MotionTrackerModel m_motionTrackerModel;
AttachedFiltersModel m_attachedModel;
int m_currentFilterIndex;
};
#endif // FILTERCONTROLLER_H

View File

@@ -0,0 +1,65 @@
/*
* Copyright (c) 2015-2024 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 <http://www.gnu.org/licenses/>.
*/
#include "scopecontroller.h"
#include "Logger.h"
#include "docks/scopedock.h"
#include "widgets/scopes/audioloudnessscopewidget.h"
#include "widgets/scopes/audiopeakmeterscopewidget.h"
#include "widgets/scopes/audiospectrumscopewidget.h"
#include "widgets/scopes/audiosurroundscopewidget.h"
#include "widgets/scopes/audiovectorscopewidget.h"
#include "widgets/scopes/audiowaveformscopewidget.h"
#include "widgets/scopes/videohistogramscopewidget.h"
#include "widgets/scopes/videorgbparadescopewidget.h"
#include "widgets/scopes/videorgbwaveformscopewidget.h"
#include "widgets/scopes/videovectorscopewidget.h"
#include "widgets/scopes/videowaveformscopewidget.h"
#include "widgets/scopes/videozoomscopewidget.h"
#include <QMainWindow>
#include <QMenu>
ScopeController::ScopeController(QMainWindow *mainWindow, QMenu *menu)
: QObject(mainWindow)
{
LOG_DEBUG() << "begin";
QMenu *scopeMenu = menu->addMenu(tr("Scopes"));
createScopeDock<AudioLoudnessScopeWidget>(mainWindow, scopeMenu);
createScopeDock<AudioPeakMeterScopeWidget>(mainWindow, scopeMenu);
createScopeDock<AudioSpectrumScopeWidget>(mainWindow, scopeMenu);
createScopeDock<AudioSurroundScopeWidget>(mainWindow, scopeMenu);
createScopeDock<AudioVectorScopeWidget>(mainWindow, scopeMenu);
createScopeDock<AudioWaveformScopeWidget>(mainWindow, scopeMenu);
createScopeDock<VideoHistogramScopeWidget>(mainWindow, scopeMenu);
createScopeDock<VideoRgbParadeScopeWidget>(mainWindow, scopeMenu);
createScopeDock<VideoRgbWaveformScopeWidget>(mainWindow, scopeMenu);
createScopeDock<VideoVectorScopeWidget>(mainWindow, scopeMenu);
createScopeDock<VideoWaveformScopeWidget>(mainWindow, scopeMenu);
createScopeDock<VideoZoomScopeWidget>(mainWindow, scopeMenu);
LOG_DEBUG() << "end";
}
template<typename ScopeTYPE>
void ScopeController::createScopeDock(QMainWindow *mainWindow, QMenu *menu)
{
ScopeWidget *scopeWidget = new ScopeTYPE();
ScopeDock *scopeDock = new ScopeDock(this, scopeWidget);
scopeDock->hide();
menu->addAction(scopeDock->toggleViewAction());
mainWindow->addDockWidget(Qt::RightDockWidgetArea, scopeDock);
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2015-2016 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 <http://www.gnu.org/licenses/>.
*/
#ifndef SCOPECONTROLLER_H
#define SCOPECONTROLLER_H
#include "sharedframe.h"
#include <QObject>
#include <QString>
class QMainWindow;
class QMenu;
class QWidget;
class ScopeController Q_DECL_FINAL : public QObject
{
Q_OBJECT
public:
ScopeController(QMainWindow *mainWindow, QMenu *menu);
signals:
void newFrame(const SharedFrame &frame);
private:
template<typename ScopeTYPE>
void createScopeDock(QMainWindow *mainWindow, QMenu *menu);
};
#endif // SCOPECONTROLLER_H

136
src/database.cpp Normal file
View File

@@ -0,0 +1,136 @@
/*
* Copyright (c) 2013-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 <http://www.gnu.org/licenses/>.
*/
#include "database.h"
#include "Logger.h"
#include "dialogs/longuitask.h"
#include "settings.h"
#include <utime.h>
#include <QDir>
#include <QFileInfo>
#include <QtSql>
static QMutex g_mutex;
static Database *instance = nullptr;
static const int kMaxThumbnailCount = 5000;
static const int kDeleteThumbnailsTimeoutMs = 60000;
Database::Database(QObject *parent)
: QObject(parent)
{
m_deleteTimer.setInterval(kDeleteThumbnailsTimeoutMs);
connect(&m_deleteTimer, SIGNAL(timeout()), this, SLOT(deleteOldThumbnails()));
thumbnailsDir(); // convert from db to filesystem if needed
m_deleteTimer.start();
}
Database &Database::singleton(QObject *parent)
{
QMutexLocker locker(&g_mutex);
if (!instance) {
instance = new Database(parent);
}
return *instance;
}
static QString toFileName(const QString &s)
{
QString result = s;
return result.replace(':', '-') + +".png";
}
QDir Database::thumbnailsDir()
{
QDir dir(Settings.appDataLocation());
const char *subfolder = "thumbnails";
if (!dir.cd(subfolder)) {
if (dir.mkdir(subfolder)) {
dir.cd(subfolder);
// Convert the DB data to files on the filesystem.
LongUiTask longTask(QObject::tr("Converting Thumbnails"));
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
QDir appDir(Settings.appDataLocation());
QString dbFilePath = appDir.filePath("db.sqlite3");
db.setDatabaseName(dbFilePath);
if (db.open()) {
QSqlQuery query;
QImage img;
query.setForwardOnly(true);
int n = -1;
if (query.exec("SELECT COUNT(*) FROM thumbnails;") && query.next()) {
n = query.value(0).toInt();
}
query.exec(QStringLiteral("SELECT hash, accessed, image FROM thumbnails ORDER BY "
"accessed DESC LIMIT %1")
.arg(kMaxThumbnailCount));
for (int i = 0; query.next(); i++) {
QString fileName = toFileName(query.value(0).toString());
longTask.reportProgress(
QObject::tr(
"Please wait for this one-time update to the thumbnail cache..."),
i,
n);
if (img.loadFromData(query.value(2).toByteArray(), "PNG")) {
img.save(dir.filePath(fileName));
auto accessed = query.value(1).toDateTime();
auto offset = accessed.timeZone().offsetFromUtc(accessed);
struct utimbuf utimes
{
static_cast<time_t>(accessed.toSecsSinceEpoch() + offset),
static_cast<time_t>(accessed.toSecsSinceEpoch() + offset)
};
::utime(dir.filePath(fileName).toUtf8().constData(), &utimes);
}
}
db.close();
QSqlDatabase::removeDatabase("QSQLITE");
}
}
}
return dir;
}
bool Database::putThumbnail(const QString &hash, const QImage &image)
{
return image.save(thumbnailsDir().filePath(toFileName(hash)));
}
QImage Database::getThumbnail(const QString &hash)
{
QString filePath = thumbnailsDir().filePath(toFileName(hash));
::utime(filePath.toUtf8().constData(), nullptr);
return QImage(filePath);
}
void Database::deleteOldThumbnails()
{
auto result = QtConcurrent::run([=]() {
QDir dir = thumbnailsDir();
auto ls = dir.entryList(QDir::Files | QDir::NoDotAndDotDot | QDir::Readable, QDir::Time);
if (ls.size() - kMaxThumbnailCount > 0)
LOG_DEBUG() << "removing" << ls.size() - kMaxThumbnailCount;
for (int i = kMaxThumbnailCount; i < ls.size(); i++) {
QString filePath = dir.filePath(ls[i]);
if (!QFile::remove(filePath)) {
LOG_WARNING() << "failed to delete" << filePath;
}
}
});
}

46
src/database.h Normal file
View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2013-2021 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 <http://www.gnu.org/licenses/>.
*/
#ifndef DATABASE_H
#define DATABASE_H
#include <QDir>
#include <QImage>
#include <QTimer>
class Database : public QObject
{
Q_OBJECT
explicit Database(QObject *parent = 0);
public:
static Database &singleton(QObject *parent = 0);
bool putThumbnail(const QString &hash, const QImage &image);
QImage getThumbnail(const QString &hash);
private:
QDir thumbnailsDir();
QTimer m_deleteTimer;
private slots:
void deleteOldThumbnails();
};
#define DB Database::singleton()
#endif // DATABASE_H

161
src/dataqueue.h Normal file
View File

@@ -0,0 +1,161 @@
/*
* Copyright (c) 2015 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 <http://www.gnu.org/licenses/>.
*/
#ifndef DATAQUEUE_H
#define DATAQUEUE_H
#include <QMutex>
#include <QMutexLocker>
#include <QWaitCondition>
#include <deque>
/*!
\class DataQueue
\brief The DataQueue provides a thread safe container for passing data between
objects.
\threadsafe
DataQueue provides a limited size container for passing data between objects.
One object can add data to the queue by calling push() while another object
can remove items from the queue by calling pop().
DataQueue provides configurable behavior for handling overflows. It can
discard the oldest, discard the newest or block the object calling push()
until room has been freed in the queue by another object calling pop().
DataQueue is threadsafe and is therefore most appropriate when passing data
between objects operating in different thread contexts.
*/
template<class T>
class DataQueue
{
public:
//! Overflow behavior modes.
typedef enum {
OverflowModeDiscardOldest = 0, //!< Discard oldest items
OverflowModeDiscardNewest, //!< Discard newest items
OverflowModeWait //!< Wait for space to be free
} OverflowMode;
/*!
Constructs a DataQueue.
The \a size will be the maximum queue size and the \a mode will dictate
overflow behavior.
*/
explicit DataQueue(int maxSize, OverflowMode mode);
//! Destructs a DataQueue.
virtual ~DataQueue();
/*!
Pushes an item into the queue.
If the queue is full and overflow mode is OverflowModeWait then this
function will block until pop() is called.
*/
void push(const T &item);
/*!
Pops an item from the queue.
If the queue is empty then this function will block. If blocking is
undesired, then check the return of count() before calling pop().
*/
T pop();
//! Returns the number of items in the queue.
int count() const;
private:
std::deque<T> m_queue;
int m_maxSize;
OverflowMode m_mode;
mutable QMutex m_mutex;
QWaitCondition m_notEmptyCondition;
QWaitCondition m_notFullCondition;
};
template<class T>
DataQueue<T>::DataQueue(int maxSize, OverflowMode mode)
: m_queue()
, m_maxSize(maxSize)
, m_mode(mode)
, m_mutex()
, m_notEmptyCondition()
, m_notFullCondition()
{}
template<class T>
DataQueue<T>::~DataQueue()
{}
template<class T>
void DataQueue<T>::push(const T &item)
{
m_mutex.lock();
if (m_queue.size() == m_maxSize) {
switch (m_mode) {
case OverflowModeDiscardOldest:
m_queue.pop_front();
m_queue.push_back(item);
break;
case OverflowModeDiscardNewest:
// This item is the newest so discard it and exit
break;
case OverflowModeWait:
m_notFullCondition.wait(&m_mutex);
m_queue.push_back(item);
break;
}
} else {
m_queue.push_back(item);
if (m_queue.size() == 1) {
m_notEmptyCondition.wakeOne();
}
}
m_mutex.unlock();
}
template<class T>
T DataQueue<T>::pop()
{
T retVal;
m_mutex.lock();
if (m_queue.size() == 0) {
m_notEmptyCondition.wait(&m_mutex);
}
retVal = m_queue.front();
m_queue.pop_front();
if (m_mode == OverflowModeWait && m_queue.size() == m_maxSize - 1) {
m_notFullCondition.wakeOne();
}
m_mutex.unlock();
return retVal;
}
template<class T>
int DataQueue<T>::count() const
{
QMutexLocker locker(&m_mutex);
return m_queue.size();
}
#endif // DATAQUEUE_H

24
src/defaultlayouts.h Normal file
View File

@@ -0,0 +1,24 @@
#ifndef DEFAULTLAYOUTS_H
#define DEFAULTLAYOUTS_H
#include <QByteArray>
static const auto kLayoutLoggingDefault =
QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAHgAAADuPwCAAAAAvsAAAAYAFAAbABhAHkAbABpAHMAdABEAG8AYwBrAQAAADsAAAO4AAAAnQD////7AAAAEgBOAG8AdABlAHMARABvAGMAawAAAAAA/////wAAAFkA////AAAAAQAAAeAAAAO4/AIAAAAE/AAAADsAAAO4AAAAWQD////6AAAAAAIAAAAG+wAAABwAcAByAG8AcABlAHIAdABpAGUAcwBEAG8AYwBrAQAAAAD/////AAAAWQD////7AAAAFgBGAGkAbAB0AGUAcgBzAEQAbwBjAGsAAAAAAP////8AAAEsAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAWAGgAaQBzAHQAbwByAHkARABvAGMAawAAAAAA/////wAAAFkA////+wAAABQARQBuAGMAbwBkAGUARABvAGMAawAAAAAA/////wAAAJcA////+wAAABAASgBvAGIAcwBEAG8AYwBrAAAAAAD/////AAAAbwD////8AAACUAAAAegAAAAAAP////oAAAACAQAAAAv7AAAAHgBWAGkAZABlAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAEQA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAEQA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAAB4AUgBnAGIAVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAaAFIAZwBiAFAAYQByAGEAZABlAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAkAFYAaQBkAGUAbwBIAGkAcwB0AG8AZwByAGEAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAIgBBAHUAZABpAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAiAEEAdQBkAGkAbwBTAHAAZQBjAHQAcgB1AG0ARABvAGMAawAAAAAA/////wAAAEQA////+wAAACwAQQB1AGQAaQBvAEwAbwB1AGQAbgBlAHMAcwBNAGUAdABlAHIARABvAGMAawAAAAV5AAAB0gAAAEQA////+wAAACIAVgBpAGQAZQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVwD///8AAAADAAADrAAAAUP8AQAAAAX8AAAB6gAAAtYAAAAAAP////r/////AQAAAAL7AAAAGABUAGkAbQBlAGwAaQBuAGUARABvAGMAawAAAAAA/////wAAAMgA////+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawAAAAAA/////wAAAMgA////+wAAABIARgBpAGwAZQBzAEQAbwBjAGsBAAAB6gAAAqYAAACZAP////sAAAAUAFIAZQBjAGUAbgB0AEQAbwBjAGsBAAAEmgAAAPwAAACWAP////sAAAAkAEEAdQBkAGkAbwBQAGUAYQBrAE0AZQB0AGUAcgBEAG8AYwBrAAAABVcAAABEAAAARAD////7AAAAFgBNAGEAcgBrAGUAcgBzAEQAbwBjAGsAAAADMAAAAIMAAABEAP///wAAA6wAAAJrAAAAAQAAAAIAAAABAAAAAvwAAAABAAAAAgAAAAEAAAAWAG0AYQBpAG4AVABvAG8AbABCAGEAcgEAAAAA/////wAAAAAAAAAA");
static const auto kLayoutEditingDefault =
QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJkAAACofwCAAAAAvsAAAASAEYAaQBsAGUAcwBEAG8AYwBrAAAAADsAAAE+AAAAnQD////8AAAAOwAAAqEAAAFGAQAAGfoAAAAAAgAAAAb7AAAAGABQAGwAYQB5AGwAaQBzAHQARABvAGMAawEAAAAA/////wAAAJ0A////+wAAABYARgBpAGwAdABlAHIAcwBEAG8AYwBrAQAAAAD/////AAABLAD////7AAAAHABwAHIAbwBwAGUAcgB0AGkAZQBzAEQAbwBjAGsBAAAARgAAAxoAAABZAP////sAAAAUAEUAbgBjAG8AZABlAEQAbwBjAGsAAAAAAP////8AAACXAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAASAE4AbwB0AGUAcwBEAG8AYwBrAAAAAAD/////AAAAWQD///8AAAABAAABhwAAAqH8AgAAAAP8AAAAOwAAAqEAAACMAP////wBAAAAAvsAAAAkAEEAdQBkAGkAbwBQAGUAYQBrAE0AZQB0AGUAcgBEAG8AYwBrAQAABfkAAAB3AAAARAD////8AAAGegAAAQYAAACWAP////oAAAAAAgAAAAP7AAAAFABSAGUAYwBlAG4AdABEAG8AYwBrAQAAAAD/////AAAAcgD////7AAAAFgBoAGkAcwB0AG8AcgB5AEQAbwBjAGsBAAAAAP////8AAABZAP////sAAAAQAEoAbwBiAHMARABvAGMAawAAAABGAAAAbwAAAG8A////+wAAACIAQQB1AGQAaQBvAFMAdQByAHIAbwB1AG4AZABEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAHgBBAHUAZABpAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAFcA////AAAAAwAAB4AAAAEN/AEAAAAD/AAAAAAAAAeAAAAAyAD////6AAAAAQIAAAAC+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawEAAAAA/////wAAADQA////+wAAABgAVABpAG0AZQBsAGkAbgBlAEQAbwBjAGsBAAACcAAAAcgAAADIAP////wAAAX/AAABdAAAAAAA////+gAAAAMCAAAAC/sAAAAeAFYAaQBkAGUAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAIgBWAGkAZABlAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAAAAAAAAAPsAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAAAAAAAAAPsAAAAeAFIAZwBiAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAGgBSAGcAYgBQAGEAcgBhAGQAZQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAJABWAGkAZABlAG8ASABpAHMAdABvAGcAcgBhAG0ARABvAGMAawAAAAAA/////wAAAFcA////+wAAACIAQQB1AGQAaQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAIgBBAHUAZABpAG8AUwBwAGUAYwB0AHIAdQBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAsAEEAdQBkAGkAbwBMAG8AdQBkAG4AZQBzAHMATQBlAHQAZQByAEQAbwBjAGsAAAACZgAAAcgAAABXAP////sAAAAWAE0AYQByAGsAZQByAHMARABvAGMAawAAAAP4AAAA/gAAAEQA////AAADgQAAAqEAAAABAAAAAgAAAAgAAAAI/AAAAAEAAAACAAAAAQAAABYAbQBhAGkAbgBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA=");
static const auto kLayoutEffectsDefault =
QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAANmAAAChPwCAAAAAfwAAAA7AAAChAAAASwA/////AEAAAAD/AAAAAAAAAHfAAABkAD////6AAAAAQIAAAAC+wAAABIARgBpAGwAZQBzAEQAbwBjAGsAAAAAAP////8AAACdAP////sAAAAWAEYAaQBsAHQAZQByAHMARABvAGMAawEAAAA7AAACgAAAASwA////+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawEAAAHpAAABfQAAAMgA////+wAAABIATgBvAHQAZQBzAEQAbwBjAGsAAAAAAP////8AAABGAP///wAAAAEAAACZAAAChPwCAAAAA/sAAAAkAEEAdQBkAGkAbwBQAGUAYQBrAE0AZQB0AGUAcgBEAG8AYwBrAQAAADsAAAKEAAAAVwD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVwD///8AAAADAAAHgAAAASr8AQAAAAT8AAAAAAAAAjIAAACZAP////oAAAAAAgAAAAf7AAAAGABQAGwAYQB5AGwAaQBzAHQARABvAGMAawEAAAJmAAAByAAAAJ0A////+wAAABoAUwB1AGIAdABpAHQAbABlAHMARABvAGMAawAAAAAA/////wAAAFcA////+wAAABwAcAByAG8AcABlAHIAdABpAGUAcwBEAG8AYwBrAAAAAAD/////AAAAWQD////7AAAAFABSAGUAYwBlAG4AdABEAG8AYwBrAAAAAAD/////AAAAcgD////7AAAAFgBoAGkAcwB0AG8AcgB5AEQAbwBjAGsAAAAAAP////8AAABZAP////sAAAAUAEUAbgBjAG8AZABlAEQAbwBjAGsAAAAAAP////8AAACXAP////sAAAAQAEoAbwBiAHMARABvAGMAawAAAAAA/////wAAAG8A////+wAAABgAVABpAG0AZQBsAGkAbgBlAEQAbwBjAGsBAAACPAAABUQAAADIAP////wAAAYkAAABTwAAAAAA////+gAAAAMCAAAAC/sAAAAiAFYAaQBkAGUAbwBXAGEAdgBlAGYAbwByAG0ARABvAGMAawAAAAAA/////wAAAFcA////+wAAAB4AVgBpAGQAZQBvAFYAZQBjAHQAbwByAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAAAAAAAAAPsAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsAAAAAAP////8AAAAAAAAAAPsAAAAeAFIAZwBiAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAGgBSAGcAYgBQAGEAcgBhAGQAZQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAJABWAGkAZABlAG8ASABpAHMAdABvAGcAcgBhAG0ARABvAGMAawAAAAAA/////wAAAFcA////+wAAACIAQQB1AGQAaQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAIgBBAHUAZABpAG8AUwBwAGUAYwB0AHIAdQBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAsAEEAdQBkAGkAbwBMAG8AdQBkAG4AZQBzAHMATQBlAHQAZQByAEQAbwBjAGsAAAACZgAAAcgAAABXAP////sAAAAWAE0AYQByAGsAZQByAHMARABvAGMAawAAAAPqAAABDAAAAEQA////AAADbQAAAoQAAAABAAAAAgAAAAgAAAAI/AAAAAEAAAACAAAAAQAAABYAbQBhAGkAbgBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA=");
static const auto kLayoutColorDefault =
QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJMAAACWPwCAAAAAfwAAAA7AAACWAAAASwA////+gAAAAIBAAAABfsAAAASAEYAaQBsAGUAcwBEAG8AYwBrAAAAAAD/////AAAAmQD////7AAAAFABFAG4AYwBvAGQAZQBEAG8AYwBrAAAAAAD/////AAABVgD////7AAAAFgBGAGkAbAB0AGUAcgBzAEQAbwBjAGsBAAAAAAAAAxAAAAGQAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAASAE4AbwB0AGUAcwBEAG8AYwBrAAAAAAD/////AAAARgD///8AAAABAAABXgAAA7j8AgAAAAP8AAAAOwAAARMAAABxAP////wBAAAAAvsAAAAkAEEAdQBkAGkAbwBQAGUAYQBrAE0AZQB0AGUAcgBEAG8AYwBrAAAABfkAAABEAAAARAD////8AAAGIgAAAV4AAAB7AP////oAAAAAAgAAAAL7AAAAHgBWAGkAZABlAG8AVgBlAGMAdABvAHIARABvAGMAawEAAAB2AAAA4gAAAFcA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawEAAAAA/////wAAAFcA/////AAAAVgAAAFhAAAAcQEAABn6AAAAAAEAAAAG+wAAABoAUgBnAGIAUABhAHIAYQBkAGUARABvAGMAawEAAAAA/////wAAAEQA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawEAAAXwAAABegAAAAAAAAAA+wAAAB4AUgBnAGIAVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsBAAAAAP////8AAABEAP////sAAAAiAFYAaQBkAGUAbwBXAGEAdgBlAGYAbwByAG0ARABvAGMAawEAAAAA/////wAAAEQA////+wAAACIAQQB1AGQAaQBvAFMAdQByAHIAbwB1AG4AZABEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAHgBBAHUAZABpAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAEQA/////AAAAsMAAAEwAAAAVwD////6AAAAAAEAAAAC+wAAACQAVgBpAGQAZQBvAEgAaQBzAHQAbwBnAHIAYQBtAEQAbwBjAGsBAAAAAP////8AAABEAP////sAAAAaAFYAaQBkAGUAbwBaAG8AbwBtAEQAbwBjAGsBAAAF8AAAAXoAAAAAAAAAAAAAAAMAAAYYAAABVvwBAAAABPwAAAAAAAAByQAAAJkA////+gAAAAACAAAABfsAAAAYAFAAbABhAHkAbABpAHMAdABEAG8AYwBrAQAAAAD/////AAAAnQD////7AAAAFABSAGUAYwBlAG4AdABEAG8AYwBrAAAAAAD/////AAAAcgD////7AAAAHABwAHIAbwBwAGUAcgB0AGkAZQBzAEQAbwBjAGsAAAAAAP////8AAABZAP////sAAAAWAGgAaQBzAHQAbwByAHkARABvAGMAawAAAAAA/////wAAAFkA////+wAAABAASgBvAGIAcwBEAG8AYwBrAAAAAAD/////AAAAbwD////8AAAB0wAABEUAAADIAP////oAAAAAAgAAAAL7AAAAGABUAGkAbQBlAGwAaQBuAGUARABvAGMAawEAAAIqAAACBAAAAMgA////+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawAAAAAA/////wAAADQA/////AAABOkAAAEBAAAAAAD////6/////wIAAAAD+wAAACIAQQB1AGQAaQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAIgBBAHUAZABpAG8AUwBwAGUAYwB0AHIAdQBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAsAEEAdQBkAGkAbwBMAG8AdQBkAG4AZQBzAHMATQBlAHQAZQByAEQAbwBjAGsAAAACZgAAAcgAAABXAP////sAAAAWAE0AYQByAGsAZQByAHMARABvAGMAawAAAALfAAAAswAAAEQA////AAADwgAAAlgAAAABAAAAAgAAAAgAAAAC/AAAAAEAAAACAAAAAQAAABYAbQBhAGkAbgBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA=");
static const auto kLayoutAudioDefault =
QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJsAAACP/wCAAAAAfwAAAA7AAACPwAAASwA////+gAAAAIBAAAABfsAAAASAEYAaQBsAGUAcwBEAG8AYwBrAAAAAAD/////AAAAmQD////7AAAAFABFAG4AYwBvAGQAZQBEAG8AYwBrAAAAAAD/////AAABVgD////7AAAAFgBGAGkAbAB0AGUAcgBzAEQAbwBjAGsBAAAAAAAAAxAAAAGQAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAASAE4AbwB0AGUAcwBEAG8AYwBrAAAAAAD/////AAAARgD///8AAAABAAABPgAAA7j8AgAAAAP8AAAAOwAAAawAAABXAP////wBAAAAAvsAAAAkAEEAdQBkAGkAbwBQAGUAYQBrAE0AZQB0AGUAcgBEAG8AYwBrAQAABkIAAABXAAAARAD////8AAAGowAAAN0AAABEAP////oAAAAAAgAAAAP7AAAALABBAHUAZABpAG8ATABvAHUAZABuAGUAcwBzAE0AZQB0AGUAcgBEAG8AYwBrAQAAAAD/////AAAAVwD////7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAAAAAEYAAAD+AAAAVwD////7AAAAHgBWAGkAZABlAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAFcA/////AAAAfEAAADWAAAAVwD////6AAAAAAEAAAAH+wAAACIAQQB1AGQAaQBvAFMAcABlAGMAdAByAHUAbQBEAG8AYwBrAQAAAAD/////AAAARAD////7AAAAGgBSAGcAYgBQAGEAcgBhAGQAZQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAAAABfAAAAF6AAAAAAAAAAD7AAAAHgBSAGcAYgBXAGEAdgBlAGYAbwByAG0ARABvAGMAawAAAAAA/////wAAAEQA////+wAAACIAVgBpAGQAZQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABEAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAARAD////8AAAC0QAAASIAAABXAP////oAAAAAAQAAAAP7AAAAIgBBAHUAZABpAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsBAAAAAP////8AAABEAP////sAAAAkAFYAaQBkAGUAbwBIAGkAcwB0AG8AZwByAGEAbQBEAG8AYwBrAAAAAAD/////AAAARAD////7AAAAGgBWAGkAZABlAG8AWgBvAG8AbQBEAG8AYwBrAAAABfAAAAF6AAAAAAAAAAAAAAADAAAGOAAAAW/8AQAAAAP8AAAAAAAAAXwAAACZAP////oAAAAAAgAAAAX7AAAAGABQAGwAYQB5AGwAaQBzAHQARABvAGMAawEAAAAA/////wAAAJ0A////+wAAABQAUgBlAGMAZQBuAHQARABvAGMAawAAAAAA/////wAAAHIA////+wAAABwAcAByAG8AcABlAHIAdABpAGUAcwBEAG8AYwBrAAAAAAD/////AAAAWQD////7AAAAFgBoAGkAcwB0AG8AcgB5AEQAbwBjAGsAAAAAAP////8AAABZAP////sAAAAQAEoAbwBiAHMARABvAGMAawAAAAAA/////wAAAG8A/////AAAAYYAAASyAAAAyAD////6AAAAAAIAAAAC+wAAABgAVABpAG0AZQBsAGkAbgBlAEQAbwBjAGsBAAACKgAAAgQAAADIAP////sAAAAaAEsAZQB5AGYAcgBhAG0AZQBzAEQAbwBjAGsBAAAAAP////8AAAA0AP////sAAAAWAE0AYQByAGsAZQByAHMARABvAGMAawAAAALUAAAA3gAAAEQA////AAADwgAAAj8AAAABAAAAAgAAAAgAAAAC/AAAAAEAAAACAAAAAQAAABYAbQBhAGkAbgBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA=");
static const auto kLayoutPlayerDefault =
QByteArray::fromBase64("AAAA/wAAAAD9AAAAAwAAAAAAAAJFAAADuPwCAAAAAfwAAAA7AAADuAAAAAAA/////AEAAAAD/AAAAAAAAAJFAAAAAAD////6/////wIAAAAC+wAAABIARgBpAGwAZQBzAEQAbwBjAGsAAAAAAP////8AAACdAP////sAAAAYAFAAbABhAHkAbABpAHMAdABEAG8AYwBrAAAAADsAAAO4AAAAnQD////8AAAAAAAAAkUAAAAAAP////r/////AgAAAAT7AAAAHABwAHIAbwBwAGUAcgB0AGkAZQBzAEQAbwBjAGsAAAAARgAAAxoAAABZAP////sAAAAaAFMAdQBiAHQAaQB0AGwAZQBzAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAWAEYAaQBsAHQAZQByAHMARABvAGMAawAAAAAA/////wAAASwA////+wAAABQARQBuAGMAbwBkAGUARABvAGMAawAAAAAA/////wAAAJcA////+wAAABIATgBvAHQAZQBzAEQAbwBjAGsAAAAAAP////8AAABGAP///wAAAAEAAAGLAAADuPwCAAAAA/wAAAA7AAADuAAAAAAA/////AEAAAAC+wAAACQAQQB1AGQAaQBvAFAAZQBhAGsATQBlAHQAZQByAEQAbwBjAGsAAAAF3wAAAYsAAABEAP////wAAAX1AAABiwAAAAAA////+v////8CAAAAA/sAAAAUAFIAZQBjAGUAbgB0AEQAbwBjAGsAAAAAAP////8AAAByAP////sAAAAWAGgAaQBzAHQAbwByAHkARABvAGMAawAAAAAA/////wAAAFkA////+wAAABAASgBvAGIAcwBEAG8AYwBrAAAAAEYAAABvAAAAbwD////7AAAAIgBBAHUAZABpAG8AUwB1AHIAcgBvAHUAbgBkAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAeAEEAdQBkAGkAbwBWAGUAYwB0AG8AcgBEAG8AYwBrAAAAAAD/////AAAAVwD///8AAAADAAAE9gAAAS/8AQAAAAP8AAAAAAAAA+UAAAAAAP////r/////AgAAAAL7AAAAGABUAGkAbQBlAGwAaQBuAGUARABvAGMAawAAAAAA/////wAAAMgA////+wAAABoASwBlAHkAZgByAGEAbQBlAHMARABvAGMAawAAAANmAAAAyAAAADQA/////AAAAAAAAAdzAAAAAAD////6AAAAAwIAAAAL+wAAACIAVgBpAGQAZQBvAFcAYQB2AGUAZgBvAHIAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAHgBWAGkAZABlAG8AVgBlAGMAdABvAHIARABvAGMAawAAAAAA/////wAAAFcA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAFcA////+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAABoAVgBpAGQAZQBvAFoAbwBvAG0ARABvAGMAawAAAAAA/////wAAAAAAAAAA+wAAAB4AUgBnAGIAVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAaAFIAZwBiAFAAYQByAGEAZABlAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAkAFYAaQBkAGUAbwBIAGkAcwB0AG8AZwByAGEAbQBEAG8AYwBrAAAAAAD/////AAAAVwD////7AAAAIgBBAHUAZABpAG8AVwBhAHYAZQBmAG8AcgBtAEQAbwBjAGsAAAAAAP////8AAABXAP////sAAAAiAEEAdQBkAGkAbwBTAHAAZQBjAHQAcgB1AG0ARABvAGMAawAAAAAA/////wAAAFcA////+wAAACwAQQB1AGQAaQBvAEwAbwB1AGQAbgBlAHMAcwBNAGUAdABlAHIARABvAGMAawAAAAJmAAAByAAAAFcA////+wAAABYATQBhAHIAawBlAHIAcwBEAG8AYwBrAAAAAAAAAAT2AAAARAD///8AAAeAAAADuAAAAAEAAAACAAAACAAAAAj8AAAAAQAAAAIAAAABAAAAFgBtAGEAaQBuAFQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA==");
#endif // DEFAULTLAYOUTS_H

View File

@@ -0,0 +1,377 @@
/*
* Copyright (c) 20222-2024 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 <http://www.gnu.org/licenses/>.
*/
#include "actionsdialog.h"
#include "widgets/statuslabelwidget.h"
#include <QAction>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QKeyEvent>
#include <QKeySequenceEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QSortFilterProxyModel>
#include <QStyledItemDelegate>
#include <QToolButton>
#include <QTreeView>
#include <QVBoxLayout>
static const unsigned int editorWidth = 180;
class ShortcutEditor : public QWidget
{
Q_OBJECT
public:
ShortcutEditor(QWidget *parent = nullptr)
: QWidget(parent)
{
setMinimumWidth(editorWidth);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
QHBoxLayout *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
seqEdit = new QKeySequenceEdit();
layout->addWidget(seqEdit);
QToolButton *applyButton = new QToolButton();
applyButton->setIcon(
QIcon::fromTheme("dialog-ok", QIcon(":/icons/oxygen/32x32/actions/dialog-ok.png")));
applyButton->setText(tr("Apply"));
applyButton->setToolTip(tr("Apply"));
connect(applyButton, &QToolButton::clicked, this, [this]() { emit applied(); });
layout->addWidget(applyButton);
QToolButton *defaultButton = new QToolButton();
defaultButton->setIcon(
QIcon::fromTheme("edit-undo", QIcon(":/icons/oxygen/32x32/actions/edit-undo.png")));
defaultButton->setText(tr("Set to default"));
defaultButton->setToolTip(tr("Set to default"));
connect(defaultButton, &QToolButton::clicked, this, [&]() {
seqEdit->setKeySequence(defaultSeq);
});
layout->addWidget(defaultButton);
QToolButton *clearButton = new QToolButton();
clearButton->setIcon(
QIcon::fromTheme("edit-clear", QIcon(":/icons/oxygen/32x32/actions/edit-clear.png")));
clearButton->setText(tr("Clear shortcut"));
clearButton->setToolTip(tr("Clear shortcut"));
connect(clearButton, &QToolButton::clicked, this, [&]() { seqEdit->clear(); });
layout->addWidget(clearButton);
setLayout(layout);
QMetaObject::invokeMethod(seqEdit, "setFocus", Qt::QueuedConnection);
}
~ShortcutEditor() = default;
QKeySequenceEdit *seqEdit;
QKeySequence defaultSeq;
signals:
void applied();
};
class ShortcutItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
ShortcutItemDelegate(QObject *parent = nullptr)
: QStyledItemDelegate(parent)
{}
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() == ActionsModel::COLUMN_SEQUENCE1
|| (index.column() == ActionsModel::COLUMN_SEQUENCE2
&& !index.data(ActionsModel::HardKeyRole).isValid())) {
// Hard key shortcuts are in column 2 and are not editable.
m_currentEditor = new ShortcutEditor(parent);
connect(m_currentEditor, &ShortcutEditor::applied, this, [this]() {
auto dialog = static_cast<ActionsDialog *>(QObject::parent());
dialog->saveCurrentEditor();
});
m_currentEditor->setFocus();
return m_currentEditor;
}
return nullptr;
}
void destroyEditor(QWidget *editor, const QModelIndex &index) const
{
m_currentEditor = nullptr;
QStyledItemDelegate::destroyEditor(editor, index);
}
void setEditorData(QWidget *editor, const QModelIndex &index) const
{
ShortcutEditor *widget = dynamic_cast<ShortcutEditor *>(editor);
if (widget) {
widget->seqEdit->setKeySequence(index.data(Qt::EditRole).value<QKeySequence>());
widget->defaultSeq = index.data(ActionsModel::DefaultKeyRole).value<QKeySequence>();
}
}
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QKeySequence newSeq = static_cast<ShortcutEditor *>(editor)->seqEdit->keySequence();
model->setData(index, newSeq);
}
ShortcutEditor *currentEditor() const { return m_currentEditor; }
private:
mutable ShortcutEditor *m_currentEditor = nullptr;
};
class KeyPressFilter : public QObject
{
Q_OBJECT
public:
KeyPressFilter(QObject *parent = 0)
: QObject(parent)
{}
protected:
bool eventFilter(QObject *obj, QEvent *event) override
{
if (event->type() == QEvent::KeyPress) {
auto keyEvent = static_cast<QKeyEvent *>(event);
if (!keyEvent->modifiers()
&& (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)) {
auto window = static_cast<QWidget *>(parent());
window->close();
}
}
return QObject::eventFilter(obj, event);
}
};
class PrivateTreeView : public QTreeView
{
Q_OBJECT
public:
PrivateTreeView(QWidget *parent = nullptr)
: QTreeView(parent)
{}
virtual bool edit(const QModelIndex &index,
QAbstractItemView::EditTrigger trigger,
QEvent *event) override
{
bool editInProgress = QTreeView::edit(index, trigger, event);
if (editInProgress && trigger == QAbstractItemView::AllEditTriggers
&& (index.column() == ActionsModel::COLUMN_SEQUENCE1
|| index.column() == ActionsModel::COLUMN_SEQUENCE2)) {
if (state() != QAbstractItemView::EditingState)
emit editRejected();
}
return editInProgress;
}
#ifdef Q_OS_MAC
virtual void keyPressEvent(QKeyEvent *event) override
{
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
emit activated(currentIndex());
} else if (event->key() == Qt::Key_F2) {
edit(currentIndex(), QAbstractItemView::EditKeyPressed, event);
} else {
QAbstractItemView::keyPressEvent(event);
}
}
#endif
signals:
void editRejected();
};
class SearchKeyPressFilter : public QObject
{
Q_OBJECT
public:
SearchKeyPressFilter(QObject *parent = 0)
: QObject(parent)
{}
protected:
bool eventFilter(QObject *obj, QEvent *event) override
{
if (event->type() == QEvent::KeyPress) {
auto keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Down || keyEvent->key() == Qt::Key_Up) {
auto dialog = static_cast<ActionsDialog *>(parent());
dialog->focusSearchResults();
event->accept();
}
}
return QObject::eventFilter(obj, event);
}
};
// Include this so that ShortcutItemDelegate can be declared in the source file.
#include "actionsdialog.moc"
ActionsDialog::ActionsDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("Actions and Shortcuts"));
setSizeGripEnabled(true);
QVBoxLayout *vlayout = new QVBoxLayout();
// Search Bar
QHBoxLayout *searchLayout = new QHBoxLayout();
m_searchField = new QLineEdit(this);
m_searchField->setPlaceholderText(tr("search"));
m_searchField->installEventFilter(new SearchKeyPressFilter(this));
connect(m_searchField, &QLineEdit::textChanged, this, [&](const QString &text) {
if (m_proxyModel) {
m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_proxyModel->setFilterFixedString(text);
}
});
connect(m_searchField, &QLineEdit::returnPressed, this, &ActionsDialog::focusSearchResults);
searchLayout->addWidget(m_searchField);
QToolButton *clearSearchButton = new QToolButton(this);
clearSearchButton->setIcon(
QIcon::fromTheme("edit-clear", QIcon(":/icons/oxygen/32x32/actions/edit-clear.png")));
clearSearchButton->setMaximumSize(22, 22);
clearSearchButton->setToolTip(tr("Clear search"));
clearSearchButton->setAutoRaise(true);
connect(clearSearchButton, &QAbstractButton::clicked, m_searchField, &QLineEdit::clear);
searchLayout->addWidget(clearSearchButton);
vlayout->addLayout(searchLayout);
m_proxyModel = new QSortFilterProxyModel(this);
m_proxyModel->setSourceModel(&m_model);
m_proxyModel->setFilterKeyColumn(-1);
// List
m_table = new PrivateTreeView();
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
m_table->setItemsExpandable(false);
m_table->setRootIsDecorated(false);
m_table->setUniformRowHeights(true);
m_table->setSortingEnabled(true);
m_table->setEditTriggers(QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed);
m_table->setItemDelegateForColumn(1, new ShortcutItemDelegate(this));
m_table->setItemDelegateForColumn(2, new ShortcutItemDelegate(this));
m_table->setModel(m_proxyModel);
m_table->setWordWrap(false);
m_table->setSortingEnabled(true);
m_table->header()->setStretchLastSection(false);
m_table->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
m_table->header()->setSectionResizeMode(1, QHeaderView::QHeaderView::Fixed);
m_table->header()->setSectionResizeMode(2, QHeaderView::QHeaderView::Fixed);
m_table->header()->resizeSection(1, editorWidth);
m_table->header()->resizeSection(2, editorWidth);
m_table->sortByColumn(ActionsModel::COLUMN_ACTION, Qt::AscendingOrder);
m_table->installEventFilter(new KeyPressFilter(this));
connect(m_table->selectionModel(),
&QItemSelectionModel::selectionChanged,
this,
[&](const QItemSelection &selected, const QItemSelection &deselected) {
m_status->showText(tr("Click on the selected shortcut to show the editor"),
5,
nullptr,
QPalette::AlternateBase);
});
connect(m_table, &PrivateTreeView::editRejected, this, [&]() {
m_status->showText(tr("Reserved shortcuts can not be edited"),
5,
nullptr,
QPalette::AlternateBase);
});
connect(m_table->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
[&](const QModelIndex &current) {
if (current.column() == 0) {
m_table->setCurrentIndex(m_proxyModel->index(current.row(), 1));
}
});
vlayout->addWidget(m_table);
QHBoxLayout *hlayout = new QHBoxLayout();
m_status = new StatusLabelWidget();
connect(&m_model, &ActionsModel::editError, this, [&](const QString &message) {
m_status->showText(message, 5, nullptr);
});
hlayout->addWidget(m_status);
// Button Box
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
buttonBox->button(QDialogButtonBox::Close)->setAutoDefault(false);
connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
hlayout->addWidget(buttonBox);
vlayout->addLayout(hlayout);
setLayout(vlayout);
connect(m_table, &QAbstractItemView::activated, this, [&](const QModelIndex &index) {
auto action = m_model.action(m_proxyModel->mapToSource(index));
if (action && action->isEnabled()) {
action->trigger();
}
});
int tableWidth = 38;
for (int i = 0; i < m_table->model()->columnCount(); i++) {
tableWidth += m_table->columnWidth(i);
}
resize(tableWidth, 600);
}
void ActionsDialog::saveCurrentEditor()
{
auto delegate = static_cast<ShortcutItemDelegate *>(
m_table->itemDelegateForColumn(m_table->currentIndex().column()));
if (delegate) {
auto editor = delegate->currentEditor();
if (editor && editor->seqEdit) {
m_proxyModel->setData(m_table->currentIndex(), editor->seqEdit->keySequence());
emit delegate->closeEditor(editor);
}
}
}
void ActionsDialog::focusSearchResults()
{
m_table->setCurrentIndex(m_proxyModel->index(0, 1));
m_table->setFocus();
}
void ActionsDialog::hideEvent(QHideEvent *event)
{
Q_UNUSED(event)
saveCurrentEditor();
}
void ActionsDialog::showEvent(QShowEvent *event)
{
Q_UNUSED(event)
m_searchField->setFocus();
m_searchField->clear();
m_table->clearSelection();
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (c) 2022-2024 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 <http://www.gnu.org/licenses/>.
*/
#ifndef ACTIONSDIALOG_H
#define ACTIONSDIALOG_H
#include "models/actionsmodel.h"
#include <QDialog>
class PrivateTreeView;
class QLineEdit;
class QSortFilterProxyModel;
class StatusLabelWidget;
class QKeySequenceEdit;
class ActionsDialog : public QDialog
{
Q_OBJECT
public:
explicit ActionsDialog(QWidget *parent = 0);
void saveCurrentEditor();
public slots:
void focusSearchResults();
protected:
void hideEvent(QHideEvent *event);
void showEvent(QShowEvent *event);
private:
QLineEdit *m_searchField;
ActionsModel m_model;
PrivateTreeView *m_table;
QSortFilterProxyModel *m_proxyModel;
StatusLabelWidget *m_status;
};
#endif // ACTIONSDIALOG_H

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2012-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 <http://www.gnu.org/licenses/>.
*/
#include "addencodepresetdialog.h"
#include "ui_addencodepresetdialog.h"
AddEncodePresetDialog::AddEncodePresetDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::AddEncodePresetDialog)
{
ui->setupUi(this);
}
AddEncodePresetDialog::~AddEncodePresetDialog()
{
delete ui;
}
void AddEncodePresetDialog::setProperties(const QString &properties)
{
ui->propertiesEdit->setPlainText(properties);
}
QString AddEncodePresetDialog::presetName() const
{
return ui->nameEdit->text();
}
QString AddEncodePresetDialog::properties() const
{
const auto &extension = ui->extensionEdit->text();
if (!extension.isEmpty()) {
return ui->propertiesEdit->toPlainText() + "\nmeta.preset.extension=" + extension;
}
return ui->propertiesEdit->toPlainText();
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2012 Meltytech, LLC
* Author: Dan Dennedy <dan@dennedy.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef ADDENCODEPRESETDIALOG_H
#define ADDENCODEPRESETDIALOG_H
#include <QDialog>
namespace Ui {
class AddEncodePresetDialog;
}
class AddEncodePresetDialog : public QDialog
{
Q_OBJECT
public:
explicit AddEncodePresetDialog(QWidget *parent = 0);
~AddEncodePresetDialog();
void setProperties(const QString &);
QString presetName() const;
QString properties() const;
private:
Ui::AddEncodePresetDialog *ui;
};
#endif // ADDENCODEPRESETDIALOG_H

View File

@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddEncodePresetDialog</class>
<widget class="QDialog" name="AddEncodePresetDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>350</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="nameEdit"/>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>File name extension</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="extensionEdit">
<property name="placeholderText">
<string>for example, mp4</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QPlainTextEdit" name="propertiesEdit"/>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Make final changes to the preset including removing items you do not want to include, or copy/paste the clipboard.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>7</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>nameEdit</tabstop>
<tabstop>extensionEdit</tabstop>
<tabstop>propertiesEdit</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AddEncodePresetDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>363</x>
<y>278</y>
</hint>
<hint type="destinationlabel">
<x>355</x>
<y>-5</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AddEncodePresetDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>271</x>
<y>274</y>
</hint>
<hint type="destinationlabel">
<x>276</x>
<y>9</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,506 @@
/*
* Copyright (c) 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 <http://www.gnu.org/licenses/>.
*/
#include "alignaudiodialog.h"
#include "Logger.h"
#include "commands/timelinecommands.h"
#include "dialogs/alignmentarray.h"
#include "dialogs/longuitask.h"
#include "mainwindow.h"
#include "mltcontroller.h"
#include "models/multitrackmodel.h"
#include "proxymanager.h"
#include "qmltypes/qmlapplication.h"
#include "settings.h"
#include "shotcut_mlt_properties.h"
#include "util.h"
#include <QApplication>
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QIcon>
#include <QLabel>
#include <QLocale>
#include <QPainter>
#include <QPushButton>
#include <QStyledItemDelegate>
#include <QTreeView>
class AudioReader : public QObject
{
Q_OBJECT
public:
AudioReader(QString producerXml, AlignmentArray *array, int in = -1, int out = -1)
: QObject()
, m_producerXml(producerXml)
, m_array(array)
, m_in(in)
, m_out(out)
{}
void init(int maxLength) { m_array->init(maxLength); }
void process()
{
QScopedPointer<Mlt::Producer> producer(
new Mlt::Producer(MLT.profile(), "xml-string", m_producerXml.toUtf8().constData()));
if (m_in >= 0) {
producer->set_in_and_out(m_in, m_out);
}
size_t frameCount = producer->get_playtime();
std::vector<double> values(frameCount);
int progress = 0;
for (size_t i = 0; i < frameCount; ++i) {
int frequency = 48000;
int channels = 1;
mlt_audio_format format = mlt_audio_s16;
std::unique_ptr<Mlt::Frame> frame(producer->get_frame(i));
mlt_position position = mlt_frame_get_position(frame->get_frame());
int samples = mlt_audio_calculate_frame_samples(float(producer->get_fps()),
frequency,
position);
int16_t *data = static_cast<int16_t *>(
frame->get_audio(format, frequency, channels, samples));
double sampleTotal = 0;
// Add all values from the frame
for (int k = 0; k < samples; ++k) {
sampleTotal += std::abs(data[k]);
}
// Average the sample values
values[i] = sampleTotal / samples;
int newProgress = 100 * i / frameCount;
if (newProgress != progress) {
progress = newProgress;
emit progressUpdate(progress);
}
}
m_array->setValues(values);
}
signals:
void progressUpdate(int);
private:
QString m_producerXml;
AlignmentArray *m_array;
int m_in;
int m_out;
};
class ClipAudioReader : public QObject
{
Q_OBJECT
public:
ClipAudioReader(QString producerXml, AlignmentArray &referenceArray, int index, int in, int out)
: QObject()
, m_referenceArray(referenceArray)
, m_reader(producerXml, &m_clipArray, in, out)
, m_index(index)
{
connect(&m_reader, SIGNAL(progressUpdate(int)), this, SLOT(onReaderProgressUpdate(int)));
}
void init(int maxLength) { m_reader.init(maxLength); }
void start() { m_future = QtConcurrent::run(&ClipAudioReader::process, this); }
bool isFinished() { return m_future.isFinished(); }
void process()
{
onReaderProgressUpdate(0);
m_reader.process();
double speed = 1.0;
int offset = 0;
double quality;
double speedRange = Settings.audioReferenceSpeedRange();
if (speedRange != 0.0) {
quality = m_referenceArray.calculateOffsetAndSpeed(m_clipArray,
&speed,
&offset,
speedRange);
} else {
quality = m_referenceArray.calculateOffset(m_clipArray, &offset);
}
onReaderProgressUpdate(100);
emit finished(m_index, offset, speed, quality);
}
public slots:
void onReaderProgressUpdate(int progress)
{
progress = progress * 99 / 100; // Reader goes from 0-99
emit progressUpdate(m_index, progress);
}
signals:
void progressUpdate(int index, int percent);
void finished(int index, int offset, double speed, double quality);
private:
AlignmentArray m_clipArray;
AlignmentArray &m_referenceArray;
AudioReader m_reader;
int m_index;
QFuture<void> m_future;
bool m_calculateSpeed;
};
class AlignTableDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
const AlignClipsModel *model = dynamic_cast<const AlignClipsModel *>(index.model());
switch (index.column()) {
case AlignClipsModel::COLUMN_ERROR: {
QIcon icon;
if (!index.data().toString().isEmpty()) {
icon = QIcon(":/icons/oxygen/32x32/status/task-reject.png");
} else if (model->getProgress(index.row()) == 100) {
icon = QIcon(":/icons/oxygen/32x32/status/task-complete.png");
}
icon.paint(painter, option.rect, Qt::AlignCenter);
break;
}
case AlignClipsModel::COLUMN_NAME: {
int progress = model->getProgress(index.row());
if (progress > 0) {
QStyleOptionProgressBar progressBarOption;
progressBarOption.rect = option.rect;
progressBarOption.minimum = 0;
progressBarOption.maximum = 100;
progressBarOption.progress = progress;
QApplication::style()->drawControl(QStyle::CE_ProgressBar,
&progressBarOption,
painter);
}
painter->drawText(option.rect,
Qt::AlignLeft | Qt::AlignVCenter,
index.data().toString());
break;
}
case AlignClipsModel::COLUMN_OFFSET:
case AlignClipsModel::COLUMN_SPEED:
QStyledItemDelegate::paint(painter, option, index);
break;
default:
LOG_ERROR() << "Invalid Column" << index.row() << index.column();
break;
}
}
};
// Include this so that AlignTableDelegate can be declared in the source file.
#include "alignaudiodialog.moc"
AlignAudioDialog::AlignAudioDialog(QString title,
MultitrackModel *model,
const QVector<QUuid> &uuids,
QWidget *parent)
: QDialog(parent)
, m_model(model)
, m_uuids(uuids)
, m_uiTask(nullptr)
{
int row = 0;
setWindowTitle(title);
setWindowModality(QmlApplication::dialogModality());
QGridLayout *glayout = new QGridLayout();
glayout->setHorizontalSpacing(4);
glayout->setVerticalSpacing(2);
// Track Combo
glayout->addWidget(new QLabel(tr("Reference audio track")), row, 0, Qt::AlignRight);
m_trackCombo = new QComboBox();
int trackCount = m_model->trackList().size();
for (int i = 0; i < trackCount; i++) {
m_trackCombo->addItem(m_model->getTrackName(i), QVariant(i));
}
int defaultTrack = Settings.audioReferenceTrack();
if (defaultTrack < trackCount) {
m_trackCombo->setCurrentIndex(defaultTrack);
}
if (!connect(m_trackCombo,
QOverload<int>::of(&QComboBox::activated),
this,
&AlignAudioDialog::rebuildClipList))
connect(m_trackCombo, SIGNAL(activated(const QString &)), SLOT(rebuildClipList()));
glayout->addWidget(m_trackCombo, row++, 1, Qt::AlignLeft);
// Speed combo box
glayout->addWidget(new QLabel(tr("Speed adjustment range")), row, 0, Qt::AlignRight);
m_speedCombo = new QComboBox();
m_speedCombo->setToolTip("Larger speed adjustment ranges take longer to process.");
m_speedCombo->addItem(tr("None") + QStringLiteral(" (%L1%)").arg(0), QVariant(0));
m_speedCombo->addItem(tr("Narrow") + QStringLiteral(" (%L1%)").arg((double) 0.1, 0, 'g', 2),
QVariant(0.001));
m_speedCombo->addItem(tr("Normal") + QStringLiteral(" (%L1%)").arg((double) 0.5, 0, 'g', 2),
QVariant(0.005));
m_speedCombo->addItem(tr("Wide") + QStringLiteral(" (%L1%)").arg(1), QVariant(0.01));
m_speedCombo->addItem(tr("Very wide") + QStringLiteral(" (%L1%)").arg(5), QVariant(0.05));
double defaultRange = Settings.audioReferenceSpeedRange();
for (int i = 0; i < m_speedCombo->count(); i++) {
if (m_speedCombo->itemData(i).toDouble() == defaultRange) {
m_speedCombo->setCurrentIndex(i);
break;
}
}
if (!connect(m_speedCombo,
QOverload<int>::of(&QComboBox::activated),
this,
&AlignAudioDialog::rebuildClipList))
connect(m_speedCombo, SIGNAL(activated(const QString &)), SLOT(rebuildClipList()));
glayout->addWidget(m_speedCombo, row++, 1, Qt::AlignLeft);
// List
m_table = new QTreeView();
m_table->setSelectionMode(QAbstractItemView::NoSelection);
m_table->setItemsExpandable(false);
m_table->setRootIsDecorated(false);
m_table->setUniformRowHeights(true);
m_table->setSortingEnabled(false);
m_table->setModel(&m_alignClipsModel);
m_table->setWordWrap(false);
m_delegate = new AlignTableDelegate();
m_table->setItemDelegate(m_delegate);
m_table->header()->setStretchLastSection(false);
qreal rowHeight = fontMetrics().height() * devicePixelRatioF();
m_table->header()->setMinimumSectionSize(rowHeight);
m_table->header()->setSectionResizeMode(AlignClipsModel::COLUMN_ERROR, QHeaderView::Fixed);
m_table->setColumnWidth(AlignClipsModel::COLUMN_ERROR, rowHeight);
m_table->header()->setSectionResizeMode(AlignClipsModel::COLUMN_NAME, QHeaderView::Stretch);
m_table->header()->setSectionResizeMode(AlignClipsModel::COLUMN_OFFSET, QHeaderView::Fixed);
m_table->setColumnWidth(AlignClipsModel::COLUMN_OFFSET,
fontMetrics().horizontalAdvance("-00:00:00:00") * devicePixelRatioF()
+ 8);
m_table->header()->setSectionResizeMode(AlignClipsModel::COLUMN_SPEED,
QHeaderView::ResizeToContents);
glayout->addWidget(m_table, row++, 0, 1, 2);
// Button Box + cancel
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel);
connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
glayout->addWidget(m_buttonBox, row++, 0, 1, 2);
// Process button
QPushButton *processButton = m_buttonBox->addButton(tr("Process"), QDialogButtonBox::ActionRole);
connect(processButton, SIGNAL(pressed()), this, SLOT(process()));
// Apply button
m_applyButton = m_buttonBox->addButton(tr("Apply"), QDialogButtonBox::ApplyRole);
connect(m_applyButton, SIGNAL(pressed()), this, SLOT(apply()));
// Process and apply button
m_processAndApplyButton = m_buttonBox->addButton(tr("Process + Apply"),
QDialogButtonBox::AcceptRole);
connect(m_processAndApplyButton, SIGNAL(pressed()), this, SLOT(processAndApply()));
this->setLayout(glayout);
this->setModal(true);
rebuildClipList();
resize(500, 300);
}
AlignAudioDialog::~AlignAudioDialog()
{
delete m_delegate;
delete m_uiTask;
}
void AlignAudioDialog::rebuildClipList()
{
QStringList stringList;
m_alignClipsModel.clear();
int referenceIndex = m_trackCombo->currentData().toInt();
Settings.setAudioReferenceTrack(referenceIndex);
Settings.setAudioReferenceSpeedRange(m_speedCombo->currentData().toDouble());
m_applyButton->setEnabled(false);
for (const auto &uuid : m_uuids) {
int trackIndex, clipIndex;
auto info = m_model->findClipByUuid(uuid, trackIndex, clipIndex);
if (info && info->cut && info->cut->is_valid()) {
QString error;
QString clipName = info->producer->get(kShotcutCaptionProperty);
if (clipName.isNull() || clipName.isEmpty())
clipName = Util::baseName(ProxyManager::resource(*info->producer));
if (clipName == "<producer>" || clipName.isNull() || clipName.isEmpty())
clipName = QString::fromUtf8(info->producer->get("mlt_service"));
if (trackIndex == referenceIndex) {
error = tr("This clip will be skipped because it is on the reference track.");
} else {
// Only support avformat clips
QString shotcutProducer(info->producer->get(kShotcutProducerProperty));
QString service(info->producer->get("mlt_service"));
if (!service.startsWith("avformat") && !shotcutProducer.startsWith("avformat"))
error = tr("This item can not be aligned.");
}
m_alignClipsModel.addClip(clipName,
AlignClipsModel::INVALID_OFFSET,
AlignClipsModel::INVALID_OFFSET,
error);
}
}
}
void AlignAudioDialog::process()
{
m_uiTask = new LongUiTask(tr("Align Audio"));
m_uiTask->setMinimumDuration(0);
int referenceTrackIndex = m_trackCombo->currentData().toInt();
auto mlt_index = m_model->trackList().at(referenceTrackIndex).mlt_index;
QScopedPointer<Mlt::Producer> track(m_model->tractor()->track(mlt_index));
int maxLength = track->get_playtime();
bool validClip = false;
QString xml = MLT.XML(track.data());
AlignmentArray trackArray;
AudioReader trackReader(MLT.XML(track.data()), &trackArray);
connect(&trackReader, SIGNAL(progressUpdate(int)), this, SLOT(updateReferenceProgress(int)));
QList<ClipAudioReader *> m_clipReaders;
for (const auto &uuid : m_uuids) {
int trackIndex, clipIndex;
auto info = m_model->findClipByUuid(uuid, trackIndex, clipIndex);
if (!info || !info->cut || !info->cut->is_valid()) {
continue;
}
QString shotcutProducer(info->producer->get(kShotcutProducerProperty));
QString service(info->producer->get("mlt_service"));
if (!service.startsWith("avformat") && !shotcutProducer.startsWith("avformat")) {
m_clipReaders.append(nullptr);
} else if (trackIndex == referenceTrackIndex) {
m_clipReaders.append(nullptr);
} else {
QString xml = MLT.XML(info->cut);
ClipAudioReader *clipReader = new ClipAudioReader(xml,
trackArray,
m_clipReaders.size(),
info->frame_in,
info->frame_out);
connect(clipReader,
SIGNAL(progressUpdate(int, int)),
this,
SLOT(updateClipProgress(int, int)));
connect(clipReader,
SIGNAL(finished(int, int, double, double)),
this,
SLOT(clipFinished(int, int, double, double)));
m_clipReaders.append(clipReader);
maxLength = qMax(maxLength, info->frame_count);
validClip = true;
}
}
if (!validClip) {
m_uiTask->deleteLater();
m_uiTask = nullptr;
return;
}
trackReader.init(maxLength);
for (const auto &clipReader : m_clipReaders) {
if (clipReader)
clipReader->init(maxLength);
}
trackReader.process();
for (const auto &clipReader : m_clipReaders) {
if (clipReader)
clipReader->start();
}
for (const auto &clipReader : m_clipReaders) {
if (clipReader) {
while (!clipReader->isFinished()) {
QThread::msleep(10);
QCoreApplication::processEvents();
}
clipReader->deleteLater();
}
}
m_uiTask->deleteLater();
m_uiTask = nullptr;
m_applyButton->setEnabled(true);
}
void AlignAudioDialog::apply()
{
Timeline::AlignClipsCommand *command = new Timeline::AlignClipsCommand(*m_model);
int referenceTrackIndex = m_trackCombo->currentData().toInt();
int alignmentCount = 0;
int modelIndex = 0;
for (const auto &uuid : m_uuids) {
int trackIndex, clipIndex;
auto info = m_model->findClipByUuid(uuid, trackIndex, clipIndex);
if (!info || !info->cut || !info->cut->is_valid()) {
continue;
}
if (trackIndex != referenceTrackIndex) {
int offset = m_alignClipsModel.getOffset(modelIndex);
if (offset != AlignClipsModel::INVALID_OFFSET) {
double speedCompensation = m_alignClipsModel.getSpeed(modelIndex);
command->addAlignment(uuid, offset, speedCompensation);
alignmentCount++;
}
}
modelIndex++;
}
if (alignmentCount > 0) {
MAIN.undoStack()->push(command);
} else {
delete command;
}
accept();
}
void AlignAudioDialog::processAndApply()
{
process();
apply();
}
void AlignAudioDialog::updateReferenceProgress(int percent)
{
if (m_uiTask) {
m_uiTask->reportProgress(tr("Analyze Reference Track"), percent, 100);
}
}
void AlignAudioDialog::updateClipProgress(int index, int percent)
{
m_alignClipsModel.updateProgress(index, percent);
if (m_uiTask) {
m_uiTask->reportProgress(tr("Analyze Clips"), 0, 0);
}
}
void AlignAudioDialog::clipFinished(int index, int offset, double speed, double quality)
{
QString error;
LOG_INFO() << "Clip" << index << "Offset:" << offset << "Speed:" << speed
<< "Quality:" << quality;
if (quality < 0.01) {
error = tr("Alignment not found.");
offset = AlignClipsModel::INVALID_OFFSET;
speed = AlignClipsModel::INVALID_OFFSET;
}
m_alignClipsModel.updateOffsetAndSpeed(index, offset, speed, error);
m_alignClipsModel.updateProgress(index, 100);
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (c) 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 <http://www.gnu.org/licenses/>.
*/
#ifndef ALIGNAUDIODIALOG_H
#define ALIGNAUDIODIALOG_H
#include "models/alignclipsmodel.h"
#include <QDialog>
#include <QUuid>
class QComboBox;
class QDialogButtonBox;
class QLabel;
class QListWidget;
class QLineEdit;
class QPushButton;
class QTreeView;
class AlignTableDelegate;
class MultitrackModel;
class LongUiTask;
class AlignAudioDialog : public QDialog
{
Q_OBJECT
public:
explicit AlignAudioDialog(QString title,
MultitrackModel *model,
const QVector<QUuid> &uuids,
QWidget *parent = 0);
virtual ~AlignAudioDialog();
private slots:
void rebuildClipList();
void process();
void apply();
void processAndApply();
void updateReferenceProgress(int percent);
void updateClipProgress(int index, int percent);
void clipFinished(int index, int offset, double speed, double quality);
private:
AlignTableDelegate *m_delegate;
MultitrackModel *m_model;
AlignClipsModel m_alignClipsModel;
QVector<QUuid> m_uuids;
QComboBox *m_trackCombo;
QComboBox *m_speedCombo;
QTreeView *m_table;
QDialogButtonBox *m_buttonBox;
QPushButton *m_applyButton;
QPushButton *m_processAndApplyButton;
LongUiTask *m_uiTask;
};
#endif // ALIGNAUDIODIALOG_H

View File

@@ -0,0 +1,227 @@
/*
* Copyright (c) 2022 Meltytech, LLC
*
* Author: André Caldas de Souza <andrecaldas@unb.br>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "alignmentarray.h"
#include <QDebug>
#include <QMutexLocker>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
#include <numeric>
// FFTW plan functions are not threadsafe
static QMutex s_fftwPlanningMutex;
AlignmentArray::AlignmentArray()
: m_forwardBuf(nullptr)
, m_backwardBuf(nullptr)
, m_autocorrelationMax(std::numeric_limits<double>::min())
, m_isTransformed(false)
{}
AlignmentArray::AlignmentArray(size_t minimum_size)
: AlignmentArray()
{
init(minimum_size);
}
AlignmentArray::~AlignmentArray()
{
QMutexLocker locker(&s_fftwPlanningMutex);
if (m_forwardBuf) {
fftw_free(reinterpret_cast<fftw_complex *>(m_forwardBuf));
fftw_destroy_plan(m_forwardPlan);
fftw_free(reinterpret_cast<fftw_complex *>(m_backwardBuf));
fftw_destroy_plan(m_backwardPlan);
}
}
void AlignmentArray::init(size_t minimumSize)
{
QMutexLocker locker(&m_transformMutex);
m_minimumSize = minimumSize;
m_actualComplexSize = (minimumSize * 2) - 1;
if (m_forwardBuf) {
QMutexLocker locker(&s_fftwPlanningMutex);
fftw_free(reinterpret_cast<fftw_complex *>(m_forwardBuf));
m_forwardBuf = nullptr;
fftw_destroy_plan(m_forwardPlan);
fftw_free(reinterpret_cast<fftw_complex *>(m_backwardBuf));
m_backwardBuf = nullptr;
fftw_destroy_plan(m_backwardPlan);
}
}
void AlignmentArray::setValues(const std::vector<double> &values)
{
QMutexLocker locker(&m_transformMutex);
m_values = values;
m_isTransformed = false;
}
double AlignmentArray::calculateOffset(AlignmentArray &from, int *offset)
{
// Create a destination for the correlation values
s_fftwPlanningMutex.lock();
fftw_complex *buf = fftw_alloc_complex(m_actualComplexSize);
std::complex<double> *correlationBuf = reinterpret_cast<std::complex<double> *>(buf);
fftw_plan correlationPlan
= fftw_plan_dft_1d(m_actualComplexSize, buf, buf, FFTW_BACKWARD, FFTW_ESTIMATE);
std::fill(correlationBuf, correlationBuf + m_actualComplexSize, std::complex<double>(0));
s_fftwPlanningMutex.unlock();
// Ensure the two sequences are transformed
transform();
from.transform();
// Calculate the cross-correlation signal
for (size_t i = 0; i < m_actualComplexSize; ++i) {
correlationBuf[i] = m_forwardBuf[i] * std::conj(from.m_forwardBuf[i]);
}
// Convert to time series
fftw_execute(correlationPlan);
// Find the maximum correlation offset
double max = 0;
for (size_t i = 0; i < m_actualComplexSize; ++i) {
double norm = std::norm(correlationBuf[i]);
if (max < norm) {
*offset = i;
max = norm;
}
}
if (2 * *offset > (int) m_actualComplexSize) {
*offset -= ((int) m_actualComplexSize);
}
s_fftwPlanningMutex.lock();
fftw_free(correlationBuf);
fftw_destroy_plan(correlationPlan);
s_fftwPlanningMutex.unlock();
// Normalize the best score by dividing by the max autocorrelation of the two signals
// (Pearson's correlation coefficient)
double correlationCoefficient = sqrt(m_autocorrelationMax) * sqrt(from.m_autocorrelationMax);
return max / correlationCoefficient;
}
double AlignmentArray::calculateOffsetAndSpeed(AlignmentArray &from,
double *speed,
int *offset,
double speedRange)
{
// The minimum speed step results in one frame of stretch.
// Do not try to compensate for more than 1 frame of speed difference.
double minimumSpeedStep = 1.0 / (double) from.m_values.size();
double speedStep = 0.0005;
double bestSpeed = 1.0;
int bestOffset = 0;
double bestScore = calculateOffset(from, &bestOffset);
AlignmentArray stretched(m_minimumSize);
double speedMin = bestSpeed - speedRange;
double speedMax = bestSpeed + speedRange;
while (speedStep > (minimumSpeedStep / 10)) {
for (double s = speedMin; s <= speedMax; s += speedStep) {
if (s == bestSpeed) {
continue;
}
// Stretch the original values to simulate a speed compensation
double factor = 1.0 / s;
size_t stretchedSize = std::floor((double) from.m_values.size() * factor);
std::vector<double> strechedValues(stretchedSize);
// Nearest neighbor interpolation
for (size_t i = 0; i < stretchedSize; i++) {
size_t srcIndex = std::round(s * i);
strechedValues[i] = from.m_values[srcIndex];
}
stretched.setValues(strechedValues);
double score = calculateOffset(stretched, offset);
if (score > bestScore) {
bestScore = score;
bestSpeed = s;
bestOffset = *offset;
}
}
speedStep /= 10;
speedMin = bestSpeed - (speedStep * 5);
speedMax = bestSpeed + (speedStep * 5);
}
*speed = bestSpeed;
*offset = bestOffset;
return bestScore;
}
void AlignmentArray::transform()
{
QMutexLocker locker(&m_transformMutex);
if (!m_isTransformed) {
if (!m_forwardBuf) {
// Create the plans while the global planning mutex is locked
s_fftwPlanningMutex.lock();
fftw_complex *buf = nullptr;
// Allocate the forward buffer and plan
buf = fftw_alloc_complex(m_actualComplexSize);
m_forwardBuf = reinterpret_cast<std::complex<double> *>(buf);
m_forwardPlan
= fftw_plan_dft_1d(m_actualComplexSize, buf, buf, FFTW_FORWARD, FFTW_ESTIMATE);
// Allocate the backward buffer and plan
buf = fftw_alloc_complex(m_actualComplexSize);
m_backwardBuf = reinterpret_cast<std::complex<double> *>(buf);
m_backwardPlan
= fftw_plan_dft_1d(m_actualComplexSize, buf, buf, FFTW_BACKWARD, FFTW_ESTIMATE);
s_fftwPlanningMutex.unlock();
}
std::fill(m_forwardBuf, m_forwardBuf + m_actualComplexSize, std::complex<double>(0));
std::fill(m_backwardBuf, m_backwardBuf + m_actualComplexSize, std::complex<double>(0));
// Calculate the mean and standard deviation to be used to normalize the values.
double accum = 0.0;
std::for_each(m_values.begin(), m_values.end(), [&](const double d) { accum += d; });
double mean = accum / m_values.size();
accum = 0;
std::for_each(m_values.begin(), m_values.end(), [&](const double d) {
accum += (d - mean) * (d - mean);
});
double stddev = sqrt(accum / (m_values.size() - 1));
// Fill the transform array
// Normalize the input values: Subtract the mean and divide by the standard deviation.
for (size_t i = 0; i < m_values.size(); i++) {
m_forwardBuf[i] = (m_values[i] - mean) / stddev;
}
// Perform the forward DFT
fftw_execute(m_forwardPlan);
// Perform autocorrelation to calculate the maximum correlation value
for (size_t i = 0; i < m_actualComplexSize; i++) {
m_backwardBuf[i] = m_forwardBuf[i] * std::conj(m_forwardBuf[i]);
}
// Convert back to time series
fftw_execute(m_backwardPlan);
// Find the maximum autocorrelation value
for (size_t i = 0; i < m_actualComplexSize; i++) {
double norm = std::norm(m_backwardBuf[i]);
if (norm > m_autocorrelationMax)
m_autocorrelationMax = norm;
}
m_isTransformed = true;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2022 Meltytech, LLC
*
* Author: André Caldas de Souza <andrecaldas@unb.br>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef ALIGNMENTARRAY_H
#define ALIGNMENTARRAY_H
#include <QMutex>
#include <complex>
#include <fftw3.h>
#include <vector>
class AlignmentArray
{
public:
AlignmentArray();
AlignmentArray(size_t minimum_size);
virtual ~AlignmentArray();
void init(size_t minimum_size);
void setValues(const std::vector<double> &values);
double calculateOffset(AlignmentArray &from, int *offset);
double calculateOffsetAndSpeed(AlignmentArray &from,
double *speed,
int *offset,
double speedRange);
private:
void transform();
std::vector<double> m_values;
fftw_plan m_forwardPlan;
std::complex<double> *m_forwardBuf;
fftw_plan m_backwardPlan;
std::complex<double> *m_backwardBuf;
double m_autocorrelationMax;
size_t m_minimumSize;
size_t m_actualComplexSize;
bool m_isTransformed;
QMutex m_transformMutex;
};
#endif

View File

@@ -0,0 +1,186 @@
/*
* Copyright (c) 2023-2024 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 <http://www.gnu.org/licenses/>.
*/
#include "bitratedialog.h"
#include "dialogs/saveimagedialog.h"
#include "settings.h"
#include <QDialogButtonBox>
#include <QJsonObject>
#include <QPushButton>
#include <QQueue>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QtCharts/QBarCategoryAxis>
#include <QtCharts/QBarSet>
#include <QtCharts/QChartView>
#include <QtCharts/QLegend>
#include <QtCharts/QLineSeries>
#include <QtCharts/QSplineSeries>
#include <QtCharts/QStackedBarSeries>
#include <QtCharts/QValueAxis>
static const auto kSlidingWindowSize = 30;
BitrateDialog::BitrateDialog(const QString &resource,
double fps,
const QJsonArray &data,
QWidget *parent)
: QDialog(parent)
{
setMinimumSize(400, 200);
setModal(true);
setWindowTitle(tr("Bitrate Viewer"));
setSizeGripEnabled(true);
double time = 0.0;
double maxSize = 0.0;
double firstTime = 0.0;
double keySubtotal = 0.0;
double interSubtotal = 0.0;
double totalKbps = 0.0;
double minKbps = std::numeric_limits<double>().max();
double maxKbps = 0.0;
int periodCount = 0;
double previousSecond = 0.0;
QQueue<double> window;
auto barSeries = new QStackedBarSeries;
auto averageLine = new QSplineSeries;
QBarSet *interSet = nullptr;
auto keySet = new QBarSet(fps > 0.0 ? "I" : tr("Audio"));
barSeries->setBarWidth(1.0);
if (fps > 0.0) {
interSet = new QBarSet("P/B");
barSeries->append(interSet);
}
barSeries->append(keySet);
averageLine->setName(tr("Average"));
for (int i = 0; i < data.size(); ++i) {
auto o = data[i].toObject();
auto pts = o["pts_time"].toString().toDouble();
auto duration = o["duration_time"].toString().toDouble();
auto size = o["size"].toString().toDouble() * 8.0 / 1000.0; // Kb
if (pts > 0.0)
time = pts + qMax(0.0, duration);
if (i == 0)
firstTime = time;
time -= firstTime;
if (o["flags"].toString()[0] == 'K') {
keySubtotal += size;
} else {
interSubtotal += size;
}
totalKbps += size;
// Every second as the period
if (time >= (previousSecond + 1.0) || (i + 1) == data.size()) {
// For the min, max, and overall average
auto kbps = interSubtotal + keySubtotal;
if (kbps < minKbps)
minKbps = kbps;
if (kbps > maxKbps)
maxKbps = kbps;
// Add a bar to the graph for each period
int n = qMax(1, int(time - previousSecond));
for (int j = 0; j < n; ++j) {
if (interSet)
interSet->append(interSubtotal);
keySet->append(keySubtotal);
++periodCount;
}
// For the smoothed average
while (window.size() >= kSlidingWindowSize)
window.dequeue();
window.enqueue(kbps);
double sum = 0.0;
for (auto &v : window)
sum += v;
// subtract 0.5 from the time because the X axis tick marks are centered
// under the bar such that "0s" is actually at 0.5s
averageLine->append(time - 0.5, sum / window.size());
// Reset counters
interSubtotal = 0.0;
keySubtotal = 0.0;
previousSecond = std::floor(time);
}
}
auto chart = new QChart();
chart->addSeries(barSeries);
chart->addSeries(averageLine);
chart->setTheme(Settings.theme() == "dark" ? QChart::ChartThemeDark : QChart::ChartThemeLight);
averageLine->setColor(Qt::yellow);
chart->setTitle(tr("Bitrates for %1 ~~ Avg. %2 Min. %3 Max. %4 Kb/s")
.arg(resource)
.arg(qRound(totalKbps / time))
.arg(qRound(minKbps))
.arg(qRound(maxKbps)));
auto axisX = new QValueAxis();
chart->addAxis(axisX, Qt::AlignBottom);
barSeries->attachAxis(axisX);
averageLine->attachAxis(axisX);
axisX->setRange(0.0, time);
axisX->setLabelFormat("%.0f s");
axisX->setTickType(QValueAxis::TicksDynamic);
axisX->setTickInterval(periodCount > 100 ? 10.0 : 5.0);
QValueAxis *axisY = new QValueAxis();
chart->addAxis(axisY, Qt::AlignLeft);
barSeries->attachAxis(axisY);
averageLine->attachAxis(axisY);
axisY->setRange(0.0, maxKbps);
axisY->setLabelFormat("%.0f Kb/s");
chart->legend()->setVisible(true);
chart->legend()->setAlignment(Qt::AlignBottom);
QChartView *chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
auto layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 8, 8);
layout->setSpacing(8);
auto scrollArea = new QScrollArea(this);
scrollArea->setWidget(chartView);
layout->addWidget(scrollArea);
auto buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Close, this);
buttons->button(QDialogButtonBox::Close)->setDefault(true);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, [=] {
QImage image(chartView->size(), QImage::Format_RGB32);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing);
chartView->render(&painter);
painter.end();
SaveImageDialog(this, tr("Save Bitrate Graph"), image).exec();
});
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
chartView->setMinimumWidth(qMax(1010, periodCount * 5));
chartView->setMinimumHeight(520);
resize(1024, 576);
show();
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2023 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 <http://www.gnu.org/licenses/>.
*/
#ifndef BITRATEDIALOG_H
#define BITRATEDIALOG_H
#include <QDialog>
#include <QJsonArray>
#include <QString>
class BitrateDialog : public QDialog
{
Q_OBJECT
public:
explicit BitrateDialog(const QString &resource,
double fps,
const QJsonArray &data,
QWidget *parent = nullptr);
};
#endif // BITRATEDIALOG_H

View File

@@ -0,0 +1,171 @@
/*
* Copyright (c) 2013-2025 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 <http://www.gnu.org/licenses/>.
*/
#include "customprofiledialog.h"
#include "ui_customprofiledialog.h"
#include "mltcontroller.h"
#include "settings.h"
#include "util.h"
#include <QDesktopServices>
#include <QDir>
#include <QRegularExpression>
CustomProfileDialog::CustomProfileDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::CustomProfileDialog)
, m_fps(0.0)
{
ui->setupUi(this);
ui->widthSpinner->setValue(MLT.profile().width());
ui->heightSpinner->setValue(MLT.profile().height());
ui->aspectNumSpinner->setValue(MLT.profile().display_aspect_num());
ui->aspectDenSpinner->setValue(MLT.profile().display_aspect_den());
ui->fpsSpinner->setValue(MLT.profile().fps());
ui->scanModeCombo->setCurrentIndex(MLT.profile().progressive());
switch (MLT.profile().colorspace()) {
case 601:
ui->colorspaceCombo->setCurrentIndex(0);
break;
case 2020:
ui->colorspaceCombo->setCurrentIndex(2);
break;
default:
ui->colorspaceCombo->setCurrentIndex(1);
break;
}
}
CustomProfileDialog::~CustomProfileDialog()
{
delete ui;
}
QString CustomProfileDialog::profileName() const
{
// Replace characters that are not allowed in Windows file names
QString filename = ui->nameEdit->text();
static QRegularExpression re("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]");
filename = filename.replace(re, QStringLiteral("_"));
return filename;
}
void CustomProfileDialog::on_buttonBox_accepted()
{
MLT.profile().set_explicit(1);
MLT.profile().set_width(ui->widthSpinner->value());
MLT.profile().set_height(ui->heightSpinner->value());
MLT.profile().set_display_aspect(ui->aspectNumSpinner->value(), ui->aspectDenSpinner->value());
QSize sar(ui->aspectNumSpinner->value() * ui->heightSpinner->value(),
ui->aspectDenSpinner->value() * ui->widthSpinner->value());
auto gcd = Util::greatestCommonDivisor(sar.width(), sar.height());
MLT.profile().set_sample_aspect(sar.width() / gcd, sar.height() / gcd);
int numerator, denominator;
Util::normalizeFrameRate(ui->fpsSpinner->value(), numerator, denominator);
MLT.profile().set_frame_rate(numerator, denominator);
MLT.profile().set_progressive(ui->scanModeCombo->currentIndex());
switch (ui->colorspaceCombo->currentIndex()) {
case 0:
MLT.profile().set_colorspace(601);
break;
case 2:
MLT.profile().set_colorspace(2020);
break;
default:
MLT.profile().set_colorspace(709);
break;
}
MLT.updatePreviewProfile();
MLT.setPreviewScale(Settings.playerPreviewScale());
// Save it to a file
if (!ui->nameEdit->text().isEmpty()) {
QDir dir(Settings.appDataLocation());
QString subdir("profiles");
if (!dir.exists())
dir.mkpath(dir.path());
if (!dir.cd(subdir)) {
if (dir.mkdir(subdir))
dir.cd(subdir);
}
Mlt::Properties p;
p.set("width", MLT.profile().width());
p.set("height", MLT.profile().height());
p.set("sample_aspect_num", MLT.profile().sample_aspect_num());
p.set("sample_aspect_den", MLT.profile().sample_aspect_den());
p.set("display_aspect_num", MLT.profile().display_aspect_num());
p.set("display_aspect_den", MLT.profile().display_aspect_den());
p.set("progressive", MLT.profile().progressive());
p.set("colorspace", MLT.profile().colorspace());
p.set("frame_rate_num", MLT.profile().frame_rate_num());
p.set("frame_rate_den", MLT.profile().frame_rate_den());
p.save(dir.filePath(profileName()).toUtf8().constData());
}
}
void CustomProfileDialog::on_widthSpinner_editingFinished()
{
ui->widthSpinner->setValue(Util::coerceMultiple(ui->widthSpinner->value()));
}
void CustomProfileDialog::on_heightSpinner_editingFinished()
{
ui->heightSpinner->setValue(Util::coerceMultiple(ui->heightSpinner->value()));
}
void CustomProfileDialog::on_fpsSpinner_editingFinished()
{
if (ui->fpsSpinner->value() != m_fps) {
const QString caption(tr("Video Mode Frames/sec"));
if (ui->fpsSpinner->value() == 23.98 || ui->fpsSpinner->value() == 23.976) {
Util::showFrameRateDialog(caption, 24000, ui->fpsSpinner, this);
} else if (ui->fpsSpinner->value() == 29.97) {
Util::showFrameRateDialog(caption, 30000, ui->fpsSpinner, this);
} else if (ui->fpsSpinner->value() == 47.95) {
Util::showFrameRateDialog(caption, 48000, ui->fpsSpinner, this);
} else if (ui->fpsSpinner->value() == 59.94) {
Util::showFrameRateDialog(caption, 60000, ui->fpsSpinner, this);
}
m_fps = ui->fpsSpinner->value();
}
}
void CustomProfileDialog::on_fpsComboBox_textActivated(const QString &arg1)
{
if (arg1.isEmpty())
return;
ui->fpsSpinner->setValue(arg1.toDouble());
}
void CustomProfileDialog::on_resolutionComboBox_textActivated(const QString &arg1)
{
if (arg1.isEmpty())
return;
auto parts = arg1.split(' ');
ui->widthSpinner->setValue(parts[0].toInt());
ui->heightSpinner->setValue(parts[2].toInt());
}
void CustomProfileDialog::on_aspectRatioComboBox_textActivated(const QString &arg1)
{
if (arg1.isEmpty())
return;
auto parts = arg1.split(' ')[0].split(':');
ui->aspectNumSpinner->setValue(parts[0].toInt());
ui->aspectDenSpinner->setValue(parts[1].toInt());
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2013-2023 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 <http://www.gnu.org/licenses/>.
*/
#ifndef CUSTOMPROFILEDIALOG_H
#define CUSTOMPROFILEDIALOG_H
#include <QDialog>
namespace Ui {
class CustomProfileDialog;
}
class CustomProfileDialog : public QDialog
{
Q_OBJECT
public:
explicit CustomProfileDialog(QWidget *parent = 0);
~CustomProfileDialog();
QString profileName() const;
private slots:
void on_buttonBox_accepted();
void on_widthSpinner_editingFinished();
void on_heightSpinner_editingFinished();
void on_fpsSpinner_editingFinished();
void on_fpsComboBox_textActivated(const QString &arg1);
void on_resolutionComboBox_textActivated(const QString &arg1);
void on_aspectRatioComboBox_textActivated(const QString &arg1);
private:
Ui::CustomProfileDialog *ui;
double m_fps;
};
#endif // CUSTOMPROFILEDIALOG_H

View File

@@ -0,0 +1,558 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CustomProfileDialog</class>
<widget class="QDialog" name="CustomProfileDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>496</width>
<height>376</height>
</rect>
</property>
<property name="windowTitle">
<string>Add Custom Video Mode</string>
</property>
<property name="whatsThis">
<string notr="true">https://forum.shotcut.org/t/settings-video-mode/12790#p-40331-custom-3</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QGridLayout" name="gridLayout_2">
<property name="verticalSpacing">
<number>3</number>
</property>
<item row="7" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Colorspace</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>colorspaceCombo</cstring>
</property>
</widget>
</item>
<item row="7" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QComboBox" name="colorspaceCombo">
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string notr="true">ITU-R BT.601</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">ITU-R BT.709</string>
</property>
</item>
<item>
<property name="text">
<string>ITU-R BT.2020</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>Resolution</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>widthSpinner</cstring>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QLineEdit" name="nameEdit"/>
</item>
<item row="3" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QSpinBox" name="aspectNumSpinner">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>8640</number>
</property>
<property name="value">
<number>16</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_16">
<property name="text">
<string>:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="aspectDenSpinner">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>8640</number>
</property>
<property name="value">
<number>9</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="aspectRatioComboBox">
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<item>
<property name="text">
<string notr="true">16:9 (wide)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">4:3 (SD)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">9:16 (vertical)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">1:1 (square)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">1.90:1 (DCI)</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_8">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>nameEdit</cstring>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QComboBox" name="scanModeCombo">
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>Interlaced</string>
</property>
</item>
<item>
<property name="text">
<string>Progressive</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>Aspect ratio</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>aspectNumSpinner</cstring>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QSpinBox" name="widthSpinner">
<property name="minimum">
<number>16</number>
</property>
<property name="maximum">
<number>8640</number>
</property>
<property name="singleStep">
<number>2</number>
</property>
<property name="value">
<number>1280</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>x</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="heightSpinner">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>8640</number>
</property>
<property name="singleStep">
<number>2</number>
</property>
<property name="value">
<number>720</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="resolutionComboBox">
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<item>
<property name="text">
<string notr="true">1280 x 720 (HD)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">1920 x 1080 (FHD)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">2048 x 1080 (DCI 2K)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">2560 x 1440 (QHD 2K)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">2704 x 1520 (2.7K)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">3840 x 2160 (UHD 4K)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">4096 x 2160 (DCI 4K)</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">5120 x 2880 (5K)</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Frames/sec</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>fpsSpinner</cstring>
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_16">
<item>
<widget class="QDoubleSpinBox" name="fpsSpinner">
<property name="decimals">
<number>6</number>
</property>
<property name="minimum">
<double>1.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
<property name="value">
<double>25.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="fpsComboBox">
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<item>
<property name="text">
<string notr="true">23.976024</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">24</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">25</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">29.970030</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">30</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">48</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">50</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">59.940060</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">60</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_16">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Scan mode</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>scanModeCombo</cstring>
</property>
</widget>
</item>
<item row="10" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0" colspan="3">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;small&gt;(Leave Name blank to skip saving a preset and use a temporary or project-specific Video Mode.)&lt;/small&gt;</string>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>nameEdit</tabstop>
<tabstop>widthSpinner</tabstop>
<tabstop>heightSpinner</tabstop>
<tabstop>aspectNumSpinner</tabstop>
<tabstop>aspectDenSpinner</tabstop>
<tabstop>fpsSpinner</tabstop>
<tabstop>scanModeCombo</tabstop>
<tabstop>colorspaceCombo</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CustomProfileDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>251</x>
<y>234</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>240</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CustomProfileDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>251</x>
<y>234</y>
</hint>
<hint type="destinationlabel">
<x>257</x>
<y>240</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2012-2025 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 <http://www.gnu.org/licenses/>.
*/
#include "durationdialog.h"
#include "mltcontroller.h"
#include "ui_durationdialog.h"
DurationDialog::DurationDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::DurationDialog)
{
ui->setupUi(this);
ui->spinBox->setMaximum(MLT.maxFrameCount());
connect(ui->spinBox, &TimeSpinBox::accepted, this, &QDialog::accept);
}
DurationDialog::~DurationDialog()
{
delete ui;
}
void DurationDialog::setDuration(int frames)
{
ui->spinBox->setValue(frames);
}
int DurationDialog::duration() const
{
return ui->spinBox->value();
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2012-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 <http://www.gnu.org/licenses/>.
*/
#ifndef DURATIONDIALOG_H
#define DURATIONDIALOG_H
#include <QDialog>
namespace Ui {
class DurationDialog;
}
class DurationDialog : public QDialog
{
Q_OBJECT
public:
explicit DurationDialog(QWidget *parent = 0);
~DurationDialog();
void setDuration(int);
int duration() const;
private:
Ui::DurationDialog *ui;
};
#endif // DURATIONDIALOG_H

View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DurationDialog</class>
<widget class="QDialog" name="DurationDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>209</width>
<height>101</height>
</rect>
</property>
<property name="windowTitle">
<string>Set Duration</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Duration</string>
</property>
</widget>
</item>
<item>
<widget class="TimeSpinBox" name="spinBox">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>2147483647</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>3</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>TimeSpinBox</class>
<extends>QSpinBox</extends>
<header>widgets/timespinbox.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>DurationDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>DurationDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 2021 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 <http://www.gnu.org/licenses/>.
*/
#include "editmarkerdialog.h"
#include "Logger.h"
#include "widgets/editmarkerwidget.h"
#include <QDebug>
#include <QDialogButtonBox>
#include <QVBoxLayout>
EditMarkerDialog::EditMarkerDialog(
QWidget *parent, const QString &text, const QColor &color, int start, int end, int maxEnd)
: QDialog(parent)
{
setWindowTitle(tr("Edit Marker"));
QVBoxLayout *VLayout = new QVBoxLayout(this);
m_sWidget = new EditMarkerWidget(this, text, color, start, end, maxEnd);
VLayout->addWidget(m_sWidget);
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
VLayout->addWidget(m_buttonBox);
connect(m_buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(clicked(QAbstractButton *)));
setLayout(VLayout);
setModal(true);
layout()->setSizeConstraint(QLayout::SetFixedSize);
}
QString EditMarkerDialog::getText()
{
return m_sWidget->getText();
}
QColor EditMarkerDialog::getColor()
{
return m_sWidget->getColor();
}
int EditMarkerDialog::getStart()
{
return m_sWidget->getStart();
}
int EditMarkerDialog::getEnd()
{
return m_sWidget->getEnd();
}
void EditMarkerDialog::clicked(QAbstractButton *button)
{
QDialogButtonBox::ButtonRole role = m_buttonBox->buttonRole(button);
if (role == QDialogButtonBox::AcceptRole) {
accept();
} else if (role == QDialogButtonBox::RejectRole) {
reject();
} else {
LOG_DEBUG() << "Unknown role" << role;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2021 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 <http://www.gnu.org/licenses/>.
*/
#ifndef EDITMARKERDIALOG_H
#define EDITMARKERDIALOG_H
#include <QDialog>
class EditMarkerWidget;
class QAbstractButton;
class QDialogButtonBox;
class EditMarkerDialog : public QDialog
{
Q_OBJECT
public:
explicit EditMarkerDialog(
QWidget *parent, const QString &text, const QColor &color, int start, int end, int maxEnd);
QString getText();
QColor getColor();
int getStart();
int getEnd();
private slots:
void clicked(QAbstractButton *button);
private:
EditMarkerWidget *m_sWidget;
QDialogButtonBox *m_buttonBox;
};
#endif // EDITMARKERDIALOG_H

View File

@@ -0,0 +1,133 @@
/*
* Copyright (c) 2019-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 <http://www.gnu.org/licenses/>.
*/
#include "filedatedialog.h"
#include "Logger.h"
#include "mltcontroller.h"
#include "proxymanager.h"
#include "shotcut_mlt_properties.h"
#include <MltProducer.h>
#include <QComboBox>
#include <QDateTimeEdit>
#include <QDebug>
#include <QDialogButtonBox>
#include <QFileInfo>
#include <QVBoxLayout>
void addDateToCombo(QComboBox *combo, const QString &description, const QDateTime &date)
{
QDateTime local = date.toLocalTime();
QString text = local.toString("yyyy-MM-dd HH:mm:ss") + " [" + description + "]";
combo->addItem(text, local);
}
FileDateDialog::FileDateDialog(QString title, Mlt::Producer *producer, QWidget *parent)
: QDialog(parent)
, m_producer(producer)
, m_dtCombo(new QComboBox())
, m_dtEdit(new QDateTimeEdit())
{
setWindowTitle(tr("%1 File Date").arg(title));
int64_t milliseconds = producer->get_creation_time();
QDateTime creation_time;
if (!milliseconds) {
creation_time = QDateTime::currentDateTime();
} else {
// Set the date to the current producer date.
creation_time = QDateTime::fromMSecsSinceEpoch(milliseconds);
}
QVBoxLayout *VLayout = new QVBoxLayout(this);
populateDateOptions(producer);
m_dtCombo->setCurrentIndex(-1);
VLayout->addWidget(m_dtCombo);
connect(m_dtCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(dateSelected(int)));
m_dtEdit->setDisplayFormat("yyyy-MM-dd HH:mm:ss");
m_dtEdit->setCalendarPopup(true);
m_dtEdit->setTimeSpec(Qt::LocalTime);
m_dtEdit->setDateTime(creation_time);
VLayout->addWidget(m_dtEdit);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok
| QDialogButtonBox::Cancel);
VLayout->addWidget(buttonBox);
connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
this->setLayout(VLayout);
this->setModal(true);
}
void FileDateDialog::accept()
{
m_producer->set_creation_time(
(int64_t) m_dtEdit->dateTime().toTimeSpec(Qt::LocalTime).toMSecsSinceEpoch());
QDialog::accept();
}
void FileDateDialog::dateSelected(int index)
{
LOG_DEBUG() << index;
if (index > -1) {
m_dtEdit->setDateTime(m_dtCombo->itemData(index).toDateTime());
}
}
void FileDateDialog::populateDateOptions(Mlt::Producer *producer)
{
QDateTime dateTime;
// Add current value
int64_t milliseconds = producer->get_creation_time();
if (milliseconds) {
dateTime = QDateTime::fromMSecsSinceEpoch(milliseconds);
addDateToCombo(m_dtCombo, tr("Current Value"), dateTime);
}
// Add now time
addDateToCombo(m_dtCombo, tr("Now"), QDateTime::currentDateTime());
// Add system info for the file.
QString resource = ProxyManager::resource(*producer);
QFileInfo fileInfo(resource);
if (fileInfo.exists()) {
addDateToCombo(m_dtCombo, tr("System - Modified"), fileInfo.lastModified());
addDateToCombo(m_dtCombo, tr("System - Created"), fileInfo.birthTime());
}
// Add metadata dates
Mlt::Producer tmpProducer(MLT.profile(), "avformat", resource.toUtf8().constData());
if (tmpProducer.is_valid()) {
// Standard FFMpeg creation_time
dateTime = QDateTime::fromString(tmpProducer.get("meta.attr.creation_time.markup"),
Qt::ISODateWithMs);
if (dateTime.isValid()) {
addDateToCombo(m_dtCombo, tr("Metadata - Creation Time"), dateTime);
}
// Quicktime create date
dateTime = QDateTime::fromString(tmpProducer.get(
"meta.attr.com.apple.quicktime.creationdate.markup"),
Qt::ISODateWithMs);
if (dateTime.isValid()) {
addDateToCombo(m_dtCombo, tr("Metadata - QuickTime date"), dateTime);
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2019 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 <http://www.gnu.org/licenses/>.
*/
#ifndef FILEDATEDIALOG_H
#define FILEDATEDIALOG_H
#include <QDialog>
class QComboBox;
class QDateTimeEdit;
namespace Mlt {
class Producer;
}
class FileDateDialog : public QDialog
{
Q_OBJECT
public:
explicit FileDateDialog(QString title, Mlt::Producer *producer, QWidget *parent = 0);
private slots:
void accept();
void dateSelected(int index);
private:
void populateDateOptions(Mlt::Producer *producer);
Mlt::Producer *m_producer;
QComboBox *m_dtCombo;
QDateTimeEdit *m_dtEdit;
};
#endif // FILEDATEDIALOG_H

View File

@@ -0,0 +1,142 @@
/*
* Copyright (c) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "filedownloaddialog.h"
#include "Logger.h"
#include "mainwindow.h"
#include "qmltypes/qmlapplication.h"
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
static const int PROGRESS_MAX = 1000;
FileDownloadDialog::FileDownloadDialog(const QString &title, QWidget *parent)
: QProgressDialog(title, tr("Cancel"), 0, PROGRESS_MAX, parent ? parent : &MAIN)
{
setWindowTitle(title);
setModal(true);
setWindowModality(Qt::ApplicationModal);
setMinimumDuration(0);
}
FileDownloadDialog::~FileDownloadDialog() {}
void FileDownloadDialog::setSrc(const QString &src)
{
m_src = src;
}
void FileDownloadDialog::setDst(const QString &dst)
{
m_dst = dst;
}
bool FileDownloadDialog::start()
{
LOG_INFO() << "Download Source" << m_src;
LOG_INFO() << "Download Destination" << m_dst;
bool retVal = false;
QString tmpPath = m_dst + ".tmp";
m_file = new QFile(tmpPath, this);
if (!m_file || !m_file->open(QIODevice::WriteOnly)) {
LOG_ERROR() << "Unable to open file to write";
delete m_file;
return retVal;
}
QNetworkAccessManager manager(this);
QUrl url = m_src;
QNetworkRequest request(url);
request.setTransferTimeout(6000);
m_reply = manager.get(request);
QObject::connect(m_reply,
&QNetworkReply::downloadProgress,
this,
&FileDownloadDialog::onDownloadProgress);
QObject::connect(m_reply, &QNetworkReply::readyRead, this, &FileDownloadDialog::onReadyRead);
QObject::connect(m_reply, &QNetworkReply::finished, this, &FileDownloadDialog::onFinished);
QObject::connect(m_reply, &QNetworkReply::sslErrors, this, &FileDownloadDialog::sslErrors);
int result = exec();
if (result != QDialog::Accepted) {
LOG_WARNING() << "Download canceled";
m_file->remove();
} else if (m_reply->error() != QNetworkReply::NoError) {
LOG_ERROR() << m_reply->errorString();
if (m_reply->error() == QNetworkReply::UnknownNetworkError && m_src.startsWith("https:")) {
m_src.replace("https://", "http://");
return start();
}
if (m_reply->error() != QNetworkReply::NoError) {
m_file->remove();
QMessageBox::information(this, windowTitle(), tr("Download Failed"));
}
} else {
// Notify success
m_file->rename(m_dst);
retVal = true;
}
delete m_reply;
delete m_file;
return retVal;
}
void FileDownloadDialog::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
if (bytesTotal > 0) {
int progress = bytesReceived * PROGRESS_MAX / bytesTotal;
LOG_INFO() << "Download Progress" << progress / 10;
setValue(progress);
}
}
void FileDownloadDialog::onReadyRead()
{
m_file->write(m_reply->readAll());
}
void FileDownloadDialog::onFinished()
{
accept();
}
void FileDownloadDialog::sslErrors(const QList<QSslError> &errors)
{
LOG_ERROR() << "SSL Errors" << errors;
QString message = tr("The following SSL errors were encountered:");
foreach (const QSslError &error, errors) {
message = QStringLiteral("\n") + error.errorString();
}
message += tr("Attempt to ignore SSL errors?");
QMessageBox qDialog(QMessageBox::Question,
windowTitle(),
message,
QMessageBox::No | QMessageBox::Yes,
this);
qDialog.setDefaultButton(QMessageBox::Yes);
qDialog.setEscapeButton(QMessageBox::No);
qDialog.setWindowModality(QmlApplication::dialogModality());
int result = qDialog.exec();
if (result == QMessageBox::Yes) {
m_reply->ignoreSslErrors();
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2025 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 <http://www.gnu.org/licenses/>.
*/
#ifndef FILEDOWNLOADDIALOG_H
#define FILEDOWNLOADDIALOG_H
#include <QProgressDialog>
#include <QSslError>
class QFile;
class QNetworkReply;
class FileDownloadDialog : public QProgressDialog
{
public:
explicit FileDownloadDialog(const QString &title, QWidget *parent = nullptr);
~FileDownloadDialog();
void setSrc(const QString &src);
void setDst(const QString &dst);
bool start();
private slots:
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void onReadyRead();
void onFinished();
void sslErrors(const QList<QSslError> &errors);
private:
QString m_src;
QString m_dst;
QFile *m_file;
QNetworkReply *m_reply;
int m_replyCode;
};
#endif // FILEDOWNLOADDIALOG_H

View File

@@ -0,0 +1,91 @@
/*
* Copyright (c) 2018-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 <http://www.gnu.org/licenses/>.
*/
#include "listselectiondialog.h"
#include "ui_listselectiondialog.h"
#include <QListWidget>
ListSelectionDialog::ListSelectionDialog(const QStringList &list, QWidget *parent)
: QDialog(parent)
, ui(new Ui::ListSelectionDialog)
{
ui->setupUi(this);
for (auto &text : list) {
QListWidgetItem *item = new QListWidgetItem(text, ui->listWidget);
item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
item->setCheckState(Qt::Unchecked);
connect(ui->listWidget,
SIGNAL(itemActivated(QListWidgetItem *)),
SLOT(onItemActivated(QListWidgetItem *)));
}
}
ListSelectionDialog::~ListSelectionDialog()
{
delete ui;
}
void ListSelectionDialog::setColors(const QStringList &list)
{
ui->listWidget->setAlternatingRowColors(false);
ui->listWidget->setSortingEnabled(false);
for (auto &text : list) {
QListWidgetItem *item = new QListWidgetItem(text, ui->listWidget);
item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
connect(ui->listWidget,
SIGNAL(itemActivated(QListWidgetItem *)),
SLOT(onItemActivated(QListWidgetItem *)));
QColor color(text);
item->setCheckState(Qt::Checked);
if (color.isValid()) {
item->setBackground(color);
}
}
}
void ListSelectionDialog::setSelection(const QStringList &selection)
{
int n = ui->listWidget->count();
for (int i = 0; i < n; ++i) {
QListWidgetItem *item = ui->listWidget->item(i);
if (selection.indexOf(item->text()) > -1)
item->setCheckState(Qt::Checked);
}
}
QStringList ListSelectionDialog::selection() const
{
QStringList result;
int n = ui->listWidget->count();
for (int i = 0; i < n; ++i) {
QListWidgetItem *item = ui->listWidget->item(i);
if (item->checkState() == Qt::Checked)
result << item->text();
}
return result;
}
QDialogButtonBox *ListSelectionDialog::buttonBox() const
{
return ui->buttonBox;
}
void ListSelectionDialog::onItemActivated(QListWidgetItem *item)
{
item->setCheckState(item->checkState() == Qt::Checked ? Qt::Unchecked : Qt::Checked);
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2018-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 <http://www.gnu.org/licenses/>.
*/
#ifndef LISTSELECTIONDIALOG_H
#define LISTSELECTIONDIALOG_H
#include <QDialog>
namespace Ui {
class ListSelectionDialog;
}
class QListWidgetItem;
class QDialogButtonBox;
class ListSelectionDialog : public QDialog
{
Q_OBJECT
public:
explicit ListSelectionDialog(const QStringList &list, QWidget *parent = 0);
~ListSelectionDialog();
void setColors(const QStringList &colors);
void setSelection(const QStringList &selection);
QStringList selection() const;
QDialogButtonBox *buttonBox() const;
private:
Ui::ListSelectionDialog *ui;
private slots:
void onItemActivated(QListWidgetItem *item);
};
#endif // LISTSELECTIONDIALOG_H

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ListSelectionDialog</class>
<widget class="QDialog" name="ListSelectionDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>314</width>
<height>288</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QListWidget" name="listWidget">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="spacing">
<number>4</number>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ListSelectionDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ListSelectionDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,54 @@
/*
* Copyright (c) 2020 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 <http://www.gnu.org/licenses/>.
*/
#include "longuitask.h"
#include "mainwindow.h"
static QMutex g_mutex;
static LongUiTask *g_instance = nullptr;
LongUiTask::LongUiTask(QString title)
: QProgressDialog(title, QString(), 0, 0, &MAIN)
{
setWindowTitle(title);
setModal(true);
setWindowModality(Qt::ApplicationModal);
setMinimumDuration(2000);
setRange(0, 0);
g_instance = this;
}
LongUiTask::~LongUiTask()
{
g_instance = nullptr;
}
void LongUiTask::reportProgress(QString text, int value, int max)
{
setLabelText(text);
setRange(0, max - 1);
setValue(value);
QCoreApplication::processEvents();
}
void LongUiTask::cancel()
{
if (g_instance) {
g_instance->QProgressDialog::cancel();
}
}

55
src/dialogs/longuitask.h Normal file
View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2020 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 <http://www.gnu.org/licenses/>.
*/
#ifndef LONGUITASK_H
#define LONGUITASK_H
#include <QFuture>
#include <QProgressDialog>
#include <QtConcurrent/QtConcurrent>
class LongUiTask : public QProgressDialog
{
public:
explicit LongUiTask(QString title);
~LongUiTask();
template<class Ret>
Ret wait(QString text, const QFuture<Ret> &future)
{
setLabelText(text);
setRange(0, 0);
while (!future.isFinished()) {
setValue(0);
QCoreApplication::processEvents();
QThread::msleep(100);
}
return future.result();
}
template<class Ret, class Func, class... Args>
Ret runAsync(QString text, Func &&f, Args &&...args)
{
QFuture<Ret> future = QtConcurrent::run(f, std::forward<Args>(args)...);
return wait<Ret>(text, future);
}
void reportProgress(QString text, int value, int max);
static void cancel();
};
#endif // LONGUITASK_H

View File

@@ -0,0 +1,288 @@
/*
* Copyright (c) 2021-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 <http://www.gnu.org/licenses/>.
*/
#include "multifileexportdialog.h"
#include "mainwindow.h"
#include "proxymanager.h"
#include "qmltypes/qmlapplication.h"
#include "shotcut_mlt_properties.h"
#include "util.h"
#include <MltPlaylist.h>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileDialog>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
enum {
NAME_FIELD_NONE = 0,
NAME_FIELD_NAME,
NAME_FIELD_INDEX,
NAME_FIELD_DATE,
NAME_FIELD_HASH,
};
MultiFileExportDialog::MultiFileExportDialog(QString title,
Mlt::Playlist *playlist,
const QString &directory,
const QString &prefix,
const QString &extension,
QWidget *parent)
: QDialog(parent)
, m_playlist(playlist)
{
int col = 0;
setWindowTitle(title);
setWindowModality(QmlApplication::dialogModality());
QGridLayout *glayout = new QGridLayout();
glayout->setHorizontalSpacing(4);
glayout->setVerticalSpacing(2);
// Directory
glayout->addWidget(new QLabel(tr("Directory")), col, 0, Qt::AlignRight);
QHBoxLayout *dirHbox = new QHBoxLayout();
m_dir = new QLineEdit(QDir::toNativeSeparators(directory));
m_dir->setReadOnly(true);
QPushButton *browseButton = new QPushButton(this);
browseButton->setIcon(
QIcon::fromTheme("document-open", QIcon(":/icons/oxygen/32x32/actions/document-open.png")));
if (!connect(browseButton, &QAbstractButton::clicked, this, &MultiFileExportDialog::browse))
connect(browseButton, SIGNAL(clicked()), SLOT(browse()));
dirHbox->addWidget(m_dir);
dirHbox->addWidget(browseButton);
glayout->addLayout(dirHbox, col++, 1, Qt::AlignLeft);
// Prefix
glayout->addWidget(new QLabel(tr("Prefix")), col, 0, Qt::AlignRight);
m_prefix = new QLineEdit(prefix.isEmpty() ? tr("export") : prefix);
if (!connect(m_prefix, &QLineEdit::textChanged, this, &MultiFileExportDialog::rebuildList))
connect(m_prefix, SIGNAL(textChanged(const QString &)), SLOT(rebuildList()));
glayout->addWidget(m_prefix, col++, 1, Qt::AlignLeft);
// Field 1
glayout->addWidget(new QLabel(tr("Field 1")), col, 0, Qt::AlignRight);
m_field1 = new QComboBox();
fillCombo(m_field1);
if (!connect(m_field1,
QOverload<int>::of(&QComboBox::activated),
this,
&MultiFileExportDialog::rebuildList))
connect(m_field1, SIGNAL(activated(const QString &)), SLOT(rebuildList()));
glayout->addWidget(m_field1, col++, 1, Qt::AlignLeft);
// Field 2
glayout->addWidget(new QLabel(tr("Field 2")), col, 0, Qt::AlignRight);
m_field2 = new QComboBox();
fillCombo(m_field2);
if (!connect(m_field2,
QOverload<int>::of(&QComboBox::activated),
this,
&MultiFileExportDialog::rebuildList))
connect(m_field2, SIGNAL(activated(const QString &)), SLOT(rebuildList()));
glayout->addWidget(m_field2, col++, 1, Qt::AlignLeft);
// Field 3
glayout->addWidget(new QLabel(tr("Field 3")), col, 0, Qt::AlignRight);
m_field3 = new QComboBox();
fillCombo(m_field3);
m_field3->setCurrentIndex(NAME_FIELD_INDEX);
if (!connect(m_field3,
QOverload<int>::of(&QComboBox::activated),
this,
&MultiFileExportDialog::rebuildList))
connect(m_field3, SIGNAL(activated(const QString &)), SLOT(rebuildList()));
glayout->addWidget(m_field3, col++, 1, Qt::AlignLeft);
// Extension
glayout->addWidget(new QLabel(tr("Extension")), col, 0, Qt::AlignRight);
m_ext = new QLineEdit(extension);
if (!connect(m_ext, &QLineEdit::textChanged, this, &MultiFileExportDialog::rebuildList))
connect(m_ext, SIGNAL(textChanged(const QString &)), SLOT(rebuildList()));
glayout->addWidget(m_ext, col++, 1, Qt::AlignLeft);
// Error
m_errorIcon = new QLabel();
QIcon icon = QIcon(":/icons/oxygen/32x32/status/task-reject.png");
m_errorIcon->setPixmap(icon.pixmap(QSize(24, 24)));
glayout->addWidget(m_errorIcon, col, 0, Qt::AlignRight);
m_errorText = new QLabel();
glayout->addWidget(m_errorText, col++, 1, Qt::AlignLeft);
// List
m_list = new QListWidget();
m_list->setSelectionMode(QAbstractItemView::NoSelection);
m_list->setIconSize(QSize(16, 16));
glayout->addWidget(m_list, col++, 0, 1, 2);
// Buttons
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
glayout->addWidget(m_buttonBox, col++, 0, 1, 2);
connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
glayout->setColumnMinimumWidth(1,
fontMetrics().horizontalAdvance(m_dir->text())
+ browseButton->width());
this->setLayout(glayout);
this->setModal(true);
rebuildList();
resize(400, 300);
}
QStringList MultiFileExportDialog::getExportFiles()
{
return m_stringList;
}
QString MultiFileExportDialog::appendField(QString text, QComboBox *combo, int clipIndex)
{
QString field;
switch (combo->currentData().toInt()) {
default:
case NAME_FIELD_NONE:
break;
case NAME_FIELD_NAME: {
QScopedPointer<Mlt::ClipInfo> info(MAIN.playlist()->clip_info(clipIndex));
if (info && info->producer && info->producer->is_valid()) {
field = info->producer->get(kShotcutCaptionProperty);
if (field.isEmpty()) {
field = ProxyManager::resource(*info->producer);
field = QFileInfo(field).completeBaseName();
}
if (field == "<producer>") {
field = QString::fromUtf8(info->producer->get("mlt_service"));
}
}
break;
}
case NAME_FIELD_INDEX: {
int digits = QString::number(m_playlist->count()).size();
field = QStringLiteral("%1").arg(clipIndex + 1, digits, 10, QChar('0'));
break;
}
case NAME_FIELD_DATE: {
QScopedPointer<Mlt::ClipInfo> info(MAIN.playlist()->clip_info(clipIndex));
if (info && info->producer && info->producer->is_valid()) {
int64_t ms = info->producer->get_creation_time();
if (ms) {
field = QDateTime::fromMSecsSinceEpoch(ms).toString("yyyyMMdd-HHmmss");
}
}
break;
}
case NAME_FIELD_HASH: {
QScopedPointer<Mlt::ClipInfo> info(MAIN.playlist()->clip_info(clipIndex));
field = Util::getHash(*info->producer);
break;
}
}
if (text.isEmpty()) {
return field;
} else if (field.isEmpty()) {
return text;
} else {
return text + "-" + field;
}
}
void MultiFileExportDialog::fillCombo(QComboBox *combo)
{
combo->addItem(tr("None"), QVariant(NAME_FIELD_NONE));
combo->addItem(tr("Name"), QVariant(NAME_FIELD_NAME));
combo->addItem(tr("Index"), QVariant(NAME_FIELD_INDEX));
combo->addItem(tr("Date"), QVariant(NAME_FIELD_DATE));
combo->addItem(tr("Hash"), QVariant(NAME_FIELD_HASH));
}
void MultiFileExportDialog::rebuildList()
{
m_stringList.clear();
m_list->clear();
for (int i = 0; i < m_playlist->count(); i++) {
QString filename = m_prefix->text();
filename = appendField(filename, m_field1, i);
filename = appendField(filename, m_field2, i);
filename = appendField(filename, m_field3, i);
if (!filename.isEmpty()) {
filename = m_dir->text() + QDir::separator() + filename + "." + m_ext->text();
m_stringList << filename;
}
}
m_list->addItems(m_stringList);
// Detect Errors
m_errorText->setText("");
int n = m_stringList.size();
if (n == 0) {
m_errorText->setText(tr("Empty File Name"));
} else if (!QDir(m_dir->text()).exists()) {
m_errorText->setText(tr("Directory does not exist: %1").arg(m_dir->text()));
} else {
// Search for existing or duplicate files
for (int i = 0; i < n; i++) {
QString errorString;
QFileInfo fileInfo(m_stringList[i]);
if (fileInfo.exists()) {
errorString = tr("File Exists: %1").arg(m_stringList[i]);
} else {
for (int j = 0; j < n; j++) {
if (j != i && m_stringList[i] == m_stringList[j]) {
QString filename = QFileInfo(m_stringList[i]).fileName();
errorString = tr("Duplicate File Name: %1").arg(filename);
break;
}
}
}
QListWidgetItem *item = m_list->item(i);
if (errorString.isEmpty()) {
item->setIcon(QIcon(":/icons/oxygen/32x32/status/task-complete.png"));
} else {
item->setIcon(QIcon(":/icons/oxygen/32x32/status/task-reject.png"));
item->setToolTip(errorString);
m_errorText->setText(errorString);
}
}
}
if (m_errorText->text().isEmpty()) {
m_errorText->setVisible(false);
m_errorIcon->setVisible(false);
m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
} else {
m_errorText->setVisible(true);
m_errorIcon->setVisible(true);
m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
}
m_buttonBox->button(QDialogButtonBox::Ok)->setToolTip(tr("Fix file name errors before export."));
}
void MultiFileExportDialog::browse()
{
QString directory = QDir::toNativeSeparators(
QFileDialog::getExistingDirectory(this,
tr("Export Directory"),
m_dir->text(),
Util::getFileDialogOptions()));
if (!directory.isEmpty()) {
m_dir->setText(directory);
rebuildList();
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2021 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 <http://www.gnu.org/licenses/>.
*/
#ifndef MULTIFILEEXPORTDIALOG_H
#define MULTIFILEEXPORTDIALOG_H
#include <QDialog>
#include <QStringList>
class QComboBox;
class QDialogButtonBox;
class QLabel;
class QListWidget;
class QLineEdit;
namespace Mlt {
class Playlist;
}
class MultiFileExportDialog : public QDialog
{
Q_OBJECT
public:
explicit MultiFileExportDialog(QString title,
Mlt::Playlist *playlist,
const QString &directory,
const QString &prefix,
const QString &extension,
QWidget *parent = 0);
QStringList getExportFiles();
private slots:
void rebuildList();
void browse();
private:
QString appendField(QString text, QComboBox *combo, int clipIndex);
void fillCombo(QComboBox *combo);
Mlt::Playlist *m_playlist;
QLineEdit *m_dir;
QLineEdit *m_prefix;
QComboBox *m_field1;
QComboBox *m_field2;
QComboBox *m_field3;
QLineEdit *m_ext;
QLabel *m_errorIcon;
QLabel *m_errorText;
QListWidget *m_list;
QDialogButtonBox *m_buttonBox;
QStringList m_stringList;
};
#endif // MULTIFILEEXPORTDIALOG_H

View File

@@ -0,0 +1,121 @@
/*
* Copyright (c) 2023-2024 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 <http://www.gnu.org/licenses/>.
*/
#include "resourcedialog.h"
#include "Logger.h"
#include "mltcontroller.h"
#include "qmltypes/qmlapplication.h"
#include "transcodedialog.h"
#include "transcoder.h"
#include "widgets/resourcewidget.h"
#include <QDialogButtonBox>
#include <QMessageBox>
#include <QPushButton>
#include <QVBoxLayout>
ResourceDialog::ResourceDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("Resources"));
setSizeGripEnabled(true);
QVBoxLayout *vlayout = new QVBoxLayout();
m_resourceWidget = new ResourceWidget(this);
vlayout->addWidget(m_resourceWidget);
// Button Box
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
buttonBox->button(QDialogButtonBox::Close)->setAutoDefault(false);
connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
// Convert button
QPushButton *convertButton = buttonBox->addButton(tr("Convert Selected"),
QDialogButtonBox::ActionRole);
connect(convertButton, SIGNAL(pressed()), this, SLOT(convert()));
vlayout->addWidget(buttonBox);
setLayout(vlayout);
}
void ResourceDialog::search(Mlt::Producer *producer)
{
m_resourceWidget->search(producer);
}
void ResourceDialog::add(Mlt::Producer *producer)
{
m_resourceWidget->add(producer);
}
void ResourceDialog::selectTroubleClips()
{
m_resourceWidget->selectTroubleClips();
}
bool ResourceDialog::hasTroubleClips()
{
return m_resourceWidget->hasTroubleClips();
}
int ResourceDialog::producerCount()
{
return m_resourceWidget->producerCount();
}
Mlt::Producer ResourceDialog::producer(int index)
{
return m_resourceWidget->producer(index);
}
void ResourceDialog::convert()
{
QList<Mlt::Producer> producers(m_resourceWidget->getSelected());
// Only convert avformat producers
QMutableListIterator<Mlt::Producer> i(producers);
while (i.hasNext()) {
Mlt::Producer producer = i.next();
if (!QString(producer.get("mlt_service")).startsWith("avformat"))
i.remove();
}
if (producers.length() < 1) {
QMessageBox::warning(this, windowTitle(), tr("No resources to convert"));
return;
}
TranscodeDialog
dialog(tr("Choose an edit-friendly format below and then click OK to choose a file name. "
"After choosing a file name, a job is created. "
"When it is done, double-click the job to open it.\n"),
MLT.profile().progressive(),
this);
dialog.setWindowTitle(tr("Convert..."));
dialog.setWindowModality(QmlApplication::dialogModality());
Transcoder transcoder;
transcoder.setProducers(producers);
transcoder.convert(dialog);
accept();
}
void ResourceDialog::showEvent(QShowEvent *event)
{
m_resourceWidget->updateSize();
resize(m_resourceWidget->width() + 4, m_resourceWidget->height());
QDialog::showEvent(event);
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2023-2024 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 <http://www.gnu.org/licenses/>.
*/
#ifndef RESOURCEDIALOG_H
#define RESOURCEDIALOG_H
#include <QDialog>
class ResourceWidget;
namespace Mlt {
class Producer;
}
class ResourceDialog : public QDialog
{
Q_OBJECT
public:
explicit ResourceDialog(QWidget *parent = 0);
void search(Mlt::Producer *producer);
void add(Mlt::Producer *producer);
void selectTroubleClips();
bool hasTroubleClips();
int producerCount();
Mlt::Producer producer(int index);
private slots:
void convert();
protected:
virtual void showEvent(QShowEvent *event) override;
private:
ResourceWidget *m_resourceWidget;
};
#endif // RESOURCEDIALOG_H

View File

@@ -0,0 +1,133 @@
/*
* Copyright (c) 2021-2023 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 <http://www.gnu.org/licenses/>.
*/
#include "saveimagedialog.h"
#include "Logger.h"
#include "mltcontroller.h"
#include "settings.h"
#include "util.h"
#include <QDebug>
#include <QtMath>
static QString suffixFromFilter(const QString &filterText)
{
QString suffix = filterText.section("*", 1, 1).section(")", 0, 0).section(" ", 0, 0);
if (!suffix.startsWith(".")) {
suffix.clear();
}
return suffix;
}
SaveImageDialog::SaveImageDialog(QWidget *parent, const QString &caption, QImage &image)
: QFileDialog(parent, caption)
, m_image(image)
{
setModal(true);
setAcceptMode(QFileDialog::AcceptSave);
setFileMode(QFileDialog::AnyFile);
setOptions(Util::getFileDialogOptions());
setDirectory(Settings.savePath());
QString nameFilter = tr("PNG (*.png);;BMP (*.bmp);;JPEG (*.jpg *.jpeg);;PPM (*.ppm);;TIFF "
"(*.tif *.tiff);;WebP (*.webp);;All Files (*)");
setNameFilter(nameFilter);
QStringList nameFilters = nameFilter.split(";;");
QString suffix = Settings.exportFrameSuffix();
QString selectedNameFilter = nameFilters[0];
for (const auto &f : nameFilters) {
if (f.contains(suffix.toLower())) {
selectedNameFilter = f;
break;
}
}
selectNameFilter(selectedNameFilter);
// Use the current player time as a suggested file name
QString nameSuggestion
= QStringLiteral("Shotcut_%1").arg(MLT.producer()->frame_time(mlt_time_clock));
nameSuggestion = nameSuggestion.replace(":", "_");
nameSuggestion = nameSuggestion.replace(".", "_");
nameSuggestion += suffix;
selectFile(nameSuggestion);
#if !defined(Q_OS_WIN)
if (!connect(this, &QFileDialog::filterSelected, this, &SaveImageDialog::onFilterSelected))
connect(this,
SIGNAL(filterSelected(const QString &)),
SLOT(const onFilterSelected(const QString &)));
#endif
if (!connect(this, &QFileDialog::fileSelected, this, &SaveImageDialog::onFileSelected))
connect(this, SIGNAL(fileSelected(const QString &)), SLOT(onFileSelected(const QString &)));
}
void SaveImageDialog::onFilterSelected(const QString &filter)
{
// When the file type filter is changed, automatically change
// the file extension to match.
if (filter.isEmpty()) {
return;
}
QString suffix = suffixFromFilter(filter);
if (suffix.isEmpty()) {
return; // All files
}
QStringList files = selectedFiles();
if (files.size() == 0) {
return;
}
QString filename = files[0];
// Strip the suffix from the current file name
if (!QFileInfo(filename).suffix().isEmpty()) {
filename = filename.section(".", 0, -2);
}
// Add the new suffix
filename += suffix;
selectFile(filename);
}
void SaveImageDialog::onFileSelected(const QString &file)
{
if (file.isEmpty()) {
return;
}
m_saveFile = file;
QFileInfo fi(m_saveFile);
if (fi.suffix().isEmpty()) {
QString suffix = suffixFromFilter(selectedNameFilter());
if (suffix.isEmpty()) {
suffix = ".png";
}
m_saveFile += suffix;
fi = QFileInfo(m_saveFile);
}
if (Util::warnIfNotWritable(m_saveFile, this, windowTitle()))
return;
// Convert to square pixels if needed.
qreal aspectRatio = (qreal) m_image.width() / m_image.height();
if (qFloor(aspectRatio * 1000) != qFloor(MLT.profile().dar() * 1000)) {
m_image = m_image.scaled(qRound(m_image.height() * MLT.profile().dar()),
m_image.height(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
m_image.save(m_saveFile, Q_NULLPTR, (fi.suffix() == "webp") ? 80 : -1);
Settings.setSavePath(fi.path());
Settings.setExportFrameSuffix(QStringLiteral(".") + fi.suffix());
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2021 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 <http://www.gnu.org/licenses/>.
*/
#ifndef SAVEIMAGEDIALOG_H
#define SAVEIMAGEDIALOG_H
#include <QFileDialog>
#include <QImage>
#include <QString>
class SaveImageDialog : public QFileDialog
{
Q_OBJECT
public:
explicit SaveImageDialog(QWidget *parent, const QString &caption, QImage &image);
QString saveFile() { return m_saveFile; }
private slots:
void onFilterSelected(const QString &filter);
void onFileSelected(const QString &file);
private:
QImage &m_image;
QString m_saveFile;
};
#endif // SAVEIMAGEDIALOG_H

View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2020 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 <http://www.gnu.org/licenses/>.
*/
#include "slideshowgeneratordialog.h"
#include "Logger.h"
#include "widgets/slideshowgeneratorwidget.h"
#include <MltProfile.h>
#include <MltTransition.h>
#include <QDebug>
#include <QDialogButtonBox>
#include <QVBoxLayout>
SlideshowGeneratorDialog::SlideshowGeneratorDialog(QWidget *parent, Mlt::Playlist &clips)
: QDialog(parent)
{
setWindowTitle(tr("Slideshow Generator - %n Clips", nullptr, clips.count()));
QVBoxLayout *VLayout = new QVBoxLayout(this);
m_sWidget = new SlideshowGeneratorWidget(&clips, this);
VLayout->addWidget(m_sWidget);
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Close);
VLayout->addWidget(m_buttonBox);
connect(m_buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(clicked(QAbstractButton *)));
setLayout(VLayout);
setModal(true);
layout()->setSizeConstraint(QLayout::SetFixedSize);
}
Mlt::Playlist *SlideshowGeneratorDialog::getSlideshow()
{
return m_sWidget->getSlideshow();
}
void SlideshowGeneratorDialog::clicked(QAbstractButton *button)
{
QDialogButtonBox::ButtonRole role = m_buttonBox->buttonRole(button);
if (role == QDialogButtonBox::AcceptRole) {
LOG_DEBUG() << "Accept";
accept();
} else if (role == QDialogButtonBox::RejectRole) {
LOG_DEBUG() << "Reject";
reject();
} else {
LOG_DEBUG() << "Unknown role" << role;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 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 <http://www.gnu.org/licenses/>.
*/
#ifndef SLIDESHOWGENERATORDIALOG_H
#define SLIDESHOWGENERATORDIALOG_H
#include <MltPlaylist.h>
#include <QDialog>
class SlideshowGeneratorWidget;
class QAbstractButton;
class QDialogButtonBox;
class SlideshowGeneratorDialog : public QDialog
{
Q_OBJECT
public:
explicit SlideshowGeneratorDialog(QWidget *parent, Mlt::Playlist &clips);
Mlt::Playlist *getSlideshow();
private slots:
void clicked(QAbstractButton *button);
private:
SlideshowGeneratorWidget *m_sWidget;
QDialogButtonBox *m_buttonBox;
};
#endif // SLIDESHOWGENERATORDIALOG_H

View File

@@ -0,0 +1,265 @@
/*
* Copyright (c) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "speechdialog.h"
#include "Logger.h"
#include "mltcontroller.h"
#include "qmltypes/qmlapplication.h"
#include "settings.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFileDialog>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QStringList>
SpeechDialog::SpeechDialog(QWidget *parent)
{
setWindowTitle(tr("Text to Speech"));
setWindowModality(QmlApplication::dialogModality());
auto grid = new QGridLayout;
setLayout(grid);
grid->setSizeConstraint(QLayout::SetFixedSize);
// Language selector row
auto languageLabel = new QLabel(tr("Language"), this);
m_language = new QComboBox(this);
// value (userData) : visible name
m_language->addItem(tr("American English"), QStringLiteral("a"));
m_language->addItem(tr("British English"), QStringLiteral("b"));
m_language->addItem(tr("Spanish"), QStringLiteral("e"));
m_language->addItem(tr("French"), QStringLiteral("f"));
m_language->addItem(tr("Hindi"), QStringLiteral("h"));
m_language->addItem(tr("Italian"), QStringLiteral("i"));
m_language->addItem(tr("Portuguese"), QStringLiteral("p"));
m_language->addItem(tr("Japanese"), QStringLiteral("j"));
m_language->addItem(tr("Mandarin Chinese"), QStringLiteral("z"));
// Set persisted language selection
const QString savedLang = Settings.speechLanguage();
for (int i = 0; i < m_language->count(); ++i) {
if (m_language->itemData(i).toString() == savedLang) {
m_language->setCurrentIndex(i);
break;
}
}
grid->addWidget(languageLabel, 0, 0, Qt::AlignRight);
grid->addWidget(m_language, 0, 1);
// Voice selector row
auto voiceLabel = new QLabel(tr("Voice"), this);
grid->addWidget(voiceLabel, 1, 0, Qt::AlignRight);
m_voice = new QComboBox(this);
m_voice->setMinimumWidth(120);
auto icon = QIcon::fromTheme("media-playback-start",
QIcon(":/icons/oxygen/32x32/actions/media-playback-start.png"));
auto voiceButton = new QPushButton(icon, QString(), this);
auto voiceRow = new QWidget(this);
auto voiceLayout = new QHBoxLayout(voiceRow);
voiceButton->setToolTip(tr("Preview this voice"));
voiceLayout->setContentsMargins(0, 0, 0, 0);
voiceLayout->setSpacing(4);
voiceLayout->addWidget(m_voice);
voiceLayout->addWidget(voiceButton);
voiceLayout->addStretch();
grid->addWidget(voiceRow, 1, 1, 1, 2);
connect(voiceButton, &QPushButton::clicked, this, [this]() {
auto dir = QmlApplication::dataDir();
dir.cd("shotcut");
dir.cd("voices");
const auto filename = dir.filePath(m_voice->currentData().toString().append(".opus"));
LOG_DEBUG() << filename;
Mlt::Producer p(MLT.profile(), filename.toLocal8Bit().constData());
if (m_consumer && m_consumer->is_valid())
m_consumer->stop();
m_consumer.reset(new Mlt::Consumer(MLT.profile(), "sdl2_audio"));
if (!m_consumer || !m_consumer->is_valid())
return;
m_consumer->connect(p);
m_consumer->set("terminate_on_pause", 1);
m_consumer->set("video_off", 1);
m_consumer->start();
});
// Populate voices for initial selection and react to changes
populateVoices(m_language->currentData().toString());
connect(m_language, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int) {
populateVoices(m_language->currentData().toString());
});
// Set persisted voice (after voices are populated again below if needed)
const QString savedVoice = Settings.speechVoice();
if (!savedVoice.isEmpty()) {
for (int i = 0; i < m_voice->count(); ++i) {
if (m_voice->itemData(i).toString() == savedVoice) {
m_voice->setCurrentIndex(i);
break;
}
}
}
// Speed selector row
auto speedLabel = new QLabel(tr("Speed"), this);
m_speed = new QDoubleSpinBox(this);
m_speed->setRange(0.5, 2.0);
m_speed->setDecimals(2);
m_speed->setSingleStep(0.05);
m_speed->setValue(Settings.speechSpeed());
auto speedRow = new QWidget(this);
auto speedLayout = new QHBoxLayout(speedRow);
speedLayout->setContentsMargins(0, 0, 0, 0);
speedLayout->setSpacing(4);
speedLayout->addWidget(m_speed);
speedLayout->addStretch();
grid->addWidget(speedLabel, 2, 0, Qt::AlignRight);
grid->addWidget(speedRow, 2, 1, 1, 2);
// Output file row
auto outputLabel = new QLabel(tr("Output file"), this);
m_outputFile = new QLineEdit(this);
m_outputFile->setMinimumWidth(300);
m_outputFile->setPlaceholderText(tr("Click the button to set the file"));
m_outputFile->setDisabled(true);
m_outputFile->setText(QmlApplication::getNextProjectFile("speech-.wav"));
icon = QIcon::fromTheme("document-save",
QIcon(":/icons/oxygen/32x32/actions/document-save.png"));
auto browseButton = new QPushButton(icon, QString(), this);
grid->addWidget(outputLabel, 3, 0, Qt::AlignRight);
auto outputRow = new QWidget(this);
auto outputLayout = new QHBoxLayout(outputRow);
outputLayout->setContentsMargins(0, 0, 0, 0);
outputLayout->setSpacing(4);
outputLayout->addWidget(m_outputFile);
outputLayout->addWidget(browseButton);
grid->addWidget(outputRow, 3, 1, 1, 2);
connect(browseButton, &QPushButton::clicked, this, [this]() {
const QString selected = QFileDialog::getSaveFileName(this,
tr("Save Audio File"),
Settings.savePath(),
tr("WAV files (*.wav)"));
if (!selected.isEmpty()) {
QString path = selected;
if (!path.endsWith(QStringLiteral(".wav"), Qt::CaseInsensitive)) {
path += QStringLiteral(".wav");
}
m_outputFile->setText(path);
Settings.setSavePath(QFileInfo(selected).path());
}
});
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
grid->addWidget(buttonBox, 4, 0, 1, 3);
connect(buttonBox->button(QDialogButtonBox::Cancel),
&QAbstractButton::clicked,
this,
&QDialog::close);
connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, [&] {
const QString lang = m_language->currentData().toString();
const QString voice = m_voice->currentData().toString();
const double speed = m_speed->value();
QString path = m_outputFile->text().trimmed();
if (path.isEmpty()) {
LOG_DEBUG() << Settings.savePath();
// No file chosen; prompt user similarly to previous behavior
const QString selected = QFileDialog::getSaveFileName(this,
tr("Save Audio File"),
Settings.savePath(),
tr("WAV files (*.wav)"));
if (selected.isEmpty()) {
return; // user canceled
}
path = selected;
Settings.setSavePath(QFileInfo(selected).path());
}
if (!path.endsWith(QStringLiteral(".wav"), Qt::CaseInsensitive)) {
path += QStringLiteral(".wav");
}
m_outputFile->setText(path);
// Persist settings
Settings.setSpeechLanguage(lang);
Settings.setSpeechVoice(voice);
Settings.setSpeechSpeed(speed);
LOG_DEBUG() << "OK clicked, language code:" << lang << "voice:" << voice
<< "speed:" << speed << "file:" << path;
accept();
});
}
void SpeechDialog::populateVoices(const QString &langCode)
{
static const QStringList kVoices = {
QStringLiteral("af_alloy"), QStringLiteral("af_aoede"),
QStringLiteral("af_bella"), QStringLiteral("af_heart"),
QStringLiteral("af_jessica"), QStringLiteral("af_kore"),
QStringLiteral("af_nicole"), QStringLiteral("af_nova"),
QStringLiteral("af_river"), QStringLiteral("af_sarah"),
QStringLiteral("af_sky"), QStringLiteral("am_adam"),
QStringLiteral("am_echo"), QStringLiteral("am_eric"),
QStringLiteral("am_fenrir"), QStringLiteral("am_liam"),
QStringLiteral("am_michael"), QStringLiteral("am_onyx"),
QStringLiteral("am_puck"), QStringLiteral("am_santa"),
QStringLiteral("bf_alice"), QStringLiteral("bf_emma"),
QStringLiteral("bf_isabella"), QStringLiteral("bf_lily"),
QStringLiteral("bm_daniel"), QStringLiteral("bm_fable"),
QStringLiteral("bm_george"), QStringLiteral("bm_lewis"),
QStringLiteral("ef_dora"), QStringLiteral("em_alex"),
QStringLiteral("em_santa"), QStringLiteral("ff_siwis"),
QStringLiteral("hf_alpha"), QStringLiteral("hf_beta"),
QStringLiteral("hm_omega"), QStringLiteral("hm_psi"),
QStringLiteral("if_sara"), QStringLiteral("im_nicola"),
QStringLiteral("jf_alpha"), QStringLiteral("jf_gongitsune"),
QStringLiteral("jf_nezumi"), QStringLiteral("jf_tebukuro"),
QStringLiteral("jm_kumo"), QStringLiteral("pf_dora"),
QStringLiteral("pm_alex"), QStringLiteral("pm_santa"),
QStringLiteral("zf_xiaobei"), QStringLiteral("zf_xiaoni"),
QStringLiteral("zf_xiaoxiao"), QStringLiteral("zf_xiaoyi"),
QStringLiteral("zm_yunjian"), QStringLiteral("zm_yunxi"),
QStringLiteral("zm_yunxia"), QStringLiteral("zm_yunyang"),
};
m_voice->clear();
if (langCode.isEmpty()) {
return;
}
const QString prefix = langCode.left(1);
for (const auto &v : kVoices) {
if (v.startsWith(prefix)) {
const int underscore = v.indexOf('_');
if (underscore > 0 && underscore + 1 < v.size()) {
const QString name = v.mid(underscore + 1);
const QString gender = v.mid(1, 1).toLower();
if (gender == "m") {
m_voice->addItem(QStringLiteral("♂️ ") + name[0].toUpper() + name.mid(1), v);
} else if (gender == "f") {
m_voice->addItem(QStringLiteral("♀️ ") + name[0].toUpper() + name.mid(1), v);
} else {
m_voice->addItem(name[0].toUpper() + name.mid(1), v);
}
}
}
}
if (m_voice->count() == 0) {
m_voice->addItem(tr("(No voices)"), QString());
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (c) 2025 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 <http://www.gnu.org/licenses/>.
*/
#ifndef SPEECHDIALOG_H
#define SPEECHDIALOG_H
#include <MltConsumer.h>
#include <QComboBox>
#include <QDialog>
#include <QDoubleSpinBox>
#include <QLineEdit>
#include <QObject>
namespace Mlt {
class Consumer;
}
class SpeechDialog : public QDialog
{
public:
explicit SpeechDialog(QWidget *parent);
QString outputFile() const { return m_outputFile ? m_outputFile->text().trimmed() : QString(); }
QString languageCode() const
{
return m_language ? m_language->currentData().toString() : QString();
}
QString voiceCode() const { return m_voice ? m_voice->currentData().toString() : QString(); }
double speed() const { return m_speed ? m_speed->value() : 1.0; }
private:
QComboBox *m_language = nullptr;
QComboBox *m_voice = nullptr;
QDoubleSpinBox *m_speed = nullptr;
QLineEdit *m_outputFile = nullptr;
std::unique_ptr<Mlt::Consumer> m_consumer;
void populateVoices(const QString &langCode);
};
#endif // SPEECHDIALOG_H

View File

@@ -0,0 +1,98 @@
/*
* Copyright (c) 2024 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 <http://www.gnu.org/licenses/>.
*/
#include "subtitletrackdialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QList>
#include <QLocale>
static void fillLanguages(QComboBox *combo)
{
QList<QLocale> allLocales = QLocale::matchingLocales(QLocale::AnyLanguage,
QLocale::AnyScript,
QLocale::AnyTerritory);
QMap<QString, QString> iso639_2LanguageCodes;
for (const QLocale &locale : allLocales) {
QLocale::Language lang = locale.language();
if (lang != QLocale::AnyLanguage && lang != QLocale::C) {
QString langCode = QLocale::languageToCode(lang, QLocale::ISO639Part2);
QString langStr = QLocale::languageToString(lang);
if (!langCode.isEmpty() && !langStr.isEmpty()) {
iso639_2LanguageCodes.insert(langStr, langCode);
}
}
}
for (auto it = iso639_2LanguageCodes.keyValueBegin(); it != iso639_2LanguageCodes.keyValueEnd();
++it) {
QString text = QStringLiteral("%1 (%2)").arg(it->first).arg(it->second);
combo->addItem(text, it->second);
}
}
SubtitleTrackDialog::SubtitleTrackDialog(const QString &name, const QString &lang, QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("New Subtitle Track"));
QGridLayout *grid = new QGridLayout();
grid->addWidget(new QLabel(tr("Name")), 0, 0, Qt::AlignRight);
m_name = new QLineEdit(this);
m_name->setText(name);
grid->addWidget(m_name, 0, 1);
grid->addWidget(new QLabel(tr("Language")), 1, 0, Qt::AlignRight);
m_lang = new QComboBox(this);
fillLanguages(m_lang);
for (int i = 0; i < m_lang->count(); i++) {
if (m_lang->itemData(i).toString() == lang) {
m_lang->setCurrentIndex(i);
break;
}
}
grid->addWidget(m_lang, 1, 1);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok
| QDialogButtonBox::Cancel);
grid->addWidget(buttonBox, 2, 0, 2, 2);
connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
setLayout(grid);
this->setModal(true);
m_name->setFocus();
}
QString SubtitleTrackDialog::getName()
{
return m_name->text();
}
QString SubtitleTrackDialog::getLanguage()
{
return m_lang->currentData().toString();
}
void SubtitleTrackDialog::accept()
{
QDialog::accept();
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2024 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 <http://www.gnu.org/licenses/>.
*/
#ifndef SUBTITLETRACKDIALOG_H
#define SUBTITLETRACKDIALOG_H
#include <QDialog>
class QLineEdit;
class QComboBox;
class SubtitleTrackDialog : public QDialog
{
Q_OBJECT
public:
explicit SubtitleTrackDialog(const QString &name, const QString &lang, QWidget *parent);
QString getName();
QString getLanguage();
private slots:
void accept();
private:
QLineEdit *m_name;
QComboBox *m_lang;
};
#endif // SUBTITLETRACKDIALOG_H

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2020 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 <http://www.gnu.org/licenses/>.
*/
#include "systemsyncdialog.h"
#include "ui_systemsyncdialog.h"
#include "mltcontroller.h"
#include "settings.h"
SystemSyncDialog::SystemSyncDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::SystemSyncDialog)
, m_oldValue(Settings.playerVideoDelayMs())
{
ui->setupUi(this);
ui->syncSlider->setValue(Settings.playerVideoDelayMs());
ui->applyButton->hide();
}
SystemSyncDialog::~SystemSyncDialog()
{
delete ui;
}
void SystemSyncDialog::on_syncSlider_sliderReleased()
{
setDelay(ui->syncSlider->value());
}
void SystemSyncDialog::on_syncSpinBox_editingFinished()
{
ui->syncSlider->setValue(ui->syncSpinBox->value());
setDelay(ui->syncSpinBox->value());
}
void SystemSyncDialog::on_buttonBox_rejected()
{
setDelay(m_oldValue);
}
void SystemSyncDialog::on_undoButton_clicked()
{
ui->syncSlider->setValue(0);
setDelay(0);
}
void SystemSyncDialog::on_syncSpinBox_valueChanged(int arg1)
{
Q_UNUSED(arg1)
ui->applyButton->show();
}
void SystemSyncDialog::on_applyButton_clicked()
{
setDelay(ui->syncSpinBox->value());
}
void SystemSyncDialog::setDelay(int delay)
{
if (delay != Settings.playerVideoDelayMs()) {
Settings.setPlayerVideoDelayMs(delay);
MLT.consumerChanged();
}
ui->applyButton->hide();
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2020 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 <http://www.gnu.org/licenses/>.
*/
#ifndef SYSTEMSYNCDIALOG_H
#define SYSTEMSYNCDIALOG_H
#include <QDialog>
namespace Ui {
class SystemSyncDialog;
}
class SystemSyncDialog : public QDialog
{
Q_OBJECT
public:
explicit SystemSyncDialog(QWidget *parent = nullptr);
~SystemSyncDialog();
private slots:
void on_syncSlider_sliderReleased();
void on_syncSpinBox_editingFinished();
void on_buttonBox_rejected();
void on_undoButton_clicked();
void on_syncSpinBox_valueChanged(int arg1);
void on_applyButton_clicked();
private:
Ui::SystemSyncDialog *ui;
int m_oldValue;
void setDelay(int delay);
};
#endif // SYSTEMSYNCDIALOG_H

View File

@@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SystemSyncDialog</class>
<widget class="QDialog" name="SystemSyncDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>546</width>
<height>205</height>
</rect>
</property>
<property name="windowTitle">
<string>Player Synchronization</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Adjust your playback audio/video synchronization</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QSlider" name="syncSlider">
<property name="minimum">
<number>-250</number>
</property>
<property name="maximum">
<number>250</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="undoButton">
<property name="toolTip">
<string>Reset to default value 0</string>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="icon">
<iconset theme="edit-undo" resource="../../icons/resources.qrc">
<normaloff>:/icons/oxygen/32x32/actions/edit-undo.png</normaloff>:/icons/oxygen/32x32/actions/edit-undo.png</iconset>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="syncLabel">
<property name="text">
<string>Video offset</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="applyButton">
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QSpinBox" name="syncSpinBox">
<property name="suffix">
<string> ms</string>
</property>
<property name="minimum">
<number>-250</number>
</property>
<property name="maximum">
<number>250</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>130</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>syncSlider</tabstop>
</tabstops>
<resources>
<include location="../../icons/resources.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SystemSyncDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>227</x>
<y>280</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SystemSyncDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>295</x>
<y>286</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>syncSlider</sender>
<signal>valueChanged(int)</signal>
<receiver>syncSpinBox</receiver>
<slot>setValue(int)</slot>
<hints>
<hint type="sourcelabel">
<x>365</x>
<y>69</y>
</hint>
<hint type="destinationlabel">
<x>247</x>
<y>98</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,90 @@
/*
* Copyright (c) 2012-2024 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 <http://www.gnu.org/licenses/>.
*/
#include "textviewerdialog.h"
#include "ui_textviewerdialog.h"
#include "settings.h"
#include "util.h"
#include <QClipboard>
#include <QFileDialog>
#include <QPushButton>
#include <QScrollBar>
TextViewerDialog::TextViewerDialog(QWidget *parent, bool forMltXml)
: QDialog(parent)
, ui(new Ui::TextViewerDialog)
, m_forMltXml(forMltXml)
{
ui->setupUi(this);
auto button = ui->buttonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole);
connect(button, &QAbstractButton::clicked, this, [&]() {
QGuiApplication::clipboard()->setText(ui->plainTextEdit->toPlainText());
});
}
TextViewerDialog::~TextViewerDialog()
{
delete ui;
}
void TextViewerDialog::setText(const QString &s, bool scroll)
{
if (s != ui->plainTextEdit->toPlainText()) {
ui->plainTextEdit->setPlainText(s);
if (scroll)
ui->plainTextEdit->verticalScrollBar()->setValue(
ui->plainTextEdit->verticalScrollBar()->maximum());
}
}
QDialogButtonBox *TextViewerDialog::buttonBox() const
{
return ui->buttonBox;
}
void TextViewerDialog::on_buttonBox_accepted()
{
QString path = Settings.savePath();
QString caption = tr("Save Text");
QString nameFilter = tr("Text Documents (*.txt);;All Files (*)");
if (m_forMltXml) {
nameFilter = tr("MLT XML (*.mlt);;All Files (*)");
}
QString filename = QFileDialog::getSaveFileName(this,
caption,
path,
nameFilter,
nullptr,
Util::getFileDialogOptions());
if (!filename.isEmpty()) {
QFileInfo fi(filename);
if (fi.suffix().isEmpty()) {
if (m_forMltXml)
filename += ".mlt";
else
filename += ".txt";
}
if (Util::warnIfNotWritable(filename, this, caption))
return;
QFile f(filename);
f.open(QIODevice::WriteOnly | QIODevice::Text);
f.write(ui->plainTextEdit->toPlainText().toUtf8());
f.close();
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2012-2024 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 <http://www.gnu.org/licenses/>.
*/
#ifndef TEXTVIEWERDIALOG_H
#define TEXTVIEWERDIALOG_H
#include <QDialog>
class QDialogButtonBox;
namespace Ui {
class TextViewerDialog;
}
class TextViewerDialog : public QDialog
{
Q_OBJECT
public:
explicit TextViewerDialog(QWidget *parent = 0, bool forMltXml = false);
~TextViewerDialog();
void setText(const QString &s, bool scroll = false);
QDialogButtonBox *buttonBox() const;
private slots:
void on_buttonBox_accepted();
private:
Ui::TextViewerDialog *ui;
bool m_forMltXml;
};
#endif // TEXTVIEWERDIALOG_H

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TextViewerDialog</class>
<widget class="QDialog" name="TextViewerDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>709</width>
<height>398</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPlainTextEdit" name="plainTextEdit">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TextViewerDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TextViewerDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,167 @@
/*
* Copyright (c) 2017-2024 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 <http://www.gnu.org/licenses/>.
*/
#include "transcodedialog.h"
#include "ui_transcodedialog.h"
#include "mltcontroller.h"
#include "settings.h"
#include <QPushButton>
TranscodeDialog::TranscodeDialog(const QString &message, bool isProgressive, QWidget *parent)
: QDialog(parent)
, ui(new Ui::TranscodeDialog)
, m_format(0)
, m_isChecked(false)
, m_isProgressive(isProgressive)
{
ui->setupUi(this);
setWindowTitle(tr("Convert to Edit-friendly..."));
ui->messageLabel->setText(message);
ui->checkBox->hide();
ui->subclipCheckBox->hide();
ui->deinterlaceCheckBox->setChecked(false);
connect(ui->fpsCheckBox, SIGNAL(toggled(bool)), ui->fpsWidget, SLOT(setEnabled(bool)));
connect(ui->fpsCheckBox, SIGNAL(toggled(bool)), ui->fpsLabel, SLOT(setEnabled(bool)));
connect(ui->fpsCheckBox, SIGNAL(toggled(bool)), ui->frcComboBox, SLOT(setEnabled(bool)));
connect(ui->fpsCheckBox, SIGNAL(toggled(bool)), ui->frcLabel, SLOT(setEnabled(bool)));
ui->fpsCheckBox->setChecked(false);
ui->fpsWidget->setEnabled(false);
ui->fpsLabel->setEnabled(false);
ui->frcComboBox->setEnabled(false);
ui->frcLabel->setEnabled(false);
ui->fpsWidget->setFps(MLT.profile().fps());
ui->frcComboBox->addItem(tr("Duplicate (fast)"), QVariant("dup"));
ui->frcComboBox->addItem(tr("Blend"), QVariant("blend"));
ui->frcComboBox->addItem(tr("Motion Compensation (slow)"), QVariant("mci"));
ui->frcComboBox->setCurrentIndex(0);
QPushButton *advancedButton = new QPushButton(tr("Advanced"));
advancedButton->setCheckable(true);
connect(advancedButton, SIGNAL(toggled(bool)), ui->advancedWidget, SLOT(setVisible(bool)));
if (!Settings.convertAdvanced()) {
ui->advancedWidget->hide();
}
advancedButton->setChecked(Settings.convertAdvanced());
ui->advancedCheckBox->setChecked(Settings.convertAdvanced());
ui->buttonBox->addButton(advancedButton, QDialogButtonBox::ActionRole);
on_horizontalSlider_valueChanged(m_format);
}
TranscodeDialog::~TranscodeDialog()
{
delete ui;
}
void TranscodeDialog::showCheckBox()
{
ui->checkBox->show();
}
bool TranscodeDialog::deinterlace() const
{
return ui->deinterlaceCheckBox->isChecked();
}
bool TranscodeDialog::fpsOverride() const
{
return ui->fpsCheckBox->isChecked();
}
double TranscodeDialog::fps() const
{
return ui->fpsWidget->fps();
}
QString TranscodeDialog::frc() const
{
// Frame Rate Conversion Mode
return ui->frcComboBox->currentData().toString();
}
bool TranscodeDialog::get709Convert()
{
return ui->convert709CheckBox->isChecked();
}
void TranscodeDialog::set709Convert(bool enable)
{
ui->convert709CheckBox->setChecked(enable);
}
QString TranscodeDialog::sampleRate() const
{
QString sampleRate;
if (ui->sampleRateComboBox->currentIndex() == 1) {
sampleRate = "44100";
} else if (ui->sampleRateComboBox->currentIndex() == 2) {
sampleRate = "48000";
}
return sampleRate;
}
void TranscodeDialog::showSubClipCheckBox()
{
ui->subclipCheckBox->show();
}
bool TranscodeDialog::isSubClip() const
{
return ui->subclipCheckBox->isChecked();
}
void TranscodeDialog::setSubClipChecked(bool checked)
{
ui->subclipCheckBox->setChecked(checked);
}
void TranscodeDialog::setFrameRate(double fps)
{
ui->fpsCheckBox->setChecked(true);
ui->fpsWidget->setFps(fps);
}
void TranscodeDialog::on_horizontalSlider_valueChanged(int position)
{
switch (position) {
case 0:
ui->formatLabel->setText(tr("Lossy: I-frameonly %1").arg("H.264/AC-3 MP4"));
break;
case 1:
ui->formatLabel->setText(
tr("Intermediate: %1").arg(m_isProgressive ? "DNxHR/PCM MOV" : "ProRes/PCM MOV"));
break;
case 2:
ui->formatLabel->setText(tr("Lossless: %1").arg("Ut Video/PCM MKV"));
break;
}
m_format = position;
}
void TranscodeDialog::on_checkBox_clicked(bool checked)
{
m_isChecked = checked;
}
void TranscodeDialog::on_advancedCheckBox_clicked(bool checked)
{
Settings.setConvertAdvanced(checked);
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (c) 2017-2024 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 <http://www.gnu.org/licenses/>.
*/
#ifndef TRANSCODEDIALOG_H
#define TRANSCODEDIALOG_H
#include <QDialog>
namespace Ui {
class TranscodeDialog;
}
class TranscodeDialog : public QDialog
{
Q_OBJECT
public:
explicit TranscodeDialog(const QString &message, bool isProgressive, QWidget *parent = nullptr);
~TranscodeDialog();
int format() const { return m_format; }
void showCheckBox();
bool isCheckBoxChecked() const { return m_isChecked; }
bool deinterlace() const;
bool fpsOverride() const;
double fps() const;
QString frc() const;
bool get709Convert();
void set709Convert(bool enable);
QString sampleRate() const;
void showSubClipCheckBox();
bool isSubClip() const;
void setSubClipChecked(bool checked);
void setFrameRate(double fps);
private slots:
void on_horizontalSlider_valueChanged(int position);
void on_checkBox_clicked(bool checked);
void on_advancedCheckBox_clicked(bool checked);
private:
Ui::TranscodeDialog *ui;
int m_format;
bool m_isChecked;
bool m_isProgressive;
};
#endif // TRANSCODEDIALOG_H

View File

@@ -0,0 +1,344 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TranscodeDialog</class>
<widget class="QDialog" name="TranscodeDialog">
<property name="windowModality">
<enum>Qt::WindowModality::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>492</width>
<height>575</height>
</rect>
</property>
<property name="whatsThis">
<string notr="true">https://forum.shotcut.org/t/convert-to-edit-friendly-dialog/47720</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SizeConstraint::SetFixedSize</enum>
</property>
<item>
<widget class="QLabel" name="messageLabel">
<property name="text">
<string notr="true">messageLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>good</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>better</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>best</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSlider" name="horizontalSlider">
<property name="maximum">
<number>2</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>0</number>
</property>
<property name="sliderPosition">
<number>0</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksBothSides</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>medium</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>BIG</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>&lt;span style=&quot; font-weight:700; color:#ff0000;&quot;&gt;HUGE&lt;/span&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="formatLabel">
<property name="text">
<string notr="true">formatLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string comment="Convert to edit-friendly format dialog">Do not show this anymore.</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="advancedWidget" native="true">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,1">
<item row="4" column="1">
<widget class="FrameRateWidget" name="fpsWidget" native="true">
<property name="toolTip">
<string>Override the frame rate to a specific value.</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="convert709CheckBox">
<property name="toolTip">
<string>This is useful when the source video is HDR (High Dynamic Range), which requires tone-mapping to the old, standard range.</string>
</property>
<property name="text">
<string>Convert to BT.709 colorspace</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="subclipCheckBox">
<property name="toolTip">
<string>This option converts only the trimmed portion of the source
clip plus a little instead of the entire clip. When this option is
used not all of the matching source clips are replaced, instead
only the currently selected one.</string>
</property>
<property name="text">
<string>Use sub-clip</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="advancedCheckBox">
<property name="toolTip">
<string>Enable this to keep the Advanced section open for the next time this dialog appears.</string>
</property>
<property name="text">
<string>Keep Advanced open</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="frcComboBox">
<property name="toolTip">
<string>Frame rate conversion method
Duplicate: Duplicate frames.
Blend: Blend frames.
Motion Compensation: Interpolate new frames using motion compensation. This method is very slow and may result in artifacts.</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="fpsCheckBox">
<property name="toolTip">
<string>Change the frame rate from its source.</string>
</property>
<property name="text">
<string>Override frame rate</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="sampleRateLabel">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Sample rate</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="fpsLabel">
<property name="text">
<string>Frames/sec</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="deinterlaceCheckBox">
<property name="toolTip">
<string>If the source is interlaced, each interlaced field will be converted to a progressive frame resulting in double frame rate.</string>
</property>
<property name="text">
<string>Deinterlace</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="frcLabel">
<property name="text">
<string>Frame rate conversion</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="sampleRateComboBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="currentText">
<string>Same as original</string>
</property>
<item>
<property name="text">
<string>Same as original</string>
</property>
</item>
<item>
<property name="text">
<string>44100</string>
</property>
</item>
<item>
<property name="text">
<string>48000</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>FrameRateWidget</class>
<extends>QWidget</extends>
<header>widgets/frameratewidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TranscodeDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TranscodeDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,530 @@
/*
* Copyright (c) 2024-2025 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 <http://www.gnu.org/licenses/>.
*/
#include "transcribeaudiodialog.h"
#include "Logger.h"
#include "dialogs/filedownloaddialog.h"
#include "docks/timelinedock.h"
#include "mainwindow.h"
#include "models/extensionmodel.h"
#include "qmltypes/qmlapplication.h"
#include "shotcut_mlt_properties.h"
#include "util.h"
#include <MltProducer.h>
#include <QCheckBox>
#include <QClipboard>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QGridLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QSpinBox>
#include <QTreeView>
static const QString WHISPER_MODEL_EXTENSION_URL = QStringLiteral(
"https://check.shotcut.org/whispermodel.qml");
// List of supported languages from whispercpp
static const std::vector<const char *> whisperLanguages = {
"en", "zh", "de", "es", "ru", "ko", "fr", "ja", "pt", "tr", "pl", "ca", "nl", "ar", "sv",
"it", "id", "hi", "fi", "vi", "he", "uk", "el", "ms", "cs", "ro", "da", "hu", "ta", "no",
"th", "ur", "hr", "bg", "lt", "la", "mi", "ml", "cy", "sk", "te", "fa", "lv", "bn", "sr",
"az", "sl", "kn", "et", "mk", "br", "eu", "is", "hy", "ne", "mn", "bs", "kk", "sq", "sw",
"gl", "mr", "pa", "si", "km", "sn", "yo", "so", "af", "oc", "ka", "be", "tg", "sd", "gu",
"am", "yi", "lo", "uz", "fo", "ht", "ps", "tk", "nn", "mt", "sa", "lb", "my", "bo", "tl",
"mg", "as", "tt", "haw", "ln", "ha", "ba", "jw", "su", "yue",
};
static void fillLanguages(QComboBox *combo)
{
QMap<QString, QString> codeMap;
for (int i = 0; i < whisperLanguages.size(); i++) {
QString langCode = whisperLanguages[i];
QLocale::Language lang = QLocale::codeToLanguage(langCode);
if (lang == QLocale::AnyLanguage) {
LOG_ERROR() << "Language not found" << langCode;
continue;
}
QString langStr = QLocale::languageToString(lang);
if (!langCode.isEmpty() && !langStr.isEmpty()) {
codeMap.insert(langStr, langCode);
}
}
for (auto it = codeMap.keyValueBegin(); it != codeMap.keyValueEnd(); ++it) {
combo->addItem(it->first, it->second);
}
}
TranscribeAudioDialog::TranscribeAudioDialog(const QString &trackName, QWidget *parent)
: QDialog(parent)
{
m_model.load(QmlExtension::WHISPER_ID);
setWindowTitle(tr("Speech to Text"));
setWindowModality(QmlApplication::dialogModality());
Mlt::Producer *multitrack = MAIN.multitrack();
if (!multitrack || !multitrack->is_valid()) {
LOG_ERROR() << "Invalid multitrack";
return;
}
QGridLayout *grid = new QGridLayout();
grid->addWidget(new QLabel(tr("Name")), 0, 0, Qt::AlignRight);
m_name = new QLineEdit(this);
m_name->setText(trackName);
grid->addWidget(m_name, 0, 1);
grid->addWidget(new QLabel(tr("Language")), 1, 0, Qt::AlignRight);
m_lang = new QComboBox(this);
fillLanguages(m_lang);
// Try to set the default to the system language
QString currentLangCode = QLocale::languageToCode(QLocale::system().language(),
QLocale::ISO639Part1);
for (int i = 0; i < m_lang->count(); i++) {
if (m_lang->itemData(i).toString() == currentLangCode) {
m_lang->setCurrentIndex(i);
break;
}
}
// Fall back to English
if (m_lang->currentIndex() == -1) {
for (int i = 0; i < m_lang->count(); i++) {
if (m_lang->itemData(i).toString() == "en") {
m_lang->setCurrentIndex(i);
break;
}
}
}
grid->addWidget(m_lang, 1, 1);
m_translate = new QCheckBox(this);
m_translate->setCheckState(Qt::Unchecked);
grid->addWidget(m_translate, 2, 0, Qt::AlignRight);
grid->addWidget(new QLabel(tr("Translate to English")), 2, 1, Qt::AlignLeft);
grid->addWidget(new QLabel(tr("Maximum line length")), 3, 0, Qt::AlignRight);
m_maxLength = new QSpinBox(this);
m_maxLength->setRange(10, 100);
m_maxLength->setValue(42);
m_maxLength->setSuffix(" characters");
grid->addWidget(m_maxLength, 3, 1);
m_nonspoken = new QCheckBox(this);
m_nonspoken->setCheckState(Qt::Unchecked);
grid->addWidget(m_nonspoken, 4, 0, Qt::AlignRight);
grid->addWidget(new QLabel(tr("Include non-spoken sounds")), 4, 1, Qt::AlignLeft);
QLabel *tracksLabel = new QLabel(tr("Tracks with speech"));
tracksLabel->setToolTip(tr("Select tracks that contain speech to be transcribed."));
grid->addWidget(tracksLabel, 5, 0, Qt::AlignRight);
m_trackList = new QListWidget(this);
m_trackList->setSelectionMode(QAbstractItemView::NoSelection);
m_trackList->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow);
m_trackList->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
m_trackList->setToolTip(tracksLabel->toolTip());
Mlt::Tractor tractor(*multitrack);
if (!tractor.is_valid()) {
LOG_ERROR() << "Invalid tractor";
return;
}
TrackList trackList = MAIN.timelineDock()->model()->trackList();
if (trackList.size() == 0) {
LOG_ERROR() << "No tracks";
return;
}
for (int trackIndex = 0; trackIndex < trackList.size(); trackIndex++) {
std::unique_ptr<Mlt::Producer> track(tractor.track(trackList[trackIndex].mlt_index));
if (track) {
QString trackName = QString::fromUtf8(track->get(kTrackNameProperty));
if (!trackName.isEmpty()) {
QListWidgetItem *listItem = new QListWidgetItem(trackName, m_trackList);
if (track->get_int("hide") & 2) {
listItem->setCheckState(Qt::Unchecked);
} else {
listItem->setCheckState(Qt::Checked);
}
listItem->setData(Qt::UserRole, QVariant(trackList[trackIndex].mlt_index));
m_trackList->addItem(listItem);
}
}
}
grid->addWidget(m_trackList, 5, 1, Qt::AlignLeft);
// The config section is a single widget with a unique grid layout inside of it.
// The config section is hidden by hiding the config widget (and the layout it contains)
static const int maxPathWidth = 350;
m_configWidget = new QWidget(this);
QGridLayout *configLayout = new QGridLayout(this);
m_configWidget->setLayout(configLayout);
// Horizontal separator line
QFrame *line = new QFrame(m_configWidget);
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Sunken);
configLayout->addWidget(line, 0, 0, 1, 2);
// Whisper.cpp exe
configLayout->addWidget(new QLabel(tr("Whisper.cpp executable")), 1, 0, Qt::AlignRight);
m_exeLabel = new QLineEdit(this);
m_exeLabel->setFixedWidth(maxPathWidth);
m_exeLabel->setReadOnly(true);
configLayout->addWidget(m_exeLabel, 1, 1, Qt::AlignLeft);
QPushButton *exeBrowseButton = new QPushButton(this);
exeBrowseButton->setIcon(
QIcon::fromTheme("document-open", QIcon(":/icons/oxygen/32x32/actions/document-open.png")));
connect(exeBrowseButton, &QAbstractButton::clicked, this, [&] {
auto path = QFileDialog::getOpenFileName(this,
tr("Find Whisper.cpp"),
Settings.whisperExe(),
QString(),
nullptr,
Util::getFileDialogOptions());
if (QFileInfo(path).isExecutable()) {
Settings.setWhisperExe(path);
updateWhisperStatus();
}
});
configLayout->addWidget(exeBrowseButton, 1, 2, Qt::AlignLeft);
// Whisper.cpp model
configLayout->addWidget(new QLabel(tr("GGML Model")), 2, 0, Qt::AlignRight);
m_modelLabel = new QLineEdit(this);
m_modelLabel->setFixedWidth(maxPathWidth);
m_modelLabel->setPlaceholderText(tr("Select a model or browse to choose one"));
m_modelLabel->setReadOnly(true);
configLayout->addWidget(m_modelLabel, 2, 1, Qt::AlignLeft);
QPushButton *modelBrowseButton = new QPushButton(this);
modelBrowseButton->setIcon(
QIcon::fromTheme("document-open", QIcon(":/icons/oxygen/32x32/actions/document-open.png")));
connect(modelBrowseButton, &QAbstractButton::clicked, this, [&] {
auto path = QFileDialog::getOpenFileName(this,
tr("Find Whisper.cpp"),
Settings.whisperModel(),
"*.bin",
nullptr,
Util::getFileDialogOptions());
if (QFileInfo(path).exists()) {
LOG_INFO() << "Model found" << path;
Settings.setWhisperModel(path);
updateWhisperStatus();
} else {
LOG_INFO() << "Model not found" << path;
}
});
configLayout->addWidget(modelBrowseButton, 2, 2, Qt::AlignLeft);
// Update Model button
QPushButton *updateModelsButton = new QPushButton(tr("Refresh Models"), this);
connect(updateModelsButton,
&QAbstractButton::clicked,
this,
&TranscribeAudioDialog::refreshModels);
configLayout->addWidget(updateModelsButton, 3, 1, Qt::AlignLeft);
// List of models
m_table = new QTreeView();
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_table->setItemsExpandable(false);
m_table->setRootIsDecorated(false);
m_table->setUniformRowHeights(true);
m_table->setSortingEnabled(false);
m_table->setModel(&m_model);
m_table->setWordWrap(false);
m_table->header()->setStretchLastSection(false);
qreal rowHeight = fontMetrics().height() * devicePixelRatioF();
m_table->header()->setMinimumSectionSize(rowHeight);
m_table->header()->setSectionResizeMode(ExtensionModel::COLUMN_STATUS, QHeaderView::Fixed);
m_table->setColumnWidth(ExtensionModel::COLUMN_STATUS, rowHeight);
m_table->header()->setSectionResizeMode(ExtensionModel::COLUMN_NAME, QHeaderView::Stretch);
m_table->header()->setSectionResizeMode(ExtensionModel::COLUMN_SIZE, QHeaderView::Fixed);
m_table->setColumnWidth(ExtensionModel::COLUMN_SIZE,
fontMetrics().horizontalAdvance("XXX.XX XXX") * devicePixelRatioF()
+ 12);
connect(m_table, &QAbstractItemView::clicked, this, &TranscribeAudioDialog::onModelRowClicked);
m_table->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_table,
&QWidget::customContextMenuRequested,
this,
&TranscribeAudioDialog::showModelContextMenu);
configLayout->addWidget(m_table, 4, 0, 1, 3);
grid->addWidget(m_configWidget, 6, 0, 1, 2);
// Add a button box to the dialog
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
QPushButton *configButton = new QPushButton(tr("Configuration"));
configButton->setCheckable(true);
connect(configButton, &QPushButton::toggled, this, [&](bool checked) {
m_configWidget->setVisible(checked);
});
updateWhisperStatus();
QPushButton *okButton = m_buttonBox->button(QDialogButtonBox::Ok);
if (!m_buttonBox->button(QDialogButtonBox::Ok)->isEnabled()) {
// Show the config section
configButton->setChecked(true);
m_configWidget->setVisible(true);
} else {
configButton->setChecked(false);
m_configWidget->setVisible(false);
}
m_buttonBox->addButton(configButton, QDialogButtonBox::ActionRole);
grid->addWidget(m_buttonBox, 7, 0, 1, 2);
connect(m_buttonBox,
SIGNAL(clicked(QAbstractButton *)),
this,
SLOT(onButtonClicked(QAbstractButton *)));
setLayout(grid);
setModal(true);
layout()->setSizeConstraint(QLayout::SetFixedSize);
}
QList<int> TranscribeAudioDialog::tracks()
{
QList<int> tracks;
for (int i = 0; i < m_trackList->count(); i++) {
QListWidgetItem *item = m_trackList->item(i);
if (item && item->checkState() == Qt::Checked) {
tracks << item->data(Qt::UserRole).toInt();
}
}
return tracks;
}
void TranscribeAudioDialog::onButtonClicked(QAbstractButton *button)
{
QDialogButtonBox::ButtonRole role = m_buttonBox->buttonRole(button);
if (role == QDialogButtonBox::AcceptRole) {
LOG_DEBUG() << "Accept";
accept();
} else if (role == QDialogButtonBox::RejectRole) {
LOG_DEBUG() << "Reject";
reject();
} else {
LOG_DEBUG() << "Unknown role" << role;
}
}
void TranscribeAudioDialog::onModelRowClicked(const QModelIndex &index)
{
if (!m_model.downloaded(index.row())) {
QMessageBox qDialog(QMessageBox::Question,
tr("Download Model"),
tr("Are you sure you want to download %1?\n%2 of storage will be used")
.arg(m_model.getName(index.row()))
.arg(m_model.getFormattedDataSize(index.row())),
QMessageBox::No | QMessageBox::Yes,
this);
qDialog.setDefaultButton(QMessageBox::Yes);
qDialog.setEscapeButton(QMessageBox::No);
qDialog.setWindowModality(QmlApplication::dialogModality());
int result = qDialog.exec();
if (result == QMessageBox::Yes) {
downloadModel(index.row());
}
}
setCurrentModel(index.row());
updateWhisperStatus();
}
QString TranscribeAudioDialog::name()
{
return m_name->text();
}
QString TranscribeAudioDialog::language()
{
return m_lang->currentData().toString();
}
bool TranscribeAudioDialog::translate()
{
return m_translate->checkState() == Qt::Checked;
}
int TranscribeAudioDialog::maxLineLength()
{
return m_maxLength->value();
}
bool TranscribeAudioDialog::includeNonspoken()
{
return m_nonspoken->checkState() == Qt::Checked;
}
void TranscribeAudioDialog::showEvent(QShowEvent *event)
{
QDialog::showEvent(event);
bool modelFound = QFileInfo(Settings.whisperModel()).exists();
if (modelFound) {
return;
}
// No model is found. Offer to download one
QMessageBox qDialog(QMessageBox::Question,
tr("Download Model"),
tr("No models found. Download a standard model?"),
QMessageBox::No | QMessageBox::Yes,
this);
qDialog.setDefaultButton(QMessageBox::Yes);
qDialog.setEscapeButton(QMessageBox::No);
qDialog.setWindowModality(QmlApplication::dialogModality());
int result = qDialog.exec();
if (result == QMessageBox::Yes) {
refreshModels(false);
int index = m_model.getStandardIndex();
downloadModel(index);
setCurrentModel(index);
updateWhisperStatus();
}
}
void TranscribeAudioDialog::refreshModels(bool report)
{
QString localDst = QmlExtension::appDir(QmlExtension::WHISPER_ID)
.absoluteFilePath(QmlExtension::extensionFileName("whisper"));
FileDownloadDialog dlDialog(tr("Refresh Models"), this);
dlDialog.setSrc(WHISPER_MODEL_EXTENSION_URL);
dlDialog.setDst(localDst);
if (dlDialog.start() && report) {
m_model.load(QmlExtension::WHISPER_ID);
QMessageBox qDialog(QMessageBox::Information,
tr("Refresh Models"),
tr("Models refreshed"),
QMessageBox::Ok,
this);
qDialog.setWindowModality(QmlApplication::dialogModality());
qDialog.exec();
} else if (report) {
QMessageBox qDialog(QMessageBox::Critical,
tr("Refresh Models"),
tr("Failed to refresh models"),
QMessageBox::Ok,
this);
qDialog.setWindowModality(QmlApplication::dialogModality());
qDialog.exec();
}
}
void TranscribeAudioDialog::downloadModel(int index)
{
FileDownloadDialog dlDialog(tr("Download Model"), this);
dlDialog.setSrc(m_model.url(index));
dlDialog.setDst(m_model.localPath(index));
dlDialog.start();
}
void TranscribeAudioDialog::setCurrentModel(int index)
{
if (m_model.downloaded(index)) {
QString path = m_model.localPath(index);
if (QFileInfo(path).exists()) {
LOG_INFO() << "Model found" << path;
Settings.setWhisperModel(path);
} else {
LOG_ERROR() << "Model not found" << path;
}
} else {
LOG_ERROR() << "Model not downloaded" << m_model.getName(index);
}
}
void TranscribeAudioDialog::updateWhisperStatus()
{
bool exeFound = QFileInfo(Settings.whisperExe()).isExecutable();
bool modelFound = QFileInfo(Settings.whisperModel()).exists();
m_exeLabel->setText(Settings.whisperExe());
m_modelLabel->setText(Settings.whisperModel());
QPushButton *okButton = m_buttonBox->button(QDialogButtonBox::Ok);
if (!exeFound || !modelFound) {
// Disable the OK button;
okButton->setDisabled(true);
} else {
okButton->setDisabled(false);
}
if (exeFound) {
QPalette palette;
m_exeLabel->setPalette(palette);
m_exeLabel->setToolTip(tr("Path to Whisper.cpp executable"));
} else {
QPalette palette;
palette.setColor(QPalette::Text, Qt::red);
m_exeLabel->setPalette(palette);
m_exeLabel->setToolTip(tr("Whisper.cpp executable not found"));
}
if (modelFound) {
QPalette palette;
m_modelLabel->setPalette(palette);
m_modelLabel->setToolTip(tr("Path to GGML model"));
} else {
QPalette palette;
palette.setColor(QPalette::Text, Qt::red);
m_modelLabel->setPalette(palette);
if (m_modelLabel->text().isEmpty()) {
m_modelLabel->setText(m_modelLabel->placeholderText());
m_modelLabel->setToolTip(tr("Select a model"));
} else {
m_modelLabel->setToolTip(tr("GGML model not found"));
}
}
QModelIndex index = m_model.getIndexForPath(Settings.whisperModel());
m_table->setCurrentIndex(index);
}
void TranscribeAudioDialog::showModelContextMenu(QPoint p)
{
QModelIndex index = m_table->indexAt(p);
if (!index.isValid() || !m_model.downloaded(index.row())) {
updateWhisperStatus();
return;
}
QMenu *menu = new QMenu(tr("Model"));
QAction *action = new QAction(tr("Delete Model"), this);
connect(action, &QAction::triggered, this, [&]() { m_model.deleteFile(index.row()); });
QIcon icon = QIcon::fromTheme("edit-delete",
QIcon(":/icons/oxygen/32x32/actions/edit-delete.png"));
action->setIcon(icon);
menu->addAction(action);
action = new QAction(tr("Copy Model URL to Clipboard"), this);
connect(action, &QAction::triggered, this, [&]() {
QString url = m_model.url(index.row());
QGuiApplication::clipboard()->setText(url);
});
icon = QIcon::fromTheme("edit-copy", QIcon(":/icons/oxygen/32x32/actions/edit-copy.png"));
action->setIcon(icon);
menu->addAction(action);
menu->popup(QCursor::pos());
menu->exec();
updateWhisperStatus();
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2024-2025 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 <http://www.gnu.org/licenses/>.
*/
#ifndef TRANSCRIBEAUDIODIALOG_H
#define TRANSCRIBEAUDIODIALOG_H
#include "models/extensionmodel.h"
#include <QDialog>
class QAbstractButton;
class QCheckBox;
class QComboBox;
class QDialogButtonBox;
class QLineEdit;
class QListWidget;
class QSpinBox;
class QTreeView;
class TranscribeAudioDialog : public QDialog
{
Q_OBJECT
public:
explicit TranscribeAudioDialog(const QString &trackName, QWidget *parent);
QString name();
QString language();
QList<int> tracks();
bool translate();
int maxLineLength();
bool includeNonspoken();
protected:
virtual void showEvent(QShowEvent *event) override;
private slots:
void onButtonClicked(QAbstractButton *button);
void onModelRowClicked(const QModelIndex &index);
private:
void refreshModels(bool report = true);
void downloadModel(int index);
void setCurrentModel(int index);
void updateWhisperStatus();
void showModelContextMenu(QPoint p);
ExtensionModel m_model;
QLineEdit *m_name;
QComboBox *m_lang;
QCheckBox *m_translate;
QSpinBox *m_maxLength;
QCheckBox *m_nonspoken;
QListWidget *m_trackList;
QWidget *m_configWidget;
QLineEdit *m_exeLabel;
QLineEdit *m_modelLabel;
QDialogButtonBox *m_buttonBox;
QTreeView *m_table;
};
#endif // TRANSCRIBEAUDIODIALOG_H

View File

@@ -0,0 +1,155 @@
/*
* Copyright (c) 2016-2023 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 <http://www.gnu.org/licenses/>.
*/
#include "unlinkedfilesdialog.h"
#include "Logger.h"
#include "mltxmlchecker.h"
#include "settings.h"
#include "ui_unlinkedfilesdialog.h"
#include "util.h"
#include <QFileDialog>
#include <QStringList>
UnlinkedFilesDialog::UnlinkedFilesDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::UnlinkedFilesDialog)
{
ui->setupUi(this);
}
UnlinkedFilesDialog::~UnlinkedFilesDialog()
{
delete ui;
}
void UnlinkedFilesDialog::setModel(QStandardItemModel &model)
{
QStringList headers;
headers << tr("Missing");
headers << tr("Replacement");
model.setHorizontalHeaderLabels(headers);
ui->tableView->setModel(&model);
ui->tableView->resizeColumnsToContents();
}
void UnlinkedFilesDialog::on_tableView_doubleClicked(const QModelIndex &index)
{
// Use File Open dialog to choose a replacement.
QString path = Settings.openPath();
#ifdef Q_OS_MAC
path.append("/*");
#endif
QStringList filenames = QFileDialog::getOpenFileNames(this,
tr("Open File"),
path,
QString(),
nullptr,
Util::getFileDialogOptions());
if (filenames.length() > 0) {
QAbstractItemModel *model = ui->tableView->model();
QModelIndex firstColIndex = model->index(index.row(), MltXmlChecker::MissingColumn);
QModelIndex secondColIndex = model->index(index.row(), MltXmlChecker::ReplacementColumn);
QString hash = Util::getFileHash(filenames[0]);
if (hash == model->data(firstColIndex, MltXmlChecker::ShotcutHashRole)) {
// If the hashes match set icon to OK.
QIcon icon(":/icons/oxygen/32x32/status/task-complete.png");
model->setData(firstColIndex, icon, Qt::DecorationRole);
} else {
// Otherwise, set icon to warning.
QIcon icon(":/icons/oxygen/32x32/status/task-attempt.png");
model->setData(firstColIndex, icon, Qt::DecorationRole);
}
// Add chosen filename to the model.
QString filePath = QDir::toNativeSeparators(filenames[0]);
model->setData(secondColIndex, filePath);
model->setData(secondColIndex, filePath, Qt::ToolTipRole);
model->setData(secondColIndex, hash, MltXmlChecker::ShotcutHashRole);
QFileInfo fi(QFileInfo(filenames.first()));
Settings.setOpenPath(fi.path());
lookInDir(fi.dir());
}
}
bool UnlinkedFilesDialog::lookInDir(const QDir &dir, bool recurse)
{
LOG_DEBUG() << dir.canonicalPath();
// returns true if outstanding is > 0
unsigned outstanding = 0;
QAbstractItemModel *model = ui->tableView->model();
for (int row = 0; row < model->rowCount(); row++) {
QModelIndex replacementIndex = model->index(row, MltXmlChecker::ReplacementColumn);
if (model->data(replacementIndex, MltXmlChecker::ShotcutHashRole).isNull())
++outstanding;
}
if (outstanding) {
for (const auto &fileName :
dir.entryList(QDir::Files | QDir::Readable | QDir::NoDotAndDotDot)) {
QString hash = Util::getFileHash(dir.absoluteFilePath(fileName));
for (int row = 0; row < model->rowCount(); row++) {
QModelIndex replacementIndex = model->index(row, MltXmlChecker::ReplacementColumn);
if (model->data(replacementIndex, MltXmlChecker::ShotcutHashRole).isNull()) {
QModelIndex missingIndex = model->index(row, MltXmlChecker::MissingColumn);
QFileInfo missingInfo(model->data(missingIndex).toString());
QString missingHash
= model->data(missingIndex, MltXmlChecker::ShotcutHashRole).toString();
if (hash == missingHash || fileName == missingInfo.fileName()) {
if (hash == missingHash) {
QIcon icon(":/icons/oxygen/32x32/status/task-complete.png");
model->setData(missingIndex, icon, Qt::DecorationRole);
} else {
QIcon icon(":/icons/oxygen/32x32/status/task-attempt.png");
model->setData(missingIndex, icon, Qt::DecorationRole);
}
QString filePath = QDir::toNativeSeparators(dir.absoluteFilePath(fileName));
model->setData(replacementIndex, filePath);
model->setData(replacementIndex, filePath, Qt::ToolTipRole);
model->setData(replacementIndex, hash, MltXmlChecker::ShotcutHashRole);
QCoreApplication::processEvents();
if (--outstanding)
break;
else
return false;
}
}
}
}
}
if (outstanding && recurse) {
for (const QString &dirName :
dir.entryList(QDir::Dirs | QDir::Executable | QDir::NoDotAndDotDot)) {
if (!lookInDir(dir.absoluteFilePath(dirName), true))
break;
}
}
return outstanding;
}
void UnlinkedFilesDialog::on_searchFolderButton_clicked()
{
QString dirName = QFileDialog::getExistingDirectory(this,
windowTitle(),
Settings.openPath(),
Util::getFileDialogOptions());
if (!dirName.isEmpty()) {
Settings.setOpenPath(dirName);
lookInDir(dirName);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2016-1029 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 <http://www.gnu.org/licenses/>.
*/
#ifndef UNLINKEDFILESDIALOG_H
#define UNLINKEDFILESDIALOG_H
#include <QDialog>
#include <QDir>
#include <QStandardItemModel>
namespace Ui {
class UnlinkedFilesDialog;
}
class UnlinkedFilesDialog : public QDialog
{
Q_OBJECT
public:
explicit UnlinkedFilesDialog(QWidget *parent = 0);
~UnlinkedFilesDialog();
void setModel(QStandardItemModel &model);
private slots:
void on_tableView_doubleClicked(const QModelIndex &index);
void on_searchFolderButton_clicked();
private:
bool lookInDir(const QDir &dir, bool recurse = false);
Ui::UnlinkedFilesDialog *ui;
};
#endif // UNLINKEDFILESDIALOG_H

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>UnlinkedFilesDialog</class>
<widget class="QDialog" name="UnlinkedFilesDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>625</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Missing Files</string>
</property>
<property name="whatsThis">
<string notr="true">https://forum.shotcut.org/t/project-management/12574#p-39686-missing-files-7</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>There are missing files in your project. Double-click each row to locate a file.</string>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="tableView">
<property name="minimumSize">
<size>
<width>496</width>
<height>0</height>
</size>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="dragDropOverwriteMode">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="textElideMode">
<enum>Qt::TextElideMode::ElideLeft</enum>
</property>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderHighlightSections">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="searchFolderButton">
<property name="toolTip">
<string>This looks at every file in a folder to see if it matches any of the missing files.</string>
</property>
<property name="text">
<string>Search in Folder...</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>UnlinkedFilesDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>UnlinkedFilesDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

2984
src/docks/encodedock.cpp Normal file

File diff suppressed because it is too large Load Diff

205
src/docks/encodedock.h Normal file
View File

@@ -0,0 +1,205 @@
/*
* Copyright (c) 2012-2026 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 <http://www.gnu.org/licenses/>.
*/
#ifndef ENCODEDOCK_H
#define ENCODEDOCK_H
#include "settings.h"
#include <MltProperties.h>
#include <QDockWidget>
#include <QDomElement>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QStringList>
class QTreeWidgetItem;
class QTemporaryFile;
namespace Ui {
class EncodeDock;
}
class AbstractJob;
class MeltJob;
namespace Mlt {
class Service;
class Producer;
class Filter;
} // namespace Mlt
class PresetsProxyModel : public QSortFilterProxyModel
{
protected:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const;
};
class EncodeDock : public QDockWidget
{
Q_OBJECT
public:
explicit EncodeDock(QWidget *parent = 0);
~EncodeDock();
void loadPresetFromProperties(Mlt::Properties &);
bool isExportInProgress() const;
signals:
void captureStateChanged(bool);
void createOrEditFilterOnOutput(Mlt::Filter *, const QStringList & = {});
public slots:
void onAudioChannelsChanged();
void onProducerOpened();
void onProfileChanged();
void onReframeChanged();
void on_hwencodeButton_clicked();
bool detectHardwareEncoders();
private slots:
void on_presetsTree_clicked(const QModelIndex &index);
void on_presetsTree_activated(const QModelIndex &index);
void on_encodeButton_clicked();
void on_streamButton_clicked();
void on_addPresetButton_clicked();
void on_removePresetButton_clicked();
void onFinished(AbstractJob *, bool isSuccess);
void on_stopCaptureButton_clicked();
void on_videoRateControlCombo_activated(int index);
void on_audioRateControlCombo_activated(int index);
void on_scanModeCombo_currentIndexChanged(int index);
void on_presetsSearch_textChanged(const QString &search);
void on_resetButton_clicked();
void openCaptureFile();
void on_formatCombo_currentIndexChanged(int index);
void on_videoBufferDurationChanged();
void on_gopSpinner_valueChanged(int value);
void on_fromCombo_currentIndexChanged(int index);
void on_videoCodecCombo_currentIndexChanged(int index);
void on_audioCodecCombo_currentIndexChanged(int index);
void setAudioChannels(int channels);
void on_widthSpinner_editingFinished();
void on_heightSpinner_editingFinished();
void on_advancedButton_clicked(bool checked);
void on_hwencodeCheckBox_clicked(bool checked);
void on_hwdecodeCheckBox_clicked(bool checked);
void on_advancedCheckBox_clicked(bool checked);
void on_fpsSpinner_editingFinished();
void on_fpsComboBox_activated(int arg1);
void on_videoQualitySpinner_valueChanged(int vq);
void on_audioQualitySpinner_valueChanged(int aq);
void on_parallelCheckbox_clicked(bool checked);
void on_resolutionComboBox_activated(int arg1);
void on_reframeButton_clicked();
void on_aspectNumSpinner_valueChanged(int value);
void on_aspectDenSpinner_valueChanged(int value);
private:
enum {
RateControlAverage = 0,
RateControlConstant,
RateControlQuality,
RateControlConstrained
};
enum {
AudioChannels1 = 0,
AudioChannels2,
AudioChannels4,
AudioChannels6,
};
Ui::EncodeDock *ui;
Mlt::Properties *m_presets;
QScopedPointer<MeltJob> m_immediateJob;
QString m_extension;
Mlt::Properties *m_profiles;
PresetsProxyModel m_presetsModel;
QStringList m_outputFilenames;
bool m_isDefaultSettings;
double m_fps;
QStringList m_intraOnlyCodecs;
QStringList m_losslessVideoCodecs;
QStringList m_losslessAudioCodecs;
void loadPresets();
Mlt::Properties *collectProperties(int realtime, bool includeProfile = false);
void collectProperties(QDomElement &node, int realtime);
void setSubtitleProperties(QDomElement &node, Mlt::Producer *service);
QPoint addConsumerElement(
Mlt::Producer *service, QDomDocument &dom, const QString &target, int realtime, int pass);
MeltJob *convertReframe(Mlt::Producer *service,
QTemporaryFile *tmp,
const QString &target,
int realtime,
int pass,
const QThread::Priority priority);
MeltJob *createMeltJob(Mlt::Producer *service,
const QString &target,
int realtime,
int pass = 0,
const QThread::Priority priority = Settings.jobPriority());
void runMelt(const QString &target, int realtime = -1);
void enqueueAnalysis();
void enqueueMelt(const QStringList &targets, int realtime);
void encode(const QString &target);
void resetOptions();
Mlt::Producer *fromProducer(bool usePlaylistBin = false) const;
static void filterCodecParams(const QString &vcodec, QStringList &other);
void onVideoCodecComboChanged(int index, bool ignorePreset = false, bool resetBframes = true);
bool checkForMissingFiles();
QString &defaultFormatExtension();
void initSpecialCodecLists();
void setReframeEnabled(bool enabled);
void showResampleWarning(const QString &message);
void hideResampleWarning(bool hide = true);
void checkFrameRate();
void setResolutionAspectFromProfile();
};
#endif // ENCODEDOCK_H

2129
src/docks/encodedock.ui Normal file

File diff suppressed because it is too large Load Diff

1400
src/docks/filesdock.cpp Normal file

File diff suppressed because it is too large Load Diff

111
src/docks/filesdock.h Normal file
View File

@@ -0,0 +1,111 @@
/*
* Copyright (c) 2024-2025 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 <http://www.gnu.org/licenses/>.
*/
#ifndef FILESDOCK_H
#define FILESDOCK_H
#include <QDockWidget>
#include <QFileSystemModel>
#include <QHash>
#include <QMutex>
#include <QTimer>
#include <QUndoCommand>
namespace Ui {
class FilesDock;
}
class QAbstractItemView;
class QItemSelectionModel;
class QMenu;
class PlaylistIconView;
class FilesModel;
class FilesProxyModel;
class QSortFilterProxyModel;
class LineEditClear;
class QLabel;
class FilesDock : public QDockWidget
{
Q_OBJECT
public:
explicit FilesDock(QWidget *parent = 0);
~FilesDock();
struct CacheItem
{
int mediaType{-1}; // -1 = unknown
};
int getCacheMediaType(const QString &key);
void setCacheMediaType(const QString &key, int mediaType);
signals:
void selectionChanged();
public slots:
void onOpenActionTriggered();
void changeDirectory(const QString &path, bool updateLocation = true);
void changeFilesDirectory(const QModelIndex &index);
private slots:
void viewCustomContextMenuRequested(const QPoint &pos);
void onMediaTypeClicked();
void onOpenOtherAdd();
void onOpenOtherRemove();
void clearStatus();
void updateStatus();
void onLocationsEditingFinished();
void on_locationsCombo_activated(int index);
void on_addLocationButton_clicked();
void on_removeLocationButton_clicked();
protected:
void keyPressEvent(QKeyEvent *event);
void keyReleaseEvent(QKeyEvent *event);
private:
void setupActions();
void emitDataChanged(const QVector<int> &roles);
void updateViewMode();
void onUpdateThumbnailsActionTriggered();
void onSelectAllActionTriggered();
void incrementIndex(int step);
void addOpenWithMenu(QMenu *menu);
QString firstSelectedFilePath();
QString firstSelectedMediaType();
void openClip(const QString &filePath);
Ui::FilesDock *ui;
QAbstractItemView *m_view;
PlaylistIconView *m_iconsView;
std::unique_ptr<QFileSystemModel> m_dirsModel;
FilesModel *m_filesModel;
QItemSelectionModel *m_selectionModel;
QMenu *m_mainMenu;
FilesProxyModel *m_filesProxyModel;
QHash<QString, CacheItem> m_cache;
QMutex m_cacheMutex;
LineEditClear *m_searchField;
QLabel *m_label;
};
#endif // FILESDOCK_H

210
src/docks/filesdock.ui Normal file
View File

@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FilesDock</class>
<widget class="QDockWidget" name="FilesDock">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>460</width>
<height>278</height>
</rect>
</property>
<property name="windowTitle">
<string>Files</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="locationsLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Location</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="locationsCombo">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="addLocationButton">
<property name="maximumSize">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
<property name="toolTip">
<string>Add the current folder to the saved locations</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="list-add" resource="../../icons/resources.qrc">
<normaloff>:/icons/oxygen/32x32/actions/list-add.png</normaloff>:/icons/oxygen/32x32/actions/list-add.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeLocationButton">
<property name="maximumSize">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
<property name="toolTip">
<string>Remove the selected location</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="list-remove" resource="../../icons/resources.qrc">
<normaloff>:/icons/oxygen/32x32/actions/list-remove.png</normaloff>:/icons/oxygen/32x32/actions/list-remove.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="filtersLayout"/>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QTreeView" name="treeView">
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="animated">
<bool>true</bool>
</property>
</widget>
<widget class="QWidget" name="layoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="PlaylistTable" name="tableView">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="toolTip">
<string/>
</property>
<property name="dragEnabled">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>200</number>
</attribute>
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="PlaylistListView" name="listView">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::ExtendedSelection</enum>
</property>
<property name="movement">
<enum>QListView::Movement::Static</enum>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>PlaylistTable</class>
<extends>QTableView</extends>
<header>widgets/playlisttable.h</header>
</customwidget>
<customwidget>
<class>PlaylistListView</class>
<extends>QListView</extends>
<header>widgets/playlistlistview.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../icons/resources.qrc"/>
</resources>
<connections/>
</ui>

271
src/docks/filtersdock.cpp Normal file
View File

@@ -0,0 +1,271 @@
/*
* Copyright (c) 2013-2026 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 <http://www.gnu.org/licenses/>.
*/
#include "filtersdock.h"
#include "Logger.h"
#include "actions.h"
#include "controllers/filtercontroller.h"
#include "mainwindow.h"
#include "mltcontroller.h"
#include "models/attachedfiltersmodel.h"
#include "models/metadatamodel.h"
#include "models/motiontrackermodel.h"
#include "models/subtitlesmodel.h"
#include "qmltypes/qmlapplication.h"
#include "qmltypes/qmlfilter.h"
#include "qmltypes/qmlutilities.h"
#include "qmltypes/qmlview.h"
#include <QAction>
#include <QDir>
#include <QIcon>
#include <QMenu>
#include <QQmlContext>
#include <QQmlEngine>
#include <QQuickItem>
#include <QUrl>
#include <QtWidgets/QScrollArea>
FiltersDock::FiltersDock(MetadataModel *metadataModel,
AttachedFiltersModel *attachedModel,
MotionTrackerModel *motionTrackerModel,
SubtitlesModel *subtitlesModel,
QWidget *parent)
: QDockWidget(tr("Filters"), parent)
, m_qview(QmlUtilities::sharedEngine(), this)
{
LOG_DEBUG() << "begin";
setObjectName("FiltersDock");
setWhatsThis("https://forum.shotcut.org/t/about-filters/48127/1");
QIcon icon = QIcon::fromTheme("view-filter",
QIcon(":/icons/oxygen/32x32/actions/view-filter.png"));
toggleViewAction()->setIcon(icon);
setMinimumSize(200, 200);
setupActions();
m_qview.setFocusPolicy(Qt::StrongFocus);
m_qview.quickWindow()->setPersistentSceneGraph(false);
#ifndef Q_OS_MAC
m_qview.setAttribute(Qt::WA_AcceptTouchEvents);
#endif
setWidget(&m_qview);
QmlUtilities::setCommonProperties(m_qview.rootContext());
m_qview.rootContext()->setContextProperty("view", new QmlView(&m_qview));
m_qview.rootContext()->setContextProperty("metadatamodel", metadataModel);
m_qview.rootContext()->setContextProperty("motionTrackerModel", motionTrackerModel);
m_qview.rootContext()->setContextProperty("subtitlesModel", subtitlesModel);
m_qview.rootContext()->setContextProperty("attachedfiltersmodel", attachedModel);
m_qview.rootContext()->setContextProperty("producer", &m_producer);
connect(&m_producer, SIGNAL(seeked(int)), SIGNAL(seeked(int)));
connect(this, SIGNAL(producerInChanged(int)), &m_producer, SIGNAL(inChanged(int)));
connect(this, SIGNAL(producerOutChanged(int)), &m_producer, SIGNAL(outChanged(int)));
connect(m_qview.quickWindow(),
&QQuickWindow::sceneGraphInitialized,
this,
&FiltersDock::load,
Qt::QueuedConnection);
setCurrentFilter(0, 0, QmlFilter::NoCurrentFilter);
LOG_DEBUG() << "end";
}
void FiltersDock::setCurrentFilter(QmlFilter *filter, QmlMetadata *meta, int index)
{
if (filter && filter->producer().is_valid()) {
m_producer.setProducer(filter->producer());
if (mlt_service_playlist_type != filter->producer().type() && MLT.producer()
&& MLT.producer()->is_valid())
onSeeked(MLT.producer()->position());
} else {
Mlt::Producer emptyProducer(mlt_producer(0));
m_producer.setProducer(emptyProducer);
}
m_qview.rootContext()->setContextProperty("filter", filter);
m_qview.rootContext()->setContextProperty("metadata", meta);
if (filter)
connect(filter, SIGNAL(changed(QString)), SIGNAL(changed()));
else
disconnect(this, SIGNAL(changed()));
QMetaObject::invokeMethod(m_qview.rootObject(),
"setCurrentFilter",
Q_ARG(QVariant, QVariant(index)));
m_qview.setWhatsThis(meta ? meta->helpText() : QString());
}
bool FiltersDock::event(QEvent *event)
{
bool result = QDockWidget::event(event);
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange
|| event->type() == QEvent::Show) {
load();
}
return result;
}
void FiltersDock::keyPressEvent(QKeyEvent *event)
{
QDockWidget::keyPressEvent(event);
if (event->key() == Qt::Key_F) {
event->ignore();
} else if (event->key() == Qt::Key_Left || event->key() == Qt::Key_Right) {
event->accept();
}
}
void FiltersDock::onSeeked(int position)
{
if (m_producer.producer().is_valid()) {
if (MLT.isMultitrack()) {
// Make the position relative to clip's position on a timeline track.
position -= m_producer.producer().get_int(kPlaylistStartProperty);
} else {
// Make the position relative to the clip's in point.
position -= m_producer.in();
}
m_producer.seek(position);
}
}
void FiltersDock::onShowFrame(const SharedFrame &frame)
{
if (m_producer.producer().is_valid()) {
int position = frame.get_position();
onSeeked(position);
}
}
void FiltersDock::openFilterMenu() const
{
QMetaObject::invokeMethod(m_qview.rootObject(), "openFilterMenu");
}
void FiltersDock::showCopyFilterMenu()
{
QMenu menu;
menu.addAction(Actions["filtersCopyCurrentFilterAction"]);
menu.addAction(Actions["filtersCopyFiltersAction"]);
menu.addAction(Actions["filtersCopyAllFilterAction"]);
menu.exec(QCursor::pos());
}
void FiltersDock::onServiceInChanged(int delta, Mlt::Service *service)
{
if (delta && service && m_producer.producer().is_valid()
&& service->get_service() == m_producer.producer().get_service()) {
emit producerInChanged(delta);
}
}
void FiltersDock::load()
{
if (!m_qview.quickWindow()->isSceneGraphInitialized() && loadTries++ < 5) {
LOG_WARNING() << "scene graph not yet initialized";
QTimer::singleShot(300, this, &FiltersDock::load);
}
LOG_DEBUG() << "begin"
<< "isVisible" << isVisible() << "qview.status" << m_qview.status();
emit currentFilterRequested(QmlFilter::NoCurrentFilter);
QDir viewPath = QmlUtilities::qmlDir();
viewPath.cd("views");
viewPath.cd("filter");
m_qview.engine()->addImportPath(viewPath.path());
QDir modulePath = QmlUtilities::qmlDir();
modulePath.cd("modules");
m_qview.engine()->addImportPath(modulePath.path());
m_qview.setResizeMode(QQuickWidget::SizeRootObjectToView);
m_qview.quickWindow()->setColor(palette().window().color());
QUrl source = QUrl::fromLocalFile(viewPath.absoluteFilePath("filterview.qml"));
m_qview.setSource(source);
QObject::connect(m_qview.rootObject(),
SIGNAL(currentFilterRequested(int)),
SIGNAL(currentFilterRequested(int)));
QObject::connect(m_qview.rootObject(),
SIGNAL(copyFilterRequested()),
SLOT(showCopyFilterMenu()));
}
void FiltersDock::setupActions()
{
QIcon icon;
QAction *action;
action = new QAction(tr("Add"), this);
action->setShortcut(QKeySequence(Qt::Key_F));
action->setToolTip(tr("Choose a filter to add"));
icon = QIcon::fromTheme("list-add", QIcon(":/icons/oxygen/32x32/actions/list-add.png"));
action->setIcon(icon);
connect(action, &QAction::triggered, this, [=]() {
show();
raise();
m_qview.setFocus();
openFilterMenu();
});
addAction(action);
Actions.add("filtersAddFilterAction", action, windowTitle());
action = new QAction(tr("Remove"), this);
action->setShortcut(QKeySequence(Qt::SHIFT | Qt::Key_F));
action->setToolTip(tr("Remove selected filter"));
icon = QIcon::fromTheme("list-remove", QIcon(":/icons/oxygen/32x32/actions/list-remove.png"));
action->setIcon(icon);
connect(action, &QAction::triggered, this, [=]() { MAIN.filterController()->removeCurrent(); });
addAction(action);
Actions.add("filtersRemoveFilterAction", action, windowTitle());
action = new QAction(tr("Copy Enabled"), this);
action->setToolTip(tr("Copy checked filters to the clipboard"));
connect(action, &QAction::triggered, this, [=]() {
QmlApplication::singleton().copyEnabledFilters();
});
addAction(action);
Actions.add("filtersCopyFiltersAction", action, windowTitle());
action = new QAction(tr("Copy Current"), this);
action->setToolTip(tr("Copy current filter to the clipboard"));
connect(action, &QAction::triggered, this, [=]() {
QmlApplication::singleton().copyCurrentFilter();
});
addAction(action);
Actions.add("filtersCopyCurrentFilterAction", action, windowTitle());
action = new QAction(tr("Copy All"), this);
action->setToolTip(tr("Copy all filters to the clipboard"));
connect(action, &QAction::triggered, this, [=]() {
QmlApplication::singleton().copyAllFilters();
});
addAction(action);
Actions.add("filtersCopyAllFilterAction", action, windowTitle());
action = new QAction(tr("Paste Filters"), this);
action->setToolTip(tr("Paste the filters from the clipboard"));
icon = QIcon::fromTheme("edit-paste", QIcon(":/icons/oxygen/32x32/actions/edit-paste.png"));
action->setIcon(icon);
connect(action, &QAction::triggered, this, [=]() {
MAIN.filterController()->attachedModel()->pasteFilters();
});
addAction(action);
Actions.add("filtersPasteFiltersAction", action, windowTitle());
}

75
src/docks/filtersdock.h Normal file
View File

@@ -0,0 +1,75 @@
/*
* Copyright (c) 2013-2024 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 <http://www.gnu.org/licenses/>.
*/
#ifndef FILTERSDOCK_H
#define FILTERSDOCK_H
#include "qmltypes/qmlproducer.h"
#include "sharedframe.h"
#include <QDockWidget>
#include <QObject>
#include <QQuickWidget>
class QmlFilter;
class QmlMetadata;
class MetadataModel;
class AttachedFiltersModel;
class MotionTrackerModel;
class SubtitlesModel;
class FiltersDock : public QDockWidget
{
Q_OBJECT
public:
explicit FiltersDock(MetadataModel *metadataModel,
AttachedFiltersModel *attachedModel,
MotionTrackerModel *motionTrackerModel,
SubtitlesModel *subtitlesModel,
QWidget *parent = 0);
QmlProducer *qmlProducer() { return &m_producer; }
signals:
void currentFilterRequested(int attachedIndex);
void changed(); /// Notifies when a filter parameter changes.
void seeked(int);
void producerInChanged(int delta);
void producerOutChanged(int delta);
public slots:
void setCurrentFilter(QmlFilter *filter, QmlMetadata *meta, int index);
void onSeeked(int position);
void onShowFrame(const SharedFrame &frame);
void openFilterMenu() const;
void showCopyFilterMenu();
void onServiceInChanged(int delta, Mlt::Service *service);
void load();
protected:
bool event(QEvent *event);
void keyPressEvent(QKeyEvent *event);
private:
void setupActions();
QQuickWidget m_qview;
QmlProducer m_producer;
unsigned loadTries{0};
};
#endif // FILTERSDOCK_H

View File

@@ -0,0 +1,93 @@
/*
* Copyright (c) 2025 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 <http://www.gnu.org/licenses/>.
*/
#ifndef FIND_ANALYSIS_FILTER_PARSER_H
#define FIND_ANALYSIS_FILTER_PARSER_H
#include "qmltypes/qmlapplication.h"
#include <Mlt.h>
#include <QFile>
#include <QList>
#include <QString>
class FindAnalysisFilterParser : public Mlt::Parser
{
private:
QList<Mlt::Filter> m_filters;
bool m_skipAnalyzed;
public:
FindAnalysisFilterParser()
: Mlt::Parser()
, m_skipAnalyzed(true)
{}
QList<Mlt::Filter> &filters() { return m_filters; }
void skipAnalyzed(bool skip) { m_skipAnalyzed = skip; }
int on_start_filter(Mlt::Filter *filter)
{
QString serviceName = filter->get("mlt_service");
if (serviceName == "loudness" || serviceName == "vidstab") {
// If the results property does not exist, empty, or file does not exist.
QString results = filter->get("results");
if (results.isEmpty() || !m_skipAnalyzed) {
if (serviceName == "vidstab") {
// vidstab requires a filename, which is only available when using a project folder.
QString filename = filter->get("filename");
if (filename.isEmpty() || filename.endsWith("vidstab.trf")) {
filename = QmlApplication::getNextProjectFile("stab-");
}
if (!filename.isEmpty()) {
filter->set("filename", filename.toUtf8().constData());
m_filters << Mlt::Filter(*filter);
// Touch file to prevent overwriting the same file
QFile file(filename);
file.open(QIODevice::WriteOnly);
file.resize(0);
file.close();
}
} else {
m_filters << Mlt::Filter(*filter);
}
}
}
return 0;
}
int on_start_producer(Mlt::Producer *) { return 0; }
int on_end_producer(Mlt::Producer *) { return 0; }
int on_start_playlist(Mlt::Playlist *) { return 0; }
int on_end_playlist(Mlt::Playlist *) { return 0; }
int on_start_tractor(Mlt::Tractor *) { return 0; }
int on_end_tractor(Mlt::Tractor *) { return 0; }
int on_start_multitrack(Mlt::Multitrack *) { return 0; }
int on_end_multitrack(Mlt::Multitrack *) { return 0; }
int on_start_track() { return 0; }
int on_end_track() { return 0; }
int on_end_filter(Mlt::Filter *) { return 0; }
int on_start_transition(Mlt::Transition *) { return 0; }
int on_end_transition(Mlt::Transition *) { return 0; }
int on_start_chain(Mlt::Chain *) { return 0; }
int on_end_chain(Mlt::Chain *) { return 0; }
int on_start_link(Mlt::Link *) { return 0; }
int on_end_link(Mlt::Link *) { return 0; }
};
#endif // FIND_ANALYSIS_FILTER_PARSER_H

224
src/docks/jobsdock.cpp Normal file
View File

@@ -0,0 +1,224 @@
/*
* Copyright (c) 2012-2026 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 <http://www.gnu.org/licenses/>.
*/
#include "jobsdock.h"
#include "ui_jobsdock.h"
#include "Logger.h"
#include "dialogs/textviewerdialog.h"
#include "jobqueue.h"
#include <QtWidgets>
JobsDock::JobsDock(QWidget *parent)
: QDockWidget(parent)
, ui(new Ui::JobsDock)
{
LOG_DEBUG() << "begin";
ui->setupUi(this);
QIcon icon = QIcon::fromTheme("run-build", QIcon(":/icons/oxygen/32x32/actions/run-builld.png"));
toggleViewAction()->setIcon(icon);
ui->treeView->setModel(&JOBS);
QHeaderView *header = ui->treeView->header();
header->setStretchLastSection(false);
header->setSectionResizeMode(JobQueue::COLUMN_ICON, QHeaderView::Fixed);
ui->treeView->setColumnWidth(JobQueue::COLUMN_ICON, 24);
header->setSectionResizeMode(JobQueue::COLUMN_OUTPUT, QHeaderView::Stretch);
header->setSectionResizeMode(JobQueue::COLUMN_STATUS, QHeaderView::ResizeToContents);
ui->cleanButton->hide();
LOG_DEBUG() << "end";
}
JobsDock::~JobsDock()
{
JOBS.cleanup();
delete ui;
}
AbstractJob *JobsDock::currentJob() const
{
QModelIndex index = ui->treeView->currentIndex();
if (!index.isValid())
return 0;
return JOBS.jobFromIndex(index);
}
void JobsDock::onJobAdded()
{
QModelIndex index = JOBS.index(JOBS.rowCount() - 1, JobQueue::COLUMN_OUTPUT);
QProgressBar *progressBar = new QProgressBar;
progressBar->setMinimum(0);
progressBar->setMaximum(100);
progressBar->setAutoFillBackground(true);
progressBar->setTextVisible(false);
QHBoxLayout *layout = new QHBoxLayout(progressBar);
QLabel *label = new QLabel;
layout->addWidget(label);
layout->setContentsMargins(0, 0, 0, 0);
ui->treeView->setIndexWidget(index, progressBar);
ui->treeView->resizeColumnToContents(JobQueue::COLUMN_STATUS);
label->setToolTip(JOBS.data(index).toString());
label->setText(
label->fontMetrics().elidedText(JOBS.data(index).toString(),
Qt::ElideMiddle,
ui->treeView->columnWidth(JobQueue::COLUMN_OUTPUT)));
connect(JOBS.jobFromIndex(index),
SIGNAL(progressUpdated(QStandardItem *, int)),
SLOT(onProgressUpdated(QStandardItem *, int)));
show();
raise();
}
void JobsDock::onProgressUpdated(QStandardItem *item, int percent)
{
if (item) {
QModelIndex index = JOBS.index(item->row(), JobQueue::COLUMN_OUTPUT);
QProgressBar *progressBar = qobject_cast<QProgressBar *>(ui->treeView->indexWidget(index));
if (progressBar && percent > progressBar->value())
progressBar->setValue(percent);
}
}
void JobsDock::resizeEvent(QResizeEvent *event)
{
QDockWidget::resizeEvent(event);
foreach (QLabel *label, ui->treeView->findChildren<QLabel *>()) {
label->setText(
label->fontMetrics().elidedText(label->toolTip(),
Qt::ElideMiddle,
ui->treeView->columnWidth(JobQueue::COLUMN_OUTPUT)));
}
}
void JobsDock::on_treeView_customContextMenuRequested(const QPoint &pos)
{
QModelIndex index = ui->treeView->currentIndex();
QMenu menu(this);
AbstractJob *job = index.isValid() ? JOBS.jobFromIndex(index) : nullptr;
if (job) {
if (job->ran() && job->state() == QProcess::NotRunning
&& job->exitStatus() == QProcess::NormalExit) {
menu.addActions(job->successActions());
}
if (job->stopped() || (JOBS.isPaused() && !job->ran()))
menu.addAction(ui->actionRun);
if (job->state() == QProcess::Running)
menu.addAction(ui->actionStopJob);
else
menu.addAction(ui->actionRemove);
if (job->ran())
menu.addAction(ui->actionViewLog);
menu.addActions(job->standardActions());
}
for (auto job : JOBS.jobs()) {
if (job->ran() && job->state() != QProcess::Running) {
menu.addAction(ui->actionRemoveFinished);
break;
}
}
menu.exec(mapToGlobal(pos));
}
void JobsDock::on_actionStopJob_triggered()
{
QModelIndex index = ui->treeView->currentIndex();
if (!index.isValid())
return;
AbstractJob *job = JOBS.jobFromIndex(index);
if (job)
job->stop();
}
void JobsDock::on_actionViewLog_triggered()
{
QModelIndex index = ui->treeView->currentIndex();
if (!index.isValid())
return;
AbstractJob *job = JOBS.jobFromIndex(index);
if (job) {
TextViewerDialog dialog(this);
dialog.setWindowTitle(tr("Job Log"));
dialog.setText(job->log());
auto connection = connect(job, &AbstractJob::progressUpdated, this, [&]() {
dialog.setText(job->log(), true);
});
dialog.exec();
disconnect(connection);
}
}
void JobsDock::on_pauseButton_toggled(bool checked)
{
if (checked)
JOBS.pause();
else
JOBS.resume();
}
void JobsDock::on_actionRun_triggered()
{
QModelIndex index = ui->treeView->currentIndex();
if (!index.isValid())
return;
AbstractJob *job = JOBS.jobFromIndex(index);
if (job)
job->start();
}
void JobsDock::on_menuButton_clicked()
{
on_treeView_customContextMenuRequested(ui->menuButton->mapToParent(QPoint(0, 0)));
}
void JobsDock::on_treeView_doubleClicked(const QModelIndex &index)
{
AbstractJob *job = JOBS.jobFromIndex(index);
if (job && job->ran() && job->state() == QProcess::NotRunning
&& job->exitStatus() == QProcess::NormalExit) {
foreach (QAction *action, job->successActions()) {
if (action->data() == "Open") {
action->trigger();
break;
}
}
}
}
void JobsDock::on_actionRemove_triggered()
{
QModelIndex index = ui->treeView->currentIndex();
if (!index.isValid())
return;
JOBS.remove(index);
}
void JobsDock::on_actionRemoveFinished_triggered()
{
JOBS.removeFinished();
}
void JobsDock::on_JobsDock_visibilityChanged(bool visible)
{
if (visible) {
foreach (QLabel *label, ui->treeView->findChildren<QLabel *>()) {
label->setText(label->fontMetrics().elidedText(label->toolTip(),
Qt::ElideMiddle,
ui->treeView->columnWidth(
JobQueue::COLUMN_OUTPUT)));
}
}
}

62
src/docks/jobsdock.h Normal file
View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2012-2020 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 <http://www.gnu.org/licenses/>.
*/
#ifndef JOBSDOCK_H
#define JOBSDOCK_H
#include <QDockWidget>
class AbstractJob;
class QStandardItem;
namespace Ui {
class JobsDock;
}
class JobsDock : public QDockWidget
{
Q_OBJECT
public:
explicit JobsDock(QWidget *parent = 0);
~JobsDock();
AbstractJob *currentJob() const;
public slots:
void onJobAdded();
void onProgressUpdated(QStandardItem *item, int percent);
protected:
void resizeEvent(QResizeEvent *event);
private:
Ui::JobsDock *ui;
private slots:
void on_treeView_customContextMenuRequested(const QPoint &pos);
void on_actionStopJob_triggered();
void on_actionViewLog_triggered();
void on_pauseButton_toggled(bool checked);
void on_actionRun_triggered();
void on_menuButton_clicked();
void on_treeView_doubleClicked(const QModelIndex &index);
void on_actionRemove_triggered();
void on_actionRemoveFinished_triggered();
void on_JobsDock_visibilityChanged(bool visible);
};
#endif // JOBSDOCK_H

195
src/docks/jobsdock.ui Normal file
View File

@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>JobsDock</class>
<widget class="QDockWidget" name="JobsDock">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>283</width>
<height>279</height>
</rect>
</property>
<property name="whatsThis">
<string notr="true">https://forum.shotcut.org/t/jobs-panel/12945/1</string>
</property>
<property name="windowTitle">
<string>Jobs</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>283</width>
<height>219</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTreeView" name="treeView">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="textElideMode">
<enum>Qt::TextElideMode::ElideMiddle</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>6</number>
</property>
<item>
<widget class="QPushButton" name="menuButton">
<property name="toolTip">
<string>Jobs Menu</string>
</property>
<property name="icon">
<iconset theme="show-menu" resource="../../icons/resources.qrc">
<normaloff>:/icons/light/32x32/show-menu.png</normaloff>:/icons/light/32x32/show-menu.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pauseButton">
<property name="toolTip">
<string>Stop automatically processing the next pending job in
the list. This does not stop a currently running job. Right-
-click a job to open a menu to stop a currently running job.</string>
</property>
<property name="text">
<string>Pause Queue</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cleanButton">
<property name="toolTip">
<string>Remove all of the completed and failed jobs from the list</string>
</property>
<property name="text">
<string>Clean</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<action name="actionStopJob">
<property name="text">
<string>Stop This Job</string>
</property>
<property name="toolTip">
<string>Stop the currently selected job</string>
</property>
</action>
<action name="actionViewLog">
<property name="text">
<string>View Log</string>
</property>
<property name="toolTip">
<string>View the messages of MLT and FFmpeg </string>
</property>
</action>
<action name="actionRun">
<property name="text">
<string>Run</string>
</property>
<property name="toolTip">
<string>Restart a stopped job</string>
</property>
</action>
<action name="actionRemove">
<property name="text">
<string>Remove</string>
</property>
</action>
<action name="actionRemoveFinished">
<property name="text">
<string>Remove Finished</string>
</property>
<property name="toolTip">
<string>Remove Finished</string>
</property>
</action>
</widget>
<resources>
<include location="../../icons/resources.qrc"/>
</resources>
<connections/>
</ui>

1235
src/docks/keyframesdock.cpp Normal file

File diff suppressed because it is too large Load Diff

93
src/docks/keyframesdock.h Normal file
View File

@@ -0,0 +1,93 @@
/*
* Copyright (c) 2016-2023 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEYFRAMESDOCK_H
#define KEYFRAMESDOCK_H
#include "models/keyframesmodel.h"
#include "qmltypes/qmlfilter.h"
#include <QDockWidget>
#include <QQuickWidget>
#include <QScopedPointer>
class QmlFilter;
class QmlMetadata;
class AttachedFiltersModel;
class QmlProducer;
class QMenu;
class KeyframesDock : public QDockWidget
{
Q_OBJECT
Q_PROPERTY(double timeScale READ timeScale WRITE setTimeScale NOTIFY timeScaleChanged)
public:
explicit KeyframesDock(QmlProducer *qmlProducer, QWidget *parent = 0);
KeyframesModel &model() { return m_model; }
Q_INVOKABLE int seekPrevious();
Q_INVOKABLE int seekNext();
int currentParameter() const;
double timeScale() const { return m_timeScale; }
void setTimeScale(double value);
signals:
void changed(); /// Notifies when a filter parameter changes.
void setZoom(double value);
void zoomIn();
void zoomOut();
void zoomToFit();
void resetZoom();
void seekPreviousSimple();
void seekNextSimple();
void newFilter(); // Notifies when the filter itself has been changed
void timeScaleChanged();
void dockClicked();
public slots:
void setCurrentFilter(QmlFilter *filter, QmlMetadata *meta);
void load(bool force = false);
void reload();
void onProducerModified();
protected:
bool event(QEvent *event);
void keyPressEvent(QKeyEvent *event);
void keyReleaseEvent(QKeyEvent *event);
private slots:
void onDockRightClicked();
void onKeyframeRightClicked();
void onClipRightClicked();
private:
void setupActions();
QQuickWidget m_qview;
KeyframesModel m_model;
QmlMetadata *m_metadata;
QmlFilter *m_filter;
QmlProducer *m_qmlProducer;
QMenu *m_mainMenu;
QMenu *m_keyMenu;
QMenu *m_keyTypePrevMenu;
QMenu *m_keyTypeNextMenu;
QMenu *m_clipMenu;
double m_timeScale{1.0};
};
#endif // KEYFRAMESDOCK_H

464
src/docks/markersdock.cpp Normal file
View File

@@ -0,0 +1,464 @@
/*
* Copyright (c) 2021-2026 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 <http://www.gnu.org/licenses/>.
*/
#include "markersdock.h"
#include "Logger.h"
#include "actions.h"
#include "mainwindow.h"
#include "models/markersmodel.h"
#include "settings.h"
#include "util.h"
#include "widgets/docktoolbar.h"
#include "widgets/editmarkerwidget.h"
#include <QAction>
#include <QDebug>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QIcon>
#include <QLineEdit>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
#include <QSortFilterProxyModel>
#include <QSpacerItem>
#include <QStyledItemDelegate>
#include <QToolButton>
#include <QTreeView>
#include <QVBoxLayout>
#include <QtWidgets/QScrollArea>
class ColorItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
ColorItemDelegate(QAbstractItemView *view, QWidget *parent = nullptr)
: QStyledItemDelegate(parent)
, m_view(view)
{}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
const auto color = index.data(MarkersModel::ColorRole).value<QColor>();
const auto textColor(Util::textColor(color));
painter->fillRect(option.rect, color);
const auto point = option.rect.topLeft()
+ QPoint(2 * m_view->devicePixelRatioF(),
option.fontMetrics.ascent() + m_view->devicePixelRatioF());
painter->setPen(textColor);
painter->drawText(point, color.name());
}
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
Q_UNUSED(index);
return QSize(m_view->viewport()->width(),
option.fontMetrics.height() + 2 * m_view->devicePixelRatioF());
}
private:
QAbstractItemView *m_view;
};
class MarkerTreeView : public QTreeView
{
Q_OBJECT
public:
// Make this function public
using QTreeView::selectedIndexes;
void blockSelectionEvent(bool block) { m_blockSelectionEvent = block; }
signals:
void rowClicked(const QModelIndex &index);
void markerSelected(QModelIndex &index);
protected:
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
QTreeView::selectionChanged(selected, deselected);
if (!m_blockSelectionEvent) {
QModelIndex signalIndex;
QModelIndexList indices = selectedIndexes();
if (indices.size() > 0) {
signalIndex = indices[0];
}
emit markerSelected(signalIndex);
}
}
void mouseReleaseEvent(QMouseEvent *event)
{
QTreeView::mouseReleaseEvent(event);
QModelIndex signalIndex = indexAt(event->pos());
if (signalIndex.isValid()) {
emit rowClicked(signalIndex);
}
}
private:
bool m_blockSelectionEvent = false;
};
// Include this so that MarkerTreeView can be declared in the source file.
#include "markersdock.moc"
MarkersDock::MarkersDock(QWidget *parent)
: QDockWidget(parent)
, m_model(nullptr)
, m_proxyModel(nullptr)
, m_editInProgress(false)
{
LOG_DEBUG() << "begin";
setObjectName("MarkersDock");
QDockWidget::setWindowTitle(tr("Markers"));
QIcon icon = QIcon::fromTheme("marker", QIcon(":/icons/oxygen/32x32/actions/marker.png"));
toggleViewAction()->setIcon(icon);
setWhatsThis("https://forum.shotcut.org/t/timeline-markers/30535/1");
QScrollArea *scrollArea = new QScrollArea();
scrollArea->setFrameShape(QFrame::NoFrame);
scrollArea->setWidgetResizable(true);
QDockWidget::setWidget(scrollArea);
QVBoxLayout *vboxLayout = new QVBoxLayout();
scrollArea->setLayout(vboxLayout);
m_treeView = new MarkerTreeView();
m_treeView->setItemDelegateForColumn(0, new ColorItemDelegate(m_treeView));
m_treeView->setItemsExpandable(false);
m_treeView->setRootIsDecorated(false);
m_treeView->setUniformRowHeights(true);
m_treeView->setSortingEnabled(true);
connect(m_treeView,
SIGNAL(markerSelected(QModelIndex &)),
this,
SLOT(onSelectionChanged(QModelIndex &)));
connect(m_treeView,
SIGNAL(rowClicked(const QModelIndex &)),
this,
SLOT(onRowClicked(const QModelIndex &)));
vboxLayout->addWidget(m_treeView, 1);
QMenu *mainMenu = new QMenu("Markers", this);
mainMenu->addAction(Actions["timelineMarkerAction"]);
mainMenu->addAction(Actions["timelinePrevMarkerAction"]);
mainMenu->addAction(Actions["timelineNextMarkerAction"]);
mainMenu->addAction(Actions["timelineDeleteMarkerAction"]);
mainMenu->addAction(Actions["timelineMarkSelectedClipAction"]);
mainMenu->addAction(Actions["timelineCycleMarkerColorAction"]);
mainMenu->addAction(tr("Remove All Markers"), this, SLOT(onRemoveAllRequested()));
QAction *action;
QMenu *columnsMenu = new QMenu(tr("Columns"), this);
action = columnsMenu->addAction(tr("Color"), this, SLOT(onColorColumnToggled(bool)));
action->setCheckable(true);
action->setChecked(Settings.markersShowColumn("color"));
action = columnsMenu->addAction(tr("Name"), this, SLOT(onTextColumnToggled(bool)));
action->setCheckable(true);
action->setChecked(Settings.markersShowColumn("text"));
action = columnsMenu->addAction(tr("Start"), this, SLOT(onStartColumnToggled(bool)));
action->setCheckable(true);
action->setChecked(Settings.markersShowColumn("start"));
action = columnsMenu->addAction(tr("End"), this, SLOT(onEndColumnToggled(bool)));
action->setCheckable(true);
action->setChecked(Settings.markersShowColumn("end"));
action = columnsMenu->addAction(tr("Duration"), this, SLOT(onDurationColumnToggled(bool)));
action->setCheckable(true);
action->setChecked(Settings.markersShowColumn("duration"));
mainMenu->addMenu(columnsMenu);
Actions.loadFromMenu(mainMenu);
DockToolBar *toolbar = new DockToolBar(tr("Markers Controls"));
toolbar->setAreaHint(Qt::BottomToolBarArea);
QToolButton *menuButton = new QToolButton(this);
menuButton->setIcon(
QIcon::fromTheme("show-menu", QIcon(":/icons/oxygen/32x32/actions/show-menu.png")));
menuButton->setToolTip(tr("Markers Menu"));
menuButton->setAutoRaise(true);
menuButton->setMenu(mainMenu);
menuButton->setPopupMode(QToolButton::QToolButton::InstantPopup);
toolbar->addWidget(menuButton);
m_addButton = new QToolButton(this);
m_addButton->setIcon(
QIcon::fromTheme("list-add", QIcon(":/icons/oxygen/32x32/actions/list-add.png")));
m_addButton->setToolTip(tr("Add a marker at the current time"));
m_addButton->setAutoRaise(true);
if (!connect(m_addButton, &QAbstractButton::clicked, this, &MarkersDock::onAddRequested))
connect(m_addButton, SIGNAL(clicked()), SLOT(onAddRequested()));
toolbar->addWidget(m_addButton);
m_removeButton = new QToolButton(this);
m_removeButton->setIcon(
QIcon::fromTheme("list-remove", QIcon(":/icons/oxygen/32x32/actions/list-remove.png")));
m_removeButton->setToolTip(tr("Remove the selected marker"));
m_removeButton->setAutoRaise(true);
if (!connect(m_removeButton, &QAbstractButton::clicked, this, &MarkersDock::onRemoveRequested))
connect(m_removeButton, SIGNAL(clicked()), SLOT(onRemoveRequested()));
toolbar->addWidget(m_removeButton);
m_clearButton = new QToolButton(this);
m_clearButton->setIcon(
QIcon::fromTheme("window-close", QIcon(":/icons/oxygen/32x32/actions/window-close.png")));
m_clearButton->setToolTip(tr("Deselect the marker"));
m_clearButton->setAutoRaise(true);
if (!connect(m_clearButton,
&QAbstractButton::clicked,
this,
&MarkersDock::onClearSelectionRequested))
connect(m_clearButton, SIGNAL(clicked()), SLOT(onClearSelectionRequested()));
toolbar->addWidget(m_clearButton);
m_searchField = new QLineEdit(this);
m_searchField->setPlaceholderText(tr("search"));
if (!connect(m_searchField, &QLineEdit::textChanged, this, &MarkersDock::onSearchChanged))
connect(m_searchField, SIGNAL(textChanged(const QString &)), SLOT(onSearchChanged()));
toolbar->addWidget(m_searchField);
m_clearSearchButton = new QToolButton(this);
m_clearSearchButton->setIcon(
QIcon::fromTheme("edit-clear", QIcon(":/icons/oxygen/32x32/actions/edit-clear.png")));
m_clearSearchButton->setToolTip(tr("Clear search"));
m_clearSearchButton->setAutoRaise(true);
if (!connect(m_clearSearchButton, &QAbstractButton::clicked, m_searchField, &QLineEdit::clear))
connect(m_clearSearchButton, SIGNAL(clicked()), m_searchField, SLOT(clear()));
toolbar->addWidget(m_clearSearchButton);
vboxLayout->addWidget(toolbar);
enableButtons(false);
m_editMarkerWidget = new EditMarkerWidget(this, "", "", 0, 0, 0);
m_editMarkerWidget->setVisible(false);
connect(m_editMarkerWidget, SIGNAL(valuesChanged()), SLOT(onValuesChanged()));
vboxLayout->addWidget(m_editMarkerWidget);
vboxLayout->addStretch();
LOG_DEBUG() << "end";
}
MarkersDock::~MarkersDock() {}
void MarkersDock::setModel(MarkersModel *model)
{
m_treeView->blockSelectionEvent(true);
m_model = model;
m_proxyModel = new QSortFilterProxyModel(this);
m_proxyModel->setSourceModel(m_model);
m_proxyModel->setFilterKeyColumn(1);
m_treeView->setModel(m_proxyModel);
m_treeView->setColumnHidden(0, !Settings.markersShowColumn("color"));
m_treeView->setColumnHidden(1, !Settings.markersShowColumn("text"));
m_treeView->setColumnHidden(2, !Settings.markersShowColumn("start"));
m_treeView->setColumnHidden(3, !Settings.markersShowColumn("end"));
m_treeView->setColumnHidden(4, !Settings.markersShowColumn("duration"));
m_treeView->sortByColumn(Settings.getMarkerSortColumn(), Settings.getMarkerSortOrder());
connect(m_model,
SIGNAL(rowsInserted(const QModelIndex &, int, int)),
this,
SLOT(onRowsInserted(const QModelIndex &, int, int)));
connect(m_model,
SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector<int> &)),
this,
SLOT(onDataChanged(const QModelIndex &, const QModelIndex &, const QVector<int> &)));
connect(m_model, SIGNAL(modelReset()), this, SLOT(onModelReset()));
connect(m_treeView->header(),
SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)),
this,
SLOT(onSortIndicatorChanged(int, Qt::SortOrder)));
m_treeView->blockSelectionEvent(false);
}
void MarkersDock::onMarkerSelectionRequest(int markerIndex)
{
QModelIndex sourceIndex = m_model->modelIndexForRow(markerIndex);
QModelIndex insertedIndex = m_proxyModel->mapFromSource(sourceIndex);
if (insertedIndex.isValid()) {
m_treeView->setCurrentIndex(insertedIndex);
}
}
void MarkersDock::onSelectionChanged(QModelIndex &index)
{
if (m_model && m_proxyModel && MAIN.multitrack() && index.isValid()) {
QModelIndex realIndex = m_proxyModel->mapToSource(index);
if (realIndex.isValid()) {
Markers::Marker marker = m_model->getMarker(realIndex.row());
enableButtons(true);
m_editMarkerWidget->setVisible(true);
QSignalBlocker editBlocker(m_editMarkerWidget);
m_editMarkerWidget->setValues(marker.text,
marker.color,
marker.start,
marker.end,
MAIN.multitrack()->get_length() - 1);
return;
}
}
m_editMarkerWidget->setVisible(false);
enableButtons(false);
}
void MarkersDock::onRowClicked(const QModelIndex &index)
{
if (m_model && m_proxyModel && MAIN.multitrack() && index.isValid()) {
QModelIndex realIndex = m_proxyModel->mapToSource(index);
if (realIndex.isValid()) {
Markers::Marker marker = m_model->getMarker(realIndex.row());
emit seekRequested(marker.start);
}
}
}
void MarkersDock::onAddRequested()
{
emit addRequested();
}
void MarkersDock::onRemoveRequested()
{
if (m_model && m_proxyModel) {
QModelIndexList indices = m_treeView->selectedIndexes();
if (indices.size() > 0) {
QModelIndex realIndex = m_proxyModel->mapToSource(indices[0]);
if (realIndex.isValid()) {
m_model->remove(realIndex.row());
}
}
}
}
void MarkersDock::onClearSelectionRequested()
{
m_treeView->clearSelection();
}
void MarkersDock::onRemoveAllRequested()
{
m_model->clear();
}
void MarkersDock::onSearchChanged()
{
if (m_proxyModel) {
m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_proxyModel->setFilterFixedString(m_searchField->text());
}
}
void MarkersDock::onColorColumnToggled(bool checked)
{
Settings.setMarkersShowColumn("color", checked);
m_treeView->setColumnHidden(0, !checked);
}
void MarkersDock::onTextColumnToggled(bool checked)
{
Settings.setMarkersShowColumn("text", checked);
m_treeView->setColumnHidden(1, !checked);
}
void MarkersDock::onStartColumnToggled(bool checked)
{
Settings.setMarkersShowColumn("start", checked);
m_treeView->setColumnHidden(2, !checked);
}
void MarkersDock::onEndColumnToggled(bool checked)
{
Settings.setMarkersShowColumn("end", checked);
m_treeView->setColumnHidden(3, !checked);
}
void MarkersDock::onDurationColumnToggled(bool checked)
{
Settings.setMarkersShowColumn("duration", checked);
m_treeView->setColumnHidden(4, !checked);
}
void MarkersDock::onRowsInserted(const QModelIndex &parent, int first, int last)
{
Q_UNUSED(parent);
Q_UNUSED(last);
QModelIndex sourceIndex = m_model->modelIndexForRow(first);
QModelIndex insertedIndex = m_proxyModel->mapFromSource(sourceIndex);
m_treeView->setCurrentIndex(insertedIndex);
}
void MarkersDock::onDataChanged(const QModelIndex &topLeft,
const QModelIndex &bottomRight,
const QVector<int> &roles)
{
Q_UNUSED(topLeft);
Q_UNUSED(bottomRight);
Q_UNUSED(roles);
if (m_model && m_proxyModel && !m_editInProgress) {
QModelIndexList indices = m_treeView->selectedIndexes();
if (indices.size() > 0) {
QModelIndex realIndex = m_proxyModel->mapToSource(indices[0]);
if (realIndex.isValid()) {
Markers::Marker marker = m_model->getMarker(realIndex.row());
m_editMarkerWidget->setVisible(true);
QSignalBlocker editBlocker(m_editMarkerWidget);
m_editMarkerWidget->setValues(marker.text,
marker.color,
marker.start,
marker.end,
MAIN.multitrack()->get_length() - 1);
return;
}
}
}
}
void MarkersDock::onValuesChanged()
{
if (m_model && m_proxyModel) {
QModelIndexList indices = m_treeView->selectedIndexes();
if (indices.size() > 0) {
QModelIndex realIndex = m_proxyModel->mapToSource(indices[0]);
if (realIndex.isValid()) {
Markers::Marker marker;
marker.text = m_editMarkerWidget->getText();
marker.color = m_editMarkerWidget->getColor();
marker.start = m_editMarkerWidget->getStart();
marker.end = m_editMarkerWidget->getEnd();
m_editInProgress = true;
m_model->update(realIndex.row(), marker);
m_editInProgress = false;
}
}
}
}
void MarkersDock::onModelReset()
{
m_treeView->clearSelection();
m_editMarkerWidget->setVisible(false);
}
void MarkersDock::onSortIndicatorChanged(int logicalIndex, Qt::SortOrder order)
{
Settings.setMarkerSort(logicalIndex, order);
}
void MarkersDock::enableButtons(bool enable)
{
m_removeButton->setEnabled(enable);
m_clearButton->setEnabled(enable);
}

84
src/docks/markersdock.h Normal file
View File

@@ -0,0 +1,84 @@
/*
* Copyright (c) 2021 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 <http://www.gnu.org/licenses/>.
*/
#ifndef MARKERSDOCK_H
#define MARKERSDOCK_H
#include <QDockWidget>
#include <QItemSelectionModel>
class EditMarkerWidget;
class MarkerTreeView;
class MarkersModel;
class QLineEdit;
class QSortFilterProxyModel;
class QToolButton;
class MarkersDock : public QDockWidget
{
Q_OBJECT
public:
explicit MarkersDock(QWidget *parent = 0);
~MarkersDock();
void setModel(MarkersModel *model);
signals:
void seekRequested(int pos);
void addRequested();
void addAroundSelectionRequested();
public slots:
void onMarkerSelectionRequest(int markerIndex);
private slots:
void onSelectionChanged(QModelIndex &index);
void onRowClicked(const QModelIndex &index);
void onAddRequested();
void onRemoveRequested();
void onClearSelectionRequested();
void onRemoveAllRequested();
void onSearchChanged();
void onColorColumnToggled(bool checked);
void onTextColumnToggled(bool checked);
void onStartColumnToggled(bool checked);
void onEndColumnToggled(bool checked);
void onDurationColumnToggled(bool checked);
void onRowsInserted(const QModelIndex &parent, int first, int last);
void onDataChanged(const QModelIndex &topLeft,
const QModelIndex &bottomRight,
const QVector<int> &roles = QVector<int>());
void onValuesChanged();
void onModelReset();
void onSortIndicatorChanged(int logicalIndex, Qt::SortOrder order);
private:
void enableButtons(bool enable);
MarkersModel *m_model;
QSortFilterProxyModel *m_proxyModel;
MarkerTreeView *m_treeView;
QToolButton *m_addButton;
QToolButton *m_removeButton;
QToolButton *m_clearButton;
QLineEdit *m_searchField;
QToolButton *m_clearSearchButton;
EditMarkerWidget *m_editMarkerWidget;
bool m_editInProgress;
};
#endif // MARKERSDOCK_H

191
src/docks/notesdock.cpp Normal file
View File

@@ -0,0 +1,191 @@
/*
* Copyright (c) 2022-2026 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 <http://www.gnu.org/licenses/>.
*/
#include "notesdock.h"
#include "Logger.h"
#include "actions.h"
#include "dialogs/speechdialog.h"
#include "jobqueue.h"
#include "jobs/kokorodokijob.h"
#include "mltcontroller.h"
#include "settings.h"
#include "util.h"
#include "widgets/docktoolbar.h"
#include <QAction>
#include <QApplication>
#include <QIcon>
#include <QMenu>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QTimer>
#include <QVBoxLayout>
#include <QWheelEvent>
class TextEditor : public QPlainTextEdit
{
Q_DECLARE_TR_FUNCTIONS(TextEditor)
public:
explicit TextEditor(QWidget *parent = nullptr)
: QPlainTextEdit()
{
setObjectName("Notes");
zoomIn(Settings.notesZoom());
setTabChangesFocus(false);
setTabStopDistance(fontMetrics().horizontalAdvance("XXXX")); // Tabstop = 4 spaces
setContextMenuPolicy(Qt::CustomContextMenu);
auto icon = QIcon::fromTheme("zoom-out", QIcon(":/icons/oxygen/32x32/actions/zoom-out.png"));
auto action = new QAction(icon, tr("Decrease Text Size"), this);
action->setShortcut(Qt::CTRL | Qt::ALT | Qt::Key_Minus);
addAction(action);
Actions.add("notesDecreaseTextSize", action, objectName());
connect(action, &QAction::triggered, this, [=]() { setZoom(-4); });
icon = QIcon::fromTheme("zoom-in", QIcon(":/icons/oxygen/32x32/actions/zoom-in.png"));
action = new QAction(icon, tr("Increase Text Size"), this);
action->setShortcut(Qt::CTRL | Qt::ALT | Qt::Key_Equal);
addAction(action);
Actions.add("notesIncreaseTextSize", action, objectName());
connect(action, &QAction::triggered, this, [=]() { setZoom(4); });
#ifdef EXTERNAL_LAUNCHERS
icon = QIcon::fromTheme("text-speak", QIcon(":/icons/oxygen/32x32/actions/text-speak.png"));
action = new QAction(icon, tr("Text to Speech..."), this);
addAction(action);
Actions.add("notesTextToSpeech", action, objectName());
connect(action, &QAction::triggered, this, [=] {
if (toPlainText().isEmpty() || !KokorodokiJob::checkDockerImage(this))
return;
m_speechDialog.reset(new SpeechDialog(this));
if (m_speechDialog->exec() != QDialog::Accepted)
return;
KokorodokiJob::prepareAndRun(this, [=]() {
const auto outFile = m_speechDialog->outputFile();
if (outFile.isEmpty())
return;
QFileInfo outInfo(outFile);
auto txtFile = new QTemporaryFile(outInfo.dir().filePath("XXXXXX.txt"));
if (!txtFile->open()) {
LOG_ERROR() << "Failed to create temp text file" << txtFile->fileName();
txtFile->deleteLater();
return;
}
txtFile->write(toPlainText().toUtf8());
txtFile->close();
const auto lang = m_speechDialog->languageCode();
const auto voice = m_speechDialog->voiceCode();
const auto spd = m_speechDialog->speed();
auto job = new KokorodokiJob(txtFile->fileName(), outFile, lang, voice, spd);
txtFile->setParent(job); // auto-delete with job
job->setPostJobAction(new OpenPostJobAction(outFile, outFile, QString()));
JOBS.add(job);
});
});
#endif
connect(this, &QWidget::customContextMenuRequested, this, [=](const QPoint &pos) {
std::unique_ptr<QMenu> menu{createStandardContextMenu()};
actions().at(0)->setEnabled(Settings.notesZoom() > 0);
menu->addActions(actions());
menu->exec(mapToGlobal(pos));
});
}
void setZoom(int delta)
{
auto zoom = Settings.notesZoom();
zoomIn((zoom + delta >= 0) ? delta : -zoom);
Settings.setNotesZoom(std::max<int>(0, zoom + delta));
}
protected:
void wheelEvent(QWheelEvent *event) override
{
if (event->modifiers() & Qt::ControlModifier) {
setZoom((event->angleDelta().y() < 0) ? -1 : 1);
event->accept();
} else {
QPlainTextEdit::wheelEvent(event);
}
}
private:
std::unique_ptr<SpeechDialog> m_speechDialog;
};
NotesDock::NotesDock(QWidget *parent)
: QDockWidget(tr("Notes"), parent)
, m_textEdit(new TextEditor(this))
, m_blockUpdate(false)
{
LOG_DEBUG() << "begin";
setObjectName("NotesDock");
QIcon icon = QIcon::fromTheme("document-edit",
QIcon(":/icons/oxygen/32x32/actions/document-edit.png"));
toggleViewAction()->setIcon(icon);
setWhatsThis("https://forum.shotcut.org/t/notes-panel/33110/1");
QObject::connect(m_textEdit, SIGNAL(textChanged()), SLOT(onTextChanged()));
// Wrap the text editor with a container so we can place a toolbar beneath it.
auto container = new QWidget(this);
auto layout = new QVBoxLayout(container);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(m_textEdit, /*stretch*/ 1);
// Create a DockToolBar to hold custom actions defined in TextEditor.
auto toolbar = new DockToolBar(tr("Notes Controls"), container);
toolbar->setAreaHint(Qt::BottomToolBarArea);
const auto actions = m_textEdit->actions();
toolbar->addAction(actions.at(0)); // Decrease Text Size
toolbar->addAction(actions.at(1)); // Increase Text Size
#ifdef EXTERNAL_LAUNCHERS
toolbar->addSeparator();
toolbar->addAction(actions.at(2)); // Text to Speech
#endif
layout->addWidget(toolbar, /*stretch*/ 0);
QDockWidget::setWidget(container);
LOG_DEBUG() << "end";
}
QString NotesDock::getText()
{
return m_textEdit->toPlainText();
}
void NotesDock::setText(const QString &text)
{
m_blockUpdate = true;
m_textEdit->setPlainText(text);
m_blockUpdate = false;
}
void NotesDock::onTextChanged()
{
if (!m_blockUpdate) {
emit modified();
}
}

Some files were not shown because too many files have changed in this diff Show More