Allow dropping files into the timeline

This commit is contained in:
Martín Capello 2024-09-10 18:10:34 -03:00
parent f7eed69d7c
commit 722c74189c
4 changed files with 335 additions and 2 deletions

View File

@ -282,6 +282,7 @@ target_sources(app-lib PRIVATE
cmd/copy_region.cpp
cmd/crop_cel.cpp
cmd/deselect_mask.cpp
cmd/drop_on_timeline.cpp
cmd/flatten_layers.cpp
cmd/flip_image.cpp
cmd/flip_mask.cpp

View File

@ -0,0 +1,231 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/cmd/drop_on_timeline.h"
#include "app/cmd/add_layer.h"
#include "app/cmd/set_pixel_format.h"
#include "app/context_flags.h"
#include "app/console.h"
#include "app/doc.h"
#include "app/doc_event.h"
#include "app/file/file.h"
#include "app/ui_context.h"
#include "app/util/open_file_job.h"
#include "app/tx.h"
#include "base/fs.h"
#include "doc/layer_list.h"
#include "render/dithering.h"
namespace app {
namespace cmd {
DropOnTimeline::DropOnTimeline(app::Doc* doc,
doc::frame_t frame,
doc::layer_t layerIndex,
LayerInsertion insert,
const base::paths& paths) : WithDocument(doc)
, m_size(0)
, m_paths(paths)
, m_frame(frame)
, m_layerIndex(layerIndex)
, m_insert(insert)
{
ASSERT(m_layerIndex >= 0);
for(const auto& path : m_paths)
m_size += path.size();
}
void DropOnTimeline::setupInsertionLayers(Layer** before, Layer** after, LayerGroup** group)
{
const LayerList& allLayers = document()->sprite()->allLayers();
*after = (m_insert == LayerInsertion::After ? allLayers[m_layerIndex] : nullptr);
*before = (m_insert == LayerInsertion::Before ? allLayers[m_layerIndex] : nullptr);
if (*before && (*before)->isGroup()) {
*group = static_cast<LayerGroup*>(*before);
// The user is trying to drop layers into an empty group, so there is no after
// nor before layer...
if ((*group)->layersCount() == 0) {
*after = nullptr;
*before = nullptr;
return;
}
*after = static_cast<LayerGroup*>(*before)->lastLayer();
*before = nullptr;
}
*group = (*after ? (*after)->parent() : (*before)->parent());
}
void DropOnTimeline::onExecute()
{
Doc* destDoc = document();
Console console;
Context* context = document()->context();
m_previousTotalFrames = destDoc->sprite()->totalFrames();
// Layers after/before which the dropped layers will be inserted
Layer* afterThis = nullptr;
Layer* beforeThis = nullptr;
// Parent group of the after/before layers.
LayerGroup* group = nullptr;
int flags =
FILE_LOAD_DATA_FILE |
FILE_LOAD_CREATE_PALETTE;
for(const auto& path : m_paths) {
std::unique_ptr<FileOp> fop(
FileOp::createLoadDocumentOperation(context, path, flags));
// Do nothing (the user cancelled or something like that)
if (!fop)
return;
if (fop->hasError()) {
console.printf(fop->error().c_str());
}
else {
OpenFileJob task(fop.get(), true);
task.showProgressWindow();
// Post-load processing, it is called from the GUI because may require user intervention.
fop->postLoad();
// Show any error
if (fop->hasError() && !fop->isStop())
console.printf(fop->error().c_str());
Doc* srcDoc = fop->document();
if (srcDoc) {
// If source document doesn't match the destination document's color
// mode, change it.
if (srcDoc->colorMode() != destDoc->colorMode()) {
Tx tx(srcDoc);
tx(new cmd::SetPixelFormat(
srcDoc->sprite(), destDoc->sprite()->pixelFormat(),
render::Dithering(),
Preferences::instance().quantization.rgbmapAlgorithm(),
nullptr,
nullptr,
FitCriteria::DEFAULT));
tx.commit();
}
// If there is no room for the source frames, add frames to the
// destination sprite.
if (m_frame+srcDoc->sprite()->totalFrames() > destDoc->sprite()->totalFrames()) {
destDoc->sprite()->setTotalFrames(m_frame+srcDoc->sprite()->totalFrames());
}
setupInsertionLayers(&beforeThis, &afterThis, &group);
// Insert layers from the source document.
auto allLayers = srcDoc->sprite()->allLayers();
for (auto it = allLayers.cbegin(); it != allLayers.cend(); ++it) {
auto* layer = *it;
// TODO: If we could "relocate" a layer from the source document to the
// destination document we could avoid making a copy here.
auto* layerCopy = Layer::MakeCopyWithSprite(layer, destDoc->sprite());
destDoc->copyLayerContent(layer, destDoc, layerCopy);
layerCopy->displaceFrames(0, m_frame);
if (afterThis) {
group->insertLayer(layerCopy, afterThis);
afterThis = layerCopy;
}
else if (beforeThis) {
group->insertLayerBefore(layerCopy, beforeThis);
beforeThis = nullptr;
afterThis = layerCopy;
}
else {
group->addLayer(layerCopy);
afterThis = layerCopy;
}
m_droppedLayers.push_back(layerCopy);
m_size += layerCopy->getMemSize();
}
group->incrementVersion();
}
}
}
destDoc->sprite()->incrementVersion();
destDoc->incrementVersion();
notifyDocObservers(afterThis ? afterThis : beforeThis);
}
void DropOnTimeline::onUndo()
{
Doc* doc = document();
frame_t currentTotalFrames = doc->sprite()->totalFrames();
Layer* layerBefore = nullptr;
for (auto* layer : m_droppedLayers) {
layerBefore = layer->getPrevious();
layer->parent()->removeLayer(layer);
}
doc->sprite()->setTotalFrames(m_previousTotalFrames);
m_previousTotalFrames = currentTotalFrames;
if (!layerBefore)
layerBefore = doc->sprite()->firstLayer();
notifyDocObservers(layerBefore);
}
void DropOnTimeline::onRedo()
{
Doc* doc = document();
frame_t currentTotalFrames = doc->sprite()->totalFrames();
doc->sprite()->setTotalFrames(m_previousTotalFrames);
m_previousTotalFrames = currentTotalFrames;
Layer* afterThis = nullptr;
Layer* beforeThis = nullptr;
LayerGroup* group = nullptr;
setupInsertionLayers(&beforeThis, &afterThis, &group);
for (auto it = m_droppedLayers.cbegin(); it != m_droppedLayers.cend(); ++it) {
auto* layer = *it;
if (afterThis) {
group->insertLayer(layer, afterThis);
afterThis = layer;
}
else if (beforeThis) {
group->insertLayerBefore(layer, beforeThis);
beforeThis = nullptr;
afterThis = layer;
}
else {
group->addLayer(layer);
afterThis = layer;
}
}
notifyDocObservers(afterThis ? afterThis : beforeThis);
}
void DropOnTimeline::notifyDocObservers(Layer* layer)
{
Doc* doc = document();
if (doc && layer) {
DocEvent ev(doc);
ev.sprite(doc->sprite());
ev.layer(layer);
// TODO: This is a hack, we send this notification because the timeline
// has the code we need to execute after this command. We tried using
// DocObserver::onAddLayer but it makes the redo crash.
doc->notify_observers<DocEvent&>(&DocObserver::onAfterRemoveLayer, ev);
}
}
} // namespace cmd
} // namespace app

View File

@ -0,0 +1,62 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_CMD_drop_on_timeline_H_INCLUDED
#define APP_CMD_drop_on_timeline_H_INCLUDED
#pragma once
#include "app/cmd.h"
#include "app/cmd/with_document.h"
#include "app/doc_observer.h"
#include "base/paths.h"
#include "doc/frame.h"
#include "doc/layer.h"
#include "doc/layer_list.h"
#include <vector>
namespace app {
namespace cmd {
class DropOnTimeline : public Cmd
, public WithDocument {
public:
enum class LayerInsertion {
Before,
After,
};
// Inserts the layers and frames of the documents pointed by the specified
// paths, at the specified frame and before or after the specified layer index.
DropOnTimeline(app::Doc* doc, doc::frame_t frame, doc::layer_t layerIndex,
LayerInsertion insert, const base::paths& paths);
protected:
void onExecute() override;
void onUndo() override;
void onRedo() override;
size_t onMemSize() const override {
return sizeof(*this) + m_size;
}
private:
void setupInsertionLayers(doc::Layer** before, doc::Layer** after, doc::LayerGroup** group);
void notifyAddLayer(doc::Layer* layer);
void notifyDocObservers(doc::Layer* layer);
size_t m_size;
base::paths m_paths;
doc::frame_t m_frame;
doc::layer_t m_layerIndex;
LayerInsertion m_insert;
// Holds the list of layers dropped into the document.
doc::LayerList m_droppedLayers;
// Number of frames the doc had before dropping.
doc::frame_t m_previousTotalFrames;
};
} // namespace cmd
} // namespace app
#endif

View File

@ -13,6 +13,7 @@
#include "app/app.h"
#include "app/app_menus.h"
#include "app/cmd/drop_on_timeline.h"
#include "app/cmd/set_tag_range.h"
#include "app/cmd_transaction.h"
#include "app/color_utils.h"
@ -4471,6 +4472,7 @@ void Timeline::onDragLeave(ui::DragEvent& e)
{
m_state = STATE_STANDBY;
m_range.clearRange();
m_dropRange.clearRange();
invalidate();
flushRedraw();
os::Event ev;
@ -4487,12 +4489,15 @@ void Timeline::onDrag(ui::DragEvent& e)
case PART_ROW_CONTINUOUS_ICON:
case PART_ROW_PADLOCK_ICON:
case PART_ROW_TEXT: {
m_range.startRange(nullptr, m_frame, Range::kLayers);
m_range.startRange(nullptr, -1, Range::kLayers);
break;
}
case PART_CEL:
case PART_HEADER_FRAME:
m_range.startRange(nullptr, m_frame, Range::kFrames);
m_range.startRange(nullptr, -1, Range::kFrames);
break;
default:
m_range.clearRange();
break;
}
@ -4507,9 +4512,43 @@ void Timeline::onDrop(ui::DragEvent& e)
{
Widget::onDrop(e);
// Determine at which frame and layer the content was dropped on.
frame_t frame = m_frame;
layer_t layerIndex = getLayerIndex(m_layer);
switch(m_dropRange.type()) {
case Range::kFrames:
frame = m_dropRange.firstFrame();
if (m_dropTarget.hhit == DropTarget::After)
frame++;
break;
case Range::kLayers:
layerIndex = getLayerIndex(*m_dropRange.selectedLayers().begin());
if (m_dropTarget.vhit == DropTarget::Top)
layerIndex++;
break;
}
#if _DEBUG
LOG(LogLevel::VERBOSE, "Dropped at frame: %d, and layerIndex: %d\n", frame, layerIndex);
#endif
if (e.hasPaths()) {
base::paths paths = e.getPaths();
execute_from_ui_thread([=]{
Tx tx(m_document);
tx(new cmd::DropAtTimeline(m_document, frame, layerIndex, paths));
tx.commit();
regenerateRows();
showCurrentCel();
clearClipboardRange();
m_document->notifyGeneralUpdate();
});
}
e.handled(true);
m_state = STATE_STANDBY;
m_range.clearRange();
m_dropRange.clearRange();
invalidate();
flushRedraw();
os::Event ev;