diff --git a/krita/krita.action b/krita/krita.action
index 3b926bf35d..701eca9471 100644
--- a/krita/krita.action
+++ b/krita/krita.action
@@ -1,3457 +1,3501 @@
General
Open Resources Folder
Opens a file browser at the location Krita saves resources such as brushes to.
Opens a file browser at the location Krita saves resources such as brushes to.
Open Resources Folder
0
0
false
Cleanup removed files...
Cleanup removed files
Cleanup removed files
0
0
false
C&ascade
Cascade
Cascade
10
0
false
&Tile
Tile
Tile
10
0
false
Create Resource Bundle...
Create Resource Bundle
Create Resource Bundle
0
0
false
Show File Toolbar
Show File Toolbar
Show File Toolbar
false
Show color selector
Show color selector
Show color selector
Shift+I
false
Show MyPaint shade selector
Show MyPaint shade selector
Show MyPaint shade selector
Shift+M
false
Show minimal shade selector
Show minimal shade selector
Show minimal shade selector
Shift+N
false
Show color history
Show color history
Show color history
H
false
Show common colors
Show common colors
Show common colors
U
false
Show Tool Options
Show Tool Options
Show Tool Options
\
false
Show Brush Editor
Show Brush Editor
Show Brush Editor
F5
false
Show Brush Presets
Show Brush Presets
Show Brush Presets
F6
false
Toggle Tablet Debugger
Toggle Tablet Debugger
Toggle Tablet Debugger
0
0
Ctrl+Shift+T
false
Show system information for bug reports.
Show system information for bug reports.
Show system information for bug reports.
false
Rename Composition...
Rename Composition
Rename Composition
0
0
false
Update Composition
Update Composition
Update Composition
0
0
false
Use multiple of 2 for pixel scale
Use multiple of 2 for pixel scale
Use multiple of 2 for pixel scale
Use multiple of 2 for pixel scale
1
0
true
Painting
lightness-increase
Make brush color lighter
Make brush color lighter
Make brush color lighter
0
0
L
false
lightness-decrease
Make brush color darker
Make brush color darker
Make brush color darker
0
0
K
false
Make brush color more saturated
Make brush color more saturated
Make brush color more saturated
false
Make brush color more desaturated
Make brush color more desaturated
Make brush color more desaturated
false
Shift brush color hue clockwise
Shift brush color hue clockwise
Shift brush color hue clockwise
false
Shift brush color hue counter-clockwise
Shift brush color hue counter-clockwise
Shift brush color hue counter-clockwise
false
Make brush color more red
Make brush color more red
Make brush color more red
false
Make brush color more green
Make brush color more green
Make brush color more green
false
Make brush color more blue
Make brush color more blue
Make brush color more blue
false
Make brush color more yellow
Make brush color more yellow
Make brush color more yellow
false
opacity-increase
Increase opacity
Increase opacity
Increase opacity
0
0
O
false
opacity-decrease
Decrease opacity
Decrease opacity
Decrease opacity
0
0
I
false
draw-eraser
Set eraser mode
Set eraser mode
Set eraser mode
10000
0
E
true
view-refresh
Reload Original Preset
Reload Original Preset
Reload Original Preset
10000
false
transparency-unlocked
Preserve Alpha
Preserve Alpha
Preserve Alpha
10000
true
transform_icons_penPressure
Use Pen Pressure
Use Pen Pressure
Use Pen Pressure
10000
true
symmetry-horizontal
Horizontal Mirror Tool
Horizontal Mirror Tool
Horizontal Mirror Tool
0
true
symmetry-vertical
Vertical Mirror Tool
Vertical Mirror Tool
Vertical Mirror Tool
0
true
Hide Mirror X Line
Hide Mirror X Line
Hide Mirror X Line
10000
true
Hide Mirror Y Line
Hide Mirror Y Line
Hide Mirror Y Line
10000
true
Lock
Lock X Line
Lock X Line
10000
true
Lock Y Line
Lock Y Line
Lock Y Line
10000
true
Move to Canvas Center
Move to Canvas Center X
Move to Canvas Center X
10000
false
Move to Canvas Center Y
Move to Canvas Center Y
Move to Canvas Center Y
10000
false
&Invert Selection
Invert current selection
Invert Selection
10000000000
100
Ctrl+Shift+I
false
&Toggle Selection Display Mode
Toggle Selection Display Mode
Toggle Selection Display Mode
0
0
false
Next Favourite Preset
Next Favourite Preset
Next Favourite Preset
,
false
Previous Favourite Preset
Previous Favourite Preset
Previous Favourite Preset
.
false
preset-switcher
Switch to Previous Preset
Switch to Previous Preset
Switch to Previous Preset
/
false
Hide Brushes and Stuff Toolbar
Hide Brushes and Stuff Toolbar
Hide Brushes and Stuff Toolbar
true
Reset Foreground and Background Color
Reset Foreground and Background Color
Reset Foreground and Background Color
D
false
Swap Foreground and Background Color
Swap Foreground and Background Color
Swap Foreground and Background Color
X
false
smoothing-weighted
Brush Smoothing: Weighted
Brush Smoothing: Weighted
Brush Smoothing: Weighted
false
smoothing-no
Brush Smoothing: Disabled
Brush Smoothing: Disabled
Brush Smoothing: Disabled
false
smoothing-stabilizer
Brush Smoothing: Stabilizer
Brush Smoothing: Stabilizer
Brush Smoothing: Stabilizer
false
brushsize-decrease
Decrease Brush Size
Decrease Brush Size
Decrease Brush Size
0
0
[
false
smoothing-basic
Brush Smoothing: Basic
Brush Smoothing: Basic
Brush Smoothing: Basic
false
brushsize-increase
Increase Brush Size
Increase Brush Size
Increase Brush Size
0
0
]
false
Toggle Assistant
Toggle Assistant
ToggleAssistant
Ctrl+Shift+L
true
Undo Polygon Selection Points
Undo Polygon Selection Points
Undo Polygon Selection Points
Shift+Z
false
Fill with Foreground Color (Opacity)
Fill with Foreground Color (Opacity)
Fill with Foreground Color (Opacity)
10000
1
Ctrl+Shift+Backspace
false
Fill with Background Color (Opacity)
Fill with Background Color (Opacity)
Fill with Background Color (Opacity)
10000
1
Ctrl+Backspace
false
Fill with Pattern (Opacity)
Fill with Pattern (Opacity)
Fill with Pattern (Opacity)
10000
1
false
Convert &to Shape
Convert to Shape
Convert to Shape
10000000000
0
false
&Select Opaque
Select Opaque
Select Opaque
100000
100
false
&Show Global Selection Mask
Shows global selection as a usual selection mask in <interface>Layers</interface> docker
Show Global Selection Mask
100000
100
true
Filters
color-to-alpha
&Color to Alpha...
Color to Alpha
Color to Alpha
10000
0
false
&Top Edge Detection
Top Edge Detection
Top Edge Detection
10000
0
false
&Index Colors...
Index Colors
Index Colors
10000
0
false
Emboss Horizontal &Only
Emboss Horizontal Only
Emboss Horizontal Only
10000
0
false
D&odge
Dodge
Dodge
10000
0
false
&Sharpen
Sharpen
Sharpen
10000
0
false
B&urn
Burn
Burn
10000
0
false
&Mean Removal
Mean Removal
Mean Removal
10000
0
false
&Gaussian Blur...
Gaussian Blur
Gaussian Blur
10000
0
false
Emboss &in All Directions
Emboss in All Directions
Emboss in All Directions
10000
0
false
&Small Tiles...
Small Tiles
Small Tiles
10000
0
false
&Levels...
Levels
Levels
10000
0
Ctrl+L
false
&Sobel...
Sobel
Sobel
10000
0
false
&Wave...
Wave
Wave
10000
0
false
&Motion Blur...
Motion Blur
Motion Blur
10000
0
false
&Color Adjustment curves...
Color Adjustment curves
Color Adjustment curves
10000
0
Ctrl+M
false
Pi&xelize...
Pixelize
Pixelize
10000
0
false
Emboss (&Laplacian)
Emboss (Laplacian)
Emboss (Laplacian)
10000
0
false
&Left Edge Detection
Left Edge Detection
Left Edge Detection
10000
0
false
&Blur...
Blur
Blur
10000
0
false
&Raindrops...
Raindrops
Raindrops
10000
0
false
&Bottom Edge Detection
Bottom Edge Detection
Bottom Edge Detection
10000
0
false
&Random Noise...
Random Noise
Random Noise
10000
0
false
&Brightness/Contrast curve...
Brightness/Contrast curve
Brightness/Contrast curve
10000
0
false
Colo&r Balance..
Color Balance..
Color Balance..
10000
0
Ctrl+B
false
&Phong Bumpmap...
Phong Bumpmap
Phong Bumpmap
10000
0
false
&Desaturate
Desaturate
Desaturate
10000
0
Ctrl+Shift+U
false
Color &Transfer...
Color Transfer
Color Transfer
10000
0
false
Emboss &Vertical Only
Emboss Vertical Only
Emboss Vertical Only
10000
0
false
&Lens Blur...
Lens Blur
Lens Blur
10000
0
false
M&inimize Channel
Minimize Channel
Minimize Channel
10000
0
false
M&aximize Channel
Maximize Channel
Maximize Channel
10000
0
false
&Oilpaint...
Oilpaint
Oilpaint
10000
0
false
&Right Edge Detection
Right Edge Detection
Right Edge Detection
10000
0
false
&Auto Contrast
Auto Contrast
Auto Contrast
10000
0
false
&Round Corners...
Round Corners
Round Corners
10000
0
false
&Unsharp Mask...
Unsharp Mask
Unsharp Mask
10000
0
false
&Emboss with Variable Depth...
Emboss with Variable Depth
Emboss with Variable Depth
10000
0
false
Emboss &Horizontal && Vertical
Emboss Horizontal & Vertical
Emboss Horizontal & Vertical
10000
0
false
Random &Pick...
Random Pick
Random Pick
10000
0
false
&Gaussian Noise Reduction...
Gaussian Noise Reduction
Gaussian Noise Reduction
10000
0
false
&Posterize...
Posterize
Posterize
10000
0
false
&Wavelet Noise Reducer...
Wavelet Noise Reducer
Wavelet Noise Reducer
10000
0
false
&HSV Adjustment...
HSV Adjustment
HSV Adjustment
10000
0
Ctrl+U
false
Tool Shortcuts
Dynamic Brush Tool
Dynamic Brush Tool
Dynamic Brush Tool
false
Crop Tool
Crop the image to an area
Crop the image to an area
C
false
Polygon Tool
Polygon Tool. Shift-mouseclick ends the polygon.
Polygon Tool. Shift-mouseclick ends the polygon.
false
Rectangle Tool
Rectangle Tool
Rectangle Tool
false
Multibrush Tool
Multibrush Tool
Multibrush Tool
Q
false
Lazy Brush Tool
Lazy Brush Tool
Lazy Brush Tool
Smart Patch Tool
Smart Patch Tool
Smart Patch Tool
Pan Tool
Pan Tool
Pan Tool
Select Shapes Tool
Select Shapes Tool
Select Shapes Tool
false
Color Picker
Select a color from the image or current layer
Select a color from the image or current layer
P
false
Text Editing Tool
Text editing
Text editing
false
Outline Selection Tool
Outline Selection Tool
Outline Selection Tool
false
Artistic Text Tool
Artistic text editing
Artistic text editing
false
Bezier Curve Selection Tool
Select a
Bezier Curve Selection Tool
false
Similar Color Selection Tool
Select a
Similar Color Selection Tool
false
Fill Tool
Fill a contiguous area of color with a color, or fill a selection.
Fill a contiguous area of color with a color, or fill a selection.
F
false
Line Tool
Line Tool
Line Tool
false
Freehand Path Tool
Freehand Path Tool
Freehand Path Tool
false
Bezier Curve Tool
Bezier Curve Tool. Shift-mouseclick ends the curve.
Bezier Curve Tool. Shift-mouseclick ends the curve.
false
Ellipse Tool
Ellipse Tool
Ellipse Tool
false
Freehand Brush Tool
Freehand Brush Tool
Freehand Brush Tool
B
false
Create object
Create object
Create object
false
Elliptical Selection Tool
Elliptical Selection Tool
Elliptical Selection Tool
J
false
Contiguous Selection Tool
Contiguous Selection Tool
Contiguous Selection Tool
false
Pattern editing
Pattern editing
Pattern editing
false
Review
Review
Review
false
Draw a gradient.
Draw a gradient.
Draw a gradient.
G
false
Polygonal Selection Tool
Polygonal Selection Tool
Polygonal Selection Tool
false
Measurement Tool
Measure the distance between two points
Measure the distance between two points
false
Rectangular Selection Tool
Rectangular Selection Tool
Rectangular Selection Tool
Ctrl+R
false
Move Tool
Move a layer
Move a layer
T
false
Vector Image Tool
Vector Image (EMF/WMF/SVM/SVG) tool
Vector Image (EMF/WMF/SVM/SVG) tool
false
Calligraphy
Calligraphy
Calligraphy
false
Path editing
Path editing
Path editing
false
Zoom Tool
Zoom Tool
Zoom Tool
false
Polyline Tool
Polyline Tool. Shift-mouseclick ends the polyline.
Polyline Tool. Shift-mouseclick ends the polyline.
false
Transform Tool
Transform a layer or a selection
Transform a layer or a selection
Ctrl+T
false
Assistant Tool
Assistant Tool
Assistant Tool
false
Text tool
Text tool
Text tool
false
Gradient Editing Tool
Gradient editing
Gradient editing
false
Blending Modes
Select Normal Blending Mode
Select Normal Blending Mode
Select Normal Blending Mode
0
0
Alt+Shift+N
false
Select Dissolve Blending Mode
Select Dissolve Blending Mode
Select Dissolve Blending Mode
0
0
Alt+Shift+I
false
Select Behind Blending Mode
Select Behind Blending Mode
Select Behind Blending Mode
0
0
Alt+Shift+Q
false
Select Clear Blending Mode
Select Clear Blending Mode
Select Clear Blending Mode
0
0
Alt+Shift+R
false
Select Darken Blending Mode
Select Darken Blending Mode
Select Darken Blending Mode
0
0
Alt+Shift+K
false
Select Multiply Blending Mode
Select Multiply Blending Mode
Select Multiply Blending Mode
0
0
Alt+Shift+M
false
Select Color Burn Blending Mode
Select Color Burn Blending Mode
Select Color Burn Blending Mode
0
0
Alt+Shift+B
false
Select Linear Burn Blending Mode
Select Linear Burn Blending Mode
Select Linear Burn Blending Mode
0
0
Alt+Shift+A
false
Select Lighten Blending Mode
Select Lighten Blending Mode
Select Lighten Blending Mode
0
0
Alt+Shift+G
false
Select Screen Blending Mode
Select Screen Blending Mode
Select Screen Blending Mode
0
0
Alt+Shift+S
false
Select Color Dodge Blending Mode
Select Color Dodge Blending Mode
Select Color Dodge Blending Mode
0
0
Alt+Shift+D
false
Select Linear Dodge Blending Mode
Select Linear Dodge Blending Mode
Select Linear Dodge Blending Mode
0
0
Alt+Shift+W
false
Select Overlay Blending Mode
Select Overlay Blending Mode
Select Overlay Blending Mode
0
0
Alt+Shift+O
false
Select Hard Overlay Blending Mode
Select Hard Overlay Blending Mode
Select Hard Overlay Blending Mode
0
0
Alt+Shift+P
false
Select Soft Light Blending Mode
Select Soft Light Blending Mode
Select Soft Light Blending Mode
0
0
Alt+Shift+F
false
Select Hard Light Blending Mode
Select Hard Light Blending Mode
Select Hard Light Blending Mode
0
0
Alt+Shift+H
false
Select Vivid Light Blending Mode
Select Vivid Light Blending Mode
Select Vivid Light Blending Mode
0
0
Alt+Shift+V
false
Select Linear Light Blending Mode
Select Linear Light Blending Mode
Select Linear Light Blending Mode
0
0
Alt+Shift+J
false
Select Pin Light Blending Mode
Select Pin Light Blending Mode
Select Pin Light Blending Mode
0
0
Alt+Shift+Z
false
Select Hard Mix Blending Mode
Select Hard Mix Blending Mode
Select Hard Mix Blending Mode
0
0
Alt+Shift+L
false
Select Difference Blending Mode
Select Difference Blending Mode
Select Difference Blending Mode
0
0
Alt+Shift+E
false
Select Exclusion Blending Mode
Select Exclusion Blending Mode
Select Exclusion Blending Mode
0
0
Alt+Shift+X
false
Select Hue Blending Mode
Select Hue Blending Mode
Select Hue Blending Mode
0
0
Alt+Shift+U
false
Select Saturation Blending Mode
Select Saturation Blending Mode
Select Saturation Blending Mode
0
0
Alt+Shift+T
false
Select Color Blending Mode
Select Color Blending Mode
Select Color Blending Mode
0
0
Alt+Shift+C
false
Select Luminosity Blending Mode
Select Luminosity Blending Mode
Select Luminosity Blending Mode
0
0
Alt+Shift+Y
false
Animation
Previous frame
Move to previous frame
Move to previous frame
1
0
false
Next frame
Move to next frame
Move to next frame
1
0
false
Play / pause animation
Play / pause animation
Play / pause animation
1
0
false
addblankframe
Create Blank Frame
Add blank frame
Add blank frame
100000
0
false
addduplicateframe
Create Duplicate Frame
Add duplicate frame
Add duplicate frame
100000
0
false
Toggle onion skin
Toggle onion skin
Toggle onion skin
100000
0
false
Previous Keyframe
false
Next Keyframe
false
First Frame
false
Last Frame
false
Auto Frame Mode
true
true
Show in Timeline
true
Insert Keyframe Right
Insert keyframes to the right of selection moving the tail of animation to the right
100000
0
false
Insert Keyframe Left
Insert keyframes to the left of selection moving the tail of animation to the right
100000
0
false
Insert N Keyframes Right
Insert several keyframes to the right of selection moving the tail of animation to the right
100000
0
false
Insert N Keyframes Left
Insert several keyframes to the left of selection moving the tail of animation to the right
100000
0
false
Remove Frame and Pull
Remove keyframes moving the tail of animation to the left
100000
0
false
deletekeyframe
Remove Keyframe
Remove keyframes without moving anything around
100000
0
false
Insert Column Right
Insert column to the right of selection moving the tail of animation to the right
100000
0
false
Insert Column Left
Insert column to the left of selection moving the tail of animation to the right
100000
0
false
Insert N Columns Right
Insert several columns to the right of selection moving the tail of animation to the right
100000
0
false
Insert N Columns Left
Insert several columns to the left of selection moving the tail of animation to the right
100000
0
false
Remove Column and Pull
Remove columns moving the tail of animation to the left
100000
0
false
Remove Column
Remove columns without moving anything around
100000
0
false
Insert Hold Frame
Insert a hold frame after every keyframe
100000
0
false
Insert N Hold Frames
Insert N hold frames after every keyframe
100000
0
false
Remove Hold Frame
Remove a hold frame after every keyframe
100000
0
false
Remove N Hold Frames
Remove N hold frames after every keyframe
100000
0
false
Insert Hold Column
Insert a hold column into the frame at the current position
100000
0
false
Insert N Hold Columns
Insert N hold columns into the frame at the current position
100000
0
false
Remove Hold Column
Remove a hold column from the frame at the current position
100000
0
false
Remove N Hold Columns
Remove N hold columns from the frame at the current position
100000
0
false
Mirror Frames
Mirror frames' position
100000
0
false
Mirror Columns
Mirror columns' position
100000
0
false
Copy to Clipboard
Copy frames to clipboard
100000
0
false
Cut to Clipboard
Cut frames to clipboard
100000
0
false
Paste from Clipboard
Paste frames from clipboard
100000
0
false
Copy Columns to Clipboard
Copy columns to clipboard
100000
0
false
Cut Columns to Clipboard
Cut columns to clipboard
100000
0
false
Paste Columns from Clipboard
Paste columns from clipboard
100000
0
false
+
+
+
+
+ Set Start Time
+
+
+
+ 100000
+ 0
+
+ false
+
+
+
+
+
+
+ Set End Time
+
+
+
+ 100000
+ 0
+
+ false
+
+
+
+
+
+
+ Update Playback Range
+
+
+
+ 100000
+ 0
+
+ false
+
+
+
+
Layers
Activate next layer
Activate next layer
Activate next layer
1000
0
PgUp
false
Activate previous layer
Activate previous layer
Activate previous layer
1000
0
PgDown
false
Activate previously selected layer
Activate previously selected layer
Activate previously selected layer
1000
0
;
false
groupLayer
&Group Layer
Group Layer
Group Layer
1000
0
false
cloneLayer
&Clone Layer
Clone Layer
Clone Layer
1000
0
false
vectorLayer
&Vector Layer
Vector Layer
Vector Layer
1000
0
false
filterLayer
&Filter Layer...
Filter Layer
Filter Layer
1000
0
false
fillLayer
&Fill Layer...
Fill Layer
Fill Layer
1000
0
false
fileLayer
&File Layer...
File Layer
File Layer
1000
0
false
transparencyMask
&Transparency Mask
Transparency Mask
Transparency Mask
100000
0
false
filterMask
&Filter Mask...
Filter Mask
Filter Mask
100000
0
false
filterMask
&Colorize Mask
Colorize Mask
Colorize Mask
100000
0
false
transformMask
&Transform Mask...
Transform Mask
Transform Mask
100000
0
false
selectionMask
&Local Selection
Local Selection
Local Selection
100000
0
false
view-filter
&Isolate Layer
Isolate Layer
Isolate Layer
1000
0
true
layer-locked
&Toggle layer lock
Toggle layer lock
Toggle layer lock
1000
0
false
visible
Toggle layer &visibility
Toggle layer visibility
Toggle layer visibility
1000
0
false
transparency-locked
Toggle layer &alpha
Toggle layer alpha
Toggle layer alpha
1000
0
false
transparency-enabled
Toggle layer alpha &inheritance
Toggle layer alpha inheritance
Toggle layer alpha inheritance
1000
0
false
paintLayer
&Paint Layer
Paint Layer
Paint Layer
1000
0
Insert
false
&New Layer From Visible
New layer from visible
New layer from visible
1000
0
false
duplicatelayer
&Duplicate Layer or Mask
Duplicate Layer or Mask
Duplicate Layer or Mask
1000
0
Ctrl+J
false
&Cut Selection to New Layer
Cut Selection to New Layer
Cut Selection to New Layer
100000000
1
Ctrl+Shift+J
false
Copy &Selection to New Layer
Copy Selection to New Layer
Copy Selection to New Layer
100000000
0
Ctrl+Alt+J
false
Copy Layer
Copy layer to clipboard
Copy layer to clipboard
1000
0
false
Cut Layer
Cut layer to clipboard
Cut layer to clipboard
1000
0
false
Paste Layer
Paste layer from clipboard
Paste layer from clipboard
1000
0
false
Quick Group
Create a group layer containing selected layers
Quick Group
100000
0
Ctrl+G
false
Quick Ungroup
Remove grouping of the layers or remove one layer out of the group
Quick Ungroup
100000
0
Ctrl+Alt+G
false
Quick Clipping Group
Group selected layers and add a layer with clipped alpha channel
Quick Clipping Group
100000
0
Ctrl+Shift+G
false
All Layers
Select all layers
Select all layers
10000
0
false
Visible Layers
Select all visible layers
Select all visible layers
10000
0
false
Locked Layers
Select all locked layers
Select all locked layers
10000
0
false
Invisible Layers
Select all invisible layers
Select all invisible layers
10000
0
false
Unlocked Layers
Select all unlocked layers
Select all unlocked layers
10000
0
false
document-save
&Save Layer/Mask...
Save Layer/Mask
Save Layer/Mask
1000
0
false
document-save
Save Vector Layer as SVG...
Save Vector Layer as SVG
Save Vector Layer as SVG
1000
0
false
document-save
Save &Group Layers...
Save Group Layers
Save Group Layers
100000
0
false
Convert group to &animated layer
Convert child layers into animation frames
Convert child layers into animation frames
100000
0
false
fileLayer
to &File Layer
Saves out the layers into a new image and then references that image.
Convert to File Layer
100000
0
false
I&mport Layer...
Import Layer
Import Layer
100000
0
false
paintLayer
&as Paint Layer...
as Paint Layer
as Paint Layer
1000
0
false
transparencyMask
as &Transparency Mask...
as Transparency Mask
as Transparency Mask
1000
0
false
filterMask
as &Filter Mask...
as Filter Mask
as Filter Mask
1000
0
false
selectionMask
as &Selection Mask...
as Selection Mask
as Selection Mask
1000
0
false
paintLayer
to &Paint Layer
to Paint Layer
to Paint Layer
1000
0
false
transparencyMask
to &Transparency Mask
to Transparency Mask
to Transparency Mask
1000
0
false
filterMask
to &Filter Mask...
to Filter Mask
to Filter Mask
1000
0
false
selectionMask
to &Selection Mask
to Selection Mask
to Selection Mask
1000
0
false
transparencyMask
&Alpha into Mask
Alpha into Mask
Alpha into Mask
100000
10
false
transparency-enabled
&Write as Alpha
Write as Alpha
Write as Alpha
1000000
1
false
document-save
&Save Merged...
Save Merged
Save Merged
1000000
0
false
split-layer
Split Layer...
Split Layer
Split Layer
1000
0
false
Wavelet Decompose ...
Wavelet Decompose
Wavelet Decompose
1000
1
false
symmetry-horizontal
Mirror Layer Hori&zontally
Mirror Layer Horizontally
Mirror Layer Horizontally
1000
1
false
symmetry-vertical
Mirror Layer &Vertically
Mirror Layer Vertically
Mirror Layer Vertically
1000
1
false
&Rotate Layer...
Rotate Layer
Rotate Layer
1000
1
false
object-rotate-right
Rotate &Layer 90° to the Right
Rotate Layer 90° to the Right
Rotate Layer 90° to the Right
1000
1
false
object-rotate-left
Rotate Layer &90° to the Left
Rotate Layer 90° to the Left
Rotate Layer 90° to the Left
1000
1
false
Rotate Layer &180°
Rotate Layer 180°
Rotate Layer 180°
1000
1
false
Scale &Layer to new Size...
Scale Layer to new Size
Scale Layer to new Size
100000
1
false
&Shear Layer...
Shear Layer
Shear Layer
1000
1
false
&Offset Layer...
Offset Layer
Offset Layer
100000
1
false
Clones &Array...
Clones Array
Clones Array
100000
0
false
&Edit metadata...
Edit metadata
Edit metadata
100000
1
false
&Histogram...
Histogram
Histogram
100000
0
false
&Convert Layer Color Space...
Convert Layer Color Space
Convert Layer Color Space
100000
1
false
merge-layer-below
&Merge with Layer Below
Merge with Layer Below
Merge with Layer Below
100000
0
Ctrl+E
false
&Flatten Layer
Flatten Layer
Flatten Layer
100000
0
false
Ras&terize Layer
Rasterize Layer
Rasterize Layer
10000000
1
false
Flatten ima&ge
Flatten image
Flatten image
100000
0
Ctrl+Shift+E
false
La&yer Style...
Layer Style
Layer Style
100000
1
false
Move into previous group
Move into previous group
Move into previous group
0
0
false
Move into next group
Move into next group
Move into next group
0
0
false
Rename current layer
Rename current layer
Rename current layer
100000
0
F2
false
deletelayer
&Remove Layer
Remove Layer
Remove Layer
1000
1
Shift+Delete
false
arrowupblr
Move Layer or Mask Up
Move Layer or Mask Up
Ctrl+PgUp
false
arrowdown
Move Layer or Mask Down
Move Layer or Mask Down
Ctrl+PgDown
false
properties
&Properties...
Properties
Properties
1000
1
F3
false
diff --git a/libs/image/kis_image_animation_interface.cpp b/libs/image/kis_image_animation_interface.cpp
index c918d2a9c2..fa3dfffe67 100644
--- a/libs/image/kis_image_animation_interface.cpp
+++ b/libs/image/kis_image_animation_interface.cpp
@@ -1,416 +1,429 @@
/*
* Copyright (c) 2015 Dmitry Kazakov
*
* 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) 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "kis_image_animation_interface.h"
#include
#include "kis_global.h"
#include "kis_image.h"
#include "kis_regenerate_frame_stroke_strategy.h"
#include "kis_switch_time_stroke_strategy.h"
#include "kis_keyframe_channel.h"
#include "kis_time_range.h"
#include "kis_post_execution_undo_adapter.h"
#include "commands_new/kis_switch_current_time_command.h"
#include "kis_layer_utils.h"
struct KisImageAnimationInterface::Private
{
Private()
: image(0),
externalFrameActive(false),
frameInvalidationBlocked(false),
cachedLastFrameValue(-1),
audioChannelMuted(false),
audioChannelVolume(0.5),
m_currentTime(0),
m_currentUITime(0)
{
}
Private(const Private &rhs, KisImage *newImage)
: image(newImage),
externalFrameActive(false),
frameInvalidationBlocked(false),
fullClipRange(rhs.fullClipRange),
playbackRange(rhs.playbackRange),
framerate(rhs.framerate),
cachedLastFrameValue(-1),
audioChannelFileName(rhs.audioChannelFileName),
audioChannelMuted(rhs.audioChannelMuted),
audioChannelVolume(rhs.audioChannelVolume),
m_currentTime(rhs.m_currentTime),
m_currentUITime(rhs.m_currentUITime)
{
}
KisImage *image;
bool externalFrameActive;
bool frameInvalidationBlocked;
KisTimeRange fullClipRange;
KisTimeRange playbackRange;
int framerate;
int cachedLastFrameValue;
QString audioChannelFileName;
bool audioChannelMuted;
qreal audioChannelVolume;
KisSwitchTimeStrokeStrategy::SharedTokenWSP switchToken;
inline int currentTime() const {
return m_currentTime;
}
inline int currentUITime() const {
return m_currentUITime;
}
inline void setCurrentTime(int value) {
m_currentTime = value;
}
inline void setCurrentUITime(int value) {
m_currentUITime = value;
}
private:
int m_currentTime;
int m_currentUITime;
};
KisImageAnimationInterface::KisImageAnimationInterface(KisImage *image)
: QObject(image),
m_d(new Private)
{
m_d->image = image;
m_d->framerate = 24;
m_d->fullClipRange = KisTimeRange::fromTime(0, 100);
connect(this, SIGNAL(sigInternalRequestTimeSwitch(int, bool)), SLOT(switchCurrentTimeAsync(int, bool)));
}
KisImageAnimationInterface::KisImageAnimationInterface(const KisImageAnimationInterface &rhs, KisImage *newImage)
: m_d(new Private(*rhs.m_d, newImage))
{
connect(this, SIGNAL(sigInternalRequestTimeSwitch(int, bool)), SLOT(switchCurrentTimeAsync(int, bool)));
}
KisImageAnimationInterface::~KisImageAnimationInterface()
{
}
bool KisImageAnimationInterface::hasAnimation() const
{
bool hasAnimation = false;
KisLayerUtils::recursiveApplyNodes(
m_d->image->root(),
[&hasAnimation](KisNodeSP node) {
hasAnimation |= node->isAnimated();
});
return hasAnimation;
}
int KisImageAnimationInterface::currentTime() const
{
return m_d->currentTime();
}
int KisImageAnimationInterface::currentUITime() const
{
return m_d->currentUITime();
}
const KisTimeRange& KisImageAnimationInterface::fullClipRange() const
{
return m_d->fullClipRange;
}
-void KisImageAnimationInterface::setFullClipRange(const KisTimeRange range) {
+void KisImageAnimationInterface::setFullClipRange(const KisTimeRange range)
+{
m_d->fullClipRange = range;
emit sigFullClipRangeChanged();
}
+void KisImageAnimationInterface::setFullClipRangeStartTime(int column)
+{
+ KisTimeRange newRange(column, m_d->fullClipRange.end(), false);
+ setFullClipRange(newRange);
+}
+
+void KisImageAnimationInterface::setFullClipRangeEndTime(int column)
+{
+ KisTimeRange newRange(m_d->fullClipRange.start(), column, false);
+ setFullClipRange(newRange);
+}
+
const KisTimeRange& KisImageAnimationInterface::playbackRange() const
{
return m_d->playbackRange.isValid() ? m_d->playbackRange : m_d->fullClipRange;
}
void KisImageAnimationInterface::setPlaybackRange(const KisTimeRange range)
{
m_d->playbackRange = range;
emit sigPlaybackRangeChanged();
}
int KisImageAnimationInterface::framerate() const
{
return m_d->framerate;
}
QString KisImageAnimationInterface::audioChannelFileName() const
{
return m_d->audioChannelFileName;
}
void KisImageAnimationInterface::setAudioChannelFileName(const QString &fileName)
{
QFileInfo info(fileName);
KIS_SAFE_ASSERT_RECOVER_NOOP(fileName.isEmpty() || info.isAbsolute());
m_d->audioChannelFileName = fileName.isEmpty() ? fileName : info.absoluteFilePath();
emit sigAudioChannelChanged();
}
bool KisImageAnimationInterface::isAudioMuted() const
{
return m_d->audioChannelMuted;
}
void KisImageAnimationInterface::setAudioMuted(bool value)
{
m_d->audioChannelMuted = value;
emit sigAudioChannelChanged();
}
qreal KisImageAnimationInterface::audioVolume() const
{
return m_d->audioChannelVolume;
}
void KisImageAnimationInterface::setAudioVolume(qreal value)
{
m_d->audioChannelVolume = value;
emit sigAudioVolumeChanged();
}
void KisImageAnimationInterface::setFramerate(int fps)
{
m_d->framerate = fps;
emit sigFramerateChanged();
}
KisImageWSP KisImageAnimationInterface::image() const
{
return m_d->image;
}
bool KisImageAnimationInterface::externalFrameActive() const
{
return m_d->externalFrameActive;
}
void KisImageAnimationInterface::requestTimeSwitchWithUndo(int time)
{
if (currentUITime() == time) return;
requestTimeSwitchNonGUI(time, true);
}
void KisImageAnimationInterface::setDefaultProjectionColor(const KoColor &color)
{
int savedTime = 0;
saveAndResetCurrentTime(currentTime(), &savedTime);
m_d->image->setDefaultProjectionColor(color);
restoreCurrentTime(&savedTime);
}
void KisImageAnimationInterface::requestTimeSwitchNonGUI(int time, bool useUndo)
{
emit sigInternalRequestTimeSwitch(time, useUndo);
}
void KisImageAnimationInterface::explicitlySetCurrentTime(int frameId)
{
m_d->setCurrentTime(frameId);
}
void KisImageAnimationInterface::switchCurrentTimeAsync(int frameId, bool useUndo)
{
if (currentUITime() == frameId) return;
KisTimeRange range = KisTimeRange::infinite(0);
KisTimeRange::calculateTimeRangeRecursive(m_d->image->root(), currentUITime(), range, true);
const bool needsRegeneration = !range.contains(frameId);
KisSwitchTimeStrokeStrategy::SharedTokenSP token =
m_d->switchToken.toStrongRef();
if (!token || !token->tryResetDestinationTime(frameId, needsRegeneration)) {
{
KisPostExecutionUndoAdapter *undoAdapter = useUndo ?
m_d->image->postExecutionUndoAdapter() : 0;
KisSwitchTimeStrokeStrategy *strategy =
new KisSwitchTimeStrokeStrategy(frameId, needsRegeneration,
this, undoAdapter);
m_d->switchToken = strategy->token();
KisStrokeId stroke = m_d->image->startStroke(strategy);
m_d->image->endStroke(stroke);
}
if (needsRegeneration) {
KisStrokeStrategy *strategy =
new KisRegenerateFrameStrokeStrategy(this);
KisStrokeId strokeId = m_d->image->startStroke(strategy);
m_d->image->endStroke(strokeId);
}
}
m_d->setCurrentUITime(frameId);
emit sigUiTimeChanged(frameId);
}
void KisImageAnimationInterface::requestFrameRegeneration(int frameId, const QRegion &dirtyRegion)
{
KisStrokeStrategy *strategy =
new KisRegenerateFrameStrokeStrategy(frameId,
dirtyRegion,
this);
QList jobs = KisRegenerateFrameStrokeStrategy::createJobsData(m_d->image);
KisStrokeId stroke = m_d->image->startStroke(strategy);
Q_FOREACH (KisStrokeJobData* job, jobs) {
m_d->image->addJob(stroke, job);
}
m_d->image->endStroke(stroke);
}
void KisImageAnimationInterface::saveAndResetCurrentTime(int frameId, int *savedValue)
{
m_d->externalFrameActive = true;
*savedValue = m_d->currentTime();
m_d->setCurrentTime(frameId);
}
void KisImageAnimationInterface::restoreCurrentTime(int *savedValue)
{
m_d->setCurrentTime(*savedValue);
m_d->externalFrameActive = false;
}
void KisImageAnimationInterface::notifyFrameReady()
{
emit sigFrameReady(m_d->currentTime());
}
void KisImageAnimationInterface::notifyFrameCancelled()
{
emit sigFrameCancelled();
}
KisUpdatesFacade* KisImageAnimationInterface::updatesFacade() const
{
return m_d->image;
}
void KisImageAnimationInterface::notifyNodeChanged(const KisNode *node,
const QRect &rect,
bool recursive)
{
notifyNodeChanged(node, QVector({rect}), recursive);
}
void KisImageAnimationInterface::notifyNodeChanged(const KisNode *node,
const QVector &rects,
bool recursive)
{
if (externalFrameActive() || m_d->frameInvalidationBlocked) return;
if (node->inherits("KisSelectionMask")) return;
KisKeyframeChannel *channel =
node->getKeyframeChannel(KisKeyframeChannel::Content.id());
KisTimeRange invalidateRange;
if (recursive) {
KisTimeRange::calculateTimeRangeRecursive(node, currentTime(), invalidateRange, false);
} else if (channel) {
const int currentTime = m_d->currentTime();
invalidateRange = channel->affectedFrames(currentTime);
} else {
invalidateRange = KisTimeRange::infinite(0);
}
// we compress the updated rect (atm, no one uses it anyway)
QRect unitedRect;
Q_FOREACH (const QRect &rc, rects) {
unitedRect |= rc;
}
invalidateFrames(invalidateRange, unitedRect);
}
void KisImageAnimationInterface::invalidateFrames(const KisTimeRange &range, const QRect &rect)
{
m_d->cachedLastFrameValue = -1;
emit sigFramesChanged(range, rect);
}
void KisImageAnimationInterface::blockFrameInvalidation(bool value)
{
m_d->frameInvalidationBlocked = value;
}
int findLastKeyframeTimeRecursive(KisNodeSP node)
{
int time = 0;
KisKeyframeChannel *channel;
Q_FOREACH (channel, node->keyframeChannels()) {
KisKeyframeSP keyframe = channel->lastKeyframe();
if (keyframe) {
time = std::max(time, keyframe->time());
}
}
KisNodeSP child = node->firstChild();
while (child) {
time = std::max(time, findLastKeyframeTimeRecursive(child));
child = child->nextSibling();
}
return time;
}
int KisImageAnimationInterface::totalLength()
{
if (m_d->cachedLastFrameValue < 0) {
m_d->cachedLastFrameValue = findLastKeyframeTimeRecursive(m_d->image->root());
}
int lastKey = m_d->cachedLastFrameValue;
lastKey = std::max(lastKey, m_d->fullClipRange.end());
lastKey = std::max(lastKey, m_d->currentUITime());
return lastKey + 1;
}
diff --git a/libs/image/kis_image_animation_interface.h b/libs/image/kis_image_animation_interface.h
index 5c62b921a0..0cfb92f505 100644
--- a/libs/image/kis_image_animation_interface.h
+++ b/libs/image/kis_image_animation_interface.h
@@ -1,210 +1,214 @@
/*
* Copyright (c) 2015 Dmitry Kazakov
*
* 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) 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef __KIS_IMAGE_ANIMATION_INTERFACE_H
#define __KIS_IMAGE_ANIMATION_INTERFACE_H
#include
#include
#include "kis_types.h"
#include "kritaimage_export.h"
class KisUpdatesFacade;
class KisTimeRange;
class KoColor;
namespace KisLayerUtils {
struct SwitchFrameCommand;
}
class KRITAIMAGE_EXPORT KisImageAnimationInterface : public QObject
{
Q_OBJECT
public:
KisImageAnimationInterface(KisImage *image);
KisImageAnimationInterface(const KisImageAnimationInterface &rhs, KisImage *newImage);
~KisImageAnimationInterface() override;
/**
* Returns true of the image has at least one animated layer
*/
bool hasAnimation() const;
/**
* Returns currently active frame of the underlying image. Some strokes
* can override this value and it will report a different value.
*/
int currentTime() const;
/**
* Same as currentTime, except it isn't changed when background strokes
* are running.
*/
int currentUITime() const;
/**
* While any non-current frame is being regenerated by the
* strategy, the image is kept in a special state, named
* 'externalFrameActive'. Is this state the following applies:
*
* 1) All the animated paint devices switch its state into
* frameId() defined by global time.
*
* 2) All animation-not-capable devices switch to a temporary
* content device, which *is in undefined state*. The stroke
* should regenerate the image projection manually.
*/
bool externalFrameActive() const;
void requestTimeSwitchWithUndo(int time);
void requestTimeSwitchNonGUI(int time, bool useUndo = false);
public Q_SLOTS:
/**
* Switches current frame (synchronously) and starts an
* asynchronous regeneration of the entire image.
*/
void switchCurrentTimeAsync(int frameId, bool useUndo = false);
public:
/**
* Start a background thread that will recalculate some extra frame.
* The result will be reported using two types of signals:
*
* 1) KisImage::sigImageUpdated() will be emitted for every chunk
* of updated area.
*
* 2) sigFrameReady() will be emitted in the end of the operation.
* IMPORTANT: to get the result you must connect to this signal
* with Qt::DirectConnection and fetch the result from
* frameProjection(). After the signal handler is exited, the
* data will no longer be available.
*/
void requestFrameRegeneration(int frameId, const QRegion &dirtyRegion);
void notifyNodeChanged(const KisNode *node, const QRect &rect, bool recursive);
void notifyNodeChanged(const KisNode *node, const QVector &rects, bool recursive);
void invalidateFrames(const KisTimeRange &range, const QRect &rect);
/**
* Changes the default color of the "external frame" projection of
* the image's root layer. Please note that this command should be
* executed from a context of an exclusive job!
*/
void setDefaultProjectionColor(const KoColor &color);
/**
* The current time range selected by user.
* @return current time range
*/
const KisTimeRange& fullClipRange() const;
void setFullClipRange(const KisTimeRange range);
+ void setFullClipRangeStartTime(int column);
+ void setFullClipRangeEndTime(int column);
+
+
const KisTimeRange &playbackRange() const;
void setPlaybackRange(const KisTimeRange range);
int framerate() const;
/**
* @return **absolute** file name of the audio channel file
*/
QString audioChannelFileName() const;
/**
* Sets **absolute** file name of the audio channel file. Don't try to pass
* a relative path, it'll assert!
*/
void setAudioChannelFileName(const QString &fileName);
/**
* @return is the audio channel is currently muted
*/
bool isAudioMuted() const;
/**
* Mutes the audio channel
*/
void setAudioMuted(bool value);
/**
* Returns the preferred audio value in rangle [0, 1]
*/
qreal audioVolume() const;
/**
* Set the preferred volume for the audio channel in range [0, 1]
*/
void setAudioVolume(qreal value);
public Q_SLOTS:
void setFramerate(int fps);
public:
KisImageWSP image() const;
int totalLength();
private:
// interface for:
friend class KisRegenerateFrameStrokeStrategy;
friend class KisAnimationFrameCacheTest;
friend struct KisLayerUtils::SwitchFrameCommand;
friend class KisImageTest;
void saveAndResetCurrentTime(int frameId, int *savedValue);
void restoreCurrentTime(int *savedValue);
void notifyFrameReady();
void notifyFrameCancelled();
KisUpdatesFacade* updatesFacade() const;
void blockFrameInvalidation(bool value);
friend class KisSwitchTimeStrokeStrategy;
void explicitlySetCurrentTime(int frameId);
Q_SIGNALS:
void sigFrameReady(int time);
void sigFrameCancelled();
void sigUiTimeChanged(int newTime);
void sigFramesChanged(const KisTimeRange &range, const QRect &rect);
void sigInternalRequestTimeSwitch(int frameId, bool useUndo);
void sigFramerateChanged();
void sigFullClipRangeChanged();
void sigPlaybackRangeChanged();
/**
* Emitted when the audio channel of the document is changed
*/
void sigAudioChannelChanged();
/**
* Emitted when audion volume changes. Please note that it doesn't change
* when you mute the channel! When muting, sigAudioChannelChanged() is used instead!
*/
void sigAudioVolumeChanged();
private:
struct Private;
const QScopedPointer m_d;
};
#endif /* __KIS_IMAGE_ANIMATION_INTERFACE_H */
diff --git a/libs/ui/canvas/kis_animation_player.cpp b/libs/ui/canvas/kis_animation_player.cpp
index e23c59450f..9e72f292f8 100644
--- a/libs/ui/canvas/kis_animation_player.cpp
+++ b/libs/ui/canvas/kis_animation_player.cpp
@@ -1,544 +1,546 @@
/*
* Copyright (c) 2015 Jouni Pentikäinen
*
* 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) 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "kis_animation_player.h"
#include
#include
#include
//#define PLAYER_DEBUG_FRAMERATE
#include "kis_global.h"
#include "kis_config.h"
#include "kis_config_notifier.h"
#include "kis_image.h"
#include "kis_canvas2.h"
#include "kis_animation_frame_cache.h"
#include "kis_signal_auto_connection.h"
#include "kis_image_animation_interface.h"
#include "kis_time_range.h"
#include "kis_signal_compressor.h"
#include
#include
#include "KisSyncedAudioPlayback.h"
#include "kis_signal_compressor_with_param.h"
#include "KisViewManager.h"
#include "kis_icon_utils.h"
#include "KisPart.h"
#include "dialogs/KisAsyncAnimationCacheRenderDialog.h"
#include "KisRollingMeanAccumulatorWrapper.h"
struct KisAnimationPlayer::Private
{
public:
Private(KisAnimationPlayer *_q)
: q(_q),
realFpsAccumulator(24),
droppedFpsAccumulator(24),
droppedFramesPortion(24),
dropFramesMode(true),
nextFrameExpectedTime(0),
expectedInterval(0),
expectedFrame(0),
lastTimerInterval(0),
lastPaintedFrame(0),
playbackStatisticsCompressor(1000, KisSignalCompressor::FIRST_INACTIVE),
stopAudioOnScrubbingCompressor(100, KisSignalCompressor::POSTPONE),
audioOffsetTolerance(-1)
{}
KisAnimationPlayer *q;
bool useFastFrameUpload;
bool playing;
QTimer *timer;
int initialFrame;
int firstFrame;
int lastFrame;
qreal playbackSpeed;
KisCanvas2 *canvas;
KisSignalAutoConnectionsStore cancelStrokeConnections;
QElapsedTimer realFpsTimer;
KisRollingMeanAccumulatorWrapper realFpsAccumulator;
KisRollingMeanAccumulatorWrapper droppedFpsAccumulator;
KisRollingMeanAccumulatorWrapper droppedFramesPortion;
bool dropFramesMode;
QElapsedTimer playbackTime;
int nextFrameExpectedTime;
int expectedInterval;
int expectedFrame;
int lastTimerInterval;
int lastPaintedFrame;
KisSignalCompressor playbackStatisticsCompressor;
QScopedPointer syncedAudio;
QScopedPointer > audioSyncScrubbingCompressor;
KisSignalCompressor stopAudioOnScrubbingCompressor;
int audioOffsetTolerance;
void stopImpl(bool doUpdates);
int incFrame(int frame, int inc) {
frame += inc;
if (frame > lastFrame) {
frame = firstFrame + frame - lastFrame - 1;
}
return frame;
}
qint64 frameToMSec(int value, int fps) {
return qreal(value) / fps * 1000.0;
}
int msecToFrame(qint64 value, int fps) {
return qreal(value) * fps / 1000.0;
}
};
KisAnimationPlayer::KisAnimationPlayer(KisCanvas2 *canvas)
: QObject(canvas)
, m_d(new Private(this))
{
m_d->useFastFrameUpload = false;
m_d->playing = false;
m_d->canvas = canvas;
m_d->playbackSpeed = 1.0;
m_d->timer = new QTimer(this);
connect(m_d->timer, SIGNAL(timeout()), this, SLOT(slotUpdate()));
m_d->timer->setSingleShot(true);
connect(KisConfigNotifier::instance(),
SIGNAL(dropFramesModeChanged()),
SLOT(slotUpdateDropFramesMode()));
slotUpdateDropFramesMode();
connect(&m_d->playbackStatisticsCompressor, SIGNAL(timeout()),
this, SIGNAL(sigPlaybackStatisticsUpdated()));
using namespace std::placeholders;
std::function callback(
std::bind(&KisAnimationPlayer::slotSyncScrubbingAudio, this, _1));
const int defaultScrubbingUdpatesDelay = 40; /* 40 ms == 25 fps */
m_d->audioSyncScrubbingCompressor.reset(
new KisSignalCompressorWithParam(defaultScrubbingUdpatesDelay, callback, KisSignalCompressor::FIRST_ACTIVE));
m_d->stopAudioOnScrubbingCompressor.setDelay(defaultScrubbingUdpatesDelay);
connect(&m_d->stopAudioOnScrubbingCompressor, SIGNAL(timeout()), SLOT(slotTryStopScrubbingAudio()));
connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigFramerateChanged()), SLOT(slotUpdateAudioChunkLength()));
slotUpdateAudioChunkLength();
connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigAudioChannelChanged()), SLOT(slotAudioChannelChanged()));
connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigAudioVolumeChanged()), SLOT(slotAudioVolumeChanged()));
+
slotAudioChannelChanged();
}
KisAnimationPlayer::~KisAnimationPlayer()
{}
void KisAnimationPlayer::slotUpdateDropFramesMode()
{
KisConfig cfg;
m_d->dropFramesMode = cfg.animationDropFrames();
}
void KisAnimationPlayer::slotSyncScrubbingAudio(int msecTime)
{
KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->syncedAudio);
if (!m_d->syncedAudio->isPlaying()) {
m_d->syncedAudio->play(msecTime);
} else {
m_d->syncedAudio->syncWithVideo(msecTime);
}
if (!isPlaying()) {
m_d->stopAudioOnScrubbingCompressor.start();
}
}
void KisAnimationPlayer::slotTryStopScrubbingAudio()
{
KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->syncedAudio);
if (m_d->syncedAudio && !isPlaying()) {
m_d->syncedAudio->stop();
}
}
void KisAnimationPlayer::slotAudioChannelChanged()
{
KisImageAnimationInterface *interface = m_d->canvas->image()->animationInterface();
QString fileName = interface->audioChannelFileName();
QFileInfo info(fileName);
if (info.exists() && !interface->isAudioMuted()) {
m_d->syncedAudio.reset(new KisSyncedAudioPlayback(info.absoluteFilePath()));
m_d->syncedAudio->setVolume(interface->audioVolume());
m_d->syncedAudio->setSoundOffsetTolerance(m_d->audioOffsetTolerance);
connect(m_d->syncedAudio.data(), SIGNAL(error(const QString &, const QString &)), SLOT(slotOnAudioError(const QString &, const QString &)));
} else {
m_d->syncedAudio.reset();
}
}
void KisAnimationPlayer::slotAudioVolumeChanged()
{
KisImageAnimationInterface *interface = m_d->canvas->image()->animationInterface();
if (m_d->syncedAudio) {
m_d->syncedAudio->setVolume(interface->audioVolume());
}
}
void KisAnimationPlayer::slotOnAudioError(const QString &fileName, const QString &message)
{
QString errorMessage(i18nc("floating on-canvas message", "Cannot open audio: \"%1\"\nError: %2", fileName, message));
m_d->canvas->viewManager()->showFloatingMessage(errorMessage, KisIconUtils::loadIcon("warning"));
}
void KisAnimationPlayer::connectCancelSignals()
{
m_d->cancelStrokeConnections.addConnection(
m_d->canvas->image().data(), SIGNAL(sigUndoDuringStrokeRequested()),
this, SLOT(slotCancelPlayback()));
m_d->cancelStrokeConnections.addConnection(
m_d->canvas->image().data(), SIGNAL(sigStrokeCancellationRequested()),
this, SLOT(slotCancelPlayback()));
m_d->cancelStrokeConnections.addConnection(
m_d->canvas->image().data(), SIGNAL(sigStrokeEndRequested()),
this, SLOT(slotCancelPlaybackSafe()));
m_d->cancelStrokeConnections.addConnection(
m_d->canvas->image()->animationInterface(), SIGNAL(sigFramerateChanged()),
this, SLOT(slotUpdatePlaybackTimer()));
m_d->cancelStrokeConnections.addConnection(
m_d->canvas->image()->animationInterface(), SIGNAL(sigFullClipRangeChanged()),
this, SLOT(slotUpdatePlaybackTimer()));
m_d->cancelStrokeConnections.addConnection(
m_d->canvas->image()->animationInterface(), SIGNAL(sigPlaybackRangeChanged()),
this, SLOT(slotUpdatePlaybackTimer()));
}
void KisAnimationPlayer::disconnectCancelSignals()
{
m_d->cancelStrokeConnections.clear();
}
void KisAnimationPlayer::slotUpdateAudioChunkLength()
{
const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface();
const int animationFramePeriod = qMax(1, 1000 / animation->framerate());
KisConfig cfg;
int scrubbingAudioUdpatesDelay = cfg.scrubbingAudioUpdatesDelay();
if (scrubbingAudioUdpatesDelay < 0) {
scrubbingAudioUdpatesDelay = qMax(1, animationFramePeriod);
}
m_d->audioSyncScrubbingCompressor->setDelay(scrubbingAudioUdpatesDelay);
m_d->stopAudioOnScrubbingCompressor.setDelay(scrubbingAudioUdpatesDelay);
m_d->audioOffsetTolerance = cfg.audioOffsetTolerance();
if (m_d->audioOffsetTolerance < 0) {
m_d->audioOffsetTolerance = animationFramePeriod;
}
if (m_d->syncedAudio) {
m_d->syncedAudio->setSoundOffsetTolerance(m_d->audioOffsetTolerance);
}
}
void KisAnimationPlayer::slotUpdatePlaybackTimer()
{
m_d->timer->stop();
const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface();
- const KisTimeRange &range = animation->playbackRange();
- if (!range.isValid()) return;
+ const KisTimeRange &playBackRange = animation->playbackRange();
+ if (!playBackRange.isValid()) return;
const int fps = animation->framerate();
m_d->initialFrame = animation->currentUITime();
- m_d->firstFrame = range.start();
- m_d->lastFrame = range.end();
+ m_d->firstFrame = playBackRange.start();
+ m_d->lastFrame = playBackRange.end();
m_d->expectedFrame = qBound(m_d->firstFrame, m_d->expectedFrame, m_d->lastFrame);
+
m_d->expectedInterval = qreal(1000) / fps / m_d->playbackSpeed;
m_d->lastTimerInterval = m_d->expectedInterval;
if (m_d->syncedAudio) {
m_d->syncedAudio->setSpeed(m_d->playbackSpeed);
}
m_d->timer->start(m_d->expectedInterval);
if (m_d->playbackTime.isValid()) {
m_d->playbackTime.restart();
} else {
m_d->playbackTime.start();
}
m_d->nextFrameExpectedTime = m_d->playbackTime.elapsed() + m_d->expectedInterval;
}
void KisAnimationPlayer::play()
{
{
const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface();
const KisTimeRange &range = animation->playbackRange();
if (!range.isValid()) return;
// when openGL is disabled, there is no animation cache
if (m_d->canvas->frameCache()) {
KisAsyncAnimationCacheRenderDialog dlg(m_d->canvas->frameCache(),
range,
200);
KisAsyncAnimationCacheRenderDialog::Result result =
dlg.regenerateRange(m_d->canvas->viewManager());
if (result != KisAsyncAnimationCacheRenderDialog::RenderComplete) {
return;
}
}
}
m_d->playing = true;
slotUpdatePlaybackTimer();
m_d->expectedFrame = m_d->firstFrame;
m_d->lastPaintedFrame = m_d->firstFrame;
connectCancelSignals();
if (m_d->syncedAudio) {
KisImageAnimationInterface *animationInterface = m_d->canvas->image()->animationInterface();
m_d->syncedAudio->play(m_d->frameToMSec(m_d->firstFrame, animationInterface->framerate()));
}
}
void KisAnimationPlayer::Private::stopImpl(bool doUpdates)
{
if (syncedAudio) {
syncedAudio->stop();
}
q->disconnectCancelSignals();
timer->stop();
playing = false;
if (doUpdates) {
KisImageAnimationInterface *animation = canvas->image()->animationInterface();
if (animation->currentUITime() == initialFrame) {
canvas->refetchDataFromImage();
} else {
animation->switchCurrentTimeAsync(initialFrame);
}
}
emit q->sigPlaybackStopped();
}
void KisAnimationPlayer::stop()
{
m_d->stopImpl(true);
}
void KisAnimationPlayer::forcedStopOnExit()
{
m_d->stopImpl(false);
}
bool KisAnimationPlayer::isPlaying()
{
return m_d->playing;
}
int KisAnimationPlayer::currentTime()
{
return m_d->lastPaintedFrame;
}
void KisAnimationPlayer::displayFrame(int time)
{
uploadFrame(time);
}
void KisAnimationPlayer::slotUpdate()
{
uploadFrame(-1);
}
void KisAnimationPlayer::uploadFrame(int frame)
{
KisImageAnimationInterface *animationInterface = m_d->canvas->image()->animationInterface();
if (frame < 0) {
const int currentTime = m_d->playbackTime.elapsed();
const int framesDiff = currentTime - m_d->nextFrameExpectedTime;
const qreal framesDiffNorm = qreal(framesDiff) / m_d->expectedInterval;
// qDebug() << ppVar(framesDiff)
// << ppVar(m_d->expectedFrame)
// << ppVar(framesDiffNorm)
// << ppVar(m_d->lastTimerInterval);
if (m_d->dropFramesMode) {
const int numDroppedFrames = qMax(0, qRound(framesDiffNorm));
frame = m_d->incFrame(m_d->expectedFrame, numDroppedFrames);
} else {
frame = m_d->expectedFrame;
}
m_d->nextFrameExpectedTime = currentTime + m_d->expectedInterval;
m_d->lastTimerInterval = qMax(0.0, m_d->lastTimerInterval - 0.5 * framesDiff);
m_d->expectedFrame = m_d->incFrame(frame, 1);
m_d->timer->start(m_d->lastTimerInterval);
m_d->playbackStatisticsCompressor.start();
}
if (m_d->syncedAudio) {
const int msecTime = m_d->frameToMSec(frame, animationInterface->framerate());
if (isPlaying()) {
slotSyncScrubbingAudio(msecTime);
} else {
m_d->audioSyncScrubbingCompressor->start(msecTime);
}
}
if (m_d->canvas->frameCache() && m_d->canvas->frameCache()->uploadFrame(frame)) {
m_d->canvas->updateCanvas();
m_d->useFastFrameUpload = true;
emit sigFrameChanged();
} else {
m_d->useFastFrameUpload = false;
m_d->canvas->image()->barrierLock(true);
m_d->canvas->image()->unlock();
// no OpenGL cache or the frame just not cached yet
animationInterface->switchCurrentTimeAsync(frame);
emit sigFrameChanged();
}
if (!m_d->realFpsTimer.isValid()) {
m_d->realFpsTimer.start();
} else {
const int elapsed = m_d->realFpsTimer.restart();
m_d->realFpsAccumulator(elapsed);
int numFrames = frame - m_d->lastPaintedFrame;
if (numFrames < 0) {
numFrames += m_d->lastFrame - m_d->firstFrame + 1;
}
m_d->droppedFramesPortion(qreal(int(numFrames != 1)));
if (numFrames > 0) {
m_d->droppedFpsAccumulator(qreal(elapsed) / numFrames);
}
#ifdef PLAYER_DEBUG_FRAMERATE
qDebug() << " RFPS:" << 1000.0 / m_d->realFpsAccumulator.rollingMean()
<< "DFPS:" << 1000.0 / m_d->droppedFpsAccumulator.rollingMean() << ppVar(numFrames);
#endif /* PLAYER_DEBUG_FRAMERATE */
}
m_d->lastPaintedFrame = frame;
}
qreal KisAnimationPlayer::effectiveFps() const
{
return 1000.0 / m_d->droppedFpsAccumulator.rollingMean();
}
qreal KisAnimationPlayer::realFps() const
{
return 1000.0 / m_d->realFpsAccumulator.rollingMean();
}
qreal KisAnimationPlayer::framesDroppedPortion() const
{
return m_d->droppedFramesPortion.rollingMean();
}
void KisAnimationPlayer::slotCancelPlayback()
{
stop();
}
void KisAnimationPlayer::slotCancelPlaybackSafe()
{
/**
* If there is no openGL support, then we have no (!) cache at
* all. Therefore we should regenerate frame on every time switch,
* which, yeah, can be very slow. What is more important, when
* regenerating a frame animation interface will emit a
* sigStrokeEndRequested() signal and we should ignore it. That is
* not an ideal solution, because the user will be able to paint
* on random frames while playing, but it lets users with faulty
* GPUs see at least some preview of their animation.
*/
if (m_d->useFastFrameUpload) {
stop();
}
}
qreal KisAnimationPlayer::playbackSpeed()
{
return m_d->playbackSpeed;
}
void KisAnimationPlayer::slotUpdatePlaybackSpeed(double value)
{
m_d->playbackSpeed = value;
if (m_d->playing) {
slotUpdatePlaybackTimer();
}
}
diff --git a/libs/ui/canvas/kis_animation_player.h b/libs/ui/canvas/kis_animation_player.h
index 94ecf1ce27..be25cdc00a 100644
--- a/libs/ui/canvas/kis_animation_player.h
+++ b/libs/ui/canvas/kis_animation_player.h
@@ -1,85 +1,87 @@
/*
* Copyright (c) 2015 Jouni Pentikäinen
*
* 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) 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef KIS_ANIMATION_PLAYER_H
#define KIS_ANIMATION_PLAYER_H
#include
#include
#include "kritaui_export.h"
class KisCanvas2;
class KRITAUI_EXPORT KisAnimationPlayer : public QObject
{
Q_OBJECT
public:
KisAnimationPlayer(KisCanvas2 *canvas);
~KisAnimationPlayer() override;
void play();
void stop();
void displayFrame(int time);
bool isPlaying();
int currentTime();
qreal playbackSpeed();
void forcedStopOnExit();
qreal effectiveFps() const;
qreal realFps() const;
qreal framesDroppedPortion() const;
public Q_SLOTS:
void slotUpdate();
void slotCancelPlayback();
void slotCancelPlaybackSafe();
void slotUpdatePlaybackSpeed(double value);
void slotUpdatePlaybackTimer();
void slotUpdateDropFramesMode();
private Q_SLOTS:
void slotSyncScrubbingAudio(int msecTime);
void slotTryStopScrubbingAudio();
void slotUpdateAudioChunkLength();
void slotAudioChannelChanged();
void slotAudioVolumeChanged();
void slotOnAudioError(const QString &fileName, const QString &message);
+
Q_SIGNALS:
void sigFrameChanged();
void sigPlaybackStopped();
void sigPlaybackStatisticsUpdated();
+ void sigFullClipRangeChanged();
private:
void connectCancelSignals();
void disconnectCancelSignals();
void uploadFrame(int time);
private:
struct Private;
QScopedPointer m_d;
};
#endif
diff --git a/plugins/dockers/animation/animation_docker.cpp b/plugins/dockers/animation/animation_docker.cpp
index f0e55d8b0f..f2736847cc 100644
--- a/plugins/dockers/animation/animation_docker.cpp
+++ b/plugins/dockers/animation/animation_docker.cpp
@@ -1,648 +1,658 @@
/*
* Copyright (c) 2015 Jouni Pentikäinen
*
* 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) 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "animation_docker.h"
#include "kis_global.h"
#include "kis_canvas2.h"
#include "kis_image.h"
#include
#include
#include
#include "KisViewManager.h"
#include "kis_action_manager.h"
#include "kis_image_animation_interface.h"
#include "kis_animation_player.h"
#include "kis_time_range.h"
#include "kundo2command.h"
#include "kis_post_execution_undo_adapter.h"
#include "kis_keyframe_channel.h"
#include "kis_animation_utils.h"
#include "krita_utils.h"
#include "kis_image_config.h"
#include "kis_config.h"
#include "kis_signals_blocker.h"
#include "kis_node_manager.h"
#include "kis_transform_mask_params_factory_registry.h"
#include "ui_wdg_animation.h"
void setupActionButton(const QString &text,
KisAction::ActivationFlags flags,
bool defaultValue,
QToolButton *button,
KisAction **action)
{
*action = new KisAction(text, button);
(*action)->setActivationFlags(flags);
(*action)->setCheckable(true);
(*action)->setChecked(defaultValue);
button->setDefaultAction(*action);
}
AnimationDocker::AnimationDocker()
: QDockWidget(i18n("Animation"))
, m_canvas(0)
, m_animationWidget(new Ui_WdgAnimation)
, m_mainWindow(0)
{
QWidget* mainWidget = new QWidget(this);
setWidget(mainWidget);
m_animationWidget->setupUi(mainWidget);
}
AnimationDocker::~AnimationDocker()
{
delete m_animationWidget;
}
void AnimationDocker::setCanvas(KoCanvasBase * canvas)
{
if(m_canvas == canvas)
return;
setEnabled(canvas != 0);
if (m_canvas) {
m_canvas->disconnectCanvasObserver(this);
m_canvas->image()->disconnect(this);
m_canvas->image()->animationInterface()->disconnect(this);
m_canvas->animationPlayer()->disconnect(this);
m_canvas->viewManager()->nodeManager()->disconnect(this);
}
m_canvas = dynamic_cast(canvas);
if (m_canvas && m_canvas->image()) {
KisImageAnimationInterface *animation = m_canvas->image()->animationInterface();
{
KisSignalsBlocker bloker(m_animationWidget->spinFromFrame,
m_animationWidget->spinToFrame,
m_animationWidget->intFramerate);
m_animationWidget->spinFromFrame->setValue(animation->fullClipRange().start());
m_animationWidget->spinToFrame->setValue(animation->fullClipRange().end());
m_animationWidget->intFramerate->setValue(animation->framerate());
}
connect(animation, SIGNAL(sigUiTimeChanged(int)), this, SLOT(slotGlobalTimeChanged()));
connect(m_canvas->animationPlayer(), SIGNAL(sigFrameChanged()), this, SLOT(slotGlobalTimeChanged()));
connect(m_canvas->animationPlayer(), SIGNAL(sigPlaybackStopped()), this, SLOT(slotGlobalTimeChanged()));
connect(m_canvas->animationPlayer(), SIGNAL(sigPlaybackStopped()), this, SLOT(updatePlayPauseIcon()));
connect(m_canvas->animationPlayer(), SIGNAL(sigPlaybackStatisticsUpdated()), this, SLOT(updateDropFramesIcon()));
connect(m_animationWidget->doublePlaySpeed,
SIGNAL(valueChanged(double)),
m_canvas->animationPlayer(),
SLOT(slotUpdatePlaybackSpeed(double)));
connect(m_canvas->viewManager()->nodeManager(), SIGNAL(sigNodeActivated(KisNodeSP)),
this, SLOT(slotCurrentNodeChanged(KisNodeSP)));
+ connect (animation, SIGNAL(sigFullClipRangeChanged()), this, SLOT(updateClipRange()));
+
+
+
slotGlobalTimeChanged();
slotCurrentNodeChanged(m_canvas->viewManager()->nodeManager()->activeNode());
}
}
void AnimationDocker::unsetCanvas()
{
setCanvas(0);
}
void AnimationDocker::setMainWindow(KisViewManager *view)
{
setActions(view->actionManager());
slotUpdateIcons();
connect(view->mainWindow(), SIGNAL(themeChanged()), this, SLOT(slotUpdateIcons()));
m_mainWindow = view->mainWindow();
}
void AnimationDocker::slotAddOpacityKeyframe()
{
addKeyframe(KisKeyframeChannel::Opacity.id(), false);
}
void AnimationDocker::slotDeleteOpacityKeyframe()
{
deleteKeyframe(KisKeyframeChannel::Opacity.id());
}
void AnimationDocker::slotAddTransformKeyframe()
{
if (!m_canvas) return;
KisTransformMask *mask = dynamic_cast(m_canvas->viewManager()->activeNode().data());
if (!mask) return;
const int time = m_canvas->image()->animationInterface()->currentTime();
KUndo2Command *command = new KUndo2Command(kundo2_i18n("Add transform keyframe"));
KisTransformMaskParamsFactoryRegistry::instance()->autoAddKeyframe(mask, time, KisTransformMaskParamsInterfaceSP(), command);
command->redo();
m_canvas->currentImage()->postExecutionUndoAdapter()->addCommand(toQShared(command));
}
void AnimationDocker::slotDeleteTransformKeyframe()
{
deleteKeyframe(KisKeyframeChannel::TransformArguments.id());
}
void AnimationDocker::slotUIRangeChanged()
{
if (!m_canvas || !m_canvas->image()) return;
int fromTime = m_animationWidget->spinFromFrame->value();
int toTime = m_animationWidget->spinToFrame->value();
m_canvas->image()->animationInterface()->setFullClipRange(KisTimeRange::fromTime(fromTime, toTime));
}
void AnimationDocker::slotUIFramerateChanged()
{
if (!m_canvas || !m_canvas->image()) return;
m_canvas->image()->animationInterface()->setFramerate(m_animationWidget->intFramerate->value());
}
void AnimationDocker::slotOnionSkinOptions()
{
if (m_mainWindow) {
QDockWidget *docker = m_mainWindow->dockWidget("OnionSkinsDocker");
if (docker) {
docker->setVisible(!docker->isVisible());
}
}
}
void AnimationDocker::slotGlobalTimeChanged()
{
int time = m_canvas->animationPlayer()->isPlaying() ?
m_canvas->animationPlayer()->currentTime() :
m_canvas->image()->animationInterface()->currentUITime();
m_animationWidget->intCurrentTime->setValue(time);
const int frameRate = m_canvas->image()->animationInterface()->framerate();
const int msec = 1000 * time / frameRate;
QTime realTime;
realTime = realTime.addMSecs(msec);
QString realTimeString = realTime.toString("hh:mm:ss.zzz");
m_animationWidget->intCurrentTime->setToolTip(realTimeString);
}
void AnimationDocker::slotTimeSpinBoxChanged()
{
if (!m_canvas || !m_canvas->image()) return;
int newTime = m_animationWidget->intCurrentTime->value();
KisImageAnimationInterface *animation = m_canvas->image()->animationInterface();
if (m_canvas->animationPlayer()->isPlaying() ||
newTime == animation->currentUITime()) {
return;
}
animation->requestTimeSwitchWithUndo(newTime);
}
void AnimationDocker::slotPreviousFrame()
{
if (!m_canvas) return;
KisImageAnimationInterface *animation = m_canvas->image()->animationInterface();
int time = animation->currentUITime() - 1;
if (time >= 0) {
animation->requestTimeSwitchWithUndo(time);
}
}
void AnimationDocker::slotNextFrame()
{
if (!m_canvas) return;
KisImageAnimationInterface *animation = m_canvas->image()->animationInterface();
int time = animation->currentUITime() + 1;
animation->requestTimeSwitchWithUndo(time);
}
void AnimationDocker::slotPreviousKeyFrame()
{
if (!m_canvas) return;
KisNodeSP node = m_canvas->viewManager()->activeNode();
if (!node) return;
KisImageAnimationInterface *animation = m_canvas->image()->animationInterface();
int time = animation->currentUITime();
KisKeyframeChannel *content =
node->getKeyframeChannel(KisKeyframeChannel::Content.id());
if (!content) return;
KisKeyframeSP dstKeyframe;
KisKeyframeSP keyframe = content->keyframeAt(time);
if (!keyframe) {
dstKeyframe = content->activeKeyframeAt(time);
} else {
dstKeyframe = content->previousKeyframe(keyframe);
}
if (dstKeyframe) {
animation->requestTimeSwitchWithUndo(dstKeyframe->time());
}
}
void AnimationDocker::slotNextKeyFrame()
{
if (!m_canvas) return;
KisNodeSP node = m_canvas->viewManager()->activeNode();
if (!node) return;
KisImageAnimationInterface *animation = m_canvas->image()->animationInterface();
int time = animation->currentUITime();
KisKeyframeChannel *content =
node->getKeyframeChannel(KisKeyframeChannel::Content.id());
if (!content) return;
KisKeyframeSP dstKeyframe;
KisKeyframeSP keyframe = content->activeKeyframeAt(time);
if (keyframe) {
dstKeyframe = content->nextKeyframe(keyframe);
}
if (dstKeyframe) {
animation->requestTimeSwitchWithUndo(dstKeyframe->time());
}
}
void AnimationDocker::slotFirstFrame()
{
if (!m_canvas) return;
KisImageAnimationInterface *animation = m_canvas->image()->animationInterface();
animation->requestTimeSwitchWithUndo(0);
}
void AnimationDocker::slotLastFrame()
{
if (!m_canvas) return;
KisImageAnimationInterface *animation = m_canvas->image()->animationInterface();
animation->requestTimeSwitchWithUndo(animation->totalLength() - 1);
}
void AnimationDocker::slotPlayPause()
{
if (!m_canvas) return;
if (m_canvas->animationPlayer()->isPlaying()) {
m_canvas->animationPlayer()->stop();
} else {
m_canvas->animationPlayer()->play();
}
updatePlayPauseIcon();
}
void AnimationDocker::updatePlayPauseIcon()
{
bool isPlaying = m_canvas && m_canvas->animationPlayer() && m_canvas->animationPlayer()->isPlaying();
m_playPauseAction->setIcon(isPlaying ?
KisIconUtils::loadIcon("animation_stop") :
KisIconUtils::loadIcon("animation_play"));
}
void AnimationDocker::updateLazyFrameIcon()
{
KisImageConfig cfg;
const bool value = cfg.lazyFrameCreationEnabled();
m_lazyFrameAction->setIcon(value ?
KisIconUtils::loadIcon("lazyframeOn") :
KisIconUtils::loadIcon("lazyframeOff"));
m_lazyFrameAction->setText(QString("%1 (%2)")
.arg(KisAnimationUtils::lazyFrameCreationActionName)
.arg(KritaUtils::toLocalizedOnOff(value)));
}
void AnimationDocker::updateDropFramesIcon()
{
qreal effectiveFps = 0.0;
qreal realFps = 0.0;
qreal framesDropped = 0.0;
bool isPlaying = false;
KisAnimationPlayer *player =
m_canvas && m_canvas->animationPlayer() ?
m_canvas->animationPlayer() : 0;
if (player) {
effectiveFps = player->effectiveFps();
realFps = player->realFps();
framesDropped = player->framesDroppedPortion();
isPlaying = player->isPlaying();
}
KisConfig cfg;
const bool value = cfg.animationDropFrames();
m_dropFramesAction->setIcon(value ?
KisIconUtils::loadIcon(framesDropped > 0.05 ? "droppedframes" : "dropframe") :
KisIconUtils::loadIcon("dropframe"));
QString text;
if (!isPlaying) {
text = QString("%1 (%2)")
.arg(KisAnimationUtils::dropFramesActionName)
.arg(KritaUtils::toLocalizedOnOff(value));
} else {
text = QString("%1 (%2)\n"
"%3\n"
"%4\n"
"%5")
.arg(KisAnimationUtils::dropFramesActionName)
.arg(KritaUtils::toLocalizedOnOff(value))
.arg(i18n("Effective FPS:\t%1", effectiveFps))
.arg(i18n("Real FPS:\t%1", realFps))
.arg(i18n("Frames dropped:\t%1\%", framesDropped * 100));
}
m_dropFramesAction->setText(text);
}
void AnimationDocker::slotUpdateIcons()
{
m_previousFrameAction->setIcon(KisIconUtils::loadIcon("prevframe"));
m_nextFrameAction->setIcon(KisIconUtils::loadIcon("nextframe"));
m_previousKeyFrameAction->setIcon(KisIconUtils::loadIcon("prevkeyframe"));
m_nextKeyFrameAction->setIcon(KisIconUtils::loadIcon("nextkeyframe"));
m_firstFrameAction->setIcon(KisIconUtils::loadIcon("firstframe"));
m_lastFrameAction->setIcon(KisIconUtils::loadIcon("lastframe"));
updatePlayPauseIcon();
updateLazyFrameIcon();
updateDropFramesIcon();
m_animationWidget->btnOnionSkinOptions->setIcon(KisIconUtils::loadIcon("onion_skin_options"));
m_animationWidget->btnOnionSkinOptions->setIconSize(QSize(22, 22));
m_animationWidget->btnNextKeyFrame->setIconSize(QSize(22, 22));
m_animationWidget->btnPreviousKeyFrame->setIconSize(QSize(22, 22));
m_animationWidget->btnFirstFrame->setIconSize(QSize(22, 22));
m_animationWidget->btnLastFrame->setIconSize(QSize(22, 22));
m_animationWidget->btnPreviousFrame->setIconSize(QSize(22, 22));
m_animationWidget->btnPlay->setIconSize(QSize(22, 22));
m_animationWidget->btnNextFrame->setIconSize(QSize(22, 22));
m_animationWidget->btnAddKeyframe->setIconSize(QSize(22, 22));
m_animationWidget->btnAddDuplicateFrame->setIconSize(QSize(22, 22));
m_animationWidget->btnDeleteKeyframe->setIconSize(QSize(22, 22));
m_animationWidget->btnLazyFrame->setIconSize(QSize(22, 22));
m_animationWidget->btnDropFrames->setIconSize(QSize(22, 22));
}
void AnimationDocker::slotLazyFrameChanged(bool value)
{
KisImageConfig cfg;
if (value != cfg.lazyFrameCreationEnabled()) {
cfg.setLazyFrameCreationEnabled(value);
updateLazyFrameIcon();
}
}
void AnimationDocker::slotDropFramesChanged(bool value)
{
KisConfig cfg;
if (value != cfg.animationDropFrames()) {
cfg.setAnimationDropFrames(value);
updateDropFramesIcon();
}
}
void AnimationDocker::slotCurrentNodeChanged(KisNodeSP node)
{
bool isNodeAnimatable = false;
m_newKeyframeMenu->clear();
m_deleteKeyframeMenu->clear();
if (!node.isNull()) {
if (KisAnimationUtils::supportsContentFrames(node)) {
isNodeAnimatable = true;
KisActionManager::safePopulateMenu(m_newKeyframeMenu, "add_blank_frame", m_actionManager);
KisActionManager::safePopulateMenu(m_deleteKeyframeMenu, "remove_frames", m_actionManager);
}
if (node->inherits("KisLayer")) {
isNodeAnimatable = true;
m_newKeyframeMenu->addAction(m_addOpacityKeyframeAction);
m_deleteKeyframeMenu->addAction(m_deleteOpacityKeyframeAction);
}
/*
if (node->inherits("KisTransformMask")) {
isNodeAnimatable = true;
m_newKeyframeMenu->addAction(m_addTransformKeyframeAction);
m_deleteKeyframeMenu->addAction(m_deleteTransformKeyframeAction);
}
*/
}
m_animationWidget->btnAddKeyframe->setEnabled(isNodeAnimatable);
m_animationWidget->btnAddDuplicateFrame->setEnabled(isNodeAnimatable);
m_animationWidget->btnDeleteKeyframe->setEnabled(isNodeAnimatable);
}
+void AnimationDocker::updateClipRange()
+{
+ m_animationWidget->spinFromFrame->setValue(m_canvas->image()->animationInterface()->fullClipRange().start());
+ m_animationWidget->spinToFrame->setValue(m_canvas->image()->animationInterface()->fullClipRange().end());
+}
+
void AnimationDocker::addKeyframe(const QString &channel, bool copy)
{
if (!m_canvas) return;
KisNodeSP node = m_canvas->viewManager()->activeNode();
if (!node) return;
const int time = m_canvas->image()->animationInterface()->currentTime();
KisAnimationUtils::createKeyframeLazy(m_canvas->image(), node, channel, time, copy);
}
void AnimationDocker::deleteKeyframe(const QString &channel)
{
if (!m_canvas) return;
KisNodeSP node = m_canvas->viewManager()->activeNode();
if (!node) return;
const int time = m_canvas->image()->animationInterface()->currentTime();
KisAnimationUtils::removeKeyframe(m_canvas->image(), node, channel, time);
}
void AnimationDocker::setActions(KisActionManager *actionMan)
{
m_actionManager = actionMan;
if (!m_actionManager) return;
m_previousFrameAction = new KisAction(i18n("Previous Frame"), m_animationWidget->btnPreviousFrame);
m_previousFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE);
m_animationWidget->btnPreviousFrame->setDefaultAction(m_previousFrameAction);
m_nextFrameAction = new KisAction(i18n("Next Frame"), m_animationWidget->btnNextFrame);
m_nextFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE);
m_animationWidget->btnNextFrame->setDefaultAction(m_nextFrameAction);
m_previousKeyFrameAction = new KisAction(i18n("Previous Key Frame"), m_animationWidget->btnPreviousKeyFrame);
m_previousKeyFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE);
m_animationWidget->btnPreviousKeyFrame->setDefaultAction(m_previousKeyFrameAction);
m_nextKeyFrameAction = new KisAction(i18n("Next Key Frame"), m_animationWidget->btnNextKeyFrame);
m_nextKeyFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE);
m_animationWidget->btnNextKeyFrame->setDefaultAction(m_nextKeyFrameAction);
m_firstFrameAction = new KisAction(i18n("First Frame"), m_animationWidget->btnFirstFrame);
m_firstFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE);
m_animationWidget->btnFirstFrame->setDefaultAction(m_firstFrameAction);
m_lastFrameAction = new KisAction(i18n("Last Frame"), m_animationWidget->btnLastFrame);
m_lastFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE);
m_animationWidget->btnLastFrame->setDefaultAction(m_lastFrameAction);
m_playPauseAction = new KisAction(i18n("Play / Stop"), m_animationWidget->btnPlay);
m_playPauseAction->setActivationFlags(KisAction::ACTIVE_IMAGE);
m_animationWidget->btnPlay->setDefaultAction(m_playPauseAction);
KisAction *action = 0;
action = m_actionManager->createAction("add_blank_frame");
m_animationWidget->btnAddKeyframe->setDefaultAction(action);
action = m_actionManager->createAction("add_duplicate_frame");
m_animationWidget->btnAddDuplicateFrame->setDefaultAction(action);
action = m_actionManager->createAction("remove_frames");
m_animationWidget->btnDeleteKeyframe->setDefaultAction(action);
m_newKeyframeMenu = new QMenu(this);
m_animationWidget->btnAddKeyframe->setMenu(m_newKeyframeMenu);
m_animationWidget->btnAddKeyframe->setPopupMode(QToolButton::MenuButtonPopup);
m_deleteKeyframeMenu = new QMenu(this);
m_animationWidget->btnDeleteKeyframe->setMenu(m_deleteKeyframeMenu);
m_animationWidget->btnDeleteKeyframe->setPopupMode(QToolButton::MenuButtonPopup);
m_addOpacityKeyframeAction = new KisAction(KisAnimationUtils::addOpacityKeyframeActionName);
m_deleteOpacityKeyframeAction = new KisAction(KisAnimationUtils::removeOpacityKeyframeActionName);
m_addTransformKeyframeAction = new KisAction(KisAnimationUtils::addTransformKeyframeActionName);
m_deleteTransformKeyframeAction = new KisAction(KisAnimationUtils::removeTransformKeyframeActionName);
// other new stuff
m_actionManager->addAction("previous_frame", m_previousFrameAction);
m_actionManager->addAction("next_frame", m_nextFrameAction);
m_actionManager->addAction("previous_keyframe", m_previousKeyFrameAction);
m_actionManager->addAction("next_keyframe", m_nextKeyFrameAction);
m_actionManager->addAction("first_frame", m_firstFrameAction);
m_actionManager->addAction("last_frame", m_lastFrameAction);
{
KisImageConfig cfg;
setupActionButton(KisAnimationUtils::lazyFrameCreationActionName,
KisAction::ACTIVE_IMAGE,
cfg.lazyFrameCreationEnabled(),
m_animationWidget->btnLazyFrame,
&m_lazyFrameAction);
}
{
KisConfig cfg;
setupActionButton(KisAnimationUtils::dropFramesActionName,
KisAction::ACTIVE_IMAGE,
cfg.animationDropFrames(),
m_animationWidget->btnDropFrames,
&m_dropFramesAction);
}
// these actions are created in the setupActionButton() above, so we need to add actions after that
m_actionManager->addAction("lazy_frame", m_lazyFrameAction);
m_actionManager->addAction("drop_frames", m_dropFramesAction);
m_actionManager->addAction("toggle_playback", m_playPauseAction);
QFont font;
font.setPointSize(1.7 * font.pointSize());
font.setBold(true);
m_animationWidget->intCurrentTime->setFont(font);
connect(m_previousFrameAction, SIGNAL(triggered()), this, SLOT(slotPreviousFrame()));
connect(m_nextFrameAction, SIGNAL(triggered()), this, SLOT(slotNextFrame()));
connect(m_previousKeyFrameAction, SIGNAL(triggered()), this, SLOT(slotPreviousKeyFrame()));
connect(m_nextKeyFrameAction, SIGNAL(triggered()), this, SLOT(slotNextKeyFrame()));
connect(m_firstFrameAction, SIGNAL(triggered()), this, SLOT(slotFirstFrame()));
connect(m_lastFrameAction, SIGNAL(triggered()), this, SLOT(slotLastFrame()));
connect(m_playPauseAction, SIGNAL(triggered()), this, SLOT(slotPlayPause()));
connect(m_lazyFrameAction, SIGNAL(toggled(bool)), this, SLOT(slotLazyFrameChanged(bool)));
connect(m_dropFramesAction, SIGNAL(toggled(bool)), this, SLOT(slotDropFramesChanged(bool)));
connect(m_addOpacityKeyframeAction, SIGNAL(triggered(bool)), this, SLOT(slotAddOpacityKeyframe()));
connect(m_deleteOpacityKeyframeAction, SIGNAL(triggered(bool)), this, SLOT(slotDeleteOpacityKeyframe()));
connect(m_addTransformKeyframeAction, SIGNAL(triggered(bool)), this, SLOT(slotAddTransformKeyframe()));
connect(m_deleteTransformKeyframeAction, SIGNAL(triggered(bool)), this, SLOT(slotDeleteTransformKeyframe()));
m_animationWidget->btnOnionSkinOptions->setToolTip(i18n("Onion Skins"));
connect(m_animationWidget->btnOnionSkinOptions, SIGNAL(clicked()), this, SLOT(slotOnionSkinOptions()));
connect(m_animationWidget->spinFromFrame, SIGNAL(valueChanged(int)), this, SLOT(slotUIRangeChanged()));
connect(m_animationWidget->spinToFrame, SIGNAL(valueChanged(int)), this, SLOT(slotUIRangeChanged()));
connect(m_animationWidget->intFramerate, SIGNAL(valueChanged(int)), this, SLOT(slotUIFramerateChanged()));
connect(m_animationWidget->intCurrentTime, SIGNAL(valueChanged(int)), SLOT(slotTimeSpinBoxChanged()));
}
diff --git a/plugins/dockers/animation/animation_docker.h b/plugins/dockers/animation/animation_docker.h
index 04ff6e77bf..6f4f7355d7 100644
--- a/plugins/dockers/animation/animation_docker.h
+++ b/plugins/dockers/animation/animation_docker.h
@@ -1,117 +1,119 @@
/*
* Copyright (c) 2015 Jouni Pentikäinen
*
* 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) 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef _ANIMATION_DOCKER_H_
#define _ANIMATION_DOCKER_H_
#include "kritaimage_export.h"
#include
#include
#include
#include
#include
class Ui_WdgAnimation;
class KisMainWindow;
class AnimationDocker : public QDockWidget, public KisMainwindowObserver {
Q_OBJECT
public:
AnimationDocker();
~AnimationDocker() override;
QString observerName() override { return "AnimationDocker"; }
void setCanvas(KoCanvasBase *canvas) override;
void unsetCanvas() override;
void setMainWindow(KisViewManager *kisview) override;
private Q_SLOTS:
void slotPreviousFrame();
void slotNextFrame();
void slotPreviousKeyFrame();
void slotNextKeyFrame();
void slotFirstFrame();
void slotLastFrame();
void slotPlayPause();
void slotAddOpacityKeyframe();
void slotDeleteOpacityKeyframe();
void slotAddTransformKeyframe();
void slotDeleteTransformKeyframe();
void slotUIRangeChanged();
void slotUIFramerateChanged();
void slotUpdateIcons();
void slotOnionSkinOptions();
void slotGlobalTimeChanged();
void slotTimeSpinBoxChanged();
void updatePlayPauseIcon();
void updateLazyFrameIcon();
void updateDropFramesIcon();
void slotLazyFrameChanged(bool value);
void slotDropFramesChanged(bool value);
void slotCurrentNodeChanged(KisNodeSP node);
+ void updateClipRange();
+
private:
QPointer m_canvas;
QPointer m_actionManager;
Ui_WdgAnimation *m_animationWidget;
KisAction *m_previousFrameAction;
KisAction *m_nextFrameAction;
KisAction *m_previousKeyFrameAction;
KisAction *m_nextKeyFrameAction;
KisAction *m_firstFrameAction;
KisAction *m_lastFrameAction;
KisAction *m_playPauseAction;
KisAction *m_lazyFrameAction;
KisAction *m_dropFramesAction;
QMenu *m_newKeyframeMenu;
KisAction *m_addOpacityKeyframeAction;
KisAction *m_addTransformKeyframeAction;
QMenu *m_deleteKeyframeMenu;
KisAction *m_deleteOpacityKeyframeAction;
KisAction *m_deleteTransformKeyframeAction;
KisMainWindow *m_mainWindow;
void addKeyframe(const QString &channel, bool copy);
void deleteKeyframe(const QString &channel);
void setActions(KisActionManager* actionManager);
};
#endif
diff --git a/plugins/dockers/animation/timeline_frames_model.cpp b/plugins/dockers/animation/timeline_frames_model.cpp
index 6d01a24212..b7b906d908 100644
--- a/plugins/dockers/animation/timeline_frames_model.cpp
+++ b/plugins/dockers/animation/timeline_frames_model.cpp
@@ -1,941 +1,951 @@
/*
* Copyright (c) 2015 Dmitry Kazakov
*
* 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) 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "timeline_frames_model.h"
#include
#include
#include
#include
#include
#include
#include "kis_layer.h"
#include "kis_config.h"
#include "kis_global.h"
#include "kis_debug.h"
#include "kis_image.h"
#include "kis_image_animation_interface.h"
#include "kis_undo_adapter.h"
#include "kis_node_dummies_graph.h"
#include "kis_dummies_facade_base.h"
#include "kis_signal_compressor.h"
#include "kis_signal_compressor_with_param.h"
#include "kis_keyframe_channel.h"
#include "kundo2command.h"
#include "kis_post_execution_undo_adapter.h"
#include
#include
#include "kis_animation_utils.h"
#include "timeline_color_scheme.h"
#include "kis_node_model.h"
#include "kis_projection_leaf.h"
#include "kis_time_range.h"
#include "kis_node_view_color_scheme.h"
#include "krita_utils.h"
#include
#include "kis_processing_applicator.h"
#include
#include "kis_node_uuid_info.h"
struct TimelineFramesModel::Private
{
Private()
: activeLayerIndex(0),
dummiesFacade(0),
needFinishInsertRows(false),
needFinishRemoveRows(false),
updateTimer(200, KisSignalCompressor::FIRST_INACTIVE),
parentOfRemovedNode(0)
{}
int activeLayerIndex;
QPointer dummiesFacade;
KisImageWSP image;
bool needFinishInsertRows;
bool needFinishRemoveRows;
QList updateQueue;
KisSignalCompressor updateTimer;
KisNodeDummy* parentOfRemovedNode;
QScopedPointer converter;
QScopedPointer nodeInterface;
QPersistentModelIndex lastClickedIndex;
QVariant layerName(int row) const {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return QVariant();
return dummy->node()->name();
}
bool layerEditable(int row) const {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return true;
return dummy->node()->visible() && !dummy->node()->userLocked();
}
bool frameExists(int row, int column) const {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return false;
KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id());
return (primaryChannel && primaryChannel->keyframeAt(column));
}
bool specialKeyframeExists(int row, int column) {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return false;
Q_FOREACH(KisKeyframeChannel *channel, dummy->node()->keyframeChannels()) {
if (channel->id() != KisKeyframeChannel::Content.id() && channel->keyframeAt(column)) {
return true;
}
}
return false;
}
int frameColorLabel(int row, int column) {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return -1;
KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id());
if (!primaryChannel) return -1;
KisKeyframeSP frame = primaryChannel->keyframeAt(column);
if (!frame) return -1;
return frame->colorLabel();
}
void setFrameColorLabel(int row, int column, int color) {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return;
KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id());
if (!primaryChannel) return;
KisKeyframeSP frame = primaryChannel->keyframeAt(column);
if (!frame) return;
frame->setColorLabel(color);
}
int layerColorLabel(int row) const {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return -1;
return dummy->node()->colorLabelIndex();
}
QVariant layerProperties(int row) const {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return QVariant();
PropertyList props = dummy->node()->sectionModelProperties();
return QVariant::fromValue(props);
}
bool setLayerProperties(int row, PropertyList props) {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return false;
KisNodePropertyListCommand::setNodePropertiesNoUndo(dummy->node(), image, props);
return true;
}
bool addKeyframe(int row, int column, bool copy) {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return false;
KisNodeSP node = dummy->node();
if (!KisAnimationUtils::supportsContentFrames(node)) return false;
KisAnimationUtils::createKeyframeLazy(image, node, KisKeyframeChannel::Content.id(), column, copy);
return true;
}
bool addNewLayer(int row) {
Q_UNUSED(row);
if (nodeInterface) {
KisLayerSP layer = nodeInterface->addPaintLayer();
layer->setUseInTimeline(true);
}
return true;
}
bool removeLayer(int row) {
KisNodeDummy *dummy = converter->dummyFromRow(row);
if (!dummy) return false;
if (nodeInterface) {
nodeInterface->removeNode(dummy->node());
}
return true;
}
};
TimelineFramesModel::TimelineFramesModel(QObject *parent)
: ModelWithExternalNotifications(parent),
m_d(new Private)
{
connect(&m_d->updateTimer, SIGNAL(timeout()), SLOT(processUpdateQueue()));
}
TimelineFramesModel::~TimelineFramesModel()
{
}
bool TimelineFramesModel::hasConnectionToCanvas() const
{
return m_d->dummiesFacade;
}
void TimelineFramesModel::setNodeManipulationInterface(NodeManipulationInterface *iface)
{
m_d->nodeInterface.reset(iface);
}
KisNodeSP TimelineFramesModel::nodeAt(QModelIndex index) const
{
/**
* The dummy might not exist because the user could (quickly) change
* active layer and the list of the nodes in m_d->converter will change.
*/
KisNodeDummy *dummy = m_d->converter->dummyFromRow(index.row());
return dummy ? dummy->node() : 0;
}
QMap TimelineFramesModel::channelsAt(QModelIndex index) const
{
KisNodeDummy *srcDummy = m_d->converter->dummyFromRow(index.row());
return srcDummy->node()->keyframeChannels();
}
void TimelineFramesModel::setDummiesFacade(KisDummiesFacadeBase *dummiesFacade, KisImageSP image)
{
KisDummiesFacadeBase *oldDummiesFacade = m_d->dummiesFacade;
if (m_d->dummiesFacade && m_d->image) {
m_d->image->animationInterface()->disconnect(this);
m_d->image->disconnect(this);
m_d->dummiesFacade->disconnect(this);
}
m_d->image = image;
KisTimeBasedItemModel::setImage(image);
m_d->dummiesFacade = dummiesFacade;
m_d->converter.reset();
if (m_d->dummiesFacade) {
m_d->converter.reset(new TimelineNodeListKeeper(this, m_d->dummiesFacade));
connect(m_d->dummiesFacade, SIGNAL(sigDummyChanged(KisNodeDummy*)),
SLOT(slotDummyChanged(KisNodeDummy*)));
connect(m_d->image->animationInterface(),
SIGNAL(sigFullClipRangeChanged()), SIGNAL(sigInfiniteTimelineUpdateNeeded()));
connect(m_d->image->animationInterface(),
SIGNAL(sigAudioChannelChanged()), SIGNAL(sigAudioChannelChanged()));
connect(m_d->image->animationInterface(),
SIGNAL(sigAudioVolumeChanged()), SIGNAL(sigAudioChannelChanged()));
}
if (m_d->dummiesFacade != oldDummiesFacade) {
beginResetModel();
endResetModel();
}
if (m_d->dummiesFacade) {
emit sigInfiniteTimelineUpdateNeeded();
emit sigAudioChannelChanged();
}
}
void TimelineFramesModel::slotDummyChanged(KisNodeDummy *dummy)
{
if (!m_d->updateQueue.contains(dummy)) {
m_d->updateQueue.append(dummy);
}
m_d->updateTimer.start();
}
void TimelineFramesModel::processUpdateQueue()
{
Q_FOREACH (KisNodeDummy *dummy, m_d->updateQueue) {
int row = m_d->converter->rowForDummy(dummy);
if (row >= 0) {
emit headerDataChanged (Qt::Vertical, row, row);
emit dataChanged(this->index(row, 0), this->index(row, columnCount() - 1));
}
}
m_d->updateQueue.clear();
}
void TimelineFramesModel::slotCurrentNodeChanged(KisNodeSP node)
{
if (!node) {
m_d->activeLayerIndex = -1;
return;
}
KisNodeDummy *dummy = m_d->dummiesFacade->dummyForNode(node);
if (!dummy) {
// It's perfectly normal that dummyForNode returns 0; that happens
// when views get activated while Krita is closing down.
return;
}
m_d->converter->updateActiveDummy(dummy);
const int row = m_d->converter->rowForDummy(dummy);
if (row < 0) {
qWarning() << "WARNING: TimelineFramesModel::slotCurrentNodeChanged: node not found!";
}
if (row >= 0 && m_d->activeLayerIndex != row) {
setData(index(row, 0), true, ActiveLayerRole);
}
}
int TimelineFramesModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
if(!m_d->dummiesFacade) return 0;
return m_d->converter->rowCount();
}
QVariant TimelineFramesModel::data(const QModelIndex &index, int role) const
{
if(!m_d->dummiesFacade) return QVariant();
switch (role) {
case ActiveLayerRole: {
return index.row() == m_d->activeLayerIndex;
}
case FrameEditableRole: {
return m_d->layerEditable(index.row());
}
case FrameExistsRole: {
return m_d->frameExists(index.row(), index.column());
}
case SpecialKeyframeExists: {
return m_d->specialKeyframeExists(index.row(), index.column());
}
case FrameColorLabelIndexRole: {
int label = m_d->frameColorLabel(index.row(), index.column());
return label > 0 ? label : QVariant();
}
case Qt::DisplayRole: {
return m_d->layerName(index.row());
}
case Qt::TextAlignmentRole: {
return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
}
case KoResourceModel::LargeThumbnailRole: {
KisNodeDummy *dummy = m_d->converter->dummyFromRow(index.row());
if (!dummy) {
return QVariant();
}
const int maxSize = 200;
QSize size = dummy->node()->extent().size();
size.scale(maxSize, maxSize, Qt::KeepAspectRatio);
if (size.width() == 0 || size.height() == 0) {
// No thumbnail can be shown if there isn't width or height...
return QVariant();
}
QImage image(dummy->node()->createThumbnailForFrame(size.width(), size.height(), index.column()));
return image;
}
}
return ModelWithExternalNotifications::data(index, role);
}
bool TimelineFramesModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || !m_d->dummiesFacade) return false;
switch (role) {
case ActiveLayerRole: {
if (value.toBool() &&
index.row() != m_d->activeLayerIndex) {
int prevLayer = m_d->activeLayerIndex;
m_d->activeLayerIndex = index.row();
emit dataChanged(this->index(prevLayer, 0), this->index(prevLayer, columnCount() - 1));
emit dataChanged(this->index(m_d->activeLayerIndex, 0), this->index(m_d->activeLayerIndex, columnCount() - 1));
emit headerDataChanged(Qt::Vertical, prevLayer, prevLayer);
emit headerDataChanged(Qt::Vertical, m_d->activeLayerIndex, m_d->activeLayerIndex);
KisNodeDummy *dummy = m_d->converter->dummyFromRow(m_d->activeLayerIndex);
KIS_ASSERT_RECOVER(dummy) { return true; }
emit requestCurrentNodeChanged(dummy->node());
emit sigEnsureRowVisible(m_d->activeLayerIndex);
}
break;
}
case FrameColorLabelIndexRole: {
m_d->setFrameColorLabel(index.row(), index.column(), value.toInt());
}
break;
}
return ModelWithExternalNotifications::setData(index, value, role);
}
QVariant TimelineFramesModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if(!m_d->dummiesFacade) return QVariant();
if (orientation == Qt::Vertical) {
switch (role) {
case ActiveLayerRole:
return section == m_d->activeLayerIndex;
case Qt::DisplayRole: {
QVariant value = headerData(section, orientation, Qt::ToolTipRole);
if (!value.isValid()) return value;
QString name = value.toString();
const int maxNameSize = 13;
if (name.size() > maxNameSize) {
name = QString("%1...").arg(name.left(maxNameSize));
}
return name;
}
case Qt::TextColorRole: {
// WARNING: this role doesn't work for header views! Use
// bold font to show isolated mode instead!
return QVariant();
}
case Qt::FontRole: {
KisNodeDummy *dummy = m_d->converter->dummyFromRow(section);
if (!dummy) return QVariant();
KisNodeSP node = dummy->node();
QFont baseFont;
if (node->projectionLeaf()->isDroppedMask()) {
baseFont.setStrikeOut(true);
} else if (m_d->image && m_d->image->isolatedModeRoot() &&
KisNodeModel::belongsToIsolatedGroup(m_d->image, node, m_d->dummiesFacade)) {
baseFont.setBold(true);
}
return baseFont;
}
case Qt::ToolTipRole: {
return m_d->layerName(section);
}
case TimelinePropertiesRole: {
return QVariant::fromValue(m_d->layerProperties(section));
}
case OtherLayersRole: {
TimelineNodeListKeeper::OtherLayersList list =
m_d->converter->otherLayersList();
return QVariant::fromValue(list);
}
case LayerUsedInTimelineRole: {
KisNodeDummy *dummy = m_d->converter->dummyFromRow(section);
if (!dummy) return QVariant();
return dummy->node()->useInTimeline();
}
case Qt::BackgroundRole: {
int label = m_d->layerColorLabel(section);
if (label > 0) {
KisNodeViewColorScheme scm;
QColor color = scm.colorLabel(label);
QPalette pal = qApp->palette();
color = KritaUtils::blendColors(color, pal.color(QPalette::Button), 0.3);
return QBrush(color);
} else {
return QVariant();
}
}
}
}
return ModelWithExternalNotifications::headerData(section, orientation, role);
}
bool TimelineFramesModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role)
{
if (!m_d->dummiesFacade) return false;
if (orientation == Qt::Vertical) {
switch (role) {
case ActiveLayerRole: {
setData(index(section, 0), value, role);
break;
}
case TimelinePropertiesRole: {
TimelineFramesModel::PropertyList props = value.value();
int result = m_d->setLayerProperties(section, props);
emit headerDataChanged (Qt::Vertical, section, section);
return result;
}
case LayerUsedInTimelineRole: {
KisNodeDummy *dummy = m_d->converter->dummyFromRow(section);
if (!dummy) return false;
dummy->node()->setUseInTimeline(value.toBool());
return true;
}
}
}
return ModelWithExternalNotifications::setHeaderData(section, orientation, value, role);
}
Qt::DropActions TimelineFramesModel::supportedDragActions() const
{
return Qt::MoveAction | Qt::CopyAction;
}
Qt::DropActions TimelineFramesModel::supportedDropActions() const
{
return Qt::MoveAction | Qt::CopyAction;
}
QStringList TimelineFramesModel::mimeTypes() const
{
QStringList types;
types << QLatin1String("application/x-krita-frame");
return types;
}
void TimelineFramesModel::setLastClickedIndex(const QModelIndex &index)
{
m_d->lastClickedIndex = index;
}
QMimeData* TimelineFramesModel::mimeData(const QModelIndexList &indexes) const
{
return mimeDataExtended(indexes, m_d->lastClickedIndex, UndefinedPolicy);
}
QMimeData *TimelineFramesModel::mimeDataExtended(const QModelIndexList &indexes,
const QModelIndex &baseIndex,
TimelineFramesModel::MimeCopyPolicy copyPolicy) const
{
QMimeData *data = new QMimeData();
QByteArray encoded;
QDataStream stream(&encoded, QIODevice::WriteOnly);
const int baseRow = baseIndex.row();
const int baseColumn = baseIndex.column();
stream << indexes.size();
stream << baseRow << baseColumn;
Q_FOREACH (const QModelIndex &index, indexes) {
KisNodeSP node = nodeAt(index);
KIS_SAFE_ASSERT_RECOVER(node) { continue; }
stream << index.row() - baseRow << index.column() - baseColumn;
const QByteArray uuidData = node->uuid().toRfc4122();
stream << int(uuidData.size());
stream.writeRawData(uuidData.data(), uuidData.size());
}
stream << int(copyPolicy);
data->setData("application/x-krita-frame", encoded);
return data;
}
inline void decodeBaseIndex(QByteArray *encoded, int *row, int *col)
{
int size_UNUSED = 0;
QDataStream stream(encoded, QIODevice::ReadOnly);
stream >> size_UNUSED >> *row >> *col;
}
bool TimelineFramesModel::canDropFrameData(const QMimeData */*data*/, const QModelIndex &index)
{
if (!index.isValid()) return false;
/**
* Now we support D&D around any layer, so just return 'true' all
* the time.
*/
return true;
}
bool TimelineFramesModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
Q_UNUSED(row);
Q_UNUSED(column);
return dropMimeDataExtended(data, action, parent);
}
bool TimelineFramesModel::dropMimeDataExtended(const QMimeData *data, Qt::DropAction action, const QModelIndex &parent, bool *dataMoved)
{
bool result = false;
if ((action != Qt::MoveAction && action != Qt::CopyAction) ||
!parent.isValid()) return result;
QByteArray encoded = data->data("application/x-krita-frame");
QDataStream stream(&encoded, QIODevice::ReadOnly);
int size, baseRow, baseColumn;
stream >> size >> baseRow >> baseColumn;
const QPoint offset(parent.column() - baseColumn, parent.row() - baseRow);
KisAnimationUtils::FrameMovePairList frameMoves;
for (int i = 0; i < size; i++) {
int relRow, relColumn;
stream >> relRow >> relColumn;
const int srcRow = baseRow + relRow;
const int srcColumn = baseColumn + relColumn;
int uuidLen = 0;
stream >> uuidLen;
QByteArray uuidData(uuidLen, '\0');
stream.readRawData(uuidData.data(), uuidLen);
QUuid nodeUuid = QUuid::fromRfc4122(uuidData);
KisNodeSP srcNode;
if (!nodeUuid.isNull()) {
KisNodeUuidInfo nodeInfo(nodeUuid);
srcNode = nodeInfo.findNode(m_d->image->root());
} else {
QModelIndex index = this->index(srcRow, srcColumn);
srcNode = nodeAt(index);
}
KIS_SAFE_ASSERT_RECOVER(srcNode) { continue; }
const QModelIndex dstIndex = this->index(srcRow + offset.y(), srcColumn + offset.x());
if (!dstIndex.isValid()) continue;
KisNodeSP dstNode = nodeAt(dstIndex);
KIS_SAFE_ASSERT_RECOVER(dstNode) { continue; }
Q_FOREACH (KisKeyframeChannel *channel, srcNode->keyframeChannels().values()) {
KisAnimationUtils::FrameItem srcItem(srcNode, channel->id(), srcColumn);
KisAnimationUtils::FrameItem dstItem(dstNode, channel->id(), dstIndex.column());
frameMoves << std::make_pair(srcItem, dstItem);
}
}
MimeCopyPolicy copyPolicy = UndefinedPolicy;
if (!stream.atEnd()) {
int value = 0;
stream >> value;
copyPolicy = MimeCopyPolicy(value);
}
const bool copyFrames =
copyPolicy == UndefinedPolicy ?
action == Qt::CopyAction :
copyPolicy == CopyFramesPolicy;
if (dataMoved) {
*dataMoved = !copyFrames;
}
KUndo2Command *cmd = 0;
if (!frameMoves.isEmpty()) {
KisImageBarrierLockerWithFeedback locker(m_d->image);
cmd = KisAnimationUtils::createMoveKeyframesCommand(frameMoves, copyFrames, 0);
}
if (cmd) {
KisProcessingApplicator::runSingleCommandStroke(m_d->image, cmd, KisStrokeJobData::BARRIER);
}
return cmd;
}
Qt::ItemFlags TimelineFramesModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags flags = ModelWithExternalNotifications::flags(index);
if (!index.isValid()) return flags;
if (m_d->frameExists(index.row(), index.column()) || m_d->specialKeyframeExists(index.row(), index.column())) {
if (data(index, FrameEditableRole).toBool()) {
flags |= Qt::ItemIsDragEnabled;
}
}
/**
* Basically we should forbid overrides only if we D&D a single frame
* and allow it when we D&D multiple frames. But we cannot distinguish
* it here... So allow all the time.
*/
flags |= Qt::ItemIsDropEnabled;
return flags;
}
bool TimelineFramesModel::insertRows(int row, int count, const QModelIndex &parent)
{
Q_UNUSED(parent);
KIS_ASSERT_RECOVER(count == 1) { return false; }
if (row < 0 || row > rowCount()) return false;
bool result = m_d->addNewLayer(row);
return result;
}
bool TimelineFramesModel::removeRows(int row, int count, const QModelIndex &parent)
{
Q_UNUSED(parent);
KIS_ASSERT_RECOVER(count == 1) { return false; }
if (row < 0 || row >= rowCount()) return false;
bool result = m_d->removeLayer(row);
return result;
}
bool TimelineFramesModel::insertOtherLayer(int index, int dstRow)
{
Q_UNUSED(dstRow);
TimelineNodeListKeeper::OtherLayersList list =
m_d->converter->otherLayersList();
if (index < 0 || index >= list.size()) return false;
list[index].dummy->node()->setUseInTimeline(true);
dstRow = m_d->converter->rowForDummy(list[index].dummy);
setData(this->index(dstRow, 0), true, ActiveLayerRole);
return true;
}
int TimelineFramesModel::activeLayerRow() const
{
return m_d->activeLayerIndex;
}
bool TimelineFramesModel::createFrame(const QModelIndex &dstIndex)
{
if (!dstIndex.isValid()) return false;
return m_d->addKeyframe(dstIndex.row(), dstIndex.column(), false);
}
bool TimelineFramesModel::copyFrame(const QModelIndex &dstIndex)
{
if (!dstIndex.isValid()) return false;
return m_d->addKeyframe(dstIndex.row(), dstIndex.column(), true);
}
bool TimelineFramesModel::insertFrames(int dstColumn, const QList &dstRows, int count)
{
if (dstRows.isEmpty() || count <= 0) return true;
KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Insert frame", "Insert %1 frames", count));
{
KisImageBarrierLockerWithFeedback locker(m_d->image);
QModelIndexList indexes;
Q_FOREACH (int row, dstRows) {
for (int column = dstColumn; column < columnCount(); column++) {
indexes << index(row, column);
}
}
setLastVisibleFrame(columnCount() + count - 1);
createOffsetFramesCommand(indexes, QPoint(count, 0), false, parentCommand);
Q_FOREACH (int row, dstRows) {
KisNodeDummy *dummy = m_d->converter->dummyFromRow(row);
if (!dummy) continue;
KisNodeSP node = dummy->node();
if (!KisAnimationUtils::supportsContentFrames(node)) continue;
for (int column = dstColumn; column < dstColumn + count; column++) {
KisAnimationUtils::createKeyframeCommand(m_d->image, node, KisKeyframeChannel::Content.id(), column, false, parentCommand);
}
}
const int oldTime = m_d->image->animationInterface()->currentUITime();
const int newTime = dstColumn > oldTime ? dstColumn : dstColumn + count - 1;
new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(),
oldTime,
newTime, parentCommand);
}
KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, KisStrokeJobData::BARRIER);
return true;
}
bool TimelineFramesModel::insertHoldFrames(QModelIndexList selectedIndexes, int count)
{
if (selectedIndexes.isEmpty() || count == 0) return true;
QScopedPointer parentCommand(new KUndo2Command(kundo2_i18np("Insert frame", "Insert %1 frames", count)));
{
KisImageBarrierLockerWithFeedback locker(m_d->image);
QSet uniqueKeyframesInSelection;
Q_FOREACH (const QModelIndex &index, selectedIndexes) {
KisNodeSP node = nodeAt(index);
KIS_SAFE_ASSERT_RECOVER(node) { continue; }
KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id());
if (!channel) continue;
KisKeyframeSP keyFrame = channel->activeKeyframeAt(index.column());
if (keyFrame) {
uniqueKeyframesInSelection.insert(keyFrame);
}
}
int minSelectedTime = std::numeric_limits::max();
QList keyframesToMove;
for (auto it = uniqueKeyframesInSelection.begin(); it != uniqueKeyframesInSelection.end(); ++it) {
KisKeyframeSP keyframe = *it;
minSelectedTime = qMin(minSelectedTime, keyframe->time());
KisKeyframeChannel *channel = keyframe->channel();
KisKeyframeSP nextKeyframe = channel->nextKeyframe(keyframe);
if (nextKeyframe) {
keyframesToMove << nextKeyframe;
}
}
std::sort(keyframesToMove.begin(), keyframesToMove.end(),
[] (KisKeyframeSP lhs, KisKeyframeSP rhs) {
return lhs->time() > rhs->time();
});
if (keyframesToMove.isEmpty()) return true;
const int maxColumn = columnCount();
if (count > 0) {
setLastVisibleFrame(columnCount() + count);
}
Q_FOREACH (KisKeyframeSP keyframe, keyframesToMove) {
int plannedFrameMove = count;
if (count < 0) {
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(keyframe->time() > 0, false);
KisKeyframeSP prevFrame = keyframe->channel()->previousKeyframe(keyframe);
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(prevFrame, false);
plannedFrameMove = qMax(count, prevFrame->time() - keyframe->time() + 1);
}
KisNodeDummy *dummy = m_d->dummiesFacade->dummyForNode(keyframe->channel()->node());
KIS_SAFE_ASSERT_RECOVER(dummy) { continue; }
const int row = m_d->converter->rowForDummy(dummy);
KIS_SAFE_ASSERT_RECOVER(row >= 0) { continue; }
QModelIndexList indexes;
for (int column = keyframe->time(); column < maxColumn; column++) {
indexes << index(row, column);
}
createOffsetFramesCommand(indexes, QPoint(plannedFrameMove, 0), false, parentCommand.data());
}
const int oldTime = m_d->image->animationInterface()->currentUITime();
const int newTime = minSelectedTime;
new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(),
oldTime,
newTime, parentCommand.data());
}
KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand.take(), KisStrokeJobData::BARRIER);
return true;
}
QString TimelineFramesModel::audioChannelFileName() const
{
return m_d->image ? m_d->image->animationInterface()->audioChannelFileName() : QString();
}
void TimelineFramesModel::setAudioChannelFileName(const QString &fileName)
{
KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->image);
m_d->image->animationInterface()->setAudioChannelFileName(fileName);
}
bool TimelineFramesModel::isAudioMuted() const
{
return m_d->image ? m_d->image->animationInterface()->isAudioMuted() : false;
}
void TimelineFramesModel::setAudioMuted(bool value)
{
KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->image);
m_d->image->animationInterface()->setAudioMuted(value);
}
qreal TimelineFramesModel::audioVolume() const
{
return m_d->image ? m_d->image->animationInterface()->audioVolume() : 0.5;
}
void TimelineFramesModel::setAudioVolume(qreal value)
{
KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->image);
m_d->image->animationInterface()->setAudioVolume(value);
}
+
+void TimelineFramesModel::setFullClipRangeStart(int column)
+{
+ m_d->image->animationInterface()->setFullClipRangeStartTime(column);
+}
+
+void TimelineFramesModel::setFullClipRangeEnd(int column)
+{
+ m_d->image->animationInterface()->setFullClipRangeEndTime(column);
+}
diff --git a/plugins/dockers/animation/timeline_frames_model.h b/plugins/dockers/animation/timeline_frames_model.h
index 7c93cd05dc..fd96f8edad 100644
--- a/plugins/dockers/animation/timeline_frames_model.h
+++ b/plugins/dockers/animation/timeline_frames_model.h
@@ -1,148 +1,151 @@
/*
* Copyright (c) 2015 Dmitry Kazakov
*
* 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) 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef __TIMELINE_FRAMES_MODEL_H
#define __TIMELINE_FRAMES_MODEL_H
#include
#include
#include "kritaanimationdocker_export.h"
#include "kis_node_model.h"
#include "kis_types.h"
#include "kis_node.h"
#include "timeline_node_list_keeper.h"
class KisNodeDummy;
class KisDummiesFacadeBase;
class KisAnimationPlayer;
class KRITAANIMATIONDOCKER_EXPORT TimelineFramesModel : public TimelineNodeListKeeper::ModelWithExternalNotifications
{
Q_OBJECT
public:
enum MimeCopyPolicy {
UndefinedPolicy = 0,
MoveFramesPolicy,
CopyFramesPolicy
};
public:
TimelineFramesModel(QObject *parent);
~TimelineFramesModel() override;
bool hasConnectionToCanvas() const;
void setDummiesFacade(KisDummiesFacadeBase *dummiesFacade, KisImageSP image);
bool canDropFrameData(const QMimeData *data, const QModelIndex &index);
bool insertOtherLayer(int index, int dstRow);
int activeLayerRow() const;
bool createFrame(const QModelIndex &dstIndex);
bool copyFrame(const QModelIndex &dstIndex);
bool insertFrames(int dstColumn, const QList &dstRows, int count);
bool insertHoldFrames(QModelIndexList selectedIndexes, int count);
QString audioChannelFileName() const;
void setAudioChannelFileName(const QString &fileName);
bool isAudioMuted() const;
void setAudioMuted(bool value);
qreal audioVolume() const;
void setAudioVolume(qreal value);
+ void setFullClipRangeStart(int column);
+ void setFullClipRangeEnd(int column);
+
void setLastClickedIndex(const QModelIndex &index);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) override;
Qt::DropActions supportedDragActions() const override;
Qt::DropActions supportedDropActions() const override;
QStringList mimeTypes() const override;
QMimeData * mimeData(const QModelIndexList &indexes) const override;
QMimeData * mimeDataExtended(const QModelIndexList &indexes, const QModelIndex &baseIndex, MimeCopyPolicy copyPolicy) const;
bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
bool dropMimeDataExtended(const QMimeData *data, Qt::DropAction action, const QModelIndex &parent, bool *dataMoved = 0);
Qt::ItemFlags flags(const QModelIndex &index) const override;
bool insertRows(int row, int count, const QModelIndex &parent) override;
bool removeRows(int row, int count, const QModelIndex &parent) override;
enum ItemDataRole
{
ActiveLayerRole = KisTimeBasedItemModel::UserRole,
TimelinePropertiesRole,
OtherLayersRole,
LayerUsedInTimelineRole,
FrameColorLabelIndexRole
};
// metatype is added by the original implementation
typedef KisBaseNode::Property Property;
typedef KisBaseNode::PropertyList PropertyList;
typedef TimelineNodeListKeeper::OtherLayer OtherLayer;
typedef TimelineNodeListKeeper::OtherLayersList OtherLayersList;
struct NodeManipulationInterface {
virtual ~NodeManipulationInterface() {}
virtual KisLayerSP addPaintLayer() const = 0;
virtual void removeNode(KisNodeSP node) const = 0;
};
/**
* NOTE: the model has an ownership over the interface, that is it'll
* be deleted automatically later
*/
void setNodeManipulationInterface(NodeManipulationInterface *iface);
protected:
KisNodeSP nodeAt(QModelIndex index) const override;
QMap channelsAt(QModelIndex index) const override;
private Q_SLOTS:
void slotDummyChanged(KisNodeDummy *dummy);
void processUpdateQueue();
public Q_SLOTS:
void slotCurrentNodeChanged(KisNodeSP node);
Q_SIGNALS:
void requestCurrentNodeChanged(KisNodeSP node);
void sigInfiniteTimelineUpdateNeeded();
void sigAudioChannelChanged();
void sigEnsureRowVisible(int row);
private:
struct Private;
const QScopedPointer m_d;
};
#endif /* __TIMELINE_FRAMES_MODEL_H */
diff --git a/plugins/dockers/animation/timeline_frames_view.cpp b/plugins/dockers/animation/timeline_frames_view.cpp
index 3de5a92a42..48f663c24b 100644
--- a/plugins/dockers/animation/timeline_frames_view.cpp
+++ b/plugins/dockers/animation/timeline_frames_view.cpp
@@ -1,1694 +1,1752 @@
/*
* Copyright (c) 2015 Dmitry Kazakov
*
* 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) 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "timeline_frames_view.h"
#include "timeline_frames_model.h"
#include "timeline_ruler_header.h"
#include "timeline_layers_header.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "KSharedConfig"
#include "kis_debug.h"
#include "timeline_frames_item_delegate.h"
#include "kis_zoom_button.h"
#include "kis_icon_utils.h"
#include "kis_animation_utils.h"
#include "kis_custom_modifiers_catcher.h"
#include "kis_action.h"
#include "kis_signal_compressor.h"
#include "kis_time_range.h"
#include "kis_color_label_selector_widget.h"
#include "kis_slider_spin_box.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include "config-qtmultimedia.h"
typedef QPair QItemViewPaintPair;
typedef QList QItemViewPaintPairs;
struct TimelineFramesView::Private
{
Private(TimelineFramesView *_q)
: q(_q),
fps(1),
zoomStillPointIndex(-1),
zoomStillPointOriginalOffset(0),
dragInProgress(false),
dragWasSuccessful(false),
modifiersCatcher(0),
selectionChangedCompressor(300, KisSignalCompressor::FIRST_INACTIVE)
{}
TimelineFramesView *q;
TimelineFramesModel *model;
TimelineRulerHeader *horizontalRuler;
TimelineLayersHeader *layersHeader;
int fps;
int zoomStillPointIndex;
int zoomStillPointOriginalOffset;
QPoint initialDragPanValue;
QPoint initialDragPanPos;
QToolButton *addLayersButton;
KisAction *showHideLayerAction;
QToolButton *audioOptionsButton;
KisColorLabelSelectorWidget *colorSelector;
QWidgetAction *colorSelectorAction;
KisColorLabelSelectorWidget *multiframeColorSelector;
QWidgetAction *multiframeColorSelectorAction;
QMenu *audioOptionsMenu;
QAction *openAudioAction;
QAction *audioMuteAction;
KisSliderSpinBox *volumeSlider;
QMenu *layerEditingMenu;
QMenu *existingLayersMenu;
KisZoomButton *zoomDragButton;
bool dragInProgress;
bool dragWasSuccessful;
KisCustomModifiersCatcher *modifiersCatcher;
QPoint lastPressedPosition;
Qt::KeyboardModifiers lastPressedModifier;
KisSignalCompressor selectionChangedCompressor;
QStyleOptionViewItem viewOptionsV4() const;
QItemViewPaintPairs draggablePaintPairs(const QModelIndexList &indexes, QRect *r) const;
QPixmap renderToPixmap(const QModelIndexList &indexes, QRect *r) const;
KoIconToolTip tip;
KisActionManager * actionMan = 0;
};
TimelineFramesView::TimelineFramesView(QWidget *parent)
: QTableView(parent),
m_d(new Private(this))
{
m_d->modifiersCatcher = new KisCustomModifiersCatcher(this);
m_d->modifiersCatcher->addModifier("pan-zoom", Qt::Key_Space);
m_d->modifiersCatcher->addModifier("offset-frame", Qt::Key_Alt);
setCornerButtonEnabled(false);
setSelectionBehavior(QAbstractItemView::SelectItems);
setSelectionMode(QAbstractItemView::ExtendedSelection);
setItemDelegate(new TimelineFramesItemDelegate(this));
setDragEnabled(true);
setDragDropMode(QAbstractItemView::DragDrop);
setAcceptDrops(true);
setDropIndicatorShown(true);
setDefaultDropAction(Qt::MoveAction);
m_d->horizontalRuler = new TimelineRulerHeader(this);
this->setHorizontalHeader(m_d->horizontalRuler);
connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnsLeft()), SLOT(slotInsertColumnsLeft()));
connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnsRight()), SLOT(slotInsertColumnsRight()));
connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnsLeftCustom()), SLOT(slotInsertColumnsLeftCustom()));
connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnsRightCustom()), SLOT(slotInsertColumnsRightCustom()));
connect(m_d->horizontalRuler, SIGNAL(sigRemoveColumns()), SLOT(slotRemoveColumns()));
connect(m_d->horizontalRuler, SIGNAL(sigRemoveColumnsAndShift()), SLOT(slotRemoveColumnsAndShift()));
connect(m_d->horizontalRuler, SIGNAL(sigInsertHoldColumns()), SLOT(slotInsertHoldColumns()));
connect(m_d->horizontalRuler, SIGNAL(sigRemoveHoldColumns()), SLOT(slotRemoveHoldColumns()));
connect(m_d->horizontalRuler, SIGNAL(sigInsertHoldColumnsCustom()), SLOT(slotInsertHoldColumnsCustom()));
connect(m_d->horizontalRuler, SIGNAL(sigRemoveHoldColumnsCustom()), SLOT(slotRemoveHoldColumnsCustom()));
connect(m_d->horizontalRuler, SIGNAL(sigMirrorColumns()), SLOT(slotMirrorColumns()));
connect(m_d->horizontalRuler, SIGNAL(sigCopyColumns()), SLOT(slotCopyColumns()));
connect(m_d->horizontalRuler, SIGNAL(sigCutColumns()), SLOT(slotCutColumns()));
connect(m_d->horizontalRuler, SIGNAL(sigPasteColumns()), SLOT(slotPasteColumns()));
m_d->layersHeader = new TimelineLayersHeader(this);
m_d->layersHeader->setSectionResizeMode(QHeaderView::Fixed);
m_d->layersHeader->setDefaultSectionSize(24);
m_d->layersHeader->setMinimumWidth(60);
m_d->layersHeader->setHighlightSections(true);
this->setVerticalHeader(m_d->layersHeader);
connect(horizontalScrollBar(), SIGNAL(valueChanged(int)), SLOT(slotUpdateInfiniteFramesCount()));
connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(slotUpdateInfiniteFramesCount()));
/********** New Layer Menu ***********************************************************/
m_d->addLayersButton = new QToolButton(this);
m_d->addLayersButton->setAutoRaise(true);
m_d->addLayersButton->setIcon(KisIconUtils::loadIcon("addlayer"));
m_d->addLayersButton->setIconSize(QSize(20, 20));
m_d->addLayersButton->setPopupMode(QToolButton::InstantPopup);
m_d->layerEditingMenu = new QMenu(this);
m_d->layerEditingMenu->addAction(KisAnimationUtils::newLayerActionName, this, SLOT(slotAddNewLayer()));
m_d->existingLayersMenu = m_d->layerEditingMenu->addMenu(KisAnimationUtils::addExistingLayerActionName);
m_d->layerEditingMenu->addSeparator();
m_d->layerEditingMenu->addAction(KisAnimationUtils::removeLayerActionName, this, SLOT(slotRemoveLayer()));
connect(m_d->existingLayersMenu, SIGNAL(aboutToShow()), SLOT(slotUpdateLayersMenu()));
connect(m_d->existingLayersMenu, SIGNAL(triggered(QAction*)), SLOT(slotAddExistingLayer(QAction*)));
connect(m_d->layersHeader, SIGNAL(sigRequestContextMenu(const QPoint&)), SLOT(slotLayerContextMenuRequested(const QPoint&)));
m_d->addLayersButton->setMenu(m_d->layerEditingMenu);
/********** Audio Channel Menu *******************************************************/
m_d->audioOptionsButton = new QToolButton(this);
m_d->audioOptionsButton->setAutoRaise(true);
m_d->audioOptionsButton->setIcon(KisIconUtils::loadIcon("audio-none"));
m_d->audioOptionsButton->setIconSize(QSize(20, 20)); // very small on windows if not explicity set
m_d->audioOptionsButton->setPopupMode(QToolButton::InstantPopup);
m_d->audioOptionsMenu = new QMenu(this);
#ifndef HAVE_QT_MULTIMEDIA
m_d->audioOptionsMenu->addSection(i18nc("@item:inmenu", "Audio playback is not supported in this build!"));
#endif
m_d->openAudioAction= new QAction("XXX", this);
connect(m_d->openAudioAction, SIGNAL(triggered()), this, SLOT(slotSelectAudioChannelFile()));
m_d->audioOptionsMenu->addAction(m_d->openAudioAction);
m_d->audioMuteAction = new QAction(i18nc("@item:inmenu", "Mute"), this);
m_d->audioMuteAction->setCheckable(true);
connect(m_d->audioMuteAction, SIGNAL(triggered(bool)), SLOT(slotAudioChannelMute(bool)));
m_d->audioOptionsMenu->addAction(m_d->audioMuteAction);
m_d->audioOptionsMenu->addAction(i18nc("@item:inmenu", "Remove audio"), this, SLOT(slotAudioChannelRemove()));
m_d->audioOptionsMenu->addSeparator();
m_d->volumeSlider = new KisSliderSpinBox(this);
m_d->volumeSlider->setRange(0, 100);
m_d->volumeSlider->setSuffix("%");
m_d->volumeSlider->setPrefix(i18nc("@item:inmenu, slider", "Volume:"));
m_d->volumeSlider->setSingleStep(1);
m_d->volumeSlider->setPageStep(10);
m_d->volumeSlider->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
connect(m_d->volumeSlider, SIGNAL(valueChanged(int)), SLOT(slotAudioVolumeChanged(int)));
QWidgetAction *volumeAction = new QWidgetAction(m_d->audioOptionsMenu);
volumeAction->setDefaultWidget(m_d->volumeSlider);
m_d->audioOptionsMenu->addAction(volumeAction);
m_d->audioOptionsButton->setMenu(m_d->audioOptionsMenu);
/********** Frame Editing Context Menu ***********************************************/
m_d->colorSelector = new KisColorLabelSelectorWidget(this);
m_d->colorSelectorAction = new QWidgetAction(this);
m_d->colorSelectorAction->setDefaultWidget(m_d->colorSelector);
connect(m_d->colorSelector, &KisColorLabelSelectorWidget::currentIndexChanged, this, &TimelineFramesView::slotColorLabelChanged);
m_d->multiframeColorSelector = new KisColorLabelSelectorWidget(this);
m_d->multiframeColorSelectorAction = new QWidgetAction(this);
m_d->multiframeColorSelectorAction->setDefaultWidget(m_d->multiframeColorSelector);
connect(m_d->multiframeColorSelector, &KisColorLabelSelectorWidget::currentIndexChanged, this, &TimelineFramesView::slotColorLabelChanged);
/********** Zoom Button **************************************************************/
m_d->zoomDragButton = new KisZoomButton(this);
m_d->zoomDragButton->setAutoRaise(true);
m_d->zoomDragButton->setIcon(KisIconUtils::loadIcon("zoom-horizontal"));
m_d->zoomDragButton->setIconSize(QSize(20, 20)); // this icon is very small on windows if no explicity set
m_d->zoomDragButton->setToolTip(i18nc("@info:tooltip", "Zoom Timeline. Hold down and drag left or right."));
m_d->zoomDragButton->setPopupMode(QToolButton::InstantPopup);
connect(m_d->zoomDragButton, SIGNAL(zoomLevelChanged(qreal)), SLOT(slotZoomButtonChanged(qreal)));
connect(m_d->zoomDragButton, SIGNAL(zoomStarted(qreal)), SLOT(slotZoomButtonPressed(qreal)));
setFramesPerSecond(12);
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
connect(&m_d->selectionChangedCompressor, SIGNAL(timeout()),
SLOT(slotSelectionChanged()));
connect(&m_d->selectionChangedCompressor, SIGNAL(timeout()),
SLOT(slotUpdateFrameActions()));
{
QClipboard *cb = QApplication::clipboard();
connect(cb, SIGNAL(dataChanged()), SLOT(slotUpdateFrameActions()));
}
}
TimelineFramesView::~TimelineFramesView()
{
}
void TimelineFramesView::setShowInTimeline(KisAction* action)
{
m_d->showHideLayerAction = action;
m_d->layerEditingMenu->addAction(m_d->showHideLayerAction);
}
void TimelineFramesView::setActionManager( KisActionManager * actionManager)
{
m_d->actionMan = actionManager;
m_d->horizontalRuler->setActionManager(actionManager);
if (actionManager) {
KisAction *action = 0;
action = m_d->actionMan->createAction("add_blank_frame");
connect(action, SIGNAL(triggered()), SLOT(slotNewFrame()));
action = m_d->actionMan->createAction("add_duplicate_frame");
connect(action, SIGNAL(triggered()), SLOT(slotCopyFrame()));
action = m_d->actionMan->createAction("insert_keyframes_right");
connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframesRight()));
action = m_d->actionMan->createAction("insert_n_keyframes_right");
connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframesRightCustom()));
action = m_d->actionMan->createAction("insert_keyframes_left");
connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframesLeft()));
action = m_d->actionMan->createAction("insert_n_keyframes_left");
connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframesLeftCustom()));
action = m_d->actionMan->createAction("remove_frames_and_pull");
connect(action, SIGNAL(triggered()), SLOT(slotRemoveFramesAndShift()));
action = m_d->actionMan->createAction("remove_frames");
connect(action, SIGNAL(triggered()), SLOT(slotRemoveFrame()));
action = m_d->actionMan->createAction("insert_hold_frame");
connect(action, SIGNAL(triggered()), SLOT(slotInsertHoldFrames()));
action = m_d->actionMan->createAction("insert_n_hold_frames");
connect(action, SIGNAL(triggered()), SLOT(slotInsertHoldFramesCustom()));
action = m_d->actionMan->createAction("remove_hold_frame");
connect(action, SIGNAL(triggered()), SLOT(slotRemoveHoldFrames()));
action = m_d->actionMan->createAction("remove_n_hold_frames");
connect(action, SIGNAL(triggered()), SLOT(slotRemoveHoldFramesCustom()));
action = m_d->actionMan->createAction("mirror_frames");
connect(action, SIGNAL(triggered()), SLOT(slotMirrorFrames()));
action = m_d->actionMan->createAction("copy_frames_to_clipboard");
connect(action, SIGNAL(triggered()), SLOT(slotCopyFrames()));
action = m_d->actionMan->createAction("cut_frames_to_clipboard");
connect(action, SIGNAL(triggered()), SLOT(slotCutFrames()));
action = m_d->actionMan->createAction("paste_frames_from_clipboard");
connect(action, SIGNAL(triggered()), SLOT(slotPasteFrames()));
+ action = m_d->actionMan->createAction("set_start_time");
+ connect(action, SIGNAL(triggered()), SLOT(slotSetStartTimeToCurrentPosition()));
+
+ action = m_d->actionMan->createAction("set_end_time");
+ connect(action, SIGNAL(triggered()), SLOT(slotSetEndTimeToCurrentPosition()));
+
+ action = m_d->actionMan->createAction("update_playback_range");
+ connect(action, SIGNAL(triggered()), SLOT(slotUpdatePlackbackRange()));
+
+
+
}
}
void resizeToMinimalSize(QAbstractButton *w, int minimalSize) {
QSize buttonSize = w->sizeHint();
if (buttonSize.height() > minimalSize) {
buttonSize = QSize(minimalSize, minimalSize);
}
w->resize(buttonSize);
}
void TimelineFramesView::updateGeometries()
{
QTableView::updateGeometries();
const int availableHeight = m_d->horizontalRuler->height();
const int margin = 2;
const int minimalSize = availableHeight - 2 * margin;
resizeToMinimalSize(m_d->addLayersButton, minimalSize);
resizeToMinimalSize(m_d->audioOptionsButton, minimalSize);
resizeToMinimalSize(m_d->zoomDragButton, minimalSize);
int x = 2 * margin;
int y = (availableHeight - minimalSize) / 2;
m_d->addLayersButton->move(x, 2 * y);
m_d->audioOptionsButton->move(x + minimalSize + 2 * margin, 2 * y);
const int availableWidth = m_d->layersHeader->width();
x = availableWidth - margin - minimalSize;
m_d->zoomDragButton->move(x, 2 * y);
}
void TimelineFramesView::setModel(QAbstractItemModel *model)
{
TimelineFramesModel *framesModel = qobject_cast(model);
m_d->model = framesModel;
QTableView::setModel(model);
connect(m_d->model, SIGNAL(headerDataChanged(Qt::Orientation, int, int)),
this, SLOT(slotHeaderDataChanged(Qt::Orientation, int, int)));
connect(m_d->model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
this, SLOT(slotDataChanged(QModelIndex,QModelIndex)));
connect(m_d->model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
this, SLOT(slotReselectCurrentIndex()));
connect(m_d->model, SIGNAL(sigInfiniteTimelineUpdateNeeded()),
this, SLOT(slotUpdateInfiniteFramesCount()));
connect(m_d->model, SIGNAL(sigAudioChannelChanged()),
this, SLOT(slotUpdateAudioActions()));
connect(selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
&m_d->selectionChangedCompressor, SLOT(start()));
connect(m_d->model, SIGNAL(sigEnsureRowVisible(int)), SLOT(slotEnsureRowVisible(int)));
slotUpdateAudioActions();
}
void TimelineFramesView::setFramesPerSecond(int fps)
{
m_d->fps = fps;
m_d->horizontalRuler->setFramePerSecond(fps);
// For some reason simple update sometimes doesn't work here, so
// reset the whole header
//
// m_d->horizontalRuler->reset();
}
void TimelineFramesView::slotZoomButtonPressed(qreal staticPoint)
{
m_d->zoomStillPointIndex =
qIsNaN(staticPoint) ? currentIndex().column() : staticPoint;
const int w = m_d->horizontalRuler->defaultSectionSize();
m_d->zoomStillPointOriginalOffset =
w * m_d->zoomStillPointIndex -
horizontalScrollBar()->value();
}
void TimelineFramesView::slotZoomButtonChanged(qreal zoomLevel)
{
if (m_d->horizontalRuler->setZoom(zoomLevel)) {
slotUpdateInfiniteFramesCount();
const int w = m_d->horizontalRuler->defaultSectionSize();
horizontalScrollBar()->setValue(w * m_d->zoomStillPointIndex - m_d->zoomStillPointOriginalOffset);
viewport()->update();
}
}
void TimelineFramesView::slotColorLabelChanged(int label)
{
Q_FOREACH(QModelIndex index, selectedIndexes()) {
m_d->model->setData(index, label, TimelineFramesModel::FrameColorLabelIndexRole);
}
KisImageConfig config;
config.setDefaultFrameColorLabel(label);
}
void TimelineFramesView::slotSelectAudioChannelFile()
{
if (!m_d->model) return;
QString defaultDir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
const QString currentFile = m_d->model->audioChannelFileName();
QDir baseDir = QFileInfo(currentFile).absoluteDir();
if (baseDir.exists()) {
defaultDir = baseDir.absolutePath();
}
const QString result = KisImportExportManager::askForAudioFileName(defaultDir, this);
const QFileInfo info(result);
if (info.exists()) {
m_d->model->setAudioChannelFileName(info.absoluteFilePath());
}
}
void TimelineFramesView::slotAudioChannelMute(bool value)
{
if (!m_d->model) return;
if (value != m_d->model->isAudioMuted()) {
m_d->model->setAudioMuted(value);
}
}
void TimelineFramesView::slotUpdateIcons()
{
m_d->addLayersButton->setIcon(KisIconUtils::loadIcon("addlayer"));
m_d->audioOptionsButton->setIcon(KisIconUtils::loadIcon("audio-none"));
m_d->zoomDragButton->setIcon(KisIconUtils::loadIcon("zoom-horizontal"));
}
void TimelineFramesView::slotAudioChannelRemove()
{
if (!m_d->model) return;
m_d->model->setAudioChannelFileName(QString());
}
void TimelineFramesView::slotUpdateAudioActions()
{
if (!m_d->model) return;
const QString currentFile = m_d->model->audioChannelFileName();
if (currentFile.isEmpty()) {
m_d->openAudioAction->setText(i18nc("@item:inmenu", "Open audio..."));
} else {
QFileInfo info(currentFile);
m_d->openAudioAction->setText(i18nc("@item:inmenu", "Change audio (%1)...", info.fileName()));
}
m_d->audioMuteAction->setChecked(m_d->model->isAudioMuted());
QIcon audioIcon;
if (currentFile.isEmpty()) {
audioIcon = KisIconUtils::loadIcon("audio-none");
} else {
if (m_d->model->isAudioMuted()) {
audioIcon = KisIconUtils::loadIcon("audio-volume-mute");
} else {
audioIcon = KisIconUtils::loadIcon("audio-volume-high");
}
}
m_d->audioOptionsButton->setIcon(audioIcon);
m_d->volumeSlider->setEnabled(!m_d->model->isAudioMuted());
KisSignalsBlocker b(m_d->volumeSlider);
m_d->volumeSlider->setValue(qRound(m_d->model->audioVolume() * 100.0));
}
void TimelineFramesView::slotAudioVolumeChanged(int value)
{
m_d->model->setAudioVolume(qreal(value) / 100.0);
}
+
+
+
void TimelineFramesView::slotUpdateInfiniteFramesCount()
{
if (horizontalScrollBar()->isSliderDown()) return;
const int sectionWidth = m_d->horizontalRuler->defaultSectionSize();
const int calculatedIndex =
(horizontalScrollBar()->value() +
m_d->horizontalRuler->width() - 1) / sectionWidth;
m_d->model->setLastVisibleFrame(calculatedIndex);
}
void TimelineFramesView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous)
{
QTableView::currentChanged(current, previous);
if (previous.column() != current.column()) {
m_d->model->setData(previous, false, TimelineFramesModel::ActiveFrameRole);
m_d->model->setData(current, true, TimelineFramesModel::ActiveFrameRole);
}
}
QItemSelectionModel::SelectionFlags TimelineFramesView::selectionCommand(const QModelIndex &index,
const QEvent *event) const
{
// WARNING: Copy-pasted from KisNodeView! Please keep in sync!
/**
* Qt has a bug: when we Ctrl+click on an item, the item's
* selections gets toggled on mouse *press*, whereas usually it is
* done on mouse *release*. Therefore the user cannot do a
* Ctrl+D&D with the default configuration. This code fixes the
* problem by manually returning QItemSelectionModel::NoUpdate
* flag when the user clicks on an item and returning
* QItemSelectionModel::Toggle on release.
*/
if (event &&
(event->type() == QEvent::MouseButtonPress ||
event->type() == QEvent::MouseButtonRelease) &&
index.isValid()) {
const QMouseEvent *mevent = static_cast(event);
if (mevent->button() == Qt::RightButton &&
selectionModel()->selectedIndexes().contains(index)) {
// Allow calling context menu for multiple layers
return QItemSelectionModel::NoUpdate;
}
if (event->type() == QEvent::MouseButtonPress &&
(mevent->modifiers() & Qt::ControlModifier)) {
return QItemSelectionModel::NoUpdate;
}
if (event->type() == QEvent::MouseButtonRelease &&
(mevent->modifiers() & Qt::ControlModifier)) {
return QItemSelectionModel::Toggle;
}
}
return QAbstractItemView::selectionCommand(index, event);
}
void TimelineFramesView::slotSelectionChanged()
{
int minColumn = std::numeric_limits::max();
int maxColumn = std::numeric_limits::min();
foreach (const QModelIndex &idx, selectedIndexes()) {
if (idx.column() > maxColumn) {
maxColumn = idx.column();
}
if (idx.column() < minColumn) {
minColumn = idx.column();
}
}
KisTimeRange range;
if (maxColumn > minColumn) {
range = KisTimeRange(minColumn, maxColumn - minColumn + 1);
}
m_d->model->setPlaybackRange(range);
}
void TimelineFramesView::slotReselectCurrentIndex()
{
QModelIndex index = currentIndex();
currentChanged(index, index);
}
void TimelineFramesView::slotEnsureRowVisible(int row)
{
QModelIndex index = currentIndex();
if (!index.isValid() || row < 0) return;
index = m_d->model->index(row, index.column());
scrollTo(index);
}
void TimelineFramesView::slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
{
if (m_d->model->isPlaybackActive()) return;
int selectedColumn = -1;
for (int j = topLeft.column(); j <= bottomRight.column(); j++) {
QVariant value = m_d->model->data(
m_d->model->index(topLeft.row(), j),
TimelineFramesModel::ActiveFrameRole);
if (value.isValid() && value.toBool()) {
selectedColumn = j;
break;
}
}
QModelIndex index = currentIndex();
if (!index.isValid() && selectedColumn < 0) {
return;
}
if (selectedColumn == -1) {
selectedColumn = index.column();
}
if (selectedColumn != index.column() && !m_d->dragInProgress) {
int row= index.isValid() ? index.row() : 0;
selectionModel()->setCurrentIndex(m_d->model->index(row, selectedColumn), QItemSelectionModel::ClearAndSelect);
}
}
void TimelineFramesView::slotHeaderDataChanged(Qt::Orientation orientation, int first, int last)
{
Q_UNUSED(first);
Q_UNUSED(last);
if (orientation == Qt::Horizontal) {
const int newFps = m_d->model->headerData(0, Qt::Horizontal, TimelineFramesModel::FramesPerSecondRole).toInt();
if (newFps != m_d->fps) {
setFramesPerSecond(newFps);
}
}
}
void TimelineFramesView::rowsInserted(const QModelIndex& parent, int start, int end)
{
QTableView::rowsInserted(parent, start, end);
}
inline bool isIndexDragEnabled(QAbstractItemModel *model, const QModelIndex &index) {
return (model->flags(index) & Qt::ItemIsDragEnabled);
}
QStyleOptionViewItem TimelineFramesView::Private::viewOptionsV4() const
{
QStyleOptionViewItem option = q->viewOptions();
option.locale = q->locale();
option.locale.setNumberOptions(QLocale::OmitGroupSeparator);
option.widget = q;
return option;
}
QItemViewPaintPairs TimelineFramesView::Private::draggablePaintPairs(const QModelIndexList &indexes, QRect *r) const
{
Q_ASSERT(r);
QRect &rect = *r;
const QRect viewportRect = q->viewport()->rect();
QItemViewPaintPairs ret;
for (int i = 0; i < indexes.count(); ++i) {
const QModelIndex &index = indexes.at(i);
const QRect current = q->visualRect(index);
if (current.intersects(viewportRect)) {
ret += qMakePair(current, index);
rect |= current;
}
}
rect &= viewportRect;
return ret;
}
QPixmap TimelineFramesView::Private::renderToPixmap(const QModelIndexList &indexes, QRect *r) const
{
Q_ASSERT(r);
QItemViewPaintPairs paintPairs = draggablePaintPairs(indexes, r);
if (paintPairs.isEmpty())
return QPixmap();
QPixmap pixmap(r->size());
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
QStyleOptionViewItem option = viewOptionsV4();
option.state |= QStyle::State_Selected;
for (int j = 0; j < paintPairs.count(); ++j) {
option.rect = paintPairs.at(j).first.translated(-r->topLeft());
const QModelIndex ¤t = paintPairs.at(j).second;
//adjustViewOptionsForIndex(&option, current);
q->itemDelegate(current)->paint(&painter, option, current);
}
return pixmap;
}
void TimelineFramesView::startDrag(Qt::DropActions supportedActions)
{
QModelIndexList indexes = selectionModel()->selectedIndexes();
if (!indexes.isEmpty() && m_d->modifiersCatcher->modifierPressed("offset-frame")) {
QVector rows;
int leftmostColumn = std::numeric_limits::max();
Q_FOREACH (const QModelIndex &index, indexes) {
leftmostColumn = qMin(leftmostColumn, index.column());
if (!rows.contains(index.row())) {
rows.append(index.row());
}
}
const int lastColumn = m_d->model->columnCount() - 1;
selectionModel()->clear();
Q_FOREACH (const int row, rows) {
QItemSelection sel(m_d->model->index(row, leftmostColumn), m_d->model->index(row, lastColumn));
selectionModel()->select(sel, QItemSelectionModel::Select);
}
supportedActions = Qt::MoveAction;
{
QModelIndexList indexes = selectedIndexes();
for(int i = indexes.count() - 1 ; i >= 0; --i) {
if (!isIndexDragEnabled(m_d->model, indexes.at(i)))
indexes.removeAt(i);
}
selectionModel()->clear();
if (indexes.count() > 0) {
QMimeData *data = m_d->model->mimeData(indexes);
if (!data)
return;
QRect rect;
QPixmap pixmap = m_d->renderToPixmap(indexes, &rect);
rect.adjust(horizontalOffset(), verticalOffset(), 0, 0);
QDrag *drag = new QDrag(this);
drag->setPixmap(pixmap);
drag->setMimeData(data);
drag->setHotSpot(m_d->lastPressedPosition - rect.topLeft());
drag->exec(supportedActions, Qt::MoveAction);
setCurrentIndex(currentIndex());
}
}
} else {
/**
* Workaround for Qt5's bug: if we start a dragging action right during
* Shift-selection, Qt will get crazy. We cannot workaround it easily,
* because we would need to fork mouseMoveEvent() for that (where the
* decision about drag state is done). So we just abort dragging in that
* case.
*
* BUG:373067
*/
if (m_d->lastPressedModifier & Qt::ShiftModifier) {
return;
}
/**
* Workaround for Qt5's bugs:
*
* 1) Qt doesn't treat selection the selection on D&D
* correctly, so we save it in advance and restore
* afterwards.
*
* 2) There is a private variable in QAbstractItemView:
* QAbstractItemView::Private::currentSelectionStartIndex.
* It is initialized *only* when the setCurrentIndex() is called
* explicitly on the view object, not on the selection model.
* Therefore we should explicitly call setCurrentIndex() after
* D&D, even if it already has *correct* value!
*
* 2) We should also call selectionModel()->select()
* explicitly. There are two reasons for it: 1) Qt doesn't
* maintain selection over D&D; 2) when reselecting single
* element after D&D, Qt goes crazy, because it tries to
* read *global* keyboard modifiers. Therefore if we are
* dragging with Shift or Ctrl pressed it'll get crazy. So
* just reset it explicitly.
*/
QModelIndexList selectionBefore = selectionModel()->selectedIndexes();
QModelIndex currentBefore = selectionModel()->currentIndex();
// initialize a global status variable
m_d->dragWasSuccessful = false;
QAbstractItemView::startDrag(supportedActions);
QModelIndex newCurrent;
QPoint selectionOffset;
if (m_d->dragWasSuccessful) {
newCurrent = currentIndex();
selectionOffset = QPoint(newCurrent.column() - currentBefore.column(),
newCurrent.row() - currentBefore.row());
} else {
newCurrent = currentBefore;
selectionOffset = QPoint();
}
setCurrentIndex(newCurrent);
selectionModel()->clearSelection();
Q_FOREACH (const QModelIndex &idx, selectionBefore) {
QModelIndex newIndex =
model()->index(idx.row() + selectionOffset.y(),
idx.column() + selectionOffset.x());
selectionModel()->select(newIndex, QItemSelectionModel::Select);
}
}
}
void TimelineFramesView::dragEnterEvent(QDragEnterEvent *event)
{
m_d->dragInProgress = true;
m_d->model->setScrubState(true);
QTableView::dragEnterEvent(event);
}
void TimelineFramesView::dragMoveEvent(QDragMoveEvent *event)
{
m_d->dragInProgress = true;
m_d->model->setScrubState(true);
QTableView::dragMoveEvent(event);
if (event->isAccepted()) {
QModelIndex index = indexAt(event->pos());
if (!m_d->model->canDropFrameData(event->mimeData(), index)) {
event->ignore();
} else {
selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
}
}
}
void TimelineFramesView::dropEvent(QDropEvent *event)
{
m_d->dragInProgress = false;
m_d->model->setScrubState(false);
QAbstractItemView::dropEvent(event);
m_d->dragWasSuccessful = event->isAccepted();
}
void TimelineFramesView::dragLeaveEvent(QDragLeaveEvent *event)
{
m_d->dragInProgress = false;
m_d->model->setScrubState(false);
QAbstractItemView::dragLeaveEvent(event);
}
void TimelineFramesView::createFrameEditingMenuActions(QMenu *menu, bool addFrameCreationActions)
{
slotUpdateFrameActions();
+ // calculate if selection range is set. This will determine if the update playback range is available
+ QSet rows;
+ int minColumn = 0;
+ int maxColumn = 0;
+ calculateSelectionMetrics(minColumn, maxColumn, rows);
+
+ bool selectionExists = minColumn != maxColumn;
+
+
+ if (selectionExists) {
+ KisActionManager::safePopulateMenu(menu, "update_playback_range", m_d->actionMan);
+ } else {
+ KisActionManager::safePopulateMenu(menu, "set_start_time", m_d->actionMan);
+ KisActionManager::safePopulateMenu(menu, "set_end_time", m_d->actionMan);
+ }
+ menu->addSeparator();
+
+
KisActionManager::safePopulateMenu(menu, "cut_frames_to_clipboard", m_d->actionMan);
KisActionManager::safePopulateMenu(menu, "copy_frames_to_clipboard", m_d->actionMan);
KisActionManager::safePopulateMenu(menu, "paste_frames_from_clipboard", m_d->actionMan);
- menu->addSeparator();
-
- if (addFrameCreationActions) {
- KisActionManager::safePopulateMenu(menu, "add_blank_frame", m_d->actionMan);
- KisActionManager::safePopulateMenu(menu, "add_duplicate_frame", m_d->actionMan);
- menu->addSeparator();
- }
+ menu->addSeparator();
QMenu *frames = menu->addMenu(i18nc("@item:inmenu", "Keyframes"));
KisActionManager::safePopulateMenu(frames, "insert_keyframes_right", m_d->actionMan);
KisActionManager::safePopulateMenu(frames, "insert_keyframes_left", m_d->actionMan);
+
frames->addSeparator();
KisActionManager::safePopulateMenu(frames, "insert_n_keyframes_right", m_d->actionMan);
KisActionManager::safePopulateMenu(frames, "insert_n_keyframes_left", m_d->actionMan);
QMenu *hold = menu->addMenu(i18nc("@item:inmenu", "Hold Frames"));
KisActionManager::safePopulateMenu(hold, "insert_hold_frame", m_d->actionMan);
KisActionManager::safePopulateMenu(hold, "remove_hold_frame", m_d->actionMan);
+
hold->addSeparator();
KisActionManager::safePopulateMenu(hold, "insert_n_hold_frames", m_d->actionMan);
KisActionManager::safePopulateMenu(hold, "remove_n_hold_frames", m_d->actionMan);
menu->addSeparator();
KisActionManager::safePopulateMenu(menu, "remove_frames", m_d->actionMan);
KisActionManager::safePopulateMenu(menu, "remove_frames_and_pull", m_d->actionMan);
+
+ menu->addSeparator();
+ if (addFrameCreationActions) {
+ KisActionManager::safePopulateMenu(menu, "add_blank_frame", m_d->actionMan);
+ KisActionManager::safePopulateMenu(menu, "add_duplicate_frame", m_d->actionMan);
+ menu->addSeparator();
+ }
+
}
void TimelineFramesView::mousePressEvent(QMouseEvent *event)
{
QPersistentModelIndex index = indexAt(event->pos());
if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) {
if (event->button() == Qt::RightButton) {
// TODO: try calculate index under mouse cursor even when
// it is outside any visible row
qreal staticPoint = index.isValid() ? index.column() : currentIndex().column();
m_d->zoomDragButton->beginZoom(event->pos(), staticPoint);
} else if (event->button() == Qt::LeftButton) {
m_d->initialDragPanPos = event->pos();
m_d->initialDragPanValue =
QPoint(horizontalScrollBar()->value(),
verticalScrollBar()->value());
}
event->accept();
} else if (event->button() == Qt::RightButton) {
int numSelectedItems = selectionModel()->selectedIndexes().size();
if (index.isValid() &&
numSelectedItems <= 1 &&
m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) {
model()->setData(index, true, TimelineFramesModel::ActiveLayerRole);
model()->setData(index, true, TimelineFramesModel::ActiveFrameRole);
setCurrentIndex(index);
if (model()->data(index, TimelineFramesModel::FrameExistsRole).toBool() ||
model()->data(index, TimelineFramesModel::SpecialKeyframeExists).toBool()) {
{
KisSignalsBlocker b(m_d->colorSelector);
QVariant colorLabel = index.data(TimelineFramesModel::FrameColorLabelIndexRole);
int labelIndex = colorLabel.isValid() ? colorLabel.toInt() : 0;
m_d->colorSelector->setCurrentIndex(labelIndex);
}
QMenu menu;
createFrameEditingMenuActions(&menu, false);
menu.addSeparator();
menu.addAction(m_d->colorSelectorAction);
menu.exec(event->globalPos());
} else {
{
KisSignalsBlocker b(m_d->colorSelector);
KisImageConfig cfg;
const int labelIndex = cfg.defaultFrameColorLabel();
m_d->colorSelector->setCurrentIndex(labelIndex);
}
QMenu menu;
createFrameEditingMenuActions(&menu, true);
menu.addSeparator();
menu.addAction(m_d->colorSelectorAction);
menu.exec(event->globalPos());
}
} else if (numSelectedItems > 1) {
int labelIndex = -1;
bool haveFrames = false;
Q_FOREACH(QModelIndex index, selectedIndexes()) {
haveFrames |= index.data(TimelineFramesModel::FrameExistsRole).toBool();
QVariant colorLabel = index.data(TimelineFramesModel::FrameColorLabelIndexRole);
if (colorLabel.isValid()) {
if (labelIndex == -1) {
// First label
labelIndex = colorLabel.toInt();
} else if (labelIndex != colorLabel.toInt()) {
// Mixed colors in selection
labelIndex = -1;
break;
}
}
}
if (haveFrames) {
KisSignalsBlocker b(m_d->multiframeColorSelector);
m_d->multiframeColorSelector->setCurrentIndex(labelIndex);
}
QMenu menu;
createFrameEditingMenuActions(&menu, false);
menu.addSeparator();
KisActionManager::safePopulateMenu(&menu, "mirror_frames", m_d->actionMan);
menu.addSeparator();
menu.addAction(m_d->multiframeColorSelectorAction);
menu.exec(event->globalPos());
}
} else if (event->button() == Qt::MidButton) {
QModelIndex index = model()->buddy(indexAt(event->pos()));
if (index.isValid()) {
QStyleOptionViewItem option = viewOptions();
option.rect = visualRect(index);
// The offset of the headers is needed to get the correct position inside the view.
m_d->tip.showTip(this, event->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index);
}
event->accept();
} else {
if (index.isValid()) {
m_d->model->setLastClickedIndex(index);
}
m_d->lastPressedPosition =
QPoint(horizontalOffset(), verticalOffset()) + event->pos();
m_d->lastPressedModifier = event->modifiers();
QAbstractItemView::mousePressEvent(event);
}
}
void TimelineFramesView::mouseMoveEvent(QMouseEvent *e)
{
if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) {
if (e->buttons() & Qt::RightButton) {
m_d->zoomDragButton->continueZoom(e->pos());
} else if (e->buttons() & Qt::LeftButton) {
QPoint diff = e->pos() - m_d->initialDragPanPos;
QPoint offset = QPoint(m_d->initialDragPanValue.x() - diff.x(),
m_d->initialDragPanValue.y() - diff.y());
const int height = m_d->layersHeader->defaultSectionSize();
horizontalScrollBar()->setValue(offset.x());
verticalScrollBar()->setValue(offset.y() / height);
}
e->accept();
} else if (e->buttons() == Qt::MidButton) {
QModelIndex index = model()->buddy(indexAt(e->pos()));
if (index.isValid()) {
QStyleOptionViewItem option = viewOptions();
option.rect = visualRect(index);
// The offset of the headers is needed to get the correct position inside the view.
m_d->tip.showTip(this, e->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index);
}
e->accept();
} else {
m_d->model->setScrubState(true);
QTableView::mouseMoveEvent(e);
}
}
void TimelineFramesView::mouseReleaseEvent(QMouseEvent *e)
{
if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) {
e->accept();
} else {
m_d->model->setScrubState(false);
QTableView::mouseReleaseEvent(e);
}
}
void TimelineFramesView::wheelEvent(QWheelEvent *e)
{
QModelIndex index = currentIndex();
int column= -1;
if (index.isValid()) {
column= index.column() + ((e->delta() > 0) ? 1 : -1);
}
if (column >= 0 && !m_d->dragInProgress) {
setCurrentIndex(m_d->model->index(index.row(), column));
}
}
void TimelineFramesView::slotUpdateLayersMenu()
{
QAction *action = 0;
m_d->existingLayersMenu->clear();
QVariant value = model()->headerData(0, Qt::Vertical, TimelineFramesModel::OtherLayersRole);
if (value.isValid()) {
TimelineFramesModel::OtherLayersList list = value.value();
int i = 0;
Q_FOREACH (const TimelineFramesModel::OtherLayer &l, list) {
action = m_d->existingLayersMenu->addAction(l.name);
action->setData(i++);
}
}
}
void TimelineFramesView::slotUpdateFrameActions()
{
if (!m_d->actionMan) return;
const QModelIndexList editableIndexes = calculateSelectionSpan(false, true);
const bool hasEditableFrames = !editableIndexes.isEmpty();
bool hasExistingFrames = false;
Q_FOREACH (const QModelIndex &index, editableIndexes) {
if (model()->data(index, TimelineFramesModel::FrameExistsRole).toBool()) {
hasExistingFrames = true;
break;
}
}
auto enableAction = [this] (const QString &id, bool value) {
KisAction *action = m_d->actionMan->actionByName(id);
KIS_SAFE_ASSERT_RECOVER_RETURN(action);
action->setEnabled(value);
};
enableAction("add_blank_frame", hasEditableFrames);
enableAction("add_duplicate_frame", hasEditableFrames);
enableAction("insert_keyframes_right", hasEditableFrames);
enableAction("insert_n_keyframes_right", hasEditableFrames);
enableAction("insert_keyframes_left", hasEditableFrames);
enableAction("insert_n_keyframes_left", hasEditableFrames);
enableAction("remove_frames", hasEditableFrames && hasExistingFrames);
enableAction("remove_frames_and_pull", hasEditableFrames);
enableAction("insert_hold_frame", hasEditableFrames);
enableAction("insert_n_hold_frames", hasEditableFrames);
enableAction("remove_hold_frame", hasEditableFrames);
enableAction("remove_n_hold_frames", hasEditableFrames);
enableAction("mirror_frames", hasEditableFrames && editableIndexes.size() > 1);
enableAction("copy_frames_to_clipboard", true);
enableAction("cut_frames_to_clipboard", hasEditableFrames);
QClipboard *cp = QApplication::clipboard();
const QMimeData *data = cp->mimeData();
enableAction("paste_frames_from_clipboard", data && data->hasFormat("application/x-krita-frame"));
//TODO: update column actions!
}
+void TimelineFramesView::slotSetStartTimeToCurrentPosition()
+{
+ m_d->model->setFullClipRangeStart(this->currentIndex().column());
+}
+
+void TimelineFramesView::slotSetEndTimeToCurrentPosition()
+{
+ m_d->model->setFullClipRangeEnd(this->currentIndex().column());
+}
+
+void TimelineFramesView::slotUpdatePlackbackRange()
+{
+ QSet rows;
+ int minColumn = 0;
+ int maxColumn = 0;
+
+ calculateSelectionMetrics(minColumn, maxColumn, rows);
+
+ m_d->model->setFullClipRangeStart(minColumn);
+ m_d->model->setFullClipRangeEnd(maxColumn);
+}
+
void TimelineFramesView::slotLayerContextMenuRequested(const QPoint &globalPos)
{
m_d->layerEditingMenu->exec(globalPos);
}
void TimelineFramesView::slotAddNewLayer()
{
QModelIndex index = currentIndex();
const int newRow = index.isValid() ? index.row() : 0;
model()->insertRow(newRow);
}
void TimelineFramesView::slotAddExistingLayer(QAction *action)
{
QVariant value = action->data();
if (value.isValid()) {
QModelIndex index = currentIndex();
const int newRow = index.isValid() ? index.row() + 1 : 0;
m_d->model->insertOtherLayer(value.toInt(), newRow);
}
}
void TimelineFramesView::slotRemoveLayer()
{
QModelIndex index = currentIndex();
if (!index.isValid()) return;
model()->removeRow(index.row());
}
void TimelineFramesView::slotNewFrame()
{
QModelIndex index = currentIndex();
if (!index.isValid() ||
!m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) {
return;
}
m_d->model->createFrame(index);
}
void TimelineFramesView::slotCopyFrame()
{
QModelIndex index = currentIndex();
if (!index.isValid() ||
!m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) {
return;
}
m_d->model->copyFrame(index);
}
void TimelineFramesView::calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet &rows) const
{
minColumn = std::numeric_limits::max();
maxColumn = std::numeric_limits::min();
Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) {
if (!m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) continue;
rows.insert(index.row());
minColumn = qMin(minColumn, index.column());
maxColumn = qMax(maxColumn, index.column());
}
}
void TimelineFramesView::insertFramesImpl(int insertAtColumn, int count, QSet rows, bool forceEntireColumn)
{
if (forceEntireColumn) {
rows.clear();
for (int i = 0; i < m_d->model->rowCount(); i++) {
if (!m_d->model->data(m_d->model->index(i, insertAtColumn), TimelineFramesModel::FrameEditableRole).toBool()) continue;
rows.insert(i);
}
}
if (!rows.isEmpty()) {
m_d->model->insertFrames(insertAtColumn, rows.toList(), count);
}
}
void TimelineFramesView::slotInsertKeyframesLeft(int count, bool forceEntireColumn)
{
QSet rows;
int minColumn = 0;
int maxColumn = 0;
calculateSelectionMetrics(minColumn, maxColumn, rows);
if (count <= 0) {
count = qMax(1, maxColumn - minColumn + 1);
}
insertFramesImpl(minColumn, count, rows, forceEntireColumn);
}
void TimelineFramesView::slotInsertKeyframesRight(int count, bool forceEntireColumn)
{
QSet rows;
int minColumn = 0;
int maxColumn = 0;
calculateSelectionMetrics(minColumn, maxColumn, rows);
if (count <= 0) {
count = qMax(1, maxColumn - minColumn + 1);
}
insertFramesImpl(maxColumn + 1, count, rows, forceEntireColumn);
}
void TimelineFramesView::slotInsertColumnsLeft(int count)
{
slotInsertKeyframesLeft(count, true);
}
void TimelineFramesView::slotInsertColumnsRight(int count)
{
slotInsertKeyframesRight(count, true);
}
void TimelineFramesView::slotInsertKeyframesLeftCustom()
{
bool ok = false;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Insert left"),
i18nc("@label:spinbox", "Enter number of frames"),
defaultNumberOfFramesToAdd(),
1, 10000, 1, &ok);
if (ok) {
setDefaultNumberOfFramesToAdd(count);
slotInsertKeyframesLeft(count);
}
}
void TimelineFramesView::slotInsertKeyframesRightCustom()
{
bool ok = false;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Insert right"),
i18nc("@label:spinbox", "Enter number of frames"),
defaultNumberOfFramesToAdd(),
1, 10000, 1, &ok);
if (ok) {
setDefaultNumberOfFramesToAdd(count);
slotInsertKeyframesRight(count);
}
}
void TimelineFramesView::slotInsertColumnsLeftCustom()
{
bool ok = false;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Insert left"),
i18nc("@label:spinbox", "Enter number of columns"),
defaultNumberOfColumnsToAdd(),
1, 10000, 1, &ok);
if (ok) {
setDefaultNumberOfColumnsToAdd(count);
slotInsertColumnsLeft(count);
}
}
void TimelineFramesView::slotInsertColumnsRightCustom()
{
bool ok = false;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Insert right"),
i18nc("@label:spinbox", "Enter number of columns"),
defaultNumberOfColumnsToAdd(),
1, 10000, 1, &ok);
if (ok) {
setDefaultNumberOfColumnsToAdd(count);
slotInsertColumnsRight(count);
}
}
QModelIndexList TimelineFramesView::calculateSelectionSpan(bool forceEntireColumn, bool editableOnly) const
{
QModelIndexList indexes;
if (forceEntireColumn) {
QSet rows;
int minColumn = 0;
int maxColumn = 0;
calculateSelectionMetrics(minColumn, maxColumn, rows);
rows.clear();
for (int i = 0; i < m_d->model->rowCount(); i++) {
if (editableOnly &&
!m_d->model->data(m_d->model->index(i, minColumn), TimelineFramesModel::FrameEditableRole).toBool()) continue;
for (int column = minColumn; column <= maxColumn; column++) {
indexes << m_d->model->index(i, column);
}
}
} else {
Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) {
if (!editableOnly || m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) {
indexes << index;
}
}
}
return indexes;
}
void TimelineFramesView::slotRemoveFrame(bool forceEntireColumn, bool needsOffset)
{
const QModelIndexList indexes = calculateSelectionSpan(forceEntireColumn);
if (!indexes.isEmpty()) {
if (needsOffset) {
m_d->model->removeFramesAndOffset(indexes);
} else {
m_d->model->removeFrames(indexes);
}
}
}
void TimelineFramesView::slotRemoveColumns()
{
slotRemoveFrame(true);
}
void TimelineFramesView::slotRemoveFramesAndShift(bool forceEntireColumn)
{
slotRemoveFrame(forceEntireColumn, true);
}
void TimelineFramesView::slotRemoveColumnsAndShift()
{
slotRemoveFramesAndShift(true);
}
void TimelineFramesView::slotInsertHoldFrames(int count, bool forceEntireColumn)
{
QModelIndexList indexes;
if (!forceEntireColumn) {
Q_FOREACH (const QModelIndex &index, selectionModel()->selectedIndexes()) {
if (m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) {
indexes << index;
}
}
} else {
const int column = selectionModel()->currentIndex().column();
for (int i = 0; i < m_d->model->rowCount(); i++) {
const QModelIndex index = m_d->model->index(i, column);
if (m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) {
indexes << index;
}
}
}
if (!indexes.isEmpty()) {
m_d->model->insertHoldFrames(indexes, count);
}
}
void TimelineFramesView::slotRemoveHoldFrames(int count, bool forceEntireColumn)
{
slotInsertHoldFrames(-count, forceEntireColumn);
}
void TimelineFramesView::slotInsertHoldFramesCustom()
{
bool ok = false;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Insert hold frames"),
i18nc("@label:spinbox", "Enter number of frames"),
defaultNumberOfFramesToAdd(),
1, 10000, 1, &ok);
if (ok) {
setDefaultNumberOfFramesToAdd(count);
slotInsertHoldFrames(count);
}
}
void TimelineFramesView::slotRemoveHoldFramesCustom()
{
bool ok = false;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Remove hold frames"),
i18nc("@label:spinbox", "Enter number of frames"),
defaultNumberOfFramesToRemove(),
1, 10000, 1, &ok);
if (ok) {
setDefaultNumberOfFramesToRemove(count);
slotRemoveHoldFrames(count);
}
}
void TimelineFramesView::slotInsertHoldColumns(int count)
{
slotInsertHoldFrames(count, true);
}
void TimelineFramesView::slotRemoveHoldColumns(int count)
{
slotRemoveHoldFrames(count, true);
}
void TimelineFramesView::slotInsertHoldColumnsCustom()
{
bool ok = false;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Insert hold columns"),
i18nc("@label:spinbox", "Enter number of columns"),
defaultNumberOfColumnsToAdd(),
1, 10000, 1, &ok);
if (ok) {
setDefaultNumberOfColumnsToAdd(count);
slotInsertHoldColumns(count);
}
}
void TimelineFramesView::slotRemoveHoldColumnsCustom()
{
bool ok = false;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Remove hold columns"),
i18nc("@label:spinbox", "Enter number of columns"),
defaultNumberOfColumnsToRemove(),
1, 10000, 1, &ok);
if (ok) {
setDefaultNumberOfColumnsToRemove(count);
slotRemoveHoldColumns(count);
}
}
void TimelineFramesView::slotMirrorFrames(bool forceEntireColumn)
{
const QModelIndexList indexes = calculateSelectionSpan(forceEntireColumn);
if (!indexes.isEmpty()) {
m_d->model->mirrorFrames(indexes);
}
}
void TimelineFramesView::slotMirrorColumns()
{
slotMirrorFrames(true);
}
void TimelineFramesView::cutCopyImpl(bool forceEntireColumn, bool copy)
{
const QModelIndexList indexes = calculateSelectionSpan(forceEntireColumn, !copy);
if (indexes.isEmpty()) return;
int minColumn = std::numeric_limits::max();
int minRow = std::numeric_limits::max();
Q_FOREACH (const QModelIndex &index, indexes) {
minRow = qMin(minRow, index.row());
minColumn = qMin(minColumn, index.column());
}
const QModelIndex baseIndex = m_d->model->index(minRow, minColumn);
QMimeData *data = m_d->model->mimeDataExtended(indexes,
baseIndex,
copy ?
TimelineFramesModel::CopyFramesPolicy :
TimelineFramesModel::MoveFramesPolicy);
if (data) {
QClipboard *cb = QApplication::clipboard();
cb->setMimeData(data);
}
}
void TimelineFramesView::slotCopyFrames(bool forceEntireColumn)
{
cutCopyImpl(forceEntireColumn, true);
}
void TimelineFramesView::slotCutFrames(bool forceEntireColumn)
{
cutCopyImpl(forceEntireColumn, false);
}
void TimelineFramesView::slotPasteFrames(bool forceEntireColumn)
{
const QModelIndex currentIndex =
!forceEntireColumn ? this->currentIndex() : m_d->model->index(0, this->currentIndex().column());
if (!currentIndex.isValid()) return;
QClipboard *cb = QApplication::clipboard();
const QMimeData *data = cb->mimeData();
if (data && data->hasFormat("application/x-krita-frame")) {
bool dataMoved = false;
bool result = m_d->model->dropMimeDataExtended(data, Qt::MoveAction, currentIndex, &dataMoved);
if (result && dataMoved) {
cb->clear();
}
}
}
void TimelineFramesView::slotCutColumns()
{
slotCutFrames(true);
}
void TimelineFramesView::slotPasteColumns()
{
slotPasteFrames(true);
}
void TimelineFramesView::slotCopyColumns()
{
slotCopyFrames(true);
}
int TimelineFramesView::defaultNumberOfFramesToAdd() const
{
KConfigGroup cfg = KSharedConfig::openConfig()->group("FrameActionsDefaultValues");
return cfg.readEntry("defaultNumberOfFramesToAdd", 1);
}
void TimelineFramesView::setDefaultNumberOfFramesToAdd(int value) const
{
KConfigGroup cfg = KSharedConfig::openConfig()->group("FrameActionsDefaultValues");
cfg.writeEntry("defaultNumberOfFramesToAdd", value);
}
int TimelineFramesView::defaultNumberOfColumnsToAdd() const
{
KConfigGroup cfg = KSharedConfig::openConfig()->group("FrameActionsDefaultValues");
return cfg.readEntry("defaultNumberOfColumnsToAdd", 1);
}
void TimelineFramesView::setDefaultNumberOfColumnsToAdd(int value) const
{
KConfigGroup cfg = KSharedConfig::openConfig()->group("FrameActionsDefaultValues");
cfg.writeEntry("defaultNumberOfColumnsToAdd", value);
}
int TimelineFramesView::defaultNumberOfFramesToRemove() const
{
KConfigGroup cfg = KSharedConfig::openConfig()->group("FrameActionsDefaultValues");
return cfg.readEntry("defaultNumberOfFramesToRemove", 1);
}
void TimelineFramesView::setDefaultNumberOfFramesToRemove(int value) const
{
KConfigGroup cfg = KSharedConfig::openConfig()->group("FrameActionsDefaultValues");
cfg.writeEntry("defaultNumberOfFramesToRemove", value);
}
int TimelineFramesView::defaultNumberOfColumnsToRemove() const
{
KConfigGroup cfg = KSharedConfig::openConfig()->group("FrameActionsDefaultValues");
return cfg.readEntry("defaultNumberOfColumnsToRemove", 1);
}
void TimelineFramesView::setDefaultNumberOfColumnsToRemove(int value) const
{
KConfigGroup cfg = KSharedConfig::openConfig()->group("FrameActionsDefaultValues");
cfg.writeEntry("defaultNumberOfColumnsToRemove", value);
}
bool TimelineFramesView::viewportEvent(QEvent *event)
{
if (event->type() == QEvent::ToolTip && model()) {
QHelpEvent *he = static_cast(event);
QModelIndex index = model()->buddy(indexAt(he->pos()));
if (index.isValid()) {
QStyleOptionViewItem option = viewOptions();
option.rect = visualRect(index);
// The offset of the headers is needed to get the correct position inside the view.
m_d->tip.showTip(this, he->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, index);
return true;
}
}
return QTableView::viewportEvent(event);
}
diff --git a/plugins/dockers/animation/timeline_frames_view.h b/plugins/dockers/animation/timeline_frames_view.h
index 0cedcd077d..d099160cf5 100644
--- a/plugins/dockers/animation/timeline_frames_view.h
+++ b/plugins/dockers/animation/timeline_frames_view.h
@@ -1,172 +1,176 @@
/*
* Copyright (c) 2015 Dmitry Kazakov
*
* 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) 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef __TIMELINE_FRAMES_VIEW_H
#define __TIMELINE_FRAMES_VIEW_H
#include
#include
#include "kis_action_manager.h"
#include "kritaanimationdocker_export.h"
class KisAction;
class TimelineWidget;
class KRITAANIMATIONDOCKER_EXPORT TimelineFramesView : public QTableView
{
Q_OBJECT
public:
TimelineFramesView(QWidget *parent);
~TimelineFramesView() override;
void setModel(QAbstractItemModel *model) override;
void updateGeometries() override;
void setShowInTimeline(KisAction *action);
void setActionManager( KisActionManager * actionManager);
public Q_SLOTS:
void slotSelectionChanged();
void slotUpdateIcons();
private Q_SLOTS:
void slotUpdateLayersMenu();
void slotUpdateFrameActions();
+ void slotSetStartTimeToCurrentPosition();
+ void slotSetEndTimeToCurrentPosition();
+ void slotUpdatePlackbackRange();
+
void slotAddNewLayer();
void slotAddExistingLayer(QAction *action);
void slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
void slotRemoveLayer();
void slotLayerContextMenuRequested(const QPoint &globalPos);
void slotNewFrame();
void slotCopyFrame();
void slotInsertKeyframesLeft(int count = -1, bool forceEntireColumn = false);
void slotInsertKeyframesRight(int count = -1, bool forceEntireColumn = false);
void slotInsertKeyframesLeftCustom();
void slotInsertKeyframesRightCustom();
void slotRemoveFrame(bool forceEntireColumn = false, bool needsOffset = false);
void slotRemoveFramesAndShift(bool forceEntireColumn = false);
void slotInsertColumnsLeft(int count = -1);
void slotInsertColumnsLeftCustom();
void slotInsertColumnsRight(int count = -1);
void slotInsertColumnsRightCustom();
void slotRemoveColumns();
void slotRemoveColumnsAndShift();
void slotInsertHoldFrames(int count = 1, bool forceEntireColumn = false);
void slotRemoveHoldFrames(int count = 1, bool forceEntireColumn = false);
void slotInsertHoldFramesCustom();
void slotRemoveHoldFramesCustom();
void slotInsertHoldColumns(int count = 1);
void slotRemoveHoldColumns(int count = 1);
void slotInsertHoldColumnsCustom();
void slotRemoveHoldColumnsCustom();
void slotMirrorFrames(bool forceEntireColumn = false);
void slotMirrorColumns();
void slotCopyFrames(bool forceEntireColumn = false);
void slotCutFrames(bool forceEntireColumn = false);
void slotPasteFrames(bool forceEntireColumn = false);
void slotCopyColumns();
void slotCutColumns();
void slotPasteColumns();
void slotReselectCurrentIndex();
void slotUpdateInfiniteFramesCount();
void slotHeaderDataChanged(Qt::Orientation orientation, int first, int last);
void slotZoomButtonPressed(qreal staticPoint);
void slotZoomButtonChanged(qreal value);
void slotColorLabelChanged(int);
void slotEnsureRowVisible(int row);
void slotSelectAudioChannelFile();
void slotAudioChannelMute(bool value);
void slotAudioChannelRemove();
void slotUpdateAudioActions();
void slotAudioVolumeChanged(int value);
private:
void setFramesPerSecond(int fps);
void calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet &rows) const;
void insertFramesImpl(int insertAtColumn, int count, QSet rows, bool forceEntireColumn);
void createFrameEditingMenuActions(QMenu *menu, bool addFrameCreationActions);
QModelIndexList calculateSelectionSpan(bool forceEntireColumn, bool editableOnly = true) const;
void cutCopyImpl(bool forceEntireColumn, bool copy);
int defaultNumberOfFramesToAdd() const;
void setDefaultNumberOfFramesToAdd(int value) const;
int defaultNumberOfColumnsToAdd() const;
void setDefaultNumberOfColumnsToAdd(int value) const;
int defaultNumberOfFramesToRemove() const;
void setDefaultNumberOfFramesToRemove(int value) const;
int defaultNumberOfColumnsToRemove() const;
void setDefaultNumberOfColumnsToRemove(int value) const;
protected:
QItemSelectionModel::SelectionFlags selectionCommand(const QModelIndex &index,
const QEvent *event) const override;
void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override;
void startDrag(Qt::DropActions supportedActions) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dropEvent(QDropEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
void rowsInserted(const QModelIndex& parent, int start, int end) override;
bool viewportEvent(QEvent *event) override;
private:
struct Private;
const QScopedPointer m_d;
};
#endif /* __TIMELINE_FRAMES_VIEW_H */