diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index fa434b19f..8eaabb446 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -530,6 +530,7 @@ add_library(app-lib util/expand_cel_canvas.cpp util/filetoks.cpp util/freetype_utils.cpp + util/layer_boundaries.cpp util/msk_file.cpp util/new_image_from_mask.cpp util/pic_file.cpp diff --git a/src/app/ui/timeline/timeline.cpp b/src/app/ui/timeline/timeline.cpp index 4753ccb1d..dbe5d0404 100644 --- a/src/app/ui/timeline/timeline.cpp +++ b/src/app/ui/timeline/timeline.cpp @@ -38,6 +38,7 @@ #include "app/ui/workspace.h" #include "app/ui_context.h" #include "app/util/clipboard.h" +#include "app/util/layer_boundaries.h" #include "base/bind.h" #include "base/convert_to.h" #include "base/memory.h" @@ -129,6 +130,31 @@ namespace { } } + bool is_copy_key_pressed(ui::Message* msg) { + return + msg->ctrlPressed() || // Ctrl is common on Windows + msg->altPressed(); // Alt is common on Mac OS X + } + + bool is_select_layer_in_canvas_key_pressed(ui::Message* msg) { +#ifdef __APPLE__ + return msg->cmdPressed(); +#else + return msg->ctrlPressed(); +#endif + } + + SelectLayerBoundariesOp get_select_layer_in_canvas_op(ui::Message* msg) { + if (msg->altPressed() && msg->shiftPressed()) + return SelectLayerBoundariesOp::INTERSECT; + else if (msg->shiftPressed()) + return SelectLayerBoundariesOp::ADD; + else if (msg->altPressed()) + return SelectLayerBoundariesOp::SUBTRACT; + else + return SelectLayerBoundariesOp::REPLACE; + } + } // anonymous namespace Timeline::Hit::Hit(int part, @@ -607,10 +633,18 @@ bool Timeline::onProcessMessage(Message* msg) } case PART_ROW_TEXT: { base::ScopedValue lock(m_fromTimeline, true, false); - layer_t old_layer = getLayerIndex(m_layer); - bool selectLayer = (mouseMsg->left() || !isLayerActive(m_clk.layer)); + const layer_t old_layer = getLayerIndex(m_layer); + const bool selectLayer = (mouseMsg->left() || !isLayerActive(m_clk.layer)); + const bool selectLayerInCanvas = + (m_clk.layer != -1 && + mouseMsg->left() && + is_select_layer_in_canvas_key_pressed(mouseMsg)); - if (selectLayer) { + if (selectLayerInCanvas) { + select_layer_boundaries(m_rows[m_clk.layer].layer(), m_frame, + get_select_layer_in_canvas_op(mouseMsg)); + } + else if (selectLayer) { m_state = STATE_SELECTING_LAYERS; if (clearRange) m_range.clearRange(); @@ -637,26 +671,37 @@ bool Timeline::onProcessMessage(Message* msg) break; case PART_CEL: { base::ScopedValue lock(m_fromTimeline, true, false); - layer_t old_layer = getLayerIndex(m_layer); - bool selectCel = (mouseMsg->left() + const layer_t old_layer = getLayerIndex(m_layer); + const bool selectCel = (mouseMsg->left() || !isLayerActive(m_clk.layer) || !isFrameActive(m_clk.frame)); - frame_t old_frame = m_frame; + const bool selectCelInCanvas = + (m_clk.layer != -1 && + mouseMsg->left() && + is_select_layer_in_canvas_key_pressed(mouseMsg)); + const frame_t old_frame = m_frame; - if (selectCel) { - m_state = STATE_SELECTING_CELS; - m_range.clearRange(); - m_range.startRange(m_rows[m_clk.layer].layer(), - m_clk.frame, Range::kCels); - m_startRange = m_range; + if (selectCelInCanvas) { + select_layer_boundaries(m_rows[m_clk.layer].layer(), + m_clk.frame, + get_select_layer_in_canvas_op(mouseMsg)); } + else { + if (selectCel) { + m_state = STATE_SELECTING_CELS; + m_range.clearRange(); + m_range.startRange(m_rows[m_clk.layer].layer(), + m_clk.frame, Range::kCels); + m_startRange = m_range; + } - // Select the new clicked-part. - if (old_layer != m_clk.layer - || old_frame != m_clk.frame) { - setLayer(m_rows[m_clk.layer].layer()); - setFrame(m_clk.frame, true); - invalidate(); + // Select the new clicked-part. + if (old_layer != m_clk.layer + || old_frame != m_clk.frame) { + setLayer(m_rows[m_clk.layer].layer()); + setFrame(m_clk.frame, true); + invalidate(); + } } // Change the scroll to show the new selected cel. @@ -1078,7 +1123,7 @@ bool Timeline::onProcessMessage(Message* msg) if (m_state == STATE_MOVING_RANGE && m_dropRange.type() != Range::kNone) { - dropRange(isCopyKeyPressed(mouseMsg) ? + dropRange(is_copy_key_pressed(mouseMsg) ? Timeline::kCopy: Timeline::kMove); } @@ -1636,7 +1681,7 @@ void Timeline::setCursor(ui::Message* msg, const Hit& hit) } // Moving. else if (m_state == STATE_MOVING_RANGE) { - if (isCopyKeyPressed(msg)) + if (is_copy_key_pressed(msg)) ui::set_mouse_cursor(kArrowPlusCursor); else ui::set_mouse_cursor(kMoveCursor); @@ -2919,7 +2964,7 @@ Timeline::Hit Timeline::hitTest(ui::Message* msg, const gfx::Point& mousePos) auto mouseMsg = dynamic_cast(msg); if (// With Ctrl and Alt key we can drag the range from any place (not necessary from the outline. - isCopyKeyPressed(msg) || + is_copy_key_pressed(msg) || // Drag with right-click (m_state == STATE_STANDBY && mouseMsg && @@ -2990,7 +3035,7 @@ void Timeline::updateStatusBar(ui::Message* msg) StatusBar* sb = StatusBar::instance(); if (m_state == STATE_MOVING_RANGE) { - const char* verb = isCopyKeyPressed(msg) ? "Copy": "Move"; + const char* verb = is_copy_key_pressed(msg) ? "Copy": "Move"; switch (m_range.type()) { @@ -3465,12 +3510,6 @@ void Timeline::clearClipboardRange() m_clipboard_timer.stop(); } -bool Timeline::isCopyKeyPressed(ui::Message* msg) -{ - return msg->ctrlPressed() || // Ctrl is common on Windows - msg->altPressed(); // Alt is common on Mac OS X -} - DocumentPreferences& Timeline::docPref() const { return Preferences::instance().document(m_document); diff --git a/src/app/ui/timeline/timeline.h b/src/app/ui/timeline/timeline.h index e27fbc9ca..33a4cd633 100644 --- a/src/app/ui/timeline/timeline.h +++ b/src/app/ui/timeline/timeline.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2001-2017 David Capello +// Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -283,8 +283,6 @@ namespace app { void updateDropRange(const gfx::Point& pt); void clearClipboardRange(); - bool isCopyKeyPressed(ui::Message* msg); - // The layer of the bottom (e.g. Background layer) layer_t firstLayer() const { return 0; } // The layer of the top. diff --git a/src/app/util/layer_boundaries.cpp b/src/app/util/layer_boundaries.cpp new file mode 100644 index 000000000..bfce2ac8a --- /dev/null +++ b/src/app/util/layer_boundaries.cpp @@ -0,0 +1,131 @@ +// Aseprite +// Copyright (C) 2018 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/util/layer_boundaries.h" + +#include "app/cmd/set_mask.h" +#include "app/context_access.h" +#include "app/modules/gui.h" +#include "app/transaction.h" +#include "app/ui_context.h" +#include "doc/cel.h" +#include "doc/document.h" +#include "doc/image.h" +#include "doc/image_impl.h" +#include "doc/layer.h" +#include "doc/mask.h" + +using namespace doc; + +namespace app { + +void select_layer_boundaries(Layer* layer, + const frame_t frame, + const SelectLayerBoundariesOp op) +{ + Mask newMask; + + const Cel* cel = layer->cel(frame); + if (cel) { + const Image* image = cel->image(); + if (image) { + newMask.replace(cel->bounds()); + newMask.freeze(); + { + LockImageBits maskBits(newMask.bitmap()); + auto maskIt = maskBits.begin(); + auto maskEnd = maskBits.end(); + + switch (image->pixelFormat()) { + + case IMAGE_RGB: { + LockImageBits rgbBits(image); + auto rgbIt = rgbBits.begin(); +#if _DEBUG + auto rgbEnd = rgbBits.end(); +#endif + for (; maskIt != maskEnd; ++maskIt, ++rgbIt) { + ASSERT(rgbIt != rgbEnd); + color_t c = *rgbIt; + *maskIt = (rgba_geta(c) >= 128); // TODO configurable threshold + } + break; + } + + case IMAGE_GRAYSCALE: { + LockImageBits grayBits(image); + auto grayIt = grayBits.begin(); +#if _DEBUG + auto grayEnd = grayBits.end(); +#endif + for (; maskIt != maskEnd; ++maskIt, ++grayIt) { + ASSERT(grayIt != grayEnd); + color_t c = *grayIt; + *maskIt = (graya_geta(c) >= 128); // TODO configurable threshold + } + break; + } + + case IMAGE_INDEXED: { + const doc::color_t maskColor = image->maskColor(); + LockImageBits idxBits(image); + auto idxIt = idxBits.begin(); +#if _DEBUG + auto idxEnd = idxBits.end(); +#endif + for (; maskIt != maskEnd; ++maskIt, ++idxIt) { + ASSERT(idxIt != idxEnd); + color_t c = *idxIt; + *maskIt = (c != maskColor); + } + break; + } + + } + } + newMask.unfreeze(); + } + } + + ContextWriter writer(UIContext::instance()); + app::Document* doc = writer.document(); + Sprite* spr = layer->sprite(); + ASSERT(doc == spr->document()); + + if (doc->isMaskVisible()) { + switch (op) { + case SelectLayerBoundariesOp::REPLACE: + // newMask is the new mask + break; + case SelectLayerBoundariesOp::ADD: + newMask.add(*doc->mask()); + break; + case SelectLayerBoundariesOp::SUBTRACT: { + Mask oldMask(*doc->mask()); + oldMask.subtract(newMask); + newMask.copyFrom(&oldMask); // TODO use something like std::swap() + break; + } + case SelectLayerBoundariesOp::INTERSECT: + newMask.intersect(*doc->mask()); + break; + } + } + + Transaction transaction(writer.context(), "Select Layer Boundaries", DoesntModifyDocument); + transaction.execute(new cmd::SetMask(doc, &newMask)); + transaction.commit(); + + doc->resetTransformation(); + doc->generateMaskBoundaries(); + update_screen_for_document(doc); +} + +} // namespace app diff --git a/src/app/util/layer_boundaries.h b/src/app/util/layer_boundaries.h new file mode 100644 index 000000000..cc59051df --- /dev/null +++ b/src/app/util/layer_boundaries.h @@ -0,0 +1,29 @@ +// Aseprite +// Copyright (C) 2018 David Capello +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_LAYER_BOUNDARIES_H_INCLUDED +#define APP_LAYER_BOUNDARIES_H_INCLUDED +#pragma once + +#include "doc/frame.h" + +namespace doc { + class Layer; +} + +namespace app { + + enum SelectLayerBoundariesOp { + REPLACE, ADD, SUBTRACT, INTERSECT + }; + + void select_layer_boundaries(doc::Layer* layer, + const doc::frame_t frame, + const SelectLayerBoundariesOp op); + +} // namespace app + +#endif diff --git a/src/doc/mask.cpp b/src/doc/mask.cpp index bc0018638..7db50aff8 100644 --- a/src/doc/mask.cpp +++ b/src/doc/mask.cpp @@ -1,5 +1,5 @@ // Aseprite Document Library -// Copyright (c) 2001-2016 David Capello +// Copyright (c) 2001-2018 David Capello // // This file is released under the terms of the MIT license. // Read LICENSE.txt for more information. @@ -19,6 +19,32 @@ namespace doc { +namespace { + + template + void for_each_mask_pixel(Mask& a, const Mask& b, Func f) { + a.reserve(b.bounds()); + + { + LockImageBits aBits(a.bitmap()); + auto aIt = aBits.begin(); + + auto bounds = a.bounds(); + for (int y=0; y color_t { + return a | b; + }); +} + +void Mask::subtract(const doc::Mask& mask) +{ + for_each_mask_pixel( + *this, mask, + [](color_t a, color_t b) -> color_t { + if (a) + return a - b; + else + return 0; + }); +} + +void Mask::intersect(const doc::Mask& mask) +{ + for_each_mask_pixel( + *this, mask, + [](color_t a, color_t b) -> color_t { + return a & b; + }); +} + void Mask::add(const gfx::Rect& bounds) { if (m_freeze_count == 0) diff --git a/src/doc/mask.h b/src/doc/mask.h index be542b0c0..6e0b94742 100644 --- a/src/doc/mask.h +++ b/src/doc/mask.h @@ -1,5 +1,5 @@ // Aseprite Document Library -// Copyright (c) 2001-2015 David Capello +// Copyright (c) 2001-2018 David Capello // // This file is released under the terms of the MIT license. // Read LICENSE.txt for more information. @@ -79,10 +79,14 @@ namespace doc { // Inverts the mask. void invert(); - // Adds the specified rectangle in the mask/selection + void add(const doc::Mask& mask); + void subtract(const doc::Mask& mask); + void intersect(const doc::Mask& mask); + void add(const gfx::Rect& bounds); void subtract(const gfx::Rect& bounds); void intersect(const gfx::Rect& bounds); + void byColor(const Image* image, int color, int fuzziness); void crop(const Image* image);