1// Aseprite
2// Copyright (C) 2019-2022 Igara Studio S.A.
3// Copyright (C) 2001-2017 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/ui/data_recovery_view.h"
13
14#include "app/app.h"
15#include "app/app_menus.h"
16#include "app/console.h"
17#include "app/crash/data_recovery.h"
18#include "app/crash/session.h"
19#include "app/doc.h"
20#include "app/i18n/strings.h"
21#include "app/modules/gui.h"
22#include "app/pref/preferences.h"
23#include "app/task.h"
24#include "app/ui/data_recovery_view.h"
25#include "app/ui/drop_down_button.h"
26#include "app/ui/separator_in_view.h"
27#include "app/ui/skin/skin_theme.h"
28#include "app/ui/task_widget.h"
29#include "app/ui/workspace.h"
30#include "app/ui/workspace.h"
31#include "app/ui_context.h"
32#include "base/fs.h"
33#include "fmt/format.h"
34#include "ui/alert.h"
35#include "ui/button.h"
36#include "ui/entry.h"
37#include "ui/label.h"
38#include "ui/listitem.h"
39#include "ui/message.h"
40#include "ui/resize_event.h"
41#include "ui/separator.h"
42#include "ui/size_hint_event.h"
43#include "ui/system.h"
44#include "ui/view.h"
45#include "ver/info.h"
46
47#include <algorithm>
48
49namespace app {
50
51using namespace ui;
52using namespace app::skin;
53
54namespace {
55
56class Item : public ListItem {
57public:
58 Item(crash::Session* session, const crash::Session::BackupPtr& backup)
59 : m_session(session)
60 , m_backup(backup)
61 , m_task(nullptr) {
62 updateText();
63 }
64
65 crash::Session* session() const { return m_session; }
66 const crash::Session::BackupPtr& backup() const { return m_backup; }
67
68 bool isTaskRunning() const { return m_task != nullptr; }
69
70 void restoreBackup() {
71 if (m_task)
72 return;
73 m_task = new TaskWidget(
74 [this](base::task_token& t) {
75 // Warning: This is executed from a worker thread
76 Doc* doc = m_session->restoreBackupDoc(m_backup, &t);
77 ui::execute_from_ui_thread(
78 [this, doc]{
79 onOpenDoc(doc);
80 onDeleteTaskWidget();
81 });
82 });
83 addChild(m_task);
84 parent()->layout();
85 }
86
87 void restoreRawImages(crash::RawImagesAs as) {
88 if (m_task)
89 return;
90 m_task = new TaskWidget(
91 [this, as](base::task_token& t){
92 // Warning: This is executed from a worker thread
93 Doc* doc = m_session->restoreBackupRawImages(m_backup, as, &t);
94 ui::execute_from_ui_thread(
95 [this, doc]{
96 onOpenDoc(doc);
97 onDeleteTaskWidget();
98 });
99 });
100 addChild(m_task);
101 updateView();
102 }
103
104 void deleteBackup() {
105 if (m_task)
106 return;
107
108 m_task = new TaskWidget(
109 TaskWidget::kCannotCancel,
110 [this](base::task_token& t) {
111 try {
112 // Warning: This is executed from a worker thread
113 m_session->deleteBackup(m_backup);
114 m_backup.reset(); // Delete the Backup instance
115
116 ui::execute_from_ui_thread(
117 [this]{
118 onDeleteTaskWidget();
119
120 // We cannot use this->deferDelete() here because it looks
121 // like the m_task field can be still in use.
122 setVisible(false);
123
124 updateView();
125 });
126 }
127 catch (const std::exception& ex) {
128 std::string err = ex.what();
129 if (!err.empty()) {
130 ui::execute_from_ui_thread(
131 [err]{
132 Console().printf("Error deleting file: %s", err.c_str());
133 });
134 }
135 }
136 });
137 addChild(m_task);
138 updateView();
139 }
140
141 void updateText() {
142 if (!m_task) {
143 ASSERT(m_backup);
144 if (!m_backup)
145 return;
146
147 setText(
148 m_backup->description(
149 Preferences::instance().general.showFullPath()));
150 }
151 }
152
153private:
154 void onSizeHint(SizeHintEvent& ev) override {
155 ListItem::onSizeHint(ev);
156 gfx::Size sz = ev.sizeHint();
157 sz.h += 4*guiscale();
158 ev.setSizeHint(sz);
159 }
160
161 void onResize(ResizeEvent& ev) override {
162 setBoundsQuietly(ev.bounds());
163
164 if (m_task) {
165 gfx::Size sz = m_task->sizeHint();
166 gfx::Rect cpos = childrenBounds();
167
168 int x2;
169 if (auto view = View::getView(this->parent()))
170 x2 = view->viewportBounds().x2();
171 else
172 x2 = cpos.x2();
173
174 cpos.x = x2 - sz.w;
175 cpos.w = sz.w;
176 m_task->setBounds(cpos);
177 }
178 }
179
180 void onOpenDoc(Doc* doc) {
181 if (!doc)
182 return;
183
184 Workspace* workspace = App::instance()->workspace();
185 WorkspaceView* restoreView = workspace->activeView();
186
187 // If we have this specific item selected, and the active view in
188 // the workspace is the DataRecoveryView, we can open the
189 // recovered document. In other case, opening the recovered
190 // document is confusing (because the user already selected other
191 // item, or other tab from the workspace).
192 if (dynamic_cast<DataRecoveryView*>(restoreView) &&
193 isSelected()) {
194 restoreView = nullptr;
195 }
196
197 // Check if the original path of the recovered document exists, in
198 // other case remove the path so we use the default one (last path
199 // used, or user docs folder, etc.)
200 {
201 std::string dir = base::get_file_path(doc->filename());
202 if (!base::is_directory(dir))
203 doc->setFilename(base::get_file_name(doc->filename()));
204 }
205
206 UIContext::instance()->documents().add(doc);
207
208 if (restoreView)
209 workspace->setActiveView(restoreView);
210 }
211
212 void onDeleteTaskWidget() {
213 if (m_task) {
214 removeChild(m_task);
215 m_task->deferDelete();
216 m_task = nullptr;
217 layout();
218 }
219 }
220
221 void updateView() {
222 if (auto view = View::getView(this->parent()))
223 view->updateView();
224 else
225 parent()->layout();
226 }
227
228 crash::Session* m_session;
229 crash::Session::BackupPtr m_backup;
230 TaskWidget* m_task;
231};
232
233} // anonymous namespace
234
235DataRecoveryView::DataRecoveryView(crash::DataRecovery* dataRecovery)
236 : m_dataRecovery(dataRecovery)
237 , m_openButton(Strings::recover_files_recover_sprite().c_str())
238 , m_deleteButton(Strings::recover_files_delete())
239 , m_refreshButton(Strings::recover_files_refresh())
240 , m_waitToEnableRefreshTimer(100)
241{
242 m_listBox.setMultiselect(true);
243 m_view.setExpansive(true);
244 m_view.attachToView(&m_listBox);
245
246 HBox* hbox = new HBox;
247 hbox->addChild(&m_openButton);
248 hbox->addChild(&m_refreshButton);
249 hbox->addChild(new BoxFiller);
250 hbox->addChild(&m_deleteButton);
251 addChild(hbox);
252 addChild(&m_view);
253
254 InitTheme.connect(
255 [this, hbox]{
256 auto theme = SkinTheme::get(this);
257
258 m_openButton.mainButton()->resetSizeHint();
259 gfx::Size hint = m_openButton.mainButton()->sizeHint();
260 m_openButton.mainButton()->setSizeHint(
261 gfx::Size(std::max(hint.w, 100*guiscale()), hint.h));
262
263 setBgColor(theme->colors.workspace());
264 m_view.setStyle(theme->styles.workspaceView());
265 hbox->setBorder(gfx::Border(2, 0, 2, 0)*guiscale());
266 });
267 initTheme();
268
269 fillList();
270 onChangeSelection();
271
272 m_openButton.Click.connect([this]{ onOpen(); });
273 m_openButton.DropDownClick.connect([this]{ onOpenMenu(); });
274 m_deleteButton.Click.connect([this]{ onDelete(); });
275 m_refreshButton.Click.connect([this]{ onRefresh(); });
276 m_listBox.Change.connect([this]{ onChangeSelection(); });
277 m_listBox.DoubleClickItem.connect([this]{ onOpen(); });
278 m_waitToEnableRefreshTimer.Tick.connect([this]{ onCheckIfWeCanEnableRefreshButton(); });
279
280 m_conn = Preferences::instance()
281 .general.showFullPath.AfterChange.connect(
282 [this](const bool&){ onShowFullPathPrefChange(); });
283}
284
285DataRecoveryView::~DataRecoveryView()
286{
287 m_conn.disconnect();
288}
289
290void DataRecoveryView::refreshListNotification()
291{
292 if (someItemIsBusy())
293 return;
294
295 fillList();
296 layout();
297}
298
299void DataRecoveryView::clearList()
300{
301 WidgetsList children = m_listBox.children();
302 for (auto child : children) {
303 m_listBox.removeChild(child);
304 child->deferDelete();
305 }
306}
307
308void DataRecoveryView::fillList()
309{
310 clearList();
311
312 if (m_dataRecovery->isSearching())
313 m_listBox.addChild(new ListItem(Strings::recover_files_loading()));
314 else {
315 fillListWith(true);
316 fillListWith(false);
317 }
318}
319
320void DataRecoveryView::fillListWith(const bool crashes)
321{
322 bool first = true;
323
324 for (auto& session : m_dataRecovery->sessions()) {
325 if ((session->isEmpty()) ||
326 (crashes && !session->isCrashedSession()) ||
327 (!crashes && session->isCrashedSession()))
328 continue;
329
330 if (first) {
331 first = false;
332
333 // Separator for "crash sessions" vs "old sessions"
334 auto sep = new Separator(
335 (crashes ? Strings::recover_files_crash_sessions():
336 Strings::recover_files_old_sessions()), HORIZONTAL);
337 sep->InitTheme.connect(
338 [sep]{
339 auto theme = skin::SkinTheme::get(sep);
340 sep->setStyle(theme->styles.separatorInViewReverse());
341 sep->setBorder(sep->border() + gfx::Border(0, 8, 0, 8)*guiscale());
342 });
343 sep->initTheme();
344 m_listBox.addChild(sep);
345 }
346
347 std::string title = session->name();
348 if (session->version() != get_app_version())
349 title =
350 fmt::format(Strings::recover_files_incompatible(),
351 title, session->version());
352
353 auto sep = new SeparatorInView(title, HORIZONTAL);
354 sep->InitTheme.connect(
355 [sep]{
356 sep->setBorder(sep->border() + gfx::Border(0, 8, 0, 8)*guiscale());
357 });
358 sep->initTheme();
359 m_listBox.addChild(sep);
360
361 for (auto& backup : session->backups()) {
362 auto item = new Item(session.get(), backup);
363 m_listBox.addChild(item);
364 }
365 }
366
367 // If there are no crash items, we call Empty() signal
368 if (crashes && first)
369 Empty();
370}
371
372void DataRecoveryView::disableRefresh()
373{
374 m_refreshButton.setEnabled(false);
375 m_waitToEnableRefreshTimer.start();
376}
377
378bool DataRecoveryView::someItemIsBusy()
379{
380 // Just in case check that we are not already running some task (so
381 // we cannot refresh the list)
382 for (auto widget : m_listBox.children()) {
383 if (auto item = dynamic_cast<Item*>(widget)) {
384 if (item->isTaskRunning())
385 return true;
386 }
387 }
388 return false;
389}
390
391std::string DataRecoveryView::getTabText()
392{
393 return Strings::recover_files_title();
394}
395
396TabIcon DataRecoveryView::getTabIcon()
397{
398 return TabIcon::NONE;
399}
400
401gfx::Color DataRecoveryView::getTabColor()
402{
403 return gfx::ColorNone;
404}
405
406void DataRecoveryView::onWorkspaceViewSelected()
407{
408 // Do nothing
409}
410
411bool DataRecoveryView::onCloseView(Workspace* workspace, bool quitting)
412{
413 workspace->removeView(this);
414 return true;
415}
416
417void DataRecoveryView::onTabPopup(Workspace* workspace)
418{
419 Menu* menu = AppMenus::instance()->getTabPopupMenu();
420 if (!menu)
421 return;
422
423 menu->showPopup(mousePosInDisplay(), display());
424}
425
426void DataRecoveryView::onOpen()
427{
428 disableRefresh();
429
430 for (auto widget : m_listBox.children()) {
431 if (!widget->isVisible() ||
432 !widget->isSelected())
433 continue;
434
435 if (auto item = dynamic_cast<Item*>(widget)) {
436 if (item->backup())
437 item->restoreBackup();
438 }
439 }
440}
441
442void DataRecoveryView::onOpenRaw(crash::RawImagesAs as)
443{
444 disableRefresh();
445
446 for (auto widget : m_listBox.children()) {
447 if (!widget->isVisible() ||
448 !widget->isSelected())
449 continue;
450
451 if (auto item = dynamic_cast<Item*>(widget)) {
452 if (item->backup())
453 item->restoreRawImages(as);
454 }
455 }
456}
457
458void DataRecoveryView::onOpenMenu()
459{
460 gfx::Rect bounds = m_openButton.bounds();
461
462 Menu menu;
463 MenuItem rawFrames(Strings::recover_files_raw_images_as_frames());
464 MenuItem rawLayers(Strings::recover_files_raw_images_as_layers());
465 menu.addChild(&rawFrames);
466 menu.addChild(&rawLayers);
467
468 rawFrames.Click.connect([this]{ onOpenRaw(crash::RawImagesAs::kFrames); });
469 rawLayers.Click.connect([this]{ onOpenRaw(crash::RawImagesAs::kLayers); });
470
471 menu.showPopup(gfx::Point(bounds.x, bounds.y+bounds.h), display());
472}
473
474void DataRecoveryView::onDelete()
475{
476 disableRefresh();
477
478 std::vector<Item*> items; // Items to delete.
479 for (auto widget : m_listBox.children()) {
480 if (!widget->isVisible() ||
481 !widget->isSelected())
482 continue;
483
484 if (auto item = dynamic_cast<Item*>(widget)) {
485 if (item->backup() &&
486 !item->isTaskRunning())
487 items.push_back(item);
488 }
489 }
490
491 if (items.empty())
492 return;
493
494 // Delete one backup
495 if (Alert::show(
496 fmt::format(Strings::alerts_delete_selected_backups(),
497 int(items.size()))) != 1)
498 return; // Cancel
499
500 Console console;
501 for (auto item : items)
502 item->deleteBackup();
503}
504
505void DataRecoveryView::onRefresh()
506{
507 if (someItemIsBusy())
508 return;
509
510 m_dataRecovery->launchSearch();
511
512 fillList();
513 onChangeSelection();
514 layout();
515}
516
517void DataRecoveryView::onChangeSelection()
518{
519 int count = 0;
520 for (auto widget : m_listBox.children()) {
521 if (!widget->isVisible() ||
522 !widget->isSelected())
523 continue;
524
525 if (dynamic_cast<Item*>(widget)) {
526 ++count;
527 }
528 }
529
530 m_deleteButton.setEnabled(count > 0);
531 m_openButton.setEnabled(count > 0);
532 if (count < 2) {
533 m_openButton.mainButton()->setText(
534 fmt::format(Strings::recover_files_recover_sprite(), count));
535 }
536 else {
537 m_openButton.mainButton()->setText(
538 fmt::format(Strings::recover_files_recover_n_sprites(), count));
539 }
540}
541
542void DataRecoveryView::onCheckIfWeCanEnableRefreshButton()
543{
544 if (someItemIsBusy())
545 return;
546
547 m_refreshButton.setEnabled(true);
548 m_waitToEnableRefreshTimer.stop();
549
550 onChangeSelection();
551
552 // Check if there is no more crash sessions
553 if (!thereAreCrashSessions())
554 Empty();
555
556 m_listBox.layout();
557 m_view.updateView();
558}
559
560void DataRecoveryView::onShowFullPathPrefChange()
561{
562 for (auto widget : m_listBox.children()) {
563 if (auto item = dynamic_cast<Item*>(widget)) {
564 if (!item->isTaskRunning())
565 item->updateText();
566 }
567 }
568}
569
570bool DataRecoveryView::thereAreCrashSessions() const
571{
572 for (auto widget : m_listBox.children()) {
573 if (auto item = dynamic_cast<const Item*>(widget)) {
574 if (item &&
575 item->isVisible() &&
576 item->session() &&
577 item->session()->isCrashedSession())
578 return true;
579 }
580 }
581 return false;
582}
583
584} // namespace app
585