mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-30 04:20:23 +00:00
commit
10eaec8a87
@ -105,6 +105,13 @@ else()
|
||||
target_sources(app-lib PRIVATE font_path_unix.cpp)
|
||||
endif()
|
||||
|
||||
# This defines a specific webp decoding utility function for using
|
||||
# in Windows when dragging and dropping images that are stored as
|
||||
# webp files (like Chrome does).
|
||||
if(WIN32 AND ENABLE_WEBP)
|
||||
target_sources(app-lib PRIVATE util/decode_webp.cpp)
|
||||
endif()
|
||||
|
||||
# Trial-version vs Full version (enable save command)
|
||||
if(ENABLE_TRIAL_MODE)
|
||||
target_compile_definitions(app-lib PUBLIC -DENABLE_TRIAL_MODE)
|
||||
@ -282,6 +289,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
|
||||
@ -688,6 +696,7 @@ target_sources(app-lib PRIVATE
|
||||
util/cel_ops.cpp
|
||||
util/clipboard.cpp
|
||||
util/clipboard_native.cpp
|
||||
util/conversion_to_image.cpp
|
||||
util/conversion_to_surface.cpp
|
||||
util/expand_cel_canvas.cpp
|
||||
util/filetoks.cpp
|
||||
|
@ -78,6 +78,10 @@
|
||||
#include "os/x11/system.h"
|
||||
#endif
|
||||
|
||||
#if ENABLE_WEBP && LAF_WINDOWS
|
||||
#include "app/util/decode_webp.h"
|
||||
#endif
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
@ -482,6 +486,10 @@ void App::run()
|
||||
manager->display()->nativeWindow()
|
||||
->setInterpretOneFingerGestureAsMouseMovement(
|
||||
preferences().experimental.oneFingerAsMouseMovement());
|
||||
#if ENABLE_WEBP
|
||||
// In Windows we use a custom webp decoder for drag & drop operations.
|
||||
os::set_decode_webp(util::decode_webp);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if LAF_LINUX
|
||||
|
343
src/app/cmd/drop_on_timeline.cpp
Normal file
343
src/app/cmd/drop_on_timeline.cpp
Normal file
@ -0,0 +1,343 @@
|
||||
// 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/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/util/layer_utils.h"
|
||||
#include "app/util/open_file_job.h"
|
||||
#include "app/tx.h"
|
||||
#include "doc/layer_list.h"
|
||||
#include "render/dithering.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace app {
|
||||
namespace cmd {
|
||||
|
||||
DropOnTimeline::DropOnTimeline(app::Doc* doc,
|
||||
doc::frame_t frame,
|
||||
doc::layer_t layerIndex,
|
||||
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)
|
||||
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::setupInsertionLayer(Layer** layer, LayerGroup** group)
|
||||
{
|
||||
const LayerList& allLayers = document()->sprite()->allLayers();
|
||||
*layer = allLayers[m_layerIndex];
|
||||
if (m_insert == InsertionPoint::BeforeLayer && (*layer)->isGroup()) {
|
||||
*group = static_cast<LayerGroup*>(*layer);
|
||||
// The user is trying to drop layers into an empty group, so there is no after
|
||||
// nor before layer...
|
||||
if ((*group)->layersCount() == 0) {
|
||||
*layer = nullptr;
|
||||
return;
|
||||
}
|
||||
*layer = (*group)->lastLayer();
|
||||
m_insert = InsertionPoint::AfterLayer;
|
||||
}
|
||||
|
||||
*group = (*layer)->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();
|
||||
m_previousTotalFrames = destDoc->sprite()->totalFrames();
|
||||
|
||||
int docsProcessed = 0;
|
||||
while(hasPendingWork()) {
|
||||
Doc* srcDoc;
|
||||
if (!getNextDoc(&srcDoc))
|
||||
return;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
// Save dropped layers from 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);
|
||||
m_droppedLayers.push_back(layerCopy);
|
||||
m_size += layerCopy->getMemSize();
|
||||
}
|
||||
|
||||
// Source doc is not needed anymore.
|
||||
delete srcDoc;
|
||||
}
|
||||
}
|
||||
destDoc->sprite()->incrementVersion();
|
||||
destDoc->incrementVersion();
|
||||
|
||||
insertDroppedLayers(true);
|
||||
}
|
||||
|
||||
void DropOnTimeline::onUndo()
|
||||
{
|
||||
CmdSequence::onUndo();
|
||||
|
||||
if (m_droppedLayers.empty()) {
|
||||
notifyDocObservers(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
CmdSequence::onRedo();
|
||||
|
||||
if (m_droppedLayers.empty()) {
|
||||
notifyDocObservers(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
Doc* doc = document();
|
||||
frame_t currentTotalFrames = doc->sprite()->totalFrames();
|
||||
doc->sprite()->setTotalFrames(m_previousTotalFrames);
|
||||
m_previousTotalFrames = currentTotalFrames;
|
||||
|
||||
insertDroppedLayers(false);
|
||||
}
|
||||
|
||||
void DropOnTimeline::insertDroppedLayers(bool incGroupVersion)
|
||||
{
|
||||
// Layer used as a reference to determine if the dropped layers will be
|
||||
// inserted after or before it.
|
||||
Layer* refLayer = nullptr;
|
||||
// Parent group of the reference layer layer.
|
||||
LayerGroup* group = nullptr;
|
||||
// Keep track of the current insertion point.
|
||||
InsertionPoint insert = m_insert;
|
||||
|
||||
setupInsertionLayer(&refLayer, &group);
|
||||
|
||||
for (auto it = m_droppedLayers.cbegin(); it != m_droppedLayers.cend(); ++it) {
|
||||
auto* layer = *it;
|
||||
|
||||
if (!refLayer) {
|
||||
group->addLayer(layer);
|
||||
refLayer = layer;
|
||||
insert = InsertionPoint::AfterLayer;
|
||||
}
|
||||
else if (insert == InsertionPoint::AfterLayer) {
|
||||
group->insertLayer(layer, refLayer);
|
||||
refLayer = layer;
|
||||
}
|
||||
else if (insert == InsertionPoint::BeforeLayer) {
|
||||
group->insertLayerBefore(layer, refLayer);
|
||||
refLayer = layer;
|
||||
insert = InsertionPoint::AfterLayer;
|
||||
}
|
||||
}
|
||||
|
||||
if (incGroupVersion)
|
||||
group->incrementVersion();
|
||||
|
||||
notifyDocObservers(refLayer);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
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)
|
||||
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
|
||||
} // namespace app
|
87
src/app/cmd/drop_on_timeline.h
Normal file
87
src/app/cmd/drop_on_timeline.h
Normal file
@ -0,0 +1,87 @@
|
||||
// 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_sequence.h"
|
||||
#include "app/cmd/with_document.h"
|
||||
#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"
|
||||
|
||||
namespace app {
|
||||
namespace cmd {
|
||||
|
||||
class DropOnTimeline : public CmdSequence
|
||||
, public WithDocument {
|
||||
public:
|
||||
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,
|
||||
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;
|
||||
void onRedo() override;
|
||||
size_t onMemSize() const override {
|
||||
return sizeof(*this) + m_size;
|
||||
}
|
||||
|
||||
private:
|
||||
void setupInsertionLayer(doc::Layer** layer, doc::LayerGroup** group);
|
||||
void insertDroppedLayers(bool incGroupVersion);
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace cmd
|
||||
} // namespace app
|
||||
|
||||
#endif
|
@ -14,6 +14,7 @@
|
||||
#include "app/app.h"
|
||||
#include "app/context.h"
|
||||
#include "app/site.h"
|
||||
#include "app/sprite_position.h"
|
||||
#include "app/ui/timeline/timeline.h"
|
||||
|
||||
namespace app {
|
||||
@ -118,6 +119,12 @@ size_t CmdTransaction::onMemSize() const
|
||||
|
||||
SpritePosition CmdTransaction::calcSpritePosition() const
|
||||
{
|
||||
// This check was added to allow executing transactions on documents that are
|
||||
// not part of any context. For instance, when dragging and dropping a
|
||||
// document on the timeline, the dragged document doesn't have any context (
|
||||
// it is not associated with any editor).
|
||||
if (!context())
|
||||
return SpritePosition();
|
||||
Site site = context()->activeSite();
|
||||
return SpritePosition(site.layer(), site.frame());
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ struct NewFileParams : public NewParams {
|
||||
Param<int> height { this, 0, "height" };
|
||||
Param<ColorMode> colorMode { this, ColorMode::RGB, "colorMode" };
|
||||
Param<bool> fromClipboard { this, false, "fromClipboard" };
|
||||
Param<bool> fromDraggedData { this, false, "fromDraggedData" };
|
||||
};
|
||||
|
||||
class NewFileCommand : public CommandWithNewParams<NewFileParams> {
|
||||
@ -86,8 +87,9 @@ void NewFileCommand::onExecute(Context* ctx)
|
||||
doc::Palette clipboardPalette(0, 256);
|
||||
const int ncolors = get_default_palette()->size();
|
||||
|
||||
if (params().fromClipboard()) {
|
||||
clipboardImage = ctx->clipboard()->getImage(&clipboardPalette);
|
||||
if (params().fromClipboard() || params().fromDraggedData()) {
|
||||
clipboardImage = (params().fromClipboard() ? ctx->clipboard()->getImage(&clipboardPalette)
|
||||
: ctx->draggedData()->getImage());
|
||||
if (!clipboardImage)
|
||||
return;
|
||||
|
||||
|
@ -19,12 +19,12 @@
|
||||
#include "app/file/file.h"
|
||||
#include "app/file_selector.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/job.h"
|
||||
#include "app/modules/gui.h"
|
||||
#include "app/pref/preferences.h"
|
||||
#include "app/recent_files.h"
|
||||
#include "app/ui/status_bar.h"
|
||||
#include "app/ui_context.h"
|
||||
#include "app/util/open_file_job.h"
|
||||
#include "base/fs.h"
|
||||
#include "base/thread.h"
|
||||
#include "doc/sprite.h"
|
||||
@ -34,46 +34,6 @@
|
||||
|
||||
namespace app {
|
||||
|
||||
class OpenFileJob : public Job, public IFileOpProgress {
|
||||
public:
|
||||
OpenFileJob(FileOp* fop, const bool showProgress)
|
||||
: Job(Strings::open_file_loading(), showProgress)
|
||||
, m_fop(fop)
|
||||
{
|
||||
}
|
||||
|
||||
void showProgressWindow() {
|
||||
startJob();
|
||||
|
||||
if (isCanceled())
|
||||
m_fop->stop();
|
||||
|
||||
waitJob();
|
||||
}
|
||||
|
||||
private:
|
||||
// Thread to do the hard work: load the file from the disk.
|
||||
virtual void onJob() override {
|
||||
try {
|
||||
m_fop->operate(this);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
m_fop->setError("Error loading file:\n%s", e.what());
|
||||
}
|
||||
|
||||
if (m_fop->isStop() && m_fop->document())
|
||||
delete m_fop->releaseDocument();
|
||||
|
||||
m_fop->done();
|
||||
}
|
||||
|
||||
virtual void ackFileOpProgress(double progress) override {
|
||||
jobProgress(progress);
|
||||
}
|
||||
|
||||
FileOp* m_fop;
|
||||
};
|
||||
|
||||
OpenFileCommand::OpenFileCommand()
|
||||
: Command(CommandId::OpenFile(), CmdRecordableFlag)
|
||||
, m_ui(true)
|
||||
|
@ -14,11 +14,14 @@
|
||||
#include "app/context_observer.h"
|
||||
#include "app/docs.h"
|
||||
#include "app/docs_observer.h"
|
||||
#include "app/util/conversion_to_image.h"
|
||||
#include "base/disable_copying.h"
|
||||
#include "base/exception.h"
|
||||
#include "doc/frame.h"
|
||||
#include "doc/image_ref.h"
|
||||
#include "obs/observable.h"
|
||||
#include "obs/signal.h"
|
||||
#include "os/surface.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
@ -86,6 +89,23 @@ namespace app {
|
||||
bool m_canceled;
|
||||
};
|
||||
|
||||
class DraggedData
|
||||
{
|
||||
public:
|
||||
DraggedData(const doc::ImageRef& image) {
|
||||
m_image = image;
|
||||
}
|
||||
DraggedData(const os::SurfaceRef& surface) {
|
||||
if (surface)
|
||||
convert_surface_to_image(surface.get(), 0, 0, surface->width(), surface->height(), m_image);
|
||||
}
|
||||
|
||||
const doc::ImageRef& getImage() const { return m_image; }
|
||||
|
||||
private:
|
||||
doc::ImageRef m_image = nullptr;
|
||||
};
|
||||
|
||||
class Context : public obs::observable<ContextObserver>,
|
||||
public DocsObserver {
|
||||
public:
|
||||
@ -120,6 +140,11 @@ namespace app {
|
||||
bool hasModifiedDocuments() const;
|
||||
void notifyActiveSiteChanged();
|
||||
|
||||
void setDraggedData(std::unique_ptr<DraggedData> draggedData) {
|
||||
m_draggedData = std::move(draggedData);
|
||||
}
|
||||
const DraggedData* draggedData() const { return m_draggedData.get(); }
|
||||
|
||||
void executeCommandFromMenuOrShortcut(Command* command, const Params& params = Params());
|
||||
virtual void executeCommand(Command* command, const Params& params = Params());
|
||||
|
||||
@ -159,6 +184,7 @@ namespace app {
|
||||
ContextFlags m_flags; // Last updated flags.
|
||||
Doc* m_lastSelectedDoc;
|
||||
mutable std::unique_ptr<Preferences> m_preferences;
|
||||
std::unique_ptr<DraggedData> m_draggedData = nullptr;
|
||||
|
||||
// Result of the execution of a command.
|
||||
CommandResult m_result;
|
||||
|
@ -48,6 +48,7 @@
|
||||
#include "app/snap_to_grid.h"
|
||||
#include "app/transaction.h"
|
||||
#include "app/util/autocrop.h"
|
||||
#include "app/util/layer_utils.h"
|
||||
#include "doc/algorithm/flip_image.h"
|
||||
#include "doc/algorithm/shrink_bounds.h"
|
||||
#include "doc/cel.h"
|
||||
@ -697,27 +698,13 @@ void DocApi::restackLayerBefore(Layer* layer, LayerGroup* parent, Layer* beforeT
|
||||
Layer* DocApi::duplicateLayerAfter(Layer* sourceLayer, LayerGroup* parent, Layer* afterLayer)
|
||||
{
|
||||
ASSERT(parent);
|
||||
std::unique_ptr<Layer> newLayerPtr;
|
||||
|
||||
if (sourceLayer->isTilemap()) {
|
||||
newLayerPtr.reset(new LayerTilemap(sourceLayer->sprite(),
|
||||
static_cast<LayerTilemap*>(sourceLayer)->tilesetIndex()));
|
||||
}
|
||||
else if (sourceLayer->isImage())
|
||||
newLayerPtr.reset(new LayerImage(sourceLayer->sprite()));
|
||||
else if (sourceLayer->isGroup())
|
||||
newLayerPtr.reset(new LayerGroup(sourceLayer->sprite()));
|
||||
else
|
||||
throw std::runtime_error("Invalid layer type");
|
||||
|
||||
m_document->copyLayerContent(sourceLayer, m_document, newLayerPtr.get());
|
||||
Layer* newLayerPtr = copy_layer(sourceLayer);
|
||||
|
||||
newLayerPtr->setName(newLayerPtr->name() + " Copy");
|
||||
|
||||
addLayer(parent, newLayerPtr.get(), afterLayer);
|
||||
addLayer(parent, newLayerPtr, afterLayer);
|
||||
|
||||
// Release the pointer as it is owned by the sprite now.
|
||||
return newLayerPtr.release();
|
||||
return newLayerPtr;
|
||||
}
|
||||
|
||||
Layer* DocApi::duplicateLayerBefore(Layer* sourceLayer, LayerGroup* parent, Layer* beforeLayer)
|
||||
|
@ -533,6 +533,11 @@ FileOp* FileOp::createLoadDocumentOperation(Context* context,
|
||||
fop->m_dataFilename = dataFilename;
|
||||
}
|
||||
|
||||
// Avoid creating a background layer?
|
||||
if (flags & FILE_LOAD_AVOID_BACKGROUND_LAYER) {
|
||||
fop->m_avoidBackgroundLayer = true;
|
||||
}
|
||||
|
||||
done:;
|
||||
return fop.release();
|
||||
}
|
||||
@ -939,8 +944,9 @@ void FileOp::operate(IFileOpProgress* progress)
|
||||
|
||||
// Final setup
|
||||
if (m_document) {
|
||||
// Configure the layer as the 'Background'
|
||||
if (!m_seq.has_alpha)
|
||||
// Configure the layer as the 'Background'. Only if background layers
|
||||
// are welcome.
|
||||
if (!m_seq.has_alpha && !m_avoidBackgroundLayer)
|
||||
m_seq.layer->configureAsBackground();
|
||||
|
||||
// Set the final canvas size (as the bigger loaded
|
||||
@ -1522,6 +1528,7 @@ FileOp::FileOp(FileOpType type,
|
||||
, m_oneframe(false)
|
||||
, m_createPaletteFromRgba(false)
|
||||
, m_ignoreEmpty(false)
|
||||
, m_avoidBackgroundLayer(false)
|
||||
, m_embeddedColorProfile(false)
|
||||
, m_embeddedGridBounds(false)
|
||||
{
|
||||
|
@ -27,13 +27,14 @@
|
||||
#include <string>
|
||||
|
||||
// Flags for FileOp::createLoadDocumentOperation()
|
||||
#define FILE_LOAD_SEQUENCE_NONE 0x00000001
|
||||
#define FILE_LOAD_SEQUENCE_ASK 0x00000002
|
||||
#define FILE_LOAD_SEQUENCE_ASK_CHECKBOX 0x00000004
|
||||
#define FILE_LOAD_SEQUENCE_YES 0x00000008
|
||||
#define FILE_LOAD_ONE_FRAME 0x00000010
|
||||
#define FILE_LOAD_DATA_FILE 0x00000020
|
||||
#define FILE_LOAD_CREATE_PALETTE 0x00000040
|
||||
#define FILE_LOAD_SEQUENCE_NONE 0x00000001
|
||||
#define FILE_LOAD_SEQUENCE_ASK 0x00000002
|
||||
#define FILE_LOAD_SEQUENCE_ASK_CHECKBOX 0x00000004
|
||||
#define FILE_LOAD_SEQUENCE_YES 0x00000008
|
||||
#define FILE_LOAD_ONE_FRAME 0x00000010
|
||||
#define FILE_LOAD_DATA_FILE 0x00000020
|
||||
#define FILE_LOAD_CREATE_PALETTE 0x00000040
|
||||
#define FILE_LOAD_AVOID_BACKGROUND_LAYER 0x00000080
|
||||
|
||||
namespace doc {
|
||||
class Tag;
|
||||
@ -285,6 +286,8 @@ namespace app {
|
||||
bool newBlend() const { return m_config.newBlend; }
|
||||
const FileOpConfig& config() const { return m_config; }
|
||||
|
||||
bool avoidBackgroundLayer() const { return m_avoidBackgroundLayer; }
|
||||
|
||||
private:
|
||||
FileOp(); // Undefined
|
||||
FileOp(FileOpType type,
|
||||
@ -314,6 +317,7 @@ namespace app {
|
||||
// GIF/FLI/ASE).
|
||||
bool m_createPaletteFromRgba;
|
||||
bool m_ignoreEmpty;
|
||||
bool m_avoidBackgroundLayer;
|
||||
|
||||
// True if the file contained a color profile when it was loaded.
|
||||
bool m_embeddedColorProfile;
|
||||
|
@ -95,7 +95,8 @@ bool FliFormat::onLoad(FileOp* fop)
|
||||
Sprite* sprite = new Sprite(ImageSpec(ColorMode::INDEXED, w, h), 256);
|
||||
LayerImage* layer = new LayerImage(sprite);
|
||||
sprite->root()->addLayer(layer);
|
||||
layer->configureAsBackground();
|
||||
if (!fop->avoidBackgroundLayer())
|
||||
layer->configureAsBackground();
|
||||
|
||||
// Set frames and speed
|
||||
sprite->setTotalFrames(frame_t(header.frames));
|
||||
|
@ -288,7 +288,7 @@ public:
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_layer && m_opaque)
|
||||
if (m_layer && m_opaque && !m_fop->avoidBackgroundLayer())
|
||||
m_layer->configureAsBackground();
|
||||
|
||||
// sRGB is the default color space for GIF files
|
||||
|
@ -211,7 +211,7 @@ bool WebPFormat::onLoad(FileOp* fop)
|
||||
}
|
||||
WebPAnimDecoderReset(dec);
|
||||
|
||||
if (!has_alpha)
|
||||
if (!has_alpha && !fop->avoidBackgroundLayer())
|
||||
layer->configureAsBackground();
|
||||
|
||||
WebPAnimDecoderDelete(dec);
|
||||
|
@ -28,7 +28,6 @@
|
||||
#include "app/ui/editor/editor.h"
|
||||
#include "app/ui/keyboard_shortcuts.h"
|
||||
#include "app/ui/main_menu_bar.h"
|
||||
#include "app/ui/main_menu_bar.h"
|
||||
#include "app/ui/main_window.h"
|
||||
#include "app/ui/skin/skin_property.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
|
@ -117,7 +117,7 @@ void Transaction::commit()
|
||||
else
|
||||
set_current_palette(nullptr, false);
|
||||
|
||||
if (m_ctx->isUIAvailable())
|
||||
if (m_ctx && m_ctx->isUIAvailable())
|
||||
ui::Manager::getDefault()->invalidate();
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,10 @@
|
||||
#include "app/ui/workspace_tabs.h"
|
||||
#include "app/ui_context.h"
|
||||
#include "base/fs.h"
|
||||
#include "os/event.h"
|
||||
#include "os/event_queue.h"
|
||||
#include "os/system.h"
|
||||
#include "ui/drag_event.h"
|
||||
#include "ui/message.h"
|
||||
#include "ui/splitter.h"
|
||||
#include "ui/system.h"
|
||||
@ -91,6 +94,7 @@ MainWindow::MainWindow()
|
||||
, m_devConsoleView(nullptr)
|
||||
#endif
|
||||
{
|
||||
enableFlags(ALLOW_DROP);
|
||||
}
|
||||
|
||||
// This 'initialize' function is a way to split the creation of the
|
||||
@ -434,6 +438,22 @@ void MainWindow::onActiveViewChange()
|
||||
UIContext::instance()->setActiveView(nullptr);
|
||||
}
|
||||
|
||||
void MainWindow::onDrop(ui::DragEvent& e)
|
||||
{
|
||||
if (e.hasImage() && !e.hasPaths()) {
|
||||
auto* cmd = Commands::instance()->byId(CommandId::NewFile());
|
||||
Params params;
|
||||
params.set("fromDraggedData", "true");
|
||||
UIContext::instance()->setDraggedData(std::make_unique<DraggedData>(e.getImage()));
|
||||
UIContext::instance()->executeCommand(cmd, params);
|
||||
e.handled(true);
|
||||
invalidate();
|
||||
flushRedraw();
|
||||
os::Event ev;
|
||||
os::System::instance()->eventQueue()->queueEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
bool MainWindow::isTabModified(Tabs* tabs, TabView* tabView)
|
||||
{
|
||||
if (DocView* docView = dynamic_cast<DocView*>(tabView)) {
|
||||
|
@ -119,6 +119,8 @@ namespace app {
|
||||
void onActiveViewChange();
|
||||
void onLanguageChange();
|
||||
|
||||
void onDrop(ui::DragEvent& e) override;
|
||||
|
||||
private:
|
||||
DocView* getDocView();
|
||||
HomeView* getHomeView();
|
||||
|
@ -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"
|
||||
@ -51,9 +52,12 @@
|
||||
#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"
|
||||
#include "os/event.h"
|
||||
#include "os/event_queue.h"
|
||||
#include "os/surface.h"
|
||||
#include "os/system.h"
|
||||
#include "text/font.h"
|
||||
@ -257,7 +261,7 @@ Timeline::Timeline(TooltipManager* tooltipManager)
|
||||
, m_fromTimeline(false)
|
||||
, m_aniControls(tooltipManager)
|
||||
{
|
||||
enableFlags(CTRL_RIGHT_CLICK);
|
||||
enableFlags(CTRL_RIGHT_CLICK | ALLOW_DROP);
|
||||
|
||||
m_ctxConn1 = m_context->BeforeCommandExecution.connect(
|
||||
&Timeline::onBeforeCommandExecution, this);
|
||||
@ -3538,7 +3542,7 @@ Timeline::Hit Timeline::hitTest(ui::Message* msg, const gfx::Point& mousePos)
|
||||
else
|
||||
hit.part = PART_NOTHING;
|
||||
|
||||
if (!hasCapture()) {
|
||||
if (!hasCapture() && msg) {
|
||||
gfx::Rect outline = getPartBounds(Hit(PART_RANGE_OUTLINE));
|
||||
if (outline.contains(mousePos)) {
|
||||
auto mouseMsg = dynamic_cast<MouseMessage*>(msg);
|
||||
@ -4179,8 +4183,11 @@ void Timeline::updateDropRange(const gfx::Point& pt)
|
||||
case Range::kLayers:
|
||||
m_dropRange.clearRange();
|
||||
if (!m_rows.empty()) {
|
||||
m_dropRange.startRange(m_rows[m_hot.layer].layer(), m_hot.frame, m_range.type());
|
||||
m_dropRange.endRange(m_rows[m_hot.layer].layer(), m_hot.frame);
|
||||
auto* layer = (m_hot.layer >= 0 && m_hot.layer < m_rows.size()
|
||||
? m_rows[m_hot.layer].layer()
|
||||
: nullptr);
|
||||
m_dropRange.startRange(layer, m_hot.frame, m_range.type());
|
||||
m_dropRange.endRange(layer, m_hot.frame);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -4460,6 +4467,125 @@ void Timeline::onCancel(Context* ctx)
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void Timeline::onDragEnter(ui::DragEvent& e)
|
||||
{
|
||||
m_state = STATE_MOVING_RANGE;
|
||||
}
|
||||
|
||||
void Timeline::onDragLeave(ui::DragEvent& e)
|
||||
{
|
||||
m_state = STATE_STANDBY;
|
||||
m_range.clearRange();
|
||||
m_dropRange.clearRange();
|
||||
invalidate();
|
||||
flushRedraw();
|
||||
os::Event ev;
|
||||
os::System::instance()->eventQueue()->queueEvent(ev);
|
||||
}
|
||||
|
||||
void Timeline::onDrag(ui::DragEvent& e)
|
||||
{
|
||||
Widget::onDrag(e);
|
||||
|
||||
m_range.clearRange();
|
||||
setHot(hitTest(nullptr, e.position()));
|
||||
switch (m_hot.part) {
|
||||
case PART_ROW:
|
||||
case PART_ROW_EYE_ICON:
|
||||
case PART_ROW_CONTINUOUS_ICON:
|
||||
case PART_ROW_PADLOCK_ICON:
|
||||
case PART_ROW_TEXT: {
|
||||
m_range.startRange(nullptr, -1, Range::kLayers);
|
||||
break;
|
||||
}
|
||||
case PART_CEL:
|
||||
m_range.startRange(m_rows[m_hot.layer].layer(), m_hot.frame, Range::kCels);
|
||||
m_range.endRange(m_rows[m_hot.layer].layer(), m_hot.frame);
|
||||
m_clk = m_hot;
|
||||
invalidate();
|
||||
break;
|
||||
case PART_HEADER_FRAME:
|
||||
m_range.startRange(nullptr, -1, Range::kFrames);
|
||||
break;
|
||||
}
|
||||
|
||||
updateDropRange(e.position());
|
||||
flushRedraw();
|
||||
os::Event ev;
|
||||
os::System::instance()->eventQueue()->queueEvent(ev);
|
||||
}
|
||||
|
||||
void Timeline::onDrop(ui::DragEvent& e)
|
||||
{
|
||||
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);
|
||||
InsertionPoint insert = InsertionPoint::BeforeLayer;
|
||||
DroppedOn droppedOn = DroppedOn::Unspecified;
|
||||
switch(m_dropRange.type()) {
|
||||
case Range::kCels:
|
||||
frame = m_hot.frame;
|
||||
layerIndex = m_hot.layer;
|
||||
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 ? InsertionPoint::AfterLayer
|
||||
: InsertionPoint::BeforeLayer);
|
||||
break;
|
||||
}
|
||||
|
||||
#if _DEBUG
|
||||
LOG(LogLevel::VERBOSE, "Dropped at frame: %d, and layerIndex: %d\n", frame, layerIndex);
|
||||
#endif
|
||||
|
||||
if (e.hasPaths() || e.hasImage()) {
|
||||
bool droppedImage = e.hasImage() && !e.hasPaths();
|
||||
base::paths paths = e.getPaths();
|
||||
auto surface = e.getImage();
|
||||
|
||||
execute_from_ui_thread([=]{
|
||||
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();
|
||||
});
|
||||
e.handled(true);
|
||||
}
|
||||
|
||||
m_state = STATE_STANDBY;
|
||||
m_range.clearRange();
|
||||
m_dropRange.clearRange();
|
||||
invalidate();
|
||||
flushRedraw();
|
||||
os::Event ev;
|
||||
os::System::instance()->eventQueue()->queueEvent(ev);
|
||||
}
|
||||
|
||||
int Timeline::tagFramesDuration(const Tag* tag) const
|
||||
{
|
||||
ASSERT(m_sprite);
|
||||
|
@ -46,6 +46,7 @@ namespace doc {
|
||||
namespace ui {
|
||||
class Graphics;
|
||||
class TooltipManager;
|
||||
class DragEvent;
|
||||
}
|
||||
|
||||
namespace app {
|
||||
@ -195,6 +196,11 @@ namespace app {
|
||||
bool onClear(Context* ctx) override;
|
||||
void onCancel(Context* ctx) override;
|
||||
|
||||
void onDragEnter(ui::DragEvent& e) override;
|
||||
void onDragLeave(ui::DragEvent& e) override;
|
||||
void onDrag(ui::DragEvent& e) override;
|
||||
void onDrop(ui::DragEvent& e) override;
|
||||
|
||||
private:
|
||||
struct DrawCelData;
|
||||
|
||||
|
74
src/app/util/conversion_to_image.cpp
Normal file
74
src/app/util/conversion_to_image.cpp
Normal file
@ -0,0 +1,74 @@
|
||||
// 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/util/conversion_to_image.h"
|
||||
|
||||
#include "doc/image_traits.h"
|
||||
#include "doc/pixel_format.h"
|
||||
#include "os/surface.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace app {
|
||||
|
||||
using namespace doc;
|
||||
|
||||
uint32_t convert_color_to_image(
|
||||
gfx::Color c,
|
||||
const os::SurfaceFormatData* fd)
|
||||
{
|
||||
uint8_t r = ((c & fd->redMask) >> fd->redShift);
|
||||
uint8_t g = ((c & fd->greenMask) >> fd->greenShift);
|
||||
uint8_t b = ((c & fd->blueMask) >> fd->blueShift);
|
||||
uint8_t a = ((c & fd->alphaMask) >> fd->alphaShift);
|
||||
|
||||
if (fd->pixelAlpha == os::PixelAlpha::kPremultiplied) {
|
||||
r = r * 255 / a;
|
||||
g = g * 255 / a;
|
||||
b = b * 255 / a;
|
||||
}
|
||||
|
||||
return rgba(r, g, b, a);
|
||||
}
|
||||
|
||||
// TODO: This implementation has a lot of room for improvement, I made the bare
|
||||
// minimum to make it work. Right now it only supports converting RGBA surfaces,
|
||||
// other kind of surfaces won't be converted to an image as expected.
|
||||
void convert_surface_to_image(
|
||||
const os::Surface* surface,
|
||||
int src_x, int src_y,
|
||||
int w, int h,
|
||||
ImageRef& image)
|
||||
{
|
||||
gfx::Rect srcBounds(src_x, src_y, w, h);
|
||||
srcBounds = srcBounds.createIntersection(surface->getClipBounds());
|
||||
if (srcBounds.isEmpty())
|
||||
return;
|
||||
|
||||
src_x = srcBounds.x;
|
||||
src_y = srcBounds.y;
|
||||
w = srcBounds.w;
|
||||
h = srcBounds.h;
|
||||
|
||||
image.reset(Image::create(PixelFormat::IMAGE_RGB, w, h));
|
||||
|
||||
os::SurfaceFormatData fd;
|
||||
surface->getFormat(&fd);
|
||||
|
||||
for (int v=0; v<h; ++v) {
|
||||
for (int u=0; u<w; ++u) {
|
||||
uint32_t* c = (uint32_t*)(surface->getData(u, v));
|
||||
image->putPixel(src_x + u,
|
||||
src_y + v, convert_color_to_image(*c, &fd));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace app
|
31
src/app/util/conversion_to_image.h
Normal file
31
src/app/util/conversion_to_image.h
Normal file
@ -0,0 +1,31 @@
|
||||
// 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_UTIL_CONVERSION_TO_IMAGE_H_INCLUDED
|
||||
#define APP_UTIL_CONVERSION_TO_IMAGE_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "doc/image_ref.h"
|
||||
|
||||
namespace os {
|
||||
class Surface;
|
||||
}
|
||||
|
||||
namespace doc {
|
||||
class Palette;
|
||||
}
|
||||
|
||||
namespace app {
|
||||
|
||||
void convert_surface_to_image(
|
||||
const os::Surface* surface,
|
||||
int src_x, int src_y,
|
||||
int w, int h,
|
||||
doc::ImageRef& image);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
90
src/app/util/decode_webp.cpp
Normal file
90
src/app/util/decode_webp.cpp
Normal file
@ -0,0 +1,90 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2024 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#include "app/util/decode_webp.h"
|
||||
|
||||
#include "os/system.h"
|
||||
|
||||
#include <webp/demux.h>
|
||||
#include <webp/mux.h>
|
||||
|
||||
namespace app {
|
||||
namespace util {
|
||||
|
||||
static WEBP_CSP_MODE colorMode()
|
||||
{
|
||||
auto surface = os::instance()->makeSurface(1, 1);
|
||||
os::SurfaceFormatData fd;
|
||||
surface->getFormat(&fd);
|
||||
return (fd.redShift == 0 ? MODE_RGBA : MODE_BGRA);
|
||||
}
|
||||
|
||||
os::SurfaceRef decode_webp(const uint8_t* buf, uint32_t len)
|
||||
{
|
||||
// I've considered using the FileFormatsManager here but we don't have a file
|
||||
// in this case just the bytes. At some point maye we should refactor this or
|
||||
// the WepFormat class to avoid duplicated logic.
|
||||
WebPData webp_data;
|
||||
WebPDataInit(&webp_data);
|
||||
webp_data.bytes = &buf[0];
|
||||
webp_data.size = len;
|
||||
|
||||
WebPAnimDecoderOptions dec_options;
|
||||
WebPAnimDecoderOptionsInit(&dec_options);
|
||||
dec_options.color_mode = colorMode();
|
||||
|
||||
WebPAnimDecoder* dec = WebPAnimDecoderNew(&webp_data, &dec_options);
|
||||
if (dec == nullptr) {
|
||||
// Error parsing WebP image
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WebPAnimInfo anim_info;
|
||||
if (!WebPAnimDecoderGetInfo(dec, &anim_info)) {
|
||||
// Error getting global info about the WebP animation
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WebPDecoderConfig config;
|
||||
WebPInitDecoderConfig(&config);
|
||||
auto status = WebPGetFeatures(webp_data.bytes, webp_data.size, &config.input);
|
||||
if (status != VP8_STATUS_OK) {
|
||||
// Error getting WebP features.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const int w = anim_info.canvas_width;
|
||||
const int h = anim_info.canvas_height;
|
||||
|
||||
if (anim_info.frame_count <= 0)
|
||||
return nullptr;
|
||||
|
||||
auto surface = os::instance()->makeSurface(w, h);
|
||||
|
||||
// We just want the first frame, so we don't iterate.
|
||||
WebPAnimDecoderHasMoreFrames(dec);
|
||||
|
||||
uint8_t* frame_rgba;
|
||||
int frame_timestamp = 0;
|
||||
if (!WebPAnimDecoderGetNext(dec, &frame_rgba, &frame_timestamp)) {
|
||||
// Error loading WebP frame
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint32_t* src = (const uint32_t*)frame_rgba;
|
||||
for (int y = 0; y < h; ++y, src += w) {
|
||||
memcpy(surface->getData(0, y), src, w * sizeof(uint32_t));
|
||||
}
|
||||
|
||||
WebPAnimDecoderReset(dec);
|
||||
|
||||
WebPAnimDecoderDelete(dec);
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace app
|
25
src/app/util/decode_webp.h
Normal file
25
src/app/util/decode_webp.h
Normal file
@ -0,0 +1,25 @@
|
||||
// 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_UTIL_DECODE_WEBP_H_INCLUDED
|
||||
#define APP_UTIL_DECODE_WEBP_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "os/surface.h"
|
||||
|
||||
namespace app {
|
||||
|
||||
namespace util {
|
||||
|
||||
// Decodes webp content passed in buf and returns a surface with just the first
|
||||
// frame.
|
||||
os::SurfaceRef decode_webp(const uint8_t* buf, uint32_t len);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
@ -6,11 +6,14 @@
|
||||
|
||||
#include "app/util/layer_utils.h"
|
||||
|
||||
#include "app/doc.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/ui/editor/editor.h"
|
||||
#include "app/ui/status_bar.h"
|
||||
#include "doc/layer.h"
|
||||
#include "doc/layer_tilemap.h"
|
||||
#include "doc/sprite.h"
|
||||
#include "doc/tilesets.h"
|
||||
#include "fmt/format.h"
|
||||
|
||||
namespace app {
|
||||
@ -80,4 +83,39 @@ std::string get_layer_path(const Layer* layer)
|
||||
return path;
|
||||
}
|
||||
|
||||
Layer* copy_layer(doc::Layer* layer)
|
||||
{
|
||||
return copy_layer_with_sprite(layer, layer->sprite());
|
||||
}
|
||||
|
||||
Layer* copy_layer_with_sprite(doc::Layer* layer, doc::Sprite* sprite)
|
||||
{
|
||||
std::unique_ptr<doc::Layer> clone;
|
||||
if (layer->isTilemap()) {
|
||||
auto* srcTilemap = static_cast<LayerTilemap*>(layer);
|
||||
tileset_index tilesetIndex = srcTilemap->tilesetIndex();
|
||||
// If the caller is trying to make a copy of a tilemap layer specifying a
|
||||
// different sprite as its owner, then we must copy the tilesets of the
|
||||
// given tilemap layer into the new owner.
|
||||
if (sprite != srcTilemap->sprite()) {
|
||||
auto* srcTilesetCopy = Tileset::MakeCopyCopyingImages(srcTilemap->tileset());
|
||||
tilesetIndex = sprite->tilesets()->add(srcTilesetCopy);
|
||||
}
|
||||
|
||||
clone.reset(new LayerTilemap(sprite, tilesetIndex));
|
||||
}
|
||||
else if (layer->isImage())
|
||||
clone.reset(new LayerImage(sprite));
|
||||
else if (layer->isGroup())
|
||||
clone.reset(new LayerGroup(sprite));
|
||||
else
|
||||
throw std::runtime_error("Invalid layer type");
|
||||
|
||||
if (auto* doc = dynamic_cast<app::Doc*>(sprite->document())) {
|
||||
doc->copyLayerContent(layer, doc, clone.get());
|
||||
}
|
||||
|
||||
return clone.release();
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
namespace doc {
|
||||
class Layer;
|
||||
class Sprite;
|
||||
}
|
||||
|
||||
namespace app {
|
||||
@ -31,6 +32,9 @@ namespace app {
|
||||
|
||||
std::string get_layer_path(const doc::Layer* layer);
|
||||
|
||||
doc::Layer* copy_layer(doc::Layer* layer);
|
||||
doc::Layer* copy_layer_with_sprite(doc::Layer* layer, doc::Sprite* sprite);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
||||
|
59
src/app/util/open_file_job.h
Normal file
59
src/app/util/open_file_job.h
Normal file
@ -0,0 +1,59 @@
|
||||
// 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_OPEN_FILE_JOB_H_INCLUDED
|
||||
#define APP_OPEN_FILE_JOB_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "app/file/file.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/job.h"
|
||||
|
||||
namespace app {
|
||||
|
||||
class OpenFileJob : public Job, public IFileOpProgress {
|
||||
public:
|
||||
OpenFileJob(FileOp* fop, const bool showProgress)
|
||||
: Job(Strings::open_file_loading(), showProgress)
|
||||
, m_fop(fop)
|
||||
{
|
||||
}
|
||||
|
||||
void showProgressWindow() {
|
||||
startJob();
|
||||
|
||||
if (isCanceled())
|
||||
m_fop->stop();
|
||||
|
||||
waitJob();
|
||||
}
|
||||
|
||||
private:
|
||||
// Thread to do the hard work: load the file from the disk.
|
||||
virtual void onJob() override {
|
||||
try {
|
||||
m_fop->operate(this);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
m_fop->setError("Error loading file:\n%s", e.what());
|
||||
}
|
||||
|
||||
if (m_fop->isStop() && m_fop->document())
|
||||
delete m_fop->releaseDocument();
|
||||
|
||||
m_fop->done();
|
||||
}
|
||||
|
||||
virtual void ackFileOpProgress(double progress) override {
|
||||
jobProgress(progress);
|
||||
}
|
||||
|
||||
FileOp* m_fop;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
@ -14,8 +14,10 @@
|
||||
#include "doc/cel.h"
|
||||
#include "doc/grid.h"
|
||||
#include "doc/image.h"
|
||||
#include "doc/layer_tilemap.h"
|
||||
#include "doc/primitives.h"
|
||||
#include "doc/sprite.h"
|
||||
#include "doc/tilesets.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
@ -251,10 +253,11 @@ int LayerImage::getMemSize() const
|
||||
|
||||
for (; it != end; ++it) {
|
||||
const Cel* cel = *it;
|
||||
size += cel->getMemSize();
|
||||
|
||||
const Image* image = cel->image();
|
||||
size += image->getMemSize();
|
||||
if (cel->link()) // Skip link
|
||||
continue;
|
||||
|
||||
size += cel->getMemSize();
|
||||
}
|
||||
|
||||
return size;
|
||||
@ -588,6 +591,17 @@ void LayerGroup::insertLayer(Layer* layer, Layer* after)
|
||||
layer->setParent(this);
|
||||
}
|
||||
|
||||
void LayerGroup::insertLayerBefore(Layer* layer, Layer* before)
|
||||
{
|
||||
auto before_it = m_layers.end();
|
||||
if (before) {
|
||||
before_it = std::find(m_layers.begin(), m_layers.end(), before);
|
||||
}
|
||||
m_layers.insert(before_it, layer);
|
||||
|
||||
layer->setParent(this);
|
||||
}
|
||||
|
||||
void LayerGroup::stackLayer(Layer* layer, Layer* after)
|
||||
{
|
||||
ASSERT(layer != after);
|
||||
|
@ -144,7 +144,7 @@ namespace doc {
|
||||
|
||||
BlendMode m_blendmode;
|
||||
int m_opacity;
|
||||
|
||||
|
||||
// Disable assigment
|
||||
Layer& operator=(const Layer& other);
|
||||
};
|
||||
@ -183,7 +183,7 @@ namespace doc {
|
||||
|
||||
private:
|
||||
void destroyAllCels();
|
||||
|
||||
|
||||
CelList m_cels; // List of all cels inside this layer used by frames.
|
||||
};
|
||||
|
||||
@ -203,6 +203,7 @@ namespace doc {
|
||||
void addLayer(Layer* layer);
|
||||
void removeLayer(Layer* layer);
|
||||
void insertLayer(Layer* layer, Layer* after);
|
||||
void insertLayerBefore(Layer* layer, Layer* before);
|
||||
void stackLayer(Layer* layer, Layer* after);
|
||||
|
||||
Layer* firstLayer() const { return (m_layers.empty() ? nullptr: m_layers.front()); }
|
||||
|
@ -29,8 +29,9 @@ namespace ui {
|
||||
DOUBLE_BUFFERED = 0x00002000, // The widget is painted in a back-buffer and then flipped to the main display
|
||||
TRANSPARENT = 0x00004000, // The widget has transparent parts that needs the background painted before
|
||||
CTRL_RIGHT_CLICK = 0x00008000, // The widget should transform Ctrl+click to right-click on OS X.
|
||||
ALLOW_DROP = 0x40000000, // The widget can participate as a drop target in a drag & drop operation.
|
||||
IGNORE_MOUSE = 0x80000000, // Don't process mouse messages for this widget (useful for labels, boxes, grids, etc.)
|
||||
PROPERTIES_MASK = 0x8000ffff,
|
||||
PROPERTIES_MASK = 0xC000ffff,
|
||||
|
||||
HORIZONTAL = 0x00010000,
|
||||
VERTICAL = 0x00020000,
|
||||
@ -43,7 +44,7 @@ namespace ui {
|
||||
HOMOGENEOUS = 0x01000000,
|
||||
WORDWRAP = 0x02000000,
|
||||
CHARWRAP = 0x04000000,
|
||||
ALIGN_MASK = 0x7fff0000,
|
||||
ALIGN_MASK = 0x3fff0000,
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
66
src/ui/drag_event.h
Normal file
66
src/ui/drag_event.h
Normal file
@ -0,0 +1,66 @@
|
||||
// Aseprite UI Library
|
||||
// Copyright (C) 2024 Igara Studio S.A.
|
||||
//
|
||||
// This file is released under the terms of the MIT license.
|
||||
// Read LICENSE.txt for more information.
|
||||
|
||||
#ifndef UI_DRAG_EVENT_H_INCLUDED
|
||||
#define UI_DRAG_EVENT_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "base/paths.h"
|
||||
#include "os/dnd.h"
|
||||
#include "os/surface.h"
|
||||
#include "ui/event.h"
|
||||
#include "ui/widget.h"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class DragEvent : public Event {
|
||||
public:
|
||||
DragEvent(Component* source, ui::Widget* target, os::DragEvent& ev)
|
||||
: Event(source)
|
||||
, m_position(ev.position() - target->bounds().origin())
|
||||
, m_ev(ev) {}
|
||||
|
||||
bool handled() const { return m_handled; }
|
||||
void handled(bool value) { m_handled = value; }
|
||||
|
||||
// Operations allowed by the source of the drag & drop operation. Can be a
|
||||
// bitwise combination of values.
|
||||
os::DropOperation allowedOperations() const { return m_ev.supportedOperations(); }
|
||||
|
||||
// Operation supported by the target of the drag & drop operation. Cannot
|
||||
// be a bitwise combination of values.
|
||||
os::DropOperation supportsOperation() const { return m_ev.dropResult(); }
|
||||
// Set the operation supported by the target of the drag & drop operation.
|
||||
// Cannot be a bitwise combination of values.
|
||||
void supportsOperation(os::DropOperation operation) { m_ev.dropResult(operation); }
|
||||
|
||||
const gfx::Point& position() const { return m_position; }
|
||||
|
||||
bool hasPaths() const {
|
||||
return m_ev.dataProvider()->contains(os::DragDataItemType::Paths);
|
||||
}
|
||||
|
||||
bool hasImage() const {
|
||||
return m_ev.dataProvider()->contains(os::DragDataItemType::Image);
|
||||
}
|
||||
|
||||
base::paths getPaths() const {
|
||||
return m_ev.dataProvider()->getPaths();
|
||||
}
|
||||
|
||||
os::SurfaceRef getImage() const {
|
||||
return m_ev.dataProvider()->getImage();
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_handled = false;
|
||||
gfx::Point m_position;
|
||||
os::DragEvent& m_ev;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif // UI_DRAG_EVENT_H_INCLUDED
|
@ -31,6 +31,8 @@
|
||||
#include "os/system.h"
|
||||
#include "os/window.h"
|
||||
#include "os/window_spec.h"
|
||||
#include "ui/base.h"
|
||||
#include "ui/drag_event.h"
|
||||
#include "ui/intern.h"
|
||||
#include "ui/ui.h"
|
||||
|
||||
@ -203,8 +205,10 @@ Manager::Manager(const os::WindowRef& nativeWindow)
|
||||
, m_mouseButton(kButtonNone)
|
||||
{
|
||||
// The native window can be nullptr when running tests
|
||||
if (nativeWindow)
|
||||
if (nativeWindow) {
|
||||
nativeWindow->setUserData(&m_display);
|
||||
nativeWindow->setDragTarget(this);
|
||||
}
|
||||
|
||||
ASSERT(manager_thread == std::thread::id());
|
||||
manager_thread = std::this_thread::get_id();
|
||||
@ -2006,6 +2010,97 @@ bool Manager::sendMessageToWidget(Message* msg, Widget* widget)
|
||||
return used;
|
||||
}
|
||||
|
||||
Widget* Manager::findForDragAndDrop(Widget* widget)
|
||||
{
|
||||
// If widget doesn't support drag & drop, try to find the nearest ancestor
|
||||
// that supports it.
|
||||
while(widget && !widget->hasFlags(ALLOW_DROP))
|
||||
widget = widget->parent();
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
void Manager::dragEnter(os::DragEvent& ev)
|
||||
{
|
||||
Widget* widget = findForDragAndDrop(pick(ev.position()));
|
||||
|
||||
ASSERT(!widget || widget && widget->hasFlags(ALLOW_DROP));
|
||||
|
||||
if (widget) {
|
||||
m_dragOverWidget = widget;
|
||||
DragEvent uiev(this, widget, ev);
|
||||
widget->onDragEnter(uiev);
|
||||
ev.dropResult(uiev.supportsOperation());
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::dragLeave(os::DragEvent& ev)
|
||||
{
|
||||
Widget* widget = m_dragOverWidget;
|
||||
if (widget) {
|
||||
DragEvent uiev(this, widget, ev);
|
||||
widget->onDragLeave(uiev);
|
||||
m_dragOverWidget = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::drag(os::DragEvent& ev)
|
||||
{
|
||||
Widget* widget = findForDragAndDrop(pick(ev.position()));
|
||||
|
||||
ASSERT(!widget || widget && widget->hasFlags(ALLOW_DROP));
|
||||
|
||||
if (m_dragOverWidget && m_dragOverWidget != widget) {
|
||||
DragEvent uiev(this, m_dragOverWidget, ev);
|
||||
m_dragOverWidget->onDragLeave(uiev);
|
||||
m_dragOverWidget = nullptr;
|
||||
}
|
||||
|
||||
if (widget) {
|
||||
DragEvent uiev(this, widget, ev);
|
||||
if (m_dragOverWidget != widget) {
|
||||
m_dragOverWidget = widget;
|
||||
widget->onDragEnter(uiev);
|
||||
}
|
||||
widget->onDrag(uiev);
|
||||
ev.dropResult(uiev.supportsOperation());
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::drop(os::DragEvent& ev)
|
||||
{
|
||||
m_dragOverWidget = nullptr;
|
||||
Widget* widget = findForDragAndDrop(pick(ev.position()));
|
||||
|
||||
ASSERT(!widget || widget && widget->hasFlags(ALLOW_DROP));
|
||||
|
||||
DragEvent uiev(this, widget, ev);
|
||||
while (widget) {
|
||||
widget->onDrop(uiev);
|
||||
if (uiev.handled()) {
|
||||
ev.acceptDrop(true);
|
||||
return;
|
||||
}
|
||||
// Propagate unhandled drop events to ancestors.
|
||||
// TODO: Should we propagate dragEnter, dragLeave and drag events too?
|
||||
widget = findForDragAndDrop(widget->parent());
|
||||
}
|
||||
|
||||
// There were no widget that accepted the drop, then see if we can treat it
|
||||
// like a DropFiles event.
|
||||
if (ev.dataProvider()->contains(os::DragDataItemType::Paths)) {
|
||||
ev.acceptDrop(true);
|
||||
// We must queue an os::Event to wakeup the underlying system queue on
|
||||
// masOS. If we had used the enqueueMessage() method instead, it could
|
||||
// happen that the program might look unresponsive because it is waiting
|
||||
// for an OS event.
|
||||
os::Event dropFilesEv;
|
||||
dropFilesEv.setType(os::Event::DropFiles);
|
||||
dropFilesEv.setFiles(ev.dataProvider()->getPaths());
|
||||
os::System::instance()->eventQueue()->queueEvent(dropFilesEv);
|
||||
}
|
||||
}
|
||||
|
||||
// It's like Widget::onInvalidateRegion() but optimized for the
|
||||
// Manager (as we know that all children in a Manager will be windows,
|
||||
// we can use this knowledge to avoid some calculations).
|
||||
|
@ -10,6 +10,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "gfx/region.h"
|
||||
#include "os/dnd.h"
|
||||
#include "ui/display.h"
|
||||
#include "ui/keys.h"
|
||||
#include "ui/message_type.h"
|
||||
@ -28,7 +29,8 @@ namespace ui {
|
||||
class Timer;
|
||||
class Window;
|
||||
|
||||
class Manager : public Widget {
|
||||
class Manager : public Widget
|
||||
, public os::DragTarget {
|
||||
public:
|
||||
static Manager* getDefault() { return m_defaultManager; }
|
||||
static bool widgetAssociatedToManager(Widget* widget);
|
||||
@ -161,6 +163,12 @@ namespace ui {
|
||||
int pumpQueue();
|
||||
bool sendMessageToWidget(Message* msg, Widget* widget);
|
||||
|
||||
Widget* findForDragAndDrop(Widget* widget);
|
||||
void dragEnter(os::DragEvent& ev) override;
|
||||
void dragLeave(os::DragEvent& ev) override;
|
||||
void drag(os::DragEvent& ev) override;
|
||||
void drop(os::DragEvent& ev) override;
|
||||
|
||||
static Widget* findLowestCommonAncestor(Widget* a, Widget* b);
|
||||
static bool someParentIsFocusStop(Widget* widget);
|
||||
static Widget* findMagneticWidget(Widget* widget);
|
||||
@ -189,6 +197,9 @@ namespace ui {
|
||||
|
||||
// Last pressed mouse button.
|
||||
MouseButton m_mouseButton;
|
||||
|
||||
// Widget over which the drag is being hovered in a drag & drop operation.
|
||||
Widget* m_dragOverWidget = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include "ui/cursor.h"
|
||||
#include "ui/cursor_type.h"
|
||||
#include "ui/display.h"
|
||||
#include "ui/drag_event.h"
|
||||
#include "ui/entry.h"
|
||||
#include "ui/event.h"
|
||||
#include "ui/fit_bounds.h"
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include "text/font.h"
|
||||
#include "text/font_mgr.h"
|
||||
#include "ui/app_state.h"
|
||||
#include "ui/drag_event.h"
|
||||
#include "ui/init_theme_event.h"
|
||||
#include "ui/intern.h"
|
||||
#include "ui/layout_io.h"
|
||||
@ -1813,6 +1814,38 @@ text::ShaperFeatures Widget::onGetTextShaperFeatures() const
|
||||
return text::ShaperFeatures();
|
||||
}
|
||||
|
||||
void Widget::onDragEnter(DragEvent& e)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
LOG(VERBOSE, "UI: [id=%s, type=%d]: onDragEnter(), position: (%d, %d)\n",
|
||||
id().c_str(), type(), e.position().x, e.position().y);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Widget::onDragLeave(DragEvent& e)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
LOG(VERBOSE, "UI: [id=%s, type=%d]: onDragLeave(), position: (%d, %d)\n",
|
||||
id().c_str(), type(), e.position().x, e.position().y);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Widget::onDrag(DragEvent& e)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
LOG(VERBOSE, "UI: [id=%s, type=%d]: onDrag(), position: (%d, %d)\n",
|
||||
id().c_str(), type(), e.position().x, e.position().y);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Widget::onDrop(DragEvent& e)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
LOG(VERBOSE, "UI: [id=%s, type=%d]: onDrop(), position: (%d, %d)\n",
|
||||
id().c_str(), type(), e.position().x, e.position().y);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Widget::offsetWidgets(int dx, int dy)
|
||||
{
|
||||
if (dx == 0 && dy == 0)
|
||||
|
@ -44,6 +44,7 @@ namespace ui {
|
||||
class Style;
|
||||
class Theme;
|
||||
class Window;
|
||||
class DragEvent;
|
||||
|
||||
class Widget : public Component {
|
||||
public:
|
||||
@ -445,6 +446,11 @@ namespace ui {
|
||||
virtual text::TextBlobRef onMakeTextBlob() const;
|
||||
virtual text::ShaperFeatures onGetTextShaperFeatures() const;
|
||||
|
||||
virtual void onDragEnter(DragEvent& e);
|
||||
virtual void onDragLeave(DragEvent& e);
|
||||
virtual void onDrag(DragEvent& e);
|
||||
virtual void onDrop(DragEvent& e);
|
||||
|
||||
private:
|
||||
void removeChild(const WidgetsList::iterator& it);
|
||||
void paint(Graphics* graphics,
|
||||
@ -483,6 +489,8 @@ namespace ui {
|
||||
|
||||
gfx::Border m_border; // Border separation with the parent
|
||||
int m_childSpacing; // Separation between children
|
||||
|
||||
friend Manager;
|
||||
};
|
||||
|
||||
WidgetType register_widget_type();
|
||||
|
Loading…
x
Reference in New Issue
Block a user