mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-26 08:37:08 +00:00
Before this fix, a multi-layer mask movement/scaling (with mixed layer types: normal layer and tilemap layers with different grids) caused loss of drawing areas. The heart of this solution is to correctly align the 'selection mask' and 'transform data' according to the layer's grid, and also, forcing 'site' TilemapMode/TilesetMode before each reproduceAllTransformationsWithInnerCmds() iteration. During the life of a PixelMovement object there is a tilemap mode lock. Additionally arrow keys now work to move a selected area in TilemapMode::Tiles.
937 lines
28 KiB
C++
937 lines
28 KiB
C++
// Aseprite
|
|
// Copyright (C) 2019-2024 Igara Studio S.A.
|
|
// Copyright (C) 2001-2018 David Capello
|
|
//
|
|
// This program is distributed under the terms of
|
|
// the End-User License Agreement for Aseprite.
|
|
|
|
#define MOVPIXS_TRACE(...) // TRACE(__VA_ARGS__)
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "app/ui/editor/moving_pixels_state.h"
|
|
|
|
#include "app/app.h"
|
|
#include "app/color_utils.h"
|
|
#include "app/commands/cmd_flip.h"
|
|
#include "app/commands/cmd_move_mask.h"
|
|
#include "app/commands/cmd_rotate.h"
|
|
#include "app/commands/command.h"
|
|
#include "app/commands/commands.h"
|
|
#include "app/commands/move_thing.h"
|
|
#include "app/console.h"
|
|
#include "app/modules/gui.h"
|
|
#include "app/pref/preferences.h"
|
|
#include "app/tools/ink.h"
|
|
#include "app/tools/tool.h"
|
|
#include "app/transformation.h"
|
|
#include "app/ui/context_bar.h"
|
|
#include "app/ui/editor/editor.h"
|
|
#include "app/ui/editor/editor_customization_delegate.h"
|
|
#include "app/ui/editor/pixels_movement.h"
|
|
#include "app/ui/editor/standby_state.h"
|
|
#include "app/ui/editor/transform_handles.h"
|
|
#include "app/ui/keyboard_shortcuts.h"
|
|
#include "app/ui/main_window.h"
|
|
#include "app/ui/status_bar.h"
|
|
#include "app/ui/timeline/timeline.h"
|
|
#include "app/ui_context.h"
|
|
#include "app/util/clipboard.h"
|
|
#include "app/util/layer_utils.h"
|
|
#include "base/gcd.h"
|
|
#include "base/pi.h"
|
|
#include "doc/algorithm/flip_image.h"
|
|
#include "doc/mask.h"
|
|
#include "doc/sprite.h"
|
|
#include "fmt/format.h"
|
|
#include "gfx/rect.h"
|
|
#include "ui/manager.h"
|
|
#include "ui/message.h"
|
|
#include "ui/system.h"
|
|
#include "ui/view.h"
|
|
|
|
#include <cstring>
|
|
|
|
namespace app {
|
|
|
|
using namespace ui;
|
|
|
|
MovingPixelsState::MovingPixelsState(Editor* editor,
|
|
MouseMessage* msg,
|
|
PixelsMovementPtr pixelsMovement,
|
|
HandleType handle)
|
|
: m_pixelsMovement(pixelsMovement)
|
|
, m_delayedMouseMove(this, editor, 5)
|
|
, m_editor(editor)
|
|
, m_observingEditor(false)
|
|
, m_discarded(false)
|
|
, m_renderTimer(50)
|
|
{
|
|
m_pixelsMovement->setDelegate(this);
|
|
|
|
// MovingPixelsState needs a selection tool to avoid problems
|
|
// sharing the extra cel between the drawing cursor preview and the
|
|
// pixels movement/transformation preview.
|
|
//ASSERT(!editor->getCurrentEditorInk()->isSelection());
|
|
|
|
UIContext* context = UIContext::instance();
|
|
|
|
if (handle != NoHandle) {
|
|
gfx::Point pt = editor->screenToEditor(msg->position());
|
|
m_pixelsMovement->catchImage(gfx::PointF(pt), handle);
|
|
|
|
editor->captureMouse();
|
|
}
|
|
|
|
// Setup transparent mode/mask color
|
|
if (Preferences::instance().selection.autoOpaque()) {
|
|
Preferences::instance().selection.opaque(
|
|
editor->layer()->isBackground());
|
|
}
|
|
onTransparentColorChange();
|
|
|
|
m_renderTimer.Tick.connect([this]{ onRenderTimer(); });
|
|
|
|
// Hook BeforeCommandExecution signal so we know if the user wants
|
|
// to execute other command, so we can drop pixels.
|
|
m_ctxConn =
|
|
context->BeforeCommandExecution.connect(&MovingPixelsState::onBeforeCommandExecution, this);
|
|
|
|
// Listen to any change to the transparent color from the ContextBar.
|
|
m_opaqueConn =
|
|
Preferences::instance().selection.opaque.AfterChange.connect(
|
|
[this]{ onTransparentColorChange(); });
|
|
m_transparentConn =
|
|
Preferences::instance().selection.transparentColor.AfterChange.connect(
|
|
[this]{ onTransparentColorChange(); });
|
|
|
|
// Add the current editor as filter for key message of the manager
|
|
// so we can catch the Enter key, and avoid to execute the
|
|
// PlayAnimation command.
|
|
m_editor->manager()->addMessageFilter(kKeyDownMessage, m_editor);
|
|
m_editor->manager()->addMessageFilter(kKeyUpMessage, m_editor);
|
|
m_editor->add_observer(this);
|
|
m_observingEditor = true;
|
|
|
|
ContextBar* contextBar = App::instance()->contextBar();
|
|
contextBar->updateForMovingPixels(getTransformation(editor));
|
|
contextBar->add_observer(this);
|
|
|
|
App::instance()->mainWindow()->getTimeline()->add_observer(this);
|
|
}
|
|
|
|
MovingPixelsState::~MovingPixelsState()
|
|
{
|
|
App::instance()->mainWindow()->getTimeline()->remove_observer(this);
|
|
|
|
ContextBar* contextBar = App::instance()->contextBar();
|
|
contextBar->remove_observer(this);
|
|
contextBar->updateForActiveTool();
|
|
|
|
removePixelsMovement();
|
|
removeAsEditorObserver();
|
|
m_renderTimer.stop();
|
|
|
|
m_editor->manager()->removeMessageFilter(kKeyDownMessage, m_editor);
|
|
m_editor->manager()->removeMessageFilter(kKeyUpMessage, m_editor);
|
|
|
|
m_editor->document()->generateMaskBoundaries();
|
|
}
|
|
|
|
void MovingPixelsState::translate(const gfx::PointF& delta)
|
|
{
|
|
if (m_pixelsMovement->isDragging())
|
|
m_pixelsMovement->dropImageTemporarily();
|
|
|
|
m_pixelsMovement->catchImageAgain(gfx::PointF(0, 0), MovePixelsHandle);
|
|
m_pixelsMovement->moveImage(delta, PixelsMovement::NormalMovement);
|
|
m_pixelsMovement->dropImageTemporarily();
|
|
m_editor->updateStatusBar();
|
|
}
|
|
|
|
void MovingPixelsState::rotate(double angle)
|
|
{
|
|
m_pixelsMovement->rotate(angle);
|
|
m_editor->updateStatusBar();
|
|
}
|
|
|
|
void MovingPixelsState::flip(doc::algorithm::FlipType flipType)
|
|
{
|
|
m_pixelsMovement->flipImage(flipType);
|
|
m_editor->updateStatusBar();
|
|
}
|
|
|
|
void MovingPixelsState::shift(int dx, int dy)
|
|
{
|
|
m_pixelsMovement->shift(dx, dy);
|
|
m_editor->updateStatusBar();
|
|
}
|
|
|
|
void MovingPixelsState::updateTransformation(const Transformation& t)
|
|
{
|
|
m_pixelsMovement->setTransformation(t);
|
|
m_editor->updateStatusBar();
|
|
}
|
|
|
|
void MovingPixelsState::onEnterState(Editor* editor)
|
|
{
|
|
StandbyState::onEnterState(editor);
|
|
|
|
// Get the active key action so we can lock the FineControl action
|
|
// until we release/press again the Ctrl key just in case if we
|
|
// started moving the pixels with the Ctrl key pressed.
|
|
m_lockedKeyAction = getCurrentKeyAction();
|
|
|
|
update_screen_for_document(editor->document());
|
|
}
|
|
|
|
void MovingPixelsState::onEditorGotFocus(Editor* editor)
|
|
{
|
|
ContextBar* contextBar = App::instance()->contextBar();
|
|
// Make the DropPixelsField widget visible again in the ContextBar
|
|
// when we are back to an editor in MovingPixelsState. Without this
|
|
// we would see the SelectionModeField instead which doesn't make
|
|
// sense on MovingPixelsState).
|
|
contextBar->updateForMovingPixels(getTransformation(editor));
|
|
}
|
|
|
|
EditorState::LeaveAction MovingPixelsState::onLeaveState(Editor* editor, EditorState* newState)
|
|
{
|
|
MOVPIXS_TRACE("MOVPIXS: onLeaveState\n");
|
|
|
|
ASSERT(m_pixelsMovement);
|
|
ASSERT(editor == m_editor);
|
|
|
|
onRenderTimer();
|
|
|
|
// If we are changing to another state, we've to drop the image.
|
|
if (m_pixelsMovement->isDragging())
|
|
m_pixelsMovement->dropImageTemporarily();
|
|
|
|
// Drop pixels if we are changing to a non-temporary state (a
|
|
// temporary state is something like ScrollingState).
|
|
if (!newState || !newState->isTemporalState()) {
|
|
if (!m_discarded) {
|
|
try {
|
|
m_pixelsMovement->dropImage();
|
|
}
|
|
catch (const LockedDocException& ex) {
|
|
// This is one of the worst possible scenarios. We want to
|
|
// drop pixels because we're leaving this state (e.g. the user
|
|
// changed the current frame/layer, so we came from
|
|
// onBeforeFrameChanged) and we weren't able to drop those
|
|
// pixels.
|
|
//
|
|
// TODO this problem should be caught before we reach this
|
|
// state, or this problem should cancel the frame/layer
|
|
// change.
|
|
Console::showException(ex);
|
|
}
|
|
}
|
|
|
|
editor->document()->resetTransformation();
|
|
|
|
removePixelsMovement();
|
|
|
|
editor->releaseMouse();
|
|
|
|
// Redraw the document without the transformation handles.
|
|
editor->document()->notifyGeneralUpdate();
|
|
|
|
return DiscardState;
|
|
}
|
|
else {
|
|
editor->releaseMouse();
|
|
return KeepState;
|
|
}
|
|
}
|
|
|
|
void MovingPixelsState::onActiveToolChange(Editor* editor, tools::Tool* tool)
|
|
{
|
|
ASSERT(m_pixelsMovement);
|
|
ASSERT(editor == m_editor);
|
|
|
|
// If the user changed the tool when he/she is moving pixels,
|
|
// we have to drop the pixels only if the new tool is not selection...
|
|
if (m_pixelsMovement) {
|
|
// We don't want to drop pixels in case the user change the tool
|
|
// for scrolling/zooming/picking colors.
|
|
if ((!tool->getInk(0)->isSelection() ||
|
|
!tool->getInk(1)->isSelection()) &&
|
|
(!tool->getInk(0)->isScrollMovement() ||
|
|
!tool->getInk(1)->isScrollMovement()) &&
|
|
(!tool->getInk(0)->isZoom() ||
|
|
!tool->getInk(1)->isZoom()) &&
|
|
(!tool->getInk(0)->isEyedropper() ||
|
|
!tool->getInk(1)->isEyedropper())) {
|
|
// We have to drop pixels
|
|
dropPixels();
|
|
}
|
|
// If we've temporarily gone to a non-selection tool and now we're
|
|
// back, we've just to update the context bar to show the "moving
|
|
// pixels" controls (e.g. OK/Cancel movement buttons).
|
|
else if (tool->getInk(0)->isSelection() ||
|
|
tool->getInk(1)->isSelection()) {
|
|
ContextBar* contextBar = App::instance()->contextBar();
|
|
contextBar->updateForMovingPixels(getTransformation(editor));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool MovingPixelsState::onMouseDown(Editor* editor, MouseMessage* msg)
|
|
{
|
|
ASSERT(m_pixelsMovement);
|
|
ASSERT(editor == m_editor);
|
|
|
|
m_delayedMouseMove.onMouseDown(msg);
|
|
|
|
// Set this editor as the active one and setup the ContextBar for
|
|
// moving pixels. This is needed in case that the user is working
|
|
// with a couple of Editors, in one is moving pixels and the other
|
|
// one not.
|
|
UIContext* ctx = UIContext::instance();
|
|
ctx->setActiveView(editor->getDocView());
|
|
|
|
ContextBar* contextBar = App::instance()->contextBar();
|
|
contextBar->updateForMovingPixels(getTransformation(editor));
|
|
|
|
// Start scroll loop
|
|
if (editor->checkForScroll(msg) ||
|
|
editor->checkForZoom(msg))
|
|
return true;
|
|
|
|
// Call the eyedropper command
|
|
tools::Ink* clickedInk = editor->getCurrentEditorInk();
|
|
if (clickedInk->isEyedropper()) {
|
|
callEyedropper(editor, msg);
|
|
return true;
|
|
}
|
|
|
|
Decorator* decorator = static_cast<Decorator*>(editor->decorator());
|
|
Doc* document = editor->document();
|
|
|
|
// Transform selected pixels
|
|
if (document->isMaskVisible() &&
|
|
decorator->getTransformHandles(editor) &&
|
|
(!Preferences::instance().selection.modifiersDisableHandles() ||
|
|
msg->modifiers() == kKeyNoneModifier)) {
|
|
TransformHandles* transfHandles = decorator->getTransformHandles(editor);
|
|
|
|
// Get the handle covered by the mouse.
|
|
HandleType handle = transfHandles->getHandleAtPoint(editor,
|
|
msg->position(),
|
|
getTransformation(editor));
|
|
|
|
if (handle != NoHandle) {
|
|
if (layer_is_locked(editor))
|
|
return true;
|
|
|
|
// Re-catch the image
|
|
gfx::Point pt = editor->screenToEditor(msg->position());
|
|
m_pixelsMovement->catchImageAgain(gfx::PointF(pt), handle);
|
|
|
|
editor->captureMouse();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Start "moving pixels" loop. Here we check only for left-click as
|
|
// right-click can be used to deselect/subtract selection, so we
|
|
// should drop the selection in this later case.
|
|
if (editor->isInsideSelection() && msg->left()) {
|
|
if (layer_is_locked(editor))
|
|
return true;
|
|
|
|
// In case that the user is pressing the copy-selection keyboard shortcut.
|
|
if (auto customization = editor->getCustomizationDelegate()) {
|
|
if (int(customization->getPressedKeyAction(KeyContext::TranslatingSelection) & KeyAction::CopySelection)) {
|
|
// Stamp the pixels to create the copy.
|
|
m_pixelsMovement->stampImage();
|
|
}
|
|
|
|
// Set the locked key action again to lock the FineControl until
|
|
// with press Ctrl key again.
|
|
m_lockedKeyAction = getCurrentKeyAction();
|
|
}
|
|
|
|
// Re-catch the image
|
|
m_pixelsMovement->catchImageAgain(
|
|
editor->screenToEditorF(msg->position()), MovePixelsHandle);
|
|
|
|
editor->captureMouse();
|
|
return true;
|
|
}
|
|
// End "moving pixels" loop
|
|
else {
|
|
// Drop pixels (e.g. to start drawing)
|
|
dropPixels();
|
|
}
|
|
|
|
// Use StandbyState implementation
|
|
return StandbyState::onMouseDown(editor, msg);
|
|
}
|
|
|
|
bool MovingPixelsState::onMouseUp(Editor* editor, MouseMessage* msg)
|
|
{
|
|
ASSERT(m_pixelsMovement);
|
|
ASSERT(editor == m_editor);
|
|
|
|
m_delayedMouseMove.onMouseUp(msg);
|
|
|
|
// Drop the image temporarily in this location (where the user releases the mouse)
|
|
m_pixelsMovement->dropImageTemporarily();
|
|
|
|
// Redraw the new pivot location.
|
|
editor->invalidate();
|
|
|
|
editor->releaseMouse();
|
|
return true;
|
|
}
|
|
|
|
bool MovingPixelsState::onMouseMove(Editor* editor, MouseMessage* msg)
|
|
{
|
|
ASSERT(m_pixelsMovement);
|
|
ASSERT(editor == m_editor);
|
|
|
|
// If there is a button pressed
|
|
if (m_pixelsMovement->isDragging()) {
|
|
if (m_delayedMouseMove.onMouseMove(msg))
|
|
m_renderTimer.start();
|
|
return true;
|
|
}
|
|
|
|
// Use StandbyState implementation
|
|
return StandbyState::onMouseMove(editor, msg);
|
|
}
|
|
|
|
void MovingPixelsState::onCommitMouseMove(Editor* editor,
|
|
const gfx::PointF& spritePos)
|
|
{
|
|
ASSERT(m_pixelsMovement);
|
|
|
|
if (!m_pixelsMovement->isDragging())
|
|
return;
|
|
|
|
m_pixelsMovement->setFastMode(true);
|
|
|
|
const KeyAction action = getCurrentKeyAction();
|
|
PixelsMovement::MoveModifier moveModifier = PixelsMovement::NormalMovement;
|
|
bool snapToGrid = (Preferences::instance().selection.snapToGrid() &&
|
|
m_editor->docPref().grid.snap());
|
|
if (bool(action & KeyAction::SnapToGrid))
|
|
snapToGrid = !snapToGrid;
|
|
|
|
if (snapToGrid)
|
|
moveModifier |= PixelsMovement::SnapToGridMovement;
|
|
|
|
if (int(action & KeyAction::AngleSnap))
|
|
moveModifier |= PixelsMovement::AngleSnapMovement;
|
|
|
|
if (int(action & KeyAction::MaintainAspectRatio))
|
|
moveModifier |= PixelsMovement::MaintainAspectRatioMovement;
|
|
|
|
if (int(action & KeyAction::ScaleFromCenter))
|
|
moveModifier |= PixelsMovement::ScaleFromPivot;
|
|
|
|
if (int(action & KeyAction::LockAxis))
|
|
moveModifier |= PixelsMovement::LockAxisMovement;
|
|
|
|
// Enable the FineControl only if we've already released and pressed
|
|
// the key again (because it )
|
|
if (int(action & KeyAction::FineControl) &&
|
|
int(m_lockedKeyAction & KeyAction::FineControl) == 0) {
|
|
moveModifier |= PixelsMovement::FineControl;
|
|
}
|
|
|
|
// Invalidate handles
|
|
Decorator* decorator = static_cast<Decorator*>(m_editor->decorator());
|
|
TransformHandles* transfHandles = decorator->getTransformHandles(m_editor);
|
|
const Transformation& transformation = m_pixelsMovement->getTransformation();
|
|
transfHandles->invalidateHandles(m_editor, transformation);
|
|
|
|
// Drag the image to that position
|
|
m_pixelsMovement->moveImage(spritePos, moveModifier);
|
|
|
|
// Update context bar and status bar
|
|
ContextBar* contextBar = App::instance()->contextBar();
|
|
contextBar->updateForMovingPixels(transformation);
|
|
|
|
m_editor->updateStatusBar();
|
|
}
|
|
|
|
bool MovingPixelsState::onSetCursor(Editor* editor, const gfx::Point& mouseScreenPos)
|
|
{
|
|
ASSERT(m_pixelsMovement);
|
|
ASSERT(editor == m_editor);
|
|
|
|
// Move selection
|
|
if (m_pixelsMovement->isDragging()) {
|
|
editor->showMouseCursor(kMoveCursor);
|
|
return true;
|
|
}
|
|
|
|
// Use StandbyState implementation
|
|
return StandbyState::onSetCursor(editor, mouseScreenPos);
|
|
}
|
|
|
|
bool MovingPixelsState::onKeyDown(Editor* editor, KeyMessage* msg)
|
|
{
|
|
ASSERT(m_pixelsMovement);
|
|
if (!isActiveEditor())
|
|
return false;
|
|
ASSERT(editor == m_editor);
|
|
|
|
// Reset the locked action just to indicate that we can use the
|
|
// FineControl now (e.g. if we pressed another modifier key).
|
|
m_lockedKeyAction = KeyAction::None;
|
|
|
|
if (msg->scancode() == kKeyEnter || // TODO make this key customizable
|
|
msg->scancode() == kKeyEnterPad ||
|
|
msg->scancode() == kKeyEsc) {
|
|
dropPixels();
|
|
|
|
// The escape key drop pixels and deselect the mask.
|
|
if (msg->scancode() == kKeyEsc) { // TODO make this key customizable
|
|
Command* cmd = Commands::instance()->byId(CommandId::DeselectMask());
|
|
UIContext::instance()->executeCommandFromMenuOrShortcut(cmd);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Use StandbyState implementation
|
|
return StandbyState::onKeyDown(editor, msg);
|
|
}
|
|
|
|
bool MovingPixelsState::onKeyUp(Editor* editor, KeyMessage* msg)
|
|
{
|
|
ASSERT(m_pixelsMovement);
|
|
if (!isActiveEditor())
|
|
return false;
|
|
ASSERT(editor == m_editor);
|
|
|
|
// Reset the locked action just to indicate that we can use the
|
|
// FineControl now (e.g. if we release the Ctrl key).
|
|
m_lockedKeyAction = KeyAction::None;
|
|
|
|
// Use StandbyState implementation
|
|
return StandbyState::onKeyUp(editor, msg);
|
|
}
|
|
|
|
bool MovingPixelsState::onUpdateStatusBar(Editor* editor)
|
|
{
|
|
MOVPIXS_TRACE("MOVPIXS: onUpdateStatusBar (%p)\n", m_pixelsMovement.get());
|
|
|
|
ASSERT(m_pixelsMovement);
|
|
ASSERT(editor == m_editor);
|
|
|
|
// We've received a crash report where this is nullptr when
|
|
// MovingPixelsState::onLeaveState() generates a general update
|
|
// notification (notifyGeneralUpdate()) just after the
|
|
// m_pixelsMovement is deleted with removePixelsMovement(). The
|
|
// general update signals a scroll update in the view which will ask
|
|
// for the status bar content again (Editor::notifyScrollChanged).
|
|
//
|
|
// We weren't able to reproduce this scenario anyway (which should
|
|
// be visible with the ASSERT() above).
|
|
if (!m_pixelsMovement)
|
|
return false;
|
|
|
|
const Transformation& transform(getTransformation(editor));
|
|
gfx::Size imageSize = m_pixelsMovement->getInitialImageSize();
|
|
|
|
int w = int(transform.bounds().w);
|
|
int h = int(transform.bounds().h);
|
|
int gcd = base::gcd(w, h);
|
|
StatusBar::instance()->setStatusText(
|
|
100,
|
|
fmt::format(
|
|
":pos: {} {}"
|
|
" :size: {} {}"
|
|
" :selsize: {} {} [{:.02f}% {:.02f}%]"
|
|
" :angle: {:.1f}"
|
|
" :aspect_ratio: {}:{}",
|
|
int(transform.bounds().x),
|
|
int(transform.bounds().y),
|
|
imageSize.w,
|
|
imageSize.h,
|
|
w,
|
|
h,
|
|
(double)w*100.0/imageSize.w,
|
|
(double)h*100.0/imageSize.h,
|
|
180.0 * transform.angle() / PI,
|
|
w/gcd,
|
|
h/gcd));
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MovingPixelsState::acceptQuickTool(tools::Tool* tool)
|
|
{
|
|
return
|
|
(!m_pixelsMovement ||
|
|
tool->getInk(0)->isSelection() ||
|
|
tool->getInk(0)->isEyedropper() ||
|
|
tool->getInk(0)->isScrollMovement() ||
|
|
tool->getInk(0)->isZoom());
|
|
}
|
|
|
|
void MovingPixelsState::onBeforeLayerVisibilityChange(Editor* editor,
|
|
doc::Layer* layer,
|
|
bool newState)
|
|
{
|
|
if (!isActiveDocument())
|
|
return;
|
|
|
|
// If the layer visibility of any selected layer changes, we just
|
|
// drop the pixels (it's the easiest way to avoid modifying hidden
|
|
// pixels).
|
|
if (m_pixelsMovement) {
|
|
const Site& site = m_pixelsMovement->site();
|
|
if (site.layer() == layer ||
|
|
site.range().contains(layer)) {
|
|
dropPixels();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Before executing any command, we drop the pixels (go back to standby).
|
|
void MovingPixelsState::onBeforeCommandExecution(CommandExecutionEvent& ev)
|
|
{
|
|
Command* command = ev.command();
|
|
|
|
MOVPIXS_TRACE("MOVPIXS: onBeforeCommandExecution %s\n", command->id().c_str());
|
|
|
|
// If the command is for other editor, we don't drop pixels.
|
|
if (!isActiveEditor())
|
|
return;
|
|
|
|
if (layer_is_locked(m_editor) &&
|
|
(command->id() == CommandId::Flip() ||
|
|
command->id() == CommandId::Cut() ||
|
|
command->id() == CommandId::Clear() ||
|
|
command->id() == CommandId::Rotate())) {
|
|
ev.cancel();
|
|
return;
|
|
}
|
|
|
|
// We don't need to drop the pixels if a MoveMaskCommand of Content is executed.
|
|
if (auto moveMaskCmd = dynamic_cast<MoveMaskCommand*>(command)) {
|
|
if (moveMaskCmd->getTarget() == MoveMaskCommand::Content) {
|
|
if (layer_is_locked(m_editor)) {
|
|
ev.cancel();
|
|
return;
|
|
}
|
|
gfx::Point delta = moveMaskCmd->getMoveThing().getDelta(UIContext::instance());
|
|
// Verify Shift condition of the MoveMaskCommand (i.e. wrap = true)
|
|
if (moveMaskCmd->isWrap()) {
|
|
m_pixelsMovement->shift(delta.x, delta.y);
|
|
}
|
|
else {
|
|
translate(gfx::PointF(delta));
|
|
}
|
|
// We've processed the selection content movement right here.
|
|
ev.cancel();
|
|
return;
|
|
}
|
|
}
|
|
// Don't drop pixels if the user zooms/scrolls/picks a color
|
|
// using commands.
|
|
else if ((command->id() == CommandId::Zoom()) ||
|
|
(command->id() == CommandId::Scroll()) ||
|
|
(command->id() == CommandId::Eyedropper()) ||
|
|
// DiscardBrush is used by Eyedropper command
|
|
(command->id() == CommandId::DiscardBrush())) {
|
|
// Do not drop pixels
|
|
return;
|
|
}
|
|
// Intercept the "Cut" or "Copy" command to handle them locally
|
|
// with the current m_pixelsMovement data.
|
|
else if (command->id() == CommandId::Cut() ||
|
|
command->id() == CommandId::Copy() ||
|
|
command->id() == CommandId::Clear()) {
|
|
// Copy the floating image to the clipboard on Cut/Copy.
|
|
if (command->id() != CommandId::Clear()) {
|
|
Doc* document = m_editor->document();
|
|
std::unique_ptr<Image> floatingImage;
|
|
std::unique_ptr<Mask> floatingMask;
|
|
m_pixelsMovement->getDraggedImageCopy(floatingImage, floatingMask);
|
|
|
|
if (floatingImage->isTilemap()) {
|
|
Site site = m_editor->getSite();
|
|
ASSERT(site.tileset());
|
|
Clipboard::instance()->
|
|
copyTilemap(floatingImage.get(),
|
|
floatingMask.get(),
|
|
document->sprite()->palette(m_editor->frame()),
|
|
site.tileset());
|
|
}
|
|
else {
|
|
Clipboard::instance()->
|
|
copyImage(floatingImage.get(),
|
|
floatingMask.get(),
|
|
document->sprite()->palette(m_editor->frame()));
|
|
}
|
|
}
|
|
|
|
// Clear floating pixels on Cut/Clear.
|
|
if (command->id() != CommandId::Copy()) {
|
|
m_pixelsMovement->trim();
|
|
|
|
// Should we keep the mask after an Edit > Clear command?
|
|
auto keepMask = PixelsMovement::DontKeepMask;
|
|
if (command->id() == CommandId::Clear() &&
|
|
Preferences::instance().selection.keepSelectionAfterClear()) {
|
|
keepMask = PixelsMovement::KeepMask;
|
|
}
|
|
|
|
// Discard the dragged image.
|
|
m_pixelsMovement->discardImage(PixelsMovement::CommitChanges, keepMask);
|
|
m_discarded = true;
|
|
|
|
// Quit from MovingPixelsState, back to standby.
|
|
m_editor->backToPreviousState();
|
|
}
|
|
|
|
// Cancel the command, we've simulated it.
|
|
ev.cancel();
|
|
return;
|
|
}
|
|
// Flip Horizontally/Vertically commands are handled manually to
|
|
// avoid dropping the floating region of pixels.
|
|
else if (command->id() == CommandId::Flip()) {
|
|
if (auto flipCommand = dynamic_cast<FlipCommand*>(command)) {
|
|
this->flip(flipCommand->getFlipType());
|
|
|
|
ev.cancel();
|
|
return;
|
|
}
|
|
}
|
|
// Rotate is quite simple, we can add the angle to the current transformation.
|
|
else if (command->id() == CommandId::Rotate()) {
|
|
if (auto rotate = dynamic_cast<RotateCommand*>(command)) {
|
|
if (rotate->flipMask()) {
|
|
this->rotate(rotate->angle());
|
|
|
|
ev.cancel();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// We can use previous/next frames while transforming the selection
|
|
// to switch between frames
|
|
else if (command->id() == CommandId::GotoPreviousFrame() ||
|
|
command->id() == CommandId::GotoPreviousFrameWithSameTag()) {
|
|
if (m_pixelsMovement->gotoFrame(-1)) {
|
|
ev.cancel();
|
|
return;
|
|
}
|
|
}
|
|
else if (command->id() == CommandId::GotoNextFrame() ||
|
|
command->id() == CommandId::GotoNextFrameWithSameTag()) {
|
|
if (m_pixelsMovement->gotoFrame(+1)) {
|
|
ev.cancel();
|
|
return;
|
|
}
|
|
}
|
|
else if (command->id() == CommandId::ToggleTilesMode()) {
|
|
ev.cancel();
|
|
return;
|
|
}
|
|
|
|
if (m_pixelsMovement)
|
|
dropPixels();
|
|
}
|
|
|
|
void MovingPixelsState::onDestroyEditor(Editor* editor)
|
|
{
|
|
// TODO we should call ~MovingPixelsState(), we should delete the
|
|
// whole "m_statesHistory" when an editor is deleted.
|
|
removeAsEditorObserver();
|
|
}
|
|
|
|
void MovingPixelsState::onBeforeFrameChanged(Editor* editor)
|
|
{
|
|
if (!isActiveDocument())
|
|
return;
|
|
|
|
if (m_pixelsMovement &&
|
|
!m_pixelsMovement->canHandleFrameChange()) {
|
|
dropPixels();
|
|
}
|
|
}
|
|
|
|
void MovingPixelsState::onBeforeLayerChanged(Editor* editor)
|
|
{
|
|
if (!isActiveDocument())
|
|
return;
|
|
|
|
if (m_pixelsMovement)
|
|
dropPixels();
|
|
}
|
|
|
|
void MovingPixelsState::onBeforeRangeChanged(Timeline* timeline)
|
|
{
|
|
if (!isActiveDocument())
|
|
return;
|
|
|
|
if (m_pixelsMovement)
|
|
dropPixels();
|
|
}
|
|
|
|
void MovingPixelsState::onTransparentColorChange()
|
|
{
|
|
ASSERT(m_pixelsMovement);
|
|
|
|
bool opaque = Preferences::instance().selection.opaque();
|
|
setTransparentColor(
|
|
opaque,
|
|
opaque ?
|
|
app::Color::fromMask():
|
|
Preferences::instance().selection.transparentColor());
|
|
}
|
|
|
|
void MovingPixelsState::onRenderTimer()
|
|
{
|
|
m_pixelsMovement->setFastMode(false);
|
|
m_renderTimer.stop();
|
|
}
|
|
|
|
void MovingPixelsState::onDropPixels(ContextBarObserver::DropAction action)
|
|
{
|
|
if (!isActiveEditor())
|
|
return;
|
|
|
|
switch (action) {
|
|
|
|
case ContextBarObserver::DropPixels:
|
|
dropPixels();
|
|
break;
|
|
|
|
case ContextBarObserver::CancelDrag:
|
|
m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges);
|
|
m_discarded = true;
|
|
|
|
// Quit from MovingPixelsState, back to standby.
|
|
m_editor->backToPreviousState();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void MovingPixelsState::onPivotChange()
|
|
{
|
|
ContextBar* contextBar = App::instance()->contextBar();
|
|
contextBar->updateForMovingPixels(getTransformation(m_editor));
|
|
}
|
|
|
|
void MovingPixelsState::setTransparentColor(bool opaque, const app::Color& color)
|
|
{
|
|
ASSERT(m_pixelsMovement);
|
|
|
|
Layer* layer = m_editor->layer();
|
|
ASSERT(layer);
|
|
|
|
try {
|
|
m_pixelsMovement->setMaskColor(
|
|
opaque, color_utils::color_for_target_mask(color, ColorTarget(layer)));
|
|
}
|
|
catch (const LockedDocException& ex) {
|
|
Console::showException(ex);
|
|
}
|
|
}
|
|
|
|
void MovingPixelsState::dropPixels()
|
|
{
|
|
MOVPIXS_TRACE("MOVPIXS: dropPixels\n");
|
|
|
|
// Just change to default state (StandbyState generally). We'll
|
|
// receive an onLeaveState() event after this call.
|
|
m_editor->backToPreviousState();
|
|
}
|
|
|
|
Transformation MovingPixelsState::getTransformation(Editor* editor)
|
|
{
|
|
// m_pixelsMovement can be null in the final onMouseDown(), after we
|
|
// called dropPixels() and we're just going to the previous state.
|
|
if (m_pixelsMovement)
|
|
return m_pixelsMovement->getTransformation();
|
|
else
|
|
return StandbyState::getTransformation(editor);
|
|
}
|
|
|
|
bool MovingPixelsState::isActiveDocument() const
|
|
{
|
|
Doc* doc = UIContext::instance()->activeDocument();
|
|
return (m_editor->document() == doc);
|
|
}
|
|
|
|
bool MovingPixelsState::isActiveEditor() const
|
|
{
|
|
Editor* targetEditor = UIContext::instance()->activeEditor();
|
|
return (targetEditor == m_editor);
|
|
}
|
|
|
|
void MovingPixelsState::removeAsEditorObserver()
|
|
{
|
|
if (m_observingEditor) {
|
|
m_observingEditor = false;
|
|
m_editor->remove_observer(this);
|
|
}
|
|
}
|
|
|
|
void MovingPixelsState::removePixelsMovement()
|
|
{
|
|
// Avoid receiving a onCommitMouseMove() message when
|
|
// m_pixelsMovement is already nullptr.
|
|
m_delayedMouseMove.stopTimer();
|
|
|
|
m_pixelsMovement.reset();
|
|
m_ctxConn.disconnect();
|
|
m_opaqueConn.disconnect();
|
|
m_transparentConn.disconnect();
|
|
}
|
|
|
|
KeyAction MovingPixelsState::getCurrentKeyAction() const
|
|
{
|
|
KeyContext keyContext = KeyContext::Normal;
|
|
switch (m_pixelsMovement->handle()) {
|
|
case MovePixelsHandle:
|
|
keyContext = KeyContext::TranslatingSelection;
|
|
break;
|
|
case ScaleNWHandle:
|
|
case ScaleNHandle:
|
|
case ScaleNEHandle:
|
|
case ScaleWHandle:
|
|
case ScaleEHandle:
|
|
case ScaleSWHandle:
|
|
case ScaleSHandle:
|
|
case ScaleSEHandle:
|
|
keyContext = KeyContext::ScalingSelection;
|
|
break;
|
|
case RotateNWHandle:
|
|
case RotateNEHandle:
|
|
case RotateSWHandle:
|
|
case RotateSEHandle:
|
|
keyContext = KeyContext::RotatingSelection;
|
|
break;
|
|
case SkewNHandle:
|
|
case SkewWHandle:
|
|
case SkewEHandle:
|
|
case SkewSHandle:
|
|
keyContext = KeyContext::ScalingSelection;
|
|
break;
|
|
}
|
|
|
|
// Ask to the editor the customization delegate for the active key
|
|
// action/modifier for the pixels movement (snap to grid, angle
|
|
// snap, etc.).
|
|
if (auto customizable = m_editor->getCustomizationDelegate())
|
|
return customizable->getPressedKeyAction(keyContext);
|
|
else
|
|
return KeyAction::None;
|
|
}
|
|
|
|
} // namespace app
|