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:
parent
3861f7f5d2
commit
d74ebcf932
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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!!!
|
||||
|
Reference in New Issue
Block a user