// Aseprite // Copyright (C) 2001-2017 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "app/ui/editor/editor.h" #include "app/app.h" #include "app/color.h" #include "app/color_picker.h" #include "app/color_utils.h" #include "app/commands/commands.h" #include "app/commands/params.h" #include "app/console.h" #include "app/ini_file.h" #include "app/modules/editors.h" #include "app/modules/gfx.h" #include "app/modules/gui.h" #include "app/modules/palettes.h" #include "app/pref/preferences.h" #include "app/tools/active_tool.h" #include "app/tools/ink.h" #include "app/tools/tool.h" #include "app/tools/tool_box.h" #include "app/ui/color_bar.h" #include "app/ui/context_bar.h" #include "app/ui/editor/drawing_state.h" #include "app/ui/editor/editor_customization_delegate.h" #include "app/ui/editor/editor_decorator.h" #include "app/ui/editor/glue.h" #include "app/ui/editor/moving_pixels_state.h" #include "app/ui/editor/pixels_movement.h" #include "app/ui/editor/play_state.h" #include "app/ui/editor/scrolling_state.h" #include "app/ui/editor/standby_state.h" #include "app/ui/editor/zooming_state.h" #include "app/ui/main_window.h" #include "app/ui/skin/skin_theme.h" #include "app/ui/status_bar.h" #include "app/ui/toolbar.h" #include "app/ui_context.h" #include "base/bind.h" #include "base/chrono.h" #include "base/convert_to.h" #include "base/unique_ptr.h" #include "doc/conversion_she.h" #include "doc/doc.h" #include "doc/document_event.h" #include "doc/mask_boundaries.h" #include "doc/slice.h" #include "she/surface.h" #include "she/system.h" #include "ui/ui.h" #include #include namespace app { using namespace app::skin; using namespace gfx; using namespace ui; using namespace render; // TODO these should be grouped in some kind of "performance counters" static base::Chrono renderChrono; static double renderElapsed = 0.0; class EditorPreRenderImpl : public EditorPreRender { public: EditorPreRenderImpl(Editor* editor, Image* image, const Point& offset, const Projection& proj) : m_editor(editor) , m_image(image) , m_offset(offset) , m_proj(proj) { } Editor* getEditor() override { return m_editor; } Image* getImage() override { return m_image; } void fillRect(const gfx::Rect& rect, uint32_t rgbaColor, int opacity) override { blend_rect( m_image, m_offset.x + m_proj.applyX(rect.x), m_offset.y + m_proj.applyY(rect.y), m_offset.x + m_proj.applyX(rect.x+rect.w) - 1, m_offset.y + m_proj.applyY(rect.y+rect.h) - 1, rgbaColor, opacity); } private: Editor* m_editor; Image* m_image; Point m_offset; Projection m_proj; }; class EditorPostRenderImpl : public EditorPostRender { public: EditorPostRenderImpl(Editor* editor, Graphics* g) : m_editor(editor) , m_g(g) { } Editor* getEditor() override { return m_editor; } void drawLine(int x1, int y1, int x2, int y2, gfx::Color screenColor) override { gfx::Point a(x1, y1); gfx::Point b(x2, y2); a = m_editor->editorToScreen(a); b = m_editor->editorToScreen(b); gfx::Rect bounds = m_editor->bounds(); a.x -= bounds.x; a.y -= bounds.y; b.x -= bounds.x; b.y -= bounds.y; m_g->drawLine(screenColor, a, b); } void drawRectXor(const gfx::Rect& rc) override { gfx::Rect rc2 = m_editor->editorToScreen(rc); gfx::Rect bounds = m_editor->bounds(); rc2.x -= bounds.x; rc2.y -= bounds.y; m_g->setDrawMode(Graphics::DrawMode::Xor); m_g->drawRect(gfx::rgba(255, 255, 255), rc2); m_g->setDrawMode(Graphics::DrawMode::Solid); } private: Editor* m_editor; Graphics* m_g; }; // static doc::ImageBufferPtr Editor::m_renderBuffer; // static AppRender Editor::m_renderEngine; Editor::Editor(Document* document, EditorFlags flags) : Widget(editor_type()) , m_state(new StandbyState()) , m_decorator(NULL) , m_document(document) , m_sprite(m_document->sprite()) , m_layer(m_sprite->root()->firstLayer()) , m_frame(frame_t(0)) , m_docPref(Preferences::instance().document(document)) , m_brushPreview(this) , m_lastDrawingPosition(-1, -1) , m_toolLoopModifiers(tools::ToolLoopModifiers::kNone) , m_padding(0, 0) , m_antsTimer(100, this) , m_antsOffset(0) , m_customizationDelegate(NULL) , m_docView(NULL) , m_flags(flags) , m_secondaryButton(false) , m_aniSpeed(1.0) , m_isPlaying(false) , m_showGuidesThisCel(nullptr) , m_tagFocusBand(-1) { m_proj.setPixelRatio(m_sprite->pixelRatio()); // Add the first state into the history. m_statesHistory.push(m_state); this->setFocusStop(true); App::instance()->activeToolManager()->add_observer(this); m_fgColorChangeConn = Preferences::instance().colorBar.fgColor.AfterChange.connect( base::Bind(&Editor::onFgColorChange, this)); m_contextBarBrushChangeConn = App::instance()->contextBar()->BrushChange.connect( base::Bind(&Editor::onContextBarBrushChange, this)); // Restore last site in preferences { frame_t preferredFrame = m_docPref.site.frame(); if (preferredFrame >= 0 && preferredFrame <= m_sprite->lastFrame()) setFrame(preferredFrame); LayerList layers = m_sprite->allBrowsableLayers(); layer_t layerIndex = m_docPref.site.layer(); if (layerIndex >= 0 && layerIndex < int(layers.size())) setLayer(layers[layerIndex]); } m_tiledConn = m_docPref.tiled.AfterChange.connect(base::Bind(&Editor::invalidate, this)); m_gridConn = m_docPref.grid.AfterChange.connect(base::Bind(&Editor::invalidate, this)); m_pixelGridConn = m_docPref.pixelGrid.AfterChange.connect(base::Bind(&Editor::invalidate, this)); m_bgConn = m_docPref.bg.AfterChange.connect(base::Bind(&Editor::invalidate, this)); m_onionskinConn = m_docPref.onionskin.AfterChange.connect(base::Bind(&Editor::invalidate, this)); m_symmetryModeConn = Preferences::instance().symmetryMode.enabled.AfterChange.connect(base::Bind(&Editor::invalidateIfActive, this)); m_showExtrasConn = m_docPref.show.AfterChange.connect( base::Bind(&Editor::onShowExtrasChange, this)); m_document->add_observer(this); m_state->onEnterState(this); } Editor::~Editor() { if (m_document && m_sprite) { LayerList layers = m_sprite->allBrowsableLayers(); layer_t layerIndex = doc::find_layer_index(layers, layer()); m_docPref.site.frame(frame()); m_docPref.site.layer(layerIndex); } m_observers.notifyDestroyEditor(this); m_document->remove_observer(this); App::instance()->activeToolManager()->remove_observer(this); setCustomizationDelegate(NULL); m_antsTimer.stop(); } void Editor::destroyEditorSharedInternals() { m_renderBuffer.reset(); } bool Editor::isActive() const { return (current_editor == this); } WidgetType editor_type() { static WidgetType type = kGenericWidget; if (type == kGenericWidget) type = register_widget_type(); return type; } void Editor::setStateInternal(const EditorStatePtr& newState) { m_brushPreview.hide(); // Fire before change state event, set the state, and fire after // change state event. EditorState::LeaveAction leaveAction = m_state->onLeaveState(this, newState.get()); // Push a new state if (newState) { if (leaveAction == EditorState::DiscardState) m_statesHistory.pop(); m_statesHistory.push(newState); m_state = newState; } // Go to previous state else { m_state->onBeforePopState(this); // Save the current state into "m_deletedStates" just to keep a // reference to it to avoid delete it right now. We'll delete it // in the next Editor::onProcessMessage(). // // This is necessary for PlayState because it removes itself // calling Editor::stop() from PlayState::onPlaybackTick(). If we // delete the PlayState inside the "Tick" timer signal, the // program will crash (because we're iterating the // PlayState::m_playTimer slots). m_deletedStates.push(m_state); m_statesHistory.pop(); m_state = m_statesHistory.top(); } ASSERT(m_state); // Change to the new state. m_state->onEnterState(this); // Notify observers m_observers.notifyStateChanged(this); // Redraw layer edges if (m_docPref.show.layerEdges()) invalidate(); // Setup the new mouse cursor setCursor(ui::get_mouse_position()); updateStatusBar(); } void Editor::setState(const EditorStatePtr& newState) { setStateInternal(newState); } void Editor::backToPreviousState() { setStateInternal(EditorStatePtr(NULL)); } void Editor::getInvalidDecoratoredRegion(gfx::Region& region) { // Remove decorated region that cannot be just moved because it // must be redrawn in another position when the Editor's scroll // changes (e.g. symmetry handles). if ((m_flags & kShowDecorators) && m_decorator) m_decorator->getInvalidDecoratoredRegion(this, region); } void Editor::setLayer(const Layer* layer) { bool changed = (m_layer != layer); m_observers.notifyBeforeLayerChanged(this); m_layer = const_cast(layer); m_observers.notifyAfterLayerChanged(this); if (m_document && changed) { if (// If the onion skinning depends on the active layer m_docPref.onionskin.currentLayer() || // If the user want to see the active layer edges... m_docPref.show.layerEdges() || // If there is a different opacity for nonactive-layers Preferences::instance().experimental.nonactiveLayersOpacity() < 255 || // If the automatic cel guides are visible... m_showGuidesThisCel) { // We've to redraw the whole editor invalidate(); } } // The active layer has changed. if (isActive()) UIContext::instance()->notifyActiveSiteChanged(); updateStatusBar(); } void Editor::setFrame(frame_t frame) { if (m_frame == frame) return; m_observers.notifyBeforeFrameChanged(this); { HideBrushPreview hide(m_brushPreview); m_frame = frame; } m_observers.notifyAfterFrameChanged(this); // The active frame has changed. if (isActive()) UIContext::instance()->notifyActiveSiteChanged(); invalidate(); updateStatusBar(); } void Editor::getSite(Site* site) const { site->document(m_document); site->sprite(m_sprite); site->layer(m_layer); site->frame(m_frame); } Site Editor::getSite() const { Site site; getSite(&site); return site; } void Editor::setZoom(const render::Zoom& zoom) { if (m_proj.zoom() != zoom) { m_proj.setZoom(zoom); notifyZoomChanged(); } else { // Just copy the zoom as the internal "Zoom::m_internalScale" // value might be different and we want to keep this value updated // for better zooming experience in StateWithWheelBehavior. m_proj.setZoom(zoom); } } void Editor::setDefaultScroll() { View* view = View::getView(this); Rect vp = view->viewportBounds(); setEditorScroll( gfx::Point( m_padding.x - vp.w/2 + m_proj.applyX(m_sprite->width())/2, m_padding.y - vp.h/2 + m_proj.applyY(m_sprite->height())/2)); } void Editor::setScrollAndZoomToFitScreen() { View* view = View::getView(this); Rect vp = view->viewportBounds(); Zoom zoom = m_proj.zoom(); if (float(vp.w) / float(m_sprite->width()) < float(vp.h) / float(m_sprite->height())) { if (vp.w < m_proj.applyX(m_sprite->width())) { while (vp.w < m_proj.applyX(m_sprite->width())) { if (!zoom.out()) break; m_proj.setZoom(zoom); } } else if (vp.w > m_proj.applyX(m_sprite->width())) { bool out = true; while (vp.w > m_proj.applyX(m_sprite->width())) { if (!zoom.in()) { out = false; break; } m_proj.setZoom(zoom); } if (out) { zoom.out(); m_proj.setZoom(zoom); } } } else { if (vp.h < m_proj.applyY(m_sprite->height())) { while (vp.h < m_proj.applyY(m_sprite->height())) { if (!zoom.out()) break; m_proj.setZoom(zoom); } } else if (vp.h > m_proj.applyY(m_sprite->height())) { bool out = true; while (vp.h > m_proj.applyY(m_sprite->height())) { if (!zoom.in()) { out = false; break; } m_proj.setZoom(zoom); } if (out) { zoom.out(); m_proj.setZoom(zoom); } } } updateEditor(); setEditorScroll( gfx::Point( m_padding.x - vp.w/2 + m_proj.applyX(m_sprite->width())/2, m_padding.y - vp.h/2 + m_proj.applyY(m_sprite->height())/2)); } // Sets the scroll position of the editor void Editor::setEditorScroll(const gfx::Point& scroll) { View::getView(this)->setViewScroll(scroll); } void Editor::setEditorZoom(const render::Zoom& zoom) { setZoomAndCenterInMouse( zoom, ui::get_mouse_position(), Editor::ZoomBehavior::CENTER); } void Editor::updateEditor() { View::getView(this)->updateView(); } void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& spriteRectToDraw, int dx, int dy) { // Clip from sprite and apply zoom gfx::Rect rc = m_sprite->bounds().createIntersection(spriteRectToDraw); rc = m_proj.apply(rc); int dest_x = dx + m_padding.x + rc.x; int dest_y = dy + m_padding.y + rc.y; // Clip from graphics/screen const gfx::Rect& clip = g->getClipBounds(); if (dest_x < clip.x) { rc.x += clip.x - dest_x; rc.w -= clip.x - dest_x; dest_x = clip.x; } if (dest_y < clip.y) { rc.y += clip.y - dest_y; rc.h -= clip.y - dest_y; dest_y = clip.y; } if (dest_x+rc.w > clip.x+clip.w) { rc.w = clip.x+clip.w-dest_x; } if (dest_y+rc.h > clip.y+clip.h) { rc.h = clip.y+clip.h-dest_y; } if (rc.isEmpty()) return; // Generate the rendered image if (!m_renderBuffer) m_renderBuffer.reset(new doc::ImageBuffer()); base::UniquePtr rendered(NULL); try { // Generate a "expose sprite pixels" notification. This is used by // tool managers that need to validate this region (copy pixels from // the original cel) before it can be used by the RenderEngine. { gfx::Rect expose = m_proj.remove(rc); // If the zoom level is less than 100%, we add extra pixels to // the exposed area. Those pixels could be shown in the // rendering process depending on each cel position. // E.g. when we are drawing in a cel with position < (0,0) if (m_proj.scaleX() < 1.0) expose.enlargeXW(int(1./m_proj.scaleX())); // If the zoom level is more than %100 we add an extra pixel to // expose just in case the zoom requires to display it. Note: // this is really necessary to avoid showing invalid destination // areas in ToolLoopImpl. else if (m_proj.scaleX() > 1.0) expose.enlargeXW(1); if (m_proj.scaleY() < 1.0) expose.enlargeYH(int(1./m_proj.scaleY())); else if (m_proj.scaleY() > 1.0) expose.enlargeYH(1); m_document->notifyExposeSpritePixels(m_sprite, gfx::Region(expose)); } // Create a temporary RGB bitmap to draw all to it rendered.reset(Image::create(IMAGE_RGB, rc.w, rc.h, m_renderBuffer)); m_renderEngine.setRefLayersVisiblity(true); m_renderEngine.setSelectedLayer(m_layer); if (m_flags & Editor::kUseNonactiveLayersOpacityWhenEnabled) m_renderEngine.setNonactiveLayersOpacity(Preferences::instance().experimental.nonactiveLayersOpacity()); else m_renderEngine.setNonactiveLayersOpacity(255); m_renderEngine.setProjection(m_proj); m_renderEngine.setupBackground(m_document, rendered->pixelFormat()); m_renderEngine.disableOnionskin(); if ((m_flags & kShowOnionskin) == kShowOnionskin) { if (m_docPref.onionskin.active()) { OnionskinOptions opts( (m_docPref.onionskin.type() == app::gen::OnionskinType::MERGE ? render::OnionskinType::MERGE: (m_docPref.onionskin.type() == app::gen::OnionskinType::RED_BLUE_TINT ? render::OnionskinType::RED_BLUE_TINT: render::OnionskinType::NONE))); opts.position(m_docPref.onionskin.position()); opts.prevFrames(m_docPref.onionskin.prevFrames()); opts.nextFrames(m_docPref.onionskin.nextFrames()); opts.opacityBase(m_docPref.onionskin.opacityBase()); opts.opacityStep(m_docPref.onionskin.opacityStep()); opts.layer(m_docPref.onionskin.currentLayer() ? m_layer: nullptr); FrameTag* tag = nullptr; if (m_docPref.onionskin.loopTag()) tag = m_sprite->frameTags().innerTag(m_frame); opts.loopTag(tag); m_renderEngine.setOnionskin(opts); } } ExtraCelRef extraCel = m_document->extraCel(); if (extraCel && extraCel->type() != render::ExtraType::NONE) { m_renderEngine.setExtraImage( extraCel->type(), extraCel->cel(), extraCel->image(), extraCel->blendMode(), m_layer, m_frame); } m_renderEngine.renderSprite( rendered, m_sprite, m_frame, gfx::Clip(0, 0, rc)); m_renderEngine.removeExtraImage(); } catch (const std::exception& e) { Console::showException(e); } if (rendered) { // Pre-render decorator. if ((m_flags & kShowDecorators) && m_decorator) { EditorPreRenderImpl preRender(this, rendered, Point(-rc.x, -rc.y), m_proj); m_decorator->preRenderDecorator(&preRender); } // Convert the render to a she::Surface static she::Surface* tmp; if (!tmp || tmp->width() < rc.w || tmp->height() < rc.h) { if (tmp) tmp->dispose(); tmp = she::instance()->createRgbaSurface(rc.w, rc.h); } if (tmp->nativeHandle()) { convert_image_to_surface(rendered, m_sprite->palette(m_frame), tmp, 0, 0, 0, 0, rc.w, rc.h); g->blit(tmp, 0, 0, dest_x, dest_y, rc.w, rc.h); m_brushPreview.invalidateRegion( gfx::Region( gfx::Rect(dest_x, dest_y, rc.w, rc.h))); } } } void Editor::drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& _rc) { gfx::Rect rc = _rc; // For odd zoom scales minor than 100% we have to add an extra window // just to make sure the whole rectangle is drawn. if (m_proj.scaleX() < 1.0) rc.w += int(1./m_proj.scaleX()); if (m_proj.scaleY() < 1.0) rc.h += int(1./m_proj.scaleY()); gfx::Rect client = clientBounds(); gfx::Rect spriteRect( client.x + m_padding.x, client.y + m_padding.y, m_proj.applyX(m_sprite->width()), m_proj.applyY(m_sprite->height())); gfx::Rect enclosingRect = spriteRect; // Draw the main sprite at the center. drawOneSpriteUnclippedRect(g, rc, 0, 0); gfx::Region outside(client); outside.createSubtraction(outside, gfx::Region(spriteRect)); // Document preferences if (int(m_docPref.tiled.mode()) & int(filters::TiledMode::X_AXIS)) { drawOneSpriteUnclippedRect(g, rc, -spriteRect.w, 0); drawOneSpriteUnclippedRect(g, rc, +spriteRect.w, 0); enclosingRect = gfx::Rect(spriteRect.x-spriteRect.w, spriteRect.y, spriteRect.w*3, spriteRect.h); outside.createSubtraction(outside, gfx::Region(enclosingRect)); } if (int(m_docPref.tiled.mode()) & int(filters::TiledMode::Y_AXIS)) { drawOneSpriteUnclippedRect(g, rc, 0, -spriteRect.h); drawOneSpriteUnclippedRect(g, rc, 0, +spriteRect.h); enclosingRect = gfx::Rect(spriteRect.x, spriteRect.y-spriteRect.h, spriteRect.w, spriteRect.h*3); outside.createSubtraction(outside, gfx::Region(enclosingRect)); } if (m_docPref.tiled.mode() == filters::TiledMode::BOTH) { drawOneSpriteUnclippedRect(g, rc, -spriteRect.w, -spriteRect.h); drawOneSpriteUnclippedRect(g, rc, +spriteRect.w, -spriteRect.h); drawOneSpriteUnclippedRect(g, rc, -spriteRect.w, +spriteRect.h); drawOneSpriteUnclippedRect(g, rc, +spriteRect.w, +spriteRect.h); enclosingRect = gfx::Rect( spriteRect.x-spriteRect.w, spriteRect.y-spriteRect.h, spriteRect.w*3, spriteRect.h*3); outside.createSubtraction(outside, gfx::Region(enclosingRect)); } // Fill the outside (parts of the editor that aren't covered by the // sprite). SkinTheme* theme = static_cast(this->theme()); if (m_flags & kShowOutside) { g->fillRegion(theme->colors.editorFace(), outside); } // Grids & slices { // Clipping gfx::Rect cliprc = editorToScreen(rc).offset(-bounds().origin()); cliprc = cliprc.createIntersection(spriteRect); if (!cliprc.isEmpty()) { IntersectClip clip(g, cliprc); // Draw the pixel grid if ((m_proj.zoom().scale() > 2.0) && m_docPref.show.pixelGrid()) { int alpha = m_docPref.pixelGrid.opacity(); if (m_docPref.pixelGrid.autoOpacity()) { alpha = int(alpha * (m_proj.zoom().scale()-2.) / (16.-2.)); alpha = MID(0, alpha, 255); } drawGrid(g, enclosingRect, Rect(0, 0, 1, 1), m_docPref.pixelGrid.color(), alpha); } // Draw the grid if (m_docPref.show.grid()) { gfx::Rect gridrc = m_docPref.grid.bounds(); if (m_proj.applyX(gridrc.w) > 2 && m_proj.applyY(gridrc.h) > 2) { int alpha = m_docPref.grid.opacity(); if (m_docPref.grid.autoOpacity()) { double len = (m_proj.applyX(gridrc.w) + m_proj.applyY(gridrc.h)) / 2.; alpha = int(alpha * len / 32.); alpha = MID(0, alpha, 255); } if (alpha > 8) drawGrid(g, enclosingRect, m_docPref.grid.bounds(), m_docPref.grid.color(), alpha); } } // Draw slices if (m_docPref.show.slices()) drawSlices(g); } } // Symmetry mode if (isActive() && (m_flags & Editor::kShowSymmetryLine) && Preferences::instance().symmetryMode.enabled()) { int mode = int(m_docPref.symmetry.mode()); if (mode & int(app::gen::SymmetryMode::HORIZONTAL)) { double x = m_docPref.symmetry.xAxis(); if (x > 0) { gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color()); g->drawVLine(color, spriteRect.x + m_proj.applyX(x), enclosingRect.y, enclosingRect.h); } } if (mode & int(app::gen::SymmetryMode::VERTICAL)) { double y = m_docPref.symmetry.yAxis(); if (y > 0) { gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color()); g->drawHLine(color, enclosingRect.x, spriteRect.y + m_proj.applyY(y), enclosingRect.w); } } } if (m_flags & kShowOutside) { // Draw the borders that enclose the sprite. enclosingRect.enlarge(1); g->drawRect(theme->colors.editorSpriteBorder(), enclosingRect); g->drawHLine( theme->colors.editorSpriteBottomBorder(), enclosingRect.x, enclosingRect.y+enclosingRect.h, enclosingRect.w); } // Draw active layer/cel edges bool showGuidesThisCel = this->showAutoCelGuides(); if ((m_docPref.show.layerEdges() || showGuidesThisCel) && // Show layer edges only on "standby" like states where brush // preview is shown (e.g. with this we avoid to showing the // edges in states like DrawingState, etc.). m_state->requireBrushPreview()) { Cel* cel = (m_layer ? m_layer->cel(m_frame): nullptr); if (cel) { drawCelBounds( g, cel, color_utils::color_for_ui(Preferences::instance().guides.layerEdgesColor())); if (showGuidesThisCel && m_showGuidesThisCel != cel) drawCelGuides(g, cel, m_showGuidesThisCel); } } // Draw the mask if (m_document->getMaskBoundaries()) drawMask(g); // Post-render decorator. if ((m_flags & kShowDecorators) && m_decorator) { EditorPostRenderImpl postRender(this, g); m_decorator->postRenderDecorator(&postRender); } } void Editor::drawSpriteClipped(const gfx::Region& updateRegion) { Region screenRegion; getDrawableRegion(screenRegion, kCutTopWindows); ScreenGraphics screenGraphics; GraphicsPtr editorGraphics = getGraphics(clientBounds()); for (const Rect& updateRect : updateRegion) { for (const Rect& screenRect : screenRegion) { IntersectClip clip(&screenGraphics, screenRect); if (clip) drawSpriteUnclippedRect(editorGraphics.get(), updateRect); } } } /** * Draws the boundaries, really this routine doesn't use the "mask" * field of the sprite, only the "bound" field (so you can have other * mask in the sprite and could be showed other boundaries), to * regenerate boundaries, use the sprite_generate_mask_boundaries() * routine. */ void Editor::drawMask(Graphics* g) { if ((m_flags & kShowMask) == 0 || !m_docPref.show.selectionEdges()) return; ASSERT(m_document->getMaskBoundaries()); int x = m_padding.x; int y = m_padding.y; for (const auto& seg : *m_document->getMaskBoundaries()) { CheckedDrawMode checked(g, m_antsOffset, gfx::rgba(0, 0, 0, 255), gfx::rgba(255, 255, 255, 255)); gfx::Rect bounds = m_proj.apply(seg.bounds()); if (m_proj.scaleX() >= 1.0) { if (!seg.open() && seg.vertical()) --bounds.x; } if (m_proj.scaleY() >= 1.0) { if (!seg.open() && !seg.vertical()) --bounds.y; } // The color doesn't matter, we are using CheckedDrawMode if (seg.vertical()) g->drawVLine(gfx::rgba(0, 0, 0), x+bounds.x, y+bounds.y, bounds.h); else g->drawHLine(gfx::rgba(0, 0, 0), x+bounds.x, y+bounds.y, bounds.w); } } void Editor::drawMaskSafe() { if ((m_flags & kShowMask) == 0) return; if (isVisible() && m_document && m_document->getMaskBoundaries()) { Region region; getDrawableRegion(region, kCutTopWindows); region.offset(-bounds().origin()); HideBrushPreview hide(m_brushPreview); GraphicsPtr g = getGraphics(clientBounds()); for (const gfx::Rect& rc : region) { IntersectClip clip(g.get(), rc); if (clip) drawMask(g.get()); } } } void Editor::drawGrid(Graphics* g, const gfx::Rect& spriteBounds, const Rect& gridBounds, const app::Color& color, int alpha) { if ((m_flags & kShowGrid) == 0) return; // Copy the grid bounds Rect grid(gridBounds); if (grid.w < 1 || grid.h < 1) return; // Move the grid bounds to a non-negative position. if (grid.x < 0) grid.x += (ABS(grid.x)/grid.w+1) * grid.w; if (grid.y < 0) grid.y += (ABS(grid.y)/grid.h+1) * grid.h; // Change the grid position to the first grid's tile grid.setOrigin(Point((grid.x % grid.w) - grid.w, (grid.y % grid.h) - grid.h)); if (grid.x < 0) grid.x += grid.w; if (grid.y < 0) grid.y += grid.h; // Convert the "grid" rectangle to screen coordinates grid = editorToScreen(grid); if (grid.w < 1 || grid.h < 1) return; // Adjust for client area gfx::Rect bounds = this->bounds(); grid.offset(-bounds.origin()); while (grid.x-grid.w >= spriteBounds.x) grid.x -= grid.w; while (grid.y-grid.h >= spriteBounds.y) grid.y -= grid.h; // Get the grid's color gfx::Color grid_color = color_utils::color_for_ui(color); grid_color = gfx::rgba( gfx::getr(grid_color), gfx::getg(grid_color), gfx::getb(grid_color), alpha); // Draw horizontal lines int x1 = spriteBounds.x; int y1 = grid.y; int x2 = spriteBounds.x + spriteBounds.w; int y2 = spriteBounds.y + spriteBounds.h; for (int c=y1; c<=y2; c+=grid.h) g->drawHLine(grid_color, x1, c, spriteBounds.w); // Draw vertical lines x1 = grid.x; y1 = spriteBounds.y; for (int c=x1; c<=x2; c+=grid.w) g->drawVLine(grid_color, c, y1, spriteBounds.h); } void Editor::drawSlices(ui::Graphics* g) { if ((m_flags & kShowSlices) == 0) return; if (!isVisible() || !m_document) return; for (auto slice : m_sprite->slices()) { auto key = slice->getByFrame(m_frame); if (!key) continue; doc::color_t docColor = slice->userData().color(); gfx::Color color = gfx::rgba(doc::rgba_getr(docColor), doc::rgba_getg(docColor), doc::rgba_getb(docColor), doc::rgba_geta(docColor)); gfx::Rect out = editorToScreen(key->bounds()) .offset(-bounds().origin()); // Center slices if (key->hasCenter()) { gfx::Rect in = editorToScreen(gfx::Rect(key->center()).offset(key->bounds().origin())) .offset(-bounds().origin()); auto in_color = gfx::rgba(gfx::getr(color), gfx::getg(color), gfx::getb(color), doc::rgba_geta(docColor)/4); if (in.y > out.y && in.y < out.y2()) g->drawHLine(in_color, out.x, in.y, out.w); if (in.y2() > out.y && in.y2() < out.y2()) g->drawHLine(in_color, out.x, in.y2(), out.w); if (in.x > out.x && in.x < out.x2()) g->drawVLine(in_color, in.x, out.y, out.h); if (in.x2() > out.x && in.x2() < out.x2()) g->drawVLine(in_color, in.x2(), out.y, out.h); } // Pivot if (key->hasPivot()) { gfx::Rect in = editorToScreen(gfx::Rect(key->pivot(), gfx::Size(1, 1)).offset(key->bounds().origin())) .offset(-bounds().origin()); auto in_color = gfx::rgba(gfx::getr(color), gfx::getg(color), gfx::getb(color), doc::rgba_geta(docColor)/4); g->drawRect(in_color, in); } g->drawRect(color, out); } } void Editor::drawCelBounds(ui::Graphics* g, const Cel* cel, const gfx::Color color) { g->drawRect(color, getCelScreenBounds(cel)); } void Editor::drawCelGuides(ui::Graphics* g, const Cel* cel, const Cel* mouseCel) { gfx::Rect sprCelBounds = cel->bounds(), scrCelBounds = getCelScreenBounds(cel), scrCmpBounds, sprCmpBounds; if (mouseCel) { scrCmpBounds = getCelScreenBounds(mouseCel); sprCmpBounds = mouseCel->bounds(); drawCelBounds( g, mouseCel, color_utils::color_for_ui(Preferences::instance().guides.autoGuidesColor())); } // Use whole canvas else { sprCmpBounds = m_sprite->bounds(); scrCmpBounds = editorToScreen(sprCmpBounds).offset(gfx::Point(-bounds().origin())); } const int midX = scrCelBounds.x+scrCelBounds.w/2; const int midY = scrCelBounds.y+scrCelBounds.h/2; if (sprCelBounds.x2() < sprCmpBounds.x) { drawCelHGuide(g, sprCelBounds.x2(), sprCmpBounds.x, scrCelBounds.x2(), scrCmpBounds.x, midY, scrCelBounds, scrCmpBounds, scrCmpBounds.x); } else if (sprCelBounds.x > sprCmpBounds.x2()) { drawCelHGuide(g, sprCmpBounds.x2(), sprCelBounds.x, scrCmpBounds.x2(), scrCelBounds.x, midY, scrCelBounds, scrCmpBounds, scrCmpBounds.x2()-1); } else { if (sprCelBounds.x != sprCmpBounds.x && sprCelBounds.x2() != sprCmpBounds.x) { drawCelHGuide(g, sprCmpBounds.x, sprCelBounds.x, scrCmpBounds.x, scrCelBounds.x, midY, scrCelBounds, scrCmpBounds, scrCmpBounds.x); } if (sprCelBounds.x != sprCmpBounds.x2() && sprCelBounds.x2() != sprCmpBounds.x2()) { drawCelHGuide(g, sprCmpBounds.x2(), sprCelBounds.x2(), scrCmpBounds.x2(), scrCelBounds.x2(), midY, scrCelBounds, scrCmpBounds, scrCmpBounds.x2()-1); } } if (sprCelBounds.y2() < sprCmpBounds.y) { drawCelVGuide(g, sprCelBounds.y2(), sprCmpBounds.y, scrCelBounds.y2(), scrCmpBounds.y, midX, scrCelBounds, scrCmpBounds, scrCmpBounds.y); } else if (sprCelBounds.y > sprCmpBounds.y2()) { drawCelVGuide(g, sprCmpBounds.y2(), sprCelBounds.y, scrCmpBounds.y2(), scrCelBounds.y, midX, scrCelBounds, scrCmpBounds, scrCmpBounds.y2()-1); } else { if (sprCelBounds.y != sprCmpBounds.y && sprCelBounds.y2() != sprCmpBounds.y) { drawCelVGuide(g, sprCmpBounds.y, sprCelBounds.y, scrCmpBounds.y, scrCelBounds.y, midX, scrCelBounds, scrCmpBounds, scrCmpBounds.y); } if (sprCelBounds.y != sprCmpBounds.y2() && sprCelBounds.y2() != sprCmpBounds.y2()) { drawCelVGuide(g, sprCmpBounds.y2(), sprCelBounds.y2(), scrCmpBounds.y2(), scrCelBounds.y2(), midX, scrCelBounds, scrCmpBounds, scrCmpBounds.y2()-1); } } } void Editor::drawCelHGuide(ui::Graphics* g, const int sprX1, const int sprX2, const int scrX1, const int scrX2, const int scrY, const gfx::Rect& scrCelBounds, const gfx::Rect& scrCmpBounds, const int dottedX) { gfx::Color color = color_utils::color_for_ui(Preferences::instance().guides.autoGuidesColor()); g->drawHLine(color, scrX1, scrY, scrX2 - scrX1); // Vertical guide to touch the horizontal line { CheckedDrawMode checked(g, 0, color, gfx::ColorNone); if (scrY < scrCmpBounds.y) g->drawVLine(color, dottedX, scrCelBounds.y, scrCmpBounds.y - scrCelBounds.y); else if (scrY > scrCmpBounds.y2()) g->drawVLine(color, dottedX, scrCmpBounds.y2(), scrCelBounds.y2() - scrCmpBounds.y2()); } auto text = base::convert_to(ABS(sprX2 - sprX1)) + "px"; const int textW = Graphics::measureUITextLength(text, font()); g->drawText(text, color_utils::blackandwhite_neg(color), color, gfx::Point((scrX1+scrX2)/2-textW/2, scrY-textHeight())); } void Editor::drawCelVGuide(ui::Graphics* g, const int sprY1, const int sprY2, const int scrY1, const int scrY2, const int scrX, const gfx::Rect& scrCelBounds, const gfx::Rect& scrCmpBounds, const int dottedY) { gfx::Color color = color_utils::color_for_ui(Preferences::instance().guides.autoGuidesColor()); g->drawVLine(color, scrX, scrY1, scrY2 - scrY1); // Horizontal guide to touch the vertical line { CheckedDrawMode checked(g, 0, color, gfx::ColorNone); if (scrX < scrCmpBounds.x) g->drawHLine(color, scrCelBounds.x, dottedY, scrCmpBounds.x - scrCelBounds.x); else if (scrX > scrCmpBounds.x2()) g->drawHLine(color, scrCmpBounds.x2(), dottedY, scrCelBounds.x2() - scrCmpBounds.x2()); } auto text = base::convert_to(ABS(sprY2 - sprY1)) + "px"; g->drawText(text, color_utils::blackandwhite_neg(color), color, gfx::Point(scrX, (scrY1+scrY2)/2-textHeight()/2)); } gfx::Rect Editor::getCelScreenBounds(const Cel* cel) { gfx::Rect layerEdges; if (m_layer->isReference()) { layerEdges = editorToScreenF(cel->boundsF()).offset(gfx::PointF(-bounds().origin())); } else { layerEdges = editorToScreen(cel->bounds()).offset(-bounds().origin()); } return layerEdges; } void Editor::flashCurrentLayer() { if (!Preferences::instance().experimental.flashLayer()) return; Site site = getSite(); int x, y; const Image* src_image = site.image(&x, &y); if (src_image) { m_renderEngine.removePreviewImage(); ExtraCelRef extraCel(new ExtraCel); extraCel->create(m_sprite, m_sprite->bounds(), m_frame, 255); extraCel->setType(render::ExtraType::COMPOSITE); extraCel->setBlendMode(BlendMode::NEG_BW); Image* flash_image = extraCel->image(); clear_image(flash_image, flash_image->maskColor()); copy_image(flash_image, src_image, x, y); { ExtraCelRef oldExtraCel = m_document->extraCel(); m_document->setExtraCel(extraCel); drawSpriteClipped(gfx::Region( gfx::Rect(0, 0, m_sprite->width(), m_sprite->height()))); manager()->flipDisplay(); m_document->setExtraCel(oldExtraCel); } invalidate(); } } gfx::Point Editor::autoScroll(MouseMessage* msg, AutoScroll dir) { gfx::Point mousePos = msg->position(); if (!Preferences::instance().editor.autoScroll()) return mousePos; // Hide the brush preview //HideBrushPreview hide(editor->brushPreview()); View* view = View::getView(this); gfx::Rect vp = view->viewportBounds(); if (!vp.contains(mousePos)) { gfx::Point delta = (mousePos - m_oldPos); gfx::Point deltaScroll = delta; if (!((mousePos.x < vp.x && delta.x < 0) || (mousePos.x >= vp.x+vp.w && delta.x > 0))) { delta.x = 0; } if (!((mousePos.y < vp.y && delta.y < 0) || (mousePos.y >= vp.y+vp.h && delta.y > 0))) { delta.y = 0; } gfx::Point scroll = view->viewScroll(); if (dir == AutoScroll::MouseDir) { scroll += delta; } else { scroll -= deltaScroll; } setEditorScroll(scroll); #if defined(_WIN32) || defined(__APPLE__) mousePos -= delta; ui::set_mouse_position(mousePos); #endif m_oldPos = mousePos; mousePos = gfx::Point( MID(vp.x, mousePos.x, vp.x+vp.w-1), MID(vp.y, mousePos.y, vp.y+vp.h-1)); } else m_oldPos = mousePos; return mousePos; } tools::Tool* Editor::getCurrentEditorTool() { return App::instance()->activeTool(); } tools::Ink* Editor::getCurrentEditorInk() { tools::Ink* ink = m_state->getStateInk(); if (ink) return ink; else return App::instance()->activeToolManager()->activeInk(); } bool Editor::isAutoSelectLayer() const { return App::instance()->contextBar()->isAutoSelectLayer(); } gfx::Point Editor::screenToEditor(const gfx::Point& pt) { View* view = View::getView(this); Rect vp = view->viewportBounds(); Point scroll = view->viewScroll(); return gfx::Point( m_proj.removeX(pt.x - vp.x + scroll.x - m_padding.x), m_proj.removeY(pt.y - vp.y + scroll.y - m_padding.y)); } gfx::PointF Editor::screenToEditorF(const gfx::Point& pt) { View* view = View::getView(this); Rect vp = view->viewportBounds(); Point scroll = view->viewScroll(); return gfx::PointF( m_proj.removeX(pt.x - vp.x + scroll.x - m_padding.x), m_proj.removeY(pt.y - vp.y + scroll.y - m_padding.y)); } Point Editor::editorToScreen(const gfx::Point& pt) { View* view = View::getView(this); Rect vp = view->viewportBounds(); Point scroll = view->viewScroll(); return Point( (vp.x - scroll.x + m_padding.x + m_proj.applyX(pt.x)), (vp.y - scroll.y + m_padding.y + m_proj.applyY(pt.y))); } gfx::PointF Editor::editorToScreenF(const gfx::PointF& pt) { View* view = View::getView(this); Rect vp = view->viewportBounds(); Point scroll = view->viewScroll(); return PointF( (vp.x - scroll.x + m_padding.x + m_proj.applyX(pt.x)), (vp.y - scroll.y + m_padding.y + m_proj.applyY(pt.y))); } Rect Editor::screenToEditor(const Rect& rc) { return gfx::Rect( screenToEditor(rc.origin()), screenToEditor(rc.point2())); } Rect Editor::editorToScreen(const Rect& rc) { return gfx::Rect( editorToScreen(rc.origin()), editorToScreen(rc.point2())); } gfx::RectF Editor::editorToScreenF(const gfx::RectF& rc) { return gfx::RectF( editorToScreenF(rc.origin()), editorToScreenF(rc.point2())); } void Editor::add_observer(EditorObserver* observer) { m_observers.add_observer(observer); } void Editor::remove_observer(EditorObserver* observer) { m_observers.remove_observer(observer); } void Editor::setCustomizationDelegate(EditorCustomizationDelegate* delegate) { if (m_customizationDelegate) m_customizationDelegate->dispose(); m_customizationDelegate = delegate; } // Returns the visible area of the active sprite. Rect Editor::getVisibleSpriteBounds() { // Return an empty rectangle if there is not a active sprite. if (!m_sprite) return Rect(); View* view = View::getView(this); Rect vp = view->viewportBounds(); vp = screenToEditor(vp); return vp.createIntersection(m_sprite->bounds()); } // Changes the scroll to see the given point as the center of the editor. void Editor::centerInSpritePoint(const gfx::Point& spritePos) { HideBrushPreview hide(m_brushPreview); View* view = View::getView(this); Rect vp = view->viewportBounds(); gfx::Point scroll( m_padding.x - (vp.w/2) + m_proj.applyX(1)/2 + m_proj.applyX(spritePos.x), m_padding.y - (vp.h/2) + m_proj.applyY(1)/2 + m_proj.applyY(spritePos.y)); updateEditor(); setEditorScroll(scroll); invalidate(); } void Editor::updateStatusBar() { if (!hasMouse()) return; // Setup status bar using the current editor's state m_state->onUpdateStatusBar(this); } void Editor::updateQuicktool() { if (m_customizationDelegate && !hasCapture()) { auto activeToolManager = App::instance()->activeToolManager(); tools::Tool* selectedTool = activeToolManager->selectedTool(); // Don't change quicktools if we are in a selection tool and using // the selection modifiers. if (selectedTool->getInk(0)->isSelection() && int(m_customizationDelegate->getPressedKeyAction(KeyContext::SelectionTool)) != 0) return; tools::Tool* newQuicktool = m_customizationDelegate->getQuickTool(selectedTool); // Check if the current state accept the given quicktool. if (newQuicktool && !m_state->acceptQuickTool(newQuicktool)) return; activeToolManager ->newQuickToolSelectedFromEditor(newQuicktool); } } void Editor::updateToolByTipProximity(ui::PointerType pointerType) { auto activeToolManager = App::instance()->activeToolManager(); if (pointerType == ui::PointerType::Eraser) { activeToolManager->eraserTipProximity(); } else { activeToolManager->regularTipProximity(); } } void Editor::updateToolLoopModifiersIndicators() { int modifiers = int(tools::ToolLoopModifiers::kNone); const bool autoSelectLayer = isAutoSelectLayer(); bool newAutoSelectLayer = autoSelectLayer; KeyAction action; if (m_customizationDelegate) { // When the mouse is captured, is when we are scrolling, or // drawing, or moving, or selecting, etc. So several // parameters/tool-loop-modifiers are static. if (hasCapture()) { modifiers |= (int(m_toolLoopModifiers) & (int(tools::ToolLoopModifiers::kReplaceSelection) | int(tools::ToolLoopModifiers::kAddSelection) | int(tools::ToolLoopModifiers::kSubtractSelection))); // Shape tools (line, curves, rectangles, etc.) action = m_customizationDelegate->getPressedKeyAction(KeyContext::ShapeTool); if (int(action & KeyAction::MoveOrigin)) modifiers |= int(tools::ToolLoopModifiers::kMoveOrigin); if (int(action & KeyAction::SquareAspect)) modifiers |= int(tools::ToolLoopModifiers::kSquareAspect); if (int(action & KeyAction::DrawFromCenter)) modifiers |= int(tools::ToolLoopModifiers::kFromCenter); } else { // We update the selection mode only if we're not selecting. action = m_customizationDelegate->getPressedKeyAction(KeyContext::SelectionTool); gen::SelectionMode mode = Preferences::instance().selection.mode(); if (int(action & KeyAction::SubtractSelection) || // Don't use "subtract" mode if the selection was activated // with the "right click mode = a selection-like tool" (m_secondaryButton && App::instance()->activeToolManager()->selectedTool() && App::instance()->activeToolManager()->selectedTool()->getInk(0)->isSelection())) { mode = gen::SelectionMode::SUBTRACT; } else if (int(action & KeyAction::AddSelection)) { mode = gen::SelectionMode::ADD; } switch (mode) { case gen::SelectionMode::DEFAULT: modifiers |= int(tools::ToolLoopModifiers::kReplaceSelection); break; case gen::SelectionMode::ADD: modifiers |= int(tools::ToolLoopModifiers::kAddSelection); break; case gen::SelectionMode::SUBTRACT: modifiers |= int(tools::ToolLoopModifiers::kSubtractSelection); break; } // For move tool action = m_customizationDelegate->getPressedKeyAction(KeyContext::MoveTool); if (int(action & KeyAction::AutoSelectLayer)) newAutoSelectLayer = true; else newAutoSelectLayer = Preferences::instance().editor.autoSelectLayer(); } } ContextBar* ctxBar = App::instance()->contextBar(); if (int(m_toolLoopModifiers) != modifiers) { m_toolLoopModifiers = tools::ToolLoopModifiers(modifiers); // TODO the contextbar should be a observer of the current editor ctxBar->updateToolLoopModifiersIndicators(m_toolLoopModifiers); if (auto drawingState = dynamic_cast(m_state.get())) { drawingState->notifyToolLoopModifiersChange(this); } } if (autoSelectLayer != newAutoSelectLayer) ctxBar->updateAutoSelectLayer(newAutoSelectLayer); } app::Color Editor::getColorByPosition(const gfx::Point& mousePos) { Site site = getSite(); if (site.sprite()) { gfx::PointF editorPos = screenToEditorF(mousePos); ColorPicker picker; picker.pickColor(site, editorPos, m_proj, ColorPicker::FromComposition); return picker.color(); } else return app::Color::fromMask(); } ////////////////////////////////////////////////////////////////////// // Message handler for the editor bool Editor::onProcessMessage(Message* msg) { // Delete states if (!m_deletedStates.empty()) m_deletedStates.clear(); switch (msg->type()) { case kTimerMessage: if (static_cast(msg)->timer() == &m_antsTimer) { if (isVisible() && m_sprite) { drawMaskSafe(); // Set offset to make selection-movement effect if (m_antsOffset < 7) m_antsOffset++; else m_antsOffset = 0; } else if (m_antsTimer.isRunning()) { m_antsTimer.stop(); } } break; case kMouseEnterMessage: updateToolLoopModifiersIndicators(); updateQuicktool(); break; case kMouseLeaveMessage: m_brushPreview.hide(); StatusBar::instance()->clearText(); break; case kMouseDownMessage: if (m_sprite) { MouseMessage* mouseMsg = static_cast(msg); m_oldPos = mouseMsg->position(); updateToolByTipProximity(mouseMsg->pointerType()); updateAutoCelGuides(msg); // Only when we right-click with the regular "paint bg-color // right-click mode" we will mark indicate that the secondary // button was used (m_secondaryButton == true). if (mouseMsg->right() && !m_secondaryButton) { m_secondaryButton = true; } updateToolLoopModifiersIndicators(); updateQuicktool(); setCursor(mouseMsg->position()); App::instance()->activeToolManager() ->pressButton(pointer_from_msg(this, mouseMsg)); EditorStatePtr holdState(m_state); return m_state->onMouseDown(this, mouseMsg); } break; case kMouseMoveMessage: if (m_sprite) { EditorStatePtr holdState(m_state); MouseMessage* mouseMsg = static_cast(msg); updateToolByTipProximity(mouseMsg->pointerType()); updateAutoCelGuides(msg); return m_state->onMouseMove(this, static_cast(msg)); } break; case kMouseUpMessage: if (m_sprite) { EditorStatePtr holdState(m_state); MouseMessage* mouseMsg = static_cast(msg); bool result = m_state->onMouseUp(this, mouseMsg); updateToolByTipProximity(mouseMsg->pointerType()); updateAutoCelGuides(msg); if (!hasCapture()) { App::instance()->activeToolManager()->releaseButtons(); m_secondaryButton = false; updateToolLoopModifiersIndicators(); updateQuicktool(); setCursor(mouseMsg->position()); } if (result) return true; } break; case kDoubleClickMessage: if (m_sprite) { MouseMessage* mouseMsg = static_cast(msg); EditorStatePtr holdState(m_state); updateToolByTipProximity(mouseMsg->pointerType()); bool used = m_state->onDoubleClick(this, mouseMsg); if (used) return true; } break; case kTouchMagnifyMessage: if (m_sprite) { EditorStatePtr holdState(m_state); return m_state->onTouchMagnify(this, static_cast(msg)); } break; case kKeyDownMessage: if (m_sprite) { EditorStatePtr holdState(m_state); bool used = m_state->onKeyDown(this, static_cast(msg)); updateToolLoopModifiersIndicators(); updateAutoCelGuides(msg); if (hasMouse()) { updateQuicktool(); setCursor(ui::get_mouse_position()); } if (used) return true; } break; case kKeyUpMessage: if (m_sprite) { EditorStatePtr holdState(m_state); bool used = m_state->onKeyUp(this, static_cast(msg)); updateToolLoopModifiersIndicators(); updateAutoCelGuides(msg); if (hasMouse()) { updateQuicktool(); setCursor(ui::get_mouse_position()); } if (used) return true; } break; case kFocusLeaveMessage: // As we use keys like Space-bar as modifier, we can clear the // keyboard buffer when we lost the focus. she::instance()->clearKeyboardBuffer(); break; case kMouseWheelMessage: if (m_sprite && hasMouse()) { EditorStatePtr holdState(m_state); if (m_state->onMouseWheel(this, static_cast(msg))) return true; } break; case kSetCursorMessage: setCursor(static_cast(msg)->position()); return true; } return Widget::onProcessMessage(msg); } void Editor::onSizeHint(SizeHintEvent& ev) { gfx::Size sz(0, 0); if (m_sprite) { gfx::Point padding = calcExtraPadding(m_proj); sz.w = m_proj.applyX(m_sprite->width()) + padding.x*2; sz.h = m_proj.applyY(m_sprite->height()) + padding.y*2; } else { sz.w = 4; sz.h = 4; } ev.setSizeHint(sz); } void Editor::onResize(ui::ResizeEvent& ev) { Widget::onResize(ev); m_padding = calcExtraPadding(m_proj); } void Editor::onPaint(ui::PaintEvent& ev) { HideBrushPreview hide(m_brushPreview); Graphics* g = ev.graphics(); gfx::Rect rc = clientBounds(); SkinTheme* theme = static_cast(this->theme()); // Editor without sprite if (!m_sprite) { g->fillRect(theme->colors.editorFace(), rc); } // Editor with sprite else { try { // Lock the sprite to read/render it. We wait 1/4 secs in case // the background thread is making a backup. DocumentReader documentReader(m_document, 250); // Draw the sprite in the editor renderChrono.reset(); drawSpriteUnclippedRect(g, gfx::Rect(0, 0, m_sprite->width(), m_sprite->height())); renderElapsed = renderChrono.elapsed(); // Show performance stats (TODO show performance stats in other widget) if (Preferences::instance().perf.showRenderTime()) { View* view = View::getView(this); gfx::Rect vp = view->viewportBounds(); char buf[128]; sprintf(buf, "%.3f", renderElapsed); g->drawText( buf, gfx::rgba(255, 255, 255, 255), gfx::rgba(0, 0, 0, 255), vp.origin() - bounds().origin()); } // Draw the mask boundaries if (m_document->getMaskBoundaries()) { drawMask(g); m_antsTimer.start(); } else { m_antsTimer.stop(); } } catch (const LockedDocumentException&) { // The sprite is locked to be read, so we can draw an opaque // background only. g->fillRect(theme->colors.editorFace(), rc); defer_invalid_rect(g->getClipBounds().offset(bounds().origin())); } } } void Editor::onInvalidateRegion(const gfx::Region& region) { Widget::onInvalidateRegion(region); m_brushPreview.invalidateRegion(region); } // When the current tool is changed void Editor::onActiveToolChange(tools::Tool* tool) { m_state->onActiveToolChange(this, tool); updateStatusBar(); } void Editor::onFgColorChange() { m_brushPreview.redraw(); } void Editor::onContextBarBrushChange() { m_brushPreview.redraw(); } void Editor::onShowExtrasChange() { invalidate(); } void Editor::onExposeSpritePixels(doc::DocumentEvent& ev) { if (m_state && ev.sprite() == m_sprite) m_state->onExposeSpritePixels(ev.region()); } void Editor::onSpritePixelRatioChanged(doc::DocumentEvent& ev) { m_proj.setPixelRatio(ev.sprite()->pixelRatio()); invalidate(); } void Editor::onRemoveCel(DocumentEvent& ev) { m_showGuidesThisCel = nullptr; } void Editor::onAddFrameTag(DocumentEvent& ev) { m_tagFocusBand = -1; } void Editor::onRemoveFrameTag(DocumentEvent& ev) { m_tagFocusBand = -1; } void Editor::setCursor(const gfx::Point& mouseScreenPos) { bool used = false; if (m_sprite) used = m_state->onSetCursor(this, mouseScreenPos); if (!used) showMouseCursor(kArrowCursor); } void Editor::setLastDrawingPosition(const gfx::Point& pos) { m_lastDrawingPosition = pos; } bool Editor::canDraw() { return (m_layer != NULL && m_layer->isImage() && m_layer->isVisibleHierarchy() && m_layer->isEditableHierarchy() && !m_layer->isReference()); } bool Editor::isInsideSelection() { gfx::Point spritePos = screenToEditor(ui::get_mouse_position()); KeyAction action = m_customizationDelegate->getPressedKeyAction(KeyContext::SelectionTool); return (action == KeyAction::None) && m_document && m_document->isMaskVisible() && m_document->mask()->containsPoint(spritePos.x, spritePos.y); } EditorHit Editor::calcHit(const gfx::Point& mouseScreenPos) { tools::Ink* ink = getCurrentEditorInk(); if (ink) { // Check if we can transform slices if (ink->isSlice()) { if (m_docPref.show.slices()) { for (auto slice : m_sprite->slices()) { auto key = slice->getByFrame(m_frame); if (key) { gfx::Rect bounds = editorToScreen(key->bounds()); gfx::Rect center = key->center(); // Move bounds if (bounds.contains(mouseScreenPos) && !bounds.shrink(5*guiscale()).contains(mouseScreenPos)) { int border = (mouseScreenPos.x <= bounds.x ? LEFT: 0) | (mouseScreenPos.y <= bounds.y ? TOP: 0) | (mouseScreenPos.x >= bounds.x2() ? RIGHT: 0) | (mouseScreenPos.y >= bounds.y2() ? BOTTOM: 0); EditorHit hit(EditorHit::SliceBounds); hit.setBorder(border); hit.setSlice(slice); return hit; } // Move center if (!center.isEmpty()) { center = editorToScreen( center.offset(key->bounds().origin())); bool horz1 = gfx::Rect(bounds.x, center.y-2*guiscale(), bounds.w, 5*guiscale()).contains(mouseScreenPos); bool horz2 = gfx::Rect(bounds.x, center.y2()-2*guiscale(), bounds.w, 5*guiscale()).contains(mouseScreenPos); bool vert1 = gfx::Rect(center.x-2*guiscale(), bounds.y, 5*guiscale(), bounds.h).contains(mouseScreenPos); bool vert2 = gfx::Rect(center.x2()-2*guiscale(), bounds.y, 5*guiscale(), bounds.h).contains(mouseScreenPos); if (horz1 || horz2 || vert1 || vert2) { int border = (horz1 ? TOP: 0) | (horz2 ? BOTTOM: 0) | (vert1 ? LEFT: 0) | (vert2 ? RIGHT: 0); EditorHit hit(EditorHit::SliceCenter); hit.setBorder(border); hit.setSlice(slice); return hit; } } // Move all the slice if (bounds.contains(mouseScreenPos)) { EditorHit hit(EditorHit::SliceBounds); hit.setBorder(CENTER | MIDDLE); hit.setSlice(slice); return hit; } } } } } } return EditorHit(EditorHit::None); } void Editor::setZoomAndCenterInMouse(const Zoom& zoom, const gfx::Point& mousePos, ZoomBehavior zoomBehavior) { HideBrushPreview hide(m_brushPreview); View* view = View::getView(this); Rect vp = view->viewportBounds(); Projection proj = m_proj; proj.setZoom(zoom); gfx::Point screenPos; gfx::Point spritePos; gfx::PointT subpixelPos(0.5, 0.5); switch (zoomBehavior) { case ZoomBehavior::CENTER: screenPos = gfx::Point(vp.x + vp.w/2, vp.y + vp.h/2); break; case ZoomBehavior::MOUSE: screenPos = mousePos; break; } spritePos = screenToEditor(screenPos); if (zoomBehavior == ZoomBehavior::MOUSE) { gfx::Point screenPos2 = editorToScreen(spritePos); if (m_proj.scaleX() > 1.0) { subpixelPos.x = (0.5 + screenPos.x - screenPos2.x) / m_proj.scaleX(); if (proj.scaleX() > m_proj.scaleX()) { double t = 1.0 / proj.scaleX(); if (subpixelPos.x >= 0.5-t && subpixelPos.x <= 0.5+t) subpixelPos.x = 0.5; } } if (m_proj.scaleY() > 1.0) { subpixelPos.y = (0.5 + screenPos.y - screenPos2.y) / m_proj.scaleY(); if (proj.scaleY() > m_proj.scaleY()) { double t = 1.0 / proj.scaleY(); if (subpixelPos.y >= 0.5-t && subpixelPos.y <= 0.5+t) subpixelPos.y = 0.5; } } } gfx::Point padding = calcExtraPadding(proj); gfx::Point scrollPos( padding.x - (screenPos.x-vp.x) + proj.applyX(spritePos.x+proj.removeX(1)/2) + int(proj.applyX(subpixelPos.x)), padding.y - (screenPos.y-vp.y) + proj.applyY(spritePos.y+proj.removeY(1)/2) + int(proj.applyY(subpixelPos.y))); setZoom(zoom); if ((m_proj.zoom() != zoom) || (screenPos != view->viewScroll())) { updateEditor(); setEditorScroll(scrollPos); } flushRedraw(); } void Editor::pasteImage(const Image* image, const Mask* mask) { ASSERT(image); base::UniquePtr temp_mask; if (!mask) { gfx::Rect visibleBounds = getVisibleSpriteBounds(); gfx::Rect imageBounds = image->bounds(); temp_mask.reset(new Mask); temp_mask->replace( gfx::Rect(visibleBounds.x + visibleBounds.w/2 - imageBounds.w/2, visibleBounds.y + visibleBounds.h/2 - imageBounds.h/2, imageBounds.w, imageBounds.h)); mask = temp_mask.get(); } // Change to a selection tool: it's necessary for PixelsMovement // which will use the extra cel for transformation preview, and is // not compatible with the drawing cursor preview which overwrite // the extra cel. if (!getCurrentEditorInk()->isSelection()) { tools::Tool* defaultSelectionTool = App::instance()->toolBox()->getToolById(tools::WellKnownTools::RectangularMarquee); ToolBar::instance()->selectTool(defaultSelectionTool); } Sprite* sprite = this->sprite(); // Check bounds where the image will be pasted. int x = mask->bounds().x; int y = mask->bounds().y; { Rect visibleBounds = getVisibleSpriteBounds(); // If the pasted image original location center point isn't // visible, we center the image in the editor's visible bounds. if (!visibleBounds.contains(mask->bounds().center())) { x = visibleBounds.x + visibleBounds.w/2 - image->width()/2; y = visibleBounds.y + visibleBounds.h/2 - image->height()/2; } // In other case, if the center is visible, we put the pasted // image in its original location. else { x = MID(visibleBounds.x-image->width(), x, visibleBounds.x+visibleBounds.w-1); y = MID(visibleBounds.y-image->height(), y, visibleBounds.y+visibleBounds.h-1); } // Also we always limit the image inside the sprite's bounds. x = MID(0, x, sprite->width() - image->width()); y = MID(0, y, sprite->height() - image->height()); } // Clear brush preview, as the extra cel will be replaced with the // pasted image. m_brushPreview.hide(); Mask mask2(*mask); mask2.setOrigin(x, y); PixelsMovementPtr pixelsMovement( new PixelsMovement(UIContext::instance(), getSite(), image, &mask2, "Paste")); setState(EditorStatePtr(new MovingPixelsState(this, NULL, pixelsMovement, NoHandle))); } void Editor::startSelectionTransformation(const gfx::Point& move, double angle) { if (MovingPixelsState* movingPixels = dynamic_cast(m_state.get())) { movingPixels->translate(move); if (std::fabs(angle) > 1e-5) movingPixels->rotate(angle); } else if (StandbyState* standby = dynamic_cast(m_state.get())) { standby->startSelectionTransformation(this, move, angle); } } void Editor::notifyScrollChanged() { m_observers.notifyScrollChanged(this); } void Editor::notifyZoomChanged() { m_observers.notifyZoomChanged(this); } bool Editor::checkForScroll(ui::MouseMessage* msg) { tools::Ink* clickedInk = getCurrentEditorInk(); // Start scroll loop if (msg->middle() || clickedInk->isScrollMovement()) { // TODO msg->middle() should be customizable startScrollingState(msg); return true; } else return false; } bool Editor::checkForZoom(ui::MouseMessage* msg) { tools::Ink* clickedInk = getCurrentEditorInk(); // Start scroll loop if (clickedInk->isZoom()) { startZoomingState(msg); return true; } else return false; } void Editor::startScrollingState(ui::MouseMessage* msg) { EditorStatePtr newState(new ScrollingState); setState(newState); newState->onMouseDown(this, msg); } void Editor::startZoomingState(ui::MouseMessage* msg) { EditorStatePtr newState(new ZoomingState); setState(newState); newState->onMouseDown(this, msg); } void Editor::play(const bool playOnce, const bool playAll) { ASSERT(m_state); if (!m_state) return; if (m_isPlaying) stop(); m_isPlaying = true; setState(EditorStatePtr(new PlayState(playOnce, playAll))); } void Editor::stop() { ASSERT(m_state); if (!m_state) return; if (m_isPlaying) { while (m_state && !dynamic_cast(m_state.get())) backToPreviousState(); m_isPlaying = false; ASSERT(m_state && dynamic_cast(m_state.get())); if (m_state) backToPreviousState(); } } bool Editor::isPlaying() const { return m_isPlaying; } void Editor::showAnimationSpeedMultiplierPopup(Option& playOnce, Option& playAll, const bool withStopBehaviorOptions) { const double options[] = { 0.25, 0.5, 1.0, 1.5, 2.0, 3.0 }; Menu menu; for (double option : options) { MenuItem* item = new MenuItem("Speed x" + base::convert_to(option)); item->Click.connect(base::Bind(&Editor::setAnimationSpeedMultiplier, this, option)); item->setSelected(m_aniSpeed == option); menu.addChild(item); } menu.addChild(new MenuSeparator); // Play once option { MenuItem* item = new MenuItem("Play Once"); item->Click.connect( [&playOnce]() { playOnce(!playOnce()); }); item->setSelected(playOnce()); menu.addChild(item); } // Play all option { MenuItem* item = new MenuItem("Play All Frames (Ignore Tags)"); item->Click.connect( [&playAll]() { playAll(!playAll()); }); item->setSelected(playAll()); menu.addChild(item); } if (withStopBehaviorOptions) { MenuItem* item = new MenuItem("Rewind on Stop"); item->Click.connect( []() { // Switch the "rewind_on_stop" option Preferences::instance().general.rewindOnStop( !Preferences::instance().general.rewindOnStop()); }); item->setSelected(Preferences::instance().general.rewindOnStop()); menu.addChild(item); } menu.showPopup(ui::get_mouse_position()); if (isPlaying()) { // Re-play stop(); play(playOnce(), playAll()); } } double Editor::getAnimationSpeedMultiplier() const { return m_aniSpeed; } void Editor::setAnimationSpeedMultiplier(double speed) { m_aniSpeed = speed; } void Editor::showMouseCursor(CursorType cursorType, const Cursor* cursor) { m_brushPreview.hide(); ui::set_mouse_cursor(cursorType, cursor); } void Editor::showBrushPreview(const gfx::Point& screenPos) { if (Preferences::instance().cursor.paintingCursorType() != app::gen::PaintingCursorType::SIMPLE_CROSSHAIR) ui::set_mouse_cursor(kNoCursor); m_brushPreview.show(screenPos); } // static ImageBufferPtr Editor::getRenderImageBuffer() { return m_renderBuffer; } // static gfx::Point Editor::calcExtraPadding(const Projection& proj) { View* view = View::getView(this); if (view) { Rect vp = view->viewportBounds(); return gfx::Point( std::max(vp.w/2, vp.w - proj.applyX(m_sprite->width())), std::max(vp.h/2, vp.h - proj.applyY(m_sprite->height()))); } else return gfx::Point(0, 0); } bool Editor::isMovingPixels() const { return (dynamic_cast(m_state.get()) != nullptr); } void Editor::dropMovingPixels() { ASSERT(isMovingPixels()); backToPreviousState(); } void Editor::invalidateIfActive() { if (isActive()) invalidate(); } bool Editor::showAutoCelGuides() { return (getCurrentEditorInk()->isCelMovement() && m_docPref.show.autoGuides() && m_customizationDelegate && int(m_customizationDelegate->getPressedKeyAction(KeyContext::MoveTool) & KeyAction::AutoSelectLayer)); } void Editor::updateAutoCelGuides(ui::Message* msg) { Cel* oldShowGuidesThisCel = m_showGuidesThisCel; // Check if the user is pressing the Ctrl or Cmd key on move // tool to show automatic guides. if (showAutoCelGuides() && m_state->requireBrushPreview()) { ui::MouseMessage* mouseMsg = dynamic_cast(msg); ColorPicker picker; picker.pickColor(getSite(), screenToEditorF(mouseMsg ? mouseMsg->position(): ui::get_mouse_position()), m_proj, ColorPicker::FromComposition); m_showGuidesThisCel = (picker.layer() ? picker.layer()->cel(m_frame): nullptr); } else { m_showGuidesThisCel = nullptr; } if (m_showGuidesThisCel != oldShowGuidesThisCel) invalidate(); } } // namespace app