Add ability to edit multiple slices

Related to #721 and #1651, still need more work to give a better UX to
edit static and animated slices.
This commit is contained in:
David Capello 2019-05-02 16:26:13 -03:00
parent 40957c196f
commit 73de6c8b1d
19 changed files with 606 additions and 161 deletions

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Aseprite -->
<!-- Copyright (C) 2017-2018 by David Capello -->
<!-- Copyright (C) 2019 Igara Studio S.A. -->
<!-- Copyright (C) 2017-2018 David Capello -->
<gui>
<window id="slice_properties" text="@.title">
<grid columns="2">
@ -30,16 +31,16 @@
<check text="@.center" id="center" />
<hbox homogeneous="true">
<expr id="center_x" text="0" />
<expr id="center_y" text="0" />
<expr id="center_w" text="0" />
<expr id="center_h" text="0" />
<expr id="center_x" />
<expr id="center_y" />
<expr id="center_w" />
<expr id="center_h" />
</hbox>
<check text="@.pivot" id="pivot" />
<hbox>
<expr id="pivot_x" text="0" />
<expr id="pivot_y" text="0" />
<expr id="pivot_x" />
<expr id="pivot_y" />
</hbox>
<separator horizontal="true" cell_hspan="2" />

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2017 David Capello
//
// This program is distributed under the terms of
@ -10,6 +11,8 @@
#include "app/cmd/add_slice.h"
#include "app/doc.h"
#include "app/doc_event.h"
#include "doc/slice.h"
#include "doc/slice_io.h"
#include "doc/sprite.h"
@ -42,9 +45,7 @@ void AddSlice::onUndo()
write_slice(m_stream, slice);
m_size = size_t(m_stream.tellp());
sprite->slices().remove(slice);
sprite->incrementVersion();
delete slice;
removeSlice(sprite, slice);
}
void AddSlice::onRedo()
@ -52,13 +53,37 @@ void AddSlice::onRedo()
Sprite* sprite = this->sprite();
Slice* slice = read_slice(m_stream);
sprite->slices().add(slice);
sprite->incrementVersion();
addSlice(sprite, slice);
m_stream.str(std::string());
m_stream.clear();
m_size = 0;
}
void AddSlice::addSlice(Sprite* sprite, Slice* slice)
{
sprite->slices().add(slice);
sprite->incrementVersion();
Doc* doc = static_cast<Doc*>(sprite->document());
DocEvent ev(doc);
ev.sprite(sprite);
ev.slice(slice);
doc->notify_observers<DocEvent&>(&DocObserver::onAddSlice, ev);
}
void AddSlice::removeSlice(Sprite* sprite, Slice* slice)
{
Doc* doc = static_cast<Doc*>(sprite->document());
DocEvent ev(doc);
ev.sprite(sprite);
ev.slice(slice);
doc->notify_observers<DocEvent&>(&DocObserver::onRemoveSlice, ev);
sprite->slices().remove(slice);
sprite->incrementVersion();
delete slice;
}
} // namespace cmd
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2017 David Capello
//
// This program is distributed under the terms of
@ -33,6 +34,9 @@ namespace cmd {
}
private:
void addSlice(Sprite* sprite, Slice* slice);
void removeSlice(Sprite* sprite, Slice* slice);
size_t m_size;
std::stringstream m_stream;
};

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2017-2018 David Capello
//
// This program is distributed under the terms of
@ -14,9 +15,10 @@
#include "app/commands/command.h"
#include "app/context_access.h"
#include "app/modules/gui.h"
#include "app/ui/status_bar.h"
#include "app/tx.h"
#include "app/ui/status_bar.h"
#include "base/convert_to.h"
#include "doc/selected_objects.h"
#include "doc/slice.h"
#include "doc/sprite.h"
#include "ui/alert.h"
@ -66,40 +68,63 @@ void RemoveSliceCommand::onExecute(Context* context)
const ContextReader reader(context);
const Sprite* sprite = reader.sprite();
frame_t frame = reader.frame();
const Slice* foundSlice = nullptr;
SelectedObjects slicesToDelete;
if (!m_sliceName.empty())
foundSlice = sprite->slices().getByName(m_sliceName);
else if (m_sliceId != NullId)
foundSlice = sprite->slices().getById(m_sliceId);
std::string sliceName;
{
Slice* slice = nullptr;
if (!m_sliceName.empty())
slice = sprite->slices().getByName(m_sliceName);
else if (m_sliceId != NullId)
slice = sprite->slices().getById(m_sliceId);
if (!foundSlice)
if (slice)
slicesToDelete.insert(slice->id());
else
slicesToDelete = reader.site()->selectedSlices();
}
// Nothing to delete
if (slicesToDelete.empty())
return;
std::string sliceName = foundSlice->name();
if (slicesToDelete.size() == 1) {
Slice* slice = slicesToDelete.frontAs<Slice>();
ASSERT(slice);
if (slice)
sliceName = slice->name();
}
{
ContextWriter writer(reader, 500);
Doc* document(writer.document());
Sprite* sprite(writer.sprite());
Tx tx(writer.context(), "Remove Slice");
Slice* slice = const_cast<Slice*>(foundSlice);
if (slice->size() > 1) {
tx(new cmd::SetSliceKey(slice, frame, SliceKey()));
}
else {
tx(new cmd::RemoveSlice(sprite, slice));
for (auto slice : slicesToDelete.iterateAs<Slice>()) {
ASSERT(slice);
if (!slice)
continue;
if (slice->size() > 1) {
tx(new cmd::SetSliceKey(slice, frame, SliceKey()));
}
else {
tx(new cmd::RemoveSlice(sprite, slice));
}
}
tx.commit();
document->notifyGeneralUpdate();
}
StatusBar::instance()->invalidate();
if (!sliceName.empty())
StatusBar::instance()->showTip(1000, "Slice '%s' removed", sliceName.c_str());
StatusBar::instance()->showTip(
1000, "Slice '%s' removed", sliceName.c_str());
else
StatusBar::instance()->showTip(1000, "Slice removed");
StatusBar::instance()->showTip(
1000, "%d slice(s) removed", slicesToDelete.size());
}
Command* CommandFactory::createRemoveSliceCommand()

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2017-2018 David Capello
//
// This program is distributed under the terms of
@ -64,45 +65,76 @@ void SlicePropertiesCommand::onExecute(Context* context)
const ContextReader reader(context);
const Sprite* sprite = reader.sprite();
frame_t frame = reader.frame();
const Slice* foundSlice = nullptr;
SelectedObjects slices;
if (!m_sliceName.empty())
foundSlice = sprite->slices().getByName(m_sliceName);
else if (m_sliceId != NullId)
foundSlice = sprite->slices().getById(m_sliceId);
{
Slice* slice = nullptr;
if (!m_sliceName.empty())
slice = sprite->slices().getByName(m_sliceName);
else if (m_sliceId != NullId)
slice = sprite->slices().getById(m_sliceId);
if (!foundSlice)
if (slice)
slices.insert(slice->id());
else
slices = reader.site()->selectedSlices();
}
// Nothing to delete
if (slices.empty())
return;
const doc::SliceKey* key = foundSlice->getByFrame(frame);
if (!key)
return;
SliceWindow window(sprite, foundSlice, frame);
SliceWindow window(sprite, slices, frame);
if (!window.show())
return;
{
const SliceWindow::Mods mods = window.modifiedFields();
ContextWriter writer(reader, 500);
Tx tx(writer.context(), "Slice Properties");
Slice* slice = const_cast<Slice*>(foundSlice);
std::string name = window.nameValue();
for (Slice* slice : slices.iterateAs<Slice>()) {
// Change name
if (mods & SliceWindow::kName) {
std::string name = window.nameValue();
if (slice->name() != name)
tx(new cmd::SetSliceName(slice, name));
}
if (slice->name() != name)
tx(new cmd::SetSliceName(slice, name));
// Change user data
if ((mods & SliceWindow::kUserData) &&
slice->userData() != window.userDataValue()) {
tx(new cmd::SetUserData(slice, window.userDataValue()));
}
if (slice->userData() != window.userDataValue())
tx(new cmd::SetUserData(slice, window.userDataValue()));
// Change slice properties
const doc::SliceKey* key = slice->getByFrame(frame);
if (!key)
continue;
if (key->bounds() != window.boundsValue() ||
key->center() != window.centerValue() ||
key->pivot() != window.pivotValue()) {
SliceKey newKey = *key;
newKey.setBounds(window.boundsValue());
newKey.setCenter(window.centerValue());
newKey.setPivot(window.pivotValue());
tx(new cmd::SetSliceKey(slice, frame, newKey));
gfx::Rect newBounds = newKey.bounds();
gfx::Rect newCenter = newKey.center();
gfx::Point newPivot = newKey.pivot();
if (mods & SliceWindow::kBoundsX) newBounds.x = window.boundsValue().x;
if (mods & SliceWindow::kBoundsY) newBounds.y = window.boundsValue().y;
if (mods & SliceWindow::kBoundsW) newBounds.w = window.boundsValue().w;
if (mods & SliceWindow::kBoundsH) newBounds.h = window.boundsValue().h;
if (mods & SliceWindow::kCenterX) newCenter.x = window.centerValue().x;
if (mods & SliceWindow::kCenterY) newCenter.y = window.centerValue().y;
if (mods & SliceWindow::kCenterW) newCenter.w = window.centerValue().w;
if (mods & SliceWindow::kCenterH) newCenter.h = window.centerValue().h;
if (mods & SliceWindow::kPivotX) newPivot.x = window.pivotValue().x;
if (mods & SliceWindow::kPivotY) newPivot.y = window.pivotValue().y;
newKey.setBounds(newBounds);
newKey.setCenter(newCenter);
newKey.setPivot(newPivot);
if (key->bounds() != newKey.bounds() ||
key->center() != newKey.center() ||
key->pivot() != newKey.pivot()) {
tx(new cmd::SetSliceKey(slice, frame, newKey));
}
}
tx.commit();

View File

@ -1,5 +1,6 @@
// Aseprite
// Copyright (c) 2001-2018 David Capello
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -17,6 +18,7 @@ namespace doc {
class Image;
class Layer;
class LayerImage;
class Slice;
class Sprite;
}
@ -35,6 +37,8 @@ namespace app {
, m_image(NULL)
, m_imageIndex(-1)
, m_frame(0)
, m_frameTag(nullptr)
, m_slice(nullptr)
, m_targetLayer(NULL)
, m_targetFrame(0) {
}
@ -48,6 +52,7 @@ namespace app {
int imageIndex() const { return m_imageIndex; }
frame_t frame() const { return m_frame; }
FrameTag* frameTag() const { return m_frameTag; }
Slice* slice() const { return m_slice; }
const gfx::Region& region() const { return m_region; }
void sprite(Sprite* sprite) { m_sprite = sprite; }
@ -57,6 +62,7 @@ namespace app {
void imageIndex(int imageIndex) { m_imageIndex = imageIndex; }
void frame(frame_t frame) { m_frame = frame; }
void frameTag(FrameTag* frameTag) { m_frameTag = frameTag; }
void slice(Slice* slice) { m_slice = slice; }
void region(const gfx::Region& rgn) { m_region = rgn; }
// Destination of the operation.
@ -75,6 +81,7 @@ namespace app {
int m_imageIndex;
frame_t m_frame;
FrameTag* m_frameTag;
Slice* m_slice;
gfx::Region m_region;
// For copy/move commands, the m_layer/m_frame are source of the

View File

@ -1,6 +1,6 @@
// Aseprite
// Copyright (C) 2018 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
// Copyright (C) 2018-2019 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -30,6 +30,7 @@ namespace app {
virtual void onAddLayer(DocEvent& ev) { }
virtual void onAddFrame(DocEvent& ev) { }
virtual void onAddCel(DocEvent& ev) { }
virtual void onAddSlice(DocEvent& ev) { }
virtual void onAddFrameTag(DocEvent& ev) { }
virtual void onBeforeRemoveLayer(DocEvent& ev) { }
@ -40,6 +41,7 @@ namespace app {
virtual void onRemoveFrame(DocEvent& ev) { }
virtual void onRemoveFrameTag(DocEvent& ev) { }
virtual void onRemoveCel(DocEvent& ev) { }
virtual void onRemoveSlice(DocEvent& ev) { }
virtual void onSpriteSizeChanged(DocEvent& ev) { }
virtual void onSpriteTransparentColorChanged(DocEvent& ev) { }

View File

@ -1,5 +1,6 @@
// Aseprite
// Copyright (c) 2001-2018 David Capello
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -11,6 +12,7 @@
#include "doc/frame.h"
#include "doc/selected_frames.h"
#include "doc/selected_layers.h"
#include "doc/selected_objects.h"
namespace doc {
class Cel;
@ -84,6 +86,12 @@ namespace app {
m_selectedFrames = selectedFrames;
}
const doc::SelectedObjects& selectedSlices() const { return m_selectedSlices; }
doc::SelectedObjects& selectedSlices() { return m_selectedSlices; }
void selectedSlices(const doc::SelectedObjects& set) {
m_selectedSlices = set;
}
doc::Palette* palette();
doc::Image* image(int* x = nullptr, int* y = nullptr, int* opacity = nullptr) const;
doc::Palette* palette() const;
@ -96,6 +104,7 @@ namespace app {
doc::frame_t m_frame;
doc::SelectedLayers m_selectedLayers;
doc::SelectedFrames m_selectedFrames;
doc::SelectedObjects m_selectedSlices;
};
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018 Igara Studio S.A.
// Copyright (C) 2018-2019 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -591,6 +591,9 @@ bool DocView::onClear(Context* ctx)
void DocView::onCancel(Context* ctx)
{
if (m_editor)
m_editor->cancelSelections();
// Deselect mask
if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
ContextFlags::HasVisibleMask)) {

View File

@ -380,6 +380,8 @@ void Editor::getSite(Site* site) const
site->sprite(m_sprite);
site->layer(m_layer);
site->frame(m_frame);
if (!m_selectedSlices.empty())
site->selectedSlices(m_selectedSlices);
}
Site Editor::getSite() const
@ -1003,6 +1005,7 @@ void Editor::drawSlices(ui::Graphics* g)
if (!isVisible() || !m_document)
return;
SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
gfx::Point mainOffset(mainTilePosition());
for (auto slice : m_sprite->slices()) {
@ -1053,7 +1056,15 @@ void Editor::drawSlices(ui::Graphics* g)
g->drawRect(in_color, in);
}
g->drawRect(color, out);
if (isSliceSelected(slice) &&
getCurrentEditorInk()->isSlice()) {
PaintWidgetPartInfo info;
theme->paintWidgetPart(
g, theme->styles.colorbarSelection(), out, info);
}
else {
g->drawRect(color, out);
}
}
}
@ -1610,6 +1621,42 @@ bool Editor::startStraightLineWithFreehandTool(const ui::MouseMessage* msg)
document()->lastDrawingPoint() != Doc::NoLastDrawingPoint());
}
bool Editor::isSliceSelected(const doc::Slice* slice) const
{
ASSERT(slice);
return m_selectedSlices.contains(slice->id());
}
void Editor::clearSlicesSelection()
{
m_selectedSlices.clear();
invalidate();
}
void Editor::selectSlice(const doc::Slice* slice)
{
ASSERT(slice);
m_selectedSlices.insert(slice->id());
invalidate();
}
bool Editor::selectSliceBox(const gfx::Rect& box)
{
m_selectedSlices.clear();
for (auto slice : m_sprite->slices()) {
auto key = slice->getByFrame(m_frame);
if (key && key->bounds().intersects(box))
m_selectedSlices.insert(slice->id());
}
invalidate();
return !m_selectedSlices.empty();
}
void Editor::cancelSelections()
{
m_selectedSlices.clear();
}
//////////////////////////////////////////////////////////////////////
// Message handler for the editor
@ -2040,6 +2087,15 @@ void Editor::onRemoveFrameTag(DocEvent& ev)
m_state->onRemoveFrameTag(this, ev.frameTag());
}
void Editor::onRemoveSlice(DocEvent& ev)
{
ASSERT(ev.slice());
if (ev.slice() &&
m_selectedSlices.contains(ev.slice()->id())) {
m_selectedSlices.erase(ev.slice()->id());
}
}
void Editor::setCursor(const gfx::Point& mouseScreenPos)
{
Rect vp = View::getView(this)->viewportBounds();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2018-2019 Igara Studio S.A.
// Copyright (C) 2018-2019 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -24,6 +24,7 @@
#include "doc/algorithm/flip_type.h"
#include "doc/frame.h"
#include "doc/image_buffer.h"
#include "doc/selected_objects.h"
#include "filters/tiled_mode.h"
#include "gfx/fwd.h"
#include "obs/connection.h"
@ -36,6 +37,8 @@
#include "ui/timer.h"
#include "ui/widget.h"
#include <set>
namespace doc {
class Layer;
class Sprite;
@ -275,6 +278,17 @@ namespace app {
// freehand tool is pressed.
bool startStraightLineWithFreehandTool(const ui::MouseMessage* msg);
// Functions to handle the set of selected slices.
bool isSliceSelected(const doc::Slice* slice) const;
void clearSlicesSelection();
void selectSlice(const doc::Slice* slice);
bool selectSliceBox(const gfx::Rect& box);
bool hasSelectedSlices() const { return !m_selectedSlices.empty(); }
// Called by DocView's InputChainElement::onCancel() impl when Esc
// key is pressed to cancel the active selection.
void cancelSelections();
static void registerCommands();
protected:
@ -297,6 +311,7 @@ namespace app {
void onRemoveCel(DocEvent& ev) override;
void onAddFrameTag(DocEvent& ev) override;
void onRemoveFrameTag(DocEvent& ev) override;
void onRemoveSlice(DocEvent& ev) override;
// ActiveToolObserver impl
void onActiveToolChange(tools::Tool* tool) override;
@ -422,6 +437,9 @@ namespace app {
gfx::Rect m_perfInfoBounds;
#endif
// For slices
doc::SelectedObjects m_selectedSlices;
// The render engine must be shared between all editors so when a
// DrawingState is being used in one editor, other editors for the
// same document can show the same preview image/stroke being drawn

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2017-2018 David Capello
//
// This program is distributed under the terms of
@ -19,22 +20,33 @@
#include "doc/slice.h"
#include "ui/message.h"
#include <algorithm>
#include <cmath>
namespace app {
using namespace ui;
MovingSliceState::MovingSliceState(Editor* editor, MouseMessage* msg,
const EditorHit& hit)
: m_hit(hit)
MovingSliceState::MovingSliceState(Editor* editor,
MouseMessage* msg,
const EditorHit& hit,
const doc::SelectedObjects& selectedSlices)
: m_frame(editor->frame())
, m_hit(hit)
, m_items(std::max<std::size_t>(1, selectedSlices.size()))
{
m_mouseStart = editor->screenToEditor(msg->position());
auto keyPtr = m_hit.slice()->getByFrame(editor->frame());
ASSERT(keyPtr);
if (keyPtr)
m_keyStart = m_key = *keyPtr;
if (selectedSlices.empty()) {
m_items[0] = getItemForSlice(m_hit.slice());
}
else {
int i = 0;
for (Slice* slice : selectedSlices.iterateAs<Slice>()) {
ASSERT(slice);
m_items[i++] = getItemForSlice(slice);
}
}
editor->captureMouse();
}
@ -45,12 +57,11 @@ bool MovingSliceState::onMouseUp(Editor* editor, MouseMessage* msg)
ContextWriter writer(UIContext::instance(), 1000);
Tx tx(writer.context(), "Slice Movement", ModifyDocument);
doc::SliceKey newKey = m_key;
m_hit.slice()->insert(editor->frame(), m_keyStart);
for (const auto& item : m_items) {
item.slice->insert(m_frame, item.oldKey);
tx(new cmd::SetSliceKey(item.slice, m_frame, item.newKey));
}
tx(new cmd::SetSliceKey(m_hit.slice(),
editor->frame(),
newKey));
tx.commit();
}
@ -63,54 +74,64 @@ bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
{
gfx::Point newCursorPos = editor->screenToEditor(msg->position());
gfx::Point delta = newCursorPos - m_mouseStart;
gfx::Rect totalBounds = selectedSlicesBounds();
m_key = m_keyStart;
gfx::Rect rc =
(m_hit.type() == EditorHit::SliceCenter ? m_key.center():
m_key.bounds());
ASSERT(totalBounds.w > 0);
ASSERT(totalBounds.h > 0);
// Move slice
if (m_hit.border() == (CENTER | MIDDLE)) {
rc.x += delta.x;
rc.y += delta.y;
}
else {
if (m_hit.border() & LEFT) {
for (auto& item : m_items) {
auto& key = item.newKey;
key = item.oldKey;
gfx::Rect rc =
(m_hit.type() == EditorHit::SliceCenter ? key.center():
key.bounds());
// Move slice
if (m_hit.border() == (CENTER | MIDDLE)) {
rc.x += delta.x;
rc.w -= delta.x;
if (rc.w < 1) {
rc.x += rc.w-1;
rc.w = 1;
}
}
if (m_hit.border() & TOP) {
rc.y += delta.y;
rc.h -= delta.y;
if (rc.h < 1) {
rc.y += rc.h-1;
rc.h = 1;
}
else {
if (m_hit.border() & LEFT) {
rc.x += delta.x * (totalBounds.x2() - rc.x) / totalBounds.w;
rc.w -= delta.x * rc.w / totalBounds.w;
if (rc.w < 1) {
rc.x += rc.w-1;
rc.w = 1;
}
}
if (m_hit.border() & TOP) {
rc.y += delta.y * (totalBounds.y2() - rc.y) / totalBounds.h;
rc.h -= delta.y * rc.h / totalBounds.h;
if (rc.h < 1) {
rc.y += rc.h-1;
rc.h = 1;
}
}
if (m_hit.border() & RIGHT) {
rc.x += delta.x * (rc.x - totalBounds.x) / totalBounds.w;
rc.w += delta.x * rc.w / totalBounds.w;
if (rc.w < 1)
rc.w = 1;
}
if (m_hit.border() & BOTTOM) {
rc.y += delta.y * (rc.y - totalBounds.y) / totalBounds.h;
rc.h += delta.y * rc.h / totalBounds.h;
if (rc.h < 1)
rc.h = 1;
}
}
if (m_hit.border() & RIGHT) {
rc.w += delta.x;
if (rc.w < 1)
rc.w = 1;
}
if (m_hit.border() & BOTTOM) {
rc.h += delta.y;
if (rc.h < 1)
rc.h = 1;
}
if (m_hit.type() == EditorHit::SliceCenter)
key.setCenter(rc);
else
key.setBounds(rc);
// Update the slice key
item.slice->insert(m_frame, key);
}
if (m_hit.type() == EditorHit::SliceCenter)
m_key.setCenter(rc);
else
m_key.setBounds(rc);
// Update the slice key
m_hit.slice()->insert(editor->frame(), m_key);
// Redraw the editor.
editor->invalidate();
@ -149,4 +170,25 @@ bool MovingSliceState::onSetCursor(Editor* editor, const gfx::Point& mouseScreen
return true;
}
MovingSliceState::Item MovingSliceState::getItemForSlice(doc::Slice* slice)
{
Item item;
item.slice = slice;
auto keyPtr = slice->getByFrame(m_frame);
ASSERT(keyPtr);
if (keyPtr)
item.oldKey = item.newKey = *keyPtr;
return item;
}
gfx::Rect MovingSliceState::selectedSlicesBounds() const
{
gfx::Rect bounds;
for (auto& item : m_items)
bounds |= item.oldKey.bounds();
return bounds;
}
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2017 David Capello
//
// This program is distributed under the terms of
@ -10,6 +11,8 @@
#include "app/ui/editor/editor_hit.h"
#include "app/ui/editor/standby_state.h"
#include "doc/frame.h"
#include "doc/selected_objects.h"
#include "doc/slice.h"
namespace app {
@ -17,8 +20,10 @@ namespace app {
class MovingSliceState : public StandbyState {
public:
MovingSliceState(Editor* editor, ui::MouseMessage* msg,
const EditorHit& hit);
MovingSliceState(Editor* editor,
ui::MouseMessage* msg,
const EditorHit& hit,
const doc::SelectedObjects& selectedSlices);
bool onMouseUp(Editor* editor, ui::MouseMessage* msg) override;
bool onMouseMove(Editor* editor, ui::MouseMessage* msg) override;
@ -27,10 +32,19 @@ namespace app {
bool requireBrushPreview() override { return false; }
private:
struct Item {
doc::Slice* slice;
doc::SliceKey oldKey;
doc::SliceKey newKey;
};
Item getItemForSlice(doc::Slice* slice);
gfx::Rect selectedSlicesBounds() const;
doc::frame_t m_frame;
EditorHit m_hit;
gfx::Point m_mouseStart;
doc::SliceKey m_keyStart;
doc::SliceKey m_key;
std::vector<Item> m_items;
};
} // namespace app

View File

@ -239,14 +239,28 @@ bool StandbyState::onMouseDown(Editor* editor, MouseMessage* msg)
case EditorHit::SliceBounds:
case EditorHit::SliceCenter:
if (msg->left()) {
MovingSliceState* newState = new MovingSliceState(editor, msg, hit);
// If we click outside all slices, we clear the selection of slices.
if (!hit.slice() || !site.selectedSlices().contains(hit.slice()->id())) {
editor->clearSlicesSelection();
site = Site();
editor->getSite(&site);
}
MovingSliceState* newState = new MovingSliceState(
editor, msg, hit, site.selectedSlices());
editor->setState(EditorStatePtr(newState));
}
else {
Menu* popupMenu = AppMenus::instance()->getSlicePopupMenu();
if (popupMenu) {
Params params;
params.set("id", base::convert_to<std::string>(hit.slice()->id()).c_str());
// When the editor doesn't have a set of selected slices,
// we set the specific clicked slice for the commands (in
// other case, those commands will get the selected set of
// slices from Site::selectedSlices() field).
if (!editor->hasSelectedSlices())
params.set("id", base::convert_to<std::string>(hit.slice()->id()).c_str());
AppMenuItem::setContextParams(params);
popupMenu->showPopup(msg->position());
AppMenuItem::setContextParams(Params());

View File

@ -582,19 +582,30 @@ public:
void onSliceRect(const gfx::Rect& bounds) override {
if (getMouseButton() == ToolLoop::Left) {
Slice* slice = new Slice;
SliceKey key(bounds);
slice->insert(getFrame(), key);
// Try to select slices, but if it returns false, it means that
// there are no slices in the box to be selected, so we show a
// popup menu to create a new one.
if (!m_editor->selectSliceBox(bounds) &&
(bounds.w > 1 || bounds.h > 1)) {
Slice* slice = new Slice;
SliceKey key(bounds);
slice->insert(getFrame(), key);
auto color = Preferences::instance().slices.defaultColor();
slice->userData().setColor(
doc::rgba(color.getRed(),
color.getGreen(),
color.getBlue(),
color.getAlpha()));
auto color = Preferences::instance().slices.defaultColor();
slice->userData().setColor(
doc::rgba(color.getRed(),
color.getGreen(),
color.getBlue(),
color.getAlpha()));
m_transaction.execute(new cmd::AddSlice(m_sprite, slice));
m_transaction.execute(new cmd::AddSlice(m_sprite, slice));
return;
}
}
// Cancel the operation (do not create a new transaction for this
// no-op, e.g. just change the set of selected slices).
m_canceled = true;
}
};

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2017 David Capello
//
// This program is distributed under the terms of
@ -21,45 +22,80 @@
namespace app {
SliceWindow::SliceWindow(const doc::Sprite* sprite,
const doc::Slice* slice,
const doc::frame_t frameNum)
: m_userData(slice->userData())
const doc::SelectedObjects& slices,
const doc::frame_t frame)
: m_mods(kNone)
{
name()->setText(slice->name());
const doc::SliceKey* key = slice->getByFrame(frameNum);
ASSERT(key);
if (!key)
return;
ASSERT(!slices.empty());
Slice* slice = slices.frontAs<Slice>();
m_userData = slice->userData();
userData()->Click.connect(base::Bind<void>(&SliceWindow::onPopupUserData, this));
boundsX()->setTextf("%d", key->bounds().x);
boundsY()->setTextf("%d", key->bounds().y);
boundsW()->setTextf("%d", key->bounds().w);
boundsH()->setTextf("%d", key->bounds().h);
if (slices.size() == 1) {
// If we are going to edit just one slice, we indicate like all
// fields were modified, so then the slice properties transaction
// is created comparing the window fields with the slice fields
// (and not with which field was modified).
m_mods = kAll;
center()->Click.connect(base::Bind<void>(&SliceWindow::onCenterChange, this));
pivot()->Click.connect(base::Bind<void>(&SliceWindow::onPivotChange, this));
name()->setText(slice->name());
if (key->hasCenter()) {
center()->setSelected(true);
centerX()->setTextf("%d", key->center().x);
centerY()->setTextf("%d", key->center().y);
centerW()->setTextf("%d", key->center().w);
centerH()->setTextf("%d", key->center().h);
const doc::SliceKey* key = slice->getByFrame(frame);
ASSERT(key);
if (!key)
return;
boundsX()->setTextf("%d", key->bounds().x);
boundsY()->setTextf("%d", key->bounds().y);
boundsW()->setTextf("%d", key->bounds().w);
boundsH()->setTextf("%d", key->bounds().h);
center()->Click.connect(base::Bind<void>(&SliceWindow::onCenterChange, this));
pivot()->Click.connect(base::Bind<void>(&SliceWindow::onPivotChange, this));
if (key->hasCenter()) {
center()->setSelected(true);
centerX()->setTextf("%d", key->center().x);
centerY()->setTextf("%d", key->center().y);
centerW()->setTextf("%d", key->center().w);
centerH()->setTextf("%d", key->center().h);
}
else {
onCenterChange();
}
if (key->hasPivot()) {
pivot()->setSelected(true);
pivotX()->setTextf("%d", key->pivot().x);
pivotY()->setTextf("%d", key->pivot().y);
}
else {
onPivotChange();
}
}
// Edit multiple slices
else {
onCenterChange();
}
ui::Entry* entries[] = {
name(),
boundsX(), boundsY(), boundsW(), boundsH(),
centerX(), centerY(), centerW(), centerH(),
pivotX(), pivotY() };
const Mods entryMods[] = {
kName,
kBoundsX, kBoundsY, kBoundsW, kBoundsH,
kCenterX, kCenterY, kCenterW, kCenterH,
kPivotX, kPivotY };
if (key->hasPivot()) {
pivot()->setSelected(true);
pivotX()->setTextf("%d", key->pivot().x);
pivotY()->setTextf("%d", key->pivot().y);
}
else {
onPivotChange();
for (int i=0; i<sizeof(entries)/sizeof(entries[0]); ++i) {
auto entry = entries[i];
Mods mod = entryMods[i];
entry->setSuffix("*");
entry->Change.connect(
[this, entry, mod]{
onModifyField(entry, mod);
});
}
}
}
@ -140,7 +176,16 @@ void SliceWindow::onPivotChange()
void SliceWindow::onPopupUserData()
{
show_user_data_popup(userData()->bounds(), m_userData);
if (show_user_data_popup(userData()->bounds(), m_userData))
m_mods = Mods(int(m_mods) | int(kUserData));
}
void SliceWindow::onModifyField(ui::Entry* entry,
const Mods mods)
{
if (entry)
entry->setSuffix(std::string());
m_mods = Mods(int(m_mods) | int(mods));
}
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2017 David Capello
//
// This program is distributed under the terms of
@ -10,6 +11,7 @@
#include "doc/anidir.h"
#include "doc/frame.h"
#include "doc/selected_objects.h"
#include "doc/user_data.h"
#include "slice_properties.xml.h"
@ -23,8 +25,25 @@ namespace app {
class SliceWindow : protected app::gen::SliceProperties {
public:
enum Mods {
kNone = 0x0000,
kName = 0x0001,
kBoundsX = 0x0002,
kBoundsY = 0x0004,
kBoundsW = 0x0008,
kBoundsH = 0x0010,
kCenterX = 0x0020,
kCenterY = 0x0040,
kCenterW = 0x0080,
kCenterH = 0x0100,
kPivotX = 0x0200,
kPivotY = 0x0400,
kUserData = 0x0800,
kAll = 0xffff,
};
SliceWindow(const doc::Sprite* sprite,
const doc::Slice* slice,
const doc::SelectedObjects& slices,
const doc::frame_t frame);
bool show();
@ -35,12 +54,21 @@ namespace app {
gfx::Point pivotValue() const;
const doc::UserData& userDataValue() { return m_userData; }
Mods modifiedFields() const { return m_mods; }
private:
void onCenterChange();
void onPivotChange();
void onPopupUserData();
void onModifyField(ui::Entry* entry, const Mods mods);
doc::UserData m_userData;
// Flags used to know what specific entry/checkbox was modified
// when we edit multiple-slices in the same property dialog. In
// this way we know what field modify of each slice in
// SlicePropertiesCommand::onExecute().
Mods m_mods;
};
}

107
src/doc/selected_objects.h Normal file
View File

@ -0,0 +1,107 @@
// Aseprite Document Library
// Copyright (C) 2019 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_SELECTED_OBJECTS_H_INCLUDED
#define DOC_SELECTED_OBJECTS_H_INCLUDED
#pragma once
#include "base/debug.h"
#include "doc/object.h"
#include "doc/object_id.h"
#include <set>
namespace doc {
// A set of selected objects (e.g. select set of slices)
class SelectedObjects {
public:
typedef std::set<ObjectId> Set;
typedef Set::iterator iterator;
typedef Set::const_iterator const_iterator;
template<typename T>
class IteratableAs {
public:
class IteratorAs {
public:
typedef T* value_type;
typedef std::ptrdiff_t difference_type;
typedef T** pointer;
typedef T*& reference;
typedef std::forward_iterator_tag iterator_category;
IteratorAs(const const_iterator& it) : m_it(it) { }
bool operator==(const IteratorAs& other) const {
return m_it == other.m_it;
}
bool operator!=(const IteratorAs& other) const {
return !operator==(other);
}
T* operator*() const {
return doc::get<T>(*m_it);
}
IteratorAs& operator++() {
++m_it;
return *this;
}
private:
const_iterator m_it;
};
typedef IteratorAs iterator;
iterator begin() const { return m_begin; }
iterator end() const { return m_end; }
IteratableAs(const SelectedObjects& objs)
: m_begin(objs.begin())
, m_end(objs.end()) { }
private:
IteratorAs m_begin, m_end;
};
iterator begin() { return m_set.begin(); }
iterator end() { return m_set.end(); }
const_iterator begin() const { return m_set.begin(); }
const_iterator end() const { return m_set.end(); }
bool empty() const { return m_set.empty(); }
size_t size() const { return m_set.size(); }
void clear() {
m_set.clear();
}
void insert(const ObjectId id) {
m_set.insert(id);
}
bool contains(const ObjectId id) const {
return (m_set.find(id) != end());
}
void erase(const ObjectId id) {
auto it = m_set.find(id);
if (it != end())
m_set.erase(it);
}
template<typename T>
T* frontAs() const {
ASSERT(!m_set.empty());
return doc::get<T>(*m_set.begin());
}
template<typename T>
IteratableAs<T> iterateAs() const { return IteratableAs<T>(*this); }
private:
Set m_set;
};
} // namespace doc
#endif // DOC_SELECTED_OBJECTS_H_INCLUDED

View File

@ -186,8 +186,10 @@ std::string Entry::selectedText() const
void Entry::setSuffix(const std::string& suffix)
{
m_suffix = suffix;
invalidate();
if (m_suffix != suffix) {
m_suffix = suffix;
invalidate();
}
}
void Entry::setTranslateDeadKeys(bool state)