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