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