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