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
38namespace app {
39
40using namespace app::skin;
41
42class UndoHistoryWindow : public app::gen::UndoHistory,
43 public ContextObserver,
44 public DocsObserver,
45 public DocUndoObserver {
46public:
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
307private:
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
462class UndoHistoryCommand : public Command {
463public:
464 UndoHistoryCommand();
465
466protected:
467 void onExecute(Context* ctx) override;
468};
469
470static UndoHistoryWindow* g_window = NULL;
471
472UndoHistoryCommand::UndoHistoryCommand()
473 : Command(CommandId::UndoHistory(), CmdUIOnlyFlag)
474{
475}
476
477void 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
488Command* CommandFactory::createUndoHistoryCommand()
489{
490 return new UndoHistoryCommand;
491}
492
493} // namespace app
494