Merge branch 'import-image-as-new-layer' into beta (fix #131, #4636)

This commit is contained in:
David Capello 2024-11-11 17:26:39 -03:00
commit 10eaec8a87
36 changed files with 1232 additions and 87 deletions

View File

@ -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

View File

@ -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

View 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

View 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

View File

@ -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());
}

View File

@ -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;

View File

@ -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)

View File

@ -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;

View File

@ -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)

View File

@ -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)
{

View File

@ -34,6 +34,7 @@
#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;

View File

@ -95,6 +95,7 @@ bool FliFormat::onLoad(FileOp* fop)
Sprite* sprite = new Sprite(ImageSpec(ColorMode::INDEXED, w, h), 256);
LayerImage* layer = new LayerImage(sprite);
sprite->root()->addLayer(layer);
if (!fop->avoidBackgroundLayer())
layer->configureAsBackground();
// Set frames and speed

View File

@ -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

View File

@ -211,7 +211,7 @@ bool WebPFormat::onLoad(FileOp* fop)
}
WebPAnimDecoderReset(dec);
if (!has_alpha)
if (!has_alpha && !fop->avoidBackgroundLayer())
layer->configureAsBackground();
WebPAnimDecoderDelete(dec);

View File

@ -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"

View File

@ -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();
}
}

View File

@ -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)) {

View File

@ -119,6 +119,8 @@ namespace app {
void onActiveViewChange();
void onLanguageChange();
void onDrop(ui::DragEvent& e) override;
private:
DocView* getDocView();
HomeView* getHomeView();

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"
@ -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);

View File

@ -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;

View 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

View 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

View 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

View 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

View File

@ -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

View File

@ -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

View 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

View File

@ -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);

View File

@ -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()); }

View File

@ -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
View 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

View File

@ -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).

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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();