Add progress bars to world save and undo revision close.

Tempted to write a progress manager type class the next time this comes up.

Progress counts are a little bit off.
This commit is contained in:
David Vierra 2015-05-29 11:41:02 -10:00
parent 3861f7f5d2
commit d74ebcf932
7 changed files with 144 additions and 21 deletions

View File

@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function
import contextlib
import logging
from PySide import QtGui
from mcedit2.util.showprogress import showProgress
log = logging.getLogger(__name__)
@ -55,5 +56,7 @@ class SimpleRevisionCommand(QtGui.QUndoCommand):
self.previousRevision = self.editorSession.currentRevision
self.editorSession.beginUndo()
yield
self.editorSession.commitUndo()
task = self.editorSession.commitUndoIter()
showProgress(QtGui.qApp.tr("Writing undo history"), task)
self.currentRevision = self.editorSession.currentRevision

View File

@ -16,6 +16,7 @@ from mcedit2.panels.player import PlayerPanel
from mcedit2.panels.worldinfo import WorldInfoPanel
from mcedit2.util.dialogs import NotImplementedYet
from mcedit2.util.directories import getUserSchematicsDirectory
from mceditlib.util import exhaust
from mceditlib.util.lazyprop import weakrefprop
from mcedit2.util.raycast import rayCastInBounds
from mcedit2.util.showprogress import showProgress
@ -495,7 +496,9 @@ class EditorSession(QtCore.QObject):
def save(self):
self.undoStack.clearUndoBlock()
self.worldEditor.saveChanges()
saveTask = self.worldEditor.saveChangesIter()
showProgress("Saving...", saveTask)
self.dirty = False
# - Edit -
@ -670,7 +673,11 @@ class EditorSession(QtCore.QObject):
self.worldEditor.beginUndo()
def commitUndo(self):
self.worldEditor.commitUndo()
exhaust(self.commitUndoIter())
def commitUndoIter(self):
for status in self.worldEditor.commitUndoIter():
yield status
self.revisionChanged.emit(self.worldEditor.currentRevision)
def undoForward(self):

View File

@ -24,7 +24,7 @@ from mceditlib.selection import BoundingBox
from mceditlib import nbtattr
from mceditlib.exceptions import PlayerNotFound, ChunkNotPresent, LevelFormatError
from mceditlib.revisionhistory import RevisionHistory
from mceditlib.util import exhaust
log = logging.getLogger(__name__)
@ -598,6 +598,9 @@ class AnvilWorldAdapter(object):
self.metadata.dirty = False
def saveChanges(self):
exhaust(self.saveChangesIter())
def saveChangesIter(self):
"""
Write all changes from all revisions into the world folder.
@ -608,8 +611,11 @@ class AnvilWorldAdapter(object):
raise IOError("World is opened read only.")
self.checkSessionLock()
self.revisionHistory.writeAllChanges(self.selectedRevision)
for status in self.revisionHistory.writeAllChangesIter(self.selectedRevision):
yield status
self.selectedRevision = self.revisionHistory.getHead()
yield
def close(self):
"""

View File

