diff --git a/libs/image/kis_image_animation_interface.cpp b/libs/image/kis_image_animation_interface.cpp index fa3dfffe67..d2ab2857b2 100644 --- a/libs/image/kis_image_animation_interface.cpp +++ b/libs/image/kis_image_animation_interface.cpp @@ -1,429 +1,431 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_image_animation_interface.h" #include #include "kis_global.h" #include "kis_image.h" #include "kis_regenerate_frame_stroke_strategy.h" #include "kis_switch_time_stroke_strategy.h" #include "kis_keyframe_channel.h" #include "kis_time_range.h" #include "kis_post_execution_undo_adapter.h" #include "commands_new/kis_switch_current_time_command.h" #include "kis_layer_utils.h" struct KisImageAnimationInterface::Private { Private() : image(0), externalFrameActive(false), frameInvalidationBlocked(false), cachedLastFrameValue(-1), audioChannelMuted(false), audioChannelVolume(0.5), m_currentTime(0), m_currentUITime(0) { } Private(const Private &rhs, KisImage *newImage) : image(newImage), externalFrameActive(false), frameInvalidationBlocked(false), fullClipRange(rhs.fullClipRange), playbackRange(rhs.playbackRange), framerate(rhs.framerate), cachedLastFrameValue(-1), audioChannelFileName(rhs.audioChannelFileName), audioChannelMuted(rhs.audioChannelMuted), audioChannelVolume(rhs.audioChannelVolume), m_currentTime(rhs.m_currentTime), m_currentUITime(rhs.m_currentUITime) { } KisImage *image; bool externalFrameActive; bool frameInvalidationBlocked; KisTimeRange fullClipRange; KisTimeRange playbackRange; int framerate; int cachedLastFrameValue; QString audioChannelFileName; bool audioChannelMuted; qreal audioChannelVolume; KisSwitchTimeStrokeStrategy::SharedTokenWSP switchToken; inline int currentTime() const { return m_currentTime; } inline int currentUITime() const { return m_currentUITime; } inline void setCurrentTime(int value) { m_currentTime = value; } inline void setCurrentUITime(int value) { m_currentUITime = value; } private: int m_currentTime; int m_currentUITime; }; KisImageAnimationInterface::KisImageAnimationInterface(KisImage *image) : QObject(image), m_d(new Private) { m_d->image = image; m_d->framerate = 24; m_d->fullClipRange = KisTimeRange::fromTime(0, 100); connect(this, SIGNAL(sigInternalRequestTimeSwitch(int, bool)), SLOT(switchCurrentTimeAsync(int, bool))); } KisImageAnimationInterface::KisImageAnimationInterface(const KisImageAnimationInterface &rhs, KisImage *newImage) : m_d(new Private(*rhs.m_d, newImage)) { connect(this, SIGNAL(sigInternalRequestTimeSwitch(int, bool)), SLOT(switchCurrentTimeAsync(int, bool))); } KisImageAnimationInterface::~KisImageAnimationInterface() { } bool KisImageAnimationInterface::hasAnimation() const { bool hasAnimation = false; KisLayerUtils::recursiveApplyNodes( m_d->image->root(), [&hasAnimation](KisNodeSP node) { hasAnimation |= node->isAnimated(); }); return hasAnimation; } int KisImageAnimationInterface::currentTime() const { return m_d->currentTime(); } int KisImageAnimationInterface::currentUITime() const { return m_d->currentUITime(); } const KisTimeRange& KisImageAnimationInterface::fullClipRange() const { return m_d->fullClipRange; } void KisImageAnimationInterface::setFullClipRange(const KisTimeRange range) { + KIS_SAFE_ASSERT_RECOVER_RETURN(!range.isInfinite()); m_d->fullClipRange = range; emit sigFullClipRangeChanged(); } void KisImageAnimationInterface::setFullClipRangeStartTime(int column) { KisTimeRange newRange(column, m_d->fullClipRange.end(), false); setFullClipRange(newRange); } void KisImageAnimationInterface::setFullClipRangeEndTime(int column) { KisTimeRange newRange(m_d->fullClipRange.start(), column, false); setFullClipRange(newRange); } const KisTimeRange& KisImageAnimationInterface::playbackRange() const { return m_d->playbackRange.isValid() ? m_d->playbackRange : m_d->fullClipRange; } void KisImageAnimationInterface::setPlaybackRange(const KisTimeRange range) { + KIS_SAFE_ASSERT_RECOVER_RETURN(!range.isInfinite()); m_d->playbackRange = range; emit sigPlaybackRangeChanged(); } int KisImageAnimationInterface::framerate() const { return m_d->framerate; } QString KisImageAnimationInterface::audioChannelFileName() const { return m_d->audioChannelFileName; } void KisImageAnimationInterface::setAudioChannelFileName(const QString &fileName) { QFileInfo info(fileName); KIS_SAFE_ASSERT_RECOVER_NOOP(fileName.isEmpty() || info.isAbsolute()); m_d->audioChannelFileName = fileName.isEmpty() ? fileName : info.absoluteFilePath(); emit sigAudioChannelChanged(); } bool KisImageAnimationInterface::isAudioMuted() const { return m_d->audioChannelMuted; } void KisImageAnimationInterface::setAudioMuted(bool value) { m_d->audioChannelMuted = value; emit sigAudioChannelChanged(); } qreal KisImageAnimationInterface::audioVolume() const { return m_d->audioChannelVolume; } void KisImageAnimationInterface::setAudioVolume(qreal value) { m_d->audioChannelVolume = value; emit sigAudioVolumeChanged(); } void KisImageAnimationInterface::setFramerate(int fps) { m_d->framerate = fps; emit sigFramerateChanged(); } KisImageWSP KisImageAnimationInterface::image() const { return m_d->image; } bool KisImageAnimationInterface::externalFrameActive() const { return m_d->externalFrameActive; } void KisImageAnimationInterface::requestTimeSwitchWithUndo(int time) { if (currentUITime() == time) return; requestTimeSwitchNonGUI(time, true); } void KisImageAnimationInterface::setDefaultProjectionColor(const KoColor &color) { int savedTime = 0; saveAndResetCurrentTime(currentTime(), &savedTime); m_d->image->setDefaultProjectionColor(color); restoreCurrentTime(&savedTime); } void KisImageAnimationInterface::requestTimeSwitchNonGUI(int time, bool useUndo) { emit sigInternalRequestTimeSwitch(time, useUndo); } void KisImageAnimationInterface::explicitlySetCurrentTime(int frameId) { m_d->setCurrentTime(frameId); } void KisImageAnimationInterface::switchCurrentTimeAsync(int frameId, bool useUndo) { if (currentUITime() == frameId) return; KisTimeRange range = KisTimeRange::infinite(0); KisTimeRange::calculateTimeRangeRecursive(m_d->image->root(), currentUITime(), range, true); const bool needsRegeneration = !range.contains(frameId); KisSwitchTimeStrokeStrategy::SharedTokenSP token = m_d->switchToken.toStrongRef(); if (!token || !token->tryResetDestinationTime(frameId, needsRegeneration)) { { KisPostExecutionUndoAdapter *undoAdapter = useUndo ? m_d->image->postExecutionUndoAdapter() : 0; KisSwitchTimeStrokeStrategy *strategy = new KisSwitchTimeStrokeStrategy(frameId, needsRegeneration, this, undoAdapter); m_d->switchToken = strategy->token(); KisStrokeId stroke = m_d->image->startStroke(strategy); m_d->image->endStroke(stroke); } if (needsRegeneration) { KisStrokeStrategy *strategy = new KisRegenerateFrameStrokeStrategy(this); KisStrokeId strokeId = m_d->image->startStroke(strategy); m_d->image->endStroke(strokeId); } } m_d->setCurrentUITime(frameId); emit sigUiTimeChanged(frameId); } void KisImageAnimationInterface::requestFrameRegeneration(int frameId, const QRegion &dirtyRegion) { KisStrokeStrategy *strategy = new KisRegenerateFrameStrokeStrategy(frameId, dirtyRegion, this); QList jobs = KisRegenerateFrameStrokeStrategy::createJobsData(m_d->image); KisStrokeId stroke = m_d->image->startStroke(strategy); Q_FOREACH (KisStrokeJobData* job, jobs) { m_d->image->addJob(stroke, job); } m_d->image->endStroke(stroke); } void KisImageAnimationInterface::saveAndResetCurrentTime(int frameId, int *savedValue) { m_d->externalFrameActive = true; *savedValue = m_d->currentTime(); m_d->setCurrentTime(frameId); } void KisImageAnimationInterface::restoreCurrentTime(int *savedValue) { m_d->setCurrentTime(*savedValue); m_d->externalFrameActive = false; } void KisImageAnimationInterface::notifyFrameReady() { emit sigFrameReady(m_d->currentTime()); } void KisImageAnimationInterface::notifyFrameCancelled() { emit sigFrameCancelled(); } KisUpdatesFacade* KisImageAnimationInterface::updatesFacade() const { return m_d->image; } void KisImageAnimationInterface::notifyNodeChanged(const KisNode *node, const QRect &rect, bool recursive) { notifyNodeChanged(node, QVector({rect}), recursive); } void KisImageAnimationInterface::notifyNodeChanged(const KisNode *node, const QVector &rects, bool recursive) { if (externalFrameActive() || m_d->frameInvalidationBlocked) return; if (node->inherits("KisSelectionMask")) return; KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); KisTimeRange invalidateRange; if (recursive) { KisTimeRange::calculateTimeRangeRecursive(node, currentTime(), invalidateRange, false); } else if (channel) { const int currentTime = m_d->currentTime(); invalidateRange = channel->affectedFrames(currentTime); } else { invalidateRange = KisTimeRange::infinite(0); } // we compress the updated rect (atm, no one uses it anyway) QRect unitedRect; Q_FOREACH (const QRect &rc, rects) { unitedRect |= rc; } invalidateFrames(invalidateRange, unitedRect); } void KisImageAnimationInterface::invalidateFrames(const KisTimeRange &range, const QRect &rect) { m_d->cachedLastFrameValue = -1; emit sigFramesChanged(range, rect); } void KisImageAnimationInterface::blockFrameInvalidation(bool value) { m_d->frameInvalidationBlocked = value; } int findLastKeyframeTimeRecursive(KisNodeSP node) { int time = 0; KisKeyframeChannel *channel; Q_FOREACH (channel, node->keyframeChannels()) { KisKeyframeSP keyframe = channel->lastKeyframe(); if (keyframe) { time = std::max(time, keyframe->time()); } } KisNodeSP child = node->firstChild(); while (child) { time = std::max(time, findLastKeyframeTimeRecursive(child)); child = child->nextSibling(); } return time; } int KisImageAnimationInterface::totalLength() { if (m_d->cachedLastFrameValue < 0) { m_d->cachedLastFrameValue = findLastKeyframeTimeRecursive(m_d->image->root()); } int lastKey = m_d->cachedLastFrameValue; lastKey = std::max(lastKey, m_d->fullClipRange.end()); lastKey = std::max(lastKey, m_d->currentUITime()); return lastKey + 1; } diff --git a/libs/image/kis_time_range.h b/libs/image/kis_time_range.h index 438637469b..7801c12880 100644 --- a/libs/image/kis_time_range.h +++ b/libs/image/kis_time_range.h @@ -1,150 +1,151 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_TIME_RANGE_H #define __KIS_TIME_RANGE_H #include "kritaimage_export.h" #include #include #include #include #include "kis_types.h" #include class KRITAIMAGE_EXPORT KisTimeRange : public boost::equality_comparable { public: inline KisTimeRange() : m_start(0), m_end(-1) { } inline KisTimeRange(int start, int duration) : m_start(start), m_end(start + duration - 1) { } inline KisTimeRange(int start, int end, bool) : m_start(start), m_end(end) { } bool operator==(const KisTimeRange &rhs) const { return rhs.m_start == m_start && rhs.m_end == m_end; } KisTimeRange& operator|=(const KisTimeRange &rhs) { if (!isValid()) { m_start = rhs.start(); } else if (rhs.isValid()) { m_start = std::min(m_start, rhs.start()); } if (rhs.isInfinite() || isInfinite()) { m_end = std::numeric_limits::min(); } else if (!isValid()) { m_end = rhs.end(); } else { m_end = std::max(m_end, rhs.end()); } return *this; } KisTimeRange& operator&=(const KisTimeRange &rhs) { if (!isValid()) { return *this; } else if (!rhs.isValid()) { m_start = rhs.start(); m_end = rhs.end(); return *this; } else { m_start = std::max(m_start, rhs.start()); } if (isInfinite()) { m_end = rhs.end(); } else if (!rhs.isInfinite()) { m_end = std::min(m_end, rhs.end()); } return *this; } inline int start() const { return m_start; } inline int end() const { + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!isInfinite(), m_start); return m_end; } inline int duration() const { return m_end >= m_start ? m_end - m_start + 1 : 0; } inline bool isInfinite() const { return m_end == std::numeric_limits::min(); } inline bool isValid() const { return (m_end >= m_start) || (m_end == std::numeric_limits::min() && m_start >= 0); } inline bool contains(int time) const { if (m_end == std::numeric_limits::min()) { return m_start <= time; } return m_start <= time && time <= m_end; } static inline KisTimeRange fromTime(int start, int end) { return KisTimeRange(start, end, true); } static inline KisTimeRange infinite(int start) { return KisTimeRange(start, std::numeric_limits::min(), true); } static void calculateTimeRangeRecursive(const KisNode *node, int time, KisTimeRange &range, bool exclusive); private: int m_start; int m_end; }; namespace KisDomUtils { void KRITAIMAGE_EXPORT saveValue(QDomElement *parent, const QString &tag, const KisTimeRange &range); bool KRITAIMAGE_EXPORT loadValue(const QDomElement &parent, const QString &tag, KisTimeRange *range); } Q_DECLARE_METATYPE(KisTimeRange); KRITAIMAGE_EXPORT QDebug operator<<(QDebug dbg, const KisTimeRange &r); #endif /* __KIS_TIME_RANGE_H */ diff --git a/libs/ui/actions/KisPasteActionFactory.cpp b/libs/ui/actions/KisPasteActionFactory.cpp index 646feb5631..203741f8d7 100644 --- a/libs/ui/actions/KisPasteActionFactory.cpp +++ b/libs/ui/actions/KisPasteActionFactory.cpp @@ -1,242 +1,245 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KisPasteActionFactory.h" #include "kis_image.h" #include "KisViewManager.h" #include "kis_tool_proxy.h" #include "kis_canvas2.h" #include "kis_canvas_controller.h" #include "kis_paint_device.h" #include "kis_paint_layer.h" #include "kis_shape_layer.h" #include "kis_import_catcher.h" #include "kis_clipboard.h" #include "commands/kis_image_layer_add_command.h" #include "kis_processing_applicator.h" #include #include #include #include #include #include "kis_algebra_2d.h" #include #include #include "kis_time_range.h" #include "kis_keyframe_channel.h" #include "kis_raster_keyframe_channel.h" #include "kis_painter.h" namespace { QPointF getFittingOffset(QList shapes, const QPointF &shapesOffset, const QRectF &documentRect, const qreal fitRatio) { QPointF accumulatedFitOffset; Q_FOREACH (KoShape *shape, shapes) { const QRectF bounds = shape->boundingRect(); const QPointF center = bounds.center() + shapesOffset; const qreal wMargin = (0.5 - fitRatio) * bounds.width(); const qreal hMargin = (0.5 - fitRatio) * bounds.height(); const QRectF allowedRect = documentRect.adjusted(-wMargin, -hMargin, wMargin, hMargin); const QPointF fittedCenter = KisAlgebra2D::clampPoint(center, allowedRect); accumulatedFitOffset += fittedCenter - center; } return accumulatedFitOffset; } bool tryPasteShapes(bool pasteAtCursorPosition, KisViewManager *view) { bool result = false; KoSvgPaste paste; if (paste.hasShapes()) { KoCanvasBase *canvas = view->canvasBase(); QSizeF fragmentSize; QList shapes = paste.fetchShapes(canvas->shapeController()->documentRectInPixels(), canvas->shapeController()->pixelsPerInch(), &fragmentSize); if (!shapes.isEmpty()) { KoShapeManager *shapeManager = canvas->shapeManager(); shapeManager->selection()->deselectAll(); // adjust z-index of the shapes so that they would be // pasted on the top of the stack QList topLevelShapes = shapeManager->topLevelShapes(); auto it = std::max_element(topLevelShapes.constBegin(), topLevelShapes.constEnd(), KoShape::compareShapeZIndex); if (it != topLevelShapes.constEnd()) { const int zIndexOffset = (*it)->zIndex(); std::stable_sort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); QList indexedShapes; std::transform(shapes.constBegin(), shapes.constEnd(), std::back_inserter(indexedShapes), [zIndexOffset] (KoShape *shape) { KoShapeReorderCommand::IndexedShape indexedShape(shape); indexedShape.zIndex += zIndexOffset; return indexedShape; }); indexedShapes = KoShapeReorderCommand::homogenizeZIndexesLazy(indexedShapes); KoShapeReorderCommand cmd(indexedShapes); cmd.redo(); } KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18n("Paste shapes")); canvas->shapeController()->addShapesDirect(shapes, 0, parentCommand); QPointF finalShapesOffset; if (pasteAtCursorPosition) { QRectF boundingRect = KoShape::boundingRect(shapes); const QPointF cursorPos = canvas->canvasController()->currentCursorPosition(); finalShapesOffset = cursorPos - boundingRect.center(); } else { bool foundOverlapping = false; QRectF boundingRect = KoShape::boundingRect(shapes); const QPointF offsetStep = 0.1 * QPointF(boundingRect.width(), boundingRect.height()); QPointF offset; Q_FOREACH (KoShape *shape, shapes) { QRectF br1 = shape->boundingRect(); bool hasOverlappingShape = false; do { hasOverlappingShape = false; // we cannot use shapesAt() here, because the groups are not // handled in the shape manager's tree QList conflicts = shapeManager->shapes(); Q_FOREACH (KoShape *intersectedShape, conflicts) { if (intersectedShape == shape) continue; QRectF br2 = intersectedShape->boundingRect(); const qreal tolerance = 2.0; /* pt */ if (KisAlgebra2D::fuzzyCompareRects(br1, br2, tolerance)) { br1.translate(offsetStep.x(), offsetStep.y()); offset += offsetStep; hasOverlappingShape = true; foundOverlapping = true; break; } } } while (hasOverlappingShape); if (foundOverlapping) break; } if (foundOverlapping) { finalShapesOffset = offset; } } const QRectF documentRect = canvas->shapeController()->documentRect(); finalShapesOffset += getFittingOffset(shapes, finalShapesOffset, documentRect, 0.1); if (!finalShapesOffset.isNull()) { new KoShapeMoveCommand(shapes, finalShapesOffset, parentCommand); } canvas->addCommand(parentCommand); Q_FOREACH (KoShape *shape, shapes) { canvas->selectedShapesProxy()->selection()->select(shape); } result = true; } } return result; } } #include "kis_painter.h" void KisPasteActionFactory::run(bool pasteAtCursorPosition, KisViewManager *view) { KisImageSP image = view->image(); if (!image) return; if (tryPasteShapes(pasteAtCursorPosition, view)) { return; } KisTimeRange range; const QRect fittingBounds = pasteAtCursorPosition ? QRect() : image->bounds(); KisPaintDeviceSP clip = KisClipboard::instance()->clip(fittingBounds, true, &range); if (clip) { if (pasteAtCursorPosition) { const QPointF docPos = view->canvasBase()->canvasController()->currentCursorPosition(); const QPointF imagePos = view->canvasBase()->coordinatesConverter()->documentToImage(docPos); const QPointF offset = (imagePos - QRectF(clip->exactBounds()).center()).toPoint(); clip->setX(clip->x() + offset.x()); clip->setY(clip->y() + offset.y()); } KisImportCatcher::adaptClipToImageColorSpace(clip, image); KisPaintLayerSP newLayer = new KisPaintLayer(image.data(), image->nextLayerName() + i18n("(pasted)"), OPACITY_OPAQUE_U8); KisNodeSP aboveNode = view->activeLayer(); KisNodeSP parentNode = aboveNode ? aboveNode->parent() : image->root(); if (range.isValid()) { newLayer->enableAnimation(); KisKeyframeChannel *channel = newLayer->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); KisRasterKeyframeChannel *rasterChannel = dynamic_cast(channel); rasterChannel->importFrame(range.start(), clip, 0); - rasterChannel->addKeyframe(range.end() + 1, 0); + + if (!range.isInfinite()) { + rasterChannel->addKeyframe(range.end() + 1, 0); + } } else { const QRect rc = clip->extent(); KisPainter::copyAreaOptimized(rc.topLeft(), clip, newLayer->paintDevice(), rc); } KUndo2Command *cmd = new KisImageLayerAddCommand(image, newLayer, parentNode, aboveNode); KisProcessingApplicator *ap = beginAction(view, cmd->text()); ap->applyCommand(cmd, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); endAction(ap, KisOperationConfiguration(id()).toXML()); } else { // XXX: "Add saving of XML data for Paste of shapes" view->canvasBase()->toolProxy()->paste(); } }