1 | // Aseprite |
2 | // Copyright (C) 2020-2022 Igara Studio S.A. |
3 | // Copyright (C) 2015-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/cmd.h" |
13 | #include "app/commands/command.h" |
14 | #include "app/console.h" |
15 | #include "app/context.h" |
16 | #include "app/context_observer.h" |
17 | #include "app/doc.h" |
18 | #include "app/doc_access.h" |
19 | #include "app/doc_undo.h" |
20 | #include "app/doc_undo_observer.h" |
21 | #include "app/docs_observer.h" |
22 | #include "app/modules/gui.h" |
23 | #include "app/modules/palettes.h" |
24 | #include "app/site.h" |
25 | #include "app/ui/skin/skin_theme.h" |
26 | #include "base/mem_utils.h" |
27 | #include "fmt/format.h" |
28 | #include "ui/init_theme_event.h" |
29 | #include "ui/listitem.h" |
30 | #include "ui/message.h" |
31 | #include "ui/paint_event.h" |
32 | #include "ui/size_hint_event.h" |
33 | #include "ui/view.h" |
34 | #include "undo/undo_state.h" |
35 | |
36 | #include "undo_history.xml.h" |
37 | |
38 | namespace app { |
39 | |
40 | using namespace app::skin; |
41 | |
42 | class UndoHistoryWindow : public app::gen::UndoHistory, |
43 | public ContextObserver, |
44 | public DocsObserver, |
45 | public DocUndoObserver { |
46 | public: |
47 | class ActionsList final : public ui::Widget { |
48 | public: |
49 | ActionsList(UndoHistoryWindow* window) |
50 | : m_window(window) { |
51 | setFocusStop(true); |
52 | initTheme(); |
53 | } |
54 | |
55 | void setUndoHistory(DocUndo* history) { |
56 | m_undoHistory = history; |
57 | invalidate(); |
58 | } |
59 | |
60 | void selectState(const undo::UndoState* state) { |
61 | auto view = ui::View::getView(this); |
62 | if (!view) |
63 | return; |
64 | |
65 | invalidate(); |
66 | |
67 | gfx::Point scroll = view->viewScroll(); |
68 | if (state) { |
69 | const gfx::Rect vp = view->viewportBounds(); |
70 | const gfx::Rect bounds = this->bounds(); |
71 | |
72 | gfx::Rect itemBounds(bounds.x, bounds.y, bounds.w, m_itemHeight); |
73 | |
74 | // Jump first "Initial State" |
75 | itemBounds.y += itemBounds.h; |
76 | |
77 | const undo::UndoState* s = m_undoHistory->firstState(); |
78 | while (s) { |
79 | if (s == state) |
80 | break; |
81 | itemBounds.y += itemBounds.h; |
82 | s = s->next(); |
83 | } |
84 | |
85 | if (itemBounds.y < vp.y) |
86 | scroll.y = itemBounds.y - bounds.y; |
87 | else if (itemBounds.y > vp.y + vp.h - itemBounds.h) |
88 | scroll.y = (itemBounds.y - bounds.y - vp.h + itemBounds.h); |
89 | } |
90 | else { |
91 | scroll = gfx::Point(0, 0); |
92 | } |
93 | |
94 | view->setViewScroll(scroll); |
95 | } |
96 | |
97 | obs::signal<void(const undo::UndoState*)> Change; |
98 | |
99 | protected: |
100 | void onInitTheme(ui::InitThemeEvent& ev) override { |
101 | Widget::onInitTheme(ev); |
102 | auto theme = SkinTheme::get(this); |
103 | m_itemHeight = |
104 | textHeight() + |
105 | theme->calcBorder(this, theme->styles.listItem()).height(); |
106 | } |
107 | |
108 | bool onProcessMessage(ui::Message* msg) override { |
109 | switch (msg->type()) { |
110 | |
111 | case ui::kMouseDownMessage: |
112 | captureMouse(); |
113 | [[fallthrough]]; |
114 | |
115 | case ui::kMouseMoveMessage: |
116 | if (hasCapture() && m_undoHistory) { |
117 | auto mouseMsg = static_cast<ui::MouseMessage*>(msg); |
118 | const gfx::Rect bounds = this->bounds(); |
119 | |
120 | // Mouse position in client coordinates |
121 | const gfx::Point mousePos = mouseMsg->position(); |
122 | gfx::Rect itemBounds(bounds.x, bounds.y, bounds.w, m_itemHeight); |
123 | |
124 | // First state |
125 | if (itemBounds.contains(mousePos)) { |
126 | Change(nullptr); |
127 | break; |
128 | } |
129 | itemBounds.y += itemBounds.h; |
130 | |
131 | const undo::UndoState* state = m_undoHistory->firstState(); |
132 | while (state) { |
133 | if (itemBounds.contains(mousePos)) { |
134 | Change(state); |
135 | break; |
136 | } |
137 | itemBounds.y += itemBounds.h; |
138 | state = state->next(); |
139 | } |
140 | } |
141 | break; |
142 | |
143 | case ui::kMouseUpMessage: |
144 | releaseMouse(); |
145 | break; |
146 | |
147 | case ui::kMouseWheelMessage: { |
148 | auto view = ui::View::getView(this); |
149 | if (view) { |
150 | auto mouseMsg = static_cast<ui::MouseMessage*>(msg); |
151 | gfx::Point scroll = view->viewScroll(); |
152 | |
153 | if (mouseMsg->preciseWheel()) |
154 | scroll += mouseMsg->wheelDelta(); |
155 | else |
156 | scroll += mouseMsg->wheelDelta() * 3*(m_itemHeight+4*ui::guiscale()); |
157 | |
158 | view->setViewScroll(scroll); |
159 | } |
160 | break; |
161 | } |
162 | |
163 | case ui::kKeyDownMessage: |
164 | if (hasFocus() && m_undoHistory) { |
165 | const undo::UndoState* current = m_undoHistory->currentState(); |
166 | const undo::UndoState* select = current; |
167 | auto view = ui::View::getView(this); |
168 | const gfx::Rect vp = view->viewportBounds(); |
169 | ui::KeyMessage* keymsg = static_cast<ui::KeyMessage*>(msg); |
170 | ui::KeyScancode scancode = keymsg->scancode(); |
171 | |
172 | if (keymsg->onlyCmdPressed()) { |
173 | if (scancode == ui::kKeyUp) scancode = ui::kKeyHome; |
174 | if (scancode == ui::kKeyDown) scancode = ui::kKeyEnd; |
175 | } |
176 | |
177 | switch (scancode) { |
178 | case ui::kKeyUp: |
179 | if (select) |
180 | select = select->prev(); |
181 | else |
182 | select = m_undoHistory->lastState(); |
183 | break; |
184 | case ui::kKeyDown: |
185 | if (select) |
186 | select = select->next(); |
187 | else |
188 | select = m_undoHistory->firstState(); |
189 | break; |
190 | case ui::kKeyHome: |
191 | select = nullptr; |
192 | break; |
193 | case ui::kKeyEnd: |
194 | select = m_undoHistory->lastState(); |
195 | break; |
196 | case ui::kKeyPageUp: |
197 | for (int i=0; select && i<vp.h / m_itemHeight; ++i) |
198 | select = select->prev(); |
199 | break; |
200 | case ui::kKeyPageDown: { |
201 | int i = 0; |
202 | if (!select) { |
203 | select = m_undoHistory->firstState(); |
204 | i = 1; |
205 | } |
206 | for (; select && i<vp.h / m_itemHeight; ++i) |
207 | select = select->next(); |
208 | break; |
209 | } |
210 | default: |
211 | return Widget::onProcessMessage(msg); |
212 | } |
213 | |
214 | if (select != current) |
215 | Change(select); |
216 | return true; |
217 | } |
218 | break; |
219 | |
220 | } |
221 | return Widget::onProcessMessage(msg); |
222 | } |
223 | |
224 | void onPaint(ui::PaintEvent& ev) override { |
225 | ui::Graphics* g = ev.graphics(); |
226 | auto theme = SkinTheme::get(this); |
227 | gfx::Rect bounds = clientBounds(); |
228 | |
229 | g->fillRect(theme->colors.background(), bounds); |
230 | |
231 | if (!m_undoHistory) |
232 | return; |
233 | |
234 | const undo::UndoState* currentState = m_undoHistory->currentState(); |
235 | gfx::Rect itemBounds(bounds.x, bounds.y, bounds.w, m_itemHeight); |
236 | |
237 | // First state |
238 | { |
239 | const bool selected = (currentState == nullptr); |
240 | paintItem(g, theme, nullptr, itemBounds, selected); |
241 | itemBounds.y += itemBounds.h; |
242 | } |
243 | |
244 | const undo::UndoState* state = m_undoHistory->firstState(); |
245 | while (state) { |
246 | const bool selected = (state == currentState); |
247 | paintItem(g, theme, state, itemBounds, selected); |
248 | itemBounds.y += itemBounds.h; |
249 | state = state->next(); |
250 | } |
251 | } |
252 | |
253 | void onSizeHint(ui::SizeHintEvent& ev) override { |
254 | if (m_window->m_nitems == 0) { |
255 | int size = 0; |
256 | if (m_undoHistory) { |
257 | ++size; |
258 | const undo::UndoState* state = m_undoHistory->firstState(); |
259 | while (state) { |
260 | ++size; |
261 | state = state->next(); |
262 | } |
263 | } |
264 | m_window->m_nitems = size; |
265 | } |
266 | ev.setSizeHint(gfx::Size(1, m_itemHeight * m_window->m_nitems)); |
267 | } |
268 | |
269 | private: |
270 | void paintItem(ui::Graphics* g, |
271 | SkinTheme* theme, |
272 | const undo::UndoState* state, |
273 | const gfx::Rect& itemBounds, |
274 | const bool selected) { |
275 | const std::string itemText = |
276 | (state ? static_cast<Cmd*>(state->cmd())->label() |
277 | #if _DEBUG |
278 | + std::string(" " ) + base::get_pretty_memory_size(static_cast<Cmd*>(state->cmd())->memSize()) |
279 | #endif |
280 | : std::string("Initial State" )); |
281 | |
282 | if ((g->getClipBounds() & itemBounds).isEmpty()) |
283 | return; |
284 | |
285 | auto style = theme->styles.listItem(); |
286 | |
287 | ui::PaintWidgetPartInfo info; |
288 | info.text = &itemText; |
289 | info.styleFlags = (selected ? ui::Style::Layer::kSelected: 0); |
290 | theme->paintWidgetPart(g, style, itemBounds, info); |
291 | } |
292 | |
293 | UndoHistoryWindow* m_window; |
294 | DocUndo* m_undoHistory = nullptr; |
295 | int m_itemHeight; |
296 | }; |
297 | |
298 | UndoHistoryWindow(Context* ctx) |
299 | : m_ctx(ctx) |
300 | , m_doc(nullptr) |
301 | , m_actions(this) { |
302 | m_title = text(); |
303 | m_actions.Change.connect(&UndoHistoryWindow::onChangeAction, this); |
304 | view()->attachToView(&m_actions); |
305 | } |
306 | |
307 | private: |
308 | bool onProcessMessage(ui::Message* msg) override { |
309 | switch (msg->type()) { |
310 | |
311 | case ui::kOpenMessage: |
312 | load_window_pos(this, "UndoHistory" ); |
313 | |
314 | m_ctx->add_observer(this); |
315 | m_ctx->documents().add_observer(this); |
316 | if (m_ctx->activeDocument()) { |
317 | m_frame = m_ctx->activeSite().frame(); |
318 | |
319 | attachDocument(m_ctx->activeDocument()); |
320 | } |
321 | |
322 | view()->invalidate(); |
323 | break; |
324 | |
325 | case ui::kCloseMessage: |
326 | save_window_pos(this, "UndoHistory" ); |
327 | |
328 | if (m_doc) |
329 | detachDocument(); |
330 | m_ctx->documents().remove_observer(this); |
331 | m_ctx->remove_observer(this); |
332 | break; |
333 | } |
334 | return app::gen::UndoHistory::onProcessMessage(msg); |
335 | } |
336 | |
337 | void onChangeAction(const undo::UndoState* state) { |
338 | if (m_doc && m_doc->undoHistory()->currentState() != state) { |
339 | try { |
340 | DocWriter writer(m_doc, 100); |
341 | m_doc->undoHistory()->moveToState(state); |
342 | m_doc->generateMaskBoundaries(); |
343 | |
344 | // TODO this should be an observer of the current document palette |
345 | set_current_palette(m_doc->sprite()->palette(m_frame), false); |
346 | |
347 | m_doc->notifyGeneralUpdate(); |
348 | m_actions.invalidate(); |
349 | } |
350 | catch (const std::exception& ex) { |
351 | selectState(m_doc->undoHistory()->currentState()); |
352 | Console::showException(ex); |
353 | } |
354 | } |
355 | } |
356 | |
357 | // ContextObserver |
358 | void onActiveSiteChange(const Site& site) override { |
359 | m_frame = site.frame(); |
360 | |
361 | if (m_doc == site.document()) |
362 | return; |
363 | |
364 | attachDocument(const_cast<Doc*>(site.document())); |
365 | } |
366 | |
367 | // DocsObserver |
368 | void onRemoveDocument(Doc* doc) override { |
369 | if (m_doc && m_doc == doc) |
370 | detachDocument(); |
371 | } |
372 | |
373 | // DocUndoObserver |
374 | void onAddUndoState(DocUndo* history) override { |
375 | ASSERT(history->currentState()); |
376 | |
377 | ++m_nitems; |
378 | |
379 | m_actions.invalidate(); |
380 | view()->updateView(); |
381 | |
382 | selectState(history->currentState()); |
383 | } |
384 | |
385 | void onDeleteUndoState(DocUndo* history, |
386 | undo::UndoState* state) override { |
387 | --m_nitems; |
388 | } |
389 | |
390 | void onCurrentUndoStateChange(DocUndo* history) override { |
391 | selectState(history->currentState()); |
392 | } |
393 | |
394 | void onClearRedo(DocUndo* history) override { |
395 | setUndoHistory(history); |
396 | } |
397 | |
398 | void onTotalUndoSizeChange(DocUndo* history) override { |
399 | updateTitle(); |
400 | } |
401 | |
402 | void attachDocument(Doc* doc) { |
403 | if (m_doc == doc) |
404 | return; |
405 | |
406 | detachDocument(); |
407 | m_doc = doc; |
408 | if (!doc) |
409 | return; |
410 | |
411 | DocUndo* history = m_doc->undoHistory(); |
412 | history->add_observer(this); |
413 | |
414 | setUndoHistory(history); |
415 | updateTitle(); |
416 | } |
417 | |
418 | void detachDocument() { |
419 | if (!m_doc) |
420 | return; |
421 | |
422 | m_doc->undoHistory()->remove_observer(this); |
423 | m_doc = nullptr; |
424 | |
425 | setUndoHistory(nullptr); |
426 | updateTitle(); |
427 | } |
428 | |
429 | void setUndoHistory(DocUndo* history) { |
430 | m_nitems = 0; |
431 | m_actions.setUndoHistory(history); |
432 | view()->updateView(); |
433 | |
434 | if (history) |
435 | m_actions.selectState(history->currentState()); |
436 | } |
437 | |
438 | void selectState(const undo::UndoState* state) { |
439 | m_actions.selectState(state); |
440 | } |
441 | |
442 | void updateTitle() { |
443 | if (!m_doc) |
444 | setText(m_title); |
445 | else { |
446 | setText( |
447 | fmt::format( |
448 | "{} ({})" , |
449 | m_title, |
450 | base::get_pretty_memory_size(m_doc->undoHistory()->totalUndoSize()))); |
451 | } |
452 | } |
453 | |
454 | Context* m_ctx; |
455 | Doc* m_doc; |
456 | doc::frame_t m_frame; |
457 | std::string m_title; |
458 | ActionsList m_actions; |
459 | int m_nitems = 0; |
460 | }; |
461 | |
462 | class UndoHistoryCommand : public Command { |
463 | public: |
464 | UndoHistoryCommand(); |
465 | |
466 | protected: |
467 | void onExecute(Context* ctx) override; |
468 | }; |
469 | |
470 | static UndoHistoryWindow* g_window = NULL; |
471 | |
472 | UndoHistoryCommand::UndoHistoryCommand() |
473 | : Command(CommandId::UndoHistory(), CmdUIOnlyFlag) |
474 | { |
475 | } |
476 | |
477 | void UndoHistoryCommand::onExecute(Context* ctx) |
478 | { |
479 | if (!g_window) |
480 | g_window = new UndoHistoryWindow(ctx); |
481 | |
482 | if (g_window->isVisible()) |
483 | g_window->closeWindow(nullptr); |
484 | else |
485 | g_window->openWindow(); |
486 | } |
487 | |
488 | Command* CommandFactory::createUndoHistoryCommand() |
489 | { |
490 | return new UndoHistoryCommand; |
491 | } |
492 | |
493 | } // namespace app |
494 | |