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/drawing_state.h" |
13 | |
14 | #include "app/commands/command.h" |
15 | #include "app/commands/commands.h" |
16 | #include "app/commands/params.h" |
17 | #include "app/tools/controller.h" |
18 | #include "app/tools/ink.h" |
19 | #include "app/tools/tool.h" |
20 | #include "app/tools/tool_loop.h" |
21 | #include "app/tools/tool_loop_manager.h" |
22 | #include "app/ui/editor/editor.h" |
23 | #include "app/ui/editor/editor_customization_delegate.h" |
24 | #include "app/ui/editor/editor_render.h" |
25 | #include "app/ui/editor/glue.h" |
26 | #include "app/ui/keyboard_shortcuts.h" |
27 | #include "app/ui/skin/skin_theme.h" |
28 | #include "app/ui_context.h" |
29 | #include "base/scoped_value.h" |
30 | #include "doc/layer.h" |
31 | #include "ui/message.h" |
32 | #include "ui/system.h" |
33 | |
34 | // TODO remove these headers and make this state observable by the timeline. |
35 | #include "app/app.h" |
36 | #include "app/ui/main_window.h" |
37 | #include "app/ui/timeline/timeline.h" |
38 | |
39 | #include <algorithm> |
40 | #include <cmath> |
41 | #include <cstdlib> |
42 | #include <cstring> |
43 | |
44 | namespace app { |
45 | |
46 | using namespace ui; |
47 | |
48 | static int get_delay_interval_for_tool_loop(tools::ToolLoop* toolLoop) |
49 | { |
50 | if (toolLoop->getTracePolicy() == tools::TracePolicy::Last) { |
51 | // We use the delayed mouse movement for tools like Line, |
52 | // Rectangle, etc. (tools that use the last mouse position for its |
53 | // shape, so we can discard intermediate positions). |
54 | return 5; |
55 | } |
56 | else { |
57 | // Without delay for freehand-like tools |
58 | return 0; |
59 | } |
60 | } |
61 | |
62 | DrawingState::DrawingState(Editor* editor, |
63 | tools::ToolLoop* toolLoop, |
64 | const DrawingType type) |
65 | : m_editor(editor) |
66 | , m_type(type) |
67 | , m_delayedMouseMove(this, editor, |
68 | get_delay_interval_for_tool_loop(toolLoop)) |
69 | , m_toolLoop(toolLoop) |
70 | , m_toolLoopManager(new tools::ToolLoopManager(toolLoop)) |
71 | , m_mouseMoveReceived(false) |
72 | , m_mousePressedReceived(false) |
73 | , m_processScrollChange(true) |
74 | { |
75 | m_beforeCmdConn = |
76 | UIContext::instance()->BeforeCommandExecution.connect( |
77 | &DrawingState::onBeforeCommandExecution, this); |
78 | } |
79 | |
80 | DrawingState::~DrawingState() |
81 | { |
82 | destroyLoop(nullptr); |
83 | } |
84 | |
85 | void DrawingState::initToolLoop(Editor* editor, |
86 | const ui::MouseMessage* msg, |
87 | const tools::Pointer& pointer) |
88 | { |
89 | if (msg) |
90 | m_delayedMouseMove.onMouseDown(msg); |
91 | else |
92 | m_delayedMouseMove.initSpritePos(gfx::PointF(pointer.point())); |
93 | |
94 | Tileset* tileset = m_toolLoop->getDstTileset(); |
95 | |
96 | // For selection inks we don't use a "the selected layer" for |
97 | // preview purposes, because we want the selection feedback to be at |
98 | // the top of all layers. |
99 | Layer* previewLayer = (m_toolLoop->getInk()->isSelection() ? nullptr: |
100 | m_toolLoop->getLayer()); |
101 | |
102 | // Prepare preview image (the destination image will be our preview |
103 | // in the tool-loop time, so we can see what we are drawing) |
104 | editor->renderEngine().setPreviewImage( |
105 | previewLayer, |
106 | m_toolLoop->getFrame(), |
107 | tileset ? nullptr: m_toolLoop->getDstImage(), |
108 | tileset, |
109 | m_toolLoop->getCelOrigin(), |
110 | (previewLayer && |
111 | previewLayer->isImage() ? |
112 | static_cast<LayerImage*>(m_toolLoop->getLayer())->blendMode(): |
113 | doc::BlendMode::NEG_BW)); // To preview the selection ink we use the negative black & white blender |
114 | |
115 | ASSERT(!m_toolLoopManager->isCanceled()); |
116 | |
117 | m_velocity.reset(); |
118 | m_lastPointer = pointer; |
119 | m_mouseDownPos = (msg ? msg->position(): |
120 | editor->editorToScreen(pointer.point())); |
121 | m_mouseDownTime = base::current_tick(); |
122 | |
123 | m_toolLoopManager->prepareLoop(pointer); |
124 | m_toolLoopManager->pressButton(pointer); |
125 | |
126 | ASSERT(!m_toolLoopManager->isCanceled()); |
127 | |
128 | editor->captureMouse(); |
129 | } |
130 | |
131 | void DrawingState::sendMovementToToolLoop(const tools::Pointer& pointer) |
132 | { |
133 | ASSERT(m_toolLoopManager); |
134 | m_lastPointer = pointer; |
135 | m_toolLoopManager->movement(pointer); |
136 | } |
137 | |
138 | void DrawingState::notifyToolLoopModifiersChange(Editor* editor) |
139 | { |
140 | if (!m_toolLoopManager->isCanceled()) |
141 | m_toolLoopManager->notifyToolLoopModifiersChange(); |
142 | } |
143 | |
144 | void DrawingState::onBeforePopState(Editor* editor) |
145 | { |
146 | m_beforeCmdConn.disconnect(); |
147 | StandbyState::onBeforePopState(editor); |
148 | } |
149 | |
150 | bool DrawingState::onMouseDown(Editor* editor, MouseMessage* msg) |
151 | { |
152 | // Drawing loop |
153 | ASSERT(m_toolLoopManager != NULL); |
154 | |
155 | if (!editor->hasCapture()) |
156 | editor->captureMouse(); |
157 | |
158 | tools::Pointer pointer = pointer_from_msg(editor, msg, |
159 | m_velocity.velocity()); |
160 | m_lastPointer = pointer; |
161 | |
162 | m_delayedMouseMove.onMouseDown(msg); |
163 | |
164 | // Check if this drawing state was started with a Shift+Pencil tool |
165 | // and now the user pressed the right button to draw the straight |
166 | // line with the background color. |
167 | bool recreateLoop = false; |
168 | if (m_type == DrawingType::LineFreehand && |
169 | !m_mousePressedReceived) { |
170 | recreateLoop = true; |
171 | } |
172 | |
173 | m_mousePressedReceived = true; |
174 | |
175 | // Notify the mouse button down to the tool loop manager. |
176 | m_toolLoopManager->pressButton(pointer); |
177 | |
178 | // Store the isCanceled flag, because destroyLoopIfCanceled might |
179 | // destroy the tool loop manager. |
180 | ASSERT(m_toolLoopManager); |
181 | const bool isCanceled = m_toolLoopManager->isCanceled(); |
182 | |
183 | // The user might have canceled by the tool loop clicking with the |
184 | // secondary mouse button. |
185 | destroyLoopIfCanceled(editor); |
186 | |
187 | // In this case, the user canceled the straight line preview |
188 | // (Shift+Pencil) pressing the right-click (other mouse button). Now |
189 | // we have to restart the loop calling |
190 | // checkStartDrawingStraightLine() with the right-button. |
191 | if (recreateLoop && isCanceled) { |
192 | ASSERT(!m_toolLoopManager); |
193 | checkStartDrawingStraightLine(editor, msg, &pointer); |
194 | } |
195 | |
196 | return true; |
197 | } |
198 | |
199 | bool DrawingState::onMouseUp(Editor* editor, MouseMessage* msg) |
200 | { |
201 | ASSERT(m_toolLoopManager != NULL); |
202 | |
203 | m_lastPointer = pointer_from_msg(editor, msg, m_velocity.velocity()); |
204 | m_delayedMouseMove.onMouseUp(msg); |
205 | |
206 | // Selection tools with Replace mode are cancelled with a simple click. |
207 | // ("one point" controller selection tool i.e. the magic wand, and |
208 | // selection tools with Add or Subtract mode aren't cancelled with |
209 | // one click). |
210 | if (!m_toolLoop->getInk()->isSelection() || |
211 | m_toolLoop->getController()->isOnePoint() || |
212 | !canInterpretMouseMovementAsJustOneClick() || |
213 | // In case of double-click (to select tiles) we don't want to |
214 | // deselect if the mouse is not moved. In this case the tile |
215 | // will be selected anyway even if the mouse is not moved. |
216 | m_type == DrawingType::SelectTiles || |
217 | (editor->getToolLoopModifiers() != tools::ToolLoopModifiers::kReplaceSelection && |
218 | editor->getToolLoopModifiers() != tools::ToolLoopModifiers::kIntersectSelection)) { |
219 | // Notify the release of the mouse button to the tool loop |
220 | // manager. This is the correct way to say "the user finishes the |
221 | // drawing trace correctly". |
222 | if (m_toolLoopManager->releaseButton(m_lastPointer)) |
223 | return true; |
224 | } |
225 | |
226 | destroyLoop(editor); |
227 | |
228 | // Back to standby state. |
229 | editor->backToPreviousState(); |
230 | editor->releaseMouse(); |
231 | |
232 | // Update the timeline. TODO make this state observable by the timeline. |
233 | App::instance()->timeline()->updateUsingEditor(editor); |
234 | |
235 | // Restart again? Here we handle the case to draw multiple lines |
236 | // using Shift+click with the Pencil tool. When we release the mouse |
237 | // button, if the Shift key is pressed, the whole ToolLoop starts |
238 | // again. |
239 | if (Preferences::instance().editor.straightLinePreview()) |
240 | checkStartDrawingStraightLine(editor, msg, &m_lastPointer); |
241 | |
242 | return true; |
243 | } |
244 | |
245 | bool DrawingState::onMouseMove(Editor* editor, MouseMessage* msg) |
246 | { |
247 | // It's needed to avoid some glitches with brush boundaries. |
248 | // |
249 | // TODO we should be able to avoid this if we correctly invalidate |
250 | // the BrushPreview::m_clippingRegion |
251 | HideBrushPreview hide(editor->brushPreview()); |
252 | |
253 | // Don't process onScrollChange() messages if autoScroll() changes |
254 | // the scroll. |
255 | base::ScopedValue<bool> disableScroll(m_processScrollChange, |
256 | false, m_processScrollChange); |
257 | |
258 | // Update velocity sensor. |
259 | m_velocity.updateWithDisplayPoint(msg->position()); |
260 | |
261 | // Update pointer with new mouse position |
262 | m_lastPointer = tools::Pointer(gfx::Point(m_delayedMouseMove.spritePos()), |
263 | m_velocity.velocity(), |
264 | button_from_msg(msg), |
265 | msg->pointerType(), |
266 | msg->pressure()); |
267 | |
268 | // Indicate that we've received a real mouse movement event here |
269 | // (used in the Rectangular Marquee to deselect when we just do a |
270 | // simple click without moving the mouse). |
271 | m_mouseMoveReceived = true; |
272 | gfx::Point delta = (msg->position() - m_mouseDownPos); |
273 | m_mouseMaxDelta.x = std::max(m_mouseMaxDelta.x, std::abs(delta.x)); |
274 | m_mouseMaxDelta.y = std::max(m_mouseMaxDelta.y, std::abs(delta.y)); |
275 | |
276 | // Use DelayedMouseMove for tools like line, rectangle, etc. (that |
277 | // use the only the last mouse position) to filter out rapid mouse |
278 | // movement. |
279 | m_delayedMouseMove.onMouseMove(msg); |
280 | return true; |
281 | } |
282 | |
283 | void DrawingState::onCommitMouseMove(Editor* editor, |
284 | const gfx::PointF& spritePos) |
285 | { |
286 | if (m_toolLoop && |
287 | m_toolLoopManager && |
288 | !m_toolLoopManager->isCanceled()) { |
289 | handleMouseMovement(); |
290 | } |
291 | } |
292 | |
293 | bool DrawingState::onSetCursor(Editor* editor, const gfx::Point& mouseScreenPos) |
294 | { |
295 | if (m_toolLoop->getInk()->isEyedropper()) { |
296 | auto theme = skin::SkinTheme::get(editor); |
297 | editor->showMouseCursor( |
298 | kCustomCursor, theme->cursors.eyedropper()); |
299 | } |
300 | else { |
301 | editor->showBrushPreview(mouseScreenPos); |
302 | } |
303 | return true; |
304 | } |
305 | |
306 | bool DrawingState::onKeyDown(Editor* editor, KeyMessage* msg) |
307 | { |
308 | Command* command = NULL; |
309 | Params params; |
310 | if (KeyboardShortcuts::instance() |
311 | ->getCommandFromKeyMessage(msg, &command, ¶ms)) { |
312 | // We accept some commands... |
313 | if (command->id() == CommandId::Zoom() || |
314 | command->id() == CommandId::Undo() || |
315 | command->id() == CommandId::Redo() || |
316 | command->id() == CommandId::Cancel()) { |
317 | UIContext::instance()->executeCommandFromMenuOrShortcut(command, params); |
318 | return true; |
319 | } |
320 | } |
321 | |
322 | // Return true when we cannot execute commands (true = the onKeyDown |
323 | // event was used, so the key is not used to run a command). |
324 | return !canExecuteCommands(); |
325 | } |
326 | |
327 | bool DrawingState::onKeyUp(Editor* editor, KeyMessage* msg) |
328 | { |
329 | // Cancel loop pressing Esc key... |
330 | if (msg->scancode() == ui::kKeyEsc || |
331 | // Cancel "Shift on freehand" line preview when the Shift key is |
332 | // released and the user didn't press the mouse button. |
333 | (m_type == DrawingType::LineFreehand && |
334 | !m_mousePressedReceived && |
335 | !editor->startStraightLineWithFreehandTool(nullptr))) { |
336 | m_toolLoopManager->cancel(); |
337 | } |
338 | |
339 | // The user might have canceled the tool loop pressing the 'Esc' key. |
340 | destroyLoopIfCanceled(editor); |
341 | return true; |
342 | } |
343 | |
344 | bool DrawingState::onScrollChange(Editor* editor) |
345 | { |
346 | if (m_processScrollChange) { |
347 | gfx::Point mousePos = editor->mousePosInDisplay(); |
348 | |
349 | // Update velocity sensor. |
350 | m_velocity.updateWithDisplayPoint(mousePos); // TODO add scroll as velocity? |
351 | |
352 | m_lastPointer = tools::Pointer(editor->screenToEditor(mousePos), |
353 | m_velocity.velocity(), |
354 | m_lastPointer.button(), |
355 | m_lastPointer.type(), |
356 | m_lastPointer.pressure()); |
357 | handleMouseMovement(); |
358 | } |
359 | return true; |
360 | } |
361 | |
362 | bool DrawingState::onUpdateStatusBar(Editor* editor) |
363 | { |
364 | // The status bar is updated by ToolLoopImpl::updateStatusBar() |
365 | // method called by the ToolLoopManager. |
366 | return false; |
367 | } |
368 | |
369 | void DrawingState::onExposeSpritePixels(const gfx::Region& rgn) |
370 | { |
371 | if (m_toolLoop) |
372 | m_toolLoop->validateDstImage(rgn); |
373 | } |
374 | |
375 | bool DrawingState::getGridBounds(Editor* editor, gfx::Rect& gridBounds) |
376 | { |
377 | if (m_toolLoop) { |
378 | gridBounds = m_toolLoop->getGridBounds(); |
379 | return true; |
380 | } |
381 | else |
382 | return false; |
383 | } |
384 | |
385 | void DrawingState::handleMouseMovement() |
386 | { |
387 | // Notify mouse movement to the tool |
388 | ASSERT(m_toolLoopManager); |
389 | m_toolLoopManager->movement(m_lastPointer); |
390 | } |
391 | |
392 | bool DrawingState::canInterpretMouseMovementAsJustOneClick() |
393 | { |
394 | // If the user clicked (pressed and released the mouse button) in |
395 | // less than 250 milliseconds in "the same place" (inside a 7 pixels |
396 | // rectangle actually, to detect stylus shake). |
397 | return |
398 | !m_mouseMoveReceived || |
399 | (m_mouseMaxDelta.x < 4 && |
400 | m_mouseMaxDelta.y < 4 && |
401 | (base::current_tick() - m_mouseDownTime < 250)); |
402 | } |
403 | |
404 | bool DrawingState::canExecuteCommands() |
405 | { |
406 | // Returning true here means that the user can trigger commands with |
407 | // keyboard shortcuts. In our case we want to be able to use |
408 | // keyboard shortcuts only when the Shift key was pressed to run a |
409 | // command (e.g. Shift+N), not to draw a straight line from the |
410 | // pencil (freehand) tool. |
411 | return (m_type == DrawingType::LineFreehand && |
412 | !m_mousePressedReceived); |
413 | } |
414 | |
415 | void DrawingState::onBeforeCommandExecution(CommandExecutionEvent& ev) |
416 | { |
417 | if (!m_toolLoop) |
418 | return; |
419 | |
420 | if (canExecuteCommands() || |
421 | // Undo/Redo/Cancel will cancel the ToolLoop |
422 | ev.command()->id() == CommandId::Undo() || |
423 | ev.command()->id() == CommandId::Redo() || |
424 | ev.command()->id() == CommandId::Cancel()) { |
425 | if (!canExecuteCommands()) { |
426 | // Cancel the execution of Undo/Redo/Cancel because we've |
427 | // simulated it here |
428 | ev.cancel(); |
429 | } |
430 | |
431 | m_toolLoopManager->cancel(); |
432 | destroyLoopIfCanceled(m_editor); |
433 | } |
434 | } |
435 | |
436 | void DrawingState::destroyLoopIfCanceled(Editor* editor) |
437 | { |
438 | // Cancel drawing loop |
439 | if (m_toolLoopManager->isCanceled()) { |
440 | destroyLoop(editor); |
441 | |
442 | // Change to standby state |
443 | editor->backToPreviousState(); |
444 | editor->releaseMouse(); |
445 | } |
446 | } |
447 | |
448 | void DrawingState::destroyLoop(Editor* editor) |
449 | { |
450 | if (editor) |
451 | editor->renderEngine().removePreviewImage(); |
452 | |
453 | if (m_toolLoopManager) |
454 | m_toolLoopManager->end(); |
455 | |
456 | m_toolLoopManager.reset(nullptr); |
457 | m_toolLoop.reset(nullptr); |
458 | |
459 | app_rebuild_documents_tabs(); |
460 | } |
461 | |
462 | } // namespace app |
463 | |