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
76namespace app {
77
78using namespace app::skin;
79using namespace gfx;
80using namespace ui;
81using namespace render;
82
83// TODO these should be grouped in some kind of "performance counters"
84static base::Chrono renderChrono;
85static double renderElapsed = 0.0;
86
87class EditorPostRenderImpl : public EditorPostRender {
88public:
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
131private:
132 Editor* m_editor;
133 Graphics* m_g;
134};
135
136// static
137std::unique_ptr<EditorRender> Editor::m_renderEngine = nullptr;
138
139Editor::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
217Editor::~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
236void Editor::destroyEditorSharedInternals()
237{
238 BrushPreview::destroyInternals();
239 if (m_renderEngine)
240 m_renderEngine.reset();
241}
242
243bool Editor::isActive() const
244{
245 return (current_editor == this);
246}
247
248bool 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
261WidgetType Editor::Type()
262{
263 static WidgetType type = kGenericWidget;
264 if (type == kGenericWidget)
265 type = register_widget_type();
266 return type;
267}
268
269void 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
323void Editor::setState(const EditorStatePtr& newState)
324{
325 setStateInternal(newState);
326}
327
328void Editor::backToPreviousState()
329{
330 setStateInternal(EditorStatePtr(NULL));
331}
332
333void 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
350void 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
402void 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
423void 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
457Site Editor::getSite() const
458{
459 Site site;
460 getSite(&site);
461 return site;
462}
463
464void 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
481void Editor::setDefaultScroll()
482{
483 if (Preferences::instance().editor.autoFit())
484 setScrollAndZoomToFitScreen();
485 else
486 setScrollToCenter();
487}
488
489void 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
501void 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
564void Editor::setEditorScroll(const gfx::Point& scroll)
565{
566 View::getView(this)->setViewScroll(scroll);
567}
568
569void Editor::setEditorZoom(const render::Zoom& zoom)
570{
571 setZoomAndCenterInMouse(
572 zoom, mousePosInDisplay(),
573 Editor::ZoomBehavior::CENTER);
574}
575
576void Editor::updateEditor(const bool restoreScrollPos)
577{
578 View::getView(this)->updateView(restoreScrollPos);
579}
580
581void 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 extraCel = 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
858void 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
882void 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
993void 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 */
1017void 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
1047void 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
1070void 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
1126void 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
1197void 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
1232void Editor::drawCelBounds(ui::Graphics* g, const Cel* cel, const gfx::Color color)
1233{
1234 g->drawRect(color, getCelScreenBounds(cel));
1235}
1236
1237void 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
1321void 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
1349void 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
1376gfx::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
1396void 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 extraCel(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
1421gfx::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
1471tools::Tool* Editor::getCurrentEditorTool() const
1472{
1473 return App::instance()->activeTool();
1474}
1475
1476tools::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
1485bool 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
1494gfx::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
1504gfx::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
1515gfx::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
1525Point 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
1535gfx::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
1545Rect Editor::screenToEditor(const Rect& rc)
1546{
1547 return gfx::Rect(
1548 screenToEditor(rc.origin()),
1549 screenToEditorCeiling(rc.point2()));
1550}
1551
1552Rect Editor::editorToScreen(const Rect& rc)
1553{
1554 return gfx::Rect(
1555 editorToScreen(rc.origin()),
1556 editorToScreen(rc.point2()));
1557}
1558
1559gfx::RectF Editor::editorToScreenF(const gfx::RectF& rc)
1560{
1561 return gfx::RectF(
1562 editorToScreenF(rc.origin()),
1563 editorToScreenF(rc.point2()));
1564}
1565
1566void Editor::add_observer(EditorObserver* observer)
1567{
1568 m_observers.add_observer(observer);
1569}
1570
1571void Editor::remove_observer(EditorObserver* observer)
1572{
1573 m_observers.remove_observer(observer);
1574}
1575
1576void Editor::setCustomizationDelegate(EditorCustomizationDelegate* delegate)
1577{
1578 if (m_customizationDelegate)
1579 m_customizationDelegate->dispose();
1580
1581 m_customizationDelegate = delegate;
1582}
1583
1584Rect 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.
1594Rect 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.
1610void 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
1625void 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
1634void 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
1660void 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
1672void 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
1781app::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
1797doc::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
1814bool 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
1830bool Editor::isSliceSelected(const doc::Slice* slice) const
1831{
1832 ASSERT(slice);
1833 return m_selectedSlices.contains(slice->id());
1834}
1835
1836void 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
1847void 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
1857bool 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
1873void 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
1883void Editor::cancelSelections()
1884{
1885 clearSlicesSelection();
1886}
1887
1888//////////////////////////////////////////////////////////////////////
1889// Message handler for the editor
1890
1891bool 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
2141void 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
2157void Editor::onResize(ui::ResizeEvent& ev)
2158{
2159 Widget::onResize(ev);
2160 m_padding = calcExtraPadding(m_proj);
2161}
2162
2163void 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
2240void 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
2247void Editor::onActiveToolChange(tools::Tool* tool)
2248{
2249 m_state->onActiveToolChange(this, tool);
2250 if (hasMouse()) {
2251 updateStatusBar();
2252 setCursor(mousePosInDisplay());
2253 }
2254}
2255
2256void Editor::onSamplingChange()
2257{
2258 if (m_proj.scaleX() < 1.0 &&
2259 m_proj.scaleY() < 1.0 &&
2260 isUsingNewRenderEngine()) {
2261 invalidate();
2262 }
2263}
2264
2265void Editor::onFgColorChange()
2266{
2267 m_brushPreview.redraw();
2268}
2269
2270void Editor::onContextBarBrushChange()
2271{
2272 m_brushPreview.redraw();
2273}
2274
2275void Editor::onTiledModeBeforeChange()
2276{
2277 m_oldMainTilePos = mainTilePosition();
2278}
2279
2280void 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
2304void Editor::onShowExtrasChange()
2305{
2306 invalidate();
2307}
2308
2309void 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
2316void Editor::onExposeSpritePixels(DocEvent& ev)
2317{
2318 if (m_state && ev.sprite() == m_sprite)
2319 m_state->onExposeSpritePixels(ev.region());
2320}
2321
2322void 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()
2329void 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
2340void Editor::onBeforeRemoveCel(DocEvent& ev)
2341{
2342 m_showGuidesThisCel = nullptr;
2343}
2344
2345void Editor::onAddTag(DocEvent& ev)
2346{
2347 m_tagFocusBand = -1;
2348}
2349
2350void Editor::onRemoveTag(DocEvent& ev)
2351{
2352 m_tagFocusBand = -1;
2353 if (m_state)
2354 m_state->onRemoveTag(this, ev.tag());
2355}
2356
2357void 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
2366void 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
2380bool 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
2389bool 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
2402bool 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
2418bool 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
2427EditorHit 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
2500void 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
2569void 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
2669void 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
2682void 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
2690void Editor::updateTransformation(const Transformation& transform)
2691{
2692 if (auto movingPixels = dynamic_cast<MovingPixelsState*>(m_state.get()))
2693 movingPixels->updateTransformation(transform);
2694}
2695
2696void 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
2711void Editor::notifyZoomChanged()
2712{
2713 m_observers.notifyZoomChanged(this);
2714}
2715
2716bool 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
2729bool 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
2742void Editor::startScrollingState(ui::MouseMessage* msg)
2743{
2744 EditorStatePtr newState(new ScrollingState);
2745 setState(newState);
2746 newState->onMouseDown(this, msg);
2747}
2748
2749void Editor::startZoomingState(ui::MouseMessage* msg)
2750{
2751 EditorStatePtr newState(new ZoomingState);
2752 setState(newState);
2753 newState->onMouseDown(this, msg);
2754}
2755
2756void 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
2770void 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
2788bool Editor::isPlaying() const
2789{
2790 return m_isPlaying;
2791}
2792
2793void Editor::showAnimationSpeedMultiplierPopup(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 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
2853double Editor::getAnimationSpeedMultiplier() const
2854{
2855 return m_aniSpeed;
2856}
2857
2858void Editor::setAnimationSpeedMultiplier(double speed)
2859{
2860 m_aniSpeed = speed;
2861}
2862
2863void Editor::showMouseCursor(CursorType cursorType,
2864 const Cursor* cursor)
2865{
2866 m_brushPreview.hide();
2867 ui::set_mouse_cursor(cursorType, cursor);
2868}
2869
2870void Editor::showBrushPreview(const gfx::Point& screenPos)
2871{
2872 m_brushPreview.show(screenPos);
2873}
2874
2875gfx::Point Editor::calcExtraPadding(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
2889gfx::Size Editor::canvasSize() const
2890{
2891 return m_tiledModeHelper.canvasSize();
2892}
2893
2894gfx::Point Editor::mainTilePosition() const
2895{
2896 return m_tiledModeHelper.mainTilePosition();
2897}
2898
2899void Editor::expandRegionByTiledMode(gfx::Region& rgn,
2900 const bool withProj) const
2901{
2902 m_tiledModeHelper.expandRegionByTiledMode(rgn, withProj ? &m_proj : nullptr);
2903}
2904
2905void Editor::collapseRegionByTiledMode(gfx::Region& rgn) const
2906{
2907 m_tiledModeHelper.collapseRegionByTiledMode(rgn);
2908}
2909
2910bool Editor::isMovingPixels() const
2911{
2912 return (dynamic_cast<MovingPixelsState*>(m_state.get()) != nullptr);
2913}
2914
2915void Editor::dropMovingPixels()
2916{
2917 ASSERT(isMovingPixels());
2918 backToPreviousState();
2919}
2920
2921void Editor::invalidateCanvas()
2922{
2923 if (!isVisible())
2924 return;
2925
2926 if (m_sprite)
2927 invalidateRect(editorToScreen(getVisibleSpriteBounds()));
2928 else
2929 invalidate();
2930}
2931
2932void Editor::invalidateIfActive()
2933{
2934
2935 if (isActive())
2936 invalidate();
2937}
2938
2939void 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
2977void 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