diff --git a/laf b/laf index 5afdc83f2..c293519ad 160000 --- a/laf +++ b/laf @@ -1 +1 @@ -Subproject commit 5afdc83f2ba9622aa37e520ad6b2a605d46022fb +Subproject commit c293519ad611d1c419d9666d5fee077a86ac4c87 diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 6d43d5b64..fdc1630c4 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -391,6 +391,7 @@ if(ENABLE_UI) ui/slider2.cpp ui/status_bar.cpp ui/tabs.cpp + ui/task_widget.cpp ui/timeline/ani_controls.cpp ui/timeline/timeline.cpp ui/toolbar.cpp @@ -568,6 +569,7 @@ add_library(app-lib site.cpp snap_to_grid.cpp sprite_job.cpp + task.cpp thumbnail_generator.cpp thumbnails.cpp tools/active_tool.cpp diff --git a/src/app/console.cpp b/src/app/console.cpp index 17d6d51da..f8db98928 100644 --- a/src/app/console.cpp +++ b/src/app/console.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018 Igara Studio S.A. +// Copyright (C) 2018-2019 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -167,6 +167,12 @@ void Console::printf(const char* format, ...) // static void Console::showException(const std::exception& e) { + ui::assert_ui_thread(); + if (!ui::is_ui_thread()) { + LOG(ERROR, "A problem has occurred.\n\nDetails:\n%s\n", e.what()); + return; + } + Console console; if (typeid(e) == typeid(std::bad_alloc)) console.printf("There is not enough memory to complete the action."); diff --git a/src/app/crash/backup_observer.cpp b/src/app/crash/backup_observer.cpp index c2afc6438..0ac9fe67d 100644 --- a/src/app/crash/backup_observer.cpp +++ b/src/app/crash/backup_observer.cpp @@ -5,7 +5,7 @@ // This program is distributed under the terms of // the End-User License Agreement for Aseprite. -// Uncomment if you want to test the backup process each 5 secondsh +// Uncomment if you want to test the backup process each 5 seconds. //#define TEST_BACKUPS_WITH_A_SHORT_PERIOD // Uncomment if you want to check that backups are correctly saved @@ -137,7 +137,7 @@ void BackupObserver::backgroundThread() else { DocReader reader(doc, 500); std::unique_ptr copy( - m_session->restoreBackupDocById(doc->id())); + m_session->restoreBackupDocById(doc->id(), nullptr)); DocDiff diff = compare_docs(doc, copy.get()); if (diff.anything) { TRACE("RECO: Differences (%s/%s/%s/%s/%s/%s/%s)\n", diff --git a/src/app/crash/read_document.cpp b/src/app/crash/read_document.cpp index ad995c894..7692563c7 100644 --- a/src/app/crash/read_document.cpp +++ b/src/app/crash/read_document.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018 Igara Studio S.A. +// Copyright (C) 2018-2019 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -52,12 +52,14 @@ namespace { class Reader : public SubObjectsIO { public: - Reader(const std::string& dir) + Reader(const std::string& dir, + base::task_token* t) : m_sprite(nullptr) , m_dir(dir) , m_docId(0) , m_docVersions(nullptr) - , m_loadInfo(nullptr) { + , m_loadInfo(nullptr) + , m_taskToken(t) { for (const auto& fn : base::list_files(dir)) { auto i = fn.find('-'); if (i == std::string::npos) @@ -220,7 +222,7 @@ private: m_loadInfo->width = w; m_loadInfo->height = h; m_loadInfo->frames = nframes; - return (Sprite*)1; + return (Sprite*)1; // TODO improve this } std::unique_ptr spr(new Sprite(ImageSpec(mode, w, h), 256)); @@ -244,7 +246,10 @@ private: std::map layersMap; layersMap[0] = spr->root(); // parentId = 0 is the root level - for (int i = 0; i < nlayers; ++i) { + for (int i=0; i(pair.first); + if (!lay) + continue; + + ObjectId celId = pair.second; + + Cel* cel = loadObject("cel", celId, &Reader::readCel); + if (cel) { + // Expand sprite size + if (cel->frame() > m_sprite->lastFrame()) + m_sprite->setTotalFrames(cel->frame()+1); + + lay->addCel(cel); + } + + if (m_taskToken) { + m_taskToken->set_progress(float(i) / float(m_celsToLoad.size())); + } + } + // Read palettes int npalettes = read32(s); if (npalettes >= 1 && npalettes < 0xfffff) { for (int i = 0; i < npalettes; ++i) { + if (canceled()) + return nullptr; + ObjectId palId = read32(s); Palette* pal = loadObject("pal", palId, &Reader::readPalette); if (pal) @@ -282,6 +316,9 @@ private: int nfrtags = read32(s); if (nfrtags >= 1 && nfrtags < 0xfffff) { for (int i = 0; i < nfrtags; ++i) { + if (canceled()) + return nullptr; + ObjectId tagId = read32(s); FrameTag* tag = loadObject("frtag", tagId, &Reader::readFrameTag); if (tag) @@ -293,6 +330,9 @@ private: int nslices = read32(s); if (nslices >= 1 && nslices < 0xffffff) { for (int i = 0; i < nslices; ++i) { + if (canceled()) + return nullptr; + ObjectId sliceId = read32(s); Slice* slice = loadObject("slice", sliceId, &Reader::readSlice); if (slice) @@ -345,15 +385,12 @@ private: // Cels int ncels = read32(s); for (int i=0; i("cel", celId, &Reader::readCel); - if (cel) { - // Expand sprite size - if (cel->frame() > m_sprite->lastFrame()) - m_sprite->setTotalFrames(cel->frame()+1); + if (canceled()) + return nullptr; - lay->addCel(cel); - } + // Add a new cel to load in the future after we load all layers + ObjectId celId = read32(s); + m_celsToLoad.push_back(std::make_pair(lay->id(), celId)); } return lay.release(); } @@ -417,14 +454,23 @@ private: } } + bool canceled() const { + if (m_taskToken) + return m_taskToken->canceled(); + else + return false; + } + Sprite* m_sprite; // Used to pass the sprite in LayerImage() ctor std::string m_dir; ObjectVersion m_docId; ObjVersionsMap m_objVersions; ObjVersions* m_docVersions; DocumentInfo* m_loadInfo; + std::vector > m_celsToLoad; std::map m_images; std::map m_celdatas; + base::task_token* m_taskToken; }; } // anonymous namespace @@ -434,18 +480,20 @@ private: bool read_document_info(const std::string& dir, DocumentInfo& info) { - return Reader(dir).loadDocumentInfo(info); + return Reader(dir, nullptr).loadDocumentInfo(info); } -Doc* read_document(const std::string& dir) +Doc* read_document(const std::string& dir, + base::task_token* t) { - return Reader(dir).loadDocument(); + return Reader(dir, t).loadDocument(); } Doc* read_document_with_raw_images(const std::string& dir, - RawImagesAs as) + RawImagesAs as, + base::task_token* t) { - Reader reader(dir); + Reader reader(dir, t); DocumentInfo info; if (!reader.loadDocumentInfo(info)) { @@ -462,8 +510,13 @@ Doc* read_document_with_raw_images(const std::string& dir, auto lay = new LayerImage(spr); spr->root()->addLayer(lay); + int i = 0; frame_t frame = 0; - for (const auto& fn : base::list_files(dir)) { + auto fns = base::list_files(dir); + for (const auto& fn : fns) { + if (t) + t->set_progress((i++) / fns.size()); + if (fn.compare(0, 3, "img") != 0) continue; diff --git a/src/app/crash/read_document.h b/src/app/crash/read_document.h index 119e239ea..31680dbcb 100644 --- a/src/app/crash/read_document.h +++ b/src/app/crash/read_document.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (c) 2018 Igara Studio S.A. +// Copyright (c) 2018-2019 Igara Studio S.A. // Copyright (C) 2001-2016 David Capello // // This program is distributed under the terms of @@ -10,6 +10,7 @@ #pragma once #include "app/crash/raw_images_as.h" +#include "base/task.h" #include "doc/color_mode.h" #include "doc/frame.h" @@ -34,10 +35,13 @@ namespace crash { } }; - bool read_document_info(const std::string& dir, DocumentInfo& info); - Doc* read_document(const std::string& dir); + bool read_document_info(const std::string& dir, + DocumentInfo& info); + Doc* read_document(const std::string& dir, + base::task_token* t); Doc* read_document_with_raw_images(const std::string& dir, - RawImagesAs as); + RawImagesAs as, + base::task_token* t); } // namespace crash } // namespace app diff --git a/src/app/crash/session.cpp b/src/app/crash/session.cpp index 705b580b6..09e68a451 100644 --- a/src/app/crash/session.cpp +++ b/src/app/crash/session.cpp @@ -27,6 +27,7 @@ #include "base/process.h" #include "base/split_string.h" #include "base/string.h" +#include "base/thread.h" #include "base/time.h" #include "doc/cancel_io.h" @@ -254,16 +255,17 @@ void Session::removeDocument(Doc* doc) markDocumentAsCorrectlyClosed(doc); } - catch (const std::exception&) { - // TODO Log this error + catch (const std::exception& ex) { + LOG(FATAL) << "Exception deleting document " << ex.what() << "\n"; } } -Doc* Session::restoreBackupDoc(const std::string& backupDir) +Doc* Session::restoreBackupDoc(const std::string& backupDir, + base::task_token* t) { Console console; try { - Doc* doc = read_document(backupDir); + Doc* doc = read_document(backupDir, t); if (doc) { fixFilename(doc); return doc; @@ -275,47 +277,49 @@ Doc* Session::restoreBackupDoc(const std::string& backupDir) return nullptr; } -void Session::restoreBackup(Backup* backup) +Doc* Session::restoreBackupDoc(Backup* backup, + base::task_token* t) { - Doc* doc = restoreBackupDoc(backup->dir()); - if (doc) - UIContext::instance()->documents().add(doc); + return restoreBackupDoc(backup->dir(), t); } -void Session::restoreBackupById(const ObjectId id) +Doc* Session::restoreBackupById(const doc::ObjectId id, + base::task_token* t) { std::string docDir = base::join_path(m_path, base::convert_to(int(id))); - if (!base::is_directory(docDir)) - return; - - Doc* doc = restoreBackupDoc(docDir); - if (doc) - UIContext::instance()->documents().add(doc); + if (base::is_directory(docDir)) + return restoreBackupDoc(docDir, t); + else + return nullptr; } -Doc* Session::restoreBackupDocById(const doc::ObjectId id) +Doc* Session::restoreBackupDocById(const doc::ObjectId id, + base::task_token* t) { std::string docDir = base::join_path(m_path, base::convert_to(int(id))); if (!base::is_directory(docDir)) return nullptr; - return restoreBackupDoc(docDir); + return restoreBackupDoc(docDir, t); } -void Session::restoreRawImages(Backup* backup, RawImagesAs as) +Doc* Session::restoreBackupRawImages(Backup* backup, + const RawImagesAs as, + base::task_token* t) { Console console; try { - Doc* doc = read_document_with_raw_images(backup->dir(), as); + Doc* doc = read_document_with_raw_images(backup->dir(), as, t); if (doc) { if (isCrashedSession()) fixFilename(doc); - UIContext::instance()->documents().add(doc); } + return doc; } catch (const std::exception& ex) { Console::showException(ex); } + return nullptr; } void Session::deleteBackup(Backup* backup) diff --git a/src/app/crash/session.h b/src/app/crash/session.h index 0917f1fbc..bc2476670 100644 --- a/src/app/crash/session.h +++ b/src/app/crash/session.h @@ -13,6 +13,7 @@ #include "base/disable_copying.h" #include "base/process.h" #include "base/shared_ptr.h" +#include "base/task.h" #include "doc/object_id.h" #include @@ -58,14 +59,17 @@ namespace crash { bool saveDocumentChanges(Doc* doc); void removeDocument(Doc* doc); - void restoreBackup(Backup* backup); - void restoreBackupById(const doc::ObjectId id); - Doc* restoreBackupDocById(const doc::ObjectId id); - void restoreRawImages(Backup* backup, RawImagesAs as); + Doc* restoreBackupDoc(Backup* backup, + base::task_token* t); + Doc* restoreBackupById(const doc::ObjectId id, base::task_token* t); + Doc* restoreBackupDocById(const doc::ObjectId id, base::task_token* t); + Doc* restoreBackupRawImages(Backup* backup, + const RawImagesAs as, base::task_token* t); void deleteBackup(Backup* backup); private: - Doc* restoreBackupDoc(const std::string& backupDir); + Doc* restoreBackupDoc(const std::string& backupDir, + base::task_token* t); void loadPid(); std::string pidFilename() const; std::string verFilename() const; diff --git a/src/app/modules/gui.cpp b/src/app/modules/gui.cpp index 3cb49919b..4a07231a7 100644 --- a/src/app/modules/gui.cpp +++ b/src/app/modules/gui.cpp @@ -607,10 +607,12 @@ bool CustomizedGuiManager::onProcessDevModeKeyDown(KeyMessage* msg) App::instance()->dataRecovery()->activeSession() && current_editor && current_editor->document()) { - App::instance() + Doc* doc = App::instance() ->dataRecovery() ->activeSession() - ->restoreBackupById(current_editor->document()->id()); + ->restoreBackupById(current_editor->document()->id(), nullptr); + if (doc) + UIContext::instance()->documents().add(doc); return true; } #endif // ENABLE_DATA_RECOVERY diff --git a/src/app/task.cpp b/src/app/task.cpp new file mode 100644 index 000000000..5ae0cc61d --- /dev/null +++ b/src/app/task.cpp @@ -0,0 +1,35 @@ +// Aseprite +// Copyright (C) 2019 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "app/task.h" + +#include "base/task.h" +#include "base/thread_pool.h" + +namespace app { + +static base::thread_pool tasks_pool(4); + +Task::Task() + : m_token(nullptr) +{ +} + +Task::~Task() +{ +} + +void Task::run(base::task::func_t&& func) +{ + m_task.on_execute(std::move(func)); + m_token = &m_task.start(tasks_pool); +} + +} // namespace app diff --git a/src/app/task.h b/src/app/task.h new file mode 100644 index 000000000..b5bbface1 --- /dev/null +++ b/src/app/task.h @@ -0,0 +1,59 @@ +// Aseprite +// Copyright (C) 2019 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_TASK_H_INCLUDED +#define APP_TASK_H_INCLUDED +#pragma once + +#include "base/task.h" + +#include + +namespace app { + + class Task { + public: + Task(); + ~Task(); + + void run(base::task::func_t&& func); + + bool completed() const { + return m_task.completed(); + } + + bool canceled() const { + if (m_token) + return m_token->canceled(); + else + return false; + } + + float progress() const { + if (m_token) + return m_token->progress(); + else + return 0.0f; + } + + void cancel() { + if (m_token) + m_token->cancel(); + } + + void set_progress(float progress) { + if (m_token) + m_token->set_progress(progress); + } + + private: + base::task m_task; + base::task_token* m_token; + }; + +} // namespace app + +#endif diff --git a/src/app/ui/data_recovery_view.cpp b/src/app/ui/data_recovery_view.cpp index 9186f4484..06fe6b350 100644 --- a/src/app/ui/data_recovery_view.cpp +++ b/src/app/ui/data_recovery_view.cpp @@ -11,15 +11,21 @@ #include "app/ui/data_recovery_view.h" +#include "app/app.h" #include "app/app_menus.h" #include "app/crash/data_recovery.h" #include "app/crash/session.h" #include "app/i18n/strings.h" #include "app/modules/gui.h" +#include "app/task.h" +#include "app/ui/data_recovery_view.h" #include "app/ui/drop_down_button.h" #include "app/ui/separator_in_view.h" #include "app/ui/skin/skin_theme.h" +#include "app/ui/task_widget.h" #include "app/ui/workspace.h" +#include "app/ui/workspace.h" +#include "app/ui_context.h" #include "base/bind.h" #include "fmt/format.h" #include "ui/alert.h" @@ -49,12 +55,73 @@ public: : ListItem(backup->description()) , m_session(session) , m_backup(backup) + , m_task(nullptr) { } crash::Session* session() const { return m_session; } crash::Session::Backup* backup() const { return m_backup; } + bool isTaskRunning() const { return m_task != nullptr; } + + void restoreBackup() { + if (m_task) + return; + m_task = new TaskWidget( + [this](base::task_token& t) { + // Warning: This is executed from a worker thread + Doc* doc = m_session->restoreBackupDoc(m_backup, &t); + ui::execute_from_ui_thread( + [this, doc]{ + onOpenDoc(doc); + onDeleteTaskWidget(); + }); + }); + addChild(m_task); + parent()->layout(); + } + + void restoreRawImages(crash::RawImagesAs as) { + if (m_task) + return; + m_task = new TaskWidget( + [this, as](base::task_token& t){ + // Warning: This is executed from a worker thread + Doc* doc = m_session->restoreBackupRawImages(m_backup, as, &t); + ui::execute_from_ui_thread( + [this, doc]{ + onOpenDoc(doc); + onDeleteTaskWidget(); + }); + }); + addChild(m_task); + updateView(); + } + + void deleteBackup() { + if (m_task) + return; + + m_task = new TaskWidget( + TaskWidget::kCannotCancel, + [this](base::task_token& t){ + // Warning: This is executed from a worker thread + m_session->deleteBackup(m_backup); + ui::execute_from_ui_thread( + [this]{ + onDeleteTaskWidget(); + + // We cannot use this->deferDelete() here because it looks + // like the m_task field can be still in use. + setVisible(false); + + updateView(); + }); + }); + addChild(m_task); + updateView(); + } + private: void onSizeHint(SizeHintEvent& ev) override { ListItem::onSizeHint(ev); @@ -63,8 +130,67 @@ private: ev.setSizeHint(sz); } + void onResize(ResizeEvent& ev) override { + setBoundsQuietly(ev.bounds()); + + if (m_task) { + gfx::Size sz = m_task->sizeHint(); + gfx::Rect cpos = childrenBounds(); + + int x2; + if (auto view = View::getView(this->parent())) + x2 = view->viewportBounds().x2(); + else + x2 = cpos.x2(); + + cpos.x = x2 - sz.w; + cpos.w = sz.w; + m_task->setBounds(cpos); + } + } + + void onOpenDoc(Doc* doc) { + if (!doc) + return; + + Workspace* workspace = App::instance()->workspace(); + WorkspaceView* restoreView = workspace->activeView(); + + // If we have this specific item selected, and the active view in + // the workspace is the DataRecoveryView, we can open the + // recovered document. In other case, opening the recovered + // document is confusing (because the user already selected other + // item, or other tab from the workspace). + if (dynamic_cast(restoreView) && + isSelected()) { + restoreView = nullptr; + } + + UIContext::instance()->documents().add(doc); + + if (restoreView) + workspace->setActiveView(restoreView); + } + + void onDeleteTaskWidget() { + if (m_task) { + removeChild(m_task); + m_task->deferDelete(); + m_task = nullptr; + layout(); + } + } + + void updateView() { + if (auto view = View::getView(this->parent())) + view->updateView(); + else + parent()->layout(); + } + crash::Session* m_session; crash::Session::Backup* m_backup; + TaskWidget* m_task; }; } // anonymous namespace @@ -74,6 +200,7 @@ DataRecoveryView::DataRecoveryView(crash::DataRecovery* dataRecovery) , m_openButton(Strings::recover_files_recover_sprite().c_str()) , m_deleteButton(Strings::recover_files_delete()) , m_refreshButton(Strings::recover_files_refresh()) + , m_waitToEnableRefreshTimer(100) { m_listBox.setMultiselect(true); m_view.setExpansive(true); @@ -110,6 +237,7 @@ DataRecoveryView::DataRecoveryView(crash::DataRecovery* dataRecovery) m_refreshButton.Click.connect(base::Bind(&DataRecoveryView::onRefresh, this)); m_listBox.Change.connect(base::Bind(&DataRecoveryView::onChangeSelection, this)); m_listBox.DoubleClickItem.connect(base::Bind(&DataRecoveryView::onOpen, this)); + m_waitToEnableRefreshTimer.Tick.connect(base::Bind(&DataRecoveryView::onCheckIfWeCanEnableRefreshButton, this)); } void DataRecoveryView::refreshListNotification() @@ -190,6 +318,12 @@ void DataRecoveryView::fillListWith(const bool crashes) Empty(); } +void DataRecoveryView::disableRefresh() +{ + m_refreshButton.setEnabled(false); + m_waitToEnableRefreshTimer.start(); +} + std::string DataRecoveryView::getTabText() { return Strings::recover_files_title(); @@ -221,26 +355,32 @@ void DataRecoveryView::onTabPopup(Workspace* workspace) void DataRecoveryView::onOpen() { + disableRefresh(); + for (auto widget : m_listBox.children()) { - if (!widget->isSelected()) + if (!widget->isVisible() || + !widget->isSelected()) continue; if (auto item = dynamic_cast(widget)) { if (item->backup()) - item->session()->restoreBackup(item->backup()); + item->restoreBackup(); } } } void DataRecoveryView::onOpenRaw(crash::RawImagesAs as) { + disableRefresh(); + for (auto widget : m_listBox.children()) { - if (!widget->isSelected()) + if (!widget->isVisible() || + !widget->isSelected()) continue; if (auto item = dynamic_cast(widget)) { if (item->backup()) - item->session()->restoreRawImages(item->backup(), as); + item->restoreRawImages(as); } } } @@ -263,14 +403,17 @@ void DataRecoveryView::onOpenMenu() void DataRecoveryView::onDelete() { - std::vector items; // Items to delete. + disableRefresh(); + std::vector items; // Items to delete. for (auto widget : m_listBox.children()) { - if (!widget->isSelected()) + if (!widget->isVisible() || + !widget->isSelected()) continue; if (auto item = dynamic_cast(widget)) { - if (item->backup()) + if (item->backup() && + !item->isTaskRunning()) items.push_back(item); } } @@ -284,19 +427,8 @@ void DataRecoveryView::onDelete() int(items.size()))) != 1) return; // Cancel - for (auto item : items) { - item->session()->deleteBackup(item->backup()); - m_listBox.removeChild(item); - delete item; - } - onChangeSelection(); - - // Check if there is no more crash sessions - if (!thereAreCrashSessions()) - Empty(); - - m_listBox.layout(); - m_view.updateView(); + for (auto item : items) + item->deleteBackup(); } void DataRecoveryView::onRefresh() @@ -312,7 +444,8 @@ void DataRecoveryView::onChangeSelection() { int count = 0; for (auto widget : m_listBox.children()) { - if (!widget->isSelected()) + if (!widget->isVisible() || + !widget->isSelected()) continue; if (dynamic_cast(widget)) { @@ -332,11 +465,34 @@ void DataRecoveryView::onChangeSelection() } } +void DataRecoveryView::onCheckIfWeCanEnableRefreshButton() +{ + for (auto widget : m_listBox.children()) { + if (auto item = dynamic_cast(widget)) { + if (item->isTaskRunning()) + return; + } + } + + m_refreshButton.setEnabled(true); + m_waitToEnableRefreshTimer.stop(); + + onChangeSelection(); + + // Check if there is no more crash sessions + if (!thereAreCrashSessions()) + Empty(); + + m_listBox.layout(); + m_view.updateView(); +} + bool DataRecoveryView::thereAreCrashSessions() const { for (auto widget : m_listBox.children()) { if (auto item = dynamic_cast(widget)) { if (item && + item->isVisible() && item->session() && item->session()->isCrashedSession()) return true; diff --git a/src/app/ui/data_recovery_view.h b/src/app/ui/data_recovery_view.h index c4b318a49..b223ae33a 100644 --- a/src/app/ui/data_recovery_view.h +++ b/src/app/ui/data_recovery_view.h @@ -51,6 +51,7 @@ namespace app { void clearList(); void fillList(); void fillListWith(const bool crashes); + void disableRefresh(); void onOpen(); void onOpenRaw(crash::RawImagesAs as); @@ -58,6 +59,7 @@ namespace app { void onDelete(); void onRefresh(); void onChangeSelection(); + void onCheckIfWeCanEnableRefreshButton(); bool thereAreCrashSessions() const; crash::DataRecovery* m_dataRecovery; @@ -66,6 +68,7 @@ namespace app { DropDownButton m_openButton; ui::Button m_deleteButton; ui::Button m_refreshButton; + ui::Timer m_waitToEnableRefreshTimer; }; } // namespace app diff --git a/src/app/ui/tabs.cpp b/src/app/ui/tabs.cpp index bf4d3b787..2b6911014 100644 --- a/src/app/ui/tabs.cpp +++ b/src/app/ui/tabs.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018 Igara Studio S.A. +// Copyright (C) 2018-2019 Igara Studio S.A. // Copyright (C) 2001-2017 David Capello // // This program is distributed under the terms of @@ -52,6 +52,7 @@ Tabs::Tabs(TabsDelegate* delegate) , m_clickedCloseButton(false) , m_selected(nullptr) , m_delegate(delegate) + , m_addedTab(nullptr) , m_removedTab(nullptr) , m_isDragging(false) , m_dragCopy(false) @@ -68,6 +69,7 @@ Tabs::Tabs(TabsDelegate* delegate) Tabs::~Tabs() { + m_addedTab.reset(); m_removedTab.reset(); // Stop animation @@ -99,6 +101,8 @@ void Tabs::addTab(TabView* tabView, bool from_drop, int pos) tab->oldWidth = tab->width; tab->modified = (m_delegate ? m_delegate->isTabModified(this, tabView): false); + + m_addedTab = tab; } void Tabs::removeTab(TabView* tabView, bool with_animation) @@ -497,7 +501,7 @@ void Tabs::onPaint(PaintEvent& ev) (tab->view != m_dragTab->view) || (m_dragCopy)) { int dy = 0; - if (animation() == ANI_ADDING_TAB && tab == m_selected) { + if (animation() == ANI_ADDING_TAB && tab == m_addedTab) { double t = animationTime(); dy = int(box.h - box.h * t); } @@ -791,6 +795,8 @@ void Tabs::onAnimationFrame() void Tabs::onAnimationStop(int animation) { + m_addedTab.reset(); + if (m_list.empty()) { Widget* root = window(); if (root) @@ -956,6 +962,8 @@ void Tabs::destroyFloatingTab() tab->oldX = tab->x; tab->oldWidth = 0; + + m_addedTab = tab; } } diff --git a/src/app/ui/tabs.h b/src/app/ui/tabs.h index 08a2f92b4..06ba2f43a 100644 --- a/src/app/ui/tabs.h +++ b/src/app/ui/tabs.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2019 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -234,6 +235,7 @@ namespace app { TabsDelegate* m_delegate; // Variables for animation purposes + TabPtr m_addedTab; TabPtr m_removedTab; //////////////////////////////////////// diff --git a/src/app/ui/task_widget.cpp b/src/app/ui/task_widget.cpp new file mode 100644 index 000000000..f3416f515 --- /dev/null +++ b/src/app/ui/task_widget.cpp @@ -0,0 +1,91 @@ +// Aseprite +// Copyright (C) 2019 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "app/ui/task_widget.h" + +#include "app/i18n/strings.h" +#include "app/modules/gui.h" +#include "app/ui/skin/skin_theme.h" +#include "base/clamp.h" +#include "ui/scale.h" + +#include + +namespace app { + +using namespace ui; +using namespace app::skin; + +TaskWidget::TaskWidget(const Type type, + base::task::func_t&& func) + : Box(HORIZONTAL | HOMOGENEOUS) + , m_monitorTimer(25) + , m_cancelButton("Cancel") + , m_progressBar(0, 100, 0) +{ + if (int(type) & int(kCanCancel)) { + addChild(&m_cancelButton); + + m_cancelButton.Click.connect( + [this](Event&){ + m_task.cancel(); + m_cancelButton.setEnabled(false); + m_progressBar.setEnabled(false); + }); + } + + if (int(type) & int(kWithProgress)) { + m_progressBar.setReadOnly(true); + addChild(&m_progressBar); + } + + m_monitorTimer.Tick.connect( + [this]{ + if (m_task.completed()) { + m_monitorTimer.stop(); + onComplete(); + } + else if (m_progressBar.parent()) { + float v = m_task.progress(); + if (v > 0.0f) { + TRACEARGS("progressBar setValue", + int(base::clamp(v*100.0f, 0.0f, 100.0f))); + m_progressBar.setValue( + int(base::clamp(v*100.0f, 0.0f, 100.0f))); + } + } + }); + m_monitorTimer.start(); + + InitTheme.connect( + [this]{ + auto theme = static_cast(this->theme()); + setTransparent(true); + setBgColor(gfx::ColorNone); + m_cancelButton.setTransparent(true); + m_cancelButton.setStyle(theme->styles.miniButton()); + m_cancelButton.setBgColor(gfx::ColorNone); + m_progressBar.setTransparent(true); + m_progressBar.setBgColor(gfx::ColorNone); + setup_mini_font(&m_cancelButton); + setup_mini_look(&m_progressBar); + setMaxSize(gfx::Size(std::numeric_limits::max(), textHeight())); + }); + initTheme(); + + m_task.run(std::move(func)); +} + +void TaskWidget::onComplete() +{ + // Do nothing +} + +} // namespace app diff --git a/src/app/ui/task_widget.h b/src/app/ui/task_widget.h new file mode 100644 index 000000000..9f238cca5 --- /dev/null +++ b/src/app/ui/task_widget.h @@ -0,0 +1,46 @@ +// Aseprite +// Copyright (C) 2019 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_UI_TASK_WIDGET_H_INCLUDED +#define APP_UI_TASK_WIDGET_H_INCLUDED +#pragma once + +#include "app/task.h" +#include "obs/signal.h" +#include "ui/box.h" +#include "ui/button.h" +#include "ui/slider.h" +#include "ui/timer.h" + +namespace app { + +class TaskWidget : public ui::Box { +public: + enum Type { + kCanCancel = 1, + kWithProgress = 2, + kCannotCancel = kWithProgress, + kCancelAndProgress = 3, + }; + + TaskWidget(const Type type, + base::task::func_t&& func); + TaskWidget(base::task::func_t&& func) + : TaskWidget(kCancelAndProgress, std::move(func)) { } + +protected: + virtual void onComplete(); + +private: + ui::Timer m_monitorTimer; + ui::Button m_cancelButton; + ui::Slider m_progressBar; + Task m_task; +}; + +} // namespace app + +#endif