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