Add support to dropping images into the timeline

This commit is contained in:
Martín Capello 2024-10-15 16:23:57 -03:00
parent 114a18bc4f
commit 9560650867
3 changed files with 194 additions and 115 deletions

View File

@ -47,6 +47,22 @@ DropOnTimeline::DropOnTimeline(app::Doc* doc,
m_size += path.size();
}
DropOnTimeline::DropOnTimeline(app::Doc* doc,
doc::frame_t frame,
doc::layer_t layerIndex,
InsertionPoint insert,
DroppedOn droppedOn,
const doc::ImageRef& image) : WithDocument(doc)
, m_size(0)
, m_image(image)
, m_frame(frame)
, m_layerIndex(layerIndex)
, m_insert(insert)
, m_droppedOn(droppedOn)
{
ASSERT(m_layerIndex >= 0);
}
void DropOnTimeline::setupInsertionLayers(Layer** before, Layer** after, LayerGroup** group)
{
const LayerList& allLayers = document()->sprite()->allLayers();
@ -68,12 +84,80 @@ void DropOnTimeline::setupInsertionLayers(Layer** before, Layer** after, LayerGr
*group = (*after ? (*after)->parent() : (*before)->parent());
}
bool DropOnTimeline::hasPendingWork()
{
return m_image || !m_paths.empty();
}
bool DropOnTimeline::getNextDocFromImage(Doc** srcDoc)
{
if (!m_image)
return true;
Sprite* sprite = new Sprite(m_image->spec(), 256);
LayerImage* layer = new LayerImage(sprite);
sprite->root()->addLayer(layer);
Cel* cel = new Cel(0, m_image);
layer->addCel(cel);
*srcDoc = new Doc(sprite);
m_image = nullptr;
return true;
}
bool DropOnTimeline::getNextDocFromPaths(Doc** srcDoc)
{
Console console;
Context* context = document()->context();
int flags = FILE_LOAD_DATA_FILE | FILE_LOAD_AVOID_BACKGROUND_LAYER |
FILE_LOAD_CREATE_PALETTE | FILE_LOAD_SEQUENCE_YES;
std::unique_ptr<FileOp> fop(
FileOp::createLoadDocumentOperation(context, m_paths.front(), flags));
// Do nothing (the user cancelled or something like that)
if (!fop)
return false;
base::paths fopFilenames;
fop->getFilenameList(fopFilenames);
// Remove paths that will be loaded by the current file operation.
for (const auto& filename : fopFilenames) {
auto it = std::find(m_paths.begin(), m_paths.end(), filename);
if (it != m_paths.end())
m_paths.erase(it);
}
if (fop->hasError()) {
console.printf(fop->error().c_str());
return true;
}
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());
*srcDoc = fop->releaseDocument();
return true;
}
bool DropOnTimeline::getNextDoc(Doc** srcDoc)
{
*srcDoc = nullptr;
if (m_image == nullptr && !m_paths.empty())
return getNextDocFromPaths(srcDoc);
return getNextDocFromImage(srcDoc);
}
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;
@ -81,107 +165,78 @@ void DropOnTimeline::onExecute()
// Parent group of the after/before layers.
LayerGroup* group = nullptr;
int flags =
FILE_LOAD_DATA_FILE | FILE_LOAD_AVOID_BACKGROUND_LAYER |
FILE_LOAD_CREATE_PALETTE | FILE_LOAD_SEQUENCE_YES;
int fopCount = 0;
while(!m_paths.empty()) {
std::unique_ptr<FileOp> fop(
FileOp::createLoadDocumentOperation(context, m_paths.front(), flags));
// Do nothing (the user cancelled or something like that)
if (!fop)
int docsProcessed = 0;
while(hasPendingWork()) {
Doc* srcDoc;
if (!getNextDoc(&srcDoc))
return;
fopCount++;
base::paths fopFilenames;
fop->getFilenameList(fopFilenames);
// Remove paths that will be loaded by the current file operation.
for (const auto& filename : fopFilenames) {
auto it = std::find(m_paths.begin(), m_paths.end(), filename);
if (it != m_paths.end())
m_paths.erase(it);
}
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()) {
// Execute in a source doc transaction because we don't need undo/redo
// this.
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 only one file operation and we can move the cel from the
// source document then move the cel into the destination frame.
const bool isOnlyFileOperation = (fopCount == 1 && m_paths.empty());
if (isOnlyFileOperation && canMoveCelFrom(srcDoc)) {
auto* srcLayer = static_cast<LayerImage*>(srcDoc->sprite()->firstLayer());
auto* destLayer = static_cast<LayerImage*>(destDoc->sprite()->allLayers()[m_layerIndex]);
executeAndAdd(new MoveCel(srcLayer, 0, destLayer, m_frame, false));
break;
}
// 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 = copy_layer_with_sprite(layer, destDoc->sprite());
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();
if (srcDoc) {
docsProcessed++;
// If source document doesn't match the destination document's color
// mode, change it.
if (srcDoc->colorMode() != destDoc->colorMode()) {
// Execute in a source doc transaction because we don't need undo/redo
// this.
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 only one source document to process and it has a cel that
// can be moved, then move the cel from the source doc into the
// destination doc's selected frame.
const bool isJustOneDoc = (docsProcessed == 1 && !hasPendingWork());
if (isJustOneDoc && canMoveCelFrom(srcDoc)) {
auto* srcLayer = static_cast<LayerImage*>(srcDoc->sprite()->firstLayer());
auto* destLayer = static_cast<LayerImage*>(destDoc->sprite()->allLayers()[m_layerIndex]);
executeAndAdd(new MoveCel(srcLayer, 0, destLayer, m_frame, false));
break;
}
// 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 = copy_layer_with_sprite(layer, destDoc->sprite());
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();
// Source doc is not needed anymore.
delete srcDoc;
}
}
destDoc->sprite()->incrementVersion();
@ -254,14 +309,16 @@ void DropOnTimeline::onRedo()
notifyDocObservers(afterThis ? afterThis : beforeThis);
}
// Returns true if the document srcDoc has a cel that can be moved.
// The cel from the srcDoc can be moved only when all of the following
// conditions are met:
// * Drop took place in a cel.
// * Source doc has only one layer with just one frame.
// * The layer from the source doc and the destination cel's layer are both
// Image layers.
// Otherwise this function returns false.
bool DropOnTimeline::canMoveCelFrom(app::Doc* srcDoc)
{
// The cel from the source doc can be moved only when the all of the following
// conditions are met:
// * Drop took place in a cel.
// * Source doc has only one layer with just one frame.
// * The layer from the source doc and the destination cel's layer are both
// Image layers.
auto* srcLayer = srcDoc->sprite()->firstLayer();
auto* destLayer = document()->sprite()->allLayers()[m_layerIndex];
return m_droppedOn == DroppedOn::Cel &&

View File

@ -13,6 +13,7 @@
#include "app/doc_observer.h"
#include "base/paths.h"
#include "doc/frame.h"
#include "doc/image_ref.h"
#include "doc/layer.h"
#include "doc/layer_list.h"
@ -38,6 +39,12 @@ namespace cmd {
// 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,
InsertionPoint insert, DroppedOn droppedOn, const base::paths& paths);
// Inserts the image as if it were a document with just one layer and one
// frame, at the specified frame and before or after the specified layer index.
DropOnTimeline(app::Doc* doc, doc::frame_t frame, doc::layer_t layerIndex,
InsertionPoint insert, DroppedOn droppedOn, const doc::ImageRef& image);
protected:
void onExecute() override;
void onUndo() override;
@ -51,9 +58,17 @@ namespace cmd {
bool canMoveCelFrom(app::Doc* srcDoc);
void notifyAddLayer(doc::Layer* layer);
void notifyDocObservers(doc::Layer* layer);
bool hasPendingWork();
// Sets srcDoc's Doc* pointer to the next document to be processed.
// Returns false when the user cancelled the process, or true when the
// process must go on.
bool getNextDoc(Doc** srcDoc);
bool getNextDocFromImage(Doc** srcDoc);
bool getNextDocFromPaths(Doc** srcDoc);
size_t m_size;
base::paths m_paths;
doc::ImageRef m_image = nullptr;
doc::frame_t m_frame;
doc::layer_t m_layerIndex;
InsertionPoint m_insert;

View File

@ -52,6 +52,7 @@
#include "base/memory.h"
#include "base/scoped_value.h"
#include "doc/doc.h"
#include "doc/image_ref.h"
#include "fmt/format.h"
#include "gfx/point.h"
#include "gfx/rect.h"
@ -4486,11 +4487,6 @@ void Timeline::onDrag(ui::DragEvent& e)
{
Widget::onDrag(e);
// Dropping images into the timeline is not supported yet, so do nothing.
if (e.hasImage() && !e.hasPaths()) {
return;
}
m_range.clearRange();
setHot(hitTest(nullptr, e.position()));
switch (m_hot.part) {
@ -4559,11 +4555,22 @@ void Timeline::onDrop(ui::DragEvent& e)
LOG(LogLevel::VERBOSE, "Dropped at frame: %d, and layerIndex: %d\n", frame, layerIndex);
#endif
if (e.hasPaths()) {
if (e.hasPaths() || e.hasImage()) {
bool droppedImage = e.hasImage() && !e.hasPaths();
base::paths paths = e.getPaths();
auto surface = e.getImage();
execute_from_ui_thread([=]{
Tx tx(m_document, "Drop on timeline");
tx(new cmd::DropOnTimeline(m_document, frame, layerIndex, insert, droppedOn, paths));
std::string txmsg = (droppedImage ? "Dropped image on timeline"
: "Dropped paths on timeline");
Tx tx(m_document, txmsg);
if (droppedImage) {
doc::ImageRef image = nullptr;
convert_surface_to_image(surface.get(), 0, 0, surface->width(), surface->height(), image);
tx(new cmd::DropOnTimeline(m_document, frame, layerIndex, insert, droppedOn, image));
}
else
tx(new cmd::DropOnTimeline(m_document, frame, layerIndex, insert, droppedOn, paths));
tx.commit();
m_document->notifyGeneralUpdate();
});