1// Aseprite
2// Copyright (C) 2018-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#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/editor/standby_state.h"
13
14#include "app/app.h"
15#include "app/app_menus.h"
16#include "app/color_picker.h"
17#include "app/commands/cmd_eyedropper.h"
18#include "app/commands/commands.h"
19#include "app/commands/params.h"
20#include "app/doc_range.h"
21#include "app/i18n/strings.h"
22#include "app/ini_file.h"
23#include "app/pref/preferences.h"
24#include "app/tools/active_tool.h"
25#include "app/tools/ink.h"
26#include "app/tools/pick_ink.h"
27#include "app/tools/tool.h"
28#include "app/ui/app_menuitem.h"
29#include "app/ui/doc_view.h"
30#include "app/ui/editor/dragging_value_state.h"
31#include "app/ui/editor/drawing_state.h"
32#include "app/ui/editor/editor.h"
33#include "app/ui/editor/editor_customization_delegate.h"
34#include "app/ui/editor/glue.h"
35#include "app/ui/editor/handle_type.h"
36#include "app/ui/editor/moving_cel_state.h"
37#include "app/ui/editor/moving_pixels_state.h"
38#include "app/ui/editor/moving_selection_state.h"
39#include "app/ui/editor/moving_slice_state.h"
40#include "app/ui/editor/moving_symmetry_state.h"
41#include "app/ui/editor/pivot_helpers.h"
42#include "app/ui/editor/pixels_movement.h"
43#include "app/ui/editor/scrolling_state.h"
44#include "app/ui/editor/tool_loop_impl.h"
45#include "app/ui/editor/transform_handles.h"
46#include "app/ui/editor/vec2.h"
47#include "app/ui/editor/zooming_state.h"
48#include "app/ui/main_window.h"
49#include "app/ui/skin/skin_theme.h"
50#include "app/ui/status_bar.h"
51#include "app/ui/timeline/timeline.h"
52#include "app/ui_context.h"
53#include "app/util/layer_utils.h"
54#include "app/util/new_image_from_mask.h"
55#include "app/util/readable_time.h"
56#include "base/pi.h"
57#include "base/vector2d.h"
58#include "doc/grid.h"
59#include "doc/layer.h"
60#include "doc/mask.h"
61#include "doc/slice.h"
62#include "doc/sprite.h"
63#include "fmt/format.h"
64#include "gfx/rect.h"
65#include "os/surface.h"
66#include "os/system.h"
67#include "ui/alert.h"
68#include "ui/message.h"
69#include "ui/view.h"
70
71#include <algorithm>
72#include <cmath>
73#include <cstring>
74
75namespace app {
76
77using namespace ui;
78
79#ifdef _MSC_VER
80#pragma warning(disable:4355) // warning C4355: 'this' : used in base member initializer list
81#endif
82
83StandbyState::StandbyState()
84 : m_decorator(new Decorator(this))
85 , m_transformSelectionHandlesAreVisible(false)
86{
87}
88
89StandbyState::~StandbyState()
90{
91 delete m_decorator;
92}
93
94void StandbyState::onEnterState(Editor* editor)
95{
96 StateWithWheelBehavior::onEnterState(editor);
97
98 editor->setDecorator(m_decorator);
99
100 m_pivotVisConn =
101 Preferences::instance().selection.pivotVisibility.AfterChange.connect(
102 [this, editor]{ onPivotChange(editor); });
103 m_pivotPosConn =
104 Preferences::instance().selection.pivotPosition.AfterChange.connect(
105 [this, editor]{ onPivotChange(editor); });
106}
107
108void StandbyState::onActiveToolChange(Editor* editor, tools::Tool* tool)
109{
110 // If the user change from a selection tool to a non-selection tool,
111 // or viceversa, we've to show or hide the transformation handles.
112 bool needDecorators = (tool && tool->getInk(0)->isSelection());
113 if (m_transformSelectionHandlesAreVisible != needDecorators ||
114 !editor->layer() ||
115 !editor->layer()->isReference()) {
116 m_transformSelectionHandlesAreVisible = false;
117 editor->invalidate();
118 }
119}
120
121bool StandbyState::onMouseDown(Editor* editor, MouseMessage* msg)
122{
123 if (editor->hasCapture())
124 return true;
125
126 UIContext* context = UIContext::instance();
127 tools::Ink* clickedInk = editor->getCurrentEditorInk();
128 Site site;
129 editor->getSite(&site);
130 Doc* document = site.document();
131 Layer* layer = site.layer();
132
133 // When an editor is clicked the current view is changed.
134 context->setActiveView(editor->getDocView());
135
136 // Move symmetry
137 Decorator::Handles handles;
138 if (m_decorator->getSymmetryHandles(editor, handles)) {
139 for (const auto& handle : handles) {
140 if (handle.bounds.contains(msg->position())) {
141 auto mode = (handle.align & (TOP | BOTTOM) ? app::gen::SymmetryMode::HORIZONTAL:
142 app::gen::SymmetryMode::VERTICAL);
143 bool horz = (mode == app::gen::SymmetryMode::HORIZONTAL);
144 auto& symmetry = Preferences::instance().document(editor->document()).symmetry;
145 auto& axis = (horz ? symmetry.xAxis:
146 symmetry.yAxis);
147 editor->setState(
148 EditorStatePtr(new MovingSymmetryState(editor, msg, mode, axis)));
149 return true;
150 }
151 }
152 }
153
154 // Start scroll loop
155 if (editor->checkForScroll(msg) ||
156 editor->checkForZoom(msg))
157 return true;
158
159 // Move cel X,Y coordinates
160 if (clickedInk->isCelMovement()) {
161 // Handle "Auto Select Layer"
162 if (editor->isAutoSelectLayer()) {
163 gfx::PointF cursor = editor->screenToEditorF(msg->position());
164 ColorPicker picker;
165 picker.pickColor(site, cursor,
166 editor->projection(),
167 ColorPicker::FromComposition);
168
169 auto range = App::instance()->timeline()->range();
170 if (picker.layer() &&
171 !range.contains(picker.layer())) {
172 layer = picker.layer();
173 if (layer) {
174 editor->setLayer(layer);
175 editor->flashCurrentLayer();
176 }
177 }
178 }
179
180 if (layer) {
181 // TODO we should be able to move the `Background' with tiled mode
182 if (layer->isBackground()) {
183 StatusBar::instance()->showTip(
184 1000, Strings::statusbar_tips_cannot_move_bg_layer());
185 }
186 else if (!layer->isVisibleHierarchy()) {
187 StatusBar::instance()->showTip(
188 1000,
189 fmt::format(Strings::statusbar_tips_layer_x_is_hidden(),
190 layer->name()));
191 }
192 else if (!layer->isMovable() || !layer->isEditableHierarchy()) {
193 StatusBar::instance()->showTip(
194 1000, fmt::format(Strings::statusbar_tips_layer_locked(), layer->name()));
195 }
196 else {
197 MovingCelCollect collect(editor, layer);
198 if (collect.empty()) {
199 StatusBar::instance()->showTip(
200 1000, Strings::statusbar_tips_nothing_to_move());
201 }
202 else {
203 try {
204 // Change to MovingCelState
205 HandleType handle = MovePixelsHandle;
206 if (resizeCelBounds(editor).contains(msg->position()))
207 handle = ScaleSEHandle;
208
209 MovingCelState* newState = new MovingCelState(
210 editor, msg, handle, collect);
211 editor->setState(EditorStatePtr(newState));
212 }
213 catch (const LockedDocException&) {
214 // TODO break the background task that is locking this sprite
215 StatusBar::instance()->showTip(
216 1000, Strings::statusbar_tips_recovery_task_using_sprite());
217 }
218 }
219 }
220 }
221
222 return true;
223 }
224
225 // Call the eyedropper command
226 if (clickedInk->isEyedropper()) {
227 editor->captureMouse();
228 callEyedropper(editor, msg);
229 return true;
230 }
231
232 if (clickedInk->isSlice()) {
233 EditorHit hit = editor->calcHit(msg->position());
234 switch (hit.type()) {
235 case EditorHit::SliceBounds:
236 case EditorHit::SliceCenter:
237 if (msg->left()) {
238 // If we click outside all slices, we clear the selection of slices.
239 if (!hit.slice() || !site.selectedSlices().contains(hit.slice()->id())) {
240 editor->clearSlicesSelection();
241 editor->selectSlice(hit.slice());
242
243 site = Site();
244 editor->getSite(&site);
245 }
246
247 MovingSliceState* newState = new MovingSliceState(
248 editor, msg, hit, site.selectedSlices());
249 editor->setState(EditorStatePtr(newState));
250 }
251 else {
252 Menu* popupMenu = AppMenus::instance()->getSlicePopupMenu();
253 if (popupMenu) {
254 Params params;
255 // When the editor doesn't have a set of selected slices,
256 // we set the specific clicked slice for the commands (in
257 // other case, those commands will get the selected set of
258 // slices from Site::selectedSlices() field).
259 if (!editor->hasSelectedSlices())
260 params.set("id", base::convert_to<std::string>(hit.slice()->id()).c_str());
261 AppMenuItem::setContextParams(params);
262 popupMenu->showPopup(msg->position(), editor->display());
263 AppMenuItem::setContextParams(Params());
264 }
265 }
266 return true;
267 }
268 }
269
270 // Only if the selected tool or quick tool is selection, we give the
271 // possibility to transform/move the selection. In other case,
272 // e.g. when selection is used with right-click mode, the
273 // transformation is disabled.
274 auto activeToolManager = App::instance()->activeToolManager();
275 if (clickedInk->isSelection() &&
276 ((activeToolManager->selectedTool() &&
277 activeToolManager->selectedTool()->getInk(0)->isSelection()) ||
278 (activeToolManager->quickTool() &&
279 activeToolManager->quickTool()->getInk(0)->isSelection()))) {
280 // Transform selected pixels
281 if (editor->isActive() &&
282 document->isMaskVisible() &&
283 m_decorator->getTransformHandles(editor) &&
284 (!Preferences::instance().selection.modifiersDisableHandles() ||
285 msg->modifiers() == kKeyNoneModifier)) {
286 TransformHandles* transfHandles = m_decorator->getTransformHandles(editor);
287
288 // Get the handle covered by the mouse.
289 HandleType handle = transfHandles->getHandleAtPoint(editor,
290 msg->position(),
291 getTransformation(editor));
292
293 if (handle != NoHandle) {
294 int x, y, opacity;
295 Image* image = site.image(&x, &y, &opacity);
296 if (layer && image) {
297 // Change to MovingPixelsState
298 transformSelection(editor, msg, handle);
299 }
300 return true;
301 }
302 }
303
304 // Move selection edges
305 if (overSelectionEdges(editor, msg->position())) {
306 transformSelection(editor, msg, MoveSelectionHandle);
307 return true;
308 }
309
310 // Move selected pixels
311 if (layer && editor->canStartMovingSelectionPixels() && msg->left()) {
312 // Change to MovingPixelsState
313 transformSelection(editor, msg, MovePixelsHandle);
314 return true;
315 }
316 }
317
318 // Start the Tool-Loop
319 if (layer && (layer->isImage() || clickedInk->isSelection())) {
320 tools::Pointer pointer = pointer_from_msg(editor, msg);
321
322 // Shift+click on Pencil tool starts a line onMouseDown() when the
323 // preview (onKeyDown) is disabled.
324 if (!Preferences::instance().editor.straightLinePreview() &&
325 checkStartDrawingStraightLine(editor, msg, &pointer)) {
326 // Send first mouse down to draw the straight line and start the
327 // freehand mode.
328 editor->getState()->onMouseDown(editor, msg);
329 return true;
330 }
331
332 // Disable layer edges to avoid showing the modified cel
333 // information by ExpandCelCanvas (i.e. the cel origin is changed
334 // to 0,0 coordinate.)
335 auto& layerEdgesOption = editor->docPref().show.layerEdges;
336 bool layerEdges = layerEdgesOption();
337 if (layerEdges)
338 layerEdgesOption(false);
339
340 startDrawingState(editor, msg,
341 DrawingType::Regular,
342 pointer);
343
344 // Restore layer edges
345 if (layerEdges)
346 layerEdgesOption(true);
347 return true;
348 }
349
350 return true;
351}
352
353bool StandbyState::onMouseUp(Editor* editor, MouseMessage* msg)
354{
355 editor->releaseMouse();
356 return true;
357}
358
359bool StandbyState::onMouseMove(Editor* editor, MouseMessage* msg)
360{
361 // We control eyedropper tool from here. TODO move this to another place
362 if (msg->left() || msg->right()) {
363 tools::Ink* clickedInk = editor->getCurrentEditorInk();
364 if (clickedInk->isEyedropper() &&
365 editor->hasCapture()) {
366 callEyedropper(editor, msg);
367 }
368 }
369
370 editor->updateStatusBar();
371 return true;
372}
373
374bool StandbyState::onDoubleClick(Editor* editor, MouseMessage* msg)
375{
376 if (editor->hasCapture())
377 return false;
378
379 tools::Ink* ink = editor->getCurrentEditorInk();
380
381 // Select a tile with double-click
382 if (ink->isSelection() &&
383 Preferences::instance().selection.doubleclickSelectTile()) {
384 // Drop pixels if we are in moving pixels state
385 if (dynamic_cast<MovingPixelsState*>(editor->getState().get()))
386 editor->backToPreviousState();
387
388 // Start a tool-loop selecting tiles.
389 startDrawingState(editor, msg,
390 DrawingType::SelectTiles,
391 pointer_from_msg(editor, msg));
392 return true;
393 }
394 // Show slice properties when we double-click it
395 else if (ink->isSlice()) {
396 EditorHit hit = editor->calcHit(msg->position());
397 if (hit.slice()) {
398 Command* cmd = Commands::instance()->byId(CommandId::SliceProperties());
399 Params params;
400 params.set("id", base::convert_to<std::string>(hit.slice()->id()).c_str());
401 UIContext::instance()->executeCommandFromMenuOrShortcut(cmd, params);
402 return true;
403 }
404 }
405
406 return false;
407}
408
409bool StandbyState::onSetCursor(Editor* editor, const gfx::Point& mouseScreenPos)
410{
411 tools::Ink* ink = editor->getCurrentEditorInk();
412
413 // See if the cursor is in some selection handle.
414 if (m_decorator->onSetCursor(ink, editor, mouseScreenPos))
415 return true;
416
417 auto theme = skin::SkinTheme::get(editor);
418
419 if (ink) {
420 // If the current tool change selection (e.g. rectangular marquee, etc.)
421 if (ink->isSelection()) {
422 if (overSelectionEdges(editor, mouseScreenPos)) {
423 editor->showMouseCursor(
424 kCustomCursor, theme->cursors.moveSelection());
425 return true;
426 }
427
428 // Move pixels
429 if (editor->canStartMovingSelectionPixels()) {
430 EditorCustomizationDelegate* customization = editor->getCustomizationDelegate();
431 if ((customization) &&
432 int(customization->getPressedKeyAction(KeyContext::TranslatingSelection) & KeyAction::CopySelection))
433 editor->showMouseCursor(kArrowPlusCursor);
434 else
435 editor->showMouseCursor(kMoveCursor);
436 }
437 else
438 editor->showBrushPreview(mouseScreenPos);
439 return true;
440 }
441 else if (ink->isCelMovement()) {
442 if (resizeCelBounds(editor).contains(mouseScreenPos))
443 editor->showMouseCursor(kSizeSECursor);
444 else
445 editor->showMouseCursor(kMoveCursor);
446 return true;
447 }
448 else if (ink->isSlice()) {
449 EditorHit hit = editor->calcHit(mouseScreenPos);
450 switch (hit.type()) {
451 case EditorHit::None:
452 // Do nothing, continue
453 break;
454 case EditorHit::SliceBounds:
455 case EditorHit::SliceCenter:
456 switch (hit.border()) {
457 case CENTER | MIDDLE:
458 editor->showMouseCursor(kMoveCursor);
459 break;
460 case TOP | LEFT:
461 editor->showMouseCursor(kSizeNWCursor);
462 break;
463 case TOP:
464 editor->showMouseCursor(kSizeNCursor);
465 break;
466 case TOP | RIGHT:
467 editor->showMouseCursor(kSizeNECursor);
468 break;
469 case LEFT:
470 editor->showMouseCursor(kSizeWCursor);
471 break;
472 case RIGHT:
473 editor->showMouseCursor(kSizeECursor);
474 break;
475 case BOTTOM | LEFT:
476 editor->showMouseCursor(kSizeSWCursor);
477 break;
478 case BOTTOM:
479 editor->showMouseCursor(kSizeSCursor);
480 break;
481 case BOTTOM | RIGHT:
482 editor->showMouseCursor(kSizeSECursor);
483 break;
484 }
485 return true;
486 }
487 }
488 }
489
490 return StateWithWheelBehavior::onSetCursor(editor, mouseScreenPos);
491}
492
493bool StandbyState::onKeyDown(Editor* editor, KeyMessage* msg)
494{
495 if (Preferences::instance().editor.straightLinePreview() &&
496 checkStartDrawingStraightLine(editor, nullptr, nullptr))
497 return false;
498
499 Keys keys = KeyboardShortcuts::instance()
500 ->getDragActionsFromKeyMessage(KeyContext::MouseWheel, msg);
501 if (!keys.empty()) {
502 // Don't enter DraggingValueState to change brush size if we are
503 // in a selection-like tool
504 if (keys.size() == 1 &&
505 keys[0]->wheelAction() == WheelAction::BrushSize &&
506 editor->getCurrentEditorInk()->isSelection()) {
507 return false;
508 }
509
510 EditorStatePtr newState(new DraggingValueState(editor, keys));
511 editor->setState(newState);
512 return true;
513 }
514
515 return false;
516}
517
518bool StandbyState::onKeyUp(Editor* editor, KeyMessage* msg)
519{
520 return false;
521}
522
523bool StandbyState::onUpdateStatusBar(Editor* editor)
524{
525 tools::Ink* ink = editor->getCurrentEditorInk();
526 const Sprite* sprite = editor->sprite();
527 gfx::PointF spritePos =
528 editor->screenToEditorF(editor->mousePosInDisplay())
529 - gfx::PointF(editor->mainTilePosition());
530
531 if (!sprite) {
532 StatusBar::instance()->showDefaultText();
533 }
534 // For eye-dropper
535 else if (ink->isEyedropper()) {
536 Site site = editor->getSite();
537 EyedropperCommand cmd;
538 app::Color color = Preferences::instance().colorBar.fgColor();
539 doc::tile_t tile = Preferences::instance().colorBar.fgTile();
540 cmd.pickSample(site,
541 spritePos,
542 editor->projection(),
543 color, tile);
544
545 std::string buf =
546 fmt::format(" :pos: {} {}",
547 int(std::floor(spritePos.x)),
548 int(std::floor(spritePos.y)));
549
550 if (site.tilemapMode() == TilemapMode::Tiles)
551 StatusBar::instance()->showTile(0, tile, buf);
552 else
553 StatusBar::instance()->showColor(0, color, buf);
554 }
555 else {
556 Site site = editor->getSite();
557 Mask* mask =
558 (editor->document()->isMaskVisible() ?
559 editor->document()->mask(): NULL);
560
561 std::string buf = fmt::format(
562 ":pos: {} {}",
563 int(std::floor(spritePos.x)),
564 int(std::floor(spritePos.y)));
565
566 Cel* cel = nullptr;
567 if (editor->showAutoCelGuides()) {
568 cel = editor->getSite().cel();
569 }
570
571 if (cel) {
572 buf += fmt::format(
573 " :start: {} {} :size: {} {}",
574 cel->bounds().x, cel->bounds().y,
575 cel->bounds().w, cel->bounds().h);
576 }
577 else {
578 buf += fmt::format(
579 " :size: {} {}",
580 sprite->width(),
581 sprite->height());
582 }
583
584 if (mask)
585 buf += fmt::format(" :selsize: {} {}",
586 mask->bounds().w,
587 mask->bounds().h);
588
589 if (sprite->totalFrames() > 1) {
590 buf += fmt::format(
591 " :frame: {} :clock: {}/{}",
592 site.frame()+editor->docPref().timeline.firstFrame(),
593 human_readable_time(sprite->frameDuration(site.frame())).c_str(),
594 human_readable_time(sprite->totalAnimationDuration()).c_str());
595 }
596
597 if ((editor->docPref().show.grid()) ||
598 (site.layer() && site.layer()->isTilemap())) {
599 doc::Grid grid = site.grid();
600 if (!grid.isEmpty()) {
601 gfx::Point pt = grid.canvasToTile(gfx::Point(spritePos));
602 buf += fmt::format(" :grid: {} {}", pt.x, pt.y);
603
604 // Show the tile index of this specific tile
605 if (site.layer() &&
606 site.layer()->isTilemap() &&
607 site.image()) {
608 if (site.image()->bounds().contains(pt)) {
609 buf += fmt::format(" [{}]", site.image()->getPixel(pt.x, pt.y));
610 }
611 }
612 }
613 }
614
615 if (editor->docPref().show.slices()) {
616 int count = 0;
617 for (auto slice : editor->document()->sprite()->slices()) {
618 auto key = slice->getByFrame(editor->frame());
619 if (key &&
620 key->bounds().contains(
621 int(std::floor(spritePos.x)),
622 int(std::floor(spritePos.y)))) {
623 if (++count == 3) {
624 buf += fmt::format(" :slice: ...");
625 break;
626 }
627
628 buf += fmt::format(" :slice: {}", slice->name());
629 }
630 }
631 }
632
633 StatusBar::instance()->setStatusText(0, buf);
634 }
635
636 return true;
637}
638
639DrawingState* StandbyState::startDrawingState(
640 Editor* editor,
641 const ui::MouseMessage* msg,
642 const DrawingType drawingType,
643 const tools::Pointer& pointer)
644{
645 // We need to clear and redraw the brush boundaries after the
646 // first mouse pressed/point shape if drawn. This is to avoid
647 // graphical glitches (invalid areas in the ToolLoop's src/dst
648 // images).
649 HideBrushPreview hide(editor->brushPreview());
650
651 tools::ToolLoop* toolLoop = create_tool_loop(
652 editor,
653 UIContext::instance(),
654 pointer.button(),
655 (drawingType == DrawingType::LineFreehand),
656 (drawingType == DrawingType::SelectTiles));
657 if (!toolLoop)
658 return nullptr;
659
660 EditorStatePtr newState(
661 new DrawingState(editor,
662 toolLoop,
663 drawingType));
664 editor->setState(newState);
665
666 static_cast<DrawingState*>(newState.get())
667 ->initToolLoop(editor, msg, pointer);
668
669 return static_cast<DrawingState*>(newState.get());
670}
671
672bool StandbyState::checkStartDrawingStraightLine(Editor* editor,
673 const ui::MouseMessage* msg,
674 const tools::Pointer* pointer)
675{
676 // Start line preview with shift key
677 if (canCheckStartDrawingStraightLine() &&
678 editor->startStraightLineWithFreehandTool(pointer)) {
679 tools::Pointer::Button pointerButton =
680 (pointer ? pointer->button(): tools::Pointer::Left);
681
682 DrawingState* drawingState =
683 startDrawingState(editor, msg,
684 DrawingType::LineFreehand,
685 tools::Pointer(
686 editor->document()->lastDrawingPoint(),
687 tools::Vec2(0.0f, 0.0f),
688 pointerButton,
689 pointer ? pointer->type(): PointerType::Unknown,
690 pointer ? pointer->pressure(): 0.0f));
691 if (drawingState) {
692 drawingState->sendMovementToToolLoop(
693 tools::Pointer(
694 pointer ? pointer->point(): editor->screenToEditor(editor->mousePosInDisplay()),
695 tools::Vec2(0.0f, 0.0f),
696 pointerButton,
697 pointer ? pointer->type(): tools::Pointer::Type::Unknown,
698 pointer ? pointer->pressure(): 0.0f));
699 return true;
700 }
701 }
702 return false;
703}
704
705Transformation StandbyState::getTransformation(Editor* editor)
706{
707 Transformation t = editor->document()->getTransformation();
708 set_pivot_from_preferences(t);
709 return t;
710}
711
712void StandbyState::startSelectionTransformation(Editor* editor,
713 const gfx::Point& move,
714 double angle)
715{
716 transformSelection(editor, NULL, NoHandle);
717
718 if (auto movingPixels = dynamic_cast<MovingPixelsState*>(editor->getState().get())) {
719 movingPixels->translate(gfx::PointF(move));
720 if (std::fabs(angle) > 1e-5)
721 movingPixels->rotate(angle);
722 }
723}
724
725void StandbyState::startFlipTransformation(Editor* editor, doc::algorithm::FlipType flipType)
726{
727 transformSelection(editor, NULL, NoHandle);
728
729 if (auto movingPixels = dynamic_cast<MovingPixelsState*>(editor->getState().get()))
730 movingPixels->flip(flipType);
731}
732
733void StandbyState::transformSelection(Editor* editor, MouseMessage* msg, HandleType handle)
734{
735 Doc* document = editor->document();
736 for (auto docView : UIContext::instance()->getAllDocViews(document)) {
737 if (docView->editor()->isMovingPixels()) {
738 // TODO Transfer moving pixels state to this editor
739 docView->editor()->dropMovingPixels();
740 }
741 }
742
743 // Special case: Move only selection edges
744 if (handle == MoveSelectionHandle) {
745 EditorStatePtr newState(new MovingSelectionState(editor, msg));
746 editor->setState(newState);
747 return;
748 }
749
750 Layer* layer = editor->layer();
751 if (layer && layer->isReference()) {
752 StatusBar::instance()->showTip(
753 1000,
754 fmt::format(Strings::statusbar_tips_non_transformable_reference_layer(),
755 layer->name()));
756 return;
757 }
758
759 if (layer_is_locked(editor))
760 return;
761
762 try {
763 Site site = editor->getSite();
764 ImageRef tmpImage;
765
766 if (site.layer() &&
767 site.layer()->isTilemap() &&
768 site.tilemapMode() == TilemapMode::Tiles) {
769 tmpImage.reset(new_tilemap_from_mask(site, site.document()->mask()));
770 }
771 else {
772 tmpImage.reset(new_image_from_mask(site,
773 Preferences::instance().experimental.newBlend()));
774 }
775
776 ASSERT(tmpImage);
777 if (!tmpImage) {
778 // We've received a bug report with this case, we're not sure
779 // yet how to reproduce it. Probably new_tilemap_from_mask() can
780 // return nullptr (e.g. when site.cel() is nullptr?)
781 return;
782 }
783
784 // Clear brush preview, as the extra cel will be replaced with the
785 // transformed image.
786 editor->brushPreview().hide();
787
788 PixelsMovementPtr pixelsMovement(
789 new PixelsMovement(UIContext::instance(),
790 site,
791 tmpImage.get(),
792 document->mask(),
793 "Transformation"));
794
795 // If the Ctrl key is pressed start dragging a copy of the selection
796 EditorCustomizationDelegate* customization = editor->getCustomizationDelegate();
797 if ((customization) &&
798 int(customization->getPressedKeyAction(KeyContext::TranslatingSelection) & KeyAction::CopySelection))
799 pixelsMovement->copyMask();
800 else
801 pixelsMovement->cutMask();
802
803 editor->setState(EditorStatePtr(new MovingPixelsState(editor, msg, pixelsMovement, handle)));
804 }
805 catch (const LockedDocException&) {
806 // Other editor is locking the document.
807
808 // TODO steal the PixelsMovement of the other editor and use it for this one.
809 StatusBar::instance()->showTip(
810 1000, Strings::statusbar_tips_sprite_locked_somewhere());
811 editor->showMouseCursor(kForbiddenCursor);
812 }
813 catch (const std::bad_alloc&) {
814 StatusBar::instance()->showTip(
815 1000, Strings::statusbar_tips_not_enough_transform_memory());
816 editor->showMouseCursor(kForbiddenCursor);
817 }
818}
819
820void StandbyState::callEyedropper(Editor* editor, const ui::MouseMessage* msg)
821{
822 tools::Ink* clickedInk = editor->getCurrentEditorInk();
823 if (!clickedInk->isEyedropper())
824 return;
825
826 EyedropperCommand* eyedropper =
827 (EyedropperCommand*)Commands::instance()->byId(CommandId::Eyedropper());
828 bool fg = (static_cast<tools::PickInk*>(clickedInk)->target() == tools::PickInk::Fg);
829
830 eyedropper->executeOnMousePos(UIContext::instance(), editor,
831 msg->position(), fg);
832}
833
834void StandbyState::onPivotChange(Editor* editor)
835{
836 if (editor->isActive() &&
837 editor->editorFlags() & Editor::kShowMask &&
838 editor->document()->isMaskVisible() &&
839 !editor->document()->mask()->isFrozen()) {
840 editor->invalidate();
841 }
842}
843
844gfx::Rect StandbyState::resizeCelBounds(Editor* editor) const
845{
846 gfx::Rect bounds;
847 Cel* refCel = (editor->layer() &&
848 editor->layer()->isReference() ?
849 editor->layer()->cel(editor->frame()): nullptr);
850 if (refCel) {
851 bounds = editor->editorToScreen(refCel->boundsF());
852 bounds.w /= 4;
853 bounds.h /= 4;
854 bounds.x += 3*bounds.w;
855 bounds.y += 3*bounds.h;
856 }
857 return bounds;
858}
859
860bool StandbyState::overSelectionEdges(Editor* editor,
861 const gfx::Point& mouseScreenPos) const
862{
863 // Move selection edges
864 if (Preferences::instance().selection.moveEdges() &&
865 editor->isActive() &&
866 editor->document()->isMaskVisible() &&
867 editor->document()->hasMaskBoundaries() &&
868 // TODO improve this check, how we can know that we aren't in the MovingPixelsState
869 !dynamic_cast<MovingPixelsState*>(editor->getState().get())) {
870 gfx::Point mainOffset(editor->mainTilePosition());
871
872 // For each selection edge
873 for (const auto& seg : editor->document()->maskBoundaries()) {
874 gfx::Rect segBounds = seg.bounds();
875 segBounds.offset(mainOffset);
876 segBounds = editor->editorToScreen(segBounds);
877 if (seg.vertical())
878 segBounds.w = 1;
879 else
880 segBounds.h = 1;
881
882 if (gfx::Rect(segBounds).enlarge(2*guiscale()).contains(mouseScreenPos) &&
883 !gfx::Rect(segBounds).shrink(2*guiscale()).contains(mouseScreenPos)) {
884 return true;
885 }
886 }
887 }
888 return false;
889}
890
891//////////////////////////////////////////////////////////////////////
892// Decorator
893
894StandbyState::Decorator::Decorator(StandbyState* standbyState)
895 : m_transfHandles(NULL)
896 , m_standbyState(standbyState)
897{
898}
899
900StandbyState::Decorator::~Decorator()
901{
902 delete m_transfHandles;
903}
904
905TransformHandles* StandbyState::Decorator::getTransformHandles(Editor* editor)
906{
907 if (!m_transfHandles)
908 m_transfHandles = new TransformHandles();
909
910 return m_transfHandles;
911}
912
913bool StandbyState::Decorator::onSetCursor(tools::Ink* ink, Editor* editor, const gfx::Point& mouseScreenPos)
914{
915 if (!editor->isActive())
916 return false;
917
918 if (ink &&
919 ink->isSelection() &&
920 editor->document()->isMaskVisible() &&
921 (!Preferences::instance().selection.modifiersDisableHandles() ||
922 os::instance()->keyModifiers() == kKeyNoneModifier)) {
923 auto theme = skin::SkinTheme::get(editor);
924 const Transformation transformation(m_standbyState->getTransformation(editor));
925 TransformHandles* tr = getTransformHandles(editor);
926 HandleType handle = tr->getHandleAtPoint(
927 editor, mouseScreenPos, transformation);
928
929 CursorType newCursorType = kArrowCursor;
930 const Cursor* newCursor = nullptr;
931
932 auto corners = transformation.transformedCorners();
933 auto A = corners[Transformation::Corners::LEFT_TOP];
934 auto B = corners[Transformation::Corners::RIGHT_TOP];
935 auto C = corners[Transformation::Corners::LEFT_BOTTOM];
936 auto D = corners[Transformation::Corners::RIGHT_BOTTOM];
937 vec2 v;
938
939 switch (handle) {
940 case ScaleNWHandle: case RotateNWHandle: v = to_vec2(A - D); break;
941 case ScaleNEHandle: case RotateNEHandle: v = to_vec2(B - C); break;
942 case ScaleSWHandle: case RotateSWHandle: v = to_vec2(C - B); break;
943 case ScaleSEHandle: case RotateSEHandle: v = to_vec2(D - A); break;
944 case ScaleNHandle: v = to_vec2(A - C); break;
945 case ScaleEHandle: v = to_vec2(B - A); break;
946 case ScaleSHandle: v = to_vec2(C - A); break;
947 case ScaleWHandle: v = to_vec2(A - B); break;
948 case SkewNHandle:
949 v = to_vec2(B - A);
950 v = vec2(v.y, -v.x);
951 break;
952 case SkewEHandle:
953 v = to_vec2(D - B);
954 v = vec2(v.y, -v.x);
955 break;
956 case SkewSHandle:
957 v = to_vec2(C - D);
958 v = vec2(v.y, -v.x);
959 break;
960 case SkewWHandle:
961 v = to_vec2(A - C);
962 v = vec2(v.y, -v.x);
963 break;
964 case PivotHandle:
965 break;
966 default:
967 // The cursor will be set by Editor::onSetCursor()
968 return false;
969 }
970
971 double angle = v.angle();
972 angle = base::fmod_radians(angle) + PI;
973 ASSERT(angle >= 0.0 && angle <= 2*PI);
974 const int angleInt = std::clamp<int>(std::floor(8.0 * angle / (2.0*PI) + 0.5), 0, 8) % 8;
975
976 if (handle == PivotHandle) {
977 newCursorType = kHandCursor;
978 }
979 else if (handle >= ScaleNWHandle &&
980 handle <= ScaleSEHandle) {
981 const CursorType rotated_size_cursors[8] = {
982 kSizeWCursor, kSizeNWCursor, kSizeNCursor, kSizeNECursor,
983 kSizeECursor, kSizeSECursor, kSizeSCursor, kSizeSWCursor
984 };
985 newCursorType = rotated_size_cursors[angleInt];
986 }
987 else if (handle >= RotateNWHandle &&
988 handle <= RotateSEHandle) {
989 const Cursor* rotated_rotate_cursors[8] = {
990 theme->cursors.rotateW(), theme->cursors.rotateNw(), theme->cursors.rotateN(), theme->cursors.rotateNe(),
991 theme->cursors.rotateE(), theme->cursors.rotateSe(), theme->cursors.rotateS(), theme->cursors.rotateSw()
992 };
993 newCursor = rotated_rotate_cursors[angleInt];
994 newCursorType = kCustomCursor;
995 }
996 else if (handle >= SkewNHandle &&
997 handle <= SkewSHandle) {
998 const Cursor* rotated_skew_cursors[8] = {
999 theme->cursors.skewW(), theme->cursors.skewNw(), theme->cursors.skewN(), theme->cursors.skewNe(),
1000 theme->cursors.skewE(), theme->cursors.skewSe(), theme->cursors.skewS(), theme->cursors.skewSw()
1001 };
1002 newCursor = rotated_skew_cursors[angleInt];
1003 newCursorType = kCustomCursor;
1004 }
1005
1006 editor->showMouseCursor(newCursorType, newCursor);
1007 return true;
1008 }
1009
1010 // Move symmetry
1011 Handles handles;
1012 if (getSymmetryHandles(editor, handles)) {
1013 for (const auto& handle : handles) {
1014 if (handle.bounds.contains(mouseScreenPos)) {
1015 switch (handle.align) {
1016 case TOP:
1017 case BOTTOM:
1018 editor->showMouseCursor(kSizeWECursor);
1019 break;
1020 case LEFT:
1021 case RIGHT:
1022 editor->showMouseCursor(kSizeNSCursor);
1023 break;
1024 }
1025 return true;
1026 }
1027 }
1028 }
1029
1030 return false;
1031}
1032
1033void StandbyState::Decorator::postRenderDecorator(EditorPostRender* render)
1034{
1035 Editor* editor = render->getEditor();
1036
1037 // Draw transformation handles (if the mask is visible and isn't frozen).
1038 if (editor->isActive() &&
1039 editor->editorFlags() & Editor::kShowMask &&
1040 editor->document()->isMaskVisible() &&
1041 !editor->document()->mask()->isFrozen()) {
1042 // And draw only when the user has a selection tool as active tool.
1043 tools::Ink* ink = editor->getCurrentEditorInk();
1044
1045 if (ink->isSelection()) {
1046 getTransformHandles(editor)
1047 ->drawHandles(editor, render->getGraphics(),
1048 m_standbyState->getTransformation(editor));
1049
1050 m_standbyState->m_transformSelectionHandlesAreVisible = true;
1051 }
1052 }
1053
1054 // Draw transformation handles (if the mask is visible and isn't frozen).
1055 Handles handles;
1056 if (StandbyState::Decorator::getSymmetryHandles(editor, handles)) {
1057 auto theme = skin::SkinTheme::get(editor);
1058 os::Surface* part = theme->parts.transformationHandle()->bitmap(0);
1059 ScreenGraphics g(editor->display());
1060 for (const auto& handle : handles)
1061 g.drawRgbaSurface(part, handle.bounds.x, handle.bounds.y);
1062 }
1063}
1064
1065void StandbyState::Decorator::getInvalidDecoratoredRegion(Editor* editor, gfx::Region& region)
1066{
1067 Handles handles;
1068 if (getSymmetryHandles(editor, handles)) {
1069 for (const auto& handle : handles)
1070 region.createUnion(region, gfx::Region(handle.bounds));
1071 }
1072}
1073
1074bool StandbyState::Decorator::getSymmetryHandles(Editor* editor, Handles& handles)
1075{
1076 // Draw transformation handles (if the mask is visible and isn't frozen).
1077 if (editor->isActive() &&
1078 editor->editorFlags() & Editor::kShowSymmetryLine &&
1079 Preferences::instance().symmetryMode.enabled()) {
1080 const auto& symmetry = Preferences::instance().document(editor->document()).symmetry;
1081 auto mode = symmetry.mode();
1082 if (mode != app::gen::SymmetryMode::NONE) {
1083 gfx::Rect mainTileBounds(editor->mainTilePosition(),
1084 editor->sprite()->bounds().size());
1085 gfx::Rect canvasBounds(gfx::Point(0, 0),
1086 editor->canvasSize());
1087 gfx::RectF editorViewport(View::getView(editor)->viewportBounds());
1088 auto theme = skin::SkinTheme::get(editor);
1089 os::Surface* part = theme->parts.transformationHandle()->bitmap(0);
1090
1091 if (int(mode) & int(app::gen::SymmetryMode::HORIZONTAL)) {
1092 double pos = symmetry.xAxis();
1093 gfx::PointF pt1, pt2;
1094
1095 pt1 = gfx::PointF(mainTileBounds.x+pos, canvasBounds.y);
1096 pt1 = editor->editorToScreenF(pt1);
1097 pt2 = gfx::PointF(mainTileBounds.x+pos, canvasBounds.y2());
1098 pt2 = editor->editorToScreenF(pt2);
1099 pt1.y = std::max(pt1.y-part->height(), editorViewport.y);
1100 pt2.y = std::min(pt2.y, editorViewport.point2().y-part->height());
1101 pt1.x -= part->width()/2;
1102 pt2.x -= part->width()/2;
1103
1104 handles.push_back(
1105 Handle(TOP,
1106 gfx::Rect(int(pt1.x), int(pt1.y), part->width(), part->height())));
1107 handles.push_back(
1108 Handle(BOTTOM,
1109 gfx::Rect(int(pt2.x), int(pt2.y), part->width(), part->height())));
1110 }
1111
1112 if (int(mode) & int(app::gen::SymmetryMode::VERTICAL)) {
1113 double pos = symmetry.yAxis();
1114 gfx::PointF pt1, pt2;
1115
1116 pt1 = gfx::PointF(canvasBounds.x, mainTileBounds.y+pos);
1117 pt1 = editor->editorToScreenF(pt1);
1118 pt2 = gfx::PointF(canvasBounds.x2(), mainTileBounds.y+pos);
1119 pt2 = editor->editorToScreenF(pt2);
1120 pt1.x = std::max(pt1.x-part->width(), editorViewport.x);
1121 pt2.x = std::min(pt2.x, editorViewport.point2().x-part->width());
1122 pt1.y -= part->height()/2;
1123 pt2.y -= part->height()/2;
1124
1125 handles.push_back(
1126 Handle(LEFT,
1127 gfx::Rect(int(pt1.x), int(pt1.y), part->width(), part->height())));
1128 handles.push_back(
1129 Handle(RIGHT,
1130 gfx::Rect(int(pt2.x), int(pt2.y), part->width(), part->height())));
1131 }
1132
1133 return true;
1134 }
1135 }
1136 return false;
1137}
1138
1139} // namespace app
1140