übernahme Code Shortcut
This commit is contained in:
395
src/CMakeLists.txt
Normal file
395
src/CMakeLists.txt
Normal 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()
|
||||
189
src/FlatpakWrapperGenerator.cpp
Normal file
189
src/FlatpakWrapperGenerator.cpp
Normal 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;
|
||||
}
|
||||
52
src/FlatpakWrapperGenerator.h
Normal file
52
src/FlatpakWrapperGenerator.h
Normal 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;
|
||||
};
|
||||
44
src/abstractproducerwidget.cpp
Normal file
44
src/abstractproducerwidget.cpp
Normal 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;
|
||||
}
|
||||
46
src/abstractproducerwidget.h
Normal file
46
src/abstractproducerwidget.h
Normal 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
169
src/actions.cpp
Normal 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
57
src/actions.h
Normal 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
95
src/autosavefile.cpp
Normal 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
47
src/autosavefile.h
Normal 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
|
||||
415
src/commands/filtercommands.cpp
Normal file
415
src/commands/filtercommands.cpp
Normal 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
|
||||
243
src/commands/filtercommands.h
Normal file
243
src/commands/filtercommands.h
Normal 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
|
||||
128
src/commands/markercommands.cpp
Normal file
128
src/commands/markercommands.cpp
Normal 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
|
||||
89
src/commands/markercommands.h
Normal file
89
src/commands/markercommands.h
Normal 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
|
||||
555
src/commands/playlistcommands.cpp
Normal file
555
src/commands/playlistcommands.cpp
Normal 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
|
||||
254
src/commands/playlistcommands.h
Normal file
254
src/commands/playlistcommands.h
Normal 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
|
||||
343
src/commands/subtitlecommands.cpp
Normal file
343
src/commands/subtitlecommands.cpp
Normal 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
|
||||
188
src/commands/subtitlecommands.h
Normal file
188
src/commands/subtitlecommands.h
Normal 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
|
||||
2352
src/commands/timelinecommands.cpp
Normal file
2352
src/commands/timelinecommands.cpp
Normal file
File diff suppressed because it is too large
Load Diff
892
src/commands/timelinecommands.h
Normal file
892
src/commands/timelinecommands.h
Normal 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
454
src/commands/undohelper.cpp
Normal 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
91
src/commands/undohelper.h
Normal 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
|
||||
455
src/controllers/filtercontroller.cpp
Normal file
455
src/controllers/filtercontroller.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
97
src/controllers/filtercontroller.h
Normal file
97
src/controllers/filtercontroller.h
Normal 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
|
||||
65
src/controllers/scopecontroller.cpp
Normal file
65
src/controllers/scopecontroller.cpp
Normal 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);
|
||||
}
|
||||
45
src/controllers/scopecontroller.h
Normal file
45
src/controllers/scopecontroller.h
Normal 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
136
src/database.cpp
Normal 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
46
src/database.h
Normal 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
161
src/dataqueue.h
Normal 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
24
src/defaultlayouts.h
Normal 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
|
||||
377
src/dialogs/actionsdialog.cpp
Normal file
377
src/dialogs/actionsdialog.cpp
Normal 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 ¤t) {
|
||||
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();
|
||||
}
|
||||
53
src/dialogs/actionsdialog.h
Normal file
53
src/dialogs/actionsdialog.h
Normal 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
|
||||
50
src/dialogs/addencodepresetdialog.cpp
Normal file
50
src/dialogs/addencodepresetdialog.cpp
Normal 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();
|
||||
}
|
||||
43
src/dialogs/addencodepresetdialog.h
Normal file
43
src/dialogs/addencodepresetdialog.h
Normal 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
|
||||
157
src/dialogs/addencodepresetdialog.ui
Normal file
157
src/dialogs/addencodepresetdialog.ui
Normal 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>
|
||||
506
src/dialogs/alignaudiodialog.cpp
Normal file
506
src/dialogs/alignaudiodialog.cpp
Normal 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);
|
||||
}
|
||||
71
src/dialogs/alignaudiodialog.h
Normal file
71
src/dialogs/alignaudiodialog.h
Normal 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
|
||||
227
src/dialogs/alignmentarray.cpp
Normal file
227
src/dialogs/alignmentarray.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
58
src/dialogs/alignmentarray.h
Normal file
58
src/dialogs/alignmentarray.h
Normal 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
|
||||
186
src/dialogs/bitratedialog.cpp
Normal file
186
src/dialogs/bitratedialog.cpp
Normal 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();
|
||||
}
|
||||
35
src/dialogs/bitratedialog.h
Normal file
35
src/dialogs/bitratedialog.h
Normal 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
|
||||
171
src/dialogs/customprofiledialog.cpp
Normal file
171
src/dialogs/customprofiledialog.cpp
Normal 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());
|
||||
}
|
||||
56
src/dialogs/customprofiledialog.h
Normal file
56
src/dialogs/customprofiledialog.h
Normal 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
|
||||
558
src/dialogs/customprofiledialog.ui
Normal file
558
src/dialogs/customprofiledialog.ui
Normal 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><small>(Leave Name blank to skip saving a preset and use a temporary or project-specific Video Mode.)</small></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>
|
||||
44
src/dialogs/durationdialog.cpp
Normal file
44
src/dialogs/durationdialog.cpp
Normal 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();
|
||||
}
|
||||
41
src/dialogs/durationdialog.h
Normal file
41
src/dialogs/durationdialog.h
Normal 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
|
||||
124
src/dialogs/durationdialog.ui
Normal file
124
src/dialogs/durationdialog.ui
Normal 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>
|
||||
77
src/dialogs/editmarkerdialog.cpp
Normal file
77
src/dialogs/editmarkerdialog.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
47
src/dialogs/editmarkerdialog.h
Normal file
47
src/dialogs/editmarkerdialog.h
Normal 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
|
||||
133
src/dialogs/filedatedialog.cpp
Normal file
133
src/dialogs/filedatedialog.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/dialogs/filedatedialog.h
Normal file
48
src/dialogs/filedatedialog.h
Normal 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
|
||||
142
src/dialogs/filedownloaddialog.cpp
Normal file
142
src/dialogs/filedownloaddialog.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
49
src/dialogs/filedownloaddialog.h
Normal file
49
src/dialogs/filedownloaddialog.h
Normal 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
|
||||
91
src/dialogs/listselectiondialog.cpp
Normal file
91
src/dialogs/listselectiondialog.cpp
Normal 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);
|
||||
}
|
||||
48
src/dialogs/listselectiondialog.h
Normal file
48
src/dialogs/listselectiondialog.h
Normal 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
|
||||
77
src/dialogs/listselectiondialog.ui
Normal file
77
src/dialogs/listselectiondialog.ui
Normal 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>
|
||||
54
src/dialogs/longuitask.cpp
Normal file
54
src/dialogs/longuitask.cpp
Normal 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
55
src/dialogs/longuitask.h
Normal 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
|
||||
288
src/dialogs/multifileexportdialog.cpp
Normal file
288
src/dialogs/multifileexportdialog.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
67
src/dialogs/multifileexportdialog.h
Normal file
67
src/dialogs/multifileexportdialog.h
Normal 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
|
||||
121
src/dialogs/resourcedialog.cpp
Normal file
121
src/dialogs/resourcedialog.cpp
Normal 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);
|
||||
}
|
||||
52
src/dialogs/resourcedialog.h
Normal file
52
src/dialogs/resourcedialog.h
Normal 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
|
||||
133
src/dialogs/saveimagedialog.cpp
Normal file
133
src/dialogs/saveimagedialog.cpp
Normal 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());
|
||||
}
|
||||
42
src/dialogs/saveimagedialog.h
Normal file
42
src/dialogs/saveimagedialog.h
Normal 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
|
||||
66
src/dialogs/slideshowgeneratordialog.cpp
Normal file
66
src/dialogs/slideshowgeneratordialog.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
44
src/dialogs/slideshowgeneratordialog.h
Normal file
44
src/dialogs/slideshowgeneratordialog.h
Normal 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
|
||||
265
src/dialogs/speechdialog.cpp
Normal file
265
src/dialogs/speechdialog.cpp
Normal 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());
|
||||
}
|
||||
}
|
||||
53
src/dialogs/speechdialog.h
Normal file
53
src/dialogs/speechdialog.h
Normal 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
|
||||
98
src/dialogs/subtitletrackdialog.cpp
Normal file
98
src/dialogs/subtitletrackdialog.cpp
Normal 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();
|
||||
}
|
||||
43
src/dialogs/subtitletrackdialog.h
Normal file
43
src/dialogs/subtitletrackdialog.h
Normal 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
|
||||
79
src/dialogs/systemsyncdialog.cpp
Normal file
79
src/dialogs/systemsyncdialog.cpp
Normal 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();
|
||||
}
|
||||
55
src/dialogs/systemsyncdialog.h
Normal file
55
src/dialogs/systemsyncdialog.h
Normal 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
|
||||
175
src/dialogs/systemsyncdialog.ui
Normal file
175
src/dialogs/systemsyncdialog.ui
Normal 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>
|
||||
90
src/dialogs/textviewerdialog.cpp
Normal file
90
src/dialogs/textviewerdialog.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
47
src/dialogs/textviewerdialog.h
Normal file
47
src/dialogs/textviewerdialog.h
Normal 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
|
||||
80
src/dialogs/textviewerdialog.ui
Normal file
80
src/dialogs/textviewerdialog.ui
Normal 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>
|
||||
167
src/dialogs/transcodedialog.cpp
Normal file
167
src/dialogs/transcodedialog.cpp
Normal 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-frame–only %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);
|
||||
}
|
||||
63
src/dialogs/transcodedialog.h
Normal file
63
src/dialogs/transcodedialog.h
Normal 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
|
||||
344
src/dialogs/transcodedialog.ui
Normal file
344
src/dialogs/transcodedialog.ui
Normal 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><span style=" font-weight:700; color:#ff0000;">HUGE</span></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>
|
||||
530
src/dialogs/transcribeaudiodialog.cpp
Normal file
530
src/dialogs/transcribeaudiodialog.cpp
Normal 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();
|
||||
}
|
||||
74
src/dialogs/transcribeaudiodialog.h
Normal file
74
src/dialogs/transcribeaudiodialog.h
Normal 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
|
||||
155
src/dialogs/unlinkedfilesdialog.cpp
Normal file
155
src/dialogs/unlinkedfilesdialog.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
50
src/dialogs/unlinkedfilesdialog.h
Normal file
50
src/dialogs/unlinkedfilesdialog.h
Normal 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
|
||||
137
src/dialogs/unlinkedfilesdialog.ui
Normal file
137
src/dialogs/unlinkedfilesdialog.ui
Normal 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
2984
src/docks/encodedock.cpp
Normal file
File diff suppressed because it is too large
Load Diff
205
src/docks/encodedock.h
Normal file
205
src/docks/encodedock.h
Normal 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
2129
src/docks/encodedock.ui
Normal file
File diff suppressed because it is too large
Load Diff
1400
src/docks/filesdock.cpp
Normal file
1400
src/docks/filesdock.cpp
Normal file
File diff suppressed because it is too large
Load Diff
111
src/docks/filesdock.h
Normal file
111
src/docks/filesdock.h
Normal 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
210
src/docks/filesdock.ui
Normal 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
271
src/docks/filtersdock.cpp
Normal 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
75
src/docks/filtersdock.h
Normal 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
|
||||
93
src/docks/findanalysisfilterparser.h
Normal file
93
src/docks/findanalysisfilterparser.h
Normal 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
224
src/docks/jobsdock.cpp
Normal 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
62
src/docks/jobsdock.h
Normal 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
195
src/docks/jobsdock.ui
Normal 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
1235
src/docks/keyframesdock.cpp
Normal file
File diff suppressed because it is too large
Load Diff
93
src/docks/keyframesdock.h
Normal file
93
src/docks/keyframesdock.h
Normal 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
464
src/docks/markersdock.cpp
Normal 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
84
src/docks/markersdock.h
Normal 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
191
src/docks/notesdock.cpp
Normal 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
Reference in New Issue
Block a user