| 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/editor.h" |
| 13 | |
| 14 | #include "app/app.h" |
| 15 | #include "app/color.h" |
| 16 | #include "app/color_picker.h" |
| 17 | #include "app/color_utils.h" |
| 18 | #include "app/commands/commands.h" |
| 19 | #include "app/commands/params.h" |
| 20 | #include "app/commands/quick_command.h" |
| 21 | #include "app/console.h" |
| 22 | #include "app/doc_event.h" |
| 23 | #include "app/i18n/strings.h" |
| 24 | #include "app/ini_file.h" |
| 25 | #include "app/modules/editors.h" |
| 26 | #include "app/modules/gfx.h" |
| 27 | #include "app/modules/gui.h" |
| 28 | #include "app/modules/palettes.h" |
| 29 | #include "app/pref/preferences.h" |
| 30 | #include "app/snap_to_grid.h" |
| 31 | #include "app/tools/active_tool.h" |
| 32 | #include "app/tools/controller.h" |
| 33 | #include "app/tools/ink.h" |
| 34 | #include "app/tools/tool.h" |
| 35 | #include "app/tools/tool_box.h" |
| 36 | #include "app/ui/color_bar.h" |
| 37 | #include "app/ui/context_bar.h" |
| 38 | #include "app/ui/editor/drawing_state.h" |
| 39 | #include "app/ui/editor/editor_customization_delegate.h" |
| 40 | #include "app/ui/editor/editor_decorator.h" |
| 41 | #include "app/ui/editor/editor_render.h" |
| 42 | #include "app/ui/editor/glue.h" |
| 43 | #include "app/ui/editor/moving_pixels_state.h" |
| 44 | #include "app/ui/editor/pixels_movement.h" |
| 45 | #include "app/ui/editor/play_state.h" |
| 46 | #include "app/ui/editor/scrolling_state.h" |
| 47 | #include "app/ui/editor/standby_state.h" |
| 48 | #include "app/ui/editor/zooming_state.h" |
| 49 | #include "app/ui/main_window.h" |
| 50 | #include "app/ui/skin/skin_theme.h" |
| 51 | #include "app/ui/status_bar.h" |
| 52 | #include "app/ui/timeline/timeline.h" |
| 53 | #include "app/ui/toolbar.h" |
| 54 | #include "app/ui_context.h" |
| 55 | #include "app/util/conversion_to_surface.h" |
| 56 | #include "app/util/layer_utils.h" |
| 57 | #include "base/chrono.h" |
| 58 | #include "base/convert_to.h" |
| 59 | #include "doc/doc.h" |
| 60 | #include "doc/mask_boundaries.h" |
| 61 | #include "doc/slice.h" |
| 62 | #include "fmt/format.h" |
| 63 | #include "os/color_space.h" |
| 64 | #include "os/sampling.h" |
| 65 | #include "os/surface.h" |
| 66 | #include "os/system.h" |
| 67 | #include "render/rasterize.h" |
| 68 | #include "ui/ui.h" |
| 69 | |
| 70 | #include <algorithm> |
| 71 | #include <cmath> |
| 72 | #include <cstdio> |
| 73 | #include <limits> |
| 74 | #include <memory> |
| 75 | |
| 76 | namespace app { |
| 77 | |
| 78 | using namespace app::skin; |
| 79 | using namespace gfx; |
| 80 | using namespace ui; |
| 81 | using namespace render; |
| 82 | |
| 83 | // TODO these should be grouped in some kind of "performance counters" |
| 84 | static base::Chrono renderChrono; |
| 85 | static double renderElapsed = 0.0; |
| 86 | |
| 87 | class EditorPostRenderImpl : public EditorPostRender { |
| 88 | public: |
| 89 | EditorPostRenderImpl(Editor* editor, Graphics* g) |
| 90 | : m_editor(editor) |
| 91 | , m_g(g) { |
| 92 | } |
| 93 | |
| 94 | Editor* getEditor() override { |
| 95 | return m_editor; |
| 96 | } |
| 97 | |
| 98 | Graphics* getGraphics() override { |
| 99 | return m_g; |
| 100 | } |
| 101 | |
| 102 | void drawLine(gfx::Color color, int x1, int y1, int x2, int y2) override { |
| 103 | gfx::Point a(x1, y1); |
| 104 | gfx::Point b(x2, y2); |
| 105 | a = m_editor->editorToScreen(a); |
| 106 | b = m_editor->editorToScreen(b); |
| 107 | gfx::Rect bounds = m_editor->bounds(); |
| 108 | a.x -= bounds.x; |
| 109 | a.y -= bounds.y; |
| 110 | b.x -= bounds.x; |
| 111 | b.y -= bounds.y; |
| 112 | m_g->drawLine(color, a, b); |
| 113 | } |
| 114 | |
| 115 | void drawRect(gfx::Color color, const gfx::Rect& rc) override { |
| 116 | gfx::Rect rc2 = m_editor->editorToScreen(rc); |
| 117 | gfx::Rect bounds = m_editor->bounds(); |
| 118 | rc2.x -= bounds.x; |
| 119 | rc2.y -= bounds.y; |
| 120 | m_g->drawRect(color, rc2); |
| 121 | } |
| 122 | |
| 123 | void fillRect(gfx::Color color, const gfx::Rect& rc) override { |
| 124 | gfx::Rect rc2 = m_editor->editorToScreen(rc); |
| 125 | gfx::Rect bounds = m_editor->bounds(); |
| 126 | rc2.x -= bounds.x; |
| 127 | rc2.y -= bounds.y; |
| 128 | m_g->fillRect(color, rc2); |
| 129 | } |
| 130 | |
| 131 | private: |
| 132 | Editor* m_editor; |
| 133 | Graphics* m_g; |
| 134 | }; |
| 135 | |
| 136 | // static |
| 137 | std::unique_ptr<EditorRender> Editor::m_renderEngine = nullptr; |
| 138 | |
| 139 | Editor::Editor(Doc* document, EditorFlags flags, EditorStatePtr state) |
| 140 | : Widget(Editor::Type()) |
| 141 | , m_state(state == nullptr ? std::make_shared<StandbyState>(): state) |
| 142 | , m_decorator(NULL) |
| 143 | , m_document(document) |
| 144 | , m_sprite(m_document->sprite()) |
| 145 | , m_layer(m_sprite->root()->firstLayer()) |
| 146 | , m_frame(frame_t(0)) |
| 147 | , m_docPref(Preferences::instance().document(document)) |
| 148 | , m_tiledModeHelper(app::TiledModeHelper(m_docPref.tiled.mode(), m_sprite)) |
| 149 | , m_brushPreview(this) |
| 150 | , m_toolLoopModifiers(tools::ToolLoopModifiers::kNone) |
| 151 | , m_padding(0, 0) |
| 152 | , m_antsTimer(100, this) |
| 153 | , m_antsOffset(0) |
| 154 | , m_customizationDelegate(NULL) |
| 155 | , m_docView(NULL) |
| 156 | , m_flags(flags) |
| 157 | , m_secondaryButton(false) |
| 158 | , m_flashing(Flashing::None) |
| 159 | , m_aniSpeed(1.0) |
| 160 | , m_isPlaying(false) |
| 161 | , m_showGuidesThisCel(nullptr) |
| 162 | , m_showAutoCelGuides(false) |
| 163 | , m_tagFocusBand(-1) |
| 164 | { |
| 165 | if (!m_renderEngine) |
| 166 | m_renderEngine = std::make_unique<EditorRender>(); |
| 167 | |
| 168 | m_proj.setPixelRatio(m_sprite->pixelRatio()); |
| 169 | |
| 170 | // Add the first state into the history. |
| 171 | m_statesHistory.push(m_state); |
| 172 | |
| 173 | this->setFocusStop(true); |
| 174 | |
| 175 | App::instance()->activeToolManager()->add_observer(this); |
| 176 | |
| 177 | m_fgColorChangeConn = |
| 178 | Preferences::instance().colorBar.fgColor.AfterChange.connect( |
| 179 | [this]{ onFgColorChange(); }); |
| 180 | |
| 181 | m_samplingChangeConn = |
| 182 | Preferences::instance().editor.downsampling.AfterChange.connect( |
| 183 | [this]{ onSamplingChange(); }); |
| 184 | |
| 185 | m_contextBarBrushChangeConn = |
| 186 | App::instance()->contextBar()->BrushChange.connect( |
| 187 | [this]{ onContextBarBrushChange(); }); |
| 188 | |
| 189 | // Restore last site in preferences |
| 190 | { |
| 191 | frame_t preferredFrame = m_docPref.site.frame(); |
| 192 | if (preferredFrame >= 0 && preferredFrame <= m_sprite->lastFrame()) |
| 193 | setFrame(preferredFrame); |
| 194 | |
| 195 | LayerList layers = m_sprite->allBrowsableLayers(); |
| 196 | layer_t layerIndex = m_docPref.site.layer(); |
| 197 | if (layerIndex >= 0 && layerIndex < int(layers.size())) |
| 198 | setLayer(layers[layerIndex]); |
| 199 | } |
| 200 | |
| 201 | m_tiledConnBefore = m_docPref.tiled.BeforeChange.connect([this]{ onTiledModeBeforeChange(); }); |
| 202 | m_tiledConn = m_docPref.tiled.AfterChange.connect([this]{ onTiledModeChange(); }); |
| 203 | m_gridConn = m_docPref.grid.AfterChange.connect([this]{ invalidate(); }); |
| 204 | m_pixelGridConn = m_docPref.pixelGrid.AfterChange.connect([this]{ invalidate(); }); |
| 205 | m_bgConn = m_docPref.bg.AfterChange.connect([this]{ invalidate(); }); |
| 206 | m_onionskinConn = m_docPref.onionskin.AfterChange.connect([this]{ invalidate(); }); |
| 207 | m_symmetryModeConn = Preferences::instance().symmetryMode.enabled.AfterChange.connect([this]{ invalidateIfActive(); }); |
| 208 | m_showExtrasConn = |
| 209 | m_docPref.show.AfterChange.connect( |
| 210 | [this]{ onShowExtrasChange(); }); |
| 211 | |
| 212 | m_document->add_observer(this); |
| 213 | |
| 214 | m_state->onEnterState(this); |
| 215 | } |
| 216 | |
| 217 | Editor::~Editor() |
| 218 | { |
| 219 | if (m_document && m_sprite) { |
| 220 | LayerList layers = m_sprite->allBrowsableLayers(); |
| 221 | layer_t layerIndex = doc::find_layer_index(layers, layer()); |
| 222 | |
| 223 | m_docPref.site.frame(frame()); |
| 224 | m_docPref.site.layer(layerIndex); |
| 225 | } |
| 226 | |
| 227 | m_observers.notifyDestroyEditor(this); |
| 228 | m_document->remove_observer(this); |
| 229 | App::instance()->activeToolManager()->remove_observer(this); |
| 230 | |
| 231 | setCustomizationDelegate(NULL); |
| 232 | |
| 233 | m_antsTimer.stop(); |
| 234 | } |
| 235 | |
| 236 | void Editor::destroyEditorSharedInternals() |
| 237 | { |
| 238 | BrushPreview::destroyInternals(); |
| 239 | if (m_renderEngine) |
| 240 | m_renderEngine.reset(); |
| 241 | } |
| 242 | |
| 243 | bool Editor::isActive() const |
| 244 | { |
| 245 | return (current_editor == this); |
| 246 | } |
| 247 | |
| 248 | bool Editor::isUsingNewRenderEngine() const |
| 249 | { |
| 250 | ASSERT(m_sprite); |
| 251 | return |
| 252 | (Preferences::instance().experimental.newRenderEngine() |
| 253 | // Reference layers + zoom > 100% need the old render engine for |
| 254 | // sub-pixel rendering. |
| 255 | && (!m_sprite->hasVisibleReferenceLayers() |
| 256 | || (m_proj.scaleX() <= 1.0 |
| 257 | && m_proj.scaleY() <= 1.0))); |
| 258 | } |
| 259 | |
| 260 | // static |
| 261 | WidgetType Editor::Type() |
| 262 | { |
| 263 | static WidgetType type = kGenericWidget; |
| 264 | if (type == kGenericWidget) |
| 265 | type = register_widget_type(); |
| 266 | return type; |
| 267 | } |
| 268 | |
| 269 | void Editor::setStateInternal(const EditorStatePtr& newState) |
| 270 | { |
| 271 | m_brushPreview.hide(); |
| 272 | |
| 273 | // Fire before change state event, set the state, and fire after |
| 274 | // change state event. |
| 275 | EditorState::LeaveAction leaveAction = |
| 276 | m_state->onLeaveState(this, newState.get()); |
| 277 | |
| 278 | // Push a new state |
| 279 | if (newState) { |
| 280 | if (leaveAction == EditorState::DiscardState) |
| 281 | m_statesHistory.pop(); |
| 282 | |
| 283 | m_statesHistory.push(newState); |
| 284 | m_state = newState; |
| 285 | } |
| 286 | // Go to previous state |
| 287 | else { |
| 288 | m_state->onBeforePopState(this); |
| 289 | |
| 290 | // Save the current state into "m_deletedStates" just to keep a |
| 291 | // reference to it to avoid delete it right now. We'll delete it |
| 292 | // in the next Editor::onProcessMessage(). |
| 293 | // |
| 294 | // This is necessary for PlayState because it removes itself |
| 295 | // calling Editor::stop() from PlayState::onPlaybackTick(). If we |
| 296 | // delete the PlayState inside the "Tick" timer signal, the |
| 297 | // program will crash (because we're iterating the |
| 298 | // PlayState::m_playTimer slots). |
| 299 | m_deletedStates.push(m_state); |
| 300 | |
| 301 | m_statesHistory.pop(); |
| 302 | m_state = m_statesHistory.top(); |
| 303 | } |
| 304 | |
| 305 | ASSERT(m_state); |
| 306 | |
| 307 | // Change to the new state. |
| 308 | m_state->onEnterState(this); |
| 309 | |
| 310 | // Notify observers |
| 311 | m_observers.notifyStateChanged(this); |
| 312 | |
| 313 | // Redraw layer edges |
| 314 | if (m_docPref.show.layerEdges()) |
| 315 | invalidate(); |
| 316 | |
| 317 | // Setup the new mouse cursor |
| 318 | setCursor(mousePosInDisplay()); |
| 319 | |
| 320 | updateStatusBar(); |
| 321 | } |
| 322 | |
| 323 | void Editor::setState(const EditorStatePtr& newState) |
| 324 | { |
| 325 | setStateInternal(newState); |
| 326 | } |
| 327 | |
| 328 | void Editor::backToPreviousState() |
| 329 | { |
| 330 | setStateInternal(EditorStatePtr(NULL)); |
| 331 | } |
| 332 | |
| 333 | void Editor::getInvalidDecoratoredRegion(gfx::Region& region) |
| 334 | { |
| 335 | // Remove decorated region that cannot be just moved because it |
| 336 | // must be redrawn in another position when the Editor's scroll |
| 337 | // changes (e.g. symmetry handles). |
| 338 | if ((m_flags & kShowDecorators) && m_decorator) |
| 339 | m_decorator->getInvalidDecoratoredRegion(this, region); |
| 340 | |
| 341 | #if ENABLE_DEVMODE |
| 342 | // TODO put this in other widget |
| 343 | if (Preferences::instance().perf.showRenderTime()) { |
| 344 | if (!m_perfInfoBounds.isEmpty()) |
| 345 | region |= gfx::Region(m_perfInfoBounds); |
| 346 | } |
| 347 | #endif // ENABLE_DEVMODE |
| 348 | } |
| 349 | |
| 350 | void Editor::setLayer(const Layer* layer) |
| 351 | { |
| 352 | const bool changed = (m_layer != layer); |
| 353 | const bool gridVisible = (changed && m_docPref.show.grid()); |
| 354 | |
| 355 | doc::Grid oldGrid, newGrid; |
| 356 | if (gridVisible) |
| 357 | oldGrid = getSite().grid(); |
| 358 | |
| 359 | m_observers.notifyBeforeLayerChanged(this); |
| 360 | |
| 361 | // Remove extra cel information if we change between different layer |
| 362 | // type (e.g. from a tilemap layer to an image layer). This is |
| 363 | // useful to avoid a flickering effect in the preview window (using |
| 364 | // a non-updated extra cel to patch the new "layer" with the |
| 365 | // background of the previous selected "m_layer". |
| 366 | if ((layer == nullptr) || |
| 367 | (m_layer != nullptr && m_layer->type() != layer->type())) { |
| 368 | m_document->setExtraCel(ExtraCelRef(nullptr)); |
| 369 | } |
| 370 | |
| 371 | m_layer = const_cast<Layer*>(layer); |
| 372 | m_observers.notifyAfterLayerChanged(this); |
| 373 | |
| 374 | if (gridVisible) |
| 375 | newGrid = getSite().grid(); |
| 376 | |
| 377 | if (m_document && changed) { |
| 378 | if (// If the onion skinning depends on the active layer |
| 379 | m_docPref.onionskin.currentLayer() || |
| 380 | // If the user want to see the active layer edges... |
| 381 | m_docPref.show.layerEdges() || |
| 382 | // If there is a different opacity for nonactive-layers |
| 383 | Preferences::instance().experimental.nonactiveLayersOpacity() < 255 || |
| 384 | // If the automatic cel guides are visible... |
| 385 | m_showGuidesThisCel || |
| 386 | // If grid settings changed |
| 387 | (gridVisible && |
| 388 | (oldGrid.tileSize() != newGrid.tileSize() || |
| 389 | oldGrid.origin() != newGrid.origin()))) { |
| 390 | // We've to redraw the whole editor |
| 391 | invalidate(); |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | // The active layer has changed. |
| 396 | if (isActive()) |
| 397 | UIContext::instance()->notifyActiveSiteChanged(); |
| 398 | |
| 399 | updateStatusBar(); |
| 400 | } |
| 401 | |
| 402 | void Editor::setFrame(frame_t frame) |
| 403 | { |
| 404 | if (m_frame == frame) |
| 405 | return; |
| 406 | |
| 407 | m_observers.notifyBeforeFrameChanged(this); |
| 408 | { |
| 409 | HideBrushPreview hide(m_brushPreview); |
| 410 | m_frame = frame; |
| 411 | } |
| 412 | m_observers.notifyAfterFrameChanged(this); |
| 413 | |
| 414 | // The active frame has changed. |
| 415 | if (isActive()) |
| 416 | UIContext::instance()->notifyActiveSiteChanged(); |
| 417 | |
| 418 | // Invalidate canvas area |
| 419 | invalidateCanvas(); |
| 420 | updateStatusBar(); |
| 421 | } |
| 422 | |
| 423 | void Editor::getSite(Site* site) const |
| 424 | { |
| 425 | site->document(m_document); |
| 426 | site->sprite(m_sprite); |
| 427 | site->layer(m_layer); |
| 428 | site->frame(m_frame); |
| 429 | |
| 430 | if (!m_selectedSlices.empty() && |
| 431 | getCurrentEditorInk()->isSlice()) { |
| 432 | site->selectedSlices(m_selectedSlices); |
| 433 | } |
| 434 | |
| 435 | // TODO we should not access timeline directly here |
| 436 | Timeline* timeline = App::instance()->timeline(); |
| 437 | if (timeline && |
| 438 | timeline->isVisible() && |
| 439 | timeline->range().enabled()) { |
| 440 | site->range(timeline->range()); |
| 441 | } |
| 442 | |
| 443 | if (m_layer && m_layer->isTilemap()) { |
| 444 | TilemapMode tilemapMode = site->tilemapMode(); |
| 445 | TilesetMode tilesetMode = site->tilesetMode(); |
| 446 | const ColorBar* colorbar = ColorBar::instance(); |
| 447 | ASSERT(colorbar); |
| 448 | if (colorbar) { |
| 449 | tilemapMode = colorbar->tilemapMode(); |
| 450 | tilesetMode = colorbar->tilesetMode(); |
| 451 | } |
| 452 | site->tilemapMode(tilemapMode); |
| 453 | site->tilesetMode(tilesetMode); |
| 454 | } |
| 455 | } |
| 456 | |
| 457 | Site Editor::getSite() const |
| 458 | { |
| 459 | Site site; |
| 460 | getSite(&site); |
| 461 | return site; |
| 462 | } |
| 463 | |
| 464 | void Editor::setZoom(const render::Zoom& zoom) |
| 465 | { |
| 466 | if (m_proj.zoom() != zoom) { |
| 467 | m_proj.setZoom(zoom); |
| 468 | notifyZoomChanged(); |
| 469 | |
| 470 | if (isActive()) |
| 471 | App::instance()->contextBar()->updateSamplingVisibility(); |
| 472 | } |
| 473 | else { |
| 474 | // Just copy the zoom as the internal "Zoom::m_internalScale" |
| 475 | // value might be different and we want to keep this value updated |
| 476 | // for better zooming experience in StateWithWheelBehavior. |
| 477 | m_proj.setZoom(zoom); |
| 478 | } |
| 479 | } |
| 480 | |
| 481 | void Editor::setDefaultScroll() |
| 482 | { |
| 483 | if (Preferences::instance().editor.autoFit()) |
| 484 | setScrollAndZoomToFitScreen(); |
| 485 | else |
| 486 | setScrollToCenter(); |
| 487 | } |
| 488 | |
| 489 | void Editor::setScrollToCenter() |
| 490 | { |
| 491 | View* view = View::getView(this); |
| 492 | Rect vp = view->viewportBounds(); |
| 493 | gfx::Size canvas = canvasSize(); |
| 494 | |
| 495 | setEditorScroll( |
| 496 | gfx::Point( |
| 497 | m_padding.x - vp.w/2 + m_proj.applyX(canvas.w)/2, |
| 498 | m_padding.y - vp.h/2 + m_proj.applyY(canvas.h)/2)); |
| 499 | } |
| 500 | |
| 501 | void Editor::setScrollAndZoomToFitScreen() |
| 502 | { |
| 503 | View* view = View::getView(this); |
| 504 | gfx::Rect vp = view->viewportBounds(); |
| 505 | gfx::Size canvas = canvasSize(); |
| 506 | Zoom zoom = m_proj.zoom(); |
| 507 | |
| 508 | if (float(vp.w) / float(canvas.w) < |
| 509 | float(vp.h) / float(canvas.h)) { |
| 510 | if (vp.w < m_proj.applyX(canvas.w)) { |
| 511 | while (vp.w < m_proj.applyX(canvas.w)) { |
| 512 | if (!zoom.out()) |
| 513 | break; |
| 514 | m_proj.setZoom(zoom); |
| 515 | } |
| 516 | } |
| 517 | else if (vp.w > m_proj.applyX(canvas.w)) { |
| 518 | bool out = true; |
| 519 | while (vp.w > m_proj.applyX(canvas.w)) { |
| 520 | if (!zoom.in()) { |
| 521 | out = false; |
| 522 | break; |
| 523 | } |
| 524 | m_proj.setZoom(zoom); |
| 525 | } |
| 526 | if (out) { |
| 527 | zoom.out(); |
| 528 | m_proj.setZoom(zoom); |
| 529 | } |
| 530 | } |
| 531 | } |
| 532 | else { |
| 533 | if (vp.h < m_proj.applyY(canvas.h)) { |
| 534 | while (vp.h < m_proj.applyY(canvas.h)) { |
| 535 | if (!zoom.out()) |
| 536 | break; |
| 537 | m_proj.setZoom(zoom); |
| 538 | } |
| 539 | } |
| 540 | else if (vp.h > m_proj.applyY(canvas.h)) { |
| 541 | bool out = true; |
| 542 | while (vp.h > m_proj.applyY(canvas.h)) { |
| 543 | if (!zoom.in()) { |
| 544 | out = false; |
| 545 | break; |
| 546 | } |
| 547 | m_proj.setZoom(zoom); |
| 548 | } |
| 549 | if (out) { |
| 550 | zoom.out(); |
| 551 | m_proj.setZoom(zoom); |
| 552 | } |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | updateEditor(false); |
| 557 | setEditorScroll( |
| 558 | gfx::Point( |
| 559 | m_padding.x - vp.w/2 + m_proj.applyX(canvas.w)/2, |
| 560 | m_padding.y - vp.h/2 + m_proj.applyY(canvas.h)/2)); |
| 561 | } |
| 562 | |
| 563 | // Sets the scroll position of the editor |
| 564 | void Editor::setEditorScroll(const gfx::Point& scroll) |
| 565 | { |
| 566 | View::getView(this)->setViewScroll(scroll); |
| 567 | } |
| 568 | |
| 569 | void Editor::setEditorZoom(const render::Zoom& zoom) |
| 570 | { |
| 571 | setZoomAndCenterInMouse( |
| 572 | zoom, mousePosInDisplay(), |
| 573 | Editor::ZoomBehavior::CENTER); |
| 574 | } |
| 575 | |
| 576 | void Editor::updateEditor(const bool restoreScrollPos) |
| 577 | { |
| 578 | View::getView(this)->updateView(restoreScrollPos); |
| 579 | } |
| 580 | |
| 581 | void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& spriteRectToDraw, int dx, int dy) |
| 582 | { |
| 583 | // Clip from sprite and apply zoom |
| 584 | gfx::Rect rc = m_sprite->bounds().createIntersection(spriteRectToDraw); |
| 585 | rc = m_proj.apply(rc); |
| 586 | |
| 587 | gfx::Rect dest(dx + m_padding.x + rc.x, |
| 588 | dy + m_padding.y + rc.y, 0, 0); |
| 589 | |
| 590 | // Clip from graphics/screen |
| 591 | const gfx::Rect& clip = g->getClipBounds(); |
| 592 | if (dest.x < clip.x) { |
| 593 | rc.x += clip.x - dest.x; |
| 594 | rc.w -= clip.x - dest.x; |
| 595 | dest.x = clip.x; |
| 596 | } |
| 597 | if (dest.y < clip.y) { |
| 598 | rc.y += clip.y - dest.y; |
| 599 | rc.h -= clip.y - dest.y; |
| 600 | dest.y = clip.y; |
| 601 | } |
| 602 | if (dest.x+rc.w > clip.x+clip.w) { |
| 603 | rc.w = clip.x+clip.w-dest.x; |
| 604 | } |
| 605 | if (dest.y+rc.h > clip.y+clip.h) { |
| 606 | rc.h = clip.y+clip.h-dest.y; |
| 607 | } |
| 608 | |
| 609 | if (rc.isEmpty()) |
| 610 | return; |
| 611 | |
| 612 | // Bounds of pixels from the sprite canvas that will be exposed in |
| 613 | // this render cycle. |
| 614 | gfx::Rect expose = m_proj.remove(rc); |
| 615 | |
| 616 | // If the zoom level is less than 100%, we add extra pixels to |
| 617 | // the exposed area. Those pixels could be shown in the |
| 618 | // rendering process depending on each cel position. |
| 619 | // E.g. when we are drawing in a cel with position < (0,0) |
| 620 | if (m_proj.scaleX() < 1.0) |
| 621 | expose.enlargeXW(int(1./m_proj.scaleX())); |
| 622 | // If the zoom level is more than %100 we add an extra pixel to |
| 623 | // expose just in case the zoom requires to display it. Note: |
| 624 | // this is really necessary to avoid showing invalid destination |
| 625 | // areas in ToolLoopImpl. |
| 626 | else if (m_proj.scaleX() > 1.0) |
| 627 | expose.enlargeXW(1); |
| 628 | |
| 629 | if (m_proj.scaleY() < 1.0) |
| 630 | expose.enlargeYH(int(1./m_proj.scaleY())); |
| 631 | else if (m_proj.scaleY() > 1.0) |
| 632 | expose.enlargeYH(1); |
| 633 | |
| 634 | expose &= m_sprite->bounds(); |
| 635 | |
| 636 | const int maxw = std::max(0, m_sprite->width()-expose.x); |
| 637 | const int maxh = std::max(0, m_sprite->height()-expose.y); |
| 638 | expose.w = std::clamp(expose.w, 0, maxw); |
| 639 | expose.h = std::clamp(expose.h, 0, maxh); |
| 640 | if (expose.isEmpty()) |
| 641 | return; |
| 642 | |
| 643 | // rc2 is the rectangle used to create a temporal rendered image of the sprite |
| 644 | const auto& pref = Preferences::instance(); |
| 645 | const bool newEngine = isUsingNewRenderEngine(); |
| 646 | gfx::Rect rc2; |
| 647 | if (newEngine) { |
| 648 | rc2 = expose; // New engine, exposed rectangle (without zoom) |
| 649 | dest.x = dx + m_padding.x + m_proj.applyX(rc2.x); |
| 650 | dest.y = dy + m_padding.y + m_proj.applyY(rc2.y); |
| 651 | dest.w = m_proj.applyX(rc2.w); |
| 652 | dest.h = m_proj.applyY(rc2.h); |
| 653 | } |
| 654 | else { |
| 655 | rc2 = rc; // Old engine, same rectangle with zoom |
| 656 | dest.w = rc.w; |
| 657 | dest.h = rc.h; |
| 658 | } |
| 659 | |
| 660 | std::unique_ptr<Image> rendered(nullptr); |
| 661 | try { |
| 662 | // Generate a "expose sprite pixels" notification. This is used by |
| 663 | // tool managers that need to validate this region (copy pixels from |
| 664 | // the original cel) before it can be used by the RenderEngine. |
| 665 | m_document->notifyExposeSpritePixels(m_sprite, gfx::Region(expose)); |
| 666 | |
| 667 | // Create a temporary RGB bitmap to draw all to it |
| 668 | rendered.reset(Image::create(IMAGE_RGB, rc2.w, rc2.h, |
| 669 | m_renderEngine->getRenderImageBuffer())); |
| 670 | |
| 671 | m_renderEngine->setNewBlendMethod(pref.experimental.newBlend()); |
| 672 | m_renderEngine->setRefLayersVisiblity(true); |
| 673 | m_renderEngine->setSelectedLayer(m_layer); |
| 674 | if (m_flags & Editor::kUseNonactiveLayersOpacityWhenEnabled) |
| 675 | m_renderEngine->setNonactiveLayersOpacity(pref.experimental.nonactiveLayersOpacity()); |
| 676 | else |
| 677 | m_renderEngine->setNonactiveLayersOpacity(255); |
| 678 | m_renderEngine->setProjection( |
| 679 | newEngine ? render::Projection(): m_proj); |
| 680 | m_renderEngine->setupBackground(m_document, rendered->pixelFormat()); |
| 681 | m_renderEngine->disableOnionskin(); |
| 682 | |
| 683 | if ((m_flags & kShowOnionskin) == kShowOnionskin) { |
| 684 | if (m_docPref.onionskin.active()) { |
| 685 | OnionskinOptions opts( |
| 686 | (m_docPref.onionskin.type() == app::gen::OnionskinType::MERGE ? |
| 687 | render::OnionskinType::MERGE: |
| 688 | (m_docPref.onionskin.type() == app::gen::OnionskinType::RED_BLUE_TINT ? |
| 689 | render::OnionskinType::RED_BLUE_TINT: |
| 690 | render::OnionskinType::NONE))); |
| 691 | |
| 692 | opts.position(m_docPref.onionskin.position()); |
| 693 | opts.prevFrames(m_docPref.onionskin.prevFrames()); |
| 694 | opts.nextFrames(m_docPref.onionskin.nextFrames()); |
| 695 | opts.opacityBase(m_docPref.onionskin.opacityBase()); |
| 696 | opts.opacityStep(m_docPref.onionskin.opacityStep()); |
| 697 | opts.layer(m_docPref.onionskin.currentLayer() ? m_layer: nullptr); |
| 698 | |
| 699 | Tag* tag = nullptr; |
| 700 | if (m_docPref.onionskin.loopTag()) |
| 701 | tag = m_sprite->tags().innerTag(m_frame); |
| 702 | opts.loopTag(tag); |
| 703 | |
| 704 | m_renderEngine->setOnionskin(opts); |
| 705 | } |
| 706 | } |
| 707 | |
| 708 | ExtraCelRef = m_document->extraCel(); |
| 709 | if (extraCel && |
| 710 | extraCel->type() != render::ExtraType::NONE) { |
| 711 | m_renderEngine->setExtraImage( |
| 712 | extraCel->type(), |
| 713 | extraCel->cel(), |
| 714 | extraCel->image(), |
| 715 | extraCel->blendMode(), |
| 716 | m_layer, m_frame); |
| 717 | } |
| 718 | |
| 719 | m_renderEngine->renderSprite( |
| 720 | rendered.get(), m_sprite, m_frame, gfx::Clip(0, 0, rc2)); |
| 721 | |
| 722 | m_renderEngine->removeExtraImage(); |
| 723 | |
| 724 | // If the checkered background is visible in this sprite, we save |
| 725 | // all settings of the background for this document. |
| 726 | if (!m_sprite->isOpaque()) |
| 727 | m_docPref.bg.forceSection(); |
| 728 | } |
| 729 | catch (const std::exception& e) { |
| 730 | Console::showException(e); |
| 731 | } |
| 732 | |
| 733 | if (rendered) { |
| 734 | // Convert the render to a os::Surface |
| 735 | static os::SurfaceRef tmp = nullptr; // TODO move this to other centralized place |
| 736 | |
| 737 | if (!tmp || |
| 738 | tmp->width() < rc2.w || |
| 739 | tmp->height() < rc2.h || |
| 740 | tmp->colorSpace() != m_document->osColorSpace()) { |
| 741 | const int maxw = std::max(rc2.w, tmp ? tmp->width(): 0); |
| 742 | const int maxh = std::max(rc2.h, tmp ? tmp->height(): 0); |
| 743 | tmp = os::instance()->makeSurface( |
| 744 | maxw, maxh, m_document->osColorSpace()); |
| 745 | } |
| 746 | |
| 747 | if (tmp->nativeHandle()) { |
| 748 | if (newEngine) { |
| 749 | // Without doing something on the "tmp" surface before (like |
| 750 | // just drawing a pixel), we get a strange behavior where |
| 751 | // pixels are not updated correctly on the editor (e.g. when |
| 752 | // zoom < 100%). I didn't have enough time to investigate this |
| 753 | // issue yet, but this is a partial fix/hack. |
| 754 | // |
| 755 | // TODO review why do we need to do this, it looks like some |
| 756 | // internal state of a SkCanvas or SkBitmap thing is |
| 757 | // updated after this, because convert_image_to_surface() |
| 758 | // will overwrite these pixels anyway. |
| 759 | os::Paint paint; |
| 760 | paint.color(gfx::rgba(0, 0, 0, 255)); |
| 761 | tmp->drawRect(gfx::Rect(0, 0, 1, 1), paint); |
| 762 | } |
| 763 | |
| 764 | convert_image_to_surface(rendered.get(), m_sprite->palette(m_frame), |
| 765 | tmp.get(), 0, 0, 0, 0, rc2.w, rc2.h); |
| 766 | |
| 767 | if (newEngine) { |
| 768 | os::Sampling sampling; |
| 769 | if (m_proj.scaleX() < 1.0) { |
| 770 | switch (pref.editor.downsampling()) { |
| 771 | case gen::Downsampling::NEAREST: |
| 772 | sampling = os::Sampling(os::Sampling::Filter::Nearest); |
| 773 | break; |
| 774 | case gen::Downsampling::BILINEAR: |
| 775 | sampling = os::Sampling(os::Sampling::Filter::Linear); |
| 776 | break; |
| 777 | case gen::Downsampling::BILINEAR_MIPMAP: |
| 778 | sampling = os::Sampling(os::Sampling::Filter::Linear, |
| 779 | os::Sampling::Mipmap::Nearest); |
| 780 | break; |
| 781 | case gen::Downsampling::TRILINEAR_MIPMAP: |
| 782 | sampling = os::Sampling(os::Sampling::Filter::Linear, |
| 783 | os::Sampling::Mipmap::Linear); |
| 784 | break; |
| 785 | } |
| 786 | } |
| 787 | |
| 788 | g->drawSurface(tmp.get(), |
| 789 | gfx::Rect(0, 0, rc2.w, rc2.h), |
| 790 | dest, |
| 791 | sampling, |
| 792 | nullptr); |
| 793 | } |
| 794 | else { |
| 795 | g->blit(tmp.get(), 0, 0, dest.x, dest.y, dest.w, dest.h); |
| 796 | } |
| 797 | } |
| 798 | } |
| 799 | |
| 800 | // Draw grids |
| 801 | { |
| 802 | gfx::Rect enclosingRect( |
| 803 | m_padding.x + dx, |
| 804 | m_padding.y + dy, |
| 805 | m_proj.applyX(m_sprite->width()), |
| 806 | m_proj.applyY(m_sprite->height())); |
| 807 | |
| 808 | IntersectClip clip(g, dest); |
| 809 | if (clip) { |
| 810 | // Draw the pixel grid |
| 811 | if ((m_proj.zoom().scale() > 2.0) && m_docPref.show.pixelGrid()) { |
| 812 | int alpha = m_docPref.pixelGrid.opacity(); |
| 813 | |
| 814 | if (m_docPref.pixelGrid.autoOpacity()) { |
| 815 | alpha = int(alpha * (m_proj.zoom().scale()-2.) / (16.-2.)); |
| 816 | alpha = std::clamp(alpha, 0, 255); |
| 817 | } |
| 818 | |
| 819 | drawGrid(g, enclosingRect, Rect(0, 0, 1, 1), |
| 820 | m_docPref.pixelGrid.color(), alpha); |
| 821 | |
| 822 | // Save all pixel grid settings that are unset |
| 823 | m_docPref.pixelGrid.forceSection(); |
| 824 | } |
| 825 | m_docPref.show.pixelGrid.forceDirtyFlag(); |
| 826 | |
| 827 | // Draw the grid |
| 828 | if (m_docPref.show.grid()) { |
| 829 | gfx::Rect gridrc; |
| 830 | if (!m_state->getGridBounds(this, gridrc)) |
| 831 | gridrc = getSite().gridBounds(); |
| 832 | |
| 833 | if (m_proj.applyX(gridrc.w) > 2 && |
| 834 | m_proj.applyY(gridrc.h) > 2) { |
| 835 | int alpha = m_docPref.grid.opacity(); |
| 836 | |
| 837 | if (m_docPref.grid.autoOpacity()) { |
| 838 | double len = (m_proj.applyX(gridrc.w) + |
| 839 | m_proj.applyY(gridrc.h)) / 2.; |
| 840 | alpha = int(alpha * len / 32.); |
| 841 | alpha = std::clamp(alpha, 0, 255); |
| 842 | } |
| 843 | |
| 844 | if (alpha > 8) { |
| 845 | drawGrid(g, enclosingRect, gridrc, |
| 846 | m_docPref.grid.color(), alpha); |
| 847 | } |
| 848 | } |
| 849 | |
| 850 | // Save all grid settings that are unset |
| 851 | m_docPref.grid.forceSection(); |
| 852 | } |
| 853 | m_docPref.show.grid.forceDirtyFlag(); |
| 854 | } |
| 855 | } |
| 856 | } |
| 857 | |
| 858 | void Editor::drawBackground(ui::Graphics* g) |
| 859 | { |
| 860 | if (!(m_flags & kShowOutside)) |
| 861 | return; |
| 862 | |
| 863 | auto theme = SkinTheme::get(this); |
| 864 | |
| 865 | gfx::Size canvas = canvasSize(); |
| 866 | gfx::Rect rc(0, 0, canvas.w, canvas.h); |
| 867 | rc = editorToScreen(rc); |
| 868 | rc.offset(-bounds().origin()); |
| 869 | |
| 870 | // Fill the outside (parts of the editor that aren't covered by the |
| 871 | // sprite). |
| 872 | gfx::Region outside(clientBounds()); |
| 873 | outside.createSubtraction(outside, gfx::Region(rc)); |
| 874 | g->fillRegion(theme->colors.editorFace(), outside); |
| 875 | |
| 876 | // Draw the borders that enclose the sprite. |
| 877 | rc.enlarge(1); |
| 878 | g->drawRect(theme->colors.editorSpriteBorder(), rc); |
| 879 | g->drawHLine(theme->colors.editorSpriteBottomBorder(), rc.x, rc.y2(), rc.w); |
| 880 | } |
| 881 | |
| 882 | void Editor::drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& _rc) |
| 883 | { |
| 884 | gfx::Rect rc = _rc; |
| 885 | // For odd zoom scales minor than 100% we have to add an extra window |
| 886 | // just to make sure the whole rectangle is drawn. |
| 887 | if (m_proj.scaleX() < 1.0) rc.w += int(1./m_proj.scaleX()); |
| 888 | if (m_proj.scaleY() < 1.0) rc.h += int(1./m_proj.scaleY()); |
| 889 | |
| 890 | gfx::Rect client = clientBounds(); |
| 891 | gfx::Rect spriteRect( |
| 892 | client.x + m_padding.x, |
| 893 | client.y + m_padding.y, |
| 894 | m_proj.applyX(m_sprite->width()), |
| 895 | m_proj.applyY(m_sprite->height())); |
| 896 | gfx::Rect enclosingRect = spriteRect; |
| 897 | |
| 898 | // Draw the main sprite at the center. |
| 899 | drawOneSpriteUnclippedRect(g, rc, 0, 0); |
| 900 | |
| 901 | // Document preferences |
| 902 | if (int(m_docPref.tiled.mode()) & int(filters::TiledMode::X_AXIS)) { |
| 903 | drawOneSpriteUnclippedRect(g, rc, spriteRect.w, 0); |
| 904 | drawOneSpriteUnclippedRect(g, rc, spriteRect.w*2, 0); |
| 905 | |
| 906 | enclosingRect = gfx::Rect(spriteRect.x, spriteRect.y, spriteRect.w*3, spriteRect.h); |
| 907 | } |
| 908 | |
| 909 | if (int(m_docPref.tiled.mode()) & int(filters::TiledMode::Y_AXIS)) { |
| 910 | drawOneSpriteUnclippedRect(g, rc, 0, spriteRect.h); |
| 911 | drawOneSpriteUnclippedRect(g, rc, 0, spriteRect.h*2); |
| 912 | |
| 913 | enclosingRect = gfx::Rect(spriteRect.x, spriteRect.y, spriteRect.w, spriteRect.h*3); |
| 914 | } |
| 915 | |
| 916 | if (m_docPref.tiled.mode() == filters::TiledMode::BOTH) { |
| 917 | drawOneSpriteUnclippedRect(g, rc, spriteRect.w, spriteRect.h); |
| 918 | drawOneSpriteUnclippedRect(g, rc, spriteRect.w*2, spriteRect.h); |
| 919 | drawOneSpriteUnclippedRect(g, rc, spriteRect.w, spriteRect.h*2); |
| 920 | drawOneSpriteUnclippedRect(g, rc, spriteRect.w*2, spriteRect.h*2); |
| 921 | |
| 922 | enclosingRect = gfx::Rect( |
| 923 | spriteRect.x, spriteRect.y, |
| 924 | spriteRect.w*3, spriteRect.h*3); |
| 925 | } |
| 926 | |
| 927 | // Draw slices |
| 928 | if (m_docPref.show.slices()) |
| 929 | drawSlices(g); |
| 930 | |
| 931 | // Symmetry mode |
| 932 | if (isActive() && |
| 933 | (m_flags & Editor::kShowSymmetryLine) && |
| 934 | Preferences::instance().symmetryMode.enabled()) { |
| 935 | int mode = int(m_docPref.symmetry.mode()); |
| 936 | if (mode & int(app::gen::SymmetryMode::HORIZONTAL)) { |
| 937 | double x = m_docPref.symmetry.xAxis(); |
| 938 | if (x > 0) { |
| 939 | gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color()); |
| 940 | g->drawVLine(color, |
| 941 | spriteRect.x + m_proj.applyX(mainTilePosition().x) + int(m_proj.applyX<double>(x)), |
| 942 | enclosingRect.y, |
| 943 | enclosingRect.h); |
| 944 | } |
| 945 | } |
| 946 | if (mode & int(app::gen::SymmetryMode::VERTICAL)) { |
| 947 | double y = m_docPref.symmetry.yAxis(); |
| 948 | if (y > 0) { |
| 949 | gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color()); |
| 950 | g->drawHLine(color, |
| 951 | enclosingRect.x, |
| 952 | spriteRect.y + m_proj.applyY(mainTilePosition().y) + int(m_proj.applyY<double>(y)), |
| 953 | enclosingRect.w); |
| 954 | } |
| 955 | } |
| 956 | } |
| 957 | |
| 958 | // Draw active layer/cel edges |
| 959 | if ((m_docPref.show.layerEdges() || m_showAutoCelGuides) && |
| 960 | // Show layer edges and possibly cel guides only on states that |
| 961 | // allows it (e.g scrolling state) |
| 962 | m_state->allowLayerEdges()) { |
| 963 | Cel* cel = (m_layer ? m_layer->cel(m_frame): nullptr); |
| 964 | if (cel) { |
| 965 | gfx::Color color = color_utils::color_for_ui(Preferences::instance().guides.layerEdgesColor()); |
| 966 | drawCelBounds(g, cel, color); |
| 967 | |
| 968 | // Draw tile numbers |
| 969 | if (m_docPref.show.tileNumbers() && |
| 970 | cel->layer()->isTilemap()) { |
| 971 | drawTileNumbers(g, cel); |
| 972 | } |
| 973 | |
| 974 | // Draw auto-guides to other cel |
| 975 | if (m_showAutoCelGuides && |
| 976 | m_showGuidesThisCel != cel) { |
| 977 | drawCelGuides(g, cel, m_showGuidesThisCel); |
| 978 | } |
| 979 | } |
| 980 | } |
| 981 | |
| 982 | // Draw the mask |
| 983 | if (m_document->hasMaskBoundaries()) |
| 984 | drawMask(g); |
| 985 | |
| 986 | // Post-render decorator. |
| 987 | if ((m_flags & kShowDecorators) && m_decorator) { |
| 988 | EditorPostRenderImpl postRender(this, g); |
| 989 | m_decorator->postRenderDecorator(&postRender); |
| 990 | } |
| 991 | } |
| 992 | |
| 993 | void Editor::drawSpriteClipped(const gfx::Region& updateRegion) |
| 994 | { |
| 995 | Region screenRegion; |
| 996 | getDrawableRegion(screenRegion, kCutTopWindows); |
| 997 | |
| 998 | ScreenGraphics screenGraphics(display()); |
| 999 | GraphicsPtr editorGraphics = getGraphics(clientBounds()); |
| 1000 | |
| 1001 | for (const Rect& updateRect : updateRegion) { |
| 1002 | for (const Rect& screenRect : screenRegion) { |
| 1003 | IntersectClip clip(&screenGraphics, screenRect); |
| 1004 | if (clip) |
| 1005 | drawSpriteUnclippedRect(editorGraphics.get(), updateRect); |
| 1006 | } |
| 1007 | } |
| 1008 | } |
| 1009 | |
| 1010 | /** |
| 1011 | * Draws the boundaries, really this routine doesn't use the "mask" |
| 1012 | * field of the sprite, only the "bound" field (so you can have other |
| 1013 | * mask in the sprite and could be showed other boundaries), to |
| 1014 | * regenerate boundaries, use the sprite_generate_mask_boundaries() |
| 1015 | * routine. |
| 1016 | */ |
| 1017 | void Editor::drawMask(Graphics* g) |
| 1018 | { |
| 1019 | if ((m_flags & kShowMask) == 0 || |
| 1020 | !m_docPref.show.selectionEdges()) |
| 1021 | return; |
| 1022 | |
| 1023 | ASSERT(m_document->hasMaskBoundaries()); |
| 1024 | |
| 1025 | gfx::Point pt = mainTilePosition(); |
| 1026 | pt.x = m_padding.x + m_proj.applyX(pt.x); |
| 1027 | pt.y = m_padding.y + m_proj.applyY(pt.y); |
| 1028 | |
| 1029 | // Create the mask boundaries path |
| 1030 | auto& segs = m_document->maskBoundaries(); |
| 1031 | segs.createPathIfNeeeded(); |
| 1032 | |
| 1033 | ui::Paint paint; |
| 1034 | paint.style(ui::Paint::Stroke); |
| 1035 | set_checkered_paint_mode(paint, m_antsOffset, |
| 1036 | gfx::rgba(0, 0, 0, 255), |
| 1037 | gfx::rgba(255, 255, 255, 255)); |
| 1038 | |
| 1039 | // We translate the path instead of applying a matrix to the |
| 1040 | // ui::Graphics so the "checkered" pattern is not scaled too. |
| 1041 | gfx::Path path; |
| 1042 | segs.path().transform(m_proj.scaleMatrix(), &path); |
| 1043 | path.offset(pt.x, pt.y); |
| 1044 | g->drawPath(path, paint); |
| 1045 | } |
| 1046 | |
| 1047 | void Editor::drawMaskSafe() |
| 1048 | { |
| 1049 | if ((m_flags & kShowMask) == 0) |
| 1050 | return; |
| 1051 | |
| 1052 | if (isVisible() && |
| 1053 | m_document && |
| 1054 | m_document->hasMaskBoundaries()) { |
| 1055 | Region region; |
| 1056 | getDrawableRegion(region, kCutTopWindows); |
| 1057 | region.offset(-bounds().origin()); |
| 1058 | |
| 1059 | HideBrushPreview hide(m_brushPreview); |
| 1060 | GraphicsPtr g = getGraphics(clientBounds()); |
| 1061 | |
| 1062 | for (const gfx::Rect& rc : region) { |
| 1063 | IntersectClip clip(g.get(), rc); |
| 1064 | if (clip) |
| 1065 | drawMask(g.get()); |
| 1066 | } |
| 1067 | } |
| 1068 | } |
| 1069 | |
| 1070 | void Editor::drawGrid(Graphics* g, const gfx::Rect& spriteBounds, const Rect& gridBounds, const app::Color& color, int alpha) |
| 1071 | { |
| 1072 | if ((m_flags & kShowGrid) == 0) |
| 1073 | return; |
| 1074 | |
| 1075 | // Copy the grid bounds |
| 1076 | Rect grid(gridBounds); |
| 1077 | if (grid.w < 1 || grid.h < 1) |
| 1078 | return; |
| 1079 | |
| 1080 | // Move the grid bounds to a non-negative position. |
| 1081 | if (grid.x < 0) grid.x += (ABS(grid.x)/grid.w+1) * grid.w; |
| 1082 | if (grid.y < 0) grid.y += (ABS(grid.y)/grid.h+1) * grid.h; |
| 1083 | |
| 1084 | // Change the grid position to the first grid's tile |
| 1085 | grid.setOrigin(Point((grid.x % grid.w) - grid.w, |
| 1086 | (grid.y % grid.h) - grid.h)); |
| 1087 | if (grid.x < 0) grid.x += grid.w; |
| 1088 | if (grid.y < 0) grid.y += grid.h; |
| 1089 | |
| 1090 | // Convert the "grid" rectangle to screen coordinates |
| 1091 | grid = editorToScreen(grid); |
| 1092 | if (grid.w < 1 || grid.h < 1) |
| 1093 | return; |
| 1094 | |
| 1095 | // Adjust for client area |
| 1096 | gfx::Rect bounds = this->bounds(); |
| 1097 | grid.offset(-bounds.origin()); |
| 1098 | |
| 1099 | while (grid.x-grid.w >= spriteBounds.x) grid.x -= grid.w; |
| 1100 | while (grid.y-grid.h >= spriteBounds.y) grid.y -= grid.h; |
| 1101 | |
| 1102 | // Get the grid's color |
| 1103 | gfx::Color grid_color = color_utils::color_for_ui(color); |
| 1104 | grid_color = gfx::rgba( |
| 1105 | gfx::getr(grid_color), |
| 1106 | gfx::getg(grid_color), |
| 1107 | gfx::getb(grid_color), alpha); |
| 1108 | |
| 1109 | // Draw horizontal lines |
| 1110 | int x1 = spriteBounds.x; |
| 1111 | int y1 = grid.y; |
| 1112 | int x2 = spriteBounds.x + spriteBounds.w; |
| 1113 | int y2 = spriteBounds.y + spriteBounds.h; |
| 1114 | |
| 1115 | for (int c=y1; c<=y2; c+=grid.h) |
| 1116 | g->drawHLine(grid_color, x1, c, spriteBounds.w); |
| 1117 | |
| 1118 | // Draw vertical lines |
| 1119 | x1 = grid.x; |
| 1120 | y1 = spriteBounds.y; |
| 1121 | |
| 1122 | for (int c=x1; c<=x2; c+=grid.w) |
| 1123 | g->drawVLine(grid_color, c, y1, spriteBounds.h); |
| 1124 | } |
| 1125 | |
| 1126 | void Editor::drawSlices(ui::Graphics* g) |
| 1127 | { |
| 1128 | if ((m_flags & kShowSlices) == 0) |
| 1129 | return; |
| 1130 | |
| 1131 | if (!isVisible() || !m_document) |
| 1132 | return; |
| 1133 | |
| 1134 | auto theme = SkinTheme::get(this); |
| 1135 | gfx::Point mainOffset(mainTilePosition()); |
| 1136 | |
| 1137 | for (auto slice : m_sprite->slices()) { |
| 1138 | auto key = slice->getByFrame(m_frame); |
| 1139 | if (!key) |
| 1140 | continue; |
| 1141 | |
| 1142 | doc::color_t docColor = slice->userData().color(); |
| 1143 | gfx::Color color = gfx::rgba(doc::rgba_getr(docColor), |
| 1144 | doc::rgba_getg(docColor), |
| 1145 | doc::rgba_getb(docColor), |
| 1146 | doc::rgba_geta(docColor)); |
| 1147 | gfx::Rect out = key->bounds(); |
| 1148 | out.offset(mainOffset); |
| 1149 | out = editorToScreen(out); |
| 1150 | out.offset(-bounds().origin()); |
| 1151 | |
| 1152 | // Center slices |
| 1153 | if (key->hasCenter()) { |
| 1154 | gfx::Rect in = |
| 1155 | editorToScreen(gfx::Rect(key->center()).offset(key->bounds().origin())) |
| 1156 | .offset(-bounds().origin()); |
| 1157 | |
| 1158 | auto in_color = gfx::rgba(gfx::getr(color), |
| 1159 | gfx::getg(color), |
| 1160 | gfx::getb(color), |
| 1161 | doc::rgba_geta(docColor)/4); |
| 1162 | if (in.y > out.y && in.y < out.y2()) |
| 1163 | g->drawHLine(in_color, out.x, in.y, out.w); |
| 1164 | if (in.y2() > out.y && in.y2() < out.y2()) |
| 1165 | g->drawHLine(in_color, out.x, in.y2(), out.w); |
| 1166 | if (in.x > out.x && in.x < out.x2()) |
| 1167 | g->drawVLine(in_color, in.x, out.y, out.h); |
| 1168 | if (in.x2() > out.x && in.x2() < out.x2()) |
| 1169 | g->drawVLine(in_color, in.x2(), out.y, out.h); |
| 1170 | } |
| 1171 | |
| 1172 | // Pivot |
| 1173 | if (key->hasPivot()) { |
| 1174 | gfx::Rect in = |
| 1175 | editorToScreen(gfx::Rect(key->pivot(), gfx::Size(1, 1)).offset(key->bounds().origin())) |
| 1176 | .offset(-bounds().origin()); |
| 1177 | |
| 1178 | auto in_color = gfx::rgba(gfx::getr(color), |
| 1179 | gfx::getg(color), |
| 1180 | gfx::getb(color), |
| 1181 | doc::rgba_geta(docColor)/4); |
| 1182 | g->drawRect(in_color, in); |
| 1183 | } |
| 1184 | |
| 1185 | if (isSliceSelected(slice) && |
| 1186 | getCurrentEditorInk()->isSlice()) { |
| 1187 | PaintWidgetPartInfo info; |
| 1188 | theme->paintWidgetPart( |
| 1189 | g, theme->styles.colorbarSelection(), out, info); |
| 1190 | } |
| 1191 | else { |
| 1192 | g->drawRect(color, out); |
| 1193 | } |
| 1194 | } |
| 1195 | } |
| 1196 | |
| 1197 | void Editor::drawTileNumbers(ui::Graphics* g, const Cel* cel) |
| 1198 | { |
| 1199 | gfx::Color color = color_utils::color_for_ui(Preferences::instance().guides.autoGuidesColor()); |
| 1200 | gfx::Color fgColor = color_utils::blackandwhite_neg(color); |
| 1201 | |
| 1202 | const doc::Grid grid = getSite().grid(); |
| 1203 | const gfx::Size tileSize = editorToScreen(grid.tileToCanvas(gfx::Rect(0, 0, 1, 1))).size(); |
| 1204 | if (tileSize.h > g->font()->height()) { |
| 1205 | const gfx::Point offset = |
| 1206 | gfx::Point(tileSize.w/2, |
| 1207 | tileSize.h/2 - g->font()->height()/2) |
| 1208 | + mainTilePosition(); |
| 1209 | |
| 1210 | int ti_offset = |
| 1211 | static_cast<LayerTilemap*>(cel->layer())->tileset()->baseIndex() - 1; |
| 1212 | |
| 1213 | const doc::Image* image = cel->image(); |
| 1214 | std::string text; |
| 1215 | for (int y=0; y<image->height(); ++y) { |
| 1216 | for (int x=0; x<image->width(); ++x) { |
| 1217 | doc::tile_t t = image->getPixel(x, y); |
| 1218 | if (t != doc::notile) { |
| 1219 | gfx::Point pt = editorToScreen(grid.tileToCanvas(gfx::Point(x, y))); |
| 1220 | pt -= bounds().origin(); |
| 1221 | pt += offset; |
| 1222 | |
| 1223 | text = fmt::format("{}" , int(t & doc::tile_i_mask) + ti_offset); |
| 1224 | pt.x -= g->measureUIText(text).w/2; |
| 1225 | g->drawText(text, fgColor, color, pt); |
| 1226 | } |
| 1227 | } |
| 1228 | } |
| 1229 | } |
| 1230 | } |
| 1231 | |
| 1232 | void Editor::drawCelBounds(ui::Graphics* g, const Cel* cel, const gfx::Color color) |
| 1233 | { |
| 1234 | g->drawRect(color, getCelScreenBounds(cel)); |
| 1235 | } |
| 1236 | |
| 1237 | void Editor::drawCelGuides(ui::Graphics* g, const Cel* cel, const Cel* mouseCel) |
| 1238 | { |
| 1239 | gfx::Rect |
| 1240 | sprCelBounds = cel->bounds(), |
| 1241 | scrCelBounds = getCelScreenBounds(cel), |
| 1242 | scrCmpBounds, sprCmpBounds; |
| 1243 | if (mouseCel) { |
| 1244 | scrCmpBounds = getCelScreenBounds(mouseCel); |
| 1245 | sprCmpBounds = mouseCel->bounds(); |
| 1246 | |
| 1247 | const gfx::Color color = color_utils::color_for_ui(Preferences::instance().guides.autoGuidesColor()); |
| 1248 | drawCelBounds(g, mouseCel, color); |
| 1249 | } |
| 1250 | // Use whole canvas |
| 1251 | else { |
| 1252 | sprCmpBounds = m_sprite->bounds(); |
| 1253 | scrCmpBounds = |
| 1254 | editorToScreen( |
| 1255 | gfx::Rect(sprCmpBounds).offset(mainTilePosition())) |
| 1256 | .offset(gfx::Point(-bounds().origin())); |
| 1257 | } |
| 1258 | |
| 1259 | const int midX = scrCelBounds.x+scrCelBounds.w/2; |
| 1260 | const int midY = scrCelBounds.y+scrCelBounds.h/2; |
| 1261 | |
| 1262 | if (sprCelBounds.x2() < sprCmpBounds.x) { |
| 1263 | drawCelHGuide(g, |
| 1264 | sprCelBounds.x2(), sprCmpBounds.x, |
| 1265 | scrCelBounds.x2(), scrCmpBounds.x, midY, |
| 1266 | scrCelBounds, scrCmpBounds, scrCmpBounds.x); |
| 1267 | } |
| 1268 | else if (sprCelBounds.x > sprCmpBounds.x2()) { |
| 1269 | drawCelHGuide(g, |
| 1270 | sprCmpBounds.x2(), sprCelBounds.x, |
| 1271 | scrCmpBounds.x2(), scrCelBounds.x, midY, |
| 1272 | scrCelBounds, scrCmpBounds, scrCmpBounds.x2()-1); |
| 1273 | } |
| 1274 | else { |
| 1275 | if (sprCelBounds.x != sprCmpBounds.x && |
| 1276 | sprCelBounds.x2() != sprCmpBounds.x) { |
| 1277 | drawCelHGuide(g, |
| 1278 | sprCmpBounds.x, sprCelBounds.x, |
| 1279 | scrCmpBounds.x, scrCelBounds.x, midY, |
| 1280 | scrCelBounds, scrCmpBounds, scrCmpBounds.x); |
| 1281 | } |
| 1282 | if (sprCelBounds.x != sprCmpBounds.x2() && |
| 1283 | sprCelBounds.x2() != sprCmpBounds.x2()) { |
| 1284 | drawCelHGuide(g, |
| 1285 | sprCmpBounds.x2(), sprCelBounds.x2(), |
| 1286 | scrCmpBounds.x2(), scrCelBounds.x2(), midY, |
| 1287 | scrCelBounds, scrCmpBounds, scrCmpBounds.x2()-1); |
| 1288 | } |
| 1289 | } |
| 1290 | |
| 1291 | if (sprCelBounds.y2() < sprCmpBounds.y) { |
| 1292 | drawCelVGuide(g, |
| 1293 | sprCelBounds.y2(), sprCmpBounds.y, |
| 1294 | scrCelBounds.y2(), scrCmpBounds.y, midX, |
| 1295 | scrCelBounds, scrCmpBounds, scrCmpBounds.y); |
| 1296 | } |
| 1297 | else if (sprCelBounds.y > sprCmpBounds.y2()) { |
| 1298 | drawCelVGuide(g, |
| 1299 | sprCmpBounds.y2(), sprCelBounds.y, |
| 1300 | scrCmpBounds.y2(), scrCelBounds.y, midX, |
| 1301 | scrCelBounds, scrCmpBounds, scrCmpBounds.y2()-1); |
| 1302 | } |
| 1303 | else { |
| 1304 | if (sprCelBounds.y != sprCmpBounds.y && |
| 1305 | sprCelBounds.y2() != sprCmpBounds.y) { |
| 1306 | drawCelVGuide(g, |
| 1307 | sprCmpBounds.y, sprCelBounds.y, |
| 1308 | scrCmpBounds.y, scrCelBounds.y, midX, |
| 1309 | scrCelBounds, scrCmpBounds, scrCmpBounds.y); |
| 1310 | } |
| 1311 | if (sprCelBounds.y != sprCmpBounds.y2() && |
| 1312 | sprCelBounds.y2() != sprCmpBounds.y2()) { |
| 1313 | drawCelVGuide(g, |
| 1314 | sprCmpBounds.y2(), sprCelBounds.y2(), |
| 1315 | scrCmpBounds.y2(), scrCelBounds.y2(), midX, |
| 1316 | scrCelBounds, scrCmpBounds, scrCmpBounds.y2()-1); |
| 1317 | } |
| 1318 | } |
| 1319 | } |
| 1320 | |
| 1321 | void Editor::drawCelHGuide(ui::Graphics* g, |
| 1322 | const int sprX1, const int sprX2, |
| 1323 | const int scrX1, const int scrX2, const int scrY, |
| 1324 | const gfx::Rect& scrCelBounds, const gfx::Rect& scrCmpBounds, |
| 1325 | const int dottedX) |
| 1326 | { |
| 1327 | gfx::Color color = color_utils::color_for_ui(Preferences::instance().guides.autoGuidesColor()); |
| 1328 | g->drawHLine(color, std::min(scrX1, scrX2), scrY, std::abs(scrX2 - scrX1)); |
| 1329 | |
| 1330 | // Vertical guide to touch the horizontal line |
| 1331 | { |
| 1332 | ui::Paint paint; |
| 1333 | ui::set_checkered_paint_mode(paint, 0, color, gfx::ColorNone); |
| 1334 | paint.color(color); |
| 1335 | |
| 1336 | if (scrY < scrCmpBounds.y) |
| 1337 | g->drawVLine(dottedX, scrCelBounds.y, scrCmpBounds.y - scrCelBounds.y, paint); |
| 1338 | else if (scrY > scrCmpBounds.y2()) |
| 1339 | g->drawVLine(dottedX, scrCmpBounds.y2(), scrCelBounds.y2() - scrCmpBounds.y2(), paint); |
| 1340 | } |
| 1341 | |
| 1342 | auto text = fmt::format("{}px" , ABS(sprX2 - sprX1)); |
| 1343 | const int textW = Graphics::measureUITextLength(text, font()); |
| 1344 | g->drawText(text, |
| 1345 | color_utils::blackandwhite_neg(color), color, |
| 1346 | gfx::Point((scrX1+scrX2)/2-textW/2, scrY-textHeight())); |
| 1347 | } |
| 1348 | |
| 1349 | void Editor::drawCelVGuide(ui::Graphics* g, |
| 1350 | const int sprY1, const int sprY2, |
| 1351 | const int scrY1, const int scrY2, const int scrX, |
| 1352 | const gfx::Rect& scrCelBounds, const gfx::Rect& scrCmpBounds, |
| 1353 | const int dottedY) |
| 1354 | { |
| 1355 | gfx::Color color = color_utils::color_for_ui(Preferences::instance().guides.autoGuidesColor()); |
| 1356 | g->drawVLine(color, scrX, std::min(scrY1, scrY2), std::abs(scrY2 - scrY1)); |
| 1357 | |
| 1358 | // Horizontal guide to touch the vertical line |
| 1359 | { |
| 1360 | ui::Paint paint; |
| 1361 | ui::set_checkered_paint_mode(paint, 0, color, gfx::ColorNone); |
| 1362 | paint.color(color); |
| 1363 | |
| 1364 | if (scrX < scrCmpBounds.x) |
| 1365 | g->drawHLine(scrCelBounds.x, dottedY, scrCmpBounds.x - scrCelBounds.x, paint); |
| 1366 | else if (scrX > scrCmpBounds.x2()) |
| 1367 | g->drawHLine(scrCmpBounds.x2(), dottedY, scrCelBounds.x2() - scrCmpBounds.x2(), paint); |
| 1368 | } |
| 1369 | |
| 1370 | auto text = fmt::format("{}px" , ABS(sprY2 - sprY1)); |
| 1371 | g->drawText(text, |
| 1372 | color_utils::blackandwhite_neg(color), color, |
| 1373 | gfx::Point(scrX, (scrY1+scrY2)/2-textHeight()/2)); |
| 1374 | } |
| 1375 | |
| 1376 | gfx::Rect Editor::getCelScreenBounds(const Cel* cel) |
| 1377 | { |
| 1378 | gfx::Point mainOffset(mainTilePosition()); |
| 1379 | gfx::Rect layerEdges; |
| 1380 | if (m_layer->isReference()) { |
| 1381 | layerEdges = |
| 1382 | editorToScreenF( |
| 1383 | gfx::RectF(cel->boundsF()).offset(mainOffset.x, |
| 1384 | mainOffset.y)) |
| 1385 | .offset(gfx::PointF(-bounds().origin())); |
| 1386 | } |
| 1387 | else { |
| 1388 | layerEdges = |
| 1389 | editorToScreen( |
| 1390 | gfx::Rect(cel->bounds()).offset(mainOffset)) |
| 1391 | .offset(-bounds().origin()); |
| 1392 | } |
| 1393 | return layerEdges; |
| 1394 | } |
| 1395 | |
| 1396 | void Editor::flashCurrentLayer() |
| 1397 | { |
| 1398 | if (!Preferences::instance().experimental.flashLayer()) |
| 1399 | return; |
| 1400 | |
| 1401 | Site site = getSite(); |
| 1402 | if (site.cel()) { |
| 1403 | // Hide and destroy the extra cel used by the brush preview |
| 1404 | // because we'll need to use the extra cel now for the flashing |
| 1405 | // layer. |
| 1406 | m_brushPreview.hide(); |
| 1407 | |
| 1408 | m_renderEngine->removePreviewImage(); |
| 1409 | |
| 1410 | ExtraCelRef (new ExtraCel); |
| 1411 | extraCel->setType(render::ExtraType::OVER_COMPOSITE); |
| 1412 | extraCel->setBlendMode(doc::BlendMode::NEG_BW); |
| 1413 | |
| 1414 | m_document->setExtraCel(extraCel); |
| 1415 | m_flashing = Flashing::WithFlashExtraCel; |
| 1416 | |
| 1417 | invalidateCanvas(); |
| 1418 | } |
| 1419 | } |
| 1420 | |
| 1421 | gfx::Point Editor::autoScroll(const ui::MouseMessage* msg, |
| 1422 | const AutoScroll dir) |
| 1423 | { |
| 1424 | gfx::Point mousePos = msg->position(); |
| 1425 | if (!Preferences::instance().editor.autoScroll()) |
| 1426 | return mousePos; |
| 1427 | |
| 1428 | // Hide the brush preview |
| 1429 | //HideBrushPreview hide(m_brushPreview); |
| 1430 | View* view = View::getView(this); |
| 1431 | gfx::Rect vp = view->viewportBounds(); |
| 1432 | |
| 1433 | if (!vp.contains(mousePos)) { |
| 1434 | gfx::Point delta = (mousePos - m_oldPos); |
| 1435 | gfx::Point deltaScroll = delta; |
| 1436 | |
| 1437 | if (!((mousePos.x < vp.x && delta.x < 0) || |
| 1438 | (mousePos.x >= vp.x+vp.w && delta.x > 0))) { |
| 1439 | delta.x = 0; |
| 1440 | } |
| 1441 | |
| 1442 | if (!((mousePos.y < vp.y && delta.y < 0) || |
| 1443 | (mousePos.y >= vp.y+vp.h && delta.y > 0))) { |
| 1444 | delta.y = 0; |
| 1445 | } |
| 1446 | |
| 1447 | gfx::Point scroll = view->viewScroll(); |
| 1448 | if (dir == AutoScroll::MouseDir) { |
| 1449 | scroll += delta; |
| 1450 | } |
| 1451 | else { |
| 1452 | scroll -= deltaScroll; |
| 1453 | } |
| 1454 | setEditorScroll(scroll); |
| 1455 | |
| 1456 | mousePos -= delta; |
| 1457 | ui::set_mouse_position(mousePos, |
| 1458 | display()); |
| 1459 | |
| 1460 | m_oldPos = mousePos; |
| 1461 | mousePos = gfx::Point( |
| 1462 | std::clamp(mousePos.x, vp.x, vp.x2()-1), |
| 1463 | std::clamp(mousePos.y, vp.y, vp.y2()-1)); |
| 1464 | } |
| 1465 | else |
| 1466 | m_oldPos = mousePos; |
| 1467 | |
| 1468 | return mousePos; |
| 1469 | } |
| 1470 | |
| 1471 | tools::Tool* Editor::getCurrentEditorTool() const |
| 1472 | { |
| 1473 | return App::instance()->activeTool(); |
| 1474 | } |
| 1475 | |
| 1476 | tools::Ink* Editor::getCurrentEditorInk() const |
| 1477 | { |
| 1478 | tools::Ink* ink = m_state->getStateInk(); |
| 1479 | if (ink) |
| 1480 | return ink; |
| 1481 | else |
| 1482 | return App::instance()->activeToolManager()->activeInk(); |
| 1483 | } |
| 1484 | |
| 1485 | bool Editor::isAutoSelectLayer() |
| 1486 | { |
| 1487 | tools::Ink* ink = getCurrentEditorInk(); |
| 1488 | if (ink && ink->isAutoSelectLayer()) |
| 1489 | return true; |
| 1490 | else |
| 1491 | return App::instance()->contextBar()->isAutoSelectLayer(); |
| 1492 | } |
| 1493 | |
| 1494 | gfx::Point Editor::screenToEditor(const gfx::Point& pt) |
| 1495 | { |
| 1496 | View* view = View::getView(this); |
| 1497 | Rect vp = view->viewportBounds(); |
| 1498 | Point scroll = view->viewScroll(); |
| 1499 | return gfx::Point( |
| 1500 | m_proj.removeX(pt.x - vp.x + scroll.x - m_padding.x), |
| 1501 | m_proj.removeY(pt.y - vp.y + scroll.y - m_padding.y)); |
| 1502 | } |
| 1503 | |
| 1504 | gfx::Point Editor::screenToEditorCeiling(const gfx::Point& pt) |
| 1505 | { |
| 1506 | View* view = View::getView(this); |
| 1507 | Rect vp = view->viewportBounds(); |
| 1508 | Point scroll = view->viewScroll(); |
| 1509 | return gfx::Point( |
| 1510 | m_proj.removeXCeiling(pt.x - vp.x + scroll.x - m_padding.x), |
| 1511 | m_proj.removeYCeiling(pt.y - vp.y + scroll.y - m_padding.y)); |
| 1512 | } |
| 1513 | |
| 1514 | |
| 1515 | gfx::PointF Editor::screenToEditorF(const gfx::Point& pt) |
| 1516 | { |
| 1517 | View* view = View::getView(this); |
| 1518 | Rect vp = view->viewportBounds(); |
| 1519 | Point scroll = view->viewScroll(); |
| 1520 | return gfx::PointF( |
| 1521 | m_proj.removeX<double>(pt.x - vp.x + scroll.x - m_padding.x), |
| 1522 | m_proj.removeY<double>(pt.y - vp.y + scroll.y - m_padding.y)); |
| 1523 | } |
| 1524 | |
| 1525 | Point Editor::editorToScreen(const gfx::Point& pt) |
| 1526 | { |
| 1527 | View* view = View::getView(this); |
| 1528 | Rect vp = view->viewportBounds(); |
| 1529 | Point scroll = view->viewScroll(); |
| 1530 | return Point( |
| 1531 | (vp.x - scroll.x + m_padding.x + m_proj.applyX(pt.x)), |
| 1532 | (vp.y - scroll.y + m_padding.y + m_proj.applyY(pt.y))); |
| 1533 | } |
| 1534 | |
| 1535 | gfx::PointF Editor::editorToScreenF(const gfx::PointF& pt) |
| 1536 | { |
| 1537 | View* view = View::getView(this); |
| 1538 | Rect vp = view->viewportBounds(); |
| 1539 | Point scroll = view->viewScroll(); |
| 1540 | return PointF( |
| 1541 | (vp.x - scroll.x + m_padding.x + m_proj.applyX<double>(pt.x)), |
| 1542 | (vp.y - scroll.y + m_padding.y + m_proj.applyY<double>(pt.y))); |
| 1543 | } |
| 1544 | |
| 1545 | Rect Editor::screenToEditor(const Rect& rc) |
| 1546 | { |
| 1547 | return gfx::Rect( |
| 1548 | screenToEditor(rc.origin()), |
| 1549 | screenToEditorCeiling(rc.point2())); |
| 1550 | } |
| 1551 | |
| 1552 | Rect Editor::editorToScreen(const Rect& rc) |
| 1553 | { |
| 1554 | return gfx::Rect( |
| 1555 | editorToScreen(rc.origin()), |
| 1556 | editorToScreen(rc.point2())); |
| 1557 | } |
| 1558 | |
| 1559 | gfx::RectF Editor::editorToScreenF(const gfx::RectF& rc) |
| 1560 | { |
| 1561 | return gfx::RectF( |
| 1562 | editorToScreenF(rc.origin()), |
| 1563 | editorToScreenF(rc.point2())); |
| 1564 | } |
| 1565 | |
| 1566 | void Editor::add_observer(EditorObserver* observer) |
| 1567 | { |
| 1568 | m_observers.add_observer(observer); |
| 1569 | } |
| 1570 | |
| 1571 | void Editor::remove_observer(EditorObserver* observer) |
| 1572 | { |
| 1573 | m_observers.remove_observer(observer); |
| 1574 | } |
| 1575 | |
| 1576 | void Editor::setCustomizationDelegate(EditorCustomizationDelegate* delegate) |
| 1577 | { |
| 1578 | if (m_customizationDelegate) |
| 1579 | m_customizationDelegate->dispose(); |
| 1580 | |
| 1581 | m_customizationDelegate = delegate; |
| 1582 | } |
| 1583 | |
| 1584 | Rect Editor::getViewportBounds() |
| 1585 | { |
| 1586 | ui::View* view = View::getView(this); |
| 1587 | if (view) |
| 1588 | return screenToEditor(view->viewportBounds()); |
| 1589 | else |
| 1590 | return bounds(); |
| 1591 | } |
| 1592 | |
| 1593 | // Returns the visible area of the active sprite. |
| 1594 | Rect Editor::getVisibleSpriteBounds() |
| 1595 | { |
| 1596 | if (m_sprite) |
| 1597 | return getViewportBounds().createIntersection(gfx::Rect(canvasSize())); |
| 1598 | |
| 1599 | // This cannot happen, the sprite must be != nullptr. In old |
| 1600 | // Aseprite versions we were using one Editor to show multiple |
| 1601 | // sprites (switching the sprite inside the editor). Now we have one |
| 1602 | // (or more) editor(s) for each sprite. |
| 1603 | ASSERT(false); |
| 1604 | |
| 1605 | // Return an empty rectangle if there is not a active sprite. |
| 1606 | return Rect(); |
| 1607 | } |
| 1608 | |
| 1609 | // Changes the scroll to see the given point as the center of the editor. |
| 1610 | void Editor::centerInSpritePoint(const gfx::Point& spritePos) |
| 1611 | { |
| 1612 | HideBrushPreview hide(m_brushPreview); |
| 1613 | View* view = View::getView(this); |
| 1614 | Rect vp = view->viewportBounds(); |
| 1615 | |
| 1616 | gfx::Point scroll( |
| 1617 | m_padding.x - (vp.w/2) + m_proj.applyX(1)/2 + m_proj.applyX(spritePos.x), |
| 1618 | m_padding.y - (vp.h/2) + m_proj.applyY(1)/2 + m_proj.applyY(spritePos.y)); |
| 1619 | |
| 1620 | updateEditor(false); |
| 1621 | setEditorScroll(scroll); |
| 1622 | invalidate(); |
| 1623 | } |
| 1624 | |
| 1625 | void Editor::updateStatusBar() |
| 1626 | { |
| 1627 | if (!hasMouse()) |
| 1628 | return; |
| 1629 | |
| 1630 | // Setup status bar using the current editor's state |
| 1631 | m_state->onUpdateStatusBar(this); |
| 1632 | } |
| 1633 | |
| 1634 | void Editor::updateQuicktool() |
| 1635 | { |
| 1636 | if (m_customizationDelegate && !hasCapture()) { |
| 1637 | auto atm = App::instance()->activeToolManager(); |
| 1638 | tools::Tool* selectedTool = atm->selectedTool(); |
| 1639 | |
| 1640 | // Don't change quicktools if we are in a selection tool and using |
| 1641 | // the selection modifiers. |
| 1642 | if (selectedTool->getInk(0)->isSelection() && |
| 1643 | int(m_customizationDelegate->getPressedKeyAction(KeyContext::SelectionTool)) != 0) { |
| 1644 | if (atm->quickTool()) |
| 1645 | atm->newQuickToolSelectedFromEditor(nullptr); |
| 1646 | return; |
| 1647 | } |
| 1648 | |
| 1649 | tools::Tool* newQuicktool = |
| 1650 | m_customizationDelegate->getQuickTool(selectedTool); |
| 1651 | |
| 1652 | // Check if the current state accept the given quicktool. |
| 1653 | if (newQuicktool && !m_state->acceptQuickTool(newQuicktool)) |
| 1654 | return; |
| 1655 | |
| 1656 | atm->newQuickToolSelectedFromEditor(newQuicktool); |
| 1657 | } |
| 1658 | } |
| 1659 | |
| 1660 | void Editor::updateToolByTipProximity(ui::PointerType pointerType) |
| 1661 | { |
| 1662 | auto activeToolManager = App::instance()->activeToolManager(); |
| 1663 | |
| 1664 | if (pointerType == ui::PointerType::Eraser) { |
| 1665 | activeToolManager->eraserTipProximity(); |
| 1666 | } |
| 1667 | else { |
| 1668 | activeToolManager->regularTipProximity(); |
| 1669 | } |
| 1670 | } |
| 1671 | |
| 1672 | void Editor::updateToolLoopModifiersIndicators(const bool firstFromMouseDown) |
| 1673 | { |
| 1674 | int modifiers = int(tools::ToolLoopModifiers::kNone); |
| 1675 | const bool autoSelectLayer = isAutoSelectLayer(); |
| 1676 | bool newAutoSelectLayer = autoSelectLayer; |
| 1677 | KeyAction action; |
| 1678 | |
| 1679 | if (m_customizationDelegate) { |
| 1680 | auto atm = App::instance()->activeToolManager(); |
| 1681 | |
| 1682 | // When the mouse is captured, is when we are scrolling, or |
| 1683 | // drawing, or moving, or selecting, etc. So several |
| 1684 | // parameters/tool-loop-modifiers are static. |
| 1685 | if (hasCapture()) { |
| 1686 | modifiers |= (int(m_toolLoopModifiers) & |
| 1687 | (int(tools::ToolLoopModifiers::kReplaceSelection) | |
| 1688 | int(tools::ToolLoopModifiers::kAddSelection) | |
| 1689 | int(tools::ToolLoopModifiers::kSubtractSelection) | |
| 1690 | int(tools::ToolLoopModifiers::kIntersectSelection))); |
| 1691 | |
| 1692 | tools::Tool* tool = atm->selectedTool(); |
| 1693 | tools::Controller* controller = (tool ? tool->getController(0): nullptr); |
| 1694 | tools::Ink* ink = (tool ? tool->getInk(0): nullptr); |
| 1695 | |
| 1696 | // Shape tools modifiers (line, curves, rectangles, etc.) |
| 1697 | if (controller && controller->isTwoPoints()) { |
| 1698 | action = m_customizationDelegate->getPressedKeyAction(KeyContext::ShapeTool); |
| 1699 | |
| 1700 | // For two-points-selection-like tools (Rectangular/Elliptical |
| 1701 | // Marquee) we prefer to activate the |
| 1702 | // square-aspect/rotation/etc. only when the user presses the |
| 1703 | // modifier key again in the ToolLoop (and not before starting |
| 1704 | // the loop). So Alt+selection will add a selection, but |
| 1705 | // willn't start the square-aspect until we press Alt key |
| 1706 | // again, or Alt+Shift+selection tool will subtract the |
| 1707 | // selection but will not start the rotation until we release |
| 1708 | // and press the Alt key again. |
| 1709 | if (!firstFromMouseDown || |
| 1710 | !ink || !ink->isSelection()) { |
| 1711 | if (int(action & KeyAction::MoveOrigin)) |
| 1712 | modifiers |= int(tools::ToolLoopModifiers::kMoveOrigin); |
| 1713 | if (int(action & KeyAction::SquareAspect)) |
| 1714 | modifiers |= int(tools::ToolLoopModifiers::kSquareAspect); |
| 1715 | if (int(action & KeyAction::DrawFromCenter)) |
| 1716 | modifiers |= int(tools::ToolLoopModifiers::kFromCenter); |
| 1717 | if (int(action & KeyAction::RotateShape)) |
| 1718 | modifiers |= int(tools::ToolLoopModifiers::kRotateShape); |
| 1719 | } |
| 1720 | } |
| 1721 | |
| 1722 | // Freehand modifiers |
| 1723 | if (controller && controller->isFreehand()) { |
| 1724 | action = m_customizationDelegate->getPressedKeyAction(KeyContext::FreehandTool); |
| 1725 | if (int(action & KeyAction::AngleSnapFromLastPoint)) |
| 1726 | modifiers |= int(tools::ToolLoopModifiers::kSquareAspect); |
| 1727 | } |
| 1728 | } |
| 1729 | else { |
| 1730 | // We update the selection mode only if we're not selecting. |
| 1731 | action = m_customizationDelegate->getPressedKeyAction(KeyContext::SelectionTool); |
| 1732 | |
| 1733 | gen::SelectionMode mode = Preferences::instance().selection.mode(); |
| 1734 | if (int(action & KeyAction::SubtractSelection) || |
| 1735 | // Don't use "subtract" mode if the selection was activated |
| 1736 | // with the "right click mode = a selection-like tool" |
| 1737 | (m_secondaryButton && |
| 1738 | atm->selectedTool() && |
| 1739 | atm->selectedTool()->getInk(0)->isSelection())) { |
| 1740 | mode = gen::SelectionMode::SUBTRACT; |
| 1741 | } |
| 1742 | else if (int(action & KeyAction::IntersectSelection)) { |
| 1743 | mode = gen::SelectionMode::INTERSECT; |
| 1744 | } |
| 1745 | else if (int(action & KeyAction::AddSelection)) { |
| 1746 | mode = gen::SelectionMode::ADD; |
| 1747 | } |
| 1748 | switch (mode) { |
| 1749 | case gen::SelectionMode::DEFAULT: modifiers |= int(tools::ToolLoopModifiers::kReplaceSelection); break; |
| 1750 | case gen::SelectionMode::ADD: modifiers |= int(tools::ToolLoopModifiers::kAddSelection); break; |
| 1751 | case gen::SelectionMode::SUBTRACT: modifiers |= int(tools::ToolLoopModifiers::kSubtractSelection); break; |
| 1752 | case gen::SelectionMode::INTERSECT: modifiers |= int(tools::ToolLoopModifiers::kIntersectSelection); break; |
| 1753 | } |
| 1754 | |
| 1755 | // For move tool |
| 1756 | action = m_customizationDelegate->getPressedKeyAction(KeyContext::MoveTool); |
| 1757 | if (int(action & KeyAction::AutoSelectLayer)) |
| 1758 | newAutoSelectLayer = Preferences::instance().editor.autoSelectLayerQuick(); |
| 1759 | else |
| 1760 | newAutoSelectLayer = Preferences::instance().editor.autoSelectLayer(); |
| 1761 | } |
| 1762 | } |
| 1763 | |
| 1764 | ContextBar* ctxBar = App::instance()->contextBar(); |
| 1765 | |
| 1766 | if (int(m_toolLoopModifiers) != modifiers) { |
| 1767 | m_toolLoopModifiers = tools::ToolLoopModifiers(modifiers); |
| 1768 | |
| 1769 | // TODO the contextbar should be a observer of the current editor |
| 1770 | ctxBar->updateToolLoopModifiersIndicators(m_toolLoopModifiers); |
| 1771 | |
| 1772 | if (auto drawingState = dynamic_cast<DrawingState*>(m_state.get())) { |
| 1773 | drawingState->notifyToolLoopModifiersChange(this); |
| 1774 | } |
| 1775 | } |
| 1776 | |
| 1777 | if (autoSelectLayer != newAutoSelectLayer) |
| 1778 | ctxBar->updateAutoSelectLayer(newAutoSelectLayer); |
| 1779 | } |
| 1780 | |
| 1781 | app::Color Editor::getColorByPosition(const gfx::Point& mousePos) |
| 1782 | { |
| 1783 | Site site = getSite(); |
| 1784 | if (site.sprite()) { |
| 1785 | gfx::PointF editorPos = screenToEditorF(mousePos); |
| 1786 | |
| 1787 | ColorPicker picker; |
| 1788 | site.tilemapMode(TilemapMode::Pixels); |
| 1789 | picker.pickColor(site, editorPos, m_proj, |
| 1790 | ColorPicker::FromComposition); |
| 1791 | return picker.color(); |
| 1792 | } |
| 1793 | else |
| 1794 | return app::Color::fromMask(); |
| 1795 | } |
| 1796 | |
| 1797 | doc::tile_t Editor::getTileByPosition(const gfx::Point& mousePos) |
| 1798 | { |
| 1799 | Site site = getSite(); |
| 1800 | if (site.sprite()) { |
| 1801 | gfx::PointF editorPos = screenToEditorF(mousePos); |
| 1802 | |
| 1803 | ColorPicker picker; |
| 1804 | site.tilemapMode(TilemapMode::Tiles); |
| 1805 | picker.pickColor(site, editorPos, m_proj, |
| 1806 | ColorPicker::FromComposition); |
| 1807 | |
| 1808 | return picker.tile(); |
| 1809 | } |
| 1810 | else |
| 1811 | return doc::notile; |
| 1812 | } |
| 1813 | |
| 1814 | bool Editor::startStraightLineWithFreehandTool(const tools::Pointer* pointer) |
| 1815 | { |
| 1816 | tools::Tool* tool = App::instance()->activeToolManager()->selectedTool(); |
| 1817 | // TODO add support for more buttons (X1, X2, etc.) |
| 1818 | int i = (pointer && pointer->button() == tools::Pointer::Button::Right ? 1: 0); |
| 1819 | return |
| 1820 | (isActive() && |
| 1821 | (hasMouse() || hasCapture()) && |
| 1822 | tool && |
| 1823 | tool->getController(i)->isFreehand() && |
| 1824 | tool->getInk(i)->isPaint() && |
| 1825 | (getCustomizationDelegate() |
| 1826 | ->getPressedKeyAction(KeyContext::FreehandTool) & KeyAction::StraightLineFromLastPoint) == KeyAction::StraightLineFromLastPoint && |
| 1827 | document()->lastDrawingPoint() != Doc::NoLastDrawingPoint()); |
| 1828 | } |
| 1829 | |
| 1830 | bool Editor::isSliceSelected(const doc::Slice* slice) const |
| 1831 | { |
| 1832 | ASSERT(slice); |
| 1833 | return m_selectedSlices.contains(slice->id()); |
| 1834 | } |
| 1835 | |
| 1836 | void Editor::clearSlicesSelection() |
| 1837 | { |
| 1838 | if (!m_selectedSlices.empty()) { |
| 1839 | m_selectedSlices.clear(); |
| 1840 | invalidate(); |
| 1841 | |
| 1842 | if (isActive()) |
| 1843 | UIContext::instance()->notifyActiveSiteChanged(); |
| 1844 | } |
| 1845 | } |
| 1846 | |
| 1847 | void Editor::selectSlice(const doc::Slice* slice) |
| 1848 | { |
| 1849 | ASSERT(slice); |
| 1850 | m_selectedSlices.insert(slice->id()); |
| 1851 | invalidate(); |
| 1852 | |
| 1853 | if (isActive()) |
| 1854 | UIContext::instance()->notifyActiveSiteChanged(); |
| 1855 | } |
| 1856 | |
| 1857 | bool Editor::selectSliceBox(const gfx::Rect& box) |
| 1858 | { |
| 1859 | m_selectedSlices.clear(); |
| 1860 | for (auto slice : m_sprite->slices()) { |
| 1861 | auto key = slice->getByFrame(m_frame); |
| 1862 | if (key && key->bounds().intersects(box)) |
| 1863 | m_selectedSlices.insert(slice->id()); |
| 1864 | } |
| 1865 | invalidate(); |
| 1866 | |
| 1867 | if (isActive()) |
| 1868 | UIContext::instance()->notifyActiveSiteChanged(); |
| 1869 | |
| 1870 | return !m_selectedSlices.empty(); |
| 1871 | } |
| 1872 | |
| 1873 | void Editor::selectAllSlices() |
| 1874 | { |
| 1875 | for (auto slice : m_sprite->slices()) |
| 1876 | m_selectedSlices.insert(slice->id()); |
| 1877 | invalidate(); |
| 1878 | |
| 1879 | if (isActive()) |
| 1880 | UIContext::instance()->notifyActiveSiteChanged(); |
| 1881 | } |
| 1882 | |
| 1883 | void Editor::cancelSelections() |
| 1884 | { |
| 1885 | clearSlicesSelection(); |
| 1886 | } |
| 1887 | |
| 1888 | ////////////////////////////////////////////////////////////////////// |
| 1889 | // Message handler for the editor |
| 1890 | |
| 1891 | bool Editor::onProcessMessage(Message* msg) |
| 1892 | { |
| 1893 | // Delete states |
| 1894 | if (!m_deletedStates.empty()) |
| 1895 | m_deletedStates.clear(); |
| 1896 | |
| 1897 | switch (msg->type()) { |
| 1898 | |
| 1899 | case kTimerMessage: |
| 1900 | if (static_cast<TimerMessage*>(msg)->timer() == &m_antsTimer) { |
| 1901 | if (isVisible() && m_sprite) { |
| 1902 | drawMaskSafe(); |
| 1903 | |
| 1904 | // Set offset to make selection-movement effect |
| 1905 | if (m_antsOffset < 7) |
| 1906 | m_antsOffset++; |
| 1907 | else |
| 1908 | m_antsOffset = 0; |
| 1909 | } |
| 1910 | else if (m_antsTimer.isRunning()) { |
| 1911 | m_antsTimer.stop(); |
| 1912 | } |
| 1913 | } |
| 1914 | break; |
| 1915 | |
| 1916 | case kFocusEnterMessage: { |
| 1917 | ASSERT(m_state); |
| 1918 | if (m_state) |
| 1919 | m_state->onEditorGotFocus(this); |
| 1920 | break; |
| 1921 | } |
| 1922 | |
| 1923 | case kMouseEnterMessage: |
| 1924 | m_brushPreview.hide(); |
| 1925 | updateToolLoopModifiersIndicators(); |
| 1926 | updateQuicktool(); |
| 1927 | break; |
| 1928 | |
| 1929 | case kMouseLeaveMessage: |
| 1930 | m_brushPreview.hide(); |
| 1931 | StatusBar::instance()->showDefaultText(); |
| 1932 | |
| 1933 | // Hide autoguides |
| 1934 | updateAutoCelGuides(nullptr); |
| 1935 | break; |
| 1936 | |
| 1937 | case kMouseDownMessage: |
| 1938 | if (m_sprite) { |
| 1939 | MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); |
| 1940 | |
| 1941 | // If we're going to start drawing, we cancel the flashing |
| 1942 | // layer. |
| 1943 | if (m_flashing != Flashing::None) { |
| 1944 | m_flashing = Flashing::None; |
| 1945 | invalidateCanvas(); |
| 1946 | } |
| 1947 | |
| 1948 | m_oldPos = mouseMsg->position(); |
| 1949 | updateToolByTipProximity(mouseMsg->pointerType()); |
| 1950 | updateAutoCelGuides(msg); |
| 1951 | |
| 1952 | // Only when we right-click with the regular "paint bg-color |
| 1953 | // right-click mode" we will mark indicate that the secondary |
| 1954 | // button was used (m_secondaryButton == true). |
| 1955 | if (mouseMsg->right() && !m_secondaryButton) { |
| 1956 | m_secondaryButton = true; |
| 1957 | } |
| 1958 | |
| 1959 | updateToolLoopModifiersIndicators(); |
| 1960 | updateQuicktool(); |
| 1961 | setCursor(mouseMsg->position()); |
| 1962 | |
| 1963 | App::instance()->activeToolManager() |
| 1964 | ->pressButton(pointer_from_msg(this, mouseMsg)); |
| 1965 | |
| 1966 | EditorStatePtr holdState(m_state); |
| 1967 | bool state = m_state->onMouseDown(this, mouseMsg); |
| 1968 | |
| 1969 | // Re-update the tool modifiers if the state has changed |
| 1970 | // (e.g. we are on DrawingState now). This is required for the |
| 1971 | // Line tool to be able to Shift+press mouse buttons to start |
| 1972 | // drawing lines with the angle snapped. |
| 1973 | if (m_state != holdState) |
| 1974 | updateToolLoopModifiersIndicators(true); |
| 1975 | |
| 1976 | return state; |
| 1977 | } |
| 1978 | break; |
| 1979 | |
| 1980 | case kMouseMoveMessage: |
| 1981 | if (m_sprite) { |
| 1982 | EditorStatePtr holdState(m_state); |
| 1983 | MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); |
| 1984 | |
| 1985 | updateToolByTipProximity(mouseMsg->pointerType()); |
| 1986 | updateAutoCelGuides(msg); |
| 1987 | |
| 1988 | return m_state->onMouseMove(this, static_cast<MouseMessage*>(msg)); |
| 1989 | } |
| 1990 | break; |
| 1991 | |
| 1992 | case kMouseUpMessage: |
| 1993 | if (m_sprite) { |
| 1994 | EditorStatePtr holdState(m_state); |
| 1995 | MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); |
| 1996 | bool result = m_state->onMouseUp(this, mouseMsg); |
| 1997 | |
| 1998 | updateToolByTipProximity(mouseMsg->pointerType()); |
| 1999 | updateAutoCelGuides(msg); |
| 2000 | |
| 2001 | if (!hasCapture()) { |
| 2002 | App::instance()->activeToolManager()->releaseButtons(); |
| 2003 | m_secondaryButton = false; |
| 2004 | |
| 2005 | updateToolLoopModifiersIndicators(); |
| 2006 | updateQuicktool(); |
| 2007 | setCursor(mouseMsg->position()); |
| 2008 | } |
| 2009 | |
| 2010 | if (result) |
| 2011 | return true; |
| 2012 | } |
| 2013 | break; |
| 2014 | |
| 2015 | case kDoubleClickMessage: |
| 2016 | if (m_sprite) { |
| 2017 | MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); |
| 2018 | EditorStatePtr holdState(m_state); |
| 2019 | |
| 2020 | updateToolByTipProximity(mouseMsg->pointerType()); |
| 2021 | |
| 2022 | bool used = m_state->onDoubleClick(this, mouseMsg); |
| 2023 | if (used) |
| 2024 | return true; |
| 2025 | } |
| 2026 | break; |
| 2027 | |
| 2028 | case kTouchMagnifyMessage: |
| 2029 | if (m_sprite) { |
| 2030 | EditorStatePtr holdState(m_state); |
| 2031 | return m_state->onTouchMagnify(this, static_cast<TouchMessage*>(msg)); |
| 2032 | } |
| 2033 | break; |
| 2034 | |
| 2035 | case kKeyDownMessage: |
| 2036 | #if ENABLE_DEVMODE |
| 2037 | // Switch render mode |
| 2038 | if (!msg->ctrlPressed() && |
| 2039 | static_cast<KeyMessage*>(msg)->scancode() == kKeyF1) { |
| 2040 | Preferences::instance().experimental.newRenderEngine( |
| 2041 | !Preferences::instance().experimental.newRenderEngine()); |
| 2042 | invalidateCanvas(); |
| 2043 | return true; |
| 2044 | } |
| 2045 | #endif |
| 2046 | if (m_sprite) { |
| 2047 | EditorStatePtr holdState(m_state); |
| 2048 | bool used = m_state->onKeyDown(this, static_cast<KeyMessage*>(msg)); |
| 2049 | |
| 2050 | updateToolLoopModifiersIndicators(); |
| 2051 | updateAutoCelGuides(msg); |
| 2052 | if (hasMouse()) { |
| 2053 | updateQuicktool(); |
| 2054 | setCursor(mousePosInDisplay()); |
| 2055 | } |
| 2056 | |
| 2057 | if (used) |
| 2058 | return true; |
| 2059 | } |
| 2060 | break; |
| 2061 | |
| 2062 | case kKeyUpMessage: |
| 2063 | if (m_sprite) { |
| 2064 | EditorStatePtr holdState(m_state); |
| 2065 | bool used = m_state->onKeyUp(this, static_cast<KeyMessage*>(msg)); |
| 2066 | |
| 2067 | updateToolLoopModifiersIndicators(); |
| 2068 | updateAutoCelGuides(msg); |
| 2069 | if (hasMouse()) { |
| 2070 | updateQuicktool(); |
| 2071 | setCursor(mousePosInDisplay()); |
| 2072 | } |
| 2073 | |
| 2074 | if (used) |
| 2075 | return true; |
| 2076 | } |
| 2077 | break; |
| 2078 | |
| 2079 | case kMouseWheelMessage: |
| 2080 | if (m_sprite && hasMouse()) { |
| 2081 | EditorStatePtr holdState(m_state); |
| 2082 | if (m_state->onMouseWheel(this, static_cast<MouseMessage*>(msg))) |
| 2083 | return true; |
| 2084 | } |
| 2085 | break; |
| 2086 | |
| 2087 | case kSetCursorMessage: |
| 2088 | setCursor(static_cast<MouseMessage*>(msg)->position()); |
| 2089 | return true; |
| 2090 | |
| 2091 | } |
| 2092 | |
| 2093 | bool result = Widget::onProcessMessage(msg); |
| 2094 | |
| 2095 | if (msg->type() == kPaintMessage && |
| 2096 | m_flashing != Flashing::None) { |
| 2097 | const PaintMessage* ptmsg = static_cast<const PaintMessage*>(msg); |
| 2098 | if (ptmsg->count() == 0) { |
| 2099 | if (m_flashing == Flashing::WithFlashExtraCel) { |
| 2100 | m_flashing = Flashing::WaitingDeferedPaint; |
| 2101 | |
| 2102 | // We have to defer an invalidation so we can keep the |
| 2103 | // flashing layer in the extra cel some time. |
| 2104 | defer_invalid_rect(View::getView(this)->viewportBounds()); |
| 2105 | } |
| 2106 | else if (m_flashing == Flashing::WaitingDeferedPaint) { |
| 2107 | m_flashing = Flashing::None; |
| 2108 | |
| 2109 | if (m_brushPreview.onScreen()) { |
| 2110 | m_brushPreview.hide(); |
| 2111 | |
| 2112 | // Destroy the extra cel explicitly (it could happend |
| 2113 | // automatically by the m_brushPreview.show()) just in case |
| 2114 | // that the brush preview will not use the extra cel |
| 2115 | // (e.g. in the case of the Eraser tool). |
| 2116 | m_document->setExtraCel(ExtraCelRef(nullptr)); |
| 2117 | |
| 2118 | showBrushPreview(mousePosInDisplay()); |
| 2119 | } |
| 2120 | else { |
| 2121 | m_document->setExtraCel(ExtraCelRef(nullptr)); |
| 2122 | } |
| 2123 | |
| 2124 | // Redraw all editors (without this the preview editor will |
| 2125 | // still show the flashing layer). |
| 2126 | for (auto editor : UIContext::instance()->getAllEditorsIncludingPreview(m_document)) { |
| 2127 | editor->invalidateCanvas(); |
| 2128 | |
| 2129 | // Re-generate painting messages just right now (it looks |
| 2130 | // like the widget update region is lost after the last |
| 2131 | // kPaintMessage). |
| 2132 | editor->flushRedraw(); |
| 2133 | } |
| 2134 | } |
| 2135 | } |
| 2136 | } |
| 2137 | |
| 2138 | return result; |
| 2139 | } |
| 2140 | |
| 2141 | void Editor::onSizeHint(SizeHintEvent& ev) |
| 2142 | { |
| 2143 | gfx::Size sz(0, 0); |
| 2144 | if (m_sprite) { |
| 2145 | gfx::Point padding = calcExtraPadding(m_proj); |
| 2146 | gfx::Size canvas = canvasSize(); |
| 2147 | sz.w = m_proj.applyX(canvas.w) + padding.x*2; |
| 2148 | sz.h = m_proj.applyY(canvas.h) + padding.y*2; |
| 2149 | } |
| 2150 | else { |
| 2151 | sz.w = 4; |
| 2152 | sz.h = 4; |
| 2153 | } |
| 2154 | ev.setSizeHint(sz); |
| 2155 | } |
| 2156 | |
| 2157 | void Editor::onResize(ui::ResizeEvent& ev) |
| 2158 | { |
| 2159 | Widget::onResize(ev); |
| 2160 | m_padding = calcExtraPadding(m_proj); |
| 2161 | } |
| 2162 | |
| 2163 | void Editor::onPaint(ui::PaintEvent& ev) |
| 2164 | { |
| 2165 | std::unique_ptr<HideBrushPreview> hide; |
| 2166 | if (m_flashing == Flashing::None) { |
| 2167 | // If we are drawing the editor for a tooltip background or any |
| 2168 | // other semi-transparent widget (e.g. popups), we destroy the brush |
| 2169 | // preview/extra cel to avoid drawing a part of the brush in the |
| 2170 | // transparent widget background. |
| 2171 | if (ev.isTransparentBg()) { |
| 2172 | m_brushPreview.discardBrushPreview(); |
| 2173 | } |
| 2174 | else { |
| 2175 | hide.reset(new HideBrushPreview(m_brushPreview)); |
| 2176 | } |
| 2177 | } |
| 2178 | |
| 2179 | Graphics* g = ev.graphics(); |
| 2180 | gfx::Rect rc = clientBounds(); |
| 2181 | auto theme = SkinTheme::get(this); |
| 2182 | |
| 2183 | // Editor without sprite |
| 2184 | if (!m_sprite) { |
| 2185 | g->fillRect(theme->colors.editorFace(), rc); |
| 2186 | } |
| 2187 | // Editor with sprite |
| 2188 | else { |
| 2189 | try { |
| 2190 | // Lock the sprite to read/render it. Here we don't wait if the |
| 2191 | // document is locked (e.g. a filter is being applied to the |
| 2192 | // sprite) to avoid locking the UI. |
| 2193 | DocReader documentReader(m_document, 0); |
| 2194 | |
| 2195 | // Draw the sprite in the editor |
| 2196 | renderChrono.reset(); |
| 2197 | drawBackground(g); |
| 2198 | drawSpriteUnclippedRect(g, gfx::Rect(0, 0, m_sprite->width(), m_sprite->height())); |
| 2199 | renderElapsed = renderChrono.elapsed(); |
| 2200 | |
| 2201 | #if ENABLE_DEVMODE |
| 2202 | // Show performance stats (TODO show performance stats in other widget) |
| 2203 | if (Preferences::instance().perf.showRenderTime()) { |
| 2204 | View* view = View::getView(this); |
| 2205 | gfx::Rect vp = view->viewportBounds(); |
| 2206 | char buf[128]; |
| 2207 | sprintf(buf, "%c %.4gs" , |
| 2208 | Preferences::instance().experimental.newRenderEngine() ? 'N': 'O', |
| 2209 | renderElapsed); |
| 2210 | g->drawText( |
| 2211 | buf, |
| 2212 | gfx::rgba(255, 255, 255, 255), |
| 2213 | gfx::rgba(0, 0, 0, 255), |
| 2214 | vp.origin() - bounds().origin()); |
| 2215 | |
| 2216 | m_perfInfoBounds.setOrigin(vp.origin()); |
| 2217 | m_perfInfoBounds.setSize(g->measureUIText(buf)); |
| 2218 | } |
| 2219 | #endif // ENABLE_DEVMODE |
| 2220 | |
| 2221 | // Draw the mask boundaries |
| 2222 | if (m_document->hasMaskBoundaries()) { |
| 2223 | drawMask(g); |
| 2224 | m_antsTimer.start(); |
| 2225 | } |
| 2226 | else { |
| 2227 | m_antsTimer.stop(); |
| 2228 | } |
| 2229 | } |
| 2230 | catch (const LockedDocException&) { |
| 2231 | // The sprite is locked, so we cannot render it, we can draw an |
| 2232 | // opaque background now, and defer the rendering of the sprite |
| 2233 | // for later. |
| 2234 | g->fillRect(theme->colors.editorFace(), rc); |
| 2235 | defer_invalid_rect(g->getClipBounds().offset(bounds().origin())); |
| 2236 | } |
| 2237 | } |
| 2238 | } |
| 2239 | |
| 2240 | void Editor::onInvalidateRegion(const gfx::Region& region) |
| 2241 | { |
| 2242 | Widget::onInvalidateRegion(region); |
| 2243 | m_brushPreview.invalidateRegion(region); |
| 2244 | } |
| 2245 | |
| 2246 | // When the current tool is changed |
| 2247 | void Editor::onActiveToolChange(tools::Tool* tool) |
| 2248 | { |
| 2249 | m_state->onActiveToolChange(this, tool); |
| 2250 | if (hasMouse()) { |
| 2251 | updateStatusBar(); |
| 2252 | setCursor(mousePosInDisplay()); |
| 2253 | } |
| 2254 | } |
| 2255 | |
| 2256 | void Editor::onSamplingChange() |
| 2257 | { |
| 2258 | if (m_proj.scaleX() < 1.0 && |
| 2259 | m_proj.scaleY() < 1.0 && |
| 2260 | isUsingNewRenderEngine()) { |
| 2261 | invalidate(); |
| 2262 | } |
| 2263 | } |
| 2264 | |
| 2265 | void Editor::onFgColorChange() |
| 2266 | { |
| 2267 | m_brushPreview.redraw(); |
| 2268 | } |
| 2269 | |
| 2270 | void Editor::onContextBarBrushChange() |
| 2271 | { |
| 2272 | m_brushPreview.redraw(); |
| 2273 | } |
| 2274 | |
| 2275 | void Editor::onTiledModeBeforeChange() |
| 2276 | { |
| 2277 | m_oldMainTilePos = mainTilePosition(); |
| 2278 | } |
| 2279 | |
| 2280 | void Editor::onTiledModeChange() |
| 2281 | { |
| 2282 | ASSERT(m_sprite); |
| 2283 | |
| 2284 | m_tiledModeHelper.mode(m_docPref.tiled.mode()); |
| 2285 | |
| 2286 | // Get the sprite point in the middle of the editor, so we can |
| 2287 | // restore this with the new tiled mode in the main tile. |
| 2288 | View* view = View::getView(this); |
| 2289 | gfx::Rect vp = view->viewportBounds(); |
| 2290 | gfx::Point screenPos(vp.x + vp.w/2, |
| 2291 | vp.y + vp.h/2); |
| 2292 | gfx::Point spritePos(screenToEditor(screenPos)); |
| 2293 | spritePos -= m_oldMainTilePos; |
| 2294 | |
| 2295 | // Update padding |
| 2296 | m_padding = calcExtraPadding(m_proj); |
| 2297 | |
| 2298 | spritePos += mainTilePosition(); |
| 2299 | screenPos = editorToScreen(spritePos); |
| 2300 | |
| 2301 | centerInSpritePoint(spritePos); |
| 2302 | } |
| 2303 | |
| 2304 | void Editor::() |
| 2305 | { |
| 2306 | invalidate(); |
| 2307 | } |
| 2308 | |
| 2309 | void Editor::onColorSpaceChanged(DocEvent& ev) |
| 2310 | { |
| 2311 | // As the document has a new color space, we've to redraw the |
| 2312 | // complete canvas again with the new color profile. |
| 2313 | invalidate(); |
| 2314 | } |
| 2315 | |
| 2316 | void Editor::onExposeSpritePixels(DocEvent& ev) |
| 2317 | { |
| 2318 | if (m_state && ev.sprite() == m_sprite) |
| 2319 | m_state->onExposeSpritePixels(ev.region()); |
| 2320 | } |
| 2321 | |
| 2322 | void Editor::onSpritePixelRatioChanged(DocEvent& ev) |
| 2323 | { |
| 2324 | m_proj.setPixelRatio(ev.sprite()->pixelRatio()); |
| 2325 | invalidate(); |
| 2326 | } |
| 2327 | |
| 2328 | // TODO similar to ActiveSiteHandler::onBeforeRemoveLayer() and Timeline::onBeforeRemoveLayer() |
| 2329 | void Editor::onBeforeRemoveLayer(DocEvent& ev) |
| 2330 | { |
| 2331 | m_showGuidesThisCel = nullptr; |
| 2332 | |
| 2333 | // If the layer that was removed is the selected one in the editor, |
| 2334 | // or is an ancestor of the selected one. |
| 2335 | Layer* layerToSelect = candidate_if_layer_is_deleted(layer(), ev.layer()); |
| 2336 | if (layer() != layerToSelect) |
| 2337 | setLayer(layerToSelect); |
| 2338 | } |
| 2339 | |
| 2340 | void Editor::onBeforeRemoveCel(DocEvent& ev) |
| 2341 | { |
| 2342 | m_showGuidesThisCel = nullptr; |
| 2343 | } |
| 2344 | |
| 2345 | void Editor::onAddTag(DocEvent& ev) |
| 2346 | { |
| 2347 | m_tagFocusBand = -1; |
| 2348 | } |
| 2349 | |
| 2350 | void Editor::onRemoveTag(DocEvent& ev) |
| 2351 | { |
| 2352 | m_tagFocusBand = -1; |
| 2353 | if (m_state) |
| 2354 | m_state->onRemoveTag(this, ev.tag()); |
| 2355 | } |
| 2356 | |
| 2357 | void Editor::onRemoveSlice(DocEvent& ev) |
| 2358 | { |
| 2359 | ASSERT(ev.slice()); |
| 2360 | if (ev.slice() && |
| 2361 | m_selectedSlices.contains(ev.slice()->id())) { |
| 2362 | m_selectedSlices.erase(ev.slice()->id()); |
| 2363 | } |
| 2364 | } |
| 2365 | |
| 2366 | void Editor::setCursor(const gfx::Point& mouseDisplayPos) |
| 2367 | { |
| 2368 | Rect vp = View::getView(this)->viewportBounds(); |
| 2369 | if (!vp.contains(mouseDisplayPos)) |
| 2370 | return; |
| 2371 | |
| 2372 | bool used = false; |
| 2373 | if (m_sprite) |
| 2374 | used = m_state->onSetCursor(this, mouseDisplayPos); |
| 2375 | |
| 2376 | if (!used) |
| 2377 | showMouseCursor(kArrowCursor); |
| 2378 | } |
| 2379 | |
| 2380 | bool Editor::canDraw() |
| 2381 | { |
| 2382 | return (m_layer != NULL && |
| 2383 | m_layer->isImage() && |
| 2384 | m_layer->isVisibleHierarchy() && |
| 2385 | m_layer->isEditableHierarchy() && |
| 2386 | !m_layer->isReference()); |
| 2387 | } |
| 2388 | |
| 2389 | bool Editor::isInsideSelection() |
| 2390 | { |
| 2391 | gfx::Point spritePos = screenToEditor(mousePosInDisplay()); |
| 2392 | spritePos -= mainTilePosition(); |
| 2393 | |
| 2394 | KeyAction action = m_customizationDelegate->getPressedKeyAction(KeyContext::SelectionTool); |
| 2395 | return |
| 2396 | (action == KeyAction::None) && |
| 2397 | m_document && |
| 2398 | m_document->isMaskVisible() && |
| 2399 | m_document->mask()->containsPoint(spritePos.x, spritePos.y); |
| 2400 | } |
| 2401 | |
| 2402 | bool Editor::canStartMovingSelectionPixels() |
| 2403 | { |
| 2404 | return |
| 2405 | isInsideSelection() && |
| 2406 | // We cannot move the selection when add/subtract modes are |
| 2407 | // enabled (we prefer to modify the selection on those modes |
| 2408 | // instead of moving pixels). |
| 2409 | ((int(m_toolLoopModifiers) & int(tools::ToolLoopModifiers::kReplaceSelection)) || |
| 2410 | // We can move the selection on add mode if the preferences says so. |
| 2411 | ((int(m_toolLoopModifiers) & int(tools::ToolLoopModifiers::kAddSelection)) && |
| 2412 | Preferences::instance().selection.moveOnAddMode()) || |
| 2413 | // We can move the selection when the Copy selection key (Ctrl) is pressed. |
| 2414 | (m_customizationDelegate && |
| 2415 | int(m_customizationDelegate->getPressedKeyAction(KeyContext::TranslatingSelection) & KeyAction::CopySelection))); |
| 2416 | } |
| 2417 | |
| 2418 | bool Editor::keepTimelineRange() |
| 2419 | { |
| 2420 | if (auto movingPixels = dynamic_cast<MovingPixelsState*>(m_state.get())) { |
| 2421 | if (movingPixels->canHandleFrameChange()) |
| 2422 | return true; |
| 2423 | } |
| 2424 | return false; |
| 2425 | } |
| 2426 | |
| 2427 | EditorHit Editor::calcHit(const gfx::Point& mouseScreenPos) |
| 2428 | { |
| 2429 | tools::Ink* ink = getCurrentEditorInk(); |
| 2430 | |
| 2431 | if (ink) { |
| 2432 | // Check if we can transform slices |
| 2433 | if (ink->isSlice()) { |
| 2434 | if (m_docPref.show.slices()) { |
| 2435 | gfx::Point mainOffset(mainTilePosition()); |
| 2436 | |
| 2437 | for (auto slice : m_sprite->slices()) { |
| 2438 | auto key = slice->getByFrame(m_frame); |
| 2439 | if (key) { |
| 2440 | gfx::Rect bounds = key->bounds(); |
| 2441 | bounds.offset(mainOffset); |
| 2442 | bounds = editorToScreen(bounds); |
| 2443 | |
| 2444 | gfx::Rect center = key->center(); |
| 2445 | |
| 2446 | // Move bounds |
| 2447 | if (bounds.contains(mouseScreenPos) && |
| 2448 | !bounds.shrink(5*guiscale()).contains(mouseScreenPos)) { |
| 2449 | int border = |
| 2450 | (mouseScreenPos.x <= bounds.x ? LEFT: 0) | |
| 2451 | (mouseScreenPos.y <= bounds.y ? TOP: 0) | |
| 2452 | (mouseScreenPos.x >= bounds.x2() ? RIGHT: 0) | |
| 2453 | (mouseScreenPos.y >= bounds.y2() ? BOTTOM: 0); |
| 2454 | |
| 2455 | EditorHit hit(EditorHit::SliceBounds); |
| 2456 | hit.setBorder(border); |
| 2457 | hit.setSlice(slice); |
| 2458 | return hit; |
| 2459 | } |
| 2460 | |
| 2461 | // Move center |
| 2462 | if (!center.isEmpty()) { |
| 2463 | center = editorToScreen( |
| 2464 | center.offset(key->bounds().origin())); |
| 2465 | |
| 2466 | bool horz1 = gfx::Rect(bounds.x, center.y-2*guiscale(), bounds.w, 5*guiscale()).contains(mouseScreenPos); |
| 2467 | bool horz2 = gfx::Rect(bounds.x, center.y2()-2*guiscale(), bounds.w, 5*guiscale()).contains(mouseScreenPos); |
| 2468 | bool vert1 = gfx::Rect(center.x-2*guiscale(), bounds.y, 5*guiscale(), bounds.h).contains(mouseScreenPos); |
| 2469 | bool vert2 = gfx::Rect(center.x2()-2*guiscale(), bounds.y, 5*guiscale(), bounds.h).contains(mouseScreenPos); |
| 2470 | |
| 2471 | if (horz1 || horz2 || vert1 || vert2) { |
| 2472 | int border = |
| 2473 | (horz1 ? TOP: 0) | |
| 2474 | (horz2 ? BOTTOM: 0) | |
| 2475 | (vert1 ? LEFT: 0) | |
| 2476 | (vert2 ? RIGHT: 0); |
| 2477 | EditorHit hit(EditorHit::SliceCenter); |
| 2478 | hit.setBorder(border); |
| 2479 | hit.setSlice(slice); |
| 2480 | return hit; |
| 2481 | } |
| 2482 | } |
| 2483 | |
| 2484 | // Move all the slice |
| 2485 | if (bounds.contains(mouseScreenPos)) { |
| 2486 | EditorHit hit(EditorHit::SliceBounds); |
| 2487 | hit.setBorder(CENTER | MIDDLE); |
| 2488 | hit.setSlice(slice); |
| 2489 | return hit; |
| 2490 | } |
| 2491 | } |
| 2492 | } |
| 2493 | } |
| 2494 | } |
| 2495 | } |
| 2496 | |
| 2497 | return EditorHit(EditorHit::None); |
| 2498 | } |
| 2499 | |
| 2500 | void Editor::setZoomAndCenterInMouse(const Zoom& zoom, |
| 2501 | const gfx::Point& mousePos, |
| 2502 | ZoomBehavior zoomBehavior) |
| 2503 | { |
| 2504 | HideBrushPreview hide(m_brushPreview); |
| 2505 | View* view = View::getView(this); |
| 2506 | Rect vp = view->viewportBounds(); |
| 2507 | Projection proj = m_proj; |
| 2508 | proj.setZoom(zoom); |
| 2509 | |
| 2510 | gfx::Point screenPos; |
| 2511 | gfx::Point spritePos; |
| 2512 | gfx::PointT<double> subpixelPos(0.5, 0.5); |
| 2513 | |
| 2514 | switch (zoomBehavior) { |
| 2515 | case ZoomBehavior::CENTER: |
| 2516 | screenPos = gfx::Point(vp.x + vp.w/2, |
| 2517 | vp.y + vp.h/2); |
| 2518 | break; |
| 2519 | case ZoomBehavior::MOUSE: |
| 2520 | screenPos = mousePos; |
| 2521 | break; |
| 2522 | } |
| 2523 | |
| 2524 | // Limit zooming screen position to the visible sprite bounds (we |
| 2525 | // use canvasSize() because if the tiled mode is enabled, we need |
| 2526 | // extra space for the zoom) |
| 2527 | gfx::Rect visibleBounds = editorToScreen( |
| 2528 | getViewportBounds().createIntersection(gfx::Rect(gfx::Point(0, 0), canvasSize()))); |
| 2529 | screenPos.x = std::clamp(screenPos.x, visibleBounds.x, visibleBounds.x2()-1); |
| 2530 | screenPos.y = std::clamp(screenPos.y, visibleBounds.y, visibleBounds.y2()-1); |
| 2531 | |
| 2532 | spritePos = screenToEditor(screenPos); |
| 2533 | |
| 2534 | if (zoomBehavior == ZoomBehavior::MOUSE) { |
| 2535 | gfx::Point screenPos2 = editorToScreen(spritePos); |
| 2536 | |
| 2537 | if (m_proj.scaleX() > 1.0) { |
| 2538 | subpixelPos.x = (0.5 + screenPos.x - screenPos2.x) / m_proj.scaleX(); |
| 2539 | if (proj.scaleX() > m_proj.scaleX()) { |
| 2540 | double t = 1.0 / proj.scaleX(); |
| 2541 | if (subpixelPos.x >= 0.5-t && subpixelPos.x <= 0.5+t) |
| 2542 | subpixelPos.x = 0.5; |
| 2543 | } |
| 2544 | } |
| 2545 | |
| 2546 | if (m_proj.scaleY() > 1.0) { |
| 2547 | subpixelPos.y = (0.5 + screenPos.y - screenPos2.y) / m_proj.scaleY(); |
| 2548 | if (proj.scaleY() > m_proj.scaleY()) { |
| 2549 | double t = 1.0 / proj.scaleY(); |
| 2550 | if (subpixelPos.y >= 0.5-t && subpixelPos.y <= 0.5+t) |
| 2551 | subpixelPos.y = 0.5; |
| 2552 | } |
| 2553 | } |
| 2554 | } |
| 2555 | |
| 2556 | gfx::Point padding = calcExtraPadding(proj); |
| 2557 | gfx::Point scrollPos( |
| 2558 | padding.x - (screenPos.x-vp.x) + proj.applyX(spritePos.x+proj.removeX(1)/2) + int(proj.applyX(subpixelPos.x)), |
| 2559 | padding.y - (screenPos.y-vp.y) + proj.applyY(spritePos.y+proj.removeY(1)/2) + int(proj.applyY(subpixelPos.y))); |
| 2560 | |
| 2561 | setZoom(zoom); |
| 2562 | |
| 2563 | if ((m_proj.zoom() != zoom) || (screenPos != view->viewScroll())) { |
| 2564 | updateEditor(false); |
| 2565 | setEditorScroll(scrollPos); |
| 2566 | } |
| 2567 | } |
| 2568 | |
| 2569 | void Editor::pasteImage(const Image* image, const Mask* mask) |
| 2570 | { |
| 2571 | ASSERT(image); |
| 2572 | |
| 2573 | std::unique_ptr<Mask> temp_mask; |
| 2574 | if (!mask) { |
| 2575 | gfx::Rect visibleBounds = getVisibleSpriteBounds(); |
| 2576 | gfx::Rect imageBounds = image->bounds(); |
| 2577 | |
| 2578 | temp_mask.reset(new Mask); |
| 2579 | temp_mask->replace( |
| 2580 | gfx::Rect(visibleBounds.x + visibleBounds.w/2 - imageBounds.w/2, |
| 2581 | visibleBounds.y + visibleBounds.h/2 - imageBounds.h/2, |
| 2582 | imageBounds.w, imageBounds.h)); |
| 2583 | |
| 2584 | mask = temp_mask.get(); |
| 2585 | } |
| 2586 | |
| 2587 | // Change to a selection tool: it's necessary for PixelsMovement |
| 2588 | // which will use the extra cel for transformation preview, and is |
| 2589 | // not compatible with the drawing cursor preview which overwrite |
| 2590 | // the extra cel. |
| 2591 | if (!getCurrentEditorInk()->isSelection()) { |
| 2592 | tools::Tool* defaultSelectionTool = |
| 2593 | App::instance()->toolBox()->getToolById(tools::WellKnownTools::RectangularMarquee); |
| 2594 | |
| 2595 | ToolBar::instance()->selectTool(defaultSelectionTool); |
| 2596 | } |
| 2597 | |
| 2598 | Sprite* sprite = this->sprite(); |
| 2599 | |
| 2600 | // Check bounds where the image will be pasted. |
| 2601 | int x = mask->bounds().x; |
| 2602 | int y = mask->bounds().y; |
| 2603 | { |
| 2604 | const Rect visibleBounds = getViewportBounds(); |
| 2605 | const Point maskCenter = mask->bounds().center(); |
| 2606 | |
| 2607 | // If the pasted image original location center point isn't |
| 2608 | // visible, we center the image in the editor's visible bounds. |
| 2609 | if (maskCenter.x < visibleBounds.x || |
| 2610 | maskCenter.x >= visibleBounds.x2()) { |
| 2611 | x = visibleBounds.x + visibleBounds.w/2 - image->width()/2; |
| 2612 | } |
| 2613 | // In other case, if the center is visible, we put the pasted |
| 2614 | // image in its original location. |
| 2615 | else { |
| 2616 | x = std::clamp(x, visibleBounds.x-image->width(), visibleBounds.x2()-1); |
| 2617 | } |
| 2618 | |
| 2619 | if (maskCenter.y < visibleBounds.y || |
| 2620 | maskCenter.y >= visibleBounds.y2()) { |
| 2621 | y = visibleBounds.y + visibleBounds.h/2 - image->height()/2; |
| 2622 | } |
| 2623 | else { |
| 2624 | y = std::clamp(y, visibleBounds.y-image->height(), visibleBounds.y2()-1); |
| 2625 | } |
| 2626 | |
| 2627 | // Limit the image inside the sprite's bounds. |
| 2628 | if (sprite->width() <= image->width() || |
| 2629 | sprite->height() <= image->height()) { |
| 2630 | // TODO review this (I think limits are wrong and high limit can |
| 2631 | // be negative here) |
| 2632 | x = std::clamp(x, 0, sprite->width() - image->width()); |
| 2633 | y = std::clamp(y, 0, sprite->height() - image->height()); |
| 2634 | } |
| 2635 | else { |
| 2636 | // Also we always limit the 1 image pixel inside the sprite's bounds. |
| 2637 | x = std::clamp(x, -image->width()+1, sprite->width()-1); |
| 2638 | y = std::clamp(y, -image->height()+1, sprite->height()-1); |
| 2639 | } |
| 2640 | } |
| 2641 | |
| 2642 | Site site = getSite(); |
| 2643 | |
| 2644 | // Snap to grid a pasted tilemap |
| 2645 | // TODO should we move this to PixelsMovement or MovingPixelsState? |
| 2646 | if (site.tilemapMode() == TilemapMode::Tiles) { |
| 2647 | gfx::Rect gridBounds = site.gridBounds(); |
| 2648 | gfx::Point pt = snap_to_grid(gridBounds, |
| 2649 | gfx::Point(x, y), |
| 2650 | PreferSnapTo::ClosestGridVertex); |
| 2651 | x = pt.x; |
| 2652 | y = pt.y; |
| 2653 | } |
| 2654 | |
| 2655 | // Clear brush preview, as the extra cel will be replaced with the |
| 2656 | // pasted image. |
| 2657 | m_brushPreview.hide(); |
| 2658 | |
| 2659 | Mask mask2(*mask); |
| 2660 | mask2.setOrigin(x, y); |
| 2661 | |
| 2662 | PixelsMovementPtr pixelsMovement( |
| 2663 | new PixelsMovement(UIContext::instance(), site, |
| 2664 | image, &mask2, "Paste" )); |
| 2665 | |
| 2666 | setState(EditorStatePtr(new MovingPixelsState(this, NULL, pixelsMovement, NoHandle))); |
| 2667 | } |
| 2668 | |
| 2669 | void Editor::startSelectionTransformation(const gfx::Point& move, double angle) |
| 2670 | { |
| 2671 | if (auto movingPixels = dynamic_cast<MovingPixelsState*>(m_state.get())) { |
| 2672 | movingPixels->translate(gfx::PointF(move)); |
| 2673 | if (std::fabs(angle) > 1e-5) |
| 2674 | movingPixels->rotate(angle); |
| 2675 | } |
| 2676 | else if (auto standby = dynamic_cast<StandbyState*>(m_state.get())) { |
| 2677 | ASSERT(m_document->isMaskVisible()); |
| 2678 | standby->startSelectionTransformation(this, move, angle); |
| 2679 | } |
| 2680 | } |
| 2681 | |
| 2682 | void Editor::startFlipTransformation(doc::algorithm::FlipType flipType) |
| 2683 | { |
| 2684 | if (auto movingPixels = dynamic_cast<MovingPixelsState*>(m_state.get())) |
| 2685 | movingPixels->flip(flipType); |
| 2686 | else if (auto standby = dynamic_cast<StandbyState*>(m_state.get())) |
| 2687 | standby->startFlipTransformation(this, flipType); |
| 2688 | } |
| 2689 | |
| 2690 | void Editor::updateTransformation(const Transformation& transform) |
| 2691 | { |
| 2692 | if (auto movingPixels = dynamic_cast<MovingPixelsState*>(m_state.get())) |
| 2693 | movingPixels->updateTransformation(transform); |
| 2694 | } |
| 2695 | |
| 2696 | void Editor::notifyScrollChanged() |
| 2697 | { |
| 2698 | m_observers.notifyScrollChanged(this); |
| 2699 | |
| 2700 | ASSERT(m_state); |
| 2701 | if (m_state) |
| 2702 | m_state->onScrollChange(this); |
| 2703 | |
| 2704 | // Update status bar and mouse cursor |
| 2705 | if (hasMouse()) { |
| 2706 | updateStatusBar(); |
| 2707 | setCursor(mousePosInDisplay()); |
| 2708 | } |
| 2709 | } |
| 2710 | |
| 2711 | void Editor::notifyZoomChanged() |
| 2712 | { |
| 2713 | m_observers.notifyZoomChanged(this); |
| 2714 | } |
| 2715 | |
| 2716 | bool Editor::checkForScroll(ui::MouseMessage* msg) |
| 2717 | { |
| 2718 | tools::Ink* clickedInk = getCurrentEditorInk(); |
| 2719 | |
| 2720 | // Start scroll loop |
| 2721 | if (msg->middle() || clickedInk->isScrollMovement()) { // TODO msg->middle() should be customizable |
| 2722 | startScrollingState(msg); |
| 2723 | return true; |
| 2724 | } |
| 2725 | else |
| 2726 | return false; |
| 2727 | } |
| 2728 | |
| 2729 | bool Editor::checkForZoom(ui::MouseMessage* msg) |
| 2730 | { |
| 2731 | tools::Ink* clickedInk = getCurrentEditorInk(); |
| 2732 | |
| 2733 | // Start scroll loop |
| 2734 | if (clickedInk->isZoom()) { |
| 2735 | startZoomingState(msg); |
| 2736 | return true; |
| 2737 | } |
| 2738 | else |
| 2739 | return false; |
| 2740 | } |
| 2741 | |
| 2742 | void Editor::startScrollingState(ui::MouseMessage* msg) |
| 2743 | { |
| 2744 | EditorStatePtr newState(new ScrollingState); |
| 2745 | setState(newState); |
| 2746 | newState->onMouseDown(this, msg); |
| 2747 | } |
| 2748 | |
| 2749 | void Editor::startZoomingState(ui::MouseMessage* msg) |
| 2750 | { |
| 2751 | EditorStatePtr newState(new ZoomingState); |
| 2752 | setState(newState); |
| 2753 | newState->onMouseDown(this, msg); |
| 2754 | } |
| 2755 | |
| 2756 | void Editor::play(const bool playOnce, |
| 2757 | const bool playAll) |
| 2758 | { |
| 2759 | ASSERT(m_state); |
| 2760 | if (!m_state) |
| 2761 | return; |
| 2762 | |
| 2763 | if (m_isPlaying) |
| 2764 | stop(); |
| 2765 | |
| 2766 | m_isPlaying = true; |
| 2767 | setState(EditorStatePtr(new PlayState(playOnce, playAll))); |
| 2768 | } |
| 2769 | |
| 2770 | void Editor::stop() |
| 2771 | { |
| 2772 | ASSERT(m_state); |
| 2773 | if (!m_state) |
| 2774 | return; |
| 2775 | |
| 2776 | if (m_isPlaying) { |
| 2777 | while (m_state && !dynamic_cast<PlayState*>(m_state.get())) |
| 2778 | backToPreviousState(); |
| 2779 | |
| 2780 | m_isPlaying = false; |
| 2781 | |
| 2782 | ASSERT(m_state && dynamic_cast<PlayState*>(m_state.get())); |
| 2783 | if (m_state) |
| 2784 | backToPreviousState(); |
| 2785 | } |
| 2786 | } |
| 2787 | |
| 2788 | bool Editor::isPlaying() const |
| 2789 | { |
| 2790 | return m_isPlaying; |
| 2791 | } |
| 2792 | |
| 2793 | void Editor::(Option<bool>& playOnce, |
| 2794 | Option<bool>& playAll, |
| 2795 | const bool withStopBehaviorOptions) |
| 2796 | { |
| 2797 | const double options[] = { 0.25, 0.5, 1.0, 1.5, 2.0, 3.0 }; |
| 2798 | Menu ; |
| 2799 | |
| 2800 | for (double option : options) { |
| 2801 | MenuItem* item = new MenuItem(fmt::format(Strings::preview_speed_x(), option)); |
| 2802 | item->Click.connect([this, option]{ setAnimationSpeedMultiplier(option); }); |
| 2803 | item->setSelected(m_aniSpeed == option); |
| 2804 | menu.addChild(item); |
| 2805 | } |
| 2806 | |
| 2807 | menu.addChild(new MenuSeparator); |
| 2808 | |
| 2809 | // Play once option |
| 2810 | { |
| 2811 | MenuItem* item = new MenuItem(Strings::preview_play_once()); |
| 2812 | item->Click.connect( |
| 2813 | [&playOnce]() { |
| 2814 | playOnce(!playOnce()); |
| 2815 | }); |
| 2816 | item->setSelected(playOnce()); |
| 2817 | menu.addChild(item); |
| 2818 | } |
| 2819 | |
| 2820 | // Play all option |
| 2821 | { |
| 2822 | MenuItem* item = new MenuItem(Strings::preview_play_all_no_tags()); |
| 2823 | item->Click.connect( |
| 2824 | [&playAll]() { |
| 2825 | playAll(!playAll()); |
| 2826 | }); |
| 2827 | item->setSelected(playAll()); |
| 2828 | menu.addChild(item); |
| 2829 | } |
| 2830 | |
| 2831 | if (withStopBehaviorOptions) { |
| 2832 | MenuItem* item = new MenuItem(Strings::preview_rewind_on_stop()); |
| 2833 | item->Click.connect( |
| 2834 | []() { |
| 2835 | // Switch the "rewind_on_stop" option |
| 2836 | Preferences::instance().general.rewindOnStop( |
| 2837 | !Preferences::instance().general.rewindOnStop()); |
| 2838 | }); |
| 2839 | item->setSelected(Preferences::instance().general.rewindOnStop()); |
| 2840 | menu.addChild(item); |
| 2841 | } |
| 2842 | |
| 2843 | menu.showPopup(mousePosInDisplay(), display()); |
| 2844 | |
| 2845 | if (isPlaying()) { |
| 2846 | // Re-play |
| 2847 | stop(); |
| 2848 | play(playOnce(), |
| 2849 | playAll()); |
| 2850 | } |
| 2851 | } |
| 2852 | |
| 2853 | double Editor::getAnimationSpeedMultiplier() const |
| 2854 | { |
| 2855 | return m_aniSpeed; |
| 2856 | } |
| 2857 | |
| 2858 | void Editor::setAnimationSpeedMultiplier(double speed) |
| 2859 | { |
| 2860 | m_aniSpeed = speed; |
| 2861 | } |
| 2862 | |
| 2863 | void Editor::showMouseCursor(CursorType cursorType, |
| 2864 | const Cursor* cursor) |
| 2865 | { |
| 2866 | m_brushPreview.hide(); |
| 2867 | ui::set_mouse_cursor(cursorType, cursor); |
| 2868 | } |
| 2869 | |
| 2870 | void Editor::showBrushPreview(const gfx::Point& screenPos) |
| 2871 | { |
| 2872 | m_brushPreview.show(screenPos); |
| 2873 | } |
| 2874 | |
| 2875 | gfx::Point Editor::(const Projection& proj) |
| 2876 | { |
| 2877 | View* view = View::getView(this); |
| 2878 | if (view) { |
| 2879 | Rect vp = view->viewportBounds(); |
| 2880 | gfx::Size canvas = canvasSize(); |
| 2881 | return gfx::Point( |
| 2882 | std::max<int>(vp.w/2, vp.w - proj.applyX(canvas.w)), |
| 2883 | std::max<int>(vp.h/2, vp.h - proj.applyY(canvas.h))); |
| 2884 | } |
| 2885 | else |
| 2886 | return gfx::Point(0, 0); |
| 2887 | } |
| 2888 | |
| 2889 | gfx::Size Editor::canvasSize() const |
| 2890 | { |
| 2891 | return m_tiledModeHelper.canvasSize(); |
| 2892 | } |
| 2893 | |
| 2894 | gfx::Point Editor::mainTilePosition() const |
| 2895 | { |
| 2896 | return m_tiledModeHelper.mainTilePosition(); |
| 2897 | } |
| 2898 | |
| 2899 | void Editor::expandRegionByTiledMode(gfx::Region& rgn, |
| 2900 | const bool withProj) const |
| 2901 | { |
| 2902 | m_tiledModeHelper.expandRegionByTiledMode(rgn, withProj ? &m_proj : nullptr); |
| 2903 | } |
| 2904 | |
| 2905 | void Editor::collapseRegionByTiledMode(gfx::Region& rgn) const |
| 2906 | { |
| 2907 | m_tiledModeHelper.collapseRegionByTiledMode(rgn); |
| 2908 | } |
| 2909 | |
| 2910 | bool Editor::isMovingPixels() const |
| 2911 | { |
| 2912 | return (dynamic_cast<MovingPixelsState*>(m_state.get()) != nullptr); |
| 2913 | } |
| 2914 | |
| 2915 | void Editor::dropMovingPixels() |
| 2916 | { |
| 2917 | ASSERT(isMovingPixels()); |
| 2918 | backToPreviousState(); |
| 2919 | } |
| 2920 | |
| 2921 | void Editor::invalidateCanvas() |
| 2922 | { |
| 2923 | if (!isVisible()) |
| 2924 | return; |
| 2925 | |
| 2926 | if (m_sprite) |
| 2927 | invalidateRect(editorToScreen(getVisibleSpriteBounds())); |
| 2928 | else |
| 2929 | invalidate(); |
| 2930 | } |
| 2931 | |
| 2932 | void Editor::invalidateIfActive() |
| 2933 | { |
| 2934 | |
| 2935 | if (isActive()) |
| 2936 | invalidate(); |
| 2937 | } |
| 2938 | |
| 2939 | void Editor::updateAutoCelGuides(ui::Message* msg) |
| 2940 | { |
| 2941 | Cel* oldShowGuidesThisCel = m_showGuidesThisCel; |
| 2942 | bool oldShowAutoCelGuides = m_showAutoCelGuides; |
| 2943 | |
| 2944 | m_showAutoCelGuides = ( |
| 2945 | msg && |
| 2946 | getCurrentEditorInk()->isCelMovement() && |
| 2947 | m_docPref.show.autoGuides() && |
| 2948 | m_customizationDelegate && |
| 2949 | int(m_customizationDelegate->getPressedKeyAction(KeyContext::MoveTool) & KeyAction::AutoSelectLayer)); |
| 2950 | |
| 2951 | // Check if the user is pressing the Ctrl or Cmd key on move |
| 2952 | // tool to show automatic guides. |
| 2953 | if (m_showAutoCelGuides && |
| 2954 | m_state->allowLayerEdges()) { |
| 2955 | auto mouseMsg = dynamic_cast<ui::MouseMessage*>(msg); |
| 2956 | |
| 2957 | ColorPicker picker; |
| 2958 | picker.pickColor(getSite(), |
| 2959 | screenToEditorF(mouseMsg ? mouseMsg->position(): |
| 2960 | mousePosInDisplay()), |
| 2961 | m_proj, ColorPicker::FromComposition); |
| 2962 | m_showGuidesThisCel = (picker.layer() ? picker.layer()->cel(m_frame): |
| 2963 | nullptr); |
| 2964 | } |
| 2965 | else { |
| 2966 | m_showGuidesThisCel = nullptr; |
| 2967 | } |
| 2968 | |
| 2969 | if (m_showGuidesThisCel != oldShowGuidesThisCel || |
| 2970 | m_showAutoCelGuides != oldShowAutoCelGuides) { |
| 2971 | invalidate(); |
| 2972 | updateStatusBar(); |
| 2973 | } |
| 2974 | } |
| 2975 | |
| 2976 | // static |
| 2977 | void Editor::registerCommands() |
| 2978 | { |
| 2979 | Commands::instance() |
| 2980 | ->add( |
| 2981 | new QuickCommand( |
| 2982 | CommandId::SwitchNonactiveLayersOpacity(), |
| 2983 | []{ |
| 2984 | static int oldValue = -1; |
| 2985 | auto& option = Preferences::instance().experimental.nonactiveLayersOpacity; |
| 2986 | if (oldValue == -1) { |
| 2987 | oldValue = option(); |
| 2988 | if (option() == 255) |
| 2989 | option(128); |
| 2990 | else |
| 2991 | option(255); |
| 2992 | } |
| 2993 | else { |
| 2994 | const int newValue = oldValue; |
| 2995 | oldValue = option(); |
| 2996 | option(newValue); |
| 2997 | } |
| 2998 | app_refresh_screen(); |
| 2999 | })); |
| 3000 | } |
| 3001 | |
| 3002 | } // namespace app |
| 3003 | |