From b321a75b5b5cd05d8a4283b67dc074d12e050863 Mon Sep 17 00:00:00 2001 From: David Capello Date: Tue, 12 Jun 2018 17:11:10 -0300 Subject: [PATCH] New Timeline UI to move frames inside or outside tags (fix #1656) --- src/app/document_api.cpp | 107 +++++++++++--- src/app/document_api.h | 22 ++- src/app/document_range_ops.cpp | 87 ++++++++--- src/app/document_range_ops.h | 16 ++- src/app/document_range_tests.cpp | 239 +++++++++++++++++++++++++++++++ src/app/drop_frame_place.h | 20 +++ src/app/tags_handling.h | 28 ++++ src/app/ui/timeline/timeline.cpp | 72 ++++++++-- src/app/ui/timeline/timeline.h | 13 +- 9 files changed, 540 insertions(+), 64 deletions(-) create mode 100644 src/app/drop_frame_place.h create mode 100644 src/app/tags_handling.h diff --git a/src/app/document_api.cpp b/src/app/document_api.cpp index 7f323424c..93448b4fd 100644 --- a/src/app/document_api.cpp +++ b/src/app/document_api.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2001-2017 David Capello +// Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -59,6 +59,8 @@ #include +#define TRACE_DOCAPI(...) + namespace app { DocumentApi::DocumentApi(Document* document, Transaction& transaction) @@ -181,13 +183,17 @@ void DocumentApi::trimSprite(Sprite* sprite) void DocumentApi::addFrame(Sprite* sprite, frame_t newFrame) { - copyFrame(sprite, newFrame-1, newFrame); + copyFrame(sprite, newFrame-1, newFrame, + kDropBeforeFrame, + kDefaultTagsAdjustment); } void DocumentApi::addEmptyFrame(Sprite* sprite, frame_t newFrame) { m_transaction.execute(new cmd::AddFrame(sprite, newFrame)); - adjustFrameTags(sprite, newFrame, +1, false); + adjustFrameTags(sprite, newFrame, +1, + kDropBeforeFrame, + kDefaultTagsAdjustment); } void DocumentApi::addEmptyFramesTo(Sprite* sprite, frame_t newFrame) @@ -196,18 +202,31 @@ void DocumentApi::addEmptyFramesTo(Sprite* sprite, frame_t newFrame) addEmptyFrame(sprite, sprite->totalFrames()); } -void DocumentApi::copyFrame(Sprite* sprite, frame_t fromFrame, frame_t newFrame) +void DocumentApi::copyFrame(Sprite* sprite, + const frame_t fromFrame, + const frame_t newFrame, + const DropFramePlace dropFramePlace, + const TagsHandling tagsHandling) { ASSERT(sprite); - m_transaction.execute(new cmd::CopyFrame(sprite, fromFrame, newFrame)); - adjustFrameTags(sprite, newFrame, +1, false); + m_transaction.execute( + new cmd::CopyFrame( + sprite, fromFrame, + (dropFramePlace == kDropBeforeFrame ? newFrame: + newFrame+1))); + + adjustFrameTags(sprite, newFrame, +1, + dropFramePlace, + tagsHandling); } void DocumentApi::removeFrame(Sprite* sprite, frame_t frame) { ASSERT(frame >= 0); m_transaction.execute(new cmd::RemoveFrame(sprite, frame)); - adjustFrameTags(sprite, frame, -1, false); + adjustFrameTags(sprite, frame, -1, + kDropBeforeFrame, + kDefaultTagsAdjustment); } void DocumentApi::setTotalFrames(Sprite* sprite, frame_t frames) @@ -231,13 +250,20 @@ void DocumentApi::setFrameRangeDuration(Sprite* sprite, frame_t from, frame_t to m_transaction.execute(new cmd::SetFrameDuration(sprite, fr, msecs)); } -void DocumentApi::moveFrame(Sprite* sprite, frame_t frame, frame_t beforeFrame) +void DocumentApi::moveFrame(Sprite* sprite, + const frame_t frame, + frame_t targetFrame, + const DropFramePlace dropFramePlace, + const TagsHandling tagsHandling) { - if (frame != beforeFrame && - frame >= 0 && - frame <= sprite->lastFrame() && - beforeFrame >= 0 && - beforeFrame <= sprite->lastFrame()+1) { + const frame_t beforeFrame = + (dropFramePlace == kDropBeforeFrame ? targetFrame: targetFrame+1); + + if (frame >= 0 && frame <= sprite->lastFrame() && + beforeFrame >= 0 && beforeFrame <= sprite->lastFrame()+1 && + ((frame != beforeFrame) || + (!sprite->frameTags().empty() && + tagsHandling != kDontAdjustTags))) { // Change the frame-lengths. int frlen_aux = sprite->frameDuration(frame); @@ -254,11 +280,16 @@ void DocumentApi::moveFrame(Sprite* sprite, frame_t frame, frame_t beforeFrame) setFrameDuration(sprite, beforeFrame, frlen_aux); } - adjustFrameTags(sprite, frame, -1, true); - adjustFrameTags(sprite, beforeFrame, +1, true); + if (tagsHandling != kDontAdjustTags) { + adjustFrameTags(sprite, frame, -1, dropFramePlace, tagsHandling); + if (targetFrame >= frame) + --targetFrame; + adjustFrameTags(sprite, targetFrame, +1, dropFramePlace, tagsHandling); + } // Change cel positions. - moveFrameLayer(sprite->root(), frame, beforeFrame); + if (frame != beforeFrame) + moveFrameLayer(sprite->root(), frame, beforeFrame); } } @@ -567,8 +598,21 @@ void DocumentApi::setPalette(Sprite* sprite, frame_t frame, const Palette* newPa } } -void DocumentApi::adjustFrameTags(Sprite* sprite, frame_t frame, frame_t delta, bool between) +void DocumentApi::adjustFrameTags(Sprite* sprite, + const frame_t frame, + const frame_t delta, + const DropFramePlace dropFramePlace, + const TagsHandling tagsHandling) { + TRACE_DOCAPI( + "\n adjustFrameTags %s frame %d delta=%d tags=%s:\n", + (dropFramePlace == kDropBeforeFrame ? "before": "after"), + frame, delta, + (tagsHandling == kDefaultTagsAdjustment ? "default": + tagsHandling == kFitInsideTags ? "fit-inside": + "fit-outside")); + ASSERT(tagsHandling != kDontAdjustTags); + // As FrameTag::setFrameRange() changes m_frameTags, we need to use // a copy of this collection std::vector tags(sprite->frameTags().begin(), sprite->frameTags().end()); @@ -577,15 +621,40 @@ void DocumentApi::adjustFrameTags(Sprite* sprite, frame_t frame, frame_t delta, frame_t from = tag->fromFrame(); frame_t to = tag->toFrame(); + TRACE_DOCAPI(" - [from to]=[%d %d] ->", from, to); + + // When delta = +1, frame = beforeFrame if (delta == +1) { - if (frame <= from) { ++from; } - if (frame <= to+1) { ++to; } + switch (tagsHandling) { + case kDefaultTagsAdjustment: + if (frame <= from) { ++from; } + if (frame <= to+1) { ++to; } + break; + case kFitInsideTags: + if (frame < from) { ++from; } + if (frame <= to) { ++to; } + break; + case kFitOutsideTags: + if ((frame < from) || + (frame == from && + dropFramePlace == kDropBeforeFrame)) { + ++from; + } + if ((frame < to) || + (frame == to && + dropFramePlace == kDropBeforeFrame)) { + ++to; + } + break; + } } else if (delta == -1) { if (frame < from) { --from; } if (frame <= to) { --to; } } + TRACE_DOCAPI(" [%d %d]\n", from, to); + if (from != tag->fromFrame() || to != tag->toFrame()) { if (from > to) diff --git a/src/app/document_api.h b/src/app/document_api.h index eea3fb783..5d5e972e1 100644 --- a/src/app/document_api.h +++ b/src/app/document_api.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2001-2017 David Capello +// Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -8,6 +8,8 @@ #define APP_DOCUMENT_API_H_INCLUDED #pragma once +#include "app/drop_frame_place.h" +#include "app/tags_handling.h" #include "doc/algorithm/flip_type.h" #include "doc/color.h" #include "doc/frame.h" @@ -49,12 +51,20 @@ namespace app { void addFrame(Sprite* sprite, frame_t newFrame); void addEmptyFrame(Sprite* sprite, frame_t newFrame); void addEmptyFramesTo(Sprite* sprite, frame_t newFrame); - void copyFrame(Sprite* sprite, frame_t fromFrame, frame_t newFrame); + void copyFrame(Sprite* sprite, + const frame_t fromFrame, + const frame_t newFrame, + const DropFramePlace dropFramePlace, + const TagsHandling tagsHandling); void removeFrame(Sprite* sprite, frame_t frame); void setTotalFrames(Sprite* sprite, frame_t frames); void setFrameDuration(Sprite* sprite, frame_t frame, int msecs); void setFrameRangeDuration(Sprite* sprite, frame_t from, frame_t to, int msecs); - void moveFrame(Sprite* sprite, frame_t frame, frame_t beforeFrame); + void moveFrame(Sprite* sprite, + const frame_t frame, + frame_t targetFrame, + const DropFramePlace dropFramePlace, + const TagsHandling tagsHandling); // Cels API void addCel(LayerImage* layer, Cel* cel); @@ -104,7 +114,11 @@ namespace app { private: void setCelFramePosition(Cel* cel, frame_t frame); void moveFrameLayer(Layer* layer, frame_t frame, frame_t beforeFrame); - void adjustFrameTags(Sprite* sprite, frame_t frame, frame_t delta, bool between); + void adjustFrameTags(Sprite* sprite, + const frame_t frame, + const frame_t delta, + const DropFramePlace dropFramePlace, + const TagsHandling tagsHandling); Document* m_document; Transaction& m_transaction; diff --git a/src/app/document_range_ops.cpp b/src/app/document_range_ops.cpp index e12aedead..0796be172 100644 --- a/src/app/document_range_ops.cpp +++ b/src/app/document_range_ops.cpp @@ -79,7 +79,9 @@ static DocumentRange move_or_copy_frames( DocumentApi& api, Op op, Sprite* sprite, const DocumentRange& srcRange, - frame_t dstFrame) + frame_t dstFrame, + const DocumentRangePlace place, + const TagsHandling tagsHandling) { const SelectedFrames& srcFrames = srcRange.selectedFrames(); @@ -88,13 +90,20 @@ static DocumentRange move_or_copy_frames( for (auto srcFrame : srcFrames) { std::clog << srcFrame << ", "; } - std::clog << "] => " << dstFrame << "\n"; + std::clog << "] " + << (place == kDocumentRangeBefore ? "before": + place == kDocumentRangeAfter ? "after": + "as first child") + << " " << dstFrame << "\n"; #endif auto srcFrame = srcFrames.begin(); auto srcFrameEnd = srcFrames.end(); frame_t srcDelta = 0; frame_t firstCopiedBlock = 0; + frame_t dstBeforeFrame = + (place == kDocumentRangeBefore ? dstFrame: + dstFrame+1); for (; srcFrame != srcFrameEnd; ++srcFrame) { frame_t fromFrame = (*srcFrame)+srcDelta; @@ -102,14 +111,14 @@ static DocumentRange move_or_copy_frames( switch (op) { case Move: - if ((*srcFrame) >= dstFrame) { + if ((*srcFrame) >= dstBeforeFrame) { srcDelta = 0; fromFrame = *srcFrame; } break; case Copy: - if (fromFrame >= dstFrame-1 && firstCopiedBlock) { + if (fromFrame >= dstBeforeFrame-1 && firstCopiedBlock) { srcDelta += firstCopiedBlock; fromFrame += firstCopiedBlock; firstCopiedBlock = 0; @@ -124,31 +133,41 @@ static DocumentRange move_or_copy_frames( } std::clog << "] => " << (op == Move ? "Move": "Copy") - << " " << (*srcFrame) << "+" << (srcDelta) << " -> " << dstFrame << " => "; + << " " << (*srcFrame) << "+" << (srcDelta) + << (place == kDocumentRangeBefore ? " before ": " after ") + << dstFrame << " => "; #endif switch (op) { case Move: - api.moveFrame(sprite, fromFrame, dstFrame); + api.moveFrame(sprite, fromFrame, dstFrame, + (place == kDocumentRangeBefore ? kDropBeforeFrame: + kDropAfterFrame), + tagsHandling); - if (fromFrame < dstFrame-1) { + if (fromFrame < dstBeforeFrame-1) { --srcDelta; } - else if (fromFrame > dstFrame-1) { + else if (fromFrame > dstBeforeFrame-1) { + ++dstBeforeFrame; ++dstFrame; } break; case Copy: - api.copyFrame(sprite, fromFrame, dstFrame); + api.copyFrame(sprite, fromFrame, dstFrame, + (place == kDocumentRangeBefore ? kDropBeforeFrame: + kDropAfterFrame), + tagsHandling); - if (fromFrame < dstFrame-1) { + if (fromFrame < dstBeforeFrame-1) { ++firstCopiedBlock; } - else if (fromFrame >= dstFrame-1) { + else if (fromFrame >= dstBeforeFrame-1) { ++srcDelta; } + ++dstBeforeFrame; ++dstFrame; break; } @@ -165,8 +184,8 @@ static DocumentRange move_or_copy_frames( DocumentRange result; if (!srcRange.selectedLayers().empty()) result.selectLayers(srcRange.selectedLayers()); - result.startRange(nullptr, dstFrame-srcFrames.size(), DocumentRange::kFrames); - result.endRange(nullptr, dstFrame-1); + result.startRange(nullptr, dstBeforeFrame-srcFrames.size(), DocumentRange::kFrames); + result.endRange(nullptr, dstBeforeFrame-1); return result; } @@ -183,8 +202,12 @@ static bool has_child(LayerGroup* parent, Layer* child) } static DocumentRange drop_range_op( - Document* doc, Op op, const DocumentRange& from, - DocumentRangePlace place, DocumentRange to) + Document* doc, + const Op op, + const DocumentRange& from, + DocumentRangePlace place, + const TagsHandling tagsHandling, + DocumentRange to) { // Convert "first child" operation into a insert after last child. LayerGroup* parent = nullptr; @@ -237,8 +260,12 @@ static DocumentRange drop_range_op( ((to.firstFrame() >= from.firstFrame() && to.lastFrame() <= from.lastFrame()) || (place == kDocumentRangeBefore && to.firstFrame() == from.lastFrame()+1) || - (place == kDocumentRangeAfter && to.lastFrame() == from.firstFrame()-1))) + (place == kDocumentRangeAfter && to.lastFrame() == from.firstFrame()-1)) && + // If there are tags, this might not be a no-op + (sprite->frameTags().empty() || + tagsHandling == kDontAdjustTags)) { return from; + } } break; @@ -352,9 +379,11 @@ static DocumentRange drop_range_op( if (place == kDocumentRangeBefore) dstFrame = to.firstFrame(); else - dstFrame = to.lastFrame()+1; + dstFrame = to.lastFrame(); - resultRange = move_or_copy_frames(api, op, sprite, from, dstFrame); + resultRange = move_or_copy_frames(api, op, sprite, + from, dstFrame, + place, tagsHandling); break; } @@ -434,14 +463,24 @@ static DocumentRange drop_range_op( return resultRange; } -DocumentRange move_range(Document* doc, const DocumentRange& from, const DocumentRange& to, DocumentRangePlace place) +DocumentRange move_range(Document* doc, + const DocumentRange& from, + const DocumentRange& to, + const DocumentRangePlace place, + const TagsHandling tagsHandling) { - return drop_range_op(doc, Move, from, place, DocumentRange(to)); + return drop_range_op(doc, Move, from, place, + tagsHandling, DocumentRange(to)); } -DocumentRange copy_range(Document* doc, const DocumentRange& from, const DocumentRange& to, DocumentRangePlace place) +DocumentRange copy_range(Document* doc, + const DocumentRange& from, + const DocumentRange& to, + const DocumentRangePlace place, + const TagsHandling tagsHandling) { - return drop_range_op(doc, Copy, from, place, DocumentRange(to)); + return drop_range_op(doc, Copy, from, place, + tagsHandling, DocumentRange(to)); } void reverse_frames(Document* doc, const DocumentRange& range) @@ -482,7 +521,9 @@ void reverse_frames(Document* doc, const DocumentRange& range) for (frame_t frameRev = frameEnd+1; frameRev > frameBegin; --frameRev) { - api.moveFrame(sprite, frameBegin, frameRev); + api.moveFrame(sprite, frameBegin, frameRev, + kDropBeforeFrame, + kDontAdjustTags); } } else if (swapCels) { diff --git a/src/app/document_range_ops.h b/src/app/document_range_ops.h index f16ee144d..8d035aa3e 100644 --- a/src/app/document_range_ops.h +++ b/src/app/document_range_ops.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2001-2016 David Capello +// Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -8,6 +8,8 @@ #define APP_DOCUMENT_RANGE_OPS_H_INCLUDED #pragma once +#include "app/tags_handling.h" + #include namespace app { @@ -23,8 +25,16 @@ namespace app { // These functions returns the new location of the "from" range or // throws an std::runtime_error() in case that the operation cannot // be done. (E.g. the background layer cannot be moved.) - DocumentRange move_range(Document* doc, const DocumentRange& from, const DocumentRange& to, DocumentRangePlace place); - DocumentRange copy_range(Document* doc, const DocumentRange& from, const DocumentRange& to, DocumentRangePlace place); + DocumentRange move_range(Document* doc, + const DocumentRange& from, + const DocumentRange& to, + const DocumentRangePlace place, + const TagsHandling tagsHandling = kDefaultTagsAdjustment); + DocumentRange copy_range(Document* doc, + const DocumentRange& from, + const DocumentRange& to, + const DocumentRangePlace place, + const TagsHandling tagsHandling = kDefaultTagsAdjustment); void reverse_frames(Document* doc, const DocumentRange& range); diff --git a/src/app/document_range_tests.cpp b/src/app/document_range_tests.cpp index dcb58ab15..08834be23 100644 --- a/src/app/document_range_tests.cpp +++ b/src/app/document_range_tests.cpp @@ -1284,3 +1284,242 @@ TEST(DocRangeOps2, DropInsideBugs) { doc->close(); } + +TEST_F(DocRangeOps, MoveRangeWithTags) { + FrameTag* a = new FrameTag(0, 2); + FrameTag* b = new FrameTag(3, 5); + sprite->frameTags().add(a); + sprite->frameTags().add(b); + + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(2, a->toFrame()); + EXPECT_EQ(3, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + + // Move before tag "a" (outside the tag) + EXPECT_EQ(frames_range(0, 1), + move_range(doc, + frames_range(1, 2), + frames_range(0), + kDocumentRangeBefore, + kFitOutsideTags)); + EXPECT_FRAME_ORDER6(1, 2, 0, 3, 4, 5); + EXPECT_EQ(2, a->fromFrame()); + EXPECT_EQ(2, a->toFrame()); + EXPECT_EQ(3, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + + // Check that undo for frame tags does work. + doc->undoHistory()->undo(); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(2, a->toFrame()); + EXPECT_EQ(3, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + + // Move before tag "a" (inside the tag) + EXPECT_EQ(frames_range(0, 1), + move_range(doc, + frames_range(1, 2), + frames_range(0), + kDocumentRangeBefore, + kFitInsideTags)); + EXPECT_FRAME_ORDER6(1, 2, 0, 3, 4, 5); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(2, a->toFrame()); + EXPECT_EQ(3, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + // Move after (and inside) tag "a" + EXPECT_EQ(frames_range(1, 2), + move_range(doc, + frames_range(0, 1), + frames_range(2), + kDocumentRangeAfter, + kFitInsideTags)); + EXPECT_FRAME_ORDER6(2, 0, 1, 3, 4, 5); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(2, a->toFrame()); + EXPECT_EQ(3, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + // Move between tag "a" and "b" (outside both tags) + EXPECT_EQ(frames_range(1, 2), + move_range(doc, + frames_range(0, 1), + frames_range(2), + kDocumentRangeAfter, + kFitOutsideTags)); + EXPECT_FRAME_ORDER6(2, 0, 1, 3, 4, 5); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(0, a->toFrame()); + EXPECT_EQ(3, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + EXPECT_EQ(frames_range(1, 2), + move_range(doc, + frames_range(0, 1), + frames_range(3), + kDocumentRangeBefore, + kFitOutsideTags)); + EXPECT_FRAME_ORDER6(2, 0, 1, 3, 4, 5); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(0, a->toFrame()); + EXPECT_EQ(3, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + EXPECT_EQ(frames_range(2, 3), + move_range(doc, + frames_range(2, 3), + frames_range(2), + kDocumentRangeAfter, + kFitOutsideTags)); + EXPECT_FRAME_ORDER6(0, 1, 2, 3, 4, 5); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(1, a->toFrame()); + EXPECT_EQ(4, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + EXPECT_EQ(frames_range(2, 3), + move_range(doc, + frames_range(2, 3), + frames_range(3), + kDocumentRangeBefore, + kFitOutsideTags)); + EXPECT_FRAME_ORDER6(0, 1, 2, 3, 4, 5); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(1, a->toFrame()); + EXPECT_EQ(4, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + // Move after tag "b" (inside tag) + EXPECT_EQ(frames_range(4, 5), + move_range(doc, + frames_range(0, 1), + frames_range(5), + kDocumentRangeAfter, + kFitInsideTags)); + EXPECT_FRAME_ORDER6(2, 3, 4, 5, 0, 1); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(0, a->toFrame()); + EXPECT_EQ(1, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + // Move after tag "b" (outside tag) + EXPECT_EQ(frames_range(4, 5), + move_range(doc, + frames_range(0, 1), + frames_range(5), + kDocumentRangeAfter, + kFitOutsideTags)); + EXPECT_FRAME_ORDER6(2, 3, 4, 5, 0, 1); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(0, a->toFrame()); + EXPECT_EQ(1, b->fromFrame()); + EXPECT_EQ(3, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + // Put frame 1 and 4 in the middle of both tags (outside) + DocumentRange from; + from.startRange(nullptr, 1, DocumentRange::kFrames); from.endRange(nullptr, 1); + from.startRange(nullptr, 4, DocumentRange::kFrames); from.endRange(nullptr, 4); + EXPECT_EQ(frames_range(2, 3), + move_range(doc, + from, + frames_range(2), + kDocumentRangeAfter, + kFitOutsideTags)); + EXPECT_FRAME_ORDER6(0, 2, 1, 4, 3, 5); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(1, a->toFrame()); + EXPECT_EQ(4, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + // Put frame 1 and 4 before tag "b" (inside) + EXPECT_EQ(frames_range(2, 3), + move_range(doc, + from, + frames_range(3), + kDocumentRangeBefore, + kFitInsideTags)); + EXPECT_FRAME_ORDER6(0, 2, 1, 4, 3, 5); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(1, a->toFrame()); + EXPECT_EQ(2, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); +} + +TEST_F(DocRangeOps, CopyRangeWithTags) { + FrameTag* a = new FrameTag(0, 2); + FrameTag* b = new FrameTag(3, 5); + sprite->frameTags().add(a); + sprite->frameTags().add(b); + + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(2, a->toFrame()); + EXPECT_EQ(3, b->fromFrame()); + EXPECT_EQ(5, b->toFrame()); + + // Copy before tag "a" (outside the tag) + EXPECT_EQ(frames_range(0), + copy_range(doc, + frames_range(1), + frames_range(0), + kDocumentRangeBefore, + kFitOutsideTags)); + EXPECT_FRAME_COPY3(1, 0, 1, 2, 3, 4, 5); + EXPECT_EQ(1, a->fromFrame()); + EXPECT_EQ(3, a->toFrame()); + EXPECT_EQ(4, b->fromFrame()); + EXPECT_EQ(6, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + // Copy before tag "a" (inside the tag) + EXPECT_EQ(frames_range(0), + copy_range(doc, + frames_range(1), + frames_range(0), + kDocumentRangeBefore, + kFitInsideTags)); + EXPECT_FRAME_COPY3(1, 0, 1, 2, 3, 4, 5); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(3, a->toFrame()); + EXPECT_EQ(4, b->fromFrame()); + EXPECT_EQ(6, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); + + // Copy after tag "a" (outside the tag) + EXPECT_EQ(frames_range(3), + copy_range(doc, + frames_range(1), + frames_range(2), + kDocumentRangeAfter, + kFitOutsideTags)); + EXPECT_FRAME_COPY3(0, 1, 2, 1, 3, 4, 5); + EXPECT_EQ(0, a->fromFrame()); + EXPECT_EQ(2, a->toFrame()); + EXPECT_EQ(4, b->fromFrame()); + EXPECT_EQ(6, b->toFrame()); + EXPECT_TRUE(doc->undoHistory()->canUndo()); + doc->undoHistory()->undo(); +} diff --git a/src/app/drop_frame_place.h b/src/app/drop_frame_place.h new file mode 100644 index 000000000..231a76f89 --- /dev/null +++ b/src/app/drop_frame_place.h @@ -0,0 +1,20 @@ +// Aseprite +// Copyright (C) 2018 David Capello +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_DROP_FRAME_PLACE_H_INCLUDED +#define APP_DROP_FRAME_PLACE_H_INCLUDED +#pragma once + +namespace app { + + enum DropFramePlace { + kDropBeforeFrame, + kDropAfterFrame, + }; + +} // namespace app + +#endif diff --git a/src/app/tags_handling.h b/src/app/tags_handling.h new file mode 100644 index 000000000..0f9c115e5 --- /dev/null +++ b/src/app/tags_handling.h @@ -0,0 +1,28 @@ +// Aseprite +// Copyright (C) 2018 David Capello +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_TAGS_HANDLING_H_INCLUDED +#define APP_TAGS_HANDLING_H_INCLUDED +#pragma once + +namespace app { + + // How to adjust tags when we move a frame in the border of a tag. + enum TagsHandling { + // Do not move tags. + kDontAdjustTags, + // Move tags "in the best possible way" when the user doesn't + // specify what to do about them. + kDefaultTagsAdjustment, + // Put frames inside tags. + kFitInsideTags, + // Put frames outside tags. + kFitOutsideTags, + }; + +} // namespace app + +#endif diff --git a/src/app/ui/timeline/timeline.cpp b/src/app/ui/timeline/timeline.cpp index d65b26e21..f0922cbc6 100644 --- a/src/app/ui/timeline/timeline.cpp +++ b/src/app/ui/timeline/timeline.cpp @@ -191,6 +191,14 @@ Timeline::DropTarget::DropTarget() { hhit = HNone; vhit = VNone; + outside = false; +} + +Timeline::DropTarget::DropTarget(const DropTarget& o) + : hhit(o.hhit) + , vhit(o.vhit) + , outside(o.outside) +{ } Timeline::Row::Row() @@ -2370,6 +2378,35 @@ void Timeline::drawFrameTags(ui::Graphics* g) bounds.h = bounds.y2() - frameTagBounds.y2(); bounds.y = frameTagBounds.y2(); + int dx = 0, dw = 0; + if (m_dropTarget.outside && + m_dropTarget.hhit != DropTarget::HNone && + m_dropRange.type() == DocumentRange::kFrames) { + switch (m_dropTarget.hhit) { + case DropTarget::Before: + if (m_dropRange.firstFrame() == frameTag->fromFrame()) { + dx = +frameBoxWidth()/4; + dw = -frameBoxWidth()/4; + } + else if (m_dropRange.firstFrame()-1 == frameTag->toFrame()) { + dw = -frameBoxWidth()/4; + } + break; + case DropTarget::After: + if (m_dropRange.lastFrame() == frameTag->toFrame()) { + dw = -frameBoxWidth()/4; + } + else if (m_dropRange.lastFrame()+1 == frameTag->fromFrame()) { + dx = +frameBoxWidth()/4; + dw = -frameBoxWidth()/4; + } + break; + } + } + bounds.x += dx; + bounds.w += dw; + frameTagBounds.x += dx; + gfx::Color bg = (m_tagFocusBand < 0 || pass == 1) ? frameTag->color(): theme->colors.timelineBandBg(); @@ -3573,6 +3610,7 @@ void Timeline::dropRange(DropOp op) Range newFromRange; DocumentRangePlace place = kDocumentRangeAfter; Range dropRange = m_dropRange; + bool outside = m_dropTarget.outside; switch (m_range.type()) { @@ -3605,10 +3643,16 @@ void Timeline::dropRange(DropOp op) prepareToMoveRange(); try { + TagsHandling tagsHandling = (outside ? kFitOutsideTags: + kFitInsideTags); + + invalidateRange(); if (copy) - newFromRange = copy_range(m_document, m_range, dropRange, place); + newFromRange = copy_range(m_document, m_range, dropRange, + place, tagsHandling); else - newFromRange = move_range(m_document, m_range, dropRange, place); + newFromRange = move_range(m_document, m_range, dropRange, + place, tagsHandling); // If we drop a cel in the same frame (but in another layer), // document views are not updated, so we are forcing the updating of @@ -3616,6 +3660,14 @@ void Timeline::dropRange(DropOp op) m_document->notifyGeneralUpdate(); moveRange(newFromRange); + + invalidateRange(); + + if (m_range.type() == Range::kFrames && + m_sprite && + !m_sprite->frameTags().empty()) { + invalidateRect(getFrameHeadersBounds().offset(origin())); + } } catch (const std::exception& ex) { Console::showException(ex); @@ -3675,10 +3727,10 @@ void Timeline::unlockRange() void Timeline::updateDropRange(const gfx::Point& pt) { - DropTarget::HHit oldHHit = m_dropTarget.hhit; - DropTarget::VHit oldVHit = m_dropTarget.vhit; + const DropTarget oldDropTarget = m_dropTarget; m_dropTarget.hhit = DropTarget::HNone; m_dropTarget.vhit = DropTarget::VNone; + m_dropTarget.outside = false; if (m_state != STATE_MOVING_RANGE) { m_dropRange.clearRange(); @@ -3703,10 +3755,14 @@ void Timeline::updateDropRange(const gfx::Point& pt) gfx::Rect bounds = getRangeBounds(m_dropRange); - if (pt.x < bounds.x + bounds.w/2) + if (pt.x < bounds.x + bounds.w/2) { m_dropTarget.hhit = DropTarget::Before; - else + m_dropTarget.outside = (pt.x < bounds.x+2); + } + else { m_dropTarget.hhit = DropTarget::After; + m_dropTarget.outside = (pt.x >= bounds.x2()-2); + } if (m_hot.veryBottom) m_dropTarget.vhit = DropTarget::VeryBottom; @@ -3724,10 +3780,8 @@ void Timeline::updateDropRange(const gfx::Point& pt) m_dropTarget.vhit = DropTarget::Bottom; } - if (oldHHit != m_dropTarget.hhit || - oldVHit != m_dropTarget.vhit) { + if (oldDropTarget != m_dropTarget) invalidate(); - } } void Timeline::clearClipboardRange() diff --git a/src/app/ui/timeline/timeline.h b/src/app/ui/timeline/timeline.h index 718fd6876..144f86380 100644 --- a/src/app/ui/timeline/timeline.h +++ b/src/app/ui/timeline/timeline.h @@ -206,15 +206,16 @@ namespace app { FirstChild, VeryBottom }; - DropTarget(); - + DropTarget(const DropTarget& o); + bool operator!=(const DropTarget& o) const { + return (hhit != o.hhit || + vhit != o.vhit || + outside != o.outside); + } HHit hhit; VHit vhit; - Layer* layer; - ObjectId layerId; - frame_t frame; - int xpos, ypos; + bool outside; }; struct Row {