Add support to drop a file into a timeline's cel

This commit is contained in:
Martín Capello 2024-09-18 15:31:59 -03:00
parent 84b56f4b2d
commit 9ae0416627
3 changed files with 104 additions and 33 deletions

View File

@ -12,15 +12,14 @@
#include "app/cmd/add_layer.h"
#include "app/cmd/set_pixel_format.h"
#include "app/cmd/move_cel.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"
@ -32,13 +31,15 @@ namespace cmd {
DropOnTimeline::DropOnTimeline(app::Doc* doc,
doc::frame_t frame,
doc::layer_t layerIndex,
LayerInsertion insert,
InsertionPoint insert,
DroppedOn droppedOn,
const base::paths& paths) : WithDocument(doc)
, m_size(0)
, m_paths(paths)
, m_frame(frame)
, m_layerIndex(layerIndex)
, m_insert(insert)
, m_droppedOn(droppedOn)
{
ASSERT(m_layerIndex >= 0);
for(const auto& path : m_paths)
@ -48,8 +49,8 @@ DropOnTimeline::DropOnTimeline(app::Doc* doc,
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);
*after = (m_insert == InsertionPoint::AfterLayer ? allLayers[m_layerIndex] : nullptr);
*before = (m_insert == InsertionPoint::BeforeLayer ? 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
@ -83,13 +84,20 @@ void DropOnTimeline::onExecute()
FILE_LOAD_DATA_FILE |
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));
fopCount++;
base::paths fopFilenames;
fop->getFilenameList(fopFilenames);
// Remove paths that will be loaded by the current file operation.
for (const auto& filename : fop->filenames())
m_paths.erase(std::find(m_paths.begin(), m_paths.end(), filename));
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);
}
// Do nothing (the user cancelled or something like that)
if (!fop)
@ -114,6 +122,8 @@ void DropOnTimeline::onExecute()
// 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(),
@ -125,6 +135,16 @@ void DropOnTimeline::onExecute()
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()) {
@ -171,6 +191,13 @@ void DropOnTimeline::onExecute()
void DropOnTimeline::onUndo()
{
CmdSequence::onUndo();
if (m_droppedLayers.empty()) {
notifyDocObservers(nullptr);
return;
}
Doc* doc = document();
frame_t currentTotalFrames = doc->sprite()->totalFrames();
Layer* layerBefore = nullptr;
@ -189,6 +216,13 @@ void DropOnTimeline::onUndo()
void DropOnTimeline::onRedo()
{
CmdSequence::onRedo();
if (m_droppedLayers.empty()) {
notifyDocObservers(nullptr);
return;
}
Doc* doc = document();
frame_t currentTotalFrames = doc->sprite()->totalFrames();
doc->sprite()->setTotalFrames(m_previousTotalFrames);
@ -219,18 +253,41 @@ void DropOnTimeline::onRedo()
notifyDocObservers(afterThis ? afterThis : beforeThis);
}
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 &&
srcDoc->sprite()->allLayersCount() == 1 &&
srcDoc->sprite()->totalFrames() == 1 &&
srcLayer->isImage() &&
destLayer->isImage();
}
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);
if (!doc)
return;
if (!layer) {
doc->notifyGeneralUpdate();
return;
}
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

View File

@ -8,7 +8,7 @@
#define APP_CMD_drop_on_timeline_H_INCLUDED
#pragma once
#include "app/cmd.h"
#include "app/cmd_sequence.h"
#include "app/cmd/with_document.h"
#include "app/doc_observer.h"
#include "base/paths.h"
@ -16,22 +16,28 @@
#include "doc/layer.h"
#include "doc/layer_list.h"
#include <vector>
namespace app {
namespace cmd {
class DropOnTimeline : public Cmd
class DropOnTimeline : public CmdSequence
, public WithDocument {
public:
enum class LayerInsertion {
Before,
After,
enum class InsertionPoint {
BeforeLayer,
AfterLayer,
};
enum class DroppedOn {
Unspecified,
Frame,
Layer,
Cel,
};
// 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);
InsertionPoint insert, DroppedOn droppedOn, const base::paths& paths);
protected:
void onExecute() override;
void onUndo() override;
@ -42,6 +48,7 @@ namespace cmd {
private:
void setupInsertionLayers(doc::Layer** before, doc::Layer** after, doc::LayerGroup** group);
bool canMoveCelFrom(app::Doc* srcDoc);
void notifyAddLayer(doc::Layer* layer);
void notifyDocObservers(doc::Layer* layer);
@ -49,8 +56,10 @@ namespace cmd {
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.
InsertionPoint m_insert;
DroppedOn m_droppedOn;
// Holds the list of layers dropped into the document. Used to support
// undo/redo without having to read all the files again.
doc::LayerList m_droppedLayers;
// Number of frames the doc had before dropping.
doc::frame_t m_previousTotalFrames;

View File

@ -4515,32 +4515,37 @@ void Timeline::onDrag(ui::DragEvent& e)
void Timeline::onDrop(ui::DragEvent& e)
{
using LayerInsertion = cmd::DropOnTimeline::LayerInsertion;
using InsertionPoint = cmd::DropOnTimeline::InsertionPoint;
using DroppedOn = cmd::DropOnTimeline::DroppedOn;
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);
LayerInsertion insert = LayerInsertion::Before;
InsertionPoint insert = InsertionPoint::BeforeLayer;
DroppedOn droppedOn = DroppedOn::Unspecified;
switch(m_dropRange.type()) {
case Range::kCels:
frame = m_hot.frame;
layerIndex = m_hot.layer;
insert = (m_dropTarget.vhit == DropTarget::Top ? LayerInsertion::After
: LayerInsertion::Before);
droppedOn = DroppedOn::Cel;
insert = (m_dropTarget.vhit == DropTarget::Top ? InsertionPoint::AfterLayer
: InsertionPoint::BeforeLayer);
break;
case Range::kFrames:
frame = m_dropRange.firstFrame();
droppedOn = DroppedOn::Frame;
if (m_dropTarget.hhit == DropTarget::After)
frame++;
break;
case Range::kLayers:
droppedOn = DroppedOn::Layer;
if (m_dropTarget.vhit != DropTarget::VeryBottom) {
auto* selectedLayer = *m_dropRange.selectedLayers().begin();
layerIndex = getLayerIndex(selectedLayer);
}
insert = (m_dropTarget.vhit == DropTarget::Top ? LayerInsertion::After
: LayerInsertion::Before);
insert = (m_dropTarget.vhit == DropTarget::Top ? InsertionPoint::AfterLayer
: InsertionPoint::BeforeLayer);
break;
}
@ -4551,8 +4556,8 @@ void Timeline::onDrop(ui::DragEvent& e)
if (e.hasPaths()) {
base::paths paths = e.getPaths();
execute_from_ui_thread([=]{
Tx tx(m_document);
tx(new cmd::DropOnTimeline(m_document, frame, layerIndex, insert, paths));
Tx tx(m_document, "Drop on timeline");
tx(new cmd::DropOnTimeline(m_document, frame, layerIndex, insert, droppedOn, paths));
tx.commit();
m_document->notifyGeneralUpdate();
});