@ -268,14 +268,25 @@ class RevisionHistory(object):
return changes
def writeAllChanges(self, requestedRevision=None):
for status in self.writeAllChangesIter(requestedRevision):
pass
def writeAllChangesIter(self, requestedRevision=None):
"""
Write all changes to the root world folder, preserving undo history. The previous head node is no longer
valid after calling writeAllChanges. Specify a revision to only save changes up to and including that
Write all changes to the root world folder, preserving undo history. The world folder
becomes the new head node. The previous head node is no longer valid after calling
writeAllChanges. Specify a revision to only save changes up to and including that
revision.
:return:
:rtype:
"""
# XXXXX wait for async writes to complete here
# Progress counts:
# 0-20: Orphaned chains
# 20-100: History nodes
maxprogress = 100
if isinstance(requestedRevision, RevisionHistoryNode):
requestedIndex = self.nodes.index(requestedRevision)
@ -284,6 +295,7 @@ class RevisionHistory(object):
else:
requestedIndex = requestedRevision
orphanChainProgress = 20
if self.orphanChainIndex is not None:
# Root node is orphaned - collapse orphan chain into it in reverse order
orphanNodes = []
@ -292,8 +304,13 @@ class RevisionHistory(object):
orphanNodes.append(orphanChainNode)
orphanChainNode = orphanChainNode.parentNode
for orphanChainNode in reversed(orphanNodes):
copyToFolder(self.rootFolder, orphanChainNode)
for progress, orphanChainNode in enumProgress(reversed(orphanNodes), 0, 20):
yield (progress, maxprogress, "Collapsing orphaned chain")
copyTask = copyToFolderIter(self.rootFolder, orphanChainNode)
copyTask = rescaleProgress(copyTask, progress, 20/len(orphanNodes))
for current, _, status in copyTask:
yield current, maxprogress, status
self.nodes[self.orphanChainIndex] = self.rootNode
self.orphanChainIndex = None
@ -308,7 +325,7 @@ class RevisionHistory(object):
indexes = xrange(self.rootNodeIndex+1, requestedIndex+1)
log.info("writeAllChanges: moving %s", "forwards" if direction == 1 else "backwards")
for currentIndex in indexes:
for progress, currentIndex in enumProgress(indexes, 20, 80):
# Write all changes from each node into the initial folder. Save the previous
# chunk and file data from the initial folder into a reverse revision.
@ -320,7 +337,11 @@ class RevisionHistory(object):
reverseNode.differences = self.rootNode.differences
self.rootNode.differences = currentNode.getChanges()
copyToFolder(self.rootFolder, currentNode, reverseNode)
copyTask = copyToFolderIter(self.rootFolder, currentNode, reverseNode)
copyTask = rescaleProgress(copyTask, progress, 80 / len(indexes))
for current, _, status in copyTask:
yield current, maxprogress, status
# xxx look ahead one or more nodes to skip some copies
reverseNode.setRevisionInfo(self.rootNode.getRevisionInfo())
@ -339,8 +360,54 @@ class RevisionHistory(object):
currentNode.invalid = True
shutil.rmtree(currentNode.worldFolder.filename, ignore_errors=True)
def copyToFolder(destFolder, sourceNode, presaveNode=None):
for status in copyToFolderIter(destFolder, sourceNode, presaveNode):
pass
def rescaleProgress(iterable, start, end):
"""
Given an iterable that yields (current, maximum, status) tuples, rescales current and maximum
to fit within the range [start, end]. `current` is assumed to start at zero.
:param iterable:
:param start:
:param end:
:return:
"""
d = end - start
for current, maximum, status in iterable:
yield start + current * d / maximum, end, status
def enumProgress(collection, start, end=None):
"""
Iterate through a collection, yielding (progress, value) tuples. `progress` is the value
between `start` and `end` proportional to the progress through the collection.
:param collection:
:param progress:
:return:
"""
if end is None:
end = start
start = 0
if len(collection) == 0:
return
progFraction = (end - start) / len(collection)
for i, value in enumerate(collection):
yield start + i * progFraction, value
def copyToFolderIter(destFolder, sourceNode, presaveNode=None):
# Progress counts:
# 0-10: deleted chunks
# 10-80: new/modified chunks
# 80-90: deleted files
# 90-100: new/modified files
if presaveNode:
presaveFolder = presaveNode.worldFolder
else:
@ -348,16 +415,28 @@ def copyToFolder(destFolder, sourceNode, presaveNode=None):
sourceFolder = sourceNode.worldFolder
maxprogress = 100
# Remove deleted chunks
for cx, cz, dimName in sourceNode.deadChunks:
for deadProgress, (cx, cz, dimName) in enumProgress(sourceNode.deadChunks, 0, 10):
yield deadProgress, maxprogress, "Removing deleted chunks"
if destFolder.containsChunk(cx, cz, dimName):
if presaveFolder and not presaveFolder.containsChunk(cx, cz, dimName):
presaveFolder.writeChunkBytes(cx, cz, dimName, destFolder.readChunkBytes(cx, cz, dimName))
destFolder.deleteChunk(cx, cz, dimName)
# Write new and modified chunks
for dimName in sourceFolder.listDimensions():
for cx, cz in sourceFolder.chunkPositions(dimName):
dims = list(sourceFolder.listDimensions())
dimProgress = 70 / len(dims)
for i, dimName in enumerate(dims):
progress = 10 + i * dimProgress
cPos = list(sourceFolder.chunkPositions(dimName))
for chunkProgress, (cx, cz) in enumProgress(cPos, progress, progress + dimProgress):
yield chunkProgress, maxprogress, "Writing new and modified chunks"
if presaveFolder and not presaveFolder.containsChunk(cx, cz, dimName):
if destFolder.containsChunk(cx, cz, dimName):
presaveFolder.writeChunkBytes(cx, cz, dimName, destFolder.readChunkBytes(cx, cz, dimName))
@ -366,14 +445,20 @@ def copyToFolder(destFolder, sourceNode, presaveNode=None):
destFolder.writeChunkBytes(cx, cz, dimName, sourceFolder.readChunkBytes(cx, cz, dimName))
# Remove deleted files
for path in sourceNode.deadFiles:
for delProgress, path in enumProgress(sourceNode.deadFiles, 80, 10):
yield delProgress, maxprogress, "Removing deleted files"
if destFolder.containsFile(path):
if presaveFolder and not presaveFolder.containsFile(path):
presaveFolder.writeFile(path, destFolder.readFile(path))
destFolder.deleteFile(path)
# Write new and modified files
for path in sourceFolder.listAllFiles():
files = list(sourceFolder.listAllFiles())
for delProgress, path in enumProgress(files, 90, 10):
yield delProgress, maxprogress, "Writing new and modified files"
if presaveFolder and not presaveFolder.containsFile(path):
if destFolder.containsFile(path):
presaveFolder.writeFile(path, destFolder.readFile(path))
@ -381,6 +466,8 @@ def copyToFolder(destFolder, sourceNode, presaveNode=None):
presaveNode.deleteFile(path)
destFolder.writeFile(path, sourceFolder.readFile(path))
yield maxprogress, maxprogress, "Done"
class RevisionHistoryNode(object):
def __init__(self, history, worldFolder, parentNode):
"""

View File

@ -256,6 +256,10 @@ class SchematicFileAdapter(FakeChunkedLevelAdapter):
def saveChanges(self):
return self.saveToFile(self.filename)
def saveChangesIter(self):
self.saveChanges()
yield 100, 100, "Done"
def saveToFile(self, filename):
""" save to file named filename."""

View File

@ -19,6 +19,8 @@ def exhaust(_iter):
Functions named ending in "Iter" return an iterable object that does
long-running work and yields progress information on each call. exhaust()
is used to implement the non-Iter equivalents
:type _iter: Iterable
"""
i = None
for i in _iter:

