diff --git a/docs/keyboardShortcuts.rst b/docs/keyboardShortcuts.rst index 5eb387a..aff5ddf 100644 --- a/docs/keyboardShortcuts.rst +++ b/docs/keyboardShortcuts.rst @@ -106,4 +106,7 @@ For ease of use, there are some extra shortcuts not shown in the menus themselve - Ctrl+Shift+Tab * - Reload - F5 - + * - Indent (if text is selected) + - Tab + * - Unindent (if text is selected) + - Shift+Tab diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..4d5fca7 Binary files /dev/null and b/logo.png differ diff --git a/usdmanager/__init__.py b/usdmanager/__init__.py index 71947c7..657b4c0 100755 --- a/usdmanager/__init__.py +++ b/usdmanager/__init__.py @@ -45,6 +45,7 @@ import re import shlex import signal +import shutil import stat import subprocess import sys @@ -119,7 +120,7 @@ class UsdMngrWindow(QtWidgets.QMainWindow): Links to multiple files are colored yellow. Files may or may not exist. Links that cannot be resolved or confirmed as valid files are colored red. - Ideas: + Ideas (in no particular order): - Better usdz support (https://graphics.pixar.com/usd/docs/Usdz-File-Format-Specification.html) @@ -156,7 +157,7 @@ class UsdMngrWindow(QtWidgets.QMainWindow): - Remember scroll position per file so going back in history jumps you to approximately where you were before. - + Known issues: - AddressBar file completer has problems occasionally. @@ -973,7 +974,7 @@ def saveFile(self, filePath, fileFormat=FILE_FORMAT_NONE, _checkUsd=True): logger.debug("Checking file status") path = QtCore.QFile(filePath) if path.exists() and not QtCore.QFileInfo(path).isWritable(): - self.showCriticalMessage("The file is not writable.\n{}".format(filePath), "Save File") + self.showCriticalMessage("The file is not writable.\n{}".format(filePath), title="Save File") return False logger.debug("Writing file") self.setOverrideCursor() @@ -988,7 +989,7 @@ def saveFile(self, filePath, fileFormat=FILE_FORMAT_NONE, _checkUsd=True): elif ext == ".usd" and (fileFormat == FILE_FORMAT_USDC or (fileFormat == FILE_FORMAT_NONE and self.currTab.fileFormat == FILE_FORMAT_USDC)): crate = True if crate: - fd, tmpPath = tempfile.mkstemp(suffix=".usd") + fd, tmpPath = tempfile.mkstemp(suffix=".usd", dir=self.app.tmpDir) os.close(fd) status = False if self.saveFile(tmpPath, fileFormat, False): @@ -1452,10 +1453,16 @@ def removeTab(self, index): @Slot() def toggleEdit(self): """ Switch between Browse mode and Edit mode. + + :Returns: + True if we switched modes; otherwise, False. + This only returns False if we were in Edit mode and the user cancelled due to unsaved changes. + :Rtype: + `bool` """ # Don't change between browse and edit mode if dirty. Saves if needed. if not self.dirtySave(): - return + return False # Toggle edit mode self.currTab.inEditMode = not self.currTab.inEditMode @@ -1486,6 +1493,7 @@ def toggleEdit(self): self.updateEditButtons() self.editModeChanged.emit(self.currTab.inEditMode) + return True @Slot() def undo(self): @@ -2118,6 +2126,9 @@ def toggleFullScreen(self, *args): def browserBack(self): """ Go back one step in history for the current tab. """ + # Check if there are any changes to be saved before we modify the history. + if not self.dirtySave(): + return self.currTab.historyIndex -= 1 self.currTab.updateBreadcrumb() self.setSource(self.currTab.getCurrentUrl(), isNewFile=False) @@ -2126,6 +2137,9 @@ def browserBack(self): def browserForward(self): """ Go forward one step in history for the current tab. """ + # Check if there are any changes to be saved before we modify the history. + if not self.dirtySave(): + return self.currTab.historyIndex += 1 self.currTab.updateBreadcrumb() self.setSource(self.currTab.getCurrentUrl(), isNewFile=False) @@ -2176,12 +2190,11 @@ def diffFile(self): Allows you to make comparisons using a temporary file, without saving your changes. """ path = self.currTab.getCurrentPath() - fd, tmpPath = tempfile.mkstemp(suffix=QtCore.QFileInfo(path).fileName()) + fd, tmpPath = tempfile.mkstemp(suffix=QtCore.QFileInfo(path).fileName(), dir=self.app.tmpDir) with os.fdopen(fd, 'w') as f: f.write(self.currTab.textEditor.toPlainText()) args = shlex.split(self.preferences['diffTool']) + [path, tmpPath] self.launchProcess(args) - # TODO: Cleanup temp file if/when diff tool is closed. @staticmethod def getPermissionString(path): @@ -2295,181 +2308,46 @@ def getCommentStrings(self): @Slot() def commentTextRequest(self): """ Slot called by the Comment action. """ - self.commentOutText(*self.getCommentStrings()) - + self.currTab.getCurrentTextWidget().commentOutText(*self.getCommentStrings()) + @Slot() def uncommentTextRequest(self): """ Slot called by the Uncomment action. """ - self.uncommentText(*self.getCommentStrings()) - - def commentOutText(self, commentStart="#", commentEnd=""): - """ Comment out selected lines. - - TODO: For languages that use a different syntax for multi-line comments, - use that when multiple lines are selected? - - :Parameters: - commentStart : `str` - String used for commenting out lines. - commentEnd : `str` - If the comment can be applied to multiple lines, - this is the string marking the end of the comment. - """ - textWidget = self.currTab.getCurrentTextWidget() - cursor = textWidget.textCursor() - start = cursor.selectionStart() - end = cursor.selectionEnd() - commentLen = len(commentStart) - cursor.setPosition(start) - cursor.movePosition(cursor.StartOfBlock) - cursor.beginEditBlock() - - if not commentEnd: - # Modify all blocks between selectionStart and selectionEnd - while cursor.position() <= end and not cursor.atEnd(): - cursor.insertText(commentStart) - # For every character we insert, increment the end position. - end += commentLen - prevBlock = cursor.blockNumber() - cursor.movePosition(cursor.NextBlock) - - # I think I have a bug in my code if I have to do this. - if prevBlock == cursor.blockNumber(): - break - else: - # Only modify the beginning and end lines since this can - # be a multiple-line comment. - cursor.insertText(commentStart) - cursor.setPosition(end) - cursor.movePosition(cursor.EndOfBlock) - cursor.insertText(commentEnd) - cursor.endEditBlock() - - def uncommentText(self, commentStart="#", commentEnd=""): - """ Uncomment selected lines. - - :Parameters: - commentStart : `str` - String used for commenting out lines. - commentEnd : `str` - If the comment can be applied to multiple lines, - this is the string marking the end of the comment. - """ - textWidget = self.currTab.getCurrentTextWidget() - cursor = textWidget.textCursor() - start = cursor.selectionStart() - end = cursor.selectionEnd() - commentLen = len(commentStart) - cursor.setPosition(start) - cursor.movePosition(cursor.StartOfBlock) - cursor.beginEditBlock() - if not commentEnd: - # Modify all blocks between selectionStart and selectionEnd - while cursor.position() <= end and not cursor.atEnd(): - block = cursor.block() - # Select the number of characters used in the comment string. - for i in range(len(commentStart)): - cursor.movePosition(cursor.NextCharacter, cursor.KeepAnchor) - # If the selection is all on the same line and matches the comment string, remove it. - if block.contains(cursor.selectionEnd()) and cursor.selectedText() == commentStart: - cursor.deleteChar() - end -= commentLen - prevBlock = cursor.blockNumber() - cursor.movePosition(cursor.NextBlock) - if prevBlock == cursor.blockNumber(): - break - else: - # Remove the beginning comment string. - # Do we only want to do this if there's also an end comment string in the selection? - # We probably also want to remove the comments if there is any whitespace before or after it. - # This logic may not be completely right when some comment symbols are already in the selection. - block = cursor.block() - # Select the number of characters used in the comment string. - for i in range(len(commentStart)): - cursor.movePosition(cursor.NextCharacter, cursor.KeepAnchor) - # If the selection is all on the same line and matches the comment string, remove it. - if block.contains(cursor.selectionEnd()) and cursor.selectedText() == commentStart: - cursor.deleteChar() - # Remove the end comment string. - cursor.setPosition(end - len(commentStart)) - block = cursor.block() - cursor.movePosition(cursor.EndOfBlock) - for i in range(len(commentEnd)): - cursor.movePosition(cursor.PreviousCharacter, cursor.KeepAnchor) - if block.contains(cursor.selectionStart()) and cursor.selectedText() == commentEnd: - cursor.deleteChar() - cursor.endEditBlock() + self.currTab.getCurrentTextWidget().uncommentText(*self.getCommentStrings()) @Slot() - def indentText(self, numSpaces=4): - """ Indent selected lines by the given number of spaces. - - :Parameters: - numSpaces : `int` - Number of spaces used for indenting. + def indentText(self): + """ Indent selected lines by one tab stop. """ - textWidget = self.currTab.getCurrentTextWidget() - cursor = textWidget.textCursor() - start = cursor.selectionStart() - end = cursor.selectionEnd() - cursor.setPosition(start) - cursor.movePosition(cursor.StartOfBlock) - cursor.beginEditBlock() - # Modify all blocks between selectionStart and selectionEnd - while cursor.position() <= end and not cursor.atEnd(): - for i in range(numSpaces): - cursor.insertText(" ") - # Increment end by the number of characters we inserted. - end += numSpaces - prevBlock = cursor.blockNumber() - cursor.movePosition(cursor.NextBlock) - if prevBlock == cursor.blockNumber(): - break - cursor.endEditBlock() + self.currTab.getCurrentTextWidget().indentText() @Slot() - def unindentText(self, numSpaces=4): - """ Un-indent selected lines by the given number of spaces. - - :Parameters: - numSpaces : `int` - Number of spaces used for indenting. + def unindentText(self): + """ Un-indent selected lines by one tab stop. """ - textWidget = self.currTab.getCurrentTextWidget() - cursor = textWidget.textCursor() - start = cursor.selectionStart() - end = cursor.selectionEnd() - cursor.setPosition(start) - cursor.movePosition(cursor.StartOfBlock) - cursor.beginEditBlock() - # Modify all blocks between selectionStart and selectionEnd - while cursor.position() <= end and not cursor.atEnd(): - for i in range(numSpaces): - cursor.movePosition(cursor.NextCharacter, cursor.KeepAnchor) - if cursor.selectedText() == " ": - cursor.deleteChar() - end -= 1 - else: - break - prevBlock = cursor.blockNumber() - cursor.movePosition(cursor.NextBlock) - if prevBlock == cursor.blockNumber(): - break - cursor.endEditBlock() + self.currTab.getCurrentTextWidget().unindentText() @Slot() def launchTextEditor(self): """ Launch the current file in a separate text editing application. """ - args = shlex.split(self.preferences['textEditor']) + [self.currTab.getCurrentPath()] + path = self.currTab.getCurrentPath() + args = shlex.split(self.preferences['textEditor']) + [path] self.launchProcess(args) @Slot() def launchUsdView(self): """ Launch the current file in usdview. """ - cmd = "{} {}".format(self.app.appConfig.get("usdview", "usdview"), self.currTab.getCurrentPath()) - self.launchProcess(cmd, shell=True) + app = self.app.appConfig.get("usdview", "usdview") + path = self.currTab.getCurrentPath() + # Files with spaces have to be double-quoted on Windows for usdview. + if os.name == "nt": + cmd = '{} "{}"'.format(app, path) + self.launchProcess(cmd, shell=True) + else: + args = [app, path] + self.launchProcess(args) @Slot() def launchProgramOfChoice(self, path=None): @@ -2507,11 +2385,12 @@ def launchProgramOfChoice(self, path=None): def showAboutDialog(self, *args): """ Display a modal dialog box that shows the "about" information for the application. """ + from .version import __version__ captionText = "About {}".format(self.app.appDisplayName) - aboutText = ("App Name: {0}
" - "App Path: {1}
" - "Documentation: {2}".format( - self.app.appName, self.app.appPath, self.app.appURL)) + aboutText = ("App Name: {0} {1}
" + "App Path: {2}
" + "Documentation: {3}".format( + self.app.appName, __version__, self.app.appPath, self.app.appURL)) QtWidgets.QMessageBox.about(self, captionText, aboutText) @Slot(bool) @@ -2626,7 +2505,7 @@ def readUsdCrateFile(self, fileStr): logger.debug("Binary USD file detected. Converting to ASCII representation.") self.currTab.fileFormat = FILE_FORMAT_USDC self.tabWidget.setTabIcon(self.tabWidget.currentIndex(), self.binaryIcon) - usdPath = utils.generateTemporaryUsdFile(fileStr) + usdPath = utils.generateTemporaryUsdFile(fileStr, self.app.tmpDir) with open(usdPath) as f: fileText = f.readlines() os.remove(usdPath) @@ -2647,7 +2526,7 @@ def readUsdzFile(self, fileStr): logger.debug("Uncompressing usdz file...") self.currTab.fileFormat = FILE_FORMAT_USDZ self.tabWidget.setTabIcon(self.tabWidget.currentIndex(), self.zipIcon) - usdDir = utils.unzip(fileStr) + usdDir = utils.unzip(fileStr, self.app.tmpDir) return os.path.join(usdDir, os.path.basename(fileStr)) ''' @@ -2671,7 +2550,7 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos """ # Check if the current tab is dirty before doing anything. # Perform save operations if necessary. - if not isNewFile and not self.dirtySave(): + if not self.dirtySave(): return True # Re-cast the QUrl so any query strings are evaluated properly. @@ -2791,7 +2670,7 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos # TODO: Support nested usdz references. usd = True layer = utils.queryItemValue(link, "layer") - dest = utils.unzip(fileStr, layer) + dest = utils.unzip(fileStr, layer, self.app.tmpDir) self.restoreOverrideCursor() return self.setSource(QtCore.QUrl(dest)) else: @@ -2901,13 +2780,13 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos k, v = kv.split("=", 1) local_sdf_args[k] = v if local_sdf_args: - queryParams = ["sdf={}".format("+".join("{}:{}".format(k, v) for k, v in sorted(local_sdf_args.items(), key=lambda x: x[0])))] + queryParams = ["sdf=" + "+".join("{}:{}".format(k, v) for k, v in sorted(local_sdf_args.items(), key=lambda x: x[0]))] else: queryParams = [] # .usdz file references (e.g. @set.usdz[foo/bar.usd]@) if m.group(2): - queryParams.append("layer={}".format(m.group(2))) + queryParams.append("layer=" + m.group(2)) # Make the HTML link. if exists[fullPath]: @@ -2916,11 +2795,11 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos queryParams.insert(0, "binary=1") htmlLink = '{}'.format(fullPath, "&".join(queryParams), linkPath) else: - queryStr = "?{}".format("&".join(queryParams)) if queryParams else "" + queryStr = "?" + "&".join(queryParams) if queryParams else "" htmlLink = '{}'.format(fullPath, queryStr, linkPath) elif '*' in linkPath or '<UDIM>' in linkPath or '.#.' in linkPath: # Create an orange link for files with wildcards in the path, designating zero or more files may exist. - queryStr = "?{}".format("&".join(queryParams)) if queryParams else "" + queryStr = "?" + "&".join(queryParams) if queryParams else "" htmlLink = '{}'.format(fullPath, queryStr, linkPath) else: raise ValueError @@ -3028,7 +2907,7 @@ def setSources(self, files): """ prevPath = self.currTab.getCurrentPath() for path in files: - self.setSource(QtCore.QUrl(os.path.abspath(utils.expandPath(path, prevPath))), newTab=True) + self.setSource(utils.expandUrl(path, prevPath), newTab=True) def validateFileSize(self, path): """ If a file's size is above a certain threshold, confirm the user still wants to open the file. @@ -3113,7 +2992,7 @@ def goPressed(self, *args): """ Handle loading the current path in the address bar. """ # Check if text has changed. - text = QtCore.QUrl(os.path.abspath(utils.expandPath(self.addressBar.text().strip()))) + text = utils.expandUrl(self.addressBar.text().strip()) if text != self.currTab.getCurrentUrl(): self.setSource(text) else: @@ -3231,15 +3110,15 @@ def validateAddressBar(self, address): """ self.buttonGo.setEnabled(bool(address.strip())) - def launchProcess(self, args, shell=False, **kwargs): + def launchProcess(self, args, **kwargs): """ Launch a program with the path `str` as an argument. + Any additional keyword arguments are passed to the Popen object. + :Parameters: args : `list` | `str` A sequence of program arguments with the program as the first arg. - If shell=True, this should be a single string. - shell : `bool` - Run in a shell + If also passing in shell=True, this should be a single string. :Returns: Returns process ID or None :Rtype: @@ -3247,12 +3126,16 @@ def launchProcess(self, args, shell=False, **kwargs): """ with self.overrideCursor(): try: - if shell: - cmd = args + if kwargs.get("shell"): + # With shell=True, convert args to a string to call Popen with. + logger.debug("Running Popen with shell=True") + if isinstance(args, list): + args = subprocess.list2cmdline(args) + logger.info(args) else: - cmd = subprocess.list2cmdline(args) - logger.info(cmd) - p = subprocess.Popen(args, shell=shell, **kwargs) + # Leave args as a list for Popen, but still log the string command. + logger.info(subprocess.list2cmdline(args)) + p = subprocess.Popen(args, **kwargs) return p except Exception: self.restoreOverrideCursor() @@ -3425,7 +3308,14 @@ def onOpenLinkWith(self): @Slot(str) def onBreadcrumbActivated(self, path): """ Slot called when a breadcrumb link (history for the current tab) is selected. + + :Parameters: + path : `str` + Breadcrumb path """ + # Check if there are any changes to be saved before we modify the history. + if not self.dirtySave(): + return self.currTab.historyIndex = self.currTab.findPath(path) self.currTab.updateBreadcrumb() self.setSource(self.currTab.getCurrentUrl(), isNewFile=False) @@ -3810,18 +3700,21 @@ class TextEdit(QtWidgets.QTextEdit): """ Customized QTextEdit to allow entering spaces with the Tab key. """ - def __init__(self, parent=None, tabSpaces=4): + def __init__(self, parent=None, tabSpaces=4, useSpaces=True): """ Create and initialize the tab. :Parameters: parent : `BrowserTab` Browser tab containing this text edit widget tabSpaces : `int` - Number of spaces to use instead of a tab character. - If 0, use a tab character. + Number of spaces to use instead of a tab character, if useSpaces is True. + useSpaces : `bool` + If True, use the number of tab spaces instead of a tab character; + otherwise, just use a tab character """ super(TextEdit, self).__init__(parent) self.tabSpaces = tabSpaces + self.useSpaces = useSpaces def keyPressEvent(self, e): """ Override the Tab key to insert spaces instead. @@ -3830,10 +3723,178 @@ def keyPressEvent(self, e): e : `QtGui.QKeyEvent` Key press event """ - if self.tabSpaces and e.key() == QtCore.Qt.Key_Tab and e.modifiers() == QtCore.Qt.NoModifier: - self.insertPlainText(" " * self.tabSpaces) + if e.key() == QtCore.Qt.Key_Tab: + if e.modifiers() == QtCore.Qt.NoModifier: + if self.textCursor().hasSelection(): + self.indentText() + return + elif self.useSpaces: + # Insert the spaces equivalent of a tab character. + # Otherwise, QTextEdit already handles inserting the tab character. + self.insertPlainText(" " * self.tabSpaces) + return + elif e.key() == QtCore.Qt.Key_Backtab and e.modifiers() == QtCore.Qt.ShiftModifier and self.textCursor().hasSelection(): + self.unindentText() + return + super(TextEdit, self).keyPressEvent(e) + + def commentOutText(self, commentStart="#", commentEnd=""): + """ Comment out selected lines. + + TODO: For languages that use a different syntax for multi-line comments, + use that when multiple lines are selected? + + :Parameters: + commentStart : `str` + String used for commenting out lines. + commentEnd : `str` + If the comment can be applied to multiple lines, + this is the string marking the end of the comment. + """ + cursor = self.textCursor() + start = cursor.selectionStart() + end = cursor.selectionEnd() + commentLen = len(commentStart) + cursor.setPosition(start) + cursor.movePosition(cursor.StartOfBlock) + cursor.beginEditBlock() + + if not commentEnd: + # Modify all blocks between selectionStart and selectionEnd + while cursor.position() <= end and not cursor.atEnd(): + cursor.insertText(commentStart) + # For every character we insert, increment the end position. + end += commentLen + prevBlock = cursor.blockNumber() + cursor.movePosition(cursor.NextBlock) + + # I think I have a bug in my code if I have to do this. + if prevBlock == cursor.blockNumber(): + break + else: + # Only modify the beginning and end lines since this can + # be a multiple-line comment. + cursor.insertText(commentStart) + cursor.setPosition(end) + cursor.movePosition(cursor.EndOfBlock) + cursor.insertText(commentEnd) + cursor.endEditBlock() + + def uncommentText(self, commentStart="#", commentEnd=""): + """ Uncomment selected lines. + + :Parameters: + commentStart : `str` + String used for commenting out lines. + commentEnd : `str` + If the comment can be applied to multiple lines, + this is the string marking the end of the comment. + """ + cursor = self.textCursor() + start = cursor.selectionStart() + end = cursor.selectionEnd() + commentLen = len(commentStart) + cursor.setPosition(start) + cursor.movePosition(cursor.StartOfBlock) + cursor.beginEditBlock() + if not commentEnd: + # Modify all blocks between selectionStart and selectionEnd + while cursor.position() <= end and not cursor.atEnd(): + block = cursor.block() + # Select the number of characters used in the comment string. + for i in range(len(commentStart)): + cursor.movePosition(cursor.NextCharacter, cursor.KeepAnchor) + # If the selection is all on the same line and matches the comment string, remove it. + if block.contains(cursor.selectionEnd()) and cursor.selectedText() == commentStart: + cursor.deleteChar() + end -= commentLen + prevBlock = cursor.blockNumber() + cursor.movePosition(cursor.NextBlock) + if prevBlock == cursor.blockNumber(): + break else: - super(TextEdit, self).keyPressEvent(e) + # Remove the beginning comment string. + # Do we only want to do this if there's also an end comment string in the selection? + # We probably also want to remove the comments if there is any whitespace before or after it. + # This logic may not be completely right when some comment symbols are already in the selection. + block = cursor.block() + # Select the number of characters used in the comment string. + for i in range(len(commentStart)): + cursor.movePosition(cursor.NextCharacter, cursor.KeepAnchor) + # If the selection is all on the same line and matches the comment string, remove it. + if block.contains(cursor.selectionEnd()) and cursor.selectedText() == commentStart: + cursor.deleteChar() + # Remove the end comment string. + cursor.setPosition(end - len(commentStart)) + block = cursor.block() + cursor.movePosition(cursor.EndOfBlock) + for i in range(len(commentEnd)): + cursor.movePosition(cursor.PreviousCharacter, cursor.KeepAnchor) + if block.contains(cursor.selectionStart()) and cursor.selectedText() == commentEnd: + cursor.deleteChar() + cursor.endEditBlock() + + def indentText(self): + """ Indent selected lines by one tab stop. + """ + cursor = self.textCursor() + start = cursor.selectionStart() + end = cursor.selectionEnd() + cursor.setPosition(start) + cursor.movePosition(cursor.StartOfBlock) + cursor.beginEditBlock() + # Modify all blocks between selectionStart and selectionEnd + while cursor.position() <= end and not cursor.atEnd(): + if self.useSpaces: + if self.tabSpaces: + cursor.insertText(" " * self.tabSpaces) + # Increment end by the number of characters we inserted. + end += self.tabSpaces + else: + cursor.insertText("\t") + end += 1 + prevBlock = cursor.blockNumber() + cursor.movePosition(cursor.NextBlock) + if prevBlock == cursor.blockNumber(): + break + cursor.endEditBlock() + + def unindentText(self): + """ Un-indent selected lines by one tab stop. + """ + cursor = self.textCursor() + start = cursor.selectionStart() + end = cursor.selectionEnd() + cursor.setPosition(start) + cursor.movePosition(cursor.StartOfBlock) + cursor.beginEditBlock() + # Modify all blocks between selectionStart and selectionEnd + while cursor.position() <= end and not cursor.atEnd(): + currBlock = cursor.blockNumber() + + for i in range(self.tabSpaces): + cursor.movePosition(cursor.NextCharacter, cursor.KeepAnchor) + if cursor.selectedText() == " ": + cursor.deleteChar() + end -= 1 + elif cursor.selectedText() == "\t": + cursor.deleteChar() + end -= 1 + # If we hit a tab character, that's the end of this tab stop. + break + else: + break + + # If we're still in the same block, go to the next block. + if currBlock == cursor.blockNumber(): + cursor.movePosition(cursor.NextBlock) + if currBlock == cursor.blockNumber(): + # We didn't get a new block, so we're at the end. + break + else: + # We already moved to the next block. + cursor.movePosition(cursor.StartOfLine, cursor.MoveAnchor) + cursor.endEditBlock() class BrowserTab(QtWidgets.QWidget): @@ -4041,7 +4102,8 @@ def setTabSpaces(self, useSpaces=True, tabSpaces=4): width = tabSpaces * QtGui.QFontMetricsF(font).averageCharWidth() self.textBrowser.setTabStopWidth(width) self.textEditor.setTabStopWidth(width) - self.textEditor.tabSpaces = tabSpaces if useSpaces else 0 + self.textEditor.tabSpaces = tabSpaces + self.textEditor.useSpaces = useSpaces def updateHistory(self, url, update=False, truncated=False): """ Add a newly created file to the tab's history. @@ -4136,6 +4198,7 @@ def run(self): """ self.appPath = sys.argv[0] self.appName = os.path.basename(self.appPath) + self.tmpDir = None parser = argparse.ArgumentParser(prog=os.path.basename(self.appPath), description = 'File Browser/Text Editor for quick navigation and\n' @@ -4164,8 +4227,9 @@ def run(self): # Initialize the application and settings. self._set_log_level() logger.debug("Qt version: {} {}".format(Qt.__binding__, Qt.__binding_version__)) - self.app = QtWidgets.QApplication(sys.argv)#, True) + self.app = QtWidgets.QApplication(sys.argv) self.app.setApplicationName(self.appName) + self.app.setWindowIcon(QtGui.QIcon(":images/images/logo.png")) if Qt.IsPySide2 or Qt.IsPyQt5: self.app.setApplicationDisplayName(self.appDisplayName) self.app.setOrganizationName("USD") @@ -4239,6 +4303,9 @@ def mainLoop(self): """ Start the application loop. """ if not App._eventLoopStarted: + # Create a temp directory for cache-like files. + self.tmpDir = tempfile.mkdtemp(prefix=self.appName) + logger.debug("Temp directory: {}".format(self.tmpDir)) App._eventLoopStarted = True self.app.exec_() @@ -4247,6 +4314,10 @@ def onExit(self): """ Callback when the application is exiting. """ App._eventLoopStarted = False + + # Clean up our temp dir. + if self.tmpDir is not None: + shutil.rmtree(self.tmpDir, ignore_errors=True) class Settings(QtCore.QSettings): diff --git a/usdmanager/images/logo.png b/usdmanager/images/logo.png index c21e225..f130a16 100644 Binary files a/usdmanager/images/logo.png and b/usdmanager/images/logo.png differ diff --git a/usdmanager/images_rc.py b/usdmanager/images_rc.py index 5db0cd2..567f32b 100644 --- a/usdmanager/images_rc.py +++ b/usdmanager/images_rc.py @@ -2,7 +2,7 @@ # Resource object code # -# Created by: The Resource Compiler for PyQt (Qt v4.6.2) +# Created by: The Resource Compiler for PyQt (Qt v4.8.5) # # WARNING! All changes made in this file will be lost! @@ -58,211 +58,226 @@ \xa5\x94\x52\x96\x25\x85\x10\x42\x4a\x29\x84\x10\xef\x97\x97\x97\ \xc9\xc0\xc0\x80\xfa\x0d\xea\xc1\x0a\x11\xec\x45\x65\x41\x00\x00\ \x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x0c\xa5\ +\x00\x00\x0d\x96\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ -\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\ -\x00\x41\x64\x6f\x62\x65\x20\x49\x6d\x61\x67\x65\x52\x65\x61\x64\ -\x79\x71\xc9\x65\x3c\x00\x00\x03\x28\x69\x54\x58\x74\x58\x4d\x4c\ -\x3a\x63\x6f\x6d\x2e\x61\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\ -\x00\x00\x00\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x62\x65\x67\ -\x69\x6e\x3d\x22\xef\xbb\xbf\x22\x20\x69\x64\x3d\x22\x57\x35\x4d\ -\x30\x4d\x70\x43\x65\x68\x69\x48\x7a\x72\x65\x53\x7a\x4e\x54\x63\ -\x7a\x6b\x63\x39\x64\x22\x3f\x3e\x20\x3c\x78\x3a\x78\x6d\x70\x6d\ -\x65\x74\x61\x20\x78\x6d\x6c\x6e\x73\x3a\x78\x3d\x22\x61\x64\x6f\ -\x62\x65\x3a\x6e\x73\x3a\x6d\x65\x74\x61\x2f\x22\x20\x78\x3a\x78\ -\x6d\x70\x74\x6b\x3d\x22\x41\x64\x6f\x62\x65\x20\x58\x4d\x50\x20\ -\x43\x6f\x72\x65\x20\x35\x2e\x36\x2d\x63\x31\x34\x35\x20\x37\x39\ -\x2e\x31\x36\x33\x34\x39\x39\x2c\x20\x32\x30\x31\x38\x2f\x30\x38\ -\x2f\x31\x33\x2d\x31\x36\x3a\x34\x30\x3a\x32\x32\x20\x20\x20\x20\ -\x20\x20\x20\x20\x22\x3e\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x20\ -\x78\x6d\x6c\x6e\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\ -\x39\x2f\x30\x32\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\ -\x61\x78\x2d\x6e\x73\x23\x22\x3e\x20\x3c\x72\x64\x66\x3a\x44\x65\ -\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\ -\x6f\x75\x74\x3d\x22\x22\x20\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\ -\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\ -\x65\x2e\x63\x6f\x6d\x2f\x78\x61\x70\x2f\x31\x2e\x30\x2f\x22\x20\ -\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x4d\x4d\x3d\x22\x68\x74\x74\ -\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\ -\x2f\x78\x61\x70\x2f\x31\x2e\x30\x2f\x6d\x6d\x2f\x22\x20\x78\x6d\ -\x6c\x6e\x73\x3a\x73\x74\x52\x65\x66\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ -\x61\x70\x2f\x31\x2e\x30\x2f\x73\x54\x79\x70\x65\x2f\x52\x65\x73\ -\x6f\x75\x72\x63\x65\x52\x65\x66\x23\x22\x20\x78\x6d\x70\x3a\x43\ -\x72\x65\x61\x74\x6f\x72\x54\x6f\x6f\x6c\x3d\x22\x41\x64\x6f\x62\ -\x65\x20\x50\x68\x6f\x74\x6f\x73\x68\x6f\x70\x20\x43\x43\x20\x32\ -\x30\x31\x39\x20\x28\x4d\x61\x63\x69\x6e\x74\x6f\x73\x68\x29\x22\ -\x20\x78\x6d\x70\x4d\x4d\x3a\x49\x6e\x73\x74\x61\x6e\x63\x65\x49\ -\x44\x3d\x22\x78\x6d\x70\x2e\x69\x69\x64\x3a\x37\x32\x33\x38\x34\ -\x31\x46\x39\x32\x44\x42\x39\x31\x31\x45\x39\x42\x37\x33\x43\x45\ -\x41\x34\x39\x42\x46\x39\x33\x33\x44\x34\x41\x22\x20\x78\x6d\x70\ -\x4d\x4d\x3a\x44\x6f\x63\x75\x6d\x65\x6e\x74\x49\x44\x3d\x22\x78\ -\x6d\x70\x2e\x64\x69\x64\x3a\x37\x32\x33\x38\x34\x31\x46\x41\x32\ -\x44\x42\x39\x31\x31\x45\x39\x42\x37\x33\x43\x45\x41\x34\x39\x42\ -\x46\x39\x33\x33\x44\x34\x41\x22\x3e\x20\x3c\x78\x6d\x70\x4d\x4d\ -\x3a\x44\x65\x72\x69\x76\x65\x64\x46\x72\x6f\x6d\x20\x73\x74\x52\ -\x65\x66\x3a\x69\x6e\x73\x74\x61\x6e\x63\x65\x49\x44\x3d\x22\x78\ -\x6d\x70\x2e\x69\x69\x64\x3a\x37\x32\x33\x38\x34\x31\x46\x37\x32\ -\x44\x42\x39\x31\x31\x45\x39\x42\x37\x33\x43\x45\x41\x34\x39\x42\ -\x46\x39\x33\x33\x44\x34\x41\x22\x20\x73\x74\x52\x65\x66\x3a\x64\ -\x6f\x63\x75\x6d\x65\x6e\x74\x49\x44\x3d\x22\x78\x6d\x70\x2e\x64\ -\x69\x64\x3a\x37\x32\x33\x38\x34\x31\x46\x38\x32\x44\x42\x39\x31\ -\x31\x45\x39\x42\x37\x33\x43\x45\x41\x34\x39\x42\x46\x39\x33\x33\ -\x44\x34\x41\x22\x2f\x3e\x20\x3c\x2f\x72\x64\x66\x3a\x44\x65\x73\ -\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x20\x3c\x2f\x72\x64\x66\x3a\ -\x52\x44\x46\x3e\x20\x3c\x2f\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\ -\x3e\x20\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x65\x6e\x64\x3d\ -\x22\x72\x22\x3f\x3e\x35\xc2\xef\x77\x00\x00\x09\x13\x49\x44\x41\ -\x54\x78\xda\xdc\x5b\x7d\x70\x54\xd5\x15\x3f\x77\x77\xb3\xf9\xd6\ -\x24\x18\x83\x42\x98\x88\xc4\xb8\x49\xa6\x40\x11\x50\x1c\x51\x19\ -\x12\xa9\xfe\x61\x1d\x5b\x67\x8a\x32\x6d\xc9\xd8\x69\x8b\x98\x44\ -\x8a\xe3\xe8\x8c\xa8\x7f\x38\x8e\x22\x89\x8a\xd3\xd6\x42\x46\xa5\ -\x2d\xce\xa8\x53\x75\xa6\x83\xec\x6a\x4b\x23\x52\x28\xb4\xc0\x40\ -\x12\xc2\x44\x89\x7c\x94\x40\xa2\x41\x21\x09\x59\x76\xdf\xf5\x77\ -\xee\x7b\x2f\x1f\x9b\xfd\x78\x1b\x92\xdd\x97\xdc\x99\xb7\x77\xdf\ -\x7d\xf7\xeb\x77\xce\xb9\xe7\x9e\x73\xde\x7d\x42\x4a\x49\x56\x92\ -\x10\x82\xc6\x24\xd5\xfa\x88\xea\x2a\x38\xbf\x07\x77\xbf\xc2\x55\ -\xa6\x3f\x90\x4d\x18\xe5\x0d\x3c\xfb\xfb\x40\x9d\x51\x26\xab\x98\ -\x14\xae\x84\x12\x60\x00\xbc\xd7\x8b\x1e\x43\x11\xf2\x44\x78\x10\ -\x1f\xea\x54\x52\x0d\xea\xd6\x57\x8c\x3b\x01\x1c\x94\xc8\xa4\x73\ -\x7e\x3b\x49\xb1\x34\x1c\x8d\x8d\x7c\x29\xea\x6c\x8b\x0a\x9e\x09\ -\x19\xed\x3e\x8e\x94\x58\x02\xd4\x78\xa7\x80\x3d\x95\x80\x1a\x4d\ -\x9c\xf8\xd9\x32\x48\xc9\xb5\x11\x81\x4b\xfa\x19\xfa\xda\x0a\x29\ -\xf9\x27\xae\x77\x70\xff\xa0\xfe\xdc\x1b\xf7\x94\x12\xbc\x04\xbc\ -\x1b\xc1\xfd\xdf\xc6\x20\x80\x12\x62\x8c\x58\x07\x89\x59\x33\xa4\ -\x2d\xff\xce\x40\xfb\xfd\x98\x75\x1e\x72\x69\xf4\x63\x2e\x9d\x4e\ -\x34\xbb\x09\x13\x3d\x2e\x37\x2c\xb5\xa9\x04\x90\x58\x12\x1b\xbc\ -\x4e\x6e\x80\xb9\x33\x84\xf3\x98\xab\xf8\x02\xad\x73\x15\xde\xc1\ -\x7e\xcc\xfc\x2a\x34\x6b\x07\x39\xae\xb4\xef\x12\x90\xf2\x6c\x1c\ -\xc4\x3a\x3b\x4c\x77\x90\xf8\x17\x7e\x5c\x43\x00\x87\xd7\x21\x82\ -\xde\xb0\x2f\x01\x84\x58\x0f\x0e\x59\x59\x73\x2c\xde\xaf\x85\x94\ -\xcd\xb7\x32\x02\xae\xfb\xed\x49\x00\x25\xc6\x72\x1b\xd6\xaf\xb0\ -\xb0\x87\xf1\x12\xf0\x41\xd1\x99\x6d\xc1\x79\x99\x6a\x71\x24\xa7\ -\x7d\x95\xa0\xbe\x13\xcc\x46\x67\x07\x86\x28\xaf\xa1\x7c\x37\x15\ -\xdb\x1c\x88\xfd\xc1\x10\x02\x5a\xde\xdc\xa1\x04\x85\x3d\x97\x00\ -\xa7\xfa\x4a\x00\x93\x6c\xfd\xb5\x29\x65\xcf\x1c\xd0\x73\x26\x07\ -\x97\x79\x46\x80\xd7\x89\x73\xde\x20\x5a\xac\xd4\x1b\xcf\x74\x5c\ -\x94\x8c\x54\x57\xd9\x8c\xdf\x1b\xa6\x6d\xd8\xb7\x06\xe8\xd7\x73\ -\x91\x16\x94\x55\x1d\x6b\x17\x34\x44\xe1\xeb\x87\x80\xff\x10\x45\ -\x93\x44\x45\x44\xb1\xc9\xfe\x04\x30\xd2\x15\x39\xe9\x5f\x0f\xb9\ -\x3d\xdd\x11\xa6\x8e\x67\x33\xbb\x08\x94\x05\xdc\xf7\xb5\x36\x75\ -\xc8\x60\x20\x28\x22\x10\x01\xcb\x47\x9c\x41\x56\x6d\xdf\x5d\x20\ -\xce\xe4\xd9\x7c\x98\xb5\x54\x36\x56\x49\x97\xa6\xc9\x8c\xe2\xd2\ -\x02\x4a\xcf\x4c\xed\x53\x9c\xd6\x57\xc3\xd0\x25\xb1\x03\x77\x45\ -\x44\xf1\xe9\x2a\xdb\x12\xe0\x46\x05\x5e\xa4\xe3\xa7\x13\x0a\xd8\ -\xad\x94\xb0\xa4\x9e\x9f\xac\x98\x9f\x83\xf2\x12\x94\xaf\xc5\xfd\ -\x06\x10\xe3\x71\x54\x2f\x81\xde\x58\x82\xbc\x3f\x5e\x2f\xd2\x65\ -\x4f\xf0\x4d\xcc\x5c\x37\x7e\xce\xe1\x72\x1b\x9c\xee\xc1\x55\xb0\ -\x77\xd7\x31\x3f\x40\x1e\xc5\xff\xf5\x23\x15\x6c\xfc\xde\xa3\xed\ -\x24\xe0\xc6\x86\x26\x16\xe2\x54\x70\xfc\x5b\x83\x08\x0c\xbe\x1f\ -\xca\xf2\x6a\x94\xf5\x1e\x59\x59\x36\xa6\xe3\x25\x55\x02\xfa\x7a\ -\xfd\xe9\xc1\xa0\x66\xda\x19\x59\xba\x29\x27\x9d\xb8\xf9\x8e\x39\ -\x2f\x75\xab\xc9\x0f\x12\xe4\xa1\xac\xaf\x65\x8c\xc1\x27\xc7\x10\ -\xd2\x8d\xa1\x02\x74\xf8\x11\xd6\xef\x02\xc3\x34\x36\x5d\x9b\x3d\ -\xc5\xa5\x53\xe7\xb8\x5c\xce\x54\xdd\x2c\x92\x01\x29\x44\x2e\xb8\ -\xde\x33\x39\x22\x42\x3a\xf8\x42\x76\x59\xd5\x2c\xfb\x83\x62\xee\ -\xf5\xb9\x0a\xfb\xff\xbe\xec\x06\xcf\x9d\x6a\x2f\x9f\xe5\xb9\x46\ -\xa6\xb8\x9d\x4c\x80\xac\x96\xaa\xf2\x3e\x7b\x86\xc4\x6a\x7d\xc5\ -\xe0\xd1\x2c\xcc\xbe\x17\x23\xee\x84\x85\x17\x54\x3e\x7b\x5d\x65\ -\x04\xe0\x3e\xe6\x33\xeb\x9c\x20\xcf\xf0\x87\x85\x57\x88\xdd\xd5\ -\xf3\x29\xc5\xa9\xab\x21\x3f\x96\xc2\xc2\xfa\xbd\x74\xe0\xe4\x77\ -\xca\x1b\x2e\x2c\xca\xcb\x39\x51\x3d\xef\x5b\xfb\xc5\x04\x6b\x7c\ -\x77\x01\xc8\x16\x8c\x92\x1f\x62\x90\xfc\x19\xda\x79\x45\xd4\x58\ -\x5e\xad\x77\x1e\x7a\xda\xc7\xad\xb4\x90\xa0\x05\xcf\x83\xc7\x48\ -\x5d\xfb\x29\x88\xa1\x4c\xe3\x05\x20\xea\x5e\x7b\xc5\x04\x6b\x7d\ -\x8f\x00\xfc\xc7\xe0\xfc\x55\x61\xac\xb1\x87\xf0\xbc\x5b\x81\x37\ -\x42\x57\x9e\x86\xc3\xc3\x6b\x68\xb4\x9a\x45\xfc\x37\xb7\x4e\x1f\ -\x31\x49\xa1\x62\x20\xb0\x75\xe7\x5d\x63\x9a\xb4\xbf\x4e\x84\x22\ -\xb6\xbe\x0b\xd4\x7a\x61\x7c\x18\x3e\x7a\xe4\xa8\x4e\x0e\xd6\xf8\ -\x6e\x48\xc2\xcd\x9e\x86\xa6\x59\x40\x32\x07\xf9\x6c\x94\xf3\x35\ -\xe7\xf8\xb1\xaf\x0b\x7b\xce\x5f\xa4\x92\xfc\xcc\xb0\x3a\x85\xcb\ -\x3c\x05\x19\x26\x1f\xf3\xed\x45\x00\x12\xeb\xc2\xba\xb0\x23\xd3\ -\x42\x58\x71\x72\x78\xb4\x4a\x4f\x99\x59\xa9\xc4\x04\x78\xfd\xf3\ -\x13\xf4\xe8\xe2\xc2\xb0\x8d\x37\xed\xfe\xbf\x61\xe2\x8a\x7f\x24\ -\x82\x00\x71\x2c\x01\xb9\x98\x2c\x1a\xda\xe7\xba\xfb\x46\xf8\x29\ -\x2c\xd5\xb9\x53\x32\x14\xba\xa3\x9d\xbd\xf4\xd9\xb1\x73\x23\xd6\ -\x6b\x23\x76\x82\xd6\xce\x5e\x93\x72\x6f\x8d\x26\xca\x3b\x9e\x12\ -\x60\x39\xd8\x78\xc9\x1f\xe8\x42\xb6\x0b\xd7\x41\x00\x3c\x08\xc9\ -\x3e\x70\xa4\xaa\xec\x0b\x43\x8f\xb0\xcb\xfb\xcb\xc5\x75\xff\xa1\ -\x55\x77\xcc\xa0\x07\x66\x17\xa8\xe2\x77\x0f\x9e\xa1\x8d\x3b\x8e\ -\x63\x46\xcc\x13\xb8\xb4\x75\x15\xdd\x09\x89\xd2\x59\xde\x05\x1e\ -\xfb\xe4\x10\xb2\x72\x4b\x95\x03\xc1\x3b\xe9\xb5\x65\x3b\xc2\x86\ -\xc5\xd8\x59\xa9\xf1\xfd\x05\x3c\x5e\xae\xc4\x22\x60\x8c\xef\x12\ -\xfa\x36\x40\x72\x2b\xb6\xd2\xe5\x2a\x1c\x56\x5f\x39\x2a\x50\xe3\ -\xb5\x0b\xbc\x4f\xb1\x3b\x56\x76\x7b\x58\xf0\x2a\x10\xc2\xe0\x19\ -\x58\xc5\x83\xe8\x6b\x91\x3b\xd5\xf5\x99\x3b\xcb\x4d\xee\xac\x14\ -\x72\xb9\x9d\x9f\x72\xd9\xe5\x82\x1f\x3f\x09\xa8\xf5\x71\x20\xe2\ -\x02\xfe\xa6\xc7\xd0\x05\x4f\x01\xe8\xf3\x56\xfa\x2c\x7d\xb3\x79\ -\x05\x40\xbf\xad\xb6\x15\x41\xcb\x9a\x7e\x51\xba\x7d\x2c\x40\x8d\ -\x97\x04\xb0\xd5\x5e\x62\xb8\xa5\x14\x56\xcb\x49\xf9\xaa\x55\xf0\ -\xaa\x91\x26\x9d\x66\x58\x50\xd3\x64\x52\x3c\x53\xeb\x83\x2a\x91\ -\x94\x27\x01\x35\x1b\xd7\x33\x7a\x50\x53\x89\xfc\x05\x20\xd8\x86\ -\x7c\x11\xea\x54\xd3\x04\x4b\xf1\xb9\xc3\x83\xeb\xf2\x59\xe3\x1a\ -\xae\xe0\x26\x60\x1a\x3b\xb1\xbb\x8c\x03\x0d\x13\x47\x02\xc6\x5a\ -\x03\x3b\x44\x90\x8c\x17\x41\x50\x82\xda\xc4\x96\x00\xeb\xf1\x00\ -\x33\xbf\xa5\xad\xa5\xe3\xe1\xb6\xd6\xb3\xd4\xd6\xda\x41\xad\xcd\ -\xa7\x7f\xc7\x65\x86\xdf\x91\x38\x26\x24\xe5\x88\x4c\x44\x43\x88\ -\x23\x62\xe2\xaf\xca\x4e\x48\x90\x21\x24\x92\x70\x48\x4a\x99\xc2\ -\x14\xd0\x22\x9b\xc2\x92\x36\x81\x08\x0f\x27\xc2\x0e\x48\x1c\x01\ -\x98\xa3\x42\x64\x98\x76\x44\xe3\xea\x9b\xe8\xb6\xeb\x72\x06\x02\ -\x21\xa6\x33\x74\xfb\xc6\xff\x9a\x2d\xf2\x00\xa5\x3b\x62\x84\x29\ -\x29\x01\x91\xcb\x49\x2c\xce\x52\xae\xe2\xd9\xdd\x90\x9f\xa1\xc0\ -\x87\x12\x76\xf1\xcc\x5c\x2a\xc1\x33\x03\xc1\xcf\x47\x03\xde\xde\ -\x4a\x50\xa8\xa3\x71\x62\xd5\xad\x85\x11\xab\x54\x2d\xbc\xd6\x74\ -\x87\x97\x4c\xc6\x5d\x40\x1d\x7b\x69\xed\xec\x09\x2b\xa6\x5c\x66\ -\xc4\x03\x38\x75\x4e\x3e\x02\x48\x59\xc7\x1b\xfe\xef\x3f\x3f\x39\ -\x42\xa7\x98\xba\x60\xcb\xbe\xd3\x64\xc4\x1b\xff\x30\xb9\x08\xc0\ -\x5b\xa0\x10\xfb\x0d\xb0\x72\xde\x86\x3d\x74\x29\x38\x68\xfb\x5c\ -\xd2\x24\xcd\x7d\x79\x0f\xf9\x03\x9a\x29\x1a\xfb\x55\x84\x79\x52\ -\xd9\x01\x9c\x56\x6f\x9f\x46\x0e\x71\x52\xad\xf2\xfe\x20\x8d\x7c\ -\x31\x02\xae\xb8\x1c\xd7\x69\x2f\x2f\x6d\x4f\xc4\x36\x98\x70\x53\ -\xd8\x33\x77\xfa\xea\x40\x20\x28\x4f\xb4\x7f\x23\x2e\x92\x9f\xf6\ -\x9f\x3a\xaf\x3f\x00\xf8\xb4\x0c\xb7\x2c\x2c\xca\x23\x97\xcb\xb9\ -\xb2\x85\xe8\xe9\x49\x65\x09\x7a\x1a\x9a\xd8\x7b\x76\xa2\x97\x00\ -\x0f\xcb\xdd\xb5\x7f\xd9\x55\xd4\x7b\xbe\xbf\x94\x95\x7e\x5a\x46\ -\xca\xa1\x99\xc5\xf9\xc7\x35\x4d\x0d\xc5\xf3\x4a\x47\x76\xb1\xa5\ -\xaa\x6c\x5c\x25\x20\xa1\x4b\x00\x44\xe0\x77\xfa\x8f\xe9\xdb\x9c\ -\xfc\x77\xcb\xca\xf2\x45\xc3\x9e\x6f\x6e\xda\x85\x27\xb7\x18\x01\ -\x96\x17\x5a\xaa\xca\x9f\x9c\x14\x96\x20\xbf\x21\x82\x8e\x4f\xc1\ -\x5f\xbf\x18\x38\xd6\x22\x5d\x20\x80\x16\x42\x00\xc1\xde\x81\x21\ -\x01\x18\x52\x64\x82\x12\xbd\x47\x56\x96\x8f\x1b\x01\x12\xb2\x0b\ -\xb4\x30\x00\x29\x5f\xd4\xc1\x4b\x7e\x07\xb6\x33\x14\xbc\xaa\x57\ -\x55\xc6\xe7\x01\x76\xe9\xf4\x66\x5a\xd0\xba\x78\xc1\xdb\x4e\x07\ -\x18\xc7\x5d\xf8\xc4\xc7\x45\xe3\x20\x24\x0f\xe8\x8c\x74\xd8\x81\ -\xeb\x63\xa8\x80\xc1\x1c\x1e\x34\x1b\x04\xbb\x80\xe5\x30\x31\x25\ -\xe0\x08\x94\x18\xc0\xbf\x44\x34\x70\x0a\xb4\x31\xda\x49\x0f\xae\ -\x8f\x9a\x8d\x92\x06\x8e\xc2\x3f\x1b\x0f\xf8\xe4\x4b\xc0\xe0\x37\ -\x41\x99\xb8\xab\x70\x38\x44\xee\x8c\xa2\x29\x0d\x99\xd9\xa9\x52\ -\xd3\xa4\x04\x78\xa7\x45\x85\xa9\xa4\x80\x75\x41\x5b\xeb\x99\x9f\ -\x06\xfc\x5a\x1a\x48\xd2\x81\xbe\x3f\x19\xf0\x2e\x23\xc4\x0b\x92\ -\xa7\x04\xcd\x73\xfd\x92\xde\x47\xfe\x63\x9d\x87\xd2\x38\x03\x2c\ -\x44\xfe\xd4\xec\xaf\x3a\x9f\xb8\xb9\xc8\xca\x78\x65\x6f\x36\x7f\ -\x70\xea\x44\xf7\xbd\xe7\xba\x2e\xe8\x36\xb2\x39\xbe\x1e\x41\x7b\ -\x0e\x84\x58\x17\xe9\xe3\xaa\xe4\x10\x40\x85\xb1\x44\x9a\xfa\x26\ -\x40\x3f\xf0\x14\xda\x80\xb7\x36\x06\xf2\x3a\x26\xfd\x48\xcc\x01\ -\xab\xb7\x37\xa3\xae\x27\xca\xd1\xd8\x6d\xe8\xe7\x6e\xfb\xe8\x00\ -\xdd\x77\xff\x5b\x04\xf0\xba\x8b\xab\x6b\xf6\x55\xb0\xf1\x4b\x62\ -\x48\xd2\x5b\xe4\x70\x78\x28\x3a\xd1\x7f\x84\x7a\xcb\xed\xe3\x0c\ -\xd5\x7a\xf9\x68\xc7\x32\xa2\x98\x1f\x44\xf1\x6f\x63\x8c\xde\x1e\ -\x20\x2b\x27\xc3\x25\xfd\xd1\x3e\x04\x90\x62\x26\x59\x95\x3c\x3e\ -\xf7\x17\x39\x74\xc6\x1e\x51\x1a\x59\x3a\x8b\x20\xb3\x40\xf8\x29\ -\x97\x33\xed\xb1\x73\x86\x04\xb9\x49\x0a\xab\x75\x45\x14\x65\x33\ -\x35\x9e\x41\x91\xd2\xec\x12\x0f\xe8\xb2\xcc\xff\xa8\xba\xa4\xe2\ -\x14\x91\x45\x59\xd2\x2d\x85\x53\x76\x21\xc0\x61\x8b\x27\xd5\xb9\ -\xd6\x9f\x62\xbc\x4b\xfc\xc6\x8a\xbd\x07\xf0\xef\x8d\xf6\xf3\xda\ -\xf1\xb2\x03\xee\xc2\xef\xc7\x31\x25\xa0\xae\xc2\x11\xc3\x96\x28\ -\xc6\x75\x94\x22\x1f\xca\x32\xcb\xaf\x26\x8e\x1d\x86\xd8\x02\xc9\ -\xf5\x06\x6b\xbc\x8f\xa2\xf2\x2b\xc3\x26\x3f\xf8\x31\x14\x47\x3c\ -\xa7\x11\x1f\x83\x8f\xf5\x32\xb5\xd6\x77\x3f\x1a\xbe\x17\x16\xbf\ -\xfe\x7d\xd1\x0f\xb0\xf5\x1e\xb2\x97\x2f\xc0\xdc\xab\xaf\x7c\x15\ -\xff\xa6\xe3\xda\xa4\x96\x05\x51\x3b\x26\xbb\x13\x24\x58\x03\xd0\ -\x99\x96\xc0\xeb\xba\x80\xad\xc9\x5c\x15\x48\x25\x79\x00\xf9\x57\ -\xb8\xe7\x93\xa3\xfc\x5a\xde\x19\x09\x7c\xbc\xe9\x7b\x01\x06\x00\ -\x19\x34\x01\x6e\xc9\xd6\xa8\xa6\x00\x00\x00\x00\x49\x45\x4e\x44\ -\xae\x42\x60\x82\ +\x00\x00\x80\x00\x00\x00\x80\x08\x06\x00\x00\x00\xc3\x3e\x61\xcb\ +\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\ +\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ +\xe3\x06\x0c\x15\x0f\x15\x69\x3a\x10\x2b\x00\x00\x0d\x23\x49\x44\ +\x41\x54\x78\xda\xed\x9d\x6b\x90\x14\xd5\x15\xc7\x7f\xdd\x33\xfb\ +\x5e\x77\x17\x16\xdc\x65\x05\x79\x28\x8f\x61\x02\x88\x51\x82\x51\ +\x8c\x8a\x6d\xb4\x24\x54\xa5\x8c\x26\xc4\x98\x92\x49\x52\x8a\x04\ +\x64\xc4\x07\xa9\x4a\x4c\x62\x2a\x11\x45\xd2\x46\x4a\x2a\x56\x92\ +\xd5\x32\xa5\x31\xe2\xe3\x03\x90\xc4\x4c\x51\x42\x10\x23\x2f\x79\ +\xac\xcb\x20\x8a\xe1\xb9\x2b\xcb\x63\x1f\x2c\xfb\x60\x77\xa6\xf3\ +\xa1\x17\xd8\xd9\x9d\x9e\x47\x4f\x37\x3b\x33\x7d\xcf\xa7\xdd\x9e\ +\xbe\xa7\xcf\xbd\xff\xff\xbd\x7d\xfa\xde\x73\xcf\x95\x70\xb2\xf8\ +\x03\xb3\x80\x99\xc0\x38\xa0\xa8\xe7\xea\x51\x60\x2b\xf0\x26\xaa\ +\x52\x97\xed\x4d\x20\x39\x14\xf8\xa7\x80\x5f\x24\x70\xe7\x26\xe0\ +\x21\x54\x65\xb7\x20\x40\x76\x00\x3f\x01\xf8\x18\x28\x48\xb2\xe4\ +\x1f\x50\x95\x45\x82\x00\x99\x0d\xfe\xb5\xc0\x96\x14\x34\x04\x50\ +\x95\xdb\x04\x01\x32\x13\xfc\xa1\x40\x83\x05\x9a\xde\x46\x55\xbe\ +\x23\x08\x90\x59\xe0\x4b\x40\x17\xe0\xb2\x48\xe3\x0d\xa8\xca\xa6\ +\x6c\x69\x1e\xb7\x03\xfa\xff\xbd\x16\x82\x0f\xf0\x01\xfe\x40\x0e\ +\xaa\xd2\x9d\x02\x29\x67\x00\x13\x81\xbc\x1e\x72\xee\x07\xd6\xa3\ +\x2a\x67\xc5\x08\x60\x6d\xef\x97\x81\x90\x0d\x9a\xaf\x41\x55\xb6\ +\x27\x69\xcb\xf5\xc0\x53\xc0\x2d\x31\xee\xda\x0e\x2c\x45\x55\xde\ +\x12\x23\x80\x35\x92\x6f\x93\x5e\x15\xb8\x31\x41\xe0\xf3\x81\x7f\ +\x00\x37\x27\x70\xf7\x57\x81\x55\xf8\x03\x7b\x81\x5b\x50\x95\x7a\ +\xbb\x1b\x48\xce\x72\x02\xdc\x61\x93\xde\x19\x3d\xbe\x45\x3c\xf0\ +\x47\x03\x2d\x09\x82\xdf\x5b\x26\x00\x75\x3d\xa3\x86\x20\x40\x0a\ +\x32\xd3\x46\xdd\x85\x71\xc0\x2f\x02\xf6\x01\x39\x29\xfa\x1b\x93\ +\x05\x01\xcc\x4b\x85\x8d\xba\xe3\x39\x96\x9f\x59\xf4\x8a\xdd\x8a\ +\x3f\xe0\x12\x04\x30\x27\xc7\x6c\xd4\x1d\x8a\xd1\xfb\x1f\x07\x86\ +\x59\xf4\x9c\x5c\x60\x85\x20\x80\x39\x59\x67\xa3\xee\xb6\x18\xbf\ +\x2d\xb1\xf8\x59\xf3\xf0\x07\xf2\x04\x01\x92\x97\x7f\xda\xa4\x77\ +\x23\xaa\xa2\x19\xf4\xfe\xaf\x00\x83\x6c\x78\xe6\x5c\x41\x80\xe4\ +\xa5\xc3\xae\x19\x86\x18\xbf\x3d\x61\xd3\x33\x6f\x15\x04\x48\xfa\ +\x6b\x5d\x09\x03\xf7\xd9\xa0\x79\x57\x9c\x6f\x79\x3b\x64\xa2\x20\ +\x80\x39\x79\x0d\x6b\x67\x03\xaf\x8f\x33\x0d\x9c\x6b\x53\x3d\x86\ +\x08\x02\x98\x1b\x05\x34\x0b\x3d\xf2\xb7\x51\x95\x0f\xe3\xdc\xa3\ +\xd9\x54\x93\x2e\x41\x00\xf3\x24\x38\x0e\x4c\x4b\x51\x4b\x20\xc1\ +\xa5\xe0\xd3\x36\xd5\xe2\xa8\x20\x40\x6a\x24\xd8\x0a\x78\x80\x76\ +\x13\xa5\x57\x24\x11\x0c\xf2\xae\x4d\x35\xd8\x21\x08\x90\x3a\x09\ +\xf6\xa2\x2a\x85\xc0\x6f\x12\x2c\xb1\x09\x98\x82\xaa\x2c\x4c\xe2\ +\x29\x4b\x6d\xb2\xfe\x1d\x3b\x94\x3a\x33\x28\xf4\xc2\x37\xfb\xac\ +\xc1\xe5\x45\xab\x3b\x3b\xbb\x09\x87\xf5\x57\x77\x4e\x8e\x0b\x59\ +\x96\xde\x6b\x6a\x6c\x9b\x6b\x7a\x35\x4e\x5f\xcd\x1b\x6f\xa1\xa5\ +\xad\xa8\xca\x25\x76\x34\x81\xdb\xc9\xf8\x7b\x26\x55\xad\x05\x90\ +\x24\x29\xc2\x87\xd3\x34\x76\x36\x3d\x79\xbd\xe9\xa5\xd8\xe2\xe2\ +\xfc\x7b\x5b\x5b\x3b\xb6\x59\x68\xea\x03\x76\xb5\x81\xa3\x09\x70\ +\x1e\x72\xcd\x3a\xc7\xdd\x53\x5d\x9b\x03\xbc\x5f\x77\xb8\x91\xe6\ +\xa6\x76\x2b\x54\x7e\x80\xaa\xbc\x6e\x57\xdd\x65\x01\xbf\x2d\xde\ +\xfa\x25\xc3\x47\x0e\x26\xbf\x20\x27\x55\x5d\x07\x80\x9b\xec\x34\ +\x56\x10\xc0\xca\x57\x4a\x75\xed\x09\x60\x28\x40\x38\xac\x71\xc5\ +\xb8\x4b\x29\x29\xcd\x07\x73\x23\xcc\x36\x60\x2c\xaa\x12\xb2\xd3\ +\x66\xf1\x0a\xb0\x0e\xfc\x93\xc0\xe0\xde\xd7\xc2\x61\x8d\xe1\x23\ +\xcb\x69\x2e\x6d\x6b\x3b\x7a\xb0\x31\x84\x44\x22\x8e\x5c\x18\x58\ +\x8c\xaa\x3c\x7f\x31\xec\x16\x23\x80\x35\xe0\x37\xf4\x05\xbf\x97\ +\x7f\x11\x2a\x29\x2d\x18\xce\xf3\x4a\x09\x70\x0f\xfa\x0a\x65\xdf\ +\xa5\x64\x0d\xd8\x00\x3c\x80\xaa\xb8\x2e\x16\xf8\x62\x04\xb0\x06\ +\xfc\xba\x73\xc3\xbe\x81\x54\x04\x7d\xde\xc6\x9e\x79\x88\x55\xc0\ +\xaa\x5e\x9f\x8b\xa5\xa8\x4a\xf3\x40\xda\x2f\x08\x90\x1a\xf8\xf5\ +\x40\x65\x8c\x5b\x86\x05\x7d\xde\x93\x86\xbf\x0e\x30\xf8\x82\x00\ +\xa9\xf7\xfc\xca\x38\x3d\xbf\x21\xdd\xeb\x21\x7c\x00\x73\xe0\xef\ +\x27\xf6\x0a\x63\x65\x26\x80\x2f\x46\x00\x73\xe0\x7f\x01\x8c\x8e\ +\x71\xcb\xa8\xa0\xcf\x7b\x2c\x53\xea\x23\x08\x00\x20\x81\x84\x44\ +\xcf\x34\x70\x2c\xf0\xff\x07\x8c\x8a\xa1\xe9\xf2\xa0\xcf\x7b\x38\ +\x93\xaa\xee\x68\x02\x04\x6b\xea\x26\x54\x0c\x2b\xa5\xb3\xa3\x8b\ +\xee\x50\x18\x97\x2c\xe3\xce\x91\x71\xb9\xe5\x91\x51\xc0\xaf\x89\ +\x03\xfe\x88\xa0\xcf\x7b\x24\x03\xb9\xef\x40\xf1\x07\x7e\x08\x3c\ +\x0d\x54\xc5\xb9\xf3\x55\xce\x9c\x7d\xc0\x33\x7d\xd4\x36\xc0\x1b\ +\xe3\xbe\x09\x41\x9f\xf7\xd3\x0c\x1d\xfc\x1c\x05\x7c\x01\xfa\x14\ +\x6b\x52\x01\x96\x95\x97\x95\x31\xa8\xbc\xd0\x28\xd8\x6b\x5c\xd0\ +\xe7\xfd\x2c\x53\x9b\xc4\xe5\x20\xf0\x2b\x81\x46\x8c\x26\x6d\x34\ +\xe3\xee\xd0\xda\xd2\x41\xa8\x3b\x44\x71\x49\xbf\xcd\xc6\x63\x83\ +\x3e\xef\xe7\x99\xdc\x2c\x2e\x87\x80\xef\x42\xdf\xa5\x1b\x29\xdd\ +\x61\x46\x0f\x2d\x64\xce\xd4\x4a\xbe\x3b\xb5\x82\xeb\x46\x95\x91\ +\xef\x96\xd9\x5f\xdf\x0a\x2e\x39\x62\x9c\xec\x68\xef\x42\x96\x25\ +\x0a\x8b\xce\x07\xfd\x5e\x1d\xf4\x79\xf7\x64\x7a\xd3\x38\xc5\x09\ +\x0c\xf6\xbd\x50\x5e\x98\xc3\x9a\x9f\x5c\xc5\xf4\x91\xa5\x51\x0b\ +\x2c\x7c\xf7\x53\x56\x6c\x38\x04\xf2\x85\x61\xa1\xa1\xbe\x99\xd2\ +\xb2\x02\xdc\x39\xae\xa9\x41\x9f\x77\x67\x96\x7c\x00\x0d\x48\x8f\ +\xbc\x01\x7d\xcf\xfc\x18\xf4\xed\xd3\x5d\xc0\x27\xe8\x69\x52\xb6\ +\x5b\xfc\xac\xc9\xf4\xd9\xc8\x31\x69\x58\x31\xbb\x1f\x9b\x1e\xb7\ +\xe8\xea\xda\xe3\xcc\xfe\xf3\xae\xc8\x56\xd2\xb4\x63\x3c\x7f\x5b\ +\x65\x16\x7d\x01\x5f\x34\xd0\x65\xe0\x19\x60\x71\x9c\xe7\xb6\x03\ +\x4f\xa2\x2a\xcf\x59\xf4\xdc\xe3\xf4\xda\x54\x21\x4b\x10\x5a\x9e\ +\xf8\x2e\xab\xa5\xeb\x0e\xf0\xb3\x35\x9f\xf7\xb5\xb8\x02\x55\x69\ +\xc8\x06\x02\xc8\x17\x09\xfc\x5b\x81\x4e\xe0\xd1\x04\x48\x57\x00\ +\x2c\xc3\x1f\x68\x4e\x39\x39\x82\xee\xf5\x47\xec\xa8\x59\x3f\xff\ +\x9a\xa4\x54\x2c\x99\x39\x8a\xbc\x9c\x7e\xcd\xf4\x58\xb6\x8c\x00\ +\xf2\x45\x00\x7f\x01\x10\x30\xe1\x6f\x94\x00\xbb\xf0\x07\xbe\x9d\ +\xc2\xd3\x23\xc0\x77\xcb\x12\x33\xc6\x94\x25\xad\xe4\xe1\x19\x23\ +\xfa\x5e\x5a\x20\x08\x90\x18\xf8\x77\x03\x2f\xa4\xa8\xe5\x1d\xfc\ +\x81\xa9\xa6\x4a\x76\x87\x23\x72\xec\xe4\xe7\x98\xab\xee\xc3\xdf\ +\xb8\xbc\xef\x1c\x40\x9e\x9d\x59\x3b\xb2\xe3\x2b\xc0\x1f\x18\x02\ +\xbc\x69\x91\xb6\x8d\x40\x71\xac\x1b\x3c\xd5\xb5\x95\xc0\x64\x60\ +\x2a\x70\x15\x30\xf9\xd4\xc9\xd6\x89\xc7\xea\x2e\x7c\xfd\x5d\x3e\ +\xa8\xc0\xd4\xc3\xab\x4a\xf2\x8c\x3e\xa1\x43\x82\x00\xc6\xf2\x8a\ +\x85\xba\x8a\xf0\x07\xaa\x51\x15\x9f\xa7\xba\xb6\xaa\x17\xd0\x53\ +\x7a\xfe\xf6\x44\x45\x48\x8e\xec\xf1\x87\x1a\xcd\x85\x69\x1f\x3b\ +\x1d\x35\x7f\x63\x48\x8c\x00\xc6\xbd\x7f\x28\x70\xa7\xa5\x9f\x2b\ +\x92\x74\xff\xc4\x57\x82\x73\xc3\xe1\x70\xc2\x65\x0a\x8b\x23\x7b\ +\x6e\x47\x57\xd8\xd4\xb3\x97\xaf\x3f\xd8\xd7\x75\xed\xb4\x3b\x5a\ +\x37\xd3\x7d\x80\x1f\x59\xad\x50\x0b\x6b\x52\x4b\x73\x72\x3d\xd8\ +\xed\x92\x23\xde\xdd\xdd\x61\x8d\x8d\x5f\x34\x25\xfd\xec\x17\x36\ +\xf6\x5b\xe1\x5d\x41\x96\x88\x5d\x04\xb8\xd1\x72\x8d\x12\x34\x9e\ +\x3c\x93\x5c\x11\x59\xc2\xe5\x8e\xac\xe2\x4d\x2f\x26\xb7\x63\x6b\ +\xe9\xba\x03\x74\xf6\x1f\x39\x96\x09\x02\xc4\x96\x2b\xed\x50\x7a\ +\xf6\x6c\x77\x32\x53\x57\xed\xc0\x96\xaa\x11\x83\x5e\xee\x7d\x31\ +\xac\xc1\xe4\x65\x1f\x25\xa4\x60\xed\x9e\x13\xd1\x26\x81\x4e\x66\ +\xcb\x24\x90\x9d\x4e\x60\xb9\x2d\x5a\x35\x7a\xe2\x76\x22\xe4\x0c\ +\x50\x03\xec\x44\x3f\x0d\xe4\x13\x60\x47\xd0\xe7\xed\xe8\xe5\x93\ +\xdc\x00\x8c\x3d\xf7\x6f\x4d\x7d\x2b\x43\x7e\xbe\x21\xfe\x5a\xc0\ +\xfa\x43\xe0\xea\xc7\xb8\x29\x64\x91\xd8\x45\x80\x16\x0c\x36\x4a\ +\xa4\x22\x61\x4d\x6b\x05\xe9\x55\xd0\x76\xf4\x80\xbe\x23\xe8\xf3\ +\x26\x92\x62\xdd\x03\x44\xe4\xf5\x39\xd9\xd6\xc5\x75\xcb\x37\x33\ +\xba\xa2\x88\xdb\xc7\x97\x33\xa6\xbc\x80\xd3\x9d\x21\x36\x1f\x6c\ +\xe6\xbd\x9a\x06\xc8\x75\x45\x03\x7f\x09\xaa\x72\x34\x9b\x08\x60\ +\xcf\x5a\x80\x3f\xb0\xc1\x16\x3f\x00\x3e\x44\x55\xae\x37\x69\x53\ +\x25\x50\x1f\x6b\x74\x89\xd3\x1a\x2f\xa1\x2a\x0f\x92\x65\x62\x97\ +\x0f\xb0\xc5\x26\xbd\xaf\x99\x2e\xa9\x2a\x5f\xa2\x27\x78\xde\x63\ +\xa2\x2b\x3c\x94\x8d\xe0\xdb\x39\x02\x5c\x8d\x7e\xf8\x81\xb5\xa2\ +\x2a\x92\x45\xf6\x25\x1e\x13\x08\xf3\x51\x95\x56\xb2\x54\xec\x5b\ +\x0e\xf6\x07\xf6\xf5\x76\xbc\x2c\x19\x55\x54\xe5\x6b\x16\xdb\xe8\ +\xa9\x18\x56\xba\xa7\xb3\xa3\x8b\xee\xee\x10\x2e\x97\x4b\x8f\x0a\ +\x76\x49\x6f\x34\x3c\x31\x7d\x0e\x0e\x10\x3b\xa7\x82\xef\x47\x4f\ +\xb2\x64\x95\xdc\x64\xb5\x81\x9e\x49\x55\x7b\xcf\x75\x83\x3e\xfb\ +\x02\x0e\x36\xe0\x0c\xb1\x6f\x35\x50\x4f\xa8\xf8\xa2\x45\xda\x16\ +\xa3\x2a\xed\xb6\xd9\xaa\xe9\x69\x62\x34\x0d\xc7\x89\xbd\xcb\xc1\ +\xaa\xf2\x53\xe0\x5f\x29\x6a\xf9\x23\xaa\xf2\x7b\x84\x64\x20\x01\ +\x74\x12\xdc\x91\xc2\x48\xf0\x08\xaa\x32\x4f\xc0\x94\xc9\x04\xb8\ +\x30\x12\x4c\x27\x4a\x74\xae\x81\x6c\x42\xcf\x8f\xa3\x0a\x88\x32\ +\xd7\x09\xec\x4b\x82\xcd\xc0\x44\xfc\x81\x49\xe8\x29\xdc\x6f\x44\ +\xdf\x65\x5b\x80\x3e\x9d\xbb\x17\x3d\x4d\xca\xcb\xa8\xca\x41\x01\ +\x4d\xb6\x11\xe0\x02\x11\x6a\x80\xc7\x45\xd3\x3b\xe9\x15\x20\x44\ +\x10\x40\x88\x20\x80\x10\x41\x00\x21\x82\x00\xe9\x28\x92\x9e\x31\ +\x5c\x72\x60\xba\x0c\x91\x22\x26\xc1\x14\x31\x59\xcc\x7d\x07\x8a\ +\x58\x0e\x76\x28\x01\x4c\xa6\x88\x01\x1e\x44\x55\x5e\x12\x04\xc8\ +\x6c\xf0\x45\x48\x98\x63\x09\xa0\x6f\xe4\xec\x7f\xd8\x63\x77\x38\ +\x76\x50\x68\x7f\x59\x82\xaa\x3c\x23\x08\x90\x79\x04\xe8\x17\x9d\ +\x94\x50\x8a\x98\xe8\x61\xe1\xc3\xb3\x29\x32\x58\x72\x00\xf8\xa6\ +\x53\xc4\xac\xdd\x73\x82\x59\x7f\xda\x19\x6d\x63\xc8\x90\x6c\x69\ +\x1e\x27\xcc\x03\xac\x8b\xa8\xb0\x44\x42\xe0\x03\xdc\x39\x71\x08\ +\x4f\xcf\xba\xb2\xef\x4e\x94\x72\xfc\x81\x4b\x05\x01\x32\xc7\xeb\ +\x17\x29\x62\x1c\x3c\x02\x88\x14\x31\x0e\x27\xc0\xcc\xde\xff\x88\ +\x14\x31\xce\x23\x40\x49\xef\x7f\x6c\x48\x11\x23\x08\x90\xe6\x12\ +\x91\x1e\x56\xa4\x88\x71\x1e\x01\x22\xbe\x00\x44\x8a\x18\xe7\x11\ +\xe0\x44\xef\x7f\x44\x8a\x18\xa7\x11\x40\xdf\x4d\x14\x41\x02\x91\ +\x22\xc6\x59\x23\x40\xbf\x2f\x81\x64\x52\xc4\xac\xae\x3d\x9e\xf5\ +\x29\x62\xb2\x9f\x00\xaa\xb2\x1b\x88\x38\xd1\xe3\x5c\x8a\x98\x8f\ +\x0e\x1a\x9f\xdb\xb8\xf0\xdd\x4f\x99\xdd\x7f\x1a\x18\xb2\x2c\x45\ +\x8c\x58\x0d\x14\xab\x81\xce\x90\x31\x2b\x77\xa9\x5f\xec\x6b\x58\ +\x64\x58\xe3\x58\xf1\x00\x1a\x0c\x1e\x52\xc4\x15\xe3\x2b\x72\xb7\ +\xde\x7d\x45\x57\x36\xb5\x8b\x23\x82\x42\x3d\xd5\xb5\x72\x5e\xbe\ +\x7b\xd1\xf8\x49\xc3\xc8\xcd\x73\x47\x3f\xfc\xc9\x08\xfc\xb0\x46\ +\xe5\xf0\x32\x2a\x2e\x2b\xa5\xf5\x74\xc7\x82\x6c\x6b\x1b\xa7\x44\ +\x05\x2f\x05\x90\x25\x89\x2b\x27\x54\x50\x35\xa2\x0c\xb7\xdb\x65\ +\x74\x0a\xd8\x79\xe0\x4b\xcb\x0a\x19\x3f\xa9\x8a\x41\x83\xcf\x9f\ +\x18\xb6\xdc\x53\x5d\x9b\x2b\x5e\x01\x99\xd5\xfb\xa3\xbe\xff\x65\ +\x59\xa2\xfe\x48\xd3\xed\xa7\x4e\x9e\xf1\x00\x93\x80\x4b\x81\x66\ +\xe0\x30\x9a\xb6\x71\xe2\x94\xe1\x6f\x81\x56\x10\x25\x69\xc4\x63\ +\x41\x9f\xf7\x39\x41\x80\xcc\x21\xc0\x32\xf4\x93\x4a\xfa\x4a\x6b\ +\xd0\xe7\xbd\x24\x46\xb9\xe9\xc0\x7f\x0d\x7e\xce\x0f\xfa\xbc\x9d\ +\xe2\x15\x90\xfe\xe0\xbb\x0d\xc0\x07\xb8\x36\x56\xd9\xa0\xcf\xfb\ +\x11\xfa\xb6\xf5\x68\xf2\xb0\xf0\x01\x32\x43\x8c\x3e\xd9\x5a\x82\ +\x3e\xef\xde\x04\xca\xcf\x34\xd2\xeb\xa9\xae\xcd\x13\x04\x48\xef\ +\xde\x9f\x03\x3c\x62\xf0\xf3\xb4\x44\x74\x04\x7d\xde\xcd\xc0\x69\ +\xa3\xd9\x05\x41\x80\xf4\x96\x67\x0d\xae\x37\x27\x79\xd0\xb3\xd1\ +\x19\x73\x4f\x7b\xaa\x6b\xf3\x05\x01\xd2\xb3\xf7\xe7\x02\x8b\xcc\ +\xbc\xfb\xa3\x8c\x02\x5b\x88\x76\xec\xac\x2e\x8f\x08\x02\x64\x56\ +\xef\x6f\x34\x79\xd2\xb7\x91\x2f\xf0\x5b\x4f\x75\x6d\x81\x20\x40\ +\x7a\xf5\xfe\xbc\x18\x5e\xfa\x34\x33\x3a\x83\x3e\xef\x36\xc0\x28\ +\x90\x60\xb1\x20\x40\x7a\x89\xd1\x5a\xfd\xa9\x14\x8f\x7a\x37\xf2\ +\x05\x7e\xe3\xa9\xae\x2d\x14\x04\x48\x8f\xde\x9f\x8f\x71\xc8\xf6\ +\xb4\x54\x74\x07\x7d\xde\xed\xc0\x29\x83\x9f\x1f\x15\x04\x48\x0f\ +\x31\x9a\xa2\x3d\x11\xf4\x79\xf7\x5b\xa0\x5f\x31\xb8\xfe\x6b\x4f\ +\x75\x6d\x91\x20\xc0\x00\xca\xbc\x23\xe1\x1c\x60\xbe\xc1\xcf\x96\ +\xa4\x99\x0f\xfa\xbc\x1f\xd3\x27\xc4\xac\x97\x64\xe4\x6e\xa1\xcc\ +\x5c\x0b\xf0\x07\xf2\x81\x1f\xa0\x67\x1b\x1d\x0b\xe0\x72\xcb\xa5\ +\x25\x25\xf9\x9e\x82\xe2\x3c\x4a\x4b\x0b\x90\x24\xce\x65\xff\x3e\ +\x1e\xf4\x79\x2d\xdb\xcb\xe7\xa9\xae\x9d\x8a\x7e\x38\x95\xde\x80\ +\x12\x84\xc3\x1a\x2d\xcd\xed\xd4\x1d\x6e\x7a\x05\x18\x87\xbe\x67\ +\xa0\x0b\xf8\x1c\x3d\x32\xf9\x35\x54\x45\x13\x04\x48\x1d\xf8\x41\ +\xe8\x47\xd2\xce\x36\xbc\x47\x03\x34\x8d\xb2\x21\x45\x54\x56\x95\ +\x22\x49\xd2\xe8\xa0\xcf\x7b\xc0\x62\x5f\xa3\x01\x18\x1a\x0a\x69\ +\x7c\x79\xb4\x89\x96\xc6\x36\x9d\x09\xb1\x5b\xf3\x0d\xe0\xc7\xa8\ +\xca\x99\x74\x6a\x52\x39\x83\xc0\x7f\xa8\xc7\x09\x9b\x1d\x97\xd2\ +\xb2\x44\xd3\xa9\x36\xf6\xee\xae\x0b\x07\x6b\xea\xae\xb6\xda\x14\ +\xb7\x5b\xbe\xa5\xa9\xb1\x8d\x7d\xb5\x75\xb4\x34\xb7\xeb\x5b\x8e\ +\xe3\x77\xa5\xef\x01\xad\xf8\x03\x73\x04\x01\x92\x07\x7f\x25\x66\ +\x52\xce\xcb\x92\x0c\xbc\x8d\x3f\x60\x69\x24\x4f\xcd\x8e\x23\x73\ +\xea\x0f\x37\x61\x32\xaf\xdc\xeb\xf8\x03\xbf\x14\xaf\x80\xc4\xc1\ +\x5f\x00\xbc\x60\x81\xa6\x59\xa8\xca\x5a\x0b\xec\x99\x07\xac\xb4\ +\xc0\x9e\x7b\x50\x95\x55\x82\x00\xb1\x1b\x7b\x04\x70\xc8\x32\x7d\ +\xa9\x9e\x3a\xa6\x3b\x9f\x56\x1e\x5d\x53\x31\xd0\x7b\x0c\xd2\xfd\ +\x15\xb0\xc6\x62\x42\xbd\x9f\xa2\x86\x6d\x16\xd7\x6f\xe5\x40\x37\ +\xb0\x9c\xc6\xbd\x7f\x14\x30\xd9\x62\xad\xa9\x9e\x66\xea\xb1\xd8\ +\x9e\xbb\xf0\x07\x86\x0a\x02\x18\xbd\xb3\xed\xa8\xaf\x3f\x70\x9b\ +\x49\x42\x3e\x6e\x53\x7b\x7d\x53\x10\x20\xba\xdc\x67\x93\x5e\xb3\ +\x33\x76\xf3\x6d\xb2\xe7\x5b\x82\x00\xd1\x65\x9a\x4d\x7a\x67\x98\ +\x2c\x57\x62\x93\x3d\x53\x05\x01\x32\xa3\xce\x76\x65\x56\x2f\x10\ +\x04\xb8\xb8\x12\x36\x59\xae\xdb\x26\x7b\x3a\x04\x01\xa2\x8b\x5d\ +\x47\xd0\x6f\x34\x59\xae\xc5\x26\x7b\xb6\x0b\x02\x44\x97\xbf\xda\ +\xa4\xd7\x6c\x76\x8f\x17\x6d\xb2\x67\x8d\x20\x40\x74\x59\x6d\xcb\ +\xf0\xaf\x2a\xff\x36\x55\x52\x55\x9e\x4d\xe1\xf5\x11\x4b\xde\x13\ +\x04\x88\xde\xe0\x07\x81\xdd\x16\x6b\xfd\x4f\x8a\xe5\x83\x16\xdb\ +\xb3\x0a\x55\x39\x2e\x08\x60\x2c\xb3\x2c\x26\xd5\xcd\x29\x6a\xb8\ +\xc6\xe2\xfa\xcd\x1f\xe8\x06\x4e\x6f\x02\xa8\xca\x61\x60\x61\xda\ +\x90\x49\x55\x3a\x00\xab\x4e\x0d\xb9\x67\xa0\x7b\x7f\x66\x7c\x06\ +\xaa\xca\x0a\x52\xcf\xcb\xb7\xd0\x92\xa5\x60\xdd\x9e\x97\x80\xdf\ +\xa5\xa8\xe5\x57\xe9\xb0\x14\x0c\x99\x14\x12\xa6\x47\x04\x99\xf1\ +\xc4\xef\x42\x55\xde\xb1\xc1\x9e\xb9\x40\xb5\x89\x92\xdf\x47\x55\ +\xfe\x96\x2e\xcd\x9a\x39\x13\x41\xaa\xb2\x12\x18\x0c\xbc\x95\x60\ +\x89\xbf\x00\xb9\xb6\x80\xaf\xdb\xf3\x32\x50\x06\xfc\x3d\xc1\x12\ +\x6f\x00\xc5\xe9\x04\x7e\x66\x8d\x00\x91\xbd\xef\x5c\x54\xf0\x74\ +\x60\x34\x7a\x14\x6e\x37\xb0\x0f\xd8\x78\xd1\x1b\x59\xb7\x67\x0e\ +\xf0\x75\x60\x4c\x8f\x3d\x67\xd1\x83\x59\xd6\x93\xc6\x51\xc1\xff\ +\x07\xc0\x9f\x05\x7f\x94\xb9\x48\x5b\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ \x00\x00\x0f\x91\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -1034,11 +1049,11 @@ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x03\ -\x00\x00\x00\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x0f\x98\ -\x00\x00\x00\x70\x00\x00\x00\x00\x00\x01\x00\x00\x2d\xd6\ +\x00\x00\x00\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x10\x89\ +\x00\x00\x00\x70\x00\x00\x00\x00\x00\x01\x00\x00\x2e\xc7\ \x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x02\xef\ -\x00\x00\x00\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x1f\x2d\ +\x00\x00\x00\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x20\x1e\ " def qInitResources(): diff --git a/usdmanager/utils.py b/usdmanager/utils.py index d4eae89..9824f58 100644 --- a/usdmanager/utils.py +++ b/usdmanager/utils.py @@ -83,6 +83,30 @@ def expandPath(path, parentPath=None, sdf_format_args=None): return os.path.expandvars(os.path.expanduser(os.path.normpath(path))) +def expandUrl(path, parentPath=None): + """ Expand and normalize a URL that may have variables in it and a query string after it. + + :Parameters: + path : `str` + File path + parentPath : `str` | None + Parent file path this file is defined in relation to. + Helps with asset resolution. + :Returns: + Normalized path with variables expanded. + :Rtype: + `str` + """ + sdf_format_args = {} + if "?" in path: + sdf_format_args.update(sdfQuery(QtCore.QUrl(path))) + path, query = path.split("?", 1) + query = "?" + query + else: + query = "" + return QtCore.QUrl(os.path.abspath(expandPath(path, parentPath, sdf_format_args)) + query) + + def findModules(subdir): """ Find and import all modules in a subdirectory of this project. Ignores any files starting with an underscore or tilde. @@ -107,12 +131,14 @@ def findModules(subdir): return modules -def generateTemporaryUsdFile(usdFileName): +def generateTemporaryUsdFile(usdFileName, tmpDir=None): """ Generate a temporary ASCII USD file that the user can edit. :Parameters: usdFileName : `str` Binary USD file path + tmpDir : `str` | None + Temp directory to create the new unzipped directory within :Returns: Temporary file name :Rtype: @@ -120,7 +146,7 @@ def generateTemporaryUsdFile(usdFileName): :Raises OSError: If usdcat fails """ - fd, tmpFileName = tempfile.mkstemp(suffix=".usd") + fd, tmpFileName = tempfile.mkstemp(suffix=".usd", dir=tmpDir) try: usdcat(usdFileName, tmpFileName, format="usda") finally: @@ -180,7 +206,7 @@ def usdzip(inputs, dest): # TODO: Support nested references (e.g. @set.usdz[areas/shire.usdz[architecture/BilboHouse/Table.usd]]@) -def unzip(path, layer=None): +def unzip(path, layer=None, tmpDir=None): """ Unzip a usdz format file to a temporary directory. :Parameters: @@ -189,6 +215,8 @@ def unzip(path, layer=None): layer : `str` | None Default layer within file (e.g. the portion within the square brackets here: @foo.usdz[path/to/file/within/package.usd]@) + tmpDir : `str` | None + Temp directory to create the new unzipped directory within :Returns: Destination file :Rtype: @@ -198,8 +226,7 @@ def unzip(path, layer=None): :Raises ValueError: If default layer not found """ - # TODO: Clean up this temp dir when exiting the app? - destDir = tempfile.mkdtemp(prefix="usdmanager_usdz_") + destDir = tempfile.mkdtemp(prefix="usdmanager_usdz_", dir=tmpDir) cmd = "unzip {} -d {}".format(path, destDir) logger.debug(cmd) try: diff --git a/usdmanager/version.py b/usdmanager/version.py index e4c65de..2797b2f 100644 --- a/usdmanager/version.py +++ b/usdmanager/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = '0.4.1' +__version__ = '0.5.0'