diff --git a/libs/image/CMakeLists.txt b/libs/image/CMakeLists.txt index 4144d421d3..1888af3cab 100644 --- a/libs/image/CMakeLists.txt +++ b/libs/image/CMakeLists.txt @@ -1,396 +1,397 @@ add_subdirectory( tests ) add_subdirectory( tiles3 ) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/metadata ${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_CURRENT_SOURCE_DIR}/recorder ${CMAKE_SOURCE_DIR}/sdk/tests ) include_directories(SYSTEM ${EIGEN3_INCLUDE_DIR} ${Boost_INCLUDE_DIRS} ) 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/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_marker_painter.cpp kis_progress_updater.cpp brushengine/kis_paint_information.cc brushengine/kis_random_source.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_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 commands/kis_deselect_global_selection_command.cpp commands/kis_image_change_layers_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_layer_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/kis_set_global_selection_command.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 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 filter/kis_filter.cc 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/kis_colorize_mask.cpp lazybrush/kis_colorize_stroke_strategy.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_clone_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_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 kis_stroke_job_strategy.cpp kis_stroke_strategy.cpp kis_stroke.cpp kis_strokes_queue.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 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 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_signal_compressor.cpp kis_signal_compressor_with_param.cpp kis_thread_safe_signal_compressor.cpp kis_acyclic_signal_connector.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_command_utils.cpp kis_layer_utils.cpp kis_mask_projection_plane.cpp kis_projection_leaf.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 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 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_algebra_2d.cpp kis_transparency_mask.cc kis_undo_store.cpp kis_undo_stores.cpp 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 metadata/kis_meta_data_entry.cc metadata/kis_meta_data_filter.cc metadata/kis_meta_data_filter_p.cc metadata/kis_meta_data_filter_registry.cc metadata/kis_meta_data_filter_registry_model.cc metadata/kis_meta_data_io_backend.cc metadata/kis_meta_data_merge_strategy.cc metadata/kis_meta_data_merge_strategy_p.cc metadata/kis_meta_data_merge_strategy_registry.cc metadata/kis_meta_data_parser.cc metadata/kis_meta_data_schema.cc metadata/kis_meta_data_schema_registry.cc metadata/kis_meta_data_store.cc metadata/kis_meta_data_type_info.cc metadata/kis_meta_data_validator.cc metadata/kis_meta_data_value.cc recorder/kis_action_recorder.cc recorder/kis_macro.cc recorder/kis_macro_player.cc recorder/kis_node_query_path.cc recorder/kis_play_info.cc recorder/kis_recorded_action.cc recorder/kis_recorded_action_factory_registry.cc recorder/kis_recorded_action_load_context.cpp recorder/kis_recorded_action_save_context.cpp recorder/kis_recorded_filter_action.cpp recorder/kis_recorded_fill_paint_action.cpp recorder/kis_recorded_node_action.cc recorder/kis_recorded_paint_action.cpp recorder/kis_recorded_path_paint_action.cpp recorder/kis_recorded_shape_paint_action.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_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 KisProofingConfiguration.cpp ) 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 kritaundo2 kritawidgetutils Qt5::Concurrent ) target_link_libraries(kritaimage PUBLIC ${Boost_SYSTEM_LIBRARY}) 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}) if (NOT PACKAGERS_BUILD) set_property(TARGET kritaimage APPEND PROPERTY COMPILE_OPTIONS "${Vc_ARCHITECTURE_FLAGS}") endif() 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}) ########### install schemas ############# install( FILES metadata/schemas/dc.schema metadata/schemas/exif.schema metadata/schemas/tiff.schema metadata/schemas/mkn.schema metadata/schemas/xmp.schema metadata/schemas/xmpmm.schema metadata/schemas/xmprights.schema DESTINATION ${DATA_INSTALL_DIR}/krita/metadata/schemas) diff --git a/libs/image/commands_new/kis_switch_current_time_command.cpp b/libs/image/commands_new/kis_switch_current_time_command.cpp index 67f31b5a37..2cb76bfb37 100644 --- a/libs/image/commands_new/kis_switch_current_time_command.cpp +++ b/libs/image/commands_new/kis_switch_current_time_command.cpp @@ -1,66 +1,66 @@ /* * 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_switch_current_time_command.h" #include "kis_image.h" #include "kis_image_animation_interface.h" -KisSwitchCurrentTimeCommand::KisSwitchCurrentTimeCommand(KisImageSP image, int newTime, KUndo2Command *parent) +KisSwitchCurrentTimeCommand::KisSwitchCurrentTimeCommand(KisImageAnimationInterface *animation, int oldTime, int newTime, KUndo2Command *parent) : KUndo2Command(kundo2_i18n("Switch current time"), parent), - m_image(image), - m_oldTime(image->animationInterface()->currentUITime()), + m_animation(animation), + m_oldTime(oldTime), m_newTime(newTime) { } KisSwitchCurrentTimeCommand::~KisSwitchCurrentTimeCommand() { } int KisSwitchCurrentTimeCommand::id() const { // we don't have a common commands id source in Krita yet, so // just use a random one ;) return 4 + 8 + 15 + 16 + 23 + 42; } bool KisSwitchCurrentTimeCommand::mergeWith(const KUndo2Command* command) { const KisSwitchCurrentTimeCommand *other = dynamic_cast(command); if (!other || other->id() != id()) { return false; } m_newTime = other->m_newTime; return true; } void KisSwitchCurrentTimeCommand::redo() { - m_image->animationInterface()->requestTimeSwitchNonGUI(m_newTime); + m_animation->requestTimeSwitchNonGUI(m_newTime); } void KisSwitchCurrentTimeCommand::undo() { - m_image->animationInterface()->requestTimeSwitchNonGUI(m_oldTime); + m_animation->requestTimeSwitchNonGUI(m_oldTime); } diff --git a/libs/image/commands_new/kis_switch_current_time_command.h b/libs/image/commands_new/kis_switch_current_time_command.h index 404283c155..f4a9538512 100644 --- a/libs/image/commands_new/kis_switch_current_time_command.h +++ b/libs/image/commands_new/kis_switch_current_time_command.h @@ -1,46 +1,48 @@ /* * 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_SWITCH_CURRENT_TIME_COMMAND_H #define __KIS_SWITCH_CURRENT_TIME_COMMAND_H #include "kritaimage_export.h" #include "kis_types.h" #include +class KisImageAnimationInterface; + class KRITAIMAGE_EXPORT KisSwitchCurrentTimeCommand : public KUndo2Command { public: - KisSwitchCurrentTimeCommand(KisImageSP image, int newTime, KUndo2Command *parent = 0); + KisSwitchCurrentTimeCommand(KisImageAnimationInterface *animation, int oldTime, int newTime, KUndo2Command *parent = 0); ~KisSwitchCurrentTimeCommand(); void redo(); void undo(); int id() const; bool mergeWith(const KUndo2Command* command); private: - KisImageWSP m_image; + KisImageAnimationInterface *m_animation; int m_oldTime; int m_newTime; }; #endif /* __KIS_SWITCH_CURRENT_TIME_COMMAND_H */ diff --git a/libs/image/kis_image_animation_interface.cpp b/libs/image/kis_image_animation_interface.cpp index 1b32193a26..79d8d543b9 100644 --- a/libs/image/kis_image_animation_interface.cpp +++ b/libs/image/kis_image_animation_interface.cpp @@ -1,293 +1,321 @@ /* * 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 "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), - currentTime(0), - currentUITime(0), externalFrameActive(false), frameInvalidationBlocked(false), - cachedLastFrameValue(-1) + cachedLastFrameValue(-1), + m_currentTime(0), + m_currentUITime(0), + m_currentTimeLod(0) { } KisImage *image; - int currentTime; - int currentUITime; bool externalFrameActive; bool frameInvalidationBlocked; KisTimeRange fullClipRange; KisTimeRange playbackRange; int framerate; int cachedLastFrameValue; + + inline int currentTime() const { + return image->currentLevelOfDetail() ? m_currentTime : m_currentTimeLod; + } + + inline int currentUITime() const { + return m_currentUITime; + } + inline void setCurrentTime(int value) { + image->currentLevelOfDetail() ? m_currentTime : m_currentTimeLod = value; + } + + inline void setCurrentUITime(int value) { + m_currentUITime = value; + } +private: + int m_currentTime; + int m_currentUITime; + int m_currentTimeLod; }; KisImageAnimationInterface::KisImageAnimationInterface(KisImage *image) : m_d(new Private) { m_d->image = image; m_d->framerate = 24; m_d->fullClipRange = KisTimeRange::fromTime(0, 100); - connect(this, SIGNAL(sigInternalRequestTimeSwitch(int)), SLOT(switchCurrentTimeAsync(int))); + 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; + return m_d->currentTime(); } int KisImageAnimationInterface::currentUITime() const { - return m_d->currentUITime; + return m_d->currentUITime(); } const KisTimeRange& KisImageAnimationInterface::fullClipRange() const { return m_d->fullClipRange; } void KisImageAnimationInterface::setFullClipRange(const KisTimeRange range) { m_d->fullClipRange = range; emit sigFullClipRangeChanged(); } const KisTimeRange& KisImageAnimationInterface::playbackRange() const { return m_d->playbackRange.isValid() ? m_d->playbackRange : m_d->fullClipRange; } void KisImageAnimationInterface::setPlaybackRange(const KisTimeRange range) { m_d->playbackRange = range; emit sigPlaybackRangeChanged(); } int KisImageAnimationInterface::framerate() const { return m_d->framerate; } 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 (m_d->currentUITime == time) return; - - KisSwitchCurrentTimeCommand *cmd = - new KisSwitchCurrentTimeCommand(m_d->image, time); - - cmd->redo(); - m_d->image->postExecutionUndoAdapter()->addCommand(toQShared(cmd)); + 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) +void KisImageAnimationInterface::requestTimeSwitchNonGUI(int time, bool useUndo) { - emit sigInternalRequestTimeSwitch(time); + emit sigInternalRequestTimeSwitch(time, useUndo); } -void KisImageAnimationInterface::switchCurrentTimeAsync(int frameId) +void KisImageAnimationInterface::explicitlySetCurrentTime(int frameId) { - if (m_d->currentUITime == frameId) return; + m_d->setCurrentTime(frameId); +} - m_d->image->barrierLock(); - m_d->currentTime = frameId; - m_d->currentUITime = frameId; - m_d->image->unlock(); +void KisImageAnimationInterface::switchCurrentTimeAsync(int frameId, bool useUndo) +{ + if (currentUITime() == frameId) return; - KisStrokeStrategy *strategy = - new KisRegenerateFrameStrokeStrategy(this); + { + KisPostExecutionUndoAdapter *undoAdapter = useUndo ? + m_d->image->postExecutionUndoAdapter() : 0; - KisStrokeId stroke = m_d->image->startStroke(strategy); - m_d->image->endStroke(stroke); + KisStrokeStrategy *strategy = + new KisSwitchTimeStrokeStrategy(frameId, this, undoAdapter); + + KisStrokeId stroke = m_d->image->startStroke(strategy); + m_d->image->endStroke(stroke); + } + + { + KisStrokeStrategy *strategy = + new KisRegenerateFrameStrokeStrategy(this); + + KisStrokeId stroke = m_d->image->startStroke(strategy); + m_d->image->endStroke(stroke); + } - emit sigTimeChanged(frameId); + 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->currentTime = frameId; + *savedValue = m_d->currentTime(); + m_d->setCurrentTime(frameId); } void KisImageAnimationInterface::restoreCurrentTime(int *savedValue) { - m_d->currentTime = *savedValue; + m_d->setCurrentTime(*savedValue); m_d->externalFrameActive = false; } void KisImageAnimationInterface::notifyFrameReady() { - emit sigFrameReady(m_d->currentTime); + 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) { if (externalFrameActive() || m_d->frameInvalidationBlocked) return; if (node->inherits("KisSelectionMask")) return; KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (recursive) { KisTimeRange affectedRange; KisTimeRange::calculateTimeRangeRecursive(node, currentTime(), affectedRange, false); invalidateFrames(affectedRange, rect); } else if (channel) { - const int currentTime = m_d->currentTime; + const int currentTime = m_d->currentTime(); invalidateFrames(channel->affectedFrames(currentTime), rect); } else { invalidateFrames(KisTimeRange::infinite(0), rect); } } void KisImageAnimationInterface::invalidateFrames(const KisTimeRange &range, const QRect &rect) { m_d->cachedLastFrameValue = -1; emit sigFramesChanged(range, rect); } void KisImageAnimationInterface::blockFrameInvalidation(bool value) { m_d->frameInvalidationBlocked = value; } int findLastKeyframeTimeRecursive(KisNodeSP node) { int time = 0; KisKeyframeChannel *channel; Q_FOREACH (channel, node->keyframeChannels()) { KisKeyframeSP keyframe = channel->lastKeyframe(); if (keyframe) { time = std::max(time, keyframe->time()); } } KisNodeSP child = node->firstChild(); while (child) { time = std::max(time, findLastKeyframeTimeRecursive(child)); child = child->nextSibling(); } return time; } int KisImageAnimationInterface::totalLength() { if (m_d->cachedLastFrameValue < 0) { m_d->cachedLastFrameValue = findLastKeyframeTimeRecursive(m_d->image->root()); } int lastKey = m_d->cachedLastFrameValue; lastKey = std::max(lastKey, m_d->fullClipRange.end()); - lastKey = std::max(lastKey, m_d->currentUITime); + 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 b59a5e1f48..c077d872c7 100644 --- a/libs/image/kis_image_animation_interface.h +++ b/libs/image/kis_image_animation_interface.h @@ -1,162 +1,165 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_IMAGE_ANIMATION_INTERFACE_H #define __KIS_IMAGE_ANIMATION_INTERFACE_H #include #include #include "kis_types.h" #include "kritaimage_export.h" class KisUpdatesFacade; class KisTimeRange; class KoColor; namespace KisLayerUtils { struct SwitchFrameCommand; } class KRITAIMAGE_EXPORT KisImageAnimationInterface : public QObject { Q_OBJECT public: KisImageAnimationInterface(KisImage *image); ~KisImageAnimationInterface(); /** * 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); + 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); + void switchCurrentTimeAsync(int frameId, bool useUndo = false); public: /** * Start a backgroud 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 invalidateFrames(const KisTimeRange &range, const QRect &rect); /** * Changes the default color of the "external frame" projection of * the image's root layer. Please note that this command should be * executed from a context of an exclusive job! */ void setDefaultProjectionColor(const KoColor &color); /** * The current time range selected by user. * @return current time range */ const KisTimeRange& fullClipRange() const; void setFullClipRange(const KisTimeRange range); const KisTimeRange &playbackRange() const; void setPlaybackRange(const KisTimeRange range); int framerate() const; 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 sigTimeChanged(int newTime); + void sigUiTimeChanged(int newTime); void sigFramesChanged(const KisTimeRange &range, const QRect &rect); - void sigInternalRequestTimeSwitch(int frameId); + void sigInternalRequestTimeSwitch(int frameId, bool useUndo); void sigFramerateChanged(); void sigFullClipRangeChanged(); void sigPlaybackRangeChanged(); private: struct Private; const QScopedPointer m_d; }; #endif /* __KIS_IMAGE_ANIMATION_INTERFACE_H */ diff --git a/libs/image/kis_switch_time_stroke_strategy.cpp b/libs/image/kis_switch_time_stroke_strategy.cpp new file mode 100644 index 0000000000..94b026b69a --- /dev/null +++ b/libs/image/kis_switch_time_stroke_strategy.cpp @@ -0,0 +1,78 @@ +/* + * 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_switch_time_stroke_strategy.h" + +#include "kis_image_animation_interface.h" +#include "kis_post_execution_undo_adapter.h" +#include "commands_new/kis_switch_current_time_command.h" + + +struct KisSwitchTimeStrokeStrategy::Private +{ + int frameId; + KisImageAnimationInterface *interface; + KisPostExecutionUndoAdapter *undoAdapter; +}; + +KisSwitchTimeStrokeStrategy::KisSwitchTimeStrokeStrategy(int frameId, + KisImageAnimationInterface *interface, + KisPostExecutionUndoAdapter *undoAdapter) + : KisSimpleStrokeStrategy("switch_current_frame_stroke", kundo2_i18n("Switch Frames")), + m_d(new Private) +{ + m_d->frameId = frameId; + m_d->interface = interface; + m_d->undoAdapter = undoAdapter; + + + enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); + + // switching frames is a distinct user action, so it should + // cancel the playback or any action easily + setRequestsOtherStrokesToEnd(true); + setClearsRedoOnStart(false); +} + +KisSwitchTimeStrokeStrategy::~KisSwitchTimeStrokeStrategy() +{ +} +void KisSwitchTimeStrokeStrategy::initStrokeCallback() +{ + if (m_d->frameId == m_d->interface->currentTime()) return; + + const int oldTime = m_d->interface->currentTime(); + m_d->interface->explicitlySetCurrentTime(m_d->frameId); + + if (m_d->undoAdapter) { + KUndo2CommandSP cmd( + new KisSwitchCurrentTimeCommand(m_d->interface, + oldTime, + m_d->frameId)); + m_d->undoAdapter->addCommand(cmd); + } +} +KisStrokeStrategy* KisSwitchTimeStrokeStrategy::createLodClone(int levelOfDetail) +{ + Q_UNUSED(levelOfDetail); + + /** + * We need to regenerate animation frames on LodN level as well + */ + return new KisSwitchTimeStrokeStrategy(m_d->frameId, m_d->interface, 0); +} diff --git a/libs/image/kis_switch_time_stroke_strategy.h b/libs/image/kis_switch_time_stroke_strategy.h new file mode 100644 index 0000000000..8fc1eec104 --- /dev/null +++ b/libs/image/kis_switch_time_stroke_strategy.h @@ -0,0 +1,53 @@ +/* + * 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_SWITCH_TIME_STROKE_STRATEGY_H +#define __KIS_SWITCH_TIME_STROKE_STRATEGY_H + +#include + +#include + +class KisImageAnimationInterface; +class KisPostExecutionUndoAdapter; + + +class KisSwitchTimeStrokeStrategy : public KisSimpleStrokeStrategy +{ +public: + /** + * Switches current time to \p frameId + * + * NOTE: in contrast to the other c-tor, refreshing current frame + * *does* end all the running stroke, because it is not a + * background action, but a distinct user action. + */ + KisSwitchTimeStrokeStrategy(int frameId, + KisImageAnimationInterface *interface, + KisPostExecutionUndoAdapter *undoAdapter); + ~KisSwitchTimeStrokeStrategy(); + + void initStrokeCallback(); + KisStrokeStrategy* createLodClone(int levelOfDetail); + +private: + struct Private; + const QScopedPointer m_d; +}; + +#endif /* __KIS_SWITCH_TIME_STROKE_STRATEGY_H */ diff --git a/libs/image/tests/kis_image_animation_interface_test.cpp b/libs/image/tests/kis_image_animation_interface_test.cpp index 988b93afea..f6ab260595 100644 --- a/libs/image/tests/kis_image_animation_interface_test.cpp +++ b/libs/image/tests/kis_image_animation_interface_test.cpp @@ -1,271 +1,308 @@ /* * 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(); dev1->createKeyframeChannel(KisKeyframeChannel::Content); dev2->createKeyframeChannel(KisKeyframeChannel::Content); // 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))); 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)); i->notifyNodeChanged(layer2.data(), QRect(), false); QCOMPARE(spy.count(), 1); arguments = spy.takeFirst(); QCOMPARE(arguments.at(0).value(), KisTimeRange(10, 10)); // Recursive channel = dev1->keyframeChannel(); channel->addKeyframe(13); spy.clear(); i->notifyNodeChanged(p.image->root().data(), QRect(), true); QCOMPARE(spy.count(), 1); arguments = spy.takeFirst(); QCOMPARE(arguments.at(0).value(), KisTimeRange::infinite(10)); } 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_image_animation_interface_test.h b/libs/image/tests/kis_image_animation_interface_test.h index 3ea63fe6de..b8cc5bf78c 100644 --- a/libs/image/tests/kis_image_animation_interface_test.h +++ b/libs/image/tests/kis_image_animation_interface_test.h @@ -1,48 +1,48 @@ /* * 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_TEST_H #define __KIS_IMAGE_ANIMATION_INTERFACE_TEST_H #include #include #include "kis_types.h" #include "kis_image.h" class KisImageAnimationInterfaceTest : public QObject { Q_OBJECT private Q_SLOTS: void testFrameRegeneration(); void testFramesChangedSignal(); void testAnimationCompositionBug(); void testSwitchFrameWithUndo(); - + void testSwitchFrameHangup(); void slotFrameDone(); private: KisImageSP m_image; QImage m_compositedFrame; }; #endif /* __KIS_IMAGE_ANIMATION_INTERFACE_TEST_H */ diff --git a/plugins/dockers/animation/animation_docker.cpp b/plugins/dockers/animation/animation_docker.cpp index a6be3ea426..e5e9c88db6 100644 --- a/plugins/dockers/animation/animation_docker.cpp +++ b/plugins/dockers/animation/animation_docker.cpp @@ -1,663 +1,663 @@ /* * 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); 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 / Pause"), m_animationWidget->btnPlay); m_playPauseAction->setActivationFlags(KisAction::ACTIVE_IMAGE); m_animationWidget->btnPlay->setDefaultAction(m_playPauseAction); m_addBlankFrameAction = new KisAction(KisAnimationUtils::addFrameActionName, m_animationWidget->btnAddKeyframe); m_addBlankFrameAction->setActivationFlags(KisAction::ACTIVE_NODE); m_animationWidget->btnAddKeyframe->setDefaultAction(m_addBlankFrameAction); m_addDuplicateFrameAction = new KisAction(KisAnimationUtils::duplicateFrameActionName, m_animationWidget->btnAddDuplicateFrame); m_addDuplicateFrameAction->setActivationFlags(KisAction::ACTIVE_DEVICE); m_animationWidget->btnAddDuplicateFrame->setDefaultAction(m_addDuplicateFrameAction); m_deleteKeyframeAction = new KisAction(KisAnimationUtils::removeFrameActionName, m_animationWidget->btnDeleteKeyframe); m_deleteKeyframeAction->setActivationFlags(KisAction::ACTIVE_NODE); m_animationWidget->btnDeleteKeyframe->setDefaultAction(m_deleteKeyframeAction); m_newKeyframeMenu = new QMenu(this); m_animationWidget->btnAddKeyframe->setMenu(m_newKeyframeMenu); m_animationWidget->btnAddKeyframe->setPopupMode(QToolButton::MenuButtonPopup); m_deleteKeyframeMenu = new QMenu(this); m_animationWidget->btnDeleteKeyframe->setMenu(m_deleteKeyframeMenu); m_animationWidget->btnDeleteKeyframe->setPopupMode(QToolButton::MenuButtonPopup); m_addOpacityKeyframeAction = new KisAction(KisAnimationUtils::addOpacityKeyframeActionName); m_deleteOpacityKeyframeAction = new KisAction(KisAnimationUtils::removeOpacityKeyframeActionName); m_addTransformKeyframeAction = new KisAction(KisAnimationUtils::addTransformKeyframeActionName); m_deleteTransformKeyframeAction = new KisAction(KisAnimationUtils::removeTransformKeyframeActionName); { KisImageConfig cfg; setupActionButton(KisAnimationUtils::lazyFrameCreationActionName, KisAction::ACTIVE_IMAGE, cfg.lazyFrameCreationEnabled(), m_animationWidget->btnLazyFrame, &m_lazyFrameAction); } { KisConfig cfg; setupActionButton(KisAnimationUtils::dropFramesActionName, KisAction::ACTIVE_IMAGE, cfg.animationDropFrames(), m_animationWidget->btnDropFrames, &m_dropFramesAction); } 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_addBlankFrameAction, SIGNAL(triggered()), this, SLOT(slotAddBlankFrame())); connect(m_addDuplicateFrameAction, SIGNAL(triggered()), this, SLOT(slotAddDuplicateFrame())); connect(m_deleteKeyframeAction, SIGNAL(triggered()), this, SLOT(slotDeleteKeyframe())); 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())); } 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(sigTimeChanged(int)), this, SLOT(slotGlobalTimeChanged())); + connect(animation, SIGNAL(sigUiTimeChanged(int)), this, SLOT(slotGlobalTimeChanged())); connect(m_canvas->animationPlayer(), SIGNAL(sigFrameChanged()), this, SLOT(slotGlobalTimeChanged())); connect(m_canvas->animationPlayer(), SIGNAL(sigPlaybackStopped()), this, SLOT(slotGlobalTimeChanged())); connect(m_canvas->animationPlayer(), SIGNAL(sigPlaybackStopped()), this, SLOT(updatePlayPauseIcon())); connect(m_canvas->animationPlayer(), SIGNAL(sigPlaybackStatisticsUpdated()), this, SLOT(updateDropFramesIcon())); connect(m_animationWidget->doublePlaySpeed, SIGNAL(valueChanged(double)), m_canvas->animationPlayer(), SLOT(slotUpdatePlaybackSpeed(double))); connect(m_canvas->viewManager()->nodeManager(), SIGNAL(sigNodeActivated(KisNodeSP)), this, SLOT(slotCurrentNodeChanged(KisNodeSP))); slotGlobalTimeChanged(); slotCurrentNodeChanged(m_canvas->viewManager()->nodeManager()->activeNode()); } } void AnimationDocker::unsetCanvas() { setCanvas(0); } void AnimationDocker::setMainWindow(KisViewManager *view) { KisActionManager *actionManager = view->actionManager(); actionManager->addAction("previous_frame", m_previousFrameAction); actionManager->addAction("next_frame", m_nextFrameAction); actionManager->addAction("previous_keyframe", m_previousKeyFrameAction); actionManager->addAction("next_keyframe", m_nextKeyFrameAction); actionManager->addAction("first_frame", m_firstFrameAction); actionManager->addAction("last_frame", m_lastFrameAction); actionManager->addAction("lazy_frame", m_lazyFrameAction); actionManager->addAction("drop_frames", m_dropFramesAction); actionManager->addAction("toggle_playback", m_playPauseAction); actionManager->addAction("add_blank_frame", m_addBlankFrameAction); actionManager->addAction("add_duplicate_frame", m_addDuplicateFrameAction); actionManager->addAction("delete_keyframe", m_deleteKeyframeAction); slotUpdateIcons(); connect(view->mainWindow(), SIGNAL(themeChanged()), this, SLOT(slotUpdateIcons())); m_mainWindow = view->mainWindow(); } void AnimationDocker::slotAddBlankFrame() { addKeyframe(KisKeyframeChannel::Content.id(), false); } void AnimationDocker::slotAddDuplicateFrame() { addKeyframe(KisKeyframeChannel::Content.id(), true); } void AnimationDocker::slotDeleteKeyframe() { deleteKeyframe(KisKeyframeChannel::Content.id()); } void AnimationDocker::slotAddOpacityKeyframe() { addKeyframe(KisKeyframeChannel::Opacity.id(), false); } void AnimationDocker::slotDeleteOpacityKeyframe() { deleteKeyframe(KisKeyframeChannel::Opacity.id()); } void AnimationDocker::slotAddTransformKeyframe() { if (!m_canvas) return; KisTransformMask *mask = dynamic_cast(m_canvas->viewManager()->activeNode().data()); if (!mask) return; const int time = m_canvas->image()->animationInterface()->currentTime(); KUndo2Command *command = new KUndo2Command(kundo2_i18n("Add transform keyframe")); KisTransformMaskParamsFactoryRegistry::instance()->autoAddKeyframe(mask, time, KisTransformMaskParamsInterfaceSP(), command); command->redo(); m_canvas->currentImage()->postExecutionUndoAdapter()->addCommand(toQShared(command)); } void AnimationDocker::slotDeleteTransformKeyframe() { deleteKeyframe(KisKeyframeChannel::TransformArguments.id()); } void AnimationDocker::slotUIRangeChanged() { if (!m_canvas || !m_canvas->image()) return; int fromTime = m_animationWidget->spinFromFrame->value(); int toTime = m_animationWidget->spinToFrame->value(); m_canvas->image()->animationInterface()->setFullClipRange(KisTimeRange::fromTime(fromTime, toTime)); } void AnimationDocker::slotUIFramerateChanged() { if (!m_canvas || !m_canvas->image()) return; m_canvas->image()->animationInterface()->setFramerate(m_animationWidget->intFramerate->value()); } void AnimationDocker::slotOnionSkinOptions() { if (m_mainWindow) { QDockWidget *docker = m_mainWindow->dockWidget("OnionSkinsDocker"); if (docker) { docker->setVisible(!docker->isVisible()); } } } void AnimationDocker::slotGlobalTimeChanged() { int time = m_canvas->animationPlayer()->isPlaying() ? m_canvas->animationPlayer()->currentTime() : m_canvas->image()->animationInterface()->currentUITime(); m_animationWidget->intCurrentTime->setValue(time); const int frameRate = m_canvas->image()->animationInterface()->framerate(); const int msec = 1000 * time / frameRate; QTime realTime; realTime = realTime.addMSecs(msec); QString realTimeString = realTime.toString("hh:mm:ss.zzz"); m_animationWidget->intCurrentTime->setToolTip(realTimeString); } void AnimationDocker::slotTimeSpinBoxChanged() { if (!m_canvas || !m_canvas->image()) return; int newTime = m_animationWidget->intCurrentTime->value(); KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); if (m_canvas->animationPlayer()->isPlaying() || newTime == animation->currentUITime()) { return; } animation->requestTimeSwitchWithUndo(newTime); } void AnimationDocker::slotPreviousFrame() { if (!m_canvas) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); int time = animation->currentUITime() - 1; if (time >= 0) { animation->requestTimeSwitchWithUndo(time); } } void AnimationDocker::slotNextFrame() { if (!m_canvas) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); int time = animation->currentUITime() + 1; animation->requestTimeSwitchWithUndo(time); } void AnimationDocker::slotPreviousKeyFrame() { if (!m_canvas) return; KisNodeSP node = m_canvas->viewManager()->activeNode(); if (!node) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); int time = animation->currentUITime(); KisKeyframeChannel *content = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!content) return; KisKeyframeSP dstKeyframe; KisKeyframeSP keyframe = content->keyframeAt(time); if (!keyframe) { dstKeyframe = content->activeKeyframeAt(time); } else { dstKeyframe = content->previousKeyframe(keyframe); } if (dstKeyframe) { animation->requestTimeSwitchWithUndo(dstKeyframe->time()); } } void AnimationDocker::slotNextKeyFrame() { if (!m_canvas) return; KisNodeSP node = m_canvas->viewManager()->activeNode(); if (!node) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); int time = animation->currentUITime(); KisKeyframeChannel *content = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!content) return; KisKeyframeSP dstKeyframe; KisKeyframeSP keyframe = content->activeKeyframeAt(time); if (keyframe) { dstKeyframe = content->nextKeyframe(keyframe); } if (dstKeyframe) { animation->requestTimeSwitchWithUndo(dstKeyframe->time()); } } void AnimationDocker::slotFirstFrame() { if (!m_canvas) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); animation->requestTimeSwitchWithUndo(0); } void AnimationDocker::slotLastFrame() { if (!m_canvas) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); animation->requestTimeSwitchWithUndo(animation->totalLength() - 1); } void AnimationDocker::slotPlayPause() { if (!m_canvas) return; if (m_canvas->animationPlayer()->isPlaying()) { m_canvas->animationPlayer()->stop(); } else { m_canvas->animationPlayer()->play(); } updatePlayPauseIcon(); } void AnimationDocker::updatePlayPauseIcon() { bool isPlaying = m_canvas && m_canvas->animationPlayer() && m_canvas->animationPlayer()->isPlaying(); m_playPauseAction->setIcon(isPlaying ? KisIconUtils::loadIcon("animation_stop") : KisIconUtils::loadIcon("animation_play")); } void AnimationDocker::updateLazyFrameIcon() { KisImageConfig cfg; const bool value = cfg.lazyFrameCreationEnabled(); m_lazyFrameAction->setIcon(value ? KisIconUtils::loadIcon("lazyframeOn") : KisIconUtils::loadIcon("lazyframeOff")); m_lazyFrameAction->setText(QString("%1 (%2)") .arg(KisAnimationUtils::lazyFrameCreationActionName) .arg(KritaUtils::toLocalizedOnOff(value))); } void AnimationDocker::updateDropFramesIcon() { qreal effectiveFps = 0.0; qreal realFps = 0.0; qreal framesDropped = 0.0; bool isPlaying = false; KisAnimationPlayer *player = m_canvas && m_canvas->animationPlayer() ? m_canvas->animationPlayer() : 0; if (player) { effectiveFps = player->effectiveFps(); realFps = player->realFps(); framesDropped = player->framesDroppedPortion(); isPlaying = player->isPlaying(); } KisConfig cfg; const bool value = cfg.animationDropFrames(); m_dropFramesAction->setIcon(value ? KisIconUtils::loadIcon(framesDropped > 0.05 ? "droppedframes" : "dropframe") : KisIconUtils::loadIcon("dropframe")); QString text; if (!isPlaying) { text = QString("%1 (%2)") .arg(KisAnimationUtils::dropFramesActionName) .arg(KritaUtils::toLocalizedOnOff(value)); } else { text = QString("%1 (%2)\n" "%3\n" "%4\n" "%5") .arg(KisAnimationUtils::dropFramesActionName) .arg(KritaUtils::toLocalizedOnOff(value)) .arg(i18n("Effective FPS:\t%1", effectiveFps)) .arg(i18n("Real FPS:\t%1", realFps)) .arg(i18n("Frames dropped:\t%1\%", framesDropped * 100)); } m_dropFramesAction->setText(text); } void AnimationDocker::slotUpdateIcons() { m_previousFrameAction->setIcon(KisIconUtils::loadIcon("prevframe")); m_nextFrameAction->setIcon(KisIconUtils::loadIcon("nextframe")); m_previousKeyFrameAction->setIcon(KisIconUtils::loadIcon("prevkeyframe")); m_nextKeyFrameAction->setIcon(KisIconUtils::loadIcon("nextkeyframe")); m_firstFrameAction->setIcon(KisIconUtils::loadIcon("firstframe")); m_lastFrameAction->setIcon(KisIconUtils::loadIcon("lastframe")); updatePlayPauseIcon(); m_addBlankFrameAction->setIcon(KisIconUtils::loadIcon("addblankframe")); m_addDuplicateFrameAction->setIcon(KisIconUtils::loadIcon("addduplicateframe")); m_deleteKeyframeAction->setIcon(KisIconUtils::loadIcon("deletekeyframe")); updateLazyFrameIcon(); updateDropFramesIcon(); m_animationWidget->btnOnionSkinOptions->setIcon(KisIconUtils::loadIcon("onion_skin_options")); m_animationWidget->btnOnionSkinOptions->setIconSize(QSize(22, 22)); m_animationWidget->btnNextKeyFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnPreviousKeyFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnFirstFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnLastFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnPreviousFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnPlay->setIconSize(QSize(22, 22)); m_animationWidget->btnNextFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnAddKeyframe->setIconSize(QSize(22, 22)); m_animationWidget->btnAddDuplicateFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnDeleteKeyframe->setIconSize(QSize(22, 22)); m_animationWidget->btnLazyFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnDropFrames->setIconSize(QSize(22, 22)); } void AnimationDocker::slotLazyFrameChanged(bool value) { KisImageConfig cfg; if (value != cfg.lazyFrameCreationEnabled()) { cfg.setLazyFrameCreationEnabled(value); updateLazyFrameIcon(); } } void AnimationDocker::slotDropFramesChanged(bool value) { KisConfig cfg; if (value != cfg.animationDropFrames()) { cfg.setAnimationDropFrames(value); updateDropFramesIcon(); } } void AnimationDocker::slotCurrentNodeChanged(KisNodeSP node) { bool isNodeAnimatable = false; m_newKeyframeMenu->clear(); m_deleteKeyframeMenu->clear(); if (!node.isNull()) { if (KisAnimationUtils::supportsContentFrames(node)) { isNodeAnimatable = true; m_newKeyframeMenu->addAction(m_addBlankFrameAction); m_deleteKeyframeMenu->addAction(m_deleteKeyframeAction); } 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::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); } diff --git a/plugins/dockers/animation/kis_time_based_item_model.cpp b/plugins/dockers/animation/kis_time_based_item_model.cpp index 36dfc73469..99ff688ca1 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.cpp +++ b/plugins/dockers/animation/kis_time_based_item_model.cpp @@ -1,435 +1,435 @@ /* * 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" 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 { if (image.isNull()) return 0; KisImageAnimationInterface *i = image->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; using namespace std::placeholders; std::function callback( std::bind(&KisTimeBasedItemModel::slotInternalScrubPreviewRequested, this, _1)); m_d->scrubbingCompressor.reset( new KisSignalCompressorWithParam(cfg.scribbingUpdatesDelay(), 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(sigTimeChanged(int)), SLOT(slotCurrentTimeChanged(int))); + connect(ai, SIGNAL(sigUiTimeChanged(int)), SLOT(slotCurrentTimeChanged(int))); } if (image != oldImage) { reset(); } } 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; Q_FOREACH (const QModelIndex &index, indexes) { int time = index.column(); QList channels = channelsAt(index); Q_FOREACH(KisKeyframeChannel *channel, channels) { if (channel->keyframeAt(time)) { frameItems << KisAnimationUtils::FrameItem(channel->node(), channel->id(), index.column()); } } } if (frameItems.isEmpty()) return false; KisAnimationUtils::removeKeyframes(m_d->image, frameItems); Q_FOREACH (const QModelIndex &index, indexes) { if (index.isValid()) { emit dataChanged(index, index); } } return true; } bool KisTimeBasedItemModel::offsetFrames(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, KUndo2Command *parentCommand) { bool result = false; if (srcIndexes.isEmpty()) return result; if (offset.isNull()) return result; KisAnimationUtils::sortPointsForSafeMove(&srcIndexes, offset); KisAnimationUtils::FrameItemList srcFrameItems; KisAnimationUtils::FrameItemList dstFrameItems; QModelIndexList updateIndexes; 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 false; } QList channels = channelsAt(srcIndex); Q_FOREACH(KisKeyframeChannel *channel, channels) { if (channel->keyframeAt(srcIndex.column())) { srcFrameItems << KisAnimationUtils::FrameItem(srcNode, channel->id(), srcIndex.column()); dstFrameItems << KisAnimationUtils::FrameItem(dstNode, channel->id(), dstIndex.column()); } } if (!copyFrames) { updateIndexes << srcIndex; } updateIndexes << dstIndex; } result = KisAnimationUtils::moveKeyframes(m_d->image, srcFrameItems, dstFrameItems, copyFrames, parentCommand); Q_FOREACH (const QModelIndex &index, updateIndexes) { emit dataChanged(index, index); } return result; } 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) { 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; } diff --git a/plugins/dockers/animation/tests/timeline_model_test.cpp b/plugins/dockers/animation/tests/timeline_model_test.cpp index 782d6908d1..97c9674327 100644 --- a/plugins/dockers/animation/tests/timeline_model_test.cpp +++ b/plugins/dockers/animation/tests/timeline_model_test.cpp @@ -1,298 +1,298 @@ /* * 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_model_test.h" #include "kis_image.h" #include "kis_node.h" #include "kis_paint_device.h" #include #include #include #include #include "kis_image_animation_interface.h" #include "KisDocument.h" #include "KisPart.h" #include "kis_name_server.h" #include "flake/kis_shape_controller.h" #include "kis_undo_adapter.h" #include "timeline_frames_view.h" #include "timeline_frames_model.h" #include "kis_node_dummies_graph.h" #include "commands/kis_image_layer_add_command.h" #include "commands/kis_image_layer_remove_command.h" #include "kis_double_parse_spin_box.h" #include "kis_int_parse_spin_box.h" void TimelineModelTest::init() { m_doc = KisPart::instance()->createDocument(); m_nameServer = new KisNameServer(); m_shapeController = new KisShapeController(m_doc, m_nameServer); //m_nodeModel = new KisNodeModel(0); initBase(); } void TimelineModelTest::cleanup() { cleanupBase(); //delete m_nodeModel; delete m_shapeController; delete m_nameServer; delete m_doc; } #include "timeline_frames_index_converter.h" void TimelineModelTest::testConverter() { constructImage(); addSelectionMasks(); m_shapeController->setImage(m_image); m_layer1->enableAnimation(); m_layer1->setUseInTimeline(true); m_layer2->setUseInTimeline(true); m_sel3->setUseInTimeline(true); TimelineFramesIndexConverter converter(m_shapeController); QCOMPARE(converter.rowCount(), 3); QCOMPARE(converter.rowForDummy(m_shapeController->dummyForNode(m_layer1)), 2); QCOMPARE(converter.rowForDummy(m_shapeController->dummyForNode(m_layer2)), 1); QCOMPARE(converter.rowForDummy(m_shapeController->dummyForNode(m_sel3)), 0); QCOMPARE(converter.dummyFromRow(2), m_shapeController->dummyForNode(m_layer1)); QCOMPARE(converter.dummyFromRow(1), m_shapeController->dummyForNode(m_layer2)); QCOMPARE(converter.dummyFromRow(0), m_shapeController->dummyForNode(m_sel3)); TimelineNodeListKeeper keeper(0, m_shapeController); QCOMPARE(keeper.rowCount(), 3); QCOMPARE(keeper.rowForDummy(m_shapeController->dummyForNode(m_layer1)), 2); QCOMPARE(keeper.rowForDummy(m_shapeController->dummyForNode(m_layer2)), 1); QCOMPARE(keeper.rowForDummy(m_shapeController->dummyForNode(m_sel3)), 0); QCOMPARE(keeper.dummyFromRow(2), m_shapeController->dummyForNode(m_layer1)); QCOMPARE(keeper.dummyFromRow(1), m_shapeController->dummyForNode(m_layer2)); QCOMPARE(keeper.dummyFromRow(0), m_shapeController->dummyForNode(m_sel3)); TimelineNodeListKeeper::OtherLayersList list = keeper.otherLayersList(); Q_FOREACH (const TimelineNodeListKeeper::OtherLayer &l, list) { qDebug() << ppVar(l.name) << ppVar(l.dummy->node()->name()); } } void TimelineModelTest::testModel() { QScopedPointer model(new TimelineFramesModel(0)); } struct TestingInterface : TimelineFramesModel::NodeManipulationInterface { TestingInterface(KisImageSP image) : m_image(image) {} KisLayerSP addPaintLayer() const { KisNodeSP parent = m_image->root(); KisNodeSP after = parent->lastChild(); KisPaintLayerSP layer = new KisPaintLayer(const_cast(m_image.data()), m_image->nextLayerName(), OPACITY_OPAQUE_U8, m_image->colorSpace()); m_image->undoAdapter()->addCommand( new KisImageLayerAddCommand(m_image, layer, parent, after, false, false)); return layer; } void removeNode(KisNodeSP node) const { m_image->undoAdapter()->addCommand( new KisImageLayerRemoveCommand(m_image, node)); } private: KisImageSP m_image; }; void TimelineModelTest::testView() { QDialog dlg; QFont font; font.setPointSizeF(9); dlg.setFont(font); QSpinBox *intFps = new KisIntParseSpinBox(&dlg); intFps->setValue(12); QSpinBox *intTime = new KisIntParseSpinBox(&dlg); intTime->setValue(0); intTime->setMaximum(10000); QSpinBox *intLayer = new KisIntParseSpinBox(&dlg); intLayer->setValue(0); intLayer->setMaximum(100); TimelineFramesView *framesTable = new TimelineFramesView(&dlg); TimelineFramesModel *model = new TimelineFramesModel(&dlg); constructImage(); addSelectionMasks(); m_shapeController->setImage(m_image); m_image->animationInterface()->requestTimeSwitchWithUndo(4); framesTable->setModel(model); model->setDummiesFacade(m_shapeController, m_image); model->setNodeManipulationInterface(new TestingInterface(m_image)); m_layer1->enableAnimation(); m_layer1->setUseInTimeline(true); connect(intFps, SIGNAL(valueChanged(int)), m_image->animationInterface(), SLOT(setFramerate(int))); connect(intTime, SIGNAL(valueChanged(int)), SLOT(setCurrentTime(int))); - connect(m_image->animationInterface(), SIGNAL(sigTimeChanged(int)), + connect(m_image->animationInterface(), SIGNAL(sigUiTimeChanged(int)), intTime, SLOT(setValue(int))); connect(intLayer, SIGNAL(valueChanged(int)), SLOT(setCurrentLayer(int))); connect(this, SIGNAL(sigRequestNodeChange(KisNodeSP)), model, SLOT(slotCurrentNodeChanged(KisNodeSP))); connect(model, SIGNAL(requestCurrentNodeChanged(KisNodeSP)), this, SLOT(slotGuiChangedNode(KisNodeSP))); QVBoxLayout *layout = new QVBoxLayout(&dlg); layout->addWidget(intFps); layout->addWidget(intTime); layout->addWidget(intLayer); layout->addWidget(framesTable); layout->setStretch(0, 0); layout->setStretch(1, 0); layout->setStretch(2, 0); layout->setStretch(3, 1); dlg.resize(600, 400); dlg.exec(); } void TimelineModelTest::setCurrentTime(int time) { m_image->animationInterface()->requestTimeSwitchWithUndo(time); } KisNodeDummy* findNodeFromRowAny(KisNodeDummy *root, int &startCount) { if (!startCount) { return root; } startCount--; KisNodeDummy *dummy = root->lastChild(); while (dummy) { KisNodeDummy *found = findNodeFromRowAny(dummy, startCount); if (found) return found; dummy = dummy->prevSibling(); } return 0; } void TimelineModelTest::setCurrentLayer(int row) { KisNodeDummy *root = m_shapeController->rootDummy(); KisNodeDummy *dummy = findNodeFromRowAny(root, row); if (!dummy) { qDebug() << "WARNING: Cannot find a node at pos" << row; return; } else { qDebug() << "NonGUI changed active node: " << dummy->node()->name(); } emit sigRequestNodeChange(dummy->node()); } void TimelineModelTest::slotGuiChangedNode(KisNodeSP node) { qDebug() << "GUI changed active node:" << node->name(); } #include "kis_equalizer_column.h" #include "kis_equalizer_slider.h" #include "kis_equalizer_widget.h" void TimelineModelTest::testOnionSkins() { QDialog dlg; QFont font; font.setPointSizeF(9); dlg.setFont(font); QHBoxLayout *layout = new QHBoxLayout(&dlg); KisEqualizerWidget *w = new KisEqualizerWidget(10, &dlg); connect(w, SIGNAL(sigConfigChanged()), SLOT(slotBang())); layout->addWidget(w); dlg.setLayout(layout); dlg.resize(600, 400); dlg.exec(); } void TimelineModelTest::slotBang() { ENTER_FUNCTION() << "!!!!"; } QTEST_MAIN(TimelineModelTest) diff --git a/plugins/dockers/defaultdockers/kis_layer_box.cpp b/plugins/dockers/defaultdockers/kis_layer_box.cpp index 38aebdc7a2..a885a87ae9 100644 --- a/plugins/dockers/defaultdockers/kis_layer_box.cpp +++ b/plugins/dockers/defaultdockers/kis_layer_box.cpp @@ -1,929 +1,929 @@ /* * kis_layer_box.cc - part of Krita aka Krayon aka KimageShop * * Copyright (c) 2002 Patrick Julien * Copyright (C) 2006 Gábor Lehel * Copyright (C) 2007 Thomas Zander * Copyright (C) 2007 Boudewijn Rempt * Copyright (c) 2011 José Luis Vergara * * 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_box.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 "kis_action.h" #include "kis_action_manager.h" #include "widgets/kis_cmb_composite.h" #include "widgets/kis_slider_spin_box.h" #include "KisViewManager.h" #include "kis_node_manager.h" #include "kis_node_model.h" #include "canvas/kis_canvas2.h" #include "KisDocument.h" #include "kis_dummies_facade_base.h" #include "kis_shape_controller.h" #include "kis_selection_mask.h" #include "kis_config.h" #include "KisView.h" #include "krita_utils.h" #include "sync_button_and_action.h" #include "kis_color_label_selector_widget.h" #include "kis_signals_blocker.h" #include "kis_color_filter_combo.h" #include "kis_node_filter_proxy_model.h" #include "kis_layer_utils.h" #include "ui_wdglayerbox.h" inline void KisLayerBox::connectActionToButton(KisViewManager* view, QAbstractButton *button, const QString &id) { if (!view || !button) return; KisAction *action = view->actionManager()->actionByName(id); if (!action) return; connect(button, SIGNAL(clicked()), action, SLOT(trigger())); connect(action, SIGNAL(sigEnableSlaves(bool)), button, SLOT(setEnabled(bool))); } inline void KisLayerBox::addActionToMenu(QMenu *menu, const QString &id) { if (m_canvas) { menu->addAction(m_canvas->viewManager()->actionManager()->actionByName(id)); } } KisLayerBox::KisLayerBox() : QDockWidget(i18n("Layers")) , m_canvas(0) , m_wdgLayerBox(new Ui_WdgLayerBox) , m_thumbnailCompressor(500, KisSignalCompressor::FIRST_INACTIVE) , m_colorLabelCompressor(900, KisSignalCompressor::FIRST_INACTIVE) { KisConfig cfg; QWidget* mainWidget = new QWidget(this); setWidget(mainWidget); m_opacityDelayTimer.setSingleShot(true); m_wdgLayerBox->setupUi(mainWidget); connect(m_wdgLayerBox->listLayers, SIGNAL(contextMenuRequested(const QPoint&, const QModelIndex&)), this, SLOT(slotContextMenuRequested(const QPoint&, const QModelIndex&))); connect(m_wdgLayerBox->listLayers, SIGNAL(collapsed(const QModelIndex&)), SLOT(slotCollapsed(const QModelIndex &))); connect(m_wdgLayerBox->listLayers, SIGNAL(expanded(const QModelIndex&)), SLOT(slotExpanded(const QModelIndex &))); connect(m_wdgLayerBox->listLayers, SIGNAL(selectionChanged(const QModelIndexList&)), SLOT(selectionChanged(const QModelIndexList&))); m_wdgLayerBox->bnAdd->setIcon(KisIconUtils::loadIcon("addlayer")); m_wdgLayerBox->bnDelete->setIcon(KisIconUtils::loadIcon("deletelayer")); m_wdgLayerBox->bnDelete->setIconSize(QSize(22, 22)); m_wdgLayerBox->bnRaise->setEnabled(false); m_wdgLayerBox->bnRaise->setIcon(KisIconUtils::loadIcon("arrowupblr")); m_wdgLayerBox->bnRaise->setIconSize(QSize(22, 22)); m_wdgLayerBox->bnLower->setEnabled(false); m_wdgLayerBox->bnLower->setIcon(KisIconUtils::loadIcon("arrowdown")); m_wdgLayerBox->bnLower->setIconSize(QSize(22, 22)); m_wdgLayerBox->bnProperties->setIcon(KisIconUtils::loadIcon("properties")); m_wdgLayerBox->bnProperties->setIconSize(QSize(22, 22)); m_wdgLayerBox->bnDuplicate->setIcon(KisIconUtils::loadIcon("duplicatelayer")); m_wdgLayerBox->bnDuplicate->setIconSize(QSize(22, 22)); if (cfg.sliderLabels()) { m_wdgLayerBox->opacityLabel->hide(); m_wdgLayerBox->doubleOpacity->setPrefix(QString("%1: ").arg(i18n("Opacity"))); } m_wdgLayerBox->doubleOpacity->setRange(0, 100, 0); m_wdgLayerBox->doubleOpacity->setSuffix("%"); connect(m_wdgLayerBox->doubleOpacity, SIGNAL(valueChanged(qreal)), SLOT(slotOpacitySliderMoved(qreal))); connect(&m_opacityDelayTimer, SIGNAL(timeout()), SLOT(slotOpacityChanged())); connect(m_wdgLayerBox->cmbComposite, SIGNAL(activated(int)), SLOT(slotCompositeOpChanged(int))); m_selectOpaque = new KisAction(i18n("&Select Opaque"), this); m_selectOpaque->setActivationFlags(KisAction::ACTIVE_LAYER); m_selectOpaque->setActivationConditions(KisAction::SELECTION_EDITABLE); m_selectOpaque->setObjectName("select_opaque"); connect(m_selectOpaque, SIGNAL(triggered(bool)), this, SLOT(slotSelectOpaque())); m_actions.append(m_selectOpaque); m_newLayerMenu = new QMenu(this); m_wdgLayerBox->bnAdd->setMenu(m_newLayerMenu); m_wdgLayerBox->bnAdd->setPopupMode(QToolButton::MenuButtonPopup); m_nodeModel = new KisNodeModel(this); m_filteringModel = new KisNodeFilterProxyModel(this); m_filteringModel->setNodeModel(m_nodeModel); /** * Connect model updateUI() to enable/disable controls. * Note: nodeActivated() is connected separately in setImage(), because * it needs particular order of calls: first the connection to the * node manager should be called, then updateUI() */ connect(m_nodeModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)), SLOT(updateUI())); connect(m_nodeModel, SIGNAL(rowsRemoved(const QModelIndex&, int, int)), SLOT(updateUI())); connect(m_nodeModel, SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)), SLOT(updateUI())); connect(m_nodeModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), SLOT(updateUI())); connect(m_nodeModel, SIGNAL(modelReset()), SLOT(slotModelReset())); KisAction *showGlobalSelectionMask = new KisAction(i18n("&Show Global Selection Mask"), this); showGlobalSelectionMask->setObjectName("show-global-selection-mask"); showGlobalSelectionMask->setToolTip(i18nc("@info:tooltip", "Shows global selection as a usual selection mask in Layers docker")); showGlobalSelectionMask->setCheckable(true); connect(showGlobalSelectionMask, SIGNAL(triggered(bool)), SLOT(slotEditGlobalSelection(bool))); m_actions.append(showGlobalSelectionMask); showGlobalSelectionMask->setChecked(cfg.showGlobalSelection()); m_colorSelector = new KisColorLabelSelectorWidget(this); connect(m_colorSelector, SIGNAL(currentIndexChanged(int)), SLOT(slotColorLabelChanged(int))); m_colorSelectorAction = new QWidgetAction(this); m_colorSelectorAction->setDefaultWidget(m_colorSelector); connect(m_nodeModel, SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &)), &m_colorLabelCompressor, SLOT(start())); m_wdgLayerBox->listLayers->setModel(m_filteringModel); // this connection should be done *after* the setModel() call to // happen later than the internal selection model connect(m_filteringModel.data(), &KisNodeFilterProxyModel::rowsAboutToBeRemoved, this, &KisLayerBox::slotAboutToRemoveRows); connect(m_wdgLayerBox->cmbFilter, SIGNAL(selectedColorsChanged()), SLOT(updateLayerFiltering())); setEnabled(false); connect(&m_thumbnailCompressor, SIGNAL(timeout()), SLOT(updateThumbnail())); connect(&m_colorLabelCompressor, SIGNAL(timeout()), SLOT(updateAvailableLabels())); } KisLayerBox::~KisLayerBox() { delete m_wdgLayerBox; } void expandNodesRecursively(KisNodeSP root, QPointer filteringModel, KisNodeView *nodeView) { if (!root) return; if (filteringModel.isNull()) return; if (!nodeView) return; nodeView->blockSignals(true); KisNodeSP node = root->firstChild(); while (node) { QModelIndex idx = filteringModel->indexFromNode(node); if (idx.isValid()) { nodeView->setExpanded(idx, !node->collapsed()); } if (node->childCount() > 0) { expandNodesRecursively(node, filteringModel, nodeView); } node = node->nextSibling(); } nodeView->blockSignals(false); } void KisLayerBox::setMainWindow(KisViewManager* kisview) { m_nodeManager = kisview->nodeManager(); Q_FOREACH (KisAction *action, m_actions) { kisview->actionManager()-> addAction(action->objectName(), action); } connectActionToButton(kisview, m_wdgLayerBox->bnAdd, "add_new_paint_layer"); connectActionToButton(kisview, m_wdgLayerBox->bnDuplicate, "duplicatelayer"); KisActionManager *actionManager = kisview->actionManager(); KisAction *action = actionManager->createAction("RenameCurrentLayer"); Q_ASSERT(action); connect(action, SIGNAL(triggered()), this, SLOT(slotRenameCurrentNode())); m_propertiesAction = actionManager->createAction("layer_properties"); Q_ASSERT(m_propertiesAction); new SyncButtonAndAction(m_propertiesAction, m_wdgLayerBox->bnProperties, this); connect(m_propertiesAction, SIGNAL(triggered()), this, SLOT(slotPropertiesClicked())); m_removeAction = actionManager->createAction("remove_layer"); Q_ASSERT(m_removeAction); new SyncButtonAndAction(m_removeAction, m_wdgLayerBox->bnDelete, this); connect(m_removeAction, SIGNAL(triggered()), this, SLOT(slotRmClicked())); action = actionManager->createAction("move_layer_up"); Q_ASSERT(action); new SyncButtonAndAction(action, m_wdgLayerBox->bnRaise, this); connect(action, SIGNAL(triggered()), this, SLOT(slotRaiseClicked())); action = actionManager->createAction("move_layer_down"); Q_ASSERT(action); new SyncButtonAndAction(action, m_wdgLayerBox->bnLower, this); connect(action, SIGNAL(triggered()), this, SLOT(slotLowerClicked())); } void KisLayerBox::setCanvas(KoCanvasBase *canvas) { if(m_canvas == canvas) return; setEnabled(canvas != 0); if (m_canvas) { m_canvas->disconnectCanvasObserver(this); m_nodeModel->setDummiesFacade(0, 0, 0, 0, 0); if (m_image) { KisImageAnimationInterface *animation = m_image->animationInterface(); animation->disconnect(this); } disconnect(m_image, 0, this, 0); disconnect(m_nodeManager, 0, this, 0); disconnect(m_nodeModel, 0, m_nodeManager, 0); m_nodeManager->slotSetSelectedNodes(KisNodeList()); } m_canvas = dynamic_cast(canvas); if (m_canvas) { m_image = m_canvas->image(); connect(m_image, SIGNAL(sigImageUpdated(QRect)), &m_thumbnailCompressor, SLOT(start())); KisDocument* doc = static_cast(m_canvas->imageView()->document()); KisShapeController *kritaShapeController = dynamic_cast(doc->shapeController()); KisDummiesFacadeBase *kritaDummiesFacade = static_cast(kritaShapeController); m_nodeModel->setDummiesFacade(kritaDummiesFacade, m_image, kritaShapeController, m_nodeManager->nodeSelectionAdapter(), m_nodeManager->nodeInsertionAdapter()); connect(m_image, SIGNAL(sigAboutToBeDeleted()), SLOT(notifyImageDeleted())); connect(m_image, SIGNAL(sigNodeCollapsedChanged()), SLOT(slotNodeCollapsedChanged())); // cold start if (m_nodeManager) { setCurrentNode(m_nodeManager->activeNode()); // Connection KisNodeManager -> KisLayerBox connect(m_nodeManager, SIGNAL(sigUiNeedChangeActiveNode(KisNodeSP)), this, SLOT(setCurrentNode(KisNodeSP))); connect(m_nodeManager, SIGNAL(sigUiNeedChangeSelectedNodes(const QList &)), SLOT(slotNodeManagerChangedSelection(const QList &))); } else { setCurrentNode(m_canvas->imageView()->currentNode()); } // Connection KisLayerBox -> KisNodeManager (isolate layer) connect(m_nodeModel, SIGNAL(toggleIsolateActiveNode()), m_nodeManager, SLOT(toggleIsolateActiveNode())); KisImageAnimationInterface *animation = m_image->animationInterface(); - connect(animation, &KisImageAnimationInterface::sigTimeChanged, this, &KisLayerBox::slotImageTimeChanged); + connect(animation, &KisImageAnimationInterface::sigUiTimeChanged, this, &KisLayerBox::slotImageTimeChanged); expandNodesRecursively(m_image->rootLayer(), m_filteringModel, m_wdgLayerBox->listLayers); m_wdgLayerBox->listLayers->scrollTo(m_wdgLayerBox->listLayers->currentIndex()); updateAvailableLabels(); addActionToMenu(m_newLayerMenu, "add_new_paint_layer"); addActionToMenu(m_newLayerMenu, "add_new_group_layer"); addActionToMenu(m_newLayerMenu, "add_new_clone_layer"); addActionToMenu(m_newLayerMenu, "add_new_shape_layer"); addActionToMenu(m_newLayerMenu, "add_new_adjustment_layer"); addActionToMenu(m_newLayerMenu, "add_new_fill_layer"); addActionToMenu(m_newLayerMenu, "add_new_file_layer"); m_newLayerMenu->addSeparator(); addActionToMenu(m_newLayerMenu, "add_new_transparency_mask"); addActionToMenu(m_newLayerMenu, "add_new_filter_mask"); addActionToMenu(m_newLayerMenu, "add_new_colorize_mask"); addActionToMenu(m_newLayerMenu, "add_new_transform_mask"); addActionToMenu(m_newLayerMenu, "add_new_selection_mask"); } } void KisLayerBox::unsetCanvas() { setEnabled(false); if (m_canvas) { m_newLayerMenu->clear(); } m_filteringModel->unsetDummiesFacade(); disconnect(m_image, 0, this, 0); disconnect(m_nodeManager, 0, this, 0); disconnect(m_nodeModel, 0, m_nodeManager, 0); m_nodeManager->slotSetSelectedNodes(KisNodeList()); m_canvas = 0; } void KisLayerBox::notifyImageDeleted() { setCanvas(0); } void KisLayerBox::updateUI() { if (!m_canvas) return; if (!m_nodeManager) return; KisNodeSP activeNode = m_nodeManager->activeNode(); if (activeNode != m_activeNode) { if( !m_activeNode.isNull() ) m_activeNode->disconnect(this); m_activeNode = activeNode; if (activeNode) { KisKeyframeChannel *opacityChannel = activeNode->getKeyframeChannel(KisKeyframeChannel::Opacity.id(), false); if (opacityChannel) { watchOpacityChannel(opacityChannel); } else { watchOpacityChannel(0); connect(activeNode.data(), &KisNode::keyframeChannelAdded, this, &KisLayerBox::slotKeyframeChannelAdded); } } } m_wdgLayerBox->bnRaise->setEnabled(activeNode && activeNode->isEditable(false) && (activeNode->nextSibling() || (activeNode->parent() && activeNode->parent() != m_image->root()))); m_wdgLayerBox->bnLower->setEnabled(activeNode && activeNode->isEditable(false) && (activeNode->prevSibling() || (activeNode->parent() && activeNode->parent() != m_image->root()))); m_wdgLayerBox->doubleOpacity->setEnabled(activeNode && activeNode->isEditable(false)); m_wdgLayerBox->cmbComposite->setEnabled(activeNode && activeNode->isEditable(false)); if (activeNode) { if (m_nodeManager->activePaintDevice()) { slotFillCompositeOps(m_nodeManager->activeColorSpace()); } else { slotFillCompositeOps(m_image->colorSpace()); } if (activeNode->inherits("KisColorizeMask") || activeNode->inherits("KisLayer")) { m_wdgLayerBox->doubleOpacity->setEnabled(true); slotSetOpacity(activeNode->opacity() * 100.0 / 255); const KoCompositeOp* compositeOp = activeNode->compositeOp(); if (compositeOp) { slotSetCompositeOp(compositeOp); } else { m_wdgLayerBox->cmbComposite->setEnabled(false); } const KisGroupLayer *group = qobject_cast(activeNode.data()); bool compositeSelectionActive = !(group && group->passThroughMode()); m_wdgLayerBox->cmbComposite->setEnabled(compositeSelectionActive); } else if (activeNode->inherits("KisMask")) { m_wdgLayerBox->cmbComposite->setEnabled(false); m_wdgLayerBox->doubleOpacity->setEnabled(false); } } } /** * This method is callen *only* when non-GUI code requested the * change of the current node */ void KisLayerBox::setCurrentNode(KisNodeSP node) { m_filteringModel->setActiveNode(node); QModelIndex index = node ? m_filteringModel->indexFromNode(node) : QModelIndex(); m_filteringModel->setData(index, true, KisNodeModel::ActiveRole); updateUI(); } void KisLayerBox::slotModelReset() { if(m_nodeModel->hasDummiesFacade()) { QItemSelection selection; Q_FOREACH (const KisNodeSP node, m_nodeManager->selectedNodes()) { const QModelIndex &idx = m_filteringModel->indexFromNode(node); if(idx.isValid()){ QItemSelectionRange selectionRange(idx); selection << selectionRange; } } m_wdgLayerBox->listLayers->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); } updateUI(); } void KisLayerBox::slotSetCompositeOp(const KoCompositeOp* compositeOp) { KoID opId = KoCompositeOpRegistry::instance().getKoID(compositeOp->id()); m_wdgLayerBox->cmbComposite->blockSignals(true); m_wdgLayerBox->cmbComposite->selectCompositeOp(opId); m_wdgLayerBox->cmbComposite->blockSignals(false); } void KisLayerBox::slotFillCompositeOps(const KoColorSpace* colorSpace) { m_wdgLayerBox->cmbComposite->validate(colorSpace); } // range: 0-100 void KisLayerBox::slotSetOpacity(double opacity) { Q_ASSERT(opacity >= 0 && opacity <= 100); m_wdgLayerBox->doubleOpacity->blockSignals(true); m_wdgLayerBox->doubleOpacity->setValue(opacity); m_wdgLayerBox->doubleOpacity->blockSignals(false); } void KisLayerBox::slotContextMenuRequested(const QPoint &pos, const QModelIndex &index) { KisNodeList nodes = m_nodeManager->selectedNodes(); KisNodeSP activeNode = m_nodeManager->activeNode(); if (nodes.isEmpty() || !activeNode) return; if (m_canvas) { QMenu menu; const bool singleLayer = nodes.size() == 1; if (index.isValid()) { menu.addAction(m_propertiesAction); if (singleLayer) { addActionToMenu(&menu, "layer_style"); } { KisSignalsBlocker b(m_colorSelector); m_colorSelector->setCurrentIndex(singleLayer ? activeNode->colorLabelIndex() : -1); } menu.addAction(m_colorSelectorAction); menu.addSeparator(); addActionToMenu(&menu, "cut_layer_clipboard"); addActionToMenu(&menu, "copy_layer_clipboard"); addActionToMenu(&menu, "paste_layer_from_clipboard"); menu.addAction(m_removeAction); addActionToMenu(&menu, "duplicatelayer"); addActionToMenu(&menu, "merge_layer"); if (singleLayer) { addActionToMenu(&menu, "flatten_image"); addActionToMenu(&menu, "flatten_layer"); } menu.addSeparator(); QMenu *selectMenu = menu.addMenu(i18n("&Select")); addActionToMenu(selectMenu, "select_all_layers"); addActionToMenu(selectMenu, "select_visible_layers"); addActionToMenu(selectMenu, "select_invisible_layers"); addActionToMenu(selectMenu, "select_locked_layers"); addActionToMenu(selectMenu, "select_unlocked_layers"); QMenu *groupMenu = menu.addMenu(i18n("&Group")); addActionToMenu(groupMenu, "create_quick_group"); addActionToMenu(groupMenu, "create_quick_clipping_group"); addActionToMenu(groupMenu, "quick_ungroup"); if (singleLayer) { QMenu *addLayerMenu = menu.addMenu(i18n("&Add")); addActionToMenu(addLayerMenu, "add_new_transparency_mask"); addActionToMenu(addLayerMenu, "add_new_filter_mask"); addActionToMenu(addLayerMenu, "add_new_colorize_mask"); addActionToMenu(addLayerMenu, "add_new_transform_mask"); addActionToMenu(addLayerMenu, "add_new_selection_mask"); QMenu *convertToMenu = menu.addMenu(i18n("&Convert")); addActionToMenu(convertToMenu, "convert_to_paint_layer"); addActionToMenu(convertToMenu, "convert_to_transparency_mask"); addActionToMenu(convertToMenu, "convert_to_filter_mask"); addActionToMenu(convertToMenu, "convert_to_selection_mask"); QMenu *splitAlphaMenu = menu.addMenu(i18n("S&plit Alpha")); addActionToMenu(splitAlphaMenu, "split_alpha_into_mask"); addActionToMenu(splitAlphaMenu, "split_alpha_write"); addActionToMenu(splitAlphaMenu, "split_alpha_save_merged"); } menu.addSeparator(); if (singleLayer) { addActionToMenu(&menu, "show_in_timeline"); KisNodeSP node = m_filteringModel->nodeFromIndex(index); if (node && !node->inherits("KisTransformMask")) { addActionToMenu(&menu, "isolate_layer"); } menu.addAction(m_selectOpaque); } } menu.exec(pos); } } void KisLayerBox::slotMergeLayer() { if (!m_canvas) return; m_nodeManager->mergeLayer(); } void KisLayerBox::slotMinimalView() { m_wdgLayerBox->listLayers->setDisplayMode(KisNodeView::MinimalMode); } void KisLayerBox::slotDetailedView() { m_wdgLayerBox->listLayers->setDisplayMode(KisNodeView::DetailedMode); } void KisLayerBox::slotThumbnailView() { m_wdgLayerBox->listLayers->setDisplayMode(KisNodeView::ThumbnailMode); } void KisLayerBox::slotRmClicked() { if (!m_canvas) return; m_nodeManager->removeNode(); } void KisLayerBox::slotRaiseClicked() { if (!m_canvas) return; m_nodeManager->raiseNode(); } void KisLayerBox::slotLowerClicked() { if (!m_canvas) return; m_nodeManager->lowerNode(); } void KisLayerBox::slotPropertiesClicked() { if (!m_canvas) return; if (KisNodeSP active = m_nodeManager->activeNode()) { m_nodeManager->nodeProperties(active); } } void KisLayerBox::slotCompositeOpChanged(int index) { Q_UNUSED(index); if (!m_canvas) return; QString compositeOp = m_wdgLayerBox->cmbComposite->selectedCompositeOp().id(); m_nodeManager->nodeCompositeOpChanged(m_nodeManager->activeColorSpace()->compositeOp(compositeOp)); } void KisLayerBox::slotOpacityChanged() { if (!m_canvas) return; m_blockOpacityUpdate = true; m_nodeManager->nodeOpacityChanged(m_newOpacity, true); m_blockOpacityUpdate = false; } void KisLayerBox::slotOpacitySliderMoved(qreal opacity) { m_newOpacity = opacity; m_opacityDelayTimer.start(200); } void KisLayerBox::slotCollapsed(const QModelIndex &index) { KisNodeSP node = m_filteringModel->nodeFromIndex(index); if (node) { node->setCollapsed(true); } } void KisLayerBox::slotExpanded(const QModelIndex &index) { KisNodeSP node = m_filteringModel->nodeFromIndex(index); if (node) { node->setCollapsed(false); } } void KisLayerBox::slotSelectOpaque() { if (!m_canvas) return; QAction *action = m_canvas->viewManager()->actionManager()->actionByName("selectopaque"); if (action) { action->trigger(); } } void KisLayerBox::slotNodeCollapsedChanged() { expandNodesRecursively(m_image->rootLayer(), m_filteringModel, m_wdgLayerBox->listLayers); } inline bool isSelectionMask(KisNodeSP node) { return dynamic_cast(node.data()); } KisNodeSP KisLayerBox::findNonHidableNode(KisNodeSP startNode) { if (isSelectionMask(startNode) && startNode->parent() && !startNode->parent()->parent()) { KisNodeSP node = startNode->prevSibling(); while (node && isSelectionMask(node)) { node = node->prevSibling(); } if (!node) { node = startNode->nextSibling(); while (node && isSelectionMask(node)) { node = node->nextSibling(); } } if (!node) { node = m_image->root()->lastChild(); while (node && isSelectionMask(node)) { node = node->prevSibling(); } } KIS_ASSERT_RECOVER_NOOP(node && "cannot activate any node!"); startNode = node; } return startNode; } void KisLayerBox::slotEditGlobalSelection(bool showSelections) { KisNodeSP lastActiveNode = m_nodeManager->activeNode(); KisNodeSP activateNode = lastActiveNode; if (!showSelections) { activateNode = findNonHidableNode(activateNode); } m_nodeModel->setShowGlobalSelection(showSelections); if (showSelections) { KisNodeSP newMask = m_image->rootLayer()->selectionMask(); if (newMask) { activateNode = newMask; } } if (activateNode) { if (lastActiveNode != activateNode) { m_nodeManager->slotNonUiActivatedNode(activateNode); } else { setCurrentNode(lastActiveNode); } } } void KisLayerBox::selectionChanged(const QModelIndexList selection) { if (!m_nodeManager) return; /** * When the user clears the extended selection by clicking on the * empty area of the docker, the selection should be reset on to * the active layer, which might be even unselected(!). */ if (selection.isEmpty() && m_nodeManager->activeNode()) { QModelIndex selectedIndex = m_filteringModel->indexFromNode(m_nodeManager->activeNode()); m_wdgLayerBox->listLayers->selectionModel()-> setCurrentIndex(selectedIndex, QItemSelectionModel::ClearAndSelect); return; } QList selectedNodes; Q_FOREACH (const QModelIndex &idx, selection) { selectedNodes << m_filteringModel->nodeFromIndex(idx); } m_nodeManager->slotSetSelectedNodes(selectedNodes); updateUI(); } void KisLayerBox::slotAboutToRemoveRows(const QModelIndex &parent, int start, int end) { /** * Qt has changed its behavior when deleting an item. Previously * the selection priority was on the next item in the list, and * now it has shanged to the previous item. Here we just adjust * the selected item after the node removal. Please take care that * this method overrides what was done by the corresponding method * of QItemSelectionModel, which *has already done* its work. That * is why we use (start - 1) and (end + 1) in the activation * condition. * * See bug: https://bugs.kde.org/show_bug.cgi?id=345601 */ QModelIndex currentIndex = m_wdgLayerBox->listLayers->currentIndex(); QAbstractItemModel *model = m_filteringModel; if (currentIndex.isValid() && parent == currentIndex.parent() && currentIndex.row() >= start - 1 && currentIndex.row() <= end + 1) { QModelIndex old = currentIndex; if (model && end < model->rowCount(parent) - 1) // there are rows left below the change currentIndex = model->index(end + 1, old.column(), parent); else if (start > 0) // there are rows left above the change currentIndex = model->index(start - 1, old.column(), parent); else // there are no rows left in the table currentIndex = QModelIndex(); if (currentIndex.isValid() && currentIndex != old) { m_wdgLayerBox->listLayers->setCurrentIndex(currentIndex); } } } void KisLayerBox::slotNodeManagerChangedSelection(const KisNodeList &nodes) { if (!m_nodeManager) return; QModelIndexList newSelection; Q_FOREACH(KisNodeSP node, nodes) { newSelection << m_filteringModel->indexFromNode(node); } QItemSelectionModel *model = m_wdgLayerBox->listLayers->selectionModel(); if (KritaUtils::compareListsUnordered(newSelection, model->selectedIndexes())) { return; } QItemSelection selection; Q_FOREACH(const QModelIndex &idx, newSelection) { selection.select(idx, idx); } model->select(selection, QItemSelectionModel::ClearAndSelect); } void KisLayerBox::updateThumbnail() { m_wdgLayerBox->listLayers->updateNode(m_wdgLayerBox->listLayers->currentIndex()); } void KisLayerBox::slotRenameCurrentNode() { m_wdgLayerBox->listLayers->edit(m_wdgLayerBox->listLayers->currentIndex()); } void KisLayerBox::slotColorLabelChanged(int label) { KisNodeList nodes = m_nodeManager->selectedNodes(); Q_FOREACH(KisNodeSP node, nodes) { auto applyLabelFunc = [label](KisNodeSP node) { node->setColorLabelIndex(label); }; KisLayerUtils::recursiveApplyNodes(node, applyLabelFunc); } } void KisLayerBox::updateAvailableLabels() { if (!m_image) return; m_wdgLayerBox->cmbFilter->updateAvailableLabels(m_image->root()); } void KisLayerBox::updateLayerFiltering() { m_filteringModel->setAcceptedLabels(m_wdgLayerBox->cmbFilter->selectedColors()); } void KisLayerBox::slotKeyframeChannelAdded(KisKeyframeChannel *channel) { if (channel->id() == KisKeyframeChannel::Opacity.id()) { watchOpacityChannel(channel); } } void KisLayerBox::watchOpacityChannel(KisKeyframeChannel *channel) { if (m_opacityChannel) { m_opacityChannel->disconnect(this); } m_opacityChannel = channel; if (m_opacityChannel) { connect(m_opacityChannel, &KisKeyframeChannel::sigKeyframeAdded, this, &KisLayerBox::slotOpacityKeyframeChanged); connect(m_opacityChannel, &KisKeyframeChannel::sigKeyframeRemoved, this, &KisLayerBox::slotOpacityKeyframeChanged); connect(m_opacityChannel, &KisKeyframeChannel::sigKeyframeMoved, this, &KisLayerBox::slotOpacityKeyframeMoved); connect(m_opacityChannel, &KisKeyframeChannel::sigKeyframeChanged, this, &KisLayerBox::slotOpacityKeyframeChanged); } } void KisLayerBox::slotOpacityKeyframeChanged(KisKeyframeSP keyframe) { Q_UNUSED(keyframe); if (m_blockOpacityUpdate) return; updateUI(); } void KisLayerBox::slotOpacityKeyframeMoved(KisKeyframeSP keyframe, int fromTime) { Q_UNUSED(fromTime); slotOpacityKeyframeChanged(keyframe); } void KisLayerBox::slotImageTimeChanged(int time) { Q_UNUSED(time); updateUI(); } #include "moc_kis_layer_box.cpp"