View File

@ -281,6 +281,9 @@ class WorldEditor(object):
log.info("Opened revision %d", self.currentRevision)
def commitUndo(self, revisionInfo=None):
exhaust(self.commitUndoIter(revisionInfo))
def commitUndoIter(self, revisionInfo=None):
"""
Record all changes since the last call to beginUndo into the adapter's current revision. The revision is closed
and beginUndo must be called to open the next revision.
@ -291,7 +294,9 @@ class WorldEditor(object):
:rtype:
"""
self.adapter.setRevisionInfo(revisionInfo)
self.syncToDisk()
for status in self.syncToDiskIter():
yield status
self.adapter.closeRevision()
log.info("Closed revision %d", self.currentRevision)
@ -334,8 +339,10 @@ class WorldEditor(object):
self.recentDirtyFiles.update(changes.files)
# --- Save ---
def syncToDisk(self):
exhaust(self.syncToDiskIter())
def syncToDiskIter(self):
"""
Write all loaded chunks, player files, etc to disk.
@ -344,12 +351,15 @@ class WorldEditor(object):
"""
dirtyPlayers = 0
for player in self.playerCache.itervalues():
# xxx should be in adapter?
if player.dirty:
dirtyPlayers += 1
player.save()
dirtyChunkCount = 0
for cx, cz, dimName in self._chunkDataCache:
for i, (cx, cz, dimName) in enumerate(self._chunkDataCache):
yield i, len(self._chunkDataCache), "Writing modified chunks"
chunkData = self._chunkDataCache(cx, cz, dimName)
if chunkData.dirty:
dirtyChunkCount += 1
@ -359,12 +369,16 @@ class WorldEditor(object):
log.info(u"Saved %d chunks and %d players", dirtyChunkCount, dirtyPlayers)
def saveChanges(self):
exhaust(self.saveChangesIter())
def saveChangesIter(self):
if self.readonly:
raise IOError("World is opened read only.")
self.syncToDisk()
self.playerCache.clear()
self.adapter.saveChanges()
for status in self.adapter.saveChangesIter():
yield status
def saveToFile(self, filename):
# XXXX only works with .schematics!!!