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
57namespace app {
58
59using namespace ui;
60
61MovingPixelsState::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
125MovingPixelsState::~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
143void 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
154void MovingPixelsState::rotate(double angle)
155{
156 m_pixelsMovement->rotate(angle);
157 m_editor->updateStatusBar();
158}
159
160void MovingPixelsState::flip(doc::algorithm::FlipType flipType)
161{
162 m_pixelsMovement->flipImage(flipType);
163 m_editor->updateStatusBar();
164}
165
166void MovingPixelsState::shift(int dx, int dy)
167{
168 m_pixelsMovement->shift(dx, dy);
169 m_editor->updateStatusBar();
170}
171
172void MovingPixelsState::updateTransformation(const Transformation& t)
173{
174 m_pixelsMovement->setTransformation(t);
175 m_editor->updateStatusBar();
176}
177
178void MovingPixelsState::onEnterState(Editor* editor)
179{
180 StandbyState::onEnterState(editor);
181
182 update_screen_for_document(editor->document());
183}
184
185void 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
195EditorState::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
246void 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
278bool 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
367bool 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
384bool 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
400void 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
478bool 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
493bool 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
518bool 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
529bool 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
577bool 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).
588void 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
731void 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
738void MovingPixelsState::onBeforeFrameChanged(Editor* editor)
739{
740 if (!isActiveDocument())
741 return;
742
743 if (m_pixelsMovement &&
744 !m_pixelsMovement->canHandleFrameChange()) {
745 dropPixels();
746 }
747}
748
749void MovingPixelsState::onBeforeLayerChanged(Editor* editor)
750{
751 if (!isActiveDocument())
752 return;
753
754 if (m_pixelsMovement)
755 dropPixels();
756}
757
758void MovingPixelsState::onBeforeRangeChanged(Timeline* timeline)
759{
760 if (!isActiveDocument())
761 return;
762
763 if (m_pixelsMovement)
764 dropPixels();
765}
766
767void 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
779void MovingPixelsState::onRenderTimer()
780{
781 m_pixelsMovement->setFastMode(false);
782 m_renderTimer.stop();
783}
784
785void 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
806void MovingPixelsState::onPivotChange()
807{
808 ContextBar* contextBar = App::instance()->contextBar();
809 contextBar->updateForMovingPixels(getTransformation(m_editor));
810}
811
812void 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
828void 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
837Transformation 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
847bool MovingPixelsState::isActiveDocument() const
848{
849 Doc* doc = UIContext::instance()->activeDocument();
850 return (m_editor->document() == doc);
851}
852
853bool MovingPixelsState::isActiveEditor() const
854{
855 Editor* targetEditor = UIContext::instance()->activeEditor();
856 return (targetEditor == m_editor);
857}
858
859void MovingPixelsState::removeAsEditorObserver()
860{
861 if (m_observingEditor) {
862 m_observingEditor = false;
863 m_editor->remove_observer(this);
864 }
865}
866
867void 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