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
44namespace app {
45
46using namespace ui;
47
48static 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
62DrawingState::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
80DrawingState::~DrawingState()
81{
82 destroyLoop(nullptr);
83}
84
85void 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
131void DrawingState::sendMovementToToolLoop(const tools::Pointer& pointer)
132{
133 ASSERT(m_toolLoopManager);
134 m_lastPointer = pointer;
135 m_toolLoopManager->movement(pointer);
136}
137
138void DrawingState::notifyToolLoopModifiersChange(Editor* editor)
139{
140 if (!m_toolLoopManager->isCanceled())
141 m_toolLoopManager->notifyToolLoopModifiersChange();
142}
143
144void DrawingState::onBeforePopState(Editor* editor)
145{
146 m_beforeCmdConn.disconnect();
147 StandbyState::onBeforePopState(editor);
148}
149
150bool 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
199bool 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
245bool 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
283void 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
293bool 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
306bool DrawingState::onKeyDown(Editor* editor, KeyMessage* msg)
307{
308 Command* command = NULL;
309 Params params;
310 if (KeyboardShortcuts::instance()
311 ->getCommandFromKeyMessage(msg, &command, &params)) {
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
327bool 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
344bool 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
362bool 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
369void DrawingState::onExposeSpritePixels(const gfx::Region& rgn)
370{
371 if (m_toolLoop)
372 m_toolLoop->validateDstImage(rgn);
373}
374
375bool 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
385void DrawingState::handleMouseMovement()
386{
387 // Notify mouse movement to the tool
388 ASSERT(m_toolLoopManager);
389 m_toolLoopManager->movement(m_lastPointer);
390}
391
392bool 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
404bool 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
415void 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
436void 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
448void 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