1// Aseprite
2// Copyright (C) 2019-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/tool_loop_impl.h"
13
14#include "app/app.h"
15#include "app/cmd/add_slice.h"
16#include "app/cmd/set_last_point.h"
17#include "app/cmd/set_mask.h"
18#include "app/color.h"
19#include "app/color_utils.h"
20#include "app/console.h"
21#include "app/context.h"
22#include "app/context_access.h"
23#include "app/doc_undo.h"
24#include "app/i18n/strings.h"
25#include "app/modules/gui.h"
26#include "app/modules/palettes.h"
27#include "app/pref/preferences.h"
28#include "app/tools/controller.h"
29#include "app/tools/freehand_algorithm.h"
30#include "app/tools/ink.h"
31#include "app/tools/intertwine.h"
32#include "app/tools/point_shape.h"
33#include "app/tools/symmetry.h"
34#include "app/tools/tool.h"
35#include "app/tools/tool_box.h"
36#include "app/tools/tool_loop.h"
37#include "app/tx.h"
38#include "app/ui/color_bar.h"
39#include "app/ui/context_bar.h"
40#include "app/ui/editor/editor.h"
41#include "app/ui/editor/editor_observer.h"
42#include "app/ui/main_window.h"
43#include "app/ui/optional_alert.h"
44#include "app/ui/status_bar.h"
45#include "app/ui_context.h"
46#include "app/util/expand_cel_canvas.h"
47#include "app/util/layer_utils.h"
48#include "doc/cel.h"
49#include "doc/image.h"
50#include "doc/layer.h"
51#include "doc/mask.h"
52#include "doc/palette.h"
53#include "doc/palette_picks.h"
54#include "doc/remap.h"
55#include "doc/slice.h"
56#include "doc/sprite.h"
57#include "fmt/format.h"
58#include "render/dithering.h"
59#include "render/rasterize.h"
60#include "render/render.h"
61#include "ui/ui.h"
62
63#include <algorithm>
64#include <memory>
65
66namespace app {
67
68using namespace ui;
69
70#ifdef ENABLE_UI
71
72static void fill_toolloop_params_from_tool_preferences(ToolLoopParams& params)
73{
74 ToolPreferences& toolPref =
75 Preferences::instance().tool(params.tool);
76
77 params.inkType = toolPref.ink();
78 params.opacity = toolPref.opacity();
79 params.tolerance = toolPref.tolerance();
80 params.contiguous = toolPref.contiguous();
81 params.freehandAlgorithm = toolPref.freehandAlgorithm();
82}
83
84#endif // ENABLE_UI
85
86//////////////////////////////////////////////////////////////////////
87// Common properties between drawing/preview ToolLoop impl
88
89class ToolLoopBase : public tools::ToolLoop {
90protected:
91 Editor* m_editor;
92 tools::Tool* m_tool;
93 BrushRef m_brush;
94 BrushRef m_origBrush;
95 gfx::Point m_oldPatternOrigin;
96 Doc* m_document;
97 Sprite* m_sprite;
98 Layer* m_layer;
99 frame_t m_frame;
100 TilesetMode m_tilesetMode;
101 RgbMap* m_rgbMap;
102 DocumentPreferences& m_docPref;
103 ToolPreferences& m_toolPref;
104 int m_opacity;
105 int m_tolerance;
106 bool m_contiguous;
107 bool m_snapToGrid;
108 bool m_isSelectingTiles;
109 doc::Grid m_grid;
110 gfx::Rect m_gridBounds;
111 gfx::Point m_celOrigin;
112 gfx::Point m_mainTilePos;
113 gfx::Point m_speed;
114 tools::ToolLoop::Button m_button;
115 std::unique_ptr<tools::Ink> m_ink;
116 tools::Controller* m_controller;
117 tools::PointShape* m_pointShape;
118 tools::Intertwine* m_intertwine;
119 tools::TracePolicy m_tracePolicy;
120 std::unique_ptr<tools::Symmetry> m_symmetry;
121 Shade m_shade;
122 std::unique_ptr<doc::Remap> m_shadingRemap;
123 bool m_tilesMode;
124 app::ColorTarget m_colorTarget;
125 doc::color_t m_fgColor;
126 doc::color_t m_bgColor;
127 doc::color_t m_primaryColor;
128 doc::color_t m_secondaryColor;
129 tools::DynamicsOptions m_dynamics;
130
131 // Modifiers that can be used with scripts
132 tools::ToolLoopModifiers m_staticToolModifiers;
133
134 // Visible region (on the screen) of the all editors showing the
135 // given document.
136 gfx::Region m_allVisibleRgn;
137
138 app::TiledModeHelper m_tiledModeHelper;
139
140public:
141 ToolLoopBase(Editor* editor,
142 Site& site, const doc::Grid& grid,
143 ToolLoopParams& params)
144 : m_editor(editor)
145 , m_tool(params.tool)
146 , m_brush(params.brush)
147 , m_origBrush(params.brush)
148 , m_oldPatternOrigin(m_brush->patternOrigin())
149 , m_document(site.document())
150 , m_sprite(site.sprite())
151 , m_layer(site.layer())
152 , m_frame(site.frame())
153 , m_tilesetMode(site.tilesetMode())
154 , m_rgbMap(nullptr)
155 , m_docPref(Preferences::instance().document(m_document))
156 , m_toolPref(Preferences::instance().tool(m_tool))
157 , m_opacity(params.opacity)
158 , m_tolerance(params.tolerance)
159 , m_contiguous(params.contiguous)
160 , m_snapToGrid(m_docPref.grid.snap())
161 , m_isSelectingTiles(false)
162 , m_grid(grid)
163 , m_gridBounds(grid.origin(), grid.tileSize())
164#ifdef ENABLE_UI
165 , m_mainTilePos(editor ? -editor->mainTilePosition(): gfx::Point(0, 0))
166#endif
167 , m_button(params.button)
168 , m_ink(params.ink->clone())
169 , m_controller(params.controller)
170 , m_pointShape(m_tool->getPointShape(m_button))
171 , m_intertwine(m_tool->getIntertwine(m_button))
172 , m_tracePolicy(m_tool->getTracePolicy(m_button))
173 , m_symmetry(nullptr)
174 , m_tilesMode(site.tilemapMode() == TilemapMode::Tiles)
175 , m_colorTarget(m_tilesMode ? ColorTarget(ColorTarget::BackgroundLayer,
176 IMAGE_TILEMAP, 0):
177 m_layer ? ColorTarget(m_layer):
178 ColorTarget(ColorTarget::BackgroundLayer,
179 m_sprite->pixelFormat(),
180 m_sprite->transparentColor()))
181 , m_fgColor(color_utils::color_for_target_mask(params.fg, m_colorTarget))
182 , m_bgColor(color_utils::color_for_target_mask(params.bg, m_colorTarget))
183 , m_primaryColor(m_button == tools::ToolLoop::Left ? m_fgColor: m_bgColor)
184 , m_secondaryColor(m_button == tools::ToolLoop::Left ? m_bgColor: m_fgColor)
185 , m_staticToolModifiers(params.modifiers)
186 , m_tiledModeHelper(m_docPref.tiled.mode(), m_sprite)
187 {
188 ASSERT(m_tool);
189 ASSERT(m_ink);
190 ASSERT(m_controller);
191
192 if (m_tilesMode) {
193 // Use FloodFillPointShape or TilePointShape in tiles mode
194 if (!m_pointShape->isFloodFill()) {
195 m_pointShape = App::instance()->toolBox()->getPointShapeById(
196 tools::WellKnownPointShapes::Tile);
197 }
198
199 // In selection ink, we need the Pixels tilemap mode so
200 // ExpandCelCanvas uses the whole canvas for the selection
201 // preview.
202 //
203 // TODO in the future we could improve this, using 1) a special
204 // tilemap layer to preview the selection, or 2) using a
205 // path to show the selection (so there is no preview layer
206 // at all and nor ExpandCelCanvas)
207 if (m_ink->isSelection())
208 site.tilemapMode(TilemapMode::Pixels);
209 }
210
211#ifdef ENABLE_UI // TODO add dynamics support when UI is not enabled
212 if (m_controller->isFreehand() &&
213 !m_pointShape->isFloodFill() &&
214 App::instance()->contextBar()) {
215 m_dynamics = App::instance()->contextBar()->getDynamics();
216 }
217#endif
218
219 if (m_tracePolicy == tools::TracePolicy::Accumulate) {
220 tools::ToolBox* toolbox = App::instance()->toolBox();
221
222 switch (params.freehandAlgorithm) {
223 case tools::FreehandAlgorithm::DEFAULT:
224 m_intertwine = toolbox->getIntertwinerById(tools::WellKnownIntertwiners::AsLines);
225 break;
226 case tools::FreehandAlgorithm::PIXEL_PERFECT:
227 m_intertwine = toolbox->getIntertwinerById(tools::WellKnownIntertwiners::AsPixelPerfect);
228 break;
229 case tools::FreehandAlgorithm::DOTS:
230 m_intertwine = toolbox->getIntertwinerById(tools::WellKnownIntertwiners::None);
231 break;
232 }
233
234 // Use overlap trace policy for dynamic gradient
235 if (m_dynamics.isDynamic() &&
236 m_dynamics.gradient != tools::DynamicSensor::Static &&
237 m_controller->isFreehand() &&
238 !m_ink->isEraser()) {
239 // Use overlap trace policy to accumulate changes of colors
240 // between stroke points.
241 //
242 // TODO this is connected with a condition in tools::PaintInk::prepareInk()
243 m_tracePolicy = tools::TracePolicy::Overlap;
244 }
245 }
246
247 // Symmetry mode
248 if (Preferences::instance().symmetryMode.enabled()) {
249 if (m_docPref.symmetry.mode() != gen::SymmetryMode::NONE)
250 m_symmetry.reset(new tools::Symmetry(m_docPref.symmetry.mode(),
251 m_docPref.symmetry.xAxis(),
252 m_docPref.symmetry.yAxis()));
253 }
254
255 // Ignore opacity for these inks
256 if (!tools::inkHasOpacity(params.inkType) &&
257 m_brush->type() != kImageBrushType &&
258 !m_ink->isEffect()) {
259 m_opacity = 255;
260 }
261
262#ifdef ENABLE_UI // TODO add support when UI is not enabled
263 if (params.inkType == tools::InkType::SHADING) {
264 m_shade = App::instance()->contextBar()->getShade();
265 m_shadingRemap.reset(
266 App::instance()->contextBar()->createShadeRemap(
267 m_button == tools::ToolLoop::Left));
268 }
269#endif
270
271#ifdef ENABLE_UI
272 updateAllVisibleRegion();
273#endif
274 }
275
276 ~ToolLoopBase() {
277 m_origBrush->setPatternOrigin(m_oldPatternOrigin);
278 }
279
280 void forceSnapToTiles() {
281 m_snapToGrid = true;
282 m_isSelectingTiles = true;
283 }
284
285 // IToolLoop interface
286 tools::Tool* getTool() override { return m_tool; }
287 Brush* getBrush() override { return m_brush.get(); }
288 void setBrush(const BrushRef& newBrush) override { m_brush = newBrush; }
289 Doc* getDocument() override { return m_document; }
290 Sprite* sprite() override { return m_sprite; }
291 Layer* getLayer() override { return m_layer; }
292 const Cel* getCel() override { return nullptr; }
293 bool isTilemapMode() override { return m_tilesMode; };
294 bool isManualTilesetMode() const override { return m_tilesetMode == TilesetMode::Manual; }
295 frame_t getFrame() override { return m_frame; }
296 Palette* getPalette() override { return m_sprite->palette(m_frame); }
297 RgbMap* getRgbMap() override {
298 if (!m_rgbMap) {
299 Sprite::RgbMapFor forLayer =
300 (((m_layer && m_layer->isBackground()) ||
301 (m_sprite->pixelFormat() == IMAGE_RGB)) ?
302 Sprite::RgbMapFor::OpaqueLayer:
303 Sprite::RgbMapFor::TransparentLayer);
304 m_rgbMap = m_sprite->rgbMap(m_frame, forLayer);
305 }
306 return m_rgbMap;
307 }
308 ToolLoop::Button getMouseButton() override { return m_button; }
309 doc::color_t getFgColor() override { return m_fgColor; }
310 doc::color_t getBgColor() override { return m_bgColor; }
311 doc::color_t getPrimaryColor() override { return m_primaryColor; }
312 void setPrimaryColor(doc::color_t color) override { m_primaryColor = color; }
313 doc::color_t getSecondaryColor() override { return m_secondaryColor; }
314 void setSecondaryColor(doc::color_t color) override { m_secondaryColor = color; }
315 int getOpacity() override { return m_opacity; }
316 int getTolerance() override { return m_tolerance; }
317 bool getContiguous() override { return m_contiguous; }
318 tools::ToolLoopModifiers getModifiers() override {
319 return
320 (m_staticToolModifiers == tools::ToolLoopModifiers::kNone &&
321 m_editor ? m_editor->getToolLoopModifiers():
322 m_staticToolModifiers);
323 }
324 filters::TiledMode getTiledMode() override { return m_docPref.tiled.mode(); }
325 bool getGridVisible() override { return m_docPref.show.grid(); }
326 bool getSnapToGrid() override { return m_snapToGrid; }
327 bool isSelectingTiles() override { return m_isSelectingTiles; }
328 bool getStopAtGrid() override {
329 switch (m_toolPref.floodfill.stopAtGrid()) {
330 case app::gen::StopAtGrid::NEVER:
331 return false;
332 case app::gen::StopAtGrid::IF_VISIBLE:
333 return m_docPref.show.grid();
334 case app::gen::StopAtGrid::ALWAYS:
335 return true;
336 }
337 return false;
338 }
339
340 bool isPixelConnectivityEightConnected() override {
341 return (m_toolPref.floodfill.pixelConnectivity()
342 == app::gen::PixelConnectivity::EIGHT_CONNECTED);
343 }
344
345 const doc::Grid& getGrid() const override { return m_grid; }
346 gfx::Rect getGridBounds() override { return m_gridBounds; }
347 gfx::Point getCelOrigin() override { return m_celOrigin; }
348 bool needsCelCoordinates() override { return m_ink->needsCelCoordinates(); }
349 void setSpeed(const gfx::Point& speed) override { m_speed = speed; }
350 gfx::Point getSpeed() override { return m_speed; }
351 tools::Ink* getInk() override { return m_ink.get(); }
352 tools::Controller* getController() override { return m_controller; }
353 tools::PointShape* getPointShape() override { return m_pointShape; }
354 tools::Intertwine* getIntertwine() override { return m_intertwine; }
355 tools::TracePolicy getTracePolicy() override {
356 if (m_controller->handleTracePolicy())
357 return m_controller->getTracePolicy();
358 else
359 return m_tracePolicy;
360 }
361 tools::Symmetry* getSymmetry() override { return m_symmetry.get(); }
362 const Shade& getShade() override { return m_shade; }
363 doc::Remap* getShadingRemap() override { return m_shadingRemap.get(); }
364
365 void limitDirtyAreaToViewport(gfx::Region& rgn) override {
366#ifdef ENABLE_UI
367 rgn &= m_allVisibleRgn;
368#endif // ENABLE_UI
369 }
370
371 void updateDirtyArea(const gfx::Region& dirtyArea) override {
372 if (!m_editor)
373 return;
374
375#ifdef ENABLE_UI
376 // This is necessary here so the "on sprite crosshair" is hidden,
377 // we update screen pixels with the new sprite, and then we show
378 // the crosshair saving the updated pixels. It fixes problems with
379 // filled shape tools when we release the button, or paint-bucket
380 // when we press the button.
381 HideBrushPreview hide(m_editor->brushPreview());
382#endif
383
384 m_document->notifySpritePixelsModified(
385 m_sprite, dirtyArea, m_frame);
386 }
387
388 void updateStatusBar(const char* text) override {
389#ifdef ENABLE_UI
390 if (auto statusBar = StatusBar::instance())
391 statusBar->setStatusText(0, text);
392#endif
393 }
394
395 gfx::Point statusBarPositionOffset() override {
396 return m_mainTilePos;
397 }
398
399 render::DitheringMatrix getDitheringMatrix() override {
400#ifdef ENABLE_UI // TODO add support when UI is not enabled
401 return App::instance()->contextBar()->ditheringMatrix();
402#else
403 return render::DitheringMatrix();
404#endif
405 }
406
407 render::DitheringAlgorithmBase* getDitheringAlgorithm() override {
408#ifdef ENABLE_UI // TODO add support when UI is not enabled
409 return App::instance()->contextBar()->ditheringAlgorithm();
410#else
411 return nullptr;
412#endif
413 }
414
415 render::GradientType getGradientType() override {
416#ifdef ENABLE_UI // TODO add support when UI is not enabled
417 return App::instance()->contextBar()->gradientType();
418#else
419 return render::GradientType::Linear;
420#endif
421 }
422
423 tools::DynamicsOptions getDynamics() override {
424 return m_dynamics;
425 }
426
427 void onSliceRect(const gfx::Rect& bounds) override { }
428
429 const app::TiledModeHelper& getTiledModeHelper() override {
430 return m_tiledModeHelper;
431 }
432
433#ifdef ENABLE_UI
434protected:
435 void updateAllVisibleRegion() {
436 m_allVisibleRgn.clear();
437 // TODO use the context given to the ToolLoopImpl ctor
438 for (auto e : UIContext::instance()->getAllEditorsIncludingPreview(m_document)) {
439 gfx::Region viewportRegion;
440 e->getDrawableRegion(viewportRegion, Widget::kCutTopWindows);
441 for (auto rc : viewportRegion) {
442 gfx::Region subrgn(e->screenToEditor(rc));
443 e->collapseRegionByTiledMode(subrgn);
444 m_allVisibleRgn |= subrgn;
445 }
446 }
447 }
448#endif // ENABLE_UI
449
450};
451
452//////////////////////////////////////////////////////////////////////
453// For drawing
454
455class ToolLoopImpl : public ToolLoopBase,
456 public EditorObserver {
457 Context* m_context;
458 bool m_filled;
459 bool m_previewFilled;
460 int m_sprayWidth;
461 int m_spraySpeed;
462 bool m_useMask;
463 Mask* m_mask;
464 gfx::Point m_maskOrigin;
465 bool m_internalCancel = false;
466 Tx m_tx;
467 std::unique_ptr<ExpandCelCanvas> m_expandCelCanvas;
468 Image* m_floodfillSrcImage;
469 bool m_saveLastPoint;
470
471public:
472 ToolLoopImpl(Editor* editor,
473 Site& site,
474 const doc::Grid& grid,
475 Context* context,
476 ToolLoopParams& params,
477 const bool saveLastPoint)
478 : ToolLoopBase(editor, site, grid, params)
479 , m_context(context)
480 , m_tx(m_context,
481 m_tool->getText().c_str(),
482 ((m_ink->isSelection() ||
483 m_ink->isEyedropper() ||
484 m_ink->isScrollMovement() ||
485 m_ink->isSlice() ||
486 m_ink->isZoom()) ? DoesntModifyDocument:
487 ModifyDocument))
488 , m_floodfillSrcImage(nullptr)
489 , m_saveLastPoint(saveLastPoint)
490 {
491 if (m_pointShape->isFloodFill()) {
492 if (m_tilesMode) {
493 // This will be set later to getSrcImage()
494 m_floodfillSrcImage = nullptr;
495 }
496 // Prepare a special image for floodfill when it's configured to
497 // stop using all visible layers.
498 else if (m_toolPref.floodfill.referTo() == gen::FillReferTo::ALL_LAYERS) {
499 m_floodfillSrcImage = Image::create(m_sprite->pixelFormat(),
500 m_sprite->width(),
501 m_sprite->height());
502
503 m_floodfillSrcImage->clear(m_sprite->transparentColor());
504
505 render::Render render;
506 render.setNewBlend(Preferences::instance().experimental.newBlend());
507 render.renderSprite(
508 m_floodfillSrcImage,
509 m_sprite,
510 m_frame,
511 gfx::Clip(m_sprite->bounds()));
512 }
513 else if (Cel* cel = m_layer->cel(m_frame)) {
514 m_floodfillSrcImage = render::rasterize_with_sprite_bounds(cel);
515 }
516 }
517
518 m_expandCelCanvas.reset(new ExpandCelCanvas(
519 site, m_layer,
520 m_docPref.tiled.mode(),
521 m_tx,
522 ExpandCelCanvas::Flags(
523 ExpandCelCanvas::NeedsSource |
524 (m_layer->isTilemap() &&
525 (!m_tilesMode ||
526 m_ink->isSelection()) ? ExpandCelCanvas::PixelsBounds:
527 ExpandCelCanvas::None) |
528 (m_layer->isTilemap() &&
529 site.tilemapMode() == TilemapMode::Pixels &&
530 site.tilesetMode() == TilesetMode::Manual &&
531 !m_ink->isSelection() ? ExpandCelCanvas::TilesetPreview:
532 ExpandCelCanvas::None) |
533 (m_ink->isSelection() ? ExpandCelCanvas::SelectionPreview:
534 ExpandCelCanvas::None))));
535
536 if (!m_floodfillSrcImage)
537 m_floodfillSrcImage = const_cast<Image*>(getSrcImage());
538
539 // Settings
540 switch (m_tool->getFill(m_button)) {
541 case tools::FillNone:
542 m_filled = false;
543 break;
544 case tools::FillAlways:
545 m_filled = true;
546 break;
547 case tools::FillOptional:
548 m_filled = m_toolPref.filled();
549 break;
550 }
551
552 m_previewFilled = m_toolPref.filledPreview();
553 m_sprayWidth = m_toolPref.spray.width();
554 m_spraySpeed = m_toolPref.spray.speed();
555
556 if (m_ink->isSelection()) {
557 m_useMask = false;
558 }
559 else {
560 m_useMask = m_document->isMaskVisible();
561 }
562
563 // Start with an empty mask if the user is selecting with "default selection mode"
564 if (m_ink->isSelection() &&
565 (!m_document->isMaskVisible() ||
566 (int(getModifiers()) & int(tools::ToolLoopModifiers::kReplaceSelection)))) {
567 Mask emptyMask;
568 m_tx(new cmd::SetMask(m_document, &emptyMask));
569 }
570
571 // Setup the new grid of ExpandCelCanvas which can be displaced to
572 // match the new temporal cel position (m_celOrigin).
573 m_grid = m_expandCelCanvas->getGrid();
574 m_celOrigin = m_expandCelCanvas->getCelOrigin();
575
576 m_mask = m_document->mask();
577 m_maskOrigin = (!m_mask->isEmpty() ? gfx::Point(m_mask->bounds().x-m_celOrigin.x,
578 m_mask->bounds().y-m_celOrigin.y):
579 gfx::Point(0, 0));
580
581#ifdef ENABLE_UI
582 if (m_editor)
583 m_editor->add_observer(this);
584#endif
585 }
586
587 ~ToolLoopImpl() {
588#ifdef ENABLE_UI
589 if (m_editor)
590 m_editor->remove_observer(this);
591#endif
592
593 if (m_floodfillSrcImage != getSrcImage())
594 delete m_floodfillSrcImage;
595 }
596
597 // IToolLoop interface
598 bool needsCelCoordinates() override {
599 if (m_tilesMode) {
600 // When we are painting with tiles, we don't need to adjust the
601 // coordinates by the cel position in PointShape (points will be
602 // in tiles position relative to the tilemap origin already).
603 return false;
604 }
605 else
606 return ToolLoopBase::needsCelCoordinates();
607 }
608
609 void commit() override {
610 bool redraw = false;
611
612 if (!m_internalCancel) {
613 // Freehand changes the last point
614 if (m_saveLastPoint) {
615 m_tx(new cmd::SetLastPoint(
616 m_document,
617 getController()->getLastPoint().toPoint()));
618 }
619
620 // Paint ink
621 if (m_ink->isPaint()) {
622 try {
623 ContextReader reader(m_context, 500);
624 ContextWriter writer(reader);
625 m_expandCelCanvas->commit();
626 }
627 catch (const LockedDocException& ex) {
628 Console::showException(ex);
629 }
630 }
631 // Selection ink
632 else if (m_ink->isSelection()) {
633 redraw = true;
634
635 // Show selection edges
636 if (Preferences::instance().selection.autoShowSelectionEdges())
637 m_docPref.show.selectionEdges(true);
638 }
639 // Slice ink
640 else if (m_ink->isSlice()) {
641 redraw = true;
642 }
643
644 m_tx.commit();
645 }
646 else {
647 rollback();
648 }
649
650#ifdef ENABLE_UI
651 if (redraw)
652 update_screen_for_document(m_document);
653#else
654 (void)redraw; // To avoid warning about unused variable
655#endif
656 }
657
658 void rollback() override {
659 try {
660 ContextReader reader(m_context, 500);
661 ContextWriter writer(reader);
662 m_expandCelCanvas->rollback();
663 }
664 catch (const LockedDocException& ex) {
665 Console::showException(ex);
666 }
667#ifdef ENABLE_UI
668 update_screen_for_document(m_document);
669#endif
670 }
671
672 const Cel* getCel() override { return m_expandCelCanvas->getCel(); }
673 const Image* getSrcImage() override { return m_expandCelCanvas->getSourceCanvas(); }
674 const Image* getFloodFillSrcImage() override { return m_floodfillSrcImage; }
675 Image* getDstImage() override { return m_expandCelCanvas->getDestCanvas(); }
676 Tileset* getDstTileset() override { return m_expandCelCanvas->getDestTileset(); }
677 void validateSrcImage(const gfx::Region& rgn) override {
678 m_expandCelCanvas->validateSourceCanvas(rgn);
679 }
680 void validateDstImage(const gfx::Region& rgn) override {
681 m_expandCelCanvas->validateDestCanvas(rgn);
682 }
683 void validateDstTileset(const gfx::Region& rgn) override {
684 m_expandCelCanvas->validateDestTileset(
685 rgn, getIntertwine()->forceTilemapRegionToValidate());
686 }
687 void invalidateDstImage() override {
688 m_expandCelCanvas->invalidateDestCanvas();
689 }
690 void invalidateDstImage(const gfx::Region& rgn) override {
691 m_expandCelCanvas->invalidateDestCanvas(rgn);
692 }
693 void copyValidDstToSrcImage(const gfx::Region& rgn) override {
694 m_expandCelCanvas->copyValidDestToSourceCanvas(rgn);
695 }
696
697 bool useMask() override { return m_useMask; }
698 Mask* getMask() override { return m_mask; }
699 void setMask(Mask* newMask) override {
700 m_tx(new cmd::SetMask(m_document, newMask));
701 }
702 gfx::Point getMaskOrigin() override { return m_maskOrigin; }
703 bool getFilled() override { return m_filled; }
704 bool getPreviewFilled() override { return m_previewFilled; }
705 int getSprayWidth() override { return m_sprayWidth; }
706 int getSpraySpeed() override { return m_spraySpeed; }
707
708 void onSliceRect(const gfx::Rect& bounds) override {
709#ifdef ENABLE_UI // TODO add support for slice tool from batch scripts without UI?
710 if (m_editor && getMouseButton() == ToolLoop::Left) {
711 // Try to select slices, but if it returns false, it means that
712 // there are no slices in the box to be selected, so we show a
713 // popup menu to create a new one.
714 if (!m_editor->selectSliceBox(bounds) &&
715 (bounds.w > 1 || bounds.h > 1)) {
716 Slice* slice = new Slice;
717 slice->setName(getUniqueSliceName());
718
719 SliceKey key(bounds);
720 slice->insert(getFrame(), key);
721
722 auto color = Preferences::instance().slices.defaultColor();
723 slice->userData().setColor(
724 doc::rgba(color.getRed(),
725 color.getGreen(),
726 color.getBlue(),
727 color.getAlpha()));
728
729 m_tx(new cmd::AddSlice(m_sprite, slice));
730 return;
731 }
732 }
733#endif
734
735 // Cancel the operation (do not create a new transaction for this
736 // no-op, e.g. just change the set of selected slices).
737 m_internalCancel = true;
738 }
739
740private:
741
742#ifdef ENABLE_UI
743 // EditorObserver impl
744 void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }
745 void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); }
746
747 std::string getUniqueSliceName() const {
748 std::string prefix = "Slice";
749 int max = 0;
750
751 for (Slice* slice : m_sprite->slices())
752 if (std::strncmp(slice->name().c_str(), prefix.c_str(), prefix.size()) == 0)
753 max = std::max(max, (int)std::strtol(slice->name().c_str()+prefix.size(), nullptr, 10));
754
755 return fmt::format("{} {}", prefix, max+1);
756 }
757#endif // ENABLE_UI
758
759};
760
761//////////////////////////////////////////////////////////////////////
762// For user UI painting
763
764#ifdef ENABLE_UI
765
766// TODO add inks for tilemaps
767static void adjust_ink_for_tilemaps(const Site& site,
768 ToolLoopParams& params)
769{
770 if (!params.ink->isSelection() &&
771 !params.ink->isEraser()) {
772 params.ink = App::instance()->toolBox()->getInkById(tools::WellKnownInks::PaintCopy);
773 }
774}
775
776tools::ToolLoop* create_tool_loop(
777 Editor* editor,
778 Context* context,
779 const tools::Pointer::Button button,
780 const bool convertLineToFreehand,
781 const bool selectTiles)
782{
783 Site site = editor->getSite();
784 doc::Grid grid = site.grid();
785
786 ToolLoopParams params;
787 params.tool = editor->getCurrentEditorTool();
788 params.ink = editor->getCurrentEditorInk();
789
790 if (site.tilemapMode() == TilemapMode::Tiles) {
791 adjust_ink_for_tilemaps(site, params);
792 }
793
794 if (!params.tool || !params.ink)
795 return nullptr;
796
797 if (selectTiles) {
798 params.tool = App::instance()->toolBox()->getToolById(tools::WellKnownTools::RectangularMarquee);
799 params.ink = params.tool->getInk(button == tools::Pointer::Left ? 0: 1);
800 }
801
802 // For selection tools, we can use any layer (even without layers at
803 // all), so we specify a nullptr here as the active layer. This is
804 // used as a special case by the render::Render class to show the
805 // preview image/selection stroke as a xor'd overlay in the render
806 // result.
807 //
808 // Anyway this cannot be used in 'magic wand' tool (isSelection +
809 // isFloodFill) because we need the original layer source
810 // image/pixels to stop the flood-fill algorithm.
811 if (params.ink->isSelection() &&
812 !params.tool->getPointShape(
813 button != tools::Pointer::Left ? 1: 0)->isFloodFill()) {
814 // Don't call site.layer(nullptr) because we want to keep the
815 // site.layer() to know if we are in a tilemap layer
816 }
817 else {
818 Layer* layer = site.layer();
819 if (!layer) {
820 StatusBar::instance()->showTip(
821 1000, Strings::statusbar_tips_no_active_layers());
822 return nullptr;
823 }
824 else if (!layer->isVisibleHierarchy()) {
825 StatusBar::instance()->showTip(
826 1000,
827 fmt::format(Strings::statusbar_tips_layer_x_is_hidden(),
828 layer->name()));
829 return nullptr;
830 }
831 // If the active layer is read-only.
832 else if (layer_is_locked(editor)) {
833 return nullptr;
834 }
835 // If the active layer is reference.
836 else if (layer->isReference()) {
837 StatusBar::instance()->showTip(
838 1000,
839 fmt::format(Strings::statusbar_tips_unmodifiable_reference_layer(),
840 layer->name()));
841 return nullptr;
842 }
843 }
844
845 // Get fg/bg colors
846 ColorBar* colorbar = ColorBar::instance();
847 if (site.tilemapMode() == TilemapMode::Tiles) {
848 params.fg = app::Color::fromIndex(colorbar->getFgTile()); // TODO Color::fromTileIndex?
849 params.bg = app::Color::fromIndex(colorbar->getBgTile());
850 }
851 else {
852 params.fg = colorbar->getFgColor();
853 params.bg = colorbar->getBgColor();
854 if (!params.fg.isValid() ||
855 !params.bg.isValid()) {
856 if (Preferences::instance().colorBar.showInvalidFgBgColorAlert()) {
857 OptionalAlert::show(
858 Preferences::instance().colorBar.showInvalidFgBgColorAlert,
859 1, Strings::alerts_invalid_fg_or_bg_colors());
860 return nullptr;
861 }
862 }
863 }
864
865 // Create the new tool loop
866 try {
867 params.button =
868 (button == tools::Pointer::Left ? tools::ToolLoop::Left:
869 tools::ToolLoop::Right);
870
871 params.controller =
872 (convertLineToFreehand ?
873 App::instance()->toolBox()->getControllerById(
874 tools::WellKnownControllers::LineFreehand):
875 params.tool->getController(params.button));
876
877 const bool saveLastPoint =
878 (params.ink->isPaint() &&
879 (params.controller->isFreehand() ||
880 convertLineToFreehand));
881
882 params.brush = App::instance()->contextBar()
883 ->activeBrush(params.tool, params.ink);
884
885 fill_toolloop_params_from_tool_preferences(params);
886
887 ASSERT(context->activeDocument() == editor->document());
888 auto toolLoop = new ToolLoopImpl(
889 editor, site, grid, context, params, saveLastPoint);
890
891 if (selectTiles)
892 toolLoop->forceSnapToTiles();
893
894 return toolLoop;
895 }
896 catch (const std::exception& ex) {
897 Console::showException(ex);
898 return NULL;
899 }
900}
901
902#endif // ENABLE_UI
903
904//////////////////////////////////////////////////////////////////////
905// For scripting
906
907#ifdef ENABLE_SCRIPTING
908
909tools::ToolLoop* create_tool_loop_for_script(
910 Context* context,
911 const Site& site,
912 ToolLoopParams& params)
913{
914 ASSERT(params.tool);
915 ASSERT(params.ink);
916 if (!site.layer())
917 return nullptr;
918
919 try {
920 // If we don't have the UI available, we reset the tools
921 // preferences, so scripts that are executed in batch mode have a
922 // reproducible behavior.
923 if (!context->isUIAvailable())
924 Preferences::instance().resetToolPreferences(params.tool);
925
926 Site site2(site);
927 return new ToolLoopImpl(
928 nullptr, site2, site2.grid(),
929 context, params, false);
930 }
931 catch (const std::exception& ex) {
932 Console::showException(ex);
933 return nullptr;
934 }
935}
936
937#endif // ENABLE_SCRIPTING
938
939//////////////////////////////////////////////////////////////////////
940// For UI preview
941
942#ifdef ENABLE_UI
943
944class PreviewToolLoopImpl : public ToolLoopBase {
945 Image* m_image;
946
947public:
948 PreviewToolLoopImpl(
949 Editor* editor,
950 Site& site,
951 ToolLoopParams& params,
952 Image* image,
953 const gfx::Point& celOrigin)
954 : ToolLoopBase(editor, site, site.grid(), params)
955 , m_image(image)
956 {
957 m_celOrigin = celOrigin;
958
959 // Avoid preview for spray and flood fill like tools
960 if (m_pointShape->isSpray()) {
961 m_pointShape = App::instance()->toolBox()->getPointShapeById(
962 tools::WellKnownPointShapes::Brush);
963 }
964 else if (m_pointShape->isFloodFill()) {
965 m_pointShape = App::instance()->toolBox()->getPointShapeById
966 (m_tilesMode ? tools::WellKnownPointShapes::Tile:
967 tools::WellKnownPointShapes::Pixel);
968 }
969 }
970
971 // IToolLoop interface
972 void commit() override { }
973 void rollback() override { }
974 const Image* getSrcImage() override { return m_image; }
975 const Image* getFloodFillSrcImage() override { return m_image; }
976 Image* getDstImage() override { return m_image; }
977 Tileset* getDstTileset() override { return nullptr; }
978 void validateSrcImage(const gfx::Region& rgn) override { }
979 void validateDstImage(const gfx::Region& rgn) override { }
980 void validateDstTileset(const gfx::Region& rgn) override { }
981 void invalidateDstImage() override { }
982 void invalidateDstImage(const gfx::Region& rgn) override { }
983 void copyValidDstToSrcImage(const gfx::Region& rgn) override { }
984
985 bool isManualTilesetMode() const override {
986 // Return false because this is only the preview, so we avoid
987 // creating a new tileset
988 return false;
989 }
990
991 bool useMask() override { return false; }
992 Mask* getMask() override { return nullptr; }
993 void setMask(Mask* newMask) override { }
994 gfx::Point getMaskOrigin() override { return gfx::Point(0, 0); }
995 bool getFilled() override { return false; }
996 bool getPreviewFilled() override { return false; }
997 int getSprayWidth() override { return 0; }
998 int getSpraySpeed() override { return 0; }
999
1000 tools::DynamicsOptions getDynamics() override {
1001 // Preview without dynamics
1002 return tools::DynamicsOptions();
1003 }
1004
1005};
1006
1007tools::ToolLoop* create_tool_loop_preview(
1008 Editor* editor,
1009 const doc::BrushRef& brush,
1010 Image* image,
1011 const gfx::Point& celOrigin)
1012{
1013 Site site = editor->getSite();
1014
1015 ToolLoopParams params;
1016 params.tool = editor->getCurrentEditorTool();
1017 params.ink = editor->getCurrentEditorInk();
1018
1019 if (site.tilemapMode() == TilemapMode::Tiles &&
1020 image->pixelFormat() == IMAGE_TILEMAP) {
1021 adjust_ink_for_tilemaps(site, params);
1022 }
1023
1024 if (!params.tool || !params.ink)
1025 return nullptr;
1026
1027 Layer* layer = editor->layer();
1028 if (!layer ||
1029 !layer->isVisibleHierarchy() ||
1030 !layer->isEditableHierarchy() ||
1031 layer->isReference()) {
1032 return nullptr;
1033 }
1034
1035 // Get fg/bg colors
1036 ColorBar* colorbar = ColorBar::instance();
1037 if (site.tilemapMode() == TilemapMode::Tiles) {
1038 params.fg = app::Color::fromIndex(colorbar->getFgTile()); // TODO Color::fromTileIndex?
1039 params.bg = app::Color::fromIndex(colorbar->getBgTile());
1040 }
1041 else {
1042 params.fg = colorbar->getFgColor();
1043 params.bg = colorbar->getBgColor();
1044 if (!params.fg.isValid() ||
1045 !params.bg.isValid())
1046 return nullptr;
1047 }
1048
1049 params.brush = brush;
1050 params.button = tools::ToolLoop::Left;
1051 params.controller = params.tool->getController(params.button);
1052
1053 // Create the new tool loop
1054 try {
1055 fill_toolloop_params_from_tool_preferences(params);
1056
1057 return new PreviewToolLoopImpl(
1058 editor, site, params, image, celOrigin);
1059 }
1060 catch (const std::exception& e) {
1061 LOG(ERROR, e.what());
1062 return nullptr;
1063 }
1064}
1065
1066#endif // ENABLE_UI
1067
1068//////////////////////////////////////////////////////////////////////
1069
1070} // namespace app
1071