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