1// Aseprite
2// Copyright (C) 2019-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/commands/command.h"
13#include "app/commands/new_params.h"
14#include "app/context_access.h"
15#include "app/doc_api.h"
16#include "app/modules/editors.h"
17#include "app/modules/gui.h"
18#include "app/tx.h"
19#include "app/ui/button_set.h"
20#include "app/ui/color_bar.h"
21#include "app/ui/doc_view.h"
22#include "app/ui/editor/editor.h"
23#include "app/ui/editor/select_box_state.h"
24#include "app/ui/skin/skin_theme.h"
25#include "app/ui_context.h"
26#include "doc/image.h"
27#include "doc/mask.h"
28#include "doc/sprite.h"
29#include "ui/ui.h"
30
31#include "canvas_size.xml.h"
32
33namespace app {
34
35using namespace ui;
36using namespace app::skin;
37
38// Disable warning about usage of "this" in initializer list.
39#ifdef _MSC_VER
40#pragma warning(disable:4355)
41#endif
42
43struct CanvasSizeParams : public NewParams {
44 Param<bool> ui { this, true, "ui" };
45 Param<int> left { this, 0, "left" };
46 Param<int> right { this, 0, "right" };
47 Param<int> top { this, 0, "top" };
48 Param<int> bottom { this, 0, "bottom" };
49 Param<gfx::Rect> bounds { this, gfx::Rect(0, 0, 0, 0), "bounds" };
50 Param<bool> trimOutside { this, false, "trimOutside" };
51};
52
53#ifdef ENABLE_UI
54
55// Window used to show canvas parameters.
56class CanvasSizeWindow : public app::gen::CanvasSize
57 , public SelectBoxDelegate
58{
59public:
60 enum class Dir { NW, N, NE, W, C, E, SW, S, SE };
61
62 CanvasSizeWindow(const CanvasSizeParams& params,
63 const gfx::Rect& bounds)
64 : m_editor(current_editor)
65 , m_rect(bounds)
66 , m_selectBoxState(
67 new SelectBoxState(
68 this, m_rect,
69 SelectBoxState::Flags(
70 int(SelectBoxState::Flags::Rulers) |
71 int(SelectBoxState::Flags::DarkOutside)))) {
72 setWidth(m_rect.w);
73 setHeight(m_rect.h);
74 setLeft(0);
75 setRight(0);
76 setTop(0);
77 setBottom(0);
78 updateBorderFromRect();
79
80 width() ->Change.connect([this]{ onSizeChange(); });
81 height()->Change.connect([this]{ onSizeChange(); });
82 dir() ->ItemChange.connect([this]{ onDirChange(); });
83 left() ->Change.connect([this]{ onBorderChange(); });
84 right() ->Change.connect([this]{ onBorderChange(); });
85 top() ->Change.connect([this]{ onBorderChange(); });
86 bottom()->Change.connect([this]{ onBorderChange(); });
87
88 m_editor->setState(m_selectBoxState);
89
90 dir()->setSelectedItem((int)Dir::C);
91 trim()->setSelected(params.trimOutside());
92 updateIcons();
93 }
94
95 ~CanvasSizeWindow() {
96 m_editor->backToPreviousState();
97 }
98
99 bool pressedOk() { return closer() == ok(); }
100
101 int getWidth() { return width()->textInt(); }
102 int getHeight() { return height()->textInt(); }
103 int getLeft() { return left()->textInt(); }
104 int getRight() { return right()->textInt(); }
105 int getTop() { return top()->textInt(); }
106 int getBottom() { return bottom()->textInt(); }
107 bool getTrimOutside() { return trim()->isSelected(); }
108
109protected:
110
111 // SelectBoxDelegate impleentation
112 void onChangeRectangle(const gfx::Rect& rect) override {
113 m_rect = rect;
114
115 updateSizeFromRect();
116 updateBorderFromRect();
117 updateIcons();
118 }
119
120 std::string onGetContextBarHelp() override {
121 return "Select new canvas size";
122 }
123
124 void onSizeChange() {
125 updateBorderFromSize();
126 updateRectFromBorder();
127 updateEditorBoxFromRect();
128 }
129
130 void onDirChange() {
131 updateIcons();
132 updateBorderFromSize();
133 updateRectFromBorder();
134 updateEditorBoxFromRect();
135 }
136
137 void onBorderChange() {
138 updateIcons();
139 updateRectFromBorder();
140 updateSizeFromRect();
141 updateEditorBoxFromRect();
142 }
143
144 virtual void onBroadcastMouseMessage(const gfx::Point& screenPos,
145 WidgetsList& targets) override {
146 Window::onBroadcastMouseMessage(screenPos, targets);
147
148 // Add the editor as receptor of mouse events too.
149 targets.push_back(View::getView(m_editor));
150 }
151
152private:
153
154 void updateBorderFromSize() {
155 int w = getWidth() - m_editor->sprite()->width();
156 int h = getHeight() - m_editor->sprite()->height();
157 int l, r, t, b;
158 l = r = t = b = 0;
159
160 switch ((Dir)dir()->selectedItem()) {
161 case Dir::NW:
162 case Dir::W:
163 case Dir::SW:
164 r = w;
165 break;
166 case Dir::N:
167 case Dir::C:
168 case Dir::S:
169 l = r = w/2;
170 if (w & 1)
171 r += (w >= 0 ? 1: -1);
172 break;
173 case Dir::NE:
174 case Dir::E:
175 case Dir::SE:
176 l = w;
177 break;
178 }
179
180 switch ((Dir)dir()->selectedItem()) {
181 case Dir::NW:
182 case Dir::N:
183 case Dir::NE:
184 b = h;
185 break;
186 case Dir::W:
187 case Dir::C:
188 case Dir::E:
189 b = t = h/2;
190 if (h & 1)
191 t += (h >= 0 ? 1: -1);
192 break;
193 case Dir::SW:
194 case Dir::S:
195 case Dir::SE:
196 t = h;
197 break;
198 }
199
200 setLeft(l);
201 setRight(r);
202 setTop(t);
203 setBottom(b);
204 }
205
206 void updateRectFromBorder() {
207 int left = getLeft();
208 int top = getTop();
209
210 m_rect = gfx::Rect(-left, -top,
211 m_editor->sprite()->width() + left + getRight(),
212 m_editor->sprite()->height() + top + getBottom());
213 }
214
215 void updateSizeFromRect() {
216 setWidth(m_rect.w);
217 setHeight(m_rect.h);
218 }
219
220 void updateBorderFromRect() {
221 setLeft(-m_rect.x);
222 setTop(-m_rect.y);
223 setRight((m_rect.x + m_rect.w) - current_editor->sprite()->width());
224 setBottom((m_rect.y + m_rect.h) - current_editor->sprite()->height());
225 }
226
227 void updateEditorBoxFromRect() {
228 static_cast<SelectBoxState*>(m_selectBoxState.get())->setBoxBounds(m_rect);
229
230 // Redraw new rulers position
231 m_editor->invalidate();
232 }
233
234 void updateIcons() {
235 auto theme = SkinTheme::get(this);
236
237 int sel = dir()->selectedItem();
238
239 int c = 0;
240 for (int v=0; v<3; ++v) {
241 for (int u=0; u<3; ++u) {
242 SkinPartPtr icon = theme->parts.canvasEmpty();
243
244 if (c == sel) {
245 icon = theme->parts.canvasC();
246 }
247 else if (u+1 < 3 && (u+1)+3*v == sel) {
248 icon = theme->parts.canvasW();
249 }
250 else if (u-1 >= 0 && (u-1)+3*v == sel) {
251 icon = theme->parts.canvasE();
252 }
253 else if (v+1 < 3 && u+3*(v+1) == sel) {
254 icon = theme->parts.canvasN();
255 }
256 else if (v-1 >= 0 && u+3*(v-1) == sel) {
257 icon = theme->parts.canvasS();
258 }
259 else if (u+1 < 3 && v+1 < 3 && (u+1)+3*(v+1) == sel) {
260 icon = theme->parts.canvasNw();
261 }
262 else if (u-1 >= 0 && v+1 < 3 && (u-1)+3*(v+1) == sel) {
263 icon = theme->parts.canvasNe();
264 }
265 else if (u+1 < 3 && v-1 >= 0 && (u+1)+3*(v-1) == sel) {
266 icon = theme->parts.canvasSw();
267 }
268 else if (u-1 >= 0 && v-1 >= 0 && (u-1)+3*(v-1) == sel) {
269 icon = theme->parts.canvasSe();
270 }
271
272 dir()->getItem(c)->setIcon(icon);
273 ++c;
274 }
275 }
276 }
277
278 void setWidth(int v) { width()->setTextf("%d", v); }
279 void setHeight(int v) { height()->setTextf("%d", v); }
280 void setLeft(int v) { left()->setTextf("%d", v); }
281 void setRight(int v) { right()->setTextf("%d", v); }
282 void setTop(int v) { top()->setTextf("%d", v); }
283 void setBottom(int v) { bottom()->setTextf("%d", v); }
284
285 Editor* m_editor;
286 gfx::Rect m_rect;
287 EditorStatePtr m_selectBoxState;
288};
289
290#endif // ENABLE_UI
291
292class CanvasSizeCommand : public CommandWithNewParams<CanvasSizeParams> {
293public:
294 CanvasSizeCommand();
295
296protected:
297 bool onEnabled(Context* context) override;
298 void onExecute(Context* context) override;
299};
300
301CanvasSizeCommand::CanvasSizeCommand()
302 : CommandWithNewParams(CommandId::CanvasSize(), CmdRecordableFlag)
303{
304}
305
306bool CanvasSizeCommand::onEnabled(Context* context)
307{
308 return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
309 ContextFlags::HasActiveSprite);
310}
311
312void CanvasSizeCommand::onExecute(Context* context)
313{
314#ifdef ENABLE_UI
315 const bool ui = (params().ui() && context->isUIAvailable());
316#endif
317 const ContextReader reader(context);
318 const Sprite* sprite(reader.sprite());
319 auto& params = this->params();
320
321 gfx::Rect bounds(0, 0, sprite->width(), sprite->height());
322 if (params.bounds.isSet()) {
323 bounds = params.bounds();
324 }
325 else {
326 bounds.enlarge(
327 gfx::Border(params.left(), params.top(),
328 params.right(), params.bottom()));
329 }
330
331#ifdef ENABLE_UI
332 if (ui) {
333 if (!params.trimOutside.isSet()) {
334 params.trimOutside(Preferences::instance().canvasSize.trimOutside());
335 }
336
337 // load the window widget
338 std::unique_ptr<CanvasSizeWindow> window(new CanvasSizeWindow(params, bounds));
339
340 window->remapWindow();
341
342 // Find best position for the window on the editor
343 if (DocView* docView = static_cast<UIContext*>(context)->activeView()) {
344 Display* display = ui::Manager::getDefault()->display();
345 ui::fit_bounds(display,
346 window.get(),
347 gfx::Rect(docView->bounds().x2() - window->bounds().w,
348 docView->bounds().y,
349 window->bounds().w,
350 window->bounds().h));
351 }
352 else {
353 window->centerWindow();
354 }
355
356 load_window_pos(window.get(), "CanvasSize");
357 window->setVisible(true);
358 window->openWindowInForeground();
359 save_window_pos(window.get(), "CanvasSize");
360
361 if (!window->pressedOk())
362 return;
363
364 params.left(window->getLeft());
365 params.right(window->getRight());
366 params.top(window->getTop());
367 params.bottom(window->getBottom());
368 params.trimOutside(window->getTrimOutside());
369
370 Preferences::instance().canvasSize.trimOutside(params.trimOutside());
371
372 if (params.bounds.isSet()) {
373 bounds = params.bounds();
374 }
375 else {
376 bounds.enlarge(
377 gfx::Border(params.left(), params.top(),
378 params.right(), params.bottom()));
379 }
380 }
381#endif
382
383 if (bounds.w < 1) bounds.w = 1;
384 if (bounds.h < 1) bounds.h = 1;
385
386 {
387 ContextWriter writer(reader);
388 Doc* doc = writer.document();
389 Sprite* sprite = writer.sprite();
390 Tx tx(writer.context(), "Canvas Size");
391 DocApi api = doc->getApi(tx);
392 api.cropSprite(sprite, bounds, params.trimOutside());
393 tx.commit();
394
395#ifdef ENABLE_UI
396 if (context->isUIAvailable())
397 update_screen_for_document(doc);
398#endif
399 }
400}
401
402Command* CommandFactory::createCanvasSizeCommand()
403{
404 return new CanvasSizeCommand;
405}
406
407} // namespace app
408