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 | |
33 | namespace app { |
34 | |
35 | using namespace ui; |
36 | using 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 | |
43 | struct 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. |
56 | class CanvasSizeWindow : public app::gen::CanvasSize |
57 | , public SelectBoxDelegate |
58 | { |
59 | public: |
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 | |
109 | protected: |
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 | |
152 | private: |
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 | |
292 | class CanvasSizeCommand : public CommandWithNewParams<CanvasSizeParams> { |
293 | public: |
294 | CanvasSizeCommand(); |
295 | |
296 | protected: |
297 | bool onEnabled(Context* context) override; |
298 | void onExecute(Context* context) override; |
299 | }; |
300 | |
301 | CanvasSizeCommand::CanvasSizeCommand() |
302 | : CommandWithNewParams(CommandId::CanvasSize(), CmdRecordableFlag) |
303 | { |
304 | } |
305 | |
306 | bool CanvasSizeCommand::onEnabled(Context* context) |
307 | { |
308 | return context->checkFlags(ContextFlags::ActiveDocumentIsWritable | |
309 | ContextFlags::HasActiveSprite); |
310 | } |
311 | |
312 | void 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 | |
402 | Command* CommandFactory::createCanvasSizeCommand() |
403 | { |
404 | return new CanvasSizeCommand; |
405 | } |
406 | |
407 | } // namespace app |
408 | |