diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9d0d8ddf9..0d3d65a8a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,179 +1,179 @@
project(Kdenlive)
# An odd patch version number means development version, while an even one means
# stable release. An additional number can be used for bugfix-only releases.
# KDE Application Version, managed by release script
set(KDE_APPLICATIONS_VERSION_MAJOR "19")
set(KDE_APPLICATIONS_VERSION_MINOR "03")
-set(KDE_APPLICATIONS_VERSION_MICRO "80")
+set(KDE_APPLICATIONS_VERSION_MICRO "90")
set(KDENLIVE_VERSION ${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO})
cmake_minimum_required(VERSION 3.0)
if(POLICY CMP0063)
cmake_policy(SET CMP0063 NEW)
endif()
if(POLICY CMP0053)
cmake_policy(SET CMP0053 NEW)
endif()
if(BUILD_FUZZING)
set(CMAKE_CXX_FLAGS "${KDENLIVE_CXX_FLAGS} -fsanitize=fuzzer-no-link,address")
endif()
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unknown-warning-option")
endif()
# To be switched on when releasing.
option(RELEASE_BUILD "Remove Git revision from program version" ON)
option(BUILD_TESTING "Build tests" ON)
option(BUILD_FUZZING "Build fuzzing target" OFF)
# Minimum versions of main dependencies.
set(MLT_MIN_MAJOR_VERSION 6)
set(MLT_MIN_MINOR_VERSION 12)
set(MLT_MIN_PATCH_VERSION 0)
set(MLT_MIN_VERSION ${MLT_MIN_MAJOR_VERSION}.${MLT_MIN_MINOR_VERSION}.${MLT_MIN_PATCH_VERSION})
# KDE Frameworks
find_package(ECM 5.18.0 REQUIRED CONFIG)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(FeatureSummary)
include(ECMInstallIcons)
include(GenerateExportHeader)
include(KDEInstallDirs)
include(KDECMakeSettings)
include(ECMOptionalAddSubdirectory)
include(ECMMarkNonGuiExecutable)
include(ECMAddAppIcon)
include(ECMQtDeclareLoggingCategory)
include(ECMEnableSanitizers)
add_definitions(-DTRANSLATION_DOMAIN=\"kdenlive\")
find_package(KF5 5.45.0 REQUIRED COMPONENTS Archive Bookmarks CoreAddons Config ConfigWidgets
DBusAddons KIO WidgetsAddons NotifyConfig NewStuff XmlGui Notifications GuiAddons TextWidgets IconThemes Declarative Solid
OPTIONAL_COMPONENTS DocTools FileMetaData Crash Purpose)
# Qt
set(QT_MIN_VERSION 5.7.0)
find_package(Qt5 REQUIRED COMPONENTS Core DBus Widgets Svg Quick Concurrent QuickWidgets Multimedia)
find_package(Qt5 OPTIONAL_COMPONENTS WebKitWidgets QUIET)
add_definitions(-DQT_NO_CAST_TO_ASCII -DQT_NO_URL_CAST_FROM_STRING)
set(DEFAULT_CXX_FLAGS "${DEFAULT_CXX_FLAGS} ${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS}")
# MLT
find_package(MLT ${MLT_MIN_VERSION} REQUIRED)
set_package_properties(MLT PROPERTIES DESCRIPTION "Multimedia framework"
URL "https://mltframework.org"
PURPOSE "Required to do video processing")
message(STATUS "Found MLT++: ${MLTPP_LIBRARIES}")
# Windows
include(CheckIncludeFiles)
check_include_files(malloc.h HAVE_MALLOC_H)
check_include_files(pthread.h HAVE_PTHREAD_H)
if(WIN32)
find_package(DrMinGW)
set(MLT_PREFIX "..")
else()
set(MLT_PREFIX ${MLT_ROOT_DIR})
endif()
# Optional deps status
find_package(KF5 5.23.0 OPTIONAL_COMPONENTS XmlGui QUIET)
if(KF5XmlGui_FOUND)
message(STATUS "Found KF5 >= 5.23.0 enabling icon coloring")
else()
message(STATUS "KF5 < 5.23.0 Disable icon coloring")
set(KF5_ICON_COMPATIBILITY TRUE)
endif()
if(KF5FileMetaData_FOUND)
message(STATUS "Found KF5 FileMetadata to extract file metadata")
set(KF5_FILEMETADATA TRUE)
else()
message(STATUS "KF5 FileMetadata not found, file metadata will not be available")
endif()
if(KF5Purpose_FOUND)
message(STATUS "Found KF5 Purpose, filesharing enabled")
set(KF5_PURPOSE TRUE)
else()
message(STATUS "KF5 Purpose not found, filesharing disabled")
endif()
if(KF5DocTools_FOUND)
add_subdirectory(doc)
kdoctools_install(po)
endif()
# Get current version.
set(KDENLIVE_VERSION_STRING "${KDENLIVE_VERSION}")
if(NOT RELEASE_BUILD AND EXISTS ${CMAKE_SOURCE_DIR}/.git)
# Probably a Git workspace; determine the revision.
find_package(Git QUIET)
if(GIT_FOUND)
exec_program(${GIT_EXECUTABLE} ${CMAKE_SOURCE_DIR}
ARGS "log -n 1 --pretty=format:\"%h\""
OUTPUT_VARIABLE KDENLIVE_GIT_REVISION)
message(STATUS "Kdenlive Git revision: ${KDENLIVE_GIT_REVISION}")
set(KDENLIVE_VERSION_STRING "${KDENLIVE_VERSION} (rev. ${KDENLIVE_GIT_REVISION})")
else()
message(STATUS "Kdenlive Git revision could not be determined")
endif()
endif()
find_package(RTTR 0.9.6 QUIET)
if(NOT RTTR_FOUND)
message(STATUS "RTTR not found on system, will download source and build it")
include(rttr.CMakeLists.txt)
endif()
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
set(FFMPEG_SUFFIX "" CACHE STRING "FFmpeg custom suffix")
configure_file(config-kdenlive.h.cmake config-kdenlive.h @ONLY)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wno-suggest-override")
# Sources
add_subdirectory(src)
add_subdirectory(renderer)
add_subdirectory(thumbnailer)
add_subdirectory(data)
ki18n_install(po)
include(GNUInstallDirs)
install(FILES AUTHORS COPYING README.md DESTINATION ${CMAKE_INSTALL_DOCDIR})
install(FILES kdenlive.categories DESTINATION ${KDE_INSTALL_CONFDIR})
############################
# Tests
############################
if(BUILD_TESTING)
message(STATUS "Building tests")
add_subdirectory(tests)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fexceptions")
include_directories(
${CMAKE_BINARY_DIR}
${CMAKE_BINARY_DIR}/src
${MLT_INCLUDE_DIR}
${MLTPP_INCLUDE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/lib/external
${CMAKE_CURRENT_SOURCE_DIR}/lib
src)
add_executable(runTests ${Tests_SRCS})
set_property(TARGET runTests PROPERTY CXX_STANDARD 14)
target_link_libraries(runTests kdenliveLib)
add_test(runTests runTests -d yes)
endif()
if(BUILD_FUZZING)
message(STATUS "Building fuzzing")
set(CMAKE_CXX_COMPILER /usr/bin/clang++)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDENLIVE_CXX_FLAGS} -fsanitize=fuzzer-no-link,address")
add_subdirectory(fuzzer)
endif()
diff --git a/data/effects/audiobalance.xml b/data/effects/audiobalance.xml
index 7e0d7d455..d689b7376 100644
--- a/data/effects/audiobalance.xml
+++ b/data/effects/audiobalance.xml
@@ -1,9 +1,9 @@
Balance
Adjust the left/right balance
Dan Dennedy
-
+
Balance
diff --git a/data/effects/audiopan.xml b/data/effects/audiopan.xml
index 1cd01d1df..723ee38e0 100644
--- a/data/effects/audiopan.xml
+++ b/data/effects/audiopan.xml
@@ -1,13 +1,13 @@
Pan
Adjust the left/right spread of a channel
Dan Dennedy
Left,Right
Channel
-
+
Pan
diff --git a/data/effects/boxblur.xml b/data/effects/boxblur.xml
index 761388e11..a80b8006b 100644
--- a/data/effects/boxblur.xml
+++ b/data/effects/boxblur.xml
@@ -1,15 +1,15 @@
Box Blur
Box blur (separate horizontal and vertical blur)
Leny Grisel
-
+
Horizontal multiplicator
-
+
Vertical multiplicator
-
- Blur factor
-
+
+ Blur factor
+
diff --git a/data/effects/frei0r_3dflippo.xml b/data/effects/frei0r_3dflippo.xml
index 7a9f2c24c..848a0588e 100644
--- a/data/effects/frei0r_3dflippo.xml
+++ b/data/effects/frei0r_3dflippo.xml
@@ -1,39 +1,39 @@
3dflippo
Frame rotation in 3D space
c.e. prelz AS FLUIDO
-
+
X axis rotation
-
+
Y axis rotation
-
+
Z axis rotation
-
+
X axis rotation rate
-
+
Y axis rotation rate
-
+
Z axis rotation rate
-
+
Center position (X)
-
+
Center position (Y)
Invert rotation assignment
Don't blank mask
Fill with image or black
diff --git a/data/effects/frei0r_alpha0ps.xml b/data/effects/frei0r_alpha0ps.xml
index 33811c042..e4e748aaf 100644
--- a/data/effects/frei0r_alpha0ps.xml
+++ b/data/effects/frei0r_alpha0ps.xml
@@ -1,66 +1,66 @@
Alpha operations
Display and manipulation of the alpha channel
Marko Cebokli
Image,Alpha as gray,Gray + red,Selection on black,Selection on gray,Selection on white,Selection on checkers
Display
Display input alpha
NO OP,Shave,Shrink hard,Shrink soft,Grow hard,Grow soft,Threshold
Operation
-
+
Threshold
-
+
Shrink/grow amount
Invert
Alpha operations
Display and manipulation of the alpha channel
Marko Cebokli
Image,Alpha as gray,Gray + red,Selection on black,Selection on gray,Selection on white,Selection on checkers
Display
Display input alpha
NO OP,Shave,Shrink hard,Shrink soft,Grow hard,Grow soft,Threshold,Blur
Operation
-
+
Threshold
-
+
Shrink/Grow/Blur amount
Invert
diff --git a/data/effects/frei0r_alphagrad.xml b/data/effects/frei0r_alphagrad.xml
index d8fef3081..653914918 100644
--- a/data/effects/frei0r_alphagrad.xml
+++ b/data/effects/frei0r_alphagrad.xml
@@ -1,31 +1,33 @@
Alpha gradient
Fill the alpha channel with a specified gradient
Marko Cebokli
-
+
+
Position
-
+
Transition width
-
+
Tilt
-
+
Min
-
+
Max
Write on clear,Max,Min,Add,Subtract
Operation
+
diff --git a/data/effects/frei0r_alphaspot.xml b/data/effects/frei0r_alphaspot.xml
index 463beda6e..75cebc45a 100644
--- a/data/effects/frei0r_alphaspot.xml
+++ b/data/effects/frei0r_alphaspot.xml
@@ -1,49 +1,50 @@
Alpha shapes
Draws simple shapes into the alpha channel
Marko Cebokli
+
Rectangle,Ellipse,Triangle,Diamond
Shape
-
+
Position X
-
+
Position Y
-
+
Size X
-
+
Size Y
-
+
Tilt
-
+
Transition width
-
+
Min
-
+
Max
Write on clear,Max,Min,Add,Subtract
Operation
diff --git a/data/effects/frei0r_balanc0r.xml b/data/effects/frei0r_balanc0r.xml
index 226d888e5..2ff3f3a88 100644
--- a/data/effects/frei0r_balanc0r.xml
+++ b/data/effects/frei0r_balanc0r.xml
@@ -1,25 +1,25 @@
White Balance
Adjust the white balance / color temperature
Dan Dennedy
Neutral Color
-
+
Green Tint
White Balance
Adjust the white balance / color temperature
Dan Dennedy
Neutral Color
-
+
Green Tint
diff --git a/data/effects/frei0r_bezier_curves.xml b/data/effects/frei0r_bezier_curves.xml
index bfa38147e..e87f1a668 100644
--- a/data/effects/frei0r_bezier_curves.xml
+++ b/data/effects/frei0r_bezier_curves.xml
@@ -1,17 +1,18 @@
Bézier Curves
Color curves adjustment
Till Theato, Maksim Golovkin
RGB,Red,Green,Blue,Alpha,Luma,Hue,Saturation
Channel
-
+
+
Rec. 601,Rec. 709
Luma formula
-
+
diff --git a/data/effects/frei0r_brightness.xml b/data/effects/frei0r_brightness.xml
index 397c06976..ac435cd6f 100644
--- a/data/effects/frei0r_brightness.xml
+++ b/data/effects/frei0r_brightness.xml
@@ -1,9 +1,9 @@
Brightness
Adjusts the brightness of a source image
Jean-Sebastien Senecal
-
+
Brightness
diff --git a/data/effects/frei0r_c0rners.xml b/data/effects/frei0r_c0rners.xml
index e00e1efd1..822a82204 100644
--- a/data/effects/frei0r_c0rners.xml
+++ b/data/effects/frei0r_c0rners.xml
@@ -1,69 +1,71 @@
- Corners
- Four corners geometry engine
- Marko Cebokli
-
-
- Corner 1 X
-
-
-
- Corner 1 Y
-
-
-
- Corner 2 X
-
-
-
- Corner 2 Y
-
-
-
- Corner 3 X
-
-
-
- Corner 3 Y
-
-
-
- Corner 4 X
-
-
-
- Corner 4 Y
-
-
-
- Stretch X
-
-
-
- Stretch Y
-
-
-
- Feather Alpha
-
+ Corners
+ Four corners geometry engine
+ Marko Cebokli
+
+
+ Corner 1 X
+
+
+
+ Corner 1 Y
+
+
+
+ Corner 2 X
+
+
+
+ Corner 2 Y
+
+
+
+ Corner 3 X
+
+
+
+ Corner 3 Y
+
+
+
+ Corner 4 X
+
+
+
+ Corner 4 Y
+
+
+
+ Stretch X
+
+
+
+ Stretch Y
+
+
+
+ Feather Alpha
+
+
+
+ Enable Stretch
+
+
+
+ Nearest neighbor,Bilinear,Bicubic smooth,Bicubic sharp,Spline 4x4,Spline 6x6,Lanczos
+ Interpolator
+
-
- Enable Stretch
-
+
+ Transparent Background
+
-
- Nearest neighbor,Bilinear,Bicubic smooth,Bicubic sharp,Spline 4x4,Spline 6x6,Lanczos
- Interpolator
-
+
+ Write on clear,Maximum,Minimum,Add,Subtract
+ Alpha operation
+
-
- Transparent Background
-
-
- Write on clear,Maximum,Minimum,Add,Subtract
- Alpha operation
-
diff --git a/data/effects/frei0r_cartoon.xml b/data/effects/frei0r_cartoon.xml
index 42197a6e1..8eca157e4 100644
--- a/data/effects/frei0r_cartoon.xml
+++ b/data/effects/frei0r_cartoon.xml
@@ -1,25 +1,12 @@
-
-
+
Cartoon
Cartoonify video, do a form of edge detect
Dries Pruimboom, Jaromil
-
+
Level of trip
-
+
Difference space
-
-
- Cartoon
- Cartoonify video, do a form of edge detect
- Dries Pruimboom, Jaromil
-
- Level of trip
-
-
- Difference space
-
-
-
+
diff --git a/data/effects/frei0r_cluster.xml b/data/effects/frei0r_cluster.xml
index f4e990ddf..221b958a2 100644
--- a/data/effects/frei0r_cluster.xml
+++ b/data/effects/frei0r_cluster.xml
@@ -1,12 +1,12 @@
K-Means Clustering
Clusters of a source image by color and spatial distance
binarymillenium
-
+
Amount of clusters
-
+
Weight on distance
diff --git a/data/effects/frei0r_colgate.xml b/data/effects/frei0r_colgate.xml
index 7dc29bd05..d752de654 100644
--- a/data/effects/frei0r_colgate.xml
+++ b/data/effects/frei0r_colgate.xml
@@ -1,14 +1,12 @@
White Balance (LMS space)
- Do simple color correction, in a physically meaningful
-way
+ Do simple color correction, in a physically meaningful way
Steiner H. Gunderson
Neutral Color
-
+
Color Temperature
diff --git a/data/effects/frei0r_coloradj_rgb.xml b/data/effects/frei0r_coloradj_rgb.xml
index f8adceca1..e56448c96 100644
--- a/data/effects/frei0r_coloradj_rgb.xml
+++ b/data/effects/frei0r_coloradj_rgb.xml
@@ -1,37 +1,37 @@
RGB adjustment
Simple color adjustment
Marko Cebokli
-
+
R
-
+
G
-
+
B
Add constant,Change gamma,Multiply
Action
Keep luma
Alpha controlled
Rec. 601,Rec. 709
Luma formula
diff --git a/data/effects/frei0r_contrast0r.xml b/data/effects/frei0r_contrast0r.xml
index f1d871183..60da62ca9 100644
--- a/data/effects/frei0r_contrast0r.xml
+++ b/data/effects/frei0r_contrast0r.xml
@@ -1,9 +1,9 @@
Contrast
Adjusts the contrast of a source image
Jean-Sebastien Senecal
-
+
Contrast
diff --git a/data/effects/frei0r_curves.xml b/data/effects/frei0r_curves.xml
index 0ae29c4f6..f98929174 100644
--- a/data/effects/frei0r_curves.xml
+++ b/data/effects/frei0r_curves.xml
@@ -1,117 +1,117 @@
Curves
Color curves adjustment
Maksim Golovkin
Red,Green,Blue,Luma
Channel
-
+
Rec. 601,Rec. 709
Luma formula
-
+
-
+
Number of curve points
-
+
Point 1 input value
-
+
Point 1 output value
-
+
Point 2 input value
-
+
Point 2 output value
-
+
Point 3 input value
-
+
Point 3 output value
-
+
Point 4 input value
-
+
Point 4 output value
-
+
Point 5 input value
-
+
Point 5 output value
-
+
Show graph in picture
-
+
Top Left,Top Right,Bottom Left,Bottom Right
Graph position
Curves
Color curves adjustment
Maksim Golovkin
RGB,Red,Green,Blue,Alpha,Luma,Hue,Saturation
Channel
-
+
Rec. 601,Rec. 709
Luma formula
-
+
-
+
-
+
Number of curve points
-
+
Point 1 input value
-
+
Point 1 output value
-
+
Point 2 input value
-
+
Point 2 output value
-
+
Point 3 input value
-
+
Point 3 output value
-
+
Point 4 input value
-
+
Point 4 output value
-
+
Point 5 input value
-
+
Point 5 output value
-
+
Show graph in picture
-
+
Top Left,Top Right,Bottom Left,Bottom Right
Graph position
diff --git a/data/effects/frei0r_defish0r.xml b/data/effects/frei0r_defish0r.xml
index ad90a5828..9ed0dc1e2 100644
--- a/data/effects/frei0r_defish0r.xml
+++ b/data/effects/frei0r_defish0r.xml
@@ -1,34 +1,34 @@
Defish
Non rectilinear lens mappings
Marko Cebokli
-
+
Amount
DeFish
-
- Equidistant,Orthographic,Equiarea,Stereographic
+
+ Equidistant,Orthographic,Equiarea,Stereographic
Type
-
- Fill,Center,Fit,Manual
+
+ Fill,Center,Fit,Manual
Scaling
-
+
Manual Scale
-
- Nearest neighbor,Bilinear,Bicubic smooth,Bicubic sharp,Spline 4x4,Spline 6x6,Lanczos
+
+ Nearest neighbor,Bilinear,Bicubic smooth,Bicubic sharp,Spline 4x4,Spline 6x6,Lanczos
Interpolator
-
- Square,PAL DV,NTSC DV,HDV,Manual
+
+ Square,PAL DV,NTSC DV,HDV,Manual
Aspect type
-
+
Manual Aspect
diff --git a/data/effects/frei0r_delay0r.xml b/data/effects/frei0r_delay0r.xml
index 690348ea2..9048f4650 100644
--- a/data/effects/frei0r_delay0r.xml
+++ b/data/effects/frei0r_delay0r.xml
@@ -1,9 +1,9 @@
delay0r
Video delay
Martin Bayer
-
+
Delay time
diff --git a/data/effects/frei0r_distort0r.xml b/data/effects/frei0r_distort0r.xml
index a7a930360..7b4b31567 100644
--- a/data/effects/frei0r_distort0r.xml
+++ b/data/effects/frei0r_distort0r.xml
@@ -1,12 +1,12 @@
Distort
Plasma
Gephex crew
-
+
Amplitude
-
+
Frequency
diff --git a/data/effects/frei0r_edgeglow.xml b/data/effects/frei0r_edgeglow.xml
index 2333a09e4..88ea7918d 100644
--- a/data/effects/frei0r_edgeglow.xml
+++ b/data/effects/frei0r_edgeglow.xml
@@ -1,15 +1,15 @@
Edge glow
Edge glow filter
Salsaman
-
+
Edge lightening threshold
-
+
Edge brightness upscaling multiplier
-
+
Non-edge brightness downscaling multiplier
diff --git a/data/effects/frei0r_facebl0r.xml b/data/effects/frei0r_facebl0r.xml
index 39d3db419..c6e63415d 100644
--- a/data/effects/frei0r_facebl0r.xml
+++ b/data/effects/frei0r_facebl0r.xml
@@ -1,37 +1,37 @@
Face blur
Automatically detect and blur a face using OpenCV
ZioKernel, Biilly, Jilt, Jaromil, Dan Dennedy
-
+
Search scale
The search window scale factor. For example, 120 = 1.20 = increases by 20% on each pass.
-
+
Neighbors
Minimum number of rectangles that determines an object.
-
+
Smallest
The minimum window size in pixels.
-
+
Largest
The largest size face in pixels - both horizontally and vertically (square window).
-
+
Recheck
How often to detect a face. In between checks, it does object motion tracking.
Show ellipse
Draw a blue ellipse around the face area?
diff --git a/data/effects/frei0r_facedetect.xml b/data/effects/frei0r_facedetect.xml
index 3bfe379d2..08f894ec9 100644
--- a/data/effects/frei0r_facedetect.xml
+++ b/data/effects/frei0r_facedetect.xml
@@ -1,74 +1,74 @@
Face detect
Detect faces and draw shapes on them using OpenCV
binarymillenium, Dan Dennedy
-
+
Search scale
The search window scale factor. For example, 120 = 1.20 = increases by 20% on each pass.
-
+
Neighbors
Minimum number of rectangles that determines an object.
-
+
Smallest
The minimum window size in pixels.
-
+
Recheck
How often to detect a face. In between checks, it does object motion tracking.
circle,ellipse,rectangle,random
Shape
-
+
Stroke width
0 means fill; otherwise, draw unfilled with a stroke width of this size.
-
+
Alpha
Set the alpha channel of the shape area to a percentage of fully opaque.
Antialias
Draw with anti-aliasing?
diff --git a/data/effects/frei0r_glow.xml b/data/effects/frei0r_glow.xml
index 4d275f30b..b0c0481dd 100644
--- a/data/effects/frei0r_glow.xml
+++ b/data/effects/frei0r_glow.xml
@@ -1,9 +1,9 @@
Glow
Creates a Glamorous Glow
Richard Spindler
-
+
Blur
diff --git a/data/effects/frei0r_hqdn3d.xml b/data/effects/frei0r_hqdn3d.xml
index 0a6494eeb..0420928c7 100644
--- a/data/effects/frei0r_hqdn3d.xml
+++ b/data/effects/frei0r_hqdn3d.xml
@@ -1,14 +1,16 @@
Denoiser
High quality 3D denoiser
Marko Cebokli, Daniel Moreno
-
+
+
Spatial
Amount of spatial filtering
-
+
+
Temporal
Amount of temporal filtering
diff --git a/data/effects/frei0r_hueshift0r.xml b/data/effects/frei0r_hueshift0r.xml
index a74d05efb..6932d72b1 100644
--- a/data/effects/frei0r_hueshift0r.xml
+++ b/data/effects/frei0r_hueshift0r.xml
@@ -1,9 +1,9 @@
Hue shift
Shifts the hue of a source image
Jean-Sebastien Senecal
-
+
Hue
diff --git a/data/effects/frei0r_iirblur.xml b/data/effects/frei0r_iirblur.xml
index 6ff3ee63d..a919e15ac 100644
--- a/data/effects/frei0r_iirblur.xml
+++ b/data/effects/frei0r_iirblur.xml
@@ -1,21 +1,23 @@
Blur
Blur using 2D IIR filters (Exponential, Lowpass, Gaussian)
Marko Cebokli
-
+
+
Amount
Amount of blur
-
+
+
Exponential,Lowpass,Gaussian
Type
Select blurring algorithm
Edge
Enable edge compensation
diff --git a/data/effects/frei0r_keyspillm0pup.xml b/data/effects/frei0r_keyspillm0pup.xml
index 3ca32b307..a9698cff2 100644
--- a/data/effects/frei0r_keyspillm0pup.xml
+++ b/data/effects/frei0r_keyspillm0pup.xml
@@ -1,61 +1,61 @@
Key Spill Mop Up
Reduces the visibility of key color spill in chroma keying
Marko Cebokli
Key color
Target color
Color distance, Transparency, Edge inwards, Edge outwards
Mask type
-
+
Tolerance
-
+
Slope
-
+
Hue gate
-
+
Saturation threshold
None, De-Key, Target, Desaturate, Luma adjust
Operation 1
-
+
Amount 1
None, De-Key, Target, Desaturate, Luma adjust
Operation 2
-
+
Amount 2
Show mask
Mask to Alpha
diff --git a/data/effects/frei0r_lenscorrection.xml b/data/effects/frei0r_lenscorrection.xml
index a0a416f9b..767ebb76c 100644
--- a/data/effects/frei0r_lenscorrection.xml
+++ b/data/effects/frei0r_lenscorrection.xml
@@ -1,22 +1,22 @@
Lens Correction
Allows compensation of lens distortion
Richard Spindler
-
+
Horizontal center
-
+
Vertical center
-
+
Center correction
-
+
Edges correction
-
+
Brightness
diff --git a/data/effects/frei0r_letterb0xed.xml b/data/effects/frei0r_letterb0xed.xml
index 4f3ad47d4..9cc1653ba 100644
--- a/data/effects/frei0r_letterb0xed.xml
+++ b/data/effects/frei0r_letterb0xed.xml
@@ -1,12 +1,12 @@
LetterB0xed
Adds black borders at top and bottom for cinema look
Richard Spindler
-
+
Border Width
Transparency
diff --git a/data/effects/frei0r_levels.xml b/data/effects/frei0r_levels.xml
index c9a05079a..238a40b20 100644
--- a/data/effects/frei0r_levels.xml
+++ b/data/effects/frei0r_levels.xml
@@ -1,66 +1,67 @@
Levels
Adjust levels
Maksim Golovkin
-
- Red,Green,Blue,Luma
- Channel
+
+
+ Red,Green,Blue,Luma
+ Channel
-
+
Input black level
-
+
Input white level
-
+
Gamma
-
+
Black output
-
+
White output
Show histogram
Top Left,Top Right,Bottom Left,Bottom Right
Histogram position
-
+
Levels
Adjust levels
Maksim Golovkin
Red,Green,Blue,Luma
Channel
-
+
Input black level
-
+
Input white level
-
+
Gamma
-
+
Black output
-
+
White output
Show histogram
Top Left,Top Right,Bottom Left,Bottom Right
Histogram position
diff --git a/data/effects/frei0r_lightgraffiti.xml b/data/effects/frei0r_lightgraffiti.xml
index acf331e21..953e81897 100644
--- a/data/effects/frei0r_lightgraffiti.xml
+++ b/data/effects/frei0r_lightgraffiti.xml
@@ -1,148 +1,148 @@
Light Graffiti
Light Graffiti effect.
Simon A. Eugster (Granjow)
-
+
Brightness Threshold
R+G+B) does a pixel need to be in order to be recognized as a light source?
Increasing this threshold requires brighter light sources (i.e. more white or less color, respectively) but prevents some «false alarms» where semi-bright parts, e.g. hands where colors can change quite a lot compared to the background, are incorrectly recognized as light source.]]>
-
+
Difference Threshold
max(dR, dG, dB)), in order to be recognized as light source?
Increasing this threshold makes it harder for light sources to be accepted on bright backgrounds, but decreases the danger of noise or generally bright spots counting as light source.]]>
-
+
Difference Sum Threshold
relative to the background image (dR + dG + dB
) have to change until a pixel is recognized as a light source?
Raising this value might, in some cases, avoid that some light objects lit by the light source are added to the light mask.]]>
-
+
Sensitivity
For slowly moving light source try to use a lower sensitivity to obtain a better exposure.]]>
-
+
Lower Overexposure
The light mask does not get white immediately when the light source is moving slowly or staying steady.]]>
-
+
Dimming
Dims the light mask. Lights will leave a fainting trail if it is set to a value > 0.
-
+
Background Weight
Strength of the (calculated) background image. Setting it to 100 paints the light mask directly over the background, without the painting person in the image if the video starts with a «clean» background image. (See the α parameter.)
-
+
α
The Light Graffiti effect remembers the first frame of the clip it is applied to, so the clip should always start with the painter outside of the video. If the background constantly changes, e.g. on a street, try to set α > 0 to calculate an average background image.]]>
-
+
Saturation
Increases the saturation of lights.
Show brightness statistics
Example: To adjust the brightness threshold, check this box and adjust the threshold until the whole light source is highlighted. Repeat the same with the other parameters. Only parts that are highlighted in all thresholds will count as light source.]]>
Show background difference statistics
Show background difference sum statistics
Transparent Background
Makes the background transparent, allowing to apply a composite effect and paint the light mask over a completely different video.
Nonlinear dimming
If normal dimming does not look natural enough, try this one.
Reset
Resets the light mask and the background image. This is necessary e.g. if you apply this effect to a clip in the timeline and then move the timeline cursor from outside of the clip to the middle of it. The effect receives this frame in the middle as first frame and uses it as background image. For proper threshold adjusting move the timeline cursor to the beginning of the clip, check the Reset box and uncheck it again.
Light Graffiti
Light Graffiti effect.
Simon A. Eugster (Granjow)
-
+
Brightness Threshold
R+G+B) does a pixel need to be in order to be recognized as a light source?
Increasing this threshold requires brighter light sources (i.e. more white or less color, respectively) but prevents some «false alarms» where semi-bright parts, e.g. hands where colors can change quite a lot compared to the background, are incorrectly recognized as light source.]]>
-
+
Difference Threshold
max(dR, dG, dB)), in order to be recognized as light source?
Increasing this threshold makes it harder for light sources to be accepted on bright backgrounds, but decreases the danger of noise or generally bright spots counting as light source.]]>
-
+
Difference Sum Threshold
relative to the background image (dR + dG + dB
) have to change until a pixel is recognized as a light source?
Raising this value might, in some cases, avoid that some light objects lit by the light source are added to the light mask.]]>
-
+
Sensitivity
For slowly moving light source try to use a lower sensitivity to obtain a better exposure.]]>
-
+
Lower Overexposure
The light mask does not get white immediately when the light source is moving slowly or staying steady.]]>
-
+
Dimming
Dims the light mask. Lights will leave a fainting trail if it is set to a value > 0.
-
+
Background Weight
Strength of the (calculated) background image. Setting it to 100 paints the light mask directly over the background, without the painting person in the image if the video starts with a «clean» background image. (See the α parameter.)
-
+
α
The Light Graffiti effect remembers the first frame of the clip it is applied to, so the clip should always start with the painter outside of the video. If the background constantly changes, e.g. on a street, try to set α > 0 to calculate an average background image.]]>
-
+
Saturation
Increases the saturation of lights.
Show brightness statistics
Example: To adjust the brightness threshold, check this box and adjust the threshold until the whole light source is highlighted. Repeat the same with the other parameters. Only parts that are highlighted in all thresholds will count as light source.]]>
Show background difference statistics
Show background difference sum statistics
Transparent Background
Makes the background transparent, allowing to apply a composite effect and paint the light mask over a completely different video.
Nonlinear dimming
If normal dimming does not look natural enough, try this one.
Reset
Resets the light mask and the background image. This is necessary e.g. if you apply this effect to a clip in the timeline and then move the timeline cursor from outside of the clip to the middle of it. The effect receives this frame in the middle as first frame and uses it as background image. For proper threshold adjusting move the timeline cursor to the beginning of the clip, check the Reset box and uncheck it again.
diff --git a/data/effects/frei0r_mask0mate.xml b/data/effects/frei0r_mask0mate.xml
index 26ca6eec6..0451b487d 100644
--- a/data/effects/frei0r_mask0mate.xml
+++ b/data/effects/frei0r_mask0mate.xml
@@ -1,24 +1,24 @@
Rectangular Alpha mask
Creates an square alpha-channel mask
Richard Spindler
-
+
Left
-
+
Right
-
+
Top
-
+
Bottom
Invert
-
+
Blur
diff --git a/data/effects/frei0r_medians.xml b/data/effects/frei0r_medians.xml
index 51f2d52fd..6fbee2859 100644
--- a/data/effects/frei0r_medians.xml
+++ b/data/effects/frei0r_medians.xml
@@ -1,17 +1,17 @@
Medians
Implements several median-type filters
Marko Cebokli
Cross5,Square3x3,Bilevel,Diamond3x3,Square5x5,Temp3,Temp5,ArceBI,ML3D,ML3dEX,VarSize
Type
-
+
Size
diff --git a/data/effects/frei0r_nosync0r.xml b/data/effects/frei0r_nosync0r.xml
index 8e2d4cb86..382a66dfe 100644
--- a/data/effects/frei0r_nosync0r.xml
+++ b/data/effects/frei0r_nosync0r.xml
@@ -1,9 +1,9 @@
nosync0r
Broken TV
Martin Bayer
-
+
HSync
diff --git a/data/effects/frei0r_pixeliz0r.xml b/data/effects/frei0r_pixeliz0r.xml
index ab59583d6..f250d6503 100644
--- a/data/effects/frei0r_pixeliz0r.xml
+++ b/data/effects/frei0r_pixeliz0r.xml
@@ -1,12 +1,12 @@
Pixelize
Pixelize input image.
Gephex crew
-
+
Block Size X
-
+
Block Size Y
diff --git a/data/effects/frei0r_pr0be.xml b/data/effects/frei0r_pr0be.xml
index 9f0717fe0..9990da337 100644
--- a/data/effects/frei0r_pr0be.xml
+++ b/data/effects/frei0r_pr0be.xml
@@ -1,40 +1,40 @@
Video values
Measure video values
Marko Cebokli
RGB,Y'PbPr - rec. 601,Y'PbPr - rec. 709,HSV,HSL
Measurement
-
+
X
-
+
Y
-
+
X size
-
+
Y size
256 scale
Show alpha
Big window
diff --git a/data/effects/frei0r_pr0file.xml b/data/effects/frei0r_pr0file.xml
index 5f1fa3fde..b35e5a489 100644
--- a/data/effects/frei0r_pr0file.xml
+++ b/data/effects/frei0r_pr0file.xml
@@ -1,93 +1,93 @@
Oscilloscope
2D video oscilloscope
Marko Cebokli
-
+
X
-
+
Y
-
+
Tilt
-
+
Length
R,G,B,Y',Pr,Pb,Alpha
Channel
-
+
Marker 1
-
+
Marker 2
R trace
G trace
B trace
Y trace
Pr trace
Pb trace
Alpha trace
Display average
Display RMS
Display minimum
Display maximum
256 scale
CCIR rec. 601,CCIR rec. 709
Color
-
+
Crosshair color
diff --git a/data/effects/frei0r_primaries.xml b/data/effects/frei0r_primaries.xml
index 88e4c30af..c6215ed93 100644
--- a/data/effects/frei0r_primaries.xml
+++ b/data/effects/frei0r_primaries.xml
@@ -1,10 +1,10 @@
Primaries
Reduce image to primary colors
Hedde Bosman
-
+
Factor
32 = 0]]>
diff --git a/data/effects/frei0r_saturat0r.xml b/data/effects/frei0r_saturat0r.xml
index d5c08031c..b2de7e49c 100644
--- a/data/effects/frei0r_saturat0r.xml
+++ b/data/effects/frei0r_saturat0r.xml
@@ -1,9 +1,9 @@
Saturation
Adjusts the saturation of a source image
Jean-Sebastien Senecal
-
+
Saturation
diff --git a/data/effects/frei0r_scale0tilt.xml b/data/effects/frei0r_scale0tilt.xml
index 5a7f5e4cf..12be4ebfe 100644
--- a/data/effects/frei0r_scale0tilt.xml
+++ b/data/effects/frei0r_scale0tilt.xml
@@ -1,30 +1,30 @@
Crop, Scale and Tilt
Scales, Tilts and Crops an Image
Richard Spindler
-
+
Crop left
-
+
Crop right
-
+
Crop top
-
+
Crop bottom
-
+
Scale X
-
+
Scale Y
-
+
Tilt X
-
+
Tilt Y
diff --git a/data/effects/frei0r_select0r.xml b/data/effects/frei0r_select0r.xml
index 339ad9098..2b8b348a4 100644
--- a/data/effects/frei0r_select0r.xml
+++ b/data/effects/frei0r_select0r.xml
@@ -1,99 +1,99 @@
- Chroma-key
+ Color Selection
Color based alpha selection
Marko Cebokli
Color to select
Invert selection
-
+
Delta R / A / Hue
-
+
Delta G / B / Chroma
-
+
Delta B / I / I
RGB,ABI,HCI
Selection subspace
Box,Ellipsoid,Diamond
Subspace shape
Hard,Fat,Normal,Skinny
Edge mode
Write on clear,Max,Min,Add,Subtract
Operation
Color Selection
Color based alpha selection
Marko Cebokli
Color to select
Invert selection
RGB,ABI,HCI
Selection subspace
Box,Ellipsoid,Diamond
Subspace shape
Hard,Fat,Normal,Skinny,Slope
Edge mode
-
+
Delta R / A / Hue
-
+
Delta G / B / Chroma
-
+
Delta B / I / I
-
+
Slope
Write on clear,Max,Min,Add,Subtract
Operation
diff --git a/data/effects/frei0r_sharpness.xml b/data/effects/frei0r_sharpness.xml
index a0a1ea9ed..253983bd2 100644
--- a/data/effects/frei0r_sharpness.xml
+++ b/data/effects/frei0r_sharpness.xml
@@ -1,15 +1,15 @@
Sharpen
Unsharp masking (port from Mplayer)
Marko Cebokli, Remi Guyomarch
-
+
Amount
-
+
Size
diff --git a/data/effects/frei0r_sopsat.xml b/data/effects/frei0r_sopsat.xml
index f4a9c1e3a..ed4f34026 100644
--- a/data/effects/frei0r_sopsat.xml
+++ b/data/effects/frei0r_sopsat.xml
@@ -1,112 +1,112 @@
SOP/Sat
Changes Slope, Offset, and Power of the color components, and the overall Saturation, according to the ASC CDL (Color Decision List).
Simon A. Eugster (Granjow)
-
+
Slope Red
All effects can be observed well when applied on a greyscale gradient and looking at the RGB Parade monitor.]]>
-
+
Slope Green
-
+
Slope Blue
-
+
Slope Alpha
-
+
Offset Red
Changing the offset lifts (or lowers) the brightness of each pixel by the given value.
-
+
Offset Green
-
+
Offset Blue
-
+
Offset Alpha
-
+
Power Red
Mathematically, what happens is an exponentiation of the pixel brightness on [0,1]
by the gamma value.]]>
-
+
Power Green
-
+
Power Blue
-
+
Power Alpha
-
+
Overall Saturation
The overall saturation will be changed in the last step of this filter.
SOP/Sat
Changes Slope, Offset, and Power of the color components, and the overall Saturation, according to the ASC CDL (Color Decision List).
Simon A. Eugster (Granjow)
-
+
Slope Red
All effects can be observed well when applied on a greyscale gradient and looking at the RGB Parade monitor.]]>
-
+
Slope Green
-
+
Slope Blue
-
+
Slope Alpha
-
+
Offset Red
Changing the offset lifts (or lowers) the brightness of each pixel by the given value.
-
+
Offset Green
-
+
Offset Blue
-
+
Offset Alpha
-
+
Power Red
Mathematically, what happens is an exponentiation of the pixel brightness on [0,1]
by the gamma value.]]>
-
+
Power Green
-
+
Power Blue
-
+
Power Alpha
-
+
Overall Saturation
The overall saturation will be changed in the last step of this filter.
diff --git a/data/effects/frei0r_squareblur.xml b/data/effects/frei0r_squareblur.xml
index 1c0c3be32..26e8fa3dc 100644
--- a/data/effects/frei0r_squareblur.xml
+++ b/data/effects/frei0r_squareblur.xml
@@ -1,9 +1,9 @@
Square Blur
Square blur
Drone
-
+
Kernel size
diff --git a/data/effects/frei0r_tehroxx0r.xml b/data/effects/frei0r_tehroxx0r.xml
index cc47c7fdf..d55b94ee4 100644
--- a/data/effects/frei0r_tehroxx0r.xml
+++ b/data/effects/frei0r_tehroxx0r.xml
@@ -1,9 +1,9 @@
TehRoxx0r
Something videowall-ish
Coma
-
+
Interval
diff --git a/data/effects/frei0r_threshold0r.xml b/data/effects/frei0r_threshold0r.xml
index 96fe812a7..42c3e5662 100644
--- a/data/effects/frei0r_threshold0r.xml
+++ b/data/effects/frei0r_threshold0r.xml
@@ -1,9 +1,9 @@
Threshold
Thresholds a source image
Jean-Sebastien Senecal
-
+
Threshold
diff --git a/data/effects/frei0r_timeout.xml b/data/effects/frei0r_timeout.xml
index 3b1683371..6b3c6abcf 100644
--- a/data/effects/frei0r_timeout.xml
+++ b/data/effects/frei0r_timeout.xml
@@ -1,16 +1,16 @@
Timeout indicator
Simon A. Eugster (Granjow)
Indicator color
-
+
Time
-
+
Transparency
diff --git a/data/effects/frei0r_tint0r.xml b/data/effects/frei0r_tint0r.xml
index 102a3cc7b..5246d1265 100644
--- a/data/effects/frei0r_tint0r.xml
+++ b/data/effects/frei0r_tint0r.xml
@@ -1,15 +1,15 @@
Tint
Maps source image luminance between two colors specified
Maksim Golovkin
Map black to
Map white to
-
+
Tint amount
diff --git a/data/effects/frei0r_vertigo.xml b/data/effects/frei0r_vertigo.xml
index 32e39d6dd..2c7b176ea 100644
--- a/data/effects/frei0r_vertigo.xml
+++ b/data/effects/frei0r_vertigo.xml
@@ -1,25 +1,25 @@
Vertigo
Alpha blending with zoomed and rotated images
Fukuchi Kentarou
-
+
Phase Increment
-
+
Zoom Rate
Vertigo
Alpha blending with zoomed and rotated images
Fukuchi Kentarou
-
+
Phase Increment
-
+
Zoom Rate
diff --git a/data/effects/frei0r_vignette.xml b/data/effects/frei0r_vignette.xml
index d18cc8c6f..28814b2c2 100644
--- a/data/effects/frei0r_vignette.xml
+++ b/data/effects/frei0r_vignette.xml
@@ -1,15 +1,15 @@
Vignette
Natural lens vignetting effect
Simon A. Eugster (Granjow)
-
+
Aspect ratio
-
+
Clear center size
-
+
Softness
diff --git a/data/effects/gain.xml b/data/effects/gain.xml
index 21f0af5ed..38d9f9800 100644
--- a/data/effects/gain.xml
+++ b/data/effects/gain.xml
@@ -1,9 +1,9 @@
- Gain
- Adjust the audio volume without keyframes
- Dan Dennedy
-
- Gain
-
+ Gain
+ Adjust the audio volume without keyframes
+ Dan Dennedy
+
+ Gain
+
diff --git a/data/effects/ladspa_limiter.xml b/data/effects/ladspa_limiter.xml
index 849a2409f..23663b101 100644
--- a/data/effects/ladspa_limiter.xml
+++ b/data/effects/ladspa_limiter.xml
@@ -1,15 +1,15 @@
Limiter
LADSPA limiter audio effect
http://www.ladspa.org
Input gain (dB)
Limit (dB)
-
+
Release time (s)
diff --git a/data/effects/ladspa_phaser.xml b/data/effects/ladspa_phaser.xml
index af324e902..ab4b1d4d9 100644
--- a/data/effects/ladspa_phaser.xml
+++ b/data/effects/ladspa_phaser.xml
@@ -1,19 +1,19 @@
Phaser
LADSPA phaser audio effect
http://www.ladspa.org
Rate (Hz)
-
+
Depth
Feedback
-
+
Spread
diff --git a/data/effects/ladspa_pitch.xml b/data/effects/ladspa_pitch.xml
index 52be8fd50..682b7252d 100644
--- a/data/effects/ladspa_pitch.xml
+++ b/data/effects/ladspa_pitch.xml
@@ -1,9 +1,9 @@
Pitch Shift
LADSPA change pitch audio effect
http://www.ladspa.org
-
+
Shift
\ No newline at end of file
diff --git a/data/effects/ladspa_pitch_scale.xml b/data/effects/ladspa_pitch_scale.xml
index dff80fe1a..e71307775 100644
--- a/data/effects/ladspa_pitch_scale.xml
+++ b/data/effects/ladspa_pitch_scale.xml
@@ -1,9 +1,9 @@
Pitch Scaler
LADSPA pitch scale audio effect
http://www.ladspa.org
-
+
Co-efficient
diff --git a/data/effects/ladspa_rate_scale.xml b/data/effects/ladspa_rate_scale.xml
index 66eb2c251..0aec3e8a3 100644
--- a/data/effects/ladspa_rate_scale.xml
+++ b/data/effects/ladspa_rate_scale.xml
@@ -1,10 +1,10 @@
Rate Scaler
LADSPA rate scale audio effect
http://www.ladspa.org
-
+
Rate
diff --git a/data/effects/ladspa_reverb.xml b/data/effects/ladspa_reverb.xml
index c70410445..1c0f4eea5 100644
--- a/data/effects/ladspa_reverb.xml
+++ b/data/effects/ladspa_reverb.xml
@@ -1,12 +1,12 @@
Reverb
LADSPA reverb audio effect
http://www.ladspa.org
-
+
Reverb time
-
+
Damping
diff --git a/data/effects/ladspa_room_reverb.xml b/data/effects/ladspa_room_reverb.xml
index d1b7e5f77..c9e1d8c61 100644
--- a/data/effects/ladspa_room_reverb.xml
+++ b/data/effects/ladspa_room_reverb.xml
@@ -1,15 +1,15 @@
Room Reverb
LADSPA room reverb audio effect
http://www.ladspa.org
Room size (m)
-
+
Delay (s/10)
-
+
Damping
diff --git a/data/effects/lift_gamma_gain.xml b/data/effects/lift_gamma_gain.xml
index 84b13ce6b..6a56b5034 100644
--- a/data/effects/lift_gamma_gain.xml
+++ b/data/effects/lift_gamma_gain.xml
@@ -1,32 +1,32 @@
Lift/gamma/gain
Brian Matherly
Lift: Red
-
+
Lift: Green
-
+
Lift: Blue
-
+
Gamma: Red
-
+
Gamma: Green
-
+
Gamma: Blue
-
+
Gain: Red
-
+
Gain: Green
-
+
Gain: Blue
diff --git a/data/effects/movit_deconvolution_sharpen.xml b/data/effects/movit_deconvolution_sharpen.xml
index c76b64448..2e82208db 100644
--- a/data/effects/movit_deconvolution_sharpen.xml
+++ b/data/effects/movit_deconvolution_sharpen.xml
@@ -1,20 +1,20 @@
Deconvolution sharpen (GPU)
Steinar H. Gunderson
-
+
Matrix size
Circle radius
Gaussian radius
Correlation
Noise
diff --git a/data/effects/movit_lift_gamma_gain.xml b/data/effects/movit_lift_gamma_gain.xml
index 85aa83fb7..d7bc469f2 100644
--- a/data/effects/movit_lift_gamma_gain.xml
+++ b/data/effects/movit_lift_gamma_gain.xml
@@ -1,32 +1,32 @@
Lift/gamma/gain (GPU)
Steinar H. Gunderson
Lift: Red
-
+
Lift: Green
-
+
Lift: Blue
-
+
Gamma: Red
-
+
Gamma: Green
-
+
Gamma: Blue
-
+
Gain: Red
-
+
Gain: Green
-
+
Gain: Blue
diff --git a/data/effects/movit_lift_gamma_gain2.xml b/data/effects/movit_lift_gamma_gain2.xml
index fd645876f..861ccd850 100644
--- a/data/effects/movit_lift_gamma_gain2.xml
+++ b/data/effects/movit_lift_gamma_gain2.xml
@@ -1,32 +1,32 @@
Movit: Lift/gamma/gain (colors)
Steinar H. Gunderson
-
+
Lift: Red
-
+
Lift: Green
-
+
Lift: Blue
-
+
Gamma: Red
-
+
Gamma: Green
-
+
Gamma: Blue
-
+
Gain: Red
-
+
Gain: Green
-
+
Gain: Blue
diff --git a/data/effects/movit_white_balance.xml b/data/effects/movit_white_balance.xml
index d5913b1ea..b3a13e5fb 100644
--- a/data/effects/movit_white_balance.xml
+++ b/data/effects/movit_white_balance.xml
@@ -1,12 +1,12 @@
White Balance (GPU)
Steinar H. Gunderson
Neutral Color
-
+
Color Temperature
diff --git a/data/effects/pan_zoom.xml b/data/effects/pan_zoom.xml
index cf3841606..b61d59c1e 100644
--- a/data/effects/pan_zoom.xml
+++ b/data/effects/pan_zoom.xml
@@ -1,18 +1,18 @@
Position and Zoom
Adjust size and position of clip
Charles Yates
Rectangle
-
- Distort
-
-
- Normalise
-
+
+ Distort
+
+
+ Normalise
+
Background Color
diff --git a/data/effects/qtblend.xml b/data/effects/qtblend.xml
index 158446dc3..8049a6161 100644
--- a/data/effects/qtblend.xml
+++ b/data/effects/qtblend.xml
@@ -1,44 +1,44 @@
Transform
Position, scale and opacity.
Jean-Baptiste Mardelle
Rectangle
Rotation
Alpha blend,Xor,Plus,Multiply,Screen,Overlay,Darken,Lighten,Color dodge,Color burn,Hard light,Soft light,Difference,Exclusion,Bitwise or,Bitwise and,Bitwise xor,Bitwise nor,Bitwise nand,Bitwise not xor,Destination in,Destination out
Compositing
Distort
Transform
Position, scale and opacity.
Jean-Baptiste Mardelle
Rectangle
-
+
Rotation
Alpha blend,Xor,Plus,Multiply,Screen,Overlay,Darken,Lighten,Color dodge,Color burn,Hard light,Soft light,Difference,Exclusion,Bitwise or,Bitwise and,Bitwise xor,Bitwise nor,Bitwise nand,Bitwise not xor,Destination in,Destination out
Compositing
Distort
Rotate from center
diff --git a/data/effects/rotoscoping.xml b/data/effects/rotoscoping.xml
index c77b07ac7..b3ff1d63a 100644
--- a/data/effects/rotoscoping.xml
+++ b/data/effects/rotoscoping.xml
@@ -1,32 +1,33 @@
Rotoscoping
Keyframable vector based rotoscoping
Till Theato
+
Alpha,Luma,RGB
Mode
Write on clear,Maximum,Minimum,Add,Subtract
Alpha Operation
Invert
Feather width
Feathering passes
diff --git a/data/effects/sox_echo.xml b/data/effects/sox_echo.xml
index a9780861b..a35210c02 100644
--- a/data/effects/sox_echo.xml
+++ b/data/effects/sox_echo.xml
@@ -1,18 +1,18 @@
Sox Echo
Sox echo audio effect
http://sox.sourceforge.net
-
+
Gain In
-
+
Gain Out
Delay
-
+
Decay
diff --git a/data/effects/sox_flanger.xml b/data/effects/sox_flanger.xml
index 515b3d43e..0138eb4fa 100644
--- a/data/effects/sox_flanger.xml
+++ b/data/effects/sox_flanger.xml
@@ -1,30 +1,30 @@
Sox Flanger
Sox flanger audio effect
http://sox.sourceforge.net
Delay
Depth
Regeneration
Width
-
+
Speed
Shape
Phase
Interpolation
diff --git a/data/effects/sox_phaser.xml b/data/effects/sox_phaser.xml
index f0fc204e6..3308eca25 100644
--- a/data/effects/sox_phaser.xml
+++ b/data/effects/sox_phaser.xml
@@ -1,21 +1,21 @@
Sox Phaser
Sox phaser audio effect
http://sox.sourceforge.net
-
+
Gain In
-
+
Gain Out
Delay
-
+
Decay
-
+
Speed
diff --git a/data/effects/sox_stretch.xml b/data/effects/sox_stretch.xml
index 2b2c777e5..fb072228d 100644
--- a/data/effects/sox_stretch.xml
+++ b/data/effects/sox_stretch.xml
@@ -1,12 +1,12 @@
Sox Stretch
Sox stretch audio effect
http://sox.sourceforge.net
-
+
Factor
Window
diff --git a/data/effects/speed.xml b/data/effects/speed.xml
index c09bb93ab..00971e5fb 100644
--- a/data/effects/speed.xml
+++ b/data/effects/speed.xml
@@ -1,9 +1,9 @@
Speed
Make clip play faster or slower
Brian Matherly
-
+
Speed
diff --git a/data/effects/vignette.xml b/data/effects/vignette.xml
index 3ded174e1..ae6003d90 100644
--- a/data/effects/vignette.xml
+++ b/data/effects/vignette.xml
@@ -1,24 +1,24 @@
Vignette Effect
Adjustable Vignette
Marco Gittler
-
+
smooth
-
+
radius
-
+
x
-
+
y
opacity
use cos instead of linear
diff --git a/data/effects/volume.xml b/data/effects/volume.xml
index 5393daf5e..c0bfb75c0 100644
--- a/data/effects/volume.xml
+++ b/data/effects/volume.xml
@@ -1,9 +1,9 @@
Volume (keyframable)
Adjust audio volume with keyframes
Dan Dennedy
-
+
Gain
diff --git a/data/effects/wave.xml b/data/effects/wave.xml
index a0da2d4ca..6bdc3ef68 100644
--- a/data/effects/wave.xml
+++ b/data/effects/wave.xml
@@ -1,15 +1,15 @@
Wave
Make waves on your clip with keyframes
Leny Grisel
-
+
Amplitude
Horizontal
Vertical
diff --git a/data/profiles.xml b/data/profiles.xml
index ac97276c2..1ac79366a 100644
--- a/data/profiles.xml
+++ b/data/profiles.xml
@@ -1,59 +1,59 @@
diff --git a/src/assets/abstractassetsrepository.ipp b/src/assets/abstractassetsrepository.ipp
index b10ec84ab..68f357d17 100644
--- a/src/assets/abstractassetsrepository.ipp
+++ b/src/assets/abstractassetsrepository.ipp
@@ -1,316 +1,318 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.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 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "xml/xml.hpp"
#include
#include
#include
#include
#include
#include
#ifdef Q_OS_MAC
#include
#endif
template AbstractAssetsRepository::AbstractAssetsRepository() = default;
template void AbstractAssetsRepository::init()
{
// Warning: Mlt::Factory::init() resets the locale to the default system value, make sure we keep correct locale
#ifndef Q_OS_MAC
setlocale(LC_NUMERIC, nullptr);
#else
setlocale(LC_NUMERIC_MASK, nullptr);
#endif
// Parse effects blacklist
parseBlackList(assetBlackListPath());
// Retrieve the list of MLT's available assets.
QScopedPointer assets(retrieveListFromMlt());
int max = assets->count();
QString sox = QStringLiteral("sox.");
for (int i = 0; i < max; ++i) {
Info info;
QString name = assets->get_name(i);
info.id = name;
if (name.startsWith(sox)) {
// sox effects are not usage directly (parameters not available)
continue;
}
// qDebug() << "trying to parse " < customAssets;
- for (const auto &dir : asset_dirs) {
+ // reverse order to prioritize local install
+ QListIterator dirs_it(asset_dirs);
+ for (dirs_it.toBack(); dirs_it.hasPrevious();) { auto dir=dirs_it.previous();
QDir current_dir(dir);
QStringList filter;
filter << QStringLiteral("*.xml");
QStringList fileList = current_dir.entryList(filter, QDir::Files);
for (const auto &file : fileList) {
QString path = current_dir.absoluteFilePath(file);
parseCustomAssetFile(path, customAssets);
}
}
// We add the custom assets
for (const auto &custom : customAssets) {
// Custom assets should override default ones
m_assets[custom.first] = custom.second;
/*if (m_assets.count(custom.second.mltId) > 0) {
m_assets.erase(custom.second.mltId);
}
if (m_assets.count(custom.first) == 0) {
m_assets[custom.first] = custom.second;
} else {
qDebug() << "Error: conflicting asset name " << custom.first;
}*/
}
}
template void AbstractAssetsRepository::parseBlackList(const QString &path)
{
QFile blacklist_file(path);
if (blacklist_file.open(QIODevice::ReadOnly)) {
QTextStream stream(&blacklist_file);
QString line;
while (stream.readLineInto(&line)) {
line = line.simplified();
if (!line.isEmpty() && !line.startsWith('#')) {
m_blacklist.insert(line);
}
}
blacklist_file.close();
}
}
template bool AbstractAssetsRepository::parseInfoFromMlt(const QString &assetId, Info &res)
{
QScopedPointer metadata(getMetadata(assetId));
if (metadata && metadata->is_valid()) {
if (metadata->get("title") && metadata->get("identifier") && strlen(metadata->get("title")) > 0) {
QString id = metadata->get("identifier");
res.name = metadata->get("title");
res.name[0] = res.name[0].toUpper();
res.description = metadata->get("description");
res.description.append(QString(" (%1)").arg(id));
res.author = metadata->get("creator");
res.version_str = metadata->get("version");
res.version = ceil(100 * metadata->get_double("version"));
res.id = res.mltId = assetId;
parseType(metadata, res);
// Create params
QDomDocument doc;
QDomElement eff = doc.createElement(QStringLiteral("effect"));
eff.setAttribute(QStringLiteral("tag"), id);
eff.setAttribute(QStringLiteral("id"), id);
////qCDebug(KDENLIVE_LOG)<<"Effect: "<get_data("parameters"));
for (int j = 0; param_props.is_valid() && j < param_props.count(); ++j) {
QDomElement params = doc.createElement(QStringLiteral("parameter"));
Mlt::Properties paramdesc((mlt_properties)param_props.get_data(param_props.get_name(j)));
params.setAttribute(QStringLiteral("name"), paramdesc.get("identifier"));
if (params.attribute(QStringLiteral("name")) == QLatin1String("argument")) {
// This parameter has to be given as attribute when using command line, do not show it in Kdenlive
continue;
}
if (paramdesc.get("readonly") && (strcmp(paramdesc.get("readonly"), "yes") == 0)) {
// Do not expose readonly parameters
continue;
}
if (paramdesc.get("maximum")) {
params.setAttribute(QStringLiteral("max"), paramdesc.get("maximum"));
}
if (paramdesc.get("minimum")) {
params.setAttribute(QStringLiteral("min"), paramdesc.get("minimum"));
}
QString paramType = paramdesc.get("type");
if (paramType == QLatin1String("integer")) {
if (params.attribute(QStringLiteral("min")) == QLatin1String("0") && params.attribute(QStringLiteral("max")) == QLatin1String("1")) {
params.setAttribute(QStringLiteral("type"), QStringLiteral("bool"));
} else {
params.setAttribute(QStringLiteral("type"), QStringLiteral("constant"));
}
} else if (paramType == QLatin1String("float")) {
params.setAttribute(QStringLiteral("type"), QStringLiteral("constant"));
// param type is float, set default decimals to 3
params.setAttribute(QStringLiteral("decimals"), QStringLiteral("3"));
} else if (paramType == QLatin1String("boolean")) {
params.setAttribute(QStringLiteral("type"), QStringLiteral("bool"));
} else if (paramType == QLatin1String("geometry")) {
params.setAttribute(QStringLiteral("type"), QStringLiteral("geometry"));
} else if (paramType == QLatin1String("string")) {
// string parameter are not really supported, so if we have a default value, enforce it
params.setAttribute(QStringLiteral("type"), QStringLiteral("fixed"));
if (paramdesc.get("default")) {
QString stringDefault = paramdesc.get("default");
stringDefault.remove(QLatin1Char('\''));
params.setAttribute(QStringLiteral("value"), stringDefault);
} else {
// String parameter without default, skip it completely
continue;
}
} else {
params.setAttribute(QStringLiteral("type"), paramType);
if (!QString(paramdesc.get("format")).isEmpty()) {
params.setAttribute(QStringLiteral("format"), paramdesc.get("format"));
}
}
if (!params.hasAttribute(QStringLiteral("value"))) {
if (paramdesc.get("default")) {
params.setAttribute(QStringLiteral("default"), paramdesc.get("default"));
}
if (paramdesc.get("value")) {
params.setAttribute(QStringLiteral("value"), paramdesc.get("value"));
} else {
params.setAttribute(QStringLiteral("value"), paramdesc.get("default"));
}
}
QString paramName = paramdesc.get("title");
if (!paramName.isEmpty()) {
QDomElement pname = doc.createElement(QStringLiteral("name"));
pname.appendChild(doc.createTextNode(paramName));
params.appendChild(pname);
}
if (paramdesc.get("description")) {
QDomElement comment = doc.createElement(QStringLiteral("comment"));
comment.appendChild(doc.createTextNode(paramdesc.get("description")));
params.appendChild(comment);
}
eff.appendChild(params);
}
doc.appendChild(eff);
res.xml = eff;
return true;
}
}
return false;
}
template bool AbstractAssetsRepository::exists(const QString &assetId) const
{
return m_assets.count(assetId) > 0;
}
template QVector> AbstractAssetsRepository::getNames() const
{
QVector> res;
res.reserve((int)m_assets.size());
for (const auto &asset : m_assets) {
res.push_back({asset.first, asset.second.name});
}
std::sort(res.begin(), res.end(), [](const QPair &a, const QPair &b) { return a.second < b.second; });
return res;
}
template AssetType AbstractAssetsRepository::getType(const QString &assetId) const
{
Q_ASSERT(m_assets.count(assetId) > 0);
return m_assets.at(assetId).type;
}
template QString AbstractAssetsRepository::getName(const QString &assetId) const
{
Q_ASSERT(m_assets.count(assetId) > 0);
return m_assets.at(assetId).name;
}
template QString AbstractAssetsRepository::getDescription(const QString &assetId) const
{
Q_ASSERT(m_assets.count(assetId) > 0);
return m_assets.at(assetId).description;
}
template bool AbstractAssetsRepository::parseInfoFromXml(const QDomElement ¤tAsset, Info &res) const
{
QString tag = currentAsset.attribute(QStringLiteral("tag"), QString());
QString id = currentAsset.attribute(QStringLiteral("id"), QString());
if (id.isEmpty()) {
id = tag;
}
if (!exists(tag)) {
qDebug() << "++++++ Unknown asset : " << tag;
return false;
}
// Check if there is a maximal version set
if (currentAsset.hasAttribute(QStringLiteral("version"))) {
// a specific version of the filter is required
if (m_assets.at(tag).version < (int)(100 * currentAsset.attribute(QStringLiteral("version")).toDouble())) {
return false;
}
}
res = m_assets.at(tag);
res.id = id;
res.mltId = tag;
// Update description if the xml provide one
QString description = Xml::getSubTagContent(currentAsset, QStringLiteral("description"));
if (!description.isEmpty()) {
res.description = description;
res.description.append(QString(" (%1)").arg(tag));
}
// Update name if the xml provide one
QString name = Xml::getSubTagContent(currentAsset, QStringLiteral("name"));
if (!name.isEmpty()) {
res.name = name;
}
return true;
}
template QDomElement AbstractAssetsRepository::getXml(const QString &assetId) const
{
if (m_assets.count(assetId) == 0) {
qDebug() << "Error : Requesting info on unknown transition " << assetId;
return QDomElement();
}
return m_assets.at(assetId).xml.cloneNode().toElement();
}
diff --git a/src/assets/model/assetparametermodel.cpp b/src/assets/model/assetparametermodel.cpp
index 658ec4548..d43ad2217 100644
--- a/src/assets/model/assetparametermodel.cpp
+++ b/src/assets/model/assetparametermodel.cpp
@@ -1,817 +1,811 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.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 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "assetparametermodel.hpp"
#include "assets/keyframes/model/keyframemodellist.hpp"
#include "core.h"
#include "kdenlivesettings.h"
#include "klocalizedstring.h"
#include "profiles/profilemodel.hpp"
#include
#include
#include
#include
#include
AssetParameterModel::AssetParameterModel(std::unique_ptr asset, const QDomElement &assetXml, const QString &assetId, ObjectId ownerId,
QObject *parent)
: QAbstractListModel(parent)
, monitorId(ownerId.first == ObjectType::BinClip ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor)
, m_assetId(assetId)
, m_ownerId(ownerId)
, m_asset(std::move(asset))
, m_keyframes(nullptr)
{
Q_ASSERT(m_asset->is_valid());
QDomNodeList nodeList = assetXml.elementsByTagName(QStringLiteral("parameter"));
m_hideKeyframesByDefault = assetXml.hasAttribute(QStringLiteral("hideKeyframes"));
bool needsLocaleConversion = false;
QChar separator, oldSeparator;
// Check locale, default effects xml has no LC_NUMERIC defined and always uses the C locale
QLocale locale;
if (assetXml.hasAttribute(QStringLiteral("LC_NUMERIC"))) {
QLocale effectLocale = QLocale(assetXml.attribute(QStringLiteral("LC_NUMERIC")));
if (QLocale::c().decimalPoint() != effectLocale.decimalPoint()) {
needsLocaleConversion = true;
separator = QLocale::c().decimalPoint();
oldSeparator = effectLocale.decimalPoint();
}
}
qDebug() << "XML parsing of " << assetId << ". found : " << nodeList.count();
for (int i = 0; i < nodeList.count(); ++i) {
QDomElement currentParameter = nodeList.item(i).toElement();
// Convert parameters if we need to
if (needsLocaleConversion) {
QDomNamedNodeMap attrs = currentParameter.attributes();
for (int k = 0; k < attrs.count(); ++k) {
QString nodeName = attrs.item(k).nodeName();
if (nodeName != QLatin1String("type") && nodeName != QLatin1String("name")) {
QString val = attrs.item(k).nodeValue();
if (val.contains(oldSeparator)) {
QString newVal = val.replace(oldSeparator, separator);
attrs.item(k).setNodeValue(newVal);
}
}
}
}
// Parse the basic attributes of the parameter
QString name = currentParameter.attribute(QStringLiteral("name"));
QString type = currentParameter.attribute(QStringLiteral("type"));
QString value = currentParameter.attribute(QStringLiteral("value"));
ParamRow currentRow;
currentRow.type = paramTypeFromStr(type);
currentRow.xml = currentParameter;
if (value.isNull()) {
QVariant defaultValue = parseAttribute(m_ownerId, QStringLiteral("default"), currentParameter);
value = defaultValue.type() == QVariant::Double ? locale.toString(defaultValue.toDouble()) : defaultValue.toString();
}
bool isFixed = (type == QLatin1String("fixed"));
if (isFixed) {
m_fixedParams[name] = value;
} else if (currentRow.type == ParamType::Position) {
int val = value.toInt();
if (val < 0) {
int in = pCore->getItemIn(m_ownerId);
int out = in + pCore->getItemDuration(m_ownerId);
val += out;
value = QString::number(val);
}
} else if (currentRow.type == ParamType::KeyframeParam || currentRow.type == ParamType::AnimatedRect) {
if (!value.contains(QLatin1Char('='))) {
value.prepend(QStringLiteral("%1=").arg(pCore->getItemIn(m_ownerId)));
}
}
if (!name.isEmpty()) {
setParameter(name, value, false);
// Keep track of param order
m_paramOrder.push_back(name);
}
if (isFixed) {
// fixed parameters are not displayed so we don't store them.
continue;
}
currentRow.value = value;
QString title = currentParameter.firstChildElement(QStringLiteral("name")).text();
currentRow.name = title.isEmpty() ? name : title;
m_params[name] = currentRow;
m_rows.push_back(name);
}
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Sox effects need to have a special "Effect" value set
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
}
qDebug() << "END parsing of " << assetId << ". Number of found parameters" << m_rows.size();
emit modelChanged();
}
void AssetParameterModel::prepareKeyframes()
{
if (m_keyframes) return;
int ix = 0;
for (const auto &name : m_rows) {
if (m_params[name].type == ParamType::KeyframeParam || m_params[name].type == ParamType::AnimatedRect ||
m_params[name].type == ParamType::Roto_spline) {
addKeyframeParam(index(ix, 0));
}
ix++;
}
}
void AssetParameterModel::setParameter(const QString &name, int value, bool update)
{
Q_ASSERT(m_asset->is_valid());
m_asset->set(name.toLatin1().constData(), value);
if (m_fixedParams.count(name) == 0) {
m_params[name].value = value;
} else {
m_fixedParams[name] = value;
}
if (update) {
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Warning, SOX effect, need unplug/replug
qDebug() << "// Warning, SOX effect, need unplug/replug";
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
emit replugEffect(shared_from_this());
} else if (m_assetId == QLatin1String("autotrack_rectangle") || m_assetId.startsWith(QStringLiteral("ladspa"))) {
// these effects don't understand param change and need to be rebuild
emit replugEffect(shared_from_this());
} else {
emit modelChanged();
emit dataChanged(index(0, 0), index(m_rows.count() - 1, 0), {});
}
// Update fades in timeline
pCore->updateItemModel(m_ownerId, m_assetId);
// Trigger monitor refresh
pCore->refreshProjectItem(m_ownerId);
// Invalidate timeline preview
pCore->invalidateItem(m_ownerId);
}
}
void AssetParameterModel::setParameter(const QString &name, const QString ¶mValue, bool update, const QModelIndex ¶mIndex)
{
Q_ASSERT(m_asset->is_valid());
QLocale locale;
locale.setNumberOptions(QLocale::OmitGroupSeparator);
qDebug() << "// PROCESSING PARAM CHANGE: " << name << ", UPDATE: " << update << ", VAL: " << paramValue;
// TODO: this does not really belong here, but I don't see another way to do it so that undo works
if (data(paramIndex, AssetParameterModel::TypeRole).value() == ParamType::Curve) {
QStringList vals = paramValue.split(QLatin1Char(';'), QString::SkipEmptyParts);
int points = vals.size();
m_asset->set("3", points / 10.);
// for the curve, inpoints are numbered: 6, 8, 10, 12, 14
// outpoints, 7, 9, 11, 13,15 so we need to deduce these enums
for (int i = 0; i < points; i++) {
const QString &pointVal = vals.at(i);
int idx = 2 * i + 6;
m_asset->set(QString::number(idx).toLatin1().constData(), pointVal.section(QLatin1Char('/'), 0, 0).toDouble());
idx++;
m_asset->set(QString::number(idx).toLatin1().constData(), pointVal.section(QLatin1Char('/'), 1, 1).toDouble());
}
}
bool conversionSuccess;
double doubleValue = locale.toDouble(paramValue, &conversionSuccess);
if (conversionSuccess) {
m_asset->set(name.toLatin1().constData(), doubleValue);
if (m_fixedParams.count(name) == 0) {
m_params[name].value = doubleValue;
} else {
m_fixedParams[name] = doubleValue;
}
} else {
m_asset->set(name.toLatin1().constData(), paramValue.toUtf8().constData());
qDebug() << " = = SET EFFECT PARAM: " << name << " = " << paramValue;
if (m_fixedParams.count(name) == 0) {
m_params[name].value = paramValue;
} else {
m_fixedParams[name] = paramValue;
}
}
if (update) {
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Warning, SOX effect, need unplug/replug
qDebug() << "// Warning, SOX effect, need unplug/replug";
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
emit replugEffect(shared_from_this());
} else if (m_assetId == QLatin1String("autotrack_rectangle") || m_assetId.startsWith(QStringLiteral("ladspa"))) {
// these effects don't understand param change and need to be rebuild
emit replugEffect(shared_from_this());
} else {
qDebug() << "// SENDING DATA CHANGE....";
if (paramIndex.isValid()) {
emit dataChanged(paramIndex, paramIndex);
} else {
QModelIndex ix = index(m_rows.indexOf(name), 0);
emit dataChanged(ix, ix);
}
emit modelChanged();
}
}
emit updateChildren(name);
// Update timeline view if necessary
if (m_ownerId.first == ObjectType::NoItem) {
// Used for generator clips
if (!update) emit modelChanged();
} else {
// Update fades in timeline
pCore->updateItemModel(m_ownerId, m_assetId);
// Trigger monitor refresh
pCore->refreshProjectItem(m_ownerId);
// Invalidate timeline preview
pCore->invalidateItem(m_ownerId);
}
}
void AssetParameterModel::setParameter(const QString &name, double &value)
{
Q_ASSERT(m_asset->is_valid());
m_asset->set(name.toLatin1().constData(), value);
if (m_fixedParams.count(name) == 0) {
m_params[name].value = value;
} else {
m_fixedParams[name] = value;
}
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Warning, SOX effect, need unplug/replug
qDebug() << "// Warning, SOX effect, need unplug/replug";
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
emit replugEffect(shared_from_this());
} else if (m_assetId == QLatin1String("autotrack_rectangle") || m_assetId.startsWith(QStringLiteral("ladspa"))) {
// these effects don't understand param change and need to be rebuild
emit replugEffect(shared_from_this());
} else {
emit modelChanged();
}
pCore->refreshProjectItem(m_ownerId);
pCore->invalidateItem(m_ownerId);
}
AssetParameterModel::~AssetParameterModel() = default;
QVariant AssetParameterModel::data(const QModelIndex &index, int role) const
{
if (index.row() < 0 || index.row() >= m_rows.size() || !index.isValid()) {
return QVariant();
}
QString paramName = m_rows[index.row()];
Q_ASSERT(m_params.count(paramName) > 0);
const QDomElement &element = m_params.at(paramName).xml;
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
return m_params.at(paramName).name;
case NameRole:
return paramName;
case TypeRole:
return QVariant::fromValue(m_params.at(paramName).type);
case CommentRole: {
QDomElement commentElem = element.firstChildElement(QStringLiteral("comment"));
QString comment;
if (!commentElem.isNull()) {
comment = i18n(commentElem.text().toUtf8().data());
}
return comment;
}
case InRole:
return m_asset->get_int("in");
case OutRole:
return m_asset->get_int("out");
case ParentInRole:
return pCore->getItemIn(m_ownerId);
case ParentDurationRole:
return pCore->getItemDuration(m_ownerId);
case ParentPositionRole:
return pCore->getItemPosition(m_ownerId);
case HideKeyframesFirstRole:
return m_hideKeyframesByDefault;
case MinRole:
return parseAttribute(m_ownerId, QStringLiteral("min"), element);
case MaxRole:
return parseAttribute(m_ownerId, QStringLiteral("max"), element);
case FactorRole:
return parseAttribute(m_ownerId, QStringLiteral("factor"), element, 1);
case ScaleRole:
return parseAttribute(m_ownerId, QStringLiteral("scale"), element, 0);
case DecimalsRole:
return parseAttribute(m_ownerId, QStringLiteral("decimals"), element);
case DefaultRole:
return parseAttribute(m_ownerId, QStringLiteral("default"), element);
case FilterRole:
return parseAttribute(m_ownerId, QStringLiteral("filter"), element);
case SuffixRole:
return element.attribute(QStringLiteral("suffix"));
case OpacityRole:
return element.attribute(QStringLiteral("opacity")) != QLatin1String("false");
case RelativePosRole:
return element.attribute(QStringLiteral("relative")) == QLatin1String("true");
case ShowInTimelineRole:
return !element.hasAttribute(QStringLiteral("notintimeline"));
case AlphaRole:
return element.attribute(QStringLiteral("alpha")) == QLatin1String("1");
case ValueRole: {
QString value(m_asset->get(paramName.toUtf8().constData()));
return value.isEmpty() ? (element.attribute(QStringLiteral("value")).isNull() ? parseAttribute(m_ownerId, QStringLiteral("default"), element)
: element.attribute(QStringLiteral("value")))
: value;
}
case ListValuesRole:
return element.attribute(QStringLiteral("paramlist")).split(QLatin1Char(';'));
case ListNamesRole: {
QDomElement namesElem = element.firstChildElement(QStringLiteral("paramlistdisplay"));
return i18n(namesElem.text().toUtf8().data()).split(QLatin1Char(','));
}
case List1Role:
return parseAttribute(m_ownerId, QStringLiteral("list1"), element);
case List2Role:
return parseAttribute(m_ownerId, QStringLiteral("list2"), element);
case Enum1Role:
return m_asset->get_double("1");
case Enum2Role:
return m_asset->get_double("2");
case Enum3Role:
return m_asset->get_double("3");
case Enum4Role:
return m_asset->get_double("4");
case Enum5Role:
return m_asset->get_double("5");
case Enum6Role:
return m_asset->get_double("6");
case Enum7Role:
return m_asset->get_double("7");
case Enum8Role:
return m_asset->get_double("8");
case Enum9Role:
return m_asset->get_double("9");
case Enum10Role:
return m_asset->get_double("10");
case Enum11Role:
return m_asset->get_double("11");
case Enum12Role:
return m_asset->get_double("12");
case Enum13Role:
return m_asset->get_double("13");
case Enum14Role:
return m_asset->get_double("14");
case Enum15Role:
return m_asset->get_double("15");
}
return QVariant();
}
int AssetParameterModel::rowCount(const QModelIndex &parent) const
{
qDebug() << "===================================================== Requested rowCount" << parent << m_rows.size();
if (parent.isValid()) return 0;
return m_rows.size();
}
// static
ParamType AssetParameterModel::paramTypeFromStr(const QString &type)
{
if (type == QLatin1String("double") || type == QLatin1String("float") || type == QLatin1String("constant")) {
return ParamType::Double;
}
if (type == QLatin1String("list")) {
return ParamType::List;
}
if (type == QLatin1String("bool")) {
return ParamType::Bool;
}
if (type == QLatin1String("switch")) {
return ParamType::Switch;
} else if (type == QLatin1String("simplekeyframe")) {
return ParamType::KeyframeParam;
} else if (type == QLatin1String("animatedrect")) {
return ParamType::AnimatedRect;
} else if (type == QLatin1String("geometry")) {
return ParamType::Geometry;
} else if (type == QLatin1String("addedgeometry")) {
return ParamType::Addedgeometry;
} else if (type == QLatin1String("keyframe") || type == QLatin1String("animated")) {
return ParamType::KeyframeParam;
} else if (type == QLatin1String("color")) {
return ParamType::Color;
} else if (type == QLatin1String("colorwheel")) {
return ParamType::ColorWheel;
} else if (type == QLatin1String("position")) {
return ParamType::Position;
} else if (type == QLatin1String("curve")) {
return ParamType::Curve;
} else if (type == QLatin1String("bezier_spline")) {
return ParamType::Bezier_spline;
} else if (type == QLatin1String("roto-spline")) {
return ParamType::Roto_spline;
} else if (type == QLatin1String("wipe")) {
return ParamType::Wipe;
} else if (type == QLatin1String("url")) {
return ParamType::Url;
} else if (type == QLatin1String("keywords")) {
return ParamType::Keywords;
} else if (type == QLatin1String("fontfamily")) {
return ParamType::Fontfamily;
} else if (type == QLatin1String("filterjob")) {
return ParamType::Filterjob;
} else if (type == QLatin1String("readonly")) {
return ParamType::Readonly;
} else if (type == QLatin1String("hidden")) {
return ParamType::Hidden;
}
qDebug() << "WARNING: Unknown type :" << type;
return ParamType::Double;
}
// static
QString AssetParameterModel::getDefaultKeyframes(int start, const QString &defaultValue, bool linearOnly)
{
QString keyframes = QString::number(start);
if (linearOnly) {
keyframes.append(QLatin1Char('='));
} else {
switch (KdenliveSettings::defaultkeyframeinterp()) {
case mlt_keyframe_discrete:
keyframes.append(QStringLiteral("|="));
break;
case mlt_keyframe_smooth:
keyframes.append(QStringLiteral("~="));
break;
default:
keyframes.append(QLatin1Char('='));
break;
}
}
keyframes.append(defaultValue);
return keyframes;
}
// static
QVariant AssetParameterModel::parseAttribute(const ObjectId &owner, const QString &attribute, const QDomElement &element, QVariant defaultValue)
{
if (!element.hasAttribute(attribute) && !defaultValue.isNull()) {
return defaultValue;
}
ParamType type = paramTypeFromStr(element.attribute(QStringLiteral("type")));
QString content = element.attribute(attribute);
if (content.contains(QLatin1Char('%'))) {
std::unique_ptr &profile = pCore->getCurrentProfile();
int width = profile->width();
int height = profile->height();
int in = pCore->getItemIn(owner);
int out = in + pCore->getItemDuration(owner);
// replace symbols in the double parameter
content.replace(QLatin1String("%maxWidth"), QString::number(width))
.replace(QLatin1String("%maxHeight"), QString::number(height))
.replace(QLatin1String("%width"), QString::number(width))
.replace(QLatin1String("%height"), QString::number(height))
.replace(QLatin1String("%out"), QString::number(out));
- if (type == ParamType::Double) {
+ if (type == ParamType::Double || type == ParamType::Hidden) {
// Use a Mlt::Properties to parse mathematical operators
Mlt::Properties p;
- p.set("eval", content.toLatin1().constData());
+ p.set("eval", content.prepend(QLatin1Char('@')).toLatin1().constData());
return p.get_double("eval");
}
} else if (type == ParamType::Double || type == ParamType::Hidden) {
QLocale locale;
locale.setNumberOptions(QLocale::OmitGroupSeparator);
- if (attribute == QLatin1String("default")) {
- int factor = element.attribute(QStringLiteral("factor"), QStringLiteral("1")).toInt();
- if (factor > 0) {
- return content.toDouble() / factor;
- }
- }
return locale.toDouble(content);
}
if (attribute == QLatin1String("default")) {
if (type == ParamType::RestrictedAnim) {
content = getDefaultKeyframes(0, content, true);
} else if (type == ParamType::KeyframeParam) {
return content.toDouble();
} else if (type == ParamType::List) {
bool ok;
double res = content.toDouble(&ok);
if (ok) {
return res;
}
} else if (type == ParamType::Bezier_spline) {
QLocale locale;
if (locale.decimalPoint() != QLocale::c().decimalPoint()) {
return content.replace(QLocale::c().decimalPoint(), locale.decimalPoint());
}
}
}
return content;
}
QString AssetParameterModel::getAssetId() const
{
return m_assetId;
}
QVector> AssetParameterModel::getAllParameters() const
{
QVector> res;
res.reserve((int)m_fixedParams.size() + (int)m_params.size());
for (const auto &fixed : m_fixedParams) {
res.push_back(QPair(fixed.first, fixed.second));
}
for (const auto ¶m : m_params) {
res.push_back(QPair(param.first, param.second.value));
}
return res;
}
QJsonDocument AssetParameterModel::toJson() const
{
QJsonArray list;
QLocale locale;
for (const auto &fixed : m_fixedParams) {
QJsonObject currentParam;
QModelIndex ix = index(m_rows.indexOf(fixed.first), 0);
currentParam.insert(QLatin1String("name"), QJsonValue(fixed.first));
currentParam.insert(QLatin1String("value"), fixed.second.toString());
int type = data(ix, AssetParameterModel::TypeRole).toInt();
double min = data(ix, AssetParameterModel::MinRole).toDouble();
double max = data(ix, AssetParameterModel::MaxRole).toDouble();
double factor = data(ix, AssetParameterModel::FactorRole).toDouble();
if (factor > 0) {
min /= factor;
max /= factor;
}
currentParam.insert(QLatin1String("type"), QJsonValue(type));
currentParam.insert(QLatin1String("min"), QJsonValue(min));
currentParam.insert(QLatin1String("max"), QJsonValue(max));
list.push_back(currentParam);
}
for (const auto ¶m : m_params) {
QJsonObject currentParam;
QModelIndex ix = index(m_rows.indexOf(param.first), 0);
currentParam.insert(QLatin1String("name"), QJsonValue(param.first));
currentParam.insert(QLatin1String("value"), QJsonValue(param.second.value.toString()));
int type = data(ix, AssetParameterModel::TypeRole).toInt();
double min = data(ix, AssetParameterModel::MinRole).toDouble();
double max = data(ix, AssetParameterModel::MaxRole).toDouble();
double factor = data(ix, AssetParameterModel::FactorRole).toDouble();
if (factor > 0) {
min /= factor;
max /= factor;
}
currentParam.insert(QLatin1String("type"), QJsonValue(type));
currentParam.insert(QLatin1String("min"), QJsonValue(min));
currentParam.insert(QLatin1String("max"), QJsonValue(max));
list.push_back(currentParam);
}
return QJsonDocument(list);
}
void AssetParameterModel::deletePreset(const QString &presetFile, const QString &presetName)
{
QJsonObject object;
QJsonArray array;
QFile loadFile(presetFile);
if (loadFile.exists()) {
if (loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isArray()) {
qDebug() << " * * ** JSON IS AN ARRAY, DELETING: " << presetName;
array = loadDoc.array();
QList toDelete;
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject() && val.toObject().keys().contains(presetName)) {
toDelete << i;
}
}
for (int i : toDelete) {
array.removeAt(i);
}
} else if (loadDoc.isObject()) {
QJsonObject obj = loadDoc.object();
qDebug() << " * * ** JSON IS AN OBJECT, DELETING: " << presetName;
if (obj.keys().contains(presetName)) {
obj.remove(presetName);
} else {
qDebug() << " * * ** JSON DOES NOT CONTAIN: " << obj.keys();
}
array.append(obj);
}
loadFile.close();
} else if (!loadFile.open(QIODevice::ReadWrite)) {
// TODO: error message
}
}
if (!loadFile.open(QIODevice::WriteOnly)) {
// TODO: error message
}
loadFile.write(QJsonDocument(array).toJson());
}
void AssetParameterModel::savePreset(const QString &presetFile, const QString &presetName)
{
QJsonObject object;
QJsonArray array;
QJsonDocument doc = toJson();
QFile loadFile(presetFile);
if (loadFile.exists()) {
if (loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isArray()) {
array = loadDoc.array();
QList toDelete;
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject() && val.toObject().keys().contains(presetName)) {
toDelete << i;
}
}
for (int i : toDelete) {
array.removeAt(i);
}
} else if (loadDoc.isObject()) {
QJsonObject obj = loadDoc.object();
if (obj.keys().contains(presetName)) {
obj.remove(presetName);
}
array.append(obj);
}
loadFile.close();
} else if (!loadFile.open(QIODevice::ReadWrite)) {
// TODO: error message
}
}
if (!loadFile.open(QIODevice::WriteOnly)) {
// TODO: error message
}
object[presetName] = doc.array();
array.append(object);
loadFile.write(QJsonDocument(array).toJson());
}
const QStringList AssetParameterModel::getPresetList(const QString &presetFile) const
{
QFile loadFile(presetFile);
if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isObject()) {
qDebug() << "// PRESET LIST IS AN OBJECT!!!";
return loadDoc.object().keys();
} else if (loadDoc.isArray()) {
qDebug() << "// PRESET LIST IS AN ARRAY!!!";
QStringList result;
QJsonArray array = loadDoc.array();
for (auto &&i : array) {
QJsonValue val = i;
if (val.isObject()) {
result << val.toObject().keys();
}
}
return result;
}
}
return QStringList();
}
const QVector> AssetParameterModel::loadPreset(const QString &presetFile, const QString &presetName)
{
QFile loadFile(presetFile);
QVector> params;
if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isObject() && loadDoc.object().contains(presetName)) {
qDebug() << "..........\n..........\nLOADING OBJECT JSON";
QJsonValue val = loadDoc.object().value(presetName);
if (val.isObject()) {
QVariantMap map = val.toObject().toVariantMap();
QMap::const_iterator i = map.constBegin();
while (i != map.constEnd()) {
params.append({i.key(), i.value()});
++i;
}
}
} else if (loadDoc.isArray()) {
QJsonArray array = loadDoc.array();
for (auto &&i : array) {
QJsonValue val = i;
if (val.isObject() && val.toObject().contains(presetName)) {
QJsonValue preset = val.toObject().value(presetName);
if (preset.isArray()) {
QJsonArray paramArray = preset.toArray();
for (auto &&j : paramArray) {
QJsonValue v1 = j;
if (v1.isObject()) {
QJsonObject ob = v1.toObject();
params.append({ob.value("name").toString(), ob.value("value").toVariant()});
}
}
}
qDebug() << "// LOADED PRESET: " << presetName << "\n" << params;
break;
}
}
}
}
return params;
}
void AssetParameterModel::setParameters(const QVector> ¶ms)
{
QLocale locale;
for (const auto ¶m : params) {
if (param.second.type() == QVariant::Double) {
setParameter(param.first, locale.toString(param.second.toDouble()), false);
} else {
setParameter(param.first, param.second.toString(), false);
}
}
if (m_keyframes) {
m_keyframes->refresh();
}
// emit modelChanged();
emit dataChanged(index(0), index(m_rows.count()), {});
}
ObjectId AssetParameterModel::getOwnerId() const
{
return m_ownerId;
}
void AssetParameterModel::addKeyframeParam(const QModelIndex &index)
{
if (m_keyframes) {
m_keyframes->addParameter(index);
} else {
m_keyframes.reset(new KeyframeModelList(shared_from_this(), index, pCore->undoStack()));
}
}
std::shared_ptr AssetParameterModel::getKeyframeModel()
{
return m_keyframes;
}
void AssetParameterModel::resetAsset(std::unique_ptr asset)
{
m_asset = std::move(asset);
}
bool AssetParameterModel::hasMoreThanOneKeyframe() const
{
if (m_keyframes) {
return (!m_keyframes->isEmpty() && !m_keyframes->singleKeyframe());
}
return false;
}
int AssetParameterModel::time_to_frames(const QString &time)
{
return m_asset->time_to_frames(time.toUtf8().constData());
}
void AssetParameterModel::passProperties(Mlt::Properties &target)
{
target.set("_profile", pCore->getCurrentProfile()->get_profile(), 0);
target.set_lcnumeric(m_asset->get_lcnumeric());
}
diff --git a/src/timeline2/view/qml/Clip.qml b/src/timeline2/view/qml/Clip.qml
index 351d941c1..6d5f90d49 100644
--- a/src/timeline2/view/qml/Clip.qml
+++ b/src/timeline2/view/qml/Clip.qml
@@ -1,876 +1,876 @@
/*
* Copyright (c) 2013-2016 Meltytech, LLC
* Author: Dan Dennedy
*
* 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 .
*/
import QtQuick 2.6
import QtQuick.Controls 2.2
import Kdenlive.Controls 1.0
import QtQml.Models 2.2
import QtQuick.Window 2.2
import 'Timeline.js' as Logic
import com.enums 1.0
Rectangle {
id: clipRoot
property real timeScale: 1.0
property string clipName: ''
property string clipResource: ''
property string mltService: ''
property string effectNames
property int modelStart
property real scrollX: 0
property int inPoint: 0
property int outPoint: 0
property int clipDuration: 0
property bool isAudio: false
property int audioChannels
property bool showKeyframes: false
property bool isGrabbed: false
property bool grouped: false
property var audioLevels
property var markers
property var keyframeModel
property int clipStatus: 0
property int itemType: 0
property int fadeIn: 0
property int fadeOut: 0
property int binId: 0
property var parentTrack
property int trackIndex //Index in track repeater
property int clipId //Id of the clip in the model
property int trackId: -1 // Id of the parent track in the model
property int fakeTid: -1
property int fakePosition: 0
property int originalTrackId: -1
property int originalX: x
property int originalDuration: clipDuration
property int lastValidDuration: clipDuration
property int draggedX: x
property bool selected: false
property bool isLocked: parentTrack && parentTrack.isLocked == true
property bool hasAudio
property bool canBeAudio
property bool canBeVideo
property string hash: 'ccc' //TODO
property double speed: 1.0
property color borderColor: 'black'
property bool forceReloadThumb
width : clipDuration * timeScale;
opacity: dragProxyArea.drag.active && dragProxy.draggedItem == clipId ? 0.8 : 1.0
signal trimmingIn(var clip, real newDuration, var mouse, bool shiftTrim)
signal trimmedIn(var clip, bool shiftTrim)
signal trimmingOut(var clip, real newDuration, var mouse, bool shiftTrim)
signal trimmedOut(var clip, bool shiftTrim)
onIsGrabbedChanged: {
if (clipRoot.isGrabbed) {
clipRoot.forceActiveFocus();
mouseArea.focus = true
}
}
onInPointChanged: {
if (parentTrack && parentTrack.isAudio && thumbsLoader.item) {
thumbsLoader.item.reload()
}
}
onClipResourceChanged: {
if (itemType == ProducerType.Color) {
color: Qt.darker(getColor(), 1.5)
}
}
ToolTip {
visible: mouseArea.containsMouse && !dragProxyArea.pressed
font.pixelSize: root.baseUnit
delay: 1000
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
text: clipRoot.clipName + ' (' + timeline.timecode(clipRoot.inPoint) + '-' + timeline.timecode(clipRoot.outPoint) + ')'
}
}
onKeyframeModelChanged: {
console.log('keyframe model changed............')
if (effectRow.keyframecanvas) {
effectRow.keyframecanvas.requestPaint()
}
}
onClipDurationChanged: {
width = clipDuration * timeScale;
}
onModelStartChanged: {
x = modelStart * timeScale;
}
onFakePositionChanged: {
x = fakePosition * timeScale;
}
onFakeTidChanged: {
if (clipRoot.fakeTid > -1 && parentTrack) {
if (clipRoot.parent != dragContainer) {
var pos = clipRoot.mapToGlobal(clipRoot.x, clipRoot.y);
clipRoot.parent = dragContainer
pos = clipRoot.mapFromGlobal(pos.x, pos.y)
clipRoot.x = pos.x
clipRoot.y = pos.y
}
clipRoot.y = Logic.getTrackById(clipRoot.fakeTid).y
}
}
onForceReloadThumbChanged: {
// TODO: find a way to force reload of clip thumbs
if (thumbsLoader.item) {
thumbsLoader.item.reload()
}
}
onTimeScaleChanged: {
x = modelStart * timeScale;
width = clipDuration * timeScale;
labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0
if (parentTrack && parentTrack.isAudio) {
thumbsLoader.item.reload();
}
}
onScrollXChanged: {
labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0
}
- border.color: selected? activePalette.highlight : grouped ? root.groupColor : borderColor
+ border.color: selected ? root.selectionColor : grouped ? root.groupColor : borderColor
border.width: isGrabbed ? 8 : 1.5
function updateDrag() {
var itemPos = mapToItem(tracksContainerArea, 0, 0, clipRoot.width, clipRoot.height)
initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false)
}
function getColor() {
if (clipStatus == ClipState.Disabled) {
return 'grey'
}
if (itemType == ProducerType.Color) {
var color = clipResource.substring(clipResource.length - 9)
if (color[0] == '#') {
return color
}
return '#' + color.substring(color.length - 8, color.length - 2)
}
return isAudio? root.audioColor : root.videoColor
}
/* function reparent(track) {
console.log('TrackId: ',trackId)
parent = track
height = track.height
parentTrack = track
trackId = parentTrack.trackId
console.log('Reparenting clip to Track: ', trackId)
//generateWaveform()
}
*/
property bool variableThumbs: (isAudio || itemType == ProducerType.Color || mltService === '')
property bool isImage: itemType == ProducerType.Image
property string baseThumbPath: variableThumbs ? '' : 'image://thumbnail/' + binId + '/' + (isImage ? '#0' : '#')
property string inThumbPath: (variableThumbs || isImage ) ? baseThumbPath : baseThumbPath + Math.floor(inPoint * speed)
property string outThumbPath: (variableThumbs || isImage ) ? baseThumbPath : baseThumbPath + Math.floor(outPoint * speed)
DropArea { //Drop area for clips
anchors.fill: clipRoot
keys: 'kdenlive/effect'
property string dropData
property string dropSource
property int dropRow: -1
onEntered: {
dropData = drag.getDataAsString('kdenlive/effect')
dropSource = drag.getDataAsString('kdenlive/effectsource')
}
onDropped: {
console.log("Add effect: ", dropData)
if (dropSource == '') {
// drop from effects list
controller.addClipEffect(clipRoot.clipId, dropData);
} else {
controller.copyClipEffect(clipRoot.clipId, dropSource);
}
dropSource = ''
dropRow = -1
drag.acceptProposedAction
}
}
onAudioLevelsChanged: {
if (parentTrack && parentTrack.isAudio && thumbsLoader.item) {
thumbsLoader.item.reload()
}
}
MouseArea {
id: mouseArea
visible: root.activeTool === 0
anchors.fill: clipRoot
acceptedButtons: Qt.RightButton
hoverEnabled: true
cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor
onPressed: {
root.stopScrolling = true
if (mouse.button == Qt.RightButton) {
if (timeline.selection.indexOf(clipRoot.clipId) == -1) {
controller.requestAddToSelection(clipRoot.clipId, true)
}
clipMenu.clipId = clipRoot.clipId
clipMenu.clipStatus = clipRoot.clipStatus
clipMenu.clipFrame = Math.round(mouse.x / timeline.scaleFactor)
clipMenu.grouped = clipRoot.grouped
clipMenu.trackId = clipRoot.trackId
clipMenu.canBeAudio = clipRoot.canBeAudio
clipMenu.canBeVideo = clipRoot.canBeVideo
clipMenu.popup()
}
}
Keys.onShortcutOverride: event.accepted = clipRoot.isGrabbed && (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down)
Keys.onLeftPressed: {
controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart - 1, true, true, true);
}
Keys.onRightPressed: {
controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart + 1, true, true, true);
}
Keys.onUpPressed: {
controller.requestClipMove(clipRoot.clipId, controller.getNextTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true);
}
Keys.onDownPressed: {
controller.requestClipMove(clipRoot.clipId, controller.getPreviousTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true);
}
onPositionChanged: {
var mapped = parentTrack.mapFromItem(clipRoot, mouse.x, mouse.y).x
root.mousePosChanged(Math.round(mapped / timeline.scaleFactor))
}
onEntered: {
var itemPos = mapToItem(tracksContainerArea, 0, 0, width, height)
initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false)
}
onExited: {
endDrag()
}
onWheel: zoomByWheel(wheel)
}
Item {
// Thumbs container
anchors.fill: parent
anchors.leftMargin: 0
anchors.rightMargin: 0
anchors.topMargin: parent.border.width
anchors.bottomMargin: parent.border.width
clip: true
Loader {
id: thumbsLoader
asynchronous: true
visible: status == Loader.Ready
anchors.fill: parent
source: parentTrack.isAudio ? (timeline.showAudioThumbnails ? "ClipAudioThumbs.qml" : "") : itemType == ProducerType.Color ? "" : timeline.showThumbnails ? "ClipThumbs.qml" : ""
onLoaded: {
item.reload()
}
}
}
Item {
// Clipping container
id: container
anchors.fill: parent
anchors.margins: 1.5
clip: true
Rectangle {
// text background
id: labelRect
color: clipRoot.selected ? 'darkred' : '#66000000'
width: label.width + 2
height: label.height
visible: clipRoot.width > width / 2
Text {
id: label
text: clipName + (clipRoot.speed != 1.0 ? ' [' + Math.round(clipRoot.speed*100) + '%]': '')
font.pixelSize: root.baseUnit * 1.2
anchors {
top: labelRect.top
left: labelRect.left
topMargin: 1
leftMargin: 1
}
color: 'white'
style: Text.Outline
styleColor: 'black'
}
}
Rectangle {
// effects
id: effectsRect
color: '#555555'
width: effectLabel.width + 2
height: effectLabel.height
x: labelRect.x
anchors.top: labelRect.bottom
visible: labelRect.visible && clipRoot.effectNames != ''
Text {
id: effectLabel
text: clipRoot.effectNames
font.pixelSize: root.baseUnit * 1.2
anchors {
top: effectsRect.top
left: effectsRect.left
topMargin: 1
leftMargin: 1
// + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + 1
}
color: 'white'
//style: Text.Outline
styleColor: 'black'
}
}
Repeater {
model: markers
delegate:
Item {
anchors.fill: parent
Rectangle {
id: markerBase
width: 1
height: parent.height
x: (model.frame - clipRoot.inPoint) * timeScale;
color: model.color
}
Rectangle {
visible: mlabel.visible
opacity: 0.7
x: markerBase.x
radius: 2
width: mlabel.width + 4
height: mlabel.height
anchors {
bottom: parent.verticalCenter
}
color: model.color
MouseArea {
z: 10
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onDoubleClicked: timeline.editMarker(clipRoot.binId, model.frame)
onClicked: timeline.position = (clipRoot.x + markerBase.x) / timeline.scaleFactor
}
}
Text {
id: mlabel
visible: timeline.showMarkers && parent.width > width * 1.5
text: model.comment
font.pixelSize: root.baseUnit
x: markerBase.x
anchors {
bottom: parent.verticalCenter
topMargin: 2
leftMargin: 2
}
color: 'white'
}
}
}
KeyframeView {
id: effectRow
visible: clipRoot.showKeyframes && clipRoot.keyframeModel
selected: clipRoot.selected
inPoint: clipRoot.inPoint
outPoint: clipRoot.outPoint
masterObject: clipRoot
kfrModel: clipRoot.keyframeModel
}
}
states: [
State {
name: 'locked'
when: isLocked
PropertyChanges {
target: clipRoot
color: root.lockedColor
opacity: 0.8
z: 0
}
},
State {
name: 'normal'
when: clipRoot.selected === false
PropertyChanges {
target: clipRoot
color: Qt.darker(getColor(), 1.5)
z: 0
}
},
State {
name: 'selected'
when: clipRoot.selected === true
PropertyChanges {
target: clipRoot
color: getColor()
z: 3
}
}
]
Rectangle {
id: compositionIn
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.bottomMargin: 2
anchors.leftMargin: 4
width: root.baseUnit * 1.2
height: width
radius: 2
color: Qt.darker('mediumpurple')
border.width: 2
border.color: 'green'
opacity: 0
enabled: !clipRoot.isAudio && !dragProxy.isComposition
visible: clipRoot.width > 4 * width
MouseArea {
id: compInArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: parent.opacity = 0.7
onExited: {
if (!pressed) {
parent.opacity = 0
}
}
onPressed: {
timeline.addCompositionToClip('', clipRoot.clipId, 0)
}
onReleased: {
parent.opacity = 0
}
ToolTip {
visible: compInArea.containsMouse && !dragProxyArea.pressed
font.pixelSize: root.baseUnit
delay: 1000
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
text: 'Click to add composition'
}
}
}
}
Rectangle {
id: compositionOut
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: 2
anchors.rightMargin: 4
width: root.baseUnit * 1.2
height: width
radius: 2
color: Qt.darker('mediumpurple')
border.width: 2
border.color: 'green'
opacity: 0
enabled: !clipRoot.isAudio
visible: clipRoot.width > 4 * width
MouseArea {
id: compOutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
parent.opacity = 0.7
}
onExited: {
if (!pressed) {
parent.opacity = 0
}
}
onPressed: {
timeline.addCompositionToClip('', clipRoot.clipId, clipRoot.clipDuration - 1)
}
onReleased: {
parent.opacity = 0
}
ToolTip {
visible: compOutArea.containsMouse && !dragProxyArea.pressed
font.pixelSize: root.baseUnit
delay: 1000
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
text: 'Click to add composition'
}
}
}
}
TimelineTriangle {
id: fadeInTriangle
fillColor: 'green'
width: Math.min(clipRoot.fadeIn * timeScale, clipRoot.width)
height: clipRoot.height - clipRoot.border.width * 2
anchors.left: clipRoot.left
anchors.top: clipRoot.top
anchors.margins: clipRoot.border.width
opacity: 0.3
}
Rectangle {
id: fadeInControl
anchors.left: fadeInTriangle.width > radius? undefined : fadeInTriangle.left
anchors.horizontalCenter: fadeInTriangle.width > radius? fadeInTriangle.right : undefined
anchors.top: fadeInTriangle.top
anchors.topMargin: -10
width: root.baseUnit * 2
height: width
radius: width / 2
color: '#FF66FFFF'
border.width: 2
border.color: 'green'
enabled: !isLocked && !dragProxy.isComposition
opacity: 0
visible : clipRoot.width > 3 * width
Drag.active: fadeInMouseArea.drag.active
MouseArea {
id: fadeInMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
drag.target: parent
drag.minimumX: -root.baseUnit
drag.maximumX: container.width
drag.axis: Drag.XAxis
drag.smoothed: false
property int startX
property int startFadeIn
onEntered: parent.opacity = 0.7
onExited: {
if (!pressed) {
parent.opacity = 0
}
}
onPressed: {
root.stopScrolling = true
startX = Math.round(parent.x / timeScale)
startFadeIn = clipRoot.fadeIn
parent.anchors.left = undefined
parent.anchors.horizontalCenter = undefined
parent.opacity = 1
fadeInTriangle.opacity = 0.5
// parentTrack.clipSelected(clipRoot, parentTrack) TODO
}
onReleased: {
root.stopScrolling = false
fadeInTriangle.opacity = 0.3
parent.opacity = 0
if (fadeInTriangle.width > parent.radius)
parent.anchors.horizontalCenter = fadeInTriangle.right
else
parent.anchors.left = fadeInTriangle.left
console.log('released fade: ', clipRoot.fadeIn)
timeline.adjustFade(clipRoot.clipId, 'fadein', clipRoot.fadeIn, startFadeIn)
bubbleHelp.hide()
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var delta = Math.round(parent.x / timeScale) - startX
var duration = Math.max(0, startFadeIn + delta)
duration = Math.min(duration, clipRoot.clipDuration)
if (duration != clipRoot.fadeIn) {
timeline.adjustFade(clipRoot.clipId, 'fadein', duration, -1)
// Show fade duration as time in a "bubble" help.
var s = timeline.timecode(Math.max(duration, 0))
bubbleHelp.show(clipRoot.x, parentTrack.y + clipRoot.height, s)
}
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: fadeInMouseArea.containsMouse && !fadeInMouseArea.pressed
NumberAnimation {
from: 1.0
to: 0.7
duration: 250
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 0.7
to: 1.0
duration: 250
easing.type: Easing.InOutQuad
}
}
}
TimelineTriangle {
id: fadeOutCanvas
fillColor: 'red'
width: Math.min(clipRoot.fadeOut * timeScale, clipRoot.width)
height: clipRoot.height - clipRoot.border.width * 2
anchors.right: clipRoot.right
anchors.top: clipRoot.top
anchors.margins: clipRoot.border.width
opacity: 0.3
transform: Scale { xScale: -1; origin.x: fadeOutCanvas.width / 2}
}
Rectangle {
id: fadeOutControl
anchors.right: fadeOutCanvas.width > radius? undefined : fadeOutCanvas.right
anchors.horizontalCenter: fadeOutCanvas.width > radius? fadeOutCanvas.left : undefined
anchors.top: fadeOutCanvas.top
anchors.topMargin: -10
width: root.baseUnit * 2
height: width
radius: width / 2
color: '#66FFFFFF'
border.width: 2
border.color: 'red'
opacity: 0
enabled: !isLocked && !dragProxy.isComposition
Drag.active: fadeOutMouseArea.drag.active
visible : clipRoot.width > 3 * width
MouseArea {
id: fadeOutMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
drag.target: parent
drag.axis: Drag.XAxis
drag.minimumX: -root.baseUnit
drag.maximumX: container.width
property int startX
property int startFadeOut
onEntered: parent.opacity = 0.7
onExited: {
if (!pressed) {
parent.opacity = 0
}
}
drag.smoothed: false
onPressed: {
root.stopScrolling = true
startX = Math.round(parent.x / timeScale)
startFadeOut = clipRoot.fadeOut
parent.anchors.right = undefined
parent.anchors.horizontalCenter = undefined
parent.opacity = 1
fadeOutCanvas.opacity = 0.5
}
onReleased: {
fadeOutCanvas.opacity = 0.3
parent.opacity = 0
root.stopScrolling = false
if (fadeOutCanvas.width > parent.radius)
parent.anchors.horizontalCenter = fadeOutCanvas.left
else
parent.anchors.right = fadeOutCanvas.right
timeline.adjustFade(clipRoot.clipId, 'fadeout', clipRoot.fadeOut, startFadeOut)
bubbleHelp.hide()
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var delta = startX - Math.round(parent.x / timeScale)
var duration = Math.max(0, startFadeOut + delta)
duration = Math.min(duration, clipRoot.clipDuration)
if (clipRoot.fadeOut != duration) {
timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, -1)
// Show fade duration as time in a "bubble" help.
var s = timeline.timecode(Math.max(duration, 0))
bubbleHelp.show(clipRoot.x + clipRoot.width, parentTrack.y + clipRoot.height, s)
}
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: fadeOutMouseArea.containsMouse && !fadeOutMouseArea.pressed
NumberAnimation {
from: 1.0
to: 0.7
duration: 250
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 0.7
to: 1.0
duration: 250
easing.type: Easing.InOutQuad
}
}
}
Rectangle {
id: trimIn
anchors.left: clipRoot.left
anchors.leftMargin: 0
height: parent.height
enabled: !isLocked
width: 5
color: isAudio? 'green' : 'lawngreen'
opacity: 0
Drag.active: trimInMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
visible: root.activeTool === 0 && !mouseArea.drag.active
MouseArea {
id: trimInMouseArea
anchors.fill: parent
hoverEnabled: true
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
property bool shiftTrim: false
property bool sizeChanged: false
cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor);
onPressed: {
root.stopScrolling = true
clipRoot.originalX = clipRoot.x
clipRoot.originalDuration = clipDuration
parent.anchors.left = undefined
shiftTrim = mouse.modifiers & Qt.ShiftModifier
parent.opacity = 0
}
onReleased: {
root.stopScrolling = false
parent.anchors.left = clipRoot.left
if (sizeChanged) {
clipRoot.trimmedIn(clipRoot, shiftTrim)
sizeChanged = false
}
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var delta = Math.round((trimIn.x) / timeScale)
if (delta !== 0) {
if (delta < -modelStart) {
delta = -modelStart
}
var newDuration = clipDuration - delta
sizeChanged = true
clipRoot.trimmingIn(clipRoot, newDuration, mouse, shiftTrim)
}
}
}
onEntered: {
if (!pressed) {
parent.opacity = 0.5
}
}
onExited: {
parent.opacity = 0
}
}
}
Rectangle {
id: trimOut
anchors.right: clipRoot.right
anchors.rightMargin: 0
height: parent.height
width: 5
color: 'red'
opacity: 0
enabled: !isLocked
Drag.active: trimOutMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
visible: root.activeTool === 0 && !mouseArea.drag.active
MouseArea {
id: trimOutMouseArea
anchors.fill: parent
hoverEnabled: true
property bool shiftTrim: false
property bool sizeChanged: false
cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor);
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
onPressed: {
root.stopScrolling = true
clipRoot.originalDuration = clipDuration
parent.anchors.right = undefined
shiftTrim = mouse.modifiers & Qt.ShiftModifier
parent.opacity = 0
}
onReleased: {
root.stopScrolling = false
parent.anchors.right = clipRoot.right
if (sizeChanged) {
clipRoot.trimmedOut(clipRoot, shiftTrim)
sizeChanged = false
}
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var newDuration = Math.round((parent.x + parent.width) / timeScale)
if (newDuration != clipDuration) {
sizeChanged = true
clipRoot.trimmingOut(clipRoot, newDuration, mouse, shiftTrim)
}
}
}
onEntered: {
if (!pressed) {
parent.opacity = 0.5
}
}
onExited: parent.opacity = 0
}
}
/*MenuItem {
id: mergeItem
text: i18n('Merge with next clip')
onTriggered: timeline.mergeClipWithNext(trackIndex, index, false)
}
MenuItem {
text: i18n('Rebuild Audio Waveform')
onTriggered: timeline.remakeAudioLevels(trackIndex, index)
}*/
/*onPopupVisibleChanged: {
if (visible && application.OS !== 'OS X' && __popupGeometry.height > 0) {
// Try to fix menu running off screen. This only works intermittently.
menu.__yOffset = Math.min(0, Screen.height - (__popupGeometry.y + __popupGeometry.height + 40))
menu.__xOffset = Math.min(0, Screen.width - (__popupGeometry.x + __popupGeometry.width))
}
}*/
}
diff --git a/src/timeline2/view/qml/timeline.qml b/src/timeline2/view/qml/timeline.qml
index 944d829db..57eb8b837 100644
--- a/src/timeline2/view/qml/timeline.qml
+++ b/src/timeline2/view/qml/timeline.qml
@@ -1,1392 +1,1393 @@
import QtQuick 2.6
import QtQml.Models 2.2
import QtQuick.Controls 1.4 as OLD
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Dialogs 1.2
import Kdenlive.Controls 1.0
import QtQuick.Window 2.2
import 'Timeline.js' as Logic
Rectangle {
id: root
objectName: "timelineview"
SystemPalette { id: activePalette }
color: activePalette.window
property bool validMenu: false
property color textColor: activePalette.text
signal clipClicked()
signal mousePosChanged(int position)
signal zoomIn(bool onMouse)
signal zoomOut(bool onMouse)
FontMetrics {
id: fontMetrics
font.family: "Arial"
}
ClipMenu {
id: clipMenu
}
CompositionMenu {
id: compositionMenu
}
function fitZoom() {
return scrollView.width / (timeline.duration * 1.1)
}
function scrollPos() {
return scrollView.flickableItem.contentX
}
function goToStart(pos) {
scrollView.flickableItem.contentX = pos
}
function updatePalette() {
root.color = activePalette.window
root.textColor = activePalette.text
playhead.fillColor = activePalette.windowText
ruler.repaintRuler()
}
function moveSelectedTrack(offset) {
var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack)
var newTrack = cTrack + offset
var max = tracksRepeater.count;
if (newTrack < 0) {
newTrack = max - 1;
} else if (newTrack >= max) {
newTrack = 0;
}
console.log('Setting curr tk: ', newTrack, 'MAX: ',max)
timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId
}
function zoomByWheel(wheel) {
if (wheel.modifiers & Qt.AltModifier) {
if (wheel.angleDelta.x > 0) {
timeline.triggerAction('monitor_seek_snap_backward')
} else {
timeline.triggerAction('monitor_seek_snap_forward')
}
} else if (wheel.modifiers & Qt.ControlModifier) {
if (wheel.angleDelta.y > 0) {
root.zoomIn(true);
} else {
root.zoomOut(true);
}
} else {
var newScroll = Math.min(scrollView.flickableItem.contentX - wheel.angleDelta.y, timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width))
scrollView.flickableItem.contentX = Math.max(newScroll, 0)
}
wheel.accepted = true
}
function continuousScrolling(x) {
// This provides continuous scrolling at the left/right edges.
if (x > scrollView.flickableItem.contentX + scrollView.width - 50) {
scrollTimer.item = clip
scrollTimer.backwards = false
scrollTimer.start()
} else if (x < 50) {
scrollView.flickableItem.contentX = 0;
scrollTimer.stop()
} else if (x < scrollView.flickableItem.contentX + 50) {
scrollTimer.item = clip
scrollTimer.backwards = true
scrollTimer.start()
} else {
scrollTimer.stop()
}
}
function getTrackYFromId(a_track) {
return Logic.getTrackYFromId(a_track)
}
function getTrackYFromMltIndex(a_track) {
return Logic.getTrackYFromMltIndex(a_track)
}
function getTracksCount() {
return Logic.getTracksList()
}
function getMousePos() {
return (scrollView.flickableItem.contentX + tracksArea.mouseX) / timeline.scaleFactor
}
function getScrollPos() {
return scrollView.flickableItem.contentX
}
function setScrollPos(pos) {
return scrollView.flickableItem.contentX = pos
}
function getCopiedItemId() {
return copiedClip
}
function getMouseTrack() {
return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.flickableItem.contentY)
}
function getTrackColor(audio, header) {
var col = activePalette.alternateBase
if (audio) {
col = Qt.tint(col, "#06FF00CC")
}
if (header) {
col = Qt.darker(col, 1.05)
}
return col
}
function clearDropData() {
clipBeingDroppedId = -1
droppedPosition = -1
droppedTrack = -1
scrollTimer.running = false
scrollTimer.stop()
}
function initDrag(itemObject, itemCoord, itemId, itemPos, itemTrack, isComposition) {
dragProxy.x = itemObject.modelStart * timeScale
dragProxy.y = itemCoord.y
dragProxy.width = itemObject.clipDuration * timeScale
dragProxy.height = itemCoord.height
dragProxy.masterObject = itemObject
dragProxy.draggedItem = itemId
dragProxy.sourceTrack = itemTrack
dragProxy.sourceFrame = itemPos
dragProxy.isComposition = isComposition
dragProxy.verticalOffset = isComposition ? itemObject.displayHeight : 0
}
function endDrag() {
dragProxy.draggedItem = -1
dragProxy.x = 0
dragProxy.y = 0
dragProxy.width = 0
dragProxy.height = 0
dragProxy.verticalOffset = 0
}
property int headerWidth: timeline.headerWidth()
property int activeTool: 0
property real baseUnit: fontMetrics.font.pointSize
property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.2)
property color frameColor: Qt.rgba(activePalette.shadow.r, activePalette.shadow.g, activePalette.shadow.b, 0.3)
property bool stopScrolling: false
property int duration: timeline.duration
property color audioColor: timeline.audioColor
property color videoColor: timeline.videoColor
property color lockedColor: timeline.lockedColor
+ property color selectionColor: timeline.selectionColor
property color groupColor: timeline.groupColor
property int clipBeingDroppedId: -1
property string clipBeingDroppedData
property int droppedPosition: -1
property int droppedTrack: -1
property int clipBeingMovedId: -1
property int spacerGroup: -1
property int spacerFrame: -1
property int spacerClickFrame: -1
property real timeScale: timeline.scaleFactor
property real snapping: timeline.snap ? 10 / Math.sqrt(timeScale) - 0.5 : -1
property var timelineSelection: timeline.selection
property int trackHeight
property int copiedClip: -1
property int zoomOnMouse: -1
property int viewActiveTrack: timeline.activeTrack
//onCurrentTrackChanged: timeline.selection = []
onTimeScaleChanged: {
if (root.zoomOnMouse >= 0) {
scrollView.flickableItem.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX)
root.zoomOnMouse = -1
} else {
scrollView.flickableItem.contentX = Math.max(0, (timeline.seekPosition > -1 ? timeline.seekPosition : timeline.position) * timeline.scaleFactor - (scrollView.width / 2))
}
//root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
ruler.adjustStepSize()
if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
// update dragged item pos
dragProxy.masterObject.updateDrag()
}
}
onViewActiveTrackChanged: {
var tk = Logic.getTrackById(timeline.activeTrack)
if (tk.y < scrollView.flickableItem.contentY) {
scrollView.flickableItem.contentY = Math.max(0, tk.y - scrollView.height / 3)
} else if (tk.y + tk.height > scrollView.flickableItem.contentY + scrollView.viewport.height) {
scrollView.flickableItem.contentY = Math.min(trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height, tk.y - scrollView.height / 3)
}
}
DropArea { //Drop area for compositions
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
x: headerWidth
keys: 'kdenlive/composition'
onEntered: {
console.log("Trying to drop composition")
if (clipBeingMovedId == -1) {
console.log("No clip being moved")
var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY)
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
droppedPosition = frame
if (track >= 0 && !controller.isAudioTrack(track)) {
clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
console.log("Trying to insert",track, frame, clipBeingDroppedData)
clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
console.log("id",clipBeingDroppedId)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
drag.acceptProposedAction()
} else {
drag.accepted = false
}
}
}
onPositionChanged: {
if (clipBeingMovedId == -1) {
var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY)
if (track !=-1) {
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
if (clipBeingDroppedId >= 0){
if (controller.isAudioTrack(track)) {
// Don't allow moving composition to an audio track
track = controller.getCompositionTrackId(clipBeingDroppedId)
}
controller.requestCompositionMove(clipBeingDroppedId, track, frame, true, false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else if (!controller.isAudioTrack(track)) {
clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
}
}
}
}
onExited:{
if (clipBeingDroppedId != -1) {
controller.requestItemDeletion(clipBeingDroppedId, false)
}
clearDropData()
}
onDropped: {
if (clipBeingDroppedId != -1) {
var frame = controller.getCompositionPosition(clipBeingDroppedId)
var track = controller.getCompositionTrackId(clipBeingDroppedId)
// we simulate insertion at the final position so that stored undo has correct value
controller.requestItemDeletion(clipBeingDroppedId, false)
timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
}
clearDropData()
}
}
DropArea { //Drop area for bin/clips
/** @brief local helper function to handle the insertion of multiple dragged items */
function insertAndMaybeGroup(track, frame, droppedData) {
var binIds = droppedData.split(";")
if (binIds.length == 0) {
return -1
}
var id = -1
if (binIds.length == 1) {
id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
} else {
var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
// if the clip insertion succeeded, request the clips to be grouped
if (ids.length > 0) {
timeline.selectItems(ids)
id = ids[0]
}
}
return id
}
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
x: headerWidth
keys: 'kdenlive/producerslist'
onEntered: {
if (clipBeingMovedId == -1) {
//var track = Logic.getTrackIdFromPos(drag.y)
var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
if (track >= 0 && track < tracksRepeater.count) {
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
droppedPosition = frame
timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
//drag.acceptProposedAction()
clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
console.log('dropped data: ', clipBeingDroppedData)
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else {
drag.accepted = false
}
}
}
onExited:{
if (clipBeingDroppedId != -1) {
controller.requestItemDeletion(clipBeingDroppedId, false)
}
clearDropData()
}
onPositionChanged: {
if (clipBeingMovedId == -1) {
var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
if (track >= 0 && track < tracksRepeater.count) {
timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
if (clipBeingDroppedId >= 0){
controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else {
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
}
}
}
}
onDropped: {
if (clipBeingDroppedId != -1) {
var frame = controller.getClipPosition(clipBeingDroppedId)
var track = controller.getClipTrackId(clipBeingDroppedId)
/* We simulate insertion at the final position so that stored undo has correct value
* NOTE: even if dropping multiple clips, requesting the deletion of the first one is
* enough as internally it will request the group deletion
*/
controller.requestItemDeletion(clipBeingDroppedId, false)
var binIds = clipBeingDroppedData.split(";")
if (binIds.length == 1) {
timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false)
} else {
timeline.insertClips(track, frame, binIds, true, true)
}
}
clearDropData()
}
}
OLD.Menu {
id: menu
property int clickedX
property int clickedY
onAboutToHide: {
timeline.ungrabHack()
}
OLD.MenuItem {
text: i18n('Paste')
iconName: 'edit-paste'
visible: copiedClip != -1
onTriggered: {
timeline.pasteItem()
}
}
OLD.MenuItem {
text: i18n('Insert Space')
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.insertSpace(track, frame);
}
}
OLD.MenuItem {
text: i18n('Remove Space On Active Track')
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.removeSpace(track, frame);
}
}
OLD.MenuItem {
text: i18n('Remove Space')
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.removeSpace(track, frame, true);
}
}
OLD.MenuItem {
id: addGuideMenu
text: i18n('Add Guide')
onTriggered: {
timeline.switchGuide(timeline.position);
}
}
OLD.MenuItem {
id: editGuideMenu
text: i18n('Edit Guide')
visible: false
onTriggered: {
timeline.editGuide(timeline.position);
}
}
AssetMenu {
title: i18n('Insert a composition...')
menuModel: transitionModel
isTransition: true
onAssetSelected: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.round((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
var id = timeline.insertComposition(track, frame, assetId, true)
if (id == -1) {
compositionFail.open()
}
}
}
onAboutToShow: {
if (guidesModel.hasMarker(timeline.position)) {
// marker at timeline position
addGuideMenu.text = i18n('Remove Guide')
editGuideMenu.visible = true
} else {
addGuideMenu.text = i18n('Add Guide')
editGuideMenu.visible = false
}
console.log("pop menu")
}
}
OLD.Menu {
id: rulermenu
property int clickedX
property int clickedY
OLD.MenuItem {
id: addGuideMenu2
text: i18n('Add Guide')
onTriggered: {
timeline.switchGuide(timeline.position);
}
}
OLD.MenuItem {
id: editGuideMenu2
text: i18n('Edit Guide')
visible: false
onTriggered: {
timeline.editGuide(timeline.position);
}
}
OLD.MenuItem {
id: addProjectNote
text: i18n('Add Project Note')
onTriggered: {
timeline.triggerAction('add_project_note')
}
}
onAboutToShow: {
if (guidesModel.hasMarker(timeline.position)) {
// marker at timeline position
addGuideMenu2.text = i18n('Remove Guide')
editGuideMenu2.visible = true
} else {
addGuideMenu2.text = i18n('Add Guide')
editGuideMenu2.visible = false
}
console.log("pop menu")
}
}
MessageDialog {
id: compositionFail
title: i18n("Timeline error")
icon: StandardIcon.Warning
text: i18n("Impossible to add a composition at that position. There might not be enough space")
standardButtons: StandardButton.Ok
}
OLD.Menu {
id: headerMenu
property int trackId: -1
property int thumbsFormat: 0
property bool audioTrack: false
property bool recEnabled: false
onAboutToHide: {
timeline.ungrabHack()
}
OLD.MenuItem {
text: i18n('Add Track')
onTriggered: {
timeline.addTrack(timeline.activeTrack)
}
}
OLD.MenuItem {
text: i18n('Delete Track')
onTriggered: {
timeline.deleteTrack(timeline.activeTrack)
}
}
OLD.MenuItem {
visible: headerMenu.audioTrack
id: showRec
text: "Show Record Controls"
onTriggered: {
controller.setTrackProperty(headerMenu.trackId, "kdenlive:audio_rec", showRec.checked ? '1' : '0')
}
checkable: true
checked: headerMenu.recEnabled
}
OLD.MenuItem {
visible: headerMenu.audioTrack
id: configRec
text: "Configure Recording"
onTriggered: {
timeline.showConfig(4,2)
}
}
OLD.Menu {
title: i18n('Track thumbnails')
visible: !headerMenu.audioTrack
OLD.ExclusiveGroup { id: thumbStyle }
OLD.MenuItem {
text: "In frame"
id: inFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 2)
checkable: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: "In / out frames"
id: inOutFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 0)
checkable: true
checked: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: "All frames"
id: allFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 1)
checkable: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: "No thumbnails"
id: noFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 3)
checkable: true
exclusiveGroup: thumbStyle
}
onAboutToShow: {
switch(headerMenu.thumbsFormat) {
case 3:
noFrame.checked = true
break
case 2:
inFrame.checked = true
break
case 1:
allFrame.checked = true
break
default:
inOutFrame.checked = true
break
}
}
}
}
Row {
Column {
id: headerContainer
z: 1
Rectangle {
id: cornerstone
property bool selected: false
// Padding between toolbar and track headers.
width: headerWidth
height: ruler.height
color: 'transparent' //selected? shotcutBlue : activePalette.window
border.color: selected? 'red' : 'transparent'
border.width: selected? 1 : 0
z: 1
}
Flickable {
// Non-slider scroll area for the track headers.
id: headerFlick
contentY: scrollView.flickableItem.contentY
width: headerWidth
height: 100
interactive: false
MouseArea {
width: trackHeaders.width
height: trackHeaders.height
acceptedButtons: Qt.NoButton
onWheel: {
var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height)
scrollView.flickableItem.contentY = Math.max(newScroll, 0)
}
}
Column {
id: trackHeaders
spacing: 0
Repeater {
id: trackHeaderRepeater
model: multitrack
TrackHead {
trackName: model.name
thumbsFormat: model.thumbsFormat
trackTag: model.trackTag
isDisabled: model.disabled
isComposite: model.composite
isLocked: model.locked
isActive: model.trackActive
isAudio: model.audio
showAudioRecord: model.audioRecord
effectNames: model.effectNames
isStackEnabled: model.isStackEnabled
width: headerWidth
height: model.trackHeight
current: item === timeline.activeTrack
trackId: item
onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked
collapsed: height <= collapsedHeight
onMyTrackHeightChanged: {
trackBaseRepeater.itemAt(index).height = myTrackHeight
tracksRepeater.itemAt(index).height = myTrackHeight
height = myTrackHeight
collapsed = height <= collapsedHeight
if (!collapsed) {
controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight)
controller.setTrackProperty(trackId, "kdenlive:collapsed", "0")
} else {
controller.setTrackProperty(trackId, "kdenlive:collapsed", collapsedHeight)
}
// hack: change property to trigger transition adjustment
root.trackHeight = root.trackHeight === 1 ? 0 : 1
}
onClicked: {
timeline.activeTrack = tracksRepeater.itemAt(index).trackInternalId
console.log('track name: ',index, ' = ', model.name,'/',tracksRepeater.itemAt(index).trackInternalId)
//timeline.selectTrackHead(currentTrack)
}
}
}
}
Column {
id: trackHeadersResizer
spacing: 0
width: 5
Rectangle {
id: resizer
height: trackHeaders.height
width: 3
x: root.headerWidth - 2
color: 'red'
opacity: 0
Drag.active: headerMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
MouseArea {
id: headerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeHorCursor
drag.target: parent
drag.axis: Drag.XAxis
drag.minimumX: 2 * baseUnit
property double startX
property double originalX
drag.smoothed: false
onPressed: {
root.stopScrolling = true
}
onReleased: {
root.stopScrolling = false
parent.opacity = 0
}
onEntered: parent.opacity = 0.5
onExited: parent.opacity = 0
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
parent.opacity = 0.5
headerWidth = Math.max(10, mapToItem(null, x, y).x + 2)
timeline.setHeaderWidth(headerWidth)
}
}
}
}
}
}
}
MouseArea {
id: tracksArea
property real clickX
property real clickY
width: root.width - headerWidth
height: root.height
Keys.onDownPressed: {
root.moveSelectedTrack(1)
}
Keys.onUpPressed: {
root.moveSelectedTrack(-1)
}
// This provides continuous scrubbing and scimming at the left/right edges.
hoverEnabled: true
acceptedButtons: Qt.RightButton | Qt.LeftButton | Qt.MidButton
cursorShape: tracksArea.mouseY < ruler.height || root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor
onWheel: {
if (wheel.modifiers & Qt.AltModifier) {
// Alt + wheel = seek to next snap point
if (wheel.angleDelta.x > 0) {
timeline.triggerAction('monitor_seek_snap_backward')
} else {
timeline.triggerAction('monitor_seek_snap_forward')
}
} else {
var delta = wheel.modifiers & Qt.ShiftModifier ? timeline.fps() : 1
if (timeline.seekPosition > -1) {
timeline.seekPosition = Math.min(timeline.seekPosition - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1)
} else {
timeline.seekPosition = Math.min(timeline.position - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1)
}
timeline.position = timeline.seekPosition
}
}
onPressed: {
focus = true
if (mouse.buttons === Qt.MidButton || (root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier)) {
clickX = mouseX
clickY = mouseY
return
}
if (root.activeTool === 0 && mouse.modifiers & Qt.ShiftModifier && mouse.y > ruler.height) {
// rubber selection
rubberSelect.x = mouse.x + tracksArea.x
rubberSelect.y = mouse.y
rubberSelect.originX = mouse.x
rubberSelect.originY = rubberSelect.y
rubberSelect.width = 0
rubberSelect.height = 0
} else if (mouse.button & Qt.LeftButton) {
if (dragProxy.draggedItem > -1) {
mouse.accepted = false
return
}
if (root.activeTool === 2 && mouse.y > ruler.height) {
// spacer tool
var y = mouse.y - ruler.height + scrollView.flickableItem.contentY
var frame = (scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor
var track = (mouse.modifiers & Qt.ControlModifier) ? tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId : -1
spacerGroup = timeline.requestSpacerStartOperation(track, frame)
if (spacerGroup > -1) {
drag.axis = Drag.XAxis
Drag.active = true
Drag.proposedAction = Qt.MoveAction
spacerClickFrame = frame
spacerFrame = controller.getItemPosition(spacerGroup)
}
} else if (root.activeTool === 0 || mouse.y <= ruler.height) {
if (mouse.y > ruler.height) {
controller.requestClearSelection();
}
timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
timeline.position = timeline.seekPosition
} else if (root.activeTool === 1) {
// razor tool
var y = mouse.y - ruler.height + scrollView.flickableItem.contentY
timeline.cutClipUnderCursor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId)
}
} else if (mouse.button & Qt.RightButton) {
menu.clickedX = mouse.x
menu.clickedY = mouse.y
if (mouse.y > ruler.height) {
timeline.activeTrack = tracksRepeater.itemAt(Logic.getTrackIndexFromPos(mouse.y - ruler.height + scrollView.flickableItem.contentY)).trackInternalId
menu.popup()
} else {
// ruler menu
rulermenu.popup()
}
}
}
property bool scim: false
onExited: {
scim = false
}
onPositionChanged: {
if (pressed && ((mouse.buttons === Qt.MidButton) || (mouse.buttons === Qt.LeftButton && root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier))) {
var newScroll = Math.min(scrollView.flickableItem.contentX - (mouseX - clickX), timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width))
var vertScroll = Math.min(scrollView.flickableItem.contentY - (mouseY - clickY), trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height)
scrollView.flickableItem.contentX = Math.max(newScroll, 0)
scrollView.flickableItem.contentY = Math.max(vertScroll, 0)
clickX = mouseX
clickY = mouseY
return
}
if (dragProxy.draggedItem > -1) {
mouse.accepted = false
return
}
var mousePos = Math.max(0, Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor))
root.mousePosChanged(mousePos)
ruler.showZoneLabels = mouse.y < ruler.height
if (mouse.modifiers & Qt.ShiftModifier && mouse.buttons === Qt.LeftButton && root.activeTool === 0 && !rubberSelect.visible && rubberSelect.y > 0) {
// rubber selection
rubberSelect.visible = true
}
if (rubberSelect.visible) {
var newX = mouse.x
var newY = mouse.y
if (newX < rubberSelect.originX) {
rubberSelect.x = newX + tracksArea.x
rubberSelect.width = rubberSelect.originX - newX
} else {
rubberSelect.x = rubberSelect.originX + tracksArea.x
rubberSelect.width = newX - rubberSelect.originX
}
if (newY < rubberSelect.originY) {
rubberSelect.y = newY
rubberSelect.height = rubberSelect.originY - newY
} else {
rubberSelect.y = rubberSelect.originY
rubberSelect.height= newY - rubberSelect.originY
}
} else if (mouse.buttons === Qt.LeftButton) {
if (root.activeTool === 0 || mouse.y < ruler.height) {
timeline.seekPosition = Math.max(0, Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1))
timeline.position = timeline.seekPosition
} else if (root.activeTool === 2 && spacerGroup > -1) {
// Move group
var track = controller.getItemTrackId(spacerGroup)
var frame = Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) + spacerFrame - spacerClickFrame
frame = controller.suggestItemMove(spacerGroup, track, frame, timeline.position, Math.floor(root.snapping))
continuousScrolling(mouse.x + scrollView.flickableItem.contentX)
}
scim = true
} else {
scim = false
if (root.activeTool === 1) {
cutLine.x = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor) * timeline.scaleFactor - scrollView.flickableItem.contentX
if (mouse.modifiers & Qt.ShiftModifier) {
timeline.position = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor)
}
}
}
}
onReleased: {
if (rubberSelect.visible) {
rubberSelect.visible = false
var y = rubberSelect.y - ruler.height + scrollView.flickableItem.contentY
var topTrack = Logic.getTrackIndexFromPos(Math.max(0, y))
var bottomTrack = Logic.getTrackIndexFromPos(y + rubberSelect.height)
if (bottomTrack >= topTrack) {
var t = []
for (var i = topTrack; i <= bottomTrack; i++) {
t.push(tracksRepeater.itemAt(i).trackInternalId)
}
var startFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x) / timeline.scaleFactor
var endFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x + rubberSelect.width) / timeline.scaleFactor
timeline.selectItems(t, startFrame, endFrame, mouse.modifiers & Qt.ControlModifier);
}
rubberSelect.y = -1
} else if (mouse.modifiers & Qt.ShiftModifier) {
// Shift click, process seek
timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
timeline.position = timeline.seekPosition
}
if (spacerGroup > -1) {
var frame = controller.getItemPosition(spacerGroup)
timeline.requestSpacerEndOperation(spacerGroup, spacerFrame, frame);
spacerClickFrame = -1
spacerFrame = -1
spacerGroup = -1
}
scim = false
}
Timer {
id: scrubTimer
interval: 25
repeat: true
running: parent.scim && parent.containsMouse
&& (parent.mouseX < 50 || parent.mouseX > parent.width - 50)
&& (timeline.position * timeline.scaleFactor >= 50)
onTriggered: {
if (parent.mouseX < 50)
timeline.seekPosition = Math.max(0, timeline.position - 10)
else
timeline.seekPosition = Math.min(timeline.position + 10, timeline.fullDuration - 1)
}
}
Column {
Flickable {
// Non-slider scroll area for the Ruler.
id: rulercontainer
width: root.width - headerWidth
height: fontMetrics.font.pixelSize * 2
contentX: scrollView.flickableItem.contentX
contentWidth: Math.max(parent.width, timeline.fullDuration * timeScale)
interactive: false
clip: true
Ruler {
id: ruler
width: rulercontainer.contentWidth
height: parent.height
Rectangle {
id: seekCursor
visible: timeline.seekPosition > -1
color: activePalette.highlight
width: 4
height: ruler.height
opacity: 0.5
x: timeline.seekPosition * timeline.scaleFactor
}
TimelinePlayhead {
id: playhead
visible: timeline.position > -1
height: baseUnit
width: baseUnit * 1.5
fillColor: activePalette.windowText
anchors.bottom: parent.bottom
x: timeline.position * timeline.scaleFactor - (width / 2)
}
}
}
OLD.ScrollView {
id: scrollView
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
// Click and drag should seek, not scroll the timeline view
flickableItem.interactive: false
clip: true
Rectangle {
id: tracksContainerArea
width: Math.max(scrollView.width - scrollView.__verticalScrollBar.width, timeline.fullDuration * timeScale)
height: trackHeaders.height
//Math.max(trackHeaders.height, scrollView.contentHeight - scrollView.__horizontalScrollBar.height)
color: root.color
Rectangle {
// Drag proxy, responsible for clip / composition move
id: dragProxy
x: 0
y: 0
width: 0
height: 0
property int draggedItem: -1
property int sourceTrack
property int sourceFrame
property bool isComposition
property int verticalOffset
property var masterObject
color: 'green'
opacity: 0.8
MouseArea {
id: dragProxyArea
anchors.fill: parent
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
drag.minimumX: 0
property int dragFrame
property bool shiftClick: false
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
onPressed: {
console.log('+++++++++++++++++++ DRAG CLICKED +++++++++++++')
if (mouse.modifiers & Qt.ControlModifier || !(controller.isClip(dragProxy.draggedItem) || controller.isComposition(dragProxy.draggedItem))) {
mouse.accepted = false
return
}
dragFrame = -1
timeline.activeTrack = dragProxy.sourceTrack
if (mouse.modifiers & Qt.ShiftModifier) {
if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) {
console.log('ADD SELECTION: ', dragProxy.draggedItem)
controller.requestAddToSelection(dragProxy.draggedItem)
} else {
console.log('REMOVE SELECTION: ', dragProxy.draggedItem)
controller.requestRemoveFromSelection(dragProxy.draggedItem)
//endDrag()
shiftClick = true
return
}
shiftClick = true
} else {
if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) {
controller.requestAddToSelection(dragProxy.draggedItem, /*clear=*/ true)
}
shiftClick = false
}
timeline.showAsset(dragProxy.draggedItem)
root.stopScrolling = true
clipBeingMovedId = dragProxy.draggedItem
if (dragProxy.draggedItem > -1) {
var tk = controller.getItemTrackId(dragProxy.draggedItem)
var x = controller.getItemPosition(dragProxy.draggedItem)
var posx = Math.round((parent.x)/ root.timeScale)
var clickAccepted = true
if (controller.normalEdit() && (tk != Logic.getTrackIdFromPos(parent.y) || x != posx)) {
console.log('INCORRECT DRAG, Trying to recover item: ', parent.y,' XPOS: ',x,'=',posx,'\n!!!!!!!!!!')
// Try to find correct item
var track = Logic.getTrackById(tk)
var container = track.children[0].children[0].children[0]
var tentativeClip = container.childAt(mouseX + parent.x, 5)
if (tentativeClip && tentativeClip.clipId) {
clickAccepted = true
dragProxy.draggedItem = tentativeClip.clipId
dragProxy.x = tentativeClip.x
dragProxy.y = tentativeClip.y
dragProxy.width = tentativeClip.width
dragProxy.height = tentativeClip.height
dragProxy.masterObject = tentativeClip
dragProxy.sourceTrack = tk
dragProxy.sourceFrame = tentativeClip.modelStart
dragProxy.isComposition = tentativeClip.isComposition
} else {
clickAccepted = false
mouse.accepted = false
dragProxy.draggedItem = -1
dragProxy.masterObject = undefined
parent.x = 0
parent.y = 0
parent.width = 0
parent.height = 0
}
}
if (clickAccepted && dragProxy.draggedItem != -1) {
focus = true;
dragProxy.masterObject.originalX = dragProxy.masterObject.x
dragProxy.masterObject.originalTrackId = dragProxy.masterObject.trackId
dragProxy.masterObject.forceActiveFocus();
}
} else {
mouse.accepted = false
parent.x = 0
parent.y = 0
parent.width = 0
parent.height = 0
}
}
onPositionChanged: {
// we have to check item validity in the controller, because they could have been deleted since the beginning of the drag
if (!shiftClick && dragProxy.draggedItem > -1 && mouse.buttons === Qt.LeftButton && (controller.isClip(dragProxy.draggedItem) || controller.isComposition(dragProxy.draggedItem))) {
continuousScrolling(mouse.x + parent.x)
var mapped = tracksContainerArea.mapFromItem(dragProxy, mouse.x, mouse.y).x
root.mousePosChanged(Math.round(mapped / timeline.scaleFactor))
var posx = Math.round((parent.x)/ root.timeScale)
var posy = Math.min(Math.max(0, mouse.y + parent.y - dragProxy.verticalOffset), tracksContainerArea.height)
var tId = Logic.getTrackIdFromPos(posy)
if (dragProxy.masterObject && tId == dragProxy.masterObject.trackId) {
if (posx == dragFrame) {
return
}
}
if (dragProxy.isComposition) {
dragFrame = controller.suggestCompositionMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping))
timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem)
} else {
if (!controller.normalEdit() && dragProxy.masterObject.parent != dragContainer) {
var pos = dragProxy.masterObject.mapToGlobal(dragProxy.masterObject.x, dragProxy.masterObject.y);
dragProxy.masterObject.parent = dragContainer
pos = dragProxy.masterObject.mapFromGlobal(pos.x, pos.y)
dragProxy.masterObject.x = pos.x
dragProxy.masterObject.y = pos.y
console.log('bringing item to front')
}
dragFrame = controller.suggestClipMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping))
timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem)
}
var delta = dragFrame - dragProxy.sourceFrame
if (delta != 0) {
var s = timeline.timecode(Math.abs(delta))
// remove leading zeroes
if (s.substring(0, 3) === '00:')
s = s.substring(3)
s = ((delta < 0)? '-' : (delta > 0)? '+' : '') + s
bubbleHelp.show(parent.x, ruler.height, s)
} else bubbleHelp.hide()
}
}
onReleased: {
clipBeingMovedId = -1
if (!shiftClick && dragProxy.draggedItem > -1 && dragFrame > -1 && (controller.isClip(dragProxy.draggedItem) || controller.isComposition(dragProxy.draggedItem))) {
var tId = controller.getItemTrackId(dragProxy.draggedItem)
if (dragProxy.isComposition) {
controller.requestCompositionMove(dragProxy.draggedItem, dragProxy.sourceTrack, dragProxy.sourceFrame, true, false, false)
controller.requestCompositionMove(dragProxy.draggedItem, tId, dragFrame , true, true, true)
} else {
if (controller.normalEdit()) {
controller.requestClipMove(dragProxy.draggedItem, dragProxy.sourceTrack, dragProxy.sourceFrame, true, false, false)
controller.requestClipMove(dragProxy.draggedItem, tId, dragFrame , true, true, true)
} else {
// Fake move, only process final move
timeline.endFakeMove(dragProxy.draggedItem, dragFrame , true, true, true)
}
}
dragProxy.x = controller.getItemPosition(dragProxy.draggedItem) * timeline.scaleFactor
dragProxy.sourceFrame = dragFrame
bubbleHelp.hide()
}
}
onDoubleClicked: {
if (dragProxy.masterObject.keyframeModel) {
var newVal = (dragProxy.height - mouseY) / dragProxy.height
var newPos = Math.round(mouseX / timeScale) + dragProxy.masterObject.inPoint
timeline.addEffectKeyframe(dragProxy.draggedItem, newPos, newVal)
}
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: zoomByWheel(wheel)
cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : tracksArea.cursorShape
}
Column {
// These make the striped background for the tracks.
// It is important that these are not part of the track visual hierarchy;
// otherwise, the clips will be obscured by the Track's background.
Repeater {
model: multitrack
id: trackBaseRepeater
delegate: Rectangle {
width: tracksContainerArea.width
border.width: 1
border.color: root.frameColor
height: model.trackHeight
color: tracksRepeater.itemAt(index) ? ((tracksRepeater.itemAt(index).trackInternalId === timeline.activeTrack) ? Qt.tint(getTrackColor(tracksRepeater.itemAt(index).isAudio, false), selectedTrackColor) : getTrackColor(tracksRepeater.itemAt(index).isAudio, false)) : 'red'
}
}
}
Column {
id: tracksContainer
Repeater { id: tracksRepeater; model: trackDelegateModel }
Item {
id: dragContainer
}
Repeater { id: guidesRepeater; model: guidesDelegateModel }
}
Rectangle {
id: cursor
visible: timeline.position > -1
color: root.textColor
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: parent.height
x: timeline.position * timeline.scaleFactor
}
}
}
}
/*CornerSelectionShadow {
y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0
clip: timeline.selection.length ?
tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[0]) : null
opacity: clip && clip.x + clip.width < scrollView.flickableItem.contentX ? 1 : 0
}
CornerSelectionShadow {
y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0
clip: timeline.selection.length ?
tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[timeline.selection.length - 1]) : null
opacity: clip && clip.x > scrollView.flickableItem.contentX + scrollView.width ? 1 : 0
anchors.right: parent.right
mirrorGradient: true
}*/
Rectangle {
id: cutLine
visible: root.activeTool == 1 && tracksArea.mouseY > ruler.height
color: 'red'
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: root.height - scrollView.__horizontalScrollBar.height - ruler.height
x: 0
//x: timeline.position * timeline.scaleFactor - scrollView.flickableItem.contentX
y: ruler.height
}
}
}
Rectangle {
id: bubbleHelp
property alias text: bubbleHelpLabel.text
color: root.color //application.toolTipBaseColor
width: bubbleHelpLabel.width + 8
height: bubbleHelpLabel.height + 8
radius: 4
states: [
State { name: 'invisible'; PropertyChanges { target: bubbleHelp; opacity: 0} },
State { name: 'visible'; PropertyChanges { target: bubbleHelp; opacity: 0.8} }
]
state: 'invisible'
transitions: [
Transition {
from: 'invisible'
to: 'visible'
OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad }
},
Transition {
from: 'visible'
to: 'invisible'
OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad }
}
]
Label {
id: bubbleHelpLabel
color: activePalette.text //application.toolTipTextColor
anchors.centerIn: parent
font.pixelSize: root.baseUnit
}
function show(x, y, text) {
bubbleHelp.x = x + tracksArea.x - scrollView.flickableItem.contentX - bubbleHelpLabel.width
bubbleHelp.y = y + tracksArea.y - scrollView.flickableItem.contentY - bubbleHelpLabel.height
bubbleHelp.text = text
if (bubbleHelp.state !== 'visible')
bubbleHelp.state = 'visible'
}
function hide() {
bubbleHelp.state = 'invisible'
bubbleHelp.opacity = 0
}
}
Rectangle {
id: rubberSelect
property int originX
property int originY
y: -1
color: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.4)
border.color: activePalette.highlight
border.width: 1
visible: false
}
/*DropShadow {
source: bubbleHelp
anchors.fill: bubbleHelp
opacity: bubbleHelp.opacity
horizontalOffset: 3
verticalOffset: 3
radius: 8
color: '#80000000'
transparentBorder: true
fast: true
}*/
DelegateModel {
id: trackDelegateModel
model: multitrack
delegate: Track {
trackModel: multitrack
rootIndex: trackDelegateModel.modelIndex(index)
height: trackHeight
timeScale: timeline.scaleFactor
width: tracksContainerArea.width
isAudio: audio
trackThumbsFormat: thumbsFormat
isCurrentTrack: item === timeline.activeTrack
trackInternalId: item
z: tracksRepeater.count - index
}
}
DelegateModel {
id: guidesDelegateModel
model: guidesModel
Item {
id: guideRoot
z: 20
Rectangle {
id: guideBase
width: 1
height: tracksContainer.height
x: model.frame * timeScale;
color: model.color
}
Rectangle {
visible: mlabel.visible
opacity: 0.7
x: guideBase.x
y: mlabel.y
radius: 2
width: mlabel.width + 4
height: mlabel.height
color: model.color
MouseArea {
z: 10
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
property int startX
drag.axis: Drag.XAxis
drag.target: guideRoot
onPressed: {
drag.target = guideRoot
startX = guideRoot.x
}
onReleased: {
if (startX != guideRoot.x) {
timeline.moveGuide(model.frame, model.frame + guideRoot.x / timeline.scaleFactor)
}
drag.target = undefined
}
onPositionChanged: {
if (pressed) {
var frame = Math.round(model.frame + guideRoot.x / timeline.scaleFactor)
frame = controller.suggestSnapPoint(frame, root.snapping)
guideRoot.x = (frame - model.frame) * timeline.scaleFactor
}
}
drag.smoothed: false
onDoubleClicked: {
timeline.editGuide(model.frame)
drag.target = undefined
}
onClicked: timeline.position = guideBase.x / timeline.scaleFactor
}
}
Text {
id: mlabel
visible: timeline.showMarkers
text: model.comment
font.pixelSize: root.baseUnit
x: guideBase.x + 2
y: scrollView.flickableItem.contentY
color: 'white'
}
}
}
Connections {
target: timeline
onPositionChanged: if (!stopScrolling) Logic.scrollIfNeeded()
onFrameFormatChanged: ruler.adjustFormat()
onSelectionChanged: {
//cornerstone.selected = timeline.isMultitrackSelected()
if (dragProxy.draggedItem > -1 && !timeline.exists(dragProxy.draggedItem)) {
endDrag()
}
}
}
// This provides continuous scrolling at the left/right edges.
Timer {
id: scrollTimer
interval: 25
repeat: true
triggeredOnStart: true
property var item
property bool backwards
onTriggered: {
var delta = backwards? -10 : 10
if (item) item.x += delta
scrollView.flickableItem.contentX += delta
if (scrollView.flickableItem.contentX <= 0 || clipBeingMovedId == -1)
stop()
}
}
}
diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp
index f035d209d..c064731b5 100644
--- a/src/timeline2/view/timelinecontroller.cpp
+++ b/src/timeline2/view/timelinecontroller.cpp
@@ -1,2210 +1,2216 @@
/***************************************************************************
* Copyright (C) 2017 by Jean-Baptiste Mardelle *
* This file is part of Kdenlive. See www.kdenlive.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 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "timelinecontroller.h"
#include "../model/timelinefunctions.hpp"
#include "assets/keyframes/model/keyframemodellist.hpp"
#include "bin/bin.h"
#include "bin/clipcreator.hpp"
#include "bin/model/markerlistmodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectfolder.h"
#include "bin/projectitemmodel.h"
#include "core.h"
#include "dialogs/spacerdialog.h"
#include "dialogs/speeddialog.h"
#include "doc/kdenlivedoc.h"
#include "effects/effectsrepository.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "kdenlivesettings.h"
#include "lib/audio/audioEnvelope.h"
#include "mainwindow.h"
#include "monitor/monitormanager.h"
#include "previewmanager.h"
#include "project/projectmanager.h"
#include "timeline2/model/clipmodel.hpp"
#include "timeline2/model/compositionmodel.hpp"
#include "timeline2/model/groupsmodel.hpp"
#include "timeline2/model/timelineitemmodel.hpp"
#include "timeline2/model/trackmodel.hpp"
#include "timeline2/view/dialogs/clipdurationdialog.h"
#include "timeline2/view/dialogs/trackdialog.h"
#include "transitions/transitionsrepository.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
int TimelineController::m_duration = 0;
TimelineController::TimelineController(QObject *parent)
: QObject(parent)
, m_root(nullptr)
, m_usePreview(false)
, m_position(0)
, m_seekPosition(-1)
, m_activeTrack(0)
, m_audioRef(-1)
, m_zone(-1, -1)
, m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250)
, m_timelinePreview(nullptr)
{
m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview"));
connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview);
connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
m_disablePreview->setEnabled(false);
}
TimelineController::~TimelineController()
{
delete m_timelinePreview;
m_timelinePreview = nullptr;
}
void TimelineController::setModel(std::shared_ptr model)
{
delete m_timelinePreview;
m_zone = QPoint(-1, -1);
m_timelinePreview = nullptr;
m_model = std::move(model);
connect(m_model.get(), &TimelineItemModel::requestClearAssetView, [&](int id) { pCore->clearAssetPanel(id); });
connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); });
connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection);
connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration);
connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
}
void TimelineController::setTargetTracks(QPair targets)
{
setVideoTarget(targets.first >= 0 && targets.first < m_model->getTracksCount() ? m_model->getTrackIndexFromPosition(targets.first) : -1);
setAudioTarget(targets.second >= 0 && targets.second < m_model->getTracksCount() ? m_model->getTrackIndexFromPosition(targets.second) : -1);
}
std::shared_ptr TimelineController::getModel() const
{
return m_model;
}
void TimelineController::setRoot(QQuickItem *root)
{
m_root = root;
}
Mlt::Tractor *TimelineController::tractor()
{
return m_model->tractor();
}
int TimelineController::getCurrentItem()
{
// TODO: if selection is empty, return topmost clip under timeline cursor
auto selection = m_model->getCurrentSelection();
if (selection.empty()) {
return -1;
}
// TODO: if selection contains more than 1 clip, return topmost clip under timeline cursor in selection
return *(selection.begin());
}
double TimelineController::scaleFactor() const
{
return m_scale;
}
const QString TimelineController::getTrackNameFromMltIndex(int trackPos)
{
if (trackPos == -1) {
return i18n("unknown");
}
if (trackPos == 0) {
return i18n("Black");
}
return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1));
}
const QString TimelineController::getTrackNameFromIndex(int trackIndex)
{
QString trackName = m_model->getTrackFullName(trackIndex);
return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName;
}
QMap TimelineController::getTrackNames(bool videoOnly)
{
QMap names;
for (const auto &track : m_model->m_iteratorTable) {
if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) {
continue;
}
QString trackName = m_model->getTrackFullName(track.first);
names[m_model->getTrackMltIndex(track.first)] = trackName;
}
return names;
}
void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse)
{
if (m_root) {
m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMin(getMousePos(), duration()) : -1);
m_scale = scale;
emit scaleFactorChanged();
} else {
qWarning("Timeline root not created, impossible to zoom in");
}
}
void TimelineController::setScaleFactor(double scale)
{
m_scale = scale;
// Update mainwindow's zoom slider
emit updateZoom(scale);
// inform qml
emit scaleFactorChanged();
}
int TimelineController::duration() const
{
return m_duration;
}
int TimelineController::fullDuration() const
{
return m_duration + TimelineModel::seekDuration;
}
void TimelineController::checkDuration()
{
int currentLength = m_model->duration();
if (currentLength != m_duration) {
m_duration = currentLength;
emit durationChanged();
}
}
int TimelineController::selectedTrack() const
{
std::unordered_set sel = m_model->getCurrentSelection();
if (sel.empty()) return -1;
std::vector> selected_tracks; // contains pairs of (track position, track id) for each selected item
for (int s : sel) {
int tid = m_model->getItemTrackId(s);
selected_tracks.push_back({m_model->getTrackPosition(tid), tid});
}
// sort by track position
std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; });
return selected_tracks.front().second;
}
void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent)
{
QList toSelect;
int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, timelinePosition())
: m_model->getCompositionByPosition(m_activeTrack, timelinePosition());
if (currentClip == -1) {
pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500);
return;
}
if (!select) {
m_model->requestRemoveFromSelection(currentClip);
} else {
m_model->requestAddToSelection(currentClip, !addToCurrent);
}
}
QList TimelineController::selection() const
{
if (!m_root) return QList();
QList items;
for (int id : m_model->getCurrentSelection()) {
items << id;
}
return items;
}
void TimelineController::selectItems(const QList &ids)
{
std::unordered_set ids_s(ids.begin(), ids.end());
m_model->requestSetSelection(ids_s);
}
void TimelineController::setScrollPos(int pos)
{
if (pos > 0 && m_root) {
QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos));
}
}
void TimelineController::resetView()
{
m_model->_resetView();
if (m_root) {
QMetaObject::invokeMethod(m_root, "updatePalette");
}
emit colorsChanged();
}
bool TimelineController::snap()
{
return KdenliveSettings::snaptopoints();
}
bool TimelineController::ripple()
{
return false;
}
bool TimelineController::scrub()
{
return false;
}
int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets)
{
int id;
if (tid == -1) {
tid = m_activeTrack;
}
if (position == -1) {
position = timelinePosition();
}
if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) {
id = -1;
}
return id;
}
QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView)
{
QList clipIds;
if (tid == -1) {
tid = m_activeTrack;
}
if (position == -1) {
position = timelinePosition();
}
TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView);
// we don't need to check the return value of the above function, in case of failure it will return an empty list of ids.
return clipIds;
}
int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo)
{
int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position);
if (clipId > 0) {
int minimum = m_model->getClipPosition(clipId);
return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo);
}
return insertComposition(tid, position, transitionId, logUndo);
}
int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo)
{
int id;
int minimum = m_model->getClipPosition(clipId);
int clip_duration = m_model->getClipPlaytime(clipId);
int position = minimum;
if (offset > clip_duration / 2) {
position += offset;
}
position = qMin(minimum + clip_duration - 1, position);
int duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position);
int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid);
bool revert = false;
if (lowerVideoTrackId > 0) {
int bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position);
if (bottomId > 0) {
QPair bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime());
if (bottom.first > minimum && position > bottom.first) {
int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first);
if (test_duration > 0) {
position = bottom.first;
duration = test_duration;
revert = true;
}
}
}
int duration2 = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position);
if (duration2 > 0) {
duration = (duration > 0) ? qMin(duration, duration2) : duration2;
}
}
if (duration < 0) {
duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration());
} else if (duration <= 1) {
// if suggested composition duration is lower than 4 frames, use default
duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration());
if (minimum + clip_duration - position < 3) {
position = minimum + clip_duration - duration;
}
}
std::unique_ptr props(nullptr);
if (revert) {
props = std::make_unique();
if (transitionId == QLatin1String("dissolve")) {
props->set("reverse", 1);
} else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) {
props->set("invert", 1);
} else if (transitionId == QLatin1String("wipe")) {
props->set("geometry", "0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0");
}
}
if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
id = -1;
pCore->displayMessage(i18n("Could not add composition at selected position"), InformationMessage, 500);
}
return id;
}
int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo)
{
int id;
int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration());
if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) {
id = -1;
}
return id;
}
void TimelineController::deleteSelectedClips()
{
auto sel = m_model->getCurrentSelection();
if (sel.empty()) {
return;
}
// only need to delete the first item, the others will be deleted in cascade
m_model->requestItemDeletion(*sel.begin());
}
void TimelineController::copyItem()
{
std::unordered_set selectedIds = m_model->getCurrentSelection();
if (selectedIds.empty()) {
return;
}
int clipId = *(selectedIds.begin());
QString copyString = TimelineFunctions::copyClips(m_model, selectedIds);
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(copyString);
m_root->setProperty("copiedClip", clipId);
m_model->requestSetSelection(selectedIds);
}
bool TimelineController::pasteItem()
{
QClipboard *clipboard = QApplication::clipboard();
QString txt = clipboard->text();
int tid = getMouseTrack();
int position = getMousePos();
if (tid == -1) {
tid = m_activeTrack;
}
if (position == -1) {
position = timelinePosition();
}
return TimelineFunctions::pasteClips(m_model, txt, tid, position);
}
void TimelineController::triggerAction(const QString &name)
{
pCore->triggerAction(name);
}
QString TimelineController::timecode(int frames)
{
return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
}
bool TimelineController::showThumbnails() const
{
return KdenliveSettings::videothumbnails();
}
bool TimelineController::showAudioThumbnails() const
{
return KdenliveSettings::audiothumbnails();
}
bool TimelineController::showMarkers() const
{
return KdenliveSettings::showmarkers();
}
bool TimelineController::audioThumbFormat() const
{
return KdenliveSettings::displayallchannels();
}
bool TimelineController::showWaveforms() const
{
return KdenliveSettings::audiothumbnails();
}
void TimelineController::addTrack(int tid)
{
if (tid == -1) {
tid = m_activeTrack;
}
QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow());
if (d->exec() == QDialog::Accepted) {
int newTid;
bool audioRecTrack = d->addRecTrack();
bool addAVTrack = d->addAVTrack();
m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack());
if (addAVTrack) {
int newTid2;
int mirrorPos = 0;
int mirrorId = m_model->getMirrorAudioTrackId(newTid);
if (mirrorId > -1) {
mirrorPos = m_model->getTrackMltIndex(mirrorId);
}
m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true);
}
m_model->buildTrackCompositing(true);
m_model->_resetView();
if (audioRecTrack) {
m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1"));
}
}
}
void TimelineController::deleteTrack(int tid)
{
if (tid == -1) {
tid = m_activeTrack;
}
QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow(), true);
if (d->exec() == QDialog::Accepted) {
int selectedTrackIx = d->selectedTrackId();
m_model->requestTrackDeletion(selectedTrackIx);
m_model->buildTrackCompositing(true);
if (m_activeTrack == selectedTrackIx) {
setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1));
}
}
}
void TimelineController::showConfig(int page, int tab)
{
pCore->showConfigDialog(page, tab);
}
void TimelineController::gotoNextSnap()
{
setPosition(m_model->getNextSnapPos(timelinePosition()));
}
void TimelineController::gotoPreviousSnap()
{
setPosition(m_model->getPreviousSnapPos(timelinePosition()));
}
void TimelineController::groupSelection()
{
const auto selection = m_model->getCurrentSelection();
if (selection.size() < 2) {
pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500);
return;
}
m_model->requestClearSelection();
m_model->requestClipsGroup(selection);
m_model->requestSetSelection(selection);
}
void TimelineController::unGroupSelection(int cid)
{
auto ids = m_model->getCurrentSelection();
// ask to unselect if needed
m_model->requestClearSelection();
if (cid > -1) {
ids.insert(cid);
}
if (!ids.empty()) {
m_model->requestClipsUngroup(ids);
}
}
void TimelineController::setInPoint()
{
int cursorPos = timelinePosition();
const auto selection = m_model->getCurrentSelection();
if (!selection.empty()) {
for (int id : selection) {
int start = m_model->getItemPosition(id);
if (start == cursorPos) {
continue;
}
int size = start + m_model->getItemPlaytime(id) - cursorPos;
m_model->requestItemResize(id, size, false, true, 0, false);
}
}
}
int TimelineController::timelinePosition() const
{
return m_seekPosition >= 0 ? m_seekPosition : m_position;
}
void TimelineController::setOutPoint()
{
int cursorPos = timelinePosition();
const auto selection = m_model->getCurrentSelection();
if (!selection.empty()) {
for (int id : selection) {
int start = m_model->getItemPosition(id);
if (start + m_model->getItemPlaytime(id) == cursorPos) {
continue;
}
int size = cursorPos - start;
m_model->requestItemResize(id, size, true, true, 0, false);
}
}
}
void TimelineController::editMarker(const QString &cid, int frame)
{
std::shared_ptr clip = pCore->bin()->getBinClip(cid);
GenTime pos(frame, pCore->getCurrentFps());
clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get());
}
void TimelineController::editGuide(int frame)
{
if (frame == -1) {
frame = timelinePosition();
}
auto guideModel = pCore->projectManager()->current()->getGuideModel();
GenTime pos(frame, pCore->getCurrentFps());
guideModel->editMarkerGui(pos, qApp->activeWindow(), false);
}
void TimelineController::moveGuide(int frame, int newFrame)
{
auto guideModel = pCore->projectManager()->current()->getGuideModel();
GenTime pos(frame, pCore->getCurrentFps());
GenTime newPos(newFrame, pCore->getCurrentFps());
guideModel->editMarker(pos, newPos);
}
void TimelineController::switchGuide(int frame, bool deleteOnly)
{
bool markerFound = false;
if (frame == -1) {
frame = timelinePosition();
}
CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound);
if (!markerFound) {
if (deleteOnly) {
pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500);
return;
}
GenTime pos(frame, pCore->getCurrentFps());
pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide"));
} else {
pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time());
}
}
void TimelineController::addAsset(const QVariantMap &data)
{
QString effect = data.value(QStringLiteral("kdenlive/effect")).toString();
const auto selection = m_model->getCurrentSelection();
if (!selection.empty()) {
QList effectSelection;
for (int id : selection) {
if (m_model->isClip(id)) {
effectSelection << id;
}
}
bool foundMatch = false;
for (int id : effectSelection) {
if (m_model->addClipEffect(id, effect, false)) {
foundMatch = true;
}
}
if (!foundMatch) {
QString effectName = EffectsRepository::get()->getName(effect);
pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500);
}
} else {
pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500);
}
}
void TimelineController::requestRefresh()
{
pCore->requestMonitorRefresh();
}
void TimelineController::showAsset(int id)
{
if (m_model->isComposition(id)) {
emit showTransitionModel(id, m_model->getCompositionParameterModel(id));
} else if (m_model->isClip(id)) {
QModelIndex clipIx = m_model->makeClipIndexFromID(id);
QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString();
bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt();
qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes;
emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes);
}
}
void TimelineController::showTrackAsset(int trackId)
{
emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false);
}
void TimelineController::setPosition(int position)
{
setSeekPosition(position);
emit seeked(position);
}
void TimelineController::setAudioTarget(int track)
{
m_model->m_audioTarget = track;
emit audioTargetChanged();
}
void TimelineController::setVideoTarget(int track)
{
m_model->m_videoTarget = track;
emit videoTargetChanged();
}
void TimelineController::setActiveTrack(int track)
{
m_activeTrack = track;
emit activeTrackChanged();
}
void TimelineController::setSeekPosition(int position)
{
m_seekPosition = position;
emit seekPositionChanged();
}
void TimelineController::onSeeked(int position)
{
m_position = position;
emit positionChanged();
if (m_seekPosition > -1 && position == m_seekPosition) {
m_seekPosition = -1;
emit seekPositionChanged();
}
}
void TimelineController::setZone(const QPoint &zone)
{
if (m_zone.x() > 0) {
m_model->removeSnap(m_zone.x());
}
if (m_zone.y() > 0) {
m_model->removeSnap(m_zone.y() - 1);
}
if (zone.x() > 0) {
m_model->addSnap(zone.x());
}
if (zone.y() > 0) {
m_model->addSnap(zone.y() - 1);
}
m_zone = zone;
emit zoneChanged();
}
void TimelineController::setZoneIn(int inPoint)
{
if (m_zone.x() > 0) {
m_model->removeSnap(m_zone.x());
}
if (inPoint > 0) {
m_model->addSnap(inPoint);
}
m_zone.setX(inPoint);
emit zoneMoved(m_zone);
}
void TimelineController::setZoneOut(int outPoint)
{
if (m_zone.y() > 0) {
m_model->removeSnap(m_zone.y() - 1);
}
if (outPoint > 0) {
m_model->addSnap(outPoint - 1);
}
m_zone.setY(outPoint);
emit zoneMoved(m_zone);
}
void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect)
{
std::unordered_set itemsToSelect;
if (addToSelect) {
itemsToSelect = m_model->getCurrentSelection();
}
for (int i = 0; i < tracks.count(); i++) {
if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) {
continue;
}
auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, true);
itemsToSelect.insert(currentClips.begin(), currentClips.end());
}
m_model->requestSetSelection(itemsToSelect);
}
void TimelineController::requestClipCut(int clipId, int position)
{
if (position == -1) {
position = timelinePosition();
}
TimelineFunctions::requestClipCut(m_model, clipId, position);
}
void TimelineController::cutClipUnderCursor(int position, int track)
{
if (position == -1) {
position = timelinePosition();
}
QMutexLocker lk(&m_metaMutex);
bool foundClip = false;
const auto selection = m_model->getCurrentSelection();
for (int cid : selection) {
if (m_model->isClip(cid)) {
if (TimelineFunctions::requestClipCut(m_model, cid, position)) {
foundClip = true;
// Cutting clips in the selection group is handled in TimelineFunctions
break;
}
} else {
qDebug() << "//// TODO: COMPOSITION CUT!!!";
}
}
if (!foundClip) {
if (track == -1) {
track = m_activeTrack;
}
if (track >= 0) {
int cid = m_model->getClipByPosition(track, position);
if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) {
foundClip = true;
}
}
}
if (!foundClip) {
pCore->displayMessage(i18n("No clip to cut"), InformationMessage, 500);
}
}
int TimelineController::requestSpacerStartOperation(int trackId, int position)
{
return TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position);
}
bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition)
{
return TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition);
}
void TimelineController::seekCurrentClip(bool seekToEnd)
{
const auto selection = m_model->getCurrentSelection();
for (int cid : selection) {
int start = m_model->getItemPosition(cid);
if (seekToEnd) {
start += m_model->getItemPlaytime(cid);
}
setPosition(start);
break;
}
}
void TimelineController::seekToClip(int cid, bool seekToEnd)
{
int start = m_model->getItemPosition(cid);
if (seekToEnd) {
start += m_model->getItemPlaytime(cid);
}
setPosition(start);
}
void TimelineController::seekToMouse()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue));
int mousePos = returnedValue.toInt();
setPosition(mousePos);
}
int TimelineController::getMousePos()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue));
return returnedValue.toInt();
}
int TimelineController::getMouseTrack()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue));
return returnedValue.toInt();
}
void TimelineController::refreshItem(int id)
{
int in = m_model->getItemPosition(id);
if (in > m_position || (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly())) {
return;
}
if (m_position <= in + m_model->getItemPlaytime(id)) {
pCore->requestMonitorRefresh();
}
}
QPoint TimelineController::getTracksCount() const
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue));
QVariantList tracks = returnedValue.toList();
QPoint p(tracks.at(0).toInt(), tracks.at(1).toInt());
return p;
}
QStringList TimelineController::extractCompositionLumas() const
{
return m_model->extractCompositionLumas();
}
void TimelineController::addEffectToCurrentClip(const QStringList &effectData)
{
QList activeClips;
for (int track = m_model->getTracksCount() - 1; track >= 0; track--) {
int trackIx = m_model->getTrackIndexFromPosition(track);
int cid = m_model->getClipByPosition(trackIx, timelinePosition());
if (cid > -1) {
activeClips << cid;
}
}
if (!activeClips.isEmpty()) {
if (effectData.count() == 4) {
QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3);
m_model->copyClipEffect(activeClips.first(), effectString);
} else {
m_model->addClipEffect(activeClips.first(), effectData.constFirst());
}
}
}
void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration)
{
if (duration <= 0) {
// remove fade
m_model->removeFade(cid, effectId == QLatin1String("fadein"));
} else {
m_model->adjustEffectLength(cid, effectId, duration, initialDuration);
}
}
QPair TimelineController::getCompositionATrack(int cid) const
{
QPair result;
std::shared_ptr compo = m_model->getCompositionPtr(cid);
if (compo) {
result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId()));
}
return result;
}
void TimelineController::setCompositionATrack(int cid, int aTrack)
{
TimelineFunctions::setCompositionATrack(m_model, cid, aTrack);
}
bool TimelineController::compositionAutoTrack(int cid) const
{
std::shared_ptr compo = m_model->getCompositionPtr(cid);
return compo && compo->getForcedTrack() == -1;
}
const QString TimelineController::getClipBinId(int clipId) const
{
return m_model->getClipBinId(clipId);
}
void TimelineController::focusItem(int itemId)
{
int start = m_model->getItemPosition(itemId);
setPosition(start);
}
int TimelineController::headerWidth() const
{
return qMax(10, KdenliveSettings::headerwidth());
}
void TimelineController::setHeaderWidth(int width)
{
KdenliveSettings::setHeaderwidth(width);
}
bool TimelineController::createSplitOverlay(Mlt::Filter *filter)
{
if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) {
return true;
}
int clipId = getCurrentItem();
if (clipId == -1) {
pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500);
return false;
}
std::shared_ptr clip = m_model->getClipPtr(clipId);
const QString binId = clip->binId();
// Get clean bin copy of the clip
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId);
std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut()));
// Get copy of timeline producer
std::shared_ptr clipProducer(new Mlt::Producer(*clip));
// Built tractor and compositing
Mlt::Tractor trac(*m_model->m_tractor->profile());
Mlt::Playlist play(*m_model->m_tractor->profile());
Mlt::Playlist play2(*m_model->m_tractor->profile());
play.append(*clipProducer.get());
play2.append(*binProd);
trac.set_track(play, 0);
trac.set_track(play2, 1);
play2.attach(*filter);
QString splitTransition = TransitionsRepository::get()->getCompositingTransition();
Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData());
t.set("always_active", 1);
trac.plant_transition(t, 0, 1);
int startPos = m_model->getClipPosition(clipId);
// plug in overlay playlist
auto *overlay = new Mlt::Playlist(*m_model->m_tractor->profile());
overlay->insert_blank(0, startPos);
Mlt::Producer split(trac.get_producer());
overlay->insert_at(startPos, &split, 1);
// insert in tractor
if (!m_timelinePreview) {
initializePreview();
}
m_timelinePreview->setOverlayTrack(overlay);
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
return true;
}
void TimelineController::removeSplitOverlay()
{
if (m_timelinePreview && !m_timelinePreview->hasOverlayTrack()) {
return;
}
// disconnect
m_timelinePreview->removeOverlayTrack();
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
void TimelineController::addPreviewRange(bool add)
{
if (m_zone.isNull()) {
return;
}
if (!m_timelinePreview) {
initializePreview();
}
if (m_timelinePreview) {
m_timelinePreview->addPreviewRange(m_zone, add);
}
}
void TimelineController::clearPreviewRange()
{
if (m_timelinePreview) {
m_timelinePreview->clearPreviewRange();
}
}
void TimelineController::startPreviewRender()
{
// Timeline preview stuff
if (!m_timelinePreview) {
initializePreview();
} else if (m_disablePreview->isChecked()) {
m_disablePreview->setChecked(false);
disablePreview(false);
}
if (m_timelinePreview) {
if (!m_usePreview) {
m_timelinePreview->buildPreviewTrack();
m_usePreview = true;
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
m_timelinePreview->startPreviewRender();
}
}
void TimelineController::stopPreviewRender()
{
if (m_timelinePreview) {
m_timelinePreview->abortRendering();
}
}
void TimelineController::initializePreview()
{
if (m_timelinePreview) {
// Update parameters
if (!m_timelinePreview->loadParams()) {
if (m_usePreview) {
// Disconnect preview track
m_timelinePreview->disconnectTrack();
m_usePreview = false;
}
delete m_timelinePreview;
m_timelinePreview = nullptr;
}
} else {
m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get());
if (!m_timelinePreview->initialize()) {
// TODO warn user
delete m_timelinePreview;
m_timelinePreview = nullptr;
} else {
}
}
QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone"));
if (previewRender) {
previewRender->setEnabled(m_timelinePreview != nullptr);
}
m_disablePreview->setEnabled(m_timelinePreview != nullptr);
m_disablePreview->blockSignals(true);
m_disablePreview->setChecked(false);
m_disablePreview->blockSignals(false);
}
void TimelineController::disablePreview(bool disable)
{
if (disable) {
m_timelinePreview->deletePreviewTrack();
m_usePreview = false;
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
} else {
if (!m_usePreview) {
if (!m_timelinePreview->buildPreviewTrack()) {
// preview track already exists, reconnect
m_model->m_tractor->lock();
m_timelinePreview->reconnectTrack();
m_model->m_tractor->unlock();
}
m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime());
m_usePreview = true;
}
}
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
QVariantList TimelineController::dirtyChunks() const
{
return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList();
}
QVariantList TimelineController::renderedChunks() const
{
return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList();
}
int TimelineController::workingPreview() const
{
return m_timelinePreview ? m_timelinePreview->workingPreview : -1;
}
bool TimelineController::useRuler() const
{
return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1;
}
void TimelineController::resetPreview()
{
if (m_timelinePreview) {
m_timelinePreview->clearPreviewRange();
initializePreview();
}
}
void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable)
{
if (chunks.isEmpty() && dirty.isEmpty()) {
return;
}
if (!m_timelinePreview) {
initializePreview();
}
QVariantList renderedChunks;
QVariantList dirtyChunks;
QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts);
QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts);
for (const QString &frame : chunksList) {
renderedChunks << frame.toInt();
}
for (const QString &frame : dirtyList) {
dirtyChunks << frame.toInt();
}
m_disablePreview->blockSignals(true);
m_disablePreview->setChecked(enable);
m_disablePreview->blockSignals(false);
if (!enable) {
m_timelinePreview->buildPreviewTrack();
m_usePreview = true;
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate);
}
QMap TimelineController::documentProperties()
{
QMap props = pCore->currentDoc()->documentProperties();
int audioTarget = m_model->m_audioTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_audioTarget);
int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget);
int activeTrack = m_activeTrack == -1 ? -1 : m_model->getTrackPosition(m_activeTrack);
props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget));
props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget));
props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack));
props.insert(QStringLiteral("position"), QString::number(timelinePosition()));
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue));
int scrollPos = returnedValue.toInt();
props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos));
props.insert(QStringLiteral("zonein"), QString::number(m_zone.x()));
props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y()));
if (m_timelinePreview) {
QPair chunks = m_timelinePreview->previewChunks();
props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(',')));
props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(',')));
}
props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked()));
return props;
}
void TimelineController::insertSpace(int trackId, int frame)
{
if (frame == -1) {
frame = timelinePosition();
}
if (trackId == -1) {
trackId = m_activeTrack;
}
QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow());
if (d->exec() != QDialog::Accepted) {
delete d;
return;
}
int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame);
int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps());
delete d;
if (cid == -1) {
pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500);
return;
}
int start = m_model->getItemPosition(cid);
requestSpacerEndOperation(cid, start, start + spaceDuration);
}
void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks)
{
if (frame == -1) {
frame = timelinePosition();
}
if (trackId == -1) {
trackId = m_activeTrack;
}
bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks);
if (!res) {
pCore->displayMessage(i18n("Cannot remove space at given position"), InformationMessage, 500);
}
}
void TimelineController::invalidateItem(int cid)
{
if (!m_timelinePreview || !m_model->isItem(cid) || m_model->getItemTrackId(cid) == -1) {
return;
}
int start = m_model->getItemPosition(cid);
int end = start + m_model->getItemPlaytime(cid);
m_timelinePreview->invalidatePreview(start, end);
}
void TimelineController::invalidateZone(int in, int out)
{
if (!m_timelinePreview) {
return;
}
m_timelinePreview->invalidatePreview(in, out);
}
void TimelineController::changeItemSpeed(int clipId, double speed)
{
if (qFuzzyCompare(speed, -1)) {
speed = 100 * m_model->getClipSpeed(clipId);
double duration = m_model->getItemPlaytime(clipId);
// this is the max speed so that the clip is at least one frame long
double maxSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId));
// this is the min speed so that the clip doesn't bump into the next one on track
double minSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)) / (duration + double(m_model->getBlankSizeNearClip(clipId, true)) - 1);
// if there is a split partner, we must also take it into account
int partner = m_model->getClipSplitPartner(clipId);
if (partner != -1) {
double duration2 = m_model->getItemPlaytime(partner);
double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner));
double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true)) - 1);
minSpeed = std::max(minSpeed, minSpeed2);
maxSpeed = std::min(maxSpeed, maxSpeed2);
}
// speed = QInputDialog::getDouble(QApplication::activeWindow(), i18n("Clip Speed"), i18n("Percentage"), speed, minSpeed, maxSpeed, 2, &ok);
QScopedPointer d(new SpeedDialog(QApplication::activeWindow(), std::abs(speed), minSpeed, maxSpeed, speed < 0));
if (d->exec() != QDialog::Accepted) {
return;
}
speed = d->getValue();
qDebug() << "requesting speed " << speed;
}
m_model->requestClipTimeWarp(clipId, speed);
}
void TimelineController::switchCompositing(int mode)
{
// m_model->m_tractor->lock();
QScopedPointer service(m_model->m_tractor->field());
Mlt::Field *field = m_model->m_tractor->field();
field->lock();
while ((service != nullptr) && service->is_valid()) {
if (service->type() == transition_type) {
Mlt::Transition t((mlt_transition)service->get_service());
QString serviceName = t.get("mlt_service");
if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) {
// remove all compositing transitions
field->disconnect_service(t);
}
}
service.reset(service->producer());
}
if (mode > 0) {
const QString compositeGeometry =
QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height());
// Loop through tracks
for (int track = 1; track < m_model->getTracksCount(); track++) {
if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) {
// This is a video track
Mlt::Transition t(*m_model->m_tractor->profile(),
mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData());
t.set("always_active", 1);
t.set("a_track", 0);
t.set("b_track", track + 1);
if (mode == 1) {
t.set("valign", "middle");
t.set("halign", "centre");
t.set("fill", 1);
t.set("aligned", 0);
t.set("geometry", compositeGeometry.toUtf8().constData());
}
t.set("internal_added", 237);
field->plant_transition(t, 0, track + 1);
}
}
}
field->unlock();
delete field;
pCore->requestMonitorRefresh();
}
void TimelineController::extractZone(QPoint zone, bool liftOnly)
{
QVector tracks;
auto it = m_model->m_allTracks.cbegin();
while (it != m_model->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (target_track != audioTarget() && target_track != videoTarget() && !m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
++it;
continue;
}
tracks << target_track;
++it;
}
if (m_zone == QPoint()) {
// Use current timeline position and clip zone length
zone.setY(timelinePosition() + zone.y() - zone.x());
zone.setX(timelinePosition());
}
TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly);
}
void TimelineController::extract(int clipId)
{
// TODO: grouped clips?
int in = m_model->getClipPosition(clipId);
QPoint zone(in, in + m_model->getClipPlaytime(clipId));
int track = m_model->getClipTrackId(clipId);
TimelineFunctions::extractZone(m_model, QVector() << track, zone, false);
}
int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite)
{
std::shared_ptr clip = pCore->bin()->getBinClip(binId);
int aTrack = -1;
int vTrack = -1;
if (clip->hasAudio()) {
aTrack = audioTarget();
}
if (clip->hasVideo()) {
vTrack = videoTarget();
}
if (aTrack == -1 && vTrack == -1) {
// No target tracks defined, use active track
if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) {
aTrack = m_activeTrack;
vTrack = m_model->getMirrorVideoTrackId(aTrack);
} else {
vTrack = m_activeTrack;
aTrack = m_model->getMirrorAudioTrackId(vTrack);
}
}
int insertPoint;
QPoint sourceZone;
if (useRuler() && m_zone != QPoint()) {
// We want to use timeline zone for in/out insert points
insertPoint = m_zone.x();
sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x());
} else {
// Use current timeline pos and clip zone for in/out
insertPoint = timelinePosition();
sourceZone = zone;
}
QList target_tracks;
if (vTrack > -1) {
target_tracks << vTrack;
}
if (aTrack > -1) {
target_tracks << aTrack;
}
return TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite) ? insertPoint + (sourceZone.y() - sourceZone.x())
: -1;
}
void TimelineController::updateClip(int clipId, const QVector &roles)
{
QModelIndex ix = m_model->makeClipIndexFromID(clipId);
if (ix.isValid()) {
m_model->dataChanged(ix, ix, roles);
}
}
void TimelineController::showClipKeyframes(int clipId, bool value)
{
TimelineFunctions::showClipKeyframes(m_model, clipId, value);
}
void TimelineController::showCompositionKeyframes(int clipId, bool value)
{
TimelineFunctions::showCompositionKeyframes(m_model, clipId, value);
}
void TimelineController::switchEnableState(int clipId)
{
TimelineFunctions::switchEnableState(m_model, clipId);
}
void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset)
{
int track = m_model->getClipTrackId(clipId);
int compoId = -1;
if (assetId.isEmpty()) {
QStringList compositions = KdenliveSettings::favorite_transitions();
if (compositions.isEmpty()) {
pCore->displayMessage(i18n("Select a favorite composition"), InformationMessage, 500);
return;
}
compoId = insertNewComposition(track, clipId, offset, compositions.first(), true);
} else {
compoId = insertNewComposition(track, clipId, offset, assetId, true);
}
if (compoId > 0) {
m_model->requestSetSelection({compoId});
}
}
void TimelineController::addEffectToClip(const QString &assetId, int clipId)
{
qDebug() << "/// ADDING ASSET: " << assetId;
m_model->addClipEffect(clipId, assetId);
}
bool TimelineController::splitAV()
{
int cid = *m_model->getCurrentSelection().begin();
if (m_model->isClip(cid)) {
std::shared_ptr clip = m_model->getClipPtr(cid);
if (clip->clipState() == PlaylistState::AudioOnly) {
return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget());
} else {
return TimelineFunctions::requestSplitAudio(m_model, cid, audioTarget());
}
}
pCore->displayMessage(i18n("No clip found to perform AV split operation"), InformationMessage, 500);
return false;
}
void TimelineController::splitAudio(int clipId)
{
TimelineFunctions::requestSplitAudio(m_model, clipId, audioTarget());
}
void TimelineController::splitVideo(int clipId)
{
TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget());
}
void TimelineController::setAudioRef(int clipId)
{
m_audioRef = clipId;
std::unique_ptr envelope(new AudioEnvelope(getClipBinId(clipId), clipId));
m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope)));
connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, [&](int cid, int shift) {
int pos = m_model->getClipPosition(m_audioRef) + shift + m_model->getClipIn(m_audioRef);
bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true);
if (!result) {
pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), InformationMessage, 500);
}
});
connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage);
}
void TimelineController::alignAudio(int clipId)
{
// find other clip
if (m_audioRef == -1 || m_audioRef == clipId) {
pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500);
return;
}
const QString masterBinClipId = getClipBinId(m_audioRef);
if (m_model->m_groups->isInGroup(clipId)) {
std::unordered_set groupIds = m_model->getGroupElements(clipId);
// Check that no item is grouped with our audioRef item
// TODO
m_model->requestClearSelection();
}
const QString otherBinId = getClipBinId(clipId);
if (otherBinId == masterBinClipId) {
// easy, same clip.
int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(clipId);
if (newPos) {
bool result = m_model->requestClipMove(clipId, m_model->getClipTrackId(clipId), newPos, true, true);
if (!result) {
pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500);
}
return;
}
}
// Perform audio calculation
AudioEnvelope *envelope = new AudioEnvelope(getClipBinId(clipId), clipId, (size_t)m_model->getClipIn(clipId), (size_t)m_model->getClipPlaytime(clipId),
(size_t)m_model->getClipPosition(clipId));
m_audioCorrelator->addChild(envelope);
}
void TimelineController::switchTrackActive(int trackId)
{
if (trackId == -1) {
trackId = m_activeTrack;
}
bool active = m_model->getTrackById_const(trackId)->isTimelineActive();
m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1"));
}
void TimelineController::switchTrackLock(bool applyToAll)
{
if (!applyToAll) {
// apply to active track only
bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked();
m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:locked_track"), locked ? QStringLiteral("0") : QStringLiteral("1"));
} else {
// Invert track lock
// Get track states first
QMap trackLockState;
int unlockedTracksCount = 0;
int tracksCount = m_model->getTracksCount();
for (int track = tracksCount - 1; track >= 0; track--) {
int trackIx = m_model->getTrackIndexFromPosition(track);
bool isLocked = m_model->getTrackById_const(trackIx)->getProperty("kdenlive:locked_track").toInt() == 1;
if (!isLocked) {
unlockedTracksCount++;
}
trackLockState.insert(trackIx, isLocked);
}
if (unlockedTracksCount == tracksCount) {
// do not lock all tracks, leave active track unlocked
trackLockState.insert(m_activeTrack, true);
}
QMapIterator i(trackLockState);
while (i.hasNext()) {
i.next();
m_model->setTrackProperty(i.key(), QStringLiteral("kdenlive:locked_track"), i.value() ? QStringLiteral("0") : QStringLiteral("1"));
}
}
}
void TimelineController::switchTargetTrack()
{
bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1;
if (isAudio) {
setAudioTarget(audioTarget() == m_activeTrack ? -1 : m_activeTrack);
} else {
setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack);
}
}
int TimelineController::audioTarget() const
{
return m_model->m_audioTarget;
}
int TimelineController::videoTarget() const
{
return m_model->m_videoTarget;
}
void TimelineController::resetTrackHeight()
{
int tracksCount = m_model->getTracksCount();
for (int track = tracksCount - 1; track >= 0; track--) {
int trackIx = m_model->getTrackIndexFromPosition(track);
m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
}
QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
}
void TimelineController::selectAll()
{
std::unordered_set ids;
for (auto clp : m_model->m_allClips) {
ids.insert(clp.first);
}
for (auto clp : m_model->m_allCompositions) {
ids.insert(clp.first);
}
m_model->requestSetSelection(ids);
}
void TimelineController::selectCurrentTrack()
{
std::unordered_set ids;
for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) {
ids.insert(clp.first);
}
for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) {
ids.insert(clp.first);
}
m_model->requestSetSelection(ids);
}
void TimelineController::pasteEffects(int targetId)
{
std::unordered_set targetIds;
if (targetId == -1) {
std::unordered_set sel = m_model->getCurrentSelection();
if (sel.empty()) {
pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500);
}
for (int s : sel) {
if (m_model->isGroup(s)) {
std::unordered_set sub = m_model->m_groups->getLeaves(s);
for (int current_id : sub) {
if (m_model->isClip(current_id)) {
targetIds.insert(current_id);
}
}
} else if (m_model->isClip(s)) {
targetIds.insert(s);
}
}
} else {
if (m_model->m_groups->isInGroup(targetId)) {
targetId = m_model->m_groups->getRootId(targetId);
}
if (m_model->isGroup(targetId)) {
std::unordered_set sub = m_model->m_groups->getLeaves(targetId);
for (int current_id : sub) {
if (m_model->isClip(current_id)) {
targetIds.insert(current_id);
}
}
} else if (m_model->isClip(targetId)) {
targetIds.insert(targetId);
}
}
if (targetIds.empty()) {
pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500);
}
QClipboard *clipboard = QApplication::clipboard();
QString txt = clipboard->text();
if (txt.isEmpty()) {
pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500);
return;
}
QDomDocument copiedItems;
copiedItems.setContent(txt);
if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) {
pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500);
return;
}
QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
if (clips.isEmpty()) {
pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500);
return;
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects"));
for (int i = 1; i < clips.size(); i++) {
QDomNodeList subs = clips.at(i).childNodes();
for (int j = 0; j < subs.size(); j++) {
effects.appendChild(subs.at(j));
}
}
bool result = true;
for (int target : targetIds) {
std::shared_ptr destStack = m_model->getClipEffectStackModel(target);
result = result && destStack->fromXml(effects, undo, redo);
if (!result) {
break;
}
}
if (result) {
pCore->pushUndo(undo, redo, i18n("Paste effects"));
} else {
pCore->displayMessage(i18n("Cannot paste effect on selected clip"), InformationMessage, 500);
undo();
}
}
double TimelineController::fps() const
{
return pCore->getCurrentFps();
}
void TimelineController::editItemDuration(int id)
{
int start = m_model->getItemPosition(id);
int in = 0;
int duration = m_model->getItemPlaytime(id);
int maxLength = -1;
bool isComposition = false;
if (m_model->isClip(id)) {
in = m_model->getClipIn(id);
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(id));
if (clip && clip->hasLimitedDuration()) {
maxLength = clip->getProducerDuration();
}
} else if (m_model->isComposition(id)) {
// nothing to do
isComposition = true;
} else {
pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500);
return;
}
int trackId = m_model->getItemTrackId(id);
int maxFrame = qMax(0, start + duration +
(isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true)
: m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true)));
int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false)
: m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false)));
int partner = isComposition ? -1 : m_model->getClipSplitPartner(id);
QPointer dialog =
new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow());
if (dialog->exec() == QDialog::Accepted) {
std::function undo = []() { return true; };
std::function redo = []() { return true; };
int newPos = dialog->startPos().frames(pCore->getCurrentFps());
int newIn = dialog->cropStart().frames(pCore->getCurrentFps());
int newDuration = dialog->duration().frames(pCore->getCurrentFps());
bool result = true;
if (newPos < start) {
if (!isComposition) {
result = m_model->requestClipMove(id, trackId, newPos, true, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, undo, redo);
}
} else {
result = m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo);
}
if (result && newIn != in) {
m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo);
}
}
if (newDuration != duration + (in - newIn)) {
result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo);
}
}
} else {
// perform resize first
if (newIn != in) {
result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo);
}
}
if (newDuration != duration + (in - newIn)) {
result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo);
}
}
if (start != newPos || newIn != in) {
if (!isComposition) {
result = result && m_model->requestClipMove(id, trackId, newPos, true, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, undo, redo);
}
} else {
result = result &&
m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo);
}
}
}
if (result) {
pCore->pushUndo(undo, redo, i18n("Edit item"));
} else {
undo();
}
}
}
void TimelineController::updateClipActions()
{
if (m_model->getCurrentSelection().empty()) {
for (QAction *act : clipActions) {
act->setEnabled(false);
}
emit timelineClipSelected(false);
return;
}
std::shared_ptr clip(nullptr);
int item = *m_model->getCurrentSelection().begin();
if (m_model->isClip(item)) {
clip = m_model->getClipPtr(item);
}
for (QAction *act : clipActions) {
bool enableAction = true;
const QChar actionData = act->data().toChar();
if (actionData == QLatin1Char('G')) {
enableAction = isInSelection(item);
} else if (actionData == QLatin1Char('U')) {
enableAction = isInSelection(item) || (m_model->m_groups->isInGroup(item) && !isInSelection(item));
} else if (actionData == QLatin1Char('A')) {
enableAction = clip && clip->clipState() == PlaylistState::AudioOnly;
} else if (actionData == QLatin1Char('V')) {
enableAction = clip && clip->clipState() == PlaylistState::VideoOnly;
} else if (actionData == QLatin1Char('D')) {
enableAction = clip && clip->clipState() == PlaylistState::Disabled;
} else if (actionData == QLatin1Char('E')) {
enableAction = clip && clip->clipState() != PlaylistState::Disabled;
} else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) {
enableAction = clip && clip->canBeVideo() && clip->canBeAudio();
if (enableAction && actionData == QLatin1Char('S')) {
act->setText(clip->clipState() == PlaylistState::AudioOnly ? i18n("Split video") : i18n("Split audio"));
}
} else if (actionData == QLatin1Char('C') && clip == nullptr) {
enableAction = false;
}
act->setEnabled(enableAction);
}
emit timelineClipSelected(clip != nullptr);
}
const QString TimelineController::getAssetName(const QString &assetId, bool isTransition)
{
return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId);
}
void TimelineController::grabCurrent()
{
if (m_model->getCurrentSelection().empty()) {
// TODO: error displayMessage
return;
}
int id = *m_model->getCurrentSelection().begin();
if (m_model->isClip(id)) {
std::shared_ptr clip = m_model->getClipPtr(id);
clip->setGrab(!clip->isGrabbed());
QModelIndex ix = m_model->makeClipIndexFromID(id);
if (ix.isValid()) {
m_model->dataChanged(ix, ix, {TimelineItemModel::GrabbedRole});
}
} else if (m_model->isComposition(id)) {
std::shared_ptr clip = m_model->getCompositionPtr(id);
clip->setGrab(!clip->isGrabbed());
QModelIndex ix = m_model->makeCompositionIndexFromID(id);
if (ix.isValid()) {
m_model->dataChanged(ix, ix, {TimelineItemModel::GrabbedRole});
}
}
}
int TimelineController::getItemMovingTrack(int itemId) const
{
if (m_model->isClip(itemId)) {
int trackId = m_model->m_allClips[itemId]->getFakeTrackId();
return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId;
}
return m_model->m_allCompositions[itemId]->getCurrentTrackId();
}
bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
Q_ASSERT(m_model->m_allClips.count(clipId) > 0);
int trackId = m_model->m_allClips[clipId]->getFakeTrackId();
if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) {
qDebug() << "* * ** END FAKE; NO MOVE RQSTED";
return true;
}
if (m_model->m_groups->isInGroup(clipId)) {
// element is in a group.
int groupId = m_model->m_groups->getRootId(clipId);
int current_trackId = m_model->getClipTrackId(clipId);
int track_pos1 = m_model->getTrackPosition(trackId);
int track_pos2 = m_model->getTrackPosition(current_trackId);
int delta_track = track_pos1 - track_pos2;
int delta_pos = position - m_model->m_allClips[clipId]->getPosition();
return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
}
qDebug() << "//////\n//////\nENDING FAKE MNOVE: " << trackId << ", POS: " << position;
std::function undo = []() { return true; };
std::function redo = []() { return true; };
int duration = m_model->getClipPlaytime(clipId);
int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId();
bool res = true;
if (currentTrack > -1) {
res = res && m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo);
}
if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo);
} else if (m_model->m_editMode == TimelineMode::InsertEdit) {
int startClipId = m_model->getClipByPosition(trackId, position);
if (startClipId > -1) {
// There is a clip, cut
res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo);
}
res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo);
}
res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo);
if (res) {
if (logUndo) {
pCore->pushUndo(undo, redo, i18n("Move item"));
}
} else {
qDebug() << "//// FAKE FAILED";
undo();
}
return res;
}
bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
if (res && logUndo) {
pCore->pushUndo(undo, redo, i18n("Move group"));
}
return res;
}
bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo)
{
Q_ASSERT(m_model->m_allGroups.count(groupId) > 0);
bool ok = true;
auto all_items = m_model->m_groups->getLeaves(groupId);
Q_ASSERT(all_items.size() > 1);
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
// Sort clips. We need to delete from right to left to avoid confusing the view
std::vector sorted_clips(all_items.begin(), all_items.end());
std::sort(sorted_clips.begin(), sorted_clips.end(), [this](int clipId1, int clipId2) {
int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition();
int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition();
return p2 <= p1;
});
// Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
// This way, we ensure that no conflict will arise with clips inside the group being moved
// First, remove clips
int audio_delta, video_delta;
audio_delta = video_delta = delta_track;
int master_trackId = m_model->getItemTrackId(clipId);
if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) {
// Master clip is audio, so reverse delta for video clips
video_delta = -delta_track;
} else {
audio_delta = -delta_track;
}
int min = -1;
int max = -1;
std::unordered_map old_track_ids, old_position, old_forced_track, new_track_ids;
for (int item : sorted_clips) {
int old_trackId = m_model->getItemTrackId(item);
old_track_ids[item] = old_trackId;
if (old_trackId != -1) {
bool updateThisView = true;
if (m_model->isClip(item)) {
int current_track_position = m_model->getTrackPosition(old_trackId);
int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta;
int target_track_position = current_track_position + d;
auto it = m_model->m_allTracks.cbegin();
std::advance(it, target_track_position);
int target_track = (*it)->getId();
new_track_ids[item] = target_track;
old_position[item] = m_model->m_allClips[item]->getPosition();
int duration = m_model->m_allClips[item]->getPlaytime();
min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos);
max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration);
ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo);
} else {
// ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo);
old_position[item] = m_model->m_allCompositions[item]->getPosition();
old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack();
}
if (!ok) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
}
}
bool res = true;
if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
for (int item : sorted_clips) {
if (m_model->isClip(item) && new_track_ids.count(item) > 0) {
int target_track = new_track_ids[item];
int target_position = old_position[item] + delta_pos;
int duration = m_model->m_allClips[item]->getPlaytime();
res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo);
}
}
} else if (m_model->m_editMode == TimelineMode::InsertEdit) {
QList processedTracks;
for (int item : sorted_clips) {
int target_track = new_track_ids[item];
if (processedTracks.contains(target_track)) {
// already processed
continue;
}
processedTracks << target_track;
int target_position = min;
int startClipId = m_model->getClipByPosition(target_track, target_position);
if (startClipId > -1) {
// There is a clip, cut
res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo);
}
}
res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo);
}
for (int item : sorted_clips) {
if (m_model->isClip(item)) {
int target_track = new_track_ids[item];
int target_position = old_position[item] + delta_pos;
ok = ok && m_model->requestClipMove(item, target_track, target_position, updateView, finalMove, undo, redo);
} else {
// ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo);
}
if (!ok) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
}
return true;
}
QStringList TimelineController::getThumbKeys()
{
QStringList result;
for (const auto &clp : m_model->m_allClips) {
const QString binId = getClipBinId(clp.first);
std::shared_ptr binClip = pCore->bin()->getBinClip(binId);
result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".png");
result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".png");
}
result.removeDuplicates();
return result;
}
bool TimelineController::isInSelection(int itemId)
{
return m_model->getCurrentSelection().count(itemId) > 0;
}
bool TimelineController::exists(int itemId)
{
return m_model->isClip(itemId) || m_model->isComposition(itemId);
}
void TimelineController::slotMultitrackView(bool enable)
{
TimelineFunctions::enableMultitrackView(m_model, enable);
}
void TimelineController::saveTimelineSelection(const QDir &targetDir)
{
TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir);
}
void TimelineController::addEffectKeyframe(int cid, int frame, double val)
{
if (m_model->isClip(cid)) {
std::shared_ptr destStack = m_model->getClipEffectStackModel(cid);
destStack->addEffectKeyFrame(frame, val);
} else if (m_model->isComposition(cid)) {
std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
listModel->addKeyframe(frame, val);
}
}
void TimelineController::removeEffectKeyframe(int cid, int frame)
{
if (m_model->isClip(cid)) {
std::shared_ptr destStack = m_model->getClipEffectStackModel(cid);
destStack->removeKeyFrame(frame);
} else if (m_model->isComposition(cid)) {
std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps()));
}
}
void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue)
{
if (m_model->isClip(cid)) {
std::shared_ptr destStack = m_model->getClipEffectStackModel(cid);
destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue);
} else if (m_model->isComposition(cid)) {
std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue);
}
}
bool TimelineController::darkBackground() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup());
return scheme.background(KColorScheme::NormalBackground).color().value() < 0.5;
}
QColor TimelineController::videoColor() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup());
return scheme.foreground(KColorScheme::LinkText).color();
}
QColor TimelineController::audioColor() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary);
return scheme.foreground(KColorScheme::ActiveText).color();
}
QColor TimelineController::lockedColor() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup());
return scheme.foreground(KColorScheme::NegativeText).color();
}
QColor TimelineController::groupColor() const
+{
+ KColorScheme scheme(QApplication::palette().currentColorGroup());
+ return scheme.foreground(KColorScheme::ActiveText).color();
+}
+
+QColor TimelineController::selectionColor() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary);
return scheme.foreground(KColorScheme::NeutralText).color();
}
void TimelineController::switchRecording(int trackId)
{
if (!pCore->isMediaCapturing()) {
qDebug() << "start recording" << trackId;
if (!m_model->isTrack(trackId)) {
qDebug() << "ERROR: Starting to capture on invalid track " << trackId;
}
if (m_model->getTrackById_const(trackId)->isLocked()) {
pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500);
return;
}
m_recordStart.first = timelinePosition();
int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first);
if (maximumSpace == INT_MAX) {
m_recordStart.second = 0;
} else {
m_recordStart.second = maximumSpace - m_recordStart.first;
if (m_recordStart.second < 8) {
pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or "
"choose a different track"),
ErrorMessage, 500);
return;
}
}
pCore->monitorManager()->slotSwitchMonitors(false);
pCore->startMediaCapture(true, false);
pCore->monitorManager()->slotPlay();
} else {
QString recordedFile = pCore->stopMediaCapture(true, false);
pCore->monitorManager()->slotPause();
if (recordedFile.isEmpty()) {
return;
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
std::function callBack = [this, trackId](const QString &binId) {
int id = -1;
qDebug() << "callback " << binId << " " << trackId << ", MAXIMUM SPACE: " << m_recordStart.second;
bool res = false;
if (m_recordStart.second > 0) {
// Limited space on track
QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(m_recordStart.second - 1);
res = m_model->requestClipInsertion(binClipId, trackId, m_recordStart.first, id, true, true, false);
} else {
res = m_model->requestClipInsertion(binId, trackId, m_recordStart.first, id, true, true, false);
}
};
QString binId = ClipCreator::createClipFromFile(recordedFile, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel(), undo,
redo, callBack);
if (binId != QStringLiteral("-1")) {
pCore->pushUndo(undo, redo, i18n("Record audio"));
}
}
}
diff --git a/src/timeline2/view/timelinecontroller.h b/src/timeline2/view/timelinecontroller.h
index d8c4d4a29..6b7d699e1 100644
--- a/src/timeline2/view/timelinecontroller.h
+++ b/src/timeline2/view/timelinecontroller.h
@@ -1,518 +1,520 @@
/***************************************************************************
* Copyright (C) 2017 by Jean-Baptiste Mardelle *
* This file is part of Kdenlive. See www.kdenlive.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 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* 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 . *
***************************************************************************/
#ifndef TIMELINECONTROLLER_H
#define TIMELINECONTROLLER_H
#include "definitions.h"
#include "lib/audio/audioCorrelation.h"
#include "timeline2/model/timelineitemmodel.hpp"
#include
#include
class PreviewManager;
class QAction;
class QQuickItem;
// see https://bugreports.qt.io/browse/QTBUG-57714, don't expose a QWidget as a context property
class TimelineController : public QObject
{
Q_OBJECT
/* @brief holds a list of currently selected clips (list of clipId's)
*/
Q_PROPERTY(QList selection READ selection NOTIFY selectionChanged)
/* @brief holds the timeline zoom factor
*/
Q_PROPERTY(double scaleFactor READ scaleFactor WRITE setScaleFactor NOTIFY scaleFactorChanged)
/* @brief holds the current project duration
*/
Q_PROPERTY(int duration READ duration NOTIFY durationChanged)
Q_PROPERTY(int fullDuration READ fullDuration NOTIFY durationChanged)
Q_PROPERTY(bool audioThumbFormat READ audioThumbFormat NOTIFY audioThumbFormatChanged)
/* @brief holds the current timeline position
*/
Q_PROPERTY(int position READ position WRITE setPosition NOTIFY positionChanged)
Q_PROPERTY(int zoneIn READ zoneIn WRITE setZoneIn NOTIFY zoneChanged)
Q_PROPERTY(int zoneOut READ zoneOut WRITE setZoneOut NOTIFY zoneChanged)
Q_PROPERTY(int seekPosition READ seekPosition WRITE setSeekPosition NOTIFY seekPositionChanged)
Q_PROPERTY(bool ripple READ ripple NOTIFY rippleChanged)
Q_PROPERTY(bool scrub READ scrub NOTIFY scrubChanged)
Q_PROPERTY(bool snap READ snap NOTIFY snapChanged)
Q_PROPERTY(bool showThumbnails READ showThumbnails NOTIFY showThumbnailsChanged)
Q_PROPERTY(bool showMarkers READ showMarkers NOTIFY showMarkersChanged)
Q_PROPERTY(bool showAudioThumbnails READ showAudioThumbnails NOTIFY showAudioThumbnailsChanged)
Q_PROPERTY(QVariantList dirtyChunks READ dirtyChunks NOTIFY dirtyChunksChanged)
Q_PROPERTY(QVariantList renderedChunks READ renderedChunks NOTIFY renderedChunksChanged)
Q_PROPERTY(int workingPreview READ workingPreview NOTIFY workingPreviewChanged)
Q_PROPERTY(bool useRuler READ useRuler NOTIFY useRulerChanged)
Q_PROPERTY(int activeTrack READ activeTrack WRITE setActiveTrack NOTIFY activeTrackChanged)
Q_PROPERTY(int audioTarget READ audioTarget WRITE setAudioTarget NOTIFY audioTargetChanged)
Q_PROPERTY(int videoTarget READ videoTarget WRITE setVideoTarget NOTIFY videoTargetChanged)
Q_PROPERTY(QColor videoColor READ videoColor NOTIFY colorsChanged)
Q_PROPERTY(QColor audioColor READ audioColor NOTIFY colorsChanged)
Q_PROPERTY(QColor lockedColor READ lockedColor NOTIFY colorsChanged)
+ Q_PROPERTY(QColor selectionColor READ selectionColor NOTIFY colorsChanged)
Q_PROPERTY(QColor groupColor READ groupColor NOTIFY colorsChanged)
public:
TimelineController(QObject *parent);
~TimelineController() override;
/** @brief Sets the model that this widgets displays */
void setModel(std::shared_ptr model);
std::shared_ptr getModel() const;
void setRoot(QQuickItem *root);
/** @brief Edit an item's in/out points with a dialog
*/
Q_INVOKABLE void editItemDuration(int itemId);
/** @brief Returns the topmost track containing a selected item (-1 if selection is embty) */
Q_INVOKABLE int selectedTrack() const;
/** @brief Select the clip in active track under cursor
@param type is the type of the object (clip or composition)
@param select: true if the object should be selected and false if it should be deselected
@param addToCurrent: if true, the object will be added to the new selection
*/
void selectCurrentItem(ObjectType type, bool select, bool addToCurrent = false);
/** @brief Select all timeline items
*/
void selectAll();
/* @brief Select all items in one track
*/
void selectCurrentTrack();
/** @brief Select multiple objects on the timeline
@param tracks List of ids of tracks from which to select
@param start/endFrame Interval from which to select the items
@param addToSelect if true, the old selection is retained
*/
Q_INVOKABLE void selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect);
/** @brief request a selection with a list of ids*/
Q_INVOKABLE void selectItems(const QList &ids);
/* @brief Returns true is item is selected as well as other items */
Q_INVOKABLE bool isInSelection(int itemId);
/* @brief Show/hide audio record controls on a track
*/
Q_INVOKABLE void switchRecording(int trackId);
/* @brief Open Kdenlive's config diablog on a defined page and tab
*/
Q_INVOKABLE void showConfig(int page, int tab);
/* @brief returns current timeline's zoom factor
*/
Q_INVOKABLE double scaleFactor() const;
/* @brief set current timeline's zoom factor
*/
void setScaleFactorOnMouse(double scale, bool zoomOnMouse);
void setScaleFactor(double scale);
/* @brief Returns the project's duration (tractor)
*/
Q_INVOKABLE int duration() const;
Q_INVOKABLE int fullDuration() const;
/* @brief Returns the current cursor position (frame currently displayed by MLT)
*/
Q_INVOKABLE int position() const { return m_position; }
/* @brief Returns the seek request position (-1 = no seek pending)
*/
Q_INVOKABLE int seekPosition() const { return m_seekPosition; }
Q_INVOKABLE int audioTarget() const;
Q_INVOKABLE int videoTarget() const;
Q_INVOKABLE int activeTrack() const { return m_activeTrack; }
Q_INVOKABLE QColor videoColor() const;
Q_INVOKABLE QColor audioColor() const;
Q_INVOKABLE QColor lockedColor() const;
+ Q_INVOKABLE QColor selectionColor() const;
Q_INVOKABLE QColor groupColor() const;
/* @brief Request a seek operation
@param position is the desired new timeline position
*/
Q_INVOKABLE int zoneIn() const { return m_zone.x(); }
Q_INVOKABLE int zoneOut() const { return m_zone.y(); }
Q_INVOKABLE void setZoneIn(int inPoint);
Q_INVOKABLE void setZoneOut(int outPoint);
void setZone(const QPoint &zone);
/* @brief Request a seek operation
@param position is the desired new timeline position
*/
Q_INVOKABLE void setPosition(int position);
Q_INVOKABLE bool snap();
Q_INVOKABLE bool ripple();
Q_INVOKABLE bool scrub();
Q_INVOKABLE QString timecode(int frames);
/* @brief Request inserting a new clip in timeline (dragged from bin or monitor)
@param tid is the destination track
@param position is the timeline position
@param xml is the data describing the dropped clip
@param logUndo if set to false, no undo object is stored
@return the id of the inserted clip
*/
Q_INVOKABLE int insertClip(int tid, int position, const QString &xml, bool logUndo, bool refreshView, bool useTargets);
/* @brief Request inserting multiple clips into the timeline (dragged from bin or monitor)
* @param tid is the destination track
* @param position is the timeline position
* @param binIds the IDs of the bins being dropped
* @param logUndo if set to false, no undo object is stored
* @return the ids of the inserted clips
*/
Q_INVOKABLE QList insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView);
Q_INVOKABLE void copyItem();
Q_INVOKABLE bool pasteItem();
/* @brief Request inserting a new composition in timeline (dragged from compositions list)
@param tid is the destination track
@param position is the timeline position
@param transitionId is the data describing the dropped composition
@param logUndo if set to false, no undo object is stored
@return the id of the inserted composition
*/
Q_INVOKABLE int insertComposition(int tid, int position, const QString &transitionId, bool logUndo);
/* @brief Request inserting a new composition in timeline (dragged from compositions list)
this function will check if there is a clip at insert point and
adjust the composition length accordingly
@param tid is the destination track
@param position is the timeline position
@param transitionId is the data describing the dropped composition
@param logUndo if set to false, no undo object is stored
@return the id of the inserted composition
*/
Q_INVOKABLE int insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo);
Q_INVOKABLE int insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo);
/* @brief Request deletion of the currently selected clips
*/
Q_INVOKABLE void deleteSelectedClips();
Q_INVOKABLE void triggerAction(const QString &name);
/* @brief Do we want to display video thumbnails
*/
bool showThumbnails() const;
bool showAudioThumbnails() const;
bool showMarkers() const;
bool audioThumbFormat() const;
/* @brief Do we want to display audio thumbnails
*/
Q_INVOKABLE bool showWaveforms() const;
/* @brief Insert a timeline track
*/
Q_INVOKABLE void addTrack(int tid);
/* @brief Remove a timeline track
*/
Q_INVOKABLE void deleteTrack(int tid);
/* @brief Group selected items in timeline
*/
Q_INVOKABLE void groupSelection();
/* @brief Ungroup selected items in timeline
*/
Q_INVOKABLE void unGroupSelection(int cid = -1);
/* @brief Ask for edit marker dialog
*/
Q_INVOKABLE void editMarker(const QString &cid, int frame);
/* @brief Ask for edit timeline guide dialog
*/
Q_INVOKABLE void editGuide(int frame = -1);
Q_INVOKABLE void moveGuide(int frame, int newFrame);
/* @brief Add a timeline guide
*/
Q_INVOKABLE void switchGuide(int frame = -1, bool deleteOnly = false);
/* @brief Request monitor refresh
*/
Q_INVOKABLE void requestRefresh();
/* @brief Show the asset of the given item in the AssetPanel
If the id corresponds to a clip, we show the corresponding effect stack
If the id corresponds to a composition, we show its properties
*/
Q_INVOKABLE void showAsset(int id);
Q_INVOKABLE void showTrackAsset(int trackId);
Q_INVOKABLE bool exists(int itemId);
Q_INVOKABLE int headerWidth() const;
Q_INVOKABLE void setHeaderWidth(int width);
/* @brief Seek to next snap point
*/
void gotoNextSnap();
/* @brief Seek to previous snap point
*/
void gotoPreviousSnap();
/* @brief Set current item's start point to cursor position
*/
void setInPoint();
/* @brief Set current item's end point to cursor position
*/
void setOutPoint();
/* @brief Return the project's tractor
*/
Mlt::Tractor *tractor();
/* @brief Get the list of currently selected clip id's
*/
QList selection() const;
/* @brief Add an asset (effect, composition)
*/
void addAsset(const QVariantMap &data);
/* @brief Cuts the clip on current track at timeline position
*/
Q_INVOKABLE void cutClipUnderCursor(int position = -1, int track = -1);
/* @brief Request a spacer operation
*/
Q_INVOKABLE int requestSpacerStartOperation(int trackId, int position);
/* @brief Request a spacer operation
*/
Q_INVOKABLE bool requestSpacerEndOperation(int clipId, int startPosition, int endPosition);
/* @brief Request a Fade in effect for clip
*/
Q_INVOKABLE void adjustFade(int cid, const QString &effectId, int duration, int initialDuration);
Q_INVOKABLE const QString getTrackNameFromMltIndex(int trackPos);
/* @brief Request inserting space in a track
*/
Q_INVOKABLE void insertSpace(int trackId = -1, int frame = -1);
Q_INVOKABLE void removeSpace(int trackId = -1, int frame = -1, bool affectAllTracks = false);
/* @brief If clip is enabled, disable, otherwise enable
*/
Q_INVOKABLE void switchEnableState(int clipId);
Q_INVOKABLE void addCompositionToClip(const QString &assetId, int clipId, int offset);
Q_INVOKABLE void addEffectToClip(const QString &assetId, int clipId);
Q_INVOKABLE void requestClipCut(int clipId, int position);
Q_INVOKABLE void extract(int clipId);
Q_INVOKABLE void splitAudio(int clipId);
Q_INVOKABLE void splitVideo(int clipId);
Q_INVOKABLE void setAudioRef(int clipId);
Q_INVOKABLE void alignAudio(int clipId);
Q_INVOKABLE bool endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline);
Q_INVOKABLE int getItemMovingTrack(int itemId) const;
bool endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo);
bool endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo);
bool splitAV();
/* @brief Seeks to selected clip start / end
*/
Q_INVOKABLE void pasteEffects(int targetId = -1);
Q_INVOKABLE double fps() const;
Q_INVOKABLE void addEffectKeyframe(int cid, int frame, double val);
Q_INVOKABLE void removeEffectKeyframe(int cid, int frame);
Q_INVOKABLE void updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue = QVariant());
Q_INVOKABLE void switchTrackActive(int trackId = -1);
void switchTrackLock(bool applyToAll = false);
void switchTargetTrack();
const QString getTrackNameFromIndex(int trackIndex);
/* @brief Seeks to selected clip start / end
*/
void seekCurrentClip(bool seekToEnd = false);
/* @brief Seeks to a clip start (or end) based on it's clip id
*/
void seekToClip(int cid, bool seekToEnd);
/* @brief Returns the number of tracks (audioTrakcs, videoTracks)
*/
QPoint getTracksCount() const;
/* @brief Request monitor refresh if item (clip or composition) is under timeline cursor
*/
void refreshItem(int id);
/* @brief Seek timeline to mouse position
*/
void seekToMouse();
/* @brief Returns a list of all luma files used in the project
*/
QStringList extractCompositionLumas() const;
/* @brief Get the frame where mouse is positioned
*/
int getMousePos();
/* @brief Get the frame where mouse is positioned
*/
int getMouseTrack();
/* @brief Returns a map of track ids/track names
*/
QMap getTrackNames(bool videoOnly);
/* @brief Returns the transition a track index for a composition (MLT index / Track id)
*/
QPair getCompositionATrack(int cid) const;
void setCompositionATrack(int cid, int aTrack);
/* @brief Return true if composition's a_track is automatic (no forced track)
*/
bool compositionAutoTrack(int cid) const;
const QString getClipBinId(int clipId) const;
void focusItem(int itemId);
/* @brief Create and display a split clip view to compare effect
*/
bool createSplitOverlay(Mlt::Filter *filter);
/* @brief Delete the split clip view to compare effect
*/
void removeSplitOverlay();
/* @brief Add current timeline zone to preview rendering
*/
void addPreviewRange(bool add);
/* @brief Clear current timeline zone from preview rendering
*/
void clearPreviewRange();
void startPreviewRender();
void stopPreviewRender();
QVariantList dirtyChunks() const;
QVariantList renderedChunks() const;
/* @brief returns the frame currently processed by timeline preview, -1 if none
*/
int workingPreview() const;
/** @brief Return true if we want to use timeline ruler zone for editing */
bool useRuler() const;
/* @brief Load timeline preview from saved doc
*/
void loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable);
/* @brief Return document properties with added settings from timeline
*/
QMap documentProperties();
/** @brief Change track compsiting mode */
void switchCompositing(int mode);
/** @brief Change a clip item's speed in timeline */
Q_INVOKABLE void changeItemSpeed(int clipId, double speed);
/** @brief Delete selected zone and fill gap by moving following clips
* @param lift if true, the zone will simply be deleted but clips won't be moved
*/
void extractZone(QPoint zone, bool liftOnly = false);
/** @brief Insert clip monitor into timeline
* @returns the zone end position or -1 on fail
*/
int insertZone(const QString &binId, QPoint zone, bool overwrite);
void updateClip(int clipId, const QVector &roles);
void showClipKeyframes(int clipId, bool value);
void showCompositionKeyframes(int clipId, bool value);
/** @brief Returns last usable timeline position (seek request or current pos) */
int timelinePosition() const;
/** @brief Adjust all timeline tracks height */
void resetTrackHeight();
/** @brief timeline preview params changed, reset */
void resetPreview();
/** @brief Set target tracks (video, audio) */
void setTargetTracks(QPair targets);
/** @brief Return asset's display name from it's id (effect or composition) */
Q_INVOKABLE const QString getAssetName(const QString &assetId, bool isTransition);
/** @brief Set keyboard grabbing on current selection */
void grabCurrent();
/** @brief Returns keys for all used thumbnails */
QStringList getThumbKeys();
public slots:
void resetView();
Q_INVOKABLE void setSeekPosition(int position);
Q_INVOKABLE void setAudioTarget(int track);
Q_INVOKABLE void setVideoTarget(int track);
Q_INVOKABLE void setActiveTrack(int track);
void onSeeked(int position);
void addEffectToCurrentClip(const QStringList &effectData);
/** @brief Dis / enable timeline preview. */
void disablePreview(bool disable);
void invalidateItem(int cid);
void invalidateZone(int in, int out);
void checkDuration();
/** @brief Dis / enable multi track view. */
void slotMultitrackView(bool enable);
/** @brief Save timeline selected clips to target folder. */
void saveTimelineSelection(const QDir &targetDir);
/** @brief Restore timeline scroll pos on open. */
void setScrollPos(int pos);
private slots:
void updateClipActions();
public:
/** @brief a list of actions that have to be enabled/disabled depending on the timeline selection */
QList clipActions;
private:
QQuickItem *m_root;
KActionCollection *m_actionCollection;
std::shared_ptr m_model;
bool m_usePreview;
int m_position;
int m_seekPosition;
int m_audioTarget;
int m_videoTarget;
int m_activeTrack;
int m_audioRef;
QPair m_recordStart;
QPoint m_zone;
double m_scale;
static int m_duration;
PreviewManager *m_timelinePreview;
QAction *m_disablePreview;
std::shared_ptr m_audioCorrelator;
QMutex m_metaMutex;
int getCurrentItem();
void initializePreview();
bool darkBackground() const;
signals:
void selected(Mlt::Producer *producer);
void selectionChanged();
void frameFormatChanged();
void trackHeightChanged();
void scaleFactorChanged();
void audioThumbFormatChanged();
void durationChanged();
void positionChanged();
void seekPositionChanged();
void audioTargetChanged();
void videoTargetChanged();
void activeTrackChanged();
void colorsChanged();
void showThumbnailsChanged();
void showAudioThumbnailsChanged();
void showMarkersChanged();
void rippleChanged();
void scrubChanged();
void seeked(int position);
void zoneChanged();
void zoneMoved(const QPoint &zone);
/* @brief Requests that a given parameter model is displayed in the asset panel */
void showTransitionModel(int tid, std::shared_ptr);
void showItemEffectStack(const QString &clipName, std::shared_ptr, QSize frameSize, bool showKeyframes);
/* @brief notify of chunks change
*/
void dirtyChunksChanged();
void renderedChunksChanged();
void workingPreviewChanged();
void useRulerChanged();
void updateZoom(double);
/* @brief emitted when timeline selection changes, true if a clip is selected
*/
void timelineClipSelected(bool);
/* @brief User enabled / disabled snapping, update timeline behavior
*/
void snapChanged();
Q_INVOKABLE void ungrabHack();
};
#endif