Implements automatic snapshotting & restoring from snapshots.

This commit is contained in:
Lucio Andrés Illanes Albornoz 2019-09-28 19:45:45 +02:00
parent 451a708d7a
commit 19957a2006
7 changed files with 125 additions and 19 deletions

View File

@ -20,7 +20,6 @@
Release roadmap:
1) {copy,cut,delete,insert from,paste}, edit asset in new canvas, import from {canvas,object}
2) operators: crop, scale, shift, slice
3) auto{load,save} & {backup,restore}
4) tools: unicode block elements
3) tools: unicode block elements
vim:ff=dos tw=0

View File

@ -79,6 +79,9 @@ class RoarCanvasCommands(RoarCanvasCommandsFile, RoarCanvasCommandsEdit, RoarCan
if "dirty" in self.lastPanelState \
and self.lastPanelState["dirty"]:
textItems.append("*")
if "backupStatus" in self.lastPanelState \
and self.lastPanelState["backupStatus"] == True:
textItems.append("Saving backup...")
self.parentFrame.statusBar.SetStatusText(" | ".join(textItems))
if ("undoInhibit" in self.lastPanelState) \
and (self.lastPanelState["undoInhibit"]):

View File

@ -18,10 +18,10 @@ except ImportError:
from GuiFrame import GuiCommandDecorator, GuiSubMenuDecorator, NID_MENU_SEP
from RtlPlatform import getLocalConfPathName
import io, os, wx
import datetime, io, os, stat, wx
class RoarCanvasCommandsFile():
def _import(self, f, newDirty, pathName):
def _import(self, f, newDirty, pathName, emptyPathName=False):
rc = False
self.parentCanvas.SetCursor(wx.Cursor(wx.CURSOR_WAIT))
try:
@ -29,7 +29,11 @@ class RoarCanvasCommandsFile():
if rc:
self.parentCanvas.update(newSize, False, newMap, dirty=newDirty)
self.parentCanvas.dirty = newDirty
self.canvasPathName = newPathName
if not emptyPathName:
self.canvasPathName = newPathName
else:
self.canvasPathName = None
self.parentCanvas._snapshotsReset()
self.update(dirty=self.parentCanvas.dirty, pathName=self.canvasPathName, undoLevel=-1)
self.parentCanvas.canvas.journal.resetCursor()
self.parentCanvas.canvas.journal.resetUndo()
@ -41,7 +45,7 @@ class RoarCanvasCommandsFile():
self.parentCanvas.SetCursor(wx.Cursor(wx.NullCursor))
return rc, newPathName
def _importFile(self, f, newDirty, wildcard):
def _importFile(self, f, newDirty, wildcard, emptyPathName=False):
with wx.FileDialog(self.parentCanvas, "Open...", os.getcwd(), "", wildcard, wx.FD_OPEN) as dialog:
if self.lastDir != None:
dialog.SetDirectory(self.lastDir)
@ -49,7 +53,7 @@ class RoarCanvasCommandsFile():
return False, None
elif self._promptSaveChanges():
pathName = dialog.GetPath(); self.lastDir = os.path.dirname(pathName); self._storeLastDir(self.lastDir);
return self._import(f, newDirty, pathName)
return self._import(f, newDirty, pathName, emptyPathName=emptyPathName)
return False, None
def _loadLastDir(self):
@ -65,6 +69,11 @@ class RoarCanvasCommandsFile():
for lastFile in inFile.readlines():
self._pushRecent(lastFile.rstrip("\r\n"), False)
def _popSnapshot(self, pathName):
if pathName in self.snapshots.keys():
self.canvasRestore.attrDict["menu"].Delete(self.snapshots[pathName]["menuItemId"])
del self.snapshots[pathName]
def _promptSaveChanges(self):
if self.parentCanvas.dirty:
message = "Do you want to save changes to {}?".format(self.canvasPathName if self.canvasPathName != None else "(Untitled)")
@ -88,7 +97,7 @@ class RoarCanvasCommandsFile():
if (numLastFiles + 1) > 8:
self.canvasOpenRecent.attrDict["menu"].Delete(self.lastFiles[0]["menuItemId"])
del self.lastFiles[0]
menuItemWindow = self.canvasOpenRecent.attrDict["menu"].Insert(self.canvasOpenRecent.attrDict["menu"].GetMenuItemCount() - 2, menuItemId, "{}".format(pathName), pathName)
menuItemWindow = self.canvasOpenRecent.attrDict["menu"].Insert(self.canvasOpenRecent.attrDict["menu"].GetMenuItemCount() - 2, menuItemId, pathName, pathName)
self.parentFrame.menuItemsById[self.canvasOpenRecent.attrDict["id"]].Enable(True)
self.parentFrame.Bind(wx.EVT_MENU, lambda event: self.canvasOpenRecent(event, pathName), menuItemWindow)
self.lastFiles += [{"menuItemId":menuItemId, "menuItemWindow":menuItemWindow, "pathName":pathName}]
@ -98,6 +107,21 @@ class RoarCanvasCommandsFile():
for lastFile in [l["pathName"] for l in self.lastFiles]:
print(lastFile, file=outFile)
def _pushSnapshot(self, pathName):
menuItemId = wx.NewId()
if not (pathName in self.snapshots.keys()):
label = datetime.datetime.fromtimestamp(os.stat(pathName)[stat.ST_MTIME]).strftime("%c")
menuItemWindow = self.canvasRestore.attrDict["menu"].Insert(self.canvasRestore.attrDict["menu"].GetMenuItemCount() - 2, menuItemId, label)
self.parentFrame.menuItemsById[self.canvasRestore.attrDict["id"]].Enable(True)
self.parentFrame.Bind(wx.EVT_MENU, lambda event: self.canvasRestore(event, pathName), menuItemWindow)
self.snapshots[pathName] = {"menuItemId":menuItemId, "menuItemWindow":menuItemWindow}
def _resetSnapshots(self):
for pathName in list(self.snapshots.keys()):
self._popSnapshot(pathName)
if self.canvasRestore.attrDict["id"] in self.parentFrame.menuItemsById:
self.parentFrame.menuItemsById[self.canvasRestore.attrDict["id"]].Enable(False)
def _storeLastDir(self, pathName):
localConfFileName = getLocalConfPathName("RecentDir.txt")
with open(localConfFileName, "w", encoding="utf-8") as outFile:
@ -194,7 +218,7 @@ class RoarCanvasCommandsFile():
def canvasImportAnsi_(pathName):
rc, error = self.parentCanvas.canvas.importStore.importAnsiFile(pathName)
return (rc, error, self.parentCanvas.canvas.importStore.outMap, pathName, self.parentCanvas.canvas.importStore.inSize)
self._importFile(canvasImportAnsi_, True, "ANSI files (*.ans;*.txt)|*.ans;*.txt|All Files (*.*)|*.*")
self._importFile(canvasImportAnsi_, True, "ANSI files (*.ans;*.txt)|*.ans;*.txt|All Files (*.*)|*.*", emptyPathName=True)
@GuiCommandDecorator("Import from clipboard", "Import from &clipboard", None, None, None)
def canvasImportFromClipboard(self, event):
@ -209,15 +233,14 @@ class RoarCanvasCommandsFile():
else:
rc, error = False, "Clipboard does not contain text data and/or cannot be opened"
return (rc, error, self.parentCanvas.canvas.importStore.outMap, None, self.parentCanvas.canvas.importStore.inSize)
if self._promptSaveChanges():
self._import(canvasImportFromClipboard_, True, None)
self._import(canvasImportFromClipboard_, True, None, emptyPathName=True)
@GuiCommandDecorator("Import SAUCE...", "Import &SAUCE...", None, None, None)
def canvasImportSauce(self, event):
def canvasImportSauce_(pathName):
rc, error = self.parentCanvas.canvas.importStore.importSauceFile(pathName)
return (rc, error, self.parentCanvas.canvas.importStore.outMap, pathName, self.parentCanvas.canvas.importStore.inSize)
self._importFile(canvasImportSauce_, True, "SAUCE files (*.ans;*.txt)|*.ans;*.txt|All Files (*.*)|*.*")
self._importFile(canvasImportSauce_, True, "SAUCE files (*.ans;*.txt)|*.ans;*.txt|All Files (*.*)|*.*", emptyPathName=True)
@GuiCommandDecorator("New", "&New", ["", wx.ART_NEW], [wx.ACCEL_CTRL, ord("N")], None)
def canvasNew(self, event, newCanvasSize=None):
@ -250,6 +273,22 @@ class RoarCanvasCommandsFile():
numLastFile = [i for i in range(len(self.lastFiles)) if self.lastFiles[i]["pathName"] == pathName][0]
self.canvasOpenRecent.attrDict["menu"].Delete(self.lastFiles[numLastFile]["menuItemId"]); del self.lastFiles[numLastFile];
@GuiSubMenuDecorator("Restore Snapshot", "Res&tore Snapshot", None, None, False)
def canvasRestore(self, event, pathName=None):
def canvasImportmIRC(pathName_):
rc, error = self.parentCanvas.canvas.importStore.importTextFile(pathName)
return (rc, error, self.parentCanvas.canvas.importStore.outMap, self.canvasPathName, self.parentCanvas.canvas.importStore.inSize)
if self._promptSaveChanges():
rc, newPathName = self._import(canvasImportmIRC, False, self.canvasPathName)
@GuiCommandDecorator("Restore from file", "Restore from &file", None, None, False)
def canvasRestoreFile(self, event):
def canvasImportmIRC(pathName):
rc, error = self.parentCanvas.canvas.importStore.importTextFile(pathName)
return (rc, error, self.parentCanvas.canvas.importStore.outMap, pathName, self.parentCanvas.canvas.importStore.inSize)
if self._promptSaveChanges():
rc, newPathName = self._import(canvasImportmIRC, False, self.canvasPathName)
@GuiCommandDecorator("Save", "&Save", ["", wx.ART_FILE_SAVE], [wx.ACCEL_CTRL, ord("S")], None)
def canvasSave(self, event, newDirty=False):
if self.canvasPathName == None:
@ -284,11 +323,11 @@ class RoarCanvasCommandsFile():
return False
def __init__(self):
self.imgurApiKey, self.lastFiles, self.lastDir = ImgurApiKey.imgurApiKey if haveImgurApiKey else None, [], None
self.imgurApiKey, self.lastFiles, self.lastDir, self.snapshots = ImgurApiKey.imgurApiKey if haveImgurApiKey else None, [], None, {}
self.accels = ()
self.menus = (
("&File",
self.canvasNew, self.canvasOpen, self.canvasOpenRecent, self.canvasSave, self.canvasSaveAs, NID_MENU_SEP,
self.canvasNew, self.canvasOpen, self.canvasOpenRecent, self.canvasRestore, self.canvasSave, self.canvasSaveAs, NID_MENU_SEP,
("&Export...", self.canvasExportAsAnsi, self.canvasExportToClipboard, self.canvasExportImgur, self.canvasExportPastebin, self.canvasExportAsPng,),
("&Import...", self.canvasImportAnsi, self.canvasImportFromClipboard, self.canvasImportSauce,),
NID_MENU_SEP,

View File

@ -5,9 +5,11 @@
#
from GuiWindow import GuiWindow
from Rtl import natural_sort
from RtlPlatform import getLocalConfPathName
from ToolObject import ToolObject
from ToolText import ToolText
import copy, json, wx, sys
import copy, hashlib, json, os, re, time, wx, sys
class RoarCanvasWindowDropTarget(wx.TextDropTarget):
def done(self):
@ -60,6 +62,50 @@ class RoarCanvasWindow(GuiWindow):
eventDc.SetDeviceOrigin(*eventDcOrigin)
self.commands.update(dirty=self.dirty, cellPos=self.brushPos, undoLevel=self.canvas.journal.patchesUndoLevel)
def _snapshotsReset(self):
self._snapshotFiles, self._snapshotsUpdateLast = [], time.time()
self.commands._resetSnapshots()
if self.commands.canvasPathName != None:
canvasPathName = os.path.abspath(self.commands.canvasPathName)
canvasFileName = os.path.basename(canvasPathName)
canvasPathNameHash = hashlib.sha1(canvasPathName.encode()).hexdigest()
self._snapshotsDirName = os.path.join(getLocalConfPathName(), "{}_{}".format(canvasFileName, canvasPathNameHash))
if os.path.exists(self._snapshotsDirName):
for snapshotFile in natural_sort([f for f in os.listdir(self._snapshotsDirName) \
if (re.match(r'snapshot\d+\.txt$', f)) and os.path.isfile(os.path.join(self._snapshotsDirName, f))]):
self.commands._pushSnapshot(os.path.join(self._snapshotsDirName, snapshotFile))
else:
self._snapshotsDirName = None
def _snapshotsUpdate(self):
if self._snapshotsDirName != None:
t = time.time()
if (t > self._snapshotsUpdateLast) and ((t - self._snapshotsUpdateLast) >= (5 * 60)):
try:
if not os.path.exists(self._snapshotsDirName):
os.makedirs(self._snapshotsDirName)
self._snapshotFiles = natural_sort([f for f in os.listdir(self._snapshotsDirName)
if (re.match(r'snapshot\d+\.txt$', f)) and os.path.isfile(os.path.join(self._snapshotsDirName, f))])
if self._snapshotFiles != []:
snapshotsCount, snapshotIndex = len(self._snapshotFiles), abs(int(re.match(r'snapshot(\d+)\.txt$', self._snapshotFiles[-1])[1])) + 1
else:
snapshotsCount, snapshotIndex = 0, 1
snapshotPathName = os.path.join(self._snapshotsDirName, "snapshot{}.txt".format(snapshotIndex));
self.commands.update(snapshotStatus=True)
with open(snapshotPathName, "w", encoding="utf-8") as outFile:
self.SetCursor(wx.Cursor(wx.CURSOR_WAIT))
self.canvas.exportStore.exportTextFile(self.canvas.map, self.canvas.size, outFile)
self.SetCursor(wx.Cursor(wx.NullCursor))
self.commands.update(snapshotStatus=False); self._snapshotsUpdateLast = time.time();
self._snapshotFiles += [os.path.basename(snapshotPathName)];
self.commands._pushSnapshot(snapshotPathName)
if len(self._snapshotFiles) > 72:
for snapshotFile in self._snapshotFiles[:len(self._snapshotFiles) - 8]:
self.commands._popSnapshot(os.path.join(self._snapshotsDirName, snapshotFile))
os.remove(os.path.join(self._snapshotsDirName, snapshotFile)); snapshotsCount -= 1;
except:
print("Exception during _snapshotsUpdate(): {}".format(sys.exc_info()[1]))
def applyOperator(self, currentTool, mapPoint, mouseLeftDown, mousePoint, operator, viewRect):
eventDc, patches, patchesCursor, rc = self.backend.getDeviceContext(self.GetClientSize(), self), None, None, True
if (currentTool.__class__ == ToolObject) and (currentTool.toolState >= currentTool.TS_SELECT):
@ -81,16 +127,19 @@ class RoarCanvasWindow(GuiWindow):
rc, patches, patchesCursor = currentTool.onSelectEvent(self.canvas, (0, 0), True, wx.MOD_NONE, None, currentTool.targetRect)
patchesCursor = [] if patchesCursor == None else patchesCursor
patchesCursor += currentTool._drawSelectRect(currentTool.targetRect)
self._applyPatches(eventDc, patches, patchesCursor, rc)
else:
patches = []
for numRow in range(len(region)):
for numCol in range(len(region[numRow])):
patches += [[numCol, numRow, *region[numRow][numCol]]]
self._applyPatches(eventDc, patches, patchesCursor, rc)
self._applyPatches(eventDc, patches, patchesCursor, rc)
if (patches != None) and (len(patches) > 0):
self._snapshotsUpdate()
return rc
def applyTool(self, eventDc, eventMouse, keyChar, keyCode, keyModifiers, mapPoint, mouseDragging, mouseLeftDown, mouseRightDown, tool, viewRect, force=False):
dirty, patches, patchesCursor, rc = False, None, None, False
patches, patchesCursor, rc = None, None, False
if eventMouse:
self.lastCellState = None if force else self.lastCellState
if ((mapPoint[0] < self.canvas.size[0]) and (mapPoint[1] < self.canvas.size[1])) \
@ -118,6 +167,8 @@ class RoarCanvasWindow(GuiWindow):
self.commands.update(toolName=newToolName, undoInhibit=False)
else:
self.commands.update(undoInhibit=False)
if (patches != None) and (len(patches) > 0):
self._snapshotsUpdate()
return rc
def onKeyboardInput(self, event):
@ -237,6 +288,8 @@ class RoarCanvasWindow(GuiWindow):
eventDc.SetDeviceOrigin(*eventDcOrigin)
self.Scroll(*viewRect); self.dirty = dirty;
self.commands.update(dirty=self.dirty, size=newSize, undoLevel=self.canvas.journal.patchesUndoLevel)
if commitUndo:
self._snapshotsUpdate()
def undo(self, redo=False):
deltaPatches = self.canvas.journal.popUndo() if not redo else self.canvas.journal.popRedo()
@ -276,5 +329,6 @@ class RoarCanvasWindow(GuiWindow):
self.dropTarget = RoarCanvasWindowDropTarget(self)
self.SetDropTarget(self.dropTarget)
self.Bind(wx.EVT_MOUSEWHEEL, self.onMouseWheel)
self._snapshotsReset()
# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120

View File

@ -62,10 +62,15 @@ class RoarClient(GuiFrame):
for menuItem in self.canvasPanel.commands.menus[3][1:]:
menuItemWindow = self.canvasPanel.operatorsMenu.Append(menuItem.attrDict["id"], menuItem.attrDict["label"], menuItem.attrDict["caption"])
self.Bind(wx.EVT_MENU, self.onMenu, menuItemWindow)
self.canvasPanel.commands.canvasOpenRecent.attrDict["menu"].AppendSeparator()
self.canvasPanel.commands.canvasClearRecent.attrDict["id"] = wx.NewId()
self.canvasPanel.commands.canvasOpenRecent.attrDict["menu"].AppendSeparator()
self.canvasPanel.commands.canvasRestore.attrDict["menu"].AppendSeparator()
self.canvasPanel.commands.canvasRestoreFile.attrDict["id"] = wx.NewId()
menuItemWindow = self.canvasPanel.commands.canvasOpenRecent.attrDict["menu"].Append(self.canvasPanel.commands.canvasClearRecent.attrDict["id"], self.canvasPanel.commands.canvasClearRecent.attrDict["label"], self.canvasPanel.commands.canvasClearRecent.attrDict["caption"])
menuItemWindow = self.canvasPanel.commands.canvasRestore.attrDict["menu"].Append(self.canvasPanel.commands.canvasRestoreFile.attrDict["id"], self.canvasPanel.commands.canvasRestoreFile.attrDict["label"], self.canvasPanel.commands.canvasRestoreFile.attrDict["caption"])
self.canvasPanel.commands.canvasOpenRecent.attrDict["menu"].Bind(wx.EVT_MENU, self.canvasPanel.commands.canvasClearRecent, menuItemWindow)
self.canvasPanel.commands.canvasRestore.attrDict["menu"].Bind(wx.EVT_MENU, self.canvasPanel.commands.canvasRestoreFile, menuItemWindow)
self.Bind(wx.EVT_CLOSE, self.onClose); self.Bind(wx.EVT_SIZE, self.onSize);
self.toolBarPanes[0].BestSize(0, 0).Right(); self.toolBarPanes[1].BestSize(0, 0).Right(); self.auiManager.Update();

View File

@ -5,13 +5,18 @@
# This project is licensed under the terms of the MIT licence.
#
import statistics, time
import re, statistics, time
timeState = [None, None, 0, []]
def flatten(l):
return [li for l_ in l for li in l_]
def natural_sort(l):
convert = lambda text: int(text) if text.isdigit() else text.lower()
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
return sorted(l, key=alphanum_key)
def timePrint(pfx):
timeState[0] = time.time() if timeState[0] == None else timeState[0]
t1 = time.time(); td = t1 - timeState[0]

View File

@ -25,6 +25,7 @@ def main(*argv):
if (len(argv) >= 2) and (argv[1].endswith(".lst")):
roarClient.assetsWindow._load_list(argv[1])
roarClient.canvasPanel.commands.canvasPathName = argv[0]
roarClient.canvasPanel._snapshotsReset()
rc, error = roarClient.canvasPanel.canvas.importStore.importTextFile(argv[0])
if rc:
roarClient.canvasPanel.update(roarClient.canvasPanel.canvas.importStore.inSize, False, roarClient.canvasPanel.canvas.importStore.outMap, dirty=False)