diff --git a/libs/image/kis_layer_utils.cpp b/libs/image/kis_layer_utils.cpp index da511bd0f1..6071dd030b 100644 --- a/libs/image/kis_layer_utils.cpp +++ b/libs/image/kis_layer_utils.cpp @@ -1,1649 +1,1659 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_layer_utils.h" #include #include #include #include #include "kis_painter.h" #include "kis_image.h" #include "kis_node.h" #include "kis_layer.h" #include "kis_paint_layer.h" #include "kis_clone_layer.h" #include "kis_group_layer.h" #include "kis_selection.h" #include "kis_selection_mask.h" #include "kis_meta_data_merge_strategy.h" #include #include "commands/kis_image_layer_add_command.h" #include "commands/kis_image_layer_remove_command.h" #include "commands/kis_image_layer_move_command.h" #include "commands/kis_image_change_layers_command.h" #include "commands_new/kis_activate_selection_mask_command.h" #include "commands/kis_image_change_visibility_command.h" #include "kis_abstract_projection_plane.h" #include "kis_processing_applicator.h" #include "kis_image_animation_interface.h" #include "kis_keyframe_channel.h" #include "kis_command_utils.h" #include "commands_new/kis_change_projection_color_command.h" #include "kis_layer_properties_icons.h" #include "lazybrush/kis_colorize_mask.h" #include "commands/kis_node_property_list_command.h" #include "commands/kis_node_compositeop_command.h" #include #include #include "krita_utils.h" #include "kis_image_signal_router.h" namespace KisLayerUtils { void fetchSelectionMasks(KisNodeList mergedNodes, QVector &selectionMasks) { foreach (KisNodeSP node, mergedNodes) { Q_FOREACH(KisNodeSP child, node->childNodes(QStringList("KisSelectionMask"), KoProperties())) { KisSelectionMaskSP mask = qobject_cast(child.data()); if (mask) { selectionMasks.append(mask); } } } } struct MergeDownInfoBase { MergeDownInfoBase(KisImageSP _image) : image(_image), storage(new SwitchFrameCommand::SharedStorage()) { } virtual ~MergeDownInfoBase() {} KisImageWSP image; QVector selectionMasks; KisNodeSP dstNode; SwitchFrameCommand::SharedStorageSP storage; QSet frames; bool pinnedToTimeline = false; bool enableOnionSkins = false; virtual KisNodeList allSrcNodes() = 0; KisLayerSP dstLayer() { return qobject_cast(dstNode.data()); } }; struct MergeDownInfo : public MergeDownInfoBase { MergeDownInfo(KisImageSP _image, KisLayerSP _prevLayer, KisLayerSP _currLayer) : MergeDownInfoBase(_image), prevLayer(_prevLayer), currLayer(_currLayer) { frames = fetchLayerFramesRecursive(prevLayer) | fetchLayerFramesRecursive(currLayer); pinnedToTimeline = prevLayer->isPinnedToTimeline() || currLayer->isPinnedToTimeline(); const KisPaintLayer *paintLayer = qobject_cast(currLayer.data()); if (paintLayer) enableOnionSkins |= paintLayer->onionSkinEnabled(); paintLayer = qobject_cast(prevLayer.data()); if (paintLayer) enableOnionSkins |= paintLayer->onionSkinEnabled(); } KisLayerSP prevLayer; KisLayerSP currLayer; KisNodeList allSrcNodes() override { KisNodeList mergedNodes; mergedNodes << currLayer; mergedNodes << prevLayer; return mergedNodes; } }; struct MergeMultipleInfo : public MergeDownInfoBase { MergeMultipleInfo(KisImageSP _image, KisNodeList _mergedNodes) : MergeDownInfoBase(_image), mergedNodes(_mergedNodes) { foreach (KisNodeSP node, mergedNodes) { frames |= fetchLayerFramesRecursive(node); pinnedToTimeline |= node->isPinnedToTimeline(); const KisPaintLayer *paintLayer = qobject_cast(node.data()); if (paintLayer) { enableOnionSkins |= paintLayer->onionSkinEnabled(); } } } KisNodeList mergedNodes; bool nodesCompositingVaries = false; KisNodeList allSrcNodes() override { return mergedNodes; } }; typedef QSharedPointer MergeDownInfoBaseSP; typedef QSharedPointer MergeDownInfoSP; typedef QSharedPointer MergeMultipleInfoSP; struct FillSelectionMasks : public KUndo2Command { FillSelectionMasks(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { fetchSelectionMasks(m_info->allSrcNodes(), m_info->selectionMasks); } private: MergeDownInfoBaseSP m_info; }; struct DisableColorizeKeyStrokes : public KisCommandUtils::AggregateCommand { DisableColorizeKeyStrokes(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { recursiveApplyNodes(node, [this] (KisNodeSP node) { if (dynamic_cast(node.data()) && KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::colorizeEditKeyStrokes, true).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::colorizeEditKeyStrokes, false); addCommand(new KisNodePropertyListCommand(node, props)); } }); } } private: MergeDownInfoBaseSP m_info; }; struct DisableOnionSkins : public KisCommandUtils::AggregateCommand { DisableOnionSkins(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { recursiveApplyNodes(node, [this] (KisNodeSP node) { if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::onionSkins, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::onionSkins, false); addCommand(new KisNodePropertyListCommand(node, props)); } }); } } private: MergeDownInfoBaseSP m_info; }; struct DisableExtraCompositing : public KisCommandUtils::AggregateCommand { DisableExtraCompositing(MergeMultipleInfoSP info) : m_info(info) {} void populateChildCommands() override { /** * We disable extra compositing only in case all the layers have * the same compositing properties, therefore, we can just sum them using * Normal blend mode */ if (m_info->nodesCompositingVaries) return; // we should disable dirty requests on **redo only**, otherwise // the state of the layers will not be recovered on undo m_info->image->disableDirtyRequests(); Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { if (node->compositeOpId() != COMPOSITE_OVER) { addCommand(new KisNodeCompositeOpCommand(node, node->compositeOpId(), COMPOSITE_OVER)); } if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::inheritAlpha, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::inheritAlpha, false); addCommand(new KisNodePropertyListCommand(node, props)); } } m_info->image->enableDirtyRequests(); } private: MergeMultipleInfoSP m_info; }; struct DisablePassThroughForHeadsOnly : public KisCommandUtils::AggregateCommand { DisablePassThroughForHeadsOnly(MergeDownInfoBaseSP info, bool skipIfDstIsGroup = false) : m_info(info), m_skipIfDstIsGroup(skipIfDstIsGroup) { } void populateChildCommands() override { if (m_skipIfDstIsGroup && m_info->dstLayer() && m_info->dstLayer()->inherits("KisGroupLayer")) { return; } Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::passThrough, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::passThrough, false); addCommand(new KisNodePropertyListCommand(node, props)); } } } private: MergeDownInfoBaseSP m_info; bool m_skipIfDstIsGroup; }; struct RefreshHiddenAreas : public KUndo2Command { RefreshHiddenAreas(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { KisImageAnimationInterface *interface = m_info->image->animationInterface(); const QRect preparedRect = !interface->externalFrameActive() ? m_info->image->bounds() : QRect(); foreach (KisNodeSP node, m_info->allSrcNodes()) { refreshHiddenAreaAsync(m_info->image, node, preparedRect); } } private: MergeDownInfoBaseSP m_info; }; struct RefreshDelayedUpdateLayers : public KUndo2Command { RefreshDelayedUpdateLayers(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { foreach (KisNodeSP node, m_info->allSrcNodes()) { forceAllDelayedNodesUpdate(node); } } private: MergeDownInfoBaseSP m_info; }; struct KeepMergedNodesSelected : public KisCommandUtils::AggregateCommand { KeepMergedNodesSelected(MergeDownInfoSP info, bool finalizing) : m_singleInfo(info), m_finalizing(finalizing) {} KeepMergedNodesSelected(MergeMultipleInfoSP info, KisNodeSP putAfter, bool finalizing) : m_multipleInfo(info), m_finalizing(finalizing), m_putAfter(putAfter) {} void populateChildCommands() override { KisNodeSP prevNode; KisNodeSP nextNode; KisNodeList prevSelection; KisNodeList nextSelection; KisImageSP image; if (m_singleInfo) { prevNode = m_singleInfo->currLayer; nextNode = m_singleInfo->dstNode; image = m_singleInfo->image; } else if (m_multipleInfo) { prevNode = m_putAfter; nextNode = m_multipleInfo->dstNode; prevSelection = m_multipleInfo->allSrcNodes(); image = m_multipleInfo->image; } if (!m_finalizing) { addCommand(new KeepNodesSelectedCommand(prevSelection, KisNodeList(), prevNode, KisNodeSP(), image, false)); } else { addCommand(new KeepNodesSelectedCommand(KisNodeList(), nextSelection, KisNodeSP(), nextNode, image, true)); } } private: MergeDownInfoSP m_singleInfo; MergeMultipleInfoSP m_multipleInfo; bool m_finalizing; KisNodeSP m_putAfter; }; struct CreateMergedLayer : public KisCommandUtils::AggregateCommand { CreateMergedLayer(MergeDownInfoSP info) : m_info(info) {} void populateChildCommands() override { // actual merging done by KisLayer::createMergedLayer (or specialized descendant) m_info->dstNode = m_info->currLayer->createMergedLayerTemplate(m_info->prevLayer); if (m_info->frames.size() > 0) { m_info->dstNode->enableAnimation(); m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); } m_info->dstNode->setPinnedToTimeline(m_info->pinnedToTimeline); KisPaintLayer *dstPaintLayer = qobject_cast(m_info->dstNode.data()); if (dstPaintLayer) { dstPaintLayer->setOnionSkinEnabled(m_info->enableOnionSkins); } } private: MergeDownInfoSP m_info; }; struct CreateMergedLayerMultiple : public KisCommandUtils::AggregateCommand { CreateMergedLayerMultiple(MergeMultipleInfoSP info, const QString name = QString() ) : m_info(info), m_name(name) {} void populateChildCommands() override { QString mergedLayerName; if (m_name.isEmpty()){ const QString mergedLayerSuffix = i18n("Merged"); mergedLayerName = m_info->mergedNodes.first()->name(); if (!mergedLayerName.endsWith(mergedLayerSuffix)) { mergedLayerName = QString("%1 %2") .arg(mergedLayerName).arg(mergedLayerSuffix); } } else { mergedLayerName = m_name; } KisPaintLayer *dstPaintLayer = new KisPaintLayer(m_info->image, mergedLayerName, OPACITY_OPAQUE_U8); m_info->dstNode = dstPaintLayer; if (m_info->frames.size() > 0) { m_info->dstNode->enableAnimation(); m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); } auto channelFlagsLazy = [](KisNodeSP node) { KisLayer *layer = dynamic_cast(node.data()); return layer ? layer->channelFlags() : QBitArray(); }; QString compositeOpId; QBitArray channelFlags; bool compositionVaries = false; bool isFirstCycle = true; foreach (KisNodeSP node, m_info->allSrcNodes()) { if (isFirstCycle) { compositeOpId = node->compositeOpId(); channelFlags = channelFlagsLazy(node); isFirstCycle = false; } else if (compositeOpId != node->compositeOpId() || channelFlags != channelFlagsLazy(node)) { compositionVaries = true; break; } KisLayerSP layer = qobject_cast(node.data()); if (layer && layer->layerStyle()) { compositionVaries = true; break; } } if (!compositionVaries) { if (!compositeOpId.isEmpty()) { m_info->dstNode->setCompositeOpId(compositeOpId); } if (m_info->dstLayer() && !channelFlags.isEmpty()) { m_info->dstLayer()->setChannelFlags(channelFlags); } } m_info->nodesCompositingVaries = compositionVaries; m_info->dstNode->setPinnedToTimeline(m_info->pinnedToTimeline); dstPaintLayer->setOnionSkinEnabled(m_info->enableOnionSkins); } private: MergeMultipleInfoSP m_info; QString m_name; }; struct MergeLayers : public KisCommandUtils::AggregateCommand { MergeLayers(MergeDownInfoSP info) : m_info(info) {} void populateChildCommands() override { // actual merging done by KisLayer::createMergedLayer (or specialized descendant) m_info->currLayer->fillMergedLayerTemplate(m_info->dstLayer(), m_info->prevLayer); } private: MergeDownInfoSP m_info; }; struct MergeLayersMultiple : public KisCommandUtils::AggregateCommand { MergeLayersMultiple(MergeMultipleInfoSP info) : m_info(info) {} void populateChildCommands() override { KisPainter gc(m_info->dstNode->paintDevice()); foreach (KisNodeSP node, m_info->allSrcNodes()) { QRect rc = node->exactBounds() | m_info->image->bounds(); node->projectionPlane()->apply(&gc, rc); } } private: MergeMultipleInfoSP m_info; }; struct MergeMetaData : public KUndo2Command { MergeMetaData(MergeDownInfoSP info, const KisMetaData::MergeStrategy* strategy) : m_info(info), m_strategy(strategy) {} void redo() override { QRect layerProjectionExtent = m_info->currLayer->projection()->extent(); QRect prevLayerProjectionExtent = m_info->prevLayer->projection()->extent(); int prevLayerArea = prevLayerProjectionExtent.width() * prevLayerProjectionExtent.height(); int layerArea = layerProjectionExtent.width() * layerProjectionExtent.height(); QList scores; double norm = qMax(prevLayerArea, layerArea); scores.append(prevLayerArea / norm); scores.append(layerArea / norm); QList srcs; srcs.append(m_info->prevLayer->metaData()); srcs.append(m_info->currLayer->metaData()); m_strategy->merge(m_info->dstLayer()->metaData(), srcs, scores); } private: MergeDownInfoSP m_info; const KisMetaData::MergeStrategy *m_strategy; }; KeepNodesSelectedCommand::KeepNodesSelectedCommand(const KisNodeList &selectedBefore, const KisNodeList &selectedAfter, KisNodeSP activeBefore, KisNodeSP activeAfter, KisImageSP image, bool finalize, KUndo2Command *parent) : FlipFlopCommand(finalize, parent), m_selectedBefore(selectedBefore), m_selectedAfter(selectedAfter), m_activeBefore(activeBefore), m_activeAfter(activeAfter), m_image(image) { } void KeepNodesSelectedCommand::partB() { KisImageSignalType type; if (getState() == State::FINALIZING) { type = ComplexNodeReselectionSignal(m_activeAfter, m_selectedAfter); } else { type = ComplexNodeReselectionSignal(m_activeBefore, m_selectedBefore); } m_image->signalRouter()->emitNotification(type); } SelectGlobalSelectionMask::SelectGlobalSelectionMask(KisImageSP image) : m_image(image) { } void SelectGlobalSelectionMask::redo() { KisImageSignalType type = ComplexNodeReselectionSignal(m_image->rootLayer()->selectionMask(), KisNodeList()); m_image->signalRouter()->emitNotification(type); } RemoveNodeHelper::~RemoveNodeHelper() { } /** * The removal of two nodes in one go may be a bit tricky, because one * of them may be the clone of another. If we remove the source of a * clone layer, it will reincarnate into a paint layer. In this case * the pointer to the second layer will be lost. * * That's why we need to care about the order of the nodes removal: * the clone --- first, the source --- last. */ void RemoveNodeHelper::safeRemoveMultipleNodes(KisNodeList nodes, KisImageSP image) { const bool lastLayer = scanForLastLayer(image, nodes); auto isNodeWeird = [] (KisNodeSP node) { const bool normalCompositeMode = node->compositeOpId() == COMPOSITE_OVER; KisLayer *layer = dynamic_cast(node.data()); const bool hasInheritAlpha = layer && layer->alphaChannelDisabled(); return !normalCompositeMode && !hasInheritAlpha; }; while (!nodes.isEmpty()) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if (!checkIsSourceForClone(*it, nodes)) { KisNodeSP node = *it; addCommandImpl(new KisImageLayerRemoveCommand(image, node, !isNodeWeird(node), true)); it = nodes.erase(it); } else { ++it; } } } if (lastLayer) { KisLayerSP newLayer = new KisPaintLayer(image.data(), image->nextLayerName(), OPACITY_OPAQUE_U8, image->colorSpace()); addCommandImpl(new KisImageLayerAddCommand(image, newLayer, image->root(), KisNodeSP(), false, false)); } } bool RemoveNodeHelper::checkIsSourceForClone(KisNodeSP src, const KisNodeList &nodes) { foreach (KisNodeSP node, nodes) { if (node == src) continue; KisCloneLayer *clone = dynamic_cast(node.data()); if (clone && KisNodeSP(clone->copyFrom()) == src) { return true; } } return false; } bool RemoveNodeHelper::scanForLastLayer(KisImageWSP image, KisNodeList nodesToRemove) { bool removeLayers = false; Q_FOREACH(KisNodeSP nodeToRemove, nodesToRemove) { if (qobject_cast(nodeToRemove.data())) { removeLayers = true; break; } } if (!removeLayers) return false; bool lastLayer = true; KisNodeSP node = image->root()->firstChild(); while (node) { if (!nodesToRemove.contains(node) && qobject_cast(node.data()) && !node->isFakeNode()) { lastLayer = false; break; } node = node->nextSibling(); } return lastLayer; } SimpleRemoveLayers::SimpleRemoveLayers(const KisNodeList &nodes, KisImageSP image) : m_nodes(nodes), m_image(image) { } void SimpleRemoveLayers::populateChildCommands() { if (m_nodes.isEmpty()) return; safeRemoveMultipleNodes(m_nodes, m_image); } void SimpleRemoveLayers::addCommandImpl(KUndo2Command *cmd) { addCommand(cmd); } struct InsertNode : public KisCommandUtils::AggregateCommand { InsertNode(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter) {} void populateChildCommands() override { addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, m_putAfter->parent(), m_putAfter, true, false)); } private: virtual void addCommandImpl(KUndo2Command *cmd) { addCommand(cmd); } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; struct CleanUpNodes : private RemoveNodeHelper, public KisCommandUtils::AggregateCommand { CleanUpNodes(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter) {} static void findPerfectParent(KisNodeList nodesToDelete, KisNodeSP &putAfter, KisNodeSP &parent) { if (!putAfter) { putAfter = nodesToDelete.last(); } // Add the new merged node on top of the active node // -- checking all parents if they are included in nodesToDelete // Not every descendant is included in nodesToDelete even if in fact // they are going to be deleted, so we need to check it. // If we consider the path from root to the putAfter node, // if there are any nodes marked for deletion, any node afterwards // is going to be deleted, too. // example: root . . . . . ! ! . . ! ! ! ! . . . . putAfter // it should be: root . . . . . ! ! ! ! ! ! ! ! ! ! ! ! !putAfter // and here: root . . . . X ! ! . . ! ! ! ! . . . . putAfter // you can see which node is "the perfect ancestor" // (marked X; called "parent" in the function arguments). // and here: root . . . . . O ! . . ! ! ! ! . . . . putAfter // you can see which node is "the topmost deleted ancestor" (marked 'O') KisNodeSP node = putAfter->parent(); bool foundDeletedAncestor = false; KisNodeSP topmostAncestorToDelete = nullptr; while (node) { if (nodesToDelete.contains(node) && !nodesToDelete.contains(node->parent())) { foundDeletedAncestor = true; topmostAncestorToDelete = node; // Here node is to be deleted and its parent is not, // so its parent is the one of the first not deleted (="perfect") ancestors. // We need the one that is closest to the top (root) } node = node->parent(); } if (foundDeletedAncestor) { parent = topmostAncestorToDelete->parent(); putAfter = topmostAncestorToDelete; } else { parent = putAfter->parent(); // putAfter (and none of its ancestors) is to be deleted, so its parent is the first not deleted ancestor } } void populateChildCommands() override { KisNodeList nodesToDelete = m_info->allSrcNodes(); KisNodeSP parent; findPerfectParent(nodesToDelete, m_putAfter, parent); if (!parent) { KisNodeSP oldRoot = m_info->image->root(); KisNodeSP newRoot(new KisGroupLayer(m_info->image, "root", OPACITY_OPAQUE_U8)); // copy all fake nodes into the new image KisLayerUtils::recursiveApplyNodes(oldRoot, [this, oldRoot, newRoot] (KisNodeSP node) { if (node->isFakeNode() && node->parent() == oldRoot) { addCommand(new KisImageLayerAddCommand(m_info->image, node->clone(), newRoot, KisNodeSP(), false, false)); } }); addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, newRoot, KisNodeSP(), true, false)); addCommand(new KisImageChangeLayersCommand(m_info->image, oldRoot, newRoot)); } else { addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, parent, m_putAfter, true, false)); /** * We can merge selection masks, in this case dstLayer is not defined! */ if (m_info->dstLayer()) { reparentSelectionMasks(m_info->image, m_info->dstLayer(), m_info->selectionMasks); } KisNodeList safeNodesToDelete = m_info->allSrcNodes(); for (KisNodeList::iterator it = safeNodesToDelete.begin(); it != safeNodesToDelete.end(); ++it) { KisNodeSP node = *it; if (node->userLocked() && node->visible()) { addCommand(new KisImageChangeVisibilityCommand(false, node)); } } KritaUtils::filterContainer(safeNodesToDelete, [](KisNodeSP node) { return !node->userLocked(); }); safeRemoveMultipleNodes(safeNodesToDelete, m_info->image); } } private: void addCommandImpl(KUndo2Command *cmd) override { addCommand(cmd); } void reparentSelectionMasks(KisImageSP image, KisLayerSP newLayer, const QVector &selectionMasks) { KIS_SAFE_ASSERT_RECOVER_RETURN(newLayer); foreach (KisSelectionMaskSP mask, selectionMasks) { addCommand(new KisImageLayerMoveCommand(image, mask, newLayer, newLayer->lastChild())); addCommand(new KisActivateSelectionMaskCommand(mask, false)); } } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; SwitchFrameCommand::SharedStorage::~SharedStorage() { } SwitchFrameCommand::SwitchFrameCommand(KisImageSP image, int time, bool finalize, SharedStorageSP storage) : FlipFlopCommand(finalize), m_image(image), m_newTime(time), m_storage(storage) {} SwitchFrameCommand::~SwitchFrameCommand() {} void SwitchFrameCommand::partA() { KisImageAnimationInterface *interface = m_image->animationInterface(); const int currentTime = interface->currentTime(); if (currentTime == m_newTime) { m_storage->value = m_newTime; return; } interface->image()->disableUIUpdates(); interface->saveAndResetCurrentTime(m_newTime, &m_storage->value); } void SwitchFrameCommand::partB() { KisImageAnimationInterface *interface = m_image->animationInterface(); const int currentTime = interface->currentTime(); if (currentTime == m_storage->value) { return; } interface->restoreCurrentTime(&m_storage->value); interface->image()->enableUIUpdates(); } struct AddNewFrame : public KisCommandUtils::AggregateCommand { AddNewFrame(MergeDownInfoBaseSP info, int frame) : m_info(info), m_frame(frame) {} void populateChildCommands() override { KUndo2Command *cmd = new KisCommandUtils::SkipFirstRedoWrapper(); KisKeyframeChannel *channel = m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id()); KisKeyframeSP keyframe = channel->addKeyframe(m_frame, cmd); applyKeyframeColorLabel(keyframe); addCommand(cmd); } void applyKeyframeColorLabel(KisKeyframeSP dstKeyframe) { Q_FOREACH(KisNodeSP srcNode, m_info->allSrcNodes()) { Q_FOREACH(KisKeyframeChannel *channel, srcNode->keyframeChannels().values()) { KisKeyframeSP keyframe = channel->keyframeAt(m_frame); if (!keyframe.isNull() && keyframe->colorLabel() != 0) { dstKeyframe->setColorLabel(keyframe->colorLabel()); return; } } } dstKeyframe->setColorLabel(0); } private: MergeDownInfoBaseSP m_info; int m_frame; }; QSet fetchLayerFrames(KisNodeSP node) { KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!channel) return QSet(); return channel->allKeyframeIds(); } QSet fetchLayerFramesRecursive(KisNodeSP rootNode) { QSet frames = fetchLayerFrames(rootNode); KisNodeSP node = rootNode->firstChild(); while(node) { frames |= fetchLayerFramesRecursive(node); node = node->nextSibling(); } return frames; } void updateFrameJobs(FrameJobs *jobs, KisNodeSP node) { QSet frames = fetchLayerFrames(node); if (frames.isEmpty()) { (*jobs)[0].insert(node); } else { foreach (int frame, frames) { (*jobs)[frame].insert(node); } } } void updateFrameJobsRecursive(FrameJobs *jobs, KisNodeSP rootNode) { updateFrameJobs(jobs, rootNode); KisNodeSP node = rootNode->firstChild(); while(node) { updateFrameJobsRecursive(jobs, node); node = node->nextSibling(); } } /** * \see a comment in mergeMultipleLayersImpl() */ void mergeDown(KisImageSP image, KisLayerSP layer, const KisMetaData::MergeStrategy* strategy) { if (!layer->prevSibling()) return; // XXX: this breaks if we allow free mixing of masks and layers KisLayerSP prevLayer = qobject_cast(layer->prevSibling().data()); if (!prevLayer) return; if (!layer->visible() && !prevLayer->visible()) { return; } KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, kundo2_i18n("Merge Down")); if (layer->visible() && prevLayer->visible()) { MergeDownInfoSP info(new MergeDownInfo(image, prevLayer, layer)); // disable key strokes on all colorize masks, all onion skins on // paint layers and wait until update is finished with a barrier applicator.applyCommand(new DisableColorizeKeyStrokes(info)); applicator.applyCommand(new DisableOnionSkins(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); applicator.applyCommand(new KeepMergedNodesSelected(info, false)); applicator.applyCommand(new FillSelectionMasks(info)); applicator.applyCommand(new CreateMergedLayer(info), KisStrokeJobData::BARRIER); // NOTE: shape layer may have emitted spontaneous jobs during layer creation, // wait for them to complete! applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); // in two-layer mode we disable pass through only when the destination layer // is not a group layer applicator.applyCommand(new DisablePassThroughForHeadsOnly(info, true)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); if (info->frames.size() > 0) { foreach (int frame, info->frames) { applicator.applyCommand(new SwitchFrameCommand(info->image, frame, false, info->storage)); applicator.applyCommand(new AddNewFrame(info, frame)); applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new SwitchFrameCommand(info->image, frame, true, info->storage)); } } else { applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayers(info), KisStrokeJobData::BARRIER); } applicator.applyCommand(new MergeMetaData(info, strategy), KisStrokeJobData::BARRIER); applicator.applyCommand(new CleanUpNodes(info, layer), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepMergedNodesSelected(info, true)); } else if (layer->visible()) { applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), layer, KisNodeSP(), image, false)); applicator.applyCommand( new SimpleRemoveLayers(KisNodeList() << prevLayer, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), KisNodeSP(), layer, image, true)); } else if (prevLayer->visible()) { applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), layer, KisNodeSP(), image, false)); applicator.applyCommand( new SimpleRemoveLayers(KisNodeList() << layer, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), KisNodeSP(), prevLayer, image, true)); } applicator.end(); } bool checkIsChildOf(KisNodeSP node, const KisNodeList &parents) { KisNodeList nodeParents; KisNodeSP parent = node->parent(); while (parent) { nodeParents << parent; parent = parent->parent(); } foreach(KisNodeSP perspectiveParent, parents) { if (nodeParents.contains(perspectiveParent)) { return true; } } return false; } bool checkIsCloneOf(KisNodeSP node, const KisNodeList &nodes) { bool result = false; KisCloneLayer *clone = dynamic_cast(node.data()); if (clone) { KisNodeSP cloneSource = KisNodeSP(clone->copyFrom()); Q_FOREACH(KisNodeSP subtree, nodes) { result = recursiveFindNode(subtree, [cloneSource](KisNodeSP node) -> bool { return node == cloneSource; }); if (!result) { result = checkIsCloneOf(cloneSource, nodes); } if (result) { break; } } } return result; } void filterMergableNodes(KisNodeList &nodes, bool allowMasks) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if ((!allowMasks && !qobject_cast(it->data())) || checkIsChildOf(*it, nodes)) { //qDebug() << "Skipping node" << ppVar((*it)->name()); it = nodes.erase(it); } else { ++it; } } } void sortMergableNodes(KisNodeSP root, KisNodeList &inputNodes, KisNodeList &outputNodes) { KisNodeList::iterator it = std::find(inputNodes.begin(), inputNodes.end(), root); if (it != inputNodes.end()) { outputNodes << *it; inputNodes.erase(it); } if (inputNodes.isEmpty()) { return; } KisNodeSP child = root->firstChild(); while (child) { sortMergableNodes(child, inputNodes, outputNodes); child = child->nextSibling(); } /** * By the end of recursion \p inputNodes must be empty */ KIS_ASSERT_RECOVER_NOOP(root->parent() || inputNodes.isEmpty()); } KisNodeList sortMergableNodes(KisNodeSP root, KisNodeList nodes) { KisNodeList result; sortMergableNodes(root, nodes, result); return result; } KisNodeList sortAndFilterMergableInternalNodes(KisNodeList nodes, bool allowMasks) { KIS_ASSERT_RECOVER(!nodes.isEmpty()) { return nodes; } KisNodeSP root; Q_FOREACH(KisNodeSP node, nodes) { KisNodeSP localRoot = node; while (localRoot->parent()) { localRoot = localRoot->parent(); } if (!root) { root = localRoot; } KIS_ASSERT_RECOVER(root == localRoot) { return nodes; } } KisNodeList result; sortMergableNodes(root, nodes, result); filterMergableNodes(result, allowMasks); return result; } KisNodeList sortAndFilterAnyMergableNodesSafe(const KisNodeList &nodes, KisImageSP image) { KisNodeList filteredNodes = nodes; KisNodeList sortedNodes; KisLayerUtils::filterMergableNodes(filteredNodes, true); bool haveExternalNodes = false; Q_FOREACH (KisNodeSP node, nodes) { if (node->graphListener() != image->root()->graphListener()) { haveExternalNodes = true; break; } } if (!haveExternalNodes) { KisLayerUtils::sortMergableNodes(image->root(), filteredNodes, sortedNodes); } else { sortedNodes = filteredNodes; } return sortedNodes; } void addCopyOfNameTag(KisNodeSP node) { const QString prefix = i18n("Copy of"); QString newName = node->name(); if (!newName.startsWith(prefix)) { newName = QString("%1 %2").arg(prefix).arg(newName); node->setName(newName); } } KisNodeList findNodesWithProps(KisNodeSP root, const KoProperties &props, bool excludeRoot) { KisNodeList nodes; if ((!excludeRoot || root->parent()) && root->check(props)) { nodes << root; } KisNodeSP node = root->firstChild(); while (node) { nodes += findNodesWithProps(node, props, excludeRoot); node = node->nextSibling(); } return nodes; } KisNodeList filterInvisibleNodes(const KisNodeList &nodes, KisNodeList *invisibleNodes, KisNodeSP *putAfter) { KIS_ASSERT_RECOVER(invisibleNodes) { return nodes; } KIS_ASSERT_RECOVER(putAfter) { return nodes; } KisNodeList visibleNodes; int putAfterIndex = -1; Q_FOREACH(KisNodeSP node, nodes) { if (node->visible() || node->userLocked()) { visibleNodes << node; } else { *invisibleNodes << node; if (node == *putAfter) { putAfterIndex = visibleNodes.size() - 1; } } } if (!visibleNodes.isEmpty() && putAfterIndex >= 0) { putAfterIndex = qBound(0, putAfterIndex, visibleNodes.size() - 1); *putAfter = visibleNodes[putAfterIndex]; } return visibleNodes; } void filterUnlockedNodes(KisNodeList &nodes) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if ((*it)->userLocked()) { it = nodes.erase(it); } else { ++it; } } } void changeImageDefaultProjectionColor(KisImageSP image, const KoColor &color) { KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, image->root(), KisProcessingApplicator::RECURSIVE, emitSignals, kundo2_i18n("Change projection color"), 0, 142857 + 1); applicator.applyCommand(new KisChangeProjectionColorCommand(image, color), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); applicator.end(); } /** * There might be two approaches for merging multiple layers: * * 1) Consider the selected nodes as a distinct "group" and merge them * as if they were isolated from the rest of the image. The key point * of this approach is that the look of the image will change, when * merging "weird" layers, like adjustment layers or layers with * non-normal blending mode. * * 2) Merge layers in a way to keep the look of the image as unchanged as * possible. With this approach one uses a few heuristics: * * * when merging multiple layers with non-normal (but equal) blending * mode, first merge these layers together using Normal blending mode, * then set blending mode of the result to the original blending mode * * * when merging multiple layers with different blending modes or * layer styles, they are first rasterized, and then laid over each * other with their own composite op. The blending mode of the final * layer is set to Normal, so the user could clearly see that he should * choose the correct blending mode. * * Krita uses the second approach: after merge operation, the image should look * as if nothing has happened (if it is technically possible). */ void mergeMultipleLayersImpl(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter, bool flattenSingleLayer, const KUndo2MagicString &actionName, bool cleanupNodes = true, const QString layerName = QString()) { if (!putAfter) { putAfter = mergedNodes.first(); } filterMergableNodes(mergedNodes); { KisNodeList tempNodes; std::swap(mergedNodes, tempNodes); sortMergableNodes(image->root(), tempNodes, mergedNodes); } if (mergedNodes.size() <= 1 && (!flattenSingleLayer && mergedNodes.size() == 1)) return; KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; emitSignals << ComplexNodeReselectionSignal(KisNodeSP(), KisNodeList(), KisNodeSP(), mergedNodes); KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, actionName); KisNodeList originalNodes = mergedNodes; KisNodeList invisibleNodes; mergedNodes = filterInvisibleNodes(originalNodes, &invisibleNodes, &putAfter); if (!invisibleNodes.isEmpty() && !mergedNodes.isEmpty()) { /* If the putAfter node is invisible, * we should instead pick one of the nodes * to be merged to avoid a null putAfter. */ if (!putAfter->visible()){ putAfter = mergedNodes.first(); } applicator.applyCommand( new SimpleRemoveLayers(invisibleNodes, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } if (mergedNodes.size() > 1 || invisibleNodes.isEmpty()) { MergeMultipleInfoSP info(new MergeMultipleInfo(image, mergedNodes)); // disable key strokes on all colorize masks, all onion skins on // paint layers and wait until update is finished with a barrier applicator.applyCommand(new DisableColorizeKeyStrokes(info)); applicator.applyCommand(new DisableOnionSkins(info)); applicator.applyCommand(new DisablePassThroughForHeadsOnly(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); applicator.applyCommand(new KeepMergedNodesSelected(info, putAfter, false)); applicator.applyCommand(new FillSelectionMasks(info)); applicator.applyCommand(new CreateMergedLayerMultiple(info, layerName), KisStrokeJobData::BARRIER); applicator.applyCommand(new DisableExtraCompositing(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); if (!info->frames.isEmpty()) { foreach (int frame, info->frames) { applicator.applyCommand(new SwitchFrameCommand(info->image, frame, false, info->storage)); applicator.applyCommand(new AddNewFrame(info, frame)); applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayersMultiple(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new SwitchFrameCommand(info->image, frame, true, info->storage)); } } else { applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayersMultiple(info), KisStrokeJobData::BARRIER); } //applicator.applyCommand(new MergeMetaData(info, strategy), KisStrokeJobData::BARRIER); if (cleanupNodes){ applicator.applyCommand(new CleanUpNodes(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } else { applicator.applyCommand(new InsertNode(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } applicator.applyCommand(new KeepMergedNodesSelected(info, putAfter, true)); } applicator.end(); } void mergeMultipleLayers(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter) { mergeMultipleLayersImpl(image, mergedNodes, putAfter, false, kundo2_i18n("Merge Selected Nodes")); } void newLayerFromVisible(KisImageSP image, KisNodeSP putAfter) { KisNodeList mergedNodes; mergedNodes << image->root(); mergeMultipleLayersImpl(image, mergedNodes, putAfter, true, kundo2_i18n("New From Visible"), false, i18nc("New layer created from all the visible layers", "Visible")); } struct MergeSelectionMasks : public KisCommandUtils::AggregateCommand { MergeSelectionMasks(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter){} void populateChildCommands() override { KisNodeSP parent; CleanUpNodes::findPerfectParent(m_info->allSrcNodes(), m_putAfter, parent); KisLayerSP parentLayer; do { parentLayer = qobject_cast(parent.data()); parent = parent->parent(); } while(!parentLayer && parent); KisSelectionSP selection = new KisSelection(); foreach (KisNodeSP node, m_info->allSrcNodes()) { KisMaskSP mask = dynamic_cast(node.data()); if (!mask) continue; selection->pixelSelection()->applySelection( mask->selection()->pixelSelection(), SELECTION_ADD); } KisSelectionMaskSP mergedMask = new KisSelectionMask(m_info->image, i18n("Selection Mask")); mergedMask->initSelection(parentLayer); mergedMask->setSelection(selection); m_info->dstNode = mergedMask; } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; struct ActivateSelectionMask : public KisCommandUtils::AggregateCommand { ActivateSelectionMask(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { KisSelectionMaskSP mergedMask = dynamic_cast(m_info->dstNode.data()); addCommand(new KisActivateSelectionMaskCommand(mergedMask, true)); } private: MergeDownInfoBaseSP m_info; }; bool tryMergeSelectionMasks(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter) { QList selectionMasks; for (auto it = mergedNodes.begin(); it != mergedNodes.end(); /*noop*/) { KisSelectionMaskSP mask = dynamic_cast(it->data()); if (!mask) { it = mergedNodes.erase(it); } else { selectionMasks.append(mask); ++it; } } if (mergedNodes.isEmpty()) return false; KisLayerSP parentLayer = qobject_cast(selectionMasks.first()->parent().data()); KIS_ASSERT_RECOVER(parentLayer) { return 0; } KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, kundo2_i18n("Merge Selection Masks")); MergeMultipleInfoSP info(new MergeMultipleInfo(image, mergedNodes)); applicator.applyCommand(new MergeSelectionMasks(info, putAfter)); applicator.applyCommand(new CleanUpNodes(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new ActivateSelectionMask(info)); applicator.end(); return true; } void flattenLayer(KisImageSP image, KisLayerSP layer) { if (!layer->childCount() && !layer->layerStyle()) return; KisNodeList mergedNodes; mergedNodes << layer; mergeMultipleLayersImpl(image, mergedNodes, layer, true, kundo2_i18n("Flatten Layer")); } void flattenImage(KisImageSP image, KisNodeSP activeNode) { if (!activeNode) { activeNode = image->root()->lastChild(); } KisNodeList mergedNodes; mergedNodes << image->root(); mergeMultipleLayersImpl(image, mergedNodes, activeNode, true, kundo2_i18n("Flatten Image")); } KisSimpleUpdateCommand::KisSimpleUpdateCommand(KisNodeList nodes, bool finalize, KUndo2Command *parent) : FlipFlopCommand(finalize, parent), m_nodes(nodes) { } void KisSimpleUpdateCommand::partB() { updateNodes(m_nodes); } void KisSimpleUpdateCommand::updateNodes(const KisNodeList &nodes) { Q_FOREACH(KisNodeSP node, nodes) { node->setDirty(node->extent()); } } KisNodeSP recursiveFindNode(KisNodeSP node, std::function func) { if (func(node)) { return node; } node = node->firstChild(); while (node) { KisNodeSP resultNode = recursiveFindNode(node, func); if (resultNode) { return resultNode; } node = node->nextSibling(); } return 0; } KisNodeSP findNodeByUuid(KisNodeSP root, const QUuid &uuid) { return recursiveFindNode(root, [uuid] (KisNodeSP node) { return node->uuid() == uuid; }); } void forceAllDelayedNodesUpdate(KisNodeSP root) { KisLayerUtils::recursiveApplyNodes(root, [] (KisNodeSP node) { KisDelayedUpdateNodeInterface *delayedUpdate = dynamic_cast(node.data()); if (delayedUpdate) { delayedUpdate->forceUpdateTimedNode(); } }); } bool hasDelayedNodeWithUpdates(KisNodeSP root) { return recursiveFindNode(root, [] (KisNodeSP node) { KisDelayedUpdateNodeInterface *delayedUpdate = dynamic_cast(node.data()); return delayedUpdate ? delayedUpdate->hasPendingTimedUpdates() : false; }); } void forceAllHiddenOriginalsUpdate(KisNodeSP root) { KisLayerUtils::recursiveApplyNodes(root, [] (KisNodeSP node) { KisCroppedOriginalLayerInterface *croppedUpdate = dynamic_cast(node.data()); if (croppedUpdate) { croppedUpdate->forceUpdateHiddenAreaOnOriginal(); } }); } KisImageSP findImageByHierarchy(KisNodeSP node) { while (node) { const KisLayer *layer = dynamic_cast(node.data()); if (layer) { return layer->image(); } node = node->parent(); } return 0; } namespace Private { QRect realNodeChangeRect(KisNodeSP rootNode, QRect currentRect = QRect()) { KisNodeSP node = rootNode->firstChild(); while(node) { currentRect |= realNodeChangeRect(node, currentRect); node = node->nextSibling(); } if (!rootNode->isFakeNode()) { // TODO: it would be better to count up changeRect inside // node's extent() method currentRect |= rootNode->projectionPlane()->changeRect(rootNode->exactBounds()); } return currentRect; } } void refreshHiddenAreaAsync(KisImageSP image, KisNodeSP rootNode, const QRect &preparedArea) { QRect realNodeRect = Private::realNodeChangeRect(rootNode); if (!preparedArea.contains(realNodeRect)) { QRegion dirtyRegion = realNodeRect; dirtyRegion -= preparedArea; auto rc = dirtyRegion.begin(); while (rc != dirtyRegion.end()) { image->refreshGraphAsync(rootNode, *rc, realNodeRect); rc++; } } } QRect recursiveTightNodeVisibleBounds(KisNodeSP rootNode) { QRect exactBounds; recursiveApplyNodes(rootNode, [&exactBounds] (KisNodeSP node) { exactBounds |= node->projectionPlane()->tightUserVisibleBounds(); }); return exactBounds; } + KisNodeSP findRoot(KisNodeSP node) + { + if (!node) return node; + + while (node->parent()) { + node = node->parent(); + } + return node; + } + } diff --git a/libs/image/kis_layer_utils.h b/libs/image/kis_layer_utils.h index 7a7f33a41d..665e15db07 100644 --- a/libs/image/kis_layer_utils.h +++ b/libs/image/kis_layer_utils.h @@ -1,239 +1,241 @@ /* * 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_LAYER_UTILS_H #define __KIS_LAYER_UTILS_H #include #include "kundo2command.h" #include "kis_types.h" #include "kritaimage_export.h" #include "kis_command_utils.h" class KoProperties; class KoColor; class QUuid; namespace KisMetaData { class MergeStrategy; } namespace KisLayerUtils { KRITAIMAGE_EXPORT void sortMergableNodes(KisNodeSP root, QList &inputNodes, QList &outputNodes); KRITAIMAGE_EXPORT KisNodeList sortMergableNodes(KisNodeSP root, KisNodeList nodes); KRITAIMAGE_EXPORT void filterMergableNodes(KisNodeList &nodes, bool allowMasks = false); KRITAIMAGE_EXPORT KisNodeList sortAndFilterAnyMergableNodesSafe(const KisNodeList &nodes, KisImageSP image); KRITAIMAGE_EXPORT bool checkIsChildOf(KisNodeSP node, const KisNodeList &parents); KRITAIMAGE_EXPORT void filterUnlockedNodes(KisNodeList &nodes); KRITAIMAGE_EXPORT void refreshHiddenAreaAsync(KisImageSP image, KisNodeSP rootNode, const QRect &preparedArea); KRITAIMAGE_EXPORT QRect recursiveTightNodeVisibleBounds(KisNodeSP rootNode); /** * Returns true if: * o \p node is a clone of some layer in \p nodes * o \p node is a clone any child layer of any layer in \p nodes * o \p node is a clone of a clone of a ..., that in the end points * to any layer in \p nodes of their children. */ KRITAIMAGE_EXPORT bool checkIsCloneOf(KisNodeSP node, const KisNodeList &nodes); KRITAIMAGE_EXPORT void forceAllDelayedNodesUpdate(KisNodeSP root); KRITAIMAGE_EXPORT bool hasDelayedNodeWithUpdates(KisNodeSP root); KRITAIMAGE_EXPORT void forceAllHiddenOriginalsUpdate(KisNodeSP root); KRITAIMAGE_EXPORT KisNodeList sortAndFilterMergableInternalNodes(KisNodeList nodes, bool allowMasks = false); KRITAIMAGE_EXPORT void mergeDown(KisImageSP image, KisLayerSP layer, const KisMetaData::MergeStrategy* strategy); KRITAIMAGE_EXPORT QSet fetchLayerFrames(KisNodeSP node); KRITAIMAGE_EXPORT QSet fetchLayerFramesRecursive(KisNodeSP rootNode); KRITAIMAGE_EXPORT void mergeMultipleLayers(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter); KRITAIMAGE_EXPORT void newLayerFromVisible(KisImageSP image, KisNodeSP putAfter); KRITAIMAGE_EXPORT bool tryMergeSelectionMasks(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter); KRITAIMAGE_EXPORT void flattenLayer(KisImageSP image, KisLayerSP layer); KRITAIMAGE_EXPORT void flattenImage(KisImageSP image, KisNodeSP activeNode); KRITAIMAGE_EXPORT void addCopyOfNameTag(KisNodeSP node); KRITAIMAGE_EXPORT KisNodeList findNodesWithProps(KisNodeSP root, const KoProperties &props, bool excludeRoot); KRITAIMAGE_EXPORT void changeImageDefaultProjectionColor(KisImageSP image, const KoColor &color); typedef QMap > FrameJobs; void updateFrameJobs(FrameJobs *jobs, KisNodeSP node); void updateFrameJobsRecursive(FrameJobs *jobs, KisNodeSP rootNode); struct SwitchFrameCommand : public KisCommandUtils::FlipFlopCommand { struct SharedStorage { /** * For some reason the absence of a destructor in the SharedStorage * makes Krita crash on exit. Seems like some compiler weirdness... (DK) */ ~SharedStorage(); int value; }; typedef QSharedPointer SharedStorageSP; public: SwitchFrameCommand(KisImageSP image, int time, bool finalize, SharedStorageSP storage); ~SwitchFrameCommand() override; private: void partA() override; void partB() override; private: KisImageWSP m_image; int m_newTime; SharedStorageSP m_storage; }; /** * A command to keep correct set of selected/active nodes thoroughout * the action. */ class KRITAIMAGE_EXPORT KeepNodesSelectedCommand : public KisCommandUtils::FlipFlopCommand { public: KeepNodesSelectedCommand(const KisNodeList &selectedBefore, const KisNodeList &selectedAfter, KisNodeSP activeBefore, KisNodeSP activeAfter, KisImageSP image, bool finalize, KUndo2Command *parent = 0); void partB() override; private: KisNodeList m_selectedBefore; KisNodeList m_selectedAfter; KisNodeSP m_activeBefore; KisNodeSP m_activeAfter; KisImageWSP m_image; }; struct KRITAIMAGE_EXPORT SelectGlobalSelectionMask : public KUndo2Command { SelectGlobalSelectionMask(KisImageSP image); void redo() override; KisImageSP m_image; }; class KRITAIMAGE_EXPORT RemoveNodeHelper { public: virtual ~RemoveNodeHelper(); protected: virtual void addCommandImpl(KUndo2Command *cmd) = 0; void safeRemoveMultipleNodes(KisNodeList nodes, KisImageSP image); private: bool checkIsSourceForClone(KisNodeSP src, const KisNodeList &nodes); static bool scanForLastLayer(KisImageWSP image, KisNodeList nodesToRemove); }; struct SimpleRemoveLayers : private KisLayerUtils::RemoveNodeHelper, public KisCommandUtils::AggregateCommand { SimpleRemoveLayers(const KisNodeList &nodes, KisImageSP image); void populateChildCommands() override; protected: void addCommandImpl(KUndo2Command *cmd) override; private: KisNodeList m_nodes; KisImageSP m_image; KisNodeList m_selectedNodes; KisNodeSP m_activeNode; }; class KRITAIMAGE_EXPORT KisSimpleUpdateCommand : public KisCommandUtils::FlipFlopCommand { public: KisSimpleUpdateCommand(KisNodeList nodes, bool finalize, KUndo2Command *parent = 0); void partB() override; static void updateNodes(const KisNodeList &nodes); private: KisNodeList m_nodes; }; template bool checkNodesDiffer(KisNodeList nodes, std::function checkerFunc) { bool valueDiffers = false; bool initialized = false; T currentValue = T(); Q_FOREACH (KisNodeSP node, nodes) { if (!initialized) { currentValue = checkerFunc(node); initialized = true; } else if (currentValue != checkerFunc(node)) { valueDiffers = true; break; } } return valueDiffers; } /** * Applies \p func to \p node and all its children recursively */ template void recursiveApplyNodes(NodePointer node, Functor func) { func(node); node = node->firstChild(); while (node) { recursiveApplyNodes(node, func); node = node->nextSibling(); } } /** * Walks through \p node and all its children recursively until * \p func returns true. When \p func returns true, the node is * considered to be found, the search is stopped and the found * node is returned to the caller. */ KisNodeSP KRITAIMAGE_EXPORT recursiveFindNode(KisNodeSP node, std::function func); /** * Recursively searches for a node with specified Uuid */ KisNodeSP KRITAIMAGE_EXPORT findNodeByUuid(KisNodeSP root, const QUuid &uuid); KisImageSP KRITAIMAGE_EXPORT findImageByHierarchy(KisNodeSP node); template T* findNodeByType(KisNodeSP root) { return dynamic_cast(recursiveFindNode(root, [] (KisNodeSP node) { return bool(dynamic_cast(node.data())); }).data()); } + + KisNodeSP KRITAIMAGE_EXPORT findRoot(KisNodeSP node); } #endif /* __KIS_LAYER_UTILS_H */ diff --git a/libs/image/kis_lod_transform.h b/libs/image/kis_lod_transform.h index 8e2aa570ed..c0b33c56c7 100644 --- a/libs/image/kis_lod_transform.h +++ b/libs/image/kis_lod_transform.h @@ -1,193 +1,201 @@ /* * Copyright (c) 2014 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_LOD_TRANSFORM_H #define __KIS_LOD_TRANSFORM_H #include #include #include #include class KRITAIMAGE_EXPORT KisLodTransform { public: KisLodTransform(int levelOfDetail) { qreal scale = lodToScale(levelOfDetail); m_transform = QTransform::fromScale(scale, scale); m_levelOfDetail = levelOfDetail; } template KisLodTransform(PaintDeviceTypeSP device) { int levelOfDetail = device->defaultBounds()->currentLevelOfDetail(); qreal scale = lodToScale(levelOfDetail); m_transform = QTransform::fromScale(scale, scale); m_levelOfDetail = levelOfDetail; } static int scaleToLod(qreal scale, int maxLod) { return qMin(maxLod, qMax(0, qFloor(std::log2(1.0 / scale)))); } static qreal lodToScale(int levelOfDetail) { return levelOfDetail > 0 ? 1.0 / (1 << qMax(0, levelOfDetail)) : 1.0; } static qreal lodToInvScale(int levelOfDetail) { return 1 << qMax(0, levelOfDetail); } template static qreal lodToScale(PaintDeviceTypeSP device) { return lodToScale(device->defaultBounds()->currentLevelOfDetail()); } QRectF map(const QRectF &rc) const { return m_transform.mapRect(rc); } QRect map(const QRect &rc) const { return m_transform.mapRect(rc); } + QRectF mapInverted(const QRectF &rc) const { + return m_transform.inverted().mapRect(rc); + } + + QRect mapInverted(const QRect &rc) const { + return m_transform.inverted().mapRect(rc); + } + KisPaintInformation map(KisPaintInformation pi) const { QPointF pos = pi.pos(); pi.setPos(m_transform.map(pos)); pi.setLevelOfDetail(m_levelOfDetail); return pi; } template T map(const T &object) const { return m_transform.map(object); } static inline QRect alignedRect(const QRect &srcRect, int lod) { qint32 alignment = 1 << lod; qint32 x1, y1, x2, y2; srcRect.getCoords(&x1, &y1, &x2, &y2); alignByPow2Lo(x1, alignment); alignByPow2Lo(y1, alignment); /** * Here is a workaround of Qt's QRect::right()/bottom() * "historical reasons". It should be one pixel smaller * than actual right/bottom position */ alignByPow2ButOneHi(x2, alignment); alignByPow2ButOneHi(y2, alignment); QRect rect; rect.setCoords(x1, y1, x2, y2); return rect; } static inline QRect scaledRect(const QRect &srcRect, int lod) { qint32 x1, y1, x2, y2; srcRect.getCoords(&x1, &y1, &x2, &y2); KIS_ASSERT_RECOVER_NOOP(!(x1 & 1)); KIS_ASSERT_RECOVER_NOOP(!(y1 & 1)); KIS_ASSERT_RECOVER_NOOP(!((x2 + 1) & 1)); KIS_ASSERT_RECOVER_NOOP(!((y2 + 1) & 1)); x1 = divideSafe(x1, lod); y1 = divideSafe(y1, lod); x2 = divideSafe(x2 + 1, lod) - 1; y2 = divideSafe(y2 + 1, lod) - 1; QRect rect; rect.setCoords(x1, y1, x2, y2); return rect; } static QRect upscaledRect(const QRect &srcRect, int lod) { qint32 x1, y1, x2, y2; srcRect.getCoords(&x1, &y1, &x2, &y2); x1 <<= lod; y1 <<= lod; x2 <<= lod; y2 <<= lod; QRect rect; rect.setCoords(x1, y1, x2, y2); return rect; } static inline int coordToLodCoord(int x, int lod) { return divideSafe(x, lod); } private: /** * Aligns @value to the lowest integer not smaller than @value and * that is, increased by one, a divident of alignment */ static inline void alignByPow2ButOneHi(qint32 &value, qint32 alignment) { qint32 mask = alignment - 1; value |= mask; } /** * Aligns @value to the highest integer not exceeding @value and * that is a divident of @alignment */ static inline void alignByPow2Lo(qint32 &value, qint32 alignment) { qint32 mask = alignment - 1; value &= ~mask; } static inline int divideSafe(int x, int lod) { return x > 0 ? x >> lod : -( -x >> lod); } private: QTransform m_transform; int m_levelOfDetail; }; class KisLodTransformScalar { public: KisLodTransformScalar(int lod) { m_scale = KisLodTransform::lodToScale(lod); } template KisLodTransformScalar(PaintDeviceTypeSP device) { m_scale = KisLodTransform::lodToScale(device->defaultBounds()->currentLevelOfDetail()); } qreal scale(qreal value) const { return m_scale * value; } private: qreal m_scale; }; #endif /* __KIS_LOD_TRANSFORM_H */ diff --git a/libs/ui/CMakeLists.txt b/libs/ui/CMakeLists.txt index 01abdd16b3..92a8a5f96a 100644 --- a/libs/ui/CMakeLists.txt +++ b/libs/ui/CMakeLists.txt @@ -1,650 +1,651 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/qtlockedfile ) include_directories(SYSTEM ${EIGEN3_INCLUDE_DIR} ${OCIO_INCLUDE_DIR} ) if (ANDROID) add_definitions(-DQT_OPENGL_ES_3) add_definitions(-DHAS_ONLY_OPENGL_ES) include_directories (${Qt5AndroidExtras_INCLUDE_DIRS}) endif() add_subdirectory( tests ) if (APPLE) find_library(FOUNDATION_LIBRARY Foundation) find_library(APPKIT_LIBRARY AppKit) endif () set(kritaui_LIB_SRCS canvas/kis_canvas_widget_base.cpp canvas/kis_canvas2.cpp canvas/kis_canvas_updates_compressor.cpp canvas/kis_canvas_controller.cpp canvas/kis_display_color_converter.cpp canvas/kis_display_filter.cpp canvas/kis_exposure_gamma_correction_interface.cpp canvas/kis_tool_proxy.cpp canvas/kis_canvas_decoration.cc canvas/kis_coordinates_converter.cpp canvas/kis_grid_manager.cpp canvas/kis_grid_decoration.cpp canvas/kis_grid_config.cpp canvas/kis_prescaled_projection.cpp canvas/kis_qpainter_canvas.cpp canvas/kis_projection_backend.cpp canvas/kis_update_info.cpp canvas/kis_image_patch.cpp canvas/kis_image_pyramid.cpp canvas/kis_infinity_manager.cpp canvas/kis_change_guides_command.cpp canvas/kis_guides_decoration.cpp canvas/kis_guides_manager.cpp canvas/kis_guides_config.cpp canvas/kis_snap_config.cpp canvas/kis_snap_line_strategy.cpp canvas/KisSnapPointStrategy.cpp canvas/KisSnapPixelStrategy.cpp canvas/KisMirrorAxisConfig.cpp dialogs/kis_about_application.cpp dialogs/kis_dlg_adj_layer_props.cc dialogs/kis_dlg_adjustment_layer.cc dialogs/kis_dlg_filter.cpp dialogs/kis_dlg_generator_layer.cpp dialogs/kis_dlg_file_layer.cpp dialogs/kis_dlg_filter.cpp dialogs/kis_dlg_stroke_selection_properties.cpp dialogs/kis_dlg_image_properties.cc dialogs/kis_dlg_layer_properties.cc dialogs/kis_dlg_preferences.cc dialogs/slider_and_spin_box_sync.cpp dialogs/kis_dlg_layer_style.cpp dialogs/kis_dlg_png_import.cpp dialogs/kis_dlg_import_image_sequence.cpp dialogs/kis_delayed_save_dialog.cpp dialogs/KisSessionManagerDialog.cpp dialogs/KisNewWindowLayoutDialog.cpp dialogs/KisDlgChangeCloneSource.cpp dialogs/KisRecoverNamedAutosaveDialog.cpp flake/kis_node_dummies_graph.cpp flake/kis_dummies_facade_base.cpp flake/kis_dummies_facade.cpp flake/kis_node_shapes_graph.cpp flake/kis_node_shape.cpp flake/kis_shape_controller.cpp flake/kis_shape_layer.cc flake/kis_shape_layer_canvas.cpp flake/kis_shape_selection.cpp flake/kis_shape_selection_canvas.cpp flake/kis_shape_selection_model.cpp flake/kis_take_all_shapes_command.cpp brushhud/kis_uniform_paintop_property_widget.cpp brushhud/kis_brush_hud.cpp brushhud/kis_round_hud_button.cpp brushhud/kis_dlg_brush_hud_config.cpp brushhud/kis_brush_hud_properties_list.cpp brushhud/kis_brush_hud_properties_config.cpp kis_aspect_ratio_locker.cpp kis_autogradient.cc kis_bookmarked_configurations_editor.cc kis_bookmarked_configurations_model.cc kis_bookmarked_filter_configurations_model.cc KisPaintopPropertiesBase.cpp kis_canvas_resource_provider.cpp kis_derived_resources.cpp kis_categories_mapper.cpp kis_categorized_list_model.cpp kis_categorized_item_delegate.cpp kis_clipboard.cc kis_config.cc KisOcioConfiguration.cpp kis_control_frame.cpp kis_composite_ops_model.cc kis_paint_ops_model.cpp kis_custom_pattern.cc kis_file_layer.cpp kis_change_file_layer_command.h kis_safe_document_loader.cpp kis_splash_screen.cpp kis_filter_manager.cc kis_filters_model.cc KisImageBarrierLockerWithFeedback.cpp kis_image_manager.cc kis_image_view_converter.cpp kis_import_catcher.cc kis_layer_manager.cc kis_mask_manager.cc kis_mimedata.cpp kis_node_commands_adapter.cpp kis_node_manager.cpp kis_node_juggler_compressed.cpp kis_node_selection_adapter.cpp kis_node_insertion_adapter.cpp KisNodeDisplayModeAdapter.cpp kis_node_model.cpp kis_node_filter_proxy_model.cpp kis_model_index_converter_base.cpp kis_model_index_converter.cpp kis_model_index_converter_show_all.cpp kis_painting_assistant.cc kis_painting_assistants_decoration.cpp KisDecorationsManager.cpp kis_paintop_box.cc kis_paintop_option.cpp kis_paintop_options_model.cpp kis_paintop_settings_widget.cpp kis_popup_palette.cpp kis_png_converter.cpp kis_preference_set_registry.cpp KisResourceServerProvider.cpp KisSelectedShapesProxy.cpp kis_selection_decoration.cc kis_selection_manager.cc KisSelectionActionsAdapter.cpp kis_statusbar.cc kis_zoom_manager.cc kis_favorite_resource_manager.cpp kis_workspace_resource.cpp kis_action.cpp kis_action_manager.cpp KisActionPlugin.cpp kis_canvas_controls_manager.cpp kis_tooltip_manager.cpp kis_multinode_property.cpp kis_stopgradient_editor.cpp KisWelcomePageWidget.cpp KisChangeCloneLayersCommand.cpp kisexiv2/kis_exif_io.cpp kisexiv2/kis_exiv2.cpp kisexiv2/kis_iptc_io.cpp kisexiv2/kis_xmp_io.cpp opengl/kis_opengl.cpp opengl/kis_opengl_canvas2.cpp opengl/kis_opengl_canvas_debugger.cpp opengl/kis_opengl_image_textures.cpp opengl/kis_texture_tile.cpp opengl/kis_opengl_shader_loader.cpp opengl/kis_texture_tile_info_pool.cpp opengl/KisOpenGLUpdateInfoBuilder.cpp opengl/KisOpenGLModeProber.cpp opengl/KisScreenInformationAdapter.cpp kis_fps_decoration.cpp tool/KisToolChangesTracker.cpp tool/KisToolChangesTrackerData.cpp tool/kis_selection_tool_helper.cpp tool/kis_selection_tool_config_widget_helper.cpp tool/kis_rectangle_constraint_widget.cpp tool/kis_shape_tool_helper.cpp tool/kis_tool.cc tool/kis_delegated_tool_policies.cpp tool/kis_tool_freehand.cc tool/kis_speed_smoother.cpp tool/kis_painting_information_builder.cpp tool/kis_stabilized_events_sampler.cpp tool/kis_tool_freehand_helper.cpp tool/kis_tool_multihand_helper.cpp tool/kis_figure_painting_tool_helper.cpp tool/KisAsyncronousStrokeUpdateHelper.cpp tool/kis_tool_paint.cc tool/kis_tool_shape.cc tool/kis_tool_ellipse_base.cpp tool/kis_tool_rectangle_base.cpp tool/kis_tool_polyline_base.cpp tool/kis_tool_utils.cpp tool/kis_resources_snapshot.cpp tool/kis_smoothing_options.cpp tool/KisStabilizerDelayedPaintHelper.cpp tool/KisStrokeSpeedMonitor.cpp tool/strokes/freehand_stroke.cpp tool/strokes/KisStrokeEfficiencyMeasurer.cpp tool/strokes/kis_painter_based_stroke_strategy.cpp tool/strokes/kis_filter_stroke_strategy.cpp tool/strokes/kis_color_picker_stroke_strategy.cpp tool/strokes/KisFreehandStrokeInfo.cpp tool/strokes/KisMaskedFreehandStrokePainter.cpp tool/strokes/KisMaskingBrushRenderer.cpp tool/strokes/KisMaskingBrushCompositeOpFactory.cpp tool/strokes/move_stroke_strategy.cpp + tool/strokes/KisNodeSelectionRecipe.cpp tool/KisSelectionToolFactoryBase.cpp tool/KisToolPaintFactoryBase.cpp widgets/kis_cmb_composite.cc widgets/kis_cmb_contour.cpp widgets/kis_cmb_gradient.cpp widgets/kis_paintop_list_widget.cpp widgets/kis_cmb_idlist.cc widgets/kis_color_space_selector.cc widgets/kis_advanced_color_space_selector.cc widgets/kis_cie_tongue_widget.cpp widgets/kis_tone_curve_widget.cpp widgets/kis_curve_widget.cpp widgets/kis_custom_image_widget.cc widgets/kis_image_from_clipboard_widget.cpp widgets/kis_double_widget.cc widgets/kis_filter_selector_widget.cc widgets/kis_gradient_chooser.cc widgets/kis_iconwidget.cc widgets/kis_mask_widgets.cpp widgets/kis_meta_data_merge_strategy_chooser_widget.cc widgets/kis_multi_bool_filter_widget.cc widgets/kis_multi_double_filter_widget.cc widgets/kis_multi_integer_filter_widget.cc widgets/kis_multipliers_double_slider_spinbox.cpp widgets/kis_paintop_presets_popup.cpp widgets/kis_tool_options_popup.cpp widgets/kis_paintop_presets_chooser_popup.cpp widgets/kis_paintop_presets_save.cpp widgets/kis_paintop_preset_icon_library.cpp widgets/kis_pattern_chooser.cc widgets/kis_preset_chooser.cpp widgets/kis_progress_widget.cpp widgets/kis_selection_options.cc widgets/kis_scratch_pad.cpp widgets/kis_scratch_pad_event_filter.cpp widgets/kis_preset_selector_strip.cpp widgets/KisSelectionPropertySlider.cpp widgets/kis_size_group.cpp widgets/kis_size_group_p.cpp widgets/kis_wdg_generator.cpp widgets/kis_workspace_chooser.cpp widgets/kis_categorized_list_view.cpp widgets/kis_widget_chooser.cpp widgets/kis_tool_button.cpp widgets/kis_floating_message.cpp widgets/kis_lod_availability_widget.cpp widgets/kis_color_label_selector_widget.cpp widgets/kis_color_filter_combo.cpp widgets/kis_elided_label.cpp widgets/kis_stopgradient_slider_widget.cpp widgets/kis_preset_live_preview_view.cpp widgets/KisScreenColorPicker.cpp widgets/KoDualColorButton.cpp widgets/KoStrokeConfigWidget.cpp widgets/KoFillConfigWidget.cpp widgets/KisLayerStyleAngleSelector.cpp widgets/KisMemoryReportButton.cpp widgets/KisDitherWidget.cpp KisPaletteEditor.cpp dialogs/KisDlgPaletteEditor.cpp widgets/KisNewsWidget.cpp widgets/KisGamutMaskToolbar.cpp utils/kis_document_aware_spin_box_unit_manager.cpp utils/KisSpinBoxSplineUnitConverter.cpp utils/KisClipboardUtil.cpp utils/KisDitherUtil.cpp utils/KisFileIconCreator.cpp input/kis_input_manager.cpp input/kis_input_manager_p.cpp input/kis_extended_modifiers_mapper.cpp input/kis_abstract_input_action.cpp input/kis_tool_invocation_action.cpp input/kis_pan_action.cpp input/kis_alternate_invocation_action.cpp input/kis_rotate_canvas_action.cpp input/kis_zoom_action.cpp input/kis_change_frame_action.cpp input/kis_gamma_exposure_action.cpp input/kis_show_palette_action.cpp input/kis_change_primary_setting_action.cpp input/kis_abstract_shortcut.cpp input/kis_native_gesture_shortcut.cpp input/kis_single_action_shortcut.cpp input/kis_stroke_shortcut.cpp input/kis_shortcut_matcher.cpp input/kis_select_layer_action.cpp input/KisQtWidgetsTweaker.cpp input/KisInputActionGroup.cpp input/kis_zoom_and_rotate_action.cpp operations/kis_operation.cpp operations/kis_operation_configuration.cpp operations/kis_operation_registry.cpp operations/kis_operation_ui_factory.cpp operations/kis_operation_ui_widget.cpp operations/kis_filter_selection_operation.cpp actions/kis_selection_action_factories.cpp actions/KisPasteActionFactories.cpp actions/KisTransformToolActivationCommand.cpp input/kis_touch_shortcut.cpp kis_document_undo_store.cpp kis_gui_context_command.cpp kis_gui_context_command_p.cpp input/kis_tablet_debugger.cpp input/kis_input_profile_manager.cpp input/kis_input_profile.cpp input/kis_shortcut_configuration.cpp input/config/kis_input_configuration_page.cpp input/config/kis_edit_profiles_dialog.cpp input/config/kis_input_profile_model.cpp input/config/kis_input_configuration_page_item.cpp input/config/kis_action_shortcuts_model.cpp input/config/kis_input_type_delegate.cpp input/config/kis_input_mode_delegate.cpp input/config/kis_input_button.cpp input/config/kis_input_editor_delegate.cpp input/config/kis_mouse_input_editor.cpp input/config/kis_wheel_input_editor.cpp input/config/kis_key_input_editor.cpp processing/fill_processing_visitor.cpp canvas/kis_mirror_axis.cpp kis_abstract_perspective_grid.cpp KisApplication.cpp KisAutoSaveRecoveryDialog.cpp KisDetailsPane.cpp KisDocument.cpp KisCloneDocumentStroke.cpp kis_node_view_color_scheme.cpp KisImportExportFilter.cpp KisImportExportManager.cpp KisImportExportUtils.cpp kis_async_action_feedback.cpp KisMainWindow.cpp KisOpenPane.cpp KisPart.cpp KisPrintJob.cpp KisTemplate.cpp KisTemplateCreateDia.cpp KisTemplateGroup.cpp KisTemplates.cpp KisTemplatesPane.cpp KisTemplateTree.cpp KisUndoActionsUpdateManager.cpp KisView.cpp KisCanvasWindow.cpp KisImportExportErrorCode.cpp KisImportExportAdditionalChecks.cpp thememanager.cpp kis_mainwindow_observer.cpp KisViewManager.cpp kis_mirror_manager.cpp qtlockedfile/qtlockedfile.cpp qtsingleapplication/qtlocalpeer.cpp qtsingleapplication/qtsingleapplication.cpp KisApplicationArguments.cpp KisNetworkAccessManager.cpp KisRssReader.cpp KisMultiFeedRSSModel.cpp KisRemoteFileFetcher.cpp KisSaveGroupVisitor.cpp KisWindowLayoutResource.cpp KisWindowLayoutManager.cpp KisSessionResource.cpp KisReferenceImagesDecoration.cpp KisReferenceImage.cpp flake/KisReferenceImagesLayer.cpp flake/KisReferenceImagesLayer.h KisMouseClickEater.cpp KisDecorationsWrapperLayer.cpp ) if(WIN32) # Private headers are needed for: # * KisDlgCustomTabletResolution # * KisScreenInformationAdapter include_directories(SYSTEM ${Qt5Gui_PRIVATE_INCLUDE_DIRS}) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} qtlockedfile/qtlockedfile_win.cpp ) if (NOT USE_QT_TABLET_WINDOWS) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} input/wintab/kis_tablet_support_win.cpp input/wintab/kis_screen_size_choice_dialog.cpp input/wintab/kis_tablet_support_win8.cpp ) else() set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} dialogs/KisDlgCustomTabletResolution.cpp ) endif() endif() set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} kis_animation_frame_cache.cpp kis_animation_cache_populator.cpp KisAsyncAnimationRendererBase.cpp KisAsyncAnimationCacheRenderer.cpp KisAsyncAnimationFramesSavingRenderer.cpp dialogs/KisAsyncAnimationRenderDialogBase.cpp dialogs/KisAsyncAnimationCacheRenderDialog.cpp dialogs/KisAsyncAnimationFramesSaveDialog.cpp canvas/kis_animation_player.cpp kis_animation_importer.cpp KisSyncedAudioPlayback.cpp KisFrameDataSerializer.cpp KisFrameCacheStore.cpp KisFrameCacheSwapper.cpp KisAbstractFrameCacheSwapper.cpp KisInMemoryFrameCacheSwapper.cpp input/wintab/drawpile_tablettester/tablettester.cpp input/wintab/drawpile_tablettester/tablettest.cpp ) if (UNIX) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} qtlockedfile/qtlockedfile_unix.cpp ) endif() if (ENABLE_UPDATERS) if (UNIX) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} utils/KisAppimageUpdater.cpp ) endif() set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} utils/KisUpdaterBase.cpp utils/KisManualUpdater.cpp utils/KisUpdaterStatus.cpp ) endif() if(APPLE) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} input/kis_extended_modifiers_mapper_osx.mm osx.mm ) endif() if (ANDROID) set (kritaui_LIB_SRCS ${kritaui_LIB_SRCS} KisAndroidFileManager.cpp) endif() ki18n_wrap_ui(kritaui_LIB_SRCS widgets/KoFillConfigWidget.ui widgets/KoStrokeConfigWidget.ui widgets/KisDitherWidget.ui forms/wdgdlgpngimport.ui forms/wdgfullscreensettings.ui forms/wdgautogradient.ui forms/wdggeneralsettings.ui forms/wdgperformancesettings.ui forms/wdggenerators.ui forms/wdgbookmarkedconfigurationseditor.ui forms/wdgapplyprofile.ui forms/wdgcustompattern.ui forms/wdglayerproperties.ui forms/wdgcolorsettings.ui forms/wdgtabletsettings.ui forms/wdgcolorspaceselector.ui forms/wdgcolorspaceselectoradvanced.ui forms/wdgdisplaysettings.ui forms/kis_previewwidgetbase.ui forms/kis_matrix_widget.ui forms/wdgselectionoptions.ui forms/wdggeometryoptions.ui forms/wdgnewimage.ui forms/wdgimageproperties.ui forms/wdgmaskfromselection.ui forms/wdgmasksource.ui forms/wdgfilterdialog.ui forms/wdgmetadatamergestrategychooser.ui forms/wdgpaintoppresets.ui forms/wdgpaintopsettings.ui forms/wdgdlggeneratorlayer.ui forms/wdgdlgfilelayer.ui forms/wdgfilterselector.ui forms/wdgfilternodecreation.ui forms/wdgmultipliersdoublesliderspinbox.ui forms/wdgnodequerypatheditor.ui forms/wdgpresetselectorstrip.ui forms/wdgsavebrushpreset.ui forms/wdgpreseticonlibrary.ui forms/wdgrectangleconstraints.ui forms/wdgimportimagesequence.ui forms/wdgstrokeselectionproperties.ui forms/KisDetailsPaneBase.ui forms/KisOpenPaneBase.ui forms/wdgstopgradienteditor.ui forms/wdgsessionmanager.ui forms/wdgnewwindowlayout.ui forms/KisWelcomePage.ui forms/WdgDlgPaletteEditor.ui forms/KisNewsPage.ui forms/wdgGamutMaskToolbar.ui forms/wdgchangeclonesource.ui brushhud/kis_dlg_brush_hud_config.ui dialogs/kis_delayed_save_dialog.ui dialogs/KisRecoverNamedAutosaveDialog.ui input/config/kis_input_configuration_page.ui input/config/kis_edit_profiles_dialog.ui input/config/kis_input_configuration_page_item.ui input/config/kis_mouse_input_editor.ui input/config/kis_wheel_input_editor.ui input/config/kis_key_input_editor.ui layerstyles/wdgBevelAndEmboss.ui layerstyles/wdgblendingoptions.ui layerstyles/WdgColorOverlay.ui layerstyles/wdgContour.ui layerstyles/wdgdropshadow.ui layerstyles/WdgGradientOverlay.ui layerstyles/wdgInnerGlow.ui layerstyles/wdglayerstyles.ui layerstyles/WdgPatternOverlay.ui layerstyles/WdgSatin.ui layerstyles/WdgStroke.ui layerstyles/wdgstylesselector.ui layerstyles/wdgTexture.ui layerstyles/wdgKisLayerStyleAngleSelector.ui wdgsplash.ui input/wintab/kis_screen_size_choice_dialog.ui input/wintab/drawpile_tablettester/tablettest.ui ) if(WIN32) if(USE_QT_TABLET_WINDOWS) ki18n_wrap_ui(kritaui_LIB_SRCS dialogs/KisDlgCustomTabletResolution.ui ) else() ki18n_wrap_ui(kritaui_LIB_SRCS input/wintab/kis_screen_size_choice_dialog.ui ) endif() endif() add_library(kritaui SHARED ${kritaui_HEADERS_MOC} ${kritaui_LIB_SRCS} ) generate_export_header(kritaui BASE_NAME kritaui) target_link_libraries(kritaui KF5::CoreAddons KF5::Completion KF5::I18n KF5::ItemViews Qt5::Network kritaversion kritaimpex kritacolor kritaimage kritalibbrush kritawidgets kritawidgetutils kritaresources ${PNG_LIBRARIES} LibExiv2::LibExiv2 ) if (ANDROID) target_link_libraries(kritaui GLESv3) target_link_libraries(kritaui Qt5::Gui) target_link_libraries(kritaui Qt5::AndroidExtras) endif() if (HAVE_QT_MULTIMEDIA) target_link_libraries(kritaui Qt5::Multimedia) endif() if (NOT WIN32 AND NOT APPLE AND NOT ANDROID) target_link_libraries(kritaui ${X11_X11_LIB} ${X11_Xinput_LIB}) endif() if(APPLE) target_link_libraries(kritaui ${FOUNDATION_LIBRARY}) target_link_libraries(kritaui ${APPKIT_LIBRARY}) endif () target_link_libraries(kritaui ${OPENEXR_LIBRARIES}) # Add VSync disable workaround if(NOT WIN32 AND NOT APPLE AND NOT ANDROID) target_link_libraries(kritaui ${CMAKE_DL_LIBS} Qt5::X11Extras) endif() if(X11_FOUND) target_link_libraries(kritaui Qt5::X11Extras ${X11_LIBRARIES}) endif() target_include_directories(kritaui PUBLIC $ $ $ $ $ $ $ ) set_target_properties(kritaui PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} ) install(TARGETS kritaui ${INSTALL_TARGETS_DEFAULT_ARGS}) if (APPLE) install(FILES osx.stylesheet DESTINATION ${DATA_INSTALL_DIR}/krita) endif () if (UNIX AND BUILD_TESTING AND ENABLE_UPDATERS) install(FILES tests/data/AppImageUpdateDummy PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) endif () diff --git a/libs/ui/tool/strokes/KisNodeSelectionRecipe.cpp b/libs/ui/tool/strokes/KisNodeSelectionRecipe.cpp new file mode 100644 index 0000000000..1eb2045e81 --- /dev/null +++ b/libs/ui/tool/strokes/KisNodeSelectionRecipe.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 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 "KisNodeSelectionRecipe.h" + +#include "kis_layer_utils.h" +#include "kis_tool_utils.h" +#include "kis_lod_transform.h" +#include "kis_node.h" + +KisNodeSelectionRecipe::KisNodeSelectionRecipe(KisNodeList _selectedNodes) + : selectedNodes(_selectedNodes), + mode(SelectedLayer) +{ +} + +KisNodeSelectionRecipe::KisNodeSelectionRecipe(KisNodeList _selectedNodes, KisNodeSelectionRecipe::SelectionMode _mode, QPoint _pickPoint) + : selectedNodes(_selectedNodes), + mode(_mode), + pickPoint(_pickPoint) +{ +} + +KisNodeSelectionRecipe::KisNodeSelectionRecipe(const KisNodeSelectionRecipe &rhs, int levelOfDetail) + : KisNodeSelectionRecipe(rhs) +{ + KisLodTransform t(levelOfDetail); + pickPoint = t.map(rhs.pickPoint); +} + +KisNodeList KisNodeSelectionRecipe::selectNodesToProcess() const +{ + if (selectedNodes.isEmpty() || mode == SelectedLayer) { + return selectedNodes; + } + + KisNodeList result; + + const bool wholeGroup = mode == Group; + KisNodeSP node = KisToolUtils::findNode(KisLayerUtils::findRoot(selectedNodes.first()), pickPoint, wholeGroup); + if (node) { + result = {node}; + } + + return result; +} diff --git a/libs/ui/tool/strokes/KisNodeSelectionRecipe.h b/libs/ui/tool/strokes/KisNodeSelectionRecipe.h new file mode 100644 index 0000000000..ee52352bc9 --- /dev/null +++ b/libs/ui/tool/strokes/KisNodeSelectionRecipe.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 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 KISNODESELECTIONRECIPE_H +#define KISNODESELECTIONRECIPE_H + +#include "kis_types.h" +#include "kritaui_export.h" + + +class KRITAUI_EXPORT KisNodeSelectionRecipe +{ +public: + enum SelectionMode { + SelectedLayer, + FirstLayer, + Group + }; + + KisNodeSelectionRecipe(KisNodeList _selectedNodes); + KisNodeSelectionRecipe(KisNodeList _selectedNodes, SelectionMode _mode, QPoint _pickPoint); + KisNodeSelectionRecipe(const KisNodeSelectionRecipe &rhs) = default; + KisNodeSelectionRecipe(const KisNodeSelectionRecipe &rhs, int levelOfDetail); + + KisNodeList selectNodesToProcess() const; + + KisNodeList selectedNodes; + SelectionMode mode; + QPoint pickPoint; +}; + +#endif // KISNODESELECTIONRECIPE_H diff --git a/libs/ui/tool/strokes/move_stroke_strategy.cpp b/libs/ui/tool/strokes/move_stroke_strategy.cpp index 376e88ded8..0f296fe13d 100644 --- a/libs/ui/tool/strokes/move_stroke_strategy.cpp +++ b/libs/ui/tool/strokes/move_stroke_strategy.cpp @@ -1,302 +1,359 @@ /* * Copyright (c) 2011 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 "move_stroke_strategy.h" #include #include "kis_image_interfaces.h" #include "kis_node.h" #include "commands_new/kis_update_command.h" #include "commands_new/kis_node_move_command2.h" #include "kis_layer_utils.h" #include "krita_utils.h" #include "KisRunnableStrokeJobData.h" #include "KisRunnableStrokeJobUtils.h" #include "KisRunnableStrokeJobsInterface.h" -MoveStrokeStrategy::MoveStrokeStrategy(KisNodeList nodes, +MoveStrokeStrategy::MoveStrokeStrategy(KisNodeSelectionRecipe nodeSelection, KisUpdatesFacade *updatesFacade, KisStrokeUndoFacade *undoFacade) : KisStrokeStrategyUndoCommandBased(kundo2_i18n("Move"), false, undoFacade), - m_nodes(), + m_requestedNodeSelection(nodeSelection), m_updatesFacade(updatesFacade), m_updatesEnabled(true) { - m_nodes = KisLayerUtils::sortAndFilterMergableInternalNodes(nodes, true); - - KritaUtils::filterContainer(m_nodes, - [this](KisNodeSP node) { - return - !KisLayerUtils::checkIsCloneOf(node, m_nodes) && - node->isEditable(false); - }); - - Q_FOREACH(KisNodeSP subtree, m_nodes) { - KisLayerUtils::recursiveApplyNodes( - subtree, - [this](KisNodeSP node) { - if (KisLayerUtils::checkIsCloneOf(node, m_nodes) || - !node->isEditable(false)) { - - m_blacklistedNodes.insert(node); - } - }); - } - setSupportsWrapAroundMode(true); enableJob(KisSimpleStrokeStrategy::JOB_INIT, true, KisStrokeJobData::BARRIER); } -MoveStrokeStrategy::MoveStrokeStrategy(const MoveStrokeStrategy &rhs) +MoveStrokeStrategy::MoveStrokeStrategy(KisNodeList nodes, KisUpdatesFacade *updatesFacade, KisStrokeUndoFacade *undoFacade) + : MoveStrokeStrategy(KisNodeSelectionRecipe(nodes), updatesFacade, undoFacade) +{ +} + +MoveStrokeStrategy::MoveStrokeStrategy(const MoveStrokeStrategy &rhs, int lod) : QObject(), KisStrokeStrategyUndoCommandBased(rhs), + m_requestedNodeSelection(rhs.m_requestedNodeSelection, lod), m_nodes(rhs.m_nodes), m_blacklistedNodes(rhs.m_blacklistedNodes), m_updatesFacade(rhs.m_updatesFacade), m_finalOffset(rhs.m_finalOffset), m_dirtyRect(rhs.m_dirtyRect), m_dirtyRects(rhs.m_dirtyRects), m_updatesEnabled(rhs.m_updatesEnabled) { } - void MoveStrokeStrategy::saveInitialNodeOffsets(KisNodeSP node) { if (!m_blacklistedNodes.contains(node)) { m_initialNodeOffsets.insert(node, QPoint(node->x(), node->y())); } KisNodeSP child = node->firstChild(); while(child) { saveInitialNodeOffsets(child); child = child->nextSibling(); } } void MoveStrokeStrategy::initStrokeCallback() { + /** + * Our LodN moght have already prepared the list of nodes for us, + * so we should reuse it to avoid different nodes to be moved in + * LodN and Lod0 modes. + */ + if (m_updatesEnabled) { + m_nodes = m_requestedNodeSelection.selectNodesToProcess(); + + if (!m_nodes.isEmpty()) { + m_nodes = KisLayerUtils::sortAndFilterMergableInternalNodes(m_nodes, true); + } + + KritaUtils::filterContainer(m_nodes, + [this](KisNodeSP node) { + // TODO: check isolation + return + !KisLayerUtils::checkIsCloneOf(node, m_nodes) && + node->isEditable(true); + }); + Q_FOREACH(KisNodeSP subtree, m_nodes) { + KisLayerUtils::recursiveApplyNodes( + subtree, + [this](KisNodeSP node) { + if (KisLayerUtils::checkIsCloneOf(node, m_nodes) || + !node->isEditable(false)) { + + m_blacklistedNodes.insert(node); + } + }); + } + + if (m_sharedNodes) { + *m_sharedNodes = m_nodes; + } + } else { + KIS_SAFE_ASSERT_RECOVER_RETURN(m_sharedNodes); + m_nodes = *m_sharedNodes; + } + + if (m_nodes.isEmpty()) { + emit sigStrokeStartedEmpty(); + return; + } + QVector jobs; KritaUtils::addJobBarrier(jobs, [this]() { Q_FOREACH(KisNodeSP node, m_nodes) { KisLayerUtils::forceAllHiddenOriginalsUpdate(node); } }); KritaUtils::addJobBarrier(jobs, [this]() { Q_FOREACH(KisNodeSP node, m_nodes) { KisLayerUtils::forceAllDelayedNodesUpdate(node); } }); KritaUtils::addJobBarrier(jobs, [this]() { QRect handlesRect; Q_FOREACH(KisNodeSP node, m_nodes) { saveInitialNodeOffsets(node); handlesRect |= KisLayerUtils::recursiveTightNodeVisibleBounds(node); } KisStrokeStrategyUndoCommandBased::initStrokeCallback(); - emit this->sigHandlesRectCalculated(handlesRect); + if (m_updatesEnabled) { + KisLodTransform t(m_nodes.first()->projection()); + handlesRect = t.mapInverted(handlesRect); + + emit this->sigHandlesRectCalculated(handlesRect); + } + m_updateTimer.start(); }); runnableJobsInterface()->addRunnableJobs(jobs); } void MoveStrokeStrategy::finishStrokeCallback() { Q_FOREACH (KisNodeSP node, m_nodes) { KUndo2Command *updateCommand = new KisUpdateCommand(node, m_dirtyRects[node], m_updatesFacade, true); addMoveCommands(node, updateCommand); notifyCommandDone(KUndo2CommandSP(updateCommand), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } if (!m_updatesEnabled) { Q_FOREACH (KisNodeSP node, m_nodes) { m_updatesFacade->refreshGraphAsync(node, m_dirtyRects[node]); } } KisStrokeStrategyUndoCommandBased::finishStrokeCallback(); } void MoveStrokeStrategy::cancelStrokeCallback() { if (!m_nodes.isEmpty()) { m_finalOffset = QPoint(); m_hasPostponedJob = true; tryPostUpdateJob(true); } KisStrokeStrategyUndoCommandBased::cancelStrokeCallback(); } void MoveStrokeStrategy::tryPostUpdateJob(bool forceUpdate) { if (!m_hasPostponedJob) return; if (forceUpdate || (m_updateTimer.elapsed() > m_updateInterval && !m_updatesFacade->hasUpdatesRunning())) { addMutatedJob(new BarrierUpdateData(forceUpdate)); } } void MoveStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { + if (PickLayerData *pickData = dynamic_cast(data)) { + KisNodeSelectionRecipe clone = m_requestedNodeSelection; + clone.pickPoint = pickData->pos; + emit sigLayersPicked(clone.selectNodesToProcess()); + return; + } + Data *d = dynamic_cast(data); if (!m_nodes.isEmpty() && d) { /** * NOTE: we do not care about threading here, because * all our jobs are declared sequential */ m_finalOffset = d->offset; m_hasPostponedJob = true; tryPostUpdateJob(false); } else if (BarrierUpdateData *barrierData = dynamic_cast(data)) { doCanvasUpdate(barrierData->forceUpdate); } else if (KisAsyncronousStrokeUpdateHelper::UpdateData *updateData = dynamic_cast(data)) { tryPostUpdateJob(updateData->forceUpdate); } else { KisStrokeStrategyUndoCommandBased::doStrokeCallback(data); } } #include "kis_selection_mask.h" #include "kis_selection.h" void MoveStrokeStrategy::doCanvasUpdate(bool forceUpdate) { if (!forceUpdate && (m_updateTimer.elapsed() < m_updateInterval || m_updatesFacade->hasUpdatesRunning())) { return; } if (!m_hasPostponedJob) return; Q_FOREACH (KisNodeSP node, m_nodes) { QRect dirtyRect = moveNode(node, m_finalOffset); m_dirtyRects[node] |= dirtyRect; if (m_updatesEnabled) { m_updatesFacade->refreshGraphAsync(node, dirtyRect); } if (KisSelectionMask *mask = dynamic_cast(node.data())) { Q_UNUSED(mask); //mask->selection()->notifySelectionChanged(); } } m_hasPostponedJob = false; m_updateTimer.restart(); } QRect MoveStrokeStrategy::moveNode(KisNodeSP node, QPoint offset) { QRect dirtyRect; if (!m_blacklistedNodes.contains(node)) { dirtyRect = node->extent(); QPoint newOffset = m_initialNodeOffsets[node] + offset; /** * Some layers, e.g. clones need an update to change extent(), so * calculate the dirty rect manually */ QPoint currentOffset(node->x(), node->y()); dirtyRect |= dirtyRect.translated(newOffset - currentOffset); node->setX(newOffset.x()); node->setY(newOffset.y()); KisNodeMoveCommand2::tryNotifySelection(node); } KisNodeSP child = node->firstChild(); while(child) { dirtyRect |= moveNode(child, offset); child = child->nextSibling(); } return dirtyRect; } void MoveStrokeStrategy::addMoveCommands(KisNodeSP node, KUndo2Command *parent) { if (!m_blacklistedNodes.contains(node)) { QPoint nodeOffset(node->x(), node->y()); new KisNodeMoveCommand2(node, nodeOffset - m_finalOffset, nodeOffset, parent); } KisNodeSP child = node->firstChild(); while(child) { addMoveCommands(child, parent); child = child->nextSibling(); } } void MoveStrokeStrategy::setUpdatesEnabled(bool value) { m_updatesEnabled = value; } bool checkSupportsLodMoves(KisNodeSP subtree) { return !KisLayerUtils::recursiveFindNode( subtree, [](KisNodeSP node) -> bool { return !node->supportsLodMoves(); }); } KisStrokeStrategy* MoveStrokeStrategy::createLodClone(int levelOfDetail) { - Q_UNUSED(levelOfDetail); + KisNodeList nodesToCheck; - Q_FOREACH (KisNodeSP node, m_nodes) { + if (m_requestedNodeSelection.mode == KisNodeSelectionRecipe::SelectedLayer) { + nodesToCheck = m_requestedNodeSelection.selectedNodes; + } else if (!m_requestedNodeSelection.selectedNodes.isEmpty()){ + /** + * Since this function is executed in the GUI thread, we cannot properly + * pick the layers. Therefore we should use pessimistic approach and + * check if there are non-lodn-capable nodes in the entire image. + */ + nodesToCheck.append(KisLayerUtils::findRoot(m_requestedNodeSelection.selectedNodes.first())); + } + + Q_FOREACH (KisNodeSP node, nodesToCheck) { if (!checkSupportsLodMoves(node)) return 0; } - MoveStrokeStrategy *clone = new MoveStrokeStrategy(*this); + MoveStrokeStrategy *clone = new MoveStrokeStrategy(*this, levelOfDetail); + connect(clone, SIGNAL(sigHandlesRectCalculated(QRect)), this, SIGNAL(sigHandlesRectCalculated(QRect))); + connect(clone, SIGNAL(sigStrokeStartedEmpty()), this, SIGNAL(sigStrokeStartedEmpty())); + connect(clone, SIGNAL(sigLayersPicked(const KisNodeList&)), this, SIGNAL(sigLayersPicked(const KisNodeList&))); this->setUpdatesEnabled(false); + m_sharedNodes.reset(new KisNodeList()); + clone->m_sharedNodes = m_sharedNodes; return clone; } diff --git a/libs/ui/tool/strokes/move_stroke_strategy.h b/libs/ui/tool/strokes/move_stroke_strategy.h index 8a82aa0432..4d89c30c0c 100644 --- a/libs/ui/tool/strokes/move_stroke_strategy.h +++ b/libs/ui/tool/strokes/move_stroke_strategy.h @@ -1,111 +1,144 @@ /* * Copyright (c) 2011 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 __MOVE_STROKE_STRATEGY_H #define __MOVE_STROKE_STRATEGY_H #include #include #include "kritaui_export.h" #include "kis_stroke_strategy_undo_command_based.h" #include "kis_types.h" #include "kis_lod_transform.h" #include #include "KisAsyncronousStrokeUpdateHelper.h" - +#include "KisNodeSelectionRecipe.h" class KisUpdatesFacade; class KisPostExecutionUndoAdapter; class KRITAUI_EXPORT MoveStrokeStrategy : public QObject, public KisStrokeStrategyUndoCommandBased { Q_OBJECT public: class Data : public KisStrokeJobData { public: Data(QPoint _offset) : KisStrokeJobData(SEQUENTIAL, NORMAL), offset(_offset) { } KisStrokeJobData* createLodClone(int levelOfDetail) override { return new Data(*this, levelOfDetail); } QPoint offset; private: Data(const Data &rhs, int levelOfDetail) : KisStrokeJobData(rhs) { KisLodTransform t(levelOfDetail); offset = t.map(rhs.offset); } }; + class PickLayerData : public KisStrokeJobData { + public: + PickLayerData(QPoint _pos) + : KisStrokeJobData(SEQUENTIAL, NORMAL), + pos(_pos) + { + } + + KisStrokeJobData* createLodClone(int levelOfDetail) override { + return new PickLayerData(*this, levelOfDetail); + } + + QPoint pos; + + private: + PickLayerData(const PickLayerData &rhs, int levelOfDetail) + : KisStrokeJobData(rhs) + { + KisLodTransform t(levelOfDetail); + pos = t.map(rhs.pos); + } + }; + + struct BarrierUpdateData : public KisAsyncronousStrokeUpdateHelper::UpdateData { BarrierUpdateData(bool forceUpdate) : KisAsyncronousStrokeUpdateHelper::UpdateData(forceUpdate, BARRIER, EXCLUSIVE) {} }; public: - MoveStrokeStrategy(KisNodeList nodes, KisUpdatesFacade *updatesFacade, + MoveStrokeStrategy(KisNodeSelectionRecipe nodeSelection, + KisUpdatesFacade *updatesFacade, + KisStrokeUndoFacade *undoFacade); + + MoveStrokeStrategy(KisNodeList nodes, + KisUpdatesFacade *updatesFacade, KisStrokeUndoFacade *undoFacade); void initStrokeCallback() override; void finishStrokeCallback() override; void cancelStrokeCallback() override; void doStrokeCallback(KisStrokeJobData *data) override; KisStrokeStrategy* createLodClone(int levelOfDetail) override; Q_SIGNALS: void sigHandlesRectCalculated(const QRect &handlesRect); + void sigStrokeStartedEmpty(); + void sigLayersPicked(const KisNodeList &nodes); private: - MoveStrokeStrategy(const MoveStrokeStrategy &rhs); + MoveStrokeStrategy(const MoveStrokeStrategy &rhs, int lod); void setUndoEnabled(bool value); void setUpdatesEnabled(bool value); private: QRect moveNode(KisNodeSP node, QPoint offset); void addMoveCommands(KisNodeSP node, KUndo2Command *parent); void saveInitialNodeOffsets(KisNodeSP node); void doCanvasUpdate(bool forceUpdate = false); void tryPostUpdateJob(bool forceUpdate); private: + KisNodeSelectionRecipe m_requestedNodeSelection; KisNodeList m_nodes; + QSharedPointer m_sharedNodes; QSet m_blacklistedNodes; KisUpdatesFacade *m_updatesFacade; QPoint m_finalOffset; QRect m_dirtyRect; QHash m_dirtyRects; bool m_updatesEnabled; QHash m_initialNodeOffsets; QElapsedTimer m_updateTimer; bool m_hasPostponedJob = false; const int m_updateInterval = 30; }; #endif /* __MOVE_STROKE_STRATEGY_H */ diff --git a/plugins/tools/basictools/kis_tool_move.cc b/plugins/tools/basictools/kis_tool_move.cc index 1b1af76d28..6566389483 100644 --- a/plugins/tools/basictools/kis_tool_move.cc +++ b/plugins/tools/basictools/kis_tool_move.cc @@ -1,715 +1,771 @@ /* * Copyright (c) 1999 Matthias Elter * 1999 Michael Koch * 2002 Patrick Julien * 2004 Boudewijn Rempt * 2016 Michael Abrahams * * 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_tool_move.h" #include #include "kis_cursor.h" #include "kis_selection.h" #include "kis_canvas2.h" #include "kis_image.h" #include "kis_tool_utils.h" #include "kis_paint_layer.h" #include "strokes/move_stroke_strategy.h" #include "kis_tool_movetooloptionswidget.h" #include "strokes/move_selection_stroke_strategy.h" #include "kis_resources_snapshot.h" #include "kis_action_registry.h" #include "krita_utils.h" #include #include #include "kis_node_manager.h" #include "kis_selection_manager.h" #include "kis_signals_blocker.h" #include #include "KisMoveBoundsCalculationJob.h" struct KisToolMoveState : KisToolChangesTrackerData, boost::equality_comparable { KisToolMoveState(QPoint _accumulatedOffset) : accumulatedOffset(_accumulatedOffset) {} KisToolChangesTrackerData* clone() const override { return new KisToolMoveState(*this); } bool operator ==(const KisToolMoveState &rhs) { return accumulatedOffset == rhs.accumulatedOffset; } QPoint accumulatedOffset; }; KisToolMove::KisToolMove(KoCanvasBase *canvas) : KisTool(canvas, KisCursor::moveCursor()) , m_updateCursorCompressor(100, KisSignalCompressor::FIRST_ACTIVE) { setObjectName("tool_move"); m_showCoordinatesAction = action("movetool-show-coordinates"); m_showCoordinatesAction = action("movetool-show-coordinates"); connect(&m_updateCursorCompressor, SIGNAL(timeout()), this, SLOT(resetCursorStyle())); m_optionsWidget = new MoveToolOptionsWidget(0, currentImage()->xRes(), toolId()); // See https://bugs.kde.org/show_bug.cgi?id=316896 QWidget *specialSpacer = new QWidget(m_optionsWidget); specialSpacer->setObjectName("SpecialSpacer"); specialSpacer->setFixedSize(0, 0); m_optionsWidget->layout()->addWidget(specialSpacer); m_optionsWidget->setFixedHeight(m_optionsWidget->sizeHint().height()); m_showCoordinatesAction->setChecked(m_optionsWidget->showCoordinates()); m_optionsWidget->slotSetTranslate(m_handlesRect.topLeft() + currentOffset()); connect(m_optionsWidget, SIGNAL(sigSetTranslateX(int)), SLOT(moveBySpinX(int)), Qt::UniqueConnection); connect(m_optionsWidget, SIGNAL(sigSetTranslateY(int)), SLOT(moveBySpinY(int)), Qt::UniqueConnection); connect(m_optionsWidget, SIGNAL(sigRequestCommitOffsetChanges()), this, SLOT(commitChanges()), Qt::UniqueConnection); connect(this, SIGNAL(moveInNewPosition(QPoint)), m_optionsWidget, SLOT(slotSetTranslate(QPoint)), Qt::UniqueConnection); } KisToolMove::~KisToolMove() { endStroke(); } void KisToolMove::resetCursorStyle() { - KisTool::resetCursorStyle(); - if (!isActive()) return; - KisImageSP image = this->image(); - KisResourcesSnapshotSP resources = - new KisResourcesSnapshot(image, currentNode(), canvas()->resourceManager()); - KisSelectionSP selection = resources->activeSelection(); - KisNodeList nodes = fetchSelectedNodes(moveToolMode(), &m_lastCursorPos, selection); - - if (nodes.isEmpty()) { - canvas()->setCursor(Qt::ForbiddenCursor); - } -} - -KisNodeList KisToolMove::fetchSelectedNodes(MoveToolMode mode, const QPoint *pixelPoint, KisSelectionSP selection) -{ - KisNodeList nodes; - KisImageSP image = this->image(); - if (mode != MoveSelectedLayer && pixelPoint) { - const bool wholeGroup = !selection && mode == MoveGroup; - KisNodeSP node = KisToolUtils::findNode(image->root(), *pixelPoint, wholeGroup); - if (node) { - nodes = {node}; + bool canMove = true; + + if (m_strokeId && m_currentlyUsingSelection) { + /// noop; whatever the cursor position, we always show move + /// cursor, because we don't use 'layer under cursor' mode + /// for moving selections + } else if (m_strokeId && !m_currentlyUsingSelection) { + /// we cannot pick layer's pixel data while the stroke is running, + /// because it may run in lodN mode; therefore, we delegate this + /// work to the stroke itself + if (m_currentMode != MoveSelectedLayer && + (m_handlesRect.isEmpty() || + !m_handlesRect.translated(currentOffset()).contains(m_lastCursorPos))) { + + image()->addJob(m_strokeId, new MoveStrokeStrategy::PickLayerData(m_lastCursorPos)); + return; + } + } else { + KisResourcesSnapshotSP resources = + new KisResourcesSnapshot(this->image(), currentNode(), canvas()->resourceManager()); + KisSelectionSP selection = resources->activeSelection(); + + KisPaintLayerSP paintLayer = + dynamic_cast(this->currentNode().data()); + + const bool canUseSelectionMode = + paintLayer && selection && + !selection->selectedRect().isEmpty() && + !selection->selectedExactRect().isEmpty(); + + if (!canUseSelectionMode) { + KisNodeSelectionRecipe nodeSelection = + KisNodeSelectionRecipe( + this->selectedNodes(), + (KisNodeSelectionRecipe::SelectionMode)moveToolMode(), + m_lastCursorPos); + + if (nodeSelection.selectNodesToProcess().isEmpty()) { + canMove = false; + } } } - if (nodes.isEmpty()) { - nodes = this->selectedNodes(); - - KritaUtils::filterContainer(nodes, - [](KisNodeSP node) { - return node->isEditable(); - }); + if (canMove) { + KisTool::resetCursorStyle(); + } else { + useCursor(Qt::ForbiddenCursor); } - - return nodes; } bool KisToolMove::startStrokeImpl(MoveToolMode mode, const QPoint *pos) { KisNodeSP node; KisImageSP image = this->image(); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image, currentNode(), canvas()->resourceManager()); KisSelectionSP selection = resources->activeSelection(); - KisNodeList nodes = fetchSelectedNodes(mode, pos, selection); + KisPaintLayerSP paintLayer = + dynamic_cast(this->currentNode().data()); - if (nodes.size() == 1) { - node = nodes.first(); - } + const bool canUseSelectionMode = + paintLayer && selection && + !selection->selectedRect().isEmpty() && + !selection->selectedExactRect().isEmpty(); - if (nodes.isEmpty()) { - return false; - } + if (pos) { + if (m_strokeId && + (m_currentMode != mode || + m_currentlyUsingSelection != canUseSelectionMode || + (mode != MoveSelectedLayer && + !m_handlesRect.translated(currentOffset()).contains(*pos)))) { - /** - * If the target node has changed, the stroke should be - * restarted. Otherwise just continue processing current node. - */ - if (m_strokeId && !tryEndPreviousStroke(nodes)) { - return true; + selection = 0; + endStroke(); + } } - KisStrokeStrategy *strategy; + if (m_strokeId) return true; - KisPaintLayerSP paintLayer = node ? - dynamic_cast(node.data()) : 0; + KisNodeList nodes; - bool isMoveSelection = false; + KisStrokeStrategy *strategy; - if (paintLayer && selection && - (!selection->selectedRect().isEmpty() && - !selection->selectedExactRect().isEmpty())) { + bool isMoveSelection = false; + if (canUseSelectionMode) { MoveSelectionStrokeStrategy *moveStrategy = new MoveSelectionStrokeStrategy(paintLayer, selection, image.data(), image.data()); connect(moveStrategy, SIGNAL(sigHandlesRectCalculated(const QRect&)), SLOT(slotHandlesRectCalculated(const QRect&))); strategy = moveStrategy; isMoveSelection = true; + nodes = {paintLayer}; } else { + KisNodeSelectionRecipe nodeSelection = + pos ? + KisNodeSelectionRecipe( + this->selectedNodes(), + (KisNodeSelectionRecipe::SelectionMode)mode, + *pos) : + KisNodeSelectionRecipe(this->selectedNodes()); + MoveStrokeStrategy *moveStrategy = - new MoveStrokeStrategy(nodes, image.data(), image.data()); + new MoveStrokeStrategy(nodeSelection, image.data(), image.data()); connect(moveStrategy, SIGNAL(sigHandlesRectCalculated(const QRect&)), SLOT(slotHandlesRectCalculated(const QRect&))); + connect(moveStrategy, + SIGNAL(sigStrokeStartedEmpty()), + SLOT(slotStrokeStartedEmpty())); + connect(moveStrategy, + SIGNAL(sigLayersPicked(const KisNodeList&)), + SLOT(slotStrokePickedLayers(const KisNodeList&))); strategy = moveStrategy; + nodes = nodeSelection.selectedNodes; } // disable outline feedback until the stroke calcualtes // correct bounding rect m_handlesRect = QRect(); m_strokeId = image->startStroke(strategy); m_currentlyProcessingNodes = nodes; + m_currentlyUsingSelection = isMoveSelection; + m_currentMode = mode; m_accumulatedOffset = QPoint(); if (!isMoveSelection) { m_asyncUpdateHelper.startUpdateStream(image.data(), m_strokeId); } KIS_SAFE_ASSERT_RECOVER(m_changesTracker.isEmpty()) { m_changesTracker.reset(); } commitChanges(); return true; } QPoint KisToolMove::currentOffset() const { return m_accumulatedOffset + m_dragPos - m_dragStart; } void KisToolMove::notifyGuiAfterMove(bool showFloatingMessage) { if (!m_optionsWidget) return; if (m_handlesRect.isEmpty()) return; const QPoint currentTopLeft = m_handlesRect.topLeft() + currentOffset(); KisSignalsBlocker b(m_optionsWidget); emit moveInNewPosition(currentTopLeft); // TODO: fetch this info not from options widget, but from config const bool showCoordinates = m_optionsWidget->showCoordinates(); if (showCoordinates && showFloatingMessage) { KisCanvas2 *kisCanvas = static_cast(canvas()); kisCanvas->viewManager()-> showFloatingMessage( i18nc("floating message in move tool", "X: %1 px, Y: %2 px", QLocale().toString(currentTopLeft.x()), QLocale().toString(currentTopLeft.y())), QIcon(), 1000, KisFloatingMessage::High); } } bool KisToolMove::tryEndPreviousStroke(const KisNodeList &nodes) { if (!m_strokeId) return false; bool strokeEnded = false; if (!KritaUtils::compareListsUnordered(nodes, m_currentlyProcessingNodes)) { endStroke(); strokeEnded = true; } return strokeEnded; } void KisToolMove::commitChanges() { KIS_SAFE_ASSERT_RECOVER_RETURN(m_strokeId); QSharedPointer newState(new KisToolMoveState(m_accumulatedOffset)); KisToolMoveState *lastState = dynamic_cast(m_changesTracker.lastState().data()); if (lastState && *lastState == *newState) return; m_changesTracker.commitConfig(newState); } void KisToolMove::slotHandlesRectCalculated(const QRect &handlesRect) { m_handlesRect = handlesRect; notifyGuiAfterMove(false); } +void KisToolMove::slotStrokeStartedEmpty() +{ + /** + * Since the choice of nodes for the operation happens in the + * stroke itself, it may happen that there are no nodes at all. + * In such a case, we should just cancel already started stroke. + */ + cancelStroke(); +} + +void KisToolMove::slotStrokePickedLayers(const KisNodeList &nodes) +{ + if (nodes.isEmpty()) { + useCursor(Qt::ForbiddenCursor); + } else { + KisTool::resetCursorStyle(); + } +} + void KisToolMove::moveDiscrete(MoveDirection direction, bool big) { if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging if (!currentNode()) return; if (!image()) return; if (!currentNode()->isEditable()) return; // Don't move invisible nodes if (startStrokeImpl(MoveSelectedLayer, 0)) { setMode(KisTool::PAINT_MODE); } // Larger movement if "shift" key is pressed. qreal scale = big ? m_optionsWidget->moveScale() : 1.0; qreal moveStep = m_optionsWidget->moveStep() * scale; const QPoint offset = direction == Up ? QPoint( 0, -moveStep) : direction == Down ? QPoint( 0, moveStep) : direction == Left ? QPoint(-moveStep, 0) : QPoint( moveStep, 0) ; m_accumulatedOffset += offset; image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset)); notifyGuiAfterMove(); commitChanges(); setMode(KisTool::HOVER_MODE); } void KisToolMove::activate(ToolActivation toolActivation, const QSet &shapes) { KisTool::activate(toolActivation, shapes); m_actionConnections.addConnection(action("movetool-move-up"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteUp())); m_actionConnections.addConnection(action("movetool-move-down"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteDown())); m_actionConnections.addConnection(action("movetool-move-left"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteLeft())); m_actionConnections.addConnection(action("movetool-move-right"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteRight())); m_actionConnections.addConnection(action("movetool-move-up-more"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteUpMore())); m_actionConnections.addConnection(action("movetool-move-down-more"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteDownMore())); m_actionConnections.addConnection(action("movetool-move-left-more"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteLeftMore())); m_actionConnections.addConnection(action("movetool-move-right-more"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteRightMore())); m_canvasConnections.addUniqueConnection(qobject_cast(canvas())->viewManager()->nodeManager(), SIGNAL(sigUiNeedChangeSelectedNodes(KisNodeList)), this, SLOT(slotNodeChanged(KisNodeList))); m_canvasConnections.addUniqueConnection(qobject_cast(canvas())->viewManager()->selectionManager(), SIGNAL(currentSelectionChanged()), this, SLOT(slotSelectionChanged())); connect(m_showCoordinatesAction, SIGNAL(triggered(bool)), m_optionsWidget, SLOT(setShowCoordinates(bool)), Qt::UniqueConnection); connect(m_optionsWidget, SIGNAL(showCoordinatesChanged(bool)), m_showCoordinatesAction, SLOT(setChecked(bool)), Qt::UniqueConnection); connect(&m_changesTracker, SIGNAL(sigConfigChanged(KisToolChangesTrackerDataSP)), SLOT(slotTrackerChangedConfig(KisToolChangesTrackerDataSP))); slotNodeChanged(this->selectedNodes()); } void KisToolMove::paint(QPainter& gc, const KoViewConverter &converter) { Q_UNUSED(converter); if (m_strokeId && !m_handlesRect.isEmpty()) { QPainterPath handles; handles.addRect(m_handlesRect.translated(currentOffset())); QPainterPath path = pixelToView(handles); paintToolOutline(&gc, path); } } void KisToolMove::deactivate() { m_actionConnections.clear(); m_canvasConnections.clear(); disconnect(m_showCoordinatesAction, 0, this, 0); disconnect(m_optionsWidget, 0, this, 0); endStroke(); KisTool::deactivate(); } void KisToolMove::requestStrokeEnd() { endStroke(); } void KisToolMove::requestStrokeCancellation() { cancelStroke(); } void KisToolMove::requestUndoDuringStroke() { if (!m_strokeId) return; if (m_changesTracker.isEmpty()) { cancelStroke(); } else { m_changesTracker.requestUndo(); } } void KisToolMove::beginPrimaryAction(KoPointerEvent *event) { startAction(event, moveToolMode()); } void KisToolMove::continuePrimaryAction(KoPointerEvent *event) { continueAction(event); } void KisToolMove::endPrimaryAction(KoPointerEvent *event) { endAction(event); } void KisToolMove::beginAlternateAction(KoPointerEvent *event, AlternateAction action) { // Ctrl+Right click toggles between moving current layer and moving layer w/ content if (action == PickFgNode || action == PickBgImage) { MoveToolMode mode = moveToolMode(); if (mode == MoveSelectedLayer) { mode = MoveFirstLayer; } else if (mode == MoveFirstLayer) { mode = MoveSelectedLayer; } startAction(event, mode); } else { startAction(event, MoveGroup); } } void KisToolMove::continueAlternateAction(KoPointerEvent *event, AlternateAction action) { Q_UNUSED(action) continueAction(event); } void KisToolMove::endAlternateAction(KoPointerEvent *event, AlternateAction action) { Q_UNUSED(action) endAction(event); } void KisToolMove::mouseMoveEvent(KoPointerEvent *event) { m_lastCursorPos = convertToPixelCoord(event).toPoint(); KisTool::mouseMoveEvent(event); - if (moveToolMode() == MoveFirstLayer) { + if (moveToolMode() != MoveSelectedLayer || + (m_strokeId && m_currentMode != MoveSelectedLayer)) { + m_updateCursorCompressor.start(); } } void KisToolMove::startAction(KoPointerEvent *event, MoveToolMode mode) { QPoint pos = convertToPixelCoordAndSnap(event).toPoint(); m_dragStart = pos; m_dragPos = pos; if (startStrokeImpl(mode, &pos)) { setMode(KisTool::PAINT_MODE); } else { event->ignore(); m_dragPos = QPoint(); m_dragStart = QPoint(); } qobject_cast(canvas())->updateCanvas(); } void KisToolMove::continueAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); if (!m_strokeId) return; QPoint pos = convertToPixelCoordAndSnap(event).toPoint(); pos = applyModifiers(event->modifiers(), pos); m_dragPos = pos; drag(pos); notifyGuiAfterMove(); qobject_cast(canvas())->updateCanvas(); } void KisToolMove::endAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); setMode(KisTool::HOVER_MODE); if (!m_strokeId) return; QPoint pos = convertToPixelCoordAndSnap(event).toPoint(); pos = applyModifiers(event->modifiers(), pos); drag(pos); m_accumulatedOffset += pos - m_dragStart; m_dragStart = QPoint(); m_dragPos = QPoint(); commitChanges(); notifyGuiAfterMove(); qobject_cast(canvas())->updateCanvas(); } void KisToolMove::drag(const QPoint& newPos) { KisImageWSP image = currentImage(); QPoint offset = m_accumulatedOffset + newPos - m_dragStart; image->addJob(m_strokeId, new MoveStrokeStrategy::Data(offset)); } void KisToolMove::endStroke() { if (!m_strokeId) return; if (m_asyncUpdateHelper.isActive()) { m_asyncUpdateHelper.endUpdateStream(); } KisImageSP image = currentImage(); image->endStroke(m_strokeId); m_strokeId.clear(); m_changesTracker.reset(); m_currentlyProcessingNodes.clear(); + m_currentlyUsingSelection = false; + m_currentMode = MoveSelectedLayer; m_accumulatedOffset = QPoint(); qobject_cast(canvas())->updateCanvas(); } void KisToolMove::slotTrackerChangedConfig(KisToolChangesTrackerDataSP state) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_strokeId); KisToolMoveState *newState = dynamic_cast(state.data()); KIS_SAFE_ASSERT_RECOVER_RETURN(newState); if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging m_accumulatedOffset = newState->accumulatedOffset; image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset)); notifyGuiAfterMove(); } void KisToolMove::slotMoveDiscreteLeft() { moveDiscrete(MoveDirection::Left, false); } void KisToolMove::slotMoveDiscreteRight() { moveDiscrete(MoveDirection::Right, false); } void KisToolMove::slotMoveDiscreteUp() { moveDiscrete(MoveDirection::Up, false); } void KisToolMove::slotMoveDiscreteDown() { moveDiscrete(MoveDirection::Down, false); } void KisToolMove::slotMoveDiscreteLeftMore() { moveDiscrete(MoveDirection::Left, true); } void KisToolMove::slotMoveDiscreteRightMore() { moveDiscrete(MoveDirection::Right, true); } void KisToolMove::slotMoveDiscreteUpMore() { moveDiscrete(MoveDirection::Up, true); } void KisToolMove::slotMoveDiscreteDownMore() { moveDiscrete(MoveDirection::Down, true); } void KisToolMove::cancelStroke() { if (!m_strokeId) return; if (m_asyncUpdateHelper.isActive()) { m_asyncUpdateHelper.cancelUpdateStream(); } KisImageSP image = currentImage(); image->cancelStroke(m_strokeId); m_strokeId.clear(); m_changesTracker.reset(); m_currentlyProcessingNodes.clear(); + m_currentlyUsingSelection = false; + m_currentMode = MoveSelectedLayer; m_accumulatedOffset = QPoint(); notifyGuiAfterMove(); qobject_cast(canvas())->updateCanvas(); } QWidget* KisToolMove::createOptionWidget() { return m_optionsWidget; } KisToolMove::MoveToolMode KisToolMove::moveToolMode() const { if (m_optionsWidget) return m_optionsWidget->mode(); return MoveSelectedLayer; } QPoint KisToolMove::applyModifiers(Qt::KeyboardModifiers modifiers, QPoint pos) { QPoint move = pos - m_dragStart; // Snap to axis if (modifiers & Qt::ShiftModifier) { move = snapToClosestAxis(move); } // "Precision mode" - scale down movement by 1/5 if (modifiers & Qt::AltModifier) { const qreal SCALE_FACTOR = .2; move = SCALE_FACTOR * move; } return m_dragStart + move; } void KisToolMove::moveBySpinX(int newX) { if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging if (!currentNode()->isEditable()) return; // Don't move invisible nodes if (startStrokeImpl(MoveSelectedLayer, 0)) { setMode(KisTool::PAINT_MODE); } m_accumulatedOffset.rx() = newX - m_handlesRect.x(); image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset)); notifyGuiAfterMove(false); setMode(KisTool::HOVER_MODE); } void KisToolMove::moveBySpinY(int newY) { if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging if (!currentNode()->isEditable()) return; // Don't move invisible nodes if (startStrokeImpl(MoveSelectedLayer, 0)) { setMode(KisTool::PAINT_MODE); } m_accumulatedOffset.ry() = newY - m_handlesRect.y(); image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset)); notifyGuiAfterMove(false); setMode(KisTool::HOVER_MODE); } void KisToolMove::requestHandlesRectUpdate() { KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image(), currentNode(), canvas()->resourceManager()); KisSelectionSP selection = resources->activeSelection(); KisMoveBoundsCalculationJob *job = new KisMoveBoundsCalculationJob(this->selectedNodes(), selection, this); connect(job, SIGNAL(sigCalcualtionFinished(const QRect&)), SLOT(slotHandlesRectCalculated(const QRect &))); KisImageSP image = this->image(); image->addSpontaneousJob(job); notifyGuiAfterMove(false); } void KisToolMove::slotNodeChanged(const KisNodeList &nodes) { if (m_strokeId && !tryEndPreviousStroke(nodes)) { return; } requestHandlesRectUpdate(); } void KisToolMove::slotSelectionChanged() { if (m_strokeId) return; requestHandlesRectUpdate(); } QList KisToolMoveFactory::createActionsImpl() { KisActionRegistry *actionRegistry = KisActionRegistry::instance(); QList actions = KisToolPaintFactoryBase::createActionsImpl(); actions << actionRegistry->makeQAction("movetool-move-up"); actions << actionRegistry->makeQAction("movetool-move-down"); actions << actionRegistry->makeQAction("movetool-move-left"); actions << actionRegistry->makeQAction("movetool-move-right"); actions << actionRegistry->makeQAction("movetool-move-up-more"); actions << actionRegistry->makeQAction("movetool-move-down-more"); actions << actionRegistry->makeQAction("movetool-move-left-more"); actions << actionRegistry->makeQAction("movetool-move-right-more"); actions << actionRegistry->makeQAction("movetool-show-coordinates"); return actions; } diff --git a/plugins/tools/basictools/kis_tool_move.h b/plugins/tools/basictools/kis_tool_move.h index f7f07666c8..fe37827255 100644 --- a/plugins/tools/basictools/kis_tool_move.h +++ b/plugins/tools/basictools/kis_tool_move.h @@ -1,209 +1,212 @@ /* * Copyright (c) 1999 Matthias Elter * 1999 Michael Koch * 2003 Patrick Julien * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_TOOL_MOVE_H_ #define KIS_TOOL_MOVE_H_ #include #include #include #include #include #include #include #include #include #include "KisToolChangesTracker.h" #include "kis_signal_compressor.h" #include "kis_signal_auto_connection.h" #include "KisAsyncronousStrokeUpdateHelper.h" #include "kis_canvas2.h" class KoCanvasBase; class MoveToolOptionsWidget; class KisDocument; class KisToolMove : public KisTool { Q_OBJECT Q_ENUMS(MoveToolMode); public: KisToolMove(KoCanvasBase * canvas); ~KisToolMove() override; /** * @brief wantsAutoScroll * reimplemented from KoToolBase * there's an issue where autoscrolling with this tool never makes the * stroke end, so we return false here so that users don't get stuck with * the tool. See bug 362659 * @return false */ bool wantsAutoScroll() const override { return false; } public Q_SLOTS: void activate(ToolActivation toolActivation, const QSet &shapes) override; void deactivate() override; public Q_SLOTS: void requestStrokeEnd() override; void requestStrokeCancellation() override; void requestUndoDuringStroke() override; protected Q_SLOTS: void resetCursorStyle() override; public: enum MoveToolMode { MoveSelectedLayer, MoveFirstLayer, MoveGroup }; enum MoveDirection { Up, Down, Left, Right }; void beginPrimaryAction(KoPointerEvent *event) override; void continuePrimaryAction(KoPointerEvent *event) override; void endPrimaryAction(KoPointerEvent *event) override; void beginAlternateAction(KoPointerEvent *event, AlternateAction action) override; void continueAlternateAction(KoPointerEvent *event, AlternateAction action) override; void endAlternateAction(KoPointerEvent *event, AlternateAction action) override; void mouseMoveEvent(KoPointerEvent *event) override; void startAction(KoPointerEvent *event, MoveToolMode mode); void continueAction(KoPointerEvent *event); void endAction(KoPointerEvent *event); void paint(QPainter& gc, const KoViewConverter &converter) override; QWidget *createOptionWidget() override; void updateUIUnit(int newUnit); MoveToolMode moveToolMode() const; void setShowCoordinates(bool value); public Q_SLOTS: void moveDiscrete(MoveDirection direction, bool big); void moveBySpinX(int newX); void moveBySpinY(int newY); void slotNodeChanged(const KisNodeList &nodes); void slotSelectionChanged(); void commitChanges(); void slotHandlesRectCalculated(const QRect &handlesRect); + void slotStrokeStartedEmpty(); + void slotStrokePickedLayers(const KisNodeList &nodes); Q_SIGNALS: void moveToolModeChanged(); void moveInNewPosition(QPoint); private: void drag(const QPoint& newPos); void cancelStroke(); QPoint applyModifiers(Qt::KeyboardModifiers modifiers, QPoint pos); bool startStrokeImpl(MoveToolMode mode, const QPoint *pos); QPoint currentOffset() const; void notifyGuiAfterMove(bool showFloatingMessage = true); bool tryEndPreviousStroke(const KisNodeList &nodes); - KisNodeList fetchSelectedNodes(MoveToolMode mode, const QPoint *pixelPoint, KisSelectionSP selection); void requestHandlesRectUpdate(); private Q_SLOTS: void endStroke(); void slotTrackerChangedConfig(KisToolChangesTrackerDataSP state); void slotMoveDiscreteLeft(); void slotMoveDiscreteRight(); void slotMoveDiscreteUp(); void slotMoveDiscreteDown(); void slotMoveDiscreteLeftMore(); void slotMoveDiscreteRightMore(); void slotMoveDiscreteUpMore(); void slotMoveDiscreteDownMore(); private: MoveToolOptionsWidget* m_optionsWidget {0}; QPoint m_dragStart; ///< Point where current cursor dragging began QPoint m_accumulatedOffset; ///< Total offset including multiple clicks, up/down/left/right keys, etc. added together KisStrokeId m_strokeId; KisNodeList m_currentlyProcessingNodes; + bool m_currentlyUsingSelection = false; + MoveToolMode m_currentMode = MoveSelectedLayer; int m_resolution; QAction *m_showCoordinatesAction {0}; QPoint m_dragPos; QRect m_handlesRect; KisToolChangesTracker m_changesTracker; QPoint m_lastCursorPos; KisSignalCompressor m_updateCursorCompressor; KisSignalAutoConnectionsStore m_actionConnections; KisSignalAutoConnectionsStore m_canvasConnections; KisAsyncronousStrokeUpdateHelper m_asyncUpdateHelper; }; class KisToolMoveFactory : public KisToolPaintFactoryBase { public: KisToolMoveFactory() : KisToolPaintFactoryBase("KritaTransform/KisToolMove") { setToolTip(i18n("Move Tool")); setSection(TOOL_TYPE_TRANSFORM); setActivationShapeId(KRITA_TOOL_ACTIVATION_ID); setPriority(3); setIconName(koIconNameCStr("krita_tool_move")); setShortcut(QKeySequence(Qt::Key_T)); } ~KisToolMoveFactory() override {} KoToolBase * createTool(KoCanvasBase *canvas) override { return new KisToolMove(canvas); } QList createActionsImpl() override; }; #endif // KIS_TOOL_MOVE_H_ diff --git a/plugins/tools/basictools/strokes/move_selection_stroke_strategy.cpp b/plugins/tools/basictools/strokes/move_selection_stroke_strategy.cpp index a70df44608..a49da8a3da 100644 --- a/plugins/tools/basictools/strokes/move_selection_stroke_strategy.cpp +++ b/plugins/tools/basictools/strokes/move_selection_stroke_strategy.cpp @@ -1,180 +1,191 @@ /* * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "move_selection_stroke_strategy.h" #include #include #include #include "kis_image.h" #include "kis_paint_layer.h" #include "kis_painter.h" #include "kis_transaction.h" #include +#include "kis_lod_transform.h" MoveSelectionStrokeStrategy::MoveSelectionStrokeStrategy(KisPaintLayerSP paintLayer, KisSelectionSP selection, KisUpdatesFacade *updatesFacade, KisStrokeUndoFacade *undoFacade) : KisStrokeStrategyUndoCommandBased(kundo2_i18n("Move Selection"), false, undoFacade), m_paintLayer(paintLayer), m_selection(selection), m_updatesFacade(updatesFacade) { /** * Selection might have some update projection jobs pending, so we should ensure * all of them are completed before we start our stroke. */ enableJob(KisSimpleStrokeStrategy::JOB_INIT, true, KisStrokeJobData::BARRIER); enableJob(KisSimpleStrokeStrategy::JOB_FINISH); enableJob(KisSimpleStrokeStrategy::JOB_CANCEL); } MoveSelectionStrokeStrategy::MoveSelectionStrokeStrategy(const MoveSelectionStrokeStrategy &rhs) : QObject(), KisStrokeStrategyUndoCommandBased(rhs), m_paintLayer(rhs.m_paintLayer), m_selection(rhs.m_selection), m_updatesFacade(rhs.m_updatesFacade) { } void MoveSelectionStrokeStrategy::initStrokeCallback() { KisStrokeStrategyUndoCommandBased::initStrokeCallback(); KisPaintDeviceSP paintDevice = m_paintLayer->paintDevice(); KisPaintDeviceSP movedDevice = new KisPaintDevice(m_paintLayer.data(), paintDevice->colorSpace()); QRect copyRect = m_selection->selectedRect(); KisPainter gc(movedDevice); gc.setSelection(m_selection); gc.bitBlt(copyRect.topLeft(), paintDevice, copyRect); gc.end(); KisTransaction cutTransaction(name(), paintDevice); paintDevice->clearSelection(m_selection); runAndSaveCommand(KUndo2CommandSP(cutTransaction.endAndTake()), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); KisIndirectPaintingSupport *indirect = static_cast(m_paintLayer.data()); indirect->setTemporaryTarget(movedDevice); indirect->setTemporaryCompositeOp(COMPOSITE_OVER); indirect->setTemporaryOpacity(OPACITY_OPAQUE_U8); m_initialDeviceOffset = QPoint(movedDevice->x(), movedDevice->y()); m_selection->setVisible(false); - emit sigHandlesRectCalculated(movedDevice->exactBounds()); + { + QRect handlesRect = movedDevice->exactBounds(); + KisLodTransform t(paintDevice); + handlesRect = t.mapInverted(handlesRect); + + emit this->sigHandlesRectCalculated(handlesRect); + } } void MoveSelectionStrokeStrategy::finishStrokeCallback() { KisIndirectPaintingSupport *indirect = static_cast(m_paintLayer.data()); KisTransaction transaction(name(), m_paintLayer->paintDevice()); indirect->mergeToLayer(m_paintLayer, (KisPostExecutionUndoAdapter*)0, KUndo2MagicString()); runAndSaveCommand(KUndo2CommandSP(transaction.endAndTake()), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); indirect->setTemporaryTarget(0); QPoint selectionOffset(m_selection->x(), m_selection->y()); m_updatesFacade->blockUpdates(); KUndo2CommandSP moveSelectionCommand( new KisSelectionMoveCommand2(m_selection, selectionOffset, selectionOffset + m_finalOffset)); runAndSaveCommand( moveSelectionCommand, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); m_updatesFacade->unblockUpdates(); m_selection->setVisible(true); KisStrokeStrategyUndoCommandBased::finishStrokeCallback(); } void MoveSelectionStrokeStrategy::cancelStrokeCallback() { KisIndirectPaintingSupport *indirect = static_cast(m_paintLayer.data()); if (indirect) { KisPaintDeviceSP t = indirect->temporaryTarget(); if (t) { KisRegion dirtyRegion = t->region(); indirect->setTemporaryTarget(0); m_selection->setVisible(true); m_paintLayer->setDirty(dirtyRegion); } } KisStrokeStrategyUndoCommandBased::cancelStrokeCallback(); } #include "tool/strokes/move_stroke_strategy.h" void MoveSelectionStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { MoveStrokeStrategy::Data *d = dynamic_cast(data); if (d) { KisIndirectPaintingSupport *indirect = static_cast(m_paintLayer.data()); KisPaintDeviceSP movedDevice = indirect->temporaryTarget(); QRegion dirtyRegion = movedDevice->region().toQRegion(); QPoint currentDeviceOffset(movedDevice->x(), movedDevice->y()); QPoint newDeviceOffset(m_initialDeviceOffset + d->offset); dirtyRegion |= dirtyRegion.translated(newDeviceOffset - currentDeviceOffset); movedDevice->setX(newDeviceOffset.x()); movedDevice->setY(newDeviceOffset.y()); m_finalOffset = d->offset; m_paintLayer->setDirty(KisRegion::fromQRegion(dirtyRegion)); } else { KisStrokeStrategyUndoCommandBased::doStrokeCallback(data); } } KisStrokeStrategy* MoveSelectionStrokeStrategy::createLodClone(int levelOfDetail) { Q_UNUSED(levelOfDetail); + // Vector selections don't support lod-moves + if (m_selection->hasShapeSelection()) return 0; + MoveSelectionStrokeStrategy *clone = new MoveSelectionStrokeStrategy(*this); + connect(clone, SIGNAL(sigHandlesRectCalculated(QRect)), this, SIGNAL(sigHandlesRectCalculated(QRect))); return clone; }