-
Notifications
You must be signed in to change notification settings - Fork 385
/
autorecover.py
231 lines (205 loc) · 8.69 KB
/
autorecover.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# -*- coding: utf-8 -*-
# This file is part of MyPaint.
# Copyright (C) 2015-2018 by the MyPaint Development Team
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""Autorecovery UI"""
from __future__ import division, print_function
import weakref
import os.path
from gettext import gettext as _
import shutil
import logging
from lib.gibindings import Gtk
import lib.document
import lib.helpers
import lib.errors
from lib.pycompat import unicode
logger = logging.getLogger(__name__)
class Presenter (object):
"""Shows and runs a dialog, allowing the user to resume autosaves.
See also: lib.document.Document.resume_from_autosave().
"""
_THUMBNAIL_SIZE = 128
_RESPONSE_CANCEL = 0
_RESPONSE_CONTINUE = 1
_LISTSTORE_THUMBNAIL_COLUMN = 0
_LISTSTORE_DESCRIPTION_COLUMN = 1
_LISTSTORE_PATH_COLUMN = 2
_CHECK_AT_STARTUP_PREFS_KEY = "autorecovery.check_at_startup"
def __init__(self, app, **kwargs):
"""Initialize for the main app and its working doc."""
super(Presenter, self).__init__(**kwargs)
self._app = weakref.proxy(app)
builder_xml = os.path.splitext(__file__)[0] + ".glade"
builder = Gtk.Builder()
builder.set_translation_domain("mypaint")
builder.add_from_file(builder_xml)
self._dialog = builder.get_object("recovery_dialog")
self._treeview = builder.get_object("recovery_treeview")
self._liststore = builder.get_object("recovery_liststore")
self._recover_button = builder.get_object("recover_autosave_button")
self._delete_button = builder.get_object("delete_autosave_button")
at_start_checkbutton = builder.get_object("at_startup_checkbutton")
at_start_checkbutton.set_active(self.check_at_startup)
builder.connect_signals(self)
@property
def check_at_startup(self):
prefs = self._app.preferences
return bool(prefs.get(self._CHECK_AT_STARTUP_PREFS_KEY, True))
@check_at_startup.setter
def check_at_startup(self, value):
prefs = self._app.preferences
prefs[self._CHECK_AT_STARTUP_PREFS_KEY] = bool(value)
def _reload_liststore(self):
"""Load the available autosaves"""
self._liststore.clear()
doc = self._app.doc
autosaves = []
for asav in lib.document.get_available_autosaves():
# Another instance may be working in there.
if asav.cache_in_use:
logger.debug(
"Ignoring %r: in use",
asav.path,
)
continue
# Skip autosaves inside the current doc's cache folder.
asav_cachedir = os.path.dirname(asav.path)
asav_cachedir = os.path.normpath(os.path.realpath(asav_cachedir))
doc_cachedir = doc.model.cache_dir
doc_cachedir = os.path.normpath(os.path.realpath(doc_cachedir))
if doc_cachedir == asav_cachedir:
logger.debug(
"Ignoring %r: belongs to current working doc.",
asav.path,
)
continue
logger.debug(
"Making %r available for autosave recovery.",
asav.path,
)
autosaves.append(asav)
s = self._THUMBNAIL_SIZE
if len(autosaves) >= 3:
s /= 2
autosaves.sort(key=(lambda i: i.last_modified))
for autosave in reversed(autosaves):
thumb = autosave.thumbnail
if thumb is None:
flags = 0
icon_theme = Gtk.IconTheme.get_default()
thumb = icon_theme.load_icon("image-missing", s, flags)
if thumb is not None:
thumb = lib.helpers.scale_proportionally(thumb, s, s)
thumb = lib.helpers.pixbuf_thumbnail(thumb, s, s, alpha=True)
desc = autosave.get_description()
assert isinstance(desc, unicode)
assert isinstance(autosave.path, unicode)
self._liststore.append((thumb, desc, autosave.path))
return autosaves
def run(self, startup=False):
"""Show and run the dialog, and possibly resume an autosave.
:param bool startup: indicates that MyPaint is starting up.
"""
# Don't run at startup if asked not to.
if startup:
if not self.check_at_startup:
return
# Only run if there are autosaves which can be recovered.
autosaves = self._reload_liststore()
if not autosaves:
if not startup:
cache_root = lib.document.get_app_cache_root()
self._app.message_dialog(
_(u"No backups were found in the cache."),
title = _(u"No Available Backups"),
investigate_dir = cache_root,
investigate_str = _(u"Open the Cache Folder…"),
message_type = Gtk.MessageType.ERROR,
)
return
doc = self._app.doc
# Get the user to pick an autosave to recover
autosave = None
error = None
try:
self._dialog.set_transient_for(self._app.drawWindow)
self._dialog.show_all()
result = self._dialog.run()
if result == self._RESPONSE_CONTINUE:
autosave = self._get_selected_autosave()
if autosave is not None:
logger.info("Recovering %r...", autosave)
try:
doc.model.resume_from_autosave(autosave.path)
except lib.errors.FileHandlingError as e:
error = e
else:
doc.reset_view(True, True, True)
finally:
self._dialog.hide()
# If an error was detected, tell the user about it.
# They'll be given a new working doc & cache automatically.
if error:
self._app.message_dialog(
unicode(error),
title = _(u"Backup Recovery Failed"),
investigate_dir = error.investigate_dir,
investigate_str = _(u"Open the Backup’s Folder…"),
message_type = Gtk.MessageType.ERROR,
)
# If it loaded OK, get the user to save the recovered file ASAP.
elif autosave:
fh = self._app.filehandler
fh.set_filename(None)
lastmod = autosave.last_modified
strftime_tmpl = "%Y-%m-%d %H%M%S"
sugg_name_tmpl = _(u"Recovered file from {iso_datetime}.ora")
sugg_name = sugg_name_tmpl.format(
iso_datetime = lastmod.strftime(strftime_tmpl),
)
fh.save_as_dialog(fh.save_file, suggested_filename=sugg_name)
def _recovery_treeview_row_activated_cb(self, treeview, treepath, column):
"""When a row's double-clicked, resume work on that doc."""
self._dialog.response(self._RESPONSE_CONTINUE)
def _recovery_tree_selection_changed_cb(self, sel):
"""When a row's clicked, update button sensitivities etc."""
self._update_buttons()
def _update_buttons(self):
autosave = self._get_selected_autosave()
sensitive = False
if autosave is not None:
sensitive = not autosave.cache_in_use
self._recover_button.set_sensitive(sensitive)
self._delete_button.set_sensitive(sensitive)
def _get_selected_autosave(self):
sel = self._treeview.get_selection()
model, iter = sel.get_selected()
if iter is None:
return None
path = model.get_value(iter, self._LISTSTORE_PATH_COLUMN)
if not isinstance(path, unicode):
path = path.decode("utf-8")
assert isinstance(path, unicode)
if not os.path.isdir(path):
return None
return lib.document.AutosaveInfo.new_for_path(path)
def _delete_autosave_button_clicked_cb(self, button):
autosave = self._get_selected_autosave()
if autosave is None or autosave.cache_in_use \
or not os.path.isdir(autosave.path):
return
logger.info("Recursively deleting %r...", autosave.path)
try:
shutil.rmtree(autosave.path, ignore_errors=True)
except Exception:
logger.exception("Deleting %r failed.", autosave.path)
else:
logger.info("Deleted %r successfully.", autosave.path)
self._reload_liststore()
def _at_startup_checkbutton_toggled_cb(self, toggle):
self.check_at_startup = bool(toggle.get_active())