From 615e675b129075f50910b5924da1067299c272b1 Mon Sep 17 00:00:00 2001 From: mds-dwa Date: Mon, 22 Jul 2019 13:37:09 -0700 Subject: [PATCH] [BUG] Progress bar works when reading USDZ files [BUG] Find case sensitivity preference saves properly [BUG] Do not prompt to save when opening a new file [BUG] Windows support for usdcat and unzipping USDZ [ENH] Add line limit user preference for loading large files [ENH] Preferences and defaults maintenance, Advanced tab added to Preferences Version up to 0.7.0 Preferences and defaults no longer maintained in two separate places. Changing preferences no longer force reloads your current tab. Line limit increased from 10,000 to 50,000, with a user preference added to override this. Switch to zipfile module instead of unzip command for reading USDZ Signed-off-by: mds-dwa --- usdmanager/__init__.py | 127 ++++++++++++++--------- usdmanager/constants.py | 3 +- usdmanager/preferences_dialog.py | 40 +++++--- usdmanager/preferences_dialog.ui | 169 +++++++++++++++++++++++-------- usdmanager/utils.py | 42 +++++--- usdmanager/version.py | 2 +- 6 files changed, 263 insertions(+), 120 deletions(-) diff --git a/usdmanager/__init__.py b/usdmanager/__init__.py index 54c40f2..a35b65e 100755 --- a/usdmanager/__init__.py +++ b/usdmanager/__init__.py @@ -163,7 +163,6 @@ class UsdMngrWindow(QtWidgets.QMainWindow): - AddressBar file completer has problems occasionally. - Figure out why network printers aren't showing up. Linux or DWA issue? macOS and Windows are fine. - - When reading in a USDZ file, the progress bar gets stuck. - Qt.py problems: - PyQt5 @@ -208,7 +207,7 @@ def __init__(self, parent=None, **kwargs): # externally. The user's preferred programs are stored in # self.programs. self.defaultPrograms = {x: "" for x in USD_EXTS} - self.defaultPrograms.update(self.app.appConfig.get("defaultPrograms", {})) + self.defaultPrograms.update(self.app.DEFAULTS['defaultPrograms']) self.programs = self.defaultPrograms self.masterHighlighters = {} @@ -247,12 +246,14 @@ def setupUi(self): self.baseInstance = utils.loadUiWidget('main_window.ui', self) # You now have access to the widgets defined in the ui file. - self.defaultDocFont = QtGui.QFont() - self.defaultDocFont.setStyleHint(QtGui.QFont.Courier) - self.defaultDocFont.setFamily("Monospace") - self.defaultDocFont.setPointSize(9) - self.defaultDocFont.setBold(False) - self.defaultDocFont.setItalic(False) + # Update some app defaults that required the GUI to be created first. + defaultDocFont = QtGui.QFont() + defaultDocFont.setStyleHint(QtGui.QFont.Courier) + defaultDocFont.setFamily("Monospace") + defaultDocFont.setPointSize(9) + defaultDocFont.setBold(False) + defaultDocFont.setItalic(False) + self.app.DEFAULTS['font'] = defaultDocFont self.readSettings() self.compileLinkRegEx() @@ -288,13 +289,13 @@ def setupUi(self): {}""" searchPaths = QtGui.QIcon.themeSearchPaths() - extraSearchPaths = [x for x in self.app.appConfig.get("themeSearchPaths", []) if x not in searchPaths] + extraSearchPaths = [x for x in self.app.DEFAULTS['themeSearchPaths'] if x not in searchPaths] if extraSearchPaths: searchPaths = extraSearchPaths + searchPaths QtGui.QIcon.setThemeSearchPaths(searchPaths) # Set the preferred theme name for some non-standard icons. - QtGui.QIcon.setThemeName(self.app.appConfig.get("iconTheme", "crystal_project")) + QtGui.QIcon.setThemeName(self.app.DEFAULTS['iconTheme']) # Try to adhere to the freedesktop icon standards (https://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html). # Some icons are preferred from the crystal_project set, which sadly follows different naming standards. @@ -633,27 +634,27 @@ def readSettings(self): """ Read in user config settings. """ logger.debug("Reading user settings from {}".format(self.config.fileName())) - # Get basic preferences. - # TODO: Read some of these from the same places as the preferences dialog so we don't have to maintain defaults in 2 places. + default = self.app.DEFAULTS self.preferences = { - 'parseLinks': self.config.boolValue("parseLinks", True), - 'newTab': self.config.boolValue("newTab", False), - 'syntaxHighlighting': self.config.boolValue("syntaxHighlighting", True), - 'teletype': self.config.boolValue("teletype", True), - 'lineNumbers': self.config.boolValue("lineNumbers", True), - 'showAllMessages': self.config.boolValue("showAllMessages", True), - 'showHiddenFiles': self.config.boolValue("showHiddenFiles", False), - 'font': self.config.value("font", self.defaultDocFont), - 'fontSizeAdjust': int(self.config.value("fontSizeAdjust", 0)), - 'findMatchCase': self.config.boolValue("findMatchCase", self.checkBoxMatchCase.isChecked()), - 'includeVisible': self.config.boolValue("includeVisible", self.actionIncludePanel.isChecked()), - 'lastOpenWithStr': self.config.value("lastOpenWithStr", ""), - 'textEditor': self.config.value("textEditor", os.getenv("EDITOR", self.app.appConfig.get("textEditor", "nedit"))), - 'diffTool': self.config.value("diffTool", self.app.appConfig.get("diffTool", "xdiff")), - 'autoCompleteAddressBar': self.config.boolValue("autoCompleteAddressBar", True), - 'useSpaces': self.config.boolValue("useSpaces", True), - 'tabSpaces': int(self.config.value("tabSpaces", 4)), - 'theme': self.config.value("theme", None), + 'parseLinks': self.config.boolValue("parseLinks", default['parseLinks']), + 'newTab': self.config.boolValue("newTab", default['newTab']), + 'syntaxHighlighting': self.config.boolValue("syntaxHighlighting", default['syntaxHighlighting']), + 'teletype': self.config.boolValue("teletype", default['teletype']), + 'lineNumbers': self.config.boolValue("lineNumbers", default['lineNumbers']), + 'showAllMessages': self.config.boolValue("showAllMessages", default['showAllMessages']), + 'showHiddenFiles': self.config.boolValue("showHiddenFiles", default['showHiddenFiles']), + 'font': self.config.value("font", default['font']), + 'fontSizeAdjust': int(self.config.value("fontSizeAdjust", default['fontSizeAdjust'])), + 'findMatchCase': self.config.boolValue("findMatchCase", default['findMatchCase']), + 'includeVisible': self.config.boolValue("includeVisible", default['includeVisible']), + 'lastOpenWithStr': self.config.value("lastOpenWithStr", default['lastOpenWithStr']), + 'textEditor': self.config.value("textEditor", default['textEditor']), + 'diffTool': self.config.value("diffTool", default['diffTool']), + 'autoCompleteAddressBar': self.config.boolValue("autoCompleteAddressBar", default['autoCompleteAddressBar']), + 'useSpaces': self.config.boolValue("useSpaces", default['useSpaces']), + 'tabSpaces': int(self.config.value("tabSpaces", default['tabSpaces'])), + 'theme': self.config.value("theme", default['theme']), + 'lineLimit': int(self.config.value("lineLimit", default['lineLimit'])), } # Read 'programs' settings object into self.programs. @@ -736,6 +737,7 @@ def writeSettings(self): self.config.setValue("useSpaces", self.preferences['useSpaces']) self.config.setValue("tabSpaces", self.preferences['tabSpaces']) self.config.setValue("theme", self.preferences['theme']) + self.config.setValue("lineLimit", self.preferences['lineLimit']) # Write self.programs to settings object exts = self.programs.keys() @@ -1918,11 +1920,14 @@ def editPreferences(self): dlg = PreferencesDialog(self) # Open dialog. if dlg.exec_() == dlg.Accepted: - # Save new preferences. + # Users currently have to refresh to see these changes. self.preferences['parseLinks'] = dlg.getPrefParseLinks() - self.preferences['newTab'] = dlg.getPrefNewTab() self.preferences['syntaxHighlighting'] = dlg.getPrefSyntaxHighlighting() self.preferences['teletype'] = dlg.getPrefTeletypeConversion() + self.preferences['theme'] = dlg.getPrefTheme() + + # These changes do not require the user to refresh any tabs to see the change. + self.preferences['newTab'] = dlg.getPrefNewTab() self.preferences['lineNumbers'] = dlg.getPrefLineNumbers() self.preferences['showAllMessages'] = dlg.getPrefShowAllMessages() self.preferences['showHiddenFiles'] = dlg.getPrefShowHiddenFiles() @@ -1932,7 +1937,7 @@ def editPreferences(self): self.preferences['font'] = dlg.getPrefFont() self.preferences['useSpaces'] = dlg.getPrefUseSpaces() self.preferences['tabSpaces'] = dlg.getPrefTabSpaces() - self.preferences['theme'] = dlg.getPrefTheme() + self.preferences['lineLimit'] = dlg.getPrefLineLimit() # Update font and line number visibility in all tabs. self.tabWidget.setFont(self.preferences['font']) @@ -1964,9 +1969,6 @@ def editPreferences(self): else: self.addressBar.setCompleter(QtWidgets.QCompleter()) - if not self.currTab.isDirty(): - self.refreshTab() - self.writeSettings() @Slot(int) @@ -1977,7 +1979,7 @@ def updatePreference_findMatchCase(self, checked): checked : `int` State of checkbox. """ - checked = checked & QtCore.Qt.Checked + checked = checked == QtCore.Qt.Checked if checked != self.preferences['findMatchCase']: self.preferences['findMatchCase'] = checked for lang, h in self.masterHighlighters.iteritems(): @@ -2185,7 +2187,7 @@ def restoreTab(self, tab): self.menuRecentlyClosedTabs.setEnabled(False) # Update settings in the recently re-opened tab that may have changed. - if self.preferences['font'] != self.defaultDocFont: + if self.preferences['font'] != self.app.DEFAULTS['font']: tab.textBrowser.setFont(self.preferences['font']) tab.textEditor.setFont(self.preferences['font']) tab.lineNumbers.setVisible(self.preferences['lineNumbers']) @@ -2348,7 +2350,7 @@ def launchTextEditor(self): def launchUsdView(self): """ Launch the current file in usdview. """ - app = self.app.appConfig.get("usdview", "usdview") + app = self.app.DEFAULTS['usdview'] path = self.currTab.getCurrentPath() # Files with spaces have to be double-quoted on Windows for usdview. if os.name == "nt": @@ -2557,9 +2559,9 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos vScrollPos : `int` Vertical scroll bar position. """ - # Check if the current tab is dirty before doing anything. + # If we're staying in the current tab, check if the tab is dirty before doing anything. # Perform save operations if necessary. - if not self.dirtySave(): + if not newTab and not self.dirtySave(): return True # Re-cast the QUrl so any query strings are evaluated properly. @@ -2681,6 +2683,8 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos layer = utils.queryItemValue(link, "layer") dest = utils.unzip(fileStr, layer, self.app.tmpDir) self.restoreOverrideCursor() + self.statusbar.removeWidget(loadingProgressBar) + self.statusbar.removeWidget(loadingProgressLabel) return self.setSource(QtCore.QUrl(dest)) else: if ext == "usda": @@ -2709,11 +2713,13 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos # TODO: Figure out a better way to handle streaming text for large files like Crate geometry. # Large chunks of text (e.g. 2.2 billion characters) will cause Qt to segfault when creating a QString. - if length > LINE_LIMIT: - length = LINE_LIMIT + lineLimit = self.preferences['lineLimit'] + if length > lineLimit: + length = lineLimit truncated = True fileText = fileText[:length] - warning = "Extremely large file! Capping display at {:,d} lines.".format(LINE_LIMIT) + warning = "Extremely large file! Capping display at {:,d} lines. You can edit this cap in the "\ + "Advanced tab of Preferences.".format(lineLimit) loadingProgressBar.setMaximum(length - 1) if self.stopLoadingTab: @@ -4274,13 +4280,40 @@ def run(self): try: logger.info("Loading app config from {}".format(appConfigPath)) with open(appConfigPath) as f: - self.appConfig = json.load(f) + appConfig = json.load(f) except Exception as e: logger.error("Failed to load app config from {}: {}".format(appConfigPath, e)) - self.appConfig = {} + appConfig = {} + + # Define app defaults that we use when the user preference doesn't exist and when resetting preferences in the + # Preferences dialog. + self.DEFAULTS = { + 'autoCompleteAddressBar': True, + 'defaultPrograms': appConfig.get("defaultPrograms", {}), + 'diffTool': appConfig.get("diffTool", "xdiff"), + 'findMatchCase': False, + 'fontSizeAdjust': 0, + 'iconTheme': appConfig.get("iconTheme", "crystal_project"), + 'includeVisible': True, + 'lastOpenWithStr': "", + 'lineLimit': LINE_LIMIT, + 'lineNumbers': True, + 'newTab': False, + 'parseLinks': True, + 'showAllMessages': True, + 'showHiddenFiles': False, + 'syntaxHighlighting': True, + 'tabSpaces': 4, + 'teletype': True, + 'textEditor': os.getenv("EDITOR", appConfig.get("textEditor", "nedit")), + 'theme': None, + 'themeSearchPaths': appConfig.get("themeSearchPaths", []), + 'usdview': appConfig.get("usdview", "usdview"), + 'useSpaces': True, + } # Documentation URL. - self.appURL = self.appConfig.get("appURL", "https://github.com/dreamworksanimation/usdmanager") + self.appURL = appConfig.get("appURL", "https://github.com/dreamworksanimation/usdmanager") # Create a main window. window = self.newWindow() diff --git a/usdmanager/constants.py b/usdmanager/constants.py index 7fb3c29..2664ba3 100644 --- a/usdmanager/constants.py +++ b/usdmanager/constants.py @@ -52,7 +52,8 @@ # Truncate loading files with more lines than this. # Display can slow down and/or become unusable with too many lines. -LINE_LIMIT = 10000 +# This number is less important than the total number of characters and can be overridden in Preferences. +LINE_LIMIT = 50000 # Truncate loading files with more total chars than this. # QString crashes at ~2.1 billion chars, but display slows down way before that. diff --git a/usdmanager/preferences_dialog.py b/usdmanager/preferences_dialog.py index 08b1916..d045eeb 100644 --- a/usdmanager/preferences_dialog.py +++ b/usdmanager/preferences_dialog.py @@ -19,6 +19,7 @@ from Qt.QtGui import QIcon, QRegExpValidator from Qt.QtWidgets import (QAbstractButton, QDialog, QDialogButtonBox, QFontDialog, QLineEdit, QMessageBox, QVBoxLayout) +from .constants import LINE_LIMIT from .utils import loadUiWidget @@ -77,6 +78,7 @@ def setupUi(self, widget): self.lineEditTextEditor.setText(parent.preferences['textEditor']) self.lineEditDiffTool.setText(parent.preferences['diffTool']) self.themeWidget.setChecked(parent.preferences['theme'] == "dark") + self.lineLimitSpinBox.setValue(parent.preferences['lineLimit']) self.updateFontLabel() # ----- Programs tab ----- @@ -189,6 +191,15 @@ def getPrefAutoCompleteAddressBar(self): """ return self.checkBox_autoCompleteAddressBar.isChecked() + def getPrefLineLimit(self): + """ + :Returns: + Number of lines to display before truncating a file. + :Rtype: + `int` + """ + return self.lineLimitSpinBox.value() + def getPrefSyntaxHighlighting(self): """ :Returns: @@ -319,22 +330,23 @@ def restoreDefaults(self, btn): self.deleteItems(self.extLayout) # Set other preferences in the GUI. - window = self.parent().window() - self.checkBox_parseLinks.setChecked(True) - self.checkBox_newTab.setChecked(False) - self.checkBox_syntaxHighlighting.setChecked(True) - self.checkBox_teletypeConversion.setChecked(True) - self.checkBox_lineNumbers.setChecked(True) - self.checkBox_showAllMessages.setChecked(True) - self.checkBox_showHiddenFiles.setChecked(False) - self.checkBox_autoCompleteAddressBar.setChecked(True) - self.lineEditTextEditor.setText(os.getenv("EDITOR", window.app.appConfig.get("textEditor", "nedit"))) - self.lineEditDiffTool.setText(window.app.appConfig.get("diffTool", "xdiff")) - self.useSpacesCheckBox.setChecked(True) - self.useSpacesSpinBox.setValue(4) + default = self.parent().window().app.DEFAULTS + self.checkBox_parseLinks.setChecked(default['parseLinks']) + self.checkBox_newTab.setChecked(default['newTab']) + self.checkBox_syntaxHighlighting.setChecked(default['syntaxHighlighting']) + self.checkBox_teletypeConversion.setChecked(default['teletype']) + self.checkBox_lineNumbers.setChecked(default['lineNumbers']) + self.checkBox_showAllMessages.setChecked(default['showAllMessages']) + self.checkBox_showHiddenFiles.setChecked(default['showHiddenFiles']) + self.checkBox_autoCompleteAddressBar.setChecked(default['autoCompleteAddressBar']) + self.lineEditTextEditor.setText(default['textEditor']) + self.lineEditDiffTool.setText(default['diffTool']) + self.useSpacesCheckBox.setChecked(default['useSpaces']) + self.useSpacesSpinBox.setValue(default['tabSpaces']) self.themeWidget.setChecked(False) - self.docFont = window.defaultDocFont + self.docFont = default['font'] self.updateFontLabel() + self.lineLimitSpinBox.setValue(default['lineLimit']) # Re-create file association fields with the default programs. self.populateProgsAndExts(self.parent().defaultPrograms) diff --git a/usdmanager/preferences_dialog.ui b/usdmanager/preferences_dialog.ui index c4f17fc..9cdeaff 100644 --- a/usdmanager/preferences_dialog.ui +++ b/usdmanager/preferences_dialog.ui @@ -7,7 +7,7 @@ 0 0 463 - 574 + 450 @@ -35,16 +35,6 @@ General - - - - Parse files for links to other files. Disable for faster loading of larger files - - - Parse links - - - @@ -55,26 +45,6 @@ - - - - Enable syntax highlighting. Disable for faster loading of larger files - - - Enable syntax highlighting - - - - - - - Display teletype character codes properly in browse mode. Disable for faster loading of larger files - - - Display teletype colors - - - @@ -109,13 +79,6 @@ - - - - Auto complete paths in address bar - - - @@ -145,6 +108,9 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + useSpacesSpinBox + @@ -264,8 +230,8 @@ 0 0 - 119 - 72 + 421 + 187 @@ -401,6 +367,120 @@ + + + Advanced + + + + + + The following options are primarily meant for debugging or as potential optimizations: + + + true + + + + + + + Auto complete paths in address bar + + + + + + + Display teletype character codes properly in browse mode. Disable for faster loading of larger files + + + Display teletype colors + + + + + + + Enable syntax highlighting. Disable for faster loading of larger files + + + Enable syntax highlighting + + + + + + + Parse files for links to other files. Disable for faster loading of larger files + + + Parse links + + + + + + + + + Number of lines to display before truncating the file. Extremely large files can lead to application lag. If a file is truncated, it will not be editable. + + + Line Limit: + + + lineLimitSpinBox + + + + + + + + + Number of lines to display before truncating the file. Extremely large files can lead to application lag. If a file is truncated, it will not be editable. + + + 100000000 + + + 10000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + @@ -417,14 +497,10 @@ tabWidget - checkBox_parseLinks checkBox_newTab - checkBox_syntaxHighlighting - checkBox_teletypeConversion checkBox_lineNumbers checkBox_showAllMessages checkBox_showHiddenFiles - checkBox_autoCompleteAddressBar useSpacesCheckBox useSpacesSpinBox themeWidget @@ -434,6 +510,11 @@ lineEdit scrollArea buttonNewProg + checkBox_autoCompleteAddressBar + checkBox_teletypeConversion + checkBox_syntaxHighlighting + checkBox_parseLinks + lineLimitSpinBox buttonBox diff --git a/usdmanager/utils.py b/usdmanager/utils.py index 2f9a8da..2b7b685 100644 --- a/usdmanager/utils.py +++ b/usdmanager/utils.py @@ -168,7 +168,12 @@ def usdcat(inputFile, outputFile, format=None): :Raises ValueError: If invalid format given compared to output file extension. """ - cmd = "usdcat {} -o {}".format(inputFile, outputFile) + if os.name == "nt": + # Files with spaces have to be double-quoted on Windows. + cmd = 'usdcat "{}" -o "{}"'.format(inputFile, outputFile) + else: + cmd = 'usdcat {} -o {}'.format(inputFile, outputFile) + if format and outputFile.endswith(".usd"): # For usdcat, use of --usdFormat requires output file end with '.usd' extension. cmd += " --usdFormat {}".format(format) @@ -193,10 +198,20 @@ def usdzip(inputs, dest): :Raises OSError: If usdzip fails """ - if type(inputs) is list: - inputs = " ".join(inputs) - cmd = "usdzip {} {}".format(inputs, dest) - logger.debug(cmd) + if os.name == "nt": + # Files with spaces have to be double-quoted on Windows. + if type(inputs) is list: + inputs = '" "'.join(inputs) + cmd = 'usdzip "{}" "{}"'.format(inputs, dest) + logger.debug(cmd) + else: + cmd = ["usdzip"] + if type(inputs) is list: + cmd += inputs + else: + cmd.append(inputs) + cmd.append(dest) + logger.debug(subprocess.list2cmdline(cmd)) try: subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) except subprocess.CalledProcessError as e: @@ -219,18 +234,19 @@ def unzip(path, layer=None, tmpDir=None): Destination file :Rtype: `str` - :Raises OSError: - If unzip fails + :Raises zipfile.BadZipfile: + For bad ZIP files + :Raises zipfile.LargeZipFile: + When a ZIP file would require ZIP64 functionality but that has not been enabled :Raises ValueError: If default layer not found """ + from zipfile import ZipFile + destDir = tempfile.mkdtemp(prefix="usdmanager_usdz_", dir=tmpDir) - cmd = "unzip {} -d {}".format(path, destDir) - logger.debug(cmd) - try: - subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) - except subprocess.CalledProcessError as e: - raise OSError("Failed to unzip: {}".format(e.output)) + logger.debug("Extracting {} to {}".format(path, destDir)) + with ZipFile(path, 'r') as zipRef: + zipRef.extractall(destDir) if layer is not None: destFile = os.path.join(destDir, layer) diff --git a/usdmanager/version.py b/usdmanager/version.py index e1cd976..3edb855 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.6.0' +__version__ = '0.7.0'