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'