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 | |
75 | namespace app { |
76 | |
77 | using namespace ui; |
78 | |
79 | #ifdef _MSC_VER |
80 | #pragma warning(disable:4355) // warning C4355: 'this' : used in base member initializer list |
81 | #endif |
82 | |
83 | StandbyState::StandbyState() |
84 | : m_decorator(new Decorator(this)) |
85 | , m_transformSelectionHandlesAreVisible(false) |
86 | { |
87 | } |
88 | |
89 | StandbyState::~StandbyState() |
90 | { |
91 | delete m_decorator; |
92 | } |
93 | |
94 | void 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 | |
108 | void 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 | |
121 | bool 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* = 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 | |
353 | bool StandbyState::onMouseUp(Editor* editor, MouseMessage* msg) |
354 | { |
355 | editor->releaseMouse(); |
356 | return true; |
357 | } |
358 | |
359 | bool 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 | |
374 | bool 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 | |
409 | bool 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 | |
493 | bool 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 | |
518 | bool StandbyState::onKeyUp(Editor* editor, KeyMessage* msg) |
519 | { |
520 | return false; |
521 | } |
522 | |
523 | bool 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 | |
639 | DrawingState* 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 | |
672 | bool 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 | |
705 | Transformation StandbyState::getTransformation(Editor* editor) |
706 | { |
707 | Transformation t = editor->document()->getTransformation(); |
708 | set_pivot_from_preferences(t); |
709 | return t; |
710 | } |
711 | |
712 | void 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 | |
725 | void 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 | |
733 | void 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 | |
820 | void 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 | |
834 | void 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 | |
844 | gfx::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 | |
860 | bool 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 | |
894 | StandbyState::Decorator::Decorator(StandbyState* standbyState) |
895 | : m_transfHandles(NULL) |
896 | , m_standbyState(standbyState) |
897 | { |
898 | } |
899 | |
900 | StandbyState::Decorator::~Decorator() |
901 | { |
902 | delete m_transfHandles; |
903 | } |
904 | |
905 | TransformHandles* StandbyState::Decorator::getTransformHandles(Editor* editor) |
906 | { |
907 | if (!m_transfHandles) |
908 | m_transfHandles = new TransformHandles(); |
909 | |
910 | return m_transfHandles; |
911 | } |
912 | |
913 | bool 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 | |
1033 | void 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 | |
1065 | void 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 | |
1074 | bool 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 | |