Add "Recover Files..." option in Home tab

We've changed the way the "recover files" option works:
* Now it's an option that is always available (so we can open files
  even from sessions that were correctly closed in the past)
* We can open sessions from other Aseprite versions (as in a "best
  effort" approach, if it works, ok, if it doesn't -> contact user
  support)
This commit is contained in:
David Capello 2019-05-27 11:10:52 -03:00
parent 0902fa7629
commit 1b62515cd2
20 changed files with 519 additions and 113 deletions

View File

@ -638,7 +638,6 @@
<background color="check_hot_face" state="selected" />
<icon part="pal_options" />
</style>
<style id="recover_sprites_button" extends="button" border="0" padding="8" />
<style id="new_frame_button" extends="mini_button" />
<style id="color_button" extends="mini_button" border="5" font="mini" />
<style id="splitter">
@ -657,6 +656,10 @@
<background color="background" />
<background-border part="separator_horz" align="middle" />
</style>
<style id="separator_in_view_reverse">
<background color="workspace" />
<text color="background" x="4" align="left middle" />
</style>
<style id="vertical_separator" border-left="4" border-top="2" border-right="1" border-bottom="2">
<background-border part="separator_vert" align="center" />
</style>

View File

@ -132,6 +132,8 @@
<option id="expand_menubar_on_mouseover" type="bool" default="false" migrate="Options.ExpandMenuBarOnMouseover" />
<option id="data_recovery" type="bool" default="true" />
<option id="data_recovery_period" type="double" default="2.0" />
<option id="keep_edited_sprite_data" type="bool" default="true" />
<option id="keep_edited_sprite_data_lifespan" type="int" default="7" />
<option id="show_full_path" type="bool" default="true" />
<option id="timeline_position" type="TimelinePosition" default="TimelinePosition::BOTTOM" />
<option id="timeline_layer_panel_width" type="int" default="100" />

View File

@ -136,6 +136,7 @@ Aseprite
||&OK
END
restart_by_preferences_save_recovery_data_period = Automatically save recovery data every X minutes
restart_by_preferences_keep_edited_sprite_data_lifespan = Keep edited sprite data for X days
restore_all_shortcuts = <<<END
Warning
<<Do you want to restore all keyboard shortcuts
@ -619,9 +620,14 @@ height = Height:
[home_view]
title = Home
recover = Recover Lost Sprites
new_file = New File...
open_file = Open File...
recover_files = Recover Files...
recover_files_tooltip = <<<END
Recover files from crashed sessions or
closed sprite that were not saved in
previous sessions.
END
recent_files = Recent files:
recent_folders = Recent folders:
news = News:
@ -939,6 +945,18 @@ END
10_minutes = 10 Minutes
15_minutes = 15 Minutes
30_minutes = 30 Minutes
keep_edited_sprite_data = Keep edited sprite data for
keep_edited_sprite_data_tooltip = <<<END
With this option you can re-open edited documents
after closing the program for the number of specified
days.
END
1_day = 1 Day
2_days = 2 Days
3_days = 3 Days
1_week = 1 Week
2_weeks = 2 Weeks
1_month = 1 Month
show_full_path = Show full file name path
show_full_path_tooltip = <<<END
Uncheck this option if you would prefer to hide
@ -1177,6 +1195,18 @@ color = Color:
antialias = Anti-aliasing filter
antialias_tooltip = Smooth font edges
[recover_files]
title = Recover Files
recover_sprite = Recover Sprite
recover_n_sprites = Recover {} Sprite(s)
delete = Delete
refresh = Refresh
raw_images_as_frames = Raw Images as Frames
raw_images_as_layers = Raw Images as Layers
crash_sessions = Crashed Sessions
old_sessions = Previous Sessions
incompatible = [MIGHT BE INCOMPATIBLE v{1}] {0}
[replace_color]
from = From:
to = To:

View File

@ -1,18 +1,16 @@
<!-- Aseprite -->
<!-- Copyright (C) 2001-2017 by David Capello -->
<!-- Copyright (C) 2019 Igara Studio S.A. -->
<!-- Copyright (C) 2001-2017 David Capello -->
<gui>
<vbox noborders="true" id="home_view" border="4" childspacing="2" expansive="true">
<hbox noborders="true" id="recover_sprites_placeholder">
<boxfiller />
<button id="recover_sprites" text="@.recover" style="recover_sprites_button" />
<boxfiller />
</hbox>
<hbox noborders="true" id="header_placeholder">
<link id="aseprite_face" style="aseprite_face" />
<vbox border="4" childspacing="4">
<link id="new_file" text="@.new_file" style="workspace_link" />
<link id="open_file" text="@.open_file" style="workspace_link" />
<link id="recover_sprites" text="@.recover_files" style="workspace_link"
tooltip="@.recover_files_tooltip" />
</vbox>
<boxfiller />
<vbox border="4">

View File

@ -67,7 +67,7 @@
text="@.color_bar_entries_separator"
tooltip="@.color_bar_entries_separator"
pref="color_bar.entries_separator" />
<hbox>
<grid columns="2">
<check id="enable_data_recovery"
text="@.auto_save_recovery_data"
tooltip="@.auto_save_recovery_data_tooltip" />
@ -81,7 +81,18 @@
<listitem text="@.15_minutes" value="15" />
<listitem text="@.30_minutes" value="30" />
</combobox>
</hbox>
<check id="keep_edited_sprite_data"
text="@.keep_edited_sprite_data"
tooltip="@.keep_edited_sprite_data_tooltip" />
<combobox id="keep_edited_sprite_data_lifespan">
<listitem text="@.1_day" value="1" />
<listitem text="@.2_days" value="2" />
<listitem text="@.3_days" value="3" />
<listitem text="@.1_week" value="7" />
<listitem text="@.2_weeks" value="14" />
<listitem text="@.1_month" value="30" />
</combobox>
</grid>
<separator horizontal="true" />
<link id="locate_file" text="@.locate_file" />
<link id="locate_crash_folder" text="@.locate_crash_folder" />

View File

@ -137,7 +137,9 @@ public:
InputChain m_inputChain;
clipboard::ClipboardManager m_clipboardManager;
#endif
// This is a raw pointer because we want to delete this explicitly.
// This is a raw pointer because we want to delete it explicitly.
// (e.g. if an exception occurs, the ~Modules() doesn't have to
// delete m_recovery)
app::crash::DataRecovery* m_recovery;
Modules(const bool createLogInDesktop,
@ -151,22 +153,40 @@ public:
, m_recovery(nullptr) {
}
app::crash::DataRecovery* recovery() {
return m_recovery;
~Modules() {
ASSERT(m_recovery == nullptr);
}
bool hasRecoverySessions() const {
return m_recovery && !m_recovery->sessions().empty();
app::crash::DataRecovery* recovery() {
return m_recovery;
}
void createDataRecovery() {
#ifdef ENABLE_DATA_RECOVERY
m_recovery = new app::crash::DataRecovery(&m_context);
m_recovery->SessionsListIsReady.connect(
[] {
ui::assert_ui_thread();
auto app = App::instance();
if (app && app->mainWindow()) {
// Notify that the list of sessions is ready.
app->mainWindow()->dataRecoverySessionsAreReady();
}
});
#endif
}
void searchDataRecoverySessions() {
#ifdef ENABLE_DATA_RECOVERY
ASSERT(m_recovery);
if (m_recovery)
m_recovery->launchSearch();
#endif
}
void deleteDataRecovery() {
#ifdef ENABLE_DATA_RECOVERY
ASSERT(m_recovery);
delete m_recovery;
m_recovery = nullptr;
#endif
@ -269,14 +289,14 @@ void App::initialize(const AppOptions& options)
if (m_mod)
m_mod->modMainWindow(m_mainWindow.get());
// Data recovery is enabled only in GUI mode
if (preferences().general.dataRecovery())
m_modules->searchDataRecoverySessions();
// Default status of the main window.
app_rebuild_documents_tabs();
app_default_statusbar_message();
// Recover data
if (m_modules->hasRecoverySessions())
m_mainWindow->showDataRecovery(m_modules->recovery());
m_mainWindow->openWindow();
// Redraw the whole screen.
@ -409,12 +429,12 @@ void App::run()
if (isGui()) {
// Destroy the window.
m_mainWindow.reset(NULL);
// Delete backups (this is a normal shutdown, we are not handling
// exceptions, and we are not in a destructor).
m_modules->deleteDataRecovery();
}
#endif
// Delete backups (this is a normal shutdown, we are not handling
// exceptions, and we are not in a destructor).
m_modules->deleteDataRecovery();
}
// Finishes the Aseprite application.

View File

@ -242,6 +242,9 @@ public:
if (m_pref.general.dataRecovery())
enableDataRecovery()->setSelected(true);
if (m_pref.general.keepEditedSpriteData())
keepEditedSpriteData()->setSelected(true);
if (m_pref.general.showFullPath())
showFullPath()->setSelected(true);
@ -249,6 +252,10 @@ public:
dataRecoveryPeriod()->findItemIndexByValue(
base::convert_to<std::string>(m_pref.general.dataRecoveryPeriod())));
keepEditedSpriteDataLifespan()->setSelectedItemIndex(
keepEditedSpriteDataLifespan()->findItemIndexByValue(
base::convert_to<std::string>(m_pref.general.keepEditedSpriteDataLifespan())));
if (m_pref.editor.zoomFromCenterWithWheel())
zoomFromCenterWithWheel()->setSelected(true);
@ -479,6 +486,15 @@ public:
warnings += "<<- " + Strings::alerts_restart_by_preferences_save_recovery_data_period();
}
int newLifespan = base::convert_to<int>(keepEditedSpriteDataLifespan()->getValue());
if (keepEditedSpriteData()->isSelected() != m_pref.general.keepEditedSpriteData() ||
newLifespan != m_pref.general.dataRecoveryPeriod()) {
m_pref.general.keepEditedSpriteData(keepEditedSpriteData()->isSelected());
m_pref.general.keepEditedSpriteDataLifespan(newLifespan);
warnings += "<<- " + Strings::alerts_restart_by_preferences_keep_edited_sprite_data_lifespan();
}
m_pref.editor.zoomFromCenterWithWheel(zoomFromCenterWithWheel()->isSelected());
m_pref.editor.zoomFromCenterWithKeys(zoomFromCenterWithKeys()->isSelected());
m_pref.editor.showScrollbars(showScrollbars()->isSelected());

View File

@ -20,6 +20,7 @@
#include "app/app.h"
#include "app/context.h"
#include "app/crash/recovery_config.h"
#include "app/crash/session.h"
#include "app/doc.h"
#include "app/doc_access.h"
@ -56,11 +57,13 @@ public:
}
BackupObserver::BackupObserver(Session* session, Context* ctx)
: m_session(session)
BackupObserver::BackupObserver(RecoveryConfig* config,
Session* session,
Context* ctx)
: m_config(config)
, m_session(session)
, m_ctx(ctx)
, m_done(false)
, m_period(Preferences::instance().general.dataRecoveryPeriod())
, m_thread(base::Bind<void>(&BackupObserver::backgroundThread, this))
{
m_ctx->add_observer(this);
@ -86,19 +89,20 @@ void BackupObserver::onAddDocument(Doc* document)
m_documents.push_back(document);
}
void BackupObserver::onRemoveDocument(Doc* document)
void BackupObserver::onRemoveDocument(Doc* doc)
{
TRACE("RECO: Remove document %p\n", document);
TRACE("RECO: Remove document %p\n", doc);
{
base::scoped_lock hold(m_mutex);
base::remove_from_container(m_documents, document);
base::remove_from_container(m_documents, doc);
}
m_session->removeDocument(document);
// TODO save backup data of the closed document in a background thread
m_session->removeDocument(doc);
}
void BackupObserver::backgroundThread()
{
int normalPeriod = int(60.0*m_period);
int normalPeriod = int(60.0*m_config->dataRecoveryPeriod);
int lockedPeriod = 5;
#ifdef TEST_BACKUPS_WITH_A_SHORT_PERIOD
normalPeriod = 5;

View File

@ -21,13 +21,16 @@ namespace app {
class Context;
class Doc;
namespace crash {
struct RecoveryConfig;
class Session;
class BackupObserver : public ContextObserver
, public DocsObserver
, public DocObserver {
public:
BackupObserver(Session* session, Context* ctx);
BackupObserver(RecoveryConfig* config,
Session* session,
Context* ctx);
~BackupObserver();
void stop();
@ -38,12 +41,12 @@ namespace crash {
private:
void backgroundThread();
RecoveryConfig* m_config;
Session* m_session;
base::mutex m_mutex;
Context* m_ctx;
std::vector<Doc*> m_documents;
bool m_done;
double m_period;
base::thread m_thread;
};

View File

@ -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
@ -12,54 +13,34 @@
#include "app/crash/backup_observer.h"
#include "app/crash/session.h"
#include "app/pref/preferences.h"
#include "app/resource_finder.h"
#include "base/fs.h"
#include "base/time.h"
#include "ui/system.h"
#include <algorithm>
namespace app {
namespace crash {
// Flag used to avoid calling SessionsListIsReady() signal after
// DataRecovery() instance is deleted.
static bool g_stillAliveFlag = false;
DataRecovery::DataRecovery(Context* ctx)
: m_inProgress(nullptr)
, m_backup(nullptr)
, m_searching(false)
{
auto& pref = Preferences::instance();
m_config.dataRecoveryPeriod = pref.general.dataRecoveryPeriod();
m_config.keepEditedSpriteData = pref.general.keepEditedSpriteData();
m_config.keepEditedSpriteDataLifespan = pref.general.keepEditedSpriteDataLifespan();
ResourceFinder rf;
rf.includeUserDir(base::join_path("sessions", ".").c_str());
std::string sessionsDir = rf.getFirstOrCreateDefault();
// Existent sessions
TRACE("RECO: Listing sessions from '%s'\n", sessionsDir.c_str());
for (auto& itemname : base::list_files(sessionsDir)) {
std::string itempath = base::join_path(sessionsDir, itemname);
if (base::is_directory(itempath)) {
TRACE("RECO: Session '%s' ", itempath.c_str());
SessionPtr session(new Session(itempath));
if (!session->isRunning()) {
if (session->version() != VERSION) {
TRACE("cannot be loaded (incompatible version)\n");
}
else if (!session->isEmpty()) {
TRACE("to be loaded\n");
m_sessions.push_back(session);
}
else {
TRACE("to be deleted\n");
session->removeFromDisk();
}
}
else
TRACE("is running\n");
}
}
// Sort sessions from the most recent one to the oldest one
std::sort(m_sessions.begin(), m_sessions.end(),
[](const SessionPtr& a, const SessionPtr& b) {
return a->name() > b->name();
});
m_sessionsDir = rf.getFirstOrCreateDefault();
// Create a new session
base::pid pid = base::get_current_process_id();
@ -73,7 +54,7 @@ DataRecovery::DataRecovery(Context* ctx)
time.year, time.month, time.day,
time.hour, time.minute, time.second, pid);
newSessionDir = base::join_path(sessionsDir, buf);
newSessionDir = base::join_path(m_sessionsDir, buf);
if (!base::is_directory(newSessionDir))
base::make_directory(newSessionDir);
@ -83,23 +64,120 @@ DataRecovery::DataRecovery(Context* ctx)
}
} while (newSessionDir.empty());
m_inProgress.reset(new Session(newSessionDir));
m_inProgress.reset(new Session(&m_config, newSessionDir));
m_inProgress->create(pid);
TRACE("RECO: Session in progress '%s'\n", newSessionDir.c_str());
m_backup = new BackupObserver(m_inProgress.get(), ctx);
m_backup = new BackupObserver(&m_config, m_inProgress.get(), ctx);
g_stillAliveFlag = true;
}
DataRecovery::~DataRecovery()
{
g_stillAliveFlag = false;
m_thread.join();
m_backup->stop();
delete m_backup;
// We just close the session on progress. The session is not
// deleted just in case that the user want to recover some files
// from this session in the future.
if (m_inProgress)
m_inProgress->removeFromDisk();
m_inProgress->close();
m_inProgress.reset();
}
void DataRecovery::launchSearch()
{
if (m_searching)
return;
// Search current sessions in a background thread
if (m_thread.joinable())
m_thread.join();
ASSERT(!m_searching);
m_searching = true;
m_thread = std::thread(
[this]{
searchForSessions();
m_searching = false;
});
}
bool DataRecovery::hasRecoverySessions() const
{
std::unique_lock<std::mutex> lock(m_sessionsMutex);
for (const auto& session : m_sessions)
if (session->isCrashedSession())
return true;
return false;
}
DataRecovery::Sessions DataRecovery::sessions()
{
Sessions copy;
{
std::unique_lock<std::mutex> lock(m_sessionsMutex);
copy = m_sessions;
}
return copy;
}
void DataRecovery::searchForSessions()
{
Sessions sessions;
// Existent sessions
TRACE("RECO: Listing sessions from '%s'\n", m_sessionsDir.c_str());
for (auto& itemname : base::list_files(m_sessionsDir)) {
std::string itempath = base::join_path(m_sessionsDir, itemname);
if (base::is_directory(itempath)) {
TRACE("RECO: Session '%s'\n", itempath.c_str());
SessionPtr session(new Session(&m_config, itempath));
if (!session->isRunning()) {
if ((session->isEmpty()) ||
(!session->isCrashedSession() && session->isOldSession())) {
TRACE("RECO: - to be deleted (%s)\n",
session->isEmpty() ? "is empty":
(session->isOldSession() ? "is old":
"unknown reason"));
session->removeFromDisk();
}
else {
TRACE("RECO: - to be loaded\n");
sessions.push_back(session);
}
}
else
TRACE("is running\n");
}
}
// Sort sessions from the most recent one to the oldest one
std::sort(sessions.begin(), sessions.end(),
[](const SessionPtr& a, const SessionPtr& b) {
return a->name() > b->name();
});
// Assign m_sessions=sessions
{
std::unique_lock<std::mutex> lock(m_sessionsMutex);
std::swap(m_sessions, sessions);
}
ui::execute_from_ui_thread(
[this]{
if (g_stillAliveFlag)
SessionsListIsReady();
});
}
} // namespace crash
} // namespace app

View File

@ -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
@ -8,9 +9,13 @@
#define APP_CRASH_DATA_RECOVERY_H_INCLUDED
#pragma once
#include "app/crash/recovery_config.h"
#include "app/crash/session.h"
#include "base/disable_copying.h"
#include "obs/signal.h"
#include <mutex>
#include <thread>
#include <vector>
namespace app {
@ -25,15 +30,35 @@ namespace crash {
DataRecovery(Context* context);
~DataRecovery();
// Launches the thread to search for sessions.
void launchSearch();
// Returns true if there is at least one sessions with sprites to
// recover (i.e. a crashed session were changes weren't saved)
bool hasRecoverySessions() const;
Session* activeSession() { return m_inProgress.get(); }
// Returns the list of sessions that can be recovered.
const Sessions& sessions() { return m_sessions; }
// Returns a copy of the list of sessions that can be recovered.
Sessions sessions();
// Triggered in the UI-thread from the m_thread using an
// ui::execute_from_ui_thread() when the list of sessions is ready
// to be used.
obs::signal<void()> SessionsListIsReady;
private:
// Executed from m_thread to search for the list of sessions.
void searchForSessions();
std::string m_sessionsDir;
mutable std::mutex m_sessionsMutex;
std::thread m_thread;
RecoveryConfig m_config;
Sessions m_sessions;
SessionPtr m_inProgress;
BackupObserver* m_backup;
bool m_searching;
DISABLE_COPYING(DataRecovery);
};

View File

@ -0,0 +1,25 @@
// 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_CRASH_RECOVERY_CONFIG_H_INCLUDED
#define APP_CRASH_RECOVERY_CONFIG_H_INCLUDED
#pragma once
namespace app {
namespace crash {
// Structure to store the configuration from Preferences instance to
// avoid accessing to Preferences from a non-UI thread.
struct RecoveryConfig {
double dataRecoveryPeriod;
bool keepEditedSpriteData;
int keepEditedSpriteDataLifespan;
};
} // namespace crash
} // namespace app
#endif

View File

@ -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
@ -13,6 +14,7 @@
#include "app/console.h"
#include "app/context.h"
#include "app/crash/read_document.h"
#include "app/crash/recovery_config.h"
#include "app/crash/write_document.h"
#include "app/doc.h"
#include "app/doc_access.h"
@ -25,11 +27,16 @@
#include "base/process.h"
#include "base/split_string.h"
#include "base/string.h"
#include "base/time.h"
#include "doc/cancel_io.h"
namespace app {
namespace crash {
static const char* kPidFilename = "pid"; // Process ID running the session (or non-existent if the PID was closed correctly)
static const char* kVerFilename = "ver"; // File that indicates the Aseprite version used in the session
static const char* kOpenFilename = "open"; // File that indicates if the document is/was open in the session (or non-existent if the document was closed correctly)
Session::Backup::Backup(const std::string& dir)
: m_dir(dir)
{
@ -48,9 +55,11 @@ Session::Backup::Backup(const std::string& dir)
m_desc = &buf[0];
}
Session::Session(const std::string& path)
Session::Session(RecoveryConfig* config,
const std::string& path)
: m_pid(0)
, m_path(path)
, m_config(config)
{
}
@ -108,7 +117,31 @@ const Session::Backups& Session::backups()
bool Session::isRunning()
{
loadPid();
return base::is_process_running(m_pid);
if (m_pid)
return base::is_process_running(m_pid);
else
return false;
}
bool Session::isCrashedSession()
{
loadPid();
return (m_pid != 0);
}
bool Session::isOldSession()
{
if (!m_config->keepEditedSpriteData)
return true;
std::string verfile = verFilename();
if (!base::is_file(verfile))
return true;
int lifespan = m_config->keepEditedSpriteDataLifespan;
base::Time sessionTime = base::get_modification_time(verfile);
return (sessionTime.addDays(lifespan) < base::current_time());
}
bool Session::isEmpty()
@ -131,9 +164,32 @@ void Session::create(base::pid pid)
verf << VERSION;
}
void Session::close()
{
try {
// Just remove the PID file to indicate that this session was
// correctly closed
if (base::is_file(pidFilename()))
base::delete_file(pidFilename());
// If we don't have to keep the sprite data, just remove it from
// the disk.
if (!m_config->keepEditedSpriteData)
removeFromDisk();
}
catch (const std::exception&) {
// TODO Log this error
}
}
void Session::removeFromDisk()
{
try {
// Remove all backups from disk
Backups baks = backups();
for (Backup* bak : baks)
deleteBackup(bak);
if (base::is_file(pidFilename()))
base::delete_file(pidFilename());
@ -173,9 +229,20 @@ bool Session::saveDocumentChanges(Doc* doc)
base::convert_to<std::string>(doc->id()));
TRACE("RECO: Saving document '%s'...\n", dir.c_str());
// Create directory for document
if (!base::is_directory(dir))
base::make_directory(dir);
// Create "open" file to indicate that the document is open in this session
{
std::string openfile = base::join_path(dir, kOpenFilename);
if (!base::is_file(openfile)) {
std::ofstream of(FSTREAM_PATH(openfile));
if (of)
of << "open";
}
}
// Save document information
return write_document(dir, doc, &reader);
}
@ -185,11 +252,7 @@ void Session::removeDocument(Doc* doc)
try {
delete_document_internals(doc);
// Delete document backup directory
std::string dir = base::join_path(m_path,
base::convert_to<std::string>(doc->id()));
if (base::is_directory(dir))
deleteDirectory(dir);
markDocumentAsCorrectlyClosed(doc);
}
catch (const std::exception&) {
// TODO Log this error
@ -245,7 +308,8 @@ void Session::restoreRawImages(Backup* backup, RawImagesAs as)
try {
Doc* doc = read_document_with_raw_images(backup->dir(), as);
if (doc) {
fixFilename(doc);
if (isCrashedSession())
fixFilename(doc);
UIContext::instance()->documents().add(doc);
}
}
@ -285,12 +349,28 @@ void Session::loadPid()
std::string Session::pidFilename() const
{
return base::join_path(m_path, "pid");
return base::join_path(m_path, kPidFilename);
}
std::string Session::verFilename() const
{
return base::join_path(m_path, "ver");
return base::join_path(m_path, kVerFilename);
}
void Session::markDocumentAsCorrectlyClosed(app::Doc* doc)
{
std::string dir = base::join_path(
m_path, base::convert_to<std::string>(doc->id()));
ASSERT(!dir.empty());
if (dir.empty() || !base::is_directory(dir))
return;
std::string openFn = base::join_path(dir, kOpenFilename);
if (base::is_file(openFn)) {
TRACE("RECO: Document was closed correctly, deleting file '%s'\n", openFn.c_str());
base::delete_file(openFn);
}
}
void Session::deleteDirectory(const std::string& dir)

View File

@ -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
@ -21,6 +22,7 @@
namespace app {
class Doc;
namespace crash {
struct RecoveryConfig;
// A class to record/restore session information.
class Session {
@ -36,7 +38,8 @@ namespace crash {
};
typedef std::vector<Backup*> Backups;
Session(const std::string& path);
Session(RecoveryConfig* config,
const std::string& path);
~Session();
std::string name() const;
@ -44,9 +47,12 @@ namespace crash {
const Backups& backups();
bool isRunning();
bool isCrashedSession();
bool isOldSession();
bool isEmpty();
void create(base::pid pid);
void close();
void removeFromDisk();
bool saveDocumentChanges(Doc* doc);
@ -63,6 +69,7 @@ namespace crash {
void loadPid();
std::string pidFilename() const;
std::string verFilename() const;
void markDocumentAsCorrectlyClosed(Doc* doc);
void deleteDirectory(const std::string& dir);
void fixFilename(Doc* doc);
@ -70,6 +77,7 @@ namespace crash {
std::string m_path;
std::string m_version;
Backups m_backups;
RecoveryConfig* m_config;
DISABLE_COPYING(Session);
};

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -24,6 +25,7 @@
#include "ui/alert.h"
#include "ui/button.h"
#include "ui/entry.h"
#include "ui/label.h"
#include "ui/listitem.h"
#include "ui/message.h"
#include "ui/resize_event.h"
@ -69,8 +71,9 @@ private:
DataRecoveryView::DataRecoveryView(crash::DataRecovery* dataRecovery)
: m_dataRecovery(dataRecovery)
, m_openButton("Recover Sprite")
, m_deleteButton("Delete")
, m_openButton(Strings::recover_files_recover_sprite().c_str())
, m_deleteButton(Strings::recover_files_delete())
, m_refreshButton(Strings::recover_files_refresh())
{
m_listBox.setMultiselect(true);
m_view.setExpansive(true);
@ -79,6 +82,7 @@ DataRecoveryView::DataRecoveryView(crash::DataRecovery* dataRecovery)
HBox* hbox = new HBox;
hbox->addChild(&m_openButton);
hbox->addChild(&m_deleteButton);
hbox->addChild(&m_refreshButton);
addChild(hbox);
addChild(&m_view);
@ -103,23 +107,66 @@ DataRecoveryView::DataRecoveryView(crash::DataRecovery* dataRecovery)
m_openButton.Click.connect(base::Bind(&DataRecoveryView::onOpen, this));
m_openButton.DropDownClick.connect(base::Bind<void>(&DataRecoveryView::onOpenMenu, this));
m_deleteButton.Click.connect(base::Bind(&DataRecoveryView::onDelete, this));
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));
}
void DataRecoveryView::fillList()
void DataRecoveryView::refreshListNotification()
{
fillList();
layout();
}
void DataRecoveryView::clearList()
{
WidgetsList children = m_listBox.children();
for (auto child : children) {
m_listBox.removeChild(child);
child->deferDelete();
}
}
void DataRecoveryView::fillList()
{
clearList();
fillListWith(true);
fillListWith(false);
}
void DataRecoveryView::fillListWith(const bool crashes)
{
bool first = true;
for (auto& session : m_dataRecovery->sessions()) {
if (session->isEmpty())
if ((session->isEmpty()) ||
(crashes && !session->isCrashedSession()) ||
(!crashes && session->isCrashedSession()))
continue;
auto sep = new SeparatorInView(session->name(), HORIZONTAL);
if (first) {
first = false;
// Separator for "crash sessions" vs "old sessions"
auto sep = new Separator(
(crashes ? Strings::recover_files_crash_sessions():
Strings::recover_files_old_sessions()), HORIZONTAL);
sep->InitTheme.connect(
[sep]{
sep->setStyle(skin::SkinTheme::instance()->styles.separatorInViewReverse());
sep->setBorder(sep->border() + gfx::Border(0, 8, 0, 8)*guiscale());
});
sep->initTheme();
m_listBox.addChild(sep);
}
std::string title = session->name();
if (session->version() != VERSION)
title =
fmt::format(Strings::recover_files_incompatible(),
title, session->version());
auto sep = new SeparatorInView(title, HORIZONTAL);
sep->InitTheme.connect(
[sep]{
sep->setBorder(sep->border() + gfx::Border(0, 8, 0, 8)*guiscale());
@ -133,13 +180,14 @@ void DataRecoveryView::fillList()
}
}
if (m_listBox.getItemsCount() == 0)
// If there are no crash items, we call Empty() signal
if (crashes && first)
Empty();
}
std::string DataRecoveryView::getTabText()
{
return "Data Recovery";
return Strings::recover_files_title();
}
TabIcon DataRecoveryView::getTabIcon()
@ -197,8 +245,8 @@ void DataRecoveryView::onOpenMenu()
gfx::Rect bounds = m_openButton.bounds();
Menu menu;
MenuItem rawFrames("Raw Images as Frames");
MenuItem rawLayers("Raw Images as Layers");
MenuItem rawFrames(Strings::recover_files_raw_images_as_frames());
MenuItem rawLayers(Strings::recover_files_raw_images_as_layers());
menu.addChild(&rawFrames);
menu.addChild(&rawLayers);
@ -210,7 +258,7 @@ void DataRecoveryView::onOpenMenu()
void DataRecoveryView::onDelete()
{
std::vector<Item*> items;
std::vector<Item*> items; // Items to delete.
for (auto widget : m_listBox.children()) {
if (!widget->isSelected())
@ -238,10 +286,24 @@ void DataRecoveryView::onDelete()
}
onChangeSelection();
// Check if there is no more crash sessions
if (!thereAreCrashSessions())
Empty();
m_listBox.layout();
m_view.updateView();
}
void DataRecoveryView::onRefresh()
{
clearList();
onChangeSelection();
m_listBox.addChild(new ListItem("Loading..."));
layout();
m_dataRecovery->launchSearch();
}
void DataRecoveryView::onChangeSelection()
{
int count = 0;
@ -256,10 +318,27 @@ void DataRecoveryView::onChangeSelection()
m_deleteButton.setEnabled(count > 0);
m_openButton.setEnabled(count > 0);
if (count < 2)
m_openButton.mainButton()->setText("Recover Sprite");
else
m_openButton.mainButton()->setTextf("Recover %d Sprites", count);
if (count < 2) {
m_openButton.mainButton()->setText(
fmt::format(Strings::recover_files_recover_sprite(), count));
}
else {
m_openButton.mainButton()->setText(
fmt::format(Strings::recover_files_recover_n_sprites(), count));
}
}
bool DataRecoveryView::thereAreCrashSessions() const
{
for (auto widget : m_listBox.children()) {
if (auto item = dynamic_cast<const Item*>(widget)) {
if (item &&
item->session() &&
item->session()->isCrashedSession())
return true;
}
}
return false;
}
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -28,6 +29,10 @@ namespace app {
public:
DataRecoveryView(crash::DataRecovery* dataRecovery);
// Called after the "Refresh" button is pressed (onRefresh) and
// the crash::DataRecovery::SessionsListIsReady signal is received.
void refreshListNotification();
// TabView implementation
std::string getTabText() override;
TabIcon getTabIcon() override;
@ -38,24 +43,29 @@ namespace app {
bool onCloseView(Workspace* workspace, bool quitting) override;
void onTabPopup(Workspace* workspace) override;
// Triggered when the list is empty (because the user deleted all
// sessions).
// Triggered when the list is of crashed sessions is empty (or
// because the user deleted all sessions that crashed).
obs::signal<void()> Empty;
private:
void clearList();
void fillList();
void fillListWith(const bool crashes);
void onOpen();
void onOpenRaw(crash::RawImagesAs as);
void onOpenMenu();
void onDelete();
void onRefresh();
void onChangeSelection();
bool thereAreCrashSessions() const;
crash::DataRecovery* m_dataRecovery;
ui::View m_view;
ui::ListBox m_listBox;
DropDownButton m_openButton;
ui::Button m_deleteButton;
ui::Button m_refreshButton;
};
} // namespace app

View File

@ -15,6 +15,7 @@
#include "app/app_menus.h"
#include "app/commands/commands.h"
#include "app/commands/params.h"
#include "app/crash/data_recovery.h"
#include "app/i18n/strings.h"
#include "app/ui/data_recovery_view.h"
#include "app/ui/main_window.h"
@ -47,7 +48,7 @@ HomeView::HomeView()
#ifdef ENABLE_NEWS
, m_news(new NewsListBox)
#endif
, m_dataRecovery(nullptr)
, m_dataRecovery(App::instance()->dataRecovery())
, m_dataRecoveryView(nullptr)
{
newFile()->Click.connect(base::Bind(&HomeView::onNewFile, this));
@ -61,7 +62,6 @@ HomeView::HomeView()
#endif
checkUpdate()->setVisible(false);
recoverSpritesPlaceholder()->setVisible(false);
InitTheme.connect(
[this]{
@ -82,11 +82,18 @@ HomeView::~HomeView()
#endif
}
void HomeView::showDataRecovery(crash::DataRecovery* dataRecovery)
void HomeView::dataRecoverySessionsAreReady()
{
#ifdef ENABLE_DATA_RECOVERY
m_dataRecovery = dataRecovery;
recoverSpritesPlaceholder()->setVisible(true);
if (App::instance()->dataRecovery()->hasRecoverySessions()) {
// We highlight the "Recover Files" options because we came from a crash
SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
recoverSprites()->setStyle(theme->styles.workspaceUpdateLink());
layout();
}
if (m_dataRecoveryView) {
m_dataRecoveryView->refreshListNotification();
}
#endif
}
@ -195,11 +202,13 @@ void HomeView::onRecoverSprites()
if (!m_dataRecoveryView) {
m_dataRecoveryView = new DataRecoveryView(m_dataRecovery);
// Hide the "Recover Lost Sprites" button when the
// DataRecoveryView is empty.
// Restore the "Recover Files" link style when the
// DataRecoveryView is empty (so there is no more warning icon on
// it).
m_dataRecoveryView->Empty.connect(
[this]{
recoverSpritesPlaceholder()->setVisible(false);
SkinTheme* theme = static_cast<SkinTheme*>(this->theme());
recoverSprites()->setStyle(theme->styles.workspaceLink());
layout();
});
}

View File

@ -42,7 +42,9 @@ namespace app {
HomeView();
~HomeView();
void showDataRecovery(crash::DataRecovery* dataRecovery);
// When crash::DataRecovery finish to search for sessions, this
// function is called.
void dataRecoverySessionsAreReady();
// TabView implementation
std::string getTabText() override;

View File

@ -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
@ -14,6 +14,7 @@
#include "app/app.h"
#include "app/app_menus.h"
#include "app/commands/commands.h"
#include "app/crash/data_recovery.h"
#include "app/i18n/strings.h"
#include "app/ini_file.h"
#include "app/modules/editors.h"
@ -327,9 +328,9 @@ void MainWindow::popTimeline()
setTimelineVisibility(true);
}
void MainWindow::showDataRecovery(crash::DataRecovery* dataRecovery)
void MainWindow::dataRecoverySessionsAreReady()
{
getHomeView()->showDataRecovery(dataRecovery);
getHomeView()->dataRecoverySessionsAreReady();
}
bool MainWindow::onProcessMessage(ui::Message* msg)

View File

@ -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
@ -81,7 +81,9 @@ namespace app {
void setTimelineVisibility(bool visible);
void popTimeline();
void showDataRecovery(crash::DataRecovery* dataRecovery);
// When crash::DataRecovery finish to search for sessions, this
// function is called.
void dataRecoverySessionsAreReady();
// TabsDelegate implementation.
bool isTabModified(Tabs* tabs, TabView* tabView) override;