| 1 | // Aseprite |
| 2 | // Copyright (C) 2019-2022 Igara Studio S.A. |
| 3 | // Copyright (C) 2001-2018 David Capello |
| 4 | // |
| 5 | // This program is distributed under the terms of |
| 6 | // the End-User License Agreement for Aseprite. |
| 7 | |
| 8 | #define MOVPIXS_TRACE(...) // TRACE(__VA_ARGS__) |
| 9 | |
| 10 | #ifdef HAVE_CONFIG_H |
| 11 | #include "config.h" |
| 12 | #endif |
| 13 | |
| 14 | #include "app/ui/editor/moving_pixels_state.h" |
| 15 | |
| 16 | #include "app/app.h" |
| 17 | #include "app/color_utils.h" |
| 18 | #include "app/commands/cmd_flip.h" |
| 19 | #include "app/commands/cmd_move_mask.h" |
| 20 | #include "app/commands/cmd_rotate.h" |
| 21 | #include "app/commands/command.h" |
| 22 | #include "app/commands/commands.h" |
| 23 | #include "app/commands/move_thing.h" |
| 24 | #include "app/console.h" |
| 25 | #include "app/modules/gui.h" |
| 26 | #include "app/pref/preferences.h" |
| 27 | #include "app/tools/ink.h" |
| 28 | #include "app/tools/tool.h" |
| 29 | #include "app/transformation.h" |
| 30 | #include "app/ui/context_bar.h" |
| 31 | #include "app/ui/editor/editor.h" |
| 32 | #include "app/ui/editor/editor_customization_delegate.h" |
| 33 | #include "app/ui/editor/pixels_movement.h" |
| 34 | #include "app/ui/editor/standby_state.h" |
| 35 | #include "app/ui/editor/transform_handles.h" |
| 36 | #include "app/ui/keyboard_shortcuts.h" |
| 37 | #include "app/ui/main_window.h" |
| 38 | #include "app/ui/status_bar.h" |
| 39 | #include "app/ui/timeline/timeline.h" |
| 40 | #include "app/ui_context.h" |
| 41 | #include "app/util/clipboard.h" |
| 42 | #include "app/util/layer_utils.h" |
| 43 | #include "base/gcd.h" |
| 44 | #include "base/pi.h" |
| 45 | #include "doc/algorithm/flip_image.h" |
| 46 | #include "doc/mask.h" |
| 47 | #include "doc/sprite.h" |
| 48 | #include "fmt/format.h" |
| 49 | #include "gfx/rect.h" |
| 50 | #include "ui/manager.h" |
| 51 | #include "ui/message.h" |
| 52 | #include "ui/system.h" |
| 53 | #include "ui/view.h" |
| 54 | |
| 55 | #include <cstring> |
| 56 | |
| 57 | namespace app { |
| 58 | |
| 59 | using namespace ui; |
| 60 | |
| 61 | MovingPixelsState::MovingPixelsState(Editor* editor, |
| 62 | MouseMessage* msg, |
| 63 | PixelsMovementPtr pixelsMovement, |
| 64 | HandleType handle) |
| 65 | : m_pixelsMovement(pixelsMovement) |
| 66 | , m_delayedMouseMove(this, editor, 5) |
| 67 | , m_editor(editor) |
| 68 | , m_observingEditor(false) |
| 69 | , m_discarded(false) |
| 70 | , m_renderTimer(50) |
| 71 | { |
| 72 | m_pixelsMovement->setDelegate(this); |
| 73 | |
| 74 | // MovingPixelsState needs a selection tool to avoid problems |
| 75 | // sharing the extra cel between the drawing cursor preview and the |
| 76 | // pixels movement/transformation preview. |
| 77 | //ASSERT(!editor->getCurrentEditorInk()->isSelection()); |
| 78 | |
| 79 | UIContext* context = UIContext::instance(); |
| 80 | |
| 81 | if (handle != NoHandle) { |
| 82 | gfx::Point pt = editor->screenToEditor(msg->position()); |
| 83 | m_pixelsMovement->catchImage(gfx::PointF(pt), handle); |
| 84 | |
| 85 | editor->captureMouse(); |
| 86 | } |
| 87 | |
| 88 | // Setup transparent mode/mask color |
| 89 | if (Preferences::instance().selection.autoOpaque()) { |
| 90 | Preferences::instance().selection.opaque( |
| 91 | editor->layer()->isBackground()); |
| 92 | } |
| 93 | onTransparentColorChange(); |
| 94 | |
| 95 | m_renderTimer.Tick.connect([this]{ onRenderTimer(); }); |
| 96 | |
| 97 | // Hook BeforeCommandExecution signal so we know if the user wants |
| 98 | // to execute other command, so we can drop pixels. |
| 99 | m_ctxConn = |
| 100 | context->BeforeCommandExecution.connect(&MovingPixelsState::onBeforeCommandExecution, this); |
| 101 | |
| 102 | // Listen to any change to the transparent color from the ContextBar. |
| 103 | m_opaqueConn = |
| 104 | Preferences::instance().selection.opaque.AfterChange.connect( |
| 105 | [this]{ onTransparentColorChange(); }); |
| 106 | m_transparentConn = |
| 107 | Preferences::instance().selection.transparentColor.AfterChange.connect( |
| 108 | [this]{ onTransparentColorChange(); }); |
| 109 | |
| 110 | // Add the current editor as filter for key message of the manager |
| 111 | // so we can catch the Enter key, and avoid to execute the |
| 112 | // PlayAnimation command. |
| 113 | m_editor->manager()->addMessageFilter(kKeyDownMessage, m_editor); |
| 114 | m_editor->manager()->addMessageFilter(kKeyUpMessage, m_editor); |
| 115 | m_editor->add_observer(this); |
| 116 | m_observingEditor = true; |
| 117 | |
| 118 | ContextBar* contextBar = App::instance()->contextBar(); |
| 119 | contextBar->updateForMovingPixels(getTransformation(editor)); |
| 120 | contextBar->add_observer(this); |
| 121 | |
| 122 | App::instance()->mainWindow()->getTimeline()->add_observer(this); |
| 123 | } |
| 124 | |
| 125 | MovingPixelsState::~MovingPixelsState() |
| 126 | { |
| 127 | App::instance()->mainWindow()->getTimeline()->remove_observer(this); |
| 128 | |
| 129 | ContextBar* contextBar = App::instance()->contextBar(); |
| 130 | contextBar->remove_observer(this); |
| 131 | contextBar->updateForActiveTool(); |
| 132 | |
| 133 | removePixelsMovement(); |
| 134 | removeAsEditorObserver(); |
| 135 | m_renderTimer.stop(); |
| 136 | |
| 137 | m_editor->manager()->removeMessageFilter(kKeyDownMessage, m_editor); |
| 138 | m_editor->manager()->removeMessageFilter(kKeyUpMessage, m_editor); |
| 139 | |
| 140 | m_editor->document()->generateMaskBoundaries(); |
| 141 | } |
| 142 | |
| 143 | void MovingPixelsState::translate(const gfx::PointF& delta) |
| 144 | { |
| 145 | if (m_pixelsMovement->isDragging()) |
| 146 | m_pixelsMovement->dropImageTemporarily(); |
| 147 | |
| 148 | m_pixelsMovement->catchImageAgain(gfx::PointF(0, 0), MovePixelsHandle); |
| 149 | m_pixelsMovement->moveImage(delta, PixelsMovement::NormalMovement); |
| 150 | m_pixelsMovement->dropImageTemporarily(); |
| 151 | m_editor->updateStatusBar(); |
| 152 | } |
| 153 | |
| 154 | void MovingPixelsState::rotate(double angle) |
| 155 | { |
| 156 | m_pixelsMovement->rotate(angle); |
| 157 | m_editor->updateStatusBar(); |
| 158 | } |
| 159 | |
| 160 | void MovingPixelsState::flip(doc::algorithm::FlipType flipType) |
| 161 | { |
| 162 | m_pixelsMovement->flipImage(flipType); |
| 163 | m_editor->updateStatusBar(); |
| 164 | } |
| 165 | |
| 166 | void MovingPixelsState::shift(int dx, int dy) |
| 167 | { |
| 168 | m_pixelsMovement->shift(dx, dy); |
| 169 | m_editor->updateStatusBar(); |
| 170 | } |
| 171 | |
| 172 | void MovingPixelsState::updateTransformation(const Transformation& t) |
| 173 | { |
| 174 | m_pixelsMovement->setTransformation(t); |
| 175 | m_editor->updateStatusBar(); |
| 176 | } |
| 177 | |
| 178 | void MovingPixelsState::onEnterState(Editor* editor) |
| 179 | { |
| 180 | StandbyState::onEnterState(editor); |
| 181 | |
| 182 | update_screen_for_document(editor->document()); |
| 183 | } |
| 184 | |
| 185 | void MovingPixelsState::onEditorGotFocus(Editor* editor) |
| 186 | { |
| 187 | ContextBar* contextBar = App::instance()->contextBar(); |
| 188 | // Make the DropPixelsField widget visible again in the ContextBar |
| 189 | // when we are back to an editor in MovingPixelsState. Without this |
| 190 | // we would see the SelectionModeField instead which doesn't make |
| 191 | // sense on MovingPixelsState). |
| 192 | contextBar->updateForMovingPixels(getTransformation(editor)); |
| 193 | } |
| 194 | |
| 195 | EditorState::LeaveAction MovingPixelsState::onLeaveState(Editor* editor, EditorState* newState) |
| 196 | { |
| 197 | MOVPIXS_TRACE("MOVPIXS: onLeaveState\n" ); |
| 198 | |
| 199 | ASSERT(m_pixelsMovement); |
| 200 | ASSERT(editor == m_editor); |
| 201 | |
| 202 | onRenderTimer(); |
| 203 | |
| 204 | // If we are changing to another state, we've to drop the image. |
| 205 | if (m_pixelsMovement->isDragging()) |
| 206 | m_pixelsMovement->dropImageTemporarily(); |
| 207 | |
| 208 | // Drop pixels if we are changing to a non-temporary state (a |
| 209 | // temporary state is something like ScrollingState). |
| 210 | if (!newState || !newState->isTemporalState()) { |
| 211 | if (!m_discarded) { |
| 212 | try { |
| 213 | m_pixelsMovement->dropImage(); |
| 214 | } |
| 215 | catch (const LockedDocException& ex) { |
| 216 | // This is one of the worst possible scenarios. We want to |
| 217 | // drop pixels because we're leaving this state (e.g. the user |
| 218 | // changed the current frame/layer, so we came from |
| 219 | // onBeforeFrameChanged) and we weren't able to drop those |
| 220 | // pixels. |
| 221 | // |
| 222 | // TODO this problem should be caught before we reach this |
| 223 | // state, or this problem should cancel the frame/layer |
| 224 | // change. |
| 225 | Console::showException(ex); |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | editor->document()->resetTransformation(); |
| 230 | |
| 231 | removePixelsMovement(); |
| 232 | |
| 233 | editor->releaseMouse(); |
| 234 | |
| 235 | // Redraw the document without the transformation handles. |
| 236 | editor->document()->notifyGeneralUpdate(); |
| 237 | |
| 238 | return DiscardState; |
| 239 | } |
| 240 | else { |
| 241 | editor->releaseMouse(); |
| 242 | return KeepState; |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | void MovingPixelsState::onActiveToolChange(Editor* editor, tools::Tool* tool) |
| 247 | { |
| 248 | ASSERT(m_pixelsMovement); |
| 249 | ASSERT(editor == m_editor); |
| 250 | |
| 251 | // If the user changed the tool when he/she is moving pixels, |
| 252 | // we have to drop the pixels only if the new tool is not selection... |
| 253 | if (m_pixelsMovement) { |
| 254 | // We don't want to drop pixels in case the user change the tool |
| 255 | // for scrolling/zooming/picking colors. |
| 256 | if ((!tool->getInk(0)->isSelection() || |
| 257 | !tool->getInk(1)->isSelection()) && |
| 258 | (!tool->getInk(0)->isScrollMovement() || |
| 259 | !tool->getInk(1)->isScrollMovement()) && |
| 260 | (!tool->getInk(0)->isZoom() || |
| 261 | !tool->getInk(1)->isZoom()) && |
| 262 | (!tool->getInk(0)->isEyedropper() || |
| 263 | !tool->getInk(1)->isEyedropper())) { |
| 264 | // We have to drop pixels |
| 265 | dropPixels(); |
| 266 | } |
| 267 | // If we've temporarily gone to a non-selection tool and now we're |
| 268 | // back, we've just to update the context bar to show the "moving |
| 269 | // pixels" controls (e.g. OK/Cancel movement buttons). |
| 270 | else if (tool->getInk(0)->isSelection() || |
| 271 | tool->getInk(1)->isSelection()) { |
| 272 | ContextBar* contextBar = App::instance()->contextBar(); |
| 273 | contextBar->updateForMovingPixels(getTransformation(editor)); |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | bool MovingPixelsState::onMouseDown(Editor* editor, MouseMessage* msg) |
| 279 | { |
| 280 | ASSERT(m_pixelsMovement); |
| 281 | ASSERT(editor == m_editor); |
| 282 | |
| 283 | m_delayedMouseMove.onMouseDown(msg); |
| 284 | |
| 285 | // Set this editor as the active one and setup the ContextBar for |
| 286 | // moving pixels. This is needed in case that the user is working |
| 287 | // with a couple of Editors, in one is moving pixels and the other |
| 288 | // one not. |
| 289 | UIContext* ctx = UIContext::instance(); |
| 290 | ctx->setActiveView(editor->getDocView()); |
| 291 | |
| 292 | ContextBar* contextBar = App::instance()->contextBar(); |
| 293 | contextBar->updateForMovingPixels(getTransformation(editor)); |
| 294 | |
| 295 | // Start scroll loop |
| 296 | if (editor->checkForScroll(msg) || |
| 297 | editor->checkForZoom(msg)) |
| 298 | return true; |
| 299 | |
| 300 | // Call the eyedropper command |
| 301 | tools::Ink* clickedInk = editor->getCurrentEditorInk(); |
| 302 | if (clickedInk->isEyedropper()) { |
| 303 | callEyedropper(editor, msg); |
| 304 | return true; |
| 305 | } |
| 306 | |
| 307 | Decorator* decorator = static_cast<Decorator*>(editor->decorator()); |
| 308 | Doc* document = editor->document(); |
| 309 | |
| 310 | // Transform selected pixels |
| 311 | if (document->isMaskVisible() && |
| 312 | decorator->getTransformHandles(editor) && |
| 313 | (!Preferences::instance().selection.modifiersDisableHandles() || |
| 314 | msg->modifiers() == kKeyNoneModifier)) { |
| 315 | TransformHandles* transfHandles = decorator->getTransformHandles(editor); |
| 316 | |
| 317 | // Get the handle covered by the mouse. |
| 318 | HandleType handle = transfHandles->getHandleAtPoint(editor, |
| 319 | msg->position(), |
| 320 | getTransformation(editor)); |
| 321 | |
| 322 | if (handle != NoHandle) { |
| 323 | if (layer_is_locked(editor)) |
| 324 | return true; |
| 325 | |
| 326 | // Re-catch the image |
| 327 | gfx::Point pt = editor->screenToEditor(msg->position()); |
| 328 | m_pixelsMovement->catchImageAgain(gfx::PointF(pt), handle); |
| 329 | |
| 330 | editor->captureMouse(); |
| 331 | return true; |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | // Start "moving pixels" loop. Here we check only for left-click as |
| 336 | // right-click can be used to deselect/subtract selection, so we |
| 337 | // should drop the selection in this later case. |
| 338 | if (editor->isInsideSelection() && msg->left()) { |
| 339 | if (layer_is_locked(editor)) |
| 340 | return true; |
| 341 | |
| 342 | // In case that the user is pressing the copy-selection keyboard shortcut. |
| 343 | EditorCustomizationDelegate* customization = editor->getCustomizationDelegate(); |
| 344 | if ((customization) && |
| 345 | int(customization->getPressedKeyAction(KeyContext::TranslatingSelection) & KeyAction::CopySelection)) { |
| 346 | // Stamp the pixels to create the copy. |
| 347 | m_pixelsMovement->stampImage(); |
| 348 | } |
| 349 | |
| 350 | // Re-catch the image |
| 351 | m_pixelsMovement->catchImageAgain( |
| 352 | editor->screenToEditorF(msg->position()), MovePixelsHandle); |
| 353 | |
| 354 | editor->captureMouse(); |
| 355 | return true; |
| 356 | } |
| 357 | // End "moving pixels" loop |
| 358 | else { |
| 359 | // Drop pixels (e.g. to start drawing) |
| 360 | dropPixels(); |
| 361 | } |
| 362 | |
| 363 | // Use StandbyState implementation |
| 364 | return StandbyState::onMouseDown(editor, msg); |
| 365 | } |
| 366 | |
| 367 | bool MovingPixelsState::onMouseUp(Editor* editor, MouseMessage* msg) |
| 368 | { |
| 369 | ASSERT(m_pixelsMovement); |
| 370 | ASSERT(editor == m_editor); |
| 371 | |
| 372 | m_delayedMouseMove.onMouseUp(msg); |
| 373 | |
| 374 | // Drop the image temporarily in this location (where the user releases the mouse) |
| 375 | m_pixelsMovement->dropImageTemporarily(); |
| 376 | |
| 377 | // Redraw the new pivot location. |
| 378 | editor->invalidate(); |
| 379 | |
| 380 | editor->releaseMouse(); |
| 381 | return true; |
| 382 | } |
| 383 | |
| 384 | bool MovingPixelsState::onMouseMove(Editor* editor, MouseMessage* msg) |
| 385 | { |
| 386 | ASSERT(m_pixelsMovement); |
| 387 | ASSERT(editor == m_editor); |
| 388 | |
| 389 | // If there is a button pressed |
| 390 | if (m_pixelsMovement->isDragging()) { |
| 391 | if (m_delayedMouseMove.onMouseMove(msg)) |
| 392 | m_renderTimer.start(); |
| 393 | return true; |
| 394 | } |
| 395 | |
| 396 | // Use StandbyState implementation |
| 397 | return StandbyState::onMouseMove(editor, msg); |
| 398 | } |
| 399 | |
| 400 | void MovingPixelsState::onCommitMouseMove(Editor* editor, |
| 401 | const gfx::PointF& spritePos) |
| 402 | { |
| 403 | ASSERT(m_pixelsMovement); |
| 404 | |
| 405 | if (!m_pixelsMovement->isDragging()) |
| 406 | return; |
| 407 | |
| 408 | m_pixelsMovement->setFastMode(true); |
| 409 | |
| 410 | // Get the customization for the pixels movement (snap to grid, angle snap, etc.). |
| 411 | KeyContext keyContext = KeyContext::Normal; |
| 412 | switch (m_pixelsMovement->handle()) { |
| 413 | case MovePixelsHandle: |
| 414 | keyContext = KeyContext::TranslatingSelection; |
| 415 | break; |
| 416 | case ScaleNWHandle: |
| 417 | case ScaleNHandle: |
| 418 | case ScaleNEHandle: |
| 419 | case ScaleWHandle: |
| 420 | case ScaleEHandle: |
| 421 | case ScaleSWHandle: |
| 422 | case ScaleSHandle: |
| 423 | case ScaleSEHandle: |
| 424 | keyContext = KeyContext::ScalingSelection; |
| 425 | break; |
| 426 | case RotateNWHandle: |
| 427 | case RotateNEHandle: |
| 428 | case RotateSWHandle: |
| 429 | case RotateSEHandle: |
| 430 | keyContext = KeyContext::RotatingSelection; |
| 431 | break; |
| 432 | case SkewNHandle: |
| 433 | case SkewWHandle: |
| 434 | case SkewEHandle: |
| 435 | case SkewSHandle: |
| 436 | keyContext = KeyContext::ScalingSelection; |
| 437 | break; |
| 438 | } |
| 439 | |
| 440 | PixelsMovement::MoveModifier moveModifier = PixelsMovement::NormalMovement; |
| 441 | KeyAction action = m_editor->getCustomizationDelegate() |
| 442 | ->getPressedKeyAction(keyContext); |
| 443 | |
| 444 | if (int(action & KeyAction::SnapToGrid)) |
| 445 | moveModifier |= PixelsMovement::SnapToGridMovement; |
| 446 | |
| 447 | if (int(action & KeyAction::AngleSnap)) |
| 448 | moveModifier |= PixelsMovement::AngleSnapMovement; |
| 449 | |
| 450 | if (int(action & KeyAction::MaintainAspectRatio)) |
| 451 | moveModifier |= PixelsMovement::MaintainAspectRatioMovement; |
| 452 | |
| 453 | if (int(action & KeyAction::ScaleFromCenter)) |
| 454 | moveModifier |= PixelsMovement::ScaleFromPivot; |
| 455 | |
| 456 | if (int(action & KeyAction::LockAxis)) |
| 457 | moveModifier |= PixelsMovement::LockAxisMovement; |
| 458 | |
| 459 | if (int(action & KeyAction::FineControl)) |
| 460 | moveModifier |= PixelsMovement::FineControl; |
| 461 | |
| 462 | // Invalidate handles |
| 463 | Decorator* decorator = static_cast<Decorator*>(m_editor->decorator()); |
| 464 | TransformHandles* transfHandles = decorator->getTransformHandles(m_editor); |
| 465 | const Transformation& transformation = m_pixelsMovement->getTransformation(); |
| 466 | transfHandles->invalidateHandles(m_editor, transformation); |
| 467 | |
| 468 | // Drag the image to that position |
| 469 | m_pixelsMovement->moveImage(spritePos, moveModifier); |
| 470 | |
| 471 | // Update context bar and status bar |
| 472 | ContextBar* contextBar = App::instance()->contextBar(); |
| 473 | contextBar->updateForMovingPixels(transformation); |
| 474 | |
| 475 | m_editor->updateStatusBar(); |
| 476 | } |
| 477 | |
| 478 | bool MovingPixelsState::onSetCursor(Editor* editor, const gfx::Point& mouseScreenPos) |
| 479 | { |
| 480 | ASSERT(m_pixelsMovement); |
| 481 | ASSERT(editor == m_editor); |
| 482 | |
| 483 | // Move selection |
| 484 | if (m_pixelsMovement->isDragging()) { |
| 485 | editor->showMouseCursor(kMoveCursor); |
| 486 | return true; |
| 487 | } |
| 488 | |
| 489 | // Use StandbyState implementation |
| 490 | return StandbyState::onSetCursor(editor, mouseScreenPos); |
| 491 | } |
| 492 | |
| 493 | bool MovingPixelsState::onKeyDown(Editor* editor, KeyMessage* msg) |
| 494 | { |
| 495 | ASSERT(m_pixelsMovement); |
| 496 | if (!isActiveEditor()) |
| 497 | return false; |
| 498 | ASSERT(editor == m_editor); |
| 499 | |
| 500 | if (msg->scancode() == kKeyEnter || // TODO make this key customizable |
| 501 | msg->scancode() == kKeyEnterPad || |
| 502 | msg->scancode() == kKeyEsc) { |
| 503 | dropPixels(); |
| 504 | |
| 505 | // The escape key drop pixels and deselect the mask. |
| 506 | if (msg->scancode() == kKeyEsc) { // TODO make this key customizable |
| 507 | Command* cmd = Commands::instance()->byId(CommandId::DeselectMask()); |
| 508 | UIContext::instance()->executeCommandFromMenuOrShortcut(cmd); |
| 509 | } |
| 510 | |
| 511 | return true; |
| 512 | } |
| 513 | |
| 514 | // Use StandbyState implementation |
| 515 | return StandbyState::onKeyDown(editor, msg); |
| 516 | } |
| 517 | |
| 518 | bool MovingPixelsState::onKeyUp(Editor* editor, KeyMessage* msg) |
| 519 | { |
| 520 | ASSERT(m_pixelsMovement); |
| 521 | if (!isActiveEditor()) |
| 522 | return false; |
| 523 | ASSERT(editor == m_editor); |
| 524 | |
| 525 | // Use StandbyState implementation |
| 526 | return StandbyState::onKeyUp(editor, msg); |
| 527 | } |
| 528 | |
| 529 | bool MovingPixelsState::onUpdateStatusBar(Editor* editor) |
| 530 | { |
| 531 | MOVPIXS_TRACE("MOVPIXS: onUpdateStatusBar (%p)\n" , m_pixelsMovement.get()); |
| 532 | |
| 533 | ASSERT(m_pixelsMovement); |
| 534 | ASSERT(editor == m_editor); |
| 535 | |
| 536 | // We've received a crash report where this is nullptr when |
| 537 | // MovingPixelsState::onLeaveState() generates a general update |
| 538 | // notification (notifyGeneralUpdate()) just after the |
| 539 | // m_pixelsMovement is deleted with removePixelsMovement(). The |
| 540 | // general update signals a scroll update in the view which will ask |
| 541 | // for the status bar content again (Editor::notifyScrollChanged). |
| 542 | // |
| 543 | // We weren't able to reproduce this scenario anyway (which should |
| 544 | // be visible with the ASSERT() above). |
| 545 | if (!m_pixelsMovement) |
| 546 | return false; |
| 547 | |
| 548 | const Transformation& transform(getTransformation(editor)); |
| 549 | gfx::Size imageSize = m_pixelsMovement->getInitialImageSize(); |
| 550 | |
| 551 | int w = int(transform.bounds().w); |
| 552 | int h = int(transform.bounds().h); |
| 553 | int gcd = base::gcd(w, h); |
| 554 | StatusBar::instance()->setStatusText( |
| 555 | 100, |
| 556 | fmt::format( |
| 557 | ":pos: {} {}" |
| 558 | " :size: {} {}" |
| 559 | " :selsize: {} {} [{:.02f}% {:.02f}%]" |
| 560 | " :angle: {:.1f}" |
| 561 | " :aspect_ratio: {}:{}" , |
| 562 | int(transform.bounds().x), |
| 563 | int(transform.bounds().y), |
| 564 | imageSize.w, |
| 565 | imageSize.h, |
| 566 | w, |
| 567 | h, |
| 568 | (double)w*100.0/imageSize.w, |
| 569 | (double)h*100.0/imageSize.h, |
| 570 | 180.0 * transform.angle() / PI, |
| 571 | w/gcd, |
| 572 | h/gcd)); |
| 573 | |
| 574 | return true; |
| 575 | } |
| 576 | |
| 577 | bool MovingPixelsState::acceptQuickTool(tools::Tool* tool) |
| 578 | { |
| 579 | return |
| 580 | (!m_pixelsMovement || |
| 581 | tool->getInk(0)->isSelection() || |
| 582 | tool->getInk(0)->isEyedropper() || |
| 583 | tool->getInk(0)->isScrollMovement() || |
| 584 | tool->getInk(0)->isZoom()); |
| 585 | } |
| 586 | |
| 587 | // Before executing any command, we drop the pixels (go back to standby). |
| 588 | void MovingPixelsState::onBeforeCommandExecution(CommandExecutionEvent& ev) |
| 589 | { |
| 590 | Command* command = ev.command(); |
| 591 | |
| 592 | MOVPIXS_TRACE("MOVPIXS: onBeforeCommandExecution %s\n" , command->id().c_str()); |
| 593 | |
| 594 | // If the command is for other editor, we don't drop pixels. |
| 595 | if (!isActiveEditor()) |
| 596 | return; |
| 597 | |
| 598 | if (layer_is_locked(m_editor) && |
| 599 | (command->id() == CommandId::Flip() || |
| 600 | command->id() == CommandId::Cut() || |
| 601 | command->id() == CommandId::Clear() || |
| 602 | command->id() == CommandId::Rotate())) { |
| 603 | ev.cancel(); |
| 604 | return; |
| 605 | } |
| 606 | |
| 607 | // We don't need to drop the pixels if a MoveMaskCommand of Content is executed. |
| 608 | if (auto moveMaskCmd = dynamic_cast<MoveMaskCommand*>(command)) { |
| 609 | if (moveMaskCmd->getTarget() == MoveMaskCommand::Content) { |
| 610 | if (layer_is_locked(m_editor)) { |
| 611 | ev.cancel(); |
| 612 | return; |
| 613 | } |
| 614 | gfx::Point delta = moveMaskCmd->getMoveThing().getDelta(UIContext::instance()); |
| 615 | // Verify Shift condition of the MoveMaskCommand (i.e. wrap = true) |
| 616 | if (moveMaskCmd->isWrap()) { |
| 617 | m_pixelsMovement->shift(delta.x, delta.y); |
| 618 | } |
| 619 | else { |
| 620 | translate(gfx::PointF(delta)); |
| 621 | } |
| 622 | // We've processed the selection content movement right here. |
| 623 | ev.cancel(); |
| 624 | return; |
| 625 | } |
| 626 | } |
| 627 | // Don't drop pixels if the user zooms/scrolls/picks a color |
| 628 | // using commands. |
| 629 | else if ((command->id() == CommandId::Zoom()) || |
| 630 | (command->id() == CommandId::Scroll()) || |
| 631 | (command->id() == CommandId::Eyedropper()) || |
| 632 | // DiscardBrush is used by Eyedropper command |
| 633 | (command->id() == CommandId::DiscardBrush())) { |
| 634 | // Do not drop pixels |
| 635 | return; |
| 636 | } |
| 637 | // Intercept the "Cut" or "Copy" command to handle them locally |
| 638 | // with the current m_pixelsMovement data. |
| 639 | else if (command->id() == CommandId::Cut() || |
| 640 | command->id() == CommandId::Copy() || |
| 641 | command->id() == CommandId::Clear()) { |
| 642 | // Copy the floating image to the clipboard on Cut/Copy. |
| 643 | if (command->id() != CommandId::Clear()) { |
| 644 | Doc* document = m_editor->document(); |
| 645 | std::unique_ptr<Image> floatingImage; |
| 646 | std::unique_ptr<Mask> floatingMask; |
| 647 | m_pixelsMovement->getDraggedImageCopy(floatingImage, floatingMask); |
| 648 | |
| 649 | if (floatingImage->isTilemap()) { |
| 650 | Site site = m_editor->getSite(); |
| 651 | ASSERT(site.tileset()); |
| 652 | Clipboard::instance()-> |
| 653 | copyTilemap(floatingImage.get(), |
| 654 | floatingMask.get(), |
| 655 | document->sprite()->palette(m_editor->frame()), |
| 656 | site.tileset()); |
| 657 | } |
| 658 | else { |
| 659 | Clipboard::instance()-> |
| 660 | copyImage(floatingImage.get(), |
| 661 | floatingMask.get(), |
| 662 | document->sprite()->palette(m_editor->frame())); |
| 663 | } |
| 664 | } |
| 665 | |
| 666 | // Clear floating pixels on Cut/Clear. |
| 667 | if (command->id() != CommandId::Copy()) { |
| 668 | m_pixelsMovement->trim(); |
| 669 | |
| 670 | // Should we keep the mask after an Edit > Clear command? |
| 671 | auto keepMask = PixelsMovement::DontKeepMask; |
| 672 | if (command->id() == CommandId::Clear() && |
| 673 | Preferences::instance().selection.keepSelectionAfterClear()) { |
| 674 | keepMask = PixelsMovement::KeepMask; |
| 675 | } |
| 676 | |
| 677 | // Discard the dragged image. |
| 678 | m_pixelsMovement->discardImage(PixelsMovement::CommitChanges, keepMask); |
| 679 | m_discarded = true; |
| 680 | |
| 681 | // Quit from MovingPixelsState, back to standby. |
| 682 | m_editor->backToPreviousState(); |
| 683 | } |
| 684 | |
| 685 | // Cancel the command, we've simulated it. |
| 686 | ev.cancel(); |
| 687 | return; |
| 688 | } |
| 689 | // Flip Horizontally/Vertically commands are handled manually to |
| 690 | // avoid dropping the floating region of pixels. |
| 691 | else if (command->id() == CommandId::Flip()) { |
| 692 | if (auto flipCommand = dynamic_cast<FlipCommand*>(command)) { |
| 693 | this->flip(flipCommand->getFlipType()); |
| 694 | |
| 695 | ev.cancel(); |
| 696 | return; |
| 697 | } |
| 698 | } |
| 699 | // Rotate is quite simple, we can add the angle to the current transformation. |
| 700 | else if (command->id() == CommandId::Rotate()) { |
| 701 | if (auto rotate = dynamic_cast<RotateCommand*>(command)) { |
| 702 | if (rotate->flipMask()) { |
| 703 | this->rotate(rotate->angle()); |
| 704 | |
| 705 | ev.cancel(); |
| 706 | return; |
| 707 | } |
| 708 | } |
| 709 | } |
| 710 | // We can use previous/next frames while transforming the selection |
| 711 | // to switch between frames |
| 712 | else if (command->id() == CommandId::GotoPreviousFrame() || |
| 713 | command->id() == CommandId::GotoPreviousFrameWithSameTag()) { |
| 714 | if (m_pixelsMovement->gotoFrame(-1)) { |
| 715 | ev.cancel(); |
| 716 | return; |
| 717 | } |
| 718 | } |
| 719 | else if (command->id() == CommandId::GotoNextFrame() || |
| 720 | command->id() == CommandId::GotoNextFrameWithSameTag()) { |
| 721 | if (m_pixelsMovement->gotoFrame(+1)) { |
| 722 | ev.cancel(); |
| 723 | return; |
| 724 | } |
| 725 | } |
| 726 | |
| 727 | if (m_pixelsMovement) |
| 728 | dropPixels(); |
| 729 | } |
| 730 | |
| 731 | void MovingPixelsState::onDestroyEditor(Editor* editor) |
| 732 | { |
| 733 | // TODO we should call ~MovingPixelsState(), we should delete the |
| 734 | // whole "m_statesHistory" when an editor is deleted. |
| 735 | removeAsEditorObserver(); |
| 736 | } |
| 737 | |
| 738 | void MovingPixelsState::onBeforeFrameChanged(Editor* editor) |
| 739 | { |
| 740 | if (!isActiveDocument()) |
| 741 | return; |
| 742 | |
| 743 | if (m_pixelsMovement && |
| 744 | !m_pixelsMovement->canHandleFrameChange()) { |
| 745 | dropPixels(); |
| 746 | } |
| 747 | } |
| 748 | |
| 749 | void MovingPixelsState::onBeforeLayerChanged(Editor* editor) |
| 750 | { |
| 751 | if (!isActiveDocument()) |
| 752 | return; |
| 753 | |
| 754 | if (m_pixelsMovement) |
| 755 | dropPixels(); |
| 756 | } |
| 757 | |
| 758 | void MovingPixelsState::onBeforeRangeChanged(Timeline* timeline) |
| 759 | { |
| 760 | if (!isActiveDocument()) |
| 761 | return; |
| 762 | |
| 763 | if (m_pixelsMovement) |
| 764 | dropPixels(); |
| 765 | } |
| 766 | |
| 767 | void MovingPixelsState::onTransparentColorChange() |
| 768 | { |
| 769 | ASSERT(m_pixelsMovement); |
| 770 | |
| 771 | bool opaque = Preferences::instance().selection.opaque(); |
| 772 | setTransparentColor( |
| 773 | opaque, |
| 774 | opaque ? |
| 775 | app::Color::fromMask(): |
| 776 | Preferences::instance().selection.transparentColor()); |
| 777 | } |
| 778 | |
| 779 | void MovingPixelsState::onRenderTimer() |
| 780 | { |
| 781 | m_pixelsMovement->setFastMode(false); |
| 782 | m_renderTimer.stop(); |
| 783 | } |
| 784 | |
| 785 | void MovingPixelsState::onDropPixels(ContextBarObserver::DropAction action) |
| 786 | { |
| 787 | if (!isActiveEditor()) |
| 788 | return; |
| 789 | |
| 790 | switch (action) { |
| 791 | |
| 792 | case ContextBarObserver::DropPixels: |
| 793 | dropPixels(); |
| 794 | break; |
| 795 | |
| 796 | case ContextBarObserver::CancelDrag: |
| 797 | m_pixelsMovement->discardImage(PixelsMovement::DontCommitChanges); |
| 798 | m_discarded = true; |
| 799 | |
| 800 | // Quit from MovingPixelsState, back to standby. |
| 801 | m_editor->backToPreviousState(); |
| 802 | break; |
| 803 | } |
| 804 | } |
| 805 | |
| 806 | void MovingPixelsState::onPivotChange() |
| 807 | { |
| 808 | ContextBar* contextBar = App::instance()->contextBar(); |
| 809 | contextBar->updateForMovingPixels(getTransformation(m_editor)); |
| 810 | } |
| 811 | |
| 812 | void MovingPixelsState::setTransparentColor(bool opaque, const app::Color& color) |
| 813 | { |
| 814 | ASSERT(m_pixelsMovement); |
| 815 | |
| 816 | Layer* layer = m_editor->layer(); |
| 817 | ASSERT(layer); |
| 818 | |
| 819 | try { |
| 820 | m_pixelsMovement->setMaskColor( |
| 821 | opaque, color_utils::color_for_target_mask(color, ColorTarget(layer))); |
| 822 | } |
| 823 | catch (const LockedDocException& ex) { |
| 824 | Console::showException(ex); |
| 825 | } |
| 826 | } |
| 827 | |
| 828 | void MovingPixelsState::dropPixels() |
| 829 | { |
| 830 | MOVPIXS_TRACE("MOVPIXS: dropPixels\n" ); |
| 831 | |
| 832 | // Just change to default state (StandbyState generally). We'll |
| 833 | // receive an onLeaveState() event after this call. |
| 834 | m_editor->backToPreviousState(); |
| 835 | } |
| 836 | |
| 837 | Transformation MovingPixelsState::getTransformation(Editor* editor) |
| 838 | { |
| 839 | // m_pixelsMovement can be null in the final onMouseDown(), after we |
| 840 | // called dropPixels() and we're just going to the previous state. |
| 841 | if (m_pixelsMovement) |
| 842 | return m_pixelsMovement->getTransformation(); |
| 843 | else |
| 844 | return StandbyState::getTransformation(editor); |
| 845 | } |
| 846 | |
| 847 | bool MovingPixelsState::isActiveDocument() const |
| 848 | { |
| 849 | Doc* doc = UIContext::instance()->activeDocument(); |
| 850 | return (m_editor->document() == doc); |
| 851 | } |
| 852 | |
| 853 | bool MovingPixelsState::isActiveEditor() const |
| 854 | { |
| 855 | Editor* targetEditor = UIContext::instance()->activeEditor(); |
| 856 | return (targetEditor == m_editor); |
| 857 | } |
| 858 | |
| 859 | void MovingPixelsState::removeAsEditorObserver() |
| 860 | { |
| 861 | if (m_observingEditor) { |
| 862 | m_observingEditor = false; |
| 863 | m_editor->remove_observer(this); |
| 864 | } |
| 865 | } |
| 866 | |
| 867 | void MovingPixelsState::removePixelsMovement() |
| 868 | { |
| 869 | // Avoid receiving a onCommitMouseMove() message when |
| 870 | // m_pixelsMovement is already nullptr. |
| 871 | m_delayedMouseMove.stopTimer(); |
| 872 | |
| 873 | m_pixelsMovement.reset(); |
| 874 | m_ctxConn.disconnect(); |
| 875 | m_opaqueConn.disconnect(); |
| 876 | m_transparentConn.disconnect(); |
| 877 | } |
| 878 | |
| 879 | } // namespace app |
| 880 | |