diff --git a/benchmarks/KisAnimationRenderingBenchmark.cpp b/benchmarks/KisAnimationRenderingBenchmark.cpp index 24ab8d8ce7..5c41df94df 100644 --- a/benchmarks/KisAnimationRenderingBenchmark.cpp +++ b/benchmarks/KisAnimationRenderingBenchmark.cpp @@ -1,112 +1,112 @@ /* * Copyright (c) 2017 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 "KisAnimationRenderingBenchmark.h" #include #include #include "kis_time_range.h" #include "dialogs/KisAsyncAnimationFramesSaveDialog.h" #include "kis_image_animation_interface.h" #include "KisPart.h" #include "KisDocument.h" #include "kis_image.h" #include "kis_image_config.h" namespace { void removeTempFiles(const QString &filesMask) { QFileInfo info(filesMask); QDir dir(info.absolutePath()); QStringList filesList = dir.entryList({ info.fileName() }); if (!filesList.isEmpty()) { Q_FOREACH (const QString &file, filesList) { if (!dir.remove(file)) { QFAIL("Couldn't remove the old testing file!"); } } } } void runRenderingTest(KisImageSP image, int numCores, int numClones) { { KisImageConfig cfg(false); cfg.setMaxNumberOfThreads(numCores); cfg.setFrameRenderingClones(numClones); } - const KisTimeRange range = image->animationInterface()->fullClipRange(); + const KisTimeSpan range = image->animationInterface()->fullClipRange(); KisAsyncAnimationFramesSaveDialog dlg(image, range, "temp_frames.png", 0, 0); dlg.setBatchMode(true); // repeat rendering twice! for (int i = 0; i < 1; i++) { removeTempFiles(dlg.savedFilesMaskWildcard()); KisAsyncAnimationFramesSaveDialog::Result result = dlg.regenerateRange(0); QCOMPARE(result, KisAsyncAnimationFramesSaveDialog::RenderComplete); removeTempFiles(dlg.savedFilesMaskWildcard()); } } } void KisAnimationRenderingBenchmark::testCacheRendering() { const QString fileName = TestUtil::fetchDataFileLazy("miloor_turntable_002.kra", true); QVERIFY(QFileInfo(fileName).exists()); QScopedPointer doc(KisPart::instance()->createDocument()); bool loadingResult = doc->loadNativeFormat(fileName); QVERIFY(loadingResult); doc->image()->barrierLock(); doc->image()->unlock(); for (int numCores = 1; numCores <= QThread::idealThreadCount(); numCores++) { QElapsedTimer timer; timer.start(); const int numClones = qMax(1, numCores / 2); runRenderingTest(doc->image(), numCores, numClones); qDebug() << "Cores:" << numCores << "Clones:" << numClones << "Time:" << timer.elapsed(); } for (int numCores = 1; numCores <= QThread::idealThreadCount(); numCores++) { QElapsedTimer timer; timer.start(); const int numClones = numCores; runRenderingTest(doc->image(), numCores, numClones); qDebug() << "Cores:" << numCores << "Clones:" << numClones << "Time:" << timer.elapsed(); } } QTEST_MAIN(KisAnimationRenderingBenchmark) diff --git a/krita/krita.action b/krita/krita.action index d575792eb0..7f365bca83 100644 --- a/krita/krita.action +++ b/krita/krita.action @@ -1,3652 +1,3663 @@ 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 &Invert Selection Invert current selection Invert Selection 10000000000 100 Ctrl+Shift+I false Create Snapshot Create Snapshot 1 0 false Switch to Selected Snapshot Switch to selected snapshot 1 0 false Remove Selected Snapshot Remove Selected Snapshot 1 0 false 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 X 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 &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 Selection Mode: Add Selection Mode: Add Selection Mode: Add A false Selection Mode: Subtract Selection Mode: Subtract Selection Mode: Subtract S false Selection Mode: Intersect Selection Mode: Intersect Selection Mode: Intersect false Selection Mode: Replace Selection Mode: Replace Selection Mode: Replace R 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 &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 &Invert Invert Invert 10000 0 Ctrl+I 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 Colorize Mask Tool Colorize Mask Tool Colorize Mask 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 Outline Selection Tool Outline Selection Tool Outline Selection Tool false Bezier Curve Selection Tool Bezier Curve Selection Tool Bezier Curve Selection Tool false Similar Color Selection Tool Similar Color Selection Tool 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 or double-click ends the curve. Bezier Curve Tool. Shift-mouseclick or double-click 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 Gradient Editing Tool Gradient editing Gradient editing false Reference Images Tool Reference Images Tool Reference Images Tool 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 Left Insert keyframes to the left of selection, moving the tail of animation to the right. 100000 0 false Insert Keyframe Right Insert keyframes to the right of selection, moving the tail of animation to the right. 100000 0 false Insert Multiple Keyframes Insert several keyframes based on user parameters. 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 Left Insert column to the left of selection, moving the tail of animation to the right 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 Multiple Columns Insert several columns based on user parameters. 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 Multiple 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 Multiple 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 Multiple 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 Multiple 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 - + + + Create cycle + + + + 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 1000 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 Convert to &animated layer Convert layer into animation frames Convert layer 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 symmetry-horizontal Mirror All Layers Hori&zontally Mirror All Layers Horizontally Mirror All Layers Horizontally 1000 1 false symmetry-vertical Mirror All Layers &Vertically Mirror All Layers Vertically Mirror All Layers Vertically 1000 1 false &Rotate All Layers... Rotate All Layers Rotate All Layers 1000 1 false object-rotate-right Rotate All &Layers 90° to the Right Rotate All Layers 90° to the Right Rotate All Layers 90° to the Right 1000 1 false object-rotate-left Rotate All Layers &90° to the Left Rotate All Layers 90° to the Left Rotate All Layers 90° to the Left 1000 1 false Rotate All Layers &180° Rotate All Layers 180° Rotate All Layers 180° 1000 1 false Scale All &Layers to new Size... Scale All Layers to new Size Scale All Layers to new Size 100000 1 false &Shear All Layers... Shear All Layers Shear All Layers 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 Set Copy F&rom... Set the source for the selected clone layer(s). Set Copy From 1000 1 false diff --git a/libs/global/KisCollectionUtils.h b/libs/global/KisCollectionUtils.h new file mode 100644 index 0000000000..e9e243a9be --- /dev/null +++ b/libs/global/KisCollectionUtils.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019 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 KRITA_KISCOLLECTIONUTILS_H +#define KRITA_KISCOLLECTIONUTILS_H + +namespace KisCollectionUtils { + /** + * Finds the last item the in the map with a key less than or equal to the given one. + * Returns map.constEnd() if no such key exists. + */ + template + typename QMap::const_iterator lastBeforeOrAt(const QMap &map, int maximumKey) + { + typename QMap::const_iterator i = map.upperBound(maximumKey); + if (i == map.constBegin()) return map.constEnd(); + return i - 1; + } + + /** + * Finds the last item the in the map with a key strictly less than the given key. + * Returns map.constEnd() if no such key exists. + */ + template + typename QMap::const_iterator lastBefore(const QMap &map, int currentKey) + { + typename QMap::const_iterator active = lastBeforeOrAt(map, currentKey); + if (active == map.constEnd()) return map.constEnd(); + + if (currentKey > active.key()) return active; + + if (active == map.constBegin()) return map.constEnd(); + return active - 1; + } + + /** + * Finds the first item the in the map with a key greater than the given one. + * Returns map.constEnd() if no such key exists. + */ + template + typename QMap::const_iterator firstAfter(const QMap &map, int currentKey) + { + return map.upperBound(currentKey); + } +} + +#endif //KRITA_KISCOLLECTIONUTILS_H diff --git a/libs/image/CMakeLists.txt b/libs/image/CMakeLists.txt index a28bbf86ca..ceef889125 100644 --- a/libs/image/CMakeLists.txt +++ b/libs/image/CMakeLists.txt @@ -1,380 +1,381 @@ add_subdirectory( tests ) add_subdirectory( tiles3 ) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty ${CMAKE_CURRENT_SOURCE_DIR}/brushengine ${CMAKE_CURRENT_SOURCE_DIR}/commands ${CMAKE_CURRENT_SOURCE_DIR}/commands_new ${CMAKE_CURRENT_SOURCE_DIR}/filter ${CMAKE_CURRENT_SOURCE_DIR}/floodfill ${CMAKE_CURRENT_SOURCE_DIR}/generator ${CMAKE_CURRENT_SOURCE_DIR}/layerstyles ${CMAKE_CURRENT_SOURCE_DIR}/processing ${CMAKE_SOURCE_DIR}/sdk/tests ) include_directories(SYSTEM ${EIGEN3_INCLUDE_DIR} ) if(FFTW3_FOUND) include_directories(${FFTW3_INCLUDE_DIR}) endif() if(HAVE_VC) include_directories(SYSTEM ${Vc_INCLUDE_DIR} ${Qt5Core_INCLUDE_DIRS} ${Qt5Gui_INCLUDE_DIRS}) ko_compile_for_all_implementations(__per_arch_circle_mask_generator_objs kis_brush_mask_applicator_factories.cpp) else() set(__per_arch_circle_mask_generator_objs kis_brush_mask_applicator_factories.cpp) endif() set(kritaimage_LIB_SRCS tiles3/kis_tile.cc tiles3/kis_tile_data.cc tiles3/kis_tile_data_store.cc tiles3/kis_tile_data_pooler.cc tiles3/kis_tiled_data_manager.cc tiles3/KisTiledExtentManager.cpp tiles3/kis_memento_manager.cc tiles3/kis_hline_iterator.cpp tiles3/kis_vline_iterator.cpp tiles3/kis_random_accessor.cc tiles3/swap/kis_abstract_compression.cpp tiles3/swap/kis_lzf_compression.cpp tiles3/swap/kis_abstract_tile_compressor.cpp tiles3/swap/kis_legacy_tile_compressor.cpp tiles3/swap/kis_tile_compressor_2.cpp tiles3/swap/kis_chunk_allocator.cpp tiles3/swap/kis_memory_window.cpp tiles3/swap/kis_swapped_data_store.cpp tiles3/swap/kis_tile_data_swapper.cpp kis_distance_information.cpp kis_painter.cc kis_painter_blt_multi_fixed.cpp kis_marker_painter.cpp KisPrecisePaintDeviceWrapper.cpp kis_progress_updater.cpp brushengine/kis_paint_information.cc brushengine/kis_random_source.cpp brushengine/KisPerStrokeRandomSource.cpp brushengine/kis_stroke_random_source.cpp brushengine/kis_paintop.cc brushengine/kis_paintop_factory.cpp brushengine/kis_paintop_preset.cpp brushengine/kis_paintop_registry.cc brushengine/kis_paintop_settings.cpp brushengine/kis_paintop_settings_update_proxy.cpp brushengine/kis_paintop_utils.cpp brushengine/kis_no_size_paintop_settings.cpp brushengine/kis_locked_properties.cc brushengine/kis_locked_properties_proxy.cpp brushengine/kis_locked_properties_server.cpp brushengine/kis_paintop_config_widget.cpp brushengine/kis_uniform_paintop_property.cpp brushengine/kis_combo_based_paintop_property.cpp brushengine/kis_slider_based_paintop_property.cpp brushengine/kis_standard_uniform_properties_factory.cpp brushengine/KisStrokeSpeedMeasurer.cpp brushengine/KisPaintopSettingsIds.cpp commands/kis_deselect_global_selection_command.cpp commands/KisDeselectActiveSelectionCommand.cpp commands/kis_image_change_layers_command.cpp commands/kis_image_change_visibility_command.cpp commands/kis_image_command.cpp commands/kis_image_set_projection_color_space_command.cpp commands/kis_image_layer_add_command.cpp commands/kis_image_layer_move_command.cpp commands/kis_image_layer_remove_command.cpp commands/kis_image_layer_remove_command_impl.cpp commands/kis_image_lock_command.cpp commands/kis_node_command.cpp commands/kis_node_compositeop_command.cpp commands/kis_node_opacity_command.cpp commands/kis_node_property_list_command.cpp commands/kis_reselect_global_selection_command.cpp commands/KisReselectActiveSelectionCommand.cpp commands/kis_set_global_selection_command.cpp commands/KisNodeRenameCommand.cpp commands_new/kis_saved_commands.cpp commands_new/kis_processing_command.cpp commands_new/kis_image_resize_command.cpp commands_new/kis_image_set_resolution_command.cpp commands_new/kis_node_move_command2.cpp commands_new/kis_set_layer_style_command.cpp commands_new/kis_selection_move_command2.cpp commands_new/kis_update_command.cpp commands_new/kis_switch_current_time_command.cpp commands_new/kis_change_projection_color_command.cpp commands_new/kis_activate_selection_mask_command.cpp commands_new/kis_transaction_based_command.cpp commands_new/KisHoldUIUpdatesCommand.cpp processing/kis_do_nothing_processing_visitor.cpp processing/kis_simple_processing_visitor.cpp processing/kis_crop_processing_visitor.cpp processing/kis_crop_selections_processing_visitor.cpp processing/kis_transform_processing_visitor.cpp processing/kis_mirror_processing_visitor.cpp processing/KisSelectionBasedProcessingHelper.cpp filter/kis_filter.cc filter/kis_filter_category_ids.cpp filter/kis_filter_configuration.cc filter/kis_color_transformation_configuration.cc filter/kis_filter_registry.cc filter/kis_color_transformation_filter.cc generator/kis_generator.cpp generator/kis_generator_layer.cpp generator/kis_generator_registry.cpp floodfill/kis_fill_interval_map.cpp floodfill/kis_scanline_fill.cpp lazybrush/kis_min_cut_worker.cpp lazybrush/kis_lazy_fill_tools.cpp lazybrush/kis_multiway_cut.cpp lazybrush/KisWatershedWorker.cpp lazybrush/kis_colorize_mask.cpp lazybrush/kis_colorize_stroke_strategy.cpp KisDelayedUpdateNodeInterface.cpp KisCroppedOriginalLayerInterface.cpp kis_adjustment_layer.cc kis_selection_based_layer.cpp kis_node_filter_interface.cpp kis_base_accessor.cpp kis_base_node.cpp kis_base_processor.cpp kis_bookmarked_configuration_manager.cc kis_node_uuid_info.cpp kis_clone_layer.cpp kis_colorspace_convert_visitor.cpp kis_config_widget.cpp kis_convolution_kernel.cc kis_convolution_painter.cc kis_gaussian_kernel.cpp kis_edge_detection_kernel.cpp kis_cubic_curve.cpp kis_default_bounds.cpp kis_default_bounds_base.cpp kis_effect_mask.cc kis_fast_math.cpp kis_fill_painter.cc kis_filter_mask.cpp kis_filter_strategy.cc kis_transform_mask.cpp kis_transform_mask_params_interface.cpp kis_recalculate_transform_mask_job.cpp kis_recalculate_generator_layer_job.cpp kis_transform_mask_params_factory_registry.cpp kis_safe_transform.cpp kis_gradient_painter.cc kis_gradient_shape_strategy.cpp kis_cached_gradient_shape_strategy.cpp kis_polygonal_gradient_shape_strategy.cpp kis_iterator_ng.cpp kis_async_merger.cpp kis_merge_walker.cc kis_updater_context.cpp kis_update_job_item.cpp kis_stroke_strategy_undo_command_based.cpp kis_simple_stroke_strategy.cpp KisRunnableBasedStrokeStrategy.cpp KisRunnableStrokeJobDataBase.cpp KisRunnableStrokeJobData.cpp KisRunnableStrokeJobsInterface.cpp KisFakeRunnableStrokeJobsExecutor.cpp kis_stroke_job_strategy.cpp kis_stroke_strategy.cpp kis_stroke.cpp kis_strokes_queue.cpp KisStrokesQueueMutatedJobInterface.cpp kis_simple_update_queue.cpp kis_update_scheduler.cpp kis_queues_progress_updater.cpp kis_composite_progress_proxy.cpp kis_sync_lod_cache_stroke_strategy.cpp kis_lod_capable_layer_offset.cpp kis_update_time_monitor.cpp KisImageConfigNotifier.cpp kis_group_layer.cc kis_count_visitor.cpp kis_histogram.cc kis_image_interfaces.cpp kis_image_animation_interface.cpp kis_time_range.cpp kis_node_graph_listener.cpp kis_image.cc kis_image_signal_router.cpp KisImageSignals.cpp kis_image_config.cpp kis_projection_updates_filter.cpp kis_suspend_projection_updates_stroke_strategy.cpp kis_regenerate_frame_stroke_strategy.cpp kis_switch_time_stroke_strategy.cpp kis_crop_saved_extra_data.cpp kis_timed_signal_threshold.cpp kis_layer.cc kis_indirect_painting_support.cpp kis_abstract_projection_plane.cpp kis_layer_projection_plane.cpp kis_layer_utils.cpp kis_mask_projection_plane.cpp kis_projection_leaf.cpp KisSafeNodeProjectionStore.cpp kis_mask.cc kis_base_mask_generator.cpp kis_rect_mask_generator.cpp kis_circle_mask_generator.cpp kis_gauss_circle_mask_generator.cpp kis_gauss_rect_mask_generator.cpp ${__per_arch_circle_mask_generator_objs} kis_curve_circle_mask_generator.cpp kis_curve_rect_mask_generator.cpp kis_math_toolbox.cpp kis_memory_statistics_server.cpp kis_name_server.cpp kis_node.cpp kis_node_facade.cpp kis_node_progress_proxy.cpp kis_busy_progress_indicator.cpp kis_node_visitor.cpp kis_paint_device.cc kis_paint_device_debug_utils.cpp kis_fixed_paint_device.cpp KisOptimizedByteArray.cpp kis_paint_layer.cc kis_perspective_math.cpp kis_pixel_selection.cpp kis_processing_information.cpp kis_properties_configuration.cc kis_random_accessor_ng.cpp kis_random_generator.cc kis_random_sub_accessor.cpp kis_wrapped_random_accessor.cpp kis_selection.cc KisSelectionUpdateCompressor.cpp kis_selection_mask.cpp kis_update_outline_job.cpp kis_update_selection_job.cpp kis_serializable_configuration.cc kis_transaction_data.cpp kis_transform_worker.cc kis_perspectivetransform_worker.cpp bsplines/kis_bspline_1d.cpp bsplines/kis_bspline_2d.cpp bsplines/kis_nu_bspline_2d.cpp kis_warptransform_worker.cc kis_cage_transform_worker.cpp kis_liquify_transform_worker.cpp kis_green_coordinates_math.cpp kis_transparency_mask.cc kis_undo_adapter.cpp kis_macro_based_undo_store.cpp kis_surrogate_undo_adapter.cpp kis_legacy_undo_adapter.cpp kis_post_execution_undo_adapter.cpp kis_processing_visitor.cpp kis_processing_applicator.cpp krita_utils.cpp kis_outline_generator.cpp kis_layer_composition.cpp kis_selection_filters.cpp KisProofingConfiguration.h KisRecycleProjectionsJob.cpp kis_keyframe.cpp kis_keyframe_channel.cpp kis_keyframe_commands.cpp kis_scalar_keyframe_channel.cpp kis_raster_keyframe_channel.cpp kis_onion_skin_compositor.cpp kis_onion_skin_cache.cpp kis_idle_watcher.cpp + kis_animation_cycle.cpp kis_psd_layer_style.cpp kis_layer_properties_icons.cpp layerstyles/kis_multiple_projection.cpp layerstyles/kis_layer_style_filter.cpp layerstyles/kis_layer_style_filter_environment.cpp layerstyles/kis_layer_style_filter_projection_plane.cpp layerstyles/kis_layer_style_projection_plane.cpp layerstyles/kis_ls_drop_shadow_filter.cpp layerstyles/kis_ls_satin_filter.cpp layerstyles/kis_ls_stroke_filter.cpp layerstyles/kis_ls_bevel_emboss_filter.cpp layerstyles/kis_ls_overlay_filter.cpp layerstyles/kis_ls_utils.cpp layerstyles/gimp_bump_map.cpp layerstyles/KisLayerStyleKnockoutBlower.cpp KisProofingConfiguration.cpp kis_node_query_path.cc ) set(einspline_SRCS 3rdparty/einspline/bspline_create.cpp 3rdparty/einspline/bspline_data.cpp 3rdparty/einspline/multi_bspline_create.cpp 3rdparty/einspline/nubasis.cpp 3rdparty/einspline/nubspline_create.cpp 3rdparty/einspline/nugrid.cpp ) add_library(kritaimage SHARED ${kritaimage_LIB_SRCS} ${einspline_SRCS}) generate_export_header(kritaimage BASE_NAME kritaimage) target_link_libraries(kritaimage PUBLIC kritaversion kritawidgets kritaglobal kritapsd kritaodf kritapigment kritacommand kritawidgetutils kritametadata Qt5::Concurrent ) target_link_libraries(kritaimage PUBLIC ${Boost_SYSTEM_LIBRARY}) if(NOT HAVE_CXX_ATOMICS_WITHOUT_LIB) if(NOT HAVE_CXX_ATOMICS64_WITHOUT_LIB) target_link_libraries(kritaimage PUBLIC atomic) endif() endif() if(OPENEXR_FOUND) target_link_libraries(kritaimage PUBLIC ${OPENEXR_LIBRARIES}) endif() if(FFTW3_FOUND) target_link_libraries(kritaimage PRIVATE ${FFTW3_LIBRARIES}) endif() if(HAVE_VC) target_link_libraries(kritaimage PUBLIC ${Vc_LIBRARIES}) endif() if (NOT GSL_FOUND) message (WARNING "KRITA WARNING! No GNU Scientific Library was found! Krita's Shaped Gradients might be non-normalized! Please install GSL library.") else () target_link_libraries(kritaimage PRIVATE ${GSL_LIBRARIES} ${GSL_CBLAS_LIBRARIES}) endif () target_include_directories(kritaimage PUBLIC $ $ $ $ $ ) set_target_properties(kritaimage PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} ) install(TARGETS kritaimage ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/libs/image/kis_animation_cycle.cpp b/libs/image/kis_animation_cycle.cpp new file mode 100644 index 0000000000..0a3e8f072c --- /dev/null +++ b/libs/image/kis_animation_cycle.cpp @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2018 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_cycle.h" +#include +#include "kis_time_range.h" +#include "kis_keyframe_channel.h" + +KisRepeatFrame::KisRepeatFrame(KisKeyframeChannel *channel, int time, KisTimeSpan sourceRange) + : KisKeyframeBase(channel, time) + , m_range(sourceRange) +{} + +KisRepeatFrame::KisRepeatFrame(const KisRepeatFrame &rhs, KisTimeSpan newRange) + : KisRepeatFrame(rhs.channel(), rhs.time(), newRange) +{} + +KisRepeatFrame::KisRepeatFrame(const KisRepeatFrame &rhs, KisKeyframeChannel *newChannel) + : KisRepeatFrame(newChannel, rhs.time(), rhs.m_range) +{} + +QRect KisRepeatFrame::affectedRect() const +{ + QRect rect; + + for (auto keyframe : channel()->itemsWithin(m_range)) { + const QSharedPointer key = keyframe.dynamicCast(); + KIS_SAFE_ASSERT_RECOVER(key) { continue; } + + rect |= key->affectedRect(); + } + + return rect; +} + +KisTimeSpan KisRepeatFrame::sourceRange() const +{ + return m_range; +} + +int KisRepeatFrame::getOriginalTimeFor(int time) const +{ + KisTimeSpan originalRange = m_range; + const int timeWithinCycle = (time - this->time()) % originalRange.duration(); + return originalRange.start() + timeWithinCycle; +} + +KisKeyframeSP KisRepeatFrame::getOriginalKeyframeFor(int time) const +{ + return channel()->activeKeyframeAt(getOriginalTimeFor(time)); +} + +int KisRepeatFrame::firstInstanceOf(int originalTime) const +{ + const int timeWithinCycle = originalTime - m_range.start(); + + if (timeWithinCycle < this->duration()) return -1; + + return this->time() + timeWithinCycle; +} + +KisFrameSet KisRepeatFrame::instancesWithin(KisKeyframeSP original, KisTimeSpan range) const +{ + const int originalTime = original->time(); + const int interval = m_range.duration(); + const int lastOfCycle = lastFrame(); + + const int originalFrameDuration = original->duration(); + const int repeatDuration = (originalTime + originalFrameDuration < m_range.end()) + ? originalFrameDuration : (m_range.end() - originalTime + 1); + + QVector spans; + + int firstInstance = firstInstanceOf(originalTime); + + if (firstInstance == -1) { + return {}; + } else if (range.isEmpty() && lastOfCycle == -1) { + return KisFrameSet::infiniteFrom(firstInstance); + } else { + // Determine the relevant range to consider, ie. the overlap of repeat and the given range (if any) + const int firstFrame = KisTime::max(range.start(), time()); + const int lastFrame = KisTime::min(lastOfCycle, range.end()); + + // Skip past the instances before the range + const int firstInstanceEnd = firstInstance + repeatDuration - 1; + if (firstInstanceEnd < firstFrame) { + const int repetitionsToSkip = 1 + (firstFrame - firstInstanceEnd - 1) / interval; // effectively: ceil((firstFrame - firstInstanceEnd) / interval) + firstInstance += interval * repetitionsToSkip; + } + + // Find the range of each repeat + for (int repeatTime = firstInstance; repeatTime <= lastFrame; repeatTime += interval) { + const int repeatStartTime = std::max(repeatTime, firstFrame); + const bool endsWithinRange = repeatDuration != -1 && repeatTime + repeatDuration - 1 <= lastFrame; + const int repeatEndTime = endsWithinRange ? (repeatTime + repeatDuration - 1) : lastFrame; + spans.append(KisTimeSpan(repeatStartTime, repeatEndTime)); + } + } + + return KisFrameSet(spans); +} + +int KisRepeatFrame::previousVisibleFrame(int time) const +{ + if (time <= this->time()) return -1; + + const int earlierOriginalTime = getOriginalTimeFor(time - 1); + + int originalStart, originalEnd; + channel()->activeKeyframeRange(earlierOriginalTime, &originalStart, &originalEnd); + if (originalEnd == -1) return -1; + + const int durationOfOriginalKeyframe = originalEnd + 1 - originalStart; + return time - durationOfOriginalKeyframe; +} + +int KisRepeatFrame::nextVisibleFrame(int time) const +{ + const int originalTime = getOriginalTimeFor(time); + int originalStart, originalEnd; + channel()->activeKeyframeRange(originalTime, &originalStart, &originalEnd); + if (originalEnd == -1) return -1; + + const int durationOfOriginalKeyframe = originalEnd + 1 - originalStart; + const int nextFrameTime = time + durationOfOriginalKeyframe; + + const int endTime = lastFrame(); + return (endTime == -1 || nextFrameTime < endTime) ? nextFrameTime : -1; +} + +int KisRepeatFrame::lastFrame() const +{ + const KisKeyframeBaseSP next = channel()->nextItem(*this); + return next ? next->time() - 1 : -1; +} diff --git a/libs/image/kis_animation_cycle.h b/libs/image/kis_animation_cycle.h new file mode 100644 index 0000000000..e18a052ef8 --- /dev/null +++ b/libs/image/kis_animation_cycle.h @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2018 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_CYCLE_H +#define KIS_ANIMATION_CYCLE_H + +#include "kis_keyframe.h" +#include "kis_time_range.h" + +class KisTimeSpan; +class KisFrameSet; +class KisRepeatFrame; + +class KRITAIMAGE_EXPORT KisRepeatFrame : public KisKeyframeBase +{ +public: + KisRepeatFrame(KisKeyframeChannel *channel, int time, KisTimeSpan sourceRange); + KisRepeatFrame(const KisRepeatFrame &rhs, KisTimeSpan newRange); + KisRepeatFrame(const KisRepeatFrame &rhs, KisKeyframeChannel *newChannel); + + QRect affectedRect() const override; + + KisTimeSpan sourceRange() const; + + int getOriginalTimeFor(int time) const; + KisKeyframeSP getOriginalKeyframeFor(int time) const override; + + /// Returns the earliest time the original frame appears in this repeat, or -1 if it never does. + int firstInstanceOf(int originalTime) const; + + /** + * Finds the frames on which the given original keyframe is displayed by the repeat. + * If a non-empty range is given, the result will contain only matching frames. + * If an empty range is given, the result may also contain other frames between repeated ones, + * such as when the repeat continues infinitely. + */ + KisFrameSet instancesWithin(KisKeyframeSP original, KisTimeSpan range) const; + + /** Returns the time at which the previous frame within the repeat appears, + * or -1 if time is at the first repeated frame. + * NB: time should be at the start of a repeated frame + */ + int previousVisibleFrame(int time) const; + + /** Returns the time at which the next frame within the repeat appears, + * or -1 if time is at the last repeated frame. + * NB: time should be at the start of a repeated frame + */ + int nextVisibleFrame(int time) const; + + /** + * Finds the time of the next keyframe if any. + * Returns -1 if the cycle continues indefinitely. + */ + int lastFrame() const; + +private: + KisTimeSpan m_range; +}; + +#endif diff --git a/libs/image/kis_colorspace_convert_visitor.cpp b/libs/image/kis_colorspace_convert_visitor.cpp index ce003e8967..1d39568edf 100644 --- a/libs/image/kis_colorspace_convert_visitor.cpp +++ b/libs/image/kis_colorspace_convert_visitor.cpp @@ -1,155 +1,155 @@ /* * Copyright (c) 2005 C. Boemann * * 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_colorspace_convert_visitor.h" #include "kis_image.h" #include "kis_paint_device.h" #include "kis_undo_adapter.h" #include "kis_adjustment_layer.h" #include "kis_paint_layer.h" #include "kis_group_layer.h" #include "lazybrush/kis_colorize_mask.h" #include "kis_external_layer_iface.h" #include "filter/kis_filter_configuration.h" #include "filter/kis_filter_registry.h" #include "filter/kis_filter.h" #include "kis_generator.h" #include "kis_generator_registry.h" #include "generator/kis_generator_layer.h" #include "kis_time_range.h" #include KisColorSpaceConvertVisitor::KisColorSpaceConvertVisitor(KisImageWSP image, const KoColorSpace *srcColorSpace, const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) : KisNodeVisitor() , m_image(image) , m_srcColorSpace(srcColorSpace) , m_dstColorSpace(dstColorSpace) , m_renderingIntent(renderingIntent) , m_conversionFlags(conversionFlags) { } KisColorSpaceConvertVisitor::~KisColorSpaceConvertVisitor() { } bool KisColorSpaceConvertVisitor::visit(KisGroupLayer * layer) { convertPaintDevice(layer); KisLayerSP child = qobject_cast(layer->firstChild().data()); while (child) { child->accept(*this); child = qobject_cast(child->nextSibling().data()); } layer->resetCache(); return true; } bool KisColorSpaceConvertVisitor::visit(KisPaintLayer *layer) { return convertPaintDevice(layer); } bool KisColorSpaceConvertVisitor::visit(KisGeneratorLayer *layer) { layer->resetCache(); return true; } bool KisColorSpaceConvertVisitor::visit(KisAdjustmentLayer * layer) { // XXX: Make undoable! if (layer->filter()->name() == "perchannel") { // Per-channel filters need to be reset because of different number // of channels. This makes undo very tricky, but so be it. // XXX: Make this more generic for after 1.6, when we'll have many // channel-specific filters. KisFilterSP f = KisFilterRegistry::instance()->value("perchannel"); layer->setFilter(f->defaultConfiguration()); } layer->resetCache(); return true; } bool KisColorSpaceConvertVisitor::convertPaintDevice(KisLayer* layer) { if (*m_dstColorSpace == *layer->colorSpace()) return true; bool alphaLock = false; if (m_srcColorSpace->colorModelId() != m_dstColorSpace->colorModelId()) { layer->setChannelFlags(m_emptyChannelFlags); KisPaintLayer *paintLayer = 0; if ((paintLayer = dynamic_cast(layer))) { alphaLock = paintLayer->alphaLocked(); paintLayer->setChannelLockFlags(QBitArray()); } } KisImageSP image = m_image.toStrongRef(); if (!image) { return false; } KUndo2Command *parentConversionCommand = new KUndo2Command(); if (layer->original()) { layer->original()->convertTo(m_dstColorSpace, m_renderingIntent, m_conversionFlags, parentConversionCommand); } if (layer->paintDevice()) { layer->paintDevice()->convertTo(m_dstColorSpace, m_renderingIntent, m_conversionFlags, parentConversionCommand); } if (layer->projection()) { layer->projection()->convertTo(m_dstColorSpace, m_renderingIntent, m_conversionFlags, parentConversionCommand); } image->undoAdapter()->addCommand(parentConversionCommand); KisPaintLayer *paintLayer = 0; if ((paintLayer = dynamic_cast(layer))) { paintLayer->setAlphaLocked(alphaLock); } layer->setDirty(); - layer->invalidateFrames(KisTimeRange::infinite(0), layer->extent()); + layer->invalidateFrames(KisFrameSet::infiniteFrom(0), layer->extent()); return true; } bool KisColorSpaceConvertVisitor::visit(KisColorizeMask *mask) { KisImageSP image = m_image.toStrongRef(); if (!image) { return false; } KUndo2Command* cmd = mask->setColorSpace(m_dstColorSpace, m_renderingIntent, m_conversionFlags); if (cmd) { image->undoAdapter()->addCommand(cmd); } return true; } diff --git a/libs/image/kis_image.cc b/libs/image/kis_image.cc index 8b112848fc..ee87549199 100644 --- a/libs/image/kis_image.cc +++ b/libs/image/kis_image.cc @@ -1,2068 +1,2068 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2007 Boudewijn Rempt * * 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.h" #include // WORDS_BIGENDIAN #include #include #include #include #include #include #include #include #include #include #include "KoColorSpaceRegistry.h" #include "KoColor.h" #include "KoColorProfile.h" #include #include "KisProofingConfiguration.h" #include "kis_adjustment_layer.h" #include "kis_annotation.h" #include "kis_change_profile_visitor.h" #include "kis_colorspace_convert_visitor.h" #include "kis_count_visitor.h" #include "kis_filter_strategy.h" #include "kis_group_layer.h" #include "commands/kis_image_commands.h" #include "kis_layer.h" #include "kis_meta_data_merge_strategy_registry.h" #include "kis_name_server.h" #include "kis_paint_layer.h" #include "kis_projection_leaf.h" #include "kis_painter.h" #include "kis_selection.h" #include "kis_transaction.h" #include "kis_meta_data_merge_strategy.h" #include "kis_memory_statistics_server.h" #include "kis_image_config.h" #include "kis_update_scheduler.h" #include "kis_image_signal_router.h" #include "kis_image_animation_interface.h" #include "kis_stroke_strategy.h" #include "kis_simple_stroke_strategy.h" #include "kis_image_barrier_locker.h" #include "kis_undo_stores.h" #include "kis_legacy_undo_adapter.h" #include "kis_post_execution_undo_adapter.h" #include "kis_transform_worker.h" #include "kis_processing_applicator.h" #include "processing/kis_crop_processing_visitor.h" #include "processing/kis_crop_selections_processing_visitor.h" #include "processing/kis_transform_processing_visitor.h" #include "commands_new/kis_image_resize_command.h" #include "commands_new/kis_image_set_resolution_command.h" #include "commands_new/kis_activate_selection_mask_command.h" #include "kis_composite_progress_proxy.h" #include "kis_layer_composition.h" #include "kis_wrapped_rect.h" #include "kis_crop_saved_extra_data.h" #include "kis_layer_utils.h" #include "kis_lod_transform.h" #include "kis_suspend_projection_updates_stroke_strategy.h" #include "kis_sync_lod_cache_stroke_strategy.h" #include "kis_projection_updates_filter.h" #include "kis_layer_projection_plane.h" #include "kis_update_time_monitor.h" #include "tiles3/kis_lockless_stack.h" #include #include #include "kis_time_range.h" #include "KisRunnableBasedStrokeStrategy.h" #include "KisRunnableStrokeJobData.h" #include "KisRunnableStrokeJobUtils.h" #include "KisRunnableStrokeJobsInterface.h" // #define SANITY_CHECKS #ifdef SANITY_CHECKS #define SANITY_CHECK_LOCKED(name) \ if (!locked()) warnKrita() << "Locking policy failed:" << name \ << "has been called without the image" \ "being locked"; #else #define SANITY_CHECK_LOCKED(name) #endif struct KisImageSPStaticRegistrar { KisImageSPStaticRegistrar() { qRegisterMetaType("KisImageSP"); } }; static KisImageSPStaticRegistrar __registrar; class KisImage::KisImagePrivate { public: KisImagePrivate(KisImage *_q, qint32 w, qint32 h, const KoColorSpace *c, KisUndoStore *undo, KisImageAnimationInterface *_animationInterface) : q(_q) , lockedForReadOnly(false) , width(w) , height(h) , colorSpace(c ? c : KoColorSpaceRegistry::instance()->rgb8()) , nserver(1) , undoStore(undo ? undo : new KisDumbUndoStore()) , legacyUndoAdapter(undoStore.data(), _q) , postExecutionUndoAdapter(undoStore.data(), _q) , signalRouter(_q) , animationInterface(_animationInterface) , scheduler(_q, _q) , axesCenter(QPointF(0.5, 0.5)) { { KisImageConfig cfg(true); if (cfg.enableProgressReporting()) { scheduler.setProgressProxy(&compositeProgressProxy); } // Each of these lambdas defines a new factory function. scheduler.setLod0ToNStrokeStrategyFactory( [=](bool forgettable) { return KisLodSyncPair( new KisSyncLodCacheStrokeStrategy(KisImageWSP(q), forgettable), KisSyncLodCacheStrokeStrategy::createJobsData(KisImageWSP(q))); }); scheduler.setSuspendUpdatesStrokeStrategyFactory( [=]() { return KisSuspendResumePair( new KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP(q), true), KisSuspendProjectionUpdatesStrokeStrategy::createSuspendJobsData(KisImageWSP(q))); }); scheduler.setResumeUpdatesStrokeStrategyFactory( [=]() { return KisSuspendResumePair( new KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP(q), false), KisSuspendProjectionUpdatesStrokeStrategy::createResumeJobsData(KisImageWSP(q))); }); } connect(q, SIGNAL(sigImageModified()), KisMemoryStatisticsServer::instance(), SLOT(notifyImageChanged())); } ~KisImagePrivate() { /** * Stop animation interface. It may use the rootLayer. */ delete animationInterface; /** * First delete the nodes, while strokes * and undo are still alive */ rootLayer.clear(); } KisImage *q; quint32 lockCount = 0; bool lockedForReadOnly; qint32 width; qint32 height; double xres = 1.0; double yres = 1.0; const KoColorSpace * colorSpace; KisProofingConfigurationSP proofingConfig; KisSelectionSP deselectedGlobalSelection; KisGroupLayerSP rootLayer; // The layers are contained in here KisSelectionMaskSP targetOverlaySelectionMask; // the overlay switching stroke will try to switch into this mask KisSelectionMaskSP overlaySelectionMask; QList compositions; KisNodeSP isolatedRootNode; bool wrapAroundModePermitted = false; KisNameServer nserver; QScopedPointer undoStore; KisLegacyUndoAdapter legacyUndoAdapter; KisPostExecutionUndoAdapter postExecutionUndoAdapter; vKisAnnotationSP annotations; QAtomicInt disableUIUpdateSignals; KisLocklessStack savedDisabledUIUpdates; KisProjectionUpdatesFilterSP projectionUpdatesFilter; KisImageSignalRouter signalRouter; KisImageAnimationInterface *animationInterface; KisUpdateScheduler scheduler; QAtomicInt disableDirtyRequests; KisCompositeProgressProxy compositeProgressProxy; bool blockLevelOfDetail = false; QPointF axesCenter; bool allowMasksOnRootNode = false; bool tryCancelCurrentStrokeAsync(); void notifyProjectionUpdatedInPatches(const QRect &rc, QVector &jobs); }; KisImage::KisImage(KisUndoStore *undoStore, qint32 width, qint32 height, const KoColorSpace * colorSpace, const QString& name) : QObject(0) , KisShared() , m_d(new KisImagePrivate(this, width, height, colorSpace, undoStore, new KisImageAnimationInterface(this))) { // make sure KisImage belongs to the GUI thread moveToThread(qApp->thread()); connect(this, SIGNAL(sigInternalStopIsolatedModeRequested()), SLOT(stopIsolatedMode())); setObjectName(name); setRootLayer(new KisGroupLayer(this, "root", OPACITY_OPAQUE_U8)); } KisImage::~KisImage() { dbgImage << "deleting kisimage" << objectName(); /** * Request the tools to end currently running strokes */ waitForDone(); delete m_d; disconnect(); // in case Qt gets confused } KisImageSP KisImage::fromQImage(const QImage &image, KisUndoStore *undoStore) { const KoColorSpace *colorSpace = 0; switch (image.format()) { case QImage::Format_Invalid: case QImage::Format_Mono: case QImage::Format_MonoLSB: colorSpace = KoColorSpaceRegistry::instance()->graya8(); break; case QImage::Format_Indexed8: case QImage::Format_RGB32: case QImage::Format_ARGB32: case QImage::Format_ARGB32_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->rgb8(); break; case QImage::Format_RGB16: colorSpace = KoColorSpaceRegistry::instance()->rgb16(); break; case QImage::Format_ARGB8565_Premultiplied: case QImage::Format_RGB666: case QImage::Format_ARGB6666_Premultiplied: case QImage::Format_RGB555: case QImage::Format_ARGB8555_Premultiplied: case QImage::Format_RGB888: case QImage::Format_RGB444: case QImage::Format_ARGB4444_Premultiplied: case QImage::Format_RGBX8888: case QImage::Format_RGBA8888: case QImage::Format_RGBA8888_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->rgb8(); break; case QImage::Format_BGR30: case QImage::Format_A2BGR30_Premultiplied: case QImage::Format_RGB30: case QImage::Format_A2RGB30_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->rgb8(); break; case QImage::Format_Alpha8: colorSpace = KoColorSpaceRegistry::instance()->alpha8(); break; case QImage::Format_Grayscale8: colorSpace = KoColorSpaceRegistry::instance()->graya8(); break; #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) case QImage::Format_Grayscale16: colorSpace = KoColorSpaceRegistry::instance()->graya16(); break; #endif #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) case QImage::Format_RGBX64: case QImage::Format_RGBA64: case QImage::Format_RGBA64_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), 0); break; #endif default: colorSpace = 0; } KisImageSP img = new KisImage(undoStore, image.width(), image.height(), colorSpace, i18n("Imported Image")); KisPaintLayerSP layer = new KisPaintLayer(img, img->nextLayerName(), 255); layer->paintDevice()->convertFromQImage(image, 0, 0, 0); img->addNode(layer.data(), img->rootLayer().data()); return img; } KisImage *KisImage::clone(bool exactCopy) { return new KisImage(*this, 0, exactCopy); } void KisImage::copyFromImage(const KisImage &rhs) { copyFromImageImpl(rhs, REPLACE); } void KisImage::copyFromImageImpl(const KisImage &rhs, int policy) { // make sure we choose exactly one from REPLACE and CONSTRUCT KIS_ASSERT_RECOVER_RETURN((policy & REPLACE) != (policy & CONSTRUCT)); // only when replacing do we need to emit signals #define EMIT_IF_NEEDED if (!(policy & REPLACE)) {} else emit if (policy & REPLACE) { // if we are constructing the image, these are already set if (m_d->width != rhs.width() || m_d->height != rhs.height()) { m_d->width = rhs.width(); m_d->height = rhs.height(); emit sigSizeChanged(QPointF(), QPointF()); } if (m_d->colorSpace != rhs.colorSpace()) { m_d->colorSpace = rhs.colorSpace(); emit sigColorSpaceChanged(m_d->colorSpace); } } // from KisImage::KisImage(const KisImage &, KisUndoStore *, bool) setObjectName(rhs.objectName()); if (m_d->xres != rhs.m_d->xres || m_d->yres != rhs.m_d->yres) { m_d->xres = rhs.m_d->xres; m_d->yres = rhs.m_d->yres; EMIT_IF_NEEDED sigResolutionChanged(m_d->xres, m_d->yres); } m_d->allowMasksOnRootNode = rhs.m_d->allowMasksOnRootNode; if (rhs.m_d->proofingConfig) { KisProofingConfigurationSP proofingConfig(new KisProofingConfiguration(*rhs.m_d->proofingConfig)); if (policy & REPLACE) { setProofingConfiguration(proofingConfig); } else { m_d->proofingConfig = proofingConfig; } } KisNodeSP newRoot = rhs.root()->clone(); newRoot->setGraphListener(this); newRoot->setImage(this); m_d->rootLayer = dynamic_cast(newRoot.data()); setRoot(newRoot); bool exactCopy = policy & EXACT_COPY; if (exactCopy || rhs.m_d->isolatedRootNode) { QQueue linearizedNodes; KisLayerUtils::recursiveApplyNodes(rhs.root(), [&linearizedNodes](KisNodeSP node) { linearizedNodes.enqueue(node); }); KisLayerUtils::recursiveApplyNodes(newRoot, [&linearizedNodes, exactCopy, &rhs, this](KisNodeSP node) { KisNodeSP refNode = linearizedNodes.dequeue(); if (exactCopy) { node->setUuid(refNode->uuid()); } if (rhs.m_d->isolatedRootNode && rhs.m_d->isolatedRootNode == refNode) { m_d->isolatedRootNode = node; } }); } KisLayerUtils::recursiveApplyNodes(newRoot, [](KisNodeSP node) { dbgImage << "Node: " << (void *)node.data(); }); m_d->compositions.clear(); Q_FOREACH (KisLayerCompositionSP comp, rhs.m_d->compositions) { m_d->compositions << toQShared(new KisLayerComposition(*comp, this)); } EMIT_IF_NEEDED sigLayersChangedAsync(); m_d->nserver = rhs.m_d->nserver; vKisAnnotationSP newAnnotations; Q_FOREACH (KisAnnotationSP annotation, rhs.m_d->annotations) { newAnnotations << annotation->clone(); } m_d->annotations = newAnnotations; KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->projectionUpdatesFilter); KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->disableUIUpdateSignals); KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->disableDirtyRequests); m_d->blockLevelOfDetail = rhs.m_d->blockLevelOfDetail; /** * The overlay device is not inherited when cloning the image! */ if (rhs.m_d->overlaySelectionMask) { const QRect dirtyRect = rhs.m_d->overlaySelectionMask->extent(); m_d->rootLayer->setDirty(dirtyRect); } #undef EMIT_IF_NEEDED } KisImage::KisImage(const KisImage& rhs, KisUndoStore *undoStore, bool exactCopy) : KisNodeFacade(), KisNodeGraphListener(), KisShared(), m_d(new KisImagePrivate(this, rhs.width(), rhs.height(), rhs.colorSpace(), undoStore ? undoStore : new KisDumbUndoStore(), new KisImageAnimationInterface(*rhs.animationInterface(), this))) { // make sure KisImage belongs to the GUI thread moveToThread(qApp->thread()); connect(this, SIGNAL(sigInternalStopIsolatedModeRequested()), SLOT(stopIsolatedMode())); copyFromImageImpl(rhs, CONSTRUCT | (exactCopy ? EXACT_COPY : 0)); } void KisImage::aboutToAddANode(KisNode *parent, int index) { KisNodeGraphListener::aboutToAddANode(parent, index); SANITY_CHECK_LOCKED("aboutToAddANode"); } void KisImage::nodeHasBeenAdded(KisNode *parent, int index) { KisNodeGraphListener::nodeHasBeenAdded(parent, index); SANITY_CHECK_LOCKED("nodeHasBeenAdded"); m_d->signalRouter.emitNodeHasBeenAdded(parent, index); } void KisImage::aboutToRemoveANode(KisNode *parent, int index) { KisNodeSP deletedNode = parent->at(index); if (!dynamic_cast(deletedNode.data()) && deletedNode == m_d->isolatedRootNode) { emit sigInternalStopIsolatedModeRequested(); } KisNodeGraphListener::aboutToRemoveANode(parent, index); SANITY_CHECK_LOCKED("aboutToRemoveANode"); m_d->signalRouter.emitAboutToRemoveANode(parent, index); } void KisImage::nodeChanged(KisNode* node) { KisNodeGraphListener::nodeChanged(node); requestStrokeEnd(); m_d->signalRouter.emitNodeChanged(node); } void KisImage::invalidateAllFrames() { - invalidateFrames(KisTimeRange::infinite(0), QRect()); + invalidateFrames(KisFrameSet::infiniteFrom(0), QRect()); } void KisImage::setOverlaySelectionMask(KisSelectionMaskSP mask) { if (m_d->targetOverlaySelectionMask == mask) return; m_d->targetOverlaySelectionMask = mask; struct UpdateOverlaySelectionStroke : public KisSimpleStrokeStrategy { UpdateOverlaySelectionStroke(KisImageSP image) : KisSimpleStrokeStrategy("update-overlay-selection-mask", kundo2_noi18n("update-overlay-selection-mask")), m_image(image) { this->enableJob(JOB_INIT, true, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); setClearsRedoOnStart(false); } void initStrokeCallback() { KisSelectionMaskSP oldMask = m_image->m_d->overlaySelectionMask; KisSelectionMaskSP newMask = m_image->m_d->targetOverlaySelectionMask; if (oldMask == newMask) return; KIS_SAFE_ASSERT_RECOVER_RETURN(!newMask || newMask->graphListener() == m_image); m_image->m_d->overlaySelectionMask = newMask; if (oldMask || newMask) { m_image->m_d->rootLayer->notifyChildMaskChanged(); } if (oldMask) { m_image->m_d->rootLayer->setDirtyDontResetAnimationCache(oldMask->extent()); } if (newMask) { newMask->setDirty(); } m_image->undoAdapter()->emitSelectionChanged(); } private: KisImageSP m_image; }; KisStrokeId id = startStroke(new UpdateOverlaySelectionStroke(this)); endStroke(id); } KisSelectionMaskSP KisImage::overlaySelectionMask() const { return m_d->overlaySelectionMask; } bool KisImage::hasOverlaySelectionMask() const { return m_d->overlaySelectionMask; } KisSelectionSP KisImage::globalSelection() const { KisSelectionMaskSP selectionMask = m_d->rootLayer->selectionMask(); if (selectionMask) { return selectionMask->selection(); } else { return 0; } } void KisImage::setGlobalSelection(KisSelectionSP globalSelection) { KisSelectionMaskSP selectionMask = m_d->rootLayer->selectionMask(); if (!globalSelection) { if (selectionMask) { removeNode(selectionMask); } } else { if (!selectionMask) { selectionMask = new KisSelectionMask(this); selectionMask->initSelection(m_d->rootLayer); addNode(selectionMask); // If we do not set the selection now, the setActive call coming next // can be very, very expensive, depending on the size of the image. selectionMask->setSelection(globalSelection); selectionMask->setActive(true); } else { selectionMask->setSelection(globalSelection); } KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->rootLayer->childCount() > 0); KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->rootLayer->selectionMask()); } m_d->deselectedGlobalSelection = 0; m_d->legacyUndoAdapter.emitSelectionChanged(); } void KisImage::deselectGlobalSelection() { KisSelectionSP savedSelection = globalSelection(); setGlobalSelection(0); m_d->deselectedGlobalSelection = savedSelection; } bool KisImage::canReselectGlobalSelection() { return m_d->deselectedGlobalSelection; } void KisImage::reselectGlobalSelection() { if(m_d->deselectedGlobalSelection) { setGlobalSelection(m_d->deselectedGlobalSelection); } } QString KisImage::nextLayerName(const QString &_baseName) const { QString baseName = _baseName; if (m_d->nserver.currentSeed() == 0) { m_d->nserver.number(); return i18n("background"); } if (baseName.isEmpty()) { baseName = i18n("Layer"); } return QString("%1 %2").arg(baseName).arg(m_d->nserver.number()); } void KisImage::rollBackLayerName() { m_d->nserver.rollback(); } KisCompositeProgressProxy* KisImage::compositeProgressProxy() { return &m_d->compositeProgressProxy; } bool KisImage::locked() const { return m_d->lockCount != 0; } void KisImage::barrierLock(bool readOnly) { if (!locked()) { requestStrokeEnd(); m_d->scheduler.barrierLock(); m_d->lockedForReadOnly = readOnly; } else { m_d->lockedForReadOnly &= readOnly; } m_d->lockCount++; } bool KisImage::tryBarrierLock(bool readOnly) { bool result = true; if (!locked()) { result = m_d->scheduler.tryBarrierLock(); m_d->lockedForReadOnly = readOnly; } if (result) { m_d->lockCount++; m_d->lockedForReadOnly &= readOnly; } return result; } bool KisImage::isIdle(bool allowLocked) { return (allowLocked || !locked()) && m_d->scheduler.isIdle(); } void KisImage::lock() { if (!locked()) { requestStrokeEnd(); m_d->scheduler.lock(); } m_d->lockCount++; m_d->lockedForReadOnly = false; } void KisImage::unlock() { Q_ASSERT(locked()); if (locked()) { m_d->lockCount--; if (m_d->lockCount == 0) { m_d->scheduler.unlock(!m_d->lockedForReadOnly); } } } void KisImage::blockUpdates() { m_d->scheduler.blockUpdates(); } void KisImage::unblockUpdates() { m_d->scheduler.unblockUpdates(); } void KisImage::setSize(const QSize& size) { m_d->width = size.width(); m_d->height = size.height(); } void KisImage::resizeImageImpl(const QRect& newRect, bool cropLayers) { if (newRect == bounds() && !cropLayers) return; KUndo2MagicString actionName = cropLayers ? kundo2_i18n("Crop Image") : kundo2_i18n("Resize Image"); KisImageSignalVector emitSignals; emitSignals << ComplexSizeChangedSignal(newRect, newRect.size()); emitSignals << ModifiedSignal; KisCropSavedExtraData *extraData = new KisCropSavedExtraData(cropLayers ? KisCropSavedExtraData::CROP_IMAGE : KisCropSavedExtraData::RESIZE_IMAGE, newRect); KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | KisProcessingApplicator::NO_UI_UPDATES, emitSignals, actionName, extraData); if (cropLayers || !newRect.topLeft().isNull()) { KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, cropLayers, true); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } applicator.applyCommand(new KisImageResizeCommand(this, newRect.size())); applicator.end(); } void KisImage::resizeImage(const QRect& newRect) { resizeImageImpl(newRect, false); } void KisImage::cropImage(const QRect& newRect) { resizeImageImpl(newRect, true); } void KisImage::cropNode(KisNodeSP node, const QRect& newRect) { bool isLayer = qobject_cast(node.data()); KUndo2MagicString actionName = isLayer ? kundo2_i18n("Crop Layer") : kundo2_i18n("Crop Mask"); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisCropSavedExtraData *extraData = new KisCropSavedExtraData(KisCropSavedExtraData::CROP_LAYER, newRect, node); KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE, emitSignals, actionName, extraData); KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, true, false); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } void KisImage::scaleImage(const QSize &size, qreal xres, qreal yres, KisFilterStrategy *filterStrategy) { bool resolutionChanged = xres != xRes() && yres != yRes(); bool sizeChanged = size != this->size(); if (!resolutionChanged && !sizeChanged) return; KisImageSignalVector emitSignals; if (resolutionChanged) emitSignals << ResolutionChangedSignal; if (sizeChanged) emitSignals << ComplexSizeChangedSignal(bounds(), size); emitSignals << ModifiedSignal; KUndo2MagicString actionName = sizeChanged ? kundo2_i18n("Scale Image") : kundo2_i18n("Change Image Resolution"); KisProcessingApplicator::ProcessingFlags signalFlags = (resolutionChanged || sizeChanged) ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NONE; KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | signalFlags, emitSignals, actionName); qreal sx = qreal(size.width()) / this->size().width(); qreal sy = qreal(size.height()) / this->size().height(); QTransform shapesCorrection; if (resolutionChanged) { shapesCorrection = QTransform::fromScale(xRes() / xres, yRes() / yres); } KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(sx, sy, 0, 0, QPointF(), 0, 0, 0, filterStrategy, shapesCorrection); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); if (resolutionChanged) { KUndo2Command *parent = new KisResetShapesCommand(m_d->rootLayer); new KisImageSetResolutionCommand(this, xres, yres, parent); applicator.applyCommand(parent); } if (sizeChanged) { applicator.applyCommand(new KisImageResizeCommand(this, size)); } applicator.end(); } void KisImage::scaleNode(KisNodeSP node, const QPointF ¢er, qreal scaleX, qreal scaleY, KisFilterStrategy *filterStrategy, KisSelectionSP selection) { KUndo2MagicString actionName(kundo2_i18n("Scale Layer")); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; QPointF offset; { KisTransformWorker worker(0, scaleX, scaleY, 0, 0, 0, 0, 0.0, 0, 0, 0, 0); QTransform transform = worker.transform(); offset = center - transform.map(center); } KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE, emitSignals, actionName); KisTransformProcessingVisitor *visitor = new KisTransformProcessingVisitor(scaleX, scaleY, 0, 0, QPointF(), 0, offset.x(), offset.y(), filterStrategy); visitor->setSelection(selection); if (selection) { applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); } else { applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } applicator.end(); } void KisImage::rotateImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, double radians, bool resizeImage, KisSelectionSP selection) { // we can either transform (and resize) the whole image or // transform a selection, we cannot do both at the same time KIS_SAFE_ASSERT_RECOVER(!(bool(selection) && resizeImage)) { selection = 0; } const QRect baseBounds = resizeImage ? bounds() : selection ? selection->selectedExactRect() : rootNode->exactBounds(); QPointF offset; QSize newSize; { KisTransformWorker worker(0, 1.0, 1.0, 0, 0, 0, 0, radians, 0, 0, 0, 0); QTransform transform = worker.transform(); if (resizeImage) { QRect newRect = transform.mapRect(baseBounds); newSize = newRect.size(); offset = -newRect.topLeft(); } else { QPointF origin = QRectF(baseBounds).center(); newSize = size(); offset = -(transform.map(origin) - origin); } } bool sizeChanged = resizeImage && (newSize.width() != baseBounds.width() || newSize.height() != baseBounds.height()); // These signals will be emitted after processing is done KisImageSignalVector emitSignals; if (sizeChanged) emitSignals << ComplexSizeChangedSignal(baseBounds, newSize); emitSignals << ModifiedSignal; // These flags determine whether updates are transferred to the UI during processing KisProcessingApplicator::ProcessingFlags signalFlags = sizeChanged ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NONE; KisProcessingApplicator applicator(this, rootNode, KisProcessingApplicator::RECURSIVE | signalFlags, emitSignals, actionName); KisFilterStrategy *filter = KisFilterStrategyRegistry::instance()->value("Bicubic"); KisTransformProcessingVisitor *visitor = new KisTransformProcessingVisitor(1.0, 1.0, 0.0, 0.0, QPointF(), radians, offset.x(), offset.y(), filter); if (selection) { visitor->setSelection(selection); } if (selection) { applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); } else { applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } if (sizeChanged) { applicator.applyCommand(new KisImageResizeCommand(this, newSize)); } applicator.end(); } void KisImage::rotateImage(double radians) { rotateImpl(kundo2_i18n("Rotate Image"), root(), radians, true, 0); } void KisImage::rotateNode(KisNodeSP node, double radians, KisSelectionSP selection) { if (node->inherits("KisMask")) { rotateImpl(kundo2_i18n("Rotate Mask"), node, radians, false, selection); } else { rotateImpl(kundo2_i18n("Rotate Layer"), node, radians, false, selection); } } void KisImage::shearImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double angleX, double angleY, KisSelectionSP selection) { const QRect baseBounds = resizeImage ? bounds() : selection ? selection->selectedExactRect() : rootNode->exactBounds(); const QPointF origin = QRectF(baseBounds).center(); //angleX, angleY are in degrees const qreal pi = 3.1415926535897932385; const qreal deg2rad = pi / 180.0; qreal tanX = tan(angleX * deg2rad); qreal tanY = tan(angleY * deg2rad); QPointF offset; QSize newSize; { KisTransformWorker worker(0, 1.0, 1.0, tanX, tanY, origin.x(), origin.y(), 0, 0, 0, 0, 0); QRect newRect = worker.transform().mapRect(baseBounds); newSize = newRect.size(); if (resizeImage) offset = -newRect.topLeft(); } if (newSize == baseBounds.size()) return; KisImageSignalVector emitSignals; if (resizeImage) emitSignals << ComplexSizeChangedSignal(baseBounds, newSize); emitSignals << ModifiedSignal; KisProcessingApplicator::ProcessingFlags signalFlags = KisProcessingApplicator::RECURSIVE; if (resizeImage) signalFlags |= KisProcessingApplicator::NO_UI_UPDATES; KisProcessingApplicator applicator(this, rootNode, signalFlags, emitSignals, actionName); KisFilterStrategy *filter = KisFilterStrategyRegistry::instance()->value("Bilinear"); KisTransformProcessingVisitor *visitor = new KisTransformProcessingVisitor(1.0, 1.0, tanX, tanY, origin, 0, offset.x(), offset.y(), filter); if (selection) { visitor->setSelection(selection); } if (selection) { applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); } else { applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } if (resizeImage) { applicator.applyCommand(new KisImageResizeCommand(this, newSize)); } applicator.end(); } void KisImage::shearNode(KisNodeSP node, double angleX, double angleY, KisSelectionSP selection) { if (node->inherits("KisMask")) { shearImpl(kundo2_i18n("Shear Mask"), node, false, angleX, angleY, selection); } else { shearImpl(kundo2_i18n("Shear Layer"), node, false, angleX, angleY, selection); } } void KisImage::shear(double angleX, double angleY) { shearImpl(kundo2_i18n("Shear Image"), m_d->rootLayer, true, angleX, angleY, 0); } void KisImage::convertImageColorSpace(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { if (!dstColorSpace) return; const KoColorSpace *srcColorSpace = m_d->colorSpace; undoAdapter()->beginMacro(kundo2_i18n("Convert Image Color Space")); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), true)); undoAdapter()->addCommand(new KisImageSetProjectionColorSpaceCommand(KisImageWSP(this), dstColorSpace)); KisColorSpaceConvertVisitor visitor(this, srcColorSpace, dstColorSpace, renderingIntent, conversionFlags); m_d->rootLayer->accept(visitor); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), false)); undoAdapter()->endMacro(); setModified(); } bool KisImage::assignImageProfile(const KoColorProfile *profile) { if (!profile) return false; const KoColorSpace *dstCs = KoColorSpaceRegistry::instance()->colorSpace(colorSpace()->colorModelId().id(), colorSpace()->colorDepthId().id(), profile); const KoColorSpace *srcCs = colorSpace(); if (!dstCs) return false; m_d->colorSpace = dstCs; KisChangeProfileVisitor visitor(srcCs, dstCs); bool retval = m_d->rootLayer->accept(visitor); m_d->signalRouter.emitNotification(ProfileChangedSignal); return retval; } void KisImage::convertProjectionColorSpace(const KoColorSpace *dstColorSpace) { if (*m_d->colorSpace == *dstColorSpace) return; undoAdapter()->beginMacro(kundo2_i18n("Convert Projection Color Space")); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), true)); undoAdapter()->addCommand(new KisImageSetProjectionColorSpaceCommand(KisImageWSP(this), dstColorSpace)); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), false)); undoAdapter()->endMacro(); setModified(); } void KisImage::setProjectionColorSpace(const KoColorSpace * colorSpace) { m_d->colorSpace = colorSpace; m_d->rootLayer->resetCache(); m_d->signalRouter.emitNotification(ColorSpaceChangedSignal); } const KoColorSpace * KisImage::colorSpace() const { return m_d->colorSpace; } const KoColorProfile * KisImage::profile() const { return colorSpace()->profile(); } double KisImage::xRes() const { return m_d->xres; } double KisImage::yRes() const { return m_d->yres; } void KisImage::setResolution(double xres, double yres) { m_d->xres = xres; m_d->yres = yres; m_d->signalRouter.emitNotification(ResolutionChangedSignal); } QPointF KisImage::documentToPixel(const QPointF &documentCoord) const { return QPointF(documentCoord.x() * xRes(), documentCoord.y() * yRes()); } QPoint KisImage::documentToImagePixelFloored(const QPointF &documentCoord) const { QPointF pixelCoord = documentToPixel(documentCoord); return QPoint(qFloor(pixelCoord.x()), qFloor(pixelCoord.y())); } QRectF KisImage::documentToPixel(const QRectF &documentRect) const { return QRectF(documentToPixel(documentRect.topLeft()), documentToPixel(documentRect.bottomRight())); } QPointF KisImage::pixelToDocument(const QPointF &pixelCoord) const { return QPointF(pixelCoord.x() / xRes(), pixelCoord.y() / yRes()); } QPointF KisImage::pixelToDocument(const QPoint &pixelCoord) const { return QPointF((pixelCoord.x() + 0.5) / xRes(), (pixelCoord.y() + 0.5) / yRes()); } QRectF KisImage::pixelToDocument(const QRectF &pixelCoord) const { return QRectF(pixelToDocument(pixelCoord.topLeft()), pixelToDocument(pixelCoord.bottomRight())); } qint32 KisImage::width() const { return m_d->width; } qint32 KisImage::height() const { return m_d->height; } KisGroupLayerSP KisImage::rootLayer() const { Q_ASSERT(m_d->rootLayer); return m_d->rootLayer; } KisPaintDeviceSP KisImage::projection() const { if (m_d->isolatedRootNode) { return m_d->isolatedRootNode->projection(); } Q_ASSERT(m_d->rootLayer); KisPaintDeviceSP projection = m_d->rootLayer->projection(); Q_ASSERT(projection); return projection; } qint32 KisImage::nlayers() const { QStringList list; list << "KisLayer"; KisCountVisitor visitor(list, KoProperties()); m_d->rootLayer->accept(visitor); return visitor.count(); } qint32 KisImage::nHiddenLayers() const { QStringList list; list << "KisLayer"; KoProperties properties; properties.setProperty("visible", false); KisCountVisitor visitor(list, properties); m_d->rootLayer->accept(visitor); return visitor.count(); } void KisImage::flatten(KisNodeSP activeNode) { KisLayerUtils::flattenImage(this, activeNode); } void KisImage::mergeMultipleLayers(QList mergedNodes, KisNodeSP putAfter) { if (!KisLayerUtils::tryMergeSelectionMasks(this, mergedNodes, putAfter)) { KisLayerUtils::mergeMultipleLayers(this, mergedNodes, putAfter); } } void KisImage::mergeDown(KisLayerSP layer, const KisMetaData::MergeStrategy* strategy) { KisLayerUtils::mergeDown(this, layer, strategy); } void KisImage::flattenLayer(KisLayerSP layer) { KisLayerUtils::flattenLayer(this, layer); } void KisImage::setModified() { m_d->signalRouter.emitNotification(ModifiedSignal); } QImage KisImage::convertToQImage(QRect imageRect, const KoColorProfile * profile) { qint32 x; qint32 y; qint32 w; qint32 h; imageRect.getRect(&x, &y, &w, &h); return convertToQImage(x, y, w, h, profile); } QImage KisImage::convertToQImage(qint32 x, qint32 y, qint32 w, qint32 h, const KoColorProfile * profile) { KisPaintDeviceSP dev = projection(); if (!dev) return QImage(); QImage image = dev->convertToQImage(const_cast(profile), x, y, w, h, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); return image; } QImage KisImage::convertToQImage(const QSize& scaledImageSize, const KoColorProfile *profile) { if (scaledImageSize.isEmpty()) { return QImage(); } KisPaintDeviceSP dev = new KisPaintDevice(colorSpace()); KisPainter gc; gc.copyAreaOptimized(QPoint(0, 0), projection(), dev, bounds()); gc.end(); double scaleX = qreal(scaledImageSize.width()) / width(); double scaleY = qreal(scaledImageSize.height()) / height(); QPointer updater = new KoDummyUpdater(); KisTransformWorker worker(dev, scaleX, scaleY, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, updater, KisFilterStrategyRegistry::instance()->value("Bicubic")); worker.run(); delete updater; return dev->convertToQImage(profile); } void KisImage::notifyLayersChanged() { m_d->signalRouter.emitNotification(LayersChangedSignal); } QRect KisImage::bounds() const { return QRect(0, 0, width(), height()); } QRect KisImage::effectiveLodBounds() const { QRect boundRect = bounds(); const int lod = currentLevelOfDetail(); if (lod > 0) { KisLodTransform t(lod); boundRect = t.map(boundRect); } return boundRect; } KisPostExecutionUndoAdapter* KisImage::postExecutionUndoAdapter() const { const int lod = currentLevelOfDetail(); return lod > 0 ? m_d->scheduler.lodNPostExecutionUndoAdapter() : &m_d->postExecutionUndoAdapter; } const KUndo2Command* KisImage::lastExecutedCommand() const { return m_d->undoStore->presentCommand(); } void KisImage::setUndoStore(KisUndoStore *undoStore) { m_d->legacyUndoAdapter.setUndoStore(undoStore); m_d->postExecutionUndoAdapter.setUndoStore(undoStore); m_d->undoStore.reset(undoStore); } KisUndoStore* KisImage::undoStore() { return m_d->undoStore.data(); } KisUndoAdapter* KisImage::undoAdapter() const { return &m_d->legacyUndoAdapter; } void KisImage::setDefaultProjectionColor(const KoColor &color) { KIS_ASSERT_RECOVER_RETURN(m_d->rootLayer); m_d->rootLayer->setDefaultProjectionColor(color); } KoColor KisImage::defaultProjectionColor() const { KIS_ASSERT_RECOVER(m_d->rootLayer) { return KoColor(Qt::transparent, m_d->colorSpace); } return m_d->rootLayer->defaultProjectionColor(); } void KisImage::setRootLayer(KisGroupLayerSP rootLayer) { emit sigInternalStopIsolatedModeRequested(); KoColor defaultProjectionColor(Qt::transparent, m_d->colorSpace); if (m_d->rootLayer) { m_d->rootLayer->setGraphListener(0); m_d->rootLayer->disconnect(); KisPaintDeviceSP original = m_d->rootLayer->original(); defaultProjectionColor = original->defaultPixel(); } m_d->rootLayer = rootLayer; m_d->rootLayer->disconnect(); m_d->rootLayer->setGraphListener(this); m_d->rootLayer->setImage(this); setRoot(m_d->rootLayer.data()); this->setDefaultProjectionColor(defaultProjectionColor); } void KisImage::addAnnotation(KisAnnotationSP annotation) { // Find the icc annotation, if there is one vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == annotation->type()) { *it = annotation; return; } ++it; } m_d->annotations.push_back(annotation); } KisAnnotationSP KisImage::annotation(const QString& type) { vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == type) { return *it; } ++it; } return KisAnnotationSP(0); } void KisImage::removeAnnotation(const QString& type) { vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == type) { m_d->annotations.erase(it); return; } ++it; } } vKisAnnotationSP_it KisImage::beginAnnotations() { return m_d->annotations.begin(); } vKisAnnotationSP_it KisImage::endAnnotations() { return m_d->annotations.end(); } void KisImage::notifyAboutToBeDeleted() { emit sigAboutToBeDeleted(); } KisImageSignalRouter* KisImage::signalRouter() { return &m_d->signalRouter; } void KisImage::waitForDone() { requestStrokeEnd(); m_d->scheduler.waitForDone(); } KisStrokeId KisImage::startStroke(KisStrokeStrategy *strokeStrategy) { /** * Ask open strokes to end gracefully. All the strokes clients * (including the one calling this method right now) will get * a notification that they should probably end their strokes. * However this is purely their choice whether to end a stroke * or not. */ if (strokeStrategy->requestsOtherStrokesToEnd()) { requestStrokeEnd(); } /** * Some of the strokes can cancel their work with undoing all the * changes they did to the paint devices. The problem is that undo * stack will know nothing about it. Therefore, just notify it * explicitly */ if (strokeStrategy->clearsRedoOnStart()) { m_d->undoStore->purgeRedoState(); } return m_d->scheduler.startStroke(strokeStrategy); } void KisImage::KisImagePrivate::notifyProjectionUpdatedInPatches(const QRect &rc, QVector &jobs) { KisImageConfig imageConfig(true); int patchWidth = imageConfig.updatePatchWidth(); int patchHeight = imageConfig.updatePatchHeight(); for (int y = 0; y < rc.height(); y += patchHeight) { for (int x = 0; x < rc.width(); x += patchWidth) { QRect patchRect(x, y, patchWidth, patchHeight); patchRect &= rc; KritaUtils::addJobConcurrent(jobs, std::bind(&KisImage::notifyProjectionUpdated, q, patchRect)); } } } bool KisImage::startIsolatedMode(KisNodeSP node) { struct StartIsolatedModeStroke : public KisRunnableBasedStrokeStrategy { StartIsolatedModeStroke(KisNodeSP node, KisImageSP image) : KisRunnableBasedStrokeStrategy("start-isolated-mode", kundo2_noi18n("start-isolated-mode")), m_node(node), m_image(image) { this->enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); this->enableJob(JOB_DOSTROKE, true); setClearsRedoOnStart(false); } void initStrokeCallback() { // pass-though node don't have any projection prepared, so we should // explicitly regenerate it before activating isolated mode. m_node->projectionLeaf()->explicitlyRegeneratePassThroughProjection(); m_image->m_d->isolatedRootNode = m_node; emit m_image->sigIsolatedModeChanged(); // the GUI uses our thread to do the color space conversion so we // need to emit this signal in multiple threads QVector jobs; m_image->m_d->notifyProjectionUpdatedInPatches(m_image->bounds(), jobs); this->runnableJobsInterface()->addRunnableJobs(jobs); m_image->invalidateAllFrames(); } private: KisNodeSP m_node; KisImageSP m_image; }; KisStrokeId id = startStroke(new StartIsolatedModeStroke(node, this)); endStroke(id); return true; } void KisImage::stopIsolatedMode() { if (!m_d->isolatedRootNode) return; struct StopIsolatedModeStroke : public KisRunnableBasedStrokeStrategy { StopIsolatedModeStroke(KisImageSP image) : KisRunnableBasedStrokeStrategy("stop-isolated-mode", kundo2_noi18n("stop-isolated-mode")), m_image(image) { this->enableJob(JOB_INIT); this->enableJob(JOB_DOSTROKE, true); setClearsRedoOnStart(false); } void initStrokeCallback() { if (!m_image->m_d->isolatedRootNode) return; //KisNodeSP oldRootNode = m_image->m_d->isolatedRootNode; m_image->m_d->isolatedRootNode = 0; emit m_image->sigIsolatedModeChanged(); m_image->invalidateAllFrames(); // the GUI uses our thread to do the color space conversion so we // need to emit this signal in multiple threads QVector jobs; m_image->m_d->notifyProjectionUpdatedInPatches(m_image->bounds(), jobs); this->runnableJobsInterface()->addRunnableJobs(jobs); // TODO: Substitute notifyProjectionUpdated() with this code // when update optimization is implemented // // QRect updateRect = bounds() | oldRootNode->extent(); // oldRootNode->setDirty(updateRect); } private: KisImageSP m_image; }; KisStrokeId id = startStroke(new StopIsolatedModeStroke(this)); endStroke(id); } KisNodeSP KisImage::isolatedModeRoot() const { return m_d->isolatedRootNode; } void KisImage::addJob(KisStrokeId id, KisStrokeJobData *data) { KisUpdateTimeMonitor::instance()->reportJobStarted(data); m_d->scheduler.addJob(id, data); } void KisImage::endStroke(KisStrokeId id) { m_d->scheduler.endStroke(id); } bool KisImage::cancelStroke(KisStrokeId id) { return m_d->scheduler.cancelStroke(id); } bool KisImage::KisImagePrivate::tryCancelCurrentStrokeAsync() { return scheduler.tryCancelCurrentStrokeAsync(); } void KisImage::requestUndoDuringStroke() { emit sigUndoDuringStrokeRequested(); } void KisImage::requestStrokeCancellation() { if (!m_d->tryCancelCurrentStrokeAsync()) { emit sigStrokeCancellationRequested(); } } UndoResult KisImage::tryUndoUnfinishedLod0Stroke() { return m_d->scheduler.tryUndoLastStrokeAsync(); } void KisImage::requestStrokeEnd() { emit sigStrokeEndRequested(); emit sigStrokeEndRequestedActiveNodeFiltered(); } void KisImage::requestStrokeEndActiveNode() { emit sigStrokeEndRequested(); } void KisImage::refreshGraph(KisNodeSP root) { refreshGraph(root, bounds(), bounds()); } void KisImage::refreshGraph(KisNodeSP root, const QRect &rc, const QRect &cropRect) { if (!root) root = m_d->rootLayer; m_d->animationInterface->notifyNodeChanged(root.data(), rc, true); m_d->scheduler.fullRefresh(root, rc, cropRect); } void KisImage::initialRefreshGraph() { /** * NOTE: Tricky part. We set crop rect to null, so the clones * will not rely on precalculated projections of their sources */ refreshGraphAsync(0, bounds(), QRect()); waitForDone(); } void KisImage::refreshGraphAsync(KisNodeSP root) { refreshGraphAsync(root, bounds(), bounds()); } void KisImage::refreshGraphAsync(KisNodeSP root, const QRect &rc) { refreshGraphAsync(root, rc, bounds()); } void KisImage::refreshGraphAsync(KisNodeSP root, const QRect &rc, const QRect &cropRect) { if (!root) root = m_d->rootLayer; m_d->animationInterface->notifyNodeChanged(root.data(), rc, true); m_d->scheduler.fullRefreshAsync(root, rc, cropRect); } void KisImage::requestProjectionUpdateNoFilthy(KisNodeSP pseudoFilthy, const QRect &rc, const QRect &cropRect) { KIS_ASSERT_RECOVER_RETURN(pseudoFilthy); m_d->animationInterface->notifyNodeChanged(pseudoFilthy.data(), rc, false); m_d->scheduler.updateProjectionNoFilthy(pseudoFilthy, rc, cropRect); } void KisImage::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) { m_d->scheduler.addSpontaneousJob(spontaneousJob); } bool KisImage::hasUpdatesRunning() const { return m_d->scheduler.hasUpdatesRunning(); } void KisImage::setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP filter) { // update filters are *not* recursive! KIS_ASSERT_RECOVER_NOOP(!filter || !m_d->projectionUpdatesFilter); m_d->projectionUpdatesFilter = filter; } KisProjectionUpdatesFilterSP KisImage::projectionUpdatesFilter() const { return m_d->projectionUpdatesFilter; } void KisImage::disableDirtyRequests() { setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP(new KisDropAllProjectionUpdatesFilter())); } void KisImage::enableDirtyRequests() { setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP()); } void KisImage::disableUIUpdates() { m_d->disableUIUpdateSignals.ref(); } void KisImage::notifyBatchUpdateStarted() { m_d->signalRouter.emitNotifyBatchUpdateStarted(); } void KisImage::notifyBatchUpdateEnded() { m_d->signalRouter.emitNotifyBatchUpdateEnded(); } void KisImage::notifyUIUpdateCompleted(const QRect &rc) { notifyProjectionUpdated(rc); } QVector KisImage::enableUIUpdates() { m_d->disableUIUpdateSignals.deref(); QRect rect; QVector postponedUpdates; while (m_d->savedDisabledUIUpdates.pop(rect)) { postponedUpdates.append(rect); } return postponedUpdates; } void KisImage::notifyProjectionUpdated(const QRect &rc) { KisUpdateTimeMonitor::instance()->reportUpdateFinished(rc); if (!m_d->disableUIUpdateSignals) { int lod = currentLevelOfDetail(); QRect dirtyRect = !lod ? rc : KisLodTransform::upscaledRect(rc, lod); if (dirtyRect.isEmpty()) return; emit sigImageUpdated(dirtyRect); } else { m_d->savedDisabledUIUpdates.push(rc); } } void KisImage::setWorkingThreadsLimit(int value) { m_d->scheduler.setThreadsLimit(value); } int KisImage::workingThreadsLimit() const { return m_d->scheduler.threadsLimit(); } void KisImage::notifySelectionChanged() { /** * The selection is calculated asynchronously, so it is not * handled by disableUIUpdates() and other special signals of * KisImageSignalRouter */ m_d->legacyUndoAdapter.emitSelectionChanged(); /** * Editing of selection masks doesn't necessary produce a * setDirty() call, so in the end of the stroke we need to request * direct update of the UI's cache. */ if (m_d->isolatedRootNode && dynamic_cast(m_d->isolatedRootNode.data())) { notifyProjectionUpdated(bounds()); } } void KisImage::requestProjectionUpdateImpl(KisNode *node, const QVector &rects, const QRect &cropRect) { if (rects.isEmpty()) return; m_d->scheduler.updateProjection(node, rects, cropRect); } void KisImage::requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) { if (m_d->projectionUpdatesFilter && m_d->projectionUpdatesFilter->filter(this, node, rects, resetAnimationCache)) { return; } if (resetAnimationCache) { m_d->animationInterface->notifyNodeChanged(node, rects, false); } /** * Here we use 'permitted' instead of 'active' intentively, * because the updates may come after the actual stroke has been * finished. And having some more updates for the stroke not * supporting the wrap-around mode will not make much harm. */ if (m_d->wrapAroundModePermitted) { QVector allSplitRects; const QRect boundRect = effectiveLodBounds(); Q_FOREACH (const QRect &rc, rects) { KisWrappedRect splitRect(rc, boundRect); allSplitRects.append(splitRect); } requestProjectionUpdateImpl(node, allSplitRects, boundRect); } else { requestProjectionUpdateImpl(node, rects, bounds()); } KisNodeGraphListener::requestProjectionUpdate(node, rects, resetAnimationCache); } -void KisImage::invalidateFrames(const KisTimeRange &range, const QRect &rect) +void KisImage::invalidateFrames(const KisFrameSet &frames, const QRect &rect) { - m_d->animationInterface->invalidateFrames(range, rect); + m_d->animationInterface->invalidateFrames(frames, rect); } void KisImage::requestTimeSwitch(int time) { m_d->animationInterface->requestTimeSwitchNonGUI(time); } KisNode *KisImage::graphOverlayNode() const { return m_d->overlaySelectionMask.data(); } QList KisImage::compositions() { return m_d->compositions; } void KisImage::addComposition(KisLayerCompositionSP composition) { m_d->compositions.append(composition); } void KisImage::removeComposition(KisLayerCompositionSP composition) { m_d->compositions.removeAll(composition); } bool checkMasksNeedConversion(KisNodeSP root, const QRect &bounds) { KisSelectionMask *mask = dynamic_cast(root.data()); if (mask && (!bounds.contains(mask->paintDevice()->exactBounds()) || mask->selection()->hasShapeSelection())) { return true; } KisNodeSP node = root->firstChild(); while (node) { if (checkMasksNeedConversion(node, bounds)) { return true; } node = node->nextSibling(); } return false; } void KisImage::setWrapAroundModePermitted(bool value) { if (m_d->wrapAroundModePermitted != value) { requestStrokeEnd(); } m_d->wrapAroundModePermitted = value; if (m_d->wrapAroundModePermitted && checkMasksNeedConversion(root(), bounds())) { KisProcessingApplicator applicator(this, root(), KisProcessingApplicator::RECURSIVE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Crop Selections")); KisProcessingVisitorSP visitor = new KisCropSelectionsProcessingVisitor(bounds()); applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } } bool KisImage::wrapAroundModePermitted() const { return m_d->wrapAroundModePermitted; } bool KisImage::wrapAroundModeActive() const { return m_d->wrapAroundModePermitted && m_d->scheduler.wrapAroundModeSupported(); } void KisImage::setDesiredLevelOfDetail(int lod) { if (m_d->blockLevelOfDetail) { qWarning() << "WARNING: KisImage::setDesiredLevelOfDetail()" << "was called while LoD functionality was being blocked!"; return; } m_d->scheduler.setDesiredLevelOfDetail(lod); } int KisImage::currentLevelOfDetail() const { if (m_d->blockLevelOfDetail) { return 0; } return m_d->scheduler.currentLevelOfDetail(); } void KisImage::setLevelOfDetailBlocked(bool value) { KisImageBarrierLockerRaw l(this); if (value && !m_d->blockLevelOfDetail) { m_d->scheduler.setDesiredLevelOfDetail(0); } m_d->blockLevelOfDetail = value; } void KisImage::explicitRegenerateLevelOfDetail() { if (!m_d->blockLevelOfDetail) { m_d->scheduler.explicitRegenerateLevelOfDetail(); } } bool KisImage::levelOfDetailBlocked() const { return m_d->blockLevelOfDetail; } void KisImage::nodeCollapsedChanged(KisNode * node) { Q_UNUSED(node); emit sigNodeCollapsedChanged(); } KisImageAnimationInterface* KisImage::animationInterface() const { return m_d->animationInterface; } void KisImage::setProofingConfiguration(KisProofingConfigurationSP proofingConfig) { m_d->proofingConfig = proofingConfig; emit sigProofingConfigChanged(); } KisProofingConfigurationSP KisImage::proofingConfiguration() const { if (m_d->proofingConfig) { return m_d->proofingConfig; } return KisProofingConfigurationSP(); } QPointF KisImage::mirrorAxesCenter() const { return m_d->axesCenter; } void KisImage::setMirrorAxesCenter(const QPointF &value) const { m_d->axesCenter = value; } void KisImage::setAllowMasksOnRootNode(bool value) { m_d->allowMasksOnRootNode = value; } bool KisImage::allowMasksOnRootNode() const { return m_d->allowMasksOnRootNode; } diff --git a/libs/image/kis_image.h b/libs/image/kis_image.h index 042a2d1515..c16708b92f 100644 --- a/libs/image/kis_image.h +++ b/libs/image/kis_image.h @@ -1,1189 +1,1190 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2007 Boudewijn Rempt * * 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_H_ #define KIS_IMAGE_H_ #include #include #include #include #include #include #include #include "kis_types.h" #include "kis_shared.h" #include "kis_node_graph_listener.h" #include "kis_node_facade.h" #include "kis_image_interfaces.h" #include "kis_strokes_queue_undo_result.h" #include class KoColorSpace; class KoColor; class KisCompositeProgressProxy; class KisUndoStore; class KisUndoAdapter; class KisImageSignalRouter; class KisPostExecutionUndoAdapter; class KisFilterStrategy; class KoColorProfile; class KisLayerComposition; class KisSpontaneousJob; class KisImageAnimationInterface; class KUndo2MagicString; class KisProofingConfiguration; class KisPaintDevice; +class KisFrameSet; namespace KisMetaData { class MergeStrategy; } /** * This is the image class, it contains a tree of KisLayer stack and * meta information about the image. And it also provides some * functions to manipulate the whole image. */ class KRITAIMAGE_EXPORT KisImage : public QObject, public KisStrokesFacade, public KisStrokeUndoFacade, public KisUpdatesFacade, public KisProjectionUpdateListener, public KisNodeFacade, public KisNodeGraphListener, public KisShared { Q_OBJECT public: /// @p colorSpace can be null. In that case, it will be initialised to a default color space. KisImage(KisUndoStore *undoStore, qint32 width, qint32 height, const KoColorSpace *colorSpace, const QString& name); ~KisImage() override; static KisImageSP fromQImage(const QImage &image, KisUndoStore *undoStore); public: // KisNodeGraphListener implementation void aboutToAddANode(KisNode *parent, int index) override; void nodeHasBeenAdded(KisNode *parent, int index) override; void aboutToRemoveANode(KisNode *parent, int index) override; void nodeChanged(KisNode * node) override; void nodeCollapsedChanged(KisNode *node) override; void invalidateAllFrames() override; void notifySelectionChanged() override; void requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) override; - void invalidateFrames(const KisTimeRange &range, const QRect &rect) override; + void invalidateFrames(const KisFrameSet &range, const QRect &rect) override; void requestTimeSwitch(int time) override; KisNode* graphOverlayNode() const override; public: // KisProjectionUpdateListener implementation void notifyProjectionUpdated(const QRect &rc) override; public: /** * Set the number of threads used by the image's working threads */ void setWorkingThreadsLimit(int value); /** * Return the number of threads available to the image's working threads */ int workingThreadsLimit() const; /** * Makes a copy of the image with all the layers. If possible, shallow * copies of the layers are made. * * \p exactCopy shows if the copied image should look *exactly* the same as * the other one (according to it's .kra xml representation). It means that * the layers will have the same UUID keys and, therefore, you are not * expected to use the copied image anywhere except for saving. Don't use * this option if you plan to work with the copied image later. */ KisImage *clone(bool exactCopy = false); void copyFromImage(const KisImage &rhs); private: // must specify exactly one from CONSTRUCT or REPLACE. enum CopyPolicy { CONSTRUCT = 1, ///< we are copy-constructing a new KisImage REPLACE = 2, ///< we are replacing the current KisImage with another EXACT_COPY = 4, /// we need an exact copy of the original image }; void copyFromImageImpl(const KisImage &rhs, int policy); public: /** * Render the projection onto a QImage. */ QImage convertToQImage(qint32 x1, qint32 y1, qint32 width, qint32 height, const KoColorProfile * profile); /** * Render the projection onto a QImage. * (this is an overloaded function) */ QImage convertToQImage(QRect imageRect, const KoColorProfile * profile); /** * Render a thumbnail of the projection onto a QImage. */ QImage convertToQImage(const QSize& scaledImageSize, const KoColorProfile *profile); /** * [low-level] Lock the image without waiting for all the internal job queues are processed * * WARNING: Don't use it unless you really know what you are doing! Use barrierLock() instead! * * Waits for all the **currently running** internal jobs to complete and locks the image * for writing. Please note that this function does **not** wait for all the internal * queues to process, so there might be some non-finished actions pending. It means that * you just postpone these actions until you unlock() the image back. Until then, then image * might easily be frozen in some inconsistent state. * * The only sane usage for this function is to lock the image for **emergency** * processing, when some internal action or scheduler got hung up, and you just want * to fetch some data from the image without races. * * In all other cases, please use barrierLock() instead! */ void lock(); /** * Unlocks the image and starts/resumes all the pending internal jobs. If the image * has been locked for a non-readOnly access, then all the internal caches of the image * (e.g. lod-planes) are reset and regeneration jobs are scheduled. */ void unlock(); /** * @return return true if the image is in a locked state, i.e. all the internal * jobs are blocked from execution by calling wither lock() or barrierLock(). * * When the image is locked, the user can do some modifications to the image * contents safely without a perspective having race conditions with internal * image jobs. */ bool locked() const; /** * Sets the mask (it must be a part of the node hierarchy already) to be paited on * the top of all layers. This method does all the locking and syncing for you. It * is executed asynchronously. */ void setOverlaySelectionMask(KisSelectionMaskSP mask); /** * \see setOverlaySelectionMask */ KisSelectionMaskSP overlaySelectionMask() const; /** * \see setOverlaySelectionMask */ bool hasOverlaySelectionMask() const; /** * @return the global selection object or 0 if there is none. The * global selection is always read-write. */ KisSelectionSP globalSelection() const; /** * Retrieve the next automatic layername (XXX: fix to add option to return Mask X) */ QString nextLayerName(const QString &baseName = "") const; /** * Set the automatic layer name counter one back. */ void rollBackLayerName(); /** * @brief start asynchronous operation on resizing the image * * The method will resize the image to fit the new size without * dropping any pixel data. The GUI will get correct * notification with old and new sizes, so it adjust canvas origin * accordingly and avoid jumping of the canvas on screen * * @param newRect the rectangle of the image which will be visible * after operation is completed * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after this call. */ void resizeImage(const QRect& newRect); /** * @brief start asynchronous operation on cropping the image * * The method will **drop** all the image data outside \p newRect * and resize the image to fit the new size. The GUI will get correct * notification with old and new sizes, so it adjust canvas origin * accordingly and avoid jumping of the canvas on screen * * @param newRect the rectangle of the image which will be cut-out * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after this call. */ void cropImage(const QRect& newRect); /** * @brief start asynchronous operation on cropping a subtree of nodes starting at \p node * * The method will **drop** all the layer data outside \p newRect. Neither * image nor a layer will be moved anywhere * * @param node node to crop * @param newRect the rectangle of the layer which will be cut-out * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after this call. */ void cropNode(KisNodeSP node, const QRect& newRect); /** * @brief start asynchronous operation on scaling the image * @param size new image size in pixels * @param xres new image x-resolution pixels-per-pt * @param yres new image y-resolution pixels-per-pt * @param filterStrategy filtering strategy * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after this call. */ void scaleImage(const QSize &size, qreal xres, qreal yres, KisFilterStrategy *filterStrategy); /** * @brief start asynchronous operation on scaling a subtree of nodes starting at \p node * @param node node to scale * @param center the center of the scaling * @param scaleX x-scale coefficient to be applied to the node * @param scaleY y-scale coefficient to be applied to the node * @param filterStrategy filtering strategy * @param selection the selection we based on * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the image having new size * right after this call. */ void scaleNode(KisNodeSP node, const QPointF ¢er, qreal scaleX, qreal scaleY, KisFilterStrategy *filterStrategy, KisSelectionSP selection); /** * @brief start asynchronous operation on rotating the image * * The image is resized to fit the rotated rectangle * * @param radians rotation angle in radians * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void rotateImage(double radians); /** * @brief start asynchronous operation on rotating a subtree of nodes starting at \p node * * The image is not resized! * * @param node the root of the subtree to rotate * @param radians rotation angle in radians * @param selection the selection we based on * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void rotateNode(KisNodeSP node, double radians, KisSelectionSP selection); /** * @brief start asynchronous operation on shearing the image * * The image is resized to fit the sheared polygon * * @p angleX, @p angleY are given in degrees. * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void shear(double angleX, double angleY); /** * @brief start asynchronous operation on shearing a subtree of nodes starting at \p node * * The image is not resized! * * @param node the root of the subtree to rotate * @param angleX x-shear given in degrees. * @param angleY y-shear given in degrees. * @param selection the selection we based on * * Please note that the actual operation starts asynchronously in * a background, so you cannot expect the operation being completed * right after the call */ void shearNode(KisNodeSP node, double angleX, double angleY, KisSelectionSP selection); /** * Convert the image and all its layers to the dstColorSpace */ void convertImageColorSpace(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags); /** * Set the color space of the projection (and the root layer) * to dstColorSpace. No conversion is done for other layers, * their colorspace can differ. * @note No conversion is done, only regeneration, so no rendering * intent needed */ void convertProjectionColorSpace(const KoColorSpace *dstColorSpace); // Get the profile associated with this image const KoColorProfile * profile() const; /** * Set the profile of the image to the new profile and do the same for * all layers that have the same colorspace and profile of the image. * It doesn't do any pixel conversion. * * This is essential if you have loaded an image that didn't * have an embedded profile to which you want to attach the right profile. * * This does not create an undo action; only call it when creating or * loading an image. * * @returns false if the profile could not be assigned */ bool assignImageProfile(const KoColorProfile *profile); /** * Returns the current undo adapter. You can add new commands to the * undo stack using the adapter. This adapter is used for a backward * compatibility for old commands created before strokes. It blocks * all the porcessing at the scheduler, waits until it's finished * and executes commands exclusively. */ KisUndoAdapter* undoAdapter() const; /** * This adapter is used by the strokes system. The commands are added * to it *after* redo() is done (in the scheduler context). They are * wrapped into a special command and added to the undo stack. redo() * in not called. */ KisPostExecutionUndoAdapter* postExecutionUndoAdapter() const override; /** * Return the lastly executed LoD0 command. It is effectively the same * as to call undoAdapter()->presentCommand(); */ const KUndo2Command* lastExecutedCommand() const override; /** * Replace current undo store with the new one. The old store * will be deleted. * This method is used by KisDocument for dropping all the commands * during file loading. */ void setUndoStore(KisUndoStore *undoStore); /** * Return current undo store of the image */ KisUndoStore* undoStore(); /** * Tell the image it's modified; this emits the sigImageModified * signal. This happens when the image needs to be saved */ void setModified(); /** * The default colorspace of this image: new layers will have this * colorspace and the projection will have this colorspace. */ const KoColorSpace * colorSpace() const; /** * X resolution in pixels per pt */ double xRes() const; /** * Y resolution in pixels per pt */ double yRes() const; /** * Set the resolution in pixels per pt. */ void setResolution(double xres, double yres); /** * Convert a document coordinate to a pixel coordinate. * * @param documentCoord PostScript Pt coordinate to convert. */ QPointF documentToPixel(const QPointF &documentCoord) const; /** * Convert a document coordinate to an integer pixel coordinate rounded down. * * @param documentCoord PostScript Pt coordinate to convert. */ QPoint documentToImagePixelFloored(const QPointF &documentCoord) const; /** * Convert a document rectangle to a pixel rectangle. * * @param documentRect PostScript Pt rectangle to convert. */ QRectF documentToPixel(const QRectF &documentRect) const; /** * Convert a pixel coordinate to a document coordinate. * * @param pixelCoord pixel coordinate to convert. */ QPointF pixelToDocument(const QPointF &pixelCoord) const; /** * Convert an integer pixel coordinate to a document coordinate. * The document coordinate is at the centre of the pixel. * * @param pixelCoord pixel coordinate to convert. */ QPointF pixelToDocument(const QPoint &pixelCoord) const; /** * Convert a document rectangle to an integer pixel rectangle. * * @param pixelCoord pixel coordinate to convert. */ QRectF pixelToDocument(const QRectF &pixelCoord) const; /** * Return the width of the image */ qint32 width() const; /** * Return the height of the image */ qint32 height() const; /** * Return the size of the image */ QSize size() const { return QSize(width(), height()); } /** * @return the root node of the image node graph */ KisGroupLayerSP rootLayer() const; /** * Return the projection; that is, the complete, composited * representation of this image. */ KisPaintDeviceSP projection() const; /** * Return the number of layers (not other nodes) that are in this * image. */ qint32 nlayers() const; /** * Return the number of layers (not other node types) that are in * this image and that are hidden. */ qint32 nHiddenLayers() const; /** * Merge all visible layers and discard hidden ones. */ void flatten(KisNodeSP activeNode); /** * Merge the specified layer with the layer * below this layer, remove the specified layer. */ void mergeDown(KisLayerSP l, const KisMetaData::MergeStrategy* strategy); /** * flatten the layer: that is, the projection becomes the layer * and all subnodes are removed. If this is not a paint layer, it will morph * into a paint layer. */ void flattenLayer(KisLayerSP layer); /** * Merges layers in \p mergedLayers and creates a new layer above * \p putAfter */ void mergeMultipleLayers(QList mergedLayers, KisNodeSP putAfter); /// @return the exact bounds of the image in pixel coordinates. QRect bounds() const override; /** * Returns the actual bounds of the image, taking LevelOfDetail * into account. This value is used as a bounds() value of * KisDefaultBounds object. */ QRect effectiveLodBounds() const; /// use if the layers have changed _completely_ (eg. when flattening) void notifyLayersChanged(); /** * Sets the default color of the root layer projection. All the layers * will be merged on top of this very color */ void setDefaultProjectionColor(const KoColor &color); /** * \see setDefaultProjectionColor() */ KoColor defaultProjectionColor() const; void setRootLayer(KisGroupLayerSP rootLayer); /** * Add an annotation for this image. This can be anything: Gamma, EXIF, etc. * Note that the "icc" annotation is reserved for the color strategies. * If the annotation already exists, overwrite it with this one. */ void addAnnotation(KisAnnotationSP annotation); /** get the annotation with the given type, can return 0 */ KisAnnotationSP annotation(const QString& type); /** delete the annotation, if the image contains it */ void removeAnnotation(const QString& type); /** * Start of an iteration over the annotations of this image (including the ICC Profile) */ vKisAnnotationSP_it beginAnnotations(); /** end of an iteration over the annotations of this image */ vKisAnnotationSP_it endAnnotations(); /** * Called before the image is deleted and sends the sigAboutToBeDeleted signal */ void notifyAboutToBeDeleted(); KisImageSignalRouter* signalRouter(); /** * Returns whether we can reselect current global selection * * \see reselectGlobalSelection() */ bool canReselectGlobalSelection(); /** * Returns the layer compositions for the image */ QList compositions(); /** * Adds a new layer composition, will be saved with the image */ void addComposition(KisLayerCompositionSP composition); /** * Remove the layer compostion */ void removeComposition(KisLayerCompositionSP composition); /** * Permit or deny the wrap-around mode for all the paint devices * of the image. Note that permitting the wraparound mode will not * necessarily activate it right now. To be activated the wrap * around mode should be 1) permitted; 2) supported by the * currently running stroke. */ void setWrapAroundModePermitted(bool value); /** * \return whether the wrap-around mode is permitted for this * image. If the wrap around mode is permitted and the * currently running stroke supports it, the mode will be * activated for all paint devices of the image. * * \see setWrapAroundMode */ bool wrapAroundModePermitted() const; /** * \return whether the wraparound mode is activated for all the * devices of the image. The mode is activated when both * factors are true: the user permitted it and the stroke * supports it */ bool wrapAroundModeActive() const; /** * \return current level of detail which is used when processing the image. * Current working zoom = 2 ^ (- currentLevelOfDetail()). Default value is * null, which means we work on the original image. */ int currentLevelOfDetail() const; /** * Notify KisImage which level of detail should be used in the * lod-mode. Setting the mode does not guarantee the LOD to be * used. It will be activated only when the stokes supports it. */ void setDesiredLevelOfDetail(int lod); /** * Relative position of the mirror axis center * 0,0 - topleft corner of the image * 1,1 - bottomright corner of the image */ QPointF mirrorAxesCenter() const; /** * Sets the relative position of the axes center * \see mirrorAxesCenter() for details */ void setMirrorAxesCenter(const QPointF &value) const; /** * Configure the image to allow masks on the root not (as reported by * root()->allowAsChild()). By default it is not allowed (because it * looks weird from GUI point of view) */ void setAllowMasksOnRootNode(bool value); /** * \see setAllowMasksOnRootNode() */ bool allowMasksOnRootNode() const; public Q_SLOTS: /** * Explicitly start regeneration of LoD planes of all the devices * in the image. This call should be performed when the user is idle, * just to make the quality of image updates better. */ void explicitRegenerateLevelOfDetail(); public: /** * Blocks usage of level of detail functionality. After this method * has been called, no new strokes will use LoD. */ void setLevelOfDetailBlocked(bool value); /** * \see setLevelOfDetailBlocked() */ bool levelOfDetailBlocked() const; KisImageAnimationInterface *animationInterface() const; /** * @brief setProofingConfiguration, this sets the image's proofing configuration, and signals * the proofingConfiguration has changed. * @param proofingConfig - the kis proofing config that will be used instead. */ void setProofingConfiguration(KisProofingConfigurationSP proofingConfig); /** * @brief proofingConfiguration * @return the proofing configuration of the image. */ KisProofingConfigurationSP proofingConfiguration() const; public Q_SLOTS: bool startIsolatedMode(KisNodeSP node); void stopIsolatedMode(); public: KisNodeSP isolatedModeRoot() const; Q_SIGNALS: /** * Emitted whenever an action has caused the image to be * recomposited. Parameter is the rect that has been recomposited. */ void sigImageUpdated(const QRect &); /** Emitted whenever the image has been modified, so that it doesn't match with the version saved on disk. */ void sigImageModified(); /** * The signal is emitted when the size of the image is changed. * \p oldStillPoint and \p newStillPoint give the receiver the * hint about how the new and old rect of the image correspond to * each other. They specify the point of the image around which * the conversion was done. This point will stay still on the * user's screen. That is the \p newStillPoint of the new image * will be painted at the same screen position, where \p * oldStillPoint of the old image was painted. * * \param oldStillPoint is a still point represented in *old* * image coordinates * * \param newStillPoint is a still point represented in *new* * image coordinates */ void sigSizeChanged(const QPointF &oldStillPoint, const QPointF &newStillPoint); void sigProfileChanged(const KoColorProfile * profile); void sigColorSpaceChanged(const KoColorSpace* cs); void sigResolutionChanged(double xRes, double yRes); void sigRequestNodeReselection(KisNodeSP activeNode, const KisNodeList &selectedNodes); /** * Inform the model that a node was changed */ void sigNodeChanged(KisNodeSP node); /** * Inform that the image is going to be deleted */ void sigAboutToBeDeleted(); /** * The signal is emitted right after a node has been connected * to the graph of the nodes. * * WARNING: you must not request any graph-related information * about the node being run in a not-scheduler thread. If you need * information about the parent/siblings of the node connect * with Qt::DirectConnection, get needed information and then * emit another Qt::AutoConnection signal to pass this information * to your thread. See details of the implementation * in KisDummiesfacadeBase. */ void sigNodeAddedAsync(KisNodeSP node); /** * This signal is emitted right before a node is going to removed * from the graph of the nodes. * * WARNING: you must not request any graph-related information * about the node being run in a not-scheduler thread. * * \see comment in sigNodeAddedAsync() */ void sigRemoveNodeAsync(KisNodeSP node); /** * Emitted when the root node of the image has changed. * It happens, e.g. when we flatten the image. When * this happens the receiver should reload information * about the image */ void sigLayersChangedAsync(); /** * Emitted when the UI has requested the undo of the last stroke's * operation. The point is, we cannot deal with the internals of * the stroke without its creator knowing about it (which most * probably cause a crash), so we just forward this request from * the UI to the creator of the stroke. * * If your tool supports undoing part of its work, just listen to * this signal and undo when it comes */ void sigUndoDuringStrokeRequested(); /** * Emitted when the UI has requested the cancellation of * the stroke. The point is, we cannot cancel the stroke * without its creator knowing about it (which most probably * cause a crash), so we just forward this request from the UI * to the creator of the stroke. * * If your tool supports cancelling of its work in the middle * of operation, just listen to this signal and cancel * the stroke when it comes */ void sigStrokeCancellationRequested(); /** * Emitted when the image decides that the stroke should better * be ended. The point is, we cannot just end the stroke * without its creator knowing about it (which most probably * cause a crash), so we just forward this request from the UI * to the creator of the stroke. * * If your tool supports long strokes that may involve multiple * mouse actions in one stroke, just listen to this signal and * end the stroke when it comes. */ void sigStrokeEndRequested(); /** * Same as sigStrokeEndRequested() but is not emitted when the active node * is changed. */ void sigStrokeEndRequestedActiveNodeFiltered(); /** * Emitted when the isolated mode status has changed. * * Can be used by the receivers to catch a fact of forcefully * stopping the isolated mode by the image when some complex * action was requested */ void sigIsolatedModeChanged(); /** * Emitted when one or more nodes changed the collapsed state * */ void sigNodeCollapsedChanged(); /** * Emitted when the proofing configuration of the image is being changed. * */ void sigProofingConfigChanged(); /** * Internal signal for asynchronously requesting isolated mode to stop. Don't use it * outside KisImage, use sigIsolatedModeChanged() instead. */ void sigInternalStopIsolatedModeRequested(); public Q_SLOTS: KisCompositeProgressProxy* compositeProgressProxy(); bool isIdle(bool allowLocked = false); /** * @brief Wait until all the queued background jobs are completed and lock the image. * * KisImage object has a local scheduler that executes long-running image * rendering/modifying jobs (we call them "strokes") in a background. Basically, * one should either access the image from the scope of such jobs (strokes) or * just lock the image before using. * * Calling barrierLock() will wait until all the queued operations are finished * and lock the image, so you can start accessing it in a safe way. * * @p readOnly tells the image if the caller is going to modify the image during * holding the lock. Locking with non-readOnly access will reset all * the internal caches of the image (lod-planes) when the lock status * will be lifted. */ void barrierLock(bool readOnly = false); /** * @brief Tries to lock the image without waiting for the jobs to finish * * Same as barrierLock(), but doesn't block execution of the calling thread * until all the background jobs are finished. Instead, in case of presence of * unfinished jobs in the queue, it just returns false * * @return whether the lock has been acquired * @see barrierLock */ bool tryBarrierLock(bool readOnly = false); /** * Wait for all the internal image jobs to complete and return without locking * the image. This function is handly for tests or other synchronous actions, * when one needs to wait for the result of his actions. */ void waitForDone(); KisStrokeId startStroke(KisStrokeStrategy *strokeStrategy) override; void addJob(KisStrokeId id, KisStrokeJobData *data) override; void endStroke(KisStrokeId id) override; bool cancelStroke(KisStrokeId id) override; /** * @brief blockUpdates block updating the image projection */ void blockUpdates() override; /** * @brief unblockUpdates unblock updating the image project. This * only restarts the scheduler and does not schedule a full refresh. */ void unblockUpdates() override; /** * Disables notification of the UI about the changes in the image. * This feature is used by KisProcessingApplicator. It is needed * when we change the size of the image. In this case, the whole * image will be reloaded into UI by sigSizeChanged(), so there is * no need to inform the UI about individual dirty rects. * * The last call to enableUIUpdates() will return the list of updates * that were requested while they were blocked. */ void disableUIUpdates() override; /** * Notify GUI about a bunch of updates planned. GUI is expected to wait * until all the updates are completed, and render them on screen only * in the very and of the batch. */ void notifyBatchUpdateStarted() override; /** * Notify GUI that batch update has been completed. Now GUI can start * showing all of them on screen. */ void notifyBatchUpdateEnded() override; /** * Notify GUI that rect \p rc is now prepared in the image and * GUI can read data from it. * * WARNING: GUI will read the data right in the handler of this * signal, so exclusive access to the area must be guaranteed * by the caller. */ void notifyUIUpdateCompleted(const QRect &rc) override; /** * \see disableUIUpdates */ QVector enableUIUpdates() override; /** * Disables the processing of all the setDirty() requests that * come to the image. The incoming requests are effectively * *dropped*. * * This feature is used by KisProcessingApplicator. For many cases * it provides its own updates interface, which recalculates the * whole subtree of nodes. But while we change any particular * node, it can ask for an update itself. This method is a way of * blocking such intermediate (and excessive) requests. * * NOTE: this is a convenience function for setProjectionUpdatesFilter() * that installs a predefined filter that eats everything. Please * note that these calls are *not* recursive */ void disableDirtyRequests() override; /** * \see disableDirtyRequests() */ void enableDirtyRequests() override; /** * Installs a filter object that will filter all the incoming projection update * requests. If the filter return true, the incoming update is dropped. * * NOTE: you cannot set filters recursively! */ void setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP filter) override; /** * \see setProjectionUpdatesFilter() */ KisProjectionUpdatesFilterSP projectionUpdatesFilter() const override; void refreshGraphAsync(KisNodeSP root = KisNodeSP()) override; void refreshGraphAsync(KisNodeSP root, const QRect &rc) override; void refreshGraphAsync(KisNodeSP root, const QRect &rc, const QRect &cropRect) override; /** * Triggers synchronous recomposition of the projection */ void refreshGraph(KisNodeSP root = KisNodeSP()); void refreshGraph(KisNodeSP root, const QRect& rc, const QRect &cropRect); void initialRefreshGraph(); /** * Initiate a stack regeneration skipping the recalculation of the * filthy node's projection. * * Works exactly as pseudoFilthy->setDirty() with the only * exception that pseudoFilthy::updateProjection() will not be * called. That is used by KisRecalculateTransformMaskJob to avoid * cyclic dependencies. */ void requestProjectionUpdateNoFilthy(KisNodeSP pseudoFilthy, const QRect &rc, const QRect &cropRect); /** * Adds a spontaneous job to the updates queue. * * A spontaneous job may do some trivial tasks in the background, * like updating the outline of selection or purging unused tiles * from the existing paint devices. */ void addSpontaneousJob(KisSpontaneousJob *spontaneousJob); /** * \return true if there are some updates in the updates queue * Please note, that is doesn't guarantee that there are no updates * running in in the updater context at the very moment. To guarantee that * there are no updates left at all, please use barrier jobs instead. */ bool hasUpdatesRunning() const override; /** * This method is called by the UI (*not* by the creator of the * stroke) when it thinks the current stroke should undo its last * action, for example, when the user presses Ctrl+Z while some * stroke is active. * * If the creator of the stroke supports undoing of intermediate * actions, it will be notified about this request and can undo * its last action. */ void requestUndoDuringStroke(); /** * This method is called by the UI (*not* by the creator of the * stroke) when it thinks current stroke should be cancelled. If * there is a running stroke that has already been detached from * its creator (ended or cancelled), it will be forcefully * cancelled and reverted. If there is an open stroke present, and * if its creator supports cancelling, it will be notified about * the request and the stroke will be cancelled */ void requestStrokeCancellation(); /** * This method requests the last stroke executed on the image to become undone. * If the stroke is not ended, or if all the Lod0 strokes are completed, the method * returns UNDO_FAIL. If the last Lod0 is going to be finished soon, then UNDO_WAIT * is returned and the caller should just wait for its completion and call global undo * instead. UNDO_OK means one unfinished stroke has been undone. */ UndoResult tryUndoUnfinishedLod0Stroke(); /** * This method is called when image or some other part of Krita * (*not* the creator of the stroke) decides that the stroke * should be ended. If the creator of the stroke supports it, it * will be notified and the stroke will be cancelled */ void requestStrokeEnd(); /** * Same as requestStrokeEnd() but is called by view manager when * the current node is changed. Use to distinguish * sigStrokeEndRequested() and * sigStrokeEndRequestedActiveNodeFiltered() which are used by * KisNodeJugglerCompressed */ void requestStrokeEndActiveNode(); private: KisImage(const KisImage& rhs, KisUndoStore *undoStore, bool exactCopy); KisImage& operator=(const KisImage& rhs); void emitSizeChanged(); void resizeImageImpl(const QRect& newRect, bool cropLayers); void rotateImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, double radians, bool resizeImage, KisSelectionSP selection); void shearImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double angleX, double angleY, KisSelectionSP selection); void safeRemoveTwoNodes(KisNodeSP node1, KisNodeSP node2); void refreshHiddenArea(KisNodeSP rootNode, const QRect &preparedArea); void requestProjectionUpdateImpl(KisNode *node, const QVector &rects, const QRect &cropRect); friend class KisImageResizeCommand; void setSize(const QSize& size); friend class KisImageSetProjectionColorSpaceCommand; void setProjectionColorSpace(const KoColorSpace * colorSpace); friend class KisDeselectGlobalSelectionCommand; friend class KisReselectGlobalSelectionCommand; friend class KisSetGlobalSelectionCommand; friend class KisImageTest; friend class Document; // For libkis /** * Replaces the current global selection with globalSelection. If * \p globalSelection is empty, removes the selection object, so that * \ref globalSelection() will return 0 after that. */ void setGlobalSelection(KisSelectionSP globalSelection); /** * Deselects current global selection. * \ref globalSelection() will return 0 after that. */ void deselectGlobalSelection(); /** * Reselects current deselected selection * * \see deselectGlobalSelection() */ void reselectGlobalSelection(); private: class KisImagePrivate; KisImagePrivate * m_d; }; #endif // KIS_IMAGE_H_ diff --git a/libs/image/kis_image_animation_interface.cpp b/libs/image/kis_image_animation_interface.cpp index b94032f948..6191039fb2 100644 --- a/libs/image/kis_image_animation_interface.cpp +++ b/libs/image/kis_image_animation_interface.cpp @@ -1,425 +1,422 @@ /* * 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; + KisTimeSpan fullClipRange; + KisTimeSpan 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); + m_d->fullClipRange = KisTimeSpan(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 +const KisTimeSpan& KisImageAnimationInterface::fullClipRange() const { return m_d->fullClipRange; } -void KisImageAnimationInterface::setFullClipRange(const KisTimeRange range) +void KisImageAnimationInterface::setFullClipRange(const KisTimeSpan range) { - KIS_SAFE_ASSERT_RECOVER_RETURN(!range.isInfinite()); m_d->fullClipRange = range; emit sigFullClipRangeChanged(); } void KisImageAnimationInterface::setFullClipRangeStartTime(int column) { - KisTimeRange newRange(column, m_d->fullClipRange.end(), false); + KisTimeSpan newRange(column, m_d->fullClipRange.end()); setFullClipRange(newRange); } void KisImageAnimationInterface::setFullClipRangeEndTime(int column) { - KisTimeRange newRange(m_d->fullClipRange.start(), column, false); + KisTimeSpan newRange(m_d->fullClipRange.start(), column); setFullClipRange(newRange); } -const KisTimeRange& KisImageAnimationInterface::playbackRange() const +const KisTimeSpan& KisImageAnimationInterface::playbackRange() const { - return m_d->playbackRange.isValid() ? m_d->playbackRange : m_d->fullClipRange; + return !m_d->playbackRange.isEmpty() ? m_d->playbackRange : m_d->fullClipRange; } -void KisImageAnimationInterface::setPlaybackRange(const KisTimeRange range) +void KisImageAnimationInterface::setPlaybackRange(const KisTimeSpan range) { - KIS_SAFE_ASSERT_RECOVER_RETURN(!range.isInfinite()); 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; - const KisTimeRange range = KisTimeRange::calculateIdenticalFramesRecursive(m_d->image->root(), currentUITime()); - const bool needsRegeneration = !range.contains(frameId); + const bool needsRegeneration = !areFramesIdentical(m_d->image->root(), currentUITime(), 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; // even overlay selection masks are not rendered in the cache if (node->inherits("KisSelectionMask")) return; const int currentTime = m_d->currentTime(); - KisTimeRange invalidateRange; + KisFrameSet invalidateRange; if (recursive) { - invalidateRange = KisTimeRange::calculateAffectedFramesRecursive(node, currentTime); + invalidateRange = calculateAffectedFramesRecursive(node, currentTime); } else { - invalidateRange = KisTimeRange::calculateNodeAffectedFrames(node, currentTime); + invalidateRange = calculateNodeAffectedFrames(node, currentTime); } // 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) +void KisImageAnimationInterface::invalidateFrames(const KisFrameSet &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 0cfb92f505..64bf791637 100644 --- a/libs/image/kis_image_animation_interface.h +++ b/libs/image/kis_image_animation_interface.h @@ -1,214 +1,215 @@ /* * 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 KisFrameSet; +class KisTimeSpan; 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); + void invalidateFrames(const KisFrameSet &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); + const KisTimeSpan& fullClipRange() const; + void setFullClipRange(const KisTimeSpan range); void setFullClipRangeStartTime(int column); void setFullClipRangeEndTime(int column); - const KisTimeRange &playbackRange() const; - void setPlaybackRange(const KisTimeRange range); + const KisTimeSpan &playbackRange() const; + void setPlaybackRange(const KisTimeSpan 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 sigFramesChanged(const KisFrameSet &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/image/kis_keyframe.cpp b/libs/image/kis_keyframe.cpp index cb230cc17c..265433a9d9 100644 --- a/libs/image/kis_keyframe.cpp +++ b/libs/image/kis_keyframe.cpp @@ -1,132 +1,156 @@ /* * 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_image_config.h" #include "kis_keyframe.h" +#include "kis_image_config.h" #include "kis_keyframe_channel.h" #include "kis_types.h" #include struct KisKeyframeSPStaticRegistrar { KisKeyframeSPStaticRegistrar() { + qRegisterMetaType("KisKeyframeBaseSP"); qRegisterMetaType("KisKeyframeSP"); } }; static KisKeyframeSPStaticRegistrar __registrar; - -struct KisKeyframe::Private +struct KisKeyframeBase::Private { QPointer channel; int time; - InterpolationMode interpolationMode; - InterpolationTangentsMode tangentsMode; + Private(KisKeyframeChannel *channel, int time) + : channel(channel), time(time) + {} +}; + +KisKeyframeBase::KisKeyframeBase(KisKeyframeChannel *channel, int time) + : m_d(new Private(channel, time)) +{} + +KisKeyframeBase::~KisKeyframeBase() = default; + +KisKeyframeChannel *KisKeyframeBase::channel() const +{ + return m_d->channel; +} + +int KisKeyframeBase::time() const +{ + return m_d->time; +} + +void KisKeyframeBase::setTime(int time) +{ + m_d->time = time; +} + +int KisKeyframeBase::duration() const +{ + KisKeyframeBaseSP next = m_d->channel->nextItem(m_d->time); + if (!next) return -1; + + return next->time() - m_d->time; +} + +struct KisKeyframe::Private +{ + InterpolationMode interpolationMode{Constant}; + InterpolationTangentsMode tangentsMode{Smooth}; QPointF leftTangent; QPointF rightTangent; int colorLabel{0}; - - Private(KisKeyframeChannel *channel, int time) - : channel(channel), time(time), interpolationMode(Constant) - {} }; KisKeyframe::KisKeyframe(KisKeyframeChannel *channel, int time) - : m_d(new Private(channel, time)) + : KisKeyframeBase(channel, time) + , m_d(new Private()) { m_d->colorLabel = KisImageConfig(true).defaultFrameColorLabel(); } KisKeyframe::KisKeyframe(const KisKeyframe *rhs, KisKeyframeChannel *channel) - : m_d(new Private(channel, rhs->time())) + : KisKeyframeBase(channel, rhs->time()) + , m_d(new Private()) { m_d->interpolationMode = rhs->m_d->interpolationMode; m_d->tangentsMode = rhs->m_d->tangentsMode; m_d->leftTangent = rhs->m_d->leftTangent; m_d->rightTangent = rhs->m_d->rightTangent; m_d->colorLabel = rhs->m_d->colorLabel; } KisKeyframe::~KisKeyframe() {} -int KisKeyframe::time() const -{ - return m_d->time; -} - -void KisKeyframe::setTime(int time) -{ - m_d->time = time; -} - void KisKeyframe::setInterpolationMode(KisKeyframe::InterpolationMode mode) { m_d->interpolationMode = mode; } KisKeyframe::InterpolationMode KisKeyframe::interpolationMode() const { return m_d->interpolationMode; } void KisKeyframe::setTangentsMode(KisKeyframe::InterpolationTangentsMode mode) { m_d->tangentsMode = mode; } KisKeyframe::InterpolationTangentsMode KisKeyframe::tangentsMode() const { return m_d->tangentsMode; } void KisKeyframe::setInterpolationTangents(QPointF leftTangent, QPointF rightTangent) { m_d->leftTangent = leftTangent; m_d->rightTangent = rightTangent; } QPointF KisKeyframe::leftTangent() const { return m_d->leftTangent; } QPointF KisKeyframe::rightTangent() const { return m_d->rightTangent; } int KisKeyframe::colorLabel() const { return m_d->colorLabel; } void KisKeyframe::setColorLabel(int label) { m_d->colorLabel = label; } bool KisKeyframe::hasContent() const { return true; } -KisKeyframeChannel *KisKeyframe::channel() const +KisKeyframeSP KisKeyframe::getOriginalKeyframeFor(int) const { - return m_d->channel; + return channel()->keyframeAt(this->time()); } diff --git a/libs/image/kis_keyframe.h b/libs/image/kis_keyframe.h index 8e04d15f45..68eaf5cb94 100644 --- a/libs/image/kis_keyframe.h +++ b/libs/image/kis_keyframe.h @@ -1,81 +1,101 @@ /* * 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_KEYFRAME_H #define KIS_KEYFRAME_H #include #include #include #include "kritaimage_export.h" #include "kis_types.h" class KisKeyframeChannel; -class KRITAIMAGE_EXPORT KisKeyframe +class KRITAIMAGE_EXPORT KisKeyframeBase +{ +public: + KisKeyframeBase(KisKeyframeChannel *channel, int time); + virtual ~KisKeyframeBase(); + + KisKeyframeChannel *channel() const; + + int time() const; + void setTime(int time); + + int duration() const; + virtual QRect affectedRect() const = 0; + virtual KisKeyframeSP getOriginalKeyframeFor(int time) const = 0; + +private: + struct Private; + QScopedPointer m_d; + +}; +class KRITAIMAGE_EXPORT KisKeyframe : public KisKeyframeBase { public: enum InterpolationMode { Constant, Linear, Bezier }; enum InterpolationTangentsMode { Sharp, Smooth }; KisKeyframe(KisKeyframeChannel *channel, int time); - virtual ~KisKeyframe(); + ~KisKeyframe() override; /** * Create a copy of the keyframe for insertion into given channel. * Used when constructing a copy of a keyframe channel. */ virtual KisKeyframeSP cloneFor(KisKeyframeChannel *channel) const = 0; - int time() const; - void setTime(int time); - void setInterpolationMode(InterpolationMode mode); InterpolationMode interpolationMode() const; void setTangentsMode(InterpolationTangentsMode mode); InterpolationTangentsMode tangentsMode() const; void setInterpolationTangents(QPointF leftTangent, QPointF rightTangent); QPointF leftTangent() const; QPointF rightTangent() const; int colorLabel() const; void setColorLabel(int label); + virtual bool hasContent() const; // does any content exist in keyframe, or is it empty? - KisKeyframeChannel *channel() const; + KisKeyframeSP getOriginalKeyframeFor(int time) const override; protected: KisKeyframe(const KisKeyframe *rhs, KisKeyframeChannel *channel); private: struct Private; QScopedPointer m_d; }; +Q_DECLARE_METATYPE(KisKeyframeBase*) +Q_DECLARE_METATYPE(KisKeyframeBaseSP) Q_DECLARE_METATYPE(KisKeyframe*) Q_DECLARE_METATYPE(KisKeyframeSP) #endif diff --git a/libs/image/kis_keyframe_channel.cpp b/libs/image/kis_keyframe_channel.cpp index 3cc4ebc682..dc2e3282db 100644 --- a/libs/image/kis_keyframe_channel.cpp +++ b/libs/image/kis_keyframe_channel.cpp @@ -1,668 +1,1081 @@ /* * 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_keyframe_channel.h" #include "KoID.h" #include "kis_global.h" #include "kis_node.h" +#include "KisCollectionUtils.h" #include "kis_time_range.h" #include "kundo2command.h" #include "kis_keyframe_commands.h" +#include "kis_animation_cycle.h" #include const KoID KisKeyframeChannel::Content = KoID("content", ki18n("Content")); const KoID KisKeyframeChannel::Opacity = KoID("opacity", ki18n("Opacity")); const KoID KisKeyframeChannel::TransformArguments = KoID("transform_arguments", ki18n("Transform")); const KoID KisKeyframeChannel::TransformPositionX = KoID("transform_pos_x", ki18n("Position (X)")); const KoID KisKeyframeChannel::TransformPositionY = KoID("transform_pos_y", ki18n("Position (Y)")); const KoID KisKeyframeChannel::TransformScaleX = KoID("transform_scale_x", ki18n("Scale (X)")); const KoID KisKeyframeChannel::TransformScaleY = KoID("transform_scale_y", ki18n("Scale (Y)")); const KoID KisKeyframeChannel::TransformShearX = KoID("transform_shear_x", ki18n("Shear (X)")); const KoID KisKeyframeChannel::TransformShearY = KoID("transform_shear_y", ki18n("Shear (Y)")); const KoID KisKeyframeChannel::TransformRotationX = KoID("transform_rotation_x", ki18n("Rotation (X)")); const KoID KisKeyframeChannel::TransformRotationY = KoID("transform_rotation_y", ki18n("Rotation (Y)")); const KoID KisKeyframeChannel::TransformRotationZ = KoID("transform_rotation_z", ki18n("Rotation (Z)")); +/** + * Finds the last item the in the map with a key less than the given one. + * Returns map.constEnd() if no such key exists. + */ +template +typename QMap::const_iterator findActive(const QMap &map, int maximumKey) +{ + typename QMap::const_iterator i = map.upperBound(maximumKey); + if (i == map.constBegin()) return map.constEnd(); + return i - 1; +} + +/** + * Finds the last item the in the map before the "active" item (see findActive) for the given key. + * Returns map.constEnd() if no such key exists. + */ +template +typename QMap::const_iterator findPrevious(const QMap &map, int currentKey) +{ + typename QMap::const_iterator active = lastBeforeOrAt(map, currentKey); + if (active == map.constEnd()) return map.constEnd(); + + if (currentKey > active.key()) return active; + + if (active == map.constBegin()) return map.constEnd(); + return active - 1; +} + +/** + * Finds the first item the in the map with a key greater than the given one. + * Returns map.constEnd() if no such key exists. + */ +template +typename QMap::const_iterator findNext(const QMap &map, int currentKey) +{ + return map.upperBound(currentKey); +} + struct KisKeyframeChannel::Private { Private() {} Private(const Private &rhs, KisNodeWSP newParentNode) { node = newParentNode; id = rhs.id; defaultBounds = rhs.defaultBounds; haveBrokenFrameTimeBug = rhs.haveBrokenFrameTimeBug; } KeyframesMap keys; + QMap> repeats; KisNodeWSP node; KoID id; KisDefaultBoundsBaseSP defaultBounds; bool haveBrokenFrameTimeBug = false; + + void add(KisKeyframeBaseSP item) { + auto repeat = item.dynamicCast(); + if (repeat) { + repeats.insert(repeat->time(), repeat); + } else { + auto keyframe = item.dynamicCast(); + KIS_ASSERT_RECOVER_RETURN(keyframe); + keys.insert(item->time(), keyframe); + } + } + + void remove(KisKeyframeBaseSP item) { + auto repeat = item.dynamicCast(); + if (repeat) { + repeats.remove(repeat->time()); + } else { + keys.remove(item->time()); + } + } + + void moveKeyframe(KisKeyframeBaseSP keyframe, int newTime) { + remove(keyframe); + keyframe->setTime(newTime); + add(keyframe); + } }; KisKeyframeChannel::KisKeyframeChannel(const KoID &id, KisDefaultBoundsBaseSP defaultBounds) : m_d(new Private) { m_d->id = id; m_d->node = 0; m_d->defaultBounds = defaultBounds; } KisKeyframeChannel::KisKeyframeChannel(const KisKeyframeChannel &rhs, KisNode *newParentNode) : m_d(new Private(*rhs.m_d, newParentNode)) { KIS_ASSERT_RECOVER_NOOP(&rhs != this); Q_FOREACH(KisKeyframeSP keyframe, rhs.m_d->keys) { - m_d->keys.insert(keyframe->time(), keyframe->cloneFor(this)); + const KisKeyframeSP clone = keyframe->cloneFor(this); + m_d->add(clone); + } + + Q_FOREACH(const QSharedPointer repeat, rhs.m_d->repeats) { + m_d->add(toQShared(new KisRepeatFrame(*repeat, this))); } + } KisKeyframeChannel::~KisKeyframeChannel() {} QString KisKeyframeChannel::id() const { return m_d->id.id(); } QString KisKeyframeChannel::name() const { return m_d->id.name(); } void KisKeyframeChannel::setNode(KisNodeWSP node) { m_d->node = node; } KisNodeWSP KisKeyframeChannel::node() const { return m_d->node; } int KisKeyframeChannel::keyframeCount() const { return m_d->keys.count(); } KisKeyframeChannel::KeyframesMap& KisKeyframeChannel::keys() { return m_d->keys; } const KisKeyframeChannel::KeyframesMap& KisKeyframeChannel::constKeys() const { return m_d->keys; } #define LAZY_INITIALIZE_PARENT_COMMAND(cmd) \ QScopedPointer __tempCommand; \ if (!parentCommand) { \ __tempCommand.reset(new KUndo2Command()); \ cmd = __tempCommand.data(); \ } KisKeyframeSP KisKeyframeChannel::addKeyframe(int time, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); return insertKeyframe(time, KisKeyframeSP(), parentCommand); } -KisKeyframeSP KisKeyframeChannel::copyKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand) +KisKeyframeBaseSP KisKeyframeChannel::copyItem(const KisKeyframeBaseSP item, int newTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); + return insertKeyframe(newTime, item, parentCommand); +} + +KisKeyframeSP KisKeyframeChannel::copyAsKeyframe(const KisKeyframeBaseSP item, int originalTime, int newTime, KUndo2Command *parentCommand) +{ + LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); + + KisKeyframeSP keyframe = item->getOriginalKeyframeFor(originalTime); return insertKeyframe(newTime, keyframe, parentCommand); } -KisKeyframeSP KisKeyframeChannel::insertKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) +KisKeyframeSP KisKeyframeChannel::linkKeyframe(const KisKeyframeBaseSP, int, KUndo2Command*) { + return KisKeyframeSP(); +} + +KisKeyframeSP KisKeyframeChannel::insertKeyframe(int time, const KisKeyframeBaseSP copySrc, KUndo2Command *parentCommand) { - KisKeyframeSP keyframe = keyframeAt(time); - if (keyframe) { - deleteKeyframeImpl(keyframe, parentCommand, false); + KisKeyframeBaseSP oldItem = itemAt(time); + if (oldItem) { + deleteKeyframeImpl(oldItem, parentCommand, false); } Q_ASSERT(parentCommand); - keyframe = createKeyframe(time, copySrc, parentCommand); + KisKeyframeSP sourceKeyframe = copySrc ? copySrc->getOriginalKeyframeFor(copySrc->time()) : KisKeyframeSP(); + KisKeyframeSP keyframe = createKeyframe(time, sourceKeyframe, parentCommand); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, keyframe->time(), keyframe, parentCommand); cmd->redo(); return keyframe; } -bool KisKeyframeChannel::deleteKeyframe(KisKeyframeSP keyframe, KUndo2Command *parentCommand) +bool KisKeyframeChannel::deleteKeyframe(KisKeyframeBaseSP keyframe, KUndo2Command *parentCommand) { return deleteKeyframeImpl(keyframe, parentCommand, true); } -bool KisKeyframeChannel::moveKeyframe(KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand) +bool KisKeyframeChannel::moveKeyframe(KisKeyframeBaseSP keyframe, int newTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); if (newTime == keyframe->time()) return false; - KisKeyframeSP other = keyframeAt(newTime); - if (other) { - deleteKeyframeImpl(other, parentCommand, false); - } - const int srcTime = keyframe->time(); - KUndo2Command *cmd = new KisMoveFrameCommand(this, keyframe, srcTime, newTime, parentCommand); + KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newTime, keyframe, parentCommand); cmd->redo(); if (srcTime == 0) { addKeyframe(srcTime, parentCommand); } return true; } bool KisKeyframeChannel::swapFrames(int lhsTime, int rhsTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); if (lhsTime == rhsTime) return false; - KisKeyframeSP lhsFrame = keyframeAt(lhsTime); - KisKeyframeSP rhsFrame = keyframeAt(rhsTime); + KisKeyframeBaseSP lhsFrame = itemAt(lhsTime); + KisKeyframeBaseSP rhsFrame = itemAt(rhsTime); if (!lhsFrame && !rhsFrame) return false; if (lhsFrame && !rhsFrame) { moveKeyframe(lhsFrame, rhsTime, parentCommand); } else if (!lhsFrame && rhsFrame) { moveKeyframe(rhsFrame, lhsTime, parentCommand); } else { KUndo2Command *cmd = new KisSwapFramesCommand(this, lhsFrame, rhsFrame, parentCommand); cmd->redo(); } return true; } -bool KisKeyframeChannel::deleteKeyframeImpl(KisKeyframeSP keyframe, KUndo2Command *parentCommand, bool recreate) +bool KisKeyframeChannel::deleteKeyframeImpl(KisKeyframeBaseSP item, KUndo2Command *parentCommand, bool recreate) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); Q_ASSERT(parentCommand); - KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, keyframe->time(), KisKeyframeSP(), parentCommand); + KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, item->time(), KisKeyframeSP(), parentCommand); cmd->redo(); - destroyKeyframe(keyframe, parentCommand); - if (recreate && keyframe->time() == 0) { - addKeyframe(0, parentCommand); + KisKeyframeSP keyframe = item.dynamicCast(); + if (keyframe) { + destroyKeyframe(keyframe, parentCommand); + + if (recreate && keyframe->time() == 0) { + addKeyframe(0, parentCommand); + } } return true; } -void KisKeyframeChannel::moveKeyframeImpl(KisKeyframeSP keyframe, int newTime) +void KisKeyframeChannel::moveKeyframeImpl(KisKeyframeBaseSP keyframe, int newTime) { KIS_ASSERT_RECOVER_RETURN(keyframe); - KIS_ASSERT_RECOVER_RETURN(!keyframeAt(newTime)); + KIS_ASSERT_RECOVER_RETURN(!itemAt(newTime)); + KIS_SAFE_ASSERT_RECOVER_NOOP(itemAt(keyframe->time()) == keyframe); - KisTimeRange rangeSrc = affectedFrames(keyframe->time()); - QRect rectSrc = affectedRect(keyframe); + KisFrameSet rangeSrc = affectedFrames(keyframe->time()); + QRect rectSrc = keyframe->affectedRect(); emit sigKeyframeAboutToBeMoved(keyframe, newTime); - m_d->keys.remove(keyframe->time()); int oldTime = keyframe->time(); + m_d->moveKeyframe(keyframe, newTime); keyframe->setTime(newTime); - m_d->keys.insert(newTime, keyframe); emit sigKeyframeMoved(keyframe, oldTime); - KisTimeRange rangeDst = affectedFrames(keyframe->time()); - QRect rectDst = affectedRect(keyframe); + KisFrameSet rangeDst = affectedFrames(keyframe->time()); + QRect rectDst = keyframe->affectedRect(); requestUpdate(rangeSrc, rectSrc); requestUpdate(rangeDst, rectDst); } -void KisKeyframeChannel::swapKeyframesImpl(KisKeyframeSP lhsKeyframe, KisKeyframeSP rhsKeyframe) +void KisKeyframeChannel::swapKeyframesImpl(KisKeyframeBaseSP lhsKeyframe, KisKeyframeBaseSP rhsKeyframe) { KIS_ASSERT_RECOVER_RETURN(lhsKeyframe); KIS_ASSERT_RECOVER_RETURN(rhsKeyframe); - KisTimeRange rangeLhs = affectedFrames(lhsKeyframe->time()); - KisTimeRange rangeRhs = affectedFrames(rhsKeyframe->time()); + KisFrameSet rangeLhs = affectedFrames(lhsKeyframe->time()); + KisFrameSet rangeRhs = affectedFrames(rhsKeyframe->time()); - const QRect rectLhsSrc = affectedRect(lhsKeyframe); - const QRect rectRhsSrc = affectedRect(rhsKeyframe); + const QRect rectLhsSrc = lhsKeyframe->affectedRect(); + const QRect rectRhsSrc = rhsKeyframe->affectedRect(); const int lhsTime = lhsKeyframe->time(); const int rhsTime = rhsKeyframe->time(); emit sigKeyframeAboutToBeMoved(lhsKeyframe, rhsTime); emit sigKeyframeAboutToBeMoved(rhsKeyframe, lhsTime); - m_d->keys.remove(lhsTime); - m_d->keys.remove(rhsTime); + m_d->remove(lhsKeyframe); + m_d->remove(rhsKeyframe); rhsKeyframe->setTime(lhsTime); lhsKeyframe->setTime(rhsTime); - m_d->keys.insert(lhsTime, rhsKeyframe); - m_d->keys.insert(rhsTime, lhsKeyframe); + m_d->add(lhsKeyframe); + m_d->add(rhsKeyframe); emit sigKeyframeMoved(lhsKeyframe, lhsTime); emit sigKeyframeMoved(rhsKeyframe, rhsTime); - const QRect rectLhsDst = affectedRect(lhsKeyframe); - const QRect rectRhsDst = affectedRect(rhsKeyframe); + const QRect rectLhsDst = lhsKeyframe->affectedRect(); + const QRect rectRhsDst = rhsKeyframe->affectedRect(); requestUpdate(rangeLhs, rectLhsSrc | rectRhsDst); requestUpdate(rangeRhs, rectRhsSrc | rectLhsDst); } -KisKeyframeSP KisKeyframeChannel::replaceKeyframeAt(int time, KisKeyframeSP newKeyframe) +KisKeyframeBaseSP KisKeyframeChannel::replaceKeyframeAt(int time, KisKeyframeBaseSP newKeyframe) { Q_ASSERT(newKeyframe.isNull() || time == newKeyframe->time()); - KisKeyframeSP existingKeyframe = keyframeAt(time); + KisKeyframeBaseSP existingKeyframe = itemAt(time); if (!existingKeyframe.isNull()) { removeKeyframeLogical(existingKeyframe); } if (!newKeyframe.isNull()) { insertKeyframeLogical(newKeyframe); } return existingKeyframe; } -void KisKeyframeChannel::insertKeyframeLogical(KisKeyframeSP keyframe) +void KisKeyframeChannel::insertKeyframeLogical(KisKeyframeBaseSP keyframe) { const int time = keyframe->time(); emit sigKeyframeAboutToBeAdded(keyframe); - m_d->keys.insert(time, keyframe); + m_d->add(keyframe); emit sigKeyframeAdded(keyframe); - QRect rect = affectedRect(keyframe); - KisTimeRange range = affectedFrames(time); + QRect rect = keyframe->affectedRect(); + KisFrameSet range = affectedFrames(time); requestUpdate(range, rect); } -void KisKeyframeChannel::removeKeyframeLogical(KisKeyframeSP keyframe) +void KisKeyframeChannel::removeKeyframeLogical(KisKeyframeBaseSP keyframe) { - QRect rect = affectedRect(keyframe); - KisTimeRange range = affectedFrames(keyframe->time()); + QRect rect = keyframe->affectedRect(); + KisFrameSet range = affectedFrames(keyframe->time()); emit sigKeyframeAboutToBeRemoved(keyframe); - m_d->keys.remove(keyframe->time()); + m_d->remove(keyframe); emit sigKeyframeRemoved(keyframe); requestUpdate(range, rect); } KisKeyframeSP KisKeyframeChannel::keyframeAt(int time) const { - KeyframesMap::const_iterator i = m_d->keys.constFind(time); - if (i != m_d->keys.constEnd()) { - return i.value(); - } - - return KisKeyframeSP(); + return m_d->keys.value(time); } KisKeyframeSP KisKeyframeChannel::activeKeyframeAt(int time) const { - KeyframesMap::const_iterator i = activeKeyIterator(time); + KeyframesMap::const_iterator i = KisCollectionUtils::lastBeforeOrAt(m_d->keys, time); if (i != m_d->keys.constEnd()) { return i.value(); } return KisKeyframeSP(); } +KisKeyframeSP KisKeyframeChannel::visibleKeyframeAt(int time) const +{ + const QSharedPointer repeat = activeRepeatAt(time); + return repeat ? repeat->getOriginalKeyframeFor(time) : activeKeyframeAt(time); +} + KisKeyframeSP KisKeyframeChannel::currentlyActiveKeyframe() const { return activeKeyframeAt(currentTime()); } KisKeyframeSP KisKeyframeChannel::firstKeyframe() const { if (m_d->keys.isEmpty()) return KisKeyframeSP(); return m_d->keys.first(); } KisKeyframeSP KisKeyframeChannel::nextKeyframe(KisKeyframeSP keyframe) const { - KeyframesMap::const_iterator i = m_d->keys.constFind(keyframe->time()); - if (i == m_d->keys.constEnd()) return KisKeyframeSP(0); - - i++; + return nextKeyframe(keyframe->time()); +} +KisKeyframeSP KisKeyframeChannel::nextKeyframe(int time) const +{ + KeyframesMap::const_iterator i = KisCollectionUtils::firstAfter(m_d->keys, time); if (i == m_d->keys.constEnd()) return KisKeyframeSP(0); return i.value(); } KisKeyframeSP KisKeyframeChannel::previousKeyframe(KisKeyframeSP keyframe) const { - KeyframesMap::const_iterator i = m_d->keys.constFind(keyframe->time()); - if (i == m_d->keys.constBegin() || i == m_d->keys.constEnd()) return KisKeyframeSP(0); - i--; + return previousKeyframe(keyframe->time()); +} +KisKeyframeSP KisKeyframeChannel::previousKeyframe(int time) const +{ + KeyframesMap::const_iterator i = KisCollectionUtils::lastBefore(m_d->keys, time); + if (i == m_d->keys.constEnd()) return KisKeyframeSP(0); return i.value(); } KisKeyframeSP KisKeyframeChannel::lastKeyframe() const { if (m_d->keys.isEmpty()) return KisKeyframeSP(0); return (m_d->keys.end()-1).value(); } +KisKeyframeBaseSP KisKeyframeChannel::firstItem() const +{ + KisKeyframeSP firstKey = firstKeyframe(); + + KisKeyframeBaseSP firstRepeat = m_d->repeats.isEmpty() ? nullptr : m_d->repeats.first(); + if (firstRepeat && (!firstKey || firstKey->time() > firstRepeat->time())) { + return firstRepeat; + } + + return firstKey; +} + +KisKeyframeBaseSP KisKeyframeChannel::itemAt(int time) const +{ + const KisKeyframeSP keyframe = keyframeAt(time); + if (keyframe) return keyframe; + + const QSharedPointer repeat = activeRepeatAt(time); + if (repeat && time == repeat->time()) return repeat; + + return KisKeyframeBaseSP(); +} + +KisKeyframeBaseSP KisKeyframeChannel::activeItemAt(int time) const +{ + const KisKeyframeSP keyframe = activeKeyframeAt(time); + if (keyframe) return keyframe; + + return activeRepeatAt(time); +} + +KisKeyframeBaseSP KisKeyframeChannel::nextItem(const KisKeyframeBase &item) const +{ + const KisKeyframeSP keyframe = nextKeyframe(item.time()); + + auto repeatIter = KisCollectionUtils::firstAfter(m_d->repeats, item.time()); + const auto repeat = (repeatIter != m_d->repeats.constEnd()) ? repeatIter.value() : QSharedPointer(); + + if (keyframe && (!repeat || repeat->time() > keyframe->time())) return keyframe; + + return repeat; +} + +KisKeyframeBaseSP KisKeyframeChannel::previousItem(const KisKeyframeBase &item) const +{ + const KisKeyframeSP keyframe = previousKeyframe(item.time()); + + auto repeatIter = KisCollectionUtils::lastBefore(m_d->repeats, item.time()); + const auto repeat = (repeatIter != m_d->repeats.constEnd()) ? repeatIter.value() : QSharedPointer(); + if (keyframe && (!repeat || repeat->time() > keyframe->time())) return keyframe; + + return repeat; +} + +KisKeyframeBaseSP KisKeyframeChannel::nextItem(int time) const +{ + const KisKeyframeBaseSP activeItem = activeItemAt(time); + return activeItem ? nextItem(*activeItem) : nullptr; +} + +KisRangedKeyframeIterator KisKeyframeChannel::itemsWithin(KisTimeSpan range) const +{ + return KisRangedKeyframeIterator(this, range); +} + +KisVisibleKeyframeIterator KisKeyframeChannel::visibleKeyframesFrom(int time) const +{ + return KisVisibleKeyframeIterator(visibleKeyframeAt(time)); +} + +QList> KisKeyframeChannel::cycles() const +{ + return m_d->repeats.values(); +} + +KisTimeSpan KisKeyframeChannel::cycledRangeAt(int time) const +{ + QSharedPointer repeat = activeRepeatAt(time); + if (repeat) return repeat->sourceRange(); + + return KisTimeSpan(); +} + +QSharedPointer KisKeyframeChannel::activeRepeatAt(int time) const +{ + const auto repeat = KisCollectionUtils::lastBeforeOrAt(m_d->repeats, time); + if (repeat == m_d->repeats.constEnd()) return QSharedPointer(); + + const KisKeyframeSP lastKeyframe = activeKeyframeAt(time); + if (lastKeyframe && lastKeyframe->time() > repeat.value()->time()) return QSharedPointer(); + + return repeat.value(); +} + +void KisKeyframeChannel::activeKeyframeRange(int time, int *first, int *last) const +{ + *first = *last = -1; + + const KisKeyframeSP currentKeyframe = activeKeyframeAt(time); + if (currentKeyframe.isNull()) return; + + *first = currentKeyframe->time(); + + const KisKeyframeSP next = nextKeyframe(currentKeyframe); + if (!next.isNull()) { + *last = next->time() - 1; + } +} + int KisKeyframeChannel::framesHash() const { KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); int hash = 0; while (it != end) { hash += it.key(); ++it; } return hash; } QSet KisKeyframeChannel::allKeyframeIds() const { QSet frames; KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); while (it != end) { frames.insert(it.key()); ++it; } return frames; } -KisTimeRange KisKeyframeChannel::affectedFrames(int time) const +KisFrameSet KisKeyframeChannel::affectedFrames(int time) const { - if (m_d->keys.isEmpty()) return KisTimeRange::infinite(0); + if (m_d->keys.isEmpty()) return KisFrameSet::infiniteFrom(0); - KeyframesMap::const_iterator active = activeKeyIterator(time); + KeyframesMap::const_iterator active = KisCollectionUtils::lastBeforeOrAt(m_d->keys, time); KeyframesMap::const_iterator next; int from; if (active == m_d->keys.constEnd()) { // No active keyframe, ie. time is before the first keyframe from = 0; next = m_d->keys.constBegin(); } else { from = active.key(); next = active + 1; } + KisFrameSet frames; + + QSharedPointer activeRepeat = activeRepeatAt(time); + + if (activeRepeat) { + const KisKeyframeSP original = activeRepeat->getOriginalKeyframeFor(time); + return affectedFrames(original->time()); + } + if (next == m_d->keys.constEnd()) { - return KisTimeRange::infinite(from); + frames |= KisFrameSet::infiniteFrom(from); } else { - return KisTimeRange::fromTime(from, next.key() - 1); + frames |= KisFrameSet::between(from, next.key() - 1); } -} -KisTimeRange KisKeyframeChannel::identicalFrames(int time) const -{ - KeyframesMap::const_iterator active = activeKeyIterator(time); - - if (active != m_d->keys.constEnd() && (active+1) != m_d->keys.constEnd()) { - if (active->data()->interpolationMode() != KisKeyframe::Constant) { - return KisTimeRange::fromTime(time, time); + Q_FOREACH(QSharedPointer repeat, m_d->repeats) { + if (repeat->sourceRange().contains(time)) { + frames |= repeat->instancesWithin(active.value(), KisTimeSpan()); } } - return affectedFrames(time); + return frames; } -int KisKeyframeChannel::keyframeRowIndexOf(KisKeyframeSP keyframe) const +KisFrameSet KisKeyframeChannel::identicalFrames(int time, const KisTimeSpan range) const { - KeyframesMap::const_iterator it = m_d->keys.constBegin(); - KeyframesMap::const_iterator end = m_d->keys.constEnd(); - - int row = 0; - - for (; it != end; ++it) { - if (it.value().data() == keyframe) { - return row; - } - - row++; + const QSharedPointer activeRepeat = activeRepeatAt(time); + if (activeRepeat) { + const KisKeyframeSP original = activeRepeat->getOriginalKeyframeFor(time); + return identicalFrames(original->time(), range); } - return -1; -} + const KeyframesMap::const_iterator active = KisCollectionUtils::lastBeforeOrAt(m_d->keys, time); + const KisKeyframeBaseSP next = (active != m_d->keys.constEnd()) ? nextItem(*active.value()) : firstItem(); -KisKeyframeSP KisKeyframeChannel::keyframeAtRow(int row) const -{ - KeyframesMap::const_iterator it = m_d->keys.constBegin(); - KeyframesMap::const_iterator end = m_d->keys.constEnd(); + KisFrameSet frames; - for (; it != end; ++it) { - if (row <= 0) { - return it.value(); + if (active == m_d->keys.constEnd()) { + // No active keyframe, ie. time is before the first keyframe + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(next, KisFrameSet()); + frames = KisFrameSet::between(0, next->time()); + } else if (!next) { + frames = KisFrameSet::infiniteFrom((*active)->time()); + } else { + if (active->data()->interpolationMode() == KisKeyframe::Constant) { + frames = KisFrameSet::between(time, next->time() - 1); + } else { + frames = KisFrameSet::between(time, time); } + } - row--; + Q_FOREACH(QSharedPointer repeat, m_d->repeats) { + if (repeat->sourceRange().contains(time)) { + frames |= repeat->instancesWithin(active.value(), range); + } } - return KisKeyframeSP(); + return frames; } -int KisKeyframeChannel::keyframeInsertionRow(int time) const +bool KisKeyframeChannel::areFramesIdentical(int time1, int time2) const { - KeyframesMap::const_iterator it = m_d->keys.constBegin(); - KeyframesMap::const_iterator end = m_d->keys.constEnd(); - - int row = 0; - - for (; it != end; ++it) { - if (it.value()->time() > time) { - break; - } - row++; - } + const KisFrameSet identical = identicalFrames(time1, KisTimeSpan(time2, time2)); + return identical.contains(time2); +} - return row; +bool KisKeyframeChannel::isFrameAffectedBy(int targetFrame, int changedFrame) const +{ + const KisFrameSet affected = affectedFrames(changedFrame); + return affected.contains(targetFrame); } QDomElement KisKeyframeChannel::toXML(QDomDocument doc, const QString &layerFilename) { QDomElement channelElement = doc.createElement("channel"); channelElement.setAttribute("name", id()); Q_FOREACH (KisKeyframeSP keyframe, m_d->keys.values()) { QDomElement keyframeElement = doc.createElement("keyframe"); keyframeElement.setAttribute("time", keyframe->time()); keyframeElement.setAttribute("color-label", keyframe->colorLabel()); saveKeyframe(keyframe, keyframeElement, layerFilename); channelElement.appendChild(keyframeElement); } + Q_FOREACH(QSharedPointer repeat, m_d->repeats) { + QDomElement cycleElement = doc.createElement("repeat"); + cycleElement.setAttribute("time", repeat->time()); + cycleElement.setAttribute("rangeFrom", repeat->sourceRange().start()); + cycleElement.setAttribute("rangeTo", repeat->sourceRange().end()); + + channelElement.appendChild(cycleElement); + } + return channelElement; } void KisKeyframeChannel::loadXML(const QDomElement &channelNode) { - for (QDomElement keyframeNode = channelNode.firstChildElement(); !keyframeNode.isNull(); keyframeNode = keyframeNode.nextSiblingElement()) { - if (keyframeNode.nodeName().toUpper() != "KEYFRAME") continue; + for (QDomElement childNode = channelNode.firstChildElement(); !childNode.isNull(); childNode = childNode.nextSiblingElement()) { + const QString nodeName = childNode.nodeName().toUpper(); + if (nodeName == "KEYFRAME") { + const QString keyframeType = childNode.attribute("type", "").toUpper(); + + KisKeyframeSP keyframe; + keyframe = loadKeyframe(childNode); - KisKeyframeSP keyframe = loadKeyframe(keyframeNode); - KIS_SAFE_ASSERT_RECOVER(keyframe) { continue; } + KIS_SAFE_ASSERT_RECOVER(keyframe) { continue; } - if (keyframeNode.hasAttribute("color-label")) { - keyframe->setColorLabel(keyframeNode.attribute("color-label").toUInt()); + if (childNode.hasAttribute("color-label")) { + keyframe->setColorLabel(childNode.attribute("color-label").toUInt()); + } + + m_d->add(keyframe); } + } - m_d->keys.insert(keyframe->time(), keyframe); + for (QDomElement childNode = channelNode.firstChildElement(); !childNode.isNull(); childNode = childNode.nextSiblingElement()) { + if (childNode.nodeName().toUpper() == "REPEAT") { + QSharedPointer cycle = loadCycle(childNode); + if (cycle) { + m_d->add(cycle); + } + } } } +QSharedPointer KisKeyframeChannel::loadCycle(const QDomElement &cycleElement) +{ + const int time = cycleElement.attribute("time", "-1").toInt(); + const int startTime = cycleElement.attribute("rangeFrom", "-1").toInt(); + const int endTime = cycleElement.attribute("rangeTo", "-1").toInt(); + + if (startTime < 0 || endTime <= startTime) { + qWarning() << "Invalid cycle range: " << startTime << "to" << endTime; + return nullptr; + } + + QSharedPointer cycle = toQShared(new KisRepeatFrame(this, time, KisTimeSpan(startTime, endTime))); + + return cycle; +} + bool KisKeyframeChannel::swapExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand) { if (srcChannel->id() != id()) { warnKrita << "Cannot copy frames from different ids:" << ppVar(srcChannel->id()) << ppVar(id()); return KisKeyframeSP(); } LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); KisKeyframeSP srcFrame = srcChannel->keyframeAt(srcTime); KisKeyframeSP dstFrame = keyframeAt(dstTime); if (!dstFrame && srcFrame) { copyExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand); srcChannel->deleteKeyframe(srcFrame, parentCommand); } else if (dstFrame && !srcFrame) { srcChannel->copyExternalKeyframe(this, dstTime, srcTime, parentCommand); deleteKeyframe(dstFrame, parentCommand); } else if (dstFrame && srcFrame) { const int fakeFrameTime = -1; KisKeyframeSP newKeyframe = createKeyframe(fakeFrameTime, KisKeyframeSP(), parentCommand); uploadExternalKeyframe(srcChannel, srcTime, newKeyframe); srcChannel->copyExternalKeyframe(this, dstTime, srcTime, parentCommand); // do not recreate frame! deleteKeyframeImpl(dstFrame, parentCommand, false); newKeyframe->setTime(dstTime); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newKeyframe->time(), newKeyframe, parentCommand); cmd->redo(); } return true; } KisKeyframeSP KisKeyframeChannel::copyExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand) { if (srcChannel->id() != id()) { warnKrita << "Cannot copy frames from different ids:" << ppVar(srcChannel->id()) << ppVar(id()); return KisKeyframeSP(); } LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); KisKeyframeSP dstFrame = keyframeAt(dstTime); if (dstFrame) { deleteKeyframeImpl(dstFrame, parentCommand, false); } KisKeyframeSP newKeyframe = createKeyframe(dstTime, KisKeyframeSP(), parentCommand); uploadExternalKeyframe(srcChannel, srcTime, newKeyframe); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newKeyframe->time(), newKeyframe, parentCommand); + cmd->redo(); return newKeyframe; } -KisKeyframeChannel::KeyframesMap::const_iterator -KisKeyframeChannel::activeKeyIterator(int time) const +QSharedPointer KisKeyframeChannel::createRepeat(int time, KisTimeSpan sourceRange, KUndo2Command *parentCommand) { - KeyframesMap::const_iterator i = const_cast(&m_d->keys)->upperBound(time); + QSharedPointer repeatFrame = toQShared(new KisRepeatFrame(this, time, sourceRange)); + + KisKeyframeBaseSP oldItem = itemAt(time); + if (oldItem) { + deleteKeyframeImpl(oldItem, parentCommand, false); + } + + KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, time, repeatFrame, parentCommand); + cmd->redo(); - if (i == m_d->keys.constBegin()) return m_d->keys.constEnd(); - return --i; + return repeatFrame; } -void KisKeyframeChannel::requestUpdate(const KisTimeRange &range, const QRect &rect) +void KisKeyframeChannel::requestUpdate(const KisFrameSet &range, const QRect &rect) { if (m_d->node) { m_d->node->invalidateFrames(range, rect); int currentTime = m_d->defaultBounds->currentTime(); if (range.contains(currentTime)) { m_d->node->setDirty(rect); } } } void KisKeyframeChannel::workaroundBrokenFrameTimeBug(int *time) { /** * Between Krita 4.1 and 4.4 Krita had a bug which resulted in creating frames * with negative time stamp. The bug has been fixed, but there might be some files * still in the wild. * * TODO: remove this workaround in Krita 5.0, when no such file are left :) */ if (*time < 0) { qWarning() << "WARNING: Loading a file with negative animation frames!"; qWarning() << " The file has been saved with a buggy version of Krita."; qWarning() << " All the frames with negative ids will be dropped!"; qWarning() << " " << ppVar(this->id()) << ppVar(*time); m_d->haveBrokenFrameTimeBug = true; *time = 0; } if (m_d->haveBrokenFrameTimeBug) { while (keyframeAt(*time)) { (*time)++; } } } int KisKeyframeChannel::currentTime() const { return m_d->defaultBounds->currentTime(); } qreal KisKeyframeChannel::minScalarValue() const { return 0; } qreal KisKeyframeChannel::maxScalarValue() const { return 0; } qreal KisKeyframeChannel::scalarValue(const KisKeyframeSP keyframe) const { Q_UNUSED(keyframe); return 0; } void KisKeyframeChannel::setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand) { Q_UNUSED(keyframe); Q_UNUSED(value); Q_UNUSED(parentCommand); } + +KisKeyframeBaseSP firstKeyframeInRange(const KisKeyframeChannel *channel, KisTimeSpan range) +{ + KisKeyframeBaseSP active = channel->activeItemAt(range.start()); + + if (active) { + if (range.contains(active->time())) return active; + if (active->time() < range.start()) { + const KisKeyframeBaseSP next = channel->nextItem(*active); + if (next && range.contains(next->time())) return next; + } + } + + return KisKeyframeBaseSP(); +} + +KisRangedKeyframeIterator::KisRangedKeyframeIterator() +{} + +KisRangedKeyframeIterator::KisRangedKeyframeIterator(const KisKeyframeChannel *channel, KisKeyframeBaseSP keyframe, KisTimeSpan range) + : m_channel(channel) + , m_keyframe(keyframe) + , m_range(range) +{} + +KisRangedKeyframeIterator::KisRangedKeyframeIterator(const KisKeyframeChannel *channel, KisTimeSpan range) + : KisRangedKeyframeIterator(channel, firstKeyframeInRange(channel, range), range) {} + +KisRangedKeyframeIterator& KisRangedKeyframeIterator::operator++() +{ + if (!m_keyframe) return *this; + + m_keyframe = m_channel->nextItem(*m_keyframe); + if (!m_keyframe || !m_range.contains(m_keyframe->time())) { + m_keyframe = nullptr; + } + + return *this; +} + +KisRangedKeyframeIterator& KisRangedKeyframeIterator::operator--() +{ + if (!m_keyframe) { + // One-past-end state: return to last keyframe in range + const KisKeyframeBaseSP last = m_channel->activeItemAt(m_range.end()); + if (m_range.contains(last->time())) m_keyframe = last; + } else { + const KisKeyframeBaseSP previousKeyframe = m_channel->previousItem(*m_keyframe); + if (previousKeyframe && m_range.contains(previousKeyframe->time())) { + m_keyframe = previousKeyframe; + } + } + + return *this; +} + +KisKeyframeBaseSP KisRangedKeyframeIterator::operator*() const +{ + return m_keyframe; +} + +KisKeyframeBaseSP KisRangedKeyframeIterator::operator->() const +{ + return m_keyframe; +} + +KisRangedKeyframeIterator KisRangedKeyframeIterator::begin() const +{ + return KisRangedKeyframeIterator(m_channel, m_range); +} + +KisRangedKeyframeIterator KisRangedKeyframeIterator::end() const +{ + return KisRangedKeyframeIterator(m_channel, nullptr, m_range); +} + +bool KisRangedKeyframeIterator::isValid() const +{ + return m_keyframe != nullptr; +} + +bool KisRangedKeyframeIterator::operator==(const KisRangedKeyframeIterator &rhs) const +{ + return m_keyframe == rhs.m_keyframe && m_range == rhs.m_range; +} + +bool KisRangedKeyframeIterator::operator!=(const KisRangedKeyframeIterator &rhs) const +{ + return !(rhs == *this); +} + +KisVisibleKeyframeIterator::KisVisibleKeyframeIterator() = default; + +KisVisibleKeyframeIterator::KisVisibleKeyframeIterator(KisKeyframeSP keyframe) + : m_channel(keyframe->channel()) + , m_keyframe(keyframe) + , m_time(keyframe->time()) +{} + +KisVisibleKeyframeIterator& KisVisibleKeyframeIterator::operator--() +{ + const QSharedPointer repeat = m_channel->activeRepeatAt(m_time); + + if (repeat) { + const int time = repeat->previousVisibleFrame(m_time); + if (time >= 0) { + m_time = time; + return *this; + } + } + + m_keyframe = m_channel->previousKeyframe(m_keyframe->time()); + if (!m_keyframe) return invalidate(); + + m_time = m_keyframe->time(); + return *this; +} + +KisVisibleKeyframeIterator& KisVisibleKeyframeIterator::operator++() +{ + const QSharedPointer repeat = m_channel->activeRepeatAt(m_time); + + if (repeat) { + const int time = repeat->nextVisibleFrame(m_time); + if (time >= 0) { + m_time = time; + return *this; + } + } + + m_keyframe = m_channel->nextKeyframe(m_keyframe->time()); + if (!m_keyframe) return invalidate(); + + m_time = m_keyframe->time(); + + return *this; +}; + +KisKeyframeSP KisVisibleKeyframeIterator::operator*() const +{ + const KisRepeatFrame *repeat = dynamic_cast(m_keyframe.data()); + + if (repeat) { + return repeat->getOriginalKeyframeFor(m_time); + } + + return m_keyframe; +} + +KisKeyframeSP KisVisibleKeyframeIterator::operator->() const +{ + return operator*(); +} + +bool KisVisibleKeyframeIterator::isValid() const +{ + return m_channel && m_time >= 0; +} + +KisVisibleKeyframeIterator& KisVisibleKeyframeIterator::invalidate() +{ + m_channel = nullptr; + m_keyframe = KisKeyframeSP(); + m_time = -1; + + return *this; +} diff --git a/libs/image/kis_keyframe_channel.h b/libs/image/kis_keyframe_channel.h index 9170fd22a1..920e8e4b69 100644 --- a/libs/image/kis_keyframe_channel.h +++ b/libs/image/kis_keyframe_channel.h @@ -1,171 +1,258 @@ /* * 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_KEYFRAME_CHANNEL_H #define KIS_KEYFRAME_CHANNEL_H #include #include #include #include "kis_types.h" #include "KoID.h" #include "kritaimage_export.h" #include "kis_keyframe.h" #include "kis_default_bounds.h" +#include "kis_time_range.h" -class KisTimeRange; - +class KisFrameSet; +class KisTimeSpan; +class KisRepeatFrame; +class KisRangedKeyframeIterator; +class KisVisibleKeyframeIterator; +class KisDefineCycleCommand; class KRITAIMAGE_EXPORT KisKeyframeChannel : public QObject { Q_OBJECT public: // Standard Keyframe Ids static const KoID Content; static const KoID Opacity; static const KoID TransformArguments; static const KoID TransformPositionX; static const KoID TransformPositionY; static const KoID TransformScaleX; static const KoID TransformScaleY; static const KoID TransformShearX; static const KoID TransformShearY; static const KoID TransformRotationX; static const KoID TransformRotationY; static const KoID TransformRotationZ; public: KisKeyframeChannel(const KoID& id, KisDefaultBoundsBaseSP defaultBounds); KisKeyframeChannel(const KisKeyframeChannel &rhs, KisNode *newParentNode); ~KisKeyframeChannel() override; QString id() const; QString name() const; void setNode(KisNodeWSP node); KisNodeWSP node() const; KisKeyframeSP addKeyframe(int time, KUndo2Command *parentCommand = 0); - bool deleteKeyframe(KisKeyframeSP keyframe, KUndo2Command *parentCommand = 0); - bool moveKeyframe(KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0); + bool deleteKeyframe(KisKeyframeBaseSP keyframe, KUndo2Command *parentCommand = 0); + bool moveKeyframe(KisKeyframeBaseSP keyframe, int newTime, KUndo2Command *parentCommand = 0); bool swapFrames(int lhsTime, int rhsTime, KUndo2Command *parentCommand = 0); - KisKeyframeSP copyKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0); + KisKeyframeBaseSP copyItem(const KisKeyframeBaseSP item, int newTime, KUndo2Command *parentCommand = 0); + KisKeyframeSP copyAsKeyframe(const KisKeyframeBaseSP item, int originalTime, int newTime, KUndo2Command *parentCommand = 0); + virtual KisKeyframeSP linkKeyframe(const KisKeyframeBaseSP keyframe, int newTime, KUndo2Command *parentCommand = 0); KisKeyframeSP copyExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand = 0); + QSharedPointer createRepeat(int time, KisTimeSpan sourceRange, KUndo2Command *parentCommand); + bool swapExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand = 0); KisKeyframeSP keyframeAt(int time) const; KisKeyframeSP activeKeyframeAt(int time) const; + KisKeyframeSP visibleKeyframeAt(int time) const; KisKeyframeSP currentlyActiveKeyframe() const; KisKeyframeSP firstKeyframe() const; KisKeyframeSP nextKeyframe(KisKeyframeSP keyframe) const; KisKeyframeSP previousKeyframe(KisKeyframeSP keyframe) const; + KisKeyframeSP nextKeyframe(int time) const; + KisKeyframeSP previousKeyframe(int time) const; KisKeyframeSP lastKeyframe() const; + KisKeyframeBaseSP firstItem() const; + KisKeyframeBaseSP itemAt(int time) const; + KisKeyframeBaseSP activeItemAt(int time) const; + KisKeyframeBaseSP nextItem(const KisKeyframeBase &item) const; + KisKeyframeBaseSP previousItem(const KisKeyframeBase &item) const; + KisRangedKeyframeIterator itemsWithin(KisTimeSpan range) const; + KisKeyframeBaseSP nextItem(int time) const; + + KisVisibleKeyframeIterator visibleKeyframesFrom(int time) const; + + QList> cycles() const; + + /** + * Finds the original range of the cycle defined or repeated at the given time. + * @arg time a time at any frame within the original cycle or any repeat of it. + */ + KisTimeSpan cycledRangeAt(int time) const; + + /** + * Finds the repeat of a cycle at the time, if any. + */ + QSharedPointer activeRepeatAt(int time) const; + + /** + * Finds the span of time of the keyframe active at given time. + * If there is no active keyframe, first will be -1. + * If the keyframe continues indefinitely, last will be -1. + */ + void activeKeyframeRange(int time, int *first, int *last) const; + /** * Calculates a pseudo-unique keyframes hash. The hash changes * every time any frame is added/removed/moved */ int framesHash() const; QSet allKeyframeIds() const; /** * Get the set of frames affected by any changes to the value * of the active keyframe at the given time. */ - KisTimeRange affectedFrames(int time) const; + virtual KisFrameSet affectedFrames(int time) const; /** * Get a set of frames for which the channel gives identical * results, compared to the given frame. * * Note: this set may be different than the set of affected frames * due to interpolation. */ - KisTimeRange identicalFrames(int time) const; + virtual KisFrameSet identicalFrames(int time, const KisTimeSpan range) const; + virtual bool areFramesIdentical(int time1, int time2) const; + virtual bool isFrameAffectedBy(int targetFrame, int changedFrame) const; int keyframeCount() const; - int keyframeRowIndexOf(KisKeyframeSP keyframe) const; - KisKeyframeSP keyframeAtRow(int row) const; - - int keyframeInsertionRow(int time) const; - virtual bool hasScalarValue() const = 0; virtual qreal minScalarValue() const; virtual qreal maxScalarValue() const; virtual qreal scalarValue(const KisKeyframeSP keyframe) const; virtual void setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand = 0); virtual QDomElement toXML(QDomDocument doc, const QString &layerFilename); virtual void loadXML(const QDomElement &channelNode); int currentTime() const; Q_SIGNALS: - void sigKeyframeAboutToBeAdded(KisKeyframeSP keyframe); - void sigKeyframeAdded(KisKeyframeSP keyframe); - void sigKeyframeAboutToBeRemoved(KisKeyframeSP keyframe); - void sigKeyframeRemoved(KisKeyframeSP keyframe); - void sigKeyframeAboutToBeMoved(KisKeyframeSP keyframe, int toTime); - void sigKeyframeMoved(KisKeyframeSP keyframe, int fromTime); - void sigKeyframeChanged(KisKeyframeSP keyframe); + void sigKeyframeAboutToBeAdded(KisKeyframeBaseSP keyframe); + void sigKeyframeAdded(KisKeyframeBaseSP keyframe); + void sigKeyframeAboutToBeRemoved(KisKeyframeBaseSP keyframe); + void sigKeyframeRemoved(KisKeyframeBaseSP keyframe); + void sigKeyframeAboutToBeMoved(KisKeyframeBaseSP keyframe, int toTime); + void sigKeyframeMoved(KisKeyframeBaseSP keyframe, int fromTime); + void sigKeyframeChanged(KisKeyframeBaseSP keyframe); protected: typedef QMap KeyframesMap; KeyframesMap &keys(); const KeyframesMap &constKeys() const; - KeyframesMap::const_iterator activeKeyIterator(int time) const; virtual KisKeyframeSP createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) = 0; virtual void destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) = 0; virtual void uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) = 0; - virtual QRect affectedRect(KisKeyframeSP key) = 0; - virtual void requestUpdate(const KisTimeRange &range, const QRect &rect); + virtual void requestUpdate(const KisFrameSet &range, const QRect &rect); virtual KisKeyframeSP loadKeyframe(const QDomElement &keyframeNode) = 0; virtual void saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) = 0; void workaroundBrokenFrameTimeBug(int *time); private: - KisKeyframeSP replaceKeyframeAt(int time, KisKeyframeSP newKeyframe); - void insertKeyframeLogical(KisKeyframeSP keyframe); - void removeKeyframeLogical(KisKeyframeSP keyframe); - bool deleteKeyframeImpl(KisKeyframeSP keyframe, KUndo2Command *parentCommand, bool recreate); - void moveKeyframeImpl(KisKeyframeSP keyframe, int newTime); - void swapKeyframesImpl(KisKeyframeSP lhsKeyframe, KisKeyframeSP rhsKeyframe); - - friend class KisMoveFrameCommand; + KisKeyframeBaseSP replaceKeyframeAt(int time, KisKeyframeBaseSP newKeyframe); + void insertKeyframeLogical(KisKeyframeBaseSP keyframe); + void removeKeyframeLogical(KisKeyframeBaseSP keyframe); + bool deleteKeyframeImpl(KisKeyframeBaseSP keyframe, KUndo2Command *parentCommand, bool recreate); + void moveKeyframeImpl(KisKeyframeBaseSP keyframe, int newTime); + void swapKeyframesImpl(KisKeyframeBaseSP lhsKeyframe, KisKeyframeBaseSP rhsKeyframe); + friend class KisReplaceKeyframeCommand; friend class KisSwapFramesCommand; + friend class KisRangedKeyframeIterator; private: - KisKeyframeSP insertKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand); + KisKeyframeSP insertKeyframe(int time, const KisKeyframeBaseSP copySrc, KUndo2Command *parentCommand); + QSharedPointer loadCycle(const QDomElement &cycleElement); struct Private; QScopedPointer m_d; }; +class KisRangedKeyframeIterator +{ +public: + KisRangedKeyframeIterator(); + KisRangedKeyframeIterator(const KisKeyframeChannel *channel, KisTimeSpan range); + + KisRangedKeyframeIterator& operator--(); + KisRangedKeyframeIterator& operator++(); + + KisKeyframeBaseSP operator->() const; + KisKeyframeBaseSP operator*() const; + + bool operator==(const KisRangedKeyframeIterator &rhs) const; + bool operator!=(const KisRangedKeyframeIterator &rhs) const; + + KisRangedKeyframeIterator begin() const; + KisRangedKeyframeIterator end() const; + + bool isValid() const; + +private: + KisRangedKeyframeIterator(const KisKeyframeChannel *channel, KisKeyframeBaseSP keyframe, KisTimeSpan range); + + const KisKeyframeChannel *m_channel{nullptr}; + KisKeyframeBaseSP m_keyframe; + KisTimeSpan m_range; +}; + +class KisVisibleKeyframeIterator +{ +public: + KisVisibleKeyframeIterator(); + explicit KisVisibleKeyframeIterator(KisKeyframeSP keyframe); + + KisVisibleKeyframeIterator& operator--(); + KisVisibleKeyframeIterator& operator++(); + + bool isValid() const; + KisKeyframeSP operator->() const; + KisKeyframeSP operator*() const; + +private: + KisVisibleKeyframeIterator &invalidate(); + + KisKeyframeChannel *m_channel{nullptr}; + KisKeyframeSP m_keyframe; + int m_time{-1}; +}; + #endif // KIS_KEYFRAME_CHANNEL_H diff --git a/libs/image/kis_keyframe_commands.cpp b/libs/image/kis_keyframe_commands.cpp index 3030d70392..34a666cfc3 100644 --- a/libs/image/kis_keyframe_commands.cpp +++ b/libs/image/kis_keyframe_commands.cpp @@ -1,55 +1,80 @@ #include "kis_keyframe_commands.h" -KisReplaceKeyframeCommand::KisReplaceKeyframeCommand(KisKeyframeChannel *channel, int time, KisKeyframeSP keyframe, KUndo2Command *parentCommand) - : KUndo2Command(parentCommand), - m_channel(channel), - m_time(time), - m_keyframe(keyframe), - m_existingKeyframe(0) -{ -} +#include +#include +#include +#include "kis_time_range.h" + +KisReplaceKeyframeCommand::KisReplaceKeyframeCommand(KisKeyframeChannel *channel, int time, KisKeyframeBaseSP keyframe, + KUndo2Command *parentCommand) + : KUndo2Command(parentCommand) + , m_channel(channel) + , m_keyframe(keyframe) + , m_newTime(time) +{} void KisReplaceKeyframeCommand::redo() { - m_existingKeyframe = m_channel->replaceKeyframeAt(m_time, m_keyframe); + if (m_newTime >= 0) { + m_overwrittenKeyframe = m_channel->itemAt(m_newTime); + + if (m_overwrittenKeyframe) { + m_channel->removeKeyframeLogical(m_overwrittenKeyframe); + } + } + + if (m_keyframe) { + const bool currentlyOnChannel = m_channel->itemAt(m_keyframe->time()) == m_keyframe; + m_oldTime = currentlyOnChannel ? m_keyframe->time() : -1; + } + + moveKeyframeTo(m_newTime); } void KisReplaceKeyframeCommand::undo() { - m_channel->replaceKeyframeAt(m_time, m_existingKeyframe); -} + moveKeyframeTo(m_oldTime); + if (m_overwrittenKeyframe) { + m_overwrittenKeyframe->setTime(m_newTime); + m_channel->insertKeyframeLogical(m_overwrittenKeyframe); + m_overwrittenKeyframe = nullptr; + } +} -KisMoveFrameCommand::KisMoveFrameCommand(KisKeyframeChannel *channel, KisKeyframeSP keyframe, int oldTime, int newTime, KUndo2Command *parentCommand) - : KUndo2Command(parentCommand), - m_channel(channel), - m_keyframe(keyframe), - m_oldTime(oldTime), - m_newTime(newTime) +void KisReplaceKeyframeCommand::moveKeyframeTo(int dstTime) { -} + if (!m_keyframe) return; -void KisMoveFrameCommand::redo() { - m_channel->moveKeyframeImpl(m_keyframe, m_newTime); -} + const bool currentlyOnChannel = m_channel->itemAt(m_keyframe->time()) == m_keyframe; -void KisMoveFrameCommand::undo() { - m_channel->moveKeyframeImpl(m_keyframe, m_oldTime); + if (dstTime < 0) { + if (currentlyOnChannel) { + m_channel->removeKeyframeLogical(m_keyframe); + } + } else { + if (currentlyOnChannel) { + m_channel->moveKeyframeImpl(m_keyframe, dstTime); + } else { + m_keyframe->setTime(dstTime); + m_channel->insertKeyframeLogical(m_keyframe); + } + } } -KisSwapFramesCommand::KisSwapFramesCommand(KisKeyframeChannel *channel, KisKeyframeSP lhsFrame, KisKeyframeSP rhsFrame, KUndo2Command *parentCommand) +KisSwapFramesCommand::KisSwapFramesCommand(KisKeyframeChannel *channel, KisKeyframeBaseSP lhsFrame, KisKeyframeBaseSP rhsFrame, KUndo2Command *parentCommand) : KUndo2Command(parentCommand), m_channel(channel), m_lhsFrame(lhsFrame), m_rhsFrame(rhsFrame) { } void KisSwapFramesCommand::redo() { m_channel->swapKeyframesImpl(m_lhsFrame, m_rhsFrame); } void KisSwapFramesCommand::undo() { m_channel->swapKeyframesImpl(m_lhsFrame, m_rhsFrame); } diff --git a/libs/image/kis_keyframe_commands.h b/libs/image/kis_keyframe_commands.h index b8d4b59144..4416a8ba58 100644 --- a/libs/image/kis_keyframe_commands.h +++ b/libs/image/kis_keyframe_commands.h @@ -1,71 +1,62 @@ /* * 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_KEYFRAME_COMMANDS_H #define KIS_KEYFRAME_COMMANDS_H #include "kis_keyframe_channel.h" #include "kundo2command.h" #include "kritaimage_export.h" +/** + * Places the keyframe on its channel at the specified time. + * If the time is negative, the keyframe is removed from the channel. + * Otherwise, the keyframe is moved within or inserted onto the channel. + * Any overwritten keyframe will be restored on undo(). + */ class KRITAIMAGE_EXPORT KisReplaceKeyframeCommand : public KUndo2Command { public: - KisReplaceKeyframeCommand(KisKeyframeChannel *channel, int time, KisKeyframeSP keyframe, KUndo2Command *parentCommand); - - void redo() override; - void undo() override; - -private: - void doSwap(bool insert); - -private: - KisKeyframeChannel *m_channel; - int m_time; - KisKeyframeSP m_keyframe; - KisKeyframeSP m_existingKeyframe; -}; - -class KRITAIMAGE_EXPORT KisMoveFrameCommand : public KUndo2Command -{ -public: - KisMoveFrameCommand(KisKeyframeChannel *channel, KisKeyframeSP keyframe, int oldTime, int newTime, KUndo2Command *parentCommand); + KisReplaceKeyframeCommand(KisKeyframeChannel *channel, int time, KisKeyframeBaseSP keyframe, KUndo2Command *parentCommand); void redo() override; void undo() override; private: KisKeyframeChannel *m_channel; - KisKeyframeSP m_keyframe; - int m_oldTime; + KisKeyframeBaseSP m_keyframe; + KisKeyframeBaseSP m_overwrittenKeyframe; + int m_oldTime{-1}; int m_newTime; + + void moveKeyframeTo(int dstTime); }; class KRITAIMAGE_EXPORT KisSwapFramesCommand : public KUndo2Command { public: - KisSwapFramesCommand(KisKeyframeChannel *channel, KisKeyframeSP lhsFrame, KisKeyframeSP rhsFrame, KUndo2Command *parentCommand); + KisSwapFramesCommand(KisKeyframeChannel *channel, KisKeyframeBaseSP lhsFrame, KisKeyframeBaseSP rhsFrame, KUndo2Command *parentCommand); void redo() override; void undo() override; private: KisKeyframeChannel *m_channel; - KisKeyframeSP m_lhsFrame; - KisKeyframeSP m_rhsFrame; + KisKeyframeBaseSP m_lhsFrame; + KisKeyframeBaseSP m_rhsFrame; }; #endif diff --git a/libs/image/kis_layer.cc b/libs/image/kis_layer.cc index 1d573758cc..5eed2f296a 100644 --- a/libs/image/kis_layer.cc +++ b/libs/image/kis_layer.cc @@ -1,991 +1,991 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2005 C. Boemann * Copyright (c) 2009 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_layer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_debug.h" #include "kis_image.h" #include "kis_painter.h" #include "kis_mask.h" #include "kis_effect_mask.h" #include "kis_selection_mask.h" #include "kis_meta_data_store.h" #include "kis_selection.h" #include "kis_paint_layer.h" #include "kis_raster_keyframe_channel.h" #include "kis_clone_layer.h" #include "kis_psd_layer_style.h" #include "kis_layer_projection_plane.h" #include "layerstyles/kis_layer_style_projection_plane.h" #include "krita_utils.h" #include "kis_layer_properties_icons.h" #include "kis_layer_utils.h" #include "kis_projection_leaf.h" #include "KisSafeNodeProjectionStore.h" class KisCloneLayersList { public: void addClone(KisCloneLayerWSP cloneLayer) { m_clonesList.append(cloneLayer); } void removeClone(KisCloneLayerWSP cloneLayer) { m_clonesList.removeOne(cloneLayer); } void setDirty(const QRect &rect) { Q_FOREACH (KisCloneLayerSP clone, m_clonesList) { if (clone) { clone->setDirtyOriginal(rect); } } } const QList registeredClones() const { return m_clonesList; } bool hasClones() const { return !m_clonesList.isEmpty(); } private: QList m_clonesList; }; class KisLayerMasksCache { public: KisLayerMasksCache(KisLayer *parent) : m_parent(parent) { } KisSelectionMaskSP selectionMask() { QReadLocker readLock(&m_lock); if (!m_isSelectionMaskValid) { readLock.unlock(); QWriteLocker writeLock(&m_lock); if (!m_isSelectionMaskValid) { KoProperties properties; properties.setProperty("active", true); properties.setProperty("visible", true); QList masks = m_parent->childNodes(QStringList("KisSelectionMask"), properties); // return the first visible mask Q_FOREACH (KisNodeSP mask, masks) { if (mask) { m_selectionMask = dynamic_cast(mask.data()); break; } } m_isSelectionMaskValid = true; } // return under write lock return m_selectionMask; } // return under read lock return m_selectionMask; } QList effectMasks() { QReadLocker readLock(&m_lock); if (!m_isEffectMasksValid) { readLock.unlock(); QWriteLocker writeLock(&m_lock); if (!m_isEffectMasksValid) { m_effectMasks = m_parent->searchEffectMasks(0); m_isEffectMasksValid = true; } // return under write lock return m_effectMasks; } // return under read lock return m_effectMasks; } void setDirty() { QWriteLocker l(&m_lock); m_isSelectionMaskValid = false; m_isEffectMasksValid = false; m_selectionMask = 0; m_effectMasks.clear(); } private: KisLayer *m_parent; QReadWriteLock m_lock; bool m_isSelectionMaskValid = false; bool m_isEffectMasksValid = false; KisSelectionMaskSP m_selectionMask; QList m_effectMasks; }; struct Q_DECL_HIDDEN KisLayer::Private { Private(KisLayer *q) : masksCache(q) { } QBitArray channelFlags; KisMetaData::Store* metaDataStore; KisCloneLayersList clonesList; KisPSDLayerStyleSP layerStyle; KisLayerStyleProjectionPlaneSP layerStyleProjectionPlane; KisLayerProjectionPlaneSP projectionPlane; KisSafeNodeProjectionStoreSP safeProjection; KisLayerMasksCache masksCache; }; KisLayer::KisLayer(KisImageWSP image, const QString &name, quint8 opacity) : KisNode(image) , m_d(new Private(this)) { setName(name); setOpacity(opacity); m_d->metaDataStore = new KisMetaData::Store(); m_d->projectionPlane = toQShared(new KisLayerProjectionPlane(this)); m_d->safeProjection = new KisSafeNodeProjectionStore(); m_d->safeProjection->setImage(image); } KisLayer::KisLayer(const KisLayer& rhs) : KisNode(rhs) , m_d(new Private(this)) { if (this != &rhs) { m_d->metaDataStore = new KisMetaData::Store(*rhs.m_d->metaDataStore); m_d->channelFlags = rhs.m_d->channelFlags; setName(rhs.name()); m_d->projectionPlane = toQShared(new KisLayerProjectionPlane(this)); m_d->safeProjection = new KisSafeNodeProjectionStore(*rhs.m_d->safeProjection); m_d->safeProjection->setImage(image()); if (rhs.m_d->layerStyle) { m_d->layerStyle = rhs.m_d->layerStyle->clone(); if (rhs.m_d->layerStyleProjectionPlane) { m_d->layerStyleProjectionPlane = toQShared( new KisLayerStyleProjectionPlane(*rhs.m_d->layerStyleProjectionPlane, this, m_d->layerStyle)); } } } } KisLayer::~KisLayer() { delete m_d->metaDataStore; delete m_d; } const KoColorSpace * KisLayer::colorSpace() const { KisImageSP image = this->image(); if (!image) { return nullptr; } return image->colorSpace(); } const KoCompositeOp * KisLayer::compositeOp() const { /** * FIXME: This function duplicates the same function from * KisMask. We can't move it to KisBaseNode as it doesn't * know anything about parent() method of KisNode * Please think it over... */ KisNodeSP parentNode = parent(); if (!parentNode) return 0; if (!parentNode->colorSpace()) return 0; const KoCompositeOp* op = parentNode->colorSpace()->compositeOp(compositeOpId()); return op ? op : parentNode->colorSpace()->compositeOp(COMPOSITE_OVER); } KisPSDLayerStyleSP KisLayer::layerStyle() const { return m_d->layerStyle; } void KisLayer::setLayerStyle(KisPSDLayerStyleSP layerStyle) { if (layerStyle) { m_d->layerStyle = layerStyle; KisLayerStyleProjectionPlaneSP plane = !layerStyle->isEmpty() ? KisLayerStyleProjectionPlaneSP(new KisLayerStyleProjectionPlane(this)) : KisLayerStyleProjectionPlaneSP(0); m_d->layerStyleProjectionPlane = plane; } else { m_d->layerStyleProjectionPlane.clear(); m_d->layerStyle.clear(); } } KisBaseNode::PropertyList KisLayer::sectionModelProperties() const { KisBaseNode::PropertyList l = KisBaseNode::sectionModelProperties(); l << KisBaseNode::Property(KoID("opacity", i18n("Opacity")), i18n("%1%", percentOpacity())); const KoCompositeOp * compositeOp = this->compositeOp(); if (compositeOp) { l << KisBaseNode::Property(KoID("compositeop", i18n("Blending Mode")), compositeOp->description()); } if (m_d->layerStyle && !m_d->layerStyle->isEmpty()) { l << KisLayerPropertiesIcons::getProperty(KisLayerPropertiesIcons::layerStyle, m_d->layerStyle->isEnabled()); } l << KisLayerPropertiesIcons::getProperty(KisLayerPropertiesIcons::inheritAlpha, alphaChannelDisabled()); return l; } void KisLayer::setSectionModelProperties(const KisBaseNode::PropertyList &properties) { KisBaseNode::setSectionModelProperties(properties); Q_FOREACH (const KisBaseNode::Property &property, properties) { if (property.id == KisLayerPropertiesIcons::inheritAlpha.id()) { disableAlphaChannel(property.state.toBool()); } if (property.id == KisLayerPropertiesIcons::layerStyle.id()) { if (m_d->layerStyle && m_d->layerStyle->isEnabled() != property.state.toBool()) { m_d->layerStyle->setEnabled(property.state.toBool()); baseNodeChangedCallback(); baseNodeInvalidateAllFramesCallback(); } } } } void KisLayer::disableAlphaChannel(bool disable) { QBitArray newChannelFlags = m_d->channelFlags; if(newChannelFlags.isEmpty()) newChannelFlags = colorSpace()->channelFlags(true, true); if(disable) newChannelFlags &= colorSpace()->channelFlags(true, false); else newChannelFlags |= colorSpace()->channelFlags(false, true); setChannelFlags(newChannelFlags); } bool KisLayer::alphaChannelDisabled() const { QBitArray flags = colorSpace()->channelFlags(false, true) & m_d->channelFlags; return flags.count(true) == 0 && !m_d->channelFlags.isEmpty(); } void KisLayer::setChannelFlags(const QBitArray & channelFlags) { Q_ASSERT(channelFlags.isEmpty() ||((quint32)channelFlags.count() == colorSpace()->channelCount())); if (KritaUtils::compareChannelFlags(channelFlags, this->channelFlags())) { return; } if (!channelFlags.isEmpty() && channelFlags == QBitArray(channelFlags.size(), true)) { m_d->channelFlags.clear(); } else { m_d->channelFlags = channelFlags; } baseNodeChangedCallback(); baseNodeInvalidateAllFramesCallback(); } QBitArray & KisLayer::channelFlags() const { return m_d->channelFlags; } bool KisLayer::temporary() const { return nodeProperties().boolProperty("temporary", false); } void KisLayer::setTemporary(bool t) { setNodeProperty("temporary", t); } void KisLayer::setImage(KisImageWSP image) { // we own the projection device, so we should take care about it KisPaintDeviceSP projection = this->projection(); if (projection && projection != original()) { projection->setDefaultBounds(new KisDefaultBounds(image)); } m_d->safeProjection->setImage(image); KisNode::setImage(image); } bool KisLayer::canMergeAndKeepBlendOptions(KisLayerSP otherLayer) { return this->compositeOpId() == otherLayer->compositeOpId() && this->opacity() == otherLayer->opacity() && this->channelFlags() == otherLayer->channelFlags() && !this->layerStyle() && !otherLayer->layerStyle() && (this->colorSpace() == otherLayer->colorSpace() || *this->colorSpace() == *otherLayer->colorSpace()); } KisLayerSP KisLayer::createMergedLayerTemplate(KisLayerSP prevLayer) { const bool keepBlendingOptions = canMergeAndKeepBlendOptions(prevLayer); KisLayerSP newLayer = new KisPaintLayer(image(), prevLayer->name(), OPACITY_OPAQUE_U8); if (keepBlendingOptions) { newLayer->setCompositeOpId(compositeOpId()); newLayer->setOpacity(opacity()); newLayer->setChannelFlags(channelFlags()); } return newLayer; } void KisLayer::fillMergedLayerTemplate(KisLayerSP dstLayer, KisLayerSP prevLayer) { const bool keepBlendingOptions = canMergeAndKeepBlendOptions(prevLayer); QRect layerProjectionExtent = this->projection()->extent(); QRect prevLayerProjectionExtent = prevLayer->projection()->extent(); bool alphaDisabled = this->alphaChannelDisabled(); bool prevAlphaDisabled = prevLayer->alphaChannelDisabled(); KisPaintDeviceSP mergedDevice = dstLayer->paintDevice(); if (!keepBlendingOptions) { KisPainter gc(mergedDevice); KisImageSP imageSP = image().toStrongRef(); if (!imageSP) { return; } //Copy the pixels of previous layer with their actual alpha value prevLayer->disableAlphaChannel(false); prevLayer->projectionPlane()->apply(&gc, prevLayerProjectionExtent | imageSP->bounds()); //Restore the previous prevLayer disableAlpha status for correct undo/redo prevLayer->disableAlphaChannel(prevAlphaDisabled); //Paint the pixels of the current layer, using their actual alpha value if (alphaDisabled == prevAlphaDisabled) { this->disableAlphaChannel(false); } this->projectionPlane()->apply(&gc, layerProjectionExtent | imageSP->bounds()); //Restore the layer disableAlpha status for correct undo/redo this->disableAlphaChannel(alphaDisabled); } else { //Copy prevLayer KisPaintDeviceSP srcDev = prevLayer->projection(); mergedDevice->makeCloneFrom(srcDev, srcDev->extent()); //Paint layer on the copy KisPainter gc(mergedDevice); gc.bitBlt(layerProjectionExtent.topLeft(), this->projection(), layerProjectionExtent); } } void KisLayer::registerClone(KisCloneLayerWSP clone) { m_d->clonesList.addClone(clone); } void KisLayer::unregisterClone(KisCloneLayerWSP clone) { m_d->clonesList.removeClone(clone); } const QList KisLayer::registeredClones() const { return m_d->clonesList.registeredClones(); } bool KisLayer::hasClones() const { return m_d->clonesList.hasClones(); } void KisLayer::updateClones(const QRect &rect) { m_d->clonesList.setDirty(rect); } void KisLayer::notifyChildMaskChanged() { m_d->masksCache.setDirty(); } KisSelectionMaskSP KisLayer::selectionMask() const { return m_d->masksCache.selectionMask(); } KisSelectionSP KisLayer::selection() const { KisSelectionMaskSP mask = selectionMask(); if (mask) { return mask->selection(); } KisImageSP image = this->image(); if (image) { return image->globalSelection(); } return KisSelectionSP(); } /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// QList KisLayer::effectMasks() const { return m_d->masksCache.effectMasks(); } QList KisLayer::effectMasks(KisNodeSP lastNode) const { if (lastNode.isNull()) { return effectMasks(); } else { // happens rarely. return searchEffectMasks(lastNode); } } QList KisLayer::searchEffectMasks(KisNodeSP lastNode) const { QList masks; KIS_SAFE_ASSERT_RECOVER_NOOP(projectionLeaf()); KisProjectionLeafSP child = projectionLeaf()->firstChild(); while (child) { if (child->node() == lastNode) break; KIS_SAFE_ASSERT_RECOVER_NOOP(child); KIS_SAFE_ASSERT_RECOVER_NOOP(child->node()); if (child->visible()) { KisEffectMaskSP mask = dynamic_cast(const_cast(child->node().data())); if (mask) { masks.append(mask); } } child = child->nextSibling(); } return masks; } bool KisLayer::hasEffectMasks() const { return !m_d->masksCache.effectMasks().isEmpty(); } QRect KisLayer::masksChangeRect(const QList &masks, const QRect &requestedRect, bool &rectVariesFlag) const { rectVariesFlag = false; QRect prevChangeRect = requestedRect; /** * We set default value of the change rect for the case * when there is no mask at all */ QRect changeRect = requestedRect; Q_FOREACH (const KisEffectMaskSP& mask, masks) { changeRect = mask->changeRect(prevChangeRect); if (changeRect != prevChangeRect) rectVariesFlag = true; prevChangeRect = changeRect; } return changeRect; } QRect KisLayer::masksNeedRect(const QList &masks, const QRect &changeRect, QStack &applyRects, bool &rectVariesFlag) const { rectVariesFlag = false; QRect prevNeedRect = changeRect; QRect needRect; for (qint32 i = masks.size() - 1; i >= 0; i--) { applyRects.push(prevNeedRect); needRect = masks[i]->needRect(prevNeedRect); if (prevNeedRect != needRect) rectVariesFlag = true; prevNeedRect = needRect; } return needRect; } KisNode::PositionToFilthy calculatePositionToFilthy(KisNodeSP nodeInQuestion, KisNodeSP filthy, KisNodeSP parent) { if (parent == filthy || parent != filthy->parent()) { return KisNode::N_ABOVE_FILTHY; } if (nodeInQuestion == filthy) { return KisNode::N_FILTHY; } KisNodeSP node = nodeInQuestion->prevSibling(); while (node) { if (node == filthy) { return KisNode::N_ABOVE_FILTHY; } node = node->prevSibling(); } return KisNode::N_BELOW_FILTHY; } QRect KisLayer::applyMasks(const KisPaintDeviceSP source, KisPaintDeviceSP destination, const QRect &requestedRect, KisNodeSP filthyNode, KisNodeSP lastNode) const { Q_ASSERT(source); Q_ASSERT(destination); QList masks = effectMasks(lastNode); QRect changeRect; QRect needRect; if (masks.isEmpty()) { changeRect = requestedRect; if (source != destination) { copyOriginalToProjection(source, destination, requestedRect); } } else { QStack applyRects; bool changeRectVaries; bool needRectVaries; /** * FIXME: Assume that varying of the changeRect has already * been taken into account while preparing walkers */ changeRectVaries = false; changeRect = requestedRect; //changeRect = masksChangeRect(masks, requestedRect, // changeRectVaries); needRect = masksNeedRect(masks, changeRect, applyRects, needRectVaries); if (!changeRectVaries && !needRectVaries) { /** * A bit of optimization: * All filters will read/write exactly from/to the requested * rect so we needn't create temporary paint device, * just apply it onto destination */ Q_ASSERT(needRect == requestedRect); if (source != destination) { copyOriginalToProjection(source, destination, needRect); } Q_FOREACH (const KisEffectMaskSP& mask, masks) { const QRect maskApplyRect = applyRects.pop(); const QRect maskNeedRect = applyRects.isEmpty() ? needRect : applyRects.top(); PositionToFilthy maskPosition = calculatePositionToFilthy(mask, filthyNode, const_cast(this)); mask->apply(destination, maskApplyRect, maskNeedRect, maskPosition); } Q_ASSERT(applyRects.isEmpty()); } else { /** * We can't eliminate additional copy-op * as filters' behaviour may be quite insane here, * so let them work on their own paintDevice =) */ KisPaintDeviceSP tempDevice = new KisPaintDevice(colorSpace()); tempDevice->prepareClone(source); copyOriginalToProjection(source, tempDevice, needRect); QRect maskApplyRect = applyRects.pop(); QRect maskNeedRect = needRect; Q_FOREACH (const KisEffectMaskSP& mask, masks) { PositionToFilthy maskPosition = calculatePositionToFilthy(mask, filthyNode, const_cast(this)); mask->apply(tempDevice, maskApplyRect, maskNeedRect, maskPosition); if (!applyRects.isEmpty()) { maskNeedRect = maskApplyRect; maskApplyRect = applyRects.pop(); } } Q_ASSERT(applyRects.isEmpty()); KisPainter::copyAreaOptimized(changeRect.topLeft(), tempDevice, destination, changeRect); } } return changeRect; } QRect KisLayer::updateProjection(const QRect& rect, KisNodeSP filthyNode) { QRect updatedRect = rect; KisPaintDeviceSP originalDevice = original(); if (!rect.isValid() || (!visible() && !hasClones()) || !originalDevice) return QRect(); if (!needProjection() && !hasEffectMasks()) { m_d->safeProjection->releaseDevice(); } else { if (!updatedRect.isEmpty()) { KisPaintDeviceSP projection = m_d->safeProjection->getDeviceLazy(originalDevice); updatedRect = applyMasks(originalDevice, projection, updatedRect, filthyNode, 0); } } return updatedRect; } QRect KisLayer::partialChangeRect(KisNodeSP lastNode, const QRect& rect) { bool changeRectVaries = false; QRect changeRect = outgoingChangeRect(rect); changeRect = masksChangeRect(effectMasks(lastNode), changeRect, changeRectVaries); return changeRect; } /** * \p rect is a dirty rect in layer's original() coordinates! */ void KisLayer::buildProjectionUpToNode(KisPaintDeviceSP projection, KisNodeSP lastNode, const QRect& rect) { QRect changeRect = partialChangeRect(lastNode, rect); KisPaintDeviceSP originalDevice = original(); KIS_ASSERT_RECOVER_RETURN(needProjection() || hasEffectMasks()); if (!changeRect.isEmpty()) { applyMasks(originalDevice, projection, changeRect, this, lastNode); } } bool KisLayer::needProjection() const { return false; } void KisLayer::copyOriginalToProjection(const KisPaintDeviceSP original, KisPaintDeviceSP projection, const QRect& rect) const { KisPainter::copyAreaOptimized(rect.topLeft(), original, projection, rect); } KisAbstractProjectionPlaneSP KisLayer::projectionPlane() const { return m_d->layerStyleProjectionPlane ? KisAbstractProjectionPlaneSP(m_d->layerStyleProjectionPlane) : KisAbstractProjectionPlaneSP(m_d->projectionPlane); } KisLayerProjectionPlaneSP KisLayer::internalProjectionPlane() const { return m_d->projectionPlane; } KisPaintDeviceSP KisLayer::projection() const { KisPaintDeviceSP originalDevice = original(); return needProjection() || hasEffectMasks() ? m_d->safeProjection->getDeviceLazy(originalDevice) : originalDevice; } QRect KisLayer::changeRect(const QRect &rect, PositionToFilthy pos) const { QRect changeRect = rect; changeRect = incomingChangeRect(changeRect); if(pos == KisNode::N_FILTHY) { QRect projectionToBeUpdated = projection()->exactBoundsAmortized() & changeRect; bool changeRectVaries; changeRect = outgoingChangeRect(changeRect); changeRect = masksChangeRect(effectMasks(), changeRect, changeRectVaries); /** * If the projection contains some dirty areas we should also * add them to the change rect, because they might have * changed. E.g. when a visibility of the mask has chnaged * while the parent layer was invinisble. */ if (!projectionToBeUpdated.isEmpty() && !changeRect.contains(projectionToBeUpdated)) { changeRect |= projectionToBeUpdated; } } // TODO: string comparizon: optimize! if (pos != KisNode::N_FILTHY && pos != KisNode::N_FILTHY_PROJECTION && compositeOpId() != COMPOSITE_COPY) { changeRect |= rect; } return changeRect; } void KisLayer::childNodeChanged(KisNodeSP changedChildNode) { if (dynamic_cast(changedChildNode.data())) { notifyChildMaskChanged(); } } QRect KisLayer::incomingChangeRect(const QRect &rect) const { return rect; } QRect KisLayer::outgoingChangeRect(const QRect &rect) const { return rect; } QRect KisLayer::needRectForOriginal(const QRect &rect) const { QRect needRect = rect; const QList masks = effectMasks(); if (!masks.isEmpty()) { QStack applyRects; bool needRectVaries; needRect = masksNeedRect(masks, rect, applyRects, needRectVaries); } return needRect; } QImage KisLayer::createThumbnail(qint32 w, qint32 h) { if (w == 0 || h == 0) { return QImage(); } KisPaintDeviceSP originalDevice = original(); return originalDevice ? originalDevice->createThumbnail(w, h, 1, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()) : QImage(); } QImage KisLayer::createThumbnailForFrame(qint32 w, qint32 h, int time) { if (w == 0 || h == 0) { return QImage(); } KisPaintDeviceSP originalDevice = original(); - if (originalDevice ) { + if (originalDevice && originalDevice->framesInterface()) { KisRasterKeyframeChannel *channel = originalDevice->keyframeChannel(); if (channel) { KisPaintDeviceSP targetDevice = new KisPaintDevice(colorSpace()); - KisKeyframeSP keyframe = channel->activeKeyframeAt(time); + KisKeyframeSP keyframe = channel->visibleKeyframeAt(time); channel->fetchFrame(keyframe, targetDevice); return targetDevice->createThumbnail(w, h, 1, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } } return createThumbnail(w, h); } qint32 KisLayer::x() const { KisPaintDeviceSP originalDevice = original(); return originalDevice ? originalDevice->x() : 0; } qint32 KisLayer::y() const { KisPaintDeviceSP originalDevice = original(); return originalDevice ? originalDevice->y() : 0; } void KisLayer::setX(qint32 x) { KisPaintDeviceSP originalDevice = original(); if (originalDevice) originalDevice->setX(x); } void KisLayer::setY(qint32 y) { KisPaintDeviceSP originalDevice = original(); if (originalDevice) originalDevice->setY(y); } QRect KisLayer::layerExtentImpl(bool needExactBounds) const { QRect additionalMaskExtent = QRect(); QList effectMasks = this->effectMasks(); Q_FOREACH(KisEffectMaskSP mask, effectMasks) { additionalMaskExtent |= mask->nonDependentExtent(); } KisPaintDeviceSP originalDevice = original(); QRect layerExtent; if (originalDevice) { layerExtent = needExactBounds ? originalDevice->exactBounds() : originalDevice->extent(); } QRect additionalCompositeOpExtent; if (compositeOpId() == COMPOSITE_DESTINATION_IN || compositeOpId() == COMPOSITE_DESTINATION_ATOP) { additionalCompositeOpExtent = originalDevice->defaultBounds()->bounds(); } return layerExtent | additionalMaskExtent | additionalCompositeOpExtent; } QRect KisLayer::extent() const { return layerExtentImpl(false); } QRect KisLayer::exactBounds() const { return layerExtentImpl(true); } KisLayerSP KisLayer::parentLayer() const { return qobject_cast(parent().data()); } KisMetaData::Store* KisLayer::metaData() { return m_d->metaDataStore; } diff --git a/libs/image/kis_layer_utils.cpp b/libs/image/kis_layer_utils.cpp index cf4e363797..5544b23e09 100644 --- a/libs/image/kis_layer_utils.cpp +++ b/libs/image/kis_layer_utils.cpp @@ -1,1647 +1,1647 @@ /* * 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_layer_utils.h" #include #include #include #include #include "kis_painter.h" #include "kis_image.h" #include "kis_node.h" #include "kis_layer.h" #include "kis_paint_layer.h" #include "kis_clone_layer.h" #include "kis_group_layer.h" #include "kis_selection.h" #include "kis_selection_mask.h" #include "kis_meta_data_merge_strategy.h" #include #include "commands/kis_image_layer_add_command.h" #include "commands/kis_image_layer_remove_command.h" #include "commands/kis_image_layer_move_command.h" #include "commands/kis_image_change_layers_command.h" #include "commands_new/kis_activate_selection_mask_command.h" #include "commands/kis_image_change_visibility_command.h" #include "kis_abstract_projection_plane.h" #include "kis_processing_applicator.h" #include "kis_image_animation_interface.h" #include "kis_keyframe_channel.h" #include "kis_command_utils.h" #include "commands_new/kis_change_projection_color_command.h" #include "kis_layer_properties_icons.h" #include "lazybrush/kis_colorize_mask.h" #include "commands/kis_node_property_list_command.h" #include "commands/kis_node_compositeop_command.h" #include #include #include "krita_utils.h" #include "kis_image_signal_router.h" namespace KisLayerUtils { void fetchSelectionMasks(KisNodeList mergedNodes, QVector &selectionMasks) { foreach (KisNodeSP node, mergedNodes) { Q_FOREACH(KisNodeSP child, node->childNodes(QStringList("KisSelectionMask"), KoProperties())) { KisSelectionMaskSP mask = qobject_cast(child.data()); if (mask) { selectionMasks.append(mask); } } } } struct MergeDownInfoBase { MergeDownInfoBase(KisImageSP _image) : image(_image), storage(new SwitchFrameCommand::SharedStorage()) { } virtual ~MergeDownInfoBase() {} KisImageWSP image; QVector selectionMasks; KisNodeSP dstNode; SwitchFrameCommand::SharedStorageSP storage; QSet frames; bool useInTimeline = false; bool enableOnionSkins = false; virtual KisNodeList allSrcNodes() = 0; KisLayerSP dstLayer() { return qobject_cast(dstNode.data()); } }; struct MergeDownInfo : public MergeDownInfoBase { MergeDownInfo(KisImageSP _image, KisLayerSP _prevLayer, KisLayerSP _currLayer) : MergeDownInfoBase(_image), prevLayer(_prevLayer), currLayer(_currLayer) { frames = fetchLayerFramesRecursive(prevLayer) | fetchLayerFramesRecursive(currLayer); useInTimeline = prevLayer->useInTimeline() || currLayer->useInTimeline(); const KisPaintLayer *paintLayer = qobject_cast(currLayer.data()); if (paintLayer) enableOnionSkins |= paintLayer->onionSkinEnabled(); paintLayer = qobject_cast(prevLayer.data()); if (paintLayer) enableOnionSkins |= paintLayer->onionSkinEnabled(); } KisLayerSP prevLayer; KisLayerSP currLayer; KisNodeList allSrcNodes() override { KisNodeList mergedNodes; mergedNodes << currLayer; mergedNodes << prevLayer; return mergedNodes; } }; struct MergeMultipleInfo : public MergeDownInfoBase { MergeMultipleInfo(KisImageSP _image, KisNodeList _mergedNodes) : MergeDownInfoBase(_image), mergedNodes(_mergedNodes) { foreach (KisNodeSP node, mergedNodes) { frames |= fetchLayerFramesRecursive(node); useInTimeline |= node->useInTimeline(); const KisPaintLayer *paintLayer = qobject_cast(node.data()); if (paintLayer) { enableOnionSkins |= paintLayer->onionSkinEnabled(); } } } KisNodeList mergedNodes; bool nodesCompositingVaries = false; KisNodeList allSrcNodes() override { return mergedNodes; } }; typedef QSharedPointer MergeDownInfoBaseSP; typedef QSharedPointer MergeDownInfoSP; typedef QSharedPointer MergeMultipleInfoSP; struct FillSelectionMasks : public KUndo2Command { FillSelectionMasks(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { fetchSelectionMasks(m_info->allSrcNodes(), m_info->selectionMasks); } private: MergeDownInfoBaseSP m_info; }; struct DisableColorizeKeyStrokes : public KisCommandUtils::AggregateCommand { DisableColorizeKeyStrokes(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { recursiveApplyNodes(node, [this] (KisNodeSP node) { if (dynamic_cast(node.data()) && KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::colorizeEditKeyStrokes, true).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::colorizeEditKeyStrokes, false); addCommand(new KisNodePropertyListCommand(node, props)); } }); } } private: MergeDownInfoBaseSP m_info; }; struct DisableOnionSkins : public KisCommandUtils::AggregateCommand { DisableOnionSkins(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { recursiveApplyNodes(node, [this] (KisNodeSP node) { if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::onionSkins, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::onionSkins, false); addCommand(new KisNodePropertyListCommand(node, props)); } }); } } private: MergeDownInfoBaseSP m_info; }; struct DisableExtraCompositing : public KisCommandUtils::AggregateCommand { DisableExtraCompositing(MergeMultipleInfoSP info) : m_info(info) {} void populateChildCommands() override { /** * We disable extra compositing only in case all the layers have * the same compositing properties, therefore, we can just sum them using * Normal blend mode */ if (m_info->nodesCompositingVaries) return; // we should disable dirty requests on **redo only**, otherwise // the state of the layers will not be recovered on undo m_info->image->disableDirtyRequests(); Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { if (node->compositeOpId() != COMPOSITE_OVER) { addCommand(new KisNodeCompositeOpCommand(node, node->compositeOpId(), COMPOSITE_OVER)); } if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::inheritAlpha, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::inheritAlpha, false); addCommand(new KisNodePropertyListCommand(node, props)); } } m_info->image->enableDirtyRequests(); } private: MergeMultipleInfoSP m_info; }; struct DisablePassThroughForHeadsOnly : public KisCommandUtils::AggregateCommand { DisablePassThroughForHeadsOnly(MergeDownInfoBaseSP info, bool skipIfDstIsGroup = false) : m_info(info), m_skipIfDstIsGroup(skipIfDstIsGroup) { } void populateChildCommands() override { if (m_skipIfDstIsGroup && m_info->dstLayer() && m_info->dstLayer()->inherits("KisGroupLayer")) { return; } Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::passThrough, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::passThrough, false); addCommand(new KisNodePropertyListCommand(node, props)); } } } private: MergeDownInfoBaseSP m_info; bool m_skipIfDstIsGroup; }; struct RefreshHiddenAreas : public KUndo2Command { RefreshHiddenAreas(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { KisImageAnimationInterface *interface = m_info->image->animationInterface(); const QRect preparedRect = !interface->externalFrameActive() ? m_info->image->bounds() : QRect(); foreach (KisNodeSP node, m_info->allSrcNodes()) { refreshHiddenAreaAsync(m_info->image, node, preparedRect); } } private: MergeDownInfoBaseSP m_info; }; struct RefreshDelayedUpdateLayers : public KUndo2Command { RefreshDelayedUpdateLayers(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { foreach (KisNodeSP node, m_info->allSrcNodes()) { forceAllDelayedNodesUpdate(node); } } private: MergeDownInfoBaseSP m_info; }; struct KeepMergedNodesSelected : public KisCommandUtils::AggregateCommand { KeepMergedNodesSelected(MergeDownInfoSP info, bool finalizing) : m_singleInfo(info), m_finalizing(finalizing) {} KeepMergedNodesSelected(MergeMultipleInfoSP info, KisNodeSP putAfter, bool finalizing) : m_multipleInfo(info), m_finalizing(finalizing), m_putAfter(putAfter) {} void populateChildCommands() override { KisNodeSP prevNode; KisNodeSP nextNode; KisNodeList prevSelection; KisNodeList nextSelection; KisImageSP image; if (m_singleInfo) { prevNode = m_singleInfo->currLayer; nextNode = m_singleInfo->dstNode; image = m_singleInfo->image; } else if (m_multipleInfo) { prevNode = m_putAfter; nextNode = m_multipleInfo->dstNode; prevSelection = m_multipleInfo->allSrcNodes(); image = m_multipleInfo->image; } if (!m_finalizing) { addCommand(new KeepNodesSelectedCommand(prevSelection, KisNodeList(), prevNode, KisNodeSP(), image, false)); } else { addCommand(new KeepNodesSelectedCommand(KisNodeList(), nextSelection, KisNodeSP(), nextNode, image, true)); } } private: MergeDownInfoSP m_singleInfo; MergeMultipleInfoSP m_multipleInfo; bool m_finalizing; KisNodeSP m_putAfter; }; struct CreateMergedLayer : public KisCommandUtils::AggregateCommand { CreateMergedLayer(MergeDownInfoSP info) : m_info(info) {} void populateChildCommands() override { // actual merging done by KisLayer::createMergedLayer (or specialized descendant) m_info->dstNode = m_info->currLayer->createMergedLayerTemplate(m_info->prevLayer); if (m_info->frames.size() > 0) { m_info->dstNode->enableAnimation(); m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); } m_info->dstNode->setUseInTimeline(m_info->useInTimeline); KisPaintLayer *dstPaintLayer = qobject_cast(m_info->dstNode.data()); if (dstPaintLayer) { dstPaintLayer->setOnionSkinEnabled(m_info->enableOnionSkins); } } private: MergeDownInfoSP m_info; }; struct CreateMergedLayerMultiple : public KisCommandUtils::AggregateCommand { CreateMergedLayerMultiple(MergeMultipleInfoSP info, const QString name = QString() ) : m_info(info), m_name(name) {} void populateChildCommands() override { QString mergedLayerName; if (m_name.isEmpty()){ const QString mergedLayerSuffix = i18n("Merged"); mergedLayerName = m_info->mergedNodes.first()->name(); if (!mergedLayerName.endsWith(mergedLayerSuffix)) { mergedLayerName = QString("%1 %2") .arg(mergedLayerName).arg(mergedLayerSuffix); } } else { mergedLayerName = m_name; } KisPaintLayer *dstPaintLayer = new KisPaintLayer(m_info->image, mergedLayerName, OPACITY_OPAQUE_U8); m_info->dstNode = dstPaintLayer; if (m_info->frames.size() > 0) { m_info->dstNode->enableAnimation(); m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); } auto channelFlagsLazy = [](KisNodeSP node) { KisLayer *layer = dynamic_cast(node.data()); return layer ? layer->channelFlags() : QBitArray(); }; QString compositeOpId; QBitArray channelFlags; bool compositionVaries = false; bool isFirstCycle = true; foreach (KisNodeSP node, m_info->allSrcNodes()) { if (isFirstCycle) { compositeOpId = node->compositeOpId(); channelFlags = channelFlagsLazy(node); isFirstCycle = false; } else if (compositeOpId != node->compositeOpId() || channelFlags != channelFlagsLazy(node)) { compositionVaries = true; break; } KisLayerSP layer = qobject_cast(node.data()); if (layer && layer->layerStyle()) { compositionVaries = true; break; } } if (!compositionVaries) { if (!compositeOpId.isEmpty()) { m_info->dstNode->setCompositeOpId(compositeOpId); } if (m_info->dstLayer() && !channelFlags.isEmpty()) { m_info->dstLayer()->setChannelFlags(channelFlags); } } m_info->nodesCompositingVaries = compositionVaries; m_info->dstNode->setUseInTimeline(m_info->useInTimeline); dstPaintLayer->setOnionSkinEnabled(m_info->enableOnionSkins); } private: MergeMultipleInfoSP m_info; QString m_name; }; struct MergeLayers : public KisCommandUtils::AggregateCommand { MergeLayers(MergeDownInfoSP info) : m_info(info) {} void populateChildCommands() override { // actual merging done by KisLayer::createMergedLayer (or specialized descendant) m_info->currLayer->fillMergedLayerTemplate(m_info->dstLayer(), m_info->prevLayer); } private: MergeDownInfoSP m_info; }; struct MergeLayersMultiple : public KisCommandUtils::AggregateCommand { MergeLayersMultiple(MergeMultipleInfoSP info) : m_info(info) {} void populateChildCommands() override { KisPainter gc(m_info->dstNode->paintDevice()); foreach (KisNodeSP node, m_info->allSrcNodes()) { QRect rc = node->exactBounds() | m_info->image->bounds(); node->projectionPlane()->apply(&gc, rc); } } private: MergeMultipleInfoSP m_info; }; struct MergeMetaData : public KUndo2Command { MergeMetaData(MergeDownInfoSP info, const KisMetaData::MergeStrategy* strategy) : m_info(info), m_strategy(strategy) {} void redo() override { QRect layerProjectionExtent = m_info->currLayer->projection()->extent(); QRect prevLayerProjectionExtent = m_info->prevLayer->projection()->extent(); int prevLayerArea = prevLayerProjectionExtent.width() * prevLayerProjectionExtent.height(); int layerArea = layerProjectionExtent.width() * layerProjectionExtent.height(); QList scores; double norm = qMax(prevLayerArea, layerArea); scores.append(prevLayerArea / norm); scores.append(layerArea / norm); QList srcs; srcs.append(m_info->prevLayer->metaData()); srcs.append(m_info->currLayer->metaData()); m_strategy->merge(m_info->dstLayer()->metaData(), srcs, scores); } private: MergeDownInfoSP m_info; const KisMetaData::MergeStrategy *m_strategy; }; KeepNodesSelectedCommand::KeepNodesSelectedCommand(const KisNodeList &selectedBefore, const KisNodeList &selectedAfter, KisNodeSP activeBefore, KisNodeSP activeAfter, KisImageSP image, bool finalize, KUndo2Command *parent) : FlipFlopCommand(finalize, parent), m_selectedBefore(selectedBefore), m_selectedAfter(selectedAfter), m_activeBefore(activeBefore), m_activeAfter(activeAfter), m_image(image) { } void KeepNodesSelectedCommand::partB() { KisImageSignalType type; if (getState() == State::FINALIZING) { type = ComplexNodeReselectionSignal(m_activeAfter, m_selectedAfter); } else { type = ComplexNodeReselectionSignal(m_activeBefore, m_selectedBefore); } m_image->signalRouter()->emitNotification(type); } SelectGlobalSelectionMask::SelectGlobalSelectionMask(KisImageSP image) : m_image(image) { } void SelectGlobalSelectionMask::redo() { KisImageSignalType type = ComplexNodeReselectionSignal(m_image->rootLayer()->selectionMask(), KisNodeList()); m_image->signalRouter()->emitNotification(type); } RemoveNodeHelper::~RemoveNodeHelper() { } /** * The removal of two nodes in one go may be a bit tricky, because one * of them may be the clone of another. If we remove the source of a * clone layer, it will reincarnate into a paint layer. In this case * the pointer to the second layer will be lost. * * That's why we need to care about the order of the nodes removal: * the clone --- first, the source --- last. */ void RemoveNodeHelper::safeRemoveMultipleNodes(KisNodeList nodes, KisImageSP image) { const bool lastLayer = scanForLastLayer(image, nodes); auto isNodeWeird = [] (KisNodeSP node) { const bool normalCompositeMode = node->compositeOpId() == COMPOSITE_OVER; KisLayer *layer = dynamic_cast(node.data()); const bool hasInheritAlpha = layer && layer->alphaChannelDisabled(); return !normalCompositeMode && !hasInheritAlpha; }; while (!nodes.isEmpty()) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if (!checkIsSourceForClone(*it, nodes)) { KisNodeSP node = *it; addCommandImpl(new KisImageLayerRemoveCommand(image, node, !isNodeWeird(node), true)); it = nodes.erase(it); } else { ++it; } } } if (lastLayer) { KisLayerSP newLayer = new KisPaintLayer(image.data(), image->nextLayerName(), OPACITY_OPAQUE_U8, image->colorSpace()); addCommandImpl(new KisImageLayerAddCommand(image, newLayer, image->root(), KisNodeSP(), false, false)); } } bool RemoveNodeHelper::checkIsSourceForClone(KisNodeSP src, const KisNodeList &nodes) { foreach (KisNodeSP node, nodes) { if (node == src) continue; KisCloneLayer *clone = dynamic_cast(node.data()); if (clone && KisNodeSP(clone->copyFrom()) == src) { return true; } } return false; } bool RemoveNodeHelper::scanForLastLayer(KisImageWSP image, KisNodeList nodesToRemove) { bool removeLayers = false; Q_FOREACH(KisNodeSP nodeToRemove, nodesToRemove) { if (qobject_cast(nodeToRemove.data())) { removeLayers = true; break; } } if (!removeLayers) return false; bool lastLayer = true; KisNodeSP node = image->root()->firstChild(); while (node) { if (!nodesToRemove.contains(node) && qobject_cast(node.data()) && !node->isFakeNode()) { lastLayer = false; break; } node = node->nextSibling(); } return lastLayer; } SimpleRemoveLayers::SimpleRemoveLayers(const KisNodeList &nodes, KisImageSP image) : m_nodes(nodes), m_image(image) { } void SimpleRemoveLayers::populateChildCommands() { if (m_nodes.isEmpty()) return; safeRemoveMultipleNodes(m_nodes, m_image); } void SimpleRemoveLayers::addCommandImpl(KUndo2Command *cmd) { addCommand(cmd); } struct InsertNode : public KisCommandUtils::AggregateCommand { InsertNode(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter) {} void populateChildCommands() override { addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, m_putAfter->parent(), m_putAfter, true, false)); } private: virtual void addCommandImpl(KUndo2Command *cmd) { addCommand(cmd); } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; struct CleanUpNodes : private RemoveNodeHelper, public KisCommandUtils::AggregateCommand { CleanUpNodes(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter) {} static void findPerfectParent(KisNodeList nodesToDelete, KisNodeSP &putAfter, KisNodeSP &parent) { if (!putAfter) { putAfter = nodesToDelete.last(); } // Add the new merged node on top of the active node // -- checking all parents if they are included in nodesToDelete // Not every descendant is included in nodesToDelete even if in fact // they are going to be deleted, so we need to check it. // If we consider the path from root to the putAfter node, // if there are any nodes marked for deletion, any node afterwards // is going to be deleted, too. // example: root . . . . . ! ! . . ! ! ! ! . . . . putAfter // it should be: root . . . . . ! ! ! ! ! ! ! ! ! ! ! ! !putAfter // and here: root . . . . X ! ! . . ! ! ! ! . . . . putAfter // you can see which node is "the perfect ancestor" // (marked X; called "parent" in the function arguments). // and here: root . . . . . O ! . . ! ! ! ! . . . . putAfter // you can see which node is "the topmost deleted ancestor" (marked 'O') KisNodeSP node = putAfter->parent(); bool foundDeletedAncestor = false; KisNodeSP topmostAncestorToDelete = nullptr; while (node) { if (nodesToDelete.contains(node) && !nodesToDelete.contains(node->parent())) { foundDeletedAncestor = true; topmostAncestorToDelete = node; // Here node is to be deleted and its parent is not, // so its parent is the one of the first not deleted (="perfect") ancestors. // We need the one that is closest to the top (root) } node = node->parent(); } if (foundDeletedAncestor) { parent = topmostAncestorToDelete->parent(); putAfter = topmostAncestorToDelete; } else { parent = putAfter->parent(); // putAfter (and none of its ancestors) is to be deleted, so its parent is the first not deleted ancestor } } void populateChildCommands() override { KisNodeList nodesToDelete = m_info->allSrcNodes(); KisNodeSP parent; findPerfectParent(nodesToDelete, m_putAfter, parent); if (!parent) { KisNodeSP oldRoot = m_info->image->root(); KisNodeSP newRoot(new KisGroupLayer(m_info->image, "root", OPACITY_OPAQUE_U8)); // copy all fake nodes into the new image KisLayerUtils::recursiveApplyNodes(oldRoot, [this, oldRoot, newRoot] (KisNodeSP node) { if (node->isFakeNode() && node->parent() == oldRoot) { addCommand(new KisImageLayerAddCommand(m_info->image, node->clone(), newRoot, KisNodeSP(), false, false)); } }); addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, newRoot, KisNodeSP(), true, false)); addCommand(new KisImageChangeLayersCommand(m_info->image, oldRoot, newRoot)); } else { addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, parent, m_putAfter, true, false)); /** * We can merge selection masks, in this case dstLayer is not defined! */ if (m_info->dstLayer()) { reparentSelectionMasks(m_info->image, m_info->dstLayer(), m_info->selectionMasks); } KisNodeList safeNodesToDelete = m_info->allSrcNodes(); for (KisNodeList::iterator it = safeNodesToDelete.begin(); it != safeNodesToDelete.end(); ++it) { KisNodeSP node = *it; if (node->userLocked() && node->visible()) { addCommand(new KisImageChangeVisibilityCommand(false, node)); } } KritaUtils::filterContainer(safeNodesToDelete, [](KisNodeSP node) { return !node->userLocked(); }); safeRemoveMultipleNodes(safeNodesToDelete, m_info->image); } } private: void addCommandImpl(KUndo2Command *cmd) override { addCommand(cmd); } void reparentSelectionMasks(KisImageSP image, KisLayerSP newLayer, const QVector &selectionMasks) { KIS_SAFE_ASSERT_RECOVER_RETURN(newLayer); foreach (KisSelectionMaskSP mask, selectionMasks) { addCommand(new KisImageLayerMoveCommand(image, mask, newLayer, newLayer->lastChild())); addCommand(new KisActivateSelectionMaskCommand(mask, false)); } } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; SwitchFrameCommand::SharedStorage::~SharedStorage() { } SwitchFrameCommand::SwitchFrameCommand(KisImageSP image, int time, bool finalize, SharedStorageSP storage) : FlipFlopCommand(finalize), m_image(image), m_newTime(time), m_storage(storage) {} SwitchFrameCommand::~SwitchFrameCommand() {} void SwitchFrameCommand::partA() { KisImageAnimationInterface *interface = m_image->animationInterface(); const int currentTime = interface->currentTime(); if (currentTime == m_newTime) { m_storage->value = m_newTime; return; } interface->image()->disableUIUpdates(); interface->saveAndResetCurrentTime(m_newTime, &m_storage->value); } void SwitchFrameCommand::partB() { KisImageAnimationInterface *interface = m_image->animationInterface(); const int currentTime = interface->currentTime(); if (currentTime == m_storage->value) { return; } interface->restoreCurrentTime(&m_storage->value); interface->image()->enableUIUpdates(); } struct AddNewFrame : public KisCommandUtils::AggregateCommand { AddNewFrame(MergeDownInfoBaseSP info, int frame) : m_info(info), m_frame(frame) {} void populateChildCommands() override { KUndo2Command *cmd = new KisCommandUtils::SkipFirstRedoWrapper(); KisKeyframeChannel *channel = m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id()); KisKeyframeSP keyframe = channel->addKeyframe(m_frame, cmd); applyKeyframeColorLabel(keyframe); addCommand(cmd); } void applyKeyframeColorLabel(KisKeyframeSP dstKeyframe) { Q_FOREACH(KisNodeSP srcNode, m_info->allSrcNodes()) { Q_FOREACH(KisKeyframeChannel *channel, srcNode->keyframeChannels().values()) { - KisKeyframeSP keyframe = channel->keyframeAt(m_frame); + KisKeyframeSP keyframe = channel->visibleKeyframeAt(m_frame); if (!keyframe.isNull() && keyframe->colorLabel() != 0) { dstKeyframe->setColorLabel(keyframe->colorLabel()); return; } } } dstKeyframe->setColorLabel(0); } private: MergeDownInfoBaseSP m_info; int m_frame; }; QSet fetchLayerFrames(KisNodeSP node) { KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!channel) return QSet(); return channel->allKeyframeIds(); } QSet fetchLayerFramesRecursive(KisNodeSP rootNode) { QSet frames = fetchLayerFrames(rootNode); KisNodeSP node = rootNode->firstChild(); while(node) { frames |= fetchLayerFramesRecursive(node); node = node->nextSibling(); } return frames; } void updateFrameJobs(FrameJobs *jobs, KisNodeSP node) { QSet frames = fetchLayerFrames(node); if (frames.isEmpty()) { (*jobs)[0].insert(node); } else { foreach (int frame, frames) { (*jobs)[frame].insert(node); } } } void updateFrameJobsRecursive(FrameJobs *jobs, KisNodeSP rootNode) { updateFrameJobs(jobs, rootNode); KisNodeSP node = rootNode->firstChild(); while(node) { updateFrameJobsRecursive(jobs, node); node = node->nextSibling(); } } /** * \see a comment in mergeMultipleLayersImpl() */ void mergeDown(KisImageSP image, KisLayerSP layer, const KisMetaData::MergeStrategy* strategy) { if (!layer->prevSibling()) return; // XXX: this breaks if we allow free mixing of masks and layers KisLayerSP prevLayer = qobject_cast(layer->prevSibling().data()); if (!prevLayer) return; if (!layer->visible() && !prevLayer->visible()) { return; } KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, kundo2_i18n("Merge Down")); if (layer->visible() && prevLayer->visible()) { MergeDownInfoSP info(new MergeDownInfo(image, prevLayer, layer)); // disable key strokes on all colorize masks, all onion skins on // paint layers and wait until update is finished with a barrier applicator.applyCommand(new DisableColorizeKeyStrokes(info)); applicator.applyCommand(new DisableOnionSkins(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); applicator.applyCommand(new KeepMergedNodesSelected(info, false)); applicator.applyCommand(new FillSelectionMasks(info)); applicator.applyCommand(new CreateMergedLayer(info), KisStrokeJobData::BARRIER); // NOTE: shape layer may have emitted spontaneous jobs during layer creation, // wait for them to complete! applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); // in two-layer mode we disable pass through only when the destination layer // is not a group layer applicator.applyCommand(new DisablePassThroughForHeadsOnly(info, true)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); if (info->frames.size() > 0) { foreach (int frame, info->frames) { applicator.applyCommand(new SwitchFrameCommand(info->image, frame, false, info->storage)); applicator.applyCommand(new AddNewFrame(info, frame)); applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new SwitchFrameCommand(info->image, frame, true, info->storage)); } } else { applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayers(info), KisStrokeJobData::BARRIER); } applicator.applyCommand(new MergeMetaData(info, strategy), KisStrokeJobData::BARRIER); applicator.applyCommand(new CleanUpNodes(info, layer), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepMergedNodesSelected(info, true)); } else if (layer->visible()) { applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), layer, KisNodeSP(), image, false)); applicator.applyCommand( new SimpleRemoveLayers(KisNodeList() << prevLayer, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), KisNodeSP(), layer, image, true)); } else if (prevLayer->visible()) { applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), layer, KisNodeSP(), image, false)); applicator.applyCommand( new SimpleRemoveLayers(KisNodeList() << layer, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), KisNodeSP(), prevLayer, image, true)); } applicator.end(); } bool checkIsChildOf(KisNodeSP node, const KisNodeList &parents) { KisNodeList nodeParents; KisNodeSP parent = node->parent(); while (parent) { nodeParents << parent; parent = parent->parent(); } foreach(KisNodeSP perspectiveParent, parents) { if (nodeParents.contains(perspectiveParent)) { return true; } } return false; } bool checkIsCloneOf(KisNodeSP node, const KisNodeList &nodes) { bool result = false; KisCloneLayer *clone = dynamic_cast(node.data()); if (clone) { KisNodeSP cloneSource = KisNodeSP(clone->copyFrom()); Q_FOREACH(KisNodeSP subtree, nodes) { result = recursiveFindNode(subtree, [cloneSource](KisNodeSP node) -> bool { return node == cloneSource; }); if (!result) { result = checkIsCloneOf(cloneSource, nodes); } if (result) { break; } } } return result; } void filterMergableNodes(KisNodeList &nodes, bool allowMasks) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if ((!allowMasks && !qobject_cast(it->data())) || checkIsChildOf(*it, nodes)) { //qDebug() << "Skipping node" << ppVar((*it)->name()); it = nodes.erase(it); } else { ++it; } } } void sortMergableNodes(KisNodeSP root, KisNodeList &inputNodes, KisNodeList &outputNodes) { KisNodeList::iterator it = std::find(inputNodes.begin(), inputNodes.end(), root); if (it != inputNodes.end()) { outputNodes << *it; inputNodes.erase(it); } if (inputNodes.isEmpty()) { return; } KisNodeSP child = root->firstChild(); while (child) { sortMergableNodes(child, inputNodes, outputNodes); child = child->nextSibling(); } /** * By the end of recursion \p inputNodes must be empty */ KIS_ASSERT_RECOVER_NOOP(root->parent() || inputNodes.isEmpty()); } KisNodeList sortMergableNodes(KisNodeSP root, KisNodeList nodes) { KisNodeList result; sortMergableNodes(root, nodes, result); return result; } KisNodeList sortAndFilterMergableInternalNodes(KisNodeList nodes, bool allowMasks) { KIS_ASSERT_RECOVER(!nodes.isEmpty()) { return nodes; } KisNodeSP root; Q_FOREACH(KisNodeSP node, nodes) { KisNodeSP localRoot = node; while (localRoot->parent()) { localRoot = localRoot->parent(); } if (!root) { root = localRoot; } KIS_ASSERT_RECOVER(root == localRoot) { return nodes; } } KisNodeList result; sortMergableNodes(root, nodes, result); filterMergableNodes(result, allowMasks); return result; } KisNodeList sortAndFilterAnyMergableNodesSafe(const KisNodeList &nodes, KisImageSP image) { KisNodeList filteredNodes = nodes; KisNodeList sortedNodes; KisLayerUtils::filterMergableNodes(filteredNodes, true); bool haveExternalNodes = false; Q_FOREACH (KisNodeSP node, nodes) { if (node->graphListener() != image->root()->graphListener()) { haveExternalNodes = true; break; } } if (!haveExternalNodes) { KisLayerUtils::sortMergableNodes(image->root(), filteredNodes, sortedNodes); } else { sortedNodes = filteredNodes; } return sortedNodes; } void addCopyOfNameTag(KisNodeSP node) { const QString prefix = i18n("Copy of"); QString newName = node->name(); if (!newName.startsWith(prefix)) { newName = QString("%1 %2").arg(prefix).arg(newName); node->setName(newName); } } KisNodeList findNodesWithProps(KisNodeSP root, const KoProperties &props, bool excludeRoot) { KisNodeList nodes; if ((!excludeRoot || root->parent()) && root->check(props)) { nodes << root; } KisNodeSP node = root->firstChild(); while (node) { nodes += findNodesWithProps(node, props, excludeRoot); node = node->nextSibling(); } return nodes; } KisNodeList filterInvisibleNodes(const KisNodeList &nodes, KisNodeList *invisibleNodes, KisNodeSP *putAfter) { KIS_ASSERT_RECOVER(invisibleNodes) { return nodes; } KIS_ASSERT_RECOVER(putAfter) { return nodes; } KisNodeList visibleNodes; int putAfterIndex = -1; Q_FOREACH(KisNodeSP node, nodes) { if (node->visible() || node->userLocked()) { visibleNodes << node; } else { *invisibleNodes << node; if (node == *putAfter) { putAfterIndex = visibleNodes.size() - 1; } } } if (!visibleNodes.isEmpty() && putAfterIndex >= 0) { putAfterIndex = qBound(0, putAfterIndex, visibleNodes.size() - 1); *putAfter = visibleNodes[putAfterIndex]; } return visibleNodes; } void filterUnlockedNodes(KisNodeList &nodes) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if ((*it)->userLocked()) { it = nodes.erase(it); } else { ++it; } } } void changeImageDefaultProjectionColor(KisImageSP image, const KoColor &color) { KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, image->root(), KisProcessingApplicator::RECURSIVE, emitSignals, kundo2_i18n("Change projection color"), 0, 142857 + 1); applicator.applyCommand(new KisChangeProjectionColorCommand(image, color), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); applicator.end(); } /** * There might be two approaches for merging multiple layers: * * 1) Consider the selected nodes as a distinct "group" and merge them * as if they were isolated from the rest of the image. The key point * of this approach is that the look of the image will change, when * merging "weird" layers, like adjustment layers or layers with * non-normal blending mode. * * 2) Merge layers in a way to keep the look of the image as unchanged as * possible. With this approach one uses a few heuristics: * * * when merging multiple layers with non-normal (but equal) blending * mode, first merge these layers together using Normal blending mode, * then set blending mode of the result to the original blending mode * * * when merging multiple layers with different blending modes or * layer styles, they are first rasterized, and then laid over each * other with their own composite op. The blending mode of the final * layer is set to Normal, so the user could clearly see that he should * choose the correct blending mode. * * Krita uses the second approach: after merge operation, the image should look * as if nothing has happened (if it is technically possible). */ void mergeMultipleLayersImpl(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter, bool flattenSingleLayer, const KUndo2MagicString &actionName, bool cleanupNodes = true, const QString layerName = QString()) { if (!putAfter) { putAfter = mergedNodes.first(); } filterMergableNodes(mergedNodes); { KisNodeList tempNodes; std::swap(mergedNodes, tempNodes); sortMergableNodes(image->root(), tempNodes, mergedNodes); } if (mergedNodes.size() <= 1 && (!flattenSingleLayer && mergedNodes.size() == 1)) return; KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; emitSignals << ComplexNodeReselectionSignal(KisNodeSP(), KisNodeList(), KisNodeSP(), mergedNodes); KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, actionName); KisNodeList originalNodes = mergedNodes; KisNodeList invisibleNodes; mergedNodes = filterInvisibleNodes(originalNodes, &invisibleNodes, &putAfter); if (!invisibleNodes.isEmpty() && !mergedNodes.isEmpty()) { /* If the putAfter node is invisible, * we should instead pick one of the nodes * to be merged to avoid a null putAfter. */ if (!putAfter->visible()){ putAfter = mergedNodes.first(); } applicator.applyCommand( new SimpleRemoveLayers(invisibleNodes, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } if (mergedNodes.size() > 1 || invisibleNodes.isEmpty()) { MergeMultipleInfoSP info(new MergeMultipleInfo(image, mergedNodes)); // disable key strokes on all colorize masks, all onion skins on // paint layers and wait until update is finished with a barrier applicator.applyCommand(new DisableColorizeKeyStrokes(info)); applicator.applyCommand(new DisableOnionSkins(info)); applicator.applyCommand(new DisablePassThroughForHeadsOnly(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); applicator.applyCommand(new KeepMergedNodesSelected(info, putAfter, false)); applicator.applyCommand(new FillSelectionMasks(info)); applicator.applyCommand(new CreateMergedLayerMultiple(info, layerName), KisStrokeJobData::BARRIER); applicator.applyCommand(new DisableExtraCompositing(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); if (!info->frames.isEmpty()) { foreach (int frame, info->frames) { applicator.applyCommand(new SwitchFrameCommand(info->image, frame, false, info->storage)); applicator.applyCommand(new AddNewFrame(info, frame)); applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayersMultiple(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new SwitchFrameCommand(info->image, frame, true, info->storage)); } } else { applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayersMultiple(info), KisStrokeJobData::BARRIER); } //applicator.applyCommand(new MergeMetaData(info, strategy), KisStrokeJobData::BARRIER); if (cleanupNodes){ applicator.applyCommand(new CleanUpNodes(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } else { applicator.applyCommand(new InsertNode(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } applicator.applyCommand(new KeepMergedNodesSelected(info, putAfter, true)); } applicator.end(); } void mergeMultipleLayers(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter) { mergeMultipleLayersImpl(image, mergedNodes, putAfter, false, kundo2_i18n("Merge Selected Nodes")); } void newLayerFromVisible(KisImageSP image, KisNodeSP putAfter) { KisNodeList mergedNodes; mergedNodes << image->root(); mergeMultipleLayersImpl(image, mergedNodes, putAfter, true, kundo2_i18n("New From Visible"), false, i18nc("New layer created from all the visible layers", "Visible")); } struct MergeSelectionMasks : public KisCommandUtils::AggregateCommand { MergeSelectionMasks(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter){} void populateChildCommands() override { KisNodeSP parent; CleanUpNodes::findPerfectParent(m_info->allSrcNodes(), m_putAfter, parent); KisLayerSP parentLayer; do { parentLayer = qobject_cast(parent.data()); parent = parent->parent(); } while(!parentLayer && parent); KisSelectionSP selection = new KisSelection(); foreach (KisNodeSP node, m_info->allSrcNodes()) { KisMaskSP mask = dynamic_cast(node.data()); if (!mask) continue; selection->pixelSelection()->applySelection( mask->selection()->pixelSelection(), SELECTION_ADD); } KisSelectionMaskSP mergedMask = new KisSelectionMask(m_info->image); mergedMask->initSelection(parentLayer); mergedMask->setSelection(selection); m_info->dstNode = mergedMask; } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; struct ActivateSelectionMask : public KisCommandUtils::AggregateCommand { ActivateSelectionMask(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { KisSelectionMaskSP mergedMask = dynamic_cast(m_info->dstNode.data()); addCommand(new KisActivateSelectionMaskCommand(mergedMask, true)); } private: MergeDownInfoBaseSP m_info; }; bool tryMergeSelectionMasks(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter) { QList selectionMasks; for (auto it = mergedNodes.begin(); it != mergedNodes.end(); /*noop*/) { KisSelectionMaskSP mask = dynamic_cast(it->data()); if (!mask) { it = mergedNodes.erase(it); } else { selectionMasks.append(mask); ++it; } } if (mergedNodes.isEmpty()) return false; KisLayerSP parentLayer = qobject_cast(selectionMasks.first()->parent().data()); KIS_ASSERT_RECOVER(parentLayer) { return 0; } KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, kundo2_i18n("Merge Selection Masks")); MergeMultipleInfoSP info(new MergeMultipleInfo(image, mergedNodes)); applicator.applyCommand(new MergeSelectionMasks(info, putAfter)); applicator.applyCommand(new CleanUpNodes(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new ActivateSelectionMask(info)); applicator.end(); return true; } void flattenLayer(KisImageSP image, KisLayerSP layer) { if (!layer->childCount() && !layer->layerStyle()) return; KisNodeList mergedNodes; mergedNodes << layer; mergeMultipleLayersImpl(image, mergedNodes, layer, true, kundo2_i18n("Flatten Layer")); } void flattenImage(KisImageSP image, KisNodeSP activeNode) { if (!activeNode) { activeNode = image->root()->lastChild(); } KisNodeList mergedNodes; mergedNodes << image->root(); mergeMultipleLayersImpl(image, mergedNodes, activeNode, true, kundo2_i18n("Flatten Image")); } KisSimpleUpdateCommand::KisSimpleUpdateCommand(KisNodeList nodes, bool finalize, KUndo2Command *parent) : FlipFlopCommand(finalize, parent), m_nodes(nodes) { } void KisSimpleUpdateCommand::partB() { updateNodes(m_nodes); } void KisSimpleUpdateCommand::updateNodes(const KisNodeList &nodes) { Q_FOREACH(KisNodeSP node, nodes) { node->setDirty(node->extent()); } } KisNodeSP recursiveFindNode(KisNodeSP node, std::function func) { if (func(node)) { return node; } node = node->firstChild(); while (node) { KisNodeSP resultNode = recursiveFindNode(node, func); if (resultNode) { return resultNode; } node = node->nextSibling(); } return 0; } KisNodeSP findNodeByUuid(KisNodeSP root, const QUuid &uuid) { return recursiveFindNode(root, [uuid] (KisNodeSP node) { return node->uuid() == uuid; }); } void forceAllDelayedNodesUpdate(KisNodeSP root) { KisLayerUtils::recursiveApplyNodes(root, [] (KisNodeSP node) { KisDelayedUpdateNodeInterface *delayedUpdate = dynamic_cast(node.data()); if (delayedUpdate) { delayedUpdate->forceUpdateTimedNode(); } }); } bool hasDelayedNodeWithUpdates(KisNodeSP root) { return recursiveFindNode(root, [] (KisNodeSP node) { KisDelayedUpdateNodeInterface *delayedUpdate = dynamic_cast(node.data()); return delayedUpdate ? delayedUpdate->hasPendingTimedUpdates() : false; }); } void forceAllHiddenOriginalsUpdate(KisNodeSP root) { KisLayerUtils::recursiveApplyNodes(root, [] (KisNodeSP node) { KisCroppedOriginalLayerInterface *croppedUpdate = dynamic_cast(node.data()); if (croppedUpdate) { croppedUpdate->forceUpdateHiddenAreaOnOriginal(); } }); } KisImageSP findImageByHierarchy(KisNodeSP node) { while (node) { const KisLayer *layer = dynamic_cast(node.data()); if (layer) { return layer->image(); } node = node->parent(); } return 0; } namespace Private { QRect realNodeChangeRect(KisNodeSP rootNode, QRect currentRect = QRect()) { KisNodeSP node = rootNode->firstChild(); while(node) { currentRect |= realNodeChangeRect(node, currentRect); node = node->nextSibling(); } if (!rootNode->isFakeNode()) { // TODO: it would be better to count up changeRect inside // node's extent() method currentRect |= rootNode->projectionPlane()->changeRect(rootNode->exactBounds()); } return currentRect; } } void refreshHiddenAreaAsync(KisImageSP image, KisNodeSP rootNode, const QRect &preparedArea) { QRect realNodeRect = Private::realNodeChangeRect(rootNode); if (!preparedArea.contains(realNodeRect)) { QRegion dirtyRegion = realNodeRect; dirtyRegion -= preparedArea; Q_FOREACH (const QRect &rc, dirtyRegion.rects()) { image->refreshGraphAsync(rootNode, rc, realNodeRect); } } } QRect recursiveNodeExactBounds(KisNodeSP rootNode) { QRect exactBounds; recursiveApplyNodes(rootNode, [&exactBounds] (KisNodeSP node) { exactBounds |= node->exactBounds(); }); return exactBounds; } } diff --git a/libs/image/kis_node.cpp b/libs/image/kis_node.cpp index bd3ec46768..6cc55b2d2c 100644 --- a/libs/image/kis_node.cpp +++ b/libs/image/kis_node.cpp @@ -1,682 +1,682 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * 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_node.h" #include #include #include #include #include #include #include #include #include "kis_global.h" #include "kis_node_graph_listener.h" #include "kis_node_visitor.h" #include "kis_processing_visitor.h" #include "kis_node_progress_proxy.h" #include "kis_busy_progress_indicator.h" #include "kis_clone_layer.h" #include "kis_safe_read_list.h" typedef KisSafeReadList KisSafeReadNodeList; #include "kis_abstract_projection_plane.h" #include "kis_projection_leaf.h" #include "kis_undo_adapter.h" #include "kis_keyframe_channel.h" #include "kis_image.h" #include "kis_layer_utils.h" /** *The link between KisProjection and KisImageUpdater *uses queued signals with an argument of KisNodeSP type, *so we should register it beforehand */ struct KisNodeSPStaticRegistrar { KisNodeSPStaticRegistrar() { qRegisterMetaType("KisNodeSP"); } }; static KisNodeSPStaticRegistrar __registrar1; struct KisNodeListStaticRegistrar { KisNodeListStaticRegistrar() { qRegisterMetaType("KisNodeList"); } }; static KisNodeListStaticRegistrar __registrar2; /** * Note about "thread safety" of KisNode * * 1) One can *read* any information about node and node graph in any * number of threads concurrently. This operation is safe because * of the usage of KisSafeReadNodeList and will run concurrently * (lock-free). * * 2) One can *write* any information into the node or node graph in a * single thread only! Changing the graph concurrently is *not* * sane and therefore not supported. * * 3) One can *read and write* information about the node graph * concurrently, given that there is only *one* writer thread and * any number of reader threads. Please note that in this case the * node's code is just guaranteed *not to crash*, which is ensured * by nodeSubgraphLock. You need to ensure the sanity of the data * read by the reader threads yourself! */ struct Q_DECL_HIDDEN KisNode::Private { public: Private(KisNode *node) : graphListener(0) , nodeProgressProxy(0) , busyProgressIndicator(0) , projectionLeaf(new KisProjectionLeaf(node)) { } KisNodeWSP parent; KisNodeGraphListener *graphListener; KisSafeReadNodeList nodes; KisNodeProgressProxy *nodeProgressProxy; KisBusyProgressIndicator *busyProgressIndicator; QReadWriteLock nodeSubgraphLock; KisProjectionLeafSP projectionLeaf; const KisNode* findSymmetricClone(const KisNode *srcRoot, const KisNode *dstRoot, const KisNode *srcTarget); void processDuplicatedClones(const KisNode *srcDuplicationRoot, const KisNode *dstDuplicationRoot, KisNode *node); }; /** * Finds the layer in \p dstRoot subtree, which has the same path as * \p srcTarget has in \p srcRoot */ const KisNode* KisNode::Private::findSymmetricClone(const KisNode *srcRoot, const KisNode *dstRoot, const KisNode *srcTarget) { if (srcRoot == srcTarget) return dstRoot; KisSafeReadNodeList::const_iterator srcIter = srcRoot->m_d->nodes.constBegin(); KisSafeReadNodeList::const_iterator dstIter = dstRoot->m_d->nodes.constBegin(); for (; srcIter != srcRoot->m_d->nodes.constEnd(); srcIter++, dstIter++) { KIS_ASSERT_RECOVER_RETURN_VALUE((srcIter != srcRoot->m_d->nodes.constEnd()) == (dstIter != dstRoot->m_d->nodes.constEnd()), 0); const KisNode *node = findSymmetricClone(srcIter->data(), dstIter->data(), srcTarget); if (node) return node; } return 0; } /** * This function walks through a subtrees of old and new layers and * searches for clone layers. For each clone layer it checks whether * its copyFrom() lays inside the old subtree, and if it is so resets * it to the corresponding layer in the new subtree. * * That is needed when the user duplicates a group layer with all its * layer subtree. In such a case all the "internal" clones must stay * "internal" and not point to the layers of the older group. */ void KisNode::Private::processDuplicatedClones(const KisNode *srcDuplicationRoot, const KisNode *dstDuplicationRoot, KisNode *node) { if (KisCloneLayer *clone = dynamic_cast(node)) { KIS_ASSERT_RECOVER_RETURN(clone->copyFrom()); const KisNode *newCopyFrom = findSymmetricClone(srcDuplicationRoot, dstDuplicationRoot, clone->copyFrom()); if (newCopyFrom) { KisLayer *newCopyFromLayer = qobject_cast(const_cast(newCopyFrom)); KIS_ASSERT_RECOVER_RETURN(newCopyFromLayer); clone->setCopyFrom(newCopyFromLayer); } } KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, node->m_d->nodes) { KisNode *child = const_cast((*iter).data()); processDuplicatedClones(srcDuplicationRoot, dstDuplicationRoot, child); } } KisNode::KisNode(KisImageWSP image) : KisBaseNode(image), m_d(new Private(this)) { m_d->parent = 0; m_d->graphListener = 0; moveToThread(qApp->thread()); } KisNode::KisNode(const KisNode & rhs) : KisBaseNode(rhs) , m_d(new Private(this)) { m_d->parent = 0; m_d->graphListener = 0; moveToThread(qApp->thread()); // HACK ALERT: we create opacity channel in KisBaseNode, but we cannot // initialize its node from there! So workaround it here! QMap channels = keyframeChannels(); for (auto it = channels.begin(); it != channels.end(); ++it) { it.value()->setNode(this); } // NOTE: the nodes are not supposed to be added/removed while // creation of another node, so we do *no* locking here! KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, rhs.m_d->nodes) { KisNodeSP child = (*iter)->clone(); child->createNodeProgressProxy(); m_d->nodes.append(child); child->setParent(this); } m_d->processDuplicatedClones(&rhs, this, this); } KisNode::~KisNode() { if (m_d->busyProgressIndicator) { m_d->busyProgressIndicator->prepareDestroying(); m_d->busyProgressIndicator->deleteLater(); } if (m_d->nodeProgressProxy) { m_d->nodeProgressProxy->prepareDestroying(); m_d->nodeProgressProxy->deleteLater(); } { QWriteLocker l(&m_d->nodeSubgraphLock); m_d->nodes.clear(); } delete m_d; } QRect KisNode::needRect(const QRect &rect, PositionToFilthy pos) const { Q_UNUSED(pos); return rect; } QRect KisNode::changeRect(const QRect &rect, PositionToFilthy pos) const { Q_UNUSED(pos); return rect; } QRect KisNode::accessRect(const QRect &rect, PositionToFilthy pos) const { Q_UNUSED(pos); return rect; } void KisNode::childNodeChanged(KisNodeSP /*changedChildNode*/) { } KisAbstractProjectionPlaneSP KisNode::projectionPlane() const { KIS_ASSERT_RECOVER_NOOP(0 && "KisNode::projectionPlane() is not defined!"); static KisAbstractProjectionPlaneSP plane = toQShared(new KisDumbProjectionPlane()); return plane; } KisProjectionLeafSP KisNode::projectionLeaf() const { return m_d->projectionLeaf; } void KisNode::setImage(KisImageWSP image) { KisBaseNode::setImage(image); KisNodeSP node = firstChild(); while (node) { KisLayerUtils::recursiveApplyNodes(node, [image] (KisNodeSP node) { node->setImage(image); }); node = node->nextSibling(); } } bool KisNode::accept(KisNodeVisitor &v) { return v.visit(this); } void KisNode::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) { visitor.visit(this, undoAdapter); } int KisNode::graphSequenceNumber() const { return m_d->graphListener ? m_d->graphListener->graphSequenceNumber() : -1; } KisNodeGraphListener *KisNode::graphListener() const { return m_d->graphListener; } void KisNode::setGraphListener(KisNodeGraphListener *graphListener) { m_d->graphListener = graphListener; QReadLocker l(&m_d->nodeSubgraphLock); KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, m_d->nodes) { KisNodeSP child = (*iter); child->setGraphListener(graphListener); } } void KisNode::setParent(KisNodeWSP parent) { QWriteLocker l(&m_d->nodeSubgraphLock); m_d->parent = parent; } KisNodeSP KisNode::parent() const { QReadLocker l(&m_d->nodeSubgraphLock); return m_d->parent.isValid() ? KisNodeSP(m_d->parent) : KisNodeSP(); } KisBaseNodeSP KisNode::parentCallback() const { return parent(); } void KisNode::notifyParentVisibilityChanged(bool value) { QReadLocker l(&m_d->nodeSubgraphLock); KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, m_d->nodes) { KisNodeSP child = (*iter); child->notifyParentVisibilityChanged(value); } } void KisNode::baseNodeChangedCallback() { if(m_d->graphListener) { m_d->graphListener->nodeChanged(this); emit sigNodeChangedInternal(); } } void KisNode::baseNodeInvalidateAllFramesCallback() { if(m_d->graphListener) { m_d->graphListener->invalidateAllFrames(); } } void KisNode::baseNodeCollapsedChangedCallback() { if(m_d->graphListener) { m_d->graphListener->nodeCollapsedChanged(this); } } void KisNode::addKeyframeChannel(KisKeyframeChannel *channel) { channel->setNode(this); KisBaseNode::addKeyframeChannel(channel); } KisNodeSP KisNode::firstChild() const { QReadLocker l(&m_d->nodeSubgraphLock); return !m_d->nodes.isEmpty() ? m_d->nodes.first() : 0; } KisNodeSP KisNode::lastChild() const { QReadLocker l(&m_d->nodeSubgraphLock); return !m_d->nodes.isEmpty() ? m_d->nodes.last() : 0; } KisNodeSP KisNode::prevChildImpl(KisNodeSP child) { /** * Warning: mind locking policy! * * The graph locks must be *always* taken in descending * order. That is if you want to (or it implicitly happens that * you) take a lock of a parent and a chil, you must first take * the lock of a parent, and only after that ask a child to do the * same. Otherwise you'll get a deadlock. */ QReadLocker l(&m_d->nodeSubgraphLock); int i = m_d->nodes.indexOf(child) - 1; return i >= 0 ? m_d->nodes.at(i) : 0; } KisNodeSP KisNode::nextChildImpl(KisNodeSP child) { /** * See a comment in KisNode::prevChildImpl() */ QReadLocker l(&m_d->nodeSubgraphLock); int i = m_d->nodes.indexOf(child) + 1; return i > 0 && i < m_d->nodes.size() ? m_d->nodes.at(i) : 0; } KisNodeSP KisNode::prevSibling() const { KisNodeSP parentNode = parent(); return parentNode ? parentNode->prevChildImpl(const_cast(this)) : 0; } KisNodeSP KisNode::nextSibling() const { KisNodeSP parentNode = parent(); return parentNode ? parentNode->nextChildImpl(const_cast(this)) : 0; } quint32 KisNode::childCount() const { QReadLocker l(&m_d->nodeSubgraphLock); return m_d->nodes.size(); } KisNodeSP KisNode::at(quint32 index) const { QReadLocker l(&m_d->nodeSubgraphLock); if (!m_d->nodes.isEmpty() && index < (quint32)m_d->nodes.size()) { return m_d->nodes.at(index); } return 0; } int KisNode::index(const KisNodeSP node) const { QReadLocker l(&m_d->nodeSubgraphLock); return m_d->nodes.indexOf(node); } QList KisNode::childNodes(const QStringList & nodeTypes, const KoProperties & properties) const { QReadLocker l(&m_d->nodeSubgraphLock); QList nodes; KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, m_d->nodes) { if (*iter) { if (properties.isEmpty() || (*iter)->check(properties)) { bool rightType = true; if(!nodeTypes.isEmpty()) { rightType = false; Q_FOREACH (const QString &nodeType, nodeTypes) { if ((*iter)->inherits(nodeType.toLatin1())) { rightType = true; break; } } } if (rightType) { nodes.append(*iter); } } } } return nodes; } KisNodeSP KisNode::findChildByName(const QString &name) { KisNodeSP child = firstChild(); while (child) { if (child->name() == name) { return child; } if (child->childCount() > 0) { KisNodeSP grandChild = child->findChildByName(name); if (grandChild) { return grandChild; } } child = child->nextSibling(); } return 0; } bool KisNode::add(KisNodeSP newNode, KisNodeSP aboveThis) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(newNode, false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!aboveThis || aboveThis->parent().data() == this, false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(allowAsChild(newNode), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!newNode->parent(), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(index(newNode) < 0, false); int idx = aboveThis ? this->index(aboveThis) + 1 : 0; // threoretical race condition may happen here ('idx' may become // deprecated until the write lock will be held). But we ignore // it, because it is not supported to add/remove nodes from two // concurrent threads simultaneously if (m_d->graphListener) { m_d->graphListener->aboutToAddANode(this, idx); } { QWriteLocker l(&m_d->nodeSubgraphLock); newNode->createNodeProgressProxy(); m_d->nodes.insert(idx, newNode); newNode->setParent(this); newNode->setGraphListener(m_d->graphListener); } if (m_d->graphListener) { m_d->graphListener->nodeHasBeenAdded(this, idx); } childNodeChanged(newNode); return true; } bool KisNode::remove(quint32 index) { if (index < childCount()) { KisNodeSP removedNode = at(index); if (m_d->graphListener) { m_d->graphListener->aboutToRemoveANode(this, index); } { QWriteLocker l(&m_d->nodeSubgraphLock); removedNode->setGraphListener(0); removedNode->setParent(0); // after calling aboutToRemoveANode or then the model get broken according to TT's modeltest m_d->nodes.removeAt(index); } if (m_d->graphListener) { m_d->graphListener->nodeHasBeenRemoved(this, index); } childNodeChanged(removedNode); return true; } return false; } bool KisNode::remove(KisNodeSP node) { return node->parent().data() == this ? remove(index(node)) : false; } KisNodeProgressProxy* KisNode::nodeProgressProxy() const { if (m_d->nodeProgressProxy) { return m_d->nodeProgressProxy; } else if (parent()) { return parent()->nodeProgressProxy(); } return 0; } KisBusyProgressIndicator* KisNode::busyProgressIndicator() const { if (m_d->busyProgressIndicator) { return m_d->busyProgressIndicator; } else if (parent()) { return parent()->busyProgressIndicator(); } return 0; } void KisNode::createNodeProgressProxy() { if (!m_d->nodeProgressProxy) { m_d->nodeProgressProxy = new KisNodeProgressProxy(this); m_d->busyProgressIndicator = new KisBusyProgressIndicator(m_d->nodeProgressProxy); m_d->nodeProgressProxy->moveToThread(this->thread()); m_d->busyProgressIndicator->moveToThread(this->thread()); } } void KisNode::setDirty() { setDirty(extent()); } void KisNode::setDirty(const QVector &rects) { if(m_d->graphListener) { m_d->graphListener->requestProjectionUpdate(this, rects, true); } } void KisNode::setDirty(const QRegion ®ion) { setDirty(region.rects()); } void KisNode::setDirty(const QRect & rect) { setDirty(QVector({rect})); } void KisNode::setDirtyDontResetAnimationCache() { setDirtyDontResetAnimationCache(QVector({extent()})); } void KisNode::setDirtyDontResetAnimationCache(const QRect &rect) { setDirtyDontResetAnimationCache(QVector({rect})); } void KisNode::setDirtyDontResetAnimationCache(const QVector &rects) { if(m_d->graphListener) { m_d->graphListener->requestProjectionUpdate(this, rects, false); } } -void KisNode::invalidateFrames(const KisTimeRange &range, const QRect &rect) +void KisNode::invalidateFrames(const KisFrameSet &range, const QRect &rect) { if(m_d->graphListener) { m_d->graphListener->invalidateFrames(range, rect); } } void KisNode::requestTimeSwitch(int time) { if(m_d->graphListener) { m_d->graphListener->requestTimeSwitch(time); } } void KisNode::syncLodCache() { // noop. everything is done by getLodCapableDevices() } KisPaintDeviceList KisNode::getLodCapableDevices() const { KisPaintDeviceList list; KisPaintDeviceSP device = paintDevice(); if (device) { list << device; } KisPaintDeviceSP originalDevice = original(); if (originalDevice && originalDevice != device) { list << originalDevice; } list << projectionPlane()->getLodCapableDevices(); return list; } diff --git a/libs/image/kis_node.h b/libs/image/kis_node.h index 83083ca9d4..62376008f7 100644 --- a/libs/image/kis_node.h +++ b/libs/image/kis_node.h @@ -1,434 +1,434 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * 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_NODE_H #define _KIS_NODE_H #include "kis_types.h" #include "kis_base_node.h" #include "kritaimage_export.h" #include class QRect; class QStringList; class KoProperties; class KisNodeVisitor; class KisNodeGraphListener; class KisNodeProgressProxy; class KisBusyProgressIndicator; class KisAbstractProjectionPlane; class KisProjectionLeaf; class KisKeyframeChannel; -class KisTimeRange; +class KisFrameSet; class KisUndoAdapter; /** * A KisNode is a KisBaseNode that knows about its direct peers, parent * and children and whether it can have children. * * THREAD-SAFETY: All const methods of this class and setDirty calls * are considered to be thread-safe(!). All the others * especially add(), remove() and setParent() must be * protected externally. * * NOTE: your subclasses must have the Q_OBJECT declaration, even if * you do not define new signals or slots. */ class KRITAIMAGE_EXPORT KisNode : public KisBaseNode { friend class KisFilterMaskTest; Q_OBJECT public: /** * The struct describing the position of the node * against the filthy node. * NOTE: please change KisBaseRectsWalker::getPositionToFilthy * when changing this struct */ enum PositionToFilthy { N_ABOVE_FILTHY = 0x08, N_FILTHY_PROJECTION = 0x20, N_FILTHY = 0x40, N_BELOW_FILTHY = 0x80 }; /** * Create an empty node without a parent. */ KisNode(KisImageWSP image); /** * Create a copy of this node. The copy will not have a parent * node. */ KisNode(const KisNode & rhs); /** * Delete this node */ ~KisNode() override; virtual KisNodeSP clone() const = 0; bool accept(KisNodeVisitor &v) override; void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) override; /** * Re-implement this method to add constraints for the * subclasses that can be added as children to this node * * @return false if the given node is not allowed as a child to this node */ virtual bool allowAsChild(KisNodeSP) const = 0; /** * Set the entire node extent dirty; this percolates up to parent * nodes all the way to the root node. By default this is the * empty rect (through KisBaseNode::extent()) */ virtual void setDirty(); /** * Add the given rect to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node. */ void setDirty(const QRect & rect); /** * Add the given rects to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node. */ virtual void setDirty(const QVector &rects); /** * Add the given region to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node, if propagate is true; */ void setDirty(const QRegion ®ion); /** * Convenience override of multirect version of setDirtyDontResetAnimationCache() * * @see setDirtyDontResetAnimationCache(const QVector &rects) */ void setDirtyDontResetAnimationCache(); /** * Convenience override of multirect version of setDirtyDontResetAnimationCache() * * @see setDirtyDontResetAnimationCache(const QVector &rects) */ void setDirtyDontResetAnimationCache(const QRect &rect); /** * @brief setDirtyDontResetAnimationCache does almost the same thing as usual * setDirty() call, but doesn't reset the animation cache (since onlion skins are * not used when rendering animation. */ void setDirtyDontResetAnimationCache(const QVector &rects); /** * Informs that the frames in the given range are no longer valid * and need to be recached. * @param range frames to invalidate */ - void invalidateFrames(const KisTimeRange &range, const QRect &rect); + void invalidateFrames(const KisFrameSet &range, const QRect &rect); /** * Informs that the current world time should be changed. * Might be caused by e.g. undo operation */ void requestTimeSwitch(int time); /** * \return a pointer to a KisAbstractProjectionPlane interface of * the node. This interface is used by the image merging * framework to get information and to blending for the * layer. * * Please note the difference between need/change/accessRect and * the projectionPlane() interface. The former one gives * information about internal composition of the layer, and the * latter one about the total composition, including layer styles, * pass-through blending and etc. */ virtual KisAbstractProjectionPlaneSP projectionPlane() const; /** * Synchronizes LoD caches of the node with the current state of it. * The current level of detail is fetched from the image pointed by * default bounds object */ virtual void syncLodCache(); virtual KisPaintDeviceList getLodCapableDevices() const; /** * The rendering of the image may not always happen in the order * of the main graph. Pass-through nodes make some subgraphs * linear, so it the order of rendering change. projectionLeaf() * is a special interface of KisNode that represents "a graph for * projection rendering". Therefore the nodes in projectionLeaf() * graph may have a different order the main one. */ virtual KisProjectionLeafSP projectionLeaf() const; void setImage(KisImageWSP image) override; protected: /** * \return internal changeRect() of the node. Do not mix with \see * projectionPlane() * * Some filters will cause a change of pixels those are outside * a requested rect. E.g. we change a rect of 2x2, then we want to * apply a convolution filter with kernel 4x4 (changeRect is * (2+2*3)x(2+2*3)=8x8) to that area. The rect that should be updated * on the layer will be exactly 8x8. More than that the needRect for * that update will be 14x14. See \ref needeRect. */ virtual QRect changeRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * \return internal needRect() of the node. Do not mix with \see * projectionPlane() * * Some filters need pixels outside the current processing rect to * compute the new value (for instance, convolution filters) * See \ref changeRect * See \ref accessRect */ virtual QRect needRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * \return internal accessRect() of the node. Do not mix with \see * projectionPlane() * * Shows the area of image, that may be accessed during accessing * the node. * * Example. You have a layer that needs to prepare some rect on a * projection, say expectedRect. To perform this, the projection * of all the layers below of the size needRect(expectedRect) * should be calculated by the merger beforehand and the layer * will access some other area of image inside the rect * accessRect(expectedRect) during updateProjection call. * * This knowledge about real access rect of a node is used by the * scheduler to avoid collisions between two multithreaded updaters * and so avoid flickering of the image. * * Currently, this method has nondefault value for shifted clone * layers only. */ virtual QRect accessRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * Called each time direct child nodes are added or removed under this * node as parent. This does not track changes inside the child nodes * or the child nodes' properties. */ virtual void childNodeChanged(KisNodeSP changedChildNode); public: // Graph methods /** * @return the graph sequence number calculated by the associated * graph listener. You can use it for checking for changes in the * graph. */ int graphSequenceNumber() const; /** * @return the graph listener this node belongs to. 0 if the node * does not belong to a grap listener. */ KisNodeGraphListener * graphListener() const; /** * Set the graph listener for this node. The graphlistener will be * informed before and after the list of child nodes has changed. */ void setGraphListener(KisNodeGraphListener * graphListener); /** * Returns the parent node of this node. This is 0 only for a root * node; otherwise this will be an actual Node */ KisNodeSP parent() const; /** * Returns the first child node of this node, or 0 if there are no * child nodes. */ KisNodeSP firstChild() const; /** * Returns the last child node of this node, or 0 if there are no * child nodes. */ KisNodeSP lastChild() const; /** * Returns the previous sibling of this node in the parent's list. * This is the node *above* this node in the composition stack. 0 * is returned if this child has no more previous siblings (== * firstChild()) */ KisNodeSP prevSibling() const; /** * Returns the next sibling of this node in the parent's list. * This is the node *below* this node in the composition stack. 0 * is returned if this child has no more next siblings (== * lastChild()) */ KisNodeSP nextSibling() const; /** * Returns how many direct child nodes this node has (not * recursive). */ quint32 childCount() const; /** * Retrieve the child node at the specified index. * * @return 0 if there is no node at this index. */ KisNodeSP at(quint32 index) const; /** * Retrieve the index of the specified child node. * * @return -1 if the specified node is not a child node of this * node. */ int index(const KisNodeSP node) const; /** * Return a list of child nodes of the current node that conform * to the specified constraints. There are no guarantees about the * order of the nodes in the list. The function is not recursive. * * @param nodeTypes. if not empty, only nodes that inherit the * classnames in this stringlist will be returned. * @param properties. if not empty, only nodes for which * KisNodeBase::check(properties) returns true will be returned. */ QList childNodes(const QStringList & nodeTypes, const KoProperties & properties) const; /** * @brief findChildByName finds the first child that has the given name * @param name the name to look for * @return the first child with the given name */ KisNodeSP findChildByName(const QString &name); Q_SIGNALS: /** * Don't use this signal anywhere other than KisNodeShape. It's a hack. */ void sigNodeChangedInternal(); public: /** * @return the node progress proxy used by this node, if this node has no progress * proxy, it will return the proxy of its parent, if the parent has no progress proxy * it will return 0 */ KisNodeProgressProxy* nodeProgressProxy() const; KisBusyProgressIndicator* busyProgressIndicator() const; private: /** * Create a node progress proxy for this node. You need to create a progress proxy only * if the node is going to appear in the layerbox, and it needs to be created before * the layer box is made aware of the proxy. */ void createNodeProgressProxy(); protected: KisBaseNodeSP parentCallback() const override; void notifyParentVisibilityChanged(bool value) override; void baseNodeChangedCallback() override; void baseNodeInvalidateAllFramesCallback() override; void baseNodeCollapsedChangedCallback() override; protected: void addKeyframeChannel(KisKeyframeChannel* channel) override; private: friend class KisNodeFacade; friend class KisNodeTest; friend class KisLayer; // Note: only for setting the preview mask! /** * Set the parent of this node. */ void setParent(KisNodeWSP parent); /** * Add the specified node above the specified node. If aboveThis * is 0, the node is added at the bottom. */ bool add(KisNodeSP newNode, KisNodeSP aboveThis); /** * Removes the node at the specified index from the child nodes. * * @return false if there is no node at this index */ bool remove(quint32 index); /** * Removes the node from the child nodes. * * @return false if there's no such node in this node. */ bool remove(KisNodeSP node); KisNodeSP prevChildImpl(KisNodeSP child); KisNodeSP nextChildImpl(KisNodeSP child); private: struct Private; Private * const m_d; }; Q_DECLARE_METATYPE(KisNodeSP) Q_DECLARE_METATYPE(KisNodeWSP) #endif diff --git a/libs/image/kis_node_graph_listener.cpp b/libs/image/kis_node_graph_listener.cpp index f43542ced4..f773bcf761 100644 --- a/libs/image/kis_node_graph_listener.cpp +++ b/libs/image/kis_node_graph_listener.cpp @@ -1,111 +1,111 @@ /* * Copyright (c) 2007 Boudewijn Rempt * Copyright (c) 2012 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_node_graph_listener.h" #include "kis_time_range.h" #include #include struct Q_DECL_HIDDEN KisNodeGraphListener::Private { Private() : sequenceNumber(0) {} int sequenceNumber; }; KisNodeGraphListener::KisNodeGraphListener() : m_d(new Private()) { } KisNodeGraphListener::~KisNodeGraphListener() { } void KisNodeGraphListener::aboutToAddANode(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::nodeHasBeenAdded(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::aboutToRemoveANode(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::nodeHasBeenRemoved(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::aboutToMoveNode(KisNode * /*node*/, int /*oldIndex*/, int /*newIndex*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::nodeHasBeenMoved(KisNode * /*node*/, int /*oldIndex*/, int /*newIndex*/) { m_d->sequenceNumber++; } int KisNodeGraphListener::graphSequenceNumber() const { return m_d->sequenceNumber; } void KisNodeGraphListener::nodeChanged(KisNode * /*node*/) { } void KisNodeGraphListener::nodeCollapsedChanged(KisNode * /*node*/) { } void KisNodeGraphListener::invalidateAllFrames() { } void KisNodeGraphListener::notifySelectionChanged() { } void KisNodeGraphListener::requestProjectionUpdate(KisNode * /*node*/, const QVector &/*rects*/, bool /*resetAnimationCache*/) { } -void KisNodeGraphListener::invalidateFrames(const KisTimeRange &range, const QRect &rect) +void KisNodeGraphListener::invalidateFrames(const KisFrameSet &range, const QRect &rect) { Q_UNUSED(range); Q_UNUSED(rect); } void KisNodeGraphListener::requestTimeSwitch(int time) { Q_UNUSED(time); } KisNode *KisNodeGraphListener::graphOverlayNode() const { return 0; } diff --git a/libs/image/kis_node_graph_listener.h b/libs/image/kis_node_graph_listener.h index 1dfd217533..bd6bf90572 100644 --- a/libs/image/kis_node_graph_listener.h +++ b/libs/image/kis_node_graph_listener.h @@ -1,127 +1,127 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * 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_NODE_GRAPH_LISTENER_H_ #define KIS_NODE_GRAPH_LISTENER_H_ #include "kritaimage_export.h" #include -class KisTimeRange; +class KisFrameSet; class KisNode; class QRect; /** * Implementations of this class are called by nodes whenever the node * graph changes. These implementations can then emit the right * signals so Qt interview models can be updated before and after * changes. * * The reason for this go-between is that we don't want our nodes to * be QObjects, nor to have sig-slot connections between every node * and every mode. * * It also manages the sequence number of the graph. This is a number * which can be used as a checksum for whether the graph has changed * from some period of time or not. \see graphSequenceNumber() */ class KRITAIMAGE_EXPORT KisNodeGraphListener { public: KisNodeGraphListener(); virtual ~KisNodeGraphListener(); /** * Inform the model that we're going to add a node. */ virtual void aboutToAddANode(KisNode *parent, int index); /** * Inform the model we're done adding a node. */ virtual void nodeHasBeenAdded(KisNode *parent, int index); /** * Inform the model we're going to remove a node. */ virtual void aboutToRemoveANode(KisNode *parent, int index); /** * Inform the model we're done removing a node. */ virtual void nodeHasBeenRemoved(KisNode *parent, int index); /** * Inform the model we're about to start moving a node (which * includes removing and adding the same node) */ virtual void aboutToMoveNode(KisNode * node, int oldIndex, int newIndex); /** * Inform the model we're done moving the node: it has been * removed and added successfully */ virtual void nodeHasBeenMoved(KisNode * node, int oldIndex, int newIndex); virtual void nodeChanged(KisNode * node); virtual void nodeCollapsedChanged(KisNode * node); virtual void invalidateAllFrames(); /** * Inform the model that one of the selections in the graph is * changed. The sender is not passed to the function (at least for * now) because the UI should decide itself whether it needs to * fetch new selection of not. */ virtual void notifySelectionChanged(); /** * Inform the model that a node has been changed (setDirty) */ virtual void requestProjectionUpdate(KisNode * node, const QVector &rects, bool resetAnimationCache); - virtual void invalidateFrames(const KisTimeRange &range, const QRect &rect); + virtual void invalidateFrames(const KisFrameSet &range, const QRect &rect); virtual void requestTimeSwitch(int time); virtual KisNode* graphOverlayNode() const; /** * Returns the sequence of the graph. * * Every time some operation performed, which might change the * hierarchy of the nodes, the sequence number grows by one. So * if you have any information about the graph which was acquired * when the sequence number was X and now it has become Y, it * means your information is outdated. * * It is used in the scheduler for checking whether queued walkers * should be regenerated. */ int graphSequenceNumber() const; private: struct Private; QScopedPointer m_d; }; #endif diff --git a/libs/image/kis_onion_skin_compositor.cpp b/libs/image/kis_onion_skin_compositor.cpp index 9e408005e7..97d7626e23 100644 --- a/libs/image/kis_onion_skin_compositor.cpp +++ b/libs/image/kis_onion_skin_compositor.cpp @@ -1,227 +1,226 @@ /* * 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_onion_skin_compositor.h" #include "kis_paint_device.h" #include "kis_painter.h" #include "KoColor.h" #include "KoColorSpace.h" #include "KoCompositeOpRegistry.h" #include "KoColorSpaceConstants.h" #include "kis_image_config.h" #include "kis_raster_keyframe_channel.h" Q_GLOBAL_STATIC(KisOnionSkinCompositor, s_instance) struct KisOnionSkinCompositor::Private { int numberOfSkins = 0; int tintFactor = 0; QColor backwardTintColor; QColor forwardTintColor; QVector backwardOpacities; QVector forwardOpacities; int configSeqNo = 0; QList colorLabelFilter; int skinOpacity(int offset) { const QVector &bo = backwardOpacities; const QVector &fo = forwardOpacities; return offset > 0 ? fo[qAbs(offset) - 1] : bo[qAbs(offset) - 1]; } KisPaintDeviceSP setUpTintDevice(const QColor &tintColor, const KoColorSpace *colorSpace) { KisPaintDeviceSP tintDevice = new KisPaintDevice(colorSpace); KoColor color = KoColor(tintColor, colorSpace); tintDevice->setDefaultPixel(color); return tintDevice; } - KisKeyframeSP getNextFrameToComposite(KisKeyframeChannel *channel, KisKeyframeSP keyframe, bool backwards) + KisVisibleKeyframeIterator getNextFrameToComposite(KisVisibleKeyframeIterator keyframe, bool backwards) { - while (!keyframe.isNull()) { - keyframe = backwards ? channel->previousKeyframe(keyframe) : channel->nextKeyframe(keyframe); + while (keyframe.isValid()) { + keyframe = backwards ? --keyframe : ++keyframe; if (colorLabelFilter.isEmpty()) { return keyframe; - } else if (!keyframe.isNull()) { + } else if (keyframe.isValid()) { if (colorLabelFilter.contains(keyframe->colorLabel())) { return keyframe; } } } return keyframe; } void tryCompositeFrame(KisRasterKeyframeChannel *keyframes, KisKeyframeSP keyframe, KisPainter &gcFrame, KisPainter &gcDest, KisPaintDeviceSP tintSource, int opacity, const QRect &rect) { if (keyframe.isNull() || opacity == OPACITY_TRANSPARENT_U8) return; keyframes->fetchFrame(keyframe, gcFrame.device()); gcFrame.bitBlt(rect.topLeft(), tintSource, rect); gcDest.setOpacity(opacity); gcDest.bitBlt(rect.topLeft(), gcFrame.device(), rect); } void refreshConfig() { KisImageConfig config(true); numberOfSkins = config.numberOfOnionSkins(); tintFactor = config.onionSkinTintFactor(); backwardTintColor = config.onionSkinTintColorBackward(); forwardTintColor = config.onionSkinTintColorForward(); backwardOpacities.resize(numberOfSkins); forwardOpacities.resize(numberOfSkins); const int mainState = (int) config.onionSkinState(0); const qreal scaleFactor = mainState * config.onionSkinOpacity(0) / 255.0; for (int i = 0; i < numberOfSkins; i++) { int backwardState = (int) config.onionSkinState(-(i + 1)); int forwardState = (int) config.onionSkinState(i + 1); backwardOpacities[i] = scaleFactor * backwardState * config.onionSkinOpacity(-(i + 1)); forwardOpacities[i] = scaleFactor * forwardState * config.onionSkinOpacity(i + 1); } configSeqNo++; } }; KisOnionSkinCompositor *KisOnionSkinCompositor::instance() { return s_instance; } KisOnionSkinCompositor::KisOnionSkinCompositor() : m_d(new Private) { m_d->refreshConfig(); } KisOnionSkinCompositor::~KisOnionSkinCompositor() {} int KisOnionSkinCompositor::configSeqNo() const { return m_d->configSeqNo; } void KisOnionSkinCompositor::setColorLabelFilter(QList colors) { m_d->colorLabelFilter = colors; } void KisOnionSkinCompositor::composite(const KisPaintDeviceSP sourceDevice, KisPaintDeviceSP targetDevice, const QRect& rect) { KisRasterKeyframeChannel *keyframes = sourceDevice->keyframeChannel(); KisPaintDeviceSP frameDevice = new KisPaintDevice(sourceDevice->colorSpace()); KisPainter gcFrame(frameDevice); QBitArray channelFlags = targetDevice->colorSpace()->channelFlags(true, false); gcFrame.setChannelFlags(channelFlags); gcFrame.setOpacity(m_d->tintFactor); KisPaintDeviceSP backwardTintDevice = m_d->setUpTintDevice(m_d->backwardTintColor, sourceDevice->colorSpace()); KisPaintDeviceSP forwardTintDevice = m_d->setUpTintDevice(m_d->forwardTintColor, sourceDevice->colorSpace()); KisPainter gcDest(targetDevice); gcDest.setCompositeOp(sourceDevice->colorSpace()->compositeOp(COMPOSITE_BEHIND)); - KisKeyframeSP keyframeBck; - KisKeyframeSP keyframeFwd; - - int time = sourceDevice->defaultBounds()->currentTime(); - keyframeBck = keyframeFwd = keyframes->activeKeyframeAt(time); + const int time = sourceDevice->defaultBounds()->currentTime(); + KisVisibleKeyframeIterator backward = keyframes->visibleKeyframesFrom(time); + KisVisibleKeyframeIterator forward = backward; for (int offset = 1; offset <= m_d->numberOfSkins; offset++) { - keyframeBck = m_d->getNextFrameToComposite(keyframes, keyframeBck, true); - keyframeFwd = m_d->getNextFrameToComposite(keyframes, keyframeFwd, false); + backward = m_d->getNextFrameToComposite(backward, true); + forward = m_d->getNextFrameToComposite(forward, false); - if (!keyframeBck.isNull()) { - m_d->tryCompositeFrame(keyframes, keyframeBck, gcFrame, gcDest, backwardTintDevice, m_d->skinOpacity(-offset), rect); + if (backward.isValid()) { + m_d->tryCompositeFrame(keyframes, *backward, gcFrame, gcDest, backwardTintDevice, m_d->skinOpacity(-offset), rect); } - if (!keyframeFwd.isNull()) { - m_d->tryCompositeFrame(keyframes, keyframeFwd, gcFrame, gcDest, forwardTintDevice, m_d->skinOpacity(offset), rect); + if (forward.isValid()) { + m_d->tryCompositeFrame(keyframes, *forward, gcFrame, gcDest, forwardTintDevice, m_d->skinOpacity(offset), rect); } } } QRect KisOnionSkinCompositor::calculateFullExtent(const KisPaintDeviceSP device) { QRect rect; KisRasterKeyframeChannel *channel = device->keyframeChannel(); if (!channel) return rect; KisKeyframeSP keyframe = channel->firstKeyframe(); while (keyframe) { rect |= channel->frameExtents(keyframe); keyframe = channel->nextKeyframe(keyframe); } return rect; } QRect KisOnionSkinCompositor::calculateExtent(const KisPaintDeviceSP device) { QRect rect; KisKeyframeSP keyframeBck; KisKeyframeSP keyframeFwd; KisRasterKeyframeChannel *channel = device->keyframeChannel(); - keyframeBck = keyframeFwd = channel->activeKeyframeAt(device->defaultBounds()->currentTime()); + KisVisibleKeyframeIterator forward = channel->visibleKeyframesFrom(device->defaultBounds()->currentTime()); + KisVisibleKeyframeIterator backward = forward; for (int offset = 1; offset <= m_d->numberOfSkins; offset++) { - if (!keyframeBck.isNull()) { - keyframeBck = channel->previousKeyframe(keyframeBck); + if (backward.isValid()) { + --backward; if (!keyframeBck.isNull()) { - rect |= channel->frameExtents(keyframeBck); + rect |= channel->frameExtents(*backward); } } - if (!keyframeFwd.isNull()) { - keyframeFwd = channel->nextKeyframe(keyframeFwd); + if (forward.isValid()) { + ++forward; - if (!keyframeFwd.isNull()) { - rect |= channel->frameExtents(keyframeFwd); + if (forward.isValid()) { + rect |= channel->frameExtents(*forward); } } } return rect; } void KisOnionSkinCompositor::configChanged() { m_d->refreshConfig(); emit sigOnionSkinChanged(); } diff --git a/libs/image/kis_raster_keyframe_channel.cpp b/libs/image/kis_raster_keyframe_channel.cpp index 8c03273c6e..6e078e4e54 100644 --- a/libs/image/kis_raster_keyframe_channel.cpp +++ b/libs/image/kis_raster_keyframe_channel.cpp @@ -1,314 +1,378 @@ /* * 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_raster_keyframe_channel.h" #include "kis_node.h" #include "kis_dom_utils.h" #include "kis_global.h" #include "kis_paint_device.h" #include "kis_paint_device_frames_interface.h" #include "kis_time_range.h" #include "kundo2command.h" #include "kis_onion_skin_compositor.h" +#include "kis_keyframe_commands.h" + +struct KisRasterKeyframeChannel::Private +{ + Private(KisPaintDeviceWSP paintDevice, const QString filenameSuffix) + : paintDevice(paintDevice), + filenameSuffix(filenameSuffix), + onionSkinsEnabled(false) + {} + + KisPaintDeviceWSP paintDevice; + QMap framesByFilename; + QMap frameFilenames; + QMap> frameInstances; + QString filenameSuffix; + bool onionSkinsEnabled; +}; class KisRasterKeyframe : public KisKeyframe { public: KisRasterKeyframe(KisRasterKeyframeChannel *channel, int time, int frameId) : KisKeyframe(channel, time) , frameId(frameId) {} KisRasterKeyframe(const KisRasterKeyframe *rhs, KisKeyframeChannel *channel) : KisKeyframe(rhs, channel) , frameId(rhs->frameId) {} int frameId; KisKeyframeSP cloneFor(KisKeyframeChannel *channel) const override { return toQShared(new KisRasterKeyframe(this, channel)); } bool hasContent() const override { KisRasterKeyframeChannel *channel = dynamic_cast(this->channel()); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(channel, true); return channel->keyframeHasContent(this); } -}; -struct KisRasterKeyframeChannel::Private -{ - Private(KisPaintDeviceWSP paintDevice, const QString filenameSuffix) - : paintDevice(paintDevice), - filenameSuffix(filenameSuffix), - onionSkinsEnabled(false) - {} - - KisPaintDeviceWSP paintDevice; - QMap frameFilenames; - QString filenameSuffix; - bool onionSkinsEnabled; + QRect affectedRect() const override + { + KisRasterKeyframeChannel *ch = dynamic_cast(channel()); + KisRasterKeyframeChannel::Private *ch_d = ch->m_d.data(); + + // Calculate changed area as the union of the current and previous keyframe. + // This makes sure there are no artifacts left over from the previous frame + // where the new one doesn't cover the area. + KisKeyframeSP neighbor = ch->previousKeyframe(time()); + + // Using the *next* keyframe at the start of the timeline avoids artifacts + // when deleting or moving the first key + if (neighbor.isNull()) neighbor = ch->nextKeyframe(time()); + + QRect rect = ch_d->paintDevice->framesInterface()->frameBounds(frameId); + + if (!neighbor.isNull()) { + // Note: querying through frameIdAt makes sure cycle repeats resolve to their original frames + const int neighborFrameId = ch->frameIdAt(neighbor->time()); + rect |= ch_d->paintDevice->framesInterface()->frameBounds(neighborFrameId); + } + + if (ch_d->onionSkinsEnabled) { + const QRect dirtyOnionSkinsRect = + KisOnionSkinCompositor::instance()->calculateFullExtent(ch_d->paintDevice); + rect |= dirtyOnionSkinsRect; + } + + return rect; + } + }; KisRasterKeyframeChannel::KisRasterKeyframeChannel(const KoID &id, const KisPaintDeviceWSP paintDevice, KisDefaultBoundsBaseSP defaultBounds) : KisKeyframeChannel(id, defaultBounds), m_d(new Private(paintDevice, QString())) { } KisRasterKeyframeChannel::KisRasterKeyframeChannel(const KisRasterKeyframeChannel &rhs, KisNode *newParentNode, const KisPaintDeviceWSP newPaintDevice) : KisKeyframeChannel(rhs, newParentNode), m_d(new Private(newPaintDevice, rhs.m_d->filenameSuffix)) { KIS_ASSERT_RECOVER_NOOP(&rhs != this); m_d->frameFilenames = rhs.m_d->frameFilenames; m_d->onionSkinsEnabled = rhs.m_d->onionSkinsEnabled; } KisRasterKeyframeChannel::~KisRasterKeyframeChannel() { } +KisKeyframeSP KisRasterKeyframeChannel::linkKeyframe(const KisKeyframeBaseSP source, int newTime, KUndo2Command *parentCommand) +{ + KisKeyframeSP sourceKeyframe = source.dynamicCast(); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(sourceKeyframe, KisKeyframeSP()); + + const int frame = frameId(sourceKeyframe); + KisKeyframeSP newKeyframe = toQShared(new KisRasterKeyframe(this, newTime, frame)); + m_d->frameInstances[frame].append(newKeyframe); + + KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newTime, newKeyframe, parentCommand); + cmd->redo(); + + return newKeyframe; +} + int KisRasterKeyframeChannel::frameId(KisKeyframeSP keyframe) const { return frameId(keyframe.data()); } int KisRasterKeyframeChannel::frameId(const KisKeyframe *keyframe) const { const KisRasterKeyframe *key = dynamic_cast(keyframe); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(key, -1); return key->frameId; } int KisRasterKeyframeChannel::frameIdAt(int time) const { - KisKeyframeSP activeKey = activeKeyframeAt(time); + KisKeyframeSP activeKey = visibleKeyframeAt(time); if (activeKey.isNull()) return -1; return frameId(activeKey); } void KisRasterKeyframeChannel::fetchFrame(KisKeyframeSP keyframe, KisPaintDeviceSP targetDevice) { m_d->paintDevice->framesInterface()->fetchFrame(frameId(keyframe), targetDevice); } void KisRasterKeyframeChannel::importFrame(int time, KisPaintDeviceSP sourceDevice, KUndo2Command *parentCommand) { KisKeyframeSP keyframe = addKeyframe(time, parentCommand); const int frame = frameId(keyframe); m_d->paintDevice->framesInterface()->uploadFrame(frame, sourceDevice); } QRect KisRasterKeyframeChannel::frameExtents(KisKeyframeSP keyframe) { return m_d->paintDevice->framesInterface()->frameBounds(frameId(keyframe)); } QString KisRasterKeyframeChannel::frameFilename(int frameId) const { return m_d->frameFilenames.value(frameId, QString()); } void KisRasterKeyframeChannel::setFilenameSuffix(const QString &suffix) { m_d->filenameSuffix = suffix; } void KisRasterKeyframeChannel::setFrameFilename(int frameId, const QString &filename) { - Q_ASSERT(!m_d->frameFilenames.contains(frameId)); + KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->frameFilenames.contains(frameId)); m_d->frameFilenames.insert(frameId, filename); + m_d->framesByFilename.insert(filename, frameId); } QString KisRasterKeyframeChannel::chooseFrameFilename(int frameId, const QString &layerFilename) { QString filename; if (m_d->frameFilenames.isEmpty()) { // Use legacy naming convention for first keyframe filename = layerFilename + m_d->filenameSuffix; } else { filename = layerFilename + m_d->filenameSuffix + ".f" + QString::number(frameId); } setFrameFilename(frameId, filename); return filename; } KisKeyframeSP KisRasterKeyframeChannel::createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) { KisRasterKeyframe *keyframe; - if (!copySrc) { - int frameId = m_d->paintDevice->framesInterface()->createFrame(false, 0, QPoint(), parentCommand); + const bool copy = !copySrc.isNull(); + const int srcFrameId = copy ? frameId(copySrc) : 0; + const int frameId = m_d->paintDevice->framesInterface()->createFrame(copy, srcFrameId, QPoint(), parentCommand); + + if (!copy) { keyframe = new KisRasterKeyframe(this, time, frameId); } else { - int srcFrame = frameId(copySrc); - int frameId = m_d->paintDevice->framesInterface()->createFrame(true, srcFrame, QPoint(), parentCommand); + const KisRasterKeyframe *srcKeyframe = dynamic_cast(copySrc.data()); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcKeyframe, KisKeyframeSP()); - KisRasterKeyframe *srcKeyframe = dynamic_cast(copySrc.data()); - Q_ASSERT(srcKeyframe); keyframe = new KisRasterKeyframe(srcKeyframe, this); - keyframe->setTime(time); keyframe->frameId = frameId; } - return toQShared(keyframe); + KisKeyframeSP keyframeSP = toQShared(keyframe); + + m_d->frameInstances[keyframe->frameId].append(keyframeSP); + + return keyframeSP; } -void KisRasterKeyframeChannel::destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) +void KisRasterKeyframeChannel::destroyKeyframe(KisKeyframeSP keyframe, KUndo2Command *parentCommand) { - m_d->paintDevice->framesInterface()->deleteFrame(frameId(key), parentCommand); + int id = frameId(keyframe); + + QVector &instances = m_d->frameInstances[id]; + instances.removeAll(keyframe); + + if (instances.isEmpty()) { + m_d->frameInstances.remove(id); + m_d->paintDevice->framesInterface()->deleteFrame(id, parentCommand); + } } void KisRasterKeyframeChannel::uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) { KisRasterKeyframeChannel *srcRasterChannel = dynamic_cast(srcChannel); KIS_ASSERT_RECOVER_RETURN(srcRasterChannel); const int srcId = srcRasterChannel->frameIdAt(srcTime); const int dstId = frameId(dstFrame); m_d->paintDevice->framesInterface()-> uploadFrame(srcId, dstId, srcRasterChannel->m_d->paintDevice); } -QRect KisRasterKeyframeChannel::affectedRect(KisKeyframeSP key) -{ - KeyframesMap::iterator it = keys().find(key->time()); - QRect rect; - - // Calculate changed area as the union of the current and previous keyframe. - // This makes sure there are no artifacts left over from the previous frame - // where the new one doesn't cover the area. - - if (it == keys().begin()) { - // Using the *next* keyframe at the start of the timeline avoids artifacts - // when deleting or moving the first key - it++; - } else { - it--; - } - - if (it != keys().end()) { - rect = m_d->paintDevice->framesInterface()->frameBounds(frameId(it.value())); - } - - rect |= m_d->paintDevice->framesInterface()->frameBounds(frameId(key)); - - if (m_d->onionSkinsEnabled) { - const QRect dirtyOnionSkinsRect = - KisOnionSkinCompositor::instance()->calculateFullExtent(m_d->paintDevice); - rect |= dirtyOnionSkinsRect; - } - - return rect; -} - QDomElement KisRasterKeyframeChannel::toXML(QDomDocument doc, const QString &layerFilename) { m_d->frameFilenames.clear(); return KisKeyframeChannel::toXML(doc, layerFilename); } void KisRasterKeyframeChannel::loadXML(const QDomElement &channelNode) { m_d->frameFilenames.clear(); KisKeyframeChannel::loadXML(channelNode); } void KisRasterKeyframeChannel::saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) { int frame = frameId(keyframe); QString filename = frameFilename(frame); if (filename.isEmpty()) { filename = chooseFrameFilename(frame, layerFilename); } keyframeElement.setAttribute("frame", filename); QPoint offset = m_d->paintDevice->framesInterface()->frameOffset(frame); KisDomUtils::saveValue(&keyframeElement, "offset", offset); } KisKeyframeSP KisRasterKeyframeChannel::loadKeyframe(const QDomElement &keyframeNode) { int time = keyframeNode.attribute("time").toInt(); workaroundBrokenFrameTimeBug(&time); QPoint offset; KisDomUtils::loadValue(keyframeNode, "offset", &offset); QString frameFilename = keyframeNode.attribute("frame"); KisKeyframeSP keyframe; if (m_d->frameFilenames.isEmpty()) { // First keyframe loaded: use the existing frame KIS_SAFE_ASSERT_RECOVER_NOOP(keyframeCount() == 1); keyframe = constKeys().begin().value(); + const int id = frameId(keyframe); + setFrameFilename(id, frameFilename); // Remove from keys. It will get reinserted with new time once we return keys().remove(keyframe->time()); keyframe->setTime(time); - m_d->paintDevice->framesInterface()->setFrameOffset(frameId(keyframe), offset); + m_d->paintDevice->framesInterface()->setFrameOffset(id, offset); } else { - KUndo2Command tempCommand; - int frameId = m_d->paintDevice->framesInterface()->createFrame(false, 0, offset, &tempCommand); + int frameId = m_d->framesByFilename.value(frameFilename, -1); + + if (frameId == -1) { + KUndo2Command tempCommand; + frameId = m_d->paintDevice->framesInterface()->createFrame(false, 0, offset, &tempCommand); + setFrameFilename(frameId, frameFilename); + } keyframe = toQShared(new KisRasterKeyframe(this, time, frameId)); + m_d->frameInstances[frameId].append(keyframe); } - setFrameFilename(frameId(keyframe), frameFilename); - return keyframe; } bool KisRasterKeyframeChannel::keyframeHasContent(const KisKeyframe *keyframe) const { return !m_d->paintDevice->framesInterface()->frameBounds(frameId(keyframe)).isEmpty(); } bool KisRasterKeyframeChannel::hasScalarValue() const { return false; } +KisFrameSet KisRasterKeyframeChannel::affectedFrames(int time) const +{ + const int frameId = frameIdAt(time); + + if (frameId < 0) { + // Not a raster frame (e.g. repeat of a cycle) + return KisKeyframeChannel::affectedFrames(time); + } + + KisFrameSet frames; + Q_FOREACH(KisKeyframeSP keyframe, m_d->frameInstances[frameId]) { + frames |= KisKeyframeChannel::affectedFrames(keyframe->time()); + } + return frames; +} + +KisFrameSet KisRasterKeyframeChannel::identicalFrames(int time, KisTimeSpan range) const +{ + const int frameId = frameIdAt(time); + + KisFrameSet frames; + Q_FOREACH(KisKeyframeSP keyframe, m_d->frameInstances[frameId]) { + frames |= KisKeyframeChannel::identicalFrames(keyframe->time(), range); + } + return frames; +} + void KisRasterKeyframeChannel::setOnionSkinsEnabled(bool value) { m_d->onionSkinsEnabled = value; } bool KisRasterKeyframeChannel::onionSkinsEnabled() const { return m_d->onionSkinsEnabled; } diff --git a/libs/image/kis_raster_keyframe_channel.h b/libs/image/kis_raster_keyframe_channel.h index c2a791f21e..b8def578cb 100644 --- a/libs/image/kis_raster_keyframe_channel.h +++ b/libs/image/kis_raster_keyframe_channel.h @@ -1,96 +1,100 @@ /* * 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_RASTER_KEYFRAME_CHANNEL_H #define _KIS_RASTER_KEYFRAME_CHANNEL_H #include "kis_keyframe_channel.h" +class KisTimeSpan; + class KRITAIMAGE_EXPORT KisRasterKeyframeChannel : public KisKeyframeChannel { Q_OBJECT public: KisRasterKeyframeChannel(const KoID& id, const KisPaintDeviceWSP paintDevice, KisDefaultBoundsBaseSP defaultBounds); KisRasterKeyframeChannel(const KisRasterKeyframeChannel &rhs, KisNode *newParentNode, const KisPaintDeviceWSP newPaintDevice); ~KisRasterKeyframeChannel() override; + KisKeyframeSP linkKeyframe(const KisKeyframeBaseSP keyframe, int newTime, KUndo2Command *parentCommand = 0) override; + public: /** * Return the ID of the active frame at a given time. The active frame is * defined by the keyframe at the given time or the last keyframe before it. * @param time * @return active frame id */ int frameIdAt(int time) const; /** * Copy the active frame at given time to target device. * @param keyframe keyframe to copy from * @param targetDevice device to copy the frame to */ void fetchFrame(KisKeyframeSP keyframe, KisPaintDeviceSP targetDevice); /** * Copy the content of the sourceDevice into a new keyframe at given time * @param time position of new keyframe * @param sourceDevice source for content * @param parentCommand parent command used for stacking */ void importFrame(int time, KisPaintDeviceSP sourceDevice, KUndo2Command *parentCommand); QRect frameExtents(KisKeyframeSP keyframe); QString frameFilename(int frameId) const; /** * When choosing filenames for frames, this will be appended to the node filename */ void setFilenameSuffix(const QString &suffix); bool hasScalarValue() const override; + KisFrameSet affectedFrames(int time) const override; + KisFrameSet identicalFrames(int time, KisTimeSpan range) const override; QDomElement toXML(QDomDocument doc, const QString &layerFilename) override; void loadXML(const QDomElement &channelNode) override; void setOnionSkinsEnabled(bool value); bool onionSkinsEnabled() const; protected: KisKeyframeSP createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) override; - void destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) override; + void destroyKeyframe(KisKeyframeSP keyframe, KUndo2Command *parentCommand) override; void uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) override; - QRect affectedRect(KisKeyframeSP key) override; - void saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) override; KisKeyframeSP loadKeyframe(const QDomElement &keyframeNode) override; friend class KisRasterKeyframe; bool keyframeHasContent(const KisKeyframe *keyframe) const; private: void setFrameFilename(int frameId, const QString &filename); QString chooseFrameFilename(int frameId, const QString &layerFilename); int frameId(KisKeyframeSP keyframe) const; int frameId(const KisKeyframe *keyframe) const; struct Private; QScopedPointer m_d; }; #endif diff --git a/libs/image/kis_scalar_keyframe_channel.cpp b/libs/image/kis_scalar_keyframe_channel.cpp index 2cc74ea0e0..2f8d23432f 100644 --- a/libs/image/kis_scalar_keyframe_channel.cpp +++ b/libs/image/kis_scalar_keyframe_channel.cpp @@ -1,489 +1,488 @@ /* * 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_scalar_keyframe_channel.h" #include "kis_node.h" #include "kundo2command.h" #include "kis_time_range.h" #include #include struct KisScalarKeyframe : public KisKeyframe { KisScalarKeyframe(KisKeyframeChannel *channel, int time, qreal value) : KisKeyframe(channel, time) , value(value) {} KisScalarKeyframe(const KisScalarKeyframe *rhs, KisKeyframeChannel *channel) : KisKeyframe(rhs, channel) , value(rhs->value) {} qreal value; + QRect affectedRect() const override { + const KisNodeSP node = channel()->node().toStrongRef(); + + if (node) { + return node->extent(); + } else { + return QRect(); + } + } + KisKeyframeSP cloneFor(KisKeyframeChannel *channel) const override { return toQShared(new KisScalarKeyframe(this, channel)); } }; KisScalarKeyframeChannel::AddKeyframeCommand::AddKeyframeCommand(KisScalarKeyframeChannel *channel, int time, qreal value, KUndo2Command *parentCommand) : KisReplaceKeyframeCommand(channel, time, channel->createKeyframe(time, value, parentCommand), parentCommand) {} struct KisScalarKeyframeChannel::Private { public: Private(qreal min, qreal max, KisKeyframe::InterpolationMode defaultInterpolation) : minValue(min), maxValue(max), firstFreeIndex(0), defaultInterpolation(defaultInterpolation) {} Private(const Private &rhs) : minValue(rhs.minValue), maxValue(rhs.maxValue), firstFreeIndex(rhs.firstFreeIndex), defaultInterpolation(rhs.defaultInterpolation) {} qreal minValue; qreal maxValue; int firstFreeIndex; KisKeyframe::InterpolationMode defaultInterpolation; struct SetValueCommand; struct SetTangentsCommand; struct SetInterpolationModeCommand; }; KisScalarKeyframeChannel::KisScalarKeyframeChannel(const KoID &id, qreal minValue, qreal maxValue, KisDefaultBoundsBaseSP defaultBounds, KisKeyframe::InterpolationMode defaultInterpolation) : KisKeyframeChannel(id, defaultBounds), m_d(new Private(minValue, maxValue, defaultInterpolation)) { } KisScalarKeyframeChannel::KisScalarKeyframeChannel(const KisScalarKeyframeChannel &rhs, KisNode *newParentNode) : KisKeyframeChannel(rhs, newParentNode), m_d(new Private(*rhs.m_d)) { } KisScalarKeyframeChannel::~KisScalarKeyframeChannel() {} bool KisScalarKeyframeChannel::hasScalarValue() const { return true; } qreal KisScalarKeyframeChannel::minScalarValue() const { return m_d->minValue; } qreal KisScalarKeyframeChannel::maxScalarValue() const { return m_d->maxValue; } qreal KisScalarKeyframeChannel::scalarValue(const KisKeyframeSP keyframe) const { KisScalarKeyframe *key = dynamic_cast(keyframe.data()); Q_ASSERT(key != 0); return key->value; } struct KisScalarKeyframeChannel::Private::SetValueCommand : public KUndo2Command { SetValueCommand(KisScalarKeyframeChannel *channel, KisKeyframeSP keyframe, qreal oldValue, qreal newValue, KUndo2Command *parentCommand) : KUndo2Command(parentCommand), m_channel(channel), m_keyframe(keyframe), m_oldValue(oldValue), m_newValue(newValue) { } void redo() override { setValue(m_newValue); } void undo() override { setValue(m_oldValue); } void setValue(qreal value) { KisScalarKeyframe *key = dynamic_cast(m_keyframe.data()); Q_ASSERT(key != 0); key->value = value; m_channel->notifyKeyframeChanged(m_keyframe); } private: KisScalarKeyframeChannel *m_channel; KisKeyframeSP m_keyframe; qreal m_oldValue; qreal m_newValue; }; struct KisScalarKeyframeChannel::Private::SetTangentsCommand : public KUndo2Command { SetTangentsCommand(KisScalarKeyframeChannel *channel, KisKeyframeSP keyframe, KisKeyframe::InterpolationTangentsMode oldMode, QPointF oldLeftTangent, QPointF oldRightTangent, KisKeyframe::InterpolationTangentsMode newMode, QPointF newLeftTangent, QPointF newRightTangent, KUndo2Command *parentCommand) : KUndo2Command(parentCommand), m_channel(channel), m_keyframe(keyframe), m_oldMode(oldMode), m_oldLeftTangent(oldLeftTangent), m_oldRightTangent(oldRightTangent), m_newMode(newMode), m_newLeftTangent(newLeftTangent), m_newRightTangent(newRightTangent) { } void redo() override { m_keyframe->setTangentsMode(m_newMode); m_keyframe->setInterpolationTangents(m_newLeftTangent, m_newRightTangent); m_channel->notifyKeyframeChanged(m_keyframe); } void undo() override { m_keyframe->setTangentsMode(m_oldMode); m_keyframe->setInterpolationTangents(m_oldLeftTangent, m_oldRightTangent); m_channel->notifyKeyframeChanged(m_keyframe); } private: KisScalarKeyframeChannel *m_channel; KisKeyframeSP m_keyframe; KisKeyframe::InterpolationTangentsMode m_oldMode; QPointF m_oldLeftTangent; QPointF m_oldRightTangent; KisKeyframe::InterpolationTangentsMode m_newMode; QPointF m_newLeftTangent; QPointF m_newRightTangent; }; struct KisScalarKeyframeChannel::Private::SetInterpolationModeCommand : public KUndo2Command { SetInterpolationModeCommand(KisScalarKeyframeChannel *channel, KisKeyframeSP keyframe, KisKeyframe::InterpolationMode oldMode, KisKeyframe::InterpolationMode newMode, KUndo2Command *parentCommand) : KUndo2Command(parentCommand), m_channel(channel), m_keyframe(keyframe), m_oldMode(oldMode), m_newMode(newMode) { } void redo() override { m_keyframe->setInterpolationMode(m_newMode); m_channel->notifyKeyframeChanged(m_keyframe); } void undo() override { m_keyframe->setInterpolationMode(m_oldMode); m_channel->notifyKeyframeChanged(m_keyframe); } private: KisScalarKeyframeChannel *m_channel; KisKeyframeSP m_keyframe; KisKeyframe::InterpolationMode m_oldMode; KisKeyframe::InterpolationMode m_newMode; }; void KisScalarKeyframeChannel::setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand) { QScopedPointer tempCommand; if (!parentCommand) { tempCommand.reset(new KUndo2Command()); parentCommand = tempCommand.data(); } qreal oldValue = scalarValue(keyframe); KUndo2Command *cmd = new Private::SetValueCommand(this, keyframe, oldValue, value, parentCommand); cmd->redo(); } void KisScalarKeyframeChannel::setInterpolationMode(KisKeyframeSP keyframe, KisKeyframe::InterpolationMode mode, KUndo2Command *parentCommand) { QScopedPointer tempCommand; if (!parentCommand) { tempCommand.reset(new KUndo2Command()); parentCommand = tempCommand.data(); } KisKeyframe::InterpolationMode oldMode = keyframe->interpolationMode(); KUndo2Command *cmd = new Private::SetInterpolationModeCommand(this, keyframe, oldMode, mode, parentCommand); cmd->redo(); } void KisScalarKeyframeChannel::setInterpolationTangents(KisKeyframeSP keyframe, KisKeyframe::InterpolationTangentsMode mode, QPointF leftTangent, QPointF rightTangent, KUndo2Command *parentCommand) { QScopedPointer tempCommand; if (!parentCommand) { tempCommand.reset(new KUndo2Command()); parentCommand = tempCommand.data(); } KisKeyframe::InterpolationTangentsMode oldMode = keyframe->tangentsMode(); QPointF oldLeftTangent = keyframe->leftTangent(); QPointF oldRightTangent = keyframe->rightTangent(); KUndo2Command *cmd = new Private::SetTangentsCommand(this, keyframe, oldMode, oldLeftTangent, oldRightTangent, mode, leftTangent, rightTangent, parentCommand); cmd->redo(); } qreal cubicBezier(qreal p0, qreal delta1, qreal delta2, qreal p3, qreal t) { qreal p1 = p0 + delta1; qreal p2 = p3 + delta2; qreal c = 1-t; return c*c*c * p0 + 3*c*c*t * p1 + 3*c*t*t * p2 + t*t*t * p3; } void normalizeTangents(const QPointF point1, QPointF &rightTangent, QPointF &leftTangent, const QPointF point2) { // To ensure that the curve is monotonic wrt time, // check that control points lie between the endpoints. // If not, force them into range by scaling down the tangents float interval = point2.x() - point1.x(); if (rightTangent.x() < 0) rightTangent *= 0; if (leftTangent.x() > 0) leftTangent *= 0; if (rightTangent.x() > interval) { rightTangent *= interval / rightTangent.x(); } if (leftTangent.x() < -interval) { leftTangent *= interval / -leftTangent.x(); } } QPointF KisScalarKeyframeChannel::interpolate(QPointF point1, QPointF rightTangent, QPointF leftTangent, QPointF point2, qreal t) { normalizeTangents(point1, rightTangent, leftTangent, point2); qreal x = cubicBezier(point1.x(), rightTangent.x(), leftTangent.x(), point2.x(), t); qreal y = cubicBezier(point1.y(), rightTangent.y(), leftTangent.y(), point2.y(), t); return QPointF(x,y); } qreal findCubicCurveParameter(int time0, qreal delta0, qreal delta1, int time1, int time) { if (time == time0) return 0.0; if (time == time1) return 1.0; qreal min_t = 0.0; qreal max_t = 1.0; while (true) { qreal t = (max_t + min_t) / 2; qreal time_t = cubicBezier(time0, delta0, delta1, time1, t); if (time_t < time - 0.05) { min_t = t; } else if (time_t > time + 0.05) { max_t = t; } else { // Close enough return t; } } } qreal KisScalarKeyframeChannel::interpolatedValue(int time) const { KisKeyframeSP activeKey = activeKeyframeAt(time); if (activeKey.isNull()) return qQNaN(); KisKeyframeSP nextKey = nextKeyframe(activeKey); qreal result = qQNaN(); if (time == activeKey->time() || nextKey.isNull()) { result = scalarValue(activeKey); } else { switch (activeKey->interpolationMode()) { case KisKeyframe::Constant: result = scalarValue(activeKey); break; case KisKeyframe::Linear: { int time0 = activeKey->time(); int time1 = nextKey->time(); qreal value0 = scalarValue(activeKey); qreal value1 = scalarValue(nextKey); result = value0 + (value1 - value0) * (time - time0) / (time1 - time0); } break; case KisKeyframe::Bezier: { QPointF point0 = QPointF(activeKey->time(), scalarValue(activeKey)); QPointF point1 = QPointF(nextKey->time(), scalarValue(nextKey)); QPointF tangent0 = activeKey->rightTangent(); QPointF tangent1 = nextKey->leftTangent(); normalizeTangents(point0, tangent0, tangent1, point1); qreal t = findCubicCurveParameter(point0.x(), tangent0.x(), tangent1.x(), point1.x(), time); result = interpolate(point0, tangent0, tangent1, point1, t).y(); } break; default: KIS_ASSERT_RECOVER_BREAK(false); break; } } if (result > m_d->maxValue) return m_d->maxValue; if (result < m_d->minValue) return m_d->minValue; return result; } qreal KisScalarKeyframeChannel::currentValue() const { return interpolatedValue(currentTime()); } KisKeyframeSP KisScalarKeyframeChannel::createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) { if (copySrc) { KisScalarKeyframe *srcKeyframe = dynamic_cast(copySrc.data()); Q_ASSERT(srcKeyframe); KisScalarKeyframe *keyframe = new KisScalarKeyframe(srcKeyframe, this); keyframe->setTime(time); return toQShared(keyframe); } else { return createKeyframe(time, 0, parentCommand); } } KisKeyframeSP KisScalarKeyframeChannel::createKeyframe(int time, qreal value, KUndo2Command *parentCommand) { Q_UNUSED(parentCommand); KisScalarKeyframe *keyframe = new KisScalarKeyframe(this, time, value); keyframe->setInterpolationMode(m_d->defaultInterpolation); return toQShared(keyframe); } void KisScalarKeyframeChannel::destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) { Q_UNUSED(parentCommand); Q_UNUSED(key); } void KisScalarKeyframeChannel::uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) { KisScalarKeyframeChannel *srcScalarChannel = dynamic_cast(srcChannel); KIS_ASSERT_RECOVER_RETURN(srcScalarChannel); KisKeyframeSP srcFrame = srcScalarChannel->keyframeAt(srcTime); KIS_ASSERT_RECOVER_RETURN(srcFrame); KisScalarKeyframe *dstKey = dynamic_cast(dstFrame.data()); if (dstKey) { dstKey->value = srcChannel->scalarValue(srcFrame); notifyKeyframeChanged(dstFrame); } } -QRect KisScalarKeyframeChannel::affectedRect(KisKeyframeSP key) -{ - Q_UNUSED(key); - - if (node()) { - return node()->extent(); - } else { - return QRect(); - } -} - void KisScalarKeyframeChannel::saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) { Q_UNUSED(layerFilename); keyframeElement.setAttribute("value", KisDomUtils::toString(scalarValue(keyframe))); QString interpolationMode; if (keyframe->interpolationMode() == KisKeyframe::Constant) interpolationMode = "constant"; if (keyframe->interpolationMode() == KisKeyframe::Linear) interpolationMode = "linear"; if (keyframe->interpolationMode() == KisKeyframe::Bezier) interpolationMode = "bezier"; QString tangentsMode; if (keyframe->tangentsMode() == KisKeyframe::Smooth) tangentsMode = "smooth"; if (keyframe->tangentsMode() == KisKeyframe::Sharp) tangentsMode = "sharp"; keyframeElement.setAttribute("interpolation", interpolationMode); keyframeElement.setAttribute("tangents", tangentsMode); KisDomUtils::saveValue(&keyframeElement, "leftTangent", keyframe->leftTangent()); KisDomUtils::saveValue(&keyframeElement, "rightTangent", keyframe->rightTangent()); } KisKeyframeSP KisScalarKeyframeChannel::loadKeyframe(const QDomElement &keyframeNode) { int time = keyframeNode.toElement().attribute("time").toInt(); workaroundBrokenFrameTimeBug(&time); qreal value = KisDomUtils::toDouble(keyframeNode.toElement().attribute("value")); KUndo2Command tempParentCommand; KisKeyframeSP keyframe = createKeyframe(time, KisKeyframeSP(), &tempParentCommand); setScalarValue(keyframe, value); QString interpolationMode = keyframeNode.toElement().attribute("interpolation"); if (interpolationMode == "constant") { keyframe->setInterpolationMode(KisKeyframe::Constant); } else if (interpolationMode == "linear") { keyframe->setInterpolationMode(KisKeyframe::Linear); } else if (interpolationMode == "bezier") { keyframe->setInterpolationMode(KisKeyframe::Bezier); } QString tangentsMode = keyframeNode.toElement().attribute("tangents"); if (tangentsMode == "smooth") { keyframe->setTangentsMode(KisKeyframe::Smooth); } else if (tangentsMode == "sharp") { keyframe->setTangentsMode(KisKeyframe::Sharp); } QPointF leftTangent; QPointF rightTangent; KisDomUtils::loadValue(keyframeNode, "leftTangent", &leftTangent); KisDomUtils::loadValue(keyframeNode, "rightTangent", &rightTangent); keyframe->setInterpolationTangents(leftTangent, rightTangent); return keyframe; } void KisScalarKeyframeChannel::notifyKeyframeChanged(KisKeyframeSP keyframe) { - QRect rect = affectedRect(keyframe); - KisTimeRange range = affectedFrames(keyframe->time()); + QRect rect = keyframe->affectedRect(); + KisFrameSet range = affectedFrames(keyframe->time()); requestUpdate(range, rect); emit sigKeyframeChanged(keyframe); } diff --git a/libs/image/kis_scalar_keyframe_channel.h b/libs/image/kis_scalar_keyframe_channel.h index 23b5141ac0..0b6b69bf47 100644 --- a/libs/image/kis_scalar_keyframe_channel.h +++ b/libs/image/kis_scalar_keyframe_channel.h @@ -1,70 +1,68 @@ /* * 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_SCALAR_KEYFRAME_CHANNEL_H #define _KIS_SCALAR_KEYFRAME_CHANNEL_H #include "kis_keyframe_channel.h" #include "kis_keyframe_commands.h" class KRITAIMAGE_EXPORT KisScalarKeyframeChannel : public KisKeyframeChannel { Q_OBJECT public: struct KRITAIMAGE_EXPORT AddKeyframeCommand : public KisReplaceKeyframeCommand { AddKeyframeCommand(KisScalarKeyframeChannel *channel, int time, qreal value, KUndo2Command *parentCommand); }; KisScalarKeyframeChannel(const KoID& id, qreal minValue, qreal maxValue, KisDefaultBoundsBaseSP defaultBounds, KisKeyframe::InterpolationMode defaultInterpolation=KisKeyframe::Constant); KisScalarKeyframeChannel(const KisScalarKeyframeChannel &rhs, KisNode *newParentNode); ~KisScalarKeyframeChannel() override; bool hasScalarValue() const override; qreal minScalarValue() const override; qreal maxScalarValue() const override; qreal scalarValue(const KisKeyframeSP keyframe) const override; void setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand = 0) override; void setInterpolationMode(KisKeyframeSP keyframe, KisKeyframe::InterpolationMode mode, KUndo2Command *parentCommand = 0); void setInterpolationTangents(KisKeyframeSP keyframe, KisKeyframe::InterpolationTangentsMode, QPointF leftTangent, QPointF rightTangent, KUndo2Command *parentCommand); qreal interpolatedValue(int time) const; qreal currentValue() const; static QPointF interpolate(QPointF point1, QPointF rightTangent, QPointF leftTangent, QPointF point2, qreal t); protected: KisKeyframeSP createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) override; KisKeyframeSP createKeyframe(int time, qreal value, KUndo2Command *parentCommand); void destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) override; void uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) override; - QRect affectedRect(KisKeyframeSP key) override; - void saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) override; KisKeyframeSP loadKeyframe(const QDomElement &keyframeNode) override; private: void notifyKeyframeChanged(KisKeyframeSP keyframe); struct Private; QScopedPointer m_d; }; #endif diff --git a/libs/image/kis_time_range.cpp b/libs/image/kis_time_range.cpp index 6d801505a5..81e8768d68 100644 --- a/libs/image/kis_time_range.cpp +++ b/libs/image/kis_time_range.cpp @@ -1,151 +1,364 @@ /* * 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_time_range.h" #include #include "kis_keyframe_channel.h" #include "kis_node.h" #include "kis_layer_utils.h" +#include "kis_dom_utils.h" -struct KisTimeRangeStaticRegistrar { - KisTimeRangeStaticRegistrar() { - qRegisterMetaType("KisTimeRange"); +struct KisTimeTypesStaticRegistrar { + KisTimeTypesStaticRegistrar() { + qRegisterMetaType("KisTimeSpan"); + qRegisterMetaType("KisFrameSet"); } }; -static KisTimeRangeStaticRegistrar __registrar; +static KisTimeTypesStaticRegistrar __registrar; -QDebug operator<<(QDebug dbg, const KisTimeRange &r) +QDebug operator<<(QDebug dbg, const KisTimeSpan &r) { - dbg.nospace() << "KisTimeRange(" << r.start() << ", " << r.end() << ")"; + dbg.nospace() << "KisTimeSpan(" << r.start() << ", " << r.end() << ")"; return dbg.space(); } -KisTimeRange KisTimeRange::calculateIdenticalFramesRecursive(const KisNode *node, int time) +QDebug operator<<(QDebug dbg, const KisFrameSet &r) { - KisTimeRange range = KisTimeRange::infinite(0); + const QVector &spans = r.finiteSpans(); + + dbg.nospace() << "KisFrameSet("; + for (int i = 0; i < spans.size(); i++) { + if (i > 0) dbg.nospace() << ", "; + dbg.nospace() << spans[i].start() << ".." << spans[i].end(); + } + if (r.isInfinite()) dbg.nospace() << ", " << r.firstFrameOfInfinity() << "..."; + dbg.nospace() << ")"; + + return dbg.space(); +} + +KisFrameSet& KisFrameSet::operator|=(const KisFrameSet &rhs) +{ + if (rhs.isEmpty()) return *this; + if (isEmpty()) { + *this = rhs; + return *this; + } + + QVector spans; + int lIndex = 0, rIndex = 0; + + KisTimeSpan currentSpan; + + int firstOfInfinite = + !isInfinite() ? rhs.m_firstFrameOfInfinity : + (!rhs.isInfinite()) ? m_firstFrameOfInfinity : + qMin(m_firstFrameOfInfinity, rhs.m_firstFrameOfInfinity); + + while (lIndex < m_spans.size() || rIndex < rhs.m_spans.size()) { + const bool leftRemaining = (lIndex < m_spans.size()); + const bool rightRemaining = (rIndex < rhs.m_spans.size()); + const bool leftFirst = leftRemaining && (!rightRemaining || m_spans[lIndex].start() < rhs.m_spans[rIndex].start()); + + KisTimeSpan first; + if (leftFirst) { + first = m_spans[lIndex++]; + } else { + first = rhs.m_spans[rIndex++]; + } + + if (isInfinite() && firstOfInfinite <= first.end()) { + currentSpan = KisTimeSpan(); + firstOfInfinite = qMin(first.start(), firstOfInfinite); + break; + } else if (first.start() <= currentSpan.end() || currentSpan.isEmpty()) { + currentSpan = currentSpan | first; + } else { + spans.append(currentSpan); + currentSpan = first; + } + } + + if (!currentSpan.isEmpty()) { + spans.append(currentSpan); + } + + m_spans = spans; + m_firstFrameOfInfinity = firstOfInfinite; + return *this; +} + + +void addIntersectionAgainstInfinity(const QVector &src, int firstIndex , QVector &dst, int firstFrameOfInfinity) +{ + for (int index = firstIndex; index < src.size(); index++) { + const KisTimeSpan span = src[index].truncateRight(firstFrameOfInfinity); + if (!span.isEmpty()) dst.append(span); + } +} + +KisFrameSet& KisFrameSet::operator&=(const KisFrameSet &rhs) +{ + if (isEmpty() || rhs.isEmpty()) { + *this = KisFrameSet(); + return *this; + } + + QVector spans; + + int lIndex = 0, rIndex = 0; + while (lIndex < m_spans.size() && rIndex < rhs.m_spans.size()) { + KisTimeSpan span; + + const KisTimeSpan rSpan = rhs.m_spans[rIndex]; + const KisTimeSpan lSpan = m_spans[lIndex]; + + span = lSpan & rSpan; + + if (!span.isEmpty()) { + spans.append(span); + } + + if (lSpan.start() < rSpan.start()) { + lIndex++; + } else { + rIndex++; + } + } + + if (isInfinite()) addIntersectionAgainstInfinity(rhs.m_spans, rIndex, spans, m_firstFrameOfInfinity); + if (rhs.isInfinite()) addIntersectionAgainstInfinity(m_spans, lIndex, spans, rhs.m_firstFrameOfInfinity); + + int firstOfInfinite = (isInfinite() && rhs.isInfinite()) ? qMax(m_firstFrameOfInfinity, rhs.m_firstFrameOfInfinity) : -1; + + m_spans = spans; + m_firstFrameOfInfinity = firstOfInfinite; + return *this; +} + +KisFrameSet& KisFrameSet::operator-=(const KisFrameSet &rhs) +{ + if (rhs.isEmpty()) return *this; + if (isEmpty()) { + *this = KisFrameSet(); + return *this; + } + + QVector spans; + + int firstOfInfinite = (isInfinite() && !rhs.isInfinite()) ? + qMax(m_firstFrameOfInfinity, rhs.m_spans.last().end() + 1) : -1; + + KisTimeSpan currentSpan = m_spans.first(); + + int lIndex = 0, rIndex = 0; + while (lIndex < m_spans.size() && rIndex < rhs.m_spans.size()) { + const KisTimeSpan rSpan = rhs.m_spans[rIndex]; + + if (currentSpan.isEmpty() || currentSpan.end() < rSpan.start()) { + if (!currentSpan.isEmpty()) { + spans.append(currentSpan); + } + lIndex++; + currentSpan = (lIndex < m_spans.size()) ? m_spans[lIndex] : KisTimeSpan(); + } else { + const KisTimeSpan tail = currentSpan.truncateRight(rSpan.end() + 1); + const KisTimeSpan head = currentSpan.truncateLeft(rSpan.start() - 1); + + if (!head.isEmpty()) { + spans.append(head); + } + currentSpan = tail; + rIndex++; + } + } + + while (!currentSpan.isEmpty()) { + if (rhs.isInfinite() && currentSpan.end() >= rhs.firstFrameOfInfinity()) { + currentSpan = currentSpan.truncateLeft(rhs.firstFrameOfInfinity() - 1); + if (!currentSpan.isEmpty()) spans.append(currentSpan); + break; + } + + spans.append(currentSpan); + lIndex++; + currentSpan = (lIndex < m_spans.size()) ? m_spans[lIndex] : KisTimeSpan(); + } + + m_spans = spans; + m_firstFrameOfInfinity = firstOfInfinite; + return *this; +} + +int KisTime::min(int a, int b) +{ + if (a < 0) return b; + if (b < 0) return a; + return std::min(a, b); +} + +int KisTime::max(int a, int b) +{ + if (a < 0) return b; + if (b < 0) return a; + return std::max(a, b); +} + +bool areFramesIdentical(const KisNode *root, int time1, int time2) +{ + bool identical = true; + + KisLayerUtils::recursiveApplyNodes(root, + [&identical, time1, time2] (const KisNode *node) { + if (node->visible()) { + const QMap channels = node->keyframeChannels(); + + Q_FOREACH (const KisKeyframeChannel *channel, channels) { + identical &= channel->areFramesIdentical(time1, time2); + } + } + } + ); + + return identical; +} + +KisFrameSet calculateIdenticalFramesRecursive(const KisNode *node, int time, const KisTimeSpan range) +{ + KisFrameSet frames = KisFrameSet::infiniteFrom(0); KisLayerUtils::recursiveApplyNodes(node, - [&range, time] (const KisNode *node) { + [&frames, time, range] (const KisNode *node) { if (node->visible()) { - range &= calculateNodeIdenticalFrames(node, time); + frames &= calculateNodeIdenticalFrames(node, time, range); } }); - return range; + return frames; } -KisTimeRange KisTimeRange::calculateAffectedFramesRecursive(const KisNode *node, int time) +KisFrameSet calculateAffectedFramesRecursive(const KisNode *node, int time) { - KisTimeRange range; + KisFrameSet frames; KisLayerUtils::recursiveApplyNodes(node, - [&range, time] (const KisNode *node) { + [&frames, time] (const KisNode *node) { if (node->visible()) { - range |= calculateNodeIdenticalFrames(node, time); + frames |= calculateNodeAffectedFrames(node, time); } }); - return range; + return frames; +} + +int KisFrameSet::firstExcludedSince(int time) const +{ + if (isEmpty()) return time; + if (0 <= m_firstFrameOfInfinity && m_firstFrameOfInfinity <= time) return -1; + if (time < start()) return time; + if (time > m_spans.last().end()) return time; + + Q_FOREACH(const KisTimeSpan &span, m_spans) { + if (span.start() > time) return time; + if (span.end() >= time) return span.end() + 1; + } + + KIS_SAFE_ASSERT_RECOVER_NOOP(false); + return -1; } -KisTimeRange KisTimeRange::calculateNodeIdenticalFrames(const KisNode *node, int time) +KisFrameSet calculateNodeIdenticalFrames(const KisNode *node, int time, const KisTimeSpan range) { - KisTimeRange range = KisTimeRange::infinite(0); + KisFrameSet frames = KisFrameSet::infiniteFrom(0); const QMap channels = node->keyframeChannels(); Q_FOREACH (const KisKeyframeChannel *channel, channels) { // Intersection - range &= channel->identicalFrames(time); + frames &= channel->identicalFrames(time, range); } - return range; + return frames; } -KisTimeRange KisTimeRange::calculateNodeAffectedFrames(const KisNode *node, int time) +KisFrameSet calculateNodeAffectedFrames(const KisNode *node, int time) { - KisTimeRange range; + KisFrameSet range; if (!node->visible()) return range; const QMap channels = node->keyframeChannels(); // TODO: channels should report to the image which channel exactly has changed // to avoid the dirty range to be stretched into infinity! if (channels.isEmpty() || !channels.contains(KisKeyframeChannel::Content.id())) { - range = KisTimeRange::infinite(0); + range = KisFrameSet::infiniteFrom(0); return range; } Q_FOREACH (const KisKeyframeChannel *channel, channels) { // Union range |= channel->affectedFrames(time); } return range; } namespace KisDomUtils { -void saveValue(QDomElement *parent, const QString &tag, const KisTimeRange &range) -{ - QDomDocument doc = parent->ownerDocument(); - QDomElement e = doc.createElement(tag); - parent->appendChild(e); - - e.setAttribute("type", "timerange"); + void saveValue(QDomElement *parent, const QString &tag, const KisTimeSpan &range) + { + QDomDocument doc = parent->ownerDocument(); + QDomElement e = doc.createElement(tag); + parent->appendChild(e); - if (range.isValid()) { - e.setAttribute("from", toString(range.start())); + e.setAttribute("type", "timerange"); - if (!range.isInfinite()) { + if (!range.isEmpty()) { + e.setAttribute("from", toString(range.start())); e.setAttribute("to", toString(range.end())); } } -} - -bool loadValue(const QDomElement &parent, const QString &tag, KisTimeRange *range) -{ - QDomElement e; - if (!findOnlyElement(parent, tag, &e)) return false; + bool loadValue(const QDomElement &parent, const QString &tag, KisTimeSpan *range) + { + QDomElement e; + if (!findOnlyElement(parent, tag, &e)) return false; - if (!Private::checkType(e, "timerange")) return false; + if (!Private::checkType(e, "timerange")) return false; - int start = toInt(e.attribute("from", "-1")); - int end = toInt(e.attribute("to", "-1")); + int start = toInt(e.attribute("from", "-1")); + int end = toInt(e.attribute("to", "-1")); - if (start == -1) { - *range = KisTimeRange(); - } else if (end == -1) { - *range = KisTimeRange::infinite(start); - } else { - *range = KisTimeRange::fromTime(start, end); + if (start < 0 || end < 0) { + *range = KisTimeSpan(); + } else { + *range = KisTimeSpan(start, end); + } + return true; } - return true; -} } diff --git a/libs/image/kis_time_range.h b/libs/image/kis_time_range.h index d861dc565f..60ee9468bd 100644 --- a/libs/image/kis_time_range.h +++ b/libs/image/kis_time_range.h @@ -1,153 +1,277 @@ /* * 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_TIME_RANGE_H #define __KIS_TIME_RANGE_H #include "kritaimage_export.h" #include #include #include #include #include "kis_types.h" -#include -class KRITAIMAGE_EXPORT KisTimeRange : public boost::equality_comparable -{ +class QDomElement; + +/** + * Represents a finite, continuous span of time between two frames. + * Start and end frames are both included in the span. + */ +class KRITAIMAGE_EXPORT KisTimeSpan : public boost::equality_comparable { public: - inline KisTimeRange() - : m_start(0), - m_end(-1) - { - } - inline KisTimeRange(int start, int duration) - : m_start(start), - m_end(start + duration - 1) - { - } + inline KisTimeSpan() + : m_start(-1) + , m_end(-2) + {} - inline KisTimeRange(int start, int end, bool) + inline KisTimeSpan(int start, int end) : m_start(start), m_end(end) - { - } + {} - bool operator==(const KisTimeRange &rhs) const { - return rhs.m_start == m_start && rhs.m_end == m_end; + inline bool isEmpty() const { return m_end < m_start; } + inline int start() const { return m_start; } + inline int end() const { return m_end; } + + inline bool isValid() const { return isEmpty(); } + + inline int duration() const { + return !isEmpty() ? (m_end - m_start + 1) : 0; } - KisTimeRange& operator|=(const KisTimeRange &rhs) { - if (!isValid()) { - m_start = rhs.start(); - } else if (rhs.isValid()) { - m_start = std::min(m_start, rhs.start()); - } + inline bool contains(int time) const { + return !isEmpty() ? (m_start <= time && time <= m_end) : false; + } - if (rhs.isInfinite() || isInfinite()) { - m_end = std::numeric_limits::min(); - } else if (!isValid()) { - m_end = rhs.m_end; - } else { - m_end = std::max(m_end, rhs.m_end); - } + inline bool contains(const KisTimeSpan rhs) const { + return rhs.isEmpty() || (m_start <= rhs.start() && rhs.end() <= m_end); + } - return *this; + inline bool overlaps(const KisTimeSpan &other) const { + return m_start <= other.m_end && other.m_start <= m_end; } - KisTimeRange& operator&=(const KisTimeRange &rhs) { - if (!isValid()) { - return *this; - } else if (!rhs.isValid()) { - m_start = rhs.start(); - m_end = rhs.m_end; - return *this; - } else { - m_start = std::max(m_start, rhs.start()); - } + /** Returns the truncated span between the start of this one and the new end. + * Note: if end moves before the start, result will be empty. + */ + inline KisTimeSpan truncateLeft(int newEnd) const { + if (newEnd < m_start) return KisTimeSpan(); + if (m_end <= newEnd) return *this; + return KisTimeSpan(m_start, newEnd); + } - if (isInfinite()) { - m_end = rhs.m_end; - } else if (!rhs.isInfinite()) { - m_end = std::min(m_end, rhs.m_end); - } + /** Returns the truncated span between the new start and the end of this one. + * Note: if start moves beyond the end, result will be empty. + */ + inline KisTimeSpan truncateRight(int newStart) const { + if (m_end < newStart) return KisTimeSpan(); + if (newStart <= m_start) return *this; + return KisTimeSpan(newStart, m_end); + } - return *this; + /** Returns the span between the new start and the end of this one. + * Note: if start moves beyond the end, ends get swapped to create a valid range. + */ + inline KisTimeSpan startMoved(int newStart) const { + return { qMin(newStart, m_end), qMax(newStart, m_end) }; } - inline int start() const { - return m_start; + /** Returns the span between the start of this one and the new end. + * Note: if end moves before the start, ends get swapped to create a valid range. + */ + inline KisTimeSpan endMoved(int newEnd) const { + return { qMin(newEnd, m_start), qMax(newEnd, m_start) }; } - inline int end() const { - return m_end; + bool operator==(const KisTimeSpan &rhs) const { + return rhs.m_start == m_start && rhs.m_end == m_end; } - inline int duration() const { - return m_end >= m_start ? m_end - m_start + 1 : 0; + KisTimeSpan operator|(const KisTimeSpan &rhs) const { + if (isEmpty()) return rhs; + if (rhs.isEmpty()) return *this; + + int start = qMin(m_start, rhs.m_start); + int end = qMax(m_end, rhs.m_end); + + return KisTimeSpan(start, end); } - inline bool isInfinite() const { - return m_end == std::numeric_limits::min(); + KisTimeSpan operator&(const KisTimeSpan &rhs) const { + if (isEmpty() || rhs.isEmpty()) return KisTimeSpan(); + + int start = qMax(m_start, rhs.m_start); + int end = qMin(m_end, rhs.m_end); + + if (end < start) return KisTimeSpan(); + + return KisTimeSpan(start, end); } - inline bool isValid() const { - return (m_end >= m_start) || (m_end == std::numeric_limits::min() && m_start >= 0); +private: + int m_start, m_end; +}; + +/** + * Represents an arbitrary set of frames, possibly stretching to infinity. + * + */ +class KRITAIMAGE_EXPORT KisFrameSet : + public boost::equality_comparable, + public boost::andable, + public boost::orable, + public boost::subtractable +{ +public: + static KisFrameSet between(int start, int end) { return KisFrameSet(KisTimeSpan(start, end)); } + static KisFrameSet infiniteFrom(int start) { return KisFrameSet({}, start); } + + KisFrameSet() = default; + + inline explicit KisFrameSet(QVector spans, int firstFrameOfInfinity = -1) + : m_spans(spans) + , m_firstFrameOfInfinity(firstFrameOfInfinity) + { + // Normalize + + std::sort(m_spans.begin(), m_spans.end(), + [](const KisTimeSpan &a, const KisTimeSpan &b) { return a.start() < b.start(); } + ); + + mergeOverlappingSpans(); } - inline bool contains(int time) const { - if (m_end == std::numeric_limits::min()) { - return m_start <= time; - } + explicit KisFrameSet(const KisTimeSpan span) + : m_spans({span}) + {} - return m_start <= time && time <= m_end; + inline bool isInfinite() const { return m_firstFrameOfInfinity >= 0; } + + bool isEmpty() const { + return m_spans.isEmpty() && !isInfinite(); } - static inline KisTimeRange fromTime(int start, int end) { - return KisTimeRange(start, end, true); + int start() const { + if (m_spans.isEmpty()) return m_firstFrameOfInfinity; + return m_spans.first().start(); } - static inline KisTimeRange infinite(int start) { - return KisTimeRange(start, std::numeric_limits::min(), true); + /** + * List of the finite, continuous spans this set consists of. + * Note: this list does not contain the tail of infinite sets. See firstFrameOfInfinity(). + */ + inline const QVector finiteSpans() const { return m_spans; } + + /** + * If the set is infinite, the frame from which the infinite tail begins. Returns -1 if the set is finite. + */ + inline int firstFrameOfInfinity() const { + return m_firstFrameOfInfinity; } - static KisTimeRange calculateIdenticalFramesRecursive(const KisNode *node, int time); - static KisTimeRange calculateAffectedFramesRecursive(const KisNode *node, int time); + inline bool contains(int frame) const { + if (0 <= m_firstFrameOfInfinity && m_firstFrameOfInfinity <= frame) return true; + + Q_FOREACH(const KisTimeSpan &span, m_spans) { + if (span.contains(frame)) return true; + } + + return false; + } - static KisTimeRange calculateNodeIdenticalFrames(const KisNode *node, int time); - static KisTimeRange calculateNodeAffectedFrames(const KisNode *node, int time); + int firstExcludedSince(int time) const; + + bool operator==(const KisFrameSet &rhs) const { + return rhs.m_firstFrameOfInfinity == m_firstFrameOfInfinity && rhs.m_spans == m_spans; + } + + KisFrameSet& operator|=(const KisFrameSet &rhs); + KisFrameSet& operator&=(const KisFrameSet &rhs); + KisFrameSet& operator-=(const KisFrameSet &rhs); private: - int m_start; - int m_end; + inline void mergeOverlappingSpans() + { + int dst = 0; + for (int src = 1; src < m_spans.length(); src++) { + if (isInfinite() && m_firstFrameOfInfinity <= m_spans[src].end()) { + m_firstFrameOfInfinity = qMin(m_spans[src].start(), m_firstFrameOfInfinity); + break; + } + + if (m_spans[src].overlaps(m_spans[dst])) { + m_spans[dst] = m_spans[dst] | m_spans[src]; + } else { + dst++; + if (dst != src) { + m_spans[dst] = m_spans[src]; + } + } + } + + if (dst < m_spans.length() - 1) { + m_spans.resize(dst - 1); + } + } + + QVector m_spans; + int m_firstFrameOfInfinity = -1; }; +namespace KisTime { + /** + * Returns the earlier time between a and b. + * If one of the arguments is negative, the other one is returned. + */ + KRITAIMAGE_EXPORT int min(int a, int b); + + /** + * Returns the latter time between a and b. + * If one of the arguments is negative, the other one is returned. + */ + KRITAIMAGE_EXPORT int max(int a, int b); +} + +/** + * Recursively checks whether the two frames are identical in all layers + */ +KRITAIMAGE_EXPORT bool areFramesIdentical(const KisNode *root, int time1, int time2); + +KRITAIMAGE_EXPORT KisFrameSet calculateIdenticalFramesRecursive(const KisNode *node, int time, const KisTimeSpan range); +KRITAIMAGE_EXPORT KisFrameSet calculateAffectedFramesRecursive(const KisNode *node, int time); + +KRITAIMAGE_EXPORT KisFrameSet calculateNodeIdenticalFrames(const KisNode *node, int time, const KisTimeSpan range); +KRITAIMAGE_EXPORT KisFrameSet calculateNodeAffectedFrames(const KisNode *node, int time); + namespace KisDomUtils { - void KRITAIMAGE_EXPORT saveValue(QDomElement *parent, const QString &tag, const KisTimeRange &range); - bool KRITAIMAGE_EXPORT loadValue(const QDomElement &parent, const QString &tag, KisTimeRange *range); + void KRITAIMAGE_EXPORT saveValue(QDomElement *parent, const QString &tag, const KisTimeSpan &range); + bool KRITAIMAGE_EXPORT loadValue(const QDomElement &parent, const QString &tag, KisTimeSpan *range); } -Q_DECLARE_METATYPE(KisTimeRange) +Q_DECLARE_METATYPE(KisTimeSpan); +Q_DECLARE_METATYPE(KisFrameSet); -KRITAIMAGE_EXPORT QDebug operator<<(QDebug dbg, const KisTimeRange &r); +KRITAIMAGE_EXPORT QDebug operator<<(QDebug dbg, const KisTimeSpan &r); +KRITAIMAGE_EXPORT QDebug operator<<(QDebug dbg, const KisFrameSet &r); #endif /* __KIS_TIME_RANGE_H */ diff --git a/libs/image/kis_transaction_data.cpp b/libs/image/kis_transaction_data.cpp index d40648f44e..f030877cfa 100644 --- a/libs/image/kis_transaction_data.cpp +++ b/libs/image/kis_transaction_data.cpp @@ -1,324 +1,324 @@ /* * Copyright (c) 2002 Patrick Julien * * 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_transaction_data.h" #include "kis_pixel_selection.h" #include "kis_paint_device.h" #include "kis_paint_device_frames_interface.h" #include "kis_datamanager.h" #include "kis_image.h" #include "KoColor.h" //#define DEBUG_TRANSACTIONS #ifdef DEBUG_TRANSACTIONS # define DEBUG_ACTION(action) dbgKrita << action << "for" << m_d->device->dataManager() #else # define DEBUG_ACTION(action) #endif class Q_DECL_HIDDEN KisTransactionData::Private { public: KisPaintDeviceSP device; KisMementoSP memento; bool firstRedo; bool transactionFinished; QPoint oldOffset; QPoint newOffset; KoColor oldDefaultPixel; bool defaultPixelChanged = false; bool savedOutlineCacheValid; QPainterPath savedOutlineCache; KUndo2Command *flattenUndoCommand; bool resetSelectionOutlineCache; int transactionTime; int transactionFrameId; KisDataManagerSP savedDataManager; KUndo2Command newFrameCommand; void possiblySwitchCurrentTime(); KisDataManagerSP dataManager(); void moveDevice(const QPoint newOffset); void tryCreateNewFrame(KisPaintDeviceSP device, int time); }; KisTransactionData::KisTransactionData(const KUndo2MagicString& name, KisPaintDeviceSP device, bool resetSelectionOutlineCache, KUndo2Command* parent) : KUndo2Command(name, parent) , m_d(new Private()) { m_d->resetSelectionOutlineCache = resetSelectionOutlineCache; setTimedID(-1); init(device); saveSelectionOutlineCache(); } #include "kis_raster_keyframe_channel.h" #include "kis_image_config.h" void KisTransactionData::Private::tryCreateNewFrame(KisPaintDeviceSP device, int time) { if (!device->framesInterface()) return; KisImageConfig cfg(true); if (!cfg.lazyFrameCreationEnabled()) return; KisRasterKeyframeChannel *channel = device->keyframeChannel(); KIS_ASSERT_RECOVER(channel) { return; } KisKeyframeSP keyframe = channel->keyframeAt(time); if (!keyframe) { - keyframe = channel->activeKeyframeAt(time); - KisKeyframeSP newKeyframe = channel->copyKeyframe(keyframe, time, &newFrameCommand); + keyframe = channel->visibleKeyframeAt(time); + KisKeyframeSP newKeyframe = channel->copyAsKeyframe(keyframe, time, time, &newFrameCommand); newKeyframe->setColorLabel(KisImageConfig(true).defaultFrameColorLabel()); } } void KisTransactionData::init(KisPaintDeviceSP device) { m_d->device = device; DEBUG_ACTION("Transaction started"); m_d->oldOffset = QPoint(device->x(), device->y()); m_d->oldDefaultPixel = device->defaultPixel(); m_d->firstRedo = true; m_d->transactionFinished = false; m_d->flattenUndoCommand = 0; m_d->transactionTime = device->defaultBounds()->currentTime(); m_d->tryCreateNewFrame(m_d->device, m_d->transactionTime); m_d->transactionFrameId = device->framesInterface() ? device->framesInterface()->currentFrameId() : -1; m_d->savedDataManager = m_d->transactionFrameId >= 0 ? m_d->device->framesInterface()->frameDataManager(m_d->transactionFrameId) : m_d->device->dataManager(); m_d->memento = m_d->savedDataManager->getMemento(); } KisTransactionData::~KisTransactionData() { Q_ASSERT(m_d->memento); m_d->savedDataManager->purgeHistory(m_d->memento); delete m_d; } void KisTransactionData::Private::moveDevice(const QPoint newOffset) { if (transactionFrameId >= 0) { device->framesInterface()->setFrameOffset(transactionFrameId, newOffset); } else { device->moveTo(newOffset); } } void KisTransactionData::endTransaction() { if(!m_d->transactionFinished) { // make sure the time didn't change during the transaction KIS_ASSERT_RECOVER_RETURN( m_d->transactionTime == m_d->device->defaultBounds()->currentTime()); DEBUG_ACTION("Transaction ended"); m_d->transactionFinished = true; m_d->savedDataManager->commit(); m_d->newOffset = QPoint(m_d->device->x(), m_d->device->y()); m_d->defaultPixelChanged = m_d->oldDefaultPixel != m_d->device->defaultPixel(); } } void KisTransactionData::startUpdates() { if (m_d->transactionFrameId == -1 || m_d->transactionFrameId == m_d->device->framesInterface()->currentFrameId()) { QRect rc; QRect mementoExtent = m_d->memento->extent(); if (m_d->newOffset == m_d->oldOffset) { rc = mementoExtent.translated(m_d->device->x(), m_d->device->y()); } else { QRect totalExtent = m_d->savedDataManager->extent() | mementoExtent; rc = totalExtent.translated(m_d->oldOffset) | totalExtent.translated(m_d->newOffset); } if (m_d->defaultPixelChanged) { rc |= m_d->device->defaultBounds()->bounds(); } m_d->device->setDirty(rc); } else { m_d->device->framesInterface()->invalidateFrameCache(m_d->transactionFrameId); } } void KisTransactionData::possiblyNotifySelectionChanged() { KisPixelSelectionSP pixelSelection = dynamic_cast(m_d->device.data()); KisSelectionSP selection; if (pixelSelection && (selection = pixelSelection->parentSelection())) { selection->notifySelectionChanged(); } } void KisTransactionData::possiblyResetOutlineCache() { KisPixelSelectionSP pixelSelection; if (m_d->resetSelectionOutlineCache && (pixelSelection = dynamic_cast(m_d->device.data()))) { pixelSelection->invalidateOutlineCache(); } } void KisTransactionData::Private::possiblySwitchCurrentTime() { if (device->defaultBounds()->currentTime() == transactionTime) return; qWarning() << "WARNING: undo command has been executed, when another frame has been active. That shouldn't have happened."; device->requestTimeSwitch(transactionTime); } void KisTransactionData::redo() { //KUndo2QStack calls redo(), so the first call needs to be blocked if (m_d->firstRedo) { m_d->firstRedo = false; possiblyResetOutlineCache(); possiblyNotifySelectionChanged(); return; } restoreSelectionOutlineCache(false); m_d->newFrameCommand.redo(); DEBUG_ACTION("Redo()"); Q_ASSERT(m_d->memento); m_d->savedDataManager->rollforward(m_d->memento); if (m_d->newOffset != m_d->oldOffset) { m_d->moveDevice(m_d->newOffset); } m_d->possiblySwitchCurrentTime(); startUpdates(); possiblyNotifySelectionChanged(); } void KisTransactionData::undo() { DEBUG_ACTION("Undo()"); Q_ASSERT(m_d->memento); m_d->savedDataManager->rollback(m_d->memento); if (m_d->newOffset != m_d->oldOffset) { m_d->moveDevice(m_d->oldOffset); } restoreSelectionOutlineCache(true); m_d->possiblySwitchCurrentTime(); startUpdates(); possiblyNotifySelectionChanged(); m_d->newFrameCommand.undo(); } void KisTransactionData::saveSelectionOutlineCache() { m_d->savedOutlineCacheValid = false; KisPixelSelectionSP pixelSelection = dynamic_cast(m_d->device.data()); if (pixelSelection) { m_d->savedOutlineCacheValid = pixelSelection->outlineCacheValid(); if (m_d->savedOutlineCacheValid) { m_d->savedOutlineCache = pixelSelection->outlineCache(); possiblyResetOutlineCache(); } KisSelectionSP selection = pixelSelection->parentSelection(); if (selection) { m_d->flattenUndoCommand = selection->flatten(); if (m_d->flattenUndoCommand) { m_d->flattenUndoCommand->redo(); } } } } void KisTransactionData::restoreSelectionOutlineCache(bool undo) { KisPixelSelectionSP pixelSelection = dynamic_cast(m_d->device.data()); if (pixelSelection) { bool savedOutlineCacheValid; QPainterPath savedOutlineCache; savedOutlineCacheValid = pixelSelection->outlineCacheValid(); if (savedOutlineCacheValid) { savedOutlineCache = pixelSelection->outlineCache(); } if (m_d->savedOutlineCacheValid) { pixelSelection->setOutlineCache(m_d->savedOutlineCache); } else { pixelSelection->invalidateOutlineCache(); } m_d->savedOutlineCacheValid = savedOutlineCacheValid; if (m_d->savedOutlineCacheValid) { m_d->savedOutlineCache = savedOutlineCache; } if (m_d->flattenUndoCommand) { if (undo) { m_d->flattenUndoCommand->undo(); } else { m_d->flattenUndoCommand->redo(); } } } } diff --git a/libs/image/kis_types.h b/libs/image/kis_types.h index fbde256867..dcbab484a1 100644 --- a/libs/image/kis_types.h +++ b/libs/image/kis_types.h @@ -1,313 +1,316 @@ /* * Copyright (c) 2002 Patrick Julien * * 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 KISTYPES_H_ #define KISTYPES_H_ #include #include #include #include "kritaimage_export.h" template class KisWeakSharedPtr; template class KisSharedPtr; template class QSharedPointer; template class QWeakPointer; template uint qHash(KisSharedPtr ptr) { return qHash(ptr.data()); } template uint qHash(KisWeakSharedPtr ptr) { return qHash(ptr.data()); } /** * Define lots of shared pointer versions of Krita classes. * Shared pointer classes have the advantage of near automatic * memory management (but beware of circular references) * These types should never be passed by reference, * because that will mess up their reference counter. * * An example of the naming pattern used: * * KisPaintDeviceSP is a KisSharedPtr of KisPaintDevice * KisPaintDeviceWSP is a KisWeakSharedPtr of KisPaintDevice * vKisPaintDeviceSP is a QVector of KisPaintDeviceSP * vKisPaintDeviceSP_it is an iterator of vKisPaintDeviceSP * */ class KisImage; typedef KisSharedPtr KisImageSP; typedef KisWeakSharedPtr KisImageWSP; class KisPaintDevice; typedef KisSharedPtr KisPaintDeviceSP; typedef KisWeakSharedPtr KisPaintDeviceWSP; typedef QVector vKisPaintDeviceSP; typedef vKisPaintDeviceSP::iterator vKisPaintDeviceSP_it; class KisFixedPaintDevice; typedef KisSharedPtr KisFixedPaintDeviceSP; class KisMask; typedef KisSharedPtr KisMaskSP; typedef KisWeakSharedPtr KisMaskWSP; class KisNode; typedef KisSharedPtr KisNodeSP; typedef KisWeakSharedPtr KisNodeWSP; typedef QVector vKisNodeSP; typedef vKisNodeSP::iterator vKisNodeSP_it; typedef vKisNodeSP::const_iterator vKisNodeSP_cit; class KisBaseNode; typedef KisSharedPtr KisBaseNodeSP; typedef KisWeakSharedPtr KisBaseNodeWSP; class KisEffectMask; typedef KisSharedPtr KisEffectMaskSP; typedef KisWeakSharedPtr KisEffectMaskWSP; class KisFilterMask; typedef KisSharedPtr KisFilterMaskSP; typedef KisWeakSharedPtr KisFilterMaskWSP; class KisTransformMask; typedef KisSharedPtr KisTransformMaskSP; typedef KisWeakSharedPtr KisTransformMaskWSP; class KisTransformMaskParamsInterface; typedef QSharedPointer KisTransformMaskParamsInterfaceSP; typedef QWeakPointer KisTransformMaskParamsInterfaceWSP; class KisTransparencyMask; typedef KisSharedPtr KisTransparencyMaskSP; typedef KisWeakSharedPtr KisTransparencyMaskWSP; class KisColorizeMask; typedef KisSharedPtr KisColorizeMaskSP; typedef KisWeakSharedPtr KisColorizeMaskWSP; class KisLayer; typedef KisSharedPtr KisLayerSP; typedef KisWeakSharedPtr KisLayerWSP; class KisShapeLayer; typedef KisSharedPtr KisShapeLayerSP; class KisPaintLayer; typedef KisSharedPtr KisPaintLayerSP; class KisAdjustmentLayer; typedef KisSharedPtr KisAdjustmentLayerSP; class KisGeneratorLayer; typedef KisSharedPtr KisGeneratorLayerSP; class KisCloneLayer; typedef KisSharedPtr KisCloneLayerSP; typedef KisWeakSharedPtr KisCloneLayerWSP; class KisGroupLayer; typedef KisSharedPtr KisGroupLayerSP; typedef KisWeakSharedPtr KisGroupLayerWSP; class KisFileLayer; typedef KisSharedPtr KisFileLayerSP; typedef KisWeakSharedPtr KisFileLayerWSP; class KisSelection; typedef KisSharedPtr KisSelectionSP; typedef KisWeakSharedPtr KisSelectionWSP; class KisSelectionComponent; typedef KisSharedPtr KisSelectionComponentSP; class KisSelectionMask; typedef KisSharedPtr KisSelectionMaskSP; class KisPixelSelection; typedef KisSharedPtr KisPixelSelectionSP; class KisHistogram; typedef KisSharedPtr KisHistogramSP; typedef QVector vKisSegments; class KisFilter; typedef KisSharedPtr KisFilterSP; class KisLayerStyleFilter; typedef KisSharedPtr KisLayerStyleFilterSP; class KisGenerator; typedef KisSharedPtr KisGeneratorSP; class KisConvolutionKernel; typedef KisSharedPtr KisConvolutionKernelSP; class KisAnnotation; typedef KisSharedPtr KisAnnotationSP; typedef QVector vKisAnnotationSP; typedef vKisAnnotationSP::iterator vKisAnnotationSP_it; typedef vKisAnnotationSP::const_iterator vKisAnnotationSP_cit; class KisAnimationFrameCache; typedef KisSharedPtr KisAnimationFrameCacheSP; typedef KisWeakSharedPtr KisAnimationFrameCacheWSP; class KisPaintingAssistant; typedef QSharedPointer KisPaintingAssistantSP; typedef QWeakPointer KisPaintingAssistantWSP; class KisReferenceImage; typedef QSharedPointer KisReferenceImageSP; typedef QWeakPointer KisReferenceImageWSP; // Repeat iterators class KisHLineIterator2; template class KisRepeatHLineIteratorPixelBase; typedef KisRepeatHLineIteratorPixelBase< KisHLineIterator2 > KisRepeatHLineConstIteratorNG; typedef KisSharedPtr KisRepeatHLineConstIteratorSP; class KisVLineIterator2; template class KisRepeatVLineIteratorPixelBase; typedef KisRepeatVLineIteratorPixelBase< KisVLineIterator2 > KisRepeatVLineConstIteratorNG; typedef KisSharedPtr KisRepeatVLineConstIteratorSP; // NG Iterators class KisHLineIteratorNG; typedef KisSharedPtr KisHLineIteratorSP; class KisHLineConstIteratorNG; typedef KisSharedPtr KisHLineConstIteratorSP; class KisVLineIteratorNG; typedef KisSharedPtr KisVLineIteratorSP; class KisVLineConstIteratorNG; typedef KisSharedPtr KisVLineConstIteratorSP; class KisRandomConstAccessorNG; typedef KisSharedPtr KisRandomConstAccessorSP; class KisRandomAccessorNG; typedef KisSharedPtr KisRandomAccessorSP; class KisRandomSubAccessor; typedef KisSharedPtr KisRandomSubAccessorSP; // Things typedef QVector vQPointF; class KisPaintOpPreset; typedef KisSharedPtr KisPaintOpPresetSP; typedef KisWeakSharedPtr KisPaintOpPresetWSP; template class KisPinnedSharedPtr; class KisPaintOpSettings; typedef KisPinnedSharedPtr KisPaintOpSettingsSP; template class KisRestrictedSharedPtr; typedef KisRestrictedSharedPtr KisPaintOpSettingsRestrictedSP; class KisPaintOp; typedef KisSharedPtr KisPaintOpSP; class KoID; typedef QList KoIDList; class KoUpdater; template class QPointer; typedef QPointer KoUpdaterPtr; class KisProcessingVisitor; typedef KisSharedPtr KisProcessingVisitorSP; class KUndo2Command; typedef QSharedPointer KUndo2CommandSP; typedef QList KisNodeList; typedef QSharedPointer KisNodeListSP; typedef QList KisPaintDeviceList; class KisStroke; typedef QSharedPointer KisStrokeSP; typedef QWeakPointer KisStrokeWSP; typedef KisStrokeWSP KisStrokeId; class KisFilterConfiguration; typedef KisPinnedSharedPtr KisFilterConfigurationSP; class KisPropertiesConfiguration; typedef KisPinnedSharedPtr KisPropertiesConfigurationSP; class KisLockedProperties; typedef KisSharedPtr KisLockedPropertiesSP; class KisProjectionUpdatesFilter; typedef QSharedPointer KisProjectionUpdatesFilterSP; class KisAbstractProjectionPlane; typedef QSharedPointer KisAbstractProjectionPlaneSP; typedef QWeakPointer KisAbstractProjectionPlaneWSP; class KisProjectionLeaf; typedef QSharedPointer KisProjectionLeafSP; typedef QWeakPointer KisProjectionLeafWSP; class KisKeyframe; typedef QSharedPointer KisKeyframeSP; typedef QWeakPointer KisKeyframeWSP; +class KisKeyframeBase; +typedef QSharedPointer KisKeyframeBaseSP; + class KisFilterChain; typedef KisSharedPtr KisFilterChainSP; class KisProofingConfiguration; typedef QSharedPointer KisProofingConfigurationSP; typedef QWeakPointer KisProofingConfigurationWSP; class KisLayerComposition; typedef QSharedPointer KisLayerCompositionSP; typedef QWeakPointer KisLayerCompositionWSP; class KisMirrorAxis; typedef KisSharedPtr KisMirrorAxisSP; typedef KisWeakSharedPtr KisMirrorAxisWSP; #include #include #include #include #include #endif // KISTYPES_H_ diff --git a/libs/image/tests/CMakeLists.txt b/libs/image/tests/CMakeLists.txt index bfcc78a7f9..7d2f6621ba 100644 --- a/libs/image/tests/CMakeLists.txt +++ b/libs/image/tests/CMakeLists.txt @@ -1,157 +1,158 @@ # cmake in some versions for some not yet known reasons fails to run automoc # on random targets (changing target names already has an effect) # As temporary workaround skipping build of tests on these versions for now # See https://mail.kde.org/pipermail/kde-buildsystem/2015-June/010819.html # extend range of affected cmake versions as needed if(NOT ${CMAKE_VERSION} VERSION_LESS 3.1.3 AND NOT ${CMAKE_VERSION} VERSION_GREATER 3.2.3) message(WARNING "Skipping krita/image/tests, CMake in at least versions 3.1.3 - 3.2.3 seems to have a problem with automoc. \n(FRIENDLY REMINDER: PLEASE DON'T BREAK THE TESTS!)") set (HAVE_FAILING_CMAKE TRUE) else() set (HAVE_FAILING_CMAKE FALSE) endif() include_directories( ${CMAKE_BINARY_DIR}/libs/image/ ${CMAKE_SOURCE_DIR}/libs/image/ ${CMAKE_SOURCE_DIR}/libs/image/brushengine ${CMAKE_SOURCE_DIR}/libs/image/tiles3 ${CMAKE_SOURCE_DIR}/libs/image/tiles3/swap ${CMAKE_SOURCE_DIR}/sdk/tests ) include_Directories(SYSTEM ${EIGEN3_INCLUDE_DIR} ) if(HAVE_VC) include_directories(${Vc_INCLUDE_DIR}) endif() include(ECMAddTests) include(KritaAddBrokenUnitTest) macro_add_unittest_definitions() set(KisRandomGeneratorDemoSources kis_random_generator_demo.cpp kimageframe.cpp) ki18n_wrap_ui(KisRandomGeneratorDemoSources kis_random_generator_demo.ui) add_executable(KisRandomGeneratorDemo ${KisRandomGeneratorDemoSources}) target_link_libraries(KisRandomGeneratorDemo kritaimage) ecm_mark_as_test(KisRandomGeneratorDemo) ecm_add_tests( kis_base_node_test.cpp kis_fast_math_test.cpp kis_node_test.cpp kis_node_facade_test.cpp kis_fixed_paint_device_test.cpp kis_layer_test.cpp kis_effect_mask_test.cpp kis_iterator_test.cpp kis_painter_test.cpp kis_selection_test.cpp kis_count_visitor_test.cpp kis_projection_test.cpp kis_properties_configuration_test.cpp kis_transaction_test.cpp kis_pixel_selection_test.cpp kis_group_layer_test.cpp kis_paint_layer_test.cpp kis_adjustment_layer_test.cpp kis_annotation_test.cpp kis_change_profile_visitor_test.cpp kis_clone_layer_test.cpp kis_colorspace_convert_visitor_test.cpp kis_convolution_painter_test.cpp kis_crop_processing_visitor_test.cpp kis_processing_applicator_test.cpp kis_datamanager_test.cpp kis_fill_painter_test.cpp kis_filter_configuration_test.cpp kis_filter_test.cpp kis_filter_processing_information_test.cpp kis_filter_registry_test.cpp kis_filter_strategy_test.cpp kis_gradient_painter_test.cpp kis_image_commands_test.cpp kis_image_test.cpp kis_image_signal_router_test.cpp kis_iterators_ng_test.cpp kis_iterator_benchmark.cpp kis_updater_context_test.cpp kis_simple_update_queue_test.cpp kis_stroke_test.cpp kis_simple_stroke_strategy_test.cpp kis_stroke_strategy_undo_command_based_test.cpp kis_strokes_queue_test.cpp kis_mask_test.cpp kis_math_toolbox_test.cpp kis_name_server_test.cpp kis_node_commands_test.cpp kis_node_graph_listener_test.cpp kis_node_visitor_test.cpp kis_paint_information_test.cpp kis_distance_information_test.cpp kis_paintop_test.cpp kis_pattern_test.cpp kis_selection_mask_test.cpp kis_shared_ptr_test.cpp kis_bsplines_test.cpp kis_warp_transform_worker_test.cpp kis_liquify_transform_worker_test.cpp kis_transparency_mask_test.cpp kis_types_test.cpp kis_vec_test.cpp kis_filter_config_widget_test.cpp kis_mask_generator_test.cpp kis_cubic_curve_test.cpp kis_fixed_point_maths_test.cpp kis_node_query_path_test.cpp kis_filter_weights_buffer_test.cpp kis_filter_weights_applicator_test.cpp kis_fill_interval_test.cpp kis_fill_interval_map_test.cpp kis_scanline_fill_test.cpp kis_psd_layer_style_test.cpp kis_layer_style_projection_plane_test.cpp kis_lod_capable_layer_offset_test.cpp kis_algebra_2d_test.cpp kis_marker_painter_test.cpp kis_lazy_brush_test.cpp kis_colorize_mask_test.cpp kis_mask_similarity_test.cpp KisMaskGeneratorTest.cpp kis_layer_style_filter_environment_test.cpp kis_asl_parser_test.cpp KisPerStrokeRandomSourceTest.cpp KisWatershedWorkerTest.cpp kis_dom_utils_test.cpp kis_transform_worker_test.cpp kis_perspective_transform_worker_test.cpp kis_cs_conversion_test.cpp kis_processings_test.cpp kis_projection_leaf_test.cpp kis_histogram_test.cpp kis_onion_skin_compositor_test.cpp kis_paint_device_test.cpp kis_queues_progress_updater_test.cpp kis_image_animation_interface_test.cpp kis_walkers_test.cpp kis_async_merger_test.cpp kis_cage_transform_worker_test.cpp kis_random_generator_test.cpp kis_keyframing_test.cpp kis_filter_mask_test.cpp + kis_time_range_test.cpp LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) krita_add_broken_unit_tests( kis_transform_mask_test.cpp kis_layer_styles_test.cpp kis_update_scheduler_test.cpp LINK_LIBRARIES kritaimage Qt5::Test NAME_PREFIX "libs-image-" ) diff --git a/libs/image/tests/kis_dom_utils_test.cpp b/libs/image/tests/kis_dom_utils_test.cpp index 3b77ecb22e..aa2aa27462 100644 --- a/libs/image/tests/kis_dom_utils_test.cpp +++ b/libs/image/tests/kis_dom_utils_test.cpp @@ -1,177 +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. */ #include "kis_dom_utils_test.h" #include #include "kis_dom_utils.h" #include "kis_debug.h" const qreal f1 = 0.0003; const qreal f2 = 1e-15; const qreal f3 = 1356.78301; const qreal f4 = 1234567.8901; const int i1 = 0; const int i2 = 13; const int i3 = -13; inline bool checkDifference(qreal a, qreal b, qreal portionTolerance) { return qAbs(a) > 1e-10 ? qAbs(a - b) / qAbs(a) < portionTolerance : qAbs(a - b) < 1e-10; } QString saveData() { QDomDocument doc("testdoc"); QDomElement root = doc.createElement("rootNode"); doc.appendChild(root); KisDomUtils::saveValue(&root, "valueF1", f1); KisDomUtils::saveValue(&root, "valueF2", f2); KisDomUtils::saveValue(&root, "valueF3", f3); KisDomUtils::saveValue(&root, "valueF4", f4); KisDomUtils::saveValue(&root, "valueI1", i1); KisDomUtils::saveValue(&root, "valueI2", i2); KisDomUtils::saveValue(&root, "valueI3", i3); return doc.toString(); } void checkLoadData(const QString &xmlData) { QDomDocument doc; doc.setContent(xmlData); QDomElement root = doc.documentElement(); qreal value = 0.0; QVERIFY(KisDomUtils::loadValue(root, "valueF1", &value)); QVERIFY(checkDifference(f1, value, 0.01)); QVERIFY(KisDomUtils::loadValue(root, "valueF2", &value)); QVERIFY(checkDifference(f2, value, 0.01)); QVERIFY(KisDomUtils::loadValue(root, "valueF3", &value)); QVERIFY(checkDifference(f3, value, 0.01)); QVERIFY(KisDomUtils::loadValue(root, "valueF4", &value)); QVERIFY(checkDifference(f4, value, 0.01)); int iValue = 0; QVERIFY(KisDomUtils::loadValue(root, "valueI1", &iValue)); QCOMPARE(i1, iValue); QVERIFY(KisDomUtils::loadValue(root, "valueI2", &iValue)); QCOMPARE(i2, iValue); QVERIFY(KisDomUtils::loadValue(root, "valueI3", &iValue)); QCOMPARE(i3, iValue); } void KisDomUtilsTest::testC2C() { QLocale::setDefault(QLocale::C); QString xmlData = saveData(); QLocale::setDefault(QLocale::C); checkLoadData(xmlData); } void KisDomUtilsTest::testG2G() { QLocale::setDefault(QLocale::German); QString xmlData = saveData(); QLocale::setDefault(QLocale::German); checkLoadData(xmlData); } void KisDomUtilsTest::testR2R() { QLocale::setDefault(QLocale::Russian); QString xmlData = saveData(); QLocale::setDefault(QLocale::Russian); checkLoadData(xmlData); } void KisDomUtilsTest::testC2G() { QLocale::setDefault(QLocale::C); QString xmlData = saveData(); QLocale::setDefault(QLocale::German); checkLoadData(xmlData); } void KisDomUtilsTest::testR2G() { QLocale::setDefault(QLocale::Russian); QString xmlData = saveData(); QLocale::setDefault(QLocale::German); checkLoadData(xmlData); } void KisDomUtilsTest::testG2C() { QLocale::setDefault(QLocale::German); QString xmlData = saveData(); QLocale::setDefault(QLocale::C); checkLoadData(xmlData); } void KisDomUtilsTest::testG2R() { QLocale::setDefault(QLocale::German); QString xmlData = saveData(); QLocale::setDefault(QLocale::Russian); checkLoadData(xmlData); } #include "kis_time_range.h" void KisDomUtilsTest::testIntegralType() { - KisTimeRange r1(1, 10); - KisTimeRange r2(5, 15); - + KisTimeSpan r1(1, 10); + KisTimeSpan r2(5, 15); QDomDocument doc("testdoc"); QDomElement root = doc.createElement("rootNode"); doc.appendChild(root); KisDomUtils::saveValue(&root, "timeRange", r1); KisDomUtils::loadValue(root, "timeRange", &r2); QCOMPARE(r2, r1); } QTEST_MAIN(KisDomUtilsTest) diff --git a/libs/image/tests/kis_image_animation_interface_test.cpp b/libs/image/tests/kis_image_animation_interface_test.cpp index 9cd46f8007..325201a0b8 100644 --- a/libs/image/tests/kis_image_animation_interface_test.cpp +++ b/libs/image/tests/kis_image_animation_interface_test.cpp @@ -1,309 +1,309 @@ /* * 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_test.h" #include #include #include #include "kundo2command.h" #include "kis_debug.h" #include "kis_image_animation_interface.h" #include "kis_signal_compressor_with_param.h" #include "kis_raster_keyframe_channel.h" #include "kis_time_range.h" void checkFrame(KisImageAnimationInterface *i, KisImageSP image, int frameId, bool externalFrameActive, const QRect &rc) { QCOMPARE(i->currentTime(), frameId); QCOMPARE(i->externalFrameActive(), externalFrameActive); QCOMPARE(image->projection()->exactBounds(), rc); } void KisImageAnimationInterfaceTest::testFrameRegeneration() { QRect refRect(QRect(0,0,512,512)); TestUtil::MaskParent p(refRect); KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8); p.image->addNode(layer2); const QRect rc1(101,101,100,100); const QRect rc2(102,102,100,100); const QRect rc3(103,103,100,100); const QRect rc4(104,104,100,100); KisImageAnimationInterface *i = p.image->animationInterface(); KisPaintDeviceSP dev1 = p.layer->paintDevice(); KisPaintDeviceSP dev2 = layer2->paintDevice(); p.layer->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); layer2->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); // check frame 0 { dev1->fill(rc1, KoColor(Qt::red, dev1->colorSpace())); QCOMPARE(dev1->exactBounds(), rc1); dev2->fill(rc2, KoColor(Qt::green, dev1->colorSpace())); QCOMPARE(dev2->exactBounds(), rc2); p.image->refreshGraph(); checkFrame(i, p.image, 0, false, rc1 | rc2); } // switch/create frame 10 i->switchCurrentTimeAsync(10); p.image->waitForDone(); KisKeyframeChannel *channel1 = dev1->keyframeChannel(); channel1->addKeyframe(10); KisKeyframeChannel *channel2 = dev2->keyframeChannel(); channel2->addKeyframe(10); // check frame 10 { QVERIFY(dev1->exactBounds().isEmpty()); QVERIFY(dev2->exactBounds().isEmpty()); dev1->fill(rc3, KoColor(Qt::red, dev2->colorSpace())); QCOMPARE(dev1->exactBounds(), rc3); dev2->fill(rc4, KoColor(Qt::green, dev2->colorSpace())); QCOMPARE(dev2->exactBounds(), rc4); p.image->refreshGraph(); checkFrame(i, p.image, 10, false, rc3 | rc4); } // check external frame (frame 0) { SignalToFunctionProxy proxy1(std::bind(checkFrame, i, p.image, 0, true, rc1 | rc2)); connect(i, SIGNAL(sigFrameReady(int)), &proxy1, SLOT(start()), Qt::DirectConnection); i->requestFrameRegeneration(0, QRegion(refRect)); QTest::qWait(200); } // current frame (flame 10) is still unchanged checkFrame(i, p.image, 10, false, rc3 | rc4); // switch back to frame 0 i->switchCurrentTimeAsync(0); p.image->waitForDone(); // check frame 0 { QCOMPARE(dev1->exactBounds(), rc1); QCOMPARE(dev2->exactBounds(), rc2); checkFrame(i, p.image, 0, false, rc1 | rc2); } // check external frame (frame 10) { SignalToFunctionProxy proxy2(std::bind(checkFrame, i, p.image, 10, true, rc3 | rc4)); connect(i, SIGNAL(sigFrameReady(int)), &proxy2, SLOT(start()), Qt::DirectConnection); i->requestFrameRegeneration(10, QRegion(refRect)); QTest::qWait(200); } // current frame is still unchanged checkFrame(i, p.image, 0, false, rc1 | rc2); } void KisImageAnimationInterfaceTest::testFramesChangedSignal() { QRect refRect(QRect(0,0,512,512)); TestUtil::MaskParent p(refRect); KisPaintLayerSP layer1 = p.layer; KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8); p.image->addNode(layer2); layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); layer2->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); KisImageAnimationInterface *i = p.image->animationInterface(); KisPaintDeviceSP dev1 = p.layer->paintDevice(); KisPaintDeviceSP dev2 = layer2->paintDevice(); KisKeyframeChannel *channel = dev2->keyframeChannel(); channel->addKeyframe(10); channel->addKeyframe(20); // check switching a frame doesn't invalidate cache - QSignalSpy spy(i, SIGNAL(sigFramesChanged(KisTimeRange,QRect))); + QSignalSpy spy(i, SIGNAL(sigFramesChanged(KisFrameSet,QRect))); p.image->animationInterface()->switchCurrentTimeAsync(15); p.image->waitForDone(); QCOMPARE(spy.count(), 0); i->notifyNodeChanged(layer1.data(), QRect(), false); QCOMPARE(spy.count(), 1); QList arguments = spy.takeFirst(); - QCOMPARE(arguments.at(0).value(), KisTimeRange::infinite(0)); + QCOMPARE(arguments.at(0).value(), KisFrameSet::infiniteFrom(0)); i->notifyNodeChanged(layer2.data(), QRect(), false); QCOMPARE(spy.count(), 1); arguments = spy.takeFirst(); - QCOMPARE(arguments.at(0).value(), KisTimeRange(10, 10)); + QCOMPARE(arguments.at(0).value(), KisFrameSet::between(10, 19)); // Recursive channel = dev1->keyframeChannel(); channel->addKeyframe(13); spy.clear(); i->notifyNodeChanged(p.image->root().data(), QRect(), true); QCOMPARE(spy.count(), 1); arguments = spy.takeFirst(); - QEXPECT_FAIL("", "Infinite time range is expected to be (0, -2147483648), but is (1, -2147483648)", Continue); - QCOMPARE(arguments.at(0).value(), KisTimeRange::infinite(10)); + const KisFrameSet &value = arguments.at(0).value(); + QCOMPARE(value, KisFrameSet::infiniteFrom(0)); } void KisImageAnimationInterfaceTest::testAnimationCompositionBug() { QRect rect(QRect(0,0,512,512)); TestUtil::MaskParent p(rect); KUndo2Command parentCommand; KisPaintLayerSP layer1 = p.layer; KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8); p.image->addNode(layer2); layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); layer2->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); layer1->paintDevice()->fill(rect, KoColor(Qt::red, layer1->paintDevice()->colorSpace())); layer2->paintDevice()->fill(QRect(128,128,128,128), KoColor(Qt::black, layer2->paintDevice()->colorSpace())); KisKeyframeChannel *rasterChannel = layer2->getKeyframeChannel(KisKeyframeChannel::Content.id()); rasterChannel->addKeyframe(10, &parentCommand); p.image->refreshGraph(); m_image = p.image; connect(p.image->animationInterface(), SIGNAL(sigFrameReady(int)), this, SLOT(slotFrameDone()), Qt::DirectConnection); p.image->animationInterface()->requestFrameRegeneration(5, rect); QTest::qWait(200); KisPaintDeviceSP tmpDevice = new KisPaintDevice(p.image->colorSpace()); tmpDevice->fill(rect, KoColor(Qt::red, tmpDevice->colorSpace())); tmpDevice->fill(QRect(128,128,128,128), KoColor(Qt::black, tmpDevice->colorSpace())); QImage expected = tmpDevice->createThumbnail(512, 512); QVERIFY(m_compositedFrame == expected); } void KisImageAnimationInterfaceTest::slotFrameDone() { m_compositedFrame = m_image->projection()->createThumbnail(512, 512); } void KisImageAnimationInterfaceTest::testSwitchFrameWithUndo() { QRect refRect(QRect(0,0,512,512)); TestUtil::MaskParent p(refRect); KisPaintLayerSP layer1 = p.layer; layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); KisImageAnimationInterface *i = p.image->animationInterface(); KisPaintDeviceSP dev1 = p.layer->paintDevice(); KisKeyframeChannel *channel = dev1->keyframeChannel(); channel->addKeyframe(10); channel->addKeyframe(20); QCOMPARE(i->currentTime(), 0); i->requestTimeSwitchWithUndo(15); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 15); i->requestTimeSwitchWithUndo(16); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 16); // the two commands have been merged! p.undoStore->undo(); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 0); p.undoStore->redo(); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 16); } #include "kis_processing_applicator.h" void KisImageAnimationInterfaceTest::testSwitchFrameHangup() { QRect refRect(QRect(0,0,512,512)); TestUtil::MaskParent p(refRect); KisPaintLayerSP layer1 = p.layer; layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); KisImageAnimationInterface *i = p.image->animationInterface(); KisPaintDeviceSP dev1 = p.layer->paintDevice(); KisKeyframeChannel *channel = dev1->keyframeChannel(); channel->addKeyframe(10); channel->addKeyframe(20); QCOMPARE(i->currentTime(), 0); i->requestTimeSwitchWithUndo(15); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 15); KisProcessingApplicator applicator(p.image, 0); i->requestTimeSwitchWithUndo(16); applicator.end(); QTest::qWait(100); p.image->waitForDone(); QCOMPARE(i->currentTime(), 16); } QTEST_MAIN(KisImageAnimationInterfaceTest) diff --git a/libs/image/tests/kis_keyframing_test.cpp b/libs/image/tests/kis_keyframing_test.cpp index 638493a0cc..41f4d2dd52 100644 --- a/libs/image/tests/kis_keyframing_test.cpp +++ b/libs/image/tests/kis_keyframing_test.cpp @@ -1,528 +1,574 @@ /* * 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_keyframing_test.h" #include #include #include "kis_paint_device_frames_interface.h" #include "kis_keyframe_channel.h" #include "kis_scalar_keyframe_channel.h" #include "kis_raster_keyframe_channel.h" #include "kis_node.h" #include "kis_time_range.h" +#include "kis_animation_cycle.h" #include "kundo2command.h" - +#include "kis_pointer_utils.h" #include #include "testing_timed_default_bounds.h" void KisKeyframingTest::initTestCase() { cs = KoColorSpaceRegistry::instance()->rgb8(); red = new quint8[cs->pixelSize()]; green = new quint8[cs->pixelSize()]; blue = new quint8[cs->pixelSize()]; cs->fromQColor(Qt::red, red); cs->fromQColor(Qt::green, green); cs->fromQColor(Qt::blue, blue); } void KisKeyframingTest::cleanupTestCase() { delete[] red; delete[] green; delete[] blue; } void KisKeyframingTest::testScalarChannel() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); KisKeyframeSP key; bool ok; QCOMPARE(channel->hasScalarValue(), true); QCOMPARE(channel->minScalarValue(), -17.0); QCOMPARE(channel->maxScalarValue(), 31.0); QVERIFY(channel->keyframeAt(0) == 0); // Adding new keyframe key = channel->addKeyframe(42); channel->setScalarValue(key, 7.0); key = channel->keyframeAt(42); QCOMPARE(channel->scalarValue(key), 7.0); // Copying a keyframe - KisKeyframeSP key2 = channel->copyKeyframe(key, 13); + KisKeyframeSP key2 = channel->copyAsKeyframe(key, 2, 13); QVERIFY(key2 != 0); QVERIFY(channel->keyframeAt(13) == key2); QCOMPARE(channel->scalarValue(key2), 7.0); // Adding a keyframe where one exists key2 = channel->addKeyframe(13); QVERIFY(key2 != key); QCOMPARE(channel->keyframeCount(), 2); // Moving keyframes ok = channel->moveKeyframe(key, 10); QCOMPARE(ok, true); QVERIFY(channel->keyframeAt(42) == 0); key = channel->keyframeAt(10); QCOMPARE(channel->scalarValue(key), 7.0); // Moving a keyframe where another one exists ok = channel->moveKeyframe(key, 13); QCOMPARE(ok, true); QVERIFY(channel->keyframeAt(13) != key2); // Deleting a keyframe channel->deleteKeyframe(key); QVERIFY(channel->keyframeAt(10) == 0); QCOMPARE(channel->keyframeCount(), 0); delete channel; } void KisKeyframingTest::testScalarChannelUndoRedo() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); KisKeyframeSP key; QCOMPARE(channel->hasScalarValue(), true); QCOMPARE(channel->minScalarValue(), -17.0); QCOMPARE(channel->maxScalarValue(), 31.0); QVERIFY(channel->keyframeAt(0) == 0); // Adding new keyframe KUndo2Command addCmd; key = channel->addKeyframe(42, &addCmd); channel->setScalarValue(key, 7.0, &addCmd); key = channel->keyframeAt(42); QCOMPARE(channel->scalarValue(key), 7.0); addCmd.undo(); KisKeyframeSP newKey; newKey = channel->keyframeAt(42); QVERIFY(!newKey); addCmd.redo(); newKey = channel->keyframeAt(42); QVERIFY(newKey); QCOMPARE(channel->scalarValue(key), 7.0); QCOMPARE(channel->scalarValue(newKey), 7.0); delete channel; } void KisKeyframingTest::testScalarInterpolation() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), 0, 30, 0); KisKeyframeSP key1 = channel->addKeyframe(0); channel->setScalarValue(key1, 15); KisKeyframeSP key2 = channel->addKeyframe(10); channel->setScalarValue(key2, 30); // Constant key1->setInterpolationMode(KisKeyframe::Constant); QCOMPARE(channel->interpolatedValue(4), 15.0f); // Bezier key1->setInterpolationMode(KisKeyframe::Bezier); key1->setInterpolationTangents(QPointF(), QPointF(1,4)); key2->setInterpolationTangents(QPointF(-4,2), QPointF()); QVERIFY(qAbs(channel->interpolatedValue(4) - 24.9812f) < 0.1f); // Bezier, self-intersecting curve (auto-correct) channel->setScalarValue(key2, 15); key1->setInterpolationTangents(QPointF(), QPointF(13,10)); key2->setInterpolationTangents(QPointF(-13,10), QPointF()); QVERIFY(qAbs(channel->interpolatedValue(5) - 20.769f) < 0.1f); // Bezier, result outside allowed range (clamp) channel->setScalarValue(key2, 15); key1->setInterpolationTangents(QPointF(), QPointF(0, 50)); key2->setInterpolationTangents(QPointF(0, 50), QPointF()); QCOMPARE(channel->interpolatedValue(5), 30.0f); delete channel; } void KisKeyframingTest::testRasterChannel() { TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * channel = dev->createKeyframeChannel(KoID()); QCOMPARE(channel->hasScalarValue(), false); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QCOMPARE(channel->frameIdAt(0), 0); QVERIFY(channel->keyframeAt(0) != 0); KisKeyframeSP key_0 = channel->keyframeAt(0); // New keyframe KisKeyframeSP key_10 = channel->addKeyframe(10); QCOMPARE(channel->keyframeCount(), 2); QCOMPARE(dev->framesInterface()->frames().count(), 2); QVERIFY(channel->frameIdAt(10) != 0); dev->fill(0, 0, 512, 512, red); QImage thumb1a = dev->createThumbnail(50, 50); bounds->testingSetTime(10); dev->fill(0, 0, 512, 512, green); QImage thumb2a = dev->createThumbnail(50, 50); bounds->testingSetTime(0); QImage thumb1b = dev->createThumbnail(50, 50); QVERIFY(thumb2a != thumb1a); QVERIFY(thumb1b == thumb1a); // Duplicate keyframe - KisKeyframeSP key_20 = channel->copyKeyframe(key_0, 20); + KisKeyframeSP key_20 = channel->copyAsKeyframe(key_0, 0, 20); bounds->testingSetTime(20); QImage thumb3a = dev->createThumbnail(50, 50); QVERIFY(thumb3a == thumb1b); dev->fill(0, 0, 512, 512, blue); QImage thumb3b = dev->createThumbnail(50, 50); bounds->testingSetTime(0); QImage thumb1c = dev->createThumbnail(50, 50); QVERIFY(thumb3b != thumb3a); QVERIFY(thumb1c == thumb1b); // Delete keyrame QCOMPARE(channel->keyframeCount(), 3); QCOMPARE(dev->framesInterface()->frames().count(), 3); channel->deleteKeyframe(key_20); QCOMPARE(channel->keyframeCount(), 2); QCOMPARE(dev->framesInterface()->frames().count(), 2); QVERIFY(channel->keyframeAt(20) == 0); channel->deleteKeyframe(key_10); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->keyframeAt(10) == 0); channel->deleteKeyframe(key_0); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->keyframeAt(0) != 0); } void KisKeyframingTest::testChannelSignals() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); - KisKeyframeSP key; - KisKeyframeSP resKey; + KisKeyframeBaseSP key; + KisKeyframeBaseSP resKey; - qRegisterMetaType("KisKeyframeSP"); - QSignalSpy spyPreAdd(channel, SIGNAL(sigKeyframeAboutToBeAdded(KisKeyframeSP))); - QSignalSpy spyPostAdd(channel, SIGNAL(sigKeyframeAdded(KisKeyframeSP))); + qRegisterMetaType("KisKeyframeBaseSP"); + QSignalSpy spyPreAdd(channel, SIGNAL(sigKeyframeAboutToBeAdded(KisKeyframeBaseSP))); + QSignalSpy spyPostAdd(channel, SIGNAL(sigKeyframeAdded(KisKeyframeBaseSP))); - QSignalSpy spyPreRemove(channel, SIGNAL(sigKeyframeAboutToBeRemoved(KisKeyframeSP))); - QSignalSpy spyPostRemove(channel, SIGNAL(sigKeyframeRemoved(KisKeyframeSP))); + QSignalSpy spyPreRemove(channel, SIGNAL(sigKeyframeAboutToBeRemoved(KisKeyframeBaseSP))); + QSignalSpy spyPostRemove(channel, SIGNAL(sigKeyframeRemoved(KisKeyframeBaseSP))); - QSignalSpy spyPreMove(channel, SIGNAL(sigKeyframeAboutToBeMoved(KisKeyframeSP,int))); - QSignalSpy spyPostMove(channel, SIGNAL(sigKeyframeMoved(KisKeyframeSP,int))); + QSignalSpy spyPreMove(channel, SIGNAL(sigKeyframeAboutToBeMoved(KisKeyframeBaseSP,int))); + QSignalSpy spyPostMove(channel, SIGNAL(sigKeyframeMoved(KisKeyframeBaseSP,int))); QVERIFY(spyPreAdd.isValid()); QVERIFY(spyPostAdd.isValid()); QVERIFY(spyPreRemove.isValid()); QVERIFY(spyPostRemove.isValid()); QVERIFY(spyPreMove.isValid()); QVERIFY(spyPostMove.isValid()); // Adding a keyframe QCOMPARE(spyPreAdd.count(), 0); QCOMPARE(spyPostAdd.count(), 0); key = channel->addKeyframe(10); QCOMPARE(spyPreAdd.count(), 1); QCOMPARE(spyPostAdd.count(), 1); - resKey = spyPreAdd.at(0).at(0).value(); + resKey = spyPreAdd.at(0).at(0).value(); QVERIFY(resKey == key); - resKey = spyPostAdd.at(0).at(0).value(); + resKey = spyPostAdd.at(0).at(0).value(); QVERIFY(resKey == key); // Moving a keyframe QCOMPARE(spyPreMove.count(), 0); QCOMPARE(spyPostMove.count(), 0); channel->moveKeyframe(key, 15); QCOMPARE(spyPreMove.count(), 1); QCOMPARE(spyPostMove.count(), 1); - resKey = spyPreMove.at(0).at(0).value(); + resKey = spyPreMove.at(0).at(0).value(); QVERIFY(resKey == key); QCOMPARE(spyPreMove.at(0).at(1).toInt(), 15); - resKey = spyPostMove.at(0).at(0).value(); + resKey = spyPostMove.at(0).at(0).value(); QVERIFY(resKey == key); // No-op move (no signals) channel->moveKeyframe(key, 15); QCOMPARE(spyPreMove.count(), 1); QCOMPARE(spyPostMove.count(), 1); // Deleting a keyframe QCOMPARE(spyPreRemove.count(), 0); QCOMPARE(spyPostRemove.count(), 0); channel->deleteKeyframe(key); QCOMPARE(spyPreRemove.count(), 1); QCOMPARE(spyPostRemove.count(), 1); delete channel; } void KisKeyframingTest::testRasterFrameFetching() { TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); KisPaintDeviceSP devTarget = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * channel = dev->createKeyframeChannel(KoID()); channel->addKeyframe(0); channel->addKeyframe(10); channel->addKeyframe(50); bounds->testingSetTime(0); dev->fill(0, 0, 512, 512, red); QImage frame1 = dev->createThumbnail(50, 50); bounds->testingSetTime(10); dev->fill(0, 256, 512, 512, green); QImage frame2 = dev->createThumbnail(50, 50); bounds->testingSetTime(50); dev->fill(0, 0, 256, 512, blue); QImage frame3 = dev->createThumbnail(50, 50); bounds->testingSetTime(10); KisKeyframeSP keyframe = channel->activeKeyframeAt(0); channel->fetchFrame(keyframe, devTarget); QImage fetched1 = devTarget->createThumbnail(50, 50); keyframe = channel->activeKeyframeAt(10); channel->fetchFrame(keyframe, devTarget); QImage fetched2 = devTarget->createThumbnail(50, 50); keyframe = channel->activeKeyframeAt(50); channel->fetchFrame(keyframe, devTarget); QImage fetched3 = devTarget->createThumbnail(50, 50); QVERIFY(fetched1 == frame1); QVERIFY(fetched2 == frame2); QVERIFY(fetched3 == frame3); } void KisKeyframingTest::testDeleteFirstRasterChannel() { // Test Plan: // // delete // undo delete // move // undo move TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * channel = dev->createKeyframeChannel(KoID()); QCOMPARE(channel->hasScalarValue(), false); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QCOMPARE(channel->frameIdAt(0), 0); QVERIFY(channel->keyframeAt(0) != 0); KisKeyframeSP key_0 = channel->keyframeAt(0); { KUndo2Command cmd; bool deleteResult = channel->deleteKeyframe(key_0, &cmd); QVERIFY(deleteResult); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->frameIdAt(0) != 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(0) != key_0); cmd.undo(); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->frameIdAt(0) == 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(0) == key_0); } { KUndo2Command cmd; bool moveResult = channel->moveKeyframe(key_0, 1, &cmd); QVERIFY(moveResult); QCOMPARE(dev->framesInterface()->frames().count(), 2); QVERIFY(channel->frameIdAt(0) != 0); QVERIFY(channel->frameIdAt(1) == 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(1)); QVERIFY(channel->keyframeAt(0) != key_0); QVERIFY(channel->keyframeAt(1) == key_0); cmd.undo(); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->frameIdAt(0) == 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(0) == key_0); } } void KisKeyframingTest::testAffectedFrames() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); - KisTimeRange range; + KisFrameSet frames; channel->addKeyframe(10); channel->addKeyframe(20); channel->addKeyframe(30); // At a keyframe - range = channel->affectedFrames(20); - QCOMPARE(range.start(), 20); - QCOMPARE(range.end(), 29); - QCOMPARE(range.isInfinite(), false); + frames = channel->affectedFrames(20); + QCOMPARE(frames, KisFrameSet::between(20, 29)); // Between frames - range = channel->affectedFrames(25); - QCOMPARE(range.start(), 20); - QCOMPARE(range.end(), 29); - QCOMPARE(range.isInfinite(), false); + frames = channel->affectedFrames(25); + QCOMPARE(frames, KisFrameSet::between(20, 29)); // Before first frame - range = channel->affectedFrames(5); - QCOMPARE(range.start(), 0); - QCOMPARE(range.end(), 9); - QCOMPARE(range.isInfinite(), false); + frames = channel->affectedFrames(5); + QCOMPARE(frames, KisFrameSet::between(0, 9)); // After last frame - range = channel->affectedFrames(35); - QCOMPARE(range.start(), 30); - QCOMPARE(range.isInfinite(), true); + frames = channel->affectedFrames(35); + QCOMPARE(frames, KisFrameSet::infiniteFrom(30)); + + // Linked keyframes + + KisPaintDeviceSP dev = new KisPaintDevice(cs); + KisRasterKeyframeChannel * rasterChannel = dev->createKeyframeChannel(KoID()); + + auto key0 = rasterChannel->addKeyframe(0); + auto key5 = rasterChannel->addKeyframe(5); + auto key10 = rasterChannel->linkKeyframe(key0, 10, nullptr); + QCOMPARE(rasterChannel->affectedFrames(5), KisFrameSet::between(5,9)); + KisFrameSet result = rasterChannel->affectedFrames(1); + QCOMPARE(result, KisFrameSet::between(0, 4) | KisFrameSet::infiniteFrom(10)); + result = rasterChannel->affectedFrames(15); + QCOMPARE(result, KisFrameSet::between(0, 4) | KisFrameSet::infiniteFrom(10)); } void KisKeyframingTest::testMovingFrames() { TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * srcChannel = dev->createKeyframeChannel(KoID()); srcChannel->addKeyframe(0); srcChannel->addKeyframe(10); srcChannel->addKeyframe(50); KisPaintDeviceSP dev2 = new KisPaintDevice(*dev, KritaUtils::CopyAllFrames); KisRasterKeyframeChannel * dstChannel = dev->keyframeChannel(); for (int i = 0; i < 1000; i++) { const int srcTime = 50 + i; const int dstTime = 60 + i; const int src2Time = 51 + i; { KUndo2Command parentCommand; KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); KIS_ASSERT(srcKeyframe); dstChannel->copyExternalKeyframe(srcChannel, srcTime, dstTime, &parentCommand); srcChannel->deleteKeyframe(srcKeyframe, &parentCommand); } for (int j = qMax(0, i-15); j < i+5; ++j) { bounds->testingSetTime(j); QRect rc1 = dev->extent(); QRect rc2 = dev2->extent(); Q_UNUSED(rc1); Q_UNUSED(rc2); } { KUndo2Command parentCommand; KisKeyframeSP dstKeyframe = dstChannel->keyframeAt(dstTime); KIS_ASSERT(dstKeyframe); srcChannel->copyExternalKeyframe(dstChannel, dstTime, src2Time, &parentCommand); dstChannel->deleteKeyframe(dstKeyframe, &parentCommand); } } } +void KisKeyframingTest::testCycles() +{ + TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); + + KisPaintDeviceSP dev = new KisPaintDevice(cs); + dev->setDefaultBounds(bounds); + KisRasterKeyframeChannel *channel = dev->createKeyframeChannel(KoID()); + + KisKeyframeSP frame10 = channel->addKeyframe(10); + channel->addKeyframe(12); + KisKeyframeSP frame16 = channel->addKeyframe(16); + channel->addKeyframe(20); + + QScopedPointer cmd(new KUndo2Command()); + channel->createRepeat(30, {10, 19}, cmd.data()); + + // Repeats resolve to the original cycled range + QCOMPARE(channel->cycledRangeAt(29), KisTimeSpan()); + QCOMPARE(channel->cycledRangeAt(30), KisTimeSpan(10, 19)); + QCOMPARE(channel->cycledRangeAt(50), KisTimeSpan(10, 19)); + QCOMPARE(channel->cycledRangeAt(100), KisTimeSpan(10, 19)); + + // Affected frames contain original frames and repeats + QCOMPARE(channel->affectedFrames(15), KisFrameSet::between(12, 15) | KisFrameSet::infiniteFrom(32)); + QCOMPARE(channel->affectedFrames(50), KisFrameSet::between(10, 11) | KisFrameSet::infiniteFrom(30)); + + // All repeats within the queried range are reported as identical. Original is always included. + QCOMPARE(channel->identicalFrames(50, KisTimeSpan(40, 60)), KisFrameSet({{10, 11}, {40, 41}, {50, 51}, {60, 60}})); + + // Repeat ends at the next keyframe + channel->addKeyframe(42); + QCOMPARE(channel->cycledRangeAt(41), KisTimeSpan(10, 19)); + QCOMPARE(channel->cycledRangeAt(42), KisTimeSpan()); + + // Finitely many repeats are all included separately as affected + QCOMPARE(channel->affectedFrames(40), KisFrameSet({{10, 11}, {30, 31}, {40, 41}})); +} + QTEST_MAIN(KisKeyframingTest) diff --git a/libs/image/tests/kis_keyframing_test.h b/libs/image/tests/kis_keyframing_test.h index dbad43ce88..af73361d4c 100644 --- a/libs/image/tests/kis_keyframing_test.h +++ b/libs/image/tests/kis_keyframing_test.h @@ -1,55 +1,57 @@ /* * 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_KEYFRAMING_TEST_H #define KIS_KEYFRAMING_TEST_H #include #include "KoColor.h" class KisKeyframingTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testScalarChannel(); void testScalarChannelUndoRedo(); void testScalarInterpolation(); void testRasterChannel(); void testChannelSignals(); void testRasterFrameFetching(); void testDeleteFirstRasterChannel(); void testAffectedFrames(); void cleanupTestCase(); void testMovingFrames(); + void testCycles(); + private: const KoColorSpace *cs; quint8* red; quint8* green; quint8* blue; }; #endif diff --git a/libs/image/tests/kis_time_range_test.cpp b/libs/image/tests/kis_time_range_test.cpp new file mode 100644 index 0000000000..e566269c44 --- /dev/null +++ b/libs/image/tests/kis_time_range_test.cpp @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2018 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_time_range_test.h" + +#include +#include "kis_time_range.h" + +void KisTimeRangeTest::testTimeSpans() +{ + KisTimeSpan emptySpan; + QCOMPARE(emptySpan.isEmpty(), true); + QCOMPARE(emptySpan.duration(), 0); + QCOMPARE(emptySpan.contains(0), false); + + KisTimeSpan span1(7, 17); + KisTimeSpan span2(10, 20); + KisTimeSpan span3(20, 30); + + QCOMPARE(span1.isEmpty(), false); + QCOMPARE(span1.duration(), 11); + QCOMPARE(span1.contains(6), false); + QCOMPARE(span1.contains(7), true); + QCOMPARE(span1.contains(17), true); + QCOMPARE(span1.contains(18), false); + + QCOMPARE(emptySpan | span1, span1); + QCOMPARE(span1 | emptySpan, span1); + QCOMPARE(emptySpan & span1, KisTimeSpan()); + QCOMPARE(span1 & emptySpan, KisTimeSpan()); + + QCOMPARE(span1 | span2, KisTimeSpan(7, 20)); + QCOMPARE(span2 | span1, KisTimeSpan(7, 20)); + QCOMPARE(span1 & span2, KisTimeSpan(10, 17)); + QCOMPARE(span2 & span1, KisTimeSpan(10, 17)); + + QCOMPARE(span1 | span3, KisTimeSpan(7, 30)); + QCOMPARE(span3 | span1, KisTimeSpan(7, 30)); + QCOMPARE(span1 & span3, KisTimeSpan()); + QCOMPARE(span3 & span1, KisTimeSpan()); +} + +void KisTimeRangeTest::testFrameSets() +{ + KisFrameSet emptySet; + QCOMPARE(emptySet.contains(0), false); + QCOMPARE(emptySet.isEmpty(), true); + QCOMPARE(emptySet.isInfinite(), false); + QCOMPARE(emptySet, KisFrameSet()); + + KisFrameSet set1 = KisFrameSet::between(7, 17); + + QCOMPARE(set1.isEmpty(), false); + QCOMPARE(set1.isInfinite(), false); + QCOMPARE(set1.start(), 7); + QCOMPARE(set1.contains(6), false); + QCOMPARE(set1.contains(7), true); + QCOMPARE(set1.contains(17), true); + QCOMPARE(set1.contains(18), false); + + KisFrameSet set2 = KisFrameSet::between(10, 20); + + QCOMPARE(set1 == set2, false); + + KisFrameSet intersection12 = set2 & set1; + QCOMPARE(intersection12.isEmpty(), false); + QCOMPARE(intersection12.isInfinite(), false); + QCOMPARE(intersection12.start(), 10); + QCOMPARE(intersection12.contains(9), false); + QCOMPARE(intersection12.contains(10), true); + QCOMPARE(intersection12.contains(17), true); + QCOMPARE(intersection12.contains(18), false); + + KisFrameSet set3 = KisFrameSet::between(20, 30); + + QCOMPARE(set1 & set3, emptySet); + + KisFrameSet union13 = set3 | set1; + QCOMPARE(union13.isEmpty(), false); + QCOMPARE(union13.isInfinite(), false); + QCOMPARE(union13.start(), 7); + QCOMPARE(union13.contains(6), false); + QCOMPARE(union13.contains(7), true); + QCOMPARE(union13.contains(17), true); + QCOMPARE(union13.contains(18), false); + QCOMPARE(union13.contains(19), false); + QCOMPARE(union13.contains(20), true); + QCOMPARE(union13.contains(30), true); + QCOMPARE(union13.contains(31), false); + + QCOMPARE(set1 - set3, set1); + QCOMPARE(set3 - set1, set3); + + KisFrameSet set4 = KisFrameSet({KisTimeSpan(9,10), KisTimeSpan(13, 14)}, 16); + KisFrameSet expected14 = KisFrameSet({KisTimeSpan(7,8), KisTimeSpan(11,12), KisTimeSpan(15,15)}); + KisFrameSet difference14 = set1 - set4; + + QCOMPARE(difference14.isEmpty(), false); + QCOMPARE(difference14.isInfinite(), false); + QCOMPARE(difference14, expected14); + QCOMPARE(set4 - set1, KisFrameSet::infiniteFrom(18)); + + KisFrameSet set5 = KisFrameSet::infiniteFrom(9); + + QCOMPARE(set5 & set1, KisFrameSet::between(9, 17)); + QCOMPARE(set5 | set1, KisFrameSet::infiniteFrom(7)); + QCOMPARE(set5 & set2, set2); + QCOMPARE(set5 | set2, set5); +} + +QTEST_MAIN(KisTimeRangeTest) diff --git a/libs/image/tests/kis_keyframing_test.h b/libs/image/tests/kis_time_range_test.h similarity index 55% copy from libs/image/tests/kis_keyframing_test.h copy to libs/image/tests/kis_time_range_test.h index dbad43ce88..2aed5856b3 100644 --- a/libs/image/tests/kis_keyframing_test.h +++ b/libs/image/tests/kis_time_range_test.h @@ -1,55 +1,33 @@ /* - * Copyright (c) 2015 Jouni Pentikäinen + * Copyright (c) 2018 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_KEYFRAMING_TEST_H -#define KIS_KEYFRAMING_TEST_H +#ifndef KIS_TIME_RANGE_TEST +#define KIS_TIME_RANGE_TEST #include -#include "KoColor.h" -class KisKeyframingTest : public QObject +class KisTimeRangeTest : public QObject { Q_OBJECT private Q_SLOTS: - - void initTestCase(); - - void testScalarChannel(); - void testScalarChannelUndoRedo(); - void testScalarInterpolation(); - - void testRasterChannel(); - void testChannelSignals(); - void testRasterFrameFetching(); - void testDeleteFirstRasterChannel(); - void testAffectedFrames(); - void cleanupTestCase(); - - void testMovingFrames(); - -private: - - const KoColorSpace *cs; - quint8* red; - quint8* green; - quint8* blue; + void testTimeSpans(); + void testFrameSets(); }; #endif - diff --git a/libs/libkis/Document.cpp b/libs/libkis/Document.cpp index 9f731ebf4b..c6222f8990 100644 --- a/libs/libkis/Document.cpp +++ b/libs/libkis/Document.cpp @@ -1,995 +1,995 @@ /* * Copyright (c) 2016 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser 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 Lesser 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 "Document.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_animation_importer.h" #include #include #include #include struct Document::Private { Private() {} QPointer document; }; Document::Document(KisDocument *document, QObject *parent) : QObject(parent) , d(new Private) { d->document = document; } Document::~Document() { delete d; } bool Document::operator==(const Document &other) const { return (d->document == other.d->document); } bool Document::operator!=(const Document &other) const { return !(operator==(other)); } bool Document::batchmode() const { if (!d->document) return false; return d->document->fileBatchMode(); } void Document::setBatchmode(bool value) { if (!d->document) return; d->document->setFileBatchMode(value); } Node *Document::activeNode() const { QList activeNodes; Q_FOREACH(QPointer view, KisPart::instance()->views()) { if (view && view->document() == d->document) { activeNodes << view->currentNode(); } } if (activeNodes.size() > 0) { QList nodes = LibKisUtils::createNodeList(activeNodes, d->document->image()); return nodes.first(); } return 0; } void Document::setActiveNode(Node* value) { if (!value->node()) return; KisMainWindow *mainWin = KisPart::instance()->currentMainwindow(); if (!mainWin) return; KisViewManager *viewManager = mainWin->viewManager(); if (!viewManager) return; if (viewManager->document() != d->document) return; KisNodeManager *nodeManager = viewManager->nodeManager(); if (!nodeManager) return; KisNodeSelectionAdapter *selectionAdapter = nodeManager->nodeSelectionAdapter(); if (!selectionAdapter) return; selectionAdapter->setActiveNode(value->node()); } QList Document::topLevelNodes() const { if (!d->document) return QList(); Node n(d->document->image(), d->document->image()->rootLayer()); return n.childNodes(); } Node *Document::nodeByName(const QString &name) const { if (!d->document) return 0; KisNodeSP node = d->document->image()->rootLayer()->findChildByName(name); if (node.isNull()) return 0; return Node::createNode(d->document->image(), node); } QString Document::colorDepth() const { if (!d->document) return ""; return d->document->image()->colorSpace()->colorDepthId().id(); } QString Document::colorModel() const { if (!d->document) return ""; return d->document->image()->colorSpace()->colorModelId().id(); } QString Document::colorProfile() const { if (!d->document) return ""; return d->document->image()->colorSpace()->profile()->name(); } bool Document::setColorProfile(const QString &value) { if (!d->document) return false; if (!d->document->image()) return false; const KoColorProfile *profile = KoColorSpaceRegistry::instance()->profileByName(value); if (!profile) return false; bool retval = d->document->image()->assignImageProfile(profile); d->document->image()->setModified(); d->document->image()->initialRefreshGraph(); return retval; } bool Document::setColorSpace(const QString &colorModel, const QString &colorDepth, const QString &colorProfile) { if (!d->document) return false; if (!d->document->image()) return false; const KoColorSpace *colorSpace = KoColorSpaceRegistry::instance()->colorSpace(colorModel, colorDepth, colorProfile); if (!colorSpace) return false; d->document->image()->convertImageColorSpace(colorSpace, KoColorConversionTransformation::IntentPerceptual, KoColorConversionTransformation::HighQuality | KoColorConversionTransformation::NoOptimization); d->document->image()->setModified(); d->document->image()->initialRefreshGraph(); return true; } QColor Document::backgroundColor() { if (!d->document) return QColor(); if (!d->document->image()) return QColor(); const KoColor color = d->document->image()->defaultProjectionColor(); return color.toQColor(); } bool Document::setBackgroundColor(const QColor &color) { if (!d->document) return false; if (!d->document->image()) return false; KoColor background = KoColor(color, d->document->image()->colorSpace()); d->document->image()->setDefaultProjectionColor(background); d->document->image()->setModified(); d->document->image()->initialRefreshGraph(); return true; } QString Document::documentInfo() const { QDomDocument doc = KisDocument::createDomDocument("document-info" /*DTD name*/, "document-info" /*tag name*/, "1.1"); doc = d->document->documentInfo()->save(doc); return doc.toString(); } void Document::setDocumentInfo(const QString &document) { KoXmlDocument doc; QString errorMsg; int errorLine, errorColumn; doc.setContent(document, &errorMsg, &errorLine, &errorColumn); d->document->documentInfo()->load(doc); } QString Document::fileName() const { if (!d->document) return QString(); return d->document->url().toLocalFile(); } void Document::setFileName(QString value) { if (!d->document) return; QString mimeType = KisMimeDatabase::mimeTypeForFile(value, false); d->document->setMimeType(mimeType.toLatin1()); d->document->setUrl(QUrl::fromLocalFile(value)); } int Document::height() const { if (!d->document) return 0; KisImageSP image = d->document->image(); if (!image) return 0; return image->height(); } void Document::setHeight(int value) { if (!d->document) return; if (!d->document->image()) return; resizeImage(d->document->image()->bounds().x(), d->document->image()->bounds().y(), d->document->image()->width(), value); } QString Document::name() const { if (!d->document) return ""; return d->document->documentInfo()->aboutInfo("title"); } void Document::setName(QString value) { if (!d->document) return; d->document->documentInfo()->setAboutInfo("title", value); } int Document::resolution() const { if (!d->document) return 0; KisImageSP image = d->document->image(); if (!image) return 0; return qRound(d->document->image()->xRes() * 72); } void Document::setResolution(int value) { if (!d->document) return; KisImageSP image = d->document->image(); if (!image) return; d->document->image()->setResolution(value / 72.0, value / 72.0); } Node *Document::rootNode() const { if (!d->document) return 0; KisImageSP image = d->document->image(); if (!image) return 0; return Node::createNode(image, image->root()); } Selection *Document::selection() const { if (!d->document) return 0; if (!d->document->image()) return 0; if (!d->document->image()->globalSelection()) return 0; return new Selection(d->document->image()->globalSelection()); } void Document::setSelection(Selection* value) { if (!d->document) return; if (!d->document->image()) return; if (value) { d->document->image()->setGlobalSelection(value->selection()); } else { d->document->image()->setGlobalSelection(0); } } int Document::width() const { if (!d->document) return 0; KisImageSP image = d->document->image(); if (!image) return 0; return image->width(); } void Document::setWidth(int value) { if (!d->document) return; if (!d->document->image()) return; resizeImage(d->document->image()->bounds().x(), d->document->image()->bounds().y(), value, d->document->image()->height()); } int Document::xOffset() const { if (!d->document) return 0; KisImageSP image = d->document->image(); if (!image) return 0; return image->bounds().x(); } void Document::setXOffset(int x) { if (!d->document) return; if (!d->document->image()) return; resizeImage(x, d->document->image()->bounds().y(), d->document->image()->width(), d->document->image()->height()); } int Document::yOffset() const { if (!d->document) return 0; KisImageSP image = d->document->image(); if (!image) return 0; return image->bounds().y(); } void Document::setYOffset(int y) { if (!d->document) return; if (!d->document->image()) return; resizeImage(d->document->image()->bounds().x(), y, d->document->image()->width(), d->document->image()->height()); } double Document::xRes() const { if (!d->document) return 0.0; if (!d->document->image()) return 0.0; return d->document->image()->xRes()*72.0; } void Document::setXRes(double xRes) const { if (!d->document) return; if (!d->document->image()) return; d->document->image()->setResolution(xRes/72.0, d->document->image()->yRes()); } double Document::yRes() const { if (!d->document) return 0.0; if (!d->document->image()) return 0.0; return d->document->image()->yRes()*72.0; } void Document::setYRes(double yRes) const { if (!d->document) return; if (!d->document->image()) return; d->document->image()->setResolution(d->document->image()->xRes(), yRes/72.0); } QByteArray Document::pixelData(int x, int y, int w, int h) const { QByteArray ba; if (!d->document) return ba; KisImageSP image = d->document->image(); if (!image) return ba; KisPaintDeviceSP dev = image->projection(); ba.resize(w * h * dev->pixelSize()); dev->readBytes(reinterpret_cast(ba.data()), x, y, w, h); return ba; } bool Document::close() { bool retval = d->document->closeUrl(false); Q_FOREACH(KisView *view, KisPart::instance()->views()) { if (view->document() == d->document) { view->close(); view->closeView(); view->deleteLater(); } } KisPart::instance()->removeDocument(d->document); d->document = 0; return retval; } void Document::crop(int x, int y, int w, int h) { if (!d->document) return; KisImageSP image = d->document->image(); if (!image) return; QRect rc(x, y, w, h); image->cropImage(rc); } bool Document::exportImage(const QString &filename, const InfoObject &exportConfiguration) { if (!d->document) return false; const QString outputFormatString = KisMimeDatabase::mimeTypeForFile(filename, false); const QByteArray outputFormat = outputFormatString.toLatin1(); return d->document->exportDocumentSync(QUrl::fromLocalFile(filename), outputFormat, exportConfiguration.configuration()); } void Document::flatten() { if (!d->document) return; if (!d->document->image()) return; d->document->image()->flatten(0); } void Document::resizeImage(int x, int y, int w, int h) { if (!d->document) return; KisImageSP image = d->document->image(); if (!image) return; QRect rc; rc.setX(x); rc.setY(y); rc.setWidth(w); rc.setHeight(h); image->resizeImage(rc); } void Document::scaleImage(int w, int h, int xres, int yres, QString strategy) { if (!d->document) return; KisImageSP image = d->document->image(); if (!image) return; QRect rc = image->bounds(); rc.setWidth(w); rc.setHeight(h); KisFilterStrategy *actualStrategy = KisFilterStrategyRegistry::instance()->get(strategy); if (!actualStrategy) actualStrategy = KisFilterStrategyRegistry::instance()->get("Bicubic"); image->scaleImage(rc.size(), xres/72, yres/72, actualStrategy); } void Document::rotateImage(double radians) { if (!d->document) return; KisImageSP image = d->document->image(); if (!image) return; image->rotateImage(radians); } void Document::shearImage(double angleX, double angleY) { if (!d->document) return; KisImageSP image = d->document->image(); if (!image) return; image->shear(angleX, angleY); } bool Document::save() { if (!d->document) return false; if (d->document->url().isEmpty()) return false; bool retval = d->document->save(true, 0); d->document->waitForSavingToComplete(); return retval; } bool Document::saveAs(const QString &filename) { if (!d->document) return false; const QString outputFormatString = KisMimeDatabase::mimeTypeForFile(filename, false); const QByteArray outputFormat = outputFormatString.toLatin1(); QUrl oldUrl = d->document->url(); d->document->setUrl(QUrl::fromLocalFile(filename)); bool retval = d->document->saveAs(QUrl::fromLocalFile(filename), outputFormat, true); d->document->waitForSavingToComplete(); d->document->setUrl(oldUrl); return retval; } Node* Document::createNode(const QString &name, const QString &nodeType) { if (!d->document) return 0; if (!d->document->image()) return 0; KisImageSP image = d->document->image(); Node *node = 0; if (nodeType.toLower()== "paintlayer") { node = new Node(image, new KisPaintLayer(image, name, OPACITY_OPAQUE_U8)); } else if (nodeType.toLower() == "grouplayer") { node = new Node(image, new KisGroupLayer(image, name, OPACITY_OPAQUE_U8)); } else if (nodeType.toLower() == "filelayer") { node = new Node(image, new KisFileLayer(image, name, OPACITY_OPAQUE_U8)); } else if (nodeType.toLower() == "filterlayer") { node = new Node(image, new KisAdjustmentLayer(image, name, 0, 0)); } else if (nodeType.toLower() == "filllayer") { node = new Node(image, new KisGeneratorLayer(image, name, 0, 0)); } else if (nodeType.toLower() == "clonelayer") { node = new Node(image, new KisCloneLayer(0, image, name, OPACITY_OPAQUE_U8)); } else if (nodeType.toLower() == "vectorlayer") { node = new Node(image, new KisShapeLayer(d->document->shapeController(), image, name, OPACITY_OPAQUE_U8)); } else if (nodeType.toLower() == "transparencymask") { node = new Node(image, new KisTransparencyMask()); } else if (nodeType.toLower() == "filtermask") { node = new Node(image, new KisFilterMask()); } else if (nodeType.toLower() == "transformmask") { node = new Node(image, new KisTransformMask()); } else if (nodeType.toLower() == "selectionmask") { node = new Node(image, new KisSelectionMask(image)); } return node; } GroupLayer *Document::createGroupLayer(const QString &name) { if (!d->document) return 0; if (!d->document->image()) return 0; KisImageSP image = d->document->image(); return new GroupLayer(image, name); } FileLayer *Document::createFileLayer(const QString &name, const QString fileName, const QString scalingMethod) { if (!d->document) return 0; if (!d->document->image()) return 0; KisImageSP image = d->document->image(); return new FileLayer(image, name, this->fileName(), fileName, scalingMethod); } FilterLayer *Document::createFilterLayer(const QString &name, Filter &filter, Selection &selection) { if (!d->document) return 0; if (!d->document->image()) return 0; KisImageSP image = d->document->image(); return new FilterLayer(image, name, filter, selection); } FillLayer *Document::createFillLayer(const QString &name, const QString generatorName, InfoObject &configuration, Selection &selection) { if (!d->document) return 0; if (!d->document->image()) return 0; KisImageSP image = d->document->image(); KisGeneratorSP generator = KisGeneratorRegistry::instance()->value(generatorName); if (generator) { KisFilterConfigurationSP config = generator->factoryConfiguration(); Q_FOREACH(const QString property, configuration.properties().keys()) { config->setProperty(property, configuration.property(property)); } return new FillLayer(image, name, config, selection); } return 0; } CloneLayer *Document::createCloneLayer(const QString &name, const Node *source) { if (!d->document) return 0; if (!d->document->image()) return 0; KisImageSP image = d->document->image(); KisLayerSP layer = qobject_cast(source->node().data()); return new CloneLayer(image, name, layer); } VectorLayer *Document::createVectorLayer(const QString &name) { if (!d->document) return 0; if (!d->document->image()) return 0; KisImageSP image = d->document->image(); return new VectorLayer(d->document->shapeController(), image, name); } FilterMask *Document::createFilterMask(const QString &name, Filter &filter, const Node *selection_source) { if (!d->document) return 0; if (!d->document->image()) return 0; if(!selection_source) return 0; KisLayerSP layer = qobject_cast(selection_source->node().data()); if(layer.isNull()) return 0; KisImageSP image = d->document->image(); FilterMask* mask = new FilterMask(image, name, filter); qobject_cast(mask->node().data())->initSelection(layer); return mask; } FilterMask *Document::createFilterMask(const QString &name, Filter &filter, Selection &selection) { if (!d->document) return 0; if (!d->document->image()) return 0; KisImageSP image = d->document->image(); FilterMask* mask = new FilterMask(image, name, filter); qobject_cast(mask->node().data())->setSelection(selection.selection()); return mask; } SelectionMask *Document::createSelectionMask(const QString &name) { if (!d->document) return 0; if (!d->document->image()) return 0; KisImageSP image = d->document->image(); return new SelectionMask(image, name); } QImage Document::projection(int x, int y, int w, int h) const { if (!d->document || !d->document->image()) return QImage(); return d->document->image()->convertToQImage(x, y, w, h, 0); } QImage Document::thumbnail(int w, int h) const { if (!d->document || !d->document->image()) return QImage(); return d->document->generatePreview(QSize(w, h)).toImage(); } void Document::lock() { if (!d->document || !d->document->image()) return; d->document->image()->barrierLock(); } void Document::unlock() { if (!d->document || !d->document->image()) return; d->document->image()->unlock(); } void Document::waitForDone() { if (!d->document || !d->document->image()) return; d->document->image()->waitForDone(); } bool Document::tryBarrierLock() { if (!d->document || !d->document->image()) return false; return d->document->image()->tryBarrierLock(); } bool Document::isIdle() { if (!d->document || !d->document->image()) return false; return d->document->image()->isIdle(); } void Document::refreshProjection() { if (!d->document || !d->document->image()) return; d->document->image()->refreshGraph(); } QList Document::horizontalGuides() const { QList lines; if (!d->document || !d->document->image()) return lines; KisCoordinatesConverter converter; converter.setImage(d->document->image()); QTransform transform = converter.imageToDocumentTransform().inverted(); QList untransformedLines = d->document->guidesConfig().horizontalGuideLines(); for (int i = 0; i< untransformedLines.size(); i++) { qreal line = untransformedLines[i]; lines.append(transform.map(QPointF(line, line)).x()); } return lines; } QList Document::verticalGuides() const { QList lines; if (!d->document || !d->document->image()) return lines; KisCoordinatesConverter converter; converter.setImage(d->document->image()); QTransform transform = converter.imageToDocumentTransform().inverted(); QList untransformedLines = d->document->guidesConfig().verticalGuideLines(); for (int i = 0; i< untransformedLines.size(); i++) { qreal line = untransformedLines[i]; lines.append(transform.map(QPointF(line, line)).y()); } return lines; } bool Document::guidesVisible() const { return d->document->guidesConfig().showGuides(); } bool Document::guidesLocked() const { return d->document->guidesConfig().lockGuides(); } Document *Document::clone() const { if (!d->document) return 0; QPointer clone = d->document->clone(); Document * d = new Document(clone); clone->setParent(d); // It's owned by the document, not KisPart return d; } void Document::setHorizontalGuides(const QList &lines) { if (!d->document) return; KisGuidesConfig config = d->document->guidesConfig(); KisCoordinatesConverter converter; converter.setImage(d->document->image()); QTransform transform = converter.imageToDocumentTransform(); QList transformedLines; for (int i = 0; i< lines.size(); i++) { qreal line = lines[i]; transformedLines.append(transform.map(QPointF(line, line)).x()); } config.setHorizontalGuideLines(transformedLines); d->document->setGuidesConfig(config); } void Document::setVerticalGuides(const QList &lines) { if (!d->document) return; KisGuidesConfig config = d->document->guidesConfig(); KisCoordinatesConverter converter; converter.setImage(d->document->image()); QTransform transform = converter.imageToDocumentTransform(); QList transformedLines; for (int i = 0; i< lines.size(); i++) { qreal line = lines[i]; transformedLines.append(transform.map(QPointF(line, line)).y()); } config.setVerticalGuideLines(transformedLines); d->document->setGuidesConfig(config); } void Document::setGuidesVisible(bool visible) { if (!d->document) return; KisGuidesConfig config = d->document->guidesConfig(); config.setShowGuides(visible); d->document->setGuidesConfig(config); } void Document::setGuidesLocked(bool locked) { if (!d->document) return; KisGuidesConfig config = d->document->guidesConfig(); config.setLockGuides(locked); d->document->setGuidesConfig(config); } bool Document::modified() const { if (!d->document) return false; return d->document->isModified(); } QRect Document::bounds() const { if (!d->document) return QRect(); return d->document->image()->bounds(); } QPointer Document::document() const { return d->document; } /* Animation related function */ bool Document::importAnimation(const QList &files, int firstFrame, int step) { KisView *activeView = KisPart::instance()->currentMainwindow()->activeView(); KoUpdaterPtr updater = 0; if (activeView && d->document->fileBatchMode()) { updater = activeView->viewManager()->createUnthreadedUpdater(i18n("Import frames")); } KisAnimationImporter importer(d->document->image(), updater); KisImportExportErrorCode status = importer.import(files, firstFrame, step); return status.isOk(); } int Document::framesPerSecond() { if (!d->document) return false; if (!d->document->image()) return false; return d->document->image()->animationInterface()->framerate(); } void Document::setFramesPerSecond(int fps) { if (!d->document) return; if (!d->document->image()) return; d->document->image()->animationInterface()->setFramerate(fps); } void Document::setFullClipRangeStartTime(int startTime) { if (!d->document) return; if (!d->document->image()) return; d->document->image()->animationInterface()->setFullClipRangeStartTime(startTime); } int Document::fullClipRangeStartTime() { if (!d->document) return false; if (!d->document->image()) return false; return d->document->image()->animationInterface()->fullClipRange().start(); } void Document::setFullClipRangeEndTime(int endTime) { if (!d->document) return; if (!d->document->image()) return; d->document->image()->animationInterface()->setFullClipRangeEndTime(endTime); } int Document::fullClipRangeEndTime() { if (!d->document) return false; if (!d->document->image()) return false; return d->document->image()->animationInterface()->fullClipRange().end(); } int Document::animationLength() { if (!d->document) return false; if (!d->document->image()) return false; return d->document->image()->animationInterface()->totalLength(); } void Document::setPlayBackRange(int start, int stop) { if (!d->document) return; if (!d->document->image()) return; - const KisTimeRange newTimeRange = KisTimeRange(start, (stop-start)); + const KisTimeSpan newTimeRange = KisTimeSpan(start, stop); d->document->image()->animationInterface()->setPlaybackRange(newTimeRange); } int Document::playBackStartTime() { if (!d->document) return false; if (!d->document->image()) return false; return d->document->image()->animationInterface()->playbackRange().start(); } int Document::playBackEndTime() { if (!d->document) return false; if (!d->document->image()) return false; return d->document->image()->animationInterface()->playbackRange().end(); } int Document::currentTime() { if (!d->document) return false; if (!d->document->image()) return false; return d->document->image()->animationInterface()->currentTime(); } void Document::setCurrentTime(int time) { if (!d->document) return; if (!d->document->image()) return; return d->document->image()->animationInterface()->requestTimeSwitchWithUndo(time); } diff --git a/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp b/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp index b4b6c8f510..7214adddad 100644 --- a/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp +++ b/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp @@ -1,127 +1,127 @@ /* * Copyright (c) 2017 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 "KisAsyncAnimationFramesSavingRenderer.h" #include "kis_image.h" #include "kis_paint_device.h" #include "KisImportExportFilter.h" #include "KisPart.h" #include "KisDocument.h" #include "kis_time_range.h" #include "kis_paint_layer.h" struct KisAsyncAnimationFramesSavingRenderer::Private { - Private(KisImageSP image, const KisTimeRange &_range, int _sequenceNumberingOffset, KisPropertiesConfigurationSP _exportConfiguration) + Private(KisImageSP image, const KisTimeSpan &_range, int _sequenceNumberingOffset, KisPropertiesConfigurationSP _exportConfiguration) : savingDoc(KisPart::instance()->createDocument()), range(_range), sequenceNumberingOffset(_sequenceNumberingOffset), exportConfiguration(_exportConfiguration) { savingDoc->setInfiniteAutoSaveInterval(); savingDoc->setFileBatchMode(true); KisImageSP savingImage = new KisImage(savingDoc->createUndoStore(), image->bounds().width(), image->bounds().height(), image->colorSpace(), QString()); savingImage->setResolution(image->xRes(), image->yRes()); savingDoc->setCurrentImage(savingImage); KisPaintLayer* paintLayer = new KisPaintLayer(savingImage, "paint device", 255); savingImage->addNode(paintLayer, savingImage->root(), KisLayerSP(0)); savingDevice = paintLayer->paintDevice(); } QScopedPointer savingDoc; KisPaintDeviceSP savingDevice; - KisTimeRange range; + KisTimeSpan range; int sequenceNumberingOffset = 0; QString filenamePrefix; QString filenameSuffix; QByteArray outputMimeType; KisPropertiesConfigurationSP exportConfiguration; }; KisAsyncAnimationFramesSavingRenderer::KisAsyncAnimationFramesSavingRenderer(KisImageSP image, const QString &fileNamePrefix, const QString &fileNameSuffix, const QByteArray &outputMimeType, - const KisTimeRange &range, + const KisTimeSpan &range, const int sequenceNumberingOffset, KisPropertiesConfigurationSP exportConfiguration) : m_d(new Private(image, range, sequenceNumberingOffset, exportConfiguration)) { m_d->filenamePrefix = fileNamePrefix; m_d->filenameSuffix = fileNameSuffix; m_d->outputMimeType = outputMimeType; connect(this, SIGNAL(sigCompleteRegenerationInternal(int)), SLOT(notifyFrameCompleted(int))); connect(this, SIGNAL(sigCancelRegenerationInternal(int)), SLOT(notifyFrameCancelled(int))); } KisAsyncAnimationFramesSavingRenderer::~KisAsyncAnimationFramesSavingRenderer() { } void KisAsyncAnimationFramesSavingRenderer::frameCompletedCallback(int frame, const QRegion &requestedRegion) { KisImageSP image = requestedImage(); if (!image) return; KIS_SAFE_ASSERT_RECOVER (requestedRegion == image->bounds()) { emit sigCancelRegenerationInternal(frame); return; } m_d->savingDevice->makeCloneFromRough(image->projection(), image->bounds()); KisImportExportErrorCode status = ImportExportCodes::OK; QString frameNumber = QString("%1").arg(frame + m_d->sequenceNumberingOffset, 4, 10, QChar('0')); QString filename = m_d->filenamePrefix + frameNumber + m_d->filenameSuffix; if (!m_d->savingDoc->exportDocumentSync(QUrl::fromLocalFile(filename), m_d->outputMimeType, m_d->exportConfiguration)) { status = ImportExportCodes::InternalError; } if (status.isOk()) { emit sigCompleteRegenerationInternal(frame); } else { emit sigCancelRegenerationInternal(frame); } } void KisAsyncAnimationFramesSavingRenderer::frameCancelledCallback(int frame) { notifyFrameCancelled(frame); } diff --git a/libs/ui/KisAsyncAnimationFramesSavingRenderer.h b/libs/ui/KisAsyncAnimationFramesSavingRenderer.h index efa5fcaf5e..7688ca9afa 100644 --- a/libs/ui/KisAsyncAnimationFramesSavingRenderer.h +++ b/libs/ui/KisAsyncAnimationFramesSavingRenderer.h @@ -1,53 +1,53 @@ /* * Copyright (c) 2017 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 KISASYNCANIMATIONFRAMESSAVINGRENDERER_H #define KISASYNCANIMATIONFRAMESSAVINGRENDERER_H #include class KisDocument; -class KisTimeRange; +class KisTimeSpan; class KisAsyncAnimationFramesSavingRenderer : public KisAsyncAnimationRendererBase { Q_OBJECT public: KisAsyncAnimationFramesSavingRenderer(KisImageSP image, const QString &fileNamePrefix, const QString &fileNameSuffix, const QByteArray &outputMimeType, - const KisTimeRange &range, + const KisTimeSpan &range, int sequenceNumberingOffset, KisPropertiesConfigurationSP exportConfiguration); ~KisAsyncAnimationFramesSavingRenderer(); protected: void frameCompletedCallback(int frame, const QRegion &requestedRegion) override; void frameCancelledCallback(int frame) override; Q_SIGNALS: void sigCompleteRegenerationInternal(int frame); void sigCancelRegenerationInternal(int frame); private: struct Private; const QScopedPointer m_d; }; #endif // KISASYNCANIMATIONFRAMESSAVINGRENDERER_H diff --git a/libs/ui/actions/KisPasteActionFactories.cpp b/libs/ui/actions/KisPasteActionFactories.cpp index 71b3725ff3..f7b915d6ed 100644 --- a/libs/ui/actions/KisPasteActionFactories.cpp +++ b/libs/ui/actions/KisPasteActionFactories.cpp @@ -1,308 +1,308 @@ /* * Copyright (c) 2017 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 "KisPasteActionFactories.h" #include "kis_config.h" #include "kis_image.h" #include "KisViewManager.h" #include "kis_tool_proxy.h" #include "kis_canvas2.h" #include "kis_canvas_controller.h" #include "kis_group_layer.h" #include "kis_paint_device.h" #include "kis_paint_layer.h" #include "kis_shape_layer.h" #include "kis_import_catcher.h" #include "kis_clipboard.h" #include "kis_selection.h" #include "commands/kis_selection_commands.h" #include "commands/kis_image_layer_add_command.h" #include "KisTransformToolActivationCommand.h" #include "kis_processing_applicator.h" #include #include #include #include #include #include #include "kis_algebra_2d.h" #include #include #include "kis_time_range.h" #include "kis_keyframe_channel.h" #include "kis_raster_keyframe_channel.h" #include "kis_painter.h" #include #include #include namespace { QPointF getFittingOffset(QList shapes, const QPointF &shapesOffset, const QRectF &documentRect, const qreal fitRatio) { QPointF accumulatedFitOffset; Q_FOREACH (KoShape *shape, shapes) { const QRectF bounds = shape->boundingRect(); const QPointF center = bounds.center() + shapesOffset; const qreal wMargin = (0.5 - fitRatio) * bounds.width(); const qreal hMargin = (0.5 - fitRatio) * bounds.height(); const QRectF allowedRect = documentRect.adjusted(-wMargin, -hMargin, wMargin, hMargin); const QPointF fittedCenter = KisAlgebra2D::clampPoint(center, allowedRect); accumulatedFitOffset += fittedCenter - center; } return accumulatedFitOffset; } bool tryPasteShapes(bool pasteAtCursorPosition, KisViewManager *view) { bool result = false; KoSvgPaste paste; if (paste.hasShapes()) { KoCanvasBase *canvas = view->canvasBase(); QSizeF fragmentSize; QList shapes = paste.fetchShapes(canvas->shapeController()->documentRectInPixels(), canvas->shapeController()->pixelsPerInch(), &fragmentSize); if (!shapes.isEmpty()) { KoShapeManager *shapeManager = canvas->shapeManager(); shapeManager->selection()->deselectAll(); // adjust z-index of the shapes so that they would be // pasted on the top of the stack QList topLevelShapes = shapeManager->topLevelShapes(); auto it = std::max_element(topLevelShapes.constBegin(), topLevelShapes.constEnd(), KoShape::compareShapeZIndex); if (it != topLevelShapes.constEnd()) { const int zIndexOffset = (*it)->zIndex(); std::stable_sort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); QList indexedShapes; std::transform(shapes.constBegin(), shapes.constEnd(), std::back_inserter(indexedShapes), [zIndexOffset] (KoShape *shape) { KoShapeReorderCommand::IndexedShape indexedShape(shape); indexedShape.zIndex += zIndexOffset; return indexedShape; }); indexedShapes = KoShapeReorderCommand::homogenizeZIndexesLazy(indexedShapes); KoShapeReorderCommand cmd(indexedShapes); cmd.redo(); } KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18n("Paste shapes")); canvas->shapeController()->addShapesDirect(shapes, 0, parentCommand); QPointF finalShapesOffset; if (pasteAtCursorPosition) { QRectF boundingRect = KoShape::boundingRect(shapes); const QPointF cursorPos = canvas->canvasController()->currentCursorPosition(); finalShapesOffset = cursorPos - boundingRect.center(); } else { bool foundOverlapping = false; QRectF boundingRect = KoShape::boundingRect(shapes); const QPointF offsetStep = 0.1 * QPointF(boundingRect.width(), boundingRect.height()); QPointF offset; Q_FOREACH (KoShape *shape, shapes) { QRectF br1 = shape->boundingRect(); bool hasOverlappingShape = false; do { hasOverlappingShape = false; // we cannot use shapesAt() here, because the groups are not // handled in the shape manager's tree QList conflicts = shapeManager->shapes(); Q_FOREACH (KoShape *intersectedShape, conflicts) { if (intersectedShape == shape) continue; QRectF br2 = intersectedShape->boundingRect(); const qreal tolerance = 2.0; /* pt */ if (KisAlgebra2D::fuzzyCompareRects(br1, br2, tolerance)) { br1.translate(offsetStep.x(), offsetStep.y()); offset += offsetStep; hasOverlappingShape = true; foundOverlapping = true; break; } } } while (hasOverlappingShape); if (foundOverlapping) break; } if (foundOverlapping) { finalShapesOffset = offset; } } const QRectF documentRect = canvas->shapeController()->documentRect(); finalShapesOffset += getFittingOffset(shapes, finalShapesOffset, documentRect, 0.1); if (!finalShapesOffset.isNull()) { new KoShapeMoveCommand(shapes, finalShapesOffset, parentCommand); } canvas->addCommand(parentCommand); Q_FOREACH (KoShape *shape, shapes) { canvas->selectedShapesProxy()->selection()->select(shape); } result = true; } } return result; } } void KisPasteActionFactory::run(bool pasteAtCursorPosition, KisViewManager *view) { KisImageSP image = view->image(); if (!image) return; if (tryPasteShapes(pasteAtCursorPosition, view)) { return; } - KisTimeRange range; + int firstFrame, lastFrame; const QRect fittingBounds = pasteAtCursorPosition ? QRect() : image->bounds(); - KisPaintDeviceSP clip = KisClipboard::instance()->clip(fittingBounds, true, &range); + KisPaintDeviceSP clip = KisClipboard::instance()->clip(fittingBounds, true, &firstFrame, &lastFrame); if (clip) { if (pasteAtCursorPosition) { const QPointF docPos = view->canvasBase()->canvasController()->currentCursorPosition(); const QPointF imagePos = view->canvasBase()->coordinatesConverter()->documentToImage(docPos); const QPointF offset = (imagePos - QRectF(clip->exactBounds()).center()).toPoint(); clip->setX(clip->x() + offset.x()); clip->setY(clip->y() + offset.y()); } KisImportCatcher::adaptClipToImageColorSpace(clip, image); KisPaintLayerSP newLayer = new KisPaintLayer(image.data(), image->nextLayerName() + i18n("(pasted)"), OPACITY_OPAQUE_U8); KisNodeSP aboveNode = view->activeLayer(); KisNodeSP parentNode = aboveNode ? aboveNode->parent() : image->root(); - if (range.isValid()) { + if (firstFrame >= 0) { newLayer->enableAnimation(); KisKeyframeChannel *channel = newLayer->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); KisRasterKeyframeChannel *rasterChannel = dynamic_cast(channel); - rasterChannel->importFrame(range.start(), clip, 0); + rasterChannel->importFrame(firstFrame, clip, 0); - if (!range.isInfinite()) { - rasterChannel->addKeyframe(range.end() + 1, 0); + if (lastFrame >= 0) { + rasterChannel->addKeyframe(lastFrame + 1, 0); } } else { const QRect rc = clip->extent(); KisPainter::copyAreaOptimized(rc.topLeft(), clip, newLayer->paintDevice(), rc); } KUndo2Command *cmd = new KisImageLayerAddCommand(image, newLayer, parentNode, aboveNode); KisProcessingApplicator *ap = beginAction(view, cmd->text()); ap->applyCommand(cmd, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); if (KisConfig(true).activateTransformToolAfterPaste()) { KUndo2Command *deselectCmd = new KisDeselectActiveSelectionCommand(view->selection(), image); ap->applyCommand(deselectCmd, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); KUndo2Command *transformToolCmd = new KisTransformToolActivationCommand(view); ap->applyCommand(transformToolCmd, KisStrokeJobData::BARRIER, KisStrokeJobData::NORMAL); } endAction(ap, KisOperationConfiguration(id()).toXML()); } else { // XXX: "Add saving of XML data for Paste of shapes" view->canvasBase()->toolProxy()->paste(); } } void KisPasteNewActionFactory::run(KisViewManager *viewManager) { Q_UNUSED(viewManager); KisPaintDeviceSP clip = KisClipboard::instance()->clip(QRect(), true); if (!clip) return; QRect rect = clip->exactBounds(); if (rect.isEmpty()) return; KisDocument *doc = KisPart::instance()->createDocument(); doc->documentInfo()->setAboutInfo("title", i18n("Untitled")); KisImageSP image = new KisImage(doc->createUndoStore(), rect.width(), rect.height(), clip->colorSpace(), i18n("Pasted")); KisPaintLayerSP layer = new KisPaintLayer(image.data(), image->nextLayerName() + " " + i18n("(pasted)"), OPACITY_OPAQUE_U8, clip->colorSpace()); KisPainter::copyAreaOptimized(QPoint(), clip, layer->paintDevice(), rect); image->addNode(layer.data(), image->rootLayer()); doc->setCurrentImage(image); KisPart::instance()->addDocument(doc); KisMainWindow *win = viewManager->mainWindow(); win->addViewAndNotifyLoadingCompleted(doc); } void KisPasteReferenceActionFactory::run(KisViewManager *viewManager) { KisCanvas2 *canvasBase = viewManager->canvasBase(); if (!canvasBase) return; KisReferenceImage* reference = KisReferenceImage::fromClipboard(*canvasBase->coordinatesConverter()); if (!reference) return; KisDocument *doc = viewManager->document(); doc->addCommand(KisReferenceImagesLayer::addReferenceImages(doc, {reference})); KoToolManager::instance()->switchToolRequested("ToolReferenceImages"); } diff --git a/libs/ui/actions/kis_selection_action_factories.cpp b/libs/ui/actions/kis_selection_action_factories.cpp index fdf1d64350..44e5c7de42 100644 --- a/libs/ui/actions/kis_selection_action_factories.cpp +++ b/libs/ui/actions/kis_selection_action_factories.cpp @@ -1,621 +1,620 @@ /* * Copyright (c) 2012 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_selection_action_factories.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KisViewManager.h" #include "kis_canvas_resource_provider.h" #include "kis_clipboard.h" #include "kis_pixel_selection.h" #include "kis_paint_layer.h" #include "kis_image.h" #include "kis_image_barrier_locker.h" #include "kis_fill_painter.h" #include "kis_transaction.h" #include "kis_iterator_ng.h" #include "kis_processing_applicator.h" #include "kis_group_layer.h" #include "commands/kis_selection_commands.h" #include "commands/kis_image_layer_add_command.h" #include "kis_tool_proxy.h" #include "kis_canvas2.h" #include "kis_canvas_controller.h" #include "kis_selection_manager.h" #include "commands_new/kis_transaction_based_command.h" #include "kis_selection_filters.h" #include "kis_shape_selection.h" #include "kis_shape_layer.h" #include #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "kis_keyframe_channel.h" #include #include #include "kis_figure_painting_tool_helper.h" #include "kis_update_outline_job.h" namespace ActionHelper { void copyFromDevice(KisViewManager *view, KisPaintDeviceSP device, bool makeSharpClip = false, - const KisTimeRange &range = KisTimeRange()) + int firstFrame = -1, int lastFrame = -1) { KisImageWSP image = view->image(); if (!image) return; KisSelectionSP selection = view->selection(); QRect rc = (selection) ? selection->selectedExactRect() : image->bounds(); KisPaintDeviceSP clip = new KisPaintDevice(device->colorSpace()); Q_CHECK_PTR(clip); const KoColorSpace *cs = clip->colorSpace(); // TODO if the source is linked... copy from all linked layers?!? // Copy image data KisPainter::copyAreaOptimized(QPoint(), device, clip, rc); if (selection) { // Apply selection mask. KisPaintDeviceSP selectionProjection = selection->projection(); KisHLineIteratorSP layerIt = clip->createHLineIteratorNG(0, 0, rc.width()); KisHLineConstIteratorSP selectionIt = selectionProjection->createHLineIteratorNG(rc.x(), rc.y(), rc.width()); const KoColorSpace *selCs = selection->projection()->colorSpace(); for (qint32 y = 0; y < rc.height(); y++) { for (qint32 x = 0; x < rc.width(); x++) { /** * Sharp method is an exact reverse of COMPOSITE_OVER * so if you cover the cut/copied piece over its source * you get an exactly the same image without any seams */ if (makeSharpClip) { qreal dstAlpha = cs->opacityF(layerIt->rawData()); qreal sel = selCs->opacityF(selectionIt->oldRawData()); qreal newAlpha = sel * dstAlpha / (1.0 - dstAlpha + sel * dstAlpha); float mask = newAlpha / dstAlpha; cs->applyAlphaNormedFloatMask(layerIt->rawData(), &mask, 1); } else { cs->applyAlphaU8Mask(layerIt->rawData(), selectionIt->oldRawData(), 1); } layerIt->nextPixel(); selectionIt->nextPixel(); } layerIt->nextRow(); selectionIt->nextRow(); } } - KisClipboard::instance()->setClip(clip, rc.topLeft(), range); + KisClipboard::instance()->setClip(clip, rc.topLeft(), firstFrame, lastFrame); } } void KisSelectAllActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Select All")); if (!image->globalSelection()) { ap->applyCommand(new KisSetEmptyGlobalSelectionCommand(image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } struct SelectAll : public KisTransactionBasedCommand { SelectAll(KisImageSP image) : m_image(image) {} KisImageSP m_image; KUndo2Command* paint() override { KisSelectionSP selection = m_image->globalSelection(); KisSelectionTransaction transaction(selection->pixelSelection()); selection->pixelSelection()->clear(); selection->pixelSelection()->select(m_image->bounds()); return transaction.endAndTake(); } }; ap->applyCommand(new SelectAll(image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisDeselectActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; KUndo2Command *cmd = new KisDeselectActiveSelectionCommand(view->selection(), image); KisProcessingApplicator *ap = beginAction(view, cmd->text()); ap->applyCommand(cmd, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisReselectActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; KUndo2Command *cmd = new KisReselectActiveSelectionCommand(view->activeNode(), image); KisProcessingApplicator *ap = beginAction(view, cmd->text()); ap->applyCommand(cmd, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisFillActionFactory::run(const QString &fillSource, KisViewManager *view) { KisNodeSP node = view->activeNode(); if (!node || !node->hasEditablePaintDevice()) return; KisSelectionSP selection = view->selection(); QRect selectedRect = selection ? selection->selectedRect() : view->image()->bounds(); Q_UNUSED(selectedRect); KisPaintDeviceSP filled = node->paintDevice()->createCompositionSourceDevice(); Q_UNUSED(filled); bool usePattern = false; bool useBgColor = false; if (fillSource.contains("pattern")) { usePattern = true; } else if (fillSource.contains("bg")) { useBgColor = true; } KisProcessingApplicator applicator(view->image(), node, KisProcessingApplicator::NONE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Flood Fill Layer")); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(view->image(), node, view->canvasResourceProvider()->resourceManager()); if (!fillSource.contains("opacity")) { resources->setOpacity(1.0); } KisProcessingVisitorSP visitor = new FillProcessingVisitor(QPoint(0, 0), // start position selection, resources, false, // fast mode usePattern, true, // fill only selection, 0, // feathering radius 0, // sizemod 80, // threshold, false, // unmerged useBgColor); applicator.applyVisitor(visitor, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.end(); view->canvasResourceProvider()->slotPainting(); } void KisClearActionFactory::run(KisViewManager *view) { // XXX: "Add saving of XML data for Clear action" view->canvasBase()->toolProxy()->deleteSelection(); } void KisImageResizeToSelectionActionFactory::run(KisViewManager *view) { // XXX: "Add saving of XML data for Image Resize To Selection action" KisSelectionSP selection = view->selection(); if (!selection) return; view->image()->cropImage(selection->selectedExactRect()); } void KisCutCopyActionFactory::run(bool willCut, bool makeSharpClip, KisViewManager *view) { KisImageSP image = view->image(); if (!image) return; bool haveShapesSelected = view->selectionManager()->haveShapesSelected(); if (haveShapesSelected) { // XXX: "Add saving of XML data for Cut/Copy of shapes" KisImageBarrierLocker locker(image); if (willCut) { view->canvasBase()->toolProxy()->cut(); } else { view->canvasBase()->toolProxy()->copy(); } } else { KisNodeSP node = view->activeNode(); if (!node) return; KisSelectionSP selection = view->selection(); if (selection.isNull()) return; { KisImageBarrierLocker locker(image); KisPaintDeviceSP dev = node->paintDevice(); if (!dev) { dev = node->projection(); } if (!dev) { view->showFloatingMessage( i18nc("floating message when cannot copy from a node", "Cannot copy pixels from this type of layer "), QIcon(), 3000, KisFloatingMessage::Medium); return; } if (dev->exactBounds().isEmpty()) { view->showFloatingMessage( i18nc("floating message when copying empty selection", "Selection is empty: no pixels were copied "), QIcon(), 3000, KisFloatingMessage::Medium); return; } - KisTimeRange range; - + int firstFrame = -1, lastFrame = -1; KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (channel) { const int currentTime = image->animationInterface()->currentTime(); - range = channel->affectedFrames(currentTime); + channel->activeKeyframeRange(currentTime, &firstFrame, &lastFrame); } - ActionHelper::copyFromDevice(view, dev, makeSharpClip, range); + ActionHelper::copyFromDevice(view, dev, makeSharpClip, firstFrame, lastFrame); } KUndo2Command *command = 0; if (willCut && node->hasEditablePaintDevice()) { struct ClearSelection : public KisTransactionBasedCommand { ClearSelection(KisNodeSP node, KisSelectionSP sel) : m_node(node), m_sel(sel) {} KisNodeSP m_node; KisSelectionSP m_sel; KUndo2Command* paint() override { KisSelectionSP cutSelection = m_sel; // Shrinking the cutting area was previously used // for getting seamless cut-paste. Now we use makeSharpClip // instead. // QRect originalRect = cutSelection->selectedExactRect(); // static const int preciseSelectionThreshold = 16; // // if (originalRect.width() > preciseSelectionThreshold || // originalRect.height() > preciseSelectionThreshold) { // cutSelection = new KisSelection(*m_sel); // delete cutSelection->flatten(); // // KisSelectionFilter* filter = new KisShrinkSelectionFilter(1, 1, false); // // QRect processingRect = filter->changeRect(originalRect); // filter->process(cutSelection->pixelSelection(), processingRect); // } KisTransaction transaction(m_node->paintDevice()); m_node->paintDevice()->clearSelection(cutSelection); m_node->setDirty(cutSelection->selectedRect()); return transaction.endAndTake(); } }; command = new ClearSelection(node, selection); } KUndo2MagicString actionName = willCut ? kundo2_i18n("Cut") : kundo2_i18n("Copy"); KisProcessingApplicator *ap = beginAction(view, actionName); if (command) { ap->applyCommand(command, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); } KisOperationConfiguration config(id()); config.setProperty("will-cut", willCut); endAction(ap, config.toXML()); } } void KisCopyMergedActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; if (!view->blockUntilOperationsFinished(image)) return; image->barrierLock(); KisPaintDeviceSP dev = image->root()->projection(); ActionHelper::copyFromDevice(view, dev); image->unlock(); KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Copy Merged")); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisInvertSelectionOperation::runFromXML(KisViewManager* view, const KisOperationConfiguration& config) { KisSelectionFilter* filter = new KisInvertSelectionFilter(); runFilter(filter, view, config); } void KisSelectionToVectorActionFactory::run(KisViewManager *view) { KisSelectionSP selection = view->selection(); if (selection->hasShapeSelection()) { view->showFloatingMessage(i18nc("floating message", "Selection is already in a vector format "), QIcon(), 2000, KisFloatingMessage::Low); return; } if (!selection->outlineCacheValid()) { view->image()->addSpontaneousJob(new KisUpdateOutlineJob(selection, false, Qt::transparent)); if (!view->blockUntilOperationsFinished(view->image())) { return; } } QPainterPath selectionOutline = selection->outlineCache(); QTransform transform = view->canvasBase()->coordinatesConverter()->imageToDocumentTransform(); KoShape *shape = KoPathShape::createShapeFromPainterPath(transform.map(selectionOutline)); shape->setShapeId(KoPathShapeId); /** * Mark a shape that it belongs to a shape selection */ if(!shape->userData()) { shape->setUserData(new KisShapeSelectionMarker); } KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Convert to Vector Selection")); ap->applyCommand(view->canvasBase()->shapeController()->addShape(shape, 0), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisSelectionToRasterActionFactory::run(KisViewManager *view) { KisSelectionSP selection = view->selection(); if (!selection->hasShapeSelection()) { view->showFloatingMessage(i18nc("floating message", "Selection is already in a raster format "), QIcon(), 2000, KisFloatingMessage::Low); return; } KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Convert to Vector Selection")); struct RasterizeSelection : public KisTransactionBasedCommand { RasterizeSelection(KisSelectionSP sel) : m_sel(sel) {} KisSelectionSP m_sel; KUndo2Command* paint() override { // just create an empty transaction: it will rasterize the // selection and emit the necessary signals KisTransaction transaction(m_sel->pixelSelection()); return transaction.endAndTake(); } }; ap->applyCommand(new RasterizeSelection(selection), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisShapesToVectorSelectionActionFactory::run(KisViewManager* view) { const QList originalShapes = view->canvasBase()->shapeManager()->selection()->selectedShapes(); bool hasSelectionShapes = false; QList clonedShapes; Q_FOREACH (KoShape *shape, originalShapes) { if (dynamic_cast(shape->userData())) { hasSelectionShapes = true; continue; } clonedShapes << shape->cloneShape(); } if (clonedShapes.isEmpty()) { if (hasSelectionShapes) { view->showFloatingMessage(i18nc("floating message", "The shape already belongs to a selection"), QIcon(), 2000, KisFloatingMessage::Low); } return; } KisSelectionToolHelper helper(view->canvasBase(), kundo2_i18n("Convert shapes to vector selection")); helper.addSelectionShapes(clonedShapes); } void KisSelectionToShapeActionFactory::run(KisViewManager *view) { KisSelectionSP selection = view->selection(); if (!selection->outlineCacheValid()) { return; } QPainterPath selectionOutline = selection->outlineCache(); QTransform transform = view->canvasBase()->coordinatesConverter()->imageToDocumentTransform(); KoShape *shape = KoPathShape::createShapeFromPainterPath(transform.map(selectionOutline)); shape->setShapeId(KoPathShapeId); KoColor fgColor = view->canvasBase()->resourceManager()->resource(KoCanvasResourceProvider::ForegroundColor).value(); KoShapeStrokeSP border(new KoShapeStroke(1.0, fgColor.toQColor())); shape->setStroke(border); view->document()->shapeController()->addShape(shape); } void KisStrokeSelectionActionFactory::run(KisViewManager *view, StrokeSelectionOptions params) { KisImageWSP image = view->image(); if (!image) { return; } KisSelectionSP selection = view->selection(); if (!selection) { return; } int size = params.lineSize; KisPixelSelectionSP pixelSelection = selection->projection(); if (!pixelSelection->outlineCacheValid()) { pixelSelection->recalculateOutlineCache(); } QPainterPath outline = pixelSelection->outlineCache(); QColor color = params.color.toQColor(); KisNodeSP currentNode = view->canvasResourceProvider()->resourceManager()->resource(KisCanvasResourceProvider::CurrentKritaNode).value(); if (!currentNode->inherits("KisShapeLayer") && currentNode->paintDevice()) { KoCanvasResourceProvider * rManager = view->canvasResourceProvider()->resourceManager(); KisToolShapeUtils::StrokeStyle strokeStyle = KisToolShapeUtils::StrokeStyleForeground; KisToolShapeUtils::FillStyle fillStyle = params.fillStyle(); KisFigurePaintingToolHelper helper(kundo2_i18n("Draw Polyline"), image, currentNode, rManager , strokeStyle, fillStyle); helper.setFGColorOverride(params.color); helper.setSelectionOverride(0); QPen pen(Qt::red, size); pen.setJoinStyle(Qt::RoundJoin); if (fillStyle != KisToolShapeUtils::FillStyleNone) { helper.paintPainterPathQPenFill(outline, pen, params.fillColor); } else { helper.paintPainterPathQPen(outline, pen, params.fillColor); } } else if (currentNode->inherits("KisShapeLayer")) { QTransform transform = view->canvasBase()->coordinatesConverter()->imageToDocumentTransform(); KoShape *shape = KoPathShape::createShapeFromPainterPath(transform.map(outline)); shape->setShapeId(KoPathShapeId); KoShapeStrokeSP border(new KoShapeStroke(size, color)); shape->setStroke(border); view->document()->shapeController()->addShape(shape); } image->setModified(); } void KisStrokeBrushSelectionActionFactory::run(KisViewManager *view, StrokeSelectionOptions params) { KisImageWSP image = view->image(); if (!image) { return; } KisSelectionSP selection = view->selection(); if (!selection) { return; } KisPixelSelectionSP pixelSelection = selection->projection(); if (!pixelSelection->outlineCacheValid()) { pixelSelection->recalculateOutlineCache(); } KisNodeSP currentNode = view->canvasResourceProvider()->resourceManager()->resource(KisCanvasResourceProvider::CurrentKritaNode).value(); if (!currentNode->inherits("KisShapeLayer") && currentNode->paintDevice()) { KoCanvasResourceProvider * rManager = view->canvasResourceProvider()->resourceManager(); QPainterPath outline = pixelSelection->outlineCache(); KisToolShapeUtils::StrokeStyle strokeStyle = KisToolShapeUtils::StrokeStyleForeground; KisToolShapeUtils::FillStyle fillStyle = KisToolShapeUtils::FillStyleNone; KoColor color = params.color; KisFigurePaintingToolHelper helper(kundo2_i18n("Draw Polyline"), image, currentNode, rManager, strokeStyle, fillStyle); helper.setFGColorOverride(color); helper.setSelectionOverride(0); helper.paintPainterPath(outline); image->setModified(); } } diff --git a/libs/ui/canvas/kis_animation_player.cpp b/libs/ui/canvas/kis_animation_player.cpp index e7f70ef19b..d2ae42e1b5 100644 --- a/libs/ui/canvas/kis_animation_player.cpp +++ b/libs/ui/canvas/kis_animation_player.cpp @@ -1,624 +1,624 @@ /* * 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_algebra_2d.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 "kis_image_config.h" #include #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), currentFrame(0), lastTimerInterval(0), lastPaintedFrame(0), playbackStatisticsCompressor(1000, KisSignalCompressor::FIRST_INACTIVE), stopAudioOnScrubbingCompressor(100, KisSignalCompressor::POSTPONE), audioOffsetTolerance(-1) {} KisAnimationPlayer *q; bool useFastFrameUpload; bool playing; QTimer *timer; /// The frame user started playback from int uiFrame; int firstFrame; int lastFrame; qreal playbackSpeed; KisCanvas2 *canvas; KisSignalAutoConnectionsStore cancelStrokeConnections; QElapsedTimer realFpsTimer; KisRollingMeanAccumulatorWrapper realFpsAccumulator; KisRollingMeanAccumulatorWrapper droppedFpsAccumulator; KisRollingMeanAccumulatorWrapper droppedFramesPortion; bool dropFramesMode; /// Measures time since playback (re)started QElapsedTimer playbackTime; int nextFrameExpectedTime; int expectedInterval; /// The frame the current playback (re)started on int initialFrame; /// The frame currently displayed int currentFrame; 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) { const int framesFromFirst = frame - firstFrame; const int rangeLength = lastFrame - firstFrame + 1; frame = firstFrame + framesFromFirst % rangeLength; } return frame; } qint64 framesToMSec(qreal value, int fps) const { return qRound(value / fps * 1000.0); } qreal msecToFrames(qint64 value, int fps) const { return qreal(value) * fps / 1000.0; } int framesToWalltime(qreal frame, int fps) const { return qRound(framesToMSec(frame, fps) / playbackSpeed); } qreal walltimeToFrames(qint64 time, int fps) const { return msecToFrames(time, fps) * playbackSpeed; } qreal playbackTimeInFrames(int fps) const { const qint64 cycleLength = lastFrame - firstFrame + 1; const qreal framesPlayed = walltimeToFrames(playbackTime.elapsed(), fps); const qreal framesSinceFirst = std::fmod(initialFrame + framesPlayed - firstFrame, cycleLength); return firstFrame + framesSinceFirst; } }; 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(true); 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(QString,QString)), SLOT(slotOnAudioError(QString,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(true); 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); } if (m_d->playing) { slotUpdatePlaybackTimer(); } } void KisAnimationPlayer::slotUpdatePlaybackTimer() { m_d->timer->stop(); const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface(); - const KisTimeRange &playBackRange = animation->playbackRange(); - if (!playBackRange.isValid()) return; + const KisTimeSpan &playBackRange = animation->playbackRange(); + if (playBackRange.isEmpty()) return; const int fps = animation->framerate(); m_d->initialFrame = isPlaying() ? m_d->currentFrame : animation->currentUITime(); m_d->firstFrame = playBackRange.start(); m_d->lastFrame = playBackRange.end(); m_d->currentFrame = qBound(m_d->firstFrame, m_d->currentFrame, m_d->lastFrame); m_d->expectedInterval = m_d->framesToWalltime(1, fps); m_d->lastTimerInterval = m_d->expectedInterval; if (m_d->syncedAudio) { m_d->syncedAudio->setSpeed(m_d->playbackSpeed); const qint64 expectedAudioTime = m_d->framesToMSec(m_d->currentFrame, fps); if (qAbs(m_d->syncedAudio->position() - expectedAudioTime) > m_d->framesToMSec(1.5, fps)) { m_d->syncedAudio->syncWithVideo(expectedAudioTime); } } 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; + const KisTimeSpan &range = animation->playbackRange(); + if (range.isEmpty()) return; // when openGL is disabled, there is no animation cache if (m_d->canvas->frameCache()) { KisImageConfig cfg(true); const int dimensionLimit = cfg.useAnimationCacheFrameSizeLimit() ? cfg.animationCacheFrameSizeLimit() : std::numeric_limits::max(); const int maxImageDimension = KisAlgebra2D::maxDimension(m_d->canvas->image()->bounds()); const QRect regionOfInterest = cfg.useAnimationCacheRegionOfInterest() && maxImageDimension > dimensionLimit ? m_d->canvas->regionOfInterest() : m_d->canvas->coordinatesConverter()->imageRectInImagePixels(); const QRect minimalNeedRect = m_d->canvas->coordinatesConverter()->widgetRectInImagePixels().toAlignedRect() & m_d->canvas->coordinatesConverter()->imageRectInImagePixels(); m_d->canvas->frameCache()->dropLowQualityFrames(range, regionOfInterest, minimalNeedRect); KisAsyncAnimationCacheRenderDialog dlg(m_d->canvas->frameCache(), range, 200); dlg.setRegionOfInterest(regionOfInterest); KisAsyncAnimationCacheRenderDialog::Result result = dlg.regenerateRange(m_d->canvas->viewManager()); if (result != KisAsyncAnimationCacheRenderDialog::RenderComplete) { return; } m_d->canvas->setRenderingLimit(regionOfInterest); } } m_d->playing = true; m_d->uiFrame = animation->currentUITime(); m_d->currentFrame = m_d->uiFrame; slotUpdatePlaybackTimer(); m_d->lastPaintedFrame = -1; connectCancelSignals(); if (m_d->syncedAudio) { KisImageAnimationInterface *animationInterface = m_d->canvas->image()->animationInterface(); m_d->syncedAudio->play(m_d->framesToMSec(m_d->currentFrame, animationInterface->framerate())); } emit sigPlaybackStarted(); } void KisAnimationPlayer::Private::stopImpl(bool doUpdates) { if (syncedAudio) { syncedAudio->stop(); } q->disconnectCancelSignals(); timer->stop(); playing = false; canvas->setRenderingLimit(QRect()); if (doUpdates) { KisImageAnimationInterface *animation = canvas->image()->animationInterface(); if (animation->currentUITime() == uiFrame) { canvas->refetchDataFromImage(); } else { animation->switchCurrentTimeAsync(uiFrame); } } 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, true); } void KisAnimationPlayer::slotUpdate() { uploadFrame(-1, false); } void KisAnimationPlayer::uploadFrame(int frame, bool forceSyncAudio) { KisImageAnimationInterface *animationInterface = m_d->canvas->image()->animationInterface(); const int fps = animationInterface->framerate(); const bool syncToAudio = !forceSyncAudio && m_d->dropFramesMode && m_d->syncedAudio && m_d->syncedAudio->isPlaying(); if (frame < 0) { if (m_d->dropFramesMode) { const qreal currentTimeInFrames = syncToAudio ? m_d->msecToFrames(m_d->syncedAudio->position(), fps) : m_d->playbackTimeInFrames(fps); frame = qFloor(currentTimeInFrames); const int timeToNextFrame = m_d->framesToWalltime(frame + 1 - currentTimeInFrames, fps); m_d->lastTimerInterval = qMax(0, timeToNextFrame); if (frame < m_d->currentFrame) { // Returned to beginning of animation. Restart audio playback. forceSyncAudio = true; } } else { const qint64 currentTime = m_d->playbackTime.elapsed(); const qint64 framesDiff = currentTime - m_d->nextFrameExpectedTime; frame = m_d->incFrame(m_d->currentFrame, 1); m_d->nextFrameExpectedTime = currentTime + m_d->expectedInterval; m_d->lastTimerInterval = qMax(0.0, m_d->lastTimerInterval - 0.5 * framesDiff); } m_d->currentFrame = frame; m_d->timer->start(m_d->lastTimerInterval); m_d->playbackStatisticsCompressor.start(); } if (m_d->syncedAudio && (!syncToAudio || forceSyncAudio)) { const int msecTime = m_d->framesToMSec(frame, fps); if (isPlaying()) { slotSyncScrubbingAudio(msecTime); } else { m_d->audioSyncScrubbingCompressor->start(msecTime); } } bool useFallbackUploadMethod = !m_d->canvas->frameCache(); if (m_d->canvas->frameCache() && m_d->canvas->frameCache()->shouldUploadNewFrame(frame, m_d->lastPaintedFrame)) { if (m_d->canvas->frameCache()->uploadFrame(frame)) { m_d->canvas->updateCanvas(); m_d->useFastFrameUpload = true; } else { useFallbackUploadMethod = true; } } if (useFallbackUploadMethod && m_d->canvas->image()->animationInterface()->hasAnimation()) { m_d->useFastFrameUpload = false; if (m_d->canvas->image()->tryBarrierLock(true)) { m_d->canvas->image()->unlock(); // no OpenGL cache or the frame just not cached yet animationInterface->switchCurrentTimeAsync(frame); } } if (!m_d->realFpsTimer.isValid()) { m_d->realFpsTimer.start(); } else { const int elapsed = m_d->realFpsTimer.restart(); m_d->realFpsAccumulator(elapsed); if (m_d->lastPaintedFrame >= 0) { 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; emit sigFrameChanged(); } 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/dialogs/KisAsyncAnimationCacheRenderDialog.cpp b/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.cpp index 38585af521..692b2bffca 100644 --- a/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.cpp +++ b/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.cpp @@ -1,147 +1,102 @@ /* * Copyright (c) 2017 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 "KisAsyncAnimationCacheRenderDialog.h" #include "KisAsyncAnimationCacheRenderer.h" #include "kis_animation_frame_cache.h" #include #include #include namespace { -QList calcDirtyFramesList(KisAnimationFrameCacheSP cache, const KisTimeRange &playbackRange) +QList calcDirtyFramesList(KisAnimationFrameCacheSP cache, const KisTimeSpan &playbackRange) { - QList result; + QList framesToRegenerate; KisImageSP image = cache->image(); - if (!image) return result; + if (!image) return framesToRegenerate; KisImageAnimationInterface *animation = image->animationInterface(); - if (!animation->hasAnimation()) return result; + if (!animation->hasAnimation()) return framesToRegenerate; - if (playbackRange.isValid()) { - KIS_ASSERT_RECOVER_RETURN_VALUE(!playbackRange.isInfinite(), result); + KisFrameSet validFrames = cache->cachedFramesWithin(playbackRange); - // TODO: optimize check for fully-cached case - for (int frame = playbackRange.start(); frame <= playbackRange.end(); frame++) { - const KisTimeRange stillFrameRange = - KisTimeRange::calculateIdenticalFramesRecursive(image->root(), frame); + int firstDirty = validFrames.firstExcludedSince(playbackRange.start()); + while (playbackRange.contains(firstDirty)) { + framesToRegenerate.append(firstDirty); - KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(stillFrameRange.isValid(), result); + KisFrameSet duplicates = calculateIdenticalFramesRecursive(image->root(), firstDirty, playbackRange); + validFrames |= duplicates; - if (cache->frameStatus(stillFrameRange.start()) == KisAnimationFrameCache::Uncached) { - result.append(stillFrameRange.start()); - } - - if (stillFrameRange.isInfinite()) { - break; - } else { - frame = stillFrameRange.end(); - } - } + firstDirty = validFrames.firstExcludedSince(firstDirty); } - return result; + return framesToRegenerate; } } -int KisAsyncAnimationCacheRenderDialog::calcFirstDirtyFrame(KisAnimationFrameCacheSP cache, const KisTimeRange &playbackRange, const KisTimeRange &skipRange) -{ - int result = -1; - - KisImageSP image = cache->image(); - if (!image) return result; - - KisImageAnimationInterface *animation = image->animationInterface(); - if (!animation->hasAnimation()) return result; - - if (playbackRange.isValid()) { - KIS_ASSERT_RECOVER_RETURN_VALUE(!playbackRange.isInfinite(), result); - - // TODO: optimize check for fully-cached case - for (int frame = playbackRange.start(); frame <= playbackRange.end(); frame++) { - if (skipRange.contains(frame)) { - if (skipRange.isInfinite()) { - break; - } else { - frame = skipRange.end(); - continue; - } - } - - if (cache->frameStatus(frame) != KisAnimationFrameCache::Cached) { - result = frame; - break; - } - } - } - - return result; -} - - struct KisAsyncAnimationCacheRenderDialog::Private { - Private(KisAnimationFrameCacheSP _cache, const KisTimeRange &_range) + Private(KisAnimationFrameCacheSP _cache, const KisTimeSpan &_range) : cache(_cache), range(_range) { } KisAnimationFrameCacheSP cache; - KisTimeRange range; + KisTimeSpan range; }; -KisAsyncAnimationCacheRenderDialog::KisAsyncAnimationCacheRenderDialog(KisAnimationFrameCacheSP cache, const KisTimeRange &range, int busyWait) +KisAsyncAnimationCacheRenderDialog::KisAsyncAnimationCacheRenderDialog(KisAnimationFrameCacheSP cache, const KisTimeSpan &range, int busyWait) : KisAsyncAnimationRenderDialogBase(i18n("Regenerating cache..."), cache->image(), busyWait), m_d(new Private(cache, range)) { } KisAsyncAnimationCacheRenderDialog::~KisAsyncAnimationCacheRenderDialog() { } QList KisAsyncAnimationCacheRenderDialog::calcDirtyFrames() const { return calcDirtyFramesList(m_d->cache, m_d->range); } KisAsyncAnimationRendererBase *KisAsyncAnimationCacheRenderDialog::createRenderer(KisImageSP image) { Q_UNUSED(image); return new KisAsyncAnimationCacheRenderer(); } void KisAsyncAnimationCacheRenderDialog::initializeRendererForFrame(KisAsyncAnimationRendererBase *renderer, KisImageSP image, int frame) { Q_UNUSED(image); Q_UNUSED(frame); KisAsyncAnimationCacheRenderer *cacheRenderer = dynamic_cast(renderer); KIS_SAFE_ASSERT_RECOVER_RETURN(cacheRenderer); cacheRenderer->setFrameCache(m_d->cache); } diff --git a/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.h b/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.h index 6c698b1c2e..1e7db436ef 100644 --- a/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.h +++ b/libs/ui/dialogs/KisAsyncAnimationCacheRenderDialog.h @@ -1,45 +1,46 @@ /* * Copyright (c) 2017 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 KISASYNCANIMATIONCACHERENDERDIALOG_H #define KISASYNCANIMATIONCACHERENDERDIALOG_H #include "KisAsyncAnimationRenderDialogBase.h" #include "kis_types.h" +class KisTimeSpan; class KisAsyncAnimationCacheRenderDialog : public KisAsyncAnimationRenderDialogBase { public: - KisAsyncAnimationCacheRenderDialog(KisAnimationFrameCacheSP cache, const KisTimeRange &range, int busyWait = 200); + KisAsyncAnimationCacheRenderDialog(KisAnimationFrameCacheSP cache, const KisTimeSpan &range, int busyWait = 200); ~KisAsyncAnimationCacheRenderDialog(); - static int calcFirstDirtyFrame(KisAnimationFrameCacheSP cache, const KisTimeRange &playbackRange, const KisTimeRange &skipRange); + static int calcFirstDirtyFrame(KisAnimationFrameCacheSP cache, const KisTimeSpan &playbackRange, const KisTimeSpan &skipRange); protected: QList calcDirtyFrames() const override; KisAsyncAnimationRendererBase* createRenderer(KisImageSP image) override; void initializeRendererForFrame(KisAsyncAnimationRendererBase *renderer, KisImageSP image, int frame) override; private: struct Private; const QScopedPointer m_d; }; #endif // KISASYNCANIMATIONCACHERENDERDIALOG_H diff --git a/libs/ui/dialogs/KisAsyncAnimationFramesSaveDialog.cpp b/libs/ui/dialogs/KisAsyncAnimationFramesSaveDialog.cpp index 9fe9df4760..3641cf91d0 100644 --- a/libs/ui/dialogs/KisAsyncAnimationFramesSaveDialog.cpp +++ b/libs/ui/dialogs/KisAsyncAnimationFramesSaveDialog.cpp @@ -1,193 +1,193 @@ /* * Copyright (c) 2017 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 "KisAsyncAnimationFramesSaveDialog.h" #include #include #include #include "kis_properties_configuration.h" #include "KisMimeDatabase.h" #include #include #include struct KisAsyncAnimationFramesSaveDialog::Private { Private(KisImageSP _image, - const KisTimeRange &_range, + const KisTimeSpan &_range, const QString &baseFilename, int _sequenceNumberingOffset, KisPropertiesConfigurationSP _exportConfiguration) : originalImage(_image), range(_range), sequenceNumberingOffset(_sequenceNumberingOffset), exportConfiguration(_exportConfiguration) { int baseLength = baseFilename.lastIndexOf("."); if (baseLength > -1) { filenamePrefix = baseFilename.left(baseLength); filenameSuffix = baseFilename.right(baseFilename.length() - baseLength); } else { filenamePrefix = baseFilename; } outputMimeType = KisMimeDatabase::mimeTypeForFile(baseFilename, false).toLatin1(); } KisImageSP originalImage; - KisTimeRange range; + KisTimeSpan range; QString filenamePrefix; QString filenameSuffix; QByteArray outputMimeType; int sequenceNumberingOffset; KisPropertiesConfigurationSP exportConfiguration; }; KisAsyncAnimationFramesSaveDialog::KisAsyncAnimationFramesSaveDialog(KisImageSP originalImage, - const KisTimeRange &range, + const KisTimeSpan &range, const QString &baseFilename, int sequenceNumberingOffset, KisPropertiesConfigurationSP exportConfiguration) : KisAsyncAnimationRenderDialogBase(i18n("Saving frames..."), originalImage, 0), m_d(new Private(originalImage, range, baseFilename, sequenceNumberingOffset, exportConfiguration)) { } KisAsyncAnimationFramesSaveDialog::~KisAsyncAnimationFramesSaveDialog() { } KisAsyncAnimationRenderDialogBase::Result KisAsyncAnimationFramesSaveDialog::regenerateRange(KisViewManager *viewManager) { QFileInfo info(savedFilesMaskWildcard()); QDir dir(info.absolutePath()); if (!dir.exists()) { dir.mkpath(info.absolutePath()); } KIS_SAFE_ASSERT_RECOVER_NOOP(dir.exists()); QStringList filesList = dir.entryList({ info.fileName() }); if (!filesList.isEmpty()) { if (batchMode()) { return RenderFailed; } QStringList filesWithinRange; const int numberOfDigits = 4; Q_FOREACH(const QString &filename, filesList) { // Counting based on suffix, since prefix may include the path while filename doesn't int digitsPosition = filename.length() - m_d->filenameSuffix.length() - numberOfDigits; int fileNumber = filename.midRef(digitsPosition, numberOfDigits).toInt(); auto frameNumber = fileNumber - m_d->sequenceNumberingOffset; if (m_d->range.contains(frameNumber)) { filesWithinRange.append(filename); } } filesList = filesWithinRange; QStringList truncatedList = filesList; while (truncatedList.size() > 3) { truncatedList.takeLast(); } QString exampleFiles = truncatedList.join(", "); if (truncatedList.size() != filesList.size()) { exampleFiles += QString(", ..."); } QMessageBox::StandardButton result = QMessageBox::warning(0, i18n("Delete old frames?"), i18n("Frames with the same naming " "scheme exist in the destination " "directory. They are going to be " "deleted, continue?\n\n" "Directory: %1\n" "Files: %2", info.absolutePath(), exampleFiles), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (result == QMessageBox::Yes) { Q_FOREACH (const QString &file, filesList) { if (!dir.remove(file)) { QMessageBox::critical(0, i18n("Failed to delete"), i18n("Failed to delete an old frame file:\n\n" "%1\n\n" "Rendering cancelled.", dir.absoluteFilePath(file))); return RenderFailed; } } } else { return RenderCancelled; } } return KisAsyncAnimationRenderDialogBase::regenerateRange(viewManager); } QList KisAsyncAnimationFramesSaveDialog::calcDirtyFrames() const { // TODO: optimize! QList result; for (int i = m_d->range.start(); i <= m_d->range.end(); i++) { result.append(i); } return result; } KisAsyncAnimationRendererBase *KisAsyncAnimationFramesSaveDialog::createRenderer(KisImageSP image) { return new KisAsyncAnimationFramesSavingRenderer(image, m_d->filenamePrefix, m_d->filenameSuffix, m_d->outputMimeType, m_d->range, m_d->sequenceNumberingOffset, m_d->exportConfiguration); } void KisAsyncAnimationFramesSaveDialog::initializeRendererForFrame(KisAsyncAnimationRendererBase *renderer, KisImageSP image, int frame) { Q_UNUSED(renderer); Q_UNUSED(image); Q_UNUSED(frame); } QString KisAsyncAnimationFramesSaveDialog::savedFilesMask() const { return m_d->filenamePrefix + "%04d" + m_d->filenameSuffix; } QString KisAsyncAnimationFramesSaveDialog::savedFilesMaskWildcard() const { return m_d->filenamePrefix + "????" + m_d->filenameSuffix; } diff --git a/libs/ui/dialogs/KisAsyncAnimationFramesSaveDialog.h b/libs/ui/dialogs/KisAsyncAnimationFramesSaveDialog.h index cd5ba65832..972c493482 100644 --- a/libs/ui/dialogs/KisAsyncAnimationFramesSaveDialog.h +++ b/libs/ui/dialogs/KisAsyncAnimationFramesSaveDialog.h @@ -1,53 +1,54 @@ /* * Copyright (c) 2017 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 KISASYNCANIMATIONFRAMESSAVEDIALOG_H #define KISASYNCANIMATIONFRAMESSAVEDIALOG_H #include "KisAsyncAnimationRenderDialogBase.h" #include "kis_types.h" +class KisTimeSpan; class KRITAUI_EXPORT KisAsyncAnimationFramesSaveDialog : public KisAsyncAnimationRenderDialogBase { public: KisAsyncAnimationFramesSaveDialog(KisImageSP image, - const KisTimeRange &range, + const KisTimeSpan &range, const QString &baseFilename, int sequenceNumberingOffset, KisPropertiesConfigurationSP exportConfiguration); ~KisAsyncAnimationFramesSaveDialog(); Result regenerateRange(KisViewManager *viewManager) override; QString savedFilesMask() const; QString savedFilesMaskWildcard() const; protected: QList calcDirtyFrames() const override; KisAsyncAnimationRendererBase* createRenderer(KisImageSP image) override; void initializeRendererForFrame(KisAsyncAnimationRendererBase *renderer, KisImageSP image, int frame) override; private: struct Private; const QScopedPointer m_d; }; #endif // KISASYNCANIMATIONFRAMESSAVEDIALOG_H diff --git a/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.h b/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.h index c23b9e576a..423f4e371a 100644 --- a/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.h +++ b/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.h @@ -1,152 +1,151 @@ /* * Copyright (c) 2017 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 KISASYNCANIMATIONRENDERDIALOGBASE_H #define KISASYNCANIMATIONRENDERDIALOGBASE_H #include #include "kis_types.h" #include "kritaui_export.h" -class KisTimeRange; class KisAsyncAnimationRendererBase; class KisViewManager; /** * @brief KisAsyncAnimationRenderDialogBase is a special class for rendering multiple * frames of the image and putting them somewhere (saving into a file or just * pushing into an openGL cache) * * The class handles a lot of boilerplate code for you and optimizes regeneration of * the frames using multithreading, according to the user's settings. The responsibilities * of the class are the following: * * Rendering itself: * - fetch the list of dirtly frames using calcDirtyFrames() * - create some clones of the image according to the user's settings * to facilitate multithreaded rendering and processing of the frames * - if the user doesn't have enough RAM, the clones will not be created * (the memory overhead is calculated using "projections" metric of the * statistics server). * - feed the images/threads with dirty frames until the all the frames * are done * * Progress reporting: * - if batchMode() is false, the user will see a progress dialog showing * the current progress with estimate about total processing timer * - the user can also cancel the regeneration by pressing Cancel button * * Usage Details: * - one should implement two methods to make the rendering work: * - calcDirtyFrames() * - createRenderer(KisImageSP image) * - these methods will be called on the start of the rendering */ class KRITAUI_EXPORT KisAsyncAnimationRenderDialogBase : public QObject { Q_OBJECT public: enum Result { RenderComplete, RenderCancelled, RenderFailed }; public: /** * @brief construct and initialize the dialog * @param actionTitle the first line of the status reports that the user sees in the dialog * @param image the image that will be as a source of frames. Make sure the image is *not* * locked by the time regenerateRange() is called * @param busyWait the dialog will not show for the specified time (in milliseconds) */ KisAsyncAnimationRenderDialogBase(const QString &actionTitle, KisImageSP image, int busyWait = 200); virtual ~KisAsyncAnimationRenderDialogBase(); /** * @brief start generation of frames and (if not in batch mode) show the dialog * * The link to view manager is used to barrier lock with visual feedback in the * end of the operation */ virtual Result regenerateRange(KisViewManager *viewManager); /** * Set area of image that will be regenerated. If \p roi is empty, * full area of the image is regenerated. */ void setRegionOfInterest(const QRegion &roi); /** * @see setRegionOfInterest() */ QRegion regionOfInterest() const; /** * @brief setting batch mode to true will prevent any dialogs or message boxes from * showing on screen. Please take it into account that using batch mode prevents * some potentially dangerous recovery execution paths (e.g. delete the existing * frames in the destination folder). In such case the rendering will be stopped with * RenderFailed result. */ void setBatchMode(bool value); /** * @see setBatchMode */ bool batchMode() const; private Q_SLOTS: void slotFrameCompleted(int frame); void slotFrameCancelled(int frame); void slotCancelRegeneration(); void slotUpdateCompressedProgressData(); private: void tryInitiateFrameRegeneration(); void updateProgressLabel(); void cancelProcessingImpl(bool isUserCancelled); protected: /** * @brief returns a list of frames that should be regenerated by the dialog * * Called by the dialog in the very beginning of regenerateRange() */ virtual QList calcDirtyFrames() const = 0; /** * @brief create a renderer object linked to \p image * * Renderer are special objects that connect to the individual image signals, * react on them and fetch the final frames data * * @see KisAsyncAnimationRendererBase */ virtual KisAsyncAnimationRendererBase* createRenderer(KisImageSP image) = 0; virtual void initializeRendererForFrame(KisAsyncAnimationRendererBase *renderer, KisImageSP image, int frame) = 0; private: struct Private; const QScopedPointer m_d; }; #endif // KISASYNCANIMATIONRENDERDIALOGBASE_H diff --git a/libs/ui/kis_animation_cache_populator.cpp b/libs/ui/kis_animation_cache_populator.cpp index c730a52d09..ea6a3fab24 100644 --- a/libs/ui/kis_animation_cache_populator.cpp +++ b/libs/ui/kis_animation_cache_populator.cpp @@ -1,317 +1,317 @@ /* * 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_cache_populator.h" #include #include #include #include #include "kis_config.h" #include "kis_config_notifier.h" #include "KisPart.h" #include "KisDocument.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_canvas2.h" #include "kis_time_range.h" #include "kis_animation_frame_cache.h" #include "kis_update_info.h" #include "kis_signal_auto_connection.h" #include "kis_idle_watcher.h" #include "KisViewManager.h" #include "kis_node_manager.h" #include "kis_keyframe_channel.h" #include "KisAsyncAnimationCacheRenderer.h" #include "dialogs/KisAsyncAnimationCacheRenderDialog.h" struct KisAnimationCachePopulator::Private { KisAnimationCachePopulator *q; KisPart *part; QTimer timer; /** * Counts up the number of subsequent times Krita has been detected idle. */ int idleCounter; static const int IDLE_COUNT_THRESHOLD = 4; static const int IDLE_CHECK_INTERVAL = 500; static const int BETWEEN_FRAMES_INTERVAL = 10; int requestedFrame; KisAnimationFrameCacheSP requestCache; KisOpenGLUpdateInfoSP requestInfo; KisSignalAutoConnectionsStore imageRequestConnections; QFutureWatcher infoConversionWatcher; KisAsyncAnimationCacheRenderer regenerator; bool calculateAnimationCacheInBackground = true; enum State { NotWaitingForAnything, WaitingForIdle, WaitingForFrame, BetweenFrames }; State state; Private(KisAnimationCachePopulator *_q, KisPart *_part) : q(_q), part(_part), idleCounter(0), requestedFrame(-1), state(WaitingForIdle) { timer.setSingleShot(true); } void timerTimeout() { switch (state) { case WaitingForIdle: case BetweenFrames: generateIfIdle(); break; case WaitingForFrame: KIS_ASSERT_RECOVER_NOOP(0 && "WaitingForFrame cannot have a timeout. Just skip this message and report a bug"); break; case NotWaitingForAnything: KIS_ASSERT_RECOVER_NOOP(0 && "NotWaitingForAnything cannot have a timeout. Just skip this message and report a bug"); break; } } void generateIfIdle() { if (part->idleWatcher()->isIdle()) { idleCounter++; if (idleCounter >= IDLE_COUNT_THRESHOLD) { if (!tryRequestGeneration()) { enterState(NotWaitingForAnything); } return; } } else { idleCounter = 0; } enterState(WaitingForIdle); } bool tryRequestGeneration() { // Prioritize the active document KisAnimationFrameCacheSP activeDocumentCache = KisAnimationFrameCacheSP(0); KisMainWindow *activeWindow = part->currentMainwindow(); if (activeWindow && activeWindow->activeView()) { KisCanvas2 *activeCanvas = activeWindow->activeView()->canvasBase(); if (activeCanvas && activeCanvas->frameCache()) { activeDocumentCache = activeCanvas->frameCache(); // Let's skip frames affected by changes to the active node (on the active document) // This avoids constant invalidation and regeneration while drawing KisNodeSP activeNode = activeCanvas->viewManager()->nodeManager()->activeNode(); - KisTimeRange skipRange; + KisFrameSet skipRange; if (activeNode) { int currentTime = activeCanvas->currentImage()->animationInterface()->currentUITime(); if (!activeNode->keyframeChannels().isEmpty()) { Q_FOREACH (const KisKeyframeChannel *channel, activeNode->keyframeChannels()) { skipRange |= channel->affectedFrames(currentTime); } } else { - skipRange = KisTimeRange::infinite(0); + skipRange = KisFrameSet::infiniteFrom(0); } } bool requested = tryRequestGeneration(activeDocumentCache, skipRange); if (requested) return true; } } QList caches = KisAnimationFrameCache::caches(); KisAnimationFrameCache *cache; Q_FOREACH (cache, caches) { if (cache == activeDocumentCache.data()) { // We already handled this one... continue; } - bool requested = tryRequestGeneration(cache, KisTimeRange()); + bool requested = tryRequestGeneration(cache, KisFrameSet()); if (requested) return true; } return false; } - bool tryRequestGeneration(KisAnimationFrameCacheSP cache, KisTimeRange skipRange) + bool tryRequestGeneration(KisAnimationFrameCacheSP cache, KisFrameSet skipRange) { KisImageSP image = cache->image(); if (!image) return false; KisImageAnimationInterface *animation = image->animationInterface(); - KisTimeRange currentRange = animation->fullClipRange(); + KisTimeSpan currentRange = animation->fullClipRange(); - const int frame = KisAsyncAnimationCacheRenderDialog::calcFirstDirtyFrame(cache, currentRange, skipRange); + const int frame = cache->firstDirtyFrameWithin(currentRange, &skipRange); if (frame >= 0) { return regenerate(cache, frame); } return false; } bool regenerate(KisAnimationFrameCacheSP cache, int frame) { if (state == WaitingForFrame) { // Already busy, deny request return false; } /** * We should enter the state before the frame is * requested. Otherwise the signal may come earlier than we * enter it. */ enterState(WaitingForFrame); regenerator.setFrameCache(cache); // if we ever decide to add ROI to background cache // regeneration, it should be added here :) regenerator.startFrameRegeneration(cache->image(), frame); return true; } QString debugStateToString(State newState) { QString str = ""; switch (newState) { case WaitingForIdle: str = "WaitingForIdle"; break; case WaitingForFrame: str = "WaitingForFrame"; break; case NotWaitingForAnything: str = "NotWaitingForAnything"; break; case BetweenFrames: str = "BetweenFrames"; break; } return str; } void enterState(State newState) { //ENTER_FUNCTION() << debugStateToString(state) << "->" << debugStateToString(newState); state = newState; int timerTimeout = -1; switch (state) { case WaitingForIdle: timerTimeout = IDLE_CHECK_INTERVAL; break; case WaitingForFrame: // the timeout is handled by the regenerator now timerTimeout = -1; break; case NotWaitingForAnything: // frame conversion cannot be cancelled, // so there is no timeout timerTimeout = -1; break; case BetweenFrames: timerTimeout = BETWEEN_FRAMES_INTERVAL; break; } if (timerTimeout >= 0) { timer.start(timerTimeout); } else { timer.stop(); } } }; KisAnimationCachePopulator::KisAnimationCachePopulator(KisPart *part) : m_d(new Private(this, part)) { connect(&m_d->timer, SIGNAL(timeout()), this, SLOT(slotTimer())); connect(&m_d->regenerator, SIGNAL(sigFrameCancelled(int)), SLOT(slotRegeneratorFrameCancelled())); connect(&m_d->regenerator, SIGNAL(sigFrameCompleted(int)), SLOT(slotRegeneratorFrameReady())); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); slotConfigChanged(); } KisAnimationCachePopulator::~KisAnimationCachePopulator() {} bool KisAnimationCachePopulator::regenerate(KisAnimationFrameCacheSP cache, int frame) { return m_d->regenerate(cache, frame); } void KisAnimationCachePopulator::slotTimer() { m_d->timerTimeout(); } void KisAnimationCachePopulator::slotRequestRegeneration() { // skip if the user forbade background regeneration if (!m_d->calculateAnimationCacheInBackground) return; m_d->enterState(Private::WaitingForIdle); } void KisAnimationCachePopulator::slotRegeneratorFrameCancelled() { KIS_ASSERT_RECOVER_RETURN(m_d->state == Private::WaitingForFrame); m_d->enterState(Private::NotWaitingForAnything); } void KisAnimationCachePopulator::slotRegeneratorFrameReady() { m_d->enterState(Private::BetweenFrames); } void KisAnimationCachePopulator::slotConfigChanged() { KisConfig cfg(true); m_d->calculateAnimationCacheInBackground = cfg.calculateAnimationCacheInBackground(); QTimer::singleShot(1000, this, SLOT(slotRequestRegeneration())); } diff --git a/libs/ui/kis_animation_frame_cache.cpp b/libs/ui/kis_animation_frame_cache.cpp index d41d21b1e2..1c04e7d05b 100644 --- a/libs/ui/kis_animation_frame_cache.cpp +++ b/libs/ui/kis_animation_frame_cache.cpp @@ -1,414 +1,476 @@ /* * 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_frame_cache.h" #include +#include #include "kis_debug.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "KisPart.h" #include "kis_animation_cache_populator.h" #include #include "KisFrameCacheSwapper.h" #include "KisInMemoryFrameCacheSwapper.h" #include "kis_image_config.h" #include "kis_config_notifier.h" #include "opengl/kis_opengl_image_textures.h" #include #include struct KisAnimationFrameCache::Private { Private(KisOpenGLImageTexturesSP _textures) : textures(_textures) { image = textures->image(); } ~Private() { } KisOpenGLImageTexturesSP textures; KisImageWSP image; QScopedPointer swapper; int frameSizeLimit = 777; KisOpenGLUpdateInfoSP fetchFrameDataImpl(KisImageSP image, const QRect &requestedRect, int lod); - struct Frame + struct CacheEntry { - KisOpenGLUpdateInfoSP openGlFrame; + int frameId; int length; - Frame(KisOpenGLUpdateInfoSP info, int length) - : openGlFrame(info), length(length) + CacheEntry(int id, int length) + : frameId(id), length(length) {} + + bool isInfinite() const { + return length < 0; + } }; - QMap newFrames; + int nextFreeId = 0; + /// Map of cache entries by beginning time of the entry + QMap cachedFrames; + /// Maps a frame ID to a list of times, where entries with said frame ID begin + QMap> entriesById; - int getFrameIdAtTime(int time) const - { - if (newFrames.isEmpty()) return -1; + void addFrame(int start, int length, int id) { + cachedFrames.insert(start, CacheEntry(id, length)); + entriesById[id].append(start); + } - auto it = newFrames.upperBound(time); + QMap::iterator iteratorFrom(int time) { + if (cachedFrames.isEmpty()) return cachedFrames.end(); - if (it != newFrames.constBegin()) it--; + auto it = cachedFrames.upperBound(time); + if (it != cachedFrames.begin()) { + auto previous = it - 1; + if (previous.value().isInfinite() || time <= previous.key() + previous.value().length) { + return previous; + } + + } + return it; + } + + QMap::const_iterator constIteratorFrom(int time) const { + if (cachedFrames.isEmpty()) return cachedFrames.constEnd(); + + auto it = cachedFrames.upperBound(time); + if (it != cachedFrames.constBegin()) { + auto previous = it - 1; + if (previous.value().isInfinite() || time <= previous.key() + previous.value().length) { + return previous; + } + + } + return it; + } + + int getFrameIdAtTime(int time) const + { + auto it = constIteratorFrom(time); + if (it == cachedFrames.constEnd()) return -1; - KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(it != newFrames.constEnd(), 0); const int start = it.key(); - const int length = it.value(); + const CacheEntry &frame = it.value(); bool foundFrameValid = false; - - if (length == -1) { + if (frame.isInfinite()) { if (start <= time) { foundFrameValid = true; } } else { - int end = start + length - 1; + int end = start + frame.length - 1; if (start <= time && time <= end) { foundFrameValid = true; } } - return foundFrameValid ? start : -1; + return foundFrameValid ? frame.frameId : -1; } bool hasFrame(int time) const { return getFrameIdAtTime(time) >= 0; } KisOpenGLUpdateInfoSP getFrame(int time) { const int frameId = getFrameIdAtTime(time); return frameId >= 0 ? swapper->loadFrame(frameId) : 0; } - void addFrame(KisOpenGLUpdateInfoSP info, const KisTimeRange& range) + void addFrame(KisOpenGLUpdateInfoSP info, const KisFrameSet& targetFrames) { - invalidate(range); + invalidate(targetFrames); - const int length = range.isInfinite() ? -1 : range.end() - range.start() + 1; - newFrames.insert(range.start(), length); - swapper->saveFrame(range.start(), info, image->bounds()); + const int id = nextFreeId++; + swapper->saveFrame(id, info, image->bounds()); + + for (const KisTimeSpan span : targetFrames.finiteSpans()) { + addFrame(span.start(), span.duration(), id); + } + + if (targetFrames.isInfinite()) { + addFrame(targetFrames.firstFrameOfInfinity(), -1, id); + } } /** * Invalidate any cached frames within the given time range. * @param range * @return true if frames were invalidated, false if nothing was changed */ - bool invalidate(const KisTimeRange& range) + bool invalidate(const KisFrameSet& range) { - if (newFrames.isEmpty()) return false; + if (cachedFrames.isEmpty()) return false; bool cacheChanged = false; - auto it = newFrames.lowerBound(range.start()); - if (it.key() != range.start() && it != newFrames.begin()) it--; + for (const KisTimeSpan span : range.finiteSpans()) { + cacheChanged |= invalidate(span.start(), span.end()); + } + if (range.isInfinite()) { + cacheChanged |= invalidate(range.firstFrameOfInfinity(), -1); + } - while (it != newFrames.end()) { - const int start = it.key(); - const int length = it.value(); - const bool frameIsInfinite = (length == -1); - const int end = start + length - 1; + return cacheChanged; + } - if (start >= range.start()) { - if (!range.isInfinite() && start > range.end()) { - break; - } + bool invalidate(int invalidateFrom, int invalidateTo) { + const bool infinite = invalidateTo < 0; - if (!range.isInfinite() && (frameIsInfinite || end > range.end())) { - // Reinsert with a later start - int newStart = range.end() + 1; - int newLength = frameIsInfinite ? -1 : (end - newStart + 1); + bool cacheChanged = false; - newFrames.insert(newStart, newLength); - swapper->moveFrame(start, newStart); - } else { - swapper->forgetFrame(start); + auto it = iteratorFrom(invalidateFrom); + while (it != cachedFrames.end()) { + const int start = it.key(); + const CacheEntry frame = it.value(); + const bool frameIsInfinite = (frame.length == -1); + const int end = start + frame.length - 1; + + if (start >= invalidateFrom) { + if (!infinite) { + if (start > invalidateTo) { + break; + } + + if (frameIsInfinite || end > invalidateTo) { + // Shorten the entry from the beginning + int newStart = invalidateTo + 1; + int newLength = frameIsInfinite ? -1 : (end - newStart + 1); + + cachedFrames.insert(newStart, CacheEntry(frame.frameId, newLength)); + addFrame(newStart, newLength, frame.frameId); + } } - it = newFrames.erase(it); + QVector &instances = entriesById[frame.frameId]; + instances.removeAll(it.key()); + if (instances.isEmpty()) swapper->forgetFrame(frame.frameId); + it = cachedFrames.erase(it); cacheChanged = true; continue; - } else if (frameIsInfinite || end >= range.start()) { - const int newEnd = range.start() - 1; - *it = newEnd - start + 1; + } else if (frameIsInfinite || end >= invalidateFrom) { + const int newEnd = invalidateFrom - 1; + const int newLength = newEnd - start + 1; + *it = CacheEntry(frame.frameId, newLength); cacheChanged = true; } it++; } return cacheChanged; } int effectiveLevelOfDetail(const QRect &rc) const { if (!frameSizeLimit) return 0; const int maxDimension = KisAlgebra2D::maxDimension(rc); const qreal minLod = -std::log2(qreal(frameSizeLimit) / maxDimension); const int lodLimit = qMax(0, qCeil(minLod)); return lodLimit; } // TODO: verify that we don't have any leak here! typedef QMap CachesMap; static CachesMap caches; }; KisAnimationFrameCache::Private::CachesMap KisAnimationFrameCache::Private::caches; KisAnimationFrameCacheSP KisAnimationFrameCache::getFrameCache(KisOpenGLImageTexturesSP textures) { KisAnimationFrameCache *cache; Private::CachesMap::iterator it = Private::caches.find(textures); if (it == Private::caches.end()) { cache = new KisAnimationFrameCache(textures); Private::caches.insert(textures, cache); } else { cache = it.value(); } return cache; } const QList KisAnimationFrameCache::caches() { return Private::caches.values(); } KisAnimationFrameCache::KisAnimationFrameCache(KisOpenGLImageTexturesSP textures) : m_d(new Private(textures)) { // create swapping backend slotConfigChanged(); - connect(m_d->image->animationInterface(), SIGNAL(sigFramesChanged(KisTimeRange,QRect)), this, SLOT(framesChanged(KisTimeRange,QRect))); + connect(m_d->image->animationInterface(), SIGNAL(sigFramesChanged(KisFrameSet, QRect)), this, SLOT(framesChanged(KisFrameSet, QRect))); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); } KisAnimationFrameCache::~KisAnimationFrameCache() { Private::caches.remove(m_d->textures); } bool KisAnimationFrameCache::uploadFrame(int time) { KisOpenGLUpdateInfoSP info = m_d->getFrame(time); if (!info) { // Do nothing! // // Previously we were trying to start cache regeneration in this point, // but it caused even bigger slowdowns when scrubbing } else { m_d->textures->recalculateCache(info, false); } return bool(info); } bool KisAnimationFrameCache::shouldUploadNewFrame(int newTime, int oldTime) const { if (oldTime < 0) return true; - const int oldKeyframeStart = m_d->getFrameIdAtTime(oldTime); - if (oldKeyframeStart < 0) return true; - - const int oldKeyFrameLength = m_d->newFrames[oldKeyframeStart]; - return !(newTime >= oldKeyframeStart && (newTime < oldKeyframeStart + oldKeyFrameLength || oldKeyFrameLength == -1)); + const int oldFrameId = m_d->getFrameIdAtTime(oldTime); + const int newFrameId = m_d->getFrameIdAtTime(newTime); + return (oldFrameId < 0) || oldFrameId != newFrameId; } KisAnimationFrameCache::CacheStatus KisAnimationFrameCache::frameStatus(int time) const { return m_d->hasFrame(time) ? Cached : Uncached; } KisImageWSP KisAnimationFrameCache::image() { return m_d->image; } -void KisAnimationFrameCache::framesChanged(const KisTimeRange &range, const QRect &rect) +void KisAnimationFrameCache::framesChanged(const KisFrameSet &range, const QRect &rect) { Q_UNUSED(rect); - if (!range.isValid()) return; + if (range.isEmpty()) return; bool cacheChanged = m_d->invalidate(range); if (cacheChanged) { emit changed(); } } void KisAnimationFrameCache::slotConfigChanged() { - m_d->newFrames.clear(); + m_d->cachedFrames.clear(); KisImageConfig cfg(true); if (cfg.useOnDiskAnimationCacheSwapping()) { m_d->swapper.reset(new KisFrameCacheSwapper(m_d->textures->updateInfoBuilder(), cfg.swapDir())); } else { m_d->swapper.reset(new KisInMemoryFrameCacheSwapper()); } m_d->frameSizeLimit = cfg.useAnimationCacheFrameSizeLimit() ? cfg.animationCacheFrameSizeLimit() : 0; emit changed(); } KisOpenGLUpdateInfoSP KisAnimationFrameCache::Private::fetchFrameDataImpl(KisImageSP image, const QRect &requestedRect, int lod) { if (lod > 0) { KisPaintDeviceSP tempDevice = new KisPaintDevice(image->projection()->colorSpace()); tempDevice->prepareClone(image->projection()); image->projection()->generateLodCloneDevice(tempDevice, image->projection()->extent(), lod); const QRect fetchRect = KisLodTransform::alignedRect(requestedRect, lod); return textures->updateInfoBuilder().buildUpdateInfo(fetchRect, tempDevice, image->bounds(), lod, true); } else { return textures->updateCache(requestedRect, image); } } KisOpenGLUpdateInfoSP KisAnimationFrameCache::fetchFrameData(int time, KisImageSP image, const QRegion &requestedRegion) const { if (time != image->animationInterface()->currentTime()) { qWarning() << "WARNING: KisAnimationFrameCache::frameReady image's time doesn't coincide with the requested time!"; qWarning() << " " << ppVar(image->animationInterface()->currentTime()) << ppVar(time); } // the frames are always generated at full scale KIS_SAFE_ASSERT_RECOVER_NOOP(image->currentLevelOfDetail() == 0); const int lod = m_d->effectiveLevelOfDetail(requestedRegion.boundingRect()); KisOpenGLUpdateInfoSP totalInfo; Q_FOREACH (const QRect &rc, requestedRegion.rects()) { KisOpenGLUpdateInfoSP info = m_d->fetchFrameDataImpl(image, rc, lod); if (!totalInfo) { totalInfo = info; } else { const bool result = totalInfo->tryMergeWith(*info); KIS_SAFE_ASSERT_RECOVER_NOOP(result); } } return totalInfo; } void KisAnimationFrameCache::addConvertedFrameData(KisOpenGLUpdateInfoSP info, int time) { - const KisTimeRange identicalRange = - KisTimeRange::calculateIdenticalFramesRecursive(m_d->image->root(), time); + KisTimeSpan range = m_d->image->animationInterface()->fullClipRange(); + const KisFrameSet identicalFrames = calculateIdenticalFramesRecursive(m_d->image->root(), time, range); - m_d->addFrame(info, identicalRange); + m_d->addFrame(info, identicalFrames); emit changed(); } -void KisAnimationFrameCache::dropLowQualityFrames(const KisTimeRange &range, const QRect ®ionOfInterest, const QRect &minimalRect) +void KisAnimationFrameCache::dropLowQualityFrames(const KisTimeSpan &range, const QRect ®ionOfInterest, const QRect &minimalRect) { - KIS_SAFE_ASSERT_RECOVER_RETURN(!range.isInfinite()); - if (m_d->newFrames.isEmpty()) return; + if (m_d->cachedFrames.isEmpty()) return; - auto it = m_d->newFrames.upperBound(range.start()); + QVector framesToDrop; - // the vector is guaranteed to be non-empty, - // so decrementing iterator is safe - if (it != m_d->newFrames.begin()) it--; + for (auto it = m_d->constIteratorFrom(range.start()); it != m_d->cachedFrames.constEnd() && it.key() <= range.end(); it++) { + const Private::CacheEntry &frame = it.value(); - while (it != m_d->newFrames.end() && it.key() <= range.end()) { - const int frameId = it.key(); - const int frameLength = it.value(); + const QRect frameRect = m_d->swapper->frameDirtyRect(frame.frameId); + const int frameLod = m_d->swapper->frameLevelOfDetail(frame.frameId); - if (frameId + frameLength - 1 < range.start()) { - ++it; - continue; + if (frameLod > m_d->effectiveLevelOfDetail(regionOfInterest) || !frameRect.contains(minimalRect)) { + if (!framesToDrop.contains(frame.frameId)) framesToDrop.append(frame.frameId); } + } - const QRect frameRect = m_d->swapper->frameDirtyRect(frameId); - const int frameLod = m_d->swapper->frameLevelOfDetail(frameId); - - if (frameLod > m_d->effectiveLevelOfDetail(regionOfInterest) || !frameRect.contains(minimalRect)) { - m_d->swapper->forgetFrame(frameId); - it = m_d->newFrames.erase(it); - } else { - ++it; + Q_FOREACH(int frameId, framesToDrop) { + Q_FOREACH(int time, m_d->entriesById[frameId]) { + m_d->cachedFrames.remove(time); } + + m_d->entriesById.remove(frameId); + m_d->swapper->forgetFrame(frameId); } } -bool KisAnimationFrameCache::framesHaveValidRoi(const KisTimeRange &range, const QRect ®ionOfInterest) +KisFrameSet KisAnimationFrameCache::cachedFramesWithin(const KisTimeSpan range) { - KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!range.isInfinite(), false); - if (m_d->newFrames.isEmpty()) return false; + auto it = m_d->constIteratorFrom(range.start()); + if (it == m_d->cachedFrames.constEnd()) return KisFrameSet(); - auto it = m_d->newFrames.upperBound(range.start()); + QVector cachedSpans; + int firstOfInfinite = -1; - if (it != m_d->newFrames.begin()) it--; + for (; it != m_d->cachedFrames.constEnd() && it.key() <= range.end(); it++) { + const int start = it.key(); + Private::CacheEntry entry = it.value(); - int expectedNextFrameStart = it.key(); + if (entry.isInfinite()) { + firstOfInfinite = start; + } else { + cachedSpans.append(KisTimeSpan(start, start + entry.length - 1)); + } - while (it.key() <= range.end()) { - const int frameId = it.key(); - const int frameLength = it.value(); + } - if (frameId + frameLength - 1 < range.start()) { - expectedNextFrameStart = frameId + frameLength; - ++it; - continue; - } + return KisFrameSet(cachedSpans, firstOfInfinite); +} - if (expectedNextFrameStart != frameId) { - KIS_SAFE_ASSERT_RECOVER_NOOP(expectedNextFrameStart < frameId); - return false; - } +int KisAnimationFrameCache::firstDirtyFrameWithin(const KisTimeSpan range, const KisFrameSet *ignoredFrames) +{ + int candidate = range.start(); - if (!m_d->swapper->frameDirtyRect(frameId).contains(regionOfInterest)) { - return false; + for (auto it = m_d->constIteratorFrom(range.start()); it != m_d->cachedFrames.constEnd(); it++) { + const int start = it.key(); + const int end = start + it.value().length - 1; + + if (ignoredFrames) { + candidate = ignoredFrames->firstExcludedSince(candidate); } - expectedNextFrameStart = frameId + frameLength; - ++it; + if (candidate < start) { + return candidate; + } else if (candidate <= end) { + candidate = end + 1; + } } - return true; + return -1; } diff --git a/libs/ui/kis_animation_frame_cache.h b/libs/ui/kis_animation_frame_cache.h index d5321fceb1..23cf4fd4b1 100644 --- a/libs/ui/kis_animation_frame_cache.h +++ b/libs/ui/kis_animation_frame_cache.h @@ -1,89 +1,90 @@ /* * 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_FRAME_CACHE_H #define KIS_ANIMATION_FRAME_CACHE_H #include #include #include "kritaui_export.h" #include "kis_types.h" #include "kis_shared.h" class KisImage; class KisImageAnimationInterface; -class KisTimeRange; +class KisTimeSpan; +class KisFrameSet; class KisOpenGLImageTextures; typedef KisSharedPtr KisOpenGLImageTexturesSP; class KisOpenGLUpdateInfo; typedef KisSharedPtr KisOpenGLUpdateInfoSP; class KRITAUI_EXPORT KisAnimationFrameCache : public QObject, public KisShared { Q_OBJECT public: static KisAnimationFrameCacheSP getFrameCache(KisOpenGLImageTexturesSP textures); static const QList caches(); KisAnimationFrameCache(KisOpenGLImageTexturesSP textures); ~KisAnimationFrameCache() override; QImage getFrame(int time); bool uploadFrame(int time); bool shouldUploadNewFrame(int newTime, int oldTime) const; enum CacheStatus { Cached, Uncached, }; CacheStatus frameStatus(int time) const; + KisFrameSet cachedFramesWithin(KisTimeSpan range); + int firstDirtyFrameWithin(KisTimeSpan range, const KisFrameSet *ignoredFrames = 0); KisImageWSP image(); KisOpenGLUpdateInfoSP fetchFrameData(int time, KisImageSP image, const QRegion &requestedRegion) const; void addConvertedFrameData(KisOpenGLUpdateInfoSP info, int time); /** * Drops all the frames with worse level of detail values than the current * desired level of detail. */ - void dropLowQualityFrames(const KisTimeRange &range, const QRect ®ionOfInterest, const QRect &minimalRect); - - bool framesHaveValidRoi(const KisTimeRange &range, const QRect ®ionOfInterest); + void dropLowQualityFrames(const KisTimeSpan &range, const QRect ®ionOfInterest, const QRect &minimalRect); Q_SIGNALS: void changed(); private: struct Private; QScopedPointer m_d; private Q_SLOTS: - void framesChanged(const KisTimeRange &range, const QRect &rect); + void framesChanged(const KisFrameSet &range, const QRect &rect); void slotConfigChanged(); }; #endif diff --git a/libs/ui/kis_clipboard.cc b/libs/ui/kis_clipboard.cc index 0936f51a2e..829ea755cf 100644 --- a/libs/ui/kis_clipboard.cc +++ b/libs/ui/kis_clipboard.cc @@ -1,464 +1,460 @@ /* * Copyright (c) 2004 Boudewijn Rempt * * 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_clipboard.h" #include #include #include #include #include #include #include #include #include #include #include #include "KoColorSpace.h" #include "KoStore.h" #include #include #include // kritaimage #include #include #include #include #include #include #include #include // local #include "kis_config.h" #include "kis_store_paintdevice_writer.h" #include "kis_mimedata.h" Q_GLOBAL_STATIC(KisClipboard, s_instance) KisClipboard::KisClipboard() { m_pushedClipboard = false; m_hasClip = false; // Check that we don't already have a clip ready clipboardDataChanged(); // Make sure we are notified when clipboard changes connect(QApplication::clipboard(), SIGNAL(dataChanged()), this, SLOT(clipboardDataChanged())); } KisClipboard::~KisClipboard() { dbgRegistry << "deleting KisClipBoard"; } KisClipboard* KisClipboard::instance() { return s_instance; } -void KisClipboard::setClip(KisPaintDeviceSP dev, const QPoint& topLeft, const KisTimeRange &range) +void KisClipboard::setClip(KisPaintDeviceSP dev, const QPoint& topLeft, int firstFrame, int lastFrame) { if (!dev) return; m_hasClip = true; // We'll create a store (ZIP format) in memory QBuffer buffer; QByteArray mimeType("application/x-krita-selection"); KoStore* store = KoStore::createStore(&buffer, KoStore::Write, mimeType); KisStorePaintDeviceWriter writer(store); Q_ASSERT(store); Q_ASSERT(!store->bad()); // Layer data if (store->open("layerdata")) { if (!dev->write(writer)) { dev->disconnect(); store->close(); delete store; return; } store->close(); } // copied frame time limits - if (range.isValid() && store->open("timeRange")) { - store->write(QString("%1 %2").arg(range.start()).arg(range.end()).toLatin1()); + if (firstFrame >= 0 && store->open("timeRange")) { + store->write(QString("%1 %2").arg(firstFrame).arg(lastFrame).toLatin1()); store->close(); } // Coordinates if (store->open("topLeft")) { store->write(QString("%1 %2").arg(topLeft.x()).arg(topLeft.y()).toLatin1()); store->close(); } // ColorSpace id of layer data if (store->open("colormodel")) { QString csName = dev->colorSpace()->colorModelId().id(); store->write(csName.toLatin1()); store->close(); } if (store->open("colordepth")) { QString csName = dev->colorSpace()->colorDepthId().id(); store->write(csName.toLatin1()); store->close(); } if (dev->colorSpace()->profile()) { const KoColorProfile *profile = dev->colorSpace()->profile(); KisAnnotationSP annotation; if (profile && profile->type() == "icc" && !profile->rawData().isEmpty()) { annotation = new KisAnnotation("icc", profile->name(), profile->rawData()); if (annotation) { // save layer profile if (store->open("profile.icc")) { store->write(annotation->annotation()); store->close(); } } } } delete store; QMimeData *mimeData = new QMimeData; Q_CHECK_PTR(mimeData); if (mimeData) { mimeData->setData(mimeType, buffer.buffer()); } // We also create a QImage so we can interchange with other applications QImage qimage; KisConfig cfg(true); const KoColorProfile *monitorProfile = cfg.displayProfile(QApplication::desktop()->screenNumber(qApp->activeWindow())); qimage = dev->convertToQImage(monitorProfile, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); if (!qimage.isNull() && mimeData) { mimeData->setImageData(qimage); } if (mimeData) { m_pushedClipboard = true; QClipboard *cb = QApplication::clipboard(); cb->setMimeData(mimeData); } } -void KisClipboard::setClip(KisPaintDeviceSP dev, const QPoint& topLeft) -{ - setClip(dev, topLeft, KisTimeRange()); -} - -KisPaintDeviceSP KisClipboard::clip(const QRect &imageBounds, bool showPopup, KisTimeRange *clipRange) +KisPaintDeviceSP KisClipboard::clip(const QRect &imageBounds, bool showPopup, int *firstFrame, int *lastFrame) { QByteArray mimeType("application/x-krita-selection"); - if (clipRange) { - *clipRange = KisTimeRange(); + if (firstFrame && lastFrame) { + *firstFrame = -1; + *lastFrame = -1; } QClipboard *cb = QApplication::clipboard(); const QMimeData *cbData = cb->mimeData(); KisPaintDeviceSP clip; if (cbData && cbData->hasFormat(mimeType)) { QByteArray encodedData = cbData->data(mimeType); QBuffer buffer(&encodedData); KoStore* store = KoStore::createStore(&buffer, KoStore::Read, mimeType); const KoColorProfile *profile = 0; QString csDepth, csModel; // ColorSpace id of layer data if (store->hasFile("colormodel")) { store->open("colormodel"); csModel = QString(store->read(store->size())); store->close(); } if (store->hasFile("colordepth")) { store->open("colordepth"); csDepth = QString(store->read(store->size())); store->close(); } if (store->hasFile("profile.icc")) { QByteArray data; store->open("profile.icc"); data = store->read(store->size()); store->close(); profile = KoColorSpaceRegistry::instance()->createColorProfile(csModel, csDepth, data); } const KoColorSpace *cs = KoColorSpaceRegistry::instance()->colorSpace(csModel, csDepth, profile); if (cs) { clip = new KisPaintDevice(cs); if (store->hasFile("layerdata")) { store->open("layerdata"); if (!clip->read(store->device())) { clip = 0; } store->close(); } if (clip && !imageBounds.isEmpty()) { // load topLeft if (store->hasFile("topLeft")) { store->open("topLeft"); QString str = store->read(store->size()); store->close(); QStringList list = str.split(' '); if (list.size() == 2) { QPoint topLeft(list[0].toInt(), list[1].toInt()); clip->setX(topLeft.x()); clip->setY(topLeft.y()); } } QRect clipBounds = clip->exactBounds(); if (!imageBounds.contains(clipBounds) && !imageBounds.intersects(clipBounds)) { QPoint diff = imageBounds.center() - clipBounds.center(); clip->setX(clip->x() + diff.x()); clip->setY(clip->y() + diff.y()); } - if (store->hasFile("timeRange") && clipRange) { + if (store->hasFile("timeRange") && firstFrame && lastFrame) { store->open("timeRange"); QString str = store->read(store->size()); store->close(); QStringList list = str.split(' '); if (list.size() == 2) { - KisTimeRange range(list[0].toInt(), list[1].toInt(), true); - *clipRange = range; - qDebug() << "Pasted time range" << range; + *firstFrame = list[0].toInt(); + *lastFrame = list[1].toInt(); + qDebug() << "Pasted time range:" << firstFrame << "to" << lastFrame; } } } } delete store; } if (!clip) { QImage qimage = KisClipboardUtil::getImageFromClipboard(); if (qimage.isNull()) { return KisPaintDeviceSP(0); } KisConfig cfg(true); quint32 behaviour = cfg.pasteBehaviour(); bool saveColorSetting = false; if (behaviour == PASTE_ASK && showPopup) { // Ask user each time. QMessageBox mb(qApp->activeWindow()); QCheckBox dontPrompt(i18n("Remember"), &mb); dontPrompt.blockSignals(true); mb.setWindowTitle(i18nc("@title:window", "Missing Color Profile")); mb.setText(i18n("The image data you are trying to paste has no color profile information. How do you want to interpret these data? \n\n As Web (sRGB) - Use standard colors that are displayed from computer monitors. This is the most common way that images are stored. \n\nAs on Monitor - If you know a bit about color management and want to use your monitor to determine the color profile.\n\n")); // the order of how you add these buttons matters as it determines the index. mb.addButton(i18n("As &Web"), QMessageBox::AcceptRole); mb.addButton(i18n("As on &Monitor"), QMessageBox::AcceptRole); mb.addButton(i18n("Cancel"), QMessageBox::RejectRole); mb.addButton(&dontPrompt, QMessageBox::ActionRole); behaviour = mb.exec(); if (behaviour > 1) { return 0; } saveColorSetting = dontPrompt.isChecked(); // should we save this option to the config for next time? } const KoColorSpace * cs; const KoColorProfile *profile = 0; if (behaviour == PASTE_ASSUME_MONITOR) profile = cfg.displayProfile(QApplication::desktop()->screenNumber(qApp->activeWindow())); cs = KoColorSpaceRegistry::instance()->rgb8(profile); if (!cs) { cs = KoColorSpaceRegistry::instance()->rgb8(); profile = cs->profile(); } clip = new KisPaintDevice(cs); Q_CHECK_PTR(clip); clip->convertFromQImage(qimage, profile); QRect clipBounds = clip->exactBounds(); QPoint diff = imageBounds.center() - clipBounds.center(); clip->setX(diff.x()); clip->setY(diff.y()); // save the persion's selection to the configuration if the option is checked if (saveColorSetting) { cfg.setPasteBehaviour(behaviour); } } return clip; } void KisClipboard::clipboardDataChanged() { if (!m_pushedClipboard) { m_hasClip = false; QClipboard *cb = QApplication::clipboard(); if (cb->mimeData()->hasImage()) { QImage qimage = cb->image(); if (!qimage.isNull()) m_hasClip = true; const QMimeData *cbData = cb->mimeData(); QByteArray mimeType("application/x-krita-selection"); if (cbData && cbData->hasFormat(mimeType)) m_hasClip = true; } } if (m_hasClip) { emit clipCreated(); } m_pushedClipboard = false; emit clipChanged(); } bool KisClipboard::hasClip() const { return m_hasClip; } QSize KisClipboard::clipSize() const { QClipboard *cb = QApplication::clipboard(); QByteArray mimeType("application/x-krita-selection"); const QMimeData *cbData = cb->mimeData(); KisPaintDeviceSP clip; if (cbData && cbData->hasFormat(mimeType)) { QByteArray encodedData = cbData->data(mimeType); QBuffer buffer(&encodedData); KoStore* store = KoStore::createStore(&buffer, KoStore::Read, mimeType); const KoColorProfile *profile = 0; QString csDepth, csModel; // ColorSpace id of layer data if (store->hasFile("colormodel")) { store->open("colormodel"); csModel = QString(store->read(store->size())); store->close(); } if (store->hasFile("colordepth")) { store->open("colordepth"); csDepth = QString(store->read(store->size())); store->close(); } if (store->hasFile("profile.icc")) { QByteArray data; store->open("profile.icc"); data = store->read(store->size()); store->close(); profile = KoColorSpaceRegistry::instance()->createColorProfile(csModel, csDepth, data); } const KoColorSpace *cs = KoColorSpaceRegistry::instance()->colorSpace(csModel, csDepth, profile); if (!cs) { cs = KoColorSpaceRegistry::instance()->rgb8(); } clip = new KisPaintDevice(cs); if (store->hasFile("layerdata")) { store->open("layerdata"); clip->read(store->device()); store->close(); } delete store; return clip->exactBounds().size(); } else { if (cb->mimeData()->hasImage()) { QImage qimage = cb->image(); return qimage.size(); } } return QSize(); } void KisClipboard::setLayers(KisNodeList nodes, KisImageSP image, bool forceCopy) { /** * See a comment in KisMimeData::deepCopyNodes() */ QMimeData *data = KisMimeData::mimeForLayersDeepCopy(nodes, image, forceCopy); if (!data) return; QClipboard *cb = QApplication::clipboard(); cb->setMimeData(data); } bool KisClipboard::hasLayers() const { QClipboard *cb = QApplication::clipboard(); const QMimeData *cbData = cb->mimeData(); return cbData->hasFormat("application/x-krita-node"); } const QMimeData* KisClipboard::layersMimeData() const { QClipboard *cb = QApplication::clipboard(); const QMimeData *cbData = cb->mimeData(); return cbData->hasFormat("application/x-krita-node") ? cbData : 0; } diff --git a/libs/ui/kis_clipboard.h b/libs/ui/kis_clipboard.h index 12fe86a9a3..d84567aa6a 100644 --- a/libs/ui/kis_clipboard.h +++ b/libs/ui/kis_clipboard.h @@ -1,101 +1,98 @@ /* * kis_clipboard.h - part of Krayon * * Copyright (c) 2004 Boudewijn Rempt * * 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_CLIPBOARD_H_ #define __KIS_CLIPBOARD_H_ #include #include #include "kis_types.h" #include class QRect; class QMimeData; -class KisTimeRange; class KisBlockUntilOperationsFinishedMediator; enum enumPasteBehaviour { PASTE_ASSUME_WEB, PASTE_ASSUME_MONITOR, PASTE_ASK }; /** * The Krita clipboard is a clipboard that can store paint devices * instead of just qimage's. */ class KRITAUI_EXPORT KisClipboard : public QObject { Q_OBJECT Q_PROPERTY(bool clip READ hasClip NOTIFY clipChanged) public: KisClipboard(); ~KisClipboard() override; static KisClipboard* instance(); /** * Sets the clipboard to the contents of the specified paint device; also * set the system clipboard to a QImage representation of the specified * paint device. * * @param dev The paint device that will be stored on the clipboard * @param topLeft a hint about the place where the clip should be pasted by default */ - void setClip(KisPaintDeviceSP dev, const QPoint& topLeft); - - void setClip(KisPaintDeviceSP dev, const QPoint& topLeft, const KisTimeRange &range); + void setClip(KisPaintDeviceSP dev, const QPoint& topLeft, int firstFrame = -1, int lastFrame = -1); /** * Get the contents of the clipboard in the form of a paint device. */ - KisPaintDeviceSP clip(const QRect &imageBounds, bool showPopup, KisTimeRange *clipRange = 0); + KisPaintDeviceSP clip(const QRect &imageBounds, bool showPopup, int *firstFrame = 0, int *lastFrame = 0); bool hasClip() const; QSize clipSize() const; void setLayers(KisNodeList nodes, KisImageSP image, bool forceCopy = false); bool hasLayers() const; const QMimeData* layersMimeData() const; Q_SIGNALS: void clipCreated(); private Q_SLOTS: void clipboardDataChanged(); private: KisClipboard(const KisClipboard &); KisClipboard operator=(const KisClipboard &); bool m_hasClip; bool m_pushedClipboard; Q_SIGNALS: void clipChanged(); }; #endif // __KIS_CLIPBOARD_H_ diff --git a/libs/ui/tests/kis_animation_exporter_test.cpp b/libs/ui/tests/kis_animation_exporter_test.cpp index c5dbe390c9..70fc2f3b83 100644 --- a/libs/ui/tests/kis_animation_exporter_test.cpp +++ b/libs/ui/tests/kis_animation_exporter_test.cpp @@ -1,101 +1,101 @@ /* * 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_exporter_test.h" #include "dialogs/KisAsyncAnimationFramesSaveDialog.h" #include #include #include "KisPart.h" #include "kis_image.h" #include "KisDocument.h" #include "kis_image_animation_interface.h" #include "KoColor.h" #include #include "kis_time_range.h" #include "kis_keyframe_channel.h" #include void KisAnimationExporterTest::testAnimationExport() { KisDocument *document = KisPart::instance()->createDocument(); QRect rect(0,0,512,512); QRect fillRect(10,0,502,512); TestUtil::MaskParent p(rect); document->setCurrentImage(p.image); const KoColorSpace *cs = p.image->colorSpace(); KUndo2Command parentCommand; p.layer->enableAnimation(); KisKeyframeChannel *rasterChannel = p.layer->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); rasterChannel->addKeyframe(1, &parentCommand); rasterChannel->addKeyframe(2, &parentCommand); - p.image->animationInterface()->setFullClipRange(KisTimeRange::fromTime(0, 2)); + p.image->animationInterface()->setFullClipRange(KisTimeSpan(0, 2)); KisPaintDeviceSP dev = p.layer->paintDevice(); dev->fill(fillRect, KoColor(Qt::red, cs)); QImage frame0 = dev->convertToQImage(0, rect); p.image->animationInterface()->switchCurrentTimeAsync(1); p.image->waitForDone(); dev->fill(fillRect, KoColor(Qt::green, cs)); QImage frame1 = dev->convertToQImage(0, rect); p.image->animationInterface()->switchCurrentTimeAsync(2); p.image->waitForDone(); dev->fill(fillRect, KoColor(Qt::blue, cs)); QImage frame2 = dev->convertToQImage(0, rect); KisAsyncAnimationFramesSaveDialog exporter(document->image(), - KisTimeRange::fromTime(0,2), + KisTimeSpan(0,2), "export-test.png", 0, 0); exporter.setBatchMode(true); exporter.regenerateRange(0); QTest::qWait(1000); QImage exported; QPoint errpoint; exported.load("export-test0000.png"); qDebug() << exported.size() << frame0.size(); if (!TestUtil::compareQImages(errpoint, exported, frame0)) { QFAIL(QString("Failed to export identical frame0, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); } exported.load("export-test0001.png"); if (!TestUtil::compareQImages(errpoint, exported, frame1)) { QFAIL(QString("Failed to export identical frame1, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); } exported.load("export-test0002.png"); if (!TestUtil::compareQImages(errpoint, exported, frame2)) { QFAIL(QString("Failed to export identical frame2, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); } } KISTEST_MAIN(KisAnimationExporterTest) diff --git a/libs/ui/tests/kis_animation_frame_cache_test.cpp b/libs/ui/tests/kis_animation_frame_cache_test.cpp index cf3eaf3798..17d620435b 100644 --- a/libs/ui/tests/kis_animation_frame_cache_test.cpp +++ b/libs/ui/tests/kis_animation_frame_cache_test.cpp @@ -1,103 +1,103 @@ /* * 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_frame_cache_test.h" #include #include #include "kis_animation_frame_cache.h" #include "kis_image_animation_interface.h" #include "opengl/kis_opengl_image_textures.h" #include "kis_time_range.h" #include "kis_keyframe_channel.h" #include "kundo2command.h" void verifyRangeIsCachedStatus(KisAnimationFrameCacheSP cache, int start, int end, KisAnimationFrameCache::CacheStatus status) { for (int t = start; t <= end; t++) { QVERIFY2( cache->frameStatus(t) == status, qPrintable(QString("Expected status %1 for frame %2 in range %3 to %4").arg(status == KisAnimationFrameCache::Cached ? "Cached" : "Uncached").arg(t).arg(start).arg(end)) ); } } void KisAnimationFrameCacheTest::testCache() { TestUtil::MaskParent p; KisImageSP image = p.image; KisImageAnimationInterface *animation = image->animationInterface(); KisPaintLayerSP layer1 = p.layer; KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "", OPACITY_OPAQUE_U8); KisPaintLayerSP layer3 = new KisPaintLayer(p.image, "", OPACITY_OPAQUE_U8); image->addNode(layer2); image->addNode(layer3); KUndo2Command parentCommand; KisKeyframeChannel *rasterChannel2 = layer2->getKeyframeChannel(KisKeyframeChannel::Content.id()); rasterChannel2->addKeyframe(10, &parentCommand); rasterChannel2->addKeyframe(20, &parentCommand); rasterChannel2->addKeyframe(30, &parentCommand); KisKeyframeChannel *rasterChannel3 = layer2->getKeyframeChannel(KisKeyframeChannel::Content.id()); rasterChannel3->addKeyframe(17, &parentCommand); KisOpenGLImageTexturesSP glTex = KisOpenGLImageTextures::getImageTextures(image, 0, KoColorConversionTransformation::IntentPerceptual, KoColorConversionTransformation::Empty); KisAnimationFrameCacheSP cache = new KisAnimationFrameCache(glTex); int t; animation->saveAndResetCurrentTime(11, &t); animation->notifyFrameReady(); QCOMPARE(cache->frameStatus(9), KisAnimationFrameCache::Uncached); verifyRangeIsCachedStatus(cache, 10, 16, KisAnimationFrameCache::Cached); QCOMPARE(cache->frameStatus(17), KisAnimationFrameCache::Uncached); animation->saveAndResetCurrentTime(30, &t); animation->notifyFrameReady(); QCOMPARE(cache->frameStatus(29), KisAnimationFrameCache::Uncached); verifyRangeIsCachedStatus(cache, 30, 40, KisAnimationFrameCache::Cached); QCOMPARE(cache->frameStatus(9999), KisAnimationFrameCache::Cached); - image->invalidateFrames(KisTimeRange::fromTime(10, 12), QRect()); + image->invalidateFrames(KisFrameSet::between(10, 12), QRect()); verifyRangeIsCachedStatus(cache, 10, 12, KisAnimationFrameCache::Uncached); verifyRangeIsCachedStatus(cache, 13, 16, KisAnimationFrameCache::Cached); - image->invalidateFrames(KisTimeRange::fromTime(15, 20), QRect()); + image->invalidateFrames(KisFrameSet::between(15, 20), QRect()); verifyRangeIsCachedStatus(cache, 13, 14, KisAnimationFrameCache::Cached); verifyRangeIsCachedStatus(cache, 15, 20, KisAnimationFrameCache::Uncached); - image->invalidateFrames(KisTimeRange::infinite(100), QRect()); + image->invalidateFrames(KisFrameSet::infiniteFrom(100), QRect()); verifyRangeIsCachedStatus(cache, 90, 99, KisAnimationFrameCache::Cached); verifyRangeIsCachedStatus(cache, 100, 110, KisAnimationFrameCache::Uncached); - image->invalidateFrames(KisTimeRange::fromTime(90, 100), QRect()); + image->invalidateFrames(KisFrameSet::between(90, 100), QRect()); verifyRangeIsCachedStatus(cache, 80, 89, KisAnimationFrameCache::Cached); verifyRangeIsCachedStatus(cache, 90, 100, KisAnimationFrameCache::Uncached); - image->invalidateFrames(KisTimeRange::infinite(14), QRect()); + image->invalidateFrames(KisFrameSet::infiniteFrom(14), QRect()); QCOMPARE(cache->frameStatus(13), KisAnimationFrameCache::Cached); verifyRangeIsCachedStatus(cache, 15, 100, KisAnimationFrameCache::Uncached); } QTEST_MAIN(KisAnimationFrameCacheTest) diff --git a/libs/ui/utils/kis_document_aware_spin_box_unit_manager.cpp b/libs/ui/utils/kis_document_aware_spin_box_unit_manager.cpp index 4b01c77a3c..8b0a3730e3 100644 --- a/libs/ui/utils/kis_document_aware_spin_box_unit_manager.cpp +++ b/libs/ui/utils/kis_document_aware_spin_box_unit_manager.cpp @@ -1,187 +1,187 @@ /* * Copyright (c) 2017 Laurent Valentin Jospin * * 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_document_aware_spin_box_unit_manager.h" #include "KisPart.h" #include "KisMainWindow.h" #include "KisView.h" #include "KisDocument.h" #include "kis_types.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_time_range.h" KisSpinBoxUnitManager* KisDocumentAwareSpinBoxUnitManagerBuilder::buildUnitManager(QObject* parent) { return new KisDocumentAwareSpinBoxUnitManager(parent); } void KisDocumentAwareSpinBoxUnitManager::setDocumentAwarnessToExistingUnitSpinBox(KisDoubleParseUnitSpinBox* spinBox, bool setUnitFromOutsideToggle) { KisDocumentAwareSpinBoxUnitManager* manager = new KisDocumentAwareSpinBoxUnitManager(spinBox); spinBox->setUnitManager(manager); spinBox->setUnitChangeFromOutsideBehavior(setUnitFromOutsideToggle); } KisDoubleParseUnitSpinBox* KisDocumentAwareSpinBoxUnitManager::createUnitSpinBoxWithDocumentAwarness(QWidget* parent) { KisDoubleParseUnitSpinBox* spinBox = new KisDoubleParseUnitSpinBox(parent); setDocumentAwarnessToExistingUnitSpinBox(spinBox); return spinBox; } KisDocumentAwareSpinBoxUnitManager::KisDocumentAwareSpinBoxUnitManager(QObject *parent, int pPixDir): KisSpinBoxUnitManager(parent) { if (pPixDir == PIX_DIR_Y) { pixDir = PIX_DIR_Y; } else { pixDir = PIX_DIR_X; } grantDocumentRelativeUnits(); //the purpose of this class is to manage document relative units. } qreal KisDocumentAwareSpinBoxUnitManager::getConversionFactor(int dim, QString psymbol) const { QString symbol = psymbol; if (symbol == "%") { //percent can be seen as vw or vh depending of the reference side in the image. if (pixDir == PIX_DIR_X) { symbol = "vw"; } else { symbol = "vh"; } } qreal factor = KisSpinBoxUnitManager::getConversionFactor(dim, symbol); if (factor > 0) { //no errors occurred at a lower level, so the conversion factor has been get. return factor; } factor = 1; //fall back to something natural in case document is unreachable (1 px = 1 pt = 1vw = 1vh). So a virtual document of 100x100 with a resolution of 1. if (!KisPart::instance()->currentMainwindow()) { return factor; } KisView* view = KisPart::instance()->currentMainwindow()->activeView(); if (view == nullptr) { return factor; } KisDocument* doc = view->document(); if (doc == nullptr) { return factor; } KisImage* img = doc->image().data(); if (img == nullptr) { return factor; } qreal resX = img->xRes(); qreal resY = img->yRes(); qreal sizeX = img->width(); qreal sizeY = img->height(); switch (dim) { case LENGTH: if (symbol == "px") { if (pixDir == PIX_DIR_X) { factor = resX; } else { factor = resY; } } else if (symbol == "vw") { qreal docWidth = sizeX/resX; factor = 100.0/docWidth; //1 vw is 1% of document width, 1 vw in point is docWidth/100 so 1 point in vw is the inverse. } else if (symbol == "vh") { qreal docHeight = sizeY/resY; factor = 100.0/docHeight; } break; case IMLENGTH: if (symbol == "vw") { factor = 100.0/sizeX; //1 vw is 1% of document width, 1 vw in pixel is sizeX/100 so 1 pixel in vw is the inverse. } else if (symbol == "vh") { factor = 100.0/sizeY; } break; case TIME: { if (symbol == "s") { qreal fps = img->animationInterface()->framerate(); factor = 1/fps; } else if (symbol == "%") { - const KisTimeRange & time_range = img->animationInterface()->fullClipRange(); + const KisTimeSpan & time_range = img->animationInterface()->fullClipRange(); qreal n_frame = time_range.end() - time_range.start(); factor = 100/n_frame; } } break; default: break; } return factor; } qreal KisDocumentAwareSpinBoxUnitManager::getConversionConstant(int dim, QString symbol) const { if (dim == TIME && symbol == "%") { KisImage* img = KisPart::instance()->currentMainwindow()->activeView()->document()->image().data(); - const KisTimeRange & time_range = img->animationInterface()->fullClipRange(); + const KisTimeSpan & time_range = img->animationInterface()->fullClipRange(); qreal n_frame = time_range.end() - time_range.start(); return -time_range.start()*100.0/n_frame; } return KisSpinBoxUnitManager::getConversionConstant(dim, symbol); } bool KisDocumentAwareSpinBoxUnitManager::hasPercent(int unitDim) const { if (unitDim == IMLENGTH || unitDim == LENGTH) { return true; } return KisSpinBoxUnitManager::hasPercent(unitDim); } diff --git a/plugins/dockers/animation/animation_docker.cpp b/plugins/dockers/animation/animation_docker.cpp index f6d3e7897a..2b01edbc34 100644 --- a/plugins/dockers/animation/animation_docker.cpp +++ b/plugins/dockers/animation/animation_docker.cpp @@ -1,678 +1,678 @@ /* * 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(animation, SIGNAL(sigFramerateChanged()), this, SLOT(slotFrameRateChanged())); 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(sigPlaybackStarted()), 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()); } slotUpdateIcons(); } void AnimationDocker::unsetCanvas() { setCanvas(0); } void AnimationDocker::setViewManager(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)); + m_canvas->image()->animationInterface()->setFullClipRange(KisTimeSpan(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::slotFrameRateChanged() { if (!m_canvas || !m_canvas->image()) return; int fpsOnUI = m_animationWidget->intFramerate->value(); KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); if (animation->framerate() != fpsOnUI) { m_animationWidget->intFramerate->setValue(animation->framerate()); } } 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); + KisKeyframeBaseSP dstKeyframe; + KisKeyframeBaseSP keyframe = content->itemAt(time); if (!keyframe) { - dstKeyframe = content->activeKeyframeAt(time); + dstKeyframe = content->activeItemAt(time); } else { - dstKeyframe = content->previousKeyframe(keyframe); + dstKeyframe = content->previousItem(*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); + KisKeyframeBaseSP dstKeyframe; + KisKeyframeBaseSP keyframe = content->activeItemAt(time); if (keyframe) { - dstKeyframe = content->nextKeyframe(keyframe); + dstKeyframe = content->nextItem(*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(true); 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(true); 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(false); if (value != cfg.lazyFrameCreationEnabled()) { cfg.setLazyFrameCreationEnabled(value); updateLazyFrameIcon(); } } void AnimationDocker::slotDropFramesChanged(bool value) { KisConfig cfg(false); 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, this); m_deleteTransformKeyframeAction = new KisAction(KisAnimationUtils::removeTransformKeyframeActionName, this); // 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(true); setupActionButton(KisAnimationUtils::lazyFrameCreationActionName, KisAction::ACTIVE_IMAGE, cfg.lazyFrameCreationEnabled(), m_animationWidget->btnLazyFrame, &m_lazyFrameAction); } { KisConfig cfg(true); 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/kis_animation_curves_model.cpp b/plugins/dockers/animation/kis_animation_curves_model.cpp index c557e47ece..d1811bf6be 100644 --- a/plugins/dockers/animation/kis_animation_curves_model.cpp +++ b/plugins/dockers/animation/kis_animation_curves_model.cpp @@ -1,414 +1,414 @@ /* * Copyright (c) 2016 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_curves_model.h" #include #include "kis_global.h" #include "kis_image.h" #include "kis_node.h" #include "kis_keyframe_channel.h" #include "kis_scalar_keyframe_channel.h" #include "kis_post_execution_undo_adapter.h" #include "kis_animation_utils.h" #include "kis_processing_applicator.h" #include "kis_command_utils.h" #include "KisImageBarrierLockerWithFeedback.h" struct KisAnimationCurve::Private { Private(KisScalarKeyframeChannel *channel, QColor color) : channel(channel) , color(color) , visible(true) {} KisScalarKeyframeChannel *channel; QColor color; bool visible; }; KisAnimationCurve::KisAnimationCurve(KisScalarKeyframeChannel *channel, QColor color) : m_d(new Private(channel, color)) {} KisScalarKeyframeChannel *KisAnimationCurve::channel() const { return m_d->channel; } QColor KisAnimationCurve::color() const { return m_d->color; } void KisAnimationCurve::setVisible(bool visible) { m_d->visible = visible; } bool KisAnimationCurve::visible() const { return m_d->visible; } struct KisAnimationCurvesModel::Private { QList curves; int nextColorHue; KUndo2Command *undoCommand; Private() : nextColorHue(0) , undoCommand(0) {} KisAnimationCurve *getCurveAt(const QModelIndex& index) { if (!index.isValid()) return 0; int row = index.row(); if (row < 0 || row >= curves.size()) { return 0; } return curves.at(row); } int rowForCurve(KisAnimationCurve *curve) { return curves.indexOf(curve); } int rowForChannel(KisKeyframeChannel *channel) { for (int row = 0; row < curves.count(); row++) { if (curves.at(row)->channel() == channel) return row; } return -1; } QColor chooseNextColor() { if (curves.isEmpty()) nextColorHue = 0; QColor color = QColor::fromHsv(nextColorHue, 255, 255); nextColorHue += 94; // Value chosen experimentally for providing distinct colors nextColorHue = nextColorHue & 0xff; return color; } }; KisAnimationCurvesModel::KisAnimationCurvesModel(QObject *parent) : KisTimeBasedItemModel(parent) , m_d(new Private()) {} KisAnimationCurvesModel::~KisAnimationCurvesModel() { qDeleteAll(m_d->curves); } int KisAnimationCurvesModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_d->curves.size(); } QVariant KisAnimationCurvesModel::data(const QModelIndex &index, int role) const { KisAnimationCurve *curve = m_d->getCurveAt(index); if (curve) { KisScalarKeyframeChannel *channel = curve->channel(); int time = index.column(); KisKeyframeSP keyframe = channel->keyframeAt(time); switch (role) { case SpecialKeyframeExists: return !keyframe.isNull(); case ScalarValueRole: return channel->interpolatedValue(time); case LeftTangentRole: return (keyframe.isNull()) ? QVariant() : keyframe->leftTangent(); case RightTangentRole: return (keyframe.isNull()) ? QVariant() : keyframe->rightTangent(); case InterpolationModeRole: return (keyframe.isNull()) ? QVariant() : keyframe->interpolationMode(); case TangentsModeRole: return (keyframe.isNull()) ? QVariant() : keyframe->tangentsMode(); case CurveColorRole: return curve->color(); case CurveVisibleRole: return curve->visible(); case PreviousKeyframeTime: { KisKeyframeSP active = channel->activeKeyframeAt(time); if (active.isNull()) return QVariant(); if (active->time() < time) { return active->time(); } KisKeyframeSP previous = channel->previousKeyframe(active); if (previous.isNull()) return QVariant(); return previous->time(); } case NextKeyframeTime: { KisKeyframeSP active = channel->activeKeyframeAt(time); if (active.isNull()) { KisKeyframeSP first = channel->firstKeyframe(); if (!first.isNull() && first->time() > time) { return first->time(); } return QVariant(); } KisKeyframeSP next = channel->nextKeyframe(active); if (next.isNull()) return QVariant(); return next->time(); } default: break; } } return KisTimeBasedItemModel::data(index, role); } bool KisAnimationCurvesModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) return false; KisScalarKeyframeChannel *channel = m_d->getCurveAt(index)->channel(); KUndo2Command *command = m_d->undoCommand; switch (role) { case ScalarValueRole: { KisKeyframeSP keyframe = channel->keyframeAt(index.column()); if (keyframe) { if (!command) command = new KUndo2Command(kundo2_i18n("Adjust keyframe")); channel->setScalarValue(keyframe, value.toReal(), command); } else { if (!command) command = new KUndo2Command(kundo2_i18n("Insert keyframe")); auto *addKeyframeCommand = new KisScalarKeyframeChannel::AddKeyframeCommand( channel, index.column(), value.toReal(), command); addKeyframeCommand->redo(); } } break; case LeftTangentRole: case RightTangentRole: { KisKeyframeSP keyframe = channel->keyframeAt(index.column()); if (!keyframe) return false; QPointF leftTangent = (role == LeftTangentRole ? value.toPointF() : keyframe->leftTangent()); QPointF rightTangent = (role == RightTangentRole ? value.toPointF() : keyframe->rightTangent()); if (!command) command = new KUndo2Command(kundo2_i18n("Adjust tangent")); channel->setInterpolationTangents(keyframe, keyframe->tangentsMode(), leftTangent, rightTangent, command); } break; case InterpolationModeRole: { KisKeyframeSP keyframe = channel->keyframeAt(index.column()); if (!keyframe) return false; if (!command) command = new KUndo2Command(kundo2_i18n("Set interpolation mode")); channel->setInterpolationMode(keyframe, (KisKeyframe::InterpolationMode)value.toInt(), command); } break; case TangentsModeRole: { KisKeyframeSP keyframe = channel->keyframeAt(index.column()); if (!keyframe) return false; KisKeyframe::InterpolationTangentsMode mode = (KisKeyframe::InterpolationTangentsMode)value.toInt(); QPointF leftTangent = keyframe->leftTangent(); QPointF rightTangent = keyframe->rightTangent(); if (!command) command = new KUndo2Command(kundo2_i18n("Set interpolation mode")); channel->setInterpolationTangents(keyframe, mode, leftTangent, rightTangent, command); } break; default: return KisTimeBasedItemModel::setData(index, value, role); } if (command && !m_d->undoCommand) { image()->postExecutionUndoAdapter()->addCommand(toQShared(command)); } return true; } QVariant KisAnimationCurvesModel::headerData(int section, Qt::Orientation orientation, int role) const { // TODO return KisTimeBasedItemModel::headerData(section, orientation, role); } void KisAnimationCurvesModel::beginCommand(const KUndo2MagicString &text) { KIS_ASSERT_RECOVER_RETURN(!m_d->undoCommand); m_d->undoCommand = new KUndo2Command(text); } void KisAnimationCurvesModel::endCommand() { KIS_ASSERT_RECOVER_RETURN(m_d->undoCommand); image()->postExecutionUndoAdapter()->addCommand(toQShared(m_d->undoCommand)); m_d->undoCommand = 0; } bool KisAnimationCurvesModel::adjustKeyframes(const QModelIndexList &indexes, int timeOffset, qreal valueOffset) { QScopedPointer command( new KUndo2Command( kundo2_i18np("Adjust Keyframe", "Adjust %1 Keyframes", indexes.size()))); { KisImageBarrierLockerWithFeedback locker(image()); if (timeOffset != 0) { bool ok = createOffsetFramesCommand(indexes, QPoint(timeOffset, 0), false, false, command.data()); if (!ok) return false; } using KisAnimationUtils::FrameItem; using KisAnimationUtils::FrameItemList; FrameItemList frameItems; Q_FOREACH(QModelIndex index, indexes) { KisScalarKeyframeChannel *channel = m_d->getCurveAt(index)->channel(); KIS_ASSERT_RECOVER_RETURN_VALUE(channel, false); frameItems << FrameItem(channel->node(), channel->id(), index.column() + timeOffset); }; new KisCommandUtils::LambdaCommand( command.data(), [frameItems, valueOffset] () -> KUndo2Command* { QScopedPointer cmd(new KUndo2Command()); bool result = false; Q_FOREACH (const FrameItem &item, frameItems) { const int time = item.time; KisNodeSP node = item.node; KisKeyframeChannel *channel = node->getKeyframeChannel(item.channel); if (!channel) continue; KisKeyframeSP keyframe = channel->keyframeAt(time); if (!keyframe) continue; const qreal currentValue = channel->scalarValue(keyframe); channel->setScalarValue(keyframe, currentValue + valueOffset, cmd.data()); result = true; } return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0; }); } KisProcessingApplicator::runSingleCommandStroke(image(), command.take(), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); return true; } KisAnimationCurve *KisAnimationCurvesModel::addCurve(KisScalarKeyframeChannel *channel) { beginInsertRows(QModelIndex(), m_d->curves.size(), m_d->curves.size()); KisAnimationCurve *curve = new KisAnimationCurve(channel, m_d->chooseNextColor()); m_d->curves.append(curve); endInsertRows(); connect(channel, &KisScalarKeyframeChannel::sigKeyframeAdded, this, &KisAnimationCurvesModel::slotKeyframeChanged); connect(channel, &KisScalarKeyframeChannel::sigKeyframeMoved, this, &KisAnimationCurvesModel::slotKeyframeChanged); connect(channel, &KisScalarKeyframeChannel::sigKeyframeRemoved, this, &KisAnimationCurvesModel::slotKeyframeChanged); connect(channel, &KisScalarKeyframeChannel::sigKeyframeChanged, this, &KisAnimationCurvesModel::slotKeyframeChanged); return curve; } void KisAnimationCurvesModel::removeCurve(KisAnimationCurve *curve) { int index = m_d->curves.indexOf(curve); if (index < 0) return; curve->channel()->disconnect(this); beginRemoveRows(QModelIndex(), index, index); m_d->curves.removeAt(index); delete curve; endRemoveRows(); } void KisAnimationCurvesModel::setCurveVisible(KisAnimationCurve *curve, bool visible) { curve->setVisible(visible); int row = m_d->rowForCurve(curve); emit dataChanged(index(row, 0), index(row, columnCount())); } KisNodeSP KisAnimationCurvesModel::nodeAt(QModelIndex index) const { KisAnimationCurve *curve = m_d->getCurveAt(index); if (curve && curve->channel() && curve->channel()->node()) { return KisNodeSP(curve->channel()->node()); } return 0; } QMap KisAnimationCurvesModel::channelsAt(QModelIndex index) const { KisKeyframeChannel *channel = m_d->getCurveAt(index)->channel(); QMap list; list[""] = channel; return list; } -void KisAnimationCurvesModel::slotKeyframeChanged(KisKeyframeSP keyframe) +void KisAnimationCurvesModel::slotKeyframeChanged(KisKeyframeBaseSP keyframe) { int row = m_d->rowForChannel(keyframe->channel()); QModelIndex changedIndex = index(row, keyframe->time()); emit dataChanged(changedIndex, changedIndex); } diff --git a/plugins/dockers/animation/kis_animation_curves_model.h b/plugins/dockers/animation/kis_animation_curves_model.h index f09ee02f20..d3ca22a9c3 100644 --- a/plugins/dockers/animation/kis_animation_curves_model.h +++ b/plugins/dockers/animation/kis_animation_curves_model.h @@ -1,98 +1,98 @@ /* * Copyright (c) 2016 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_CURVES_MODEL_H #define _KIS_ANIMATION_CURVES_MODEL_H #include #include #include #include "kis_time_based_item_model.h" #include "kis_types.h" #include "kundo2command.h" class KisScalarKeyframeChannel; class KisAnimationCurve { public: KisAnimationCurve(KisScalarKeyframeChannel *channel, QColor color); KisScalarKeyframeChannel *channel() const; QColor color() const; void setVisible(bool visible); bool visible() const; private: struct Private; const QScopedPointer m_d; }; class KisAnimationCurvesModel : public KisTimeBasedItemModel { Q_OBJECT public: KisAnimationCurvesModel(QObject *parent); ~KisAnimationCurvesModel() override; bool hasConnectionToCanvas() const; KisAnimationCurve *addCurve(KisScalarKeyframeChannel *channel); void removeCurve(KisAnimationCurve *curve); void setCurveVisible(KisAnimationCurve *curve, bool visible); 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; /** * Begins a block of commands. The following calls to setData will be grouped to a single undo step. * Note: MUST be followed by a call to endCommand(). */ void beginCommand(const KUndo2MagicString &text); void endCommand(); bool adjustKeyframes(const QModelIndexList &indexes, int timeOffset, qreal valueOffset); enum ItemDataRole { ScalarValueRole = KisTimeBasedItemModel::UserRole + 101, InterpolationModeRole, TangentsModeRole, LeftTangentRole, RightTangentRole, CurveColorRole, CurveVisibleRole, PreviousKeyframeTime, NextKeyframeTime }; protected: KisNodeSP nodeAt(QModelIndex index) const override; QMap channelsAt(QModelIndex index) const override; private Q_SLOTS: - void slotKeyframeChanged(KisKeyframeSP keyframe); + void slotKeyframeChanged(KisKeyframeBaseSP keyframe); private: struct Private; const QScopedPointer m_d; }; #endif diff --git a/plugins/dockers/animation/kis_animation_utils.cpp b/plugins/dockers/animation/kis_animation_utils.cpp index 82a19a0b6e..5ed948494b 100644 --- a/plugins/dockers/animation/kis_animation_utils.cpp +++ b/plugins/dockers/animation/kis_animation_utils.cpp @@ -1,354 +1,360 @@ /* * 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_animation_utils.h" #include "kundo2command.h" #include "kis_algebra_2d.h" #include "kis_image.h" #include "kis_node.h" #include "kis_keyframe_channel.h" #include "kis_post_execution_undo_adapter.h" #include "kis_global.h" #include "kis_tool_utils.h" #include "kis_image_animation_interface.h" #include "kis_command_utils.h" #include "kis_processing_applicator.h" #include "kis_transaction.h" namespace KisAnimationUtils { const QString addFrameActionName = i18n("New Frame"); const QString duplicateFrameActionName = i18n("Copy Frame"); const QString removeFrameActionName = i18n("Remove Frame"); const QString removeFramesActionName = i18n("Remove Frames"); const QString lazyFrameCreationActionName = i18n("Auto Frame Mode"); const QString dropFramesActionName = i18n("Drop Frames"); const QString showLayerActionName = i18n("Show in Timeline"); const QString newLayerActionName = i18n("New Layer"); const QString addExistingLayerActionName = i18n("Add Existing Layer"); const QString removeLayerActionName = i18n("Remove Layer"); const QString addOpacityKeyframeActionName = i18n("Add opacity keyframe"); const QString addTransformKeyframeActionName = i18n("Add transform keyframe"); const QString removeOpacityKeyframeActionName = i18n("Remove opacity keyframe"); const QString removeTransformKeyframeActionName = i18n("Remove transform keyframe"); KUndo2Command* createKeyframeCommand(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy, KUndo2Command *parentCommand) { KUndo2Command *cmd = new KisCommandUtils::LambdaCommand( copy ? kundo2_i18n("Copy Keyframe") : kundo2_i18n("Add Keyframe"), parentCommand, [image, node, channelId, time, copy] () mutable -> KUndo2Command* { bool result = false; QScopedPointer cmd(new KUndo2Command()); KisKeyframeChannel *channel = node->getKeyframeChannel(channelId); bool createdChannel = false; if (!channel) { node->enableAnimation(); channel = node->getKeyframeChannel(channelId, true); if (!channel) return nullptr; createdChannel = true; } if (copy) { - if (!channel->keyframeAt(time)) { - KisKeyframeSP srcFrame = channel->activeKeyframeAt(time); - channel->copyKeyframe(srcFrame, time, cmd.data()); + if (!channel->itemAt(time)) { + KisKeyframeBaseSP srcFrame = channel->activeItemAt(time); + channel->copyItem(srcFrame, time, cmd.data()); result = true; } } else { if (channel->keyframeAt(time) && !createdChannel) { if (image->animationInterface()->currentTime() == time && channelId == KisKeyframeChannel::Content.id()) { //shortcut: clearing the image instead KisPaintDeviceSP device = node->paintDevice(); if (device) { const QRect dirtyRect = device->extent(); KisTransaction transaction(kundo2_i18n("Clear"), device, cmd.data()); device->clear(); (void) transaction.endAndTake(); // saved as 'parent' node->setDirty(dirtyRect); result = true; } } } else { channel->addKeyframe(time, cmd.data()); result = true; } } return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : nullptr; }); return cmd; } void createKeyframeLazy(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy) { KUndo2Command *cmd = createKeyframeCommand(image, node, channelId, time, copy); KisProcessingApplicator::runSingleCommandStroke(image, cmd, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); } void removeKeyframes(KisImageSP image, const FrameItemList &frames) { KIS_SAFE_ASSERT_RECOVER_RETURN(!image->locked()); KUndo2Command *cmd = new KisCommandUtils::LambdaCommand( kundo2_i18np("Remove Keyframe", "Remove Keyframes", frames.size()), [image, frames] () { bool result = false; QScopedPointer cmd(new KUndo2Command()); Q_FOREACH (const FrameItem &item, frames) { const int time = item.time; KisNodeSP node = item.node; KisKeyframeChannel *channel = 0; if (node) { channel = node->getKeyframeChannel(item.channel); } if (!channel) continue; - KisKeyframeSP keyframe = channel->keyframeAt(time); + KisKeyframeBaseSP keyframe = channel->itemAt(time); if (!keyframe) continue; channel->deleteKeyframe(keyframe, cmd.data()); result = true; } return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0; }); KisProcessingApplicator::runSingleCommandStroke(image, cmd, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); } void removeKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int time) { QVector frames; frames << FrameItem(node, channel, time); removeKeyframes(image, frames); } struct LessOperator { LessOperator(const QPoint &offset) : m_columnCoeff(-KisAlgebra2D::signPZ(offset.x())), m_rowCoeff(-1000000 * KisAlgebra2D::signZZ(offset.y())) { } bool operator()(const QModelIndex &lhs, const QModelIndex &rhs) { return m_columnCoeff * lhs.column() + m_rowCoeff * lhs.row() < m_columnCoeff * rhs.column() + m_rowCoeff * rhs.row(); } private: int m_columnCoeff; int m_rowCoeff; }; void sortPointsForSafeMove(QModelIndexList *points, const QPoint &offset) { std::sort(points->begin(), points->end(), LessOperator(offset)); } bool supportsContentFrames(KisNodeSP node) { return node->inherits("KisPaintLayer") || node->inherits("KisFilterMask") || node->inherits("KisTransparencyMask") || node->inherits("KisSelectionBasedLayer"); } void swapOneFrameItem(const FrameItem &src, const FrameItem &dst, KUndo2Command *parentCommand) { const int srcTime = src.time; KisNodeSP srcNode = src.node; KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(src.channel); const int dstTime = dst.time; KisNodeSP dstNode = dst.node; KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dst.channel, true); if (srcNode == dstNode) { if (!srcChannel) return; // TODO: add warning! srcChannel->swapFrames(srcTime, dstTime, parentCommand); } else { if (!srcChannel || !dstChannel) return; // TODO: add warning! dstChannel->swapExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand); } } - void moveOneFrameItem(const FrameItem &src, const FrameItem &dst, bool copy, bool moveEmptyFrames, KUndo2Command *parentCommand) + void moveOneFrameItem(const FrameItem &src, const FrameItem &dst, KeyframeAction action, bool moveEmptyFrames, KUndo2Command *parentCommand) { const int srcTime = src.time; KisNodeSP srcNode = src.node; KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(src.channel); const int dstTime = dst.time; KisNodeSP dstNode = dst.node; KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dst.channel, true); if (srcNode == dstNode) { if (!srcChannel) return; // TODO: add warning! - KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); - KisKeyframeSP dstKeyFrame = srcChannel->keyframeAt(dstTime); + KisKeyframeBaseSP srcKeyframe = srcChannel->itemAt(srcTime); + KisKeyframeBaseSP dstKeyFrame = srcChannel->itemAt(dstTime); if (srcKeyframe) { - if (copy) { - srcChannel->copyKeyframe(srcKeyframe, dstTime, parentCommand); + if (action == CopyKeyframes) { + srcChannel->copyItem(srcKeyframe, dstTime, parentCommand); + } else if (action == LinkKeyframes) { + srcChannel->linkKeyframe(srcKeyframe, dstTime, parentCommand); } else { srcChannel->moveKeyframe(srcKeyframe, dstTime, parentCommand); } } else { - if (dstKeyFrame && moveEmptyFrames && !copy) { + if (dstKeyFrame && moveEmptyFrames && action != CopyKeyframes) { //Destination is effectively replaced by an empty frame. dstChannel->deleteKeyframe(dstKeyFrame, parentCommand); } } } else { if (!srcChannel || !dstChannel) return; // TODO: add warning! KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); if (!srcKeyframe) return; // TODO: add warning! dstChannel->copyExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand); - if (!copy) { + if (action == MoveKeyframes) { srcChannel->deleteKeyframe(srcKeyframe, parentCommand); } } } KUndo2Command* createMoveKeyframesCommand(const FrameItemList &srcFrames, const FrameItemList &dstFrames, bool copy, bool moveEmpty, KUndo2Command *parentCommand) { FrameMovePairList srcDstPairs; for (int i = 0; i < srcFrames.size(); i++) { srcDstPairs << std::make_pair(srcFrames[i], dstFrames[i]); } - return createMoveKeyframesCommand(srcDstPairs, copy, moveEmpty, parentCommand); + return createMoveKeyframesCommand(srcDstPairs, copy ? CopyKeyframes : MoveKeyframes, moveEmpty, parentCommand); } KUndo2Command* createMoveKeyframesCommand(const FrameMovePairList &srcDstPairs, - bool copy, + KeyframeAction action, bool moveEmptyFrames, KUndo2Command *parentCommand) { KUndo2Command *cmd = new KisCommandUtils::LambdaCommand( - !copy ? - kundo2_i18np("Move Keyframe", - "Move %1 Keyframes", - srcDstPairs.size()) : + (action == CopyKeyframes) ? kundo2_i18np("Copy Keyframe", "Copy %1 Keyframes", - srcDstPairs.size()), + srcDstPairs.size()) : + (action == LinkKeyframes) ? + kundo2_i18np("Link Keyframe", + "Link %1 Keyframes", + srcDstPairs.size()) : + kundo2_i18np("Move Keyframe", + "Move %1 Keyframes", + srcDstPairs.size()), parentCommand, - [srcDstPairs, copy, moveEmptyFrames] () -> KUndo2Command* { + [srcDstPairs, action, moveEmptyFrames] () -> KUndo2Command* { bool result = false; QScopedPointer cmd(new KUndo2Command()); using MoveChain = QList; QHash moveMap; Q_FOREACH (const FrameMovePair &pair, srcDstPairs) { moveMap.insert(pair.first, {pair.second}); } auto it = moveMap.begin(); while (it != moveMap.end()) { MoveChain &chain = it.value(); const FrameItem &previousFrame = chain.last(); auto tailIt = moveMap.find(previousFrame); if (tailIt == it || tailIt == moveMap.end()) { ++it; continue; } chain.append(tailIt.value()); tailIt = moveMap.erase(tailIt); // no incrementing! we are going to check the new tail now! } for (it = moveMap.begin(); it != moveMap.end(); ++it) { MoveChain &chain = it.value(); chain.prepend(it.key()); KIS_SAFE_ASSERT_RECOVER(chain.size() > 1) { continue; } bool isCycle = false; if (chain.last() == chain.first()) { isCycle = true; chain.takeLast(); } auto frameIt = chain.rbegin(); FrameItem dstItem = *frameIt++; while (frameIt != chain.rend()) { FrameItem srcItem = *frameIt++; if (!isCycle) { - moveOneFrameItem(srcItem, dstItem, copy, moveEmptyFrames, cmd.data()); + moveOneFrameItem(srcItem, dstItem, action, moveEmptyFrames, cmd.data()); } else { swapOneFrameItem(srcItem, dstItem, cmd.data()); } dstItem = srcItem; result = true; } } return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0; }); return cmd; } QDebug operator<<(QDebug dbg, const FrameItem &item) { dbg.nospace() << "FrameItem(" << item.node->name() << ", " << item.channel << ", " << item.time << ")"; return dbg.space(); } } diff --git a/plugins/dockers/animation/kis_animation_utils.h b/plugins/dockers/animation/kis_animation_utils.h index aa86fc14a2..8b08bbd4e1 100644 --- a/plugins/dockers/animation/kis_animation_utils.h +++ b/plugins/dockers/animation/kis_animation_utils.h @@ -1,104 +1,110 @@ /* * 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_ANIMATION_UTILS_H #define __KIS_ANIMATION_UTILS_H #include "kis_types.h" #include #include #include namespace KisAnimationUtils { + enum KeyframeAction { + CopyKeyframes, + MoveKeyframes, + LinkKeyframes + }; + KUndo2Command* createKeyframeCommand(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy, KUndo2Command *parentCommand = 0); void createKeyframeLazy(KisImageSP image, KisNodeSP node, const QString &channel, int time, bool copy); struct KRITAANIMATIONDOCKER_EXPORT FrameItem : public boost::equality_comparable { FrameItem() : time(-1) {} FrameItem(KisNodeSP _node, const QString &_channel, int _time) : node(_node), channel(_channel), time(_time) {} bool operator==(const FrameItem &rhs) const { return rhs.node == node && rhs.channel == channel && rhs.time == time; } KisNodeSP node; QString channel; int time; }; KRITAANIMATIONDOCKER_EXPORT QDebug operator<<(QDebug dbg, const FrameItem &item); inline uint qHash(const FrameItem &item) { return ::qHash(item.node.data()) + ::qHash(item.channel) + ::qHash(item.time); } typedef QVector FrameItemList; typedef std::pair FrameMovePair; typedef QVector FrameMovePairList; void removeKeyframes(KisImageSP image, const FrameItemList &frames); void removeKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int time); void sortPointsForSafeMove(QModelIndexList *points, const QPoint &offset); KUndo2Command* createMoveKeyframesCommand(const FrameItemList &srcFrames, const FrameItemList &dstFrames, bool copy, bool moveEmpty, KUndo2Command *parentCommand = 0); /** * @brief implements safe moves of the frames (even if there are cycling move dependencies) * @param movePairs the jobs for the moves * @param copy shows if the frames should be copied or not * @param moveEmpty allows an empty frame to replace a populated one * @param parentCommand the command that should be a parent of the created command * @return a created undo command */ KRITAANIMATIONDOCKER_EXPORT KUndo2Command* createMoveKeyframesCommand(const FrameMovePairList &movePairs, - bool copy, bool moveEmptyFrames, KUndo2Command *parentCommand = 0); + KeyframeAction action, bool moveEmptyFrames, KUndo2Command *parentCommand = 0); bool supportsContentFrames(KisNodeSP node); extern const QString addFrameActionName; extern const QString duplicateFrameActionName; extern const QString removeFrameActionName; extern const QString removeFramesActionName; extern const QString lazyFrameCreationActionName; extern const QString dropFramesActionName; extern const QString showLayerActionName; extern const QString newLayerActionName; extern const QString addExistingLayerActionName; extern const QString removeLayerActionName; extern const QString addOpacityKeyframeActionName; extern const QString addTransformKeyframeActionName; extern const QString removeOpacityKeyframeActionName; extern const QString removeTransformKeyframeActionName; }; #endif /* __KIS_ANIMATION_UTILS_H */ diff --git a/plugins/dockers/animation/kis_time_based_item_model.cpp b/plugins/dockers/animation/kis_time_based_item_model.cpp index 38f429ecf3..252c28aa90 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.cpp +++ b/plugins/dockers/animation/kis_time_based_item_model.cpp @@ -1,516 +1,521 @@ /* * Copyright (c) 2016 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_time_based_item_model.h" #include #include #include "kis_animation_frame_cache.h" #include "kis_animation_player.h" #include "kis_signal_compressor_with_param.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "kis_animation_utils.h" #include "kis_keyframe_channel.h" #include "kis_processing_applicator.h" #include "KisImageBarrierLockerWithFeedback.h" #include "commands_new/kis_switch_current_time_command.h" #include "kis_command_utils.h" struct KisTimeBasedItemModel::Private { Private() : animationPlayer(0) , numFramesOverride(0) , activeFrameIndex(0) , scrubInProgress(false) , scrubStartFrame(-1) {} KisImageWSP image; KisAnimationFrameCacheWSP framesCache; QPointer animationPlayer; QVector cachedFrames; int numFramesOverride; int activeFrameIndex; bool scrubInProgress; int scrubStartFrame; QScopedPointer > scrubbingCompressor; int baseNumFrames() const { auto imageSP = image.toStrongRef(); if (!imageSP) return 0; KisImageAnimationInterface *i = imageSP->animationInterface(); if (!i) return 1; return i->totalLength(); } int effectiveNumFrames() const { if (image.isNull()) return 0; return qMax(baseNumFrames(), numFramesOverride); } int framesPerSecond() { return image->animationInterface()->framerate(); } }; KisTimeBasedItemModel::KisTimeBasedItemModel(QObject *parent) : QAbstractTableModel(parent) , m_d(new Private()) { KisConfig cfg(true); using namespace std::placeholders; std::function callback( std::bind(&KisTimeBasedItemModel::slotInternalScrubPreviewRequested, this, _1)); m_d->scrubbingCompressor.reset( new KisSignalCompressorWithParam(cfg.scrubbingUpdatesDelay(), callback, KisSignalCompressor::FIRST_ACTIVE)); } KisTimeBasedItemModel::~KisTimeBasedItemModel() {} void KisTimeBasedItemModel::setImage(KisImageWSP image) { KisImageWSP oldImage = m_d->image; m_d->image = image; if (image) { KisImageAnimationInterface *ai = image->animationInterface(); slotCurrentTimeChanged(ai->currentUITime()); connect(ai, SIGNAL(sigFramerateChanged()), SLOT(slotFramerateChanged())); connect(ai, SIGNAL(sigUiTimeChanged(int)), SLOT(slotCurrentTimeChanged(int))); } if (image != oldImage) { beginResetModel(); endResetModel(); } } void KisTimeBasedItemModel::setFrameCache(KisAnimationFrameCacheSP cache) { if (KisAnimationFrameCacheSP(m_d->framesCache) == cache) return; if (m_d->framesCache) { m_d->framesCache->disconnect(this); } m_d->framesCache = cache; if (m_d->framesCache) { connect(m_d->framesCache, SIGNAL(changed()), SLOT(slotCacheChanged())); } } void KisTimeBasedItemModel::setAnimationPlayer(KisAnimationPlayer *player) { if (m_d->animationPlayer == player) return; if (m_d->animationPlayer) { m_d->animationPlayer->disconnect(this); } m_d->animationPlayer = player; if (m_d->animationPlayer) { connect(m_d->animationPlayer, SIGNAL(sigPlaybackStopped()), SLOT(slotPlaybackStopped())); connect(m_d->animationPlayer, SIGNAL(sigFrameChanged()), SLOT(slotPlaybackFrameChanged())); } } void KisTimeBasedItemModel::setLastVisibleFrame(int time) { const int growThreshold = m_d->effectiveNumFrames() - 3; const int growValue = time + 8; const int shrinkThreshold = m_d->effectiveNumFrames() - 12; const int shrinkValue = qMax(m_d->baseNumFrames(), qMin(growValue, shrinkThreshold)); const bool canShrink = m_d->effectiveNumFrames() > m_d->baseNumFrames(); if (time >= growThreshold) { beginInsertColumns(QModelIndex(), m_d->effectiveNumFrames(), growValue - 1); m_d->numFramesOverride = growValue; endInsertColumns(); } else if (time < shrinkThreshold && canShrink) { beginRemoveColumns(QModelIndex(), shrinkValue, m_d->effectiveNumFrames() - 1); m_d->numFramesOverride = shrinkValue; endRemoveColumns(); } } int KisTimeBasedItemModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_d->effectiveNumFrames(); } QVariant KisTimeBasedItemModel::data(const QModelIndex &index, int role) const { switch (role) { case ActiveFrameRole: { return index.column() == m_d->activeFrameIndex; } } return QVariant(); } bool KisTimeBasedItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) return false; switch (role) { case ActiveFrameRole: { setHeaderData(index.column(), Qt::Horizontal, value, role); break; } } return false; } QVariant KisTimeBasedItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { switch (role) { case ActiveFrameRole: return section == m_d->activeFrameIndex; case FrameCachedRole: return m_d->cachedFrames.size() > section ? m_d->cachedFrames[section] : false; case FramesPerSecondRole: return m_d->framesPerSecond(); } } return QVariant(); } bool KisTimeBasedItemModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) { if (orientation == Qt::Horizontal) { switch (role) { case ActiveFrameRole: if (value.toBool() && section != m_d->activeFrameIndex) { int prevFrame = m_d->activeFrameIndex; m_d->activeFrameIndex = section; scrubTo(m_d->activeFrameIndex, m_d->scrubInProgress); /** * Optimization Hack Alert: * * ideally, we should emit all four signals, but... The * point is this code is used in a tight loop during * playback, so it should run as fast as possible. To tell * the story short, commenting out these three lines makes * playback run 15% faster ;) */ if (m_d->scrubInProgress) { //emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame)); emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex)); //emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame); //emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex); } else { emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame)); emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex)); emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame); emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex); } } } } return false; } bool KisTimeBasedItemModel::removeFrames(const QModelIndexList &indexes) { KisAnimationUtils::FrameItemList frameItems; { KisImageBarrierLockerWithFeedback locker(m_d->image); Q_FOREACH (const QModelIndex &index, indexes) { int time = index.column(); Q_FOREACH(KisKeyframeChannel *channel, channelsAt(index)) { - if (channel->keyframeAt(time)) { + if (channel->itemAt(time)) { frameItems << KisAnimationUtils::FrameItem(channel->node(), channel->id(), index.column()); } } } } if (frameItems.isEmpty()) return false; KisAnimationUtils::removeKeyframes(m_d->image, frameItems); return true; } KUndo2Command* KisTimeBasedItemModel::createOffsetFramesCommand(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, bool moveEmptyFrames, KUndo2Command *parentCommand) { if (srcIndexes.isEmpty()) return 0; if (offset.isNull()) return 0; KisAnimationUtils::sortPointsForSafeMove(&srcIndexes, offset); KisAnimationUtils::FrameItemList srcFrameItems; KisAnimationUtils::FrameItemList dstFrameItems; Q_FOREACH (const QModelIndex &srcIndex, srcIndexes) { QModelIndex dstIndex = index( srcIndex.row() + offset.y(), srcIndex.column() + offset.x()); KisNodeSP srcNode = nodeAt(srcIndex); KisNodeSP dstNode = nodeAt(dstIndex); if (!srcNode || !dstNode) return 0; Q_FOREACH(KisKeyframeChannel *channel, channelsAt(srcIndex)) { - if (moveEmptyFrames || channel->keyframeAt(srcIndex.column())) { + if (moveEmptyFrames || channel->itemAt(srcIndex.column())) { srcFrameItems << KisAnimationUtils::FrameItem(srcNode, channel->id(), srcIndex.column()); dstFrameItems << KisAnimationUtils::FrameItem(dstNode, channel->id(), dstIndex.column()); } } } KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcFrameItems.size() == dstFrameItems.size(), 0); if (srcFrameItems.isEmpty()) return 0; return KisAnimationUtils::createMoveKeyframesCommand(srcFrameItems, dstFrameItems, copyFrames, moveEmptyFrames, parentCommand); } bool KisTimeBasedItemModel::removeFramesAndOffset(QModelIndexList indicesToRemove) { if (indicesToRemove.isEmpty()) return true; std::sort(indicesToRemove.begin(), indicesToRemove.end(), [] (const QModelIndex &lhs, const QModelIndex &rhs) { return lhs.column() > rhs.column(); }); const int minColumn = indicesToRemove.last().column(); KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Remove frame and shift", "Remove %1 frames and shift", indicesToRemove.size())); { KisImageBarrierLockerWithFeedback locker(m_d->image); Q_FOREACH (const QModelIndex &index, indicesToRemove) { QModelIndexList indicesToOffset; for (int column = index.column() + 1; column < columnCount(); column++) { indicesToOffset << this->index(index.row(), column); } createOffsetFramesCommand(indicesToOffset, QPoint(-1, 0), false, true, parentCommand); } const int oldTime = m_d->image->animationInterface()->currentUITime(); const int newTime = minColumn; new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(), oldTime, newTime, parentCommand); } KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); return true; } bool KisTimeBasedItemModel::mirrorFrames(QModelIndexList indexes) { QScopedPointer parentCommand(new KUndo2Command(kundo2_i18n("Mirror Frames"))); { KisImageBarrierLockerWithFeedback locker(m_d->image); QMap rowsList; Q_FOREACH (const QModelIndex &index, indexes) { rowsList[index.row()].append(index); } Q_FOREACH (int row, rowsList.keys()) { QModelIndexList &list = rowsList[row]; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!list.isEmpty(), false); std::sort(list.begin(), list.end(), [] (const QModelIndex &lhs, const QModelIndex &rhs) { return lhs.column() < rhs.column(); }); auto srcIt = list.begin(); auto dstIt = list.end(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcIt != dstIt, false); --dstIt; QList channels = channelsAt(*srcIt).values(); while (srcIt < dstIt) { Q_FOREACH (KisKeyframeChannel *channel, channels) { channel->swapFrames(srcIt->column(), dstIt->column(), parentCommand.data()); } srcIt++; dstIt--; } } } KisProcessingApplicator::runSingleCommandStroke(m_d->image, new KisCommandUtils::SkipFirstRedoWrapper(parentCommand.take()), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); return true; } void KisTimeBasedItemModel::slotInternalScrubPreviewRequested(int time) { if (m_d->animationPlayer && !m_d->animationPlayer->isPlaying()) { m_d->animationPlayer->displayFrame(time); } } void KisTimeBasedItemModel::setScrubState(bool active) { if (!m_d->scrubInProgress && active) { m_d->scrubStartFrame = m_d->activeFrameIndex; m_d->scrubInProgress = true; } if (m_d->scrubInProgress && !active) { m_d->scrubInProgress = false; if (m_d->scrubStartFrame >= 0 && m_d->scrubStartFrame != m_d->activeFrameIndex) { scrubTo(m_d->activeFrameIndex, false); } m_d->scrubStartFrame = -1; } } void KisTimeBasedItemModel::scrubTo(int time, bool preview) { if (m_d->animationPlayer && m_d->animationPlayer->isPlaying()) return; KIS_ASSERT_RECOVER_RETURN(m_d->image); if (preview) { if (m_d->animationPlayer) { m_d->scrubbingCompressor->start(time); } } else { m_d->image->animationInterface()->requestTimeSwitchWithUndo(time); } } void KisTimeBasedItemModel::slotFramerateChanged() { emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); } void KisTimeBasedItemModel::slotCurrentTimeChanged(int time) { if (time != m_d->activeFrameIndex) { setHeaderData(time, Qt::Horizontal, true, ActiveFrameRole); } } void KisTimeBasedItemModel::slotCacheChanged() { const int numFrames = columnCount(); m_d->cachedFrames.resize(numFrames); for (int i = 0; i < numFrames; i++) { m_d->cachedFrames[i] = m_d->framesCache->frameStatus(i) == KisAnimationFrameCache::Cached; } emit headerDataChanged(Qt::Horizontal, 0, numFrames); } void KisTimeBasedItemModel::slotPlaybackFrameChanged() { if (!m_d->animationPlayer->isPlaying()) return; setData(index(0, m_d->animationPlayer->currentTime()), true, ActiveFrameRole); } void KisTimeBasedItemModel::slotPlaybackStopped() { setData(index(0, m_d->image->animationInterface()->currentUITime()), true, ActiveFrameRole); } -void KisTimeBasedItemModel::setPlaybackRange(const KisTimeRange &range) +void KisTimeBasedItemModel::setPlaybackRange(const KisTimeSpan &range) { if (m_d->image.isNull()) return; KisImageAnimationInterface *i = m_d->image->animationInterface(); i->setPlaybackRange(range); } bool KisTimeBasedItemModel::isPlaybackActive() const { return m_d->animationPlayer && m_d->animationPlayer->isPlaying(); } int KisTimeBasedItemModel::currentTime() const { return m_d->image->animationInterface()->currentUITime(); } KisImageWSP KisTimeBasedItemModel::image() const { return m_d->image; } + +int KisTimeBasedItemModel::activeFrameIndex() const +{ + return m_d->activeFrameIndex; +} diff --git a/plugins/dockers/animation/kis_time_based_item_model.h b/plugins/dockers/animation/kis_time_based_item_model.h index 82f8f2d728..2782561da2 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.h +++ b/plugins/dockers/animation/kis_time_based_item_model.h @@ -1,104 +1,106 @@ /* * Copyright (c) 2016 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_TIME_BASED_ITEM_MODEL_H #define _KIS_TIME_BASED_ITEM_MODEL_H #include #include #include #include "kritaanimationdocker_export.h" #include "kis_types.h" -class KisTimeRange; +class KisTimeSpan; class KisAnimationPlayer; class KisKeyframeChannel; class KRITAANIMATIONDOCKER_EXPORT KisTimeBasedItemModel : public QAbstractTableModel { Q_OBJECT public: KisTimeBasedItemModel(QObject *parent); ~KisTimeBasedItemModel() override; void setImage(KisImageWSP image); void setFrameCache(KisAnimationFrameCacheSP cache); void setAnimationPlayer(KisAnimationPlayer *player); void setLastVisibleFrame(int time); int columnCount(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; bool removeFrames(const QModelIndexList &indexes); bool removeFramesAndOffset(QModelIndexList indicesToRemove); bool mirrorFrames(QModelIndexList indexes); void setScrubState(bool active); void scrubTo(int time, bool preview); - void setPlaybackRange(const KisTimeRange &range); + void setPlaybackRange(const KisTimeSpan &range); bool isPlaybackActive() const; int currentTime() const; enum ItemDataRole { ActiveFrameRole = Qt::UserRole + 101, FrameExistsRole, SpecialKeyframeExists, + IdenticalWithActive, FrameCachedRole, FrameEditableRole, FramesPerSecondRole, UserRole, FrameHasContent // is it an empty frame with nothing in it? }; protected: virtual KisNodeSP nodeAt(QModelIndex index) const = 0; virtual QMap channelsAt(QModelIndex index) const = 0; KisImageWSP image() const; KUndo2Command* createOffsetFramesCommand(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, bool moveEmptyFrames, KUndo2Command *parentCommand = 0); + int activeFrameIndex() const; private Q_SLOTS: void slotFramerateChanged(); void slotCurrentTimeChanged(int time); void slotCacheChanged(); void slotInternalScrubPreviewRequested(int time); void slotPlaybackFrameChanged(); void slotPlaybackStopped(); private: struct Private; const QScopedPointer m_d; }; #endif diff --git a/plugins/dockers/animation/tests/kis_animation_utils_test.cpp b/plugins/dockers/animation/tests/kis_animation_utils_test.cpp index 31437888a9..3823505311 100644 --- a/plugins/dockers/animation/tests/kis_animation_utils_test.cpp +++ b/plugins/dockers/animation/tests/kis_animation_utils_test.cpp @@ -1,227 +1,227 @@ #include "kis_animation_utils_test.h" #include #include #include #include "kis_image_animation_interface.h" #include "kis_paint_layer.h" #include "kis_keyframe_channel.h" #include #include "kis_animation_utils.h" #include bool verifyFrames(TestUtil::MaskParent &p, const QVector &nodes, const QVector> &offsets) { KisImageAnimationInterface *i = p.image->animationInterface(); Q_FOREACH (const auto &offset, offsets) { int time = 0; QRect rc1; QRect rc2; std::tie(time, rc1, rc2) = offset; i->switchCurrentTimeAsync(time); p.image->waitForDone(); KIS_SAFE_ASSERT_RECOVER_NOOP(nodes[0]->paintDevice()->defaultBounds()->currentTime() == time); KIS_SAFE_ASSERT_RECOVER_NOOP(nodes[1]->paintDevice()->defaultBounds()->currentTime() == time); KisKeyframeChannel *channel1 = nodes[0]->getKeyframeChannel("content"); KisKeyframeChannel *channel2 = nodes[1]->getKeyframeChannel("content"); if (!rc1.isValid() && !channel1->keyframeAt(time)) { // noop } else if (nodes[0]->paintDevice()->exactBounds() != rc1) { qWarning() << "Compared values are not the same:"; qWarning() << " " << ppVar(nodes[0]->paintDevice()->exactBounds()); qWarning() << " " << ppVar(rc1); qWarning() << " " << ppVar(time); return false; } if (!rc2.isValid() && !channel2->keyframeAt(time)) { // noop } else if (nodes[1]->paintDevice()->exactBounds() != rc2) { qWarning() << "Compared values are not the same:"; qWarning() << " " << ppVar(nodes[1]->paintDevice()->exactBounds()); qWarning() << " " << ppVar(rc2); qWarning() << " " << ppVar(time); return false; } } return true; } void KisAnimationUtilsTest::test() { QRect refRect(QRect(0,0,512,512)); TestUtil::MaskParent p(refRect); const KoColor fillColor(Qt::red, p.image->colorSpace()); KisPaintLayerSP layer1 = p.layer; KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8); p.image->addNode(layer2); QVector nodes({layer1, layer2}); KisPaintDeviceSP dev1 = layer1->paintDevice(); KisPaintDeviceSP dev2 = layer2->paintDevice(); KisKeyframeChannel *channel1 = layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); KisKeyframeChannel *channel2 = layer2->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); channel1->addKeyframe(0); channel2->addKeyframe(0); channel1->addKeyframe(10); channel2->addKeyframe(10); channel1->addKeyframe(20); channel2->addKeyframe(20); KisImageAnimationInterface *i = p.image->animationInterface(); i->switchCurrentTimeAsync(0); p.image->waitForDone(); dev1->fill(QRect(0, 0, 10, 10), fillColor); dev2->fill(QRect(0, 10, 10, 10), fillColor); i->switchCurrentTimeAsync(10); p.image->waitForDone(); dev1->fill(QRect(10, 0, 10, 10), fillColor); dev2->fill(QRect(10, 10, 10, 10), fillColor); i->switchCurrentTimeAsync(20); p.image->waitForDone(); dev1->fill(QRect(20, 0, 10, 10), fillColor); dev2->fill(QRect(20, 10, 10, 10), fillColor); QVector> initialReferenceRects; initialReferenceRects << std::make_tuple( 0, QRect( 0, 0, 10, 10), QRect( 0, 10, 10, 10)); initialReferenceRects << std::make_tuple(10, QRect(10, 0, 10, 10), QRect(10, 10, 10, 10)); initialReferenceRects << std::make_tuple(20, QRect(20, 0, 10, 10), QRect(20, 10, 10, 10)); QVERIFY(verifyFrames(p, nodes, initialReferenceRects)); using namespace KisAnimationUtils; FrameMovePairList frameMoves; // // Cycling single-layer move // frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer1, "content", 10)); frameMoves << std::make_pair(FrameItem(layer1, "content", 10), FrameItem(layer1, "content", 20)); frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer1, "content", 0)); - QScopedPointer cmd(createMoveKeyframesCommand(frameMoves, false, false)); + QScopedPointer cmd(createMoveKeyframesCommand(frameMoves, MoveKeyframes, false)); cmd->redo(); QVector> referenceRects; referenceRects << std::make_tuple( 0, QRect(20, 0, 10, 10), QRect( 0, 10, 10, 10)); referenceRects << std::make_tuple(10, QRect( 0, 0, 10, 10), QRect(10, 10, 10, 10)); referenceRects << std::make_tuple(20, QRect(10, 0, 10, 10), QRect(20, 10, 10, 10)); QVERIFY(verifyFrames(p, nodes, referenceRects)); cmd->undo(); QVERIFY(verifyFrames(p, nodes, initialReferenceRects)); frameMoves.clear(); referenceRects.clear(); cmd.reset(); // // Just a complex non-cycling move // frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer2, "content", 10)); frameMoves << std::make_pair(FrameItem(layer2, "content", 10), FrameItem(layer1, "content", 10)); frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer2, "content", 20)); - cmd.reset(createMoveKeyframesCommand(frameMoves, false, false)); + cmd.reset(createMoveKeyframesCommand(frameMoves, MoveKeyframes, false)); cmd->redo(); referenceRects << std::make_tuple( 0, QRect() , QRect( 0, 10, 10, 10)); referenceRects << std::make_tuple(10, QRect(10, 10, 10, 10), QRect( 0, 0, 10, 10)); referenceRects << std::make_tuple(20, QRect() , QRect(20, 0, 10, 10)); QVERIFY(verifyFrames(p, nodes, referenceRects)); cmd->undo(); QVERIFY(verifyFrames(p, nodes, initialReferenceRects)); frameMoves.clear(); referenceRects.clear(); cmd.reset(); // // Cross-node swap of the frames // frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer2, "content", 0)); frameMoves << std::make_pair(FrameItem(layer1, "content", 10), FrameItem(layer2, "content", 10)); frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer2, "content", 20)); frameMoves << std::make_pair(FrameItem(layer2, "content", 0), FrameItem(layer1, "content", 0)); frameMoves << std::make_pair(FrameItem(layer2, "content", 10), FrameItem(layer1, "content", 10)); frameMoves << std::make_pair(FrameItem(layer2, "content", 20), FrameItem(layer1, "content", 20)); - cmd.reset(createMoveKeyframesCommand(frameMoves, false, false)); + cmd.reset(createMoveKeyframesCommand(frameMoves, MoveKeyframes, false)); cmd->redo(); referenceRects << std::make_tuple( 0, QRect( 0, 10, 10, 10), QRect( 0, 0, 10, 10)); referenceRects << std::make_tuple(10, QRect(10, 10, 10, 10), QRect(10, 0, 10, 10)); referenceRects << std::make_tuple(20, QRect(20, 10, 10, 10), QRect(20, 0, 10, 10)); QVERIFY(verifyFrames(p, nodes, referenceRects)); cmd->undo(); QVERIFY(verifyFrames(p, nodes, initialReferenceRects)); frameMoves.clear(); referenceRects.clear(); cmd.reset(); // // Cross-node move and swap // frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer2, "content", 0)); frameMoves << std::make_pair(FrameItem(layer1, "content", 10), FrameItem(layer2, "content", 10)); frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer2, "content", 20)); frameMoves << std::make_pair(FrameItem(layer2, "content", 0), FrameItem(layer1, "content", 0)); frameMoves << std::make_pair(FrameItem(layer2, "content", 10), FrameItem(layer1, "content", 9)); frameMoves << std::make_pair(FrameItem(layer2, "content", 20), FrameItem(layer1, "content", 20)); - cmd.reset(createMoveKeyframesCommand(frameMoves, false, false)); + cmd.reset(createMoveKeyframesCommand(frameMoves, MoveKeyframes, false)); cmd->redo(); referenceRects << std::make_tuple( 0, QRect( 0, 10, 10, 10), QRect( 0, 0, 10, 10)); referenceRects << std::make_tuple( 9, QRect(10, 10, 10, 10), QRect() ); referenceRects << std::make_tuple(10, QRect() , QRect(10, 0, 10, 10)); referenceRects << std::make_tuple(20, QRect(20, 10, 10, 10), QRect(20, 0, 10, 10)); QVERIFY(verifyFrames(p, nodes, referenceRects)); cmd->undo(); QVERIFY(verifyFrames(p, nodes, initialReferenceRects)); frameMoves.clear(); referenceRects.clear(); cmd.reset(); } QTEST_MAIN(KisAnimationUtilsTest) diff --git a/plugins/dockers/animation/timeline_frames_item_delegate.cpp b/plugins/dockers/animation/timeline_frames_item_delegate.cpp index c56cf423eb..b28eabb50a 100644 --- a/plugins/dockers/animation/timeline_frames_item_delegate.cpp +++ b/plugins/dockers/animation/timeline_frames_item_delegate.cpp @@ -1,265 +1,339 @@ /* * 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_item_delegate.h" #include #include #include #include "krita_utils.h" #include "timeline_frames_model.h" #include "timeline_color_scheme.h" +#include "timeline_frames_view.h" #include "kis_node_view_color_scheme.h" -TimelineFramesItemDelegate::TimelineFramesItemDelegate(QObject *parent) +TimelineFramesItemDelegate::TimelineFramesItemDelegate(QObject *parent, TimelineCycleRange *cycleRangeDelegate) : QItemDelegate(parent) + , timelineCycleRange(cycleRangeDelegate) { KisNodeViewColorScheme scm; labelColors = scm.allColorLabels(); } TimelineFramesItemDelegate::~TimelineFramesItemDelegate() { } void TimelineFramesItemDelegate::paintActiveFrameSelector(QPainter *painter, const QRect &rc, bool isCurrentFrame) { QColor lineColor = TimelineColorScheme::instance()->selectorColor(); const int lineWidth = rc.width() > 20 ? 4 : 2; const int x0 = rc.x(); const int y0 = rc.y(); const int x1 = rc.right(); const int y1 = rc.bottom(); QVector linesDark; linesDark << QLine(x0 + lineWidth / 2, y0, x0 + lineWidth / 2, y1); linesDark << QLine(x1 - lineWidth / 2 + 1, y0, x1 - lineWidth / 2 + 1, y1); QPen oldPen = painter->pen(); painter->setPen(QPen(lineColor, lineWidth)); painter->drawLines(linesDark); painter->setPen(oldPen); - - if (isCurrentFrame) { - QPen oldPen = painter->pen(); - QBrush oldBrush(painter->brush()); - - painter->setPen(QPen(lineColor, 0)); - painter->setBrush(lineColor); - - painter->drawEllipse(rc.center(), 2,2); - - painter->setBrush(oldBrush); - painter->setPen(oldPen); - } } void TimelineFramesItemDelegate::paintSpecialKeyframeIndicator(QPainter *painter, const QModelIndex &index, const QRect &rc) const { bool doesFrameExist = index.data(TimelineFramesModel::FrameExistsRole).toBool(); /// does normal keyframe exist bool isEditable = index.data(TimelineFramesModel::FrameEditableRole).toBool(); bool hasContent = index.data(TimelineFramesModel::FrameHasContent).toBool(); /// find out if frame is empty QColor color = qApp->palette().color(QPalette::Highlight); QColor baseColor = qApp->palette().color(QPalette::Base); QColor noLabelSetColor = qApp->palette().color(QPalette::Highlight); // if no color label is used // use color label if it exists. coloring follows very similar logic to the drawBackground() function except this is a bit simpler QVariant colorLabel = index.data(TimelineFramesModel::FrameColorLabelIndexRole); if (colorLabel.isValid()) { color = labelColors.at(colorLabel.toInt()); } else { color = noLabelSetColor; } if (!isEditable) { color = KritaUtils::blendColors(baseColor, color, 0.5); } if (doesFrameExist && hasContent) { color = baseColor; // special keyframe will be over filled in frame, so switch color so it is shown } QPen oldPen = painter->pen(); QBrush oldBrush(painter->brush()); painter->setPen(QPen(color, 0)); painter->setBrush(color); QPointF center = rc.center(); QPointF points[4] = { QPointF(center.x() + 4, center.y() ), QPointF(center.x() , center.y() - 4), QPointF(center.x() - 4, center.y() ), QPointF(center.x() , center.y() + 4) }; painter->drawConvexPolygon(points, 4); painter->setBrush(oldBrush); painter->setPen(oldPen); } -void TimelineFramesItemDelegate::drawBackground(QPainter *painter, const QModelIndex &index, const QRect &rc) const +void TimelineFramesItemDelegate::paintActiveInstanceIndicator(QPainter *painter, const QModelIndex &index, + const QRect &rc) const +{ + painter->save(); + + QColor lineColor = TimelineColorScheme::instance()->selectorColor(); + painter->setPen(QPen(lineColor, 0)); + painter->setBrush(lineColor); + painter->drawEllipse(rc.center(), 2,2); + + painter->restore(); +} + +void TimelineFramesItemDelegate::drawBackground(QPainter *painter, const QModelIndex &index, const QRect &rc, bool hasContentFrame) const { /// is the current layer actively selected (this is not the same as visibility) bool hasActiveLayerRole = index.data(TimelineFramesModel::ActiveLayerRole).toBool(); - bool doesFrameExist = index.data(TimelineFramesModel::FrameExistsRole).toBool(); /// does keyframe exist bool isEditable = index.data(TimelineFramesModel::FrameEditableRole).toBool(); /// is layer editable bool hasContent = index.data(TimelineFramesModel::FrameHasContent).toBool(); /// find out if frame is empty QColor color; // will get re-used for determining color QColor noLabelSetColor = qApp->palette().color(QPalette::Highlight); // if no color label is used QColor highlightColor = qApp->palette().color(QPalette::Highlight); QColor baseColor = qApp->palette().color(QPalette::Base); // pass for filling in the active row with slightly color difference if (hasActiveLayerRole) { color = KritaUtils::blendColors(baseColor, highlightColor, 0.8); painter->fillRect(rc, color); } // assign background color for frame depending on if the frame has a color label or not QVariant colorLabel = index.data(TimelineFramesModel::FrameColorLabelIndexRole); if (colorLabel.isValid()) { color = labelColors.at(colorLabel.toInt()); } else { color = noLabelSetColor; } // if layer is hidden, make the entire color more subtle. // this should be in effect for both fill color and empty outline color if (!isEditable) { color = KritaUtils::blendColors(baseColor, color, 0.7); } // how do we fill in a frame that has content // a keyframe will be totally filled in. A hold frame will have a line running through it - if (hasContent && doesFrameExist) { + if (hasContent && hasContentFrame) { painter->fillRect(rc, color); } // pass of outline for empty keyframes - if (doesFrameExist && !hasContent) { + if (hasContentFrame && !hasContent) { QPen oldPen = painter->pen(); QBrush oldBrush(painter->brush()); painter->setPen(QPen(color, 2)); painter->setBrush(Qt::NoBrush); painter->drawRect(rc); painter->setBrush(oldBrush); painter->setPen(oldPen); } // pass of hold frame line - if (!doesFrameExist && hasContent) { + int cycleMode = index.data(TimelineFramesModel::FrameCycleMode).toInt(); + + KisTimeSpan cycledRange; + if (timelineCycleRange->row() == index.row()) { + cycledRange = timelineCycleRange->range(); + + if (cycledRange.contains(index.column())) { + cycleMode = + (index.column() == cycledRange.start()) ? TimelineFramesModel::CycleMode::BeginsCycle : + (index.column() == cycledRange.end()) ? TimelineFramesModel::CycleMode::EndsCycle : + TimelineFramesModel::CycleMode::ContinuesCycle; + } + } + + if (cycleMode != TimelineFramesModel::NoCycle) { + const QColor bgColor = hasContentFrame && hasContent ? color : baseColor; + const QColor fgColor = hasContentFrame && hasContent ? baseColor : color; + drawCycleMarker(painter, rc, fgColor, bgColor, cycleMode); + + } else if (!hasContentFrame && hasContent) { // pretty much the same check as "isValid" above, but that isn't working with hold frames if (colorLabel.toInt() == 0) { color = noLabelSetColor; if (!isEditable) { color = KritaUtils::blendColors(baseColor, color, 0.7); } } QPoint lineStart(rc.x(), (rc.y() + rc.height()/2)); QPoint lineEnd(rc.x() + rc.width(), (rc.y() + rc.height()/2)); QPen holdFramePen(color); holdFramePen.setWidth(1); painter->setPen(holdFramePen); painter->drawLine(QLine(lineStart, lineEnd)); } +} + +void drawSemiOctagon(QPainter *painter, int closedEndX, int openEndX, int top, int bottom, int chamfer) { + // The "semi-circles" at the ends of cycles are actually drawn as semi-octagons + // due to Qt's inability to draw clean circles at small sizes. + const QPoint points[4] = { + QPoint(openEndX, top), + QPoint(closedEndX, top + chamfer), + QPoint(closedEndX, bottom - chamfer), + QPoint(openEndX, bottom) + }; + painter->drawPolyline(points, 4); +} +void TimelineFramesItemDelegate::drawCycleMarker(QPainter *painter, const QRect &rc, const QColor &fgColor, const QColor &bgColor, int cycleMode) const +{ + const bool isRepeat = ( + cycleMode == TimelineFramesModel::BeginsRepeat || + cycleMode == TimelineFramesModel::ContinuesRepeat || + cycleMode == TimelineFramesModel::EndsRepeat + ); + + const QColor repeatColor = isRepeat ? KritaUtils::blendColors(bgColor, fgColor, 0.7) : fgColor; + + QPen cyclePen(repeatColor); + cyclePen.setWidth(1); + painter->setPen(cyclePen); + + const int verticalMargin = rc.height() / 3; + const int horizontalMargin = 2; + const int upper = rc.y() + verticalMargin; + const int lower = rc.y() + rc.height() - verticalMargin; + const int chamfer = qMin((lower - upper) / 3, rc.width() - horizontalMargin); + + int left = rc.x(); + int right = rc.x() + rc.width(); + + if (cycleMode == TimelineFramesModel::BeginsCycle || cycleMode == TimelineFramesModel::BeginsRepeat) { + left += horizontalMargin; + drawSemiOctagon(painter, left, left + chamfer, upper, lower, chamfer); + left += chamfer; + } else if (cycleMode == TimelineFramesModel::EndsCycle || cycleMode == TimelineFramesModel::EndsRepeat) { + right -= horizontalMargin; + drawSemiOctagon(painter, right, right - chamfer, upper, lower, chamfer); + right -= horizontalMargin + chamfer; + } + painter->drawLine(QLine(QPoint(left, upper), QPoint(right, upper))); + painter->drawLine(QLine(QPoint(left, lower), QPoint(right, lower))); } void TimelineFramesItemDelegate::drawFocus(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect) const { // copied from Qt 4.8! if ((option.state & QStyle::State_HasFocus) == 0 || !rect.isValid()) return; QStyleOptionFocusRect o; o.QStyleOption::operator=(option); o.rect = rect; o.state |= QStyle::State_KeyboardFocusChange; o.state |= QStyle::State_Item; QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled; o.backgroundColor = option.palette.color(cg, (option.state & QStyle::State_Selected) ? QPalette::Highlight : QPalette::Window); const QWidget *widget = qobject_cast(parent()); QStyle *style = widget ? widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_FrameFocusRect, &o, painter, widget); } void TimelineFramesItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ +{ + bool hasContentFrame = index.data(TimelineFramesModel::FrameExistsRole).toBool(); + // draws background as well as fills normal keyframes - drawBackground(painter, index, option.rect); + drawBackground(painter, index, option.rect, hasContentFrame); // creates a semi transparent orange rectangle in the frame that is actively selected on the active row if (option.showDecorationSelected && (option.state & QStyle::State_Selected)) { QPalette::ColorGroup cg = option.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled; if (cg == QPalette::Normal && !(option.state & QStyle::State_Active)) cg = QPalette::Inactive; QBrush brush = TimelineColorScheme::instance()->selectionColor(); int oldOpacity = painter->opacity(); painter->setOpacity(0.5); painter->fillRect(option.rect, brush); painter->setOpacity(oldOpacity); } // not sure what this is drawing drawFocus(painter, option, option.rect); // opacity keyframe, but NOT normal keyframes bool specialKeys = index.data(TimelineFramesModel::SpecialKeyframeExists).toBool(); if (specialKeys) { paintSpecialKeyframeIndicator(painter, index, option.rect); } // creates a border and dot on the selected frame on the active row bool active = index.data(TimelineFramesModel::ActiveFrameRole).toBool(); bool layerIsCurrent = index.data(TimelineFramesModel::ActiveLayerRole).toBool(); if (active) { paintActiveFrameSelector(painter, option.rect, layerIsCurrent); } + + const bool isIdenticalWithActive = index.data(TimelineFramesModel::IdenticalWithActive).toBool(); + if (isIdenticalWithActive && hasContentFrame) { + paintActiveInstanceIndicator(painter, index, option.rect); + } } diff --git a/plugins/dockers/animation/timeline_frames_item_delegate.h b/plugins/dockers/animation/timeline_frames_item_delegate.h index 43bb3029d7..dd3f4cab1e 100644 --- a/plugins/dockers/animation/timeline_frames_item_delegate.h +++ b/plugins/dockers/animation/timeline_frames_item_delegate.h @@ -1,47 +1,52 @@ /* * 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_ITEM_DELEGATE_H #define __TIMELINE_FRAMES_ITEM_DELEGATE_H #include +class TimelineCycleRange; class TimelineFramesItemDelegate : public QItemDelegate { public: - TimelineFramesItemDelegate(QObject *parent); + TimelineFramesItemDelegate(QObject *parent, TimelineCycleRange *cycleRangeDelegate); ~TimelineFramesItemDelegate() override; static void paintActiveFrameSelector(QPainter *painter, const QRect &rc, bool isCurrentFrame); /// the opacity keyframe void paintSpecialKeyframeIndicator(QPainter *painter, const QModelIndex &index, const QRect &rc) const; + void paintActiveInstanceIndicator(QPainter *painter, const QModelIndex &index, const QRect &rc) const; - void drawBackground(QPainter *painter, const QModelIndex &index, const QRect &rc) const; + void drawBackground(QPainter *painter, const QModelIndex &index, const QRect &rc, bool hasContentFrame) const; void drawFocus(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect) const override; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; private: QVector labelColors; + TimelineCycleRange *timelineCycleRange; + + void drawCycleMarker(QPainter *painter, const QRect &rc, const QColor &fgColor, const QColor &bgColor, int cycleMode) const; }; #endif /* __TIMELINE_FRAMES_ITEM_DELEGATE_H */ diff --git a/plugins/dockers/animation/timeline_frames_model.cpp b/plugins/dockers/animation/timeline_frames_model.cpp index 92007fabdc..9a22885980 100644 --- a/plugins/dockers/animation/timeline_frames_model.cpp +++ b/plugins/dockers/animation/timeline_frames_model.cpp @@ -1,1023 +1,1117 @@ /* * 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 "kis_keyframe_commands.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_animation_cycle.h" #include "kis_node_view_color_scheme.h" #include "krita_utils.h" #include "KisPart.h" #include #include "KisDocument.h" #include "KisViewManager.h" #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 = getChannel(row, KisKeyframeChannel::Content.id()); - KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); - return (primaryChannel && primaryChannel->keyframeAt(column)); + return (primaryChannel && primaryChannel->itemAt(column)); } bool frameHasContent(int row, int column) { - - KisNodeDummy *dummy = converter->dummyFromRow(row); - - KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); + KisKeyframeChannel *primaryChannel = getChannel(row, KisKeyframeChannel::Content.id()); if (!primaryChannel) return false; // first check if we are a key frame - KisKeyframeSP frame = primaryChannel->activeKeyframeAt(column); + KisKeyframeBaseSP frame = primaryChannel->activeItemAt(column); if (!frame) return false; - return frame->hasContent(); + const KisKeyframe *keyframe = dynamic_cast(frame.data()); + return keyframe && keyframe->hasContent(); } 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) { + bool isIdenticalWith(int row, int column, int frame) { KisNodeDummy *dummy = converter->dummyFromRow(row); - if (!dummy) return -1; + if (!dummy) return false; + Q_FOREACH(KisKeyframeChannel *channel, dummy->node()->keyframeChannels()) { + if (channel->areFramesIdentical(column, frame)) { + return true; + } + } + return false; + } - KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); + int frameColorLabel(int row, int column) { + KisKeyframeChannel *primaryChannel = getChannel(row, KisKeyframeChannel::Content.id()); if (!primaryChannel) return -1; - KisKeyframeSP frame = primaryChannel->activeKeyframeAt(column); + KisKeyframeSP frame = primaryChannel->visibleKeyframeAt(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()); + KisKeyframeChannel *primaryChannel = getChannel(row, KisKeyframeChannel::Content.id()); if (!primaryChannel) return; - KisKeyframeSP frame = primaryChannel->keyframeAt(column); + KisKeyframeSP frame = primaryChannel->visibleKeyframeAt(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(); } + CycleMode frameCycleMode(int row, int time, int activeFrame) const{ + KisKeyframeChannel *primaryChannel = getChannel(row, KisKeyframeChannel::Content.id()); + if (!primaryChannel) return NoCycle; + + const QSharedPointer repeat = primaryChannel->activeRepeatAt(time); + + if (repeat) { + const KisTimeSpan range = repeat->sourceRange(); + const int originalTime = repeat->getOriginalTimeFor(time); + if (originalTime == range.start()) return BeginsRepeat; + if (originalTime == range.end()) return EndsRepeat; + return ContinuesRepeat; + } + + const QSharedPointer playheadRepeat = primaryChannel->activeItemAt( + activeFrame).dynamicCast(); + if (playheadRepeat) { + const KisTimeSpan range = repeat->sourceRange(); + if (time == range.start()) return BeginsCycle; + if (time == range.end()) return EndsCycle; + return ContinuesCycle; + } + + return NoCycle; + }; + + KisTimeSpan cycledRange(int row, int time) const { + KisKeyframeChannel *primaryChannel = getChannel(row, KisKeyframeChannel::Content.id()); + if (!primaryChannel) return KisTimeSpan(); + + return primaryChannel->cycledRangeAt(time); + } + + void setCycleRange(int row, int time, KisTimeSpan newRange) { + KisKeyframeChannel *channel = getChannel(row, KisKeyframeChannel::Content.id()); + if (!channel) return; + + const QSharedPointer repeatFrame = channel->activeRepeatAt(time); + if (!repeatFrame) return; + + QSharedPointer newRepeat = toQShared(new KisRepeatFrame(*repeatFrame, newRange)); + KUndo2Command *cmd = new KisReplaceKeyframeCommand(channel, repeatFrame->time(), newRepeat, nullptr); + KisProcessingApplicator::runSingleCommandStroke(image, cmd, KisStrokeJobData::BARRIER); + } + 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; nodeInterface->setNodeProperties(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; } + + KisKeyframeChannel *getChannel(int row, const QString &channelId) const { + KisNodeDummy *dummy = converter->dummyFromRow(row); + if (!dummy) return nullptr; + return dummy->node()->getKeyframeChannel(channelId); + } + }; 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())); connect(m_d->image, SIGNAL(sigImageModified()), SLOT(slotImageContentChanged())); } 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::slotImageContentChanged() { if (m_d->activeLayerIndex < 0) return; KisNodeDummy *dummy = m_d->converter->dummyFromRow(m_d->activeLayerIndex); if (!dummy) return; slotDummyChanged(dummy); } void TimelineFramesModel::processUpdateQueue() { if (!m_d->converter) return; 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 FrameHasContent: { return m_d->frameHasContent(index.row(), index.column()); } case FrameExistsRole: { return m_d->frameExists(index.row(), index.column()); } case SpecialKeyframeExists: { return m_d->specialKeyframeExists(index.row(), index.column()); } + case IdenticalWithActive: { + return m_d->isIdenticalWith(index.row(), index.column(), activeFrameIndex()); + } case FrameColorLabelIndexRole: { int label = m_d->frameColorLabel(index.row(), index.column()); return label > 0 ? label : QVariant(); } + case FrameCycleMode: { + return m_d->frameCycleMode(index.row(), index.column(), activeFrameIndex()); + } + case CycledRange: { + const KisTimeSpan &range = m_d->cycledRange(index.row(), index.column()); + return qVariantFromValue(range); + } 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()); + } + case CycledRange: { + m_d->setCycleRange(index.row(), index.column(), value.value()); } 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()->isDroppedNode()) { 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; + return Qt::MoveAction | Qt::CopyAction | Qt::LinkAction; } Qt::DropActions TimelineFramesModel::supportedDropActions() const { - return Qt::MoveAction | Qt::CopyAction; + return Qt::MoveAction | Qt::CopyAction | Qt::LinkAction; } 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(); const QByteArray uuidDataRoot = m_d->image->root()->uuid().toRfc4122(); stream << int(uuidDataRoot.size()); stream.writeRawData(uuidDataRoot.data(), uuidDataRoot.size()); 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) || + if ((action != Qt::MoveAction && action != Qt::CopyAction && action != Qt::LinkAction) || !parent.isValid()) return result; QByteArray encoded = data->data("application/x-krita-frame"); QDataStream stream(&encoded, QIODevice::ReadOnly); int uuidLenRoot = 0; stream >> uuidLenRoot; QByteArray uuidDataRoot(uuidLenRoot, '\0'); stream.readRawData(uuidDataRoot.data(), uuidLenRoot); QUuid nodeUuidRoot = QUuid::fromRfc4122(uuidDataRoot); KisPart *partInstance = KisPart::instance(); QList> documents = partInstance->documents(); KisImageSP srcImage = 0; Q_FOREACH(KisDocument *doc, documents) { KisImageSP tmpSrcImage = doc->image(); if (tmpSrcImage->root()->uuid() == nodeUuidRoot) { srcImage = tmpSrcImage; break; } } if (!srcImage) { KisPart *kisPartInstance = KisPart::instance(); kisPartInstance->currentMainwindow()->viewManager()->showFloatingMessage( i18n("Dropped frames are not available in this Krita instance") , QIcon()); return false; } 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(srcImage->root()); } else { QModelIndex index = this->index(srcRow, srcColumn); srcNode = nodeAt(index); } KIS_SAFE_ASSERT_RECOVER(srcNode) { continue; } const QModelIndex dstRowIndex = this->index(srcRow + offset.y(), 0); if (!dstRowIndex.isValid()) continue; KisNodeSP dstNode = nodeAt(dstRowIndex); 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(), srcColumn + offset.x()); 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; + KisAnimationUtils::KeyframeAction frameAction; + if (action == Qt::LinkAction) { + frameAction = KisAnimationUtils::LinkKeyframes; + } else { + frameAction = copyFrames ? KisAnimationUtils::CopyKeyframes : KisAnimationUtils::MoveKeyframes; + } + if (!frameMoves.isEmpty()) { KisImageBarrierLockerWithFeedback locker(m_d->image); - cmd = KisAnimationUtils::createMoveKeyframesCommand(frameMoves, copyFrames, false, 0); + cmd = KisAnimationUtils::createMoveKeyframesCommand(frameMoves, frameAction, true, 0); } if (cmd) { KisProcessingApplicator::runSingleCommandStroke(m_d->image, cmd, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); } 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, int timing) { if (dstRows.isEmpty() || count <= 0) return true; timing = qMax(timing, 1); 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 * timing) - 1); createOffsetFramesCommand(indexes, QPoint((count * timing), 0), false, 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 * timing); column += timing) { 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 * timing) - 1; new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(), oldTime, newTime, parentCommand); } KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); 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; + QSet uniqueKeyframesInSelection; int minSelectedTime = std::numeric_limits::max(); 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; minSelectedTime = qMin(minSelectedTime, index.column()); - KisKeyframeSP keyFrame = channel->activeKeyframeAt(index.column()); + KisKeyframeBaseSP keyFrame = channel->activeItemAt(index.column()); if (keyFrame) { uniqueKeyframesInSelection.insert(keyFrame); } } - QList keyframesToMove; + QList keyframesToMove; for (auto it = uniqueKeyframesInSelection.begin(); it != uniqueKeyframesInSelection.end(); ++it) { - KisKeyframeSP keyframe = *it; + KisKeyframeBaseSP keyframe = *it; KisKeyframeChannel *channel = keyframe->channel(); - KisKeyframeSP nextKeyframe = channel->nextKeyframe(keyframe); + KisKeyframeBaseSP nextKeyframe = channel->nextItem(*keyframe); if (nextKeyframe) { keyframesToMove << nextKeyframe; } } std::sort(keyframesToMove.begin(), keyframesToMove.end(), - [] (KisKeyframeSP lhs, KisKeyframeSP rhs) { + [] (KisKeyframeBaseSP lhs, KisKeyframeBaseSP 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) { + Q_FOREACH (KisKeyframeBaseSP keyframe, keyframesToMove) { int plannedFrameMove = count; if (count < 0) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(keyframe->time() > 0, false); - KisKeyframeSP prevFrame = keyframe->channel()->previousKeyframe(keyframe); + KisKeyframeBaseSP prevFrame = keyframe->channel()->previousItem(*keyframe); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(prevFrame, false); plannedFrameMove = qMax(count, prevFrame->time() - keyframe->time() + 1); minSelectedTime = qMin(minSelectedTime, prevFrame->time()); } 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, true, 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, KisStrokeJobData::EXCLUSIVE); return true; } +bool TimelineFramesModel::defineCycles(int time, KisTimeSpan sourceRange, QSet rows) +{ + KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Create animation cycle", "Create animation cycles", rows.size())); + + Q_FOREACH(const int row, rows) { + KisNodeDummy *dummy = m_d->converter->dummyFromRow(row); + if (dummy) { + KisKeyframeChannel *contentChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); + if (contentChannel) { + contentChannel->createRepeat(time, sourceRange, parentCommand); + } + } + } + + KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, 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 8f8d5edabe..4707eddd54 100644 --- a/plugins/dockers/animation/timeline_frames_model.h +++ b/plugins/dockers/animation/timeline_frames_model.h @@ -1,154 +1,169 @@ /* * 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 KisRepeatFrame; class KRITAANIMATIONDOCKER_EXPORT TimelineFramesModel : public TimelineNodeListKeeper::ModelWithExternalNotifications { Q_OBJECT public: enum MimeCopyPolicy { UndefinedPolicy = 0, MoveFramesPolicy, CopyFramesPolicy }; + enum CycleMode { + NoCycle = 0, + BeginsCycle, + ContinuesCycle, + EndsCycle, + BeginsRepeat, + ContinuesRepeat, + EndsRepeat + }; + 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, int timing = 1); bool insertHoldFrames(QModelIndexList selectedIndexes, int count); + bool defineCycles(int time, KisTimeSpan sourceRange, QSet rows); + 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 + FrameColorLabelIndexRole, + FrameCycleMode, + CycledRange }; // 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; virtual bool setNodeProperties(KisNodeSP node, KisImageSP image, KisBaseNode::PropertyList properties) const = 0; }; /** * NOTE: the model has an ownership over the interface, that is it'll * be deleted automatically later */ void setNodeManipulationInterface(NodeManipulationInterface *iface); KisNodeSP nodeAt(QModelIndex index) const override; protected: QMap channelsAt(QModelIndex index) const override; private Q_SLOTS: void slotDummyChanged(KisNodeDummy *dummy); void slotImageContentChanged(); 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 382d1e8e09..64eb3b4b97 100644 --- a/plugins/dockers/animation/timeline_frames_view.cpp +++ b/plugins/dockers/animation/timeline_frames_view.cpp @@ -1,1547 +1,1700 @@ /* * 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 "timeline_insert_keyframe_dialog.h" #include "timeline_frames_item_delegate.h" #include #include #include #include #include #include #include #include #include #include #include "config-qtmultimedia.h" #include "KSharedConfig" #include "KisKineticScroller.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_keyframe_channel.h" #include "kis_slider_spin_box.h" #include #include #include #include #include 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) + selectionChangedCompressor(300, KisSignalCompressor::FIRST_INACTIVE), + timelineCycleRange(_q) {} 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; TimelineInsertKeyframeDialog *insertKeyframeDialog; KisZoomButton *zoomDragButton; bool dragInProgress; bool dragWasSuccessful; KisCustomModifiersCatcher *modifiersCatcher; QPoint lastPressedPosition; Qt::KeyboardModifiers lastPressedModifier; KisSignalCompressor selectionChangedCompressor; + TimelineCycleRange timelineCycleRange; + 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)); + setItemDelegate(new TimelineFramesItemDelegate(this, &m_d->timelineCycleRange)); setDragEnabled(true); setDragDropMode(QAbstractItemView::DragDrop); setAcceptDrops(true); setDropIndicatorShown(true); setDefaultDropAction(Qt::MoveAction); + setMouseTracking(true); + m_d->horizontalRuler = new TimelineRulerHeader(this); this->setHorizontalHeader(m_d->horizontalRuler); connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnLeft()), SLOT(slotInsertKeyframeColumnLeft())); connect(m_d->horizontalRuler, SIGNAL(sigInsertColumnRight()), SLOT(slotInsertKeyframeColumnRight())); connect(m_d->horizontalRuler, SIGNAL(sigInsertMultipleColumns()), SLOT(slotInsertMultipleKeyframeColumns())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveColumns()), SLOT(slotRemoveSelectedColumns())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveColumnsAndShift()), SLOT(slotRemoveSelectedColumnsAndShift())); connect(m_d->horizontalRuler, SIGNAL(sigInsertHoldColumns()), SLOT(slotInsertHoldFrameColumn())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveHoldColumns()), SLOT(slotRemoveHoldFrameColumn())); connect(m_d->horizontalRuler, SIGNAL(sigInsertHoldColumnsCustom()), SLOT(slotInsertMultipleHoldFrameColumns())); connect(m_d->horizontalRuler, SIGNAL(sigRemoveHoldColumnsCustom()), SLOT(slotRemoveMultipleHoldFrameColumns())); 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(QPoint)), SLOT(slotLayerContextMenuRequested(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 explicitly 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(i18n("%")); 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); /********** Insert Keyframes Dialog **************************************************/ m_d->insertKeyframeDialog = new TimelineInsertKeyframeDialog(this); /********** 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 explicitly 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); { QScroller *scroller = KisKineticScroller::createPreconfiguredScroller(this); if( scroller ) { connect(scroller, SIGNAL(stateChanged(QScroller::State)), this, SLOT(slotScrollerStateChanged(QScroller::State))); } } 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(slotAddBlankFrame())); action = m_d->actionMan->createAction("add_duplicate_frame"); connect(action, SIGNAL(triggered()), SLOT(slotAddDuplicateFrame())); action = m_d->actionMan->createAction("insert_keyframe_left"); connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframeLeft())); action = m_d->actionMan->createAction("insert_keyframe_right"); connect(action, SIGNAL(triggered()), SLOT(slotInsertKeyframeRight())); action = m_d->actionMan->createAction("insert_multiple_keyframes"); connect(action, SIGNAL(triggered()), SLOT(slotInsertMultipleKeyframes())); action = m_d->actionMan->createAction("remove_frames_and_pull"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveSelectedFramesAndShift())); action = m_d->actionMan->createAction("remove_frames"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveSelectedFrames())); action = m_d->actionMan->createAction("insert_hold_frame"); connect(action, SIGNAL(triggered()), SLOT(slotInsertHoldFrame())); action = m_d->actionMan->createAction("insert_multiple_hold_frames"); connect(action, SIGNAL(triggered()), SLOT(slotInsertMultipleHoldFrames())); action = m_d->actionMan->createAction("remove_hold_frame"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveHoldFrame())); action = m_d->actionMan->createAction("remove_multiple_hold_frames"); connect(action, SIGNAL(triggered()), SLOT(slotRemoveMultipleHoldFrames())); 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())); + + action = m_d->actionMan->createAction("create_animation_cycle"); + connect(action, SIGNAL(triggered()), SLOT(slotCreateCycle())); } } 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(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(QItemSelection,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(false).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::slotScrollerStateChanged( QScroller::State state ) { KisKineticScroller::updateCursor(this, state); } void TimelineFramesView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) { + m_d->timelineCycleRange.updateRange(); + 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; + KisTimeSpan range; if (maxColumn > minColumn) { - range = KisTimeRange(minColumn, maxColumn - minColumn + 1); + range = KisTimeSpan(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); - if (event->keyboardModifiers() & Qt::ControlModifier) { + const bool control = event->keyboardModifiers() & Qt::ControlModifier; + const bool shift = event->keyboardModifiers() & Qt::ShiftModifier; + if (control && shift) { + event->setDropAction(Qt::LinkAction); + } else if (control) { event->setDropAction(Qt::CopyAction); } 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); + KisActionManager::safePopulateMenu(menu, "create_animation_cycle", 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(); { //Frames submenu. QMenu *frames = menu->addMenu(i18nc("@item:inmenu", "Keyframes")); KisActionManager::safePopulateMenu(frames, "insert_keyframe_left", m_d->actionMan); KisActionManager::safePopulateMenu(frames, "insert_keyframe_right", m_d->actionMan); frames->addSeparator(); KisActionManager::safePopulateMenu(frames, "insert_multiple_keyframes", m_d->actionMan); } { //Holds submenu. 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_multiple_hold_frames", m_d->actionMan); KisActionManager::safePopulateMenu(hold, "remove_multiple_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) { + if (m_d->timelineCycleRange.isAdjusting()) { + m_d->timelineCycleRange.cancelAdjustment(); + return; + } 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()) { + 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); const int labelIndex = KisImageConfig(true).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 (m_d->timelineCycleRange.beginAdjustment(event->pos())) { + return; } 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); + m_d->tip.showTip(this, e->pos() + QPoint(verticalHeader()->width(), horizontalHeader()->height()), option, + index); } e->accept(); - } else { + } else if (m_d->timelineCycleRange.isAdjusting()) { + m_d->timelineCycleRange.continueAdjustment(e->pos()); + } else if (e->buttons() == Qt::LeftButton) { m_d->model->setScrubState(true); QTableView::mouseMoveEvent(e); + } else { + TimelineCycleRange::Handle cycleAdjustmentHandle = m_d->timelineCycleRange.handleAt(e->pos()); + if (cycleAdjustmentHandle != TimelineCycleRange::Handle::None) { + setCursor(Qt::SizeHorCursor); + } else { + setCursor(Qt::ArrowCursor); + } } } void TimelineFramesView::mouseReleaseEvent(QMouseEvent *e) { - if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) { + if (m_d->timelineCycleRange.isAdjusting()) { + const QVariant range = qVariantFromValue(m_d->timelineCycleRange.range()); + m_d->model->setData(currentIndex(), range, TimelineFramesModel::CycledRange); + m_d->timelineCycleRange.acceptAdjustment(); + } else 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_keyframe_left", hasEditableFrames); enableAction("insert_keyframe_right", hasEditableFrames); enableAction("insert_multiple_keyframes", hasEditableFrames); enableAction("remove_frames", hasEditableFrames && hasExistingFrames); enableAction("remove_frames_and_pull", hasEditableFrames); enableAction("insert_hold_frame", hasEditableFrames); enableAction("insert_multiple_hold_frames", hasEditableFrames); enableAction("remove_hold_frame", hasEditableFrames); enableAction("remove_multiple_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::slotAddBlankFrame() { QModelIndex index = currentIndex(); if (!index.isValid() || !m_d->model->data(index, TimelineFramesModel::FrameEditableRole).toBool()) { return; } m_d->model->createFrame(index); } void TimelineFramesView::slotAddDuplicateFrame() { 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::insertKeyframes(int count, int timing, TimelineDirection direction, bool entireColumn) { QSet rows; int minColumn = 0, maxColumn = 0; calculateSelectionMetrics(minColumn, maxColumn, rows); if (count <= 0) { //Negative count? Use number of selected frames. count = qMax(1, maxColumn - minColumn + 1); } const int insertionColumn = direction == TimelineDirection::RIGHT ? maxColumn + 1 : minColumn; if (entireColumn) { rows.clear(); for (int i = 0; i < m_d->model->rowCount(); i++) { if (!m_d->model->data(m_d->model->index(i, insertionColumn), TimelineFramesModel::FrameEditableRole).toBool()) continue; rows.insert(i); } } if (!rows.isEmpty()) { m_d->model->insertFrames(insertionColumn, rows.toList(), count, timing); } } void TimelineFramesView::insertMultipleKeyframes(bool entireColumn) { int count, timing; TimelineDirection direction; if (m_d->insertKeyframeDialog->promptUserSettings(count, timing, direction)) { insertKeyframes(count, timing, direction, entireColumn); } } QModelIndexList TimelineFramesView::calculateSelectionSpan(bool entireColumn, bool editableOnly) const { QModelIndexList indexes; if (entireColumn) { 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::slotRemoveSelectedFrames(bool entireColumn, bool pull) { const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn); if (!selectedIndices.isEmpty()) { if (pull) { m_d->model->removeFramesAndOffset(selectedIndices); } else { m_d->model->removeFrames(selectedIndices); } } } void TimelineFramesView::insertOrRemoveHoldFrames(int count, bool entireColumn) { QModelIndexList indexes; if (!entireColumn) { 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()) { // add extra columns to the end of the timeline if we are adding hold frames // they will be truncated if we don't do this if (count > 0) { // Scan all the layers and find out what layer has the most keyframes // only keep a reference of layer that has the most keyframes int keyframesInLayerNode = 0; Q_FOREACH (const QModelIndex &index, indexes) { KisNodeSP layerNode = m_d->model->nodeAt(index); KisKeyframeChannel *channel = layerNode->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!channel) continue; if (keyframesInLayerNode < channel->allKeyframeIds().count()) { keyframesInLayerNode = channel->allKeyframeIds().count(); } } m_d->model->setLastVisibleFrame(m_d->model->columnCount() + count*keyframesInLayerNode); } m_d->model->insertHoldFrames(indexes, count); // bulk adding frames can add too many // trim timeline to clean up extra frames that might have been added slotUpdateInfiniteFramesCount(); } } void TimelineFramesView::insertOrRemoveMultipleHoldFrames(bool insertion, bool entireColumn) { bool ok = false; const int count = QInputDialog::getInt(this, i18nc("@title:window", "Insert or Remove Hold Frames"), i18nc("@label:spinbox", "Enter number of frames"), insertion ? m_d->insertKeyframeDialog->defaultTimingOfAddedFrames() : m_d->insertKeyframeDialog->defaultNumberOfHoldFramesToRemove(), 1, 10000, 1, &ok); if (ok) { if (insertion) { m_d->insertKeyframeDialog->setDefaultTimingOfAddedFrames(count); insertOrRemoveHoldFrames(count, entireColumn); } else { m_d->insertKeyframeDialog->setDefaultNumberOfHoldFramesToRemove(count); insertOrRemoveHoldFrames(-count, entireColumn); } } } void TimelineFramesView::slotMirrorFrames(bool entireColumn) { const QModelIndexList indexes = calculateSelectionSpan(entireColumn); if (!indexes.isEmpty()) { m_d->model->mirrorFrames(indexes); } } +void TimelineFramesView::slotCreateCycle() +{ + QSet rows; + int minColumn = 0, maxColumn = 0; + + calculateSelectionMetrics(minColumn, maxColumn, rows); + + m_d->model->defineCycles(currentIndex().column() + 1, {minColumn, maxColumn}, rows); +} + void TimelineFramesView::cutCopyImpl(bool entireColumn, bool copy) { const QModelIndexList selectedIndices = calculateSelectionSpan(entireColumn, !copy); if (selectedIndices.isEmpty()) return; int minColumn = std::numeric_limits::max(); int minRow = std::numeric_limits::max(); Q_FOREACH (const QModelIndex &index, selectedIndices) { 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(selectedIndices, baseIndex, copy ? TimelineFramesModel::CopyFramesPolicy : TimelineFramesModel::MoveFramesPolicy); if (data) { QClipboard *cb = QApplication::clipboard(); cb->setMimeData(data); } } void TimelineFramesView::slotPasteFrames(bool entireColumn) { const QModelIndex currentIndex = !entireColumn ? 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(); } } } 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); } + +TimelineCycleRange::TimelineCycleRange(TimelineFramesView *view) + : m_view(view) +{} + +KisTimeSpan TimelineCycleRange::range() const +{ + return m_currentRange; +} + +int TimelineCycleRange::row() const +{ + return m_view->currentIndex().row(); +} + +bool TimelineCycleRange::isAdjusting() const +{ + return (m_activeHandle != Handle::None); +} + +TimelineCycleRange::Handle TimelineCycleRange::handleAt(QPoint mousePosition) const +{ + Handle handle = Handle::None; + + QModelIndex index = m_view->indexAt(mousePosition); + if (index.isValid() && index.row() == row()) { + const int time = index.column(); + const QRect frameRect = m_view->visualRect(index); + const bool onLeft = mousePosition.x() < frameRect.left() + frameRect.width() / 3; + const bool onRight = mousePosition.x() > frameRect.right() - frameRect.width() / 3; + + if (time == m_currentRange.start() && onLeft) { + handle = Handle::RangeStart; + } else if (time == m_currentRange.end() && onRight) { + handle = Handle::RangeEnd; + } + } + + return handle; +} + +void TimelineCycleRange::updateRange() +{ + const KisTimeSpan oldRange = m_currentRange; + + const QModelIndex currentIndex = m_view->currentIndex(); + if (!currentIndex.isValid()) { + m_currentRange = KisTimeSpan(); + } else { + m_currentRange = currentIndex.data(TimelineFramesModel::CycledRange).value(); + } + + if (m_currentRange != oldRange) { + updateView(oldRange | m_currentRange); + } +} + +bool TimelineCycleRange::beginAdjustment(QPoint mousePosition) +{ + m_activeHandle = handleAt(mousePosition); + + return isAdjusting(); +} + +void TimelineCycleRange::continueAdjustment(QPoint mousePosition) +{ + const KisTimeSpan oldRange = m_currentRange; + + const int frameWidth = m_view->columnWidth(0); + const int newTime = m_view->columnAt(mousePosition.x() - frameWidth / 2); + + switch (m_activeHandle) { + case Handle::None: + break; + + case Handle::RangeStart: + m_currentRange = m_currentRange.startMoved(newTime); + break; + + case Handle::RangeEnd: + m_currentRange = m_currentRange.endMoved(newTime); + break; + + } + + updateView(oldRange | m_currentRange); +} + +void TimelineCycleRange::cancelAdjustment() +{ + m_activeHandle = Handle::None; + + m_currentRange = m_view->currentIndex().data(TimelineFramesModel::CycledRange).value(); +} + +void TimelineCycleRange::acceptAdjustment() +{ + m_activeHandle = Handle::None; +} + +void TimelineCycleRange::updateView(KisTimeSpan changedRange) +{ + const QAbstractItemModel *model = m_view->model(); + const QModelIndex firstIndex = model->index(row(), changedRange.start(), QModelIndex()); + const QModelIndex lastIndex = model->index(row(), changedRange.end(), QModelIndex()); + const QRect rect = m_view->visualRect(firstIndex) | m_view->visualRect(lastIndex); + m_view->viewport()->update(rect); +} diff --git a/plugins/dockers/animation/timeline_frames_view.h b/plugins/dockers/animation/timeline_frames_view.h index 214ed11978..0b3e7351a1 100644 --- a/plugins/dockers/animation/timeline_frames_view.h +++ b/plugins/dockers/animation/timeline_frames_view.h @@ -1,190 +1,223 @@ /* * 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 +#include +#include #include "kis_action_manager.h" #include "kritaanimationdocker_export.h" class KisAction; class TimelineWidget; enum TimelineDirection : short { LEFT = -1, BEFORE = -1, RIGHT = 1, AFTER = 1 }; 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(); // Layer void slotAddNewLayer(); void slotAddExistingLayer(QAction *action); void slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void slotRemoveLayer(); void slotLayerContextMenuRequested(const QPoint &globalPos); // New, Insert and Remove Frames void slotAddBlankFrame(); void slotAddDuplicateFrame(); void slotInsertKeyframeLeft() {insertKeyframes(-1, 1, TimelineDirection::LEFT, false);} void slotInsertKeyframeRight() {insertKeyframes(-1, 1, TimelineDirection::RIGHT, false);} void slotInsertKeyframeColumnLeft() {insertKeyframes(-1, 1, TimelineDirection::LEFT, true);} void slotInsertKeyframeColumnRight() {insertKeyframes(-1, 1, TimelineDirection::RIGHT, true);} void slotInsertMultipleKeyframes() {insertMultipleKeyframes(false);} void slotInsertMultipleKeyframeColumns() {insertMultipleKeyframes(true);} void slotRemoveSelectedFrames(bool entireColumn = false, bool pull = false); void slotRemoveSelectedFramesAndShift() {slotRemoveSelectedFrames(false, true);} void slotRemoveSelectedColumns() {slotRemoveSelectedFrames(true);} void slotRemoveSelectedColumnsAndShift() {slotRemoveSelectedFrames(true, true);} void slotInsertHoldFrame() {insertOrRemoveHoldFrames(1);} void slotRemoveHoldFrame() {insertOrRemoveHoldFrames(-1);} void slotInsertHoldFrameColumn() {insertOrRemoveHoldFrames(1,true);} void slotRemoveHoldFrameColumn() {insertOrRemoveHoldFrames(-1,true);} void slotInsertMultipleHoldFrames() {insertOrRemoveMultipleHoldFrames(true);} void slotRemoveMultipleHoldFrames() {insertOrRemoveMultipleHoldFrames(false);} void slotInsertMultipleHoldFrameColumns() {insertOrRemoveMultipleHoldFrames(true, true);} void slotRemoveMultipleHoldFrameColumns() {insertOrRemoveMultipleHoldFrames(false, true);} void slotMirrorFrames(bool entireColumn = false); void slotMirrorColumns() {slotMirrorFrames(true);} + void slotCreateCycle(); + // Copy-paste void slotCopyFrames() {cutCopyImpl(false, true);} void slotCutFrames() {cutCopyImpl(false, false);} void slotCopyColumns() {cutCopyImpl(true, true);} void slotCutColumns() {cutCopyImpl(true, false);} void slotPasteFrames(bool entireColumn = false); void slotPasteColumns() {slotPasteFrames(true);} 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); // Audio void slotSelectAudioChannelFile(); void slotAudioChannelMute(bool value); void slotAudioChannelRemove(); void slotUpdateAudioActions(); void slotAudioVolumeChanged(int value); // DragScroll void slotScrollerStateChanged(QScroller::State state); private: void setFramesPerSecond(int fps); void calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet &rows) const; /* Insert new keyframes/columns. * * count - Number of frames to add. If <0, use number of currently SELECTED frames. * timing - Animation timing of frames to be added (on 1s, 2s, 3s, etc.) * direction - Insert frames before (left) or after (right) selection scrubber. * entireColumn - Create frames on all layers (rows) instead of just the active layer? */ void insertKeyframes(int count = 1, int timing = 1, TimelineDirection direction = TimelineDirection::LEFT, bool entireColumn = false); void insertMultipleKeyframes(bool entireColumn = false); void insertOrRemoveHoldFrames(int count, bool entireColumn = false); void insertOrRemoveMultipleHoldFrames(bool insertion, bool entireColumn = false); void cutCopyImpl(bool entireColumn, bool copy); void createFrameEditingMenuActions(QMenu *menu, bool addFrameCreationActions); QModelIndexList calculateSelectionSpan(bool entireColumn, bool editableOnly = true) 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; }; +class TimelineCycleRange +{ +public: + enum class Handle { None, RangeStart, RangeEnd }; + + explicit TimelineCycleRange(TimelineFramesView *view); + + KisTimeSpan range() const; + int row() const; + bool isAdjusting() const; + Handle handleAt(QPoint mousePosition) const; + + void updateRange(); + + bool beginAdjustment(QPoint mousePosition); + void continueAdjustment(QPoint mousePosition); + void cancelAdjustment(); + void acceptAdjustment(); + +private: + TimelineFramesView *m_view; + + KisTimeSpan m_currentRange; + + Handle m_activeHandle{Handle::None}; + + void updateView(KisTimeSpan changedRange); +}; + #endif /* __TIMELINE_FRAMES_VIEW_H */ diff --git a/plugins/dockers/animation/timeline_node_list_keeper.cpp b/plugins/dockers/animation/timeline_node_list_keeper.cpp index 0ea8d90e9f..cc394a3dc6 100644 --- a/plugins/dockers/animation/timeline_node_list_keeper.cpp +++ b/plugins/dockers/animation/timeline_node_list_keeper.cpp @@ -1,249 +1,249 @@ /* * 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_node_list_keeper.h" #include "kis_node_dummies_graph.h" #include "kis_dummies_facade_base.h" #include "timeline_frames_index_converter.h" #include #include #include "kis_keyframe_channel.h" struct TimelineNodeListKeeper::Private { Private(TimelineNodeListKeeper *_q, ModelWithExternalNotifications *_model, KisDummiesFacadeBase *_dummiesFacade) : q(_q), model(_model), dummiesFacade(_dummiesFacade), converter(dummiesFacade) { } TimelineNodeListKeeper *q; ModelWithExternalNotifications *model; KisDummiesFacadeBase *dummiesFacade; TimelineFramesIndexConverter converter; QVector dummiesList; KisSignalMapper dummiesUpdateMapper; QSet connectionsSet; void populateDummiesList() { const int rowCount = converter.rowCount(); for (int i = 0; i < rowCount; ++i) { KisNodeDummy *dummy = converter.dummyFromRow(i); dummiesList.append(dummy); tryConnectDummy(dummy); } } void tryConnectDummy(KisNodeDummy *dummy); void disconnectDummy(KisNodeDummy *dummy); }; TimelineNodeListKeeper::TimelineNodeListKeeper(ModelWithExternalNotifications *model, KisDummiesFacadeBase *dummiesFacade) : m_d(new Private(this, model, dummiesFacade)) { KIS_ASSERT_RECOVER_RETURN(m_d->dummiesFacade); connect(m_d->dummiesFacade, SIGNAL(sigEndInsertDummy(KisNodeDummy*)), SLOT(slotEndInsertDummy(KisNodeDummy*))); connect(m_d->dummiesFacade, SIGNAL(sigBeginRemoveDummy(KisNodeDummy*)), SLOT(slotBeginRemoveDummy(KisNodeDummy*))); connect(m_d->dummiesFacade, SIGNAL(sigDummyChanged(KisNodeDummy*)), SLOT(slotDummyChanged(KisNodeDummy*))); m_d->populateDummiesList(); connect(&m_d->dummiesUpdateMapper, SIGNAL(mapped(QObject*)), SLOT(slotUpdateDummyContent(QObject*))); } TimelineNodeListKeeper::~TimelineNodeListKeeper() { } KisNodeDummy* TimelineNodeListKeeper::dummyFromRow(int row) { if (row >= 0 && row < m_d->dummiesList.size()) { return m_d->dummiesList[row]; } return 0; } int TimelineNodeListKeeper::rowForDummy(KisNodeDummy *dummy) { return m_d->dummiesList.indexOf(dummy); } int TimelineNodeListKeeper::rowCount() { return m_d->dummiesList.size(); } void TimelineNodeListKeeper::updateActiveDummy(KisNodeDummy *dummy) { bool oldRemoved = false; bool newAdded = false; KisNodeDummy *oldActiveDummy = m_d->converter.activeDummy(); m_d->converter.updateActiveDummy(dummy, &oldRemoved, &newAdded); if (oldRemoved) { slotBeginRemoveDummy(oldActiveDummy); } if (newAdded) { slotEndInsertDummy(dummy); } } void TimelineNodeListKeeper::slotUpdateDummyContent(QObject *_dummy) { KisNodeDummy *dummy = qobject_cast(_dummy); int pos = m_d->converter.rowForDummy(dummy); if (pos < 0) return; QModelIndex index0 = m_d->model->index(pos, 0); QModelIndex index1 = m_d->model->index(pos, m_d->model->columnCount() - 1); m_d->model->callIndexChanged(index0, index1); } void TimelineNodeListKeeper::Private::tryConnectDummy(KisNodeDummy *dummy) { QMap channels = dummy->node()->keyframeChannels(); if (channels.isEmpty()) { if (connectionsSet.contains(dummy)) { connectionsSet.remove(dummy); } return; } if (connectionsSet.contains(dummy)) return; Q_FOREACH(KisKeyframeChannel *channel, channels) { - connect(channel, SIGNAL(sigKeyframeAdded(KisKeyframeSP)), + connect(channel, SIGNAL(sigKeyframeAdded(KisKeyframeBaseSP)), &dummiesUpdateMapper, SLOT(map())); - connect(channel, SIGNAL(sigKeyframeAboutToBeRemoved(KisKeyframeSP)), + connect(channel, SIGNAL(sigKeyframeAboutToBeRemoved(KisKeyframeBaseSP)), &dummiesUpdateMapper, SLOT(map())); - connect(channel, SIGNAL(sigKeyframeMoved(KisKeyframeSP,int)), + connect(channel, SIGNAL(sigKeyframeMoved(KisKeyframeBaseSP,int)), &dummiesUpdateMapper, SLOT(map())); dummiesUpdateMapper.setMapping(channel, (QObject*)dummy); } connectionsSet.insert(dummy); } void TimelineNodeListKeeper::Private::disconnectDummy(KisNodeDummy *dummy) { if (!connectionsSet.contains(dummy)) return; QMap channels = dummy->node()->keyframeChannels(); if (channels.isEmpty()) { if (connectionsSet.contains(dummy)) { connectionsSet.remove(dummy); } return; } Q_FOREACH(KisKeyframeChannel *channel, channels) { channel->disconnect(&dummiesUpdateMapper); } connectionsSet.remove(dummy); } void TimelineNodeListKeeper::slotEndInsertDummy(KisNodeDummy *dummy) { KIS_ASSERT_RECOVER_RETURN(!m_d->dummiesList.contains(dummy)); if (m_d->converter.isDummyVisible(dummy)) { int pos = m_d->converter.rowForDummy(dummy); m_d->model->callBeginInsertRows(QModelIndex(), pos, pos); m_d->dummiesList.insert(pos, 1, dummy); m_d->tryConnectDummy(dummy); m_d->model->callEndInsertRows(); } } void TimelineNodeListKeeper::slotBeginRemoveDummy(KisNodeDummy *dummy) { if (m_d->dummiesList.contains(dummy)) { int pos = m_d->dummiesList.indexOf(dummy); m_d->model->callBeginRemoveRows(QModelIndex(), pos, pos); m_d->disconnectDummy(dummy); m_d->dummiesList.remove(pos); m_d->model->callEndRemoveRows(); } m_d->converter.notifyDummyRemoved(dummy); } void TimelineNodeListKeeper::slotDummyChanged(KisNodeDummy *dummy) { const bool present = m_d->dummiesList.contains(dummy); const bool shouldBe = m_d->converter.isDummyVisible(dummy); m_d->tryConnectDummy(dummy); if (!present && shouldBe) { slotEndInsertDummy(dummy); } else if (present && !shouldBe) { slotBeginRemoveDummy(dummy); } } void findOtherLayers(KisNodeDummy *root, TimelineNodeListKeeper::OtherLayersList *list, const QString &prefix) { KisNodeSP node = root->node(); if (root->parent() && !node->useInTimeline()) { *list << TimelineNodeListKeeper::OtherLayer( QString(prefix + node->name()), root); } KisNodeDummy *dummy = root->lastChild(); while(dummy) { findOtherLayers(dummy, list, prefix + " "); dummy = dummy->prevSibling(); } } TimelineNodeListKeeper::OtherLayersList TimelineNodeListKeeper::otherLayersList() const { OtherLayersList list; findOtherLayers(m_d->dummiesFacade->rootDummy(), &list, ""); return list; } diff --git a/plugins/extensions/animationrenderer/AnimationRenderer.cpp b/plugins/extensions/animationrenderer/AnimationRenderer.cpp index 718941a02e..00ca7c5714 100644 --- a/plugins/extensions/animationrenderer/AnimationRenderer.cpp +++ b/plugins/extensions/animationrenderer/AnimationRenderer.cpp @@ -1,206 +1,205 @@ /* * Copyright (c) 2016 Boudewijn Rempt * * 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 "AnimationRenderer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DlgAnimationRenderer.h" #include #include "video_saver.h" #include "KisAnimationRenderingOptions.h" K_PLUGIN_FACTORY_WITH_JSON(AnimaterionRendererFactory, "kritaanimationrenderer.json", registerPlugin();) AnimaterionRenderer::AnimaterionRenderer(QObject *parent, const QVariantList &) : KisActionPlugin(parent) { // Shows the big dialog KisAction *action = createAction("render_animation"); action->setActivationFlags(KisAction::IMAGE_HAS_ANIMATION); connect(action, SIGNAL(triggered()), this, SLOT(slotRenderAnimation())); // Re-renders the image sequence as defined in the last render action = createAction("render_animation_again"); action->setActivationFlags(KisAction::IMAGE_HAS_ANIMATION); connect(action, SIGNAL(triggered()), this, SLOT(slotRenderSequenceAgain())); } AnimaterionRenderer::~AnimaterionRenderer() { } void AnimaterionRenderer::slotRenderAnimation() { KisImageWSP image = viewManager()->image(); if (!image) return; if (!image->animationInterface()->hasAnimation()) return; KisDocument *doc = viewManager()->document(); DlgAnimationRenderer dlgAnimationRenderer(doc, viewManager()->mainWindow()); dlgAnimationRenderer.setCaption(i18n("Render Animation")); if (dlgAnimationRenderer.exec() == QDialog::Accepted) { KisAnimationRenderingOptions encoderOptions = dlgAnimationRenderer.getEncoderOptions(); renderAnimationImpl(doc, encoderOptions); } } void AnimaterionRenderer::slotRenderSequenceAgain() { KisImageWSP image = viewManager()->image(); if (!image) return; if (!image->animationInterface()->hasAnimation()) return; KisDocument *doc = viewManager()->document(); KisConfig cfg(true); KisPropertiesConfigurationSP settings = cfg.exportConfiguration("ANIMATION_EXPORT"); KisAnimationRenderingOptions encoderOptions; encoderOptions.fromProperties(settings); renderAnimationImpl(doc, encoderOptions); } void AnimaterionRenderer::renderAnimationImpl(KisDocument *doc, KisAnimationRenderingOptions encoderOptions) { const QString frameMimeType = encoderOptions.frameMimeType; const QString framesDirectory = encoderOptions.resolveAbsoluteFramesDirectory(); const QString extension = KisMimeDatabase::suffixesForMimeType(frameMimeType).first(); const QString baseFileName = QString("%1/%2.%3").arg(framesDirectory) .arg(encoderOptions.basename) .arg(extension); /** * The dialog should ensure that the size of the video is even */ KIS_SAFE_ASSERT_RECOVER( !((encoderOptions.width & 0x1 || encoderOptions.height & 0x1) && (encoderOptions.videoMimeType == "video/mp4" || encoderOptions.videoMimeType == "video/x-matroska"))) { encoderOptions.width = encoderOptions.width + (encoderOptions.width & 0x1); encoderOptions.height = encoderOptions.height + (encoderOptions.height & 0x1); } const QSize scaledSize = doc->image()->bounds().size().scaled( encoderOptions.width, encoderOptions.height, Qt::KeepAspectRatio); if ((scaledSize.width() & 0x1 || scaledSize.height() & 0x1) && (encoderOptions.videoMimeType == "video/mp4" || encoderOptions.videoMimeType == "video/x-matroska")) { QString m = "Mastroska (.mkv)"; if (encoderOptions.videoMimeType == "video/mp4") { m = "Mpeg4 (.mp4)"; } qWarning() << m <<"requires width and height to be even, resize and try again!"; doc->setErrorMessage(i18n("%1 requires width and height to be even numbers. Please resize or crop the image before exporting.", m)); QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not render animation:\n%1", doc->errorMessage())); return; } const bool batchMode = false; // TODO: fetch correctly! KisAsyncAnimationFramesSaveDialog exporter(doc->image(), - KisTimeRange::fromTime(encoderOptions.firstFrame, - encoderOptions.lastFrame), + KisTimeSpan(encoderOptions.firstFrame, encoderOptions.lastFrame), baseFileName, encoderOptions.sequenceStart, encoderOptions.frameExportConfig); exporter.setBatchMode(batchMode); KisAsyncAnimationFramesSaveDialog::Result result = exporter.regenerateRange(viewManager()->mainWindow()->viewManager()); // the folder could have been read-only or something else could happen if (encoderOptions.shouldEncodeVideo && result == KisAsyncAnimationFramesSaveDialog::RenderComplete) { const QString savedFilesMask = exporter.savedFilesMask(); const QString resultFile = encoderOptions.resolveAbsoluteVideoFilePath(); KIS_SAFE_ASSERT_RECOVER_NOOP(QFileInfo(resultFile).isAbsolute()) { const QFileInfo info(resultFile); QDir dir(info.absolutePath()); if (!dir.exists()) { dir.mkpath(info.absolutePath()); } KIS_SAFE_ASSERT_RECOVER_NOOP(dir.exists()); } KisImportExportErrorCode res; QFile fi(resultFile); if (!fi.open(QIODevice::WriteOnly)) { qWarning() << "Could not open" << fi.fileName() << "for writing!"; res = KisImportExportErrorCannotWrite(fi.error()); } else { fi.close(); } QScopedPointer encoder(new VideoSaver(doc, batchMode)); res = encoder->convert(doc, savedFilesMask, encoderOptions, batchMode); if (!res.isOk()) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not render animation:\n%1", res.errorMessage())); } if (encoderOptions.shouldDeleteSequence) { QDir d(framesDirectory); QStringList sequenceFiles = d.entryList(QStringList() << encoderOptions.basename + "*." + extension, QDir::Files); Q_FOREACH(const QString &f, sequenceFiles) { d.remove(f); } } } else if (result == KisAsyncAnimationFramesSaveDialog::RenderFailed) { viewManager()->mainWindow()->viewManager()->showFloatingMessage(i18n("Failed to render animation frames!"), QIcon()); } } #include "AnimationRenderer.moc" diff --git a/plugins/extensions/animationrenderer/video_saver.cpp b/plugins/extensions/animationrenderer/video_saver.cpp index 2394d54e2b..ca26483e4f 100644 --- a/plugins/extensions/animationrenderer/video_saver.cpp +++ b/plugins/extensions/animationrenderer/video_saver.cpp @@ -1,331 +1,331 @@ /* * Copyright (c) 2016 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 "video_saver.h" #include #include #include #include #include #include #include #include #include #include #include "kis_config.h" #include "KisAnimationRenderingOptions.h" #include #include #include #include #include #include #include #include "KisPart.h" class KisFFMpegProgressWatcher : public QObject { Q_OBJECT public: KisFFMpegProgressWatcher(QFile &progressFile, int totalFrames) : m_progressFile(progressFile), m_totalFrames(totalFrames) { connect(&m_progressWatcher, SIGNAL(fileChanged(QString)), SLOT(slotFileChanged())); m_progressWatcher.addPath(m_progressFile.fileName()); } private Q_SLOTS: void slotFileChanged() { int currentFrame = -1; bool isEnded = false; while(!m_progressFile.atEnd()) { QString line = QString(m_progressFile.readLine()).remove(QChar('\n')); QStringList var = line.split("="); if (var[0] == "frame") { currentFrame = var[1].toInt(); } else if (var[0] == "progress") { isEnded = var[1] == "end"; } } if (isEnded) { emit sigProgressChanged(100); emit sigProcessingFinished(); } else { emit sigProgressChanged(100 * currentFrame / m_totalFrames); } } Q_SIGNALS: void sigProgressChanged(int percent); void sigProcessingFinished(); private: QFileSystemWatcher m_progressWatcher; QFile &m_progressFile; int m_totalFrames; }; class KisFFMpegRunner { public: KisFFMpegRunner(const QString &ffmpegPath) : m_cancelled(false), m_ffmpegPath(ffmpegPath) {} public: KisImportExportErrorCode runFFMpeg(const QStringList &specialArgs, const QString &actionName, const QString &logPath, int totalFrames) { dbgFile << "runFFMpeg: specialArgs" << specialArgs << "actionName" << actionName << "logPath" << logPath << "totalFrames" << totalFrames; QTemporaryFile progressFile(QDir::tempPath() + QDir::separator() + "KritaFFmpegProgress.XXXXXX"); progressFile.open(); m_process.setStandardOutputFile(logPath); m_process.setProcessChannelMode(QProcess::MergedChannels); QStringList args; args << "-v" << "debug" << "-nostdin" << "-progress" << progressFile.fileName() << specialArgs; qDebug() << "\t" << m_ffmpegPath << args.join(" "); m_cancelled = false; m_process.start(m_ffmpegPath, args); return waitForFFMpegProcess(actionName, progressFile, m_process, totalFrames); } void cancel() { m_cancelled = true; m_process.kill(); } private: KisImportExportErrorCode waitForFFMpegProcess(const QString &message, QFile &progressFile, QProcess &ffmpegProcess, int totalFrames) { KisFFMpegProgressWatcher watcher(progressFile, totalFrames); QProgressDialog progress(message, "", 0, 0, KisPart::instance()->currentMainwindow()); progress.setWindowModality(Qt::ApplicationModal); progress.setCancelButton(0); progress.setMinimumDuration(0); progress.setValue(0); progress.setRange(0, 100); QEventLoop loop; loop.connect(&watcher, SIGNAL(sigProcessingFinished()), SLOT(quit())); loop.connect(&ffmpegProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(quit())); loop.connect(&ffmpegProcess, SIGNAL(error(QProcess::ProcessError)), SLOT(quit())); loop.connect(&watcher, SIGNAL(sigProgressChanged(int)), &progress, SLOT(setValue(int))); if (ffmpegProcess.state() != QProcess::NotRunning) { loop.exec(); // wait for some erroneous case ffmpegProcess.waitForFinished(5000); } KisImportExportErrorCode retval = ImportExportCodes::OK; if (ffmpegProcess.state() != QProcess::NotRunning) { // sorry... ffmpegProcess.kill(); retval = ImportExportCodes::Failure; } else if (m_cancelled) { retval = ImportExportCodes::Cancelled; } else if (ffmpegProcess.exitCode()) { retval = ImportExportCodes::Failure; } return retval; } private: QProcess m_process; bool m_cancelled; QString m_ffmpegPath; }; VideoSaver::VideoSaver(KisDocument *doc, bool batchMode) : m_image(doc->image()) , m_doc(doc) , m_batchMode(batchMode) { } VideoSaver::~VideoSaver() { } KisImageSP VideoSaver::image() { return m_image; } KisImportExportErrorCode VideoSaver::encode(const QString &savedFilesMask, const KisAnimationRenderingOptions &options) { if (!QFileInfo(options.ffmpegPath).exists()) { m_doc->setErrorMessage(i18n("ffmpeg could not be found at %1", options.ffmpegPath)); return ImportExportCodes::Failure; } KisImportExportErrorCode resultOuter = ImportExportCodes::OK; KisImageAnimationInterface *animation = m_image->animationInterface(); const int sequenceNumberingOffset = options.sequenceStart; - const KisTimeRange clipRange(sequenceNumberingOffset + options.firstFrame, - sequenceNumberingOffset + options.lastFrame); + const KisTimeSpan clipRange(sequenceNumberingOffset + options.firstFrame, + sequenceNumberingOffset + options.lastFrame); // export dimensions could be off a little bit, so the last force option tweaks the pixels for the export to work const QString exportDimensions = QString("scale=w=") .append(QString::number(options.width)) .append(":h=") .append(QString::number(options.height)) .append(":force_original_aspect_ratio=decrease"); const QString resultFile = options.resolveAbsoluteVideoFilePath(); const QDir videoDir(QFileInfo(resultFile).absolutePath()); const QFileInfo info(resultFile); const QString suffix = info.suffix().toLower(); const QString palettePath = videoDir.filePath("palette.png"); const QStringList additionalOptionsList = options.customFFMpegOptions.split(' ', QString::SkipEmptyParts); QScopedPointer runner(new KisFFMpegRunner(options.ffmpegPath)); if (suffix == "gif") { { QStringList args; args << "-r" << QString::number(options.frameRate) << "-start_number" << QString::number(clipRange.start()) << "-i" << savedFilesMask << "-vf" << "palettegen" << "-y" << palettePath; KisImportExportErrorCode result = runner->runFFMpeg(args, i18n("Fetching palette..."), videoDir.filePath("log_generate_palette_gif.log"), clipRange.duration()); if (!result.isOk()) { return result; } } { QStringList args; args << "-r" << QString::number(options.frameRate) << "-start_number" << QString::number(clipRange.start()) << "-i" << savedFilesMask << "-i" << palettePath << "-lavfi" << "[0:v][1:v] paletteuse" << "-y" << resultFile; // if we are exporting out at a different image size, we apply scaling filter if (m_image->width() != options.width || m_image->height() != options.height) { args << "-vf" << exportDimensions; } dbgFile << "savedFilesMask" << savedFilesMask << "start" << QString::number(clipRange.start()) << "duration" << clipRange.duration(); KisImportExportErrorCode result = runner->runFFMpeg(args, i18n("Encoding frames..."), videoDir.filePath("log_encode_gif.log"), clipRange.duration()); if (!result.isOk()) { return result; } } } else { QStringList args; args << "-r" << QString::number(options.frameRate) << "-start_number" << QString::number(clipRange.start()) << "-i" << savedFilesMask; QFileInfo audioFileInfo = animation->audioChannelFileName(); if (options.includeAudio && audioFileInfo.exists()) { const int msecStart = clipRange.start() * 1000 / animation->framerate(); const int msecDuration = clipRange.duration() * 1000 / animation->framerate(); const QTime startTime = QTime::fromMSecsSinceStartOfDay(msecStart); const QTime durationTime = QTime::fromMSecsSinceStartOfDay(msecDuration); const QString ffmpegTimeFormat("H:m:s.zzz"); args << "-ss" << startTime.toString(ffmpegTimeFormat); args << "-t" << durationTime.toString(ffmpegTimeFormat); args << "-i" << audioFileInfo.absoluteFilePath(); } // if we are exporting out at a different image size, we apply scaling filter // export options HAVE to go after input options, so make sure this is after the audio import if (m_image->width() != options.width || m_image->height() != options.height) { args << "-vf" << exportDimensions; } args << additionalOptionsList << "-y" << resultFile; resultOuter = runner->runFFMpeg(args, i18n("Encoding frames..."), videoDir.filePath("log_encode.log"), clipRange.duration()); } return resultOuter; } KisImportExportErrorCode VideoSaver::convert(KisDocument *document, const QString &savedFilesMask, const KisAnimationRenderingOptions &options, bool batchMode) { VideoSaver videoSaver(document, batchMode); KisImportExportErrorCode res = videoSaver.encode(savedFilesMask, options); return res; } #include "video_saver.moc" diff --git a/plugins/impex/csv/csv_loader.cpp b/plugins/impex/csv/csv_loader.cpp index 7ebf6d03fb..12de0daf6a 100644 --- a/plugins/impex/csv/csv_loader.cpp +++ b/plugins/impex/csv/csv_loader.cpp @@ -1,488 +1,488 @@ /* * Copyright (c) 2016 Laszlo Fazekas * * 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 "csv_loader.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "csv_read_line.h" #include "csv_layer_record.h" CSVLoader::CSVLoader(KisDocument *doc, bool batchMode) : m_image(0) , m_doc(doc) , m_batchMode(batchMode) , m_stop(false) { } CSVLoader::~CSVLoader() { } KisImportExportErrorCode CSVLoader::decode(QIODevice *io, const QString &filename) { QString field; int idx; int frame = 0; QString projName; int width = 0; int height = 0; int frameCount = 1; float framerate = 24.0; float pixelRatio = 1.0; int projNameIdx = -1; int widthIdx = -1; int heightIdx = -1; int frameCountIdx = -1; int framerateIdx = -1; int pixelRatioIdx = -1; QVector layers; QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); idx = filename.lastIndexOf(QRegExp("[\\/]")); QString base = (idx == -1) ? QString() : filename.left(idx + 1); //include separator QString path = filename; if (path.right(4).toUpper() == ".CSV") path = path.left(path.size() - 4); //according to the QT docs, the slash is a universal directory separator path.append(".frames/"); KisImportExportErrorCode retval = ImportExportCodes::OK; dbgFile << "pos:" << io->pos(); CSVReadLine readLine; QScopedPointer importDoc(KisPart::instance()->createDocument()); importDoc->setInfiniteAutoSaveInterval(); importDoc->setFileBatchMode(true); KisView *setView(0); if (!m_batchMode) { // TODO: use other systems of progress reporting (KisViewManager::createUnthreadedUpdater() // //show the statusbar message even if no view // Q_FOREACH (KisView* view, KisPart::instance()->views()) { // if (view && view->document() == m_doc) { // setView = view; // break; // } // } // if (!setView) { // QStatusBar *sb = KisPart::instance()->currentMainwindow()->statusBar(); // if (sb) { // sb->showMessage(i18n("Loading CSV file...")); // } // } else { // emit m_doc->statusBarMessage(i18n("Loading CSV file...")); // } // emit m_doc->sigProgress(0); // connect(m_doc, SIGNAL(sigProgressCanceled()), this, SLOT(cancel())); } int step = 0; do { qApp->processEvents(); if (m_stop) { retval = ImportExportCodes::Cancelled; break; } if ((idx = readLine.nextLine(io)) <= 0) { if ((idx < 0) ||(step < 5)) retval = ImportExportCodes::FileFormatIncorrect; break; } field = readLine.nextField(); //first field of the line if (field.isNull()) continue; //empty row switch (step) { case 0 : //skip first row step = 1; break; case 1 : //scene header names step = 2; for (idx = 0; !field.isNull(); idx++) { if (field == "Project Name") { projNameIdx = idx; } else if (field == "Width") { widthIdx = idx; } else if (field == "Height") { heightIdx = idx; } else if (field == "Frame Count") { frameCountIdx = idx; } else if (field == "Frame Rate") { framerateIdx = idx; } else if (field == "Pixel Aspect Ratio") { pixelRatioIdx = idx; } field= readLine.nextField(); } break; case 2 : //scene header values step= 3; for (idx= 0; !field.isNull(); idx++) { if (idx == projNameIdx) { projName = field; } else if (idx == widthIdx) { width = field.toInt(); } else if (idx == heightIdx) { height = field.toInt(); } else if (idx == frameCountIdx) { frameCount = field.toInt(); if (frameCount < 1) frameCount= 1; } else if (idx == framerateIdx) { framerate = field.toFloat(); } else if (idx == pixelRatioIdx) { pixelRatio = field.toFloat(); } field= readLine.nextField(); } if ((width < 1) || (height < 1)) { retval = ImportExportCodes::Failure; break; } retval = createNewImage(width, height, pixelRatio, projName.isNull() ? filename : projName); break; case 3 : //create level headers if (field[0] != '#') break; for (; !(field = readLine.nextField()).isNull(); ) { CSVLayerRecord* layerRecord = new CSVLayerRecord(); layers.append(layerRecord); } readLine.rewind(); field = readLine.nextField(); step = 4; Q_FALLTHROUGH(); case 4 : //level header if (field == "#Layers") { //layer name for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++) layers.at(idx)->name = field; break; } if (field == "#Density") { //layer opacity for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++) layers.at(idx)->density = field.toFloat(); break; } if (field == "#Blending") { //layer blending mode for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++) layers.at(idx)->blending = field; break; } if (field == "#Visible") { //layer visibility for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++) layers.at(idx)->visible = field.toInt(); break; } if (field == "#Folder") { //CSV 1.1 folder location for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++) layers.at(idx)->path = validPath(field, base); break; } if ((field.size() < 2) || (field[0] != '#') || !field[1].isDigit()) break; step = 5; Q_FALLTHROUGH(); case 5 : //frames if ((field.size() < 2) || (field[0] != '#') || !field[1].isDigit()) break; for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++) { CSVLayerRecord* layer = layers.at(idx); if (layer->last != field) { if (!m_batchMode) { //emit m_doc->sigProgress((frame * layers.size() + idx) * 100 / // (frameCount * layers.size())); } retval = setLayer(layer, importDoc.data(), path); layer->last = field; layer->frame = frame; } } frame++; break; } } while (retval.isOk()); //finish the layers if (retval.isOk()) { if (m_image) { KisImageAnimationInterface *animation = m_image->animationInterface(); if (frame > frameCount) frameCount = frame; - animation->setFullClipRange(KisTimeRange::fromTime(0,frameCount - 1)); + animation->setFullClipRange(KisTimeSpan(0,frameCount - 1)); animation->setFramerate((int)framerate); } for (idx = 0; idx < layers.size(); idx++) { CSVLayerRecord* layer = layers.at(idx); //empty layers without any pictures are dropped if ((layer->frame > 0) || !layer->last.isEmpty()) { retval = setLayer(layer, importDoc.data(), path); if (!retval.isOk()) break; } } } if (m_image) { //insert the existing layers by the right order for (idx = layers.size() - 1; idx >= 0; idx--) { CSVLayerRecord* layer = layers.at(idx); if (layer->layer) { m_image->addNode(layer->layer, m_image->root()); } } m_image->unlock(); } qDeleteAll(layers); io->close(); if (!m_batchMode) { // disconnect(m_doc, SIGNAL(sigProgressCanceled()), this, SLOT(cancel())); // emit m_doc->sigProgress(100); if (!setView) { QStatusBar *sb = KisPart::instance()->currentMainwindow()->statusBar(); if (sb) { sb->clearMessage(); } } else { emit m_doc->clearStatusBarMessage(); } } QApplication::restoreOverrideCursor(); return retval; } QString CSVLoader::convertBlending(const QString &blending) { if (blending == "Color") return COMPOSITE_OVER; if (blending == "Behind") return COMPOSITE_BEHIND; if (blending == "Erase") return COMPOSITE_ERASE; // "Shade" if (blending == "Light") return COMPOSITE_LINEAR_LIGHT; if (blending == "Colorize") return COMPOSITE_COLORIZE; if (blending == "Hue") return COMPOSITE_HUE; if (blending == "Add") return COMPOSITE_ADD; if (blending == "Sub") return COMPOSITE_INVERSE_SUBTRACT; if (blending == "Multiply") return COMPOSITE_MULT; if (blending == "Screen") return COMPOSITE_SCREEN; // "Replace" // "Substitute" if (blending == "Difference") return COMPOSITE_DIFF; if (blending == "Divide") return COMPOSITE_DIVIDE; if (blending == "Overlay") return COMPOSITE_OVERLAY; if (blending == "Light2") return COMPOSITE_DODGE; if (blending == "Shade2") return COMPOSITE_BURN; if (blending == "HardLight") return COMPOSITE_HARD_LIGHT; if (blending == "SoftLight") return COMPOSITE_SOFT_LIGHT_PHOTOSHOP; if (blending == "GrainExtract") return COMPOSITE_GRAIN_EXTRACT; if (blending == "GrainMerge") return COMPOSITE_GRAIN_MERGE; if (blending == "Sub2") return COMPOSITE_SUBTRACT; if (blending == "Darken") return COMPOSITE_DARKEN; if (blending == "Lighten") return COMPOSITE_LIGHTEN; if (blending == "Saturation") return COMPOSITE_SATURATION; return COMPOSITE_OVER; } QString CSVLoader::validPath(const QString &path,const QString &base) { //replace Windows directory separators with the universal / QString tryPath= QString(path).replace(QString("\\"), QString("/")); int i = tryPath.lastIndexOf("/"); if (i == (tryPath.size() - 1)) tryPath= tryPath.left(i); //remove the ending separator if exists if (QFileInfo(tryPath).isDir()) return tryPath.append("/"); QString scan(tryPath); i = -1; while ((i= (scan.lastIndexOf("/",i) - 1)) > 0) { //avoid testing if the next level will be the default xxxx.layers folder if ((i >= 6) && (scan.mid(i - 6, 7) == ".layers")) continue; tryPath= QString(base).append(scan.mid(i + 2)); //base already ending with a / if (QFileInfo(tryPath).isDir()) return tryPath.append("/"); } return QString(); //NULL string } KisImportExportErrorCode CSVLoader::setLayer(CSVLayerRecord* layer, KisDocument *importDoc, const QString &path) { bool result = true; if (layer->channel == 0) { //create a new document layer float opacity = layer->density; if (opacity > 1.0) opacity = 1.0; else if (opacity < 0.0) opacity = 0.0; const KoColorSpace* cs = m_image->colorSpace(); const QString layerName = (layer->name).isEmpty() ? m_image->nextLayerName() : layer->name; KisPaintLayer* paintLayer = new KisPaintLayer(m_image, layerName, (quint8)(opacity * OPACITY_OPAQUE_U8), cs); paintLayer->setCompositeOpId(convertBlending(layer->blending)); paintLayer->setVisible(layer->visible); paintLayer->enableAnimation(); layer->layer = paintLayer; layer->channel = qobject_cast (paintLayer->getKeyframeChannel(KisKeyframeChannel::Content.id(), true)); } if (!layer->last.isEmpty()) { //png image QString filename = layer->path.isNull() ? path : layer->path; filename.append(layer->last); result = importDoc->openUrl(QUrl::fromLocalFile(filename), KisDocument::DontAddToRecent); if (result) layer->channel->importFrame(layer->frame, importDoc->image()->projection(), 0); } else { //blank layer->channel->addKeyframe(layer->frame); } return (result) ? ImportExportCodes::OK : ImportExportCodes::Failure; } KisImportExportErrorCode CSVLoader::createNewImage(int width, int height, float ratio, const QString &name) { //the CSV is RGBA 8bits, sRGB if (!m_image) { const KoColorSpace* cs = KoColorSpaceRegistry::instance()->colorSpace( RGBAColorModelID.id(), Integer8BitsColorDepthID.id(), 0); if (cs) m_image = new KisImage(m_doc->createUndoStore(), width, height, cs, name); if (!m_image) return ImportExportCodes::Failure; m_image->setResolution(ratio, 1.0); m_image->lock(); } return ImportExportCodes::OK; } KisImportExportErrorCode CSVLoader::buildAnimation(QIODevice *io, const QString &filename) { return decode(io, filename); } KisImageSP CSVLoader::image() { return m_image; } void CSVLoader::cancel() { m_stop = true; } diff --git a/plugins/impex/csv/csv_saver.cpp b/plugins/impex/csv/csv_saver.cpp index ea6e7a9ba1..c5aaa79f6e 100644 --- a/plugins/impex/csv/csv_saver.cpp +++ b/plugins/impex/csv/csv_saver.cpp @@ -1,476 +1,474 @@ /* * Copyright (c) 2016 Laszlo Fazekas * * 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 "csv_saver.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "csv_layer_record.h" CSVSaver::CSVSaver(KisDocument *doc, bool batchMode) : m_image(doc->savingImage()) , m_doc(doc) , m_batchMode(batchMode) , m_stop(false) { } CSVSaver::~CSVSaver() { } KisImageSP CSVSaver::image() { return m_image; } KisImportExportErrorCode CSVSaver::encode(QIODevice *io) { int idx; int start, end; KisNodeSP node; QByteArray ba; KisKeyframeSP keyframe; QVector layers; KisImageAnimationInterface *animation = m_image->animationInterface(); QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); // XXX: Stream was unused? // //DataStream instead of TextStream for correct line endings // QDataStream stream(&f); //Using the original local path QString path = m_doc->localFilePath(); if (path.right(4).toUpper() == ".CSV") path = path.left(path.size() - 4); else { // something is wrong: the local file name is not .csv! // trying the given (probably temporary) filename as well KIS_SAFE_ASSERT_RECOVER(0 && "Wrong extension of the saved file!") { path = path.left(path.size() - 4); } } path.append(".frames"); //create directory QDir dir(path); if (!dir.exists()) { dir.mkpath("."); } //according to the QT docs, the slash is a universal directory separator path.append("/"); node = m_image->rootLayer()->firstChild(); //TODO: correct handling of the layer tree. //for now, only top level paint layers are saved idx = 0; while (node) { if (node->inherits("KisLayer")) { KisLayer* paintLayer = qobject_cast(node.data()); CSVLayerRecord* layerRecord = new CSVLayerRecord(); layers.prepend(layerRecord); //reverse order! layerRecord->name = paintLayer->name(); layerRecord->name.replace(QRegExp("[\"\\r\\n]"), "_"); if (layerRecord->name.isEmpty()) layerRecord->name= QString("Unnamed-%1").arg(idx); layerRecord->visible = (paintLayer->visible()) ? 1 : 0; layerRecord->density = (float)(paintLayer->opacity()) / OPACITY_OPAQUE_U8; layerRecord->blending = convertToBlending(paintLayer->compositeOpId()); layerRecord->layer = paintLayer; layerRecord->channel = paintLayer->original()->keyframeChannel(); layerRecord->last = ""; layerRecord->frame = 0; idx++; } node = node->nextSibling(); } - KisTimeRange range = animation->fullClipRange(); + KisTimeSpan range = animation->fullClipRange(); - start = (range.isValid()) ? range.start() : 0; + start = (!range.isEmpty()) ? range.start() : 0; - if (!range.isInfinite()) { + if (!range.isEmpty()) { end = range.end(); - - if (end < start) end = start; } else { //undefined length, searching for the last keyframe end = start; for (idx = 0; idx < layers.size(); idx++) { KisRasterKeyframeChannel *channel = layers.at(idx)->channel; if (channel) { keyframe = channel->lastKeyframe(); if ( (!keyframe.isNull()) && (keyframe->time() > end) ) end = keyframe->time(); } } } //create temporary doc for exporting QScopedPointer exportDoc(KisPart::instance()->createDocument()); createTempImage(exportDoc.data()); KisImportExportErrorCode retval= ImportExportCodes::OK; if (!m_batchMode) { // TODO: use other systems of progress reporting (KisViewManager::createUnthreadedUpdater() //emit m_doc->statusBarMessage(i18n("Saving CSV file...")); //emit m_doc->sigProgress(0); //connect(m_doc, SIGNAL(sigProgressCanceled()), this, SLOT(cancel())); } int frame = start; int step = 0; do { qApp->processEvents(); if (m_stop) { retval = ImportExportCodes::Cancelled; break; } switch(step) { case 0 : //first row if (io->write("UTF-8, TVPaint, \"CSV 1.0\"\r\n") < 0) { retval = ImportExportCodes::Failure; } break; case 1 : //scene header names if (io->write("Project Name, Width, Height, Frame Count, Layer Count, Frame Rate, Pixel Aspect Ratio, Field Mode\r\n") < 0) { retval = ImportExportCodes::Failure; } break; case 2 : //scene header values ba = QString("\"%1\", ").arg(m_image->objectName()).toUtf8(); if (io->write(ba.data()) < 0) { retval = ImportExportCodes::Failure; break; } ba = QString("%1, %2, ").arg(m_image->width()).arg(m_image->height()).toUtf8(); if (io->write(ba.data()) < 0) { retval = ImportExportCodes::Failure; break; } ba = QString("%1, %2, ").arg(end - start + 1).arg(layers.size()).toUtf8(); if (io->write(ba.data()) < 0) { retval = ImportExportCodes::Failure; break; } //the framerate is an integer here ba = QString("%1, ").arg((double)(animation->framerate()),0,'f',6).toUtf8(); if (io->write(ba.data()) < 0) { retval = ImportExportCodes::Failure; break; } ba = QString("%1, Progressive\r\n").arg((double)(m_image->xRes() / m_image->yRes()),0,'f',6).toUtf8(); if (io->write(ba.data()) < 0) { retval = ImportExportCodes::Failure; break; } break; case 3 : //layer header values if (io->write("#Layers") < 0) { //Layers retval = ImportExportCodes::Failure; break; } for (idx = 0; idx < layers.size(); idx++) { ba = QString(", \"%1\"").arg(layers.at(idx)->name).toUtf8(); if (io->write(ba.data()) < 0) break; } break; case 4 : if (io->write("\r\n#Density") < 0) { //Density retval = ImportExportCodes::Failure; break; } for (idx = 0; idx < layers.size(); idx++) { ba = QString(", %1").arg((double)(layers.at(idx)->density), 0, 'f', 6).toUtf8(); if (io->write(ba.data()) < 0) break; } break; case 5 : if (io->write("\r\n#Blending") < 0) { //Blending retval = ImportExportCodes::Failure; break; } for (idx = 0; idx < layers.size(); idx++) { ba = QString(", \"%1\"").arg(layers.at(idx)->blending).toUtf8(); if (io->write(ba.data()) < 0) break; } break; case 6 : if (io->write("\r\n#Visible") < 0) { //Visible retval = ImportExportCodes::Failure; break; } for (idx = 0; idx < layers.size(); idx++) { ba = QString(", %1").arg(layers.at(idx)->visible).toUtf8(); if (io->write(ba.data()) < 0) break; } if (idx < layers.size()) { retval = ImportExportCodes::Failure; } break; default : //frames if (frame > end) { if (io->write("\r\n") < 0) retval = ImportExportCodes::Failure; step = 8; break; } ba = QString("\r\n#%1").arg(frame, 5, 10, QChar('0')).toUtf8(); if (io->write(ba.data()) < 0) { retval = ImportExportCodes::Failure; break; } for (idx = 0; idx < layers.size(); idx++) { CSVLayerRecord *layer = layers.at(idx); KisRasterKeyframeChannel *channel = layer->channel; if (channel) { if (frame == start) { keyframe = channel->activeKeyframeAt(frame); } else { keyframe = channel->keyframeAt(frame); } } else { keyframe.clear(); // without animation } if ( !keyframe.isNull() || (frame == start) ) { if (!m_batchMode) { //emit m_doc->sigProgress(((frame - start) * layers.size() + idx) * 100 / // ((end - start) * layers.size())); } retval = getLayer(layer, exportDoc.data(), keyframe, path, frame, idx); if (!retval.isOk()) break; } ba = QString(", \"%1\"").arg(layer->last).toUtf8(); if (io->write(ba.data()) < 0) break; } if (idx < layers.size()) retval = ImportExportCodes::Failure; frame++; step = 6; //keep step here break; } step++; } while((retval.isOk()) && (step < 8)); qDeleteAll(layers); // io->close(); it seems this is not required anymore if (!m_batchMode) { //disconnect(m_doc, SIGNAL(sigProgressCanceled()), this, SLOT(cancel())); //emit m_doc->sigProgress(100); //emit m_doc->clearStatusBarMessage(); } QApplication::restoreOverrideCursor(); return retval; } QString CSVSaver::convertToBlending(const QString &opid) { if (opid == COMPOSITE_OVER) return "Color"; if (opid == COMPOSITE_BEHIND) return "Behind"; if (opid == COMPOSITE_ERASE) return "Erase"; // "Shade" if (opid == COMPOSITE_LINEAR_LIGHT) return "Light"; if (opid == COMPOSITE_COLORIZE) return "Colorize"; if (opid == COMPOSITE_HUE) return "Hue"; if ((opid == COMPOSITE_ADD) || (opid == COMPOSITE_LINEAR_DODGE)) return "Add"; if (opid == COMPOSITE_INVERSE_SUBTRACT) return "Sub"; if (opid == COMPOSITE_MULT) return "Multiply"; if (opid == COMPOSITE_SCREEN) return "Screen"; // "Replace" // "Substitute" if (opid == COMPOSITE_DIFF) return "Difference"; if (opid == COMPOSITE_DIVIDE) return "Divide"; if (opid == COMPOSITE_OVERLAY) return "Overlay"; if (opid == COMPOSITE_DODGE) return "Light2"; if (opid == COMPOSITE_BURN) return "Shade2"; if (opid == COMPOSITE_HARD_LIGHT) return "HardLight"; if ((opid == COMPOSITE_SOFT_LIGHT_PHOTOSHOP) || (opid == COMPOSITE_SOFT_LIGHT_SVG)) return "SoftLight"; if (opid == COMPOSITE_GRAIN_EXTRACT) return "GrainExtract"; if (opid == COMPOSITE_GRAIN_MERGE) return "GrainMerge"; if (opid == COMPOSITE_SUBTRACT) return "Sub2"; if (opid == COMPOSITE_DARKEN) return "Darken"; if (opid == COMPOSITE_LIGHTEN) return "Lighten"; if (opid == COMPOSITE_SATURATION) return "Saturation"; return "Color"; } KisImportExportErrorCode CSVSaver::getLayer(CSVLayerRecord* layer, KisDocument* exportDoc, KisKeyframeSP keyframe, const QString &path, int frame, int idx) { //render to the temp layer KisImageSP image = exportDoc->savingImage(); if (!image) image= exportDoc->image(); KisPaintDeviceSP device = image->rootLayer()->firstChild()->projection(); if (!keyframe.isNull()) { layer->channel->fetchFrame(keyframe, device); } else { device->makeCloneFrom(layer->layer->projection(),image->bounds()); // without animation } QRect bounds = device->exactBounds(); if (bounds.isEmpty()) { layer->last = ""; //empty frame return ImportExportCodes::OK; } layer->last = QString("frame%1-%2.png").arg(idx + 1,5,10,QChar('0')).arg(frame,5,10,QChar('0')); QString filename = path; filename.append(layer->last); //save to PNG KisSequentialConstIterator it(device, image->bounds()); const KoColorSpace* cs = device->colorSpace(); bool isThereAlpha = false; while (it.nextPixel()) { if (cs->opacityU8(it.oldRawData()) != OPACITY_OPAQUE_U8) { isThereAlpha = true; break; } } if (!KisPNGConverter::isColorSpaceSupported(cs)) { device = new KisPaintDevice(*device.data()); device->convertTo(KoColorSpaceRegistry::instance()->rgb8()); } KisPNGOptions options; options.alpha = isThereAlpha; options.interlace = false; options.compression = 8; options.tryToSaveAsIndexed = false; options.transparencyFillColor = QColor(0,0,0); options.saveSRGBProfile = true; //TVPaint can use only sRGB options.forceSRGB = false; KisPNGConverter kpc(exportDoc); KisImportExportErrorCode result = kpc.buildFile(filename, image->bounds(), image->xRes(), image->yRes(), device, image->beginAnnotations(), image->endAnnotations(), options, (KisMetaData::Store* )0 ); return result; } void CSVSaver::createTempImage(KisDocument* exportDoc) { exportDoc->setInfiniteAutoSaveInterval(); exportDoc->setFileBatchMode(true); KisImageSP exportImage = new KisImage(exportDoc->createUndoStore(), m_image->width(), m_image->height(), m_image->colorSpace(), QString()); exportImage->setResolution(m_image->xRes(), m_image->yRes()); exportDoc->setCurrentImage(exportImage); KisPaintLayer* paintLayer = new KisPaintLayer(exportImage, "paint device", OPACITY_OPAQUE_U8); exportImage->addNode(paintLayer, exportImage->rootLayer(), KisLayerSP(0)); } KisImportExportErrorCode CSVSaver::buildAnimation(QIODevice *io) { KIS_ASSERT_RECOVER_RETURN_VALUE(m_image, ImportExportCodes::InternalError); return encode(io); } void CSVSaver::cancel() { m_stop = true; } diff --git a/plugins/impex/libkra/kis_kra_loader.cpp b/plugins/impex/libkra/kis_kra_loader.cpp index 46237c8506..f85b504ce6 100644 --- a/plugins/impex/libkra/kis_kra_loader.cpp +++ b/plugins/impex/libkra/kis_kra_loader.cpp @@ -1,1265 +1,1265 @@ /* This file is part of the KDE project * Copyright (C) Boudewijn Rempt , (C) 2007 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kis_kra_loader.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "lazybrush/kis_colorize_mask.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KisResourceServerProvider.h" #include "kis_keyframe_channel.h" #include #include "KisReferenceImagesLayer.h" #include "KisReferenceImage.h" #include #include "KisDocument.h" #include "kis_config.h" #include "kis_kra_tags.h" #include "kis_kra_utils.h" #include "kis_kra_load_visitor.h" #include "kis_dom_utils.h" #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "kis_grid_config.h" #include "kis_guides_config.h" #include "kis_image_config.h" #include "KisProofingConfiguration.h" #include "kis_layer_properties_icons.h" #include "kis_node_view_color_scheme.h" #include "KisMirrorAxisConfig.h" /* Color model id comparison through the ages: 2.4 2.5 2.6 ideal ALPHA ALPHA ALPHA ALPHAU8 CMYK CMYK CMYK CMYKAU8 CMYKAF32 CMYKAF32 CMYKA16 CMYKAU16 CMYKAU16 GRAYA GRAYA GRAYA GRAYAU8 GrayF32 GRAYAF32 GRAYAF32 GRAYA16 GRAYAU16 GRAYAU16 LABA LABA LABA LABAU16 LABAF32 LABAF32 LABAU8 LABAU8 RGBA RGBA RGBA RGBAU8 RGBA16 RGBA16 RGBA16 RGBAU16 RgbAF32 RGBAF32 RGBAF32 RgbAF16 RgbAF16 RGBAF16 XYZA16 XYZA16 XYZA16 XYZAU16 XYZA8 XYZA8 XYZAU8 XyzAF16 XyzAF16 XYZAF16 XyzAF32 XYZAF32 XYZAF32 YCbCrA YCBCRA8 YCBCRA8 YCBCRAU8 YCbCrAU16 YCBCRAU16 YCBCRAU16 YCBCRF32 YCBCRF32 */ using namespace KRA; struct KisKraLoader::Private { public: KisDocument* document; QString imageName; // used to be stored in the image, is now in the documentInfo block QString imageComment; // used to be stored in the image, is now in the documentInfo block QMap layerFilenames; // temp storage during loading int syntaxVersion; // version of the fileformat we are loading vKisNodeSP selectedNodes; // the nodes that were active when saving the document. QMap assistantsFilenames; QList assistants; QMap keyframeFilenames; QVector paletteFilenames; QStringList errorMessages; QStringList warningMessages; }; void convertColorSpaceNames(QString &colorspacename, QString &profileProductName) { if (colorspacename == "Grayscale + Alpha") { colorspacename = "GRAYA"; profileProductName.clear(); } else if (colorspacename == "RgbAF32") { colorspacename = "RGBAF32"; profileProductName.clear(); } else if (colorspacename == "RgbAF16") { colorspacename = "RGBAF16"; profileProductName.clear(); } else if (colorspacename == "CMYKA16") { colorspacename = "CMYKAU16"; } else if (colorspacename == "GrayF32") { colorspacename = "GRAYAF32"; profileProductName.clear(); } else if (colorspacename == "GRAYA16") { colorspacename = "GRAYAU16"; } else if (colorspacename == "XyzAF16") { colorspacename = "XYZAF16"; profileProductName.clear(); } else if (colorspacename == "XyzAF32") { colorspacename = "XYZAF32"; profileProductName.clear(); } else if (colorspacename == "YCbCrA") { colorspacename = "YCBCRA8"; } else if (colorspacename == "YCbCrAU16") { colorspacename = "YCBCRAU16"; } } KisKraLoader::KisKraLoader(KisDocument * document, int syntaxVersion) : m_d(new Private()) { m_d->document = document; m_d->syntaxVersion = syntaxVersion; } KisKraLoader::~KisKraLoader() { delete m_d; } KisImageSP KisKraLoader::loadXML(const KoXmlElement& element) { QString attr; KisImageSP image = 0; qint32 width; qint32 height; QString profileProductName; double xres; double yres; QString colorspacename; const KoColorSpace * cs; if ((attr = element.attribute(MIME)) == NATIVE_MIMETYPE) { if ((m_d->imageName = element.attribute(NAME)).isNull()) { m_d->errorMessages << i18n("Image does not have a name."); return KisImageSP(0); } if ((attr = element.attribute(WIDTH)).isNull()) { m_d->errorMessages << i18n("Image does not specify a width."); return KisImageSP(0); } width = KisDomUtils::toInt(attr); if ((attr = element.attribute(HEIGHT)).isNull()) { m_d->errorMessages << i18n("Image does not specify a height."); return KisImageSP(0); } height = KisDomUtils::toInt(attr); m_d->imageComment = element.attribute(DESCRIPTION); xres = 100.0 / 72.0; if (!(attr = element.attribute(X_RESOLUTION)).isNull()) { qreal value = KisDomUtils::toDouble(attr); if (value > 1.0) { xres = value / 72.0; } } yres = 100.0 / 72.0; if (!(attr = element.attribute(Y_RESOLUTION)).isNull()) { qreal value = KisDomUtils::toDouble(attr); if (value > 1.0) { yres = value / 72.0; } } if ((colorspacename = element.attribute(COLORSPACE_NAME)).isNull()) { // An old file: take a reasonable default. // Krita didn't support anything else in those // days anyway. colorspacename = "RGBA"; } profileProductName = element.attribute(PROFILE); // A hack for an old colorspacename convertColorSpaceNames(colorspacename, profileProductName); QString colorspaceModel = KoColorSpaceRegistry::instance()->colorSpaceColorModelId(colorspacename).id(); QString colorspaceDepth = KoColorSpaceRegistry::instance()->colorSpaceColorDepthId(colorspacename).id(); if (profileProductName.isNull()) { // no mention of profile so get default profile"; cs = KoColorSpaceRegistry::instance()->colorSpace(colorspaceModel, colorspaceDepth, ""); } else { cs = KoColorSpaceRegistry::instance()->colorSpace(colorspaceModel, colorspaceDepth, profileProductName); } if (cs == 0) { // try once more without the profile cs = KoColorSpaceRegistry::instance()->colorSpace(colorspaceModel, colorspaceDepth, ""); if (cs == 0) { m_d->errorMessages << i18n("Image specifies an unsupported color model: %1.", colorspacename); return KisImageSP(0); } } KisProofingConfigurationSP proofingConfig = KisImageConfig(true).defaultProofingconfiguration(); if (!(attr = element.attribute(PROOFINGPROFILENAME)).isNull()) { proofingConfig->proofingProfile = attr; } if (!(attr = element.attribute(PROOFINGMODEL)).isNull()) { proofingConfig->proofingModel = attr; } if (!(attr = element.attribute(PROOFINGDEPTH)).isNull()) { proofingConfig->proofingDepth = attr; } if (!(attr = element.attribute(PROOFINGINTENT)).isNull()) { proofingConfig->intent = (KoColorConversionTransformation::Intent) KisDomUtils::toInt(attr); } if (!(attr = element.attribute(PROOFINGADAPTATIONSTATE)).isNull()) { proofingConfig->adaptationState = KisDomUtils::toDouble(attr); } if (m_d->document) { image = new KisImage(m_d->document->createUndoStore(), width, height, cs, m_d->imageName); } else { image = new KisImage(0, width, height, cs, m_d->imageName); } image->setResolution(xres, yres); loadNodes(element, image, const_cast(image->rootLayer().data())); KoXmlNode child; for (child = element.lastChild(); !child.isNull(); child = child.previousSibling()) { KoXmlElement e = child.toElement(); if(e.tagName() == CANVASPROJECTIONCOLOR) { if (e.hasAttribute(COLORBYTEDATA)) { QByteArray colorData = QByteArray::fromBase64(e.attribute(COLORBYTEDATA).toLatin1()); KoColor color((const quint8*)colorData.data(), image->colorSpace()); image->setDefaultProjectionColor(color); } } if(e.tagName() == GLOBALASSISTANTSCOLOR) { if (e.hasAttribute(SIMPLECOLORDATA)) { QString colorData = e.attribute(SIMPLECOLORDATA); m_d->document->setAssistantsGlobalColor(KisDomUtils::qStringToQColor(colorData)); } } if(e.tagName()== PROOFINGWARNINGCOLOR) { QDomDocument dom; KoXml::asQDomElement(dom, e); QDomElement eq = dom.firstChildElement(); proofingConfig->warningColor = KoColor::fromXML(eq.firstChildElement(), Integer8BitsColorDepthID.id()); } if (e.tagName().toLower() == "animation") { loadAnimationMetadata(e, image); } } image->setProofingConfiguration(proofingConfig); for (child = element.lastChild(); !child.isNull(); child = child.previousSibling()) { KoXmlElement e = child.toElement(); if(e.tagName() == "compositions") { loadCompositions(e, image); } } } KoXmlNode child; for (child = element.lastChild(); !child.isNull(); child = child.previousSibling()) { KoXmlElement e = child.toElement(); if (e.tagName() == "grid") { loadGrid(e); } else if (e.tagName() == "guides") { loadGuides(e); } else if (e.tagName() == MIRROR_AXIS) { loadMirrorAxis(e); } else if (e.tagName() == "assistants") { loadAssistantsList(e); } else if (e.tagName() == "audio") { loadAudio(e, image); } } // reading palettes from XML for (child = element.lastChild(); !child.isNull(); child = child.previousSibling()) { QDomElement e = child.toElement(); if (e.tagName() == PALETTES) { for (QDomElement paletteElement = e.lastChildElement(); !paletteElement.isNull(); paletteElement = paletteElement.previousSiblingElement()) { QString paletteName = paletteElement.attribute("filename"); m_d->paletteFilenames.append(paletteName); } break; } } return image; } void KisKraLoader::loadBinaryData(KoStore * store, KisImageSP image, const QString & uri, bool external) { // icc profile: if present, this overrides the profile product name loaded in loadXML. QString location = external ? QString() : uri; location += m_d->imageName + ICC_PATH; if (store->hasFile(location)) { if (store->open(location)) { QByteArray data; data.resize(store->size()); bool res = (store->read(data.data(), store->size()) > -1); store->close(); if (res) { const KoColorProfile *profile = KoColorSpaceRegistry::instance()->createColorProfile(image->colorSpace()->colorModelId().id(), image->colorSpace()->colorDepthId().id(), data); if (profile && profile->valid()) { res = image->assignImageProfile(profile); } if (!res) { const QString defaultProfileId = KoColorSpaceRegistry::instance()->defaultProfileForColorSpace(image->colorSpace()->id()); profile = KoColorSpaceRegistry::instance()->profileByName(defaultProfileId); Q_ASSERT(profile && profile->valid()); image->assignImageProfile(profile); } } } } //load the embed proofing profile, it only needs to be loaded into Krita, not assigned. location = external ? QString() : uri; location += m_d->imageName + ICC_PROOFING_PATH; if (store->hasFile(location)) { if (store->open(location)) { QByteArray proofingData; proofingData.resize(store->size()); bool proofingProfileRes = (store->read(proofingData.data(), store->size())>-1); store->close(); KisProofingConfigurationSP proofingConfig = image->proofingConfiguration(); if (!proofingConfig) { proofingConfig = KisImageConfig(true).defaultProofingconfiguration(); } if (proofingProfileRes) { const KoColorProfile *proofingProfile = KoColorSpaceRegistry::instance()->createColorProfile(proofingConfig->proofingModel, proofingConfig->proofingDepth, proofingData); if (proofingProfile->valid()){ KoColorSpaceRegistry::instance()->addProfile(proofingProfile); } } } } // Load the layers data: if there is a profile associated with a layer it will be set now. KisKraLoadVisitor visitor(image, store, m_d->document->shapeController(), m_d->layerFilenames, m_d->keyframeFilenames, m_d->imageName, m_d->syntaxVersion); if (external) { visitor.setExternalUri(uri); } image->rootLayer()->accept(visitor); if (!visitor.errorMessages().isEmpty()) { m_d->errorMessages.append(visitor.errorMessages()); } if (!visitor.warningMessages().isEmpty()) { m_d->warningMessages.append(visitor.warningMessages()); } // annotations // exif location = external ? QString() : uri; location += m_d->imageName + EXIF_PATH; if (store->hasFile(location)) { QByteArray data; store->open(location); data = store->read(store->size()); store->close(); image->addAnnotation(KisAnnotationSP(new KisAnnotation("exif", "", data))); } // layer styles location = external ? QString() : uri; location += m_d->imageName + LAYER_STYLES_PATH; if (store->hasFile(location)) { KisPSDLayerStyleCollectionResource *collection = new KisPSDLayerStyleCollectionResource("Embedded Styles.asl"); collection->setName(i18nc("Auto-generated layer style collection name for embedded styles (collection)", "<%1> (embedded)", m_d->imageName)); KIS_ASSERT_RECOVER_NOOP(!collection->valid()); store->open(location); { KoStoreDevice device(store); device.open(QIODevice::ReadOnly); /** * ASL loading code cannot work with non-sequential IO devices, * so convert the device beforehand! */ QByteArray buf = device.readAll(); QBuffer raDevice(&buf); raDevice.open(QIODevice::ReadOnly); collection->loadFromDevice(&raDevice); } store->close(); if (collection->valid()) { KoResourceServer *server = KisResourceServerProvider::instance()->layerStyleCollectionServer(); server->addResource(collection, false); collection->assignAllLayerStyles(image->root()); } else { warnKrita << "WARNING: Couldn't load layer styles library from .kra!"; delete collection; } } if (m_d->document && m_d->document->documentInfo()->aboutInfo("title").isNull()) m_d->document->documentInfo()->setAboutInfo("title", m_d->imageName); if (m_d->document && m_d->document->documentInfo()->aboutInfo("comment").isNull()) m_d->document->documentInfo()->setAboutInfo("comment", m_d->imageComment); loadAssistants(store, uri, external); } void KisKraLoader::loadPalettes(KoStore *store, KisDocument *doc) { QList list; Q_FOREACH (const QString &filename, m_d->paletteFilenames) { KoColorSet *newPalette = new KoColorSet(filename); store->open(m_d->imageName + PALETTE_PATH + filename); QByteArray data = store->read(store->size()); newPalette->fromByteArray(data); newPalette->setIsGlobal(false); newPalette->setIsEditable(true); store->close(); list.append(newPalette); } doc->setPaletteList(list); } vKisNodeSP KisKraLoader::selectedNodes() const { return m_d->selectedNodes; } QList KisKraLoader::assistants() const { return m_d->assistants; } QStringList KisKraLoader::errorMessages() const { return m_d->errorMessages; } QStringList KisKraLoader::warningMessages() const { return m_d->warningMessages; } QString KisKraLoader::imageName() const { return m_d->imageName; } void KisKraLoader::loadAssistants(KoStore *store, const QString &uri, bool external) { QString file_path; QString location; QMap handleMap; KisPaintingAssistant* assistant = 0; const QColor globalColor = m_d->document->assistantsGlobalColor(); QMap::const_iterator loadedAssistant = m_d->assistantsFilenames.constBegin(); while (loadedAssistant != m_d->assistantsFilenames.constEnd()){ const KisPaintingAssistantFactory* factory = KisPaintingAssistantFactoryRegistry::instance()->get(loadedAssistant.value()); if (factory) { assistant = factory->createPaintingAssistant(); location = external ? QString() : uri; location += m_d->imageName + ASSISTANTS_PATH; file_path = location + loadedAssistant.key(); assistant->loadXml(store, handleMap, file_path); assistant->setAssistantGlobalColorCache(globalColor); //If an assistant has too few handles than it should according to it's own setup, just don't load it// if (assistant->handles().size()==assistant->numHandles()){ m_d->assistants.append(toQShared(assistant)); } } loadedAssistant++; } } void KisKraLoader::loadAnimationMetadata(const KoXmlElement &element, KisImageSP image) { QDomDocument qDom; KoXml::asQDomElement(qDom, element); QDomElement qElement = qDom.firstChildElement(); float framerate; - KisTimeRange range; + KisTimeSpan range; int currentTime; KisImageAnimationInterface *animation = image->animationInterface(); if (KisDomUtils::loadValue(qElement, "framerate", &framerate)) { animation->setFramerate(framerate); } if (KisDomUtils::loadValue(qElement, "range", &range)) { animation->setFullClipRange(range); } if (KisDomUtils::loadValue(qElement, "currentTime", ¤tTime)) { animation->switchCurrentTimeAsync(currentTime); } } KisNodeSP KisKraLoader::loadNodes(const KoXmlElement& element, KisImageSP image, KisNodeSP parent) { KoXmlNode node = element.firstChild(); KoXmlNode child; if (!node.isNull()) { if (node.isElement()) { if (node.nodeName().toUpper() == LAYERS.toUpper() || node.nodeName().toUpper() == MASKS.toUpper()) { for (child = node.lastChild(); !child.isNull(); child = child.previousSibling()) { KisNodeSP node = loadNode(child.toElement(), image); if (node) { image->nextLayerName(); // Make sure the nameserver is current with the number of nodes. image->addNode(node, parent); if (node->inherits("KisLayer") && KoXml::childNodesCount(child) > 0) { loadNodes(child.toElement(), image, node); } } } } } } return parent; } KisNodeSP KisKraLoader::loadNode(const KoXmlElement& element, KisImageSP image) { // Nota bene: If you add new properties to layers, you should // ALWAYS define a default value in case the property is not // present in the layer definition: this helps a LOT with backward // compatibility. QString name = element.attribute(NAME, "No Name"); QUuid id = QUuid(element.attribute(UUID, QUuid().toString())); qint32 x = element.attribute(X, "0").toInt(); qint32 y = element.attribute(Y, "0").toInt(); qint32 opacity = element.attribute(OPACITY, QString::number(OPACITY_OPAQUE_U8)).toInt(); if (opacity < OPACITY_TRANSPARENT_U8) opacity = OPACITY_TRANSPARENT_U8; if (opacity > OPACITY_OPAQUE_U8) opacity = OPACITY_OPAQUE_U8; const KoColorSpace* colorSpace = 0; if ((element.attribute(COLORSPACE_NAME)).isNull()) { dbgFile << "No attribute color space for layer: " << name; colorSpace = image->colorSpace(); } else { QString colorspacename = element.attribute(COLORSPACE_NAME); QString profileProductName; convertColorSpaceNames(colorspacename, profileProductName); QString colorspaceModel = KoColorSpaceRegistry::instance()->colorSpaceColorModelId(colorspacename).id(); QString colorspaceDepth = KoColorSpaceRegistry::instance()->colorSpaceColorDepthId(colorspacename).id(); dbgFile << "Searching color space: " << colorspacename << colorspaceModel << colorspaceDepth << " for layer: " << name; // use default profile - it will be replaced later in completeLoading colorSpace = KoColorSpaceRegistry::instance()->colorSpace(colorspaceModel, colorspaceDepth, ""); dbgFile << "found colorspace" << colorSpace; if (!colorSpace) { m_d->warningMessages << i18n("Layer %1 specifies an unsupported color model: %2.", name, colorspacename); return 0; } } const bool visible = element.attribute(VISIBLE, "1") == "0" ? false : true; const bool locked = element.attribute(LOCKED, "0") == "0" ? false : true; const bool collapsed = element.attribute(COLLAPSED, "0") == "0" ? false : true; int colorLabelIndex = element.attribute(COLOR_LABEL, "0").toInt(); QVector labels = KisNodeViewColorScheme::instance()->allColorLabels(); if (colorLabelIndex >= labels.size()) { colorLabelIndex = labels.size() - 1; } // Now find out the layer type and do specific handling QString nodeType; if (m_d->syntaxVersion == 1) { nodeType = element.attribute("layertype"); if (nodeType.isEmpty()) { nodeType = PAINT_LAYER; } } else { nodeType = element.attribute(NODE_TYPE); } if (nodeType.isEmpty()) { m_d->warningMessages << i18n("Layer %1 has an unsupported type.", name); return 0; } KisNodeSP node = 0; if (nodeType == PAINT_LAYER) node = loadPaintLayer(element, image, name, colorSpace, opacity); else if (nodeType == GROUP_LAYER) node = loadGroupLayer(element, image, name, colorSpace, opacity); else if (nodeType == ADJUSTMENT_LAYER) node = loadAdjustmentLayer(element, image, name, colorSpace, opacity); else if (nodeType == SHAPE_LAYER) node = loadShapeLayer(element, image, name, colorSpace, opacity); else if (nodeType == GENERATOR_LAYER) node = loadGeneratorLayer(element, image, name, colorSpace, opacity); else if (nodeType == CLONE_LAYER) node = loadCloneLayer(element, image, name, colorSpace, opacity); else if (nodeType == FILTER_MASK) node = loadFilterMask(element); else if (nodeType == TRANSFORM_MASK) node = loadTransformMask(element); else if (nodeType == TRANSPARENCY_MASK) node = loadTransparencyMask(element); else if (nodeType == SELECTION_MASK) node = loadSelectionMask(image, element); else if (nodeType == COLORIZE_MASK) node = loadColorizeMask(image, element, colorSpace); else if (nodeType == FILE_LAYER) node = loadFileLayer(element, image, name, opacity); else if (nodeType == REFERENCE_IMAGES_LAYER) node = loadReferenceImagesLayer(element, image); else { m_d->warningMessages << i18n("Layer %1 has an unsupported type: %2.", name, nodeType); return 0; } // Loading the node went wrong. Return empty node and leave to // upstream to complain to the user if (!node) { m_d->warningMessages << i18n("Failure loading layer %1 of type: %2.", name, nodeType); return 0; } node->setVisible(visible, true); node->setUserLocked(locked); node->setCollapsed(collapsed); node->setColorLabelIndex(colorLabelIndex); node->setX(x); node->setY(y); node->setName(name); if (! id.isNull()) // if no uuid in file, new one has been generated already node->setUuid(id); if (node->inherits("KisLayer") || node->inherits("KisColorizeMask")) { QString compositeOpName = element.attribute(COMPOSITE_OP, "normal"); node->setCompositeOpId(compositeOpName); } if (node->inherits("KisLayer")) { KisLayer* layer = qobject_cast(node.data()); QBitArray channelFlags = stringToFlags(element.attribute(CHANNEL_FLAGS, ""), colorSpace->channelCount()); layer->setChannelFlags(channelFlags); if (element.hasAttribute(LAYER_STYLE_UUID)) { QString uuidString = element.attribute(LAYER_STYLE_UUID); QUuid uuid(uuidString); if (!uuid.isNull()) { KisPSDLayerStyleSP dumbLayerStyle(new KisPSDLayerStyle()); dumbLayerStyle->setUuid(uuid); layer->setLayerStyle(dumbLayerStyle); } else { warnKrita << "WARNING: Layer style for layer" << layer->name() << "contains invalid UUID" << uuidString; } } } if (node->inherits("KisGroupLayer")) { if (element.hasAttribute(PASS_THROUGH_MODE)) { bool value = element.attribute(PASS_THROUGH_MODE, "0") != "0"; KisGroupLayer *group = qobject_cast(node.data()); group->setPassThroughMode(value); } } const bool timelineEnabled = element.attribute(VISIBLE_IN_TIMELINE, "0") == "0" ? false : true; node->setUseInTimeline(timelineEnabled); if (node->inherits("KisPaintLayer")) { KisPaintLayer* layer = qobject_cast(node.data()); QBitArray channelLockFlags = stringToFlags(element.attribute(CHANNEL_LOCK_FLAGS, ""), colorSpace->channelCount()); layer->setChannelLockFlags(channelLockFlags); bool onionEnabled = element.attribute(ONION_SKIN_ENABLED, "0") == "0" ? false : true; layer->setOnionSkinEnabled(onionEnabled); } if (element.attribute(FILE_NAME).isNull()) { m_d->layerFilenames[node.data()] = name; } else { m_d->layerFilenames[node.data()] = element.attribute(FILE_NAME); } if (element.hasAttribute("selected") && element.attribute("selected") == "true") { m_d->selectedNodes.append(node); } if (element.hasAttribute(KEYFRAME_FILE)) { m_d->keyframeFilenames.insert(node.data(), element.attribute(KEYFRAME_FILE)); } return node; } KisNodeSP KisKraLoader::loadPaintLayer(const KoXmlElement& element, KisImageSP image, const QString& name, const KoColorSpace* cs, quint32 opacity) { Q_UNUSED(element); KisPaintLayer* layer; layer = new KisPaintLayer(image, name, opacity, cs); Q_CHECK_PTR(layer); return layer; } KisNodeSP KisKraLoader::loadFileLayer(const KoXmlElement& element, KisImageSP image, const QString& name, quint32 opacity) { QString filename = element.attribute("source", QString()); if (filename.isNull()) return 0; bool scale = (element.attribute("scale", "true") == "true"); int scalingMethod = element.attribute("scalingmethod", "-1").toInt(); if (scalingMethod < 0) { if (scale) { scalingMethod = KisFileLayer::ToImagePPI; } else { scalingMethod = KisFileLayer::None; } } QString documentPath; if (m_d->document) { documentPath = m_d->document->url().toLocalFile(); } QFileInfo info(documentPath); QString basePath = info.absolutePath(); QString fullPath = QDir(basePath).filePath(QDir::cleanPath(filename)); if (!QFileInfo(fullPath).exists()) { qApp->setOverrideCursor(Qt::ArrowCursor); QString msg = i18nc( "@info", "The file associated to a file layer with the name \"%1\" is not found.\n\n" "Expected path:\n" "%2\n\n" "Do you want to locate it manually?", name, fullPath); int result = QMessageBox::warning(0, i18nc("@title:window", "File not found"), msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (result == QMessageBox::Yes) { KoFileDialog dialog(0, KoFileDialog::OpenFile, "OpenDocument"); dialog.setMimeTypeFilters(KisImportExportManager::supportedMimeTypes(KisImportExportManager::Import)); dialog.setDefaultDir(basePath); QString url = dialog.filename(); if (!QFileInfo(basePath).exists()) { filename = url; } else { QDir d(basePath); filename = d.relativeFilePath(url); } } qApp->restoreOverrideCursor(); } KisLayer *layer = new KisFileLayer(image, basePath, filename, (KisFileLayer::ScalingMethod)scalingMethod, name, opacity); Q_CHECK_PTR(layer); return layer; } KisNodeSP KisKraLoader::loadGroupLayer(const KoXmlElement& element, KisImageSP image, const QString& name, const KoColorSpace* cs, quint32 opacity) { Q_UNUSED(element); Q_UNUSED(cs); QString attr; KisGroupLayer* layer; layer = new KisGroupLayer(image, name, opacity); Q_CHECK_PTR(layer); return layer; } KisNodeSP KisKraLoader::loadAdjustmentLayer(const KoXmlElement& element, KisImageSP image, const QString& name, const KoColorSpace* cs, quint32 opacity) { // XXX: do something with filterversion? Q_UNUSED(cs); QString attr; KisAdjustmentLayer* layer; QString filtername; QString legacy = filtername; if ((filtername = element.attribute(FILTER_NAME)).isNull()) { // XXX: Invalid adjustmentlayer! We should warn about it! warnFile << "No filter in adjustment layer"; return 0; } //get deprecated filters. if (filtername=="brightnesscontrast") { legacy = filtername; filtername = "perchannel"; } if (filtername=="left edge detections" || filtername=="right edge detections" || filtername=="top edge detections" || filtername=="bottom edge detections") { legacy = filtername; filtername = "edge detection"; } KisFilterSP f = KisFilterRegistry::instance()->value(filtername); if (!f) { warnFile << "No filter for filtername" << filtername << ""; return 0; // XXX: We don't have this filter. We should warn about it! } KisFilterConfigurationSP kfc = f->factoryConfiguration(); kfc->setProperty("legacy", legacy); if (legacy=="brightnesscontrast") { kfc->setProperty("colorModel", cs->colorModelId().id()); } // We'll load the configuration and the selection later. layer = new KisAdjustmentLayer(image, name, kfc, 0); Q_CHECK_PTR(layer); layer->setOpacity(opacity); return layer; } KisNodeSP KisKraLoader::loadShapeLayer(const KoXmlElement& element, KisImageSP image, const QString& name, const KoColorSpace* cs, quint32 opacity) { Q_UNUSED(element); Q_UNUSED(cs); QString attr; KoShapeControllerBase * shapeController = 0; if (m_d->document) { shapeController = m_d->document->shapeController(); } KisShapeLayer* layer = new KisShapeLayer(shapeController, image, name, opacity); Q_CHECK_PTR(layer); return layer; } KisNodeSP KisKraLoader::loadGeneratorLayer(const KoXmlElement& element, KisImageSP image, const QString& name, const KoColorSpace* cs, quint32 opacity) { Q_UNUSED(cs); // XXX: do something with generator version? KisGeneratorLayer* layer; QString generatorname = element.attribute(GENERATOR_NAME); if (generatorname.isNull()) { // XXX: Invalid generator layer! We should warn about it! warnFile << "No generator in generator layer"; return 0; } KisGeneratorSP generator = KisGeneratorRegistry::instance()->value(generatorname); if (!generator) { warnFile << "No generator for generatorname" << generatorname << ""; return 0; // XXX: We don't have this generator. We should warn about it! } KisFilterConfigurationSP kgc = generator->factoryConfiguration(); // We'll load the configuration and the selection later. layer = new KisGeneratorLayer(image, name, kgc, 0); Q_CHECK_PTR(layer); layer->setOpacity(opacity); return layer; } KisNodeSP KisKraLoader::loadCloneLayer(const KoXmlElement& element, KisImageSP image, const QString& name, const KoColorSpace* cs, quint32 opacity) { Q_UNUSED(cs); KisCloneLayerSP layer = new KisCloneLayer(0, image, name, opacity); KisNodeUuidInfo info; if (! (element.attribute(CLONE_FROM_UUID)).isNull()) { info = KisNodeUuidInfo(QUuid(element.attribute(CLONE_FROM_UUID))); } else { if ((element.attribute(CLONE_FROM)).isNull()) { return 0; } else { info = KisNodeUuidInfo(element.attribute(CLONE_FROM)); } } layer->setCopyFromInfo(info); if ((element.attribute(CLONE_TYPE)).isNull()) { return 0; } else { layer->setCopyType((CopyLayerType) element.attribute(CLONE_TYPE).toInt()); } return layer; } KisNodeSP KisKraLoader::loadFilterMask(const KoXmlElement& element) { QString attr; KisFilterMask* mask; QString filtername; // XXX: should we check the version? if ((filtername = element.attribute(FILTER_NAME)).isNull()) { // XXX: Invalid filter layer! We should warn about it! warnFile << "No filter in filter layer"; return 0; } KisFilterSP f = KisFilterRegistry::instance()->value(filtername); if (!f) { warnFile << "No filter for filtername" << filtername << ""; return 0; // XXX: We don't have this filter. We should warn about it! } KisFilterConfigurationSP kfc = f->factoryConfiguration(); // We'll load the configuration and the selection later. mask = new KisFilterMask(); mask->setFilter(kfc); Q_CHECK_PTR(mask); return mask; } KisNodeSP KisKraLoader::loadTransformMask(const KoXmlElement& element) { Q_UNUSED(element); KisTransformMask* mask; /** * We'll load the transform configuration later on a stage * of binary data loading */ mask = new KisTransformMask(); Q_CHECK_PTR(mask); return mask; } KisNodeSP KisKraLoader::loadTransparencyMask(const KoXmlElement& element) { Q_UNUSED(element); KisTransparencyMask* mask = new KisTransparencyMask(); Q_CHECK_PTR(mask); return mask; } KisNodeSP KisKraLoader::loadSelectionMask(KisImageSP image, const KoXmlElement& element) { KisSelectionMaskSP mask = new KisSelectionMask(image); bool active = element.attribute(ACTIVE, "1") == "0" ? false : true; mask->setActive(active); Q_CHECK_PTR(mask); return mask; } KisNodeSP KisKraLoader::loadColorizeMask(KisImageSP image, const KoXmlElement& element, const KoColorSpace *colorSpace) { KisColorizeMaskSP mask = new KisColorizeMask(); const bool editKeystrokes = element.attribute(COLORIZE_EDIT_KEYSTROKES, "1") == "0" ? false : true; const bool showColoring = element.attribute(COLORIZE_SHOW_COLORING, "1") == "0" ? false : true; KisLayerPropertiesIcons::setNodeProperty(mask, KisLayerPropertiesIcons::colorizeEditKeyStrokes, editKeystrokes, image); KisLayerPropertiesIcons::setNodeProperty(mask, KisLayerPropertiesIcons::colorizeShowColoring, showColoring, image); const bool useEdgeDetection = KisDomUtils::toInt(element.attribute(COLORIZE_USE_EDGE_DETECTION, "0")); const qreal edgeDetectionSize = KisDomUtils::toDouble(element.attribute(COLORIZE_EDGE_DETECTION_SIZE, "4")); const qreal radius = KisDomUtils::toDouble(element.attribute(COLORIZE_FUZZY_RADIUS, "0")); const int cleanUp = KisDomUtils::toInt(element.attribute(COLORIZE_CLEANUP, "0")); const bool limitToDevice = KisDomUtils::toInt(element.attribute(COLORIZE_LIMIT_TO_DEVICE, "0")); mask->setUseEdgeDetection(useEdgeDetection); mask->setEdgeDetectionSize(edgeDetectionSize); mask->setFuzzyRadius(radius); mask->setCleanUpAmount(qreal(cleanUp) / 100.0); mask->setLimitToDeviceBounds(limitToDevice); delete mask->setColorSpace(colorSpace); mask->setImage(image); return mask; } void KisKraLoader::loadCompositions(const KoXmlElement& elem, KisImageSP image) { KoXmlNode child; for (child = elem.firstChild(); !child.isNull(); child = child.nextSibling()) { KoXmlElement e = child.toElement(); QString name = e.attribute("name"); bool exportEnabled = e.attribute("exportEnabled", "1") == "0" ? false : true; KisLayerCompositionSP composition(new KisLayerComposition(image, name)); composition->setExportEnabled(exportEnabled); KoXmlNode value; for (value = child.lastChild(); !value.isNull(); value = value.previousSibling()) { KoXmlElement e = value.toElement(); QUuid uuid(e.attribute("uuid")); bool visible = e.attribute("visible", "1") == "0" ? false : true; composition->setVisible(uuid, visible); bool collapsed = e.attribute("collapsed", "1") == "0" ? false : true; composition->setCollapsed(uuid, collapsed); } image->addComposition(composition); } } void KisKraLoader::loadAssistantsList(const KoXmlElement &elem) { KoXmlNode child; int count = 0; for (child = elem.firstChild(); !child.isNull(); child = child.nextSibling()) { KoXmlElement e = child.toElement(); QString type = e.attribute("type"); QString file_name = e.attribute("filename"); m_d->assistantsFilenames.insert(file_name,type); count++; } } void KisKraLoader::loadGrid(const KoXmlElement& elem) { QDomDocument dom; KoXml::asQDomElement(dom, elem); QDomElement domElement = dom.firstChildElement(); KisGridConfig config; config.loadDynamicDataFromXml(domElement); config.loadStaticData(); m_d->document->setGridConfig(config); } void KisKraLoader::loadGuides(const KoXmlElement& elem) { QDomDocument dom; KoXml::asQDomElement(dom, elem); QDomElement domElement = dom.firstChildElement(); KisGuidesConfig guides; guides.loadFromXml(domElement); m_d->document->setGuidesConfig(guides); } void KisKraLoader::loadMirrorAxis(const KoXmlElement &elem) { QDomDocument dom; KoXml::asQDomElement(dom, elem); QDomElement domElement = dom.firstChildElement(); KisMirrorAxisConfig mirrorAxis; mirrorAxis.loadFromXml(domElement); m_d->document->setMirrorAxisConfig(mirrorAxis); } void KisKraLoader::loadAudio(const KoXmlElement& elem, KisImageSP image) { QDomDocument dom; KoXml::asQDomElement(dom, elem); QDomElement qElement = dom.firstChildElement(); QString fileName; if (KisDomUtils::loadValue(qElement, "masterChannelPath", &fileName)) { fileName = QDir::toNativeSeparators(fileName); QDir baseDirectory = QFileInfo(m_d->document->localFilePath()).absoluteDir(); fileName = baseDirectory.absoluteFilePath(fileName); QFileInfo info(fileName); if (!info.exists()) { qApp->setOverrideCursor(Qt::ArrowCursor); QString msg = i18nc( "@info", "Audio channel file \"%1\" doesn't exist!\n\n" "Expected path:\n" "%2\n\n" "Do you want to locate it manually?", info.fileName(), info.absoluteFilePath()); int result = QMessageBox::warning(0, i18nc("@title:window", "File not found"), msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (result == QMessageBox::Yes) { info.setFile(KisImportExportManager::askForAudioFileName(info.absolutePath(), 0)); } qApp->restoreOverrideCursor(); } if (info.exists()) { image->animationInterface()->setAudioChannelFileName(info.absoluteFilePath()); } } bool audioMuted = false; if (KisDomUtils::loadValue(qElement, "audioMuted", &audioMuted)) { image->animationInterface()->setAudioMuted(audioMuted); } qreal audioVolume = 0.5; if (KisDomUtils::loadValue(qElement, "audioVolume", &audioVolume)) { image->animationInterface()->setAudioVolume(audioVolume); } } KisNodeSP KisKraLoader::loadReferenceImagesLayer(const KoXmlElement &elem, KisImageSP image) { KisSharedPtr layer = new KisReferenceImagesLayer(m_d->document->shapeController(), image); m_d->document->setReferenceImagesLayer(layer, false); for (QDomElement child = elem.firstChildElement(); !child.isNull(); child = child.nextSiblingElement()) { if (child.nodeName().toLower() == "referenceimage") { auto* reference = KisReferenceImage::fromXml(child); layer->addShape(reference); } } return layer; } diff --git a/plugins/impex/libkra/tests/kis_kra_loader_test.cpp b/plugins/impex/libkra/tests/kis_kra_loader_test.cpp index 500295dcd7..d00e04955c 100644 --- a/plugins/impex/libkra/tests/kis_kra_loader_test.cpp +++ b/plugins/impex/libkra/tests/kis_kra_loader_test.cpp @@ -1,192 +1,192 @@ /* * Copyright (c) 2007 Boudewijn Rempt boud@valdyas.org * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) 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_kra_loader_test.h" #include #include #include #include #include #include #include "kis_image.h" #include "testutil.h" #include "KisPart.h" #include #include #include "kis_image_animation_interface.h" #include "kis_keyframe_channel.h" #include "kis_time_range.h" #include #include const QString KraMimetype = "application/x-krita"; void KisKraLoaderTest::initTestCase() { KisFilterRegistry::instance(); KisGeneratorRegistry::instance(); } void KisKraLoaderTest::testLoading() { QScopedPointer doc(KisPart::instance()->createDocument()); doc->loadNativeFormat(QString(FILES_DATA_DIR) + QDir::separator() + "load_test.kra"); KisImageSP image = doc->image(); image->lock(); QCOMPARE(image->nlayers(), 12); QCOMPARE(doc->documentInfo()->aboutInfo("title"), QString("test image for loading")); QCOMPARE(image->height(), 753); QCOMPARE(image->width(), 1000); QCOMPARE(image->colorSpace()->id(), KoColorSpaceRegistry::instance()->rgb8()->id()); KisNodeSP node = image->root()->firstChild(); QVERIFY(node); QCOMPARE(node->name(), QString("Background")); QVERIFY(node->inherits("KisPaintLayer")); node = node->nextSibling(); QVERIFY(node); QCOMPARE(node->name(), QString("Group 1")); QVERIFY(node->inherits("KisGroupLayer")); QCOMPARE((int) node->childCount(), 2); } void testObligeSingleChildImpl(bool transpDefaultPixel) { QString id = !transpDefaultPixel ? "single_layer_no_channel_flags_nontransp_def_pixel.kra" : "single_layer_no_channel_flags_transp_def_pixel.kra"; QString fileName = TestUtil::fetchDataFileLazy(id); QScopedPointer doc(KisPart::instance()->createDocument()); const bool result = doc->loadNativeFormat(fileName); QVERIFY(result); KisImageSP image = doc->image(); QVERIFY(image); QCOMPARE(image->nlayers(), 2); KisNodeSP root = image->root(); KisNodeSP child = root->firstChild(); QVERIFY(child); QCOMPARE(root->original(), root->projection()); if (transpDefaultPixel) { QCOMPARE(root->original(), child->projection()); } else { QVERIFY(root->original() != child->projection()); } } void KisKraLoaderTest::testObligeSingleChild() { testObligeSingleChildImpl(true); } void KisKraLoaderTest::testObligeSingleChildNonTranspPixel() { testObligeSingleChildImpl(false); } void KisKraLoaderTest::testLoadAnimated() { QScopedPointer doc(KisPart::instance()->createDocument()); doc->loadNativeFormat(QString(FILES_DATA_DIR) + QDir::separator() + "load_test_animation.kra"); KisImageSP image = doc->image(); KisNodeSP node1 = image->root()->firstChild(); KisNodeSP node2 = node1->nextSibling(); QVERIFY(node1->inherits("KisPaintLayer")); QVERIFY(node2->inherits("KisPaintLayer")); KisPaintLayerSP layer1 = qobject_cast(node1.data()); KisPaintLayerSP layer2 = qobject_cast(node2.data()); QVERIFY(layer1->isAnimated()); QVERIFY(!layer2->isAnimated()); KisKeyframeChannel *channel1 = layer1->getKeyframeChannel(KisKeyframeChannel::Content.id()); QVERIFY(channel1); QCOMPARE(channel1->keyframeCount(), 3); QCOMPARE(image->animationInterface()->framerate(), 17); - QCOMPARE(image->animationInterface()->fullClipRange(), KisTimeRange::fromTime(15, 45)); + QCOMPARE(image->animationInterface()->fullClipRange(), KisTimeSpan(15, 45)); QCOMPARE(image->animationInterface()->currentTime(), 19); KisPaintDeviceSP dev = layer1->paintDevice(); const KoColorSpace *cs = dev->colorSpace(); KoColor transparent(Qt::transparent, cs); KoColor white(Qt::white, cs); KoColor red(Qt::red, cs); image->animationInterface()->switchCurrentTimeAsync(0); image->waitForDone(); QCOMPARE(dev->exactBounds(), QRect(506, 378, 198, 198)); QCOMPARE(dev->x(), -26); QCOMPARE(dev->y(), -128); QCOMPARE(dev->defaultPixel(), transparent); image->animationInterface()->switchCurrentTimeAsync(20); image->waitForDone(); QCOMPARE(dev->nonDefaultPixelArea(), QRect(615, 416, 129, 129)); QCOMPARE(dev->x(), 502); QCOMPARE(dev->y(), 224); QCOMPARE(dev->defaultPixel(), white); image->animationInterface()->switchCurrentTimeAsync(30); image->waitForDone(); QCOMPARE(dev->nonDefaultPixelArea(), QRect(729, 452, 45, 44)); QCOMPARE(dev->x(), 645); QCOMPARE(dev->y(), -10); QCOMPARE(dev->defaultPixel(), red); } void KisKraLoaderTest::testImportFromWriteonly() { TestUtil::testImportFromWriteonly(QString(FILES_DATA_DIR), KraMimetype); } void KisKraLoaderTest::testImportIncorrectFormat() { TestUtil::testImportIncorrectFormat(QString(FILES_DATA_DIR), KraMimetype); } KISTEST_MAIN(KisKraLoaderTest) diff --git a/plugins/tools/tool_transform2/kis_animated_transform_parameters.cpp b/plugins/tools/tool_transform2/kis_animated_transform_parameters.cpp index b3ba975470..38dd84d049 100644 --- a/plugins/tools/tool_transform2/kis_animated_transform_parameters.cpp +++ b/plugins/tools/tool_transform2/kis_animated_transform_parameters.cpp @@ -1,309 +1,311 @@ /* * Copyright (c) 2016 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_animated_transform_parameters.h" #include "kis_scalar_keyframe_channel.h" #include "kis_transform_args_keyframe_channel.h" #include "tool_transform_args.h" #include "kis_time_range.h" #include "kis_transform_mask.h" struct KisAnimatedTransformMaskParameters::Private { Private() : hidden(false), argsCache() {} KisTransformArgsKeyframeChannel *rawArgsChannel{0}; KisScalarKeyframeChannel *positionXchannel{0}; KisScalarKeyframeChannel *positionYchannel{0}; KisScalarKeyframeChannel *scaleXchannel{0}; KisScalarKeyframeChannel *scaleYchannel{0}; KisScalarKeyframeChannel *shearXchannel{0}; KisScalarKeyframeChannel *shearYchannel{0}; KisScalarKeyframeChannel *rotationXchannel{0}; KisScalarKeyframeChannel *rotationYchannel{0}; KisScalarKeyframeChannel *rotationZchannel{0}; bool hidden; - KisTimeRange validRange; + KisFrameSet validRange; ToolTransformArgs argsCache; ToolTransformArgs ¤tRawArgs() { if (!rawArgsChannel) return argsCache; KisKeyframeSP keyframe = rawArgsChannel->currentlyActiveKeyframe(); if (keyframe.isNull()) return argsCache; return rawArgsChannel->transformArgs(keyframe); } KisScalarKeyframeChannel *getChannel(KisScalarKeyframeChannel * Private::*field, const KoID &channelId, KisDefaultBoundsBaseSP defaultBounds) { KisScalarKeyframeChannel *channel = this->*field; if (!channel) { channel = this->*field = new KisScalarKeyframeChannel(channelId, -qInf(), qInf(), defaultBounds, KisKeyframe::Linear); } return channel; } }; KisAnimatedTransformMaskParameters::KisAnimatedTransformMaskParameters() : KisTransformMaskAdapter(ToolTransformArgs()), m_d(new Private()) { } KisAnimatedTransformMaskParameters::KisAnimatedTransformMaskParameters(const KisTransformMaskAdapter *staticTransform) : KisTransformMaskAdapter(staticTransform->transformArgs()), m_d(new Private()) { m_d->argsCache = staticTransform->transformArgs(); } KisAnimatedTransformMaskParameters::~KisAnimatedTransformMaskParameters() {} QPointF getInterpolatedPoint(QPointF def, KisScalarKeyframeChannel *xChannel, KisScalarKeyframeChannel *yChannel) { if (xChannel) { qreal x = xChannel->currentValue(); if (!qIsNaN(x)) def.setX(x); } if (yChannel) { qreal y = yChannel->currentValue(); if (!qIsNaN(y)) def.setY(y); } return def; } qreal getInterpolatedValue(KisScalarKeyframeChannel *channel, qreal defaultValue) { if (!channel) return defaultValue; qreal value = channel->currentValue(); if (qIsNaN(value)) return defaultValue; return value; } const ToolTransformArgs &KisAnimatedTransformMaskParameters::transformArgs() const { m_d->argsCache = m_d->currentRawArgs(); QPointF pos = getInterpolatedPoint(m_d->argsCache.transformedCenter(), m_d->positionXchannel, m_d->positionYchannel); m_d->argsCache.setTransformedCenter(pos); m_d->argsCache.setScaleX(getInterpolatedValue(m_d->scaleXchannel, m_d->argsCache.scaleX())); m_d->argsCache.setScaleY(getInterpolatedValue(m_d->scaleYchannel, m_d->argsCache.scaleY())); m_d->argsCache.setShearX(getInterpolatedValue(m_d->shearXchannel, m_d->argsCache.shearX())); m_d->argsCache.setShearY(getInterpolatedValue(m_d->shearYchannel, m_d->argsCache.shearY())); m_d->argsCache.setAX(normalizeAngle(getInterpolatedValue(m_d->rotationXchannel, m_d->argsCache.aX()))); m_d->argsCache.setAY(normalizeAngle(getInterpolatedValue(m_d->rotationYchannel, m_d->argsCache.aY()))); m_d->argsCache.setAZ(normalizeAngle(getInterpolatedValue(m_d->rotationZchannel, m_d->argsCache.aZ()))); return m_d->argsCache; } QString KisAnimatedTransformMaskParameters::id() const { return "animatedtransformparams"; } void KisAnimatedTransformMaskParameters::toXML(QDomElement *e) const { Q_UNUSED(e); } KisTransformMaskParamsInterfaceSP KisAnimatedTransformMaskParameters::fromXML(const QDomElement &e) { Q_UNUSED(e); return toQShared(new KisAnimatedTransformMaskParameters()); } KisTransformMaskParamsInterfaceSP KisAnimatedTransformMaskParameters::animate(KisTransformMaskParamsInterfaceSP params) { KisTransformMaskParamsInterface *animatedParams; KisTransformMaskAdapter *tma = dynamic_cast(params.data()); if (tma) { animatedParams = new KisAnimatedTransformMaskParameters(tma); } else { animatedParams = new KisAnimatedTransformMaskParameters(); } return toQShared(animatedParams); } void KisAnimatedTransformMaskParameters::translate(const QPointF &offset) { ToolTransformArgs &args = m_d->currentRawArgs(); args.translate(offset); } KisKeyframeChannel *KisAnimatedTransformMaskParameters::getKeyframeChannel(const QString &id, KisDefaultBoundsBaseSP defaultBounds) { if (id == KisKeyframeChannel::TransformArguments.id()) { if (!m_d->rawArgsChannel) { m_d->rawArgsChannel = new KisTransformArgsKeyframeChannel(KisKeyframeChannel::TransformArguments, defaultBounds, m_d->currentRawArgs()); } return m_d->rawArgsChannel; } else { KisScalarKeyframeChannel * Private::*field = 0; KoID channelId; if (id == KisKeyframeChannel::TransformPositionX.id()) { channelId = KisKeyframeChannel::TransformPositionX; field = &Private::positionXchannel; } else if (id == KisKeyframeChannel::TransformPositionY.id()) { channelId = KisKeyframeChannel::TransformPositionY; field = &Private::positionYchannel; } else if (id == KisKeyframeChannel::TransformScaleX.id()) { channelId = KisKeyframeChannel::TransformScaleX; field = &Private::scaleXchannel; } else if (id == KisKeyframeChannel::TransformScaleY.id()) { channelId = KisKeyframeChannel::TransformScaleY; field = &Private::scaleYchannel; } else if (id == KisKeyframeChannel::TransformShearX.id()) { channelId = KisKeyframeChannel::TransformShearX; field = &Private::shearXchannel; } else if (id == KisKeyframeChannel::TransformShearY.id()) { channelId = KisKeyframeChannel::TransformShearY; field = &Private::shearYchannel; } else if (id == KisKeyframeChannel::TransformRotationX.id()) { channelId = KisKeyframeChannel::TransformRotationX; field = &Private::rotationXchannel; } else if (id == KisKeyframeChannel::TransformRotationY.id()) { channelId = KisKeyframeChannel::TransformRotationY; field = &Private::rotationYchannel; } else if (id == KisKeyframeChannel::TransformRotationZ.id()) { channelId = KisKeyframeChannel::TransformRotationZ; field = &Private::rotationZchannel; } if (field) { return m_d->getChannel(field, channelId, defaultBounds); } } return 0; } bool KisAnimatedTransformMaskParameters::isHidden() const { return m_d->hidden; } void KisAnimatedTransformMaskParameters::setHidden(bool hidden) { m_d->hidden = hidden; } void KisAnimatedTransformMaskParameters::clearChangedFlag() { int currentTime = (m_d->rawArgsChannel) ? m_d->rawArgsChannel->currentTime() : 0; - KisTimeRange validRange = KisTimeRange::infinite(0); + KisFrameSet validRange = KisFrameSet::infiniteFrom(0); + /* To be fixed when the feature development is continued if (m_d->rawArgsChannel) validRange &= m_d->rawArgsChannel->identicalFrames(currentTime); if (m_d->positionXchannel) validRange &= m_d->positionXchannel->identicalFrames(currentTime); if (m_d->positionYchannel) validRange &= m_d->positionYchannel->identicalFrames(currentTime); + */ m_d->validRange = validRange; } bool KisAnimatedTransformMaskParameters::hasChanged() const { int currentTime = (m_d->rawArgsChannel) ? m_d->rawArgsChannel->currentTime() : 0; bool valid = m_d->validRange.contains(currentTime); return !valid; } bool KisAnimatedTransformMaskParameters::isAnimated() const { return true; } void setScalarChannelValue(KisTransformMaskSP mask, const KoID &channelId, int time, qreal value, KUndo2Command *parentCommand) { KisScalarKeyframeChannel *channel = dynamic_cast(mask->getKeyframeChannel(channelId.id(), true)); KIS_ASSERT_RECOVER_RETURN(channel); new KisScalarKeyframeChannel::AddKeyframeCommand(channel, time, value, parentCommand); } void KisAnimatedTransformMaskParameters::addKeyframes(KisTransformMaskSP mask, int time, KisTransformMaskParamsInterfaceSP params, KUndo2Command *parentCommand) { KisTransformMaskParamsInterfaceSP currentParams = mask->transformParams(); if (dynamic_cast(currentParams.data()) == 0) { mask->setTransformParams(animate(currentParams)); } if (params.isNull()) { params = currentParams; } ToolTransformArgs args; auto *adapterParams = dynamic_cast(params.data()); if (adapterParams) { args = adapterParams->transformArgs(); } else { if (params->isHidden()) return; args.setOriginalCenter(mask->exactBounds().center()); args.setTransformedCenter(args.originalCenter()); // offset? } KisTransformArgsKeyframeChannel *rawArgsChannel = dynamic_cast(mask->getKeyframeChannel(KisKeyframeChannel::TransformArguments.id(), true)); if (rawArgsChannel) { new KisTransformArgsKeyframeChannel::AddKeyframeCommand(rawArgsChannel, time, args, parentCommand); } setScalarChannelValue(mask, KisKeyframeChannel::TransformPositionX, time, args.transformedCenter().x(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformPositionY, time, args.transformedCenter().y(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformScaleX, time, args.scaleX(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformScaleY, time, args.scaleY(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformShearX, time, args.shearX(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformShearY, time, args.shearY(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformRotationX, time, args.aX(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformRotationY, time, args.aY(), parentCommand); setScalarChannelValue(mask, KisKeyframeChannel::TransformRotationZ, time, args.aZ(), parentCommand); } #include "kis_transform_mask_params_factory_registry.h" struct AnimatedTransformParamsRegistrar { AnimatedTransformParamsRegistrar() { KisTransformMaskParamsFactory f(KisAnimatedTransformMaskParameters::fromXML); KisTransformMaskParamsFactoryRegistry::instance()->addFactory("animatedtransformparams", f); KisAnimatedTransformMaskParamsFactory a(KisAnimatedTransformMaskParameters::animate); KisTransformMaskParamsFactoryRegistry::instance()->setAnimatedParamsFactory(a); KisTransformMaskKeyframeFactory k(KisAnimatedTransformMaskParameters::addKeyframes); KisTransformMaskParamsFactoryRegistry::instance()->setKeyframeFactory(k); } }; static AnimatedTransformParamsRegistrar __animatedTransformParamsRegistrar; diff --git a/plugins/tools/tool_transform2/kis_transform_args_keyframe_channel.cpp b/plugins/tools/tool_transform2/kis_transform_args_keyframe_channel.cpp index d4e2301126..9b567bab40 100644 --- a/plugins/tools/tool_transform2/kis_transform_args_keyframe_channel.cpp +++ b/plugins/tools/tool_transform2/kis_transform_args_keyframe_channel.cpp @@ -1,124 +1,123 @@ /* * Copyright (c) 2016 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_transform_args_keyframe_channel.h" struct KisTransformArgsKeyframe : public KisKeyframe { KisTransformArgsKeyframe(KisTransformArgsKeyframeChannel *channel, int time) : KisKeyframe(channel, time) {} KisTransformArgsKeyframe(KisTransformArgsKeyframeChannel *channel, int time, const ToolTransformArgs &args) : KisKeyframe(channel, time) , args(args) {} KisTransformArgsKeyframe(const KisTransformArgsKeyframe *rhs, KisKeyframeChannel *channel) : KisKeyframe(rhs, channel) , args(rhs->args) {} ToolTransformArgs args; KisKeyframeSP cloneFor(KisKeyframeChannel *channel) const override { KisTransformArgsKeyframeChannel *argsChannel = dynamic_cast(channel); Q_ASSERT(argsChannel); return toQShared(new KisTransformArgsKeyframe(this, channel)); } + + QRect affectedRect() const override + { + // TODO + return QRect(); + } }; KisTransformArgsKeyframeChannel::AddKeyframeCommand::AddKeyframeCommand(KisTransformArgsKeyframeChannel *channel, int time, const ToolTransformArgs &args, KUndo2Command *parentCommand) : KisReplaceKeyframeCommand(channel, time, toQShared(new KisTransformArgsKeyframe(channel, time, args)), parentCommand) { } KisTransformArgsKeyframeChannel::KisTransformArgsKeyframeChannel(const KoID &id, KisDefaultBoundsBaseSP defaultBounds, const ToolTransformArgs &initialValue) : KisKeyframeChannel(id, defaultBounds) { KisKeyframeSP keyframe = addKeyframe(0); KisTransformArgsKeyframe *argsKeyframe = dynamic_cast(keyframe.data()); argsKeyframe->args = initialValue; } ToolTransformArgs &KisTransformArgsKeyframeChannel::transformArgs(KisKeyframeSP keyframe) const { KisTransformArgsKeyframe *key = dynamic_cast(keyframe.data()); Q_ASSERT(key != 0); return key->args; } bool KisTransformArgsKeyframeChannel::hasScalarValue() const { return false; } KisKeyframeSP KisTransformArgsKeyframeChannel::createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) { Q_UNUSED(parentCommand); KisTransformArgsKeyframe *srcKey = dynamic_cast(copySrc.data()); KisTransformArgsKeyframe *newKey; if (srcKey) { newKey = new KisTransformArgsKeyframe(this, time, srcKey->args); } else { newKey = new KisTransformArgsKeyframe(this, time); } return toQShared(newKey); } void KisTransformArgsKeyframeChannel::destroyKeyframe(KisKeyframeSP, KUndo2Command*) {} void KisTransformArgsKeyframeChannel::uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) { Q_UNUSED(srcChannel); Q_UNUSED(srcTime); Q_UNUSED(dstFrame); } -QRect KisTransformArgsKeyframeChannel::affectedRect(KisKeyframeSP key) -{ - Q_UNUSED(key); - // TODO - return QRect(); -} - KisKeyframeSP KisTransformArgsKeyframeChannel::loadKeyframe(const QDomElement &keyframeNode) { ToolTransformArgs args; args.fromXML(keyframeNode); int time = keyframeNode.attribute("time").toInt(); workaroundBrokenFrameTimeBug(&time); KisTransformArgsKeyframe *keyframe = new KisTransformArgsKeyframe(this, time, args); return toQShared(keyframe); } void KisTransformArgsKeyframeChannel::saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) { Q_UNUSED(layerFilename); KisTransformArgsKeyframe *key = dynamic_cast(keyframe.data()); KIS_ASSERT_RECOVER_RETURN(key); key->args.toXML(&keyframeElement); } diff --git a/plugins/tools/tool_transform2/kis_transform_args_keyframe_channel.h b/plugins/tools/tool_transform2/kis_transform_args_keyframe_channel.h index 05c7a169cd..5d609744c2 100644 --- a/plugins/tools/tool_transform2/kis_transform_args_keyframe_channel.h +++ b/plugins/tools/tool_transform2/kis_transform_args_keyframe_channel.h @@ -1,49 +1,48 @@ /* * Copyright (c) 2016 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_TRANSFORM_ARGS_KEYFRAME_CHANNEL_H #define _KIS_TRANSFORM_ARGS_KEYFRAME_CHANNEL_H #include "kis_keyframe_channel.h" #include "kis_keyframe_commands.h" #include "tool_transform_args.h" #include "kundo2command.h" class KisTransformArgsKeyframeChannel : public KisKeyframeChannel { public: struct AddKeyframeCommand : public KisReplaceKeyframeCommand { AddKeyframeCommand(KisTransformArgsKeyframeChannel *channel, int time, const ToolTransformArgs &args, KUndo2Command *parentCommand); }; KisTransformArgsKeyframeChannel(const KoID &id, KisDefaultBoundsBaseSP defaultBounds, const ToolTransformArgs &initialValue); ToolTransformArgs &transformArgs(KisKeyframeSP keyframe) const; bool hasScalarValue() const override; protected: KisKeyframeSP createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) override; void destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) override; void uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) override; - QRect affectedRect(KisKeyframeSP key) override; KisKeyframeSP loadKeyframe(const QDomElement &keyframeNode) override; void saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) override; }; #endif