mirror of
https://github.com/lalbornoz/roar.git
synced 2024-12-22 12:26:37 +00:00
Implements automatic snapshotting & restoring from snapshots.
This commit is contained in:
parent
451a708d7a
commit
19957a2006
@ -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
|
||||
|
@ -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"]):
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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]
|
||||
|
1
roar.py
1
roar.py
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user