diff --git a/docs/development.md b/docs/development.md index 8b9a85f..b5020e6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -49,6 +49,17 @@ Example app config JSON file: } ``` +## Language-specific parsing + +When default path parsing logic is not good enough, you can add unique parsing for file types by subclassing the +[parser.AbstractExtParser](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/parser.py). +Save your new class in a file in the parsers directory. Set the class's extensions tuple (e.g. (".html", ".xml")) for a +simple list of file extensions to match, or override the acceptsFile method for more advanced control. + +Within each parser, you can define custom menu items that will be added to the bottom of the Commands menu whenever a +parser is active. For example, the [USD parser](https://github.com/dreamworksanimation/usdmanager/blob/master/usdmanager/parsers/usd.py) +adds an "Open in usdview..." action. + ## Syntax highlighting To add syntax highlighting for additional languages, subclass the @@ -122,4 +133,4 @@ following to update [usdmanager/plugins/images_rc.py](https://github.com/dreamwo pyrcc4 usdmanager/plugins/images.rc > usdmanager/plugins/images_rc.py ``` -If using pyrcc4, be sure to replace PyQt4 with Qt in the images_rc.py's import line. \ No newline at end of file +If using pyrcc4, be sure to replace PyQt4 with Qt in the images_rc.py's import line. diff --git a/usdmanager/__init__.py b/usdmanager/__init__.py index 7c9d6db..374cbfc 100644 --- a/usdmanager/__init__.py +++ b/usdmanager/__init__.py @@ -95,21 +95,22 @@ # Qt.py compatibility. -if Qt.IsPySide2: - # Add QUrl.path missing in PySide2 build. - if not hasattr(QtCore.QUrl, "path"): - def qUrlPath(self): - """ Get the decoded URL without any query string. - - :Returns: - Decoded URL without query string - :Rtype: - `str` - """ - return self.toString(QtCore.QUrl.PrettyDecoded | QtCore.QUrl.RemoveQuery) - - QtCore.QUrl.path = qUrlPath -elif Qt.IsPyQt5: +if Qt.IsPySide2 or Qt.IsPyQt5: + if Qt.IsPySide2: + # Add QUrl.path missing in PySide2 build. + if not hasattr(QtCore.QUrl, "path"): + def qUrlPath(self): + """ Get the decoded URL without any query string. + + :Returns: + Decoded URL without query string + :Rtype: + `str` + """ + return self.toString(QtCore.QUrl.PrettyDecoded | QtCore.QUrl.RemoveQuery) + + QtCore.QUrl.path = qUrlPath + # Add query pair/value delimiters until we move fully onto Qt5 and switch to the QUrlQuery class. def queryPairDelimiter(self): """ Get the query pair delimiter character. @@ -184,7 +185,6 @@ class UsdMngrWindow(QtWidgets.QMainWindow): - Ability to write and repackage as usdz - - Plug-ins based on active file type (ABC-specific commands, USD commands, etc.) - Different extensions to search for based on file type. - Add customized print options like name of file and date headers, similar to printing a web page. - Move setSource link parsing to a thread? @@ -205,23 +205,10 @@ class UsdMngrWindow(QtWidgets.QMainWindow): Known issues: - AddressBar file completer has problems occasionally. - - Save As... doesn't add file to recent files or history menus. - Find with text containing a new line character does not work due to QTextDocument storing these as separate blocks. - Line numbers width not always immediately updated after switching to new class. - - Qt.py problems: - - - PyQt5 - - - Non-critical messages - - - QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-mdsandell' - - QXcbConnection: XCB error: 3 (BadWindow), sequence: 878, resource id: 26166399, major code: 40 - (TranslateCoords), minor code: 0 - - - PySide2 - - - Preferences dialog doesn't center on main window, can't load via loadUiType + - If a file loses edit permissions, it can stay in edit mode and let you make changes that can't be saved. """ @@ -270,6 +257,7 @@ def __init__(self, parent=None, **kwargs): # Don't include the default parser in the list we iterate through to find compatible custom parsers, # since we only use it as a fallback. self.fileParserDefault = self.fileParsers.pop() + self._prevParser = None for module in utils.findModules("parsers"): for name, cls in inspect.getmembers(module, lambda x: inspect.isclass(x) and issubclass(x, FileParser) and @@ -615,7 +603,7 @@ def customTextBrowserContextMenu(self, pos): pos : `QtCore.QPoint` Position of the right-click """ - menu = self.currTab.textBrowser.createStandardContextMenu() + menu = self.currTab.textBrowser.createStandardContextMenu(pos) actions = menu.actions() # Right now, you may see the open in new tab action even if you aren't # hovering over a link. Ideally, because of imperfection with the hovering @@ -636,8 +624,8 @@ def customTextBrowserContextMenu(self, pos): path = self.currTab.getCurrentPath() if path: menu.addSeparator() - if utils.isUsdFile(path): - menu.addAction(self.actionUsdView) + for args in self.currTab.parser.plugins: + menu.addAction(*args) menu.addAction(self.actionTextEditor) menu.addAction(self.actionOpenWith) menu.addSeparator() @@ -669,8 +657,8 @@ def customTextEditorContextMenu(self, pos): path = self.currTab.getCurrentPath() if path: menu.addSeparator() - if utils.isUsdFile(path): - menu.addAction(self.actionUsdView) + for args in self.currTab.parser.plugins: + menu.addAction(*args) menu.addAction(self.actionTextEditor) menu.addAction(self.actionOpenWith) menu.addSeparator() @@ -938,7 +926,6 @@ def connectSignals(self): self.actionUncomment.triggered.connect(self.uncommentTextRequest) self.actionIndent.triggered.connect(self.indentText) self.actionUnindent.triggered.connect(self.unindentText) - self.actionUsdView.triggered.connect(self.launchUsdView) self.actionTextEditor.triggered.connect(self.launchTextEditor) self.actionOpenWith.triggered.connect(self.launchProgramOfChoice) self.actionOpenLinkWith.triggered.connect(self.onOpenLinkWith) @@ -1304,11 +1291,13 @@ def getSaveAsPath(self, path=None, tab=None): # Now we have a valid path to save as. return filePath, fileFormat - @Slot() - def saveFileAs(self, tab=None): + @Slot(bool) + def saveFileAs(self, checked=False, tab=None): """ Save the current file with a new filename. :Parameters: + checked : `bool` + For signal only tab : `BrowserTab` | None Tab to save. Defaults to current tab. :Returns: @@ -1335,8 +1324,10 @@ def saveFileAs(self, tab=None): else: self.tabWidget.setTabIcon(idx, QtGui.QIcon()) self.tabWidget.setTabToolTip(idx, "{} - {}".format(fileName, filePath)) - tab.updateHistory(QtCore.QUrl.fromLocalFile(filePath)) + url = QtCore.QUrl.fromLocalFile(filePath) + tab.updateHistory(url) tab.updateFileStatus() + self.updateRecentMenus(url, url.toString()) self.setHighlighter(ext, tab=tab) if tab == self.currTab: self.updateButtons() @@ -1375,11 +1366,13 @@ def saveLinkAs(self): else: self.showWarningMessage("Selected file does not exist.") - @Slot() - def saveTab(self, tab=None): + @Slot(bool) + def saveTab(self, checked=False, tab=None): """ If the file already has a name, save it; otherwise, get a filename and save it. :Parameters: + checked : `bool` + For signal only tab : `BrowserTab` | None Tab to save. Defaults to current tab. :Returns: @@ -1641,19 +1634,21 @@ def removeTab(self, index): # Edit Menu Methods ### - @Slot() - # TODO: Test this doesn't get "False" as the first param from using triggered signal on an action. Probably need to - # test on Mac. - def toggleEdit(self, tab=None): + @Slot(bool) + def toggleEdit(self, checked=False, tab=None): """ Switch between Browse mode and Edit mode. + :Parameters: + checked : `bool` + Unused. For signal/slot only + tab : `BrowserTab` + Tab to toggle edit mode on :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` """ - logger.debug("toggleEdit: %s", tab) # TEMP for TODO tab = tab or self.currTab # Don't change between browse and edit mode if dirty. Saves if needed. @@ -1772,10 +1767,13 @@ def toggleFindClose(self): self.findWidget.setVisible(False) @Slot() - def find(self, flags=None, startPos=3, loop=True): + @Slot(bool) + def find(self, checked=False, flags=None, startPos=3, loop=True): """ Find next hit for the search text. :Parameters: + checked : `bool` + For signal only flags Options for document().find(). startPos : `int` @@ -2072,7 +2070,7 @@ def replaceAllInOpenFiles(self): if not status.writable: continue if not tab.inEditMode: - self.toggleEdit(tab) + self.toggleEdit(tab=tab) thisCount = self.replaceAll(findText, replaceText, tab, report=False) if thisCount: files += 1 @@ -2330,13 +2328,16 @@ def refreshSelectedTab(self): """ selectedIndex = self.tabWidget.tabBar.tabAt(self.contextMenuPos) selectedTab = self.tabWidget.widget(selectedIndex) - self.refreshTab(selectedTab) + self.refreshTab(tab=selectedTab) @Slot() - def refreshTab(self, tab=None): + @Slot(bool) + def refreshTab(self, checked=False, tab=None): """ Reload the file for a tab. :Parameters: + checked : `bool` + For signal only tab : `BrowserTab` | None Tab to refresh. Defaults to current tab if None. :Returns: @@ -2481,8 +2482,7 @@ def diffFile(self): fd, tmpPath = utils.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']) + [QtCore.QDir.toNativeSeparators(path), tmpPath] - self.launchProcess(args) + self.launchPathCommand(self.preferences['diffTool'], [QtCore.QDir.toNativeSeparators(path), tmpPath]) @staticmethod def getPermissionString(path): @@ -2607,29 +2607,23 @@ def unindentText(self): def launchTextEditor(self): """ Launch the current file in a separate text editing application. """ - path = QtCore.QDir.toNativeSeparators(self.currTab.getCurrentPath()) - args = shlex.split(self.preferences['textEditor']) + [path] - self.launchProcess(args) + self.launchPathCommand(self.preferences['textEditor'], + QtCore.QDir.toNativeSeparators(self.currTab.getCurrentPath())) @Slot() def launchUsdView(self): """ Launch the current file in usdview. """ - app = self.app.DEFAULTS['usdview'] - path = QtCore.QDir.toNativeSeparators(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) + self.launchPathCommand(self.app.DEFAULTS['usdview'], + QtCore.QDir.toNativeSeparators(self.currTab.getCurrentPath())) - @Slot() - def launchProgramOfChoice(self, path=None): + @Slot(bool) + def launchProgramOfChoice(self, checked=False, path=None): """ Open a file with a program given by the user. :Parameters: + checked : `bool` + For signal only path : `str` File to open. If None, use currently open file. """ @@ -2639,9 +2633,9 @@ def launchProgramOfChoice(self, path=None): # Get program of choice from user. prog, ok = QtWidgets.QInputDialog.getText( self, "Open with...", - "Please enter the program you would like to open this file with.\n\nYou may include command line " - "options as well, and the file path will be appended to the end of the command.\n\n" - "Example:\n usdview --unloaded\n rm\n", + "Please enter the program you would like to open this file with.\n\nYou may include command line options " + "as well, and the file path will be appended to the end of the command.\n\nUse {} if the path needs to go " + "in a specific place within the command.\n\nExample:\n usdview --unloaded\n ls {} -l\n", QtWidgets.QLineEdit.Normal, self.preferences['lastOpenWithStr']) # Return if cancel was pressed or nothing entered. if not ok or not prog: @@ -2651,8 +2645,7 @@ def launchProgramOfChoice(self, path=None): self.updatePreference('lastOpenWithStr', prog) # Launch program. - args = shlex.split(prog) + [path] - self.launchProcess(args) + self.launchPathCommand(prog, path) ### # Help Menu Methods @@ -2796,7 +2789,7 @@ def promptOnFileChange(self, path): # Make sure the tab appears as dirty so the user is prompted on exit to do so if they still haven't up to # that point. if not self.currTab.inEditMode: - self.toggleEdit(self.currTab) + self.toggleEdit(tab=self.currTab) self.currTab.setDirty() return False @@ -3021,11 +3014,9 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos if ext in self.programs and self.programs[ext]: if multFiles is not None: # Assumes program takes a space-separated list of files. - args = shlex.split(self.programs[ext]) + multFiles - self.launchProcess(args) + self.launchPathCommand(self.programs[ext], multFiles) else: - args = shlex.split(self.programs[ext]) + [nativeAbsPath] - self.launchProcess(args) + self.launchPathCommand(self.programs[ext], nativeAbsPath) return self.setSourceFinish(tab=tab) if multFiles is not None: @@ -3054,10 +3045,7 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos self.tabWidget.setTabToolTip(idx, "{} - {}".format(fileName, nativeAbsPath)) # Take care of various history menus. - self.addItemToMenu(link, self.menuHistory, slot=self.setSource, maxLen=25, start=3, end=2) - self.addItemToMenu(link, self.menuOpenRecent, slot=self.openRecent, maxLen=RECENT_FILES) - self.menuOpenRecent.setEnabled(True) - self.addRecentFileToSettings(fullUrlStr) + self.updateRecentMenus(link, fullUrlStr) if fileExists: # TODO: If files can load in parallel, this single progress bar would need to change. @@ -3067,9 +3055,11 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos try: if self.validateFileSize(fileInfo): + self._prevParser = tab.parser for parser in self.fileParsers: if parser.acceptsFile(fileInfo, link): logger.debug("Using parser %s", parser.__class__.__name__) + tab.parser = parser break else: # No matching file parser found. @@ -3082,7 +3072,7 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos return self.setSource(utils.strToUrl(dest), tab=tab) else: logger.debug("Using default parser") - parser = self.fileParserDefault + tab.parser = self.fileParserDefault # Stop Loading Tab stops the expensive parsing of the file # for links, checking if the links actually exist, etc. @@ -3151,7 +3141,7 @@ def setSource(self, link, isNewFile=True, newTab=False, hScrollPos=0, vScrollPos self.loadingProgressLabel.setVisible(False) else: if not tab.inEditMode: - self.toggleEdit(tab) + self.toggleEdit(tab=tab) tab.setDirty(True) logger.debug("Cleanup") @@ -3324,6 +3314,7 @@ def currentTabChanged(self, idx): Index of the newly selected tab """ prevMode = self.currTab.inEditMode + self._prevParser = self.currTab.parser self.currTab = self.tabWidget.widget(idx) if prevMode != self.currTab.inEditMode: self.editModeChanged.emit(self.currTab.inEditMode) @@ -3377,6 +3368,7 @@ def updateButtons(self): self.breadcrumb.setText(self.currTab.breadcrumb) self.updateEditButtons() + self.updateParserPlugins() title = self.app.appDisplayName if self.currTab.isNewTab: @@ -3397,10 +3389,6 @@ def updateButtons(self): self.actionOpenWith.setEnabled(enable) self.setWindowTitle(title) - path = self.currTab.getCurrentPath() - usd = utils.isUsdFile(path) - self.actionUsdView.setEnabled(usd) - status = self.currTab.getFileStatus() self.fileStatusButton.setText(status.text) self.fileStatusButton.setIcon(status.icon) @@ -3410,6 +3398,20 @@ def updateButtons(self): # Useful for plug-ins that may need to update their actions' enabled state. self.updatingButtons.emit() + def updateParserPlugins(self): + """ Update parser-specific UI actions when the file parser has changed + due to the display of a different file type. + """ + if self.currTab.parser != self._prevParser: + # Clear old actions up to the last separator. + for action in reversed(self.menuCommands.actions()): + if action.isSeparator(): + break + self.menuCommands.removeAction(action) + if self.currTab.parser is not None: + for args in self.currTab.parser.plugins: + self.menuCommands.addAction(*args) + def updateEditButtons(self): """ Toggle edit action and button text. """ @@ -3452,17 +3454,74 @@ def validateAddressBar(self, address): """ self.buttonGo.setEnabled(bool(address.strip())) - def launchProcess(self, args, **kwargs): - """ Launch a program with the path `str` as an argument. + def launchPathCommand(self, command, path, **kwargs): + """ Launch a command with a file path either being appended to the end + or substituting curly brackets if present. Any additional keyword arguments are passed to the Popen object. + Example: + launchPathCommand("rez-env usd_view -c 'usdview {}'", "scene.usd") + runs: rez-env usd_view -c 'usdview scene.usd' + + launchPathCommand("nedit", "scene.usd") runs: nedit scene.usd + + launchPathCommand("ls", ["foo", "bar"]) runs: ls foo bar + + :Parameters: + command : `str` | [`str`] + Command to run. If the path to open with the command cannot + simply be appended with a space at the end of the command, use + {} like standard python string formatting to denote where the + path should go. + path : `str` | [`str`] + File path `str`, space-separated list of files paths as a + single `str`, or `list` of `str` file paths + :Returns: + Returns process ID, or None if the subprocess fails + :Rtype: + `subprocess.Popen` | None + """ + if not isinstance(command, list): + if '{}' in command: + try: + quote = shlex.quote # Python 3.3+ (shlex is already imported) + except AttributeError: + from pipes import quote # Deprecated since python 2.7 + if isinstance(path, list): + path = subprocess.list2cmdline(quote(x) for x in path) + try: + command = command.format(quote(path)) + except IndexError as e: + self.showCriticalMessage("Invalid command: {}. If using curly brackets, please ensure there is only " + "one set.".format(e), details="Command: {}\nPath: {}".format(command, path)) + return + if not kwargs.get("shell"): + command = shlex.split(command) + else: + command = shlex.split(command) + if isinstance(path, list): + command += path + else: + command.append(path) + else: + if isinstance(path, list): + command += path + elif '{}' in command: + while '{}' in command: + command[command.index('{}')] = path + else: + command.append(path) + return self.launchProcess(command, **kwargs) + + def launchProcess(self, args, **kwargs): + """ Launch a subprocess. 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 also passing in shell=True, this should be a single string. :Returns: - Returns process ID or None + Returns process ID, or None if the subprocess fails :Rtype: `subprocess.Popen` | None """ @@ -3470,6 +3529,7 @@ def launchProcess(self, args, **kwargs): try: if kwargs.get("shell"): # With shell=True, convert args to a string to call Popen with. + # Properly quote any args as necessary before using this. logger.debug("Running Popen with shell=True") if isinstance(args, list): args = subprocess.list2cmdline(args) @@ -3477,11 +3537,11 @@ def launchProcess(self, args, **kwargs): else: # 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 + return subprocess.Popen(args, **kwargs) except Exception: self.restoreOverrideCursor() - self.showCriticalMessage("Operation failed. {} may not be installed.".format(args[0]), + cmd = args[0] if isinstance(args, list) else args.split()[0] + self.showCriticalMessage("Operation failed. {} may not be installed.".format(cmd), traceback.format_exc()) @Slot(bool) @@ -3552,8 +3612,8 @@ def showWarningMessage(self, message, details=None, title=None, icon=QtWidgets.Q return msgBox.exec_() return QtWidgets.QMessageBox.warning(self, title, message) - @Slot(str, str, bool) - def _changeTabName(self, text, toolTip, dirty): + @Slot(QtWidgets.QWidget, str, str, bool) + def _changeTabName(self, tab, text, toolTip, dirty): """ Change the displayed name of a tab. Called via signal from a tab when the tab's dirty state changes. @@ -3566,7 +3626,6 @@ def _changeTabName(self, text, toolTip, dirty): dirty : `bool` Dirty state of tab """ - tab = self.sender() idx = self.tabWidget.indexOf(tab) if idx == -1: logger.debug("Tab not found for %s", text) @@ -3608,7 +3667,7 @@ def dirtySave(self, tab=None): if btn == QtWidgets.QMessageBox.Cancel: return False elif btn == QtWidgets.QMessageBox.Save: - return self.saveTab(tab) + return self.saveTab(tab=tab) else: # Discard tab.setDirty(False) return True @@ -3651,7 +3710,7 @@ def onOpenLinkNewTab(self): def onOpenLinkWith(self): """ Show the "Open With..." dialog for the currently highlighted link. """ - self.launchProgramOfChoice(QtCore.QDir.toNativeSeparators(self.linkHighlighted.toLocalFile())) + self.launchProgramOfChoice(path=QtCore.QDir.toNativeSeparators(self.linkHighlighted.toLocalFile())) @Slot(str) def onBreadcrumbActivated(self, path): @@ -3672,6 +3731,20 @@ def onBreadcrumbHovered(self, path): """ self.statusbar.showMessage(path, 2000) + def updateRecentMenus(self, link, fullUrlStr): + """ Update the history and recently open files menus. + + :Parameters: + link : `QtCore.QUrl` + URL to file + fullUrlStr : `str` + URL string representation + """ + self.addItemToMenu(link, self.menuHistory, slot=self.setSource, maxLen=25, start=3, end=2) + self.addItemToMenu(link, self.menuOpenRecent, slot=self.openRecent, maxLen=RECENT_FILES) + self.menuOpenRecent.setEnabled(True) + self.addRecentFileToSettings(fullUrlStr) + class AddressBar(QtWidgets.QLineEdit): """ @@ -4247,7 +4320,7 @@ class BrowserTab(QtWidgets.QWidget): restoreTab = Signal(QtWidgets.QWidget) openFile = Signal(str) openOldUrl = Signal(QtCore.QUrl) - tabNameChanged = Signal(str, str, bool) + tabNameChanged = Signal(QtWidgets.QWidget, str, str, bool) def __init__(self, parent=None): """ Create and initialize the tab. @@ -4271,6 +4344,7 @@ def __init__(self, parent=None): self.history = [] # List of FileStatus objects self.historyIndex = -1 # First file opened will be 0. self.fileFormat = FILE_FORMAT_NONE # Used to differentiate between things like usda and usdc. + self.parser = None # File parser for the currently active file type, used to add extra Commands menu actions. font = parent.font() prefs = parent.window().preferences @@ -4576,7 +4650,7 @@ def setDirty(self, dirty=True): elif self.fileFormat == FILE_FORMAT_USDZ: tipSuffix += " (zip)" text = "*{}*".format(fileName) if dirty else fileName - self.tabNameChanged.emit(text, text + tipSuffix, dirty) + self.tabNameChanged.emit(self, text, text + tipSuffix, dirty) def setIndentSettings(self, useSpaces=True, tabSpaces=4, autoIndent=True): """ Set various indent settings, such as spaces for tabs and auto indentation @@ -4731,6 +4805,10 @@ def run(self): # Initialize the application and settings. self._set_log_level() logger.debug("Qt version: %s %s", Qt.__binding__, Qt.__binding_version__) + # Avoid the following with PySide2: "Qt WebEngine seems to be initialized from a plugin. Please set + # Qt::AA_ShareOpenGLContexts using QCoreApplication::setAttribute before constructing QGuiApplication." + if Qt.IsPySide2: + QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) self.app = QtWidgets.QApplication(sys.argv) self.app.setApplicationName(self.appName) self.app.setWindowIcon(QtGui.QIcon(":images/images/logo.png")) diff --git a/usdmanager/file_dialog.py b/usdmanager/file_dialog.py index 9841d09..c624244 100644 --- a/usdmanager/file_dialog.py +++ b/usdmanager/file_dialog.py @@ -41,6 +41,11 @@ def __init__(self, parent=None, caption="", directory="", filters=None, selected Show hidden files """ super(FileDialog, self).__init__(parent, caption, directory, ';;'.join(filters or FILE_FILTER)) + + # The following line avoids this warning with Qt5: + # "GtkDialog mapped without a transient parent. This is discouraged." + self.setOption(QFileDialog.DontUseNativeDialog) + if selectedFilter: self.selectNameFilter(selectedFilter) if showHidden: diff --git a/usdmanager/find_dialog.py b/usdmanager/find_dialog.py index ed20aa5..660eb0a 100644 --- a/usdmanager/find_dialog.py +++ b/usdmanager/find_dialog.py @@ -13,54 +13,44 @@ # See the License for the specific language governing permissions and # limitations under the License. # +""" Create the Find or Find/Replace dialog. +""" from Qt.QtCore import Slot -from Qt.QtWidgets import QStatusBar +from Qt.QtWidgets import QDialog, QStatusBar from Qt.QtGui import QIcon, QTextDocument -from .utils import loadUiType +from .utils import loadUiWidget -try: - UI_TYPE = loadUiType("find_dialog.ui") -except KeyError: - # Building docs, have a safe fallback - from Qt.QtWidgets import QDialog - UI_TYPE = QDialog -class FindDialog(UI_TYPE): +class FindDialog(QDialog): """ Find/Replace dialog """ def __init__(self, parent=None, **kwargs): """ Initialize the dialog. - + :Parameters: parent : `QtWidgets.QWidget` | None Parent widget """ super(FindDialog, self).__init__(parent, **kwargs) - self.setupUi(self) + self.setupUi() self.connectSignals() - - def setupUi(self, widget): - """ - Creates and lays out the widgets defined in the ui file. - - :Parameters: - widget : `QtWidgets.QWidget` - Base widget + + def setupUi(self): + """ Creates and lays out the widgets defined in the ui file. """ - super(FindDialog, self).setupUi(widget) + self.baseInstance = loadUiWidget('find_dialog.ui', self) self.statusBar = QStatusBar(self) self.verticalLayout.addWidget(self.statusBar) self.findBtn.setIcon(QIcon.fromTheme("edit-find")) self.replaceBtn.setIcon(QIcon.fromTheme("edit-find-replace")) - + def connectSignals(self): - """ - Connect signals to slots. + """ Connect signals to slots. """ self.findLineEdit.textChanged.connect(self.updateButtons) - + def searchFlags(self): """ Get find flags based on checked options. @@ -82,7 +72,7 @@ def searchFlags(self): def updateButtons(self, text): """ Update enabled state of buttons as entered text changes. - + :Parameters: text : `str` Currently entered find text @@ -96,13 +86,13 @@ def updateButtons(self, text): if not enabled: self.statusBar.clearMessage() self.setStyleSheet("QLineEdit#findLineEdit{background:none}") - + @Slot(bool) def updateForEditMode(self, edit): """ Show/Hide text replacement options based on if we are editing or not. If editing, allow replacement of the found text. - + :Parameters: edit : `bool` If in edit mode or not diff --git a/usdmanager/linenumbers.py b/usdmanager/linenumbers.py index cb759e5..1f5bcd2 100644 --- a/usdmanager/linenumbers.py +++ b/usdmanager/linenumbers.py @@ -33,7 +33,7 @@ class PlainTextLineNumbers(QWidget): """ def __init__(self, parent): """ Initialize the line numbers widget. - + :Parameters: parent : `QPlainTextEdit` Text widget @@ -42,17 +42,18 @@ def __init__(self, parent): self.textWidget = parent self._hiddenByUser = False self._highlightCurrentLine = True + self._movePos = None # Monospaced font to keep width from shifting. font = QFont() font.setStyleHint(QFont.Courier) font.setFamily("Monospace") self.setFont(font) - + self.connectSignals() self.updateLineWidth() self.highlightCurrentLine() - + def blockCount(self): return self.textWidget.blockCount() @@ -74,7 +75,7 @@ def highlightCurrentLine(self): """ if not self._highlightCurrentLine: return False - + extras = [x for x in self.textWidget.extraSelections() if x.format.property(QTextFormat.UserProperty) != "line"] selection = QTextEdit.ExtraSelection() lineColor = QColor(QtCore.Qt.darkGray).darker() if self.window().isDarkTheme() else \ @@ -96,7 +97,7 @@ def highlightCurrentLine(self): ''' self.textWidget.setExtraSelections(extras) return True - + def lineWidth(self, count=0): """ Calculate the width of the widget based on the block count. @@ -114,6 +115,64 @@ def lineWidth(self, count=0): # Obsolete in Qt 5. return 6 + self.fontMetrics().width(blocks) + def mouseMoveEvent(self, event): + """ Track mouse movement to select more lines if press is active. + + :Parameters: + event : `QMouseEvent` + Mouse move event + """ + if event.buttons() != QtCore.Qt.LeftButton: + event.accept() + return + + cursor = self.textWidget.textCursor() + cursor2 = self.textWidget.cursorForPosition(event.pos()) + new = cursor2.position() + if new == self._movePos: + event.accept() + return + + cursor.setPosition(self._movePos) + if new > self._movePos: + cursor.movePosition(cursor.StartOfLine) + cursor2.movePosition(cursor2.EndOfLine) + else: + cursor.movePosition(cursor.EndOfLine) + cursor2.movePosition(cursor2.StartOfLine) + cursor.setPosition(cursor2.position(), cursor.KeepAnchor) + self.textWidget.setTextCursor(cursor) + event.accept() + + def mousePressEvent(self, event): + """ Select the line that was clicked. If moved while pressed, select + multiple lines as the mouse moves. + + :Parameters: + event : `QMouseEvent` + Mouse press event + """ + if event.buttons() != QtCore.Qt.LeftButton: + event.accept() + return + + cursor = self.textWidget.cursorForPosition(event.pos()) + cursor.select(cursor.LineUnderCursor) + + # Allow Shift-selecting lines from the previous selection to new position. + if self.textWidget.textCursor().hasSelection() and event.modifiers() == QtCore.Qt.ShiftModifier: + cursor2 = self.textWidget.textCursor() + self._movePos = cursor2.position() + start = min(cursor.selectionStart(), cursor2.selectionStart()) + end = max(cursor.selectionEnd(), cursor2.selectionEnd()) + cursor.setPosition(start) + cursor.setPosition(end, cursor.KeepAnchor) + else: + self._movePos = cursor.position() + + self.textWidget.setTextCursor(cursor) + event.accept() + def onEditorResize(self): """ Adjust line numbers size if the text widget is resized. """ @@ -157,15 +216,15 @@ def paintEvent(self, event): top = bottom bottom = top + round(textWidget.blockBoundingRect(block).height()) blockNumber += 1 - + def setVisible(self, visible): super(PlainTextLineNumbers, self).setVisible(visible) self._hiddenByUser = not visible self.updateLineWidth() def sizeHint(self): - return QSize(self.lineWidth(), 0) - + return QSize(self.lineWidth(), self.textWidget.height()) + @Slot(QRect, int) def updateLineNumbers(self, rect, dY): """ Scroll the line numbers or repaint the visible numbers. @@ -176,7 +235,7 @@ def updateLineNumbers(self, rect, dY): self.update(0, rect.y(), self.width(), rect.height()) if rect.contains(self.textWidget.viewport().rect()): self.updateLineWidth() - + @Slot(int) def updateLineWidth(self, count=0): """ Adjust display of text widget to account for the widget of the line numbers. @@ -203,34 +262,34 @@ def connectSignals(self): self.textWidget.currentCharFormatChanged.connect(self.resizeAndUpdate) self.textWidget.cursorPositionChanged.connect(self.highlightCurrentLine) self.doc.blockCountChanged.connect(self.updateLineWidth) - + @Slot() def highlightCurrentLine(self): """ Make sure the active line number is redrawn in bold by calling update. """ if super(LineNumbers, self).highlightCurrentLine(): self.update() - + @Slot(QTextCharFormat) def resizeAndUpdate(self, *args): """ Resize bar if needed. """ self.updateLineWidth() super(LineNumbers, self).update() - + def paintEvent(self, event): """ Draw line numbers. """ painter = QPainter(self) painter.fillRect(event.rect(), QColor(QtCore.Qt.darkGray).darker(300) if self.window().isDarkTheme() \ else QtCore.Qt.lightGray) - + textWidget = self.textWidget doc = textWidget.document() vScrollPos = textWidget.verticalScrollBar().value() pageBtm = vScrollPos + textWidget.viewport().height() currBlock = doc.findBlock(textWidget.textCursor().position()) - + width = self.width() - 3 # 3 is magic padding number height = textWidget.fontMetrics().height() flags = QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter @@ -244,19 +303,20 @@ def paintEvent(self, event): # Find roughly the current top-most visible block. block = doc.begin() - lineHeight = doc.documentLayout().blockBoundingRect(block).height() - + layout = doc.documentLayout() + lineHeight = layout.blockBoundingRect(block).height() + block = doc.findBlockByNumber(int(vScrollPos / lineHeight)) currLine = block.blockNumber() - + while block.isValid(): currLine += 1 - + # Check if the position of the block is outside the visible area. - yPos = doc.documentLayout().blockBoundingRect(block).topLeft().y() + yPos = layout.blockBoundingRect(block).topLeft().y() if yPos > pageBtm: break - + # Make the line number for the selected line bold. font.setBold(block == currBlock) painter.setFont(font) diff --git a/usdmanager/main_window.ui b/usdmanager/main_window.ui index 8784c4b..b3ed66b 100644 --- a/usdmanager/main_window.ui +++ b/usdmanager/main_window.ui @@ -505,7 +505,7 @@ - + @@ -996,18 +996,6 @@ Open files in new tabs - - - false - - - - :/images/images/usd.png:/images/images/usd.png - - - Open with usd&view... - - false diff --git a/usdmanager/parser.py b/usdmanager/parser.py index b8e5da4..5567f58 100644 --- a/usdmanager/parser.py +++ b/usdmanager/parser.py @@ -62,13 +62,18 @@ def __init__(self, parent=None): Parent object (main window) """ super(FileParser, self).__init__(parent) + + # List of args to pass to addAction on the Commands menu. + # Each item in the list represents a new menu item. + self.plugins = [] + self.regex = None self._stop = False self._cleanup() self.progress.connect(parent.setLoadingProgress) self.status.connect(parent.loadingProgressLabel.setText) - parent.actionStop.triggered.connect(self.stop) + parent.actionStop.triggered.connect(self.stopTriggered) parent.compileLinkRegEx.connect(self.compile) def acceptsFile(self, fileInfo, link): @@ -262,7 +267,7 @@ def parseMatch(self, match, linkPath, nativeAbsPath, fileInfo): # Create an orange link for files with wildcards in the path, # designating zero or more files may exist. return '{}'.format( - fullPath, escape(linkPath)) + fullPath, escape(linkPath)) return '{}'.format(fullPath, escape(linkPath)) def parseLongLine(self, line): @@ -295,7 +300,6 @@ def read(self, path): with open(path) as f: return f.readlines() - @Slot(bool) def stop(self, stop=True): """ Request to stop parsing the active file for links. @@ -307,6 +311,18 @@ def stop(self, stop=True): """ self._stop = stop + @Slot(bool) + def stopTriggered(self, checked=False): + """ Request to stop parsing the active file for links. + + Don't override. + + :Parameters: + checked : `bool` + For signal only + """ + self.stop() + class AbstractExtParser(FileParser): """ Determines which files are supported based on extension. diff --git a/usdmanager/parsers/usd.py b/usdmanager/parsers/usd.py index 2a2454b..1c2aed0 100644 --- a/usdmanager/parsers/usd.py +++ b/usdmanager/parsers/usd.py @@ -23,6 +23,7 @@ from xml.sax.saxutils import escape, unescape from Qt.QtCore import QFileInfo, Slot +from Qt.QtGui import QIcon from .. import utils from ..constants import FILE_FORMAT_USD, FILE_FORMAT_USDA, FILE_FORMAT_USDC,\ @@ -46,10 +47,11 @@ class UsdAsciiParser(AbstractExtParser): """ exts = USD_ASCII_EXTS fileFormat = FILE_FORMAT_USDA - + def __init__(self, *args, **kwargs): super(UsdAsciiParser, self).__init__(*args, **kwargs) self.regex = None + self.plugins.append((QIcon(":/images/images/usd.png"), "Open with usdview...", self.parent().launchUsdView)) self.usdArrayRegEx = re.compile( r"((?:\s*(?:\w+\s+)?\w+\[\]\s+[\w:]+\s*=|\s*\d+:)\s*\[)" # Array attribute definition and equal sign, or a frame number and colon, plus the opening bracket. r"\s*(.*)\s*" # Everything inside the square brackets. diff --git a/usdmanager/preferences_dialog.py b/usdmanager/preferences_dialog.py index 271f28d..4d1bb36 100644 --- a/usdmanager/preferences_dialog.py +++ b/usdmanager/preferences_dialog.py @@ -14,25 +14,24 @@ # limitations under the License. # +""" Create a Preferences dialog. +""" + from Qt.QtCore import Slot, QRegExp from Qt.QtGui import QIcon, QRegExpValidator -from Qt.QtWidgets import (QAbstractButton, QDialog, QDialogButtonBox, QFontDialog, QLineEdit, QMessageBox, QVBoxLayout) +from Qt.QtWidgets import QAbstractButton, QDialog, QDialogButtonBox, QFontDialog, QLineEdit, QMessageBox, QVBoxLayout from .constants import LINE_LIMIT from .utils import loadUiWidget -# TODO: This doesn't work in either PySide version due to a NoneType icon issue. -# Without that working, the dialog doesn't position itself over the parent widget properly. -#from .utils import loadUiType -#class PreferencesDialog(loadUiType("preferences_dialog.ui")): class PreferencesDialog(QDialog): """ Preferences dialog """ def __init__(self, parent, **kwargs): """ Initialize the dialog. - + :Parameters: parent : `UsdMngrWindow` Main window @@ -44,23 +43,17 @@ def __init__(self, parent, **kwargs): self.lineEditProgs = [] self.lineEditExts = [] - self.setupUi(self) + self.setupUi() self.connectSignals() - - def setupUi(self, widget): - """ - Creates and lays out the widgets defined in the ui file. - - :Parameters: - widget : `QtWidgets.QWidget` - Base widget + + def setupUi(self): + """ Creates and lays out the widgets defined in the ui file. """ - #super(PreferencesDialog, self).setupUi(widget) # TODO: Switch back to this if we get loadUiType working. self.baseInstance = loadUiWidget("preferences_dialog.ui", self) self.setWindowIcon(QIcon.fromTheme("preferences-system")) self.buttonFont.setIcon(QIcon.fromTheme("preferences-desktop-font")) self.buttonNewProg.setIcon(QIcon.fromTheme("list-add")) - + # ----- General tab ----- # Set initial preferences. parent = self.parent() @@ -80,33 +73,33 @@ def setupUi(self, widget): self.lineLimitSpinBox.setValue(parent.preferences['lineLimit']) self.checkBox_autoIndent.setChecked(parent.preferences['autoIndent']) self.updateFontLabel() - + # ----- Programs tab ----- self.progLayout = QVBoxLayout() self.extLayout = QVBoxLayout() - + # Extensions can only be: #self.progValidator = QRegExpValidator(QRegExp("[\w,. ]+"), self) self.extValidator = QRegExpValidator(QRegExp(r"(?:\.?\w*,?\s*)+"), self) self.lineEdit.setValidator(self.extValidator) - + # Create the fields for programs and extensions. self.populateProgsAndExts(parent.programs) - + def connectSignals(self): - """ - Connect signals to slots. + """ Connect signals to slots. """ self.buttonBox.clicked.connect(self.restoreDefaults) self.buttonNewProg.clicked.connect(self.newProgField) self.buttonBox.accepted.connect(self.validate) self.buttonFont.clicked.connect(self.selectFont) - + def deleteItems(self, layout): - """ + """ Delete all items in given layout. + :Parameters: layout : `QLayout` - Delete all items in given layout. + Layout to delete items from """ if layout is not None: while layout.count(): @@ -116,141 +109,155 @@ def deleteItems(self, layout): widget.deleteLater() else: self.deleteItems(item.layout()) - + def getPrefFont(self): - """ + """ Get the user preference for font. + :Returns: Font selected for documents. :Rtype: `QFont` """ return self.docFont - + def getPrefLineNumbers(self): - """ + """ Get the user preference for displaying line numbers. + :Returns: State of "Show line numbers" check box. :Rtype: `bool` """ return self.checkBox_lineNumbers.isChecked() - + def getPrefNewTab(self): - """ + """ Get the user preference for opening links in a new tab or not. + :Returns: State of "Open links in new tabs" check box. :Rtype: `bool` """ return self.checkBox_newTab.isChecked() - + def getPrefParseLinks(self): - """ + """ Get the user preference to enable link parsing. + :Returns: Search for links in the opened file. Disable this for huge files that freeze the app. - + :Rtype: `bool` """ return self.checkBox_parseLinks.isChecked() - + def getPrefPrograms(self): - """ + """ Get the user preference for file extensions and apps to open them with. + :Returns: Dictionary of extension: program pairs of strings. :Rtype: `dict` """ return self.fileAssociations - + def getPrefShowAllMessages(self): - """ + """ Get the user preference to display all messages or just errors. + :Returns: State of "Show success messages" check box. :Rtype: `bool` """ return self.checkBox_showAllMessages.isChecked() - + def getPrefShowHiddenFiles(self): - """ + """ Get the user preference for showing hidden files by default. + :Returns: State of "Show hidden files" check box. :Rtype: `bool` """ return self.checkBox_showHiddenFiles.isChecked() - + def getPrefAutoCompleteAddressBar(self): - """ + """ Get the user preference for enabling address bar auto-completion. + :Returns: State of "Auto complete paths in address bar" check box. :Rtype: `bool` """ return self.checkBox_autoCompleteAddressBar.isChecked() - + def getPrefLineLimit(self): - """ + """ Get the user preference for line limit before truncating files. + :Returns: Number of lines to display before truncating a file. :Rtype: `int` """ return self.lineLimitSpinBox.value() - + def getPrefSyntaxHighlighting(self): - """ + """ Get the user preference to enable syntax highlighting. + :Returns: State of "Enable syntax highlighting" check box. :Rtype: `bool` """ return self.checkBox_syntaxHighlighting.isChecked() - + def getPrefTeletypeConversion(self): - """ + """ Get the user preference to enable teletype character conversion. + :Returns: State of "Display teletype colors" check box. :Rtype: `bool` """ return self.checkBox_teletypeConversion.isChecked() - + def getPrefTextEditor(self): - """ + """ Get the user-preferred text editor. + :Returns: Text in Text editor QTextEdit. :Rtype: `str` """ return self.lineEditTextEditor.text() - + def getPrefTheme(self): """ Get the selected theme. - + We may eventually make this a combo box supporting multiple themes, so use the string name instead of just a boolean. - + :Returns: Selected theme name, or None if the default :Rtype: `str` | None """ return "dark" if self.themeWidget.isChecked() else None - + def getPrefUseSpaces(self): - """ + """ Get the user preference for spaces vs. tabs. + :Returns: State of "Use spaces instead of tabs" check box. :Rtype: `bool` """ return self.useSpacesCheckBox.isChecked() - + def getPrefTabSpaces(self): - """ + """ Get the user preference for number of spaces equaling a tab. + :Returns: Number of spaces to use instead of a tab. Only use this number of use spaces is also True. @@ -258,9 +265,10 @@ def getPrefTabSpaces(self): `int` """ return self.useSpacesSpinBox.value() - + def getPrefAutoIndent(self): - """ + """ Get the user preference for auto-indentation. + :Returns: State of "Use auto indentation" check box. :Rtype: @@ -269,30 +277,34 @@ def getPrefAutoIndent(self): return self.checkBox_autoIndent.isChecked() def getPrefDiffTool(self): - """ + """ Get the user preference for diff tool. + :Returns: Text in Diff tool QTextEdit. :Rtype: `str` """ return self.lineEditDiffTool.text() - + @Slot(bool) def newProgField(self, *args): + """ Add a new line to the programs list. + """ self.lineEditProgs.append(QLineEdit(self)) self.progLayout.addWidget(self.lineEditProgs[len(self.lineEditProgs)-1]) self.lineEditExts.append(QLineEdit(self)) self.extLayout.addWidget(self.lineEditExts[len(self.lineEditExts)-1]) - + def populateProgsAndExts(self, programs): - """ + """ Fill out the UI with the user preference for programs and extensions. + :Parameters: programs : `dict` Dictionary of extension: program pairs of strings. """ self.lineEditProgs = [] self.lineEditExts = [] - + # Get unique programs. tmpSet = set() progs = [x for x in programs.values() if x not in tmpSet and not tmpSet.add(x)] @@ -315,10 +327,10 @@ def populateProgsAndExts(self, programs): self.lineEdit.setText(exts[index]) exts.pop(index) del index - - for i in range(len(progs)): + + for i, prog in enumerate(progs): # Create and populate two QLineEdit objects per extension: program pair. - self.lineEditProgs.append(QLineEdit(progs[i], self)) + self.lineEditProgs.append(QLineEdit(prog, self)) #self.lineEditProgs[i].setValidator(self.progValidator) self.progLayout.addWidget(self.lineEditProgs[i]) self.lineEditExts.append(QLineEdit(exts[i], self)) @@ -326,18 +338,17 @@ def populateProgsAndExts(self, programs): self.extLayout.addWidget(self.lineEditExts[i]) self.progWidget.setLayout(self.progLayout) self.extWidget.setLayout(self.extLayout) - + @Slot(QAbstractButton) def restoreDefaults(self, btn): - """ - Restore the GUI to the program's default settings. + """ Restore the GUI to the program's default settings. Don't update the actual preferences (that happens if OK is pressed). """ if btn == self.buttonBox.button(QDialogButtonBox.RestoreDefaults): # Delete old QLineEdit objects. self.deleteItems(self.progLayout) self.deleteItems(self.extLayout) - + # Set other preferences in the GUI. default = self.parent().window().app.DEFAULTS self.checkBox_parseLinks.setChecked(default['parseLinks']) @@ -357,27 +368,30 @@ def restoreDefaults(self, btn): self.updateFontLabel() self.lineLimitSpinBox.setValue(default['lineLimit']) self.checkBox_autoIndent.setChecked(default['autoIndent']) - + # Re-create file association fields with the default programs. self.populateProgsAndExts(self.parent().defaultPrograms) - + @Slot(bool) def selectFont(self, *args): + """ Update the user's font preference. + """ font, ok = QFontDialog.getFont(self.docFont, self, "Select Font") if ok: self.docFont = font self.updateFontLabel() - + def updateFontLabel(self): + """ Update the UI font label to show the user's selected font. + """ bold = "Bold " if self.docFont.bold() else "" italic = "Italic " if self.docFont.italic() else "" self.labelFont.setText("Document font: {}pt {}{}{}".format(self.docFont.pointSize(), bold, italic, self.docFont.family())) - + @Slot() def validate(self): - """ - Make sure everything has valid input. + """ Make sure everything has valid input. Make sure there are no duplicate extensions. Accepts or rejects accepted() signal accordingly. """ @@ -388,13 +402,13 @@ def validate(self): lineEdit.setStyleSheet("background-color:salmon") QMessageBox.warning(self, "Warning", "One or more extension is invalid.") return - + # Get file extensions for this app to handle. extText = self.lineEdit.text() # Strip out periods and spaces. extText = extText.replace(' ', '').replace('.', '') progList = [[x, ""] for x in extText.split(',') if x] - + for i in range(len(self.lineEditProgs)): extText = self.lineEditExts[i].text() progText = self.lineEditProgs[i].text() @@ -402,7 +416,7 @@ def validate(self): for ext in extText.split(','): if ext: progList.append([ext, progText]) - + # Make sure there aren't any duplicate extensions. tmpSet = set() uniqueExt = [ext for ext, prog in progList if ext not in tmpSet and not tmpSet.add(ext)] @@ -411,6 +425,6 @@ def validate(self): else: QMessageBox.warning(self, "Warning", "You have entered the same extension for two or more programs.") return - + # Accept if we made it this far. self.accept() diff --git a/usdmanager/utils.py b/usdmanager/utils.py index 4e613c4..79f4d0f 100644 --- a/usdmanager/utils.py +++ b/usdmanager/utils.py @@ -30,12 +30,6 @@ import Qt from Qt import QtCore, QtWidgets -if Qt.IsPySide: - import pysideuic as uic -elif Qt.IsPySide2: - import pyside2uic as uic -else: - uic = Qt._uic from .constants import USD_EXTS, USD_AMBIGUOUS_EXTS, USD_ASCII_EXTS, USD_CRATE_EXTS @@ -65,8 +59,8 @@ def expandPath(path, parentPath=None, sdf_format_args=None, extractedDir=None): sdf_format_args : `dict` | None Dictionary of key/value `str` pairs from a path's :SDF_FORMAT_ARGS: extractedDir: `str` | None - If the file is part of an extracted usdz archive, this is the path - to the extracted dir of the archive. + If the file is part of an extracted usdz archive, this is the path + to the extracted dir of the archive. :Returns: Normalized path with variables expanded. :Rtype: @@ -292,18 +286,13 @@ def usdcat(inputFile, outputFile, format=None): :Raises ValueError: If invalid format given compared to output file extension. """ - 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) - + cmd = ['usdcat', inputFile, '-o', outputFile] if format and outputFile.endswith(".usd"): # For usdcat, use of --usdFormat requires output file end with '.usd' extension. - cmd += " --usdFormat {}".format(format) - logger.debug(cmd) + cmd += ['--usdFormat', format] + logger.debug(subprocess.list2cmdline(cmd)) try: - subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) + subprocess.check_output(cmd, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: raise OSError("Failed to convert file {}: {}".format(inputFile, e.output)) @@ -319,22 +308,15 @@ def usdzip(inputs, dest): :Raises OSError: If usdzip fails """ - 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) + cmd = ["usdzip"] + if type(inputs) is list: + cmd += inputs else: - cmd = ["usdzip"] - if type(inputs) is list: - cmd += inputs - else: - cmd.append(inputs) - cmd.append(dest) - logger.debug(subprocess.list2cmdline(cmd)) + cmd.append(inputs) + cmd.append(dest) + logger.debug(subprocess.list2cmdline(cmd)) try: - subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) + subprocess.check_output(cmd, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: raise OSError("Failed to zip: {}".format(e.output)) @@ -508,60 +490,6 @@ def isUsdFile(path): return isUsdExt(os.path.splitext(path)[1]) -def loadUiType(uiFile, sourceFile=None, className="DefaultWidgetClass"): - """ Used to define a custom widget's class. - - :Parameters: - uiFile : `str` - UI file path. Can be relative if loading from the same directory as sourceFile. - sourceFile : `str` - File path of loading module. - Used to help find embedded resources and to find uiFile when the file path is relative. - className : `str` - Class name - :Returns: - Class type - :Rtype: - `type` - """ - import sys - import xml.etree.ElementTree as xml - if isPy3(): - from io import StringIO - else: - from StringIO import StringIO - - if not os.path.exists(uiFile) and not os.path.isabs(uiFile): - if sourceFile is None: - uiFile = resource_filename(__name__, uiFile) - sourceDir = os.path.dirname(uiFile) - else: - sourceDir = os.path.dirname(sourceFile) - uiFile = os.path.join(sourceDir, uiFile) - else: - sourceDir = os.path.dirname(uiFile) - - # Search for resources in this tool's directory. - if sourceDir not in sys.path: - sys.path.insert(0, sourceDir) - - parsed = xml.parse(uiFile) - widget_class = parsed.find('widget').get('class') - form_class = parsed.find('class').text - - with open(uiFile) as f: - o = StringIO() - frame = {} - uic.compileUi(f, o, indent=0) - pyc = compile(o.getvalue(), "", "exec") - exec(pyc) in frame - - # Fetch the base_class and form class based on their type. - form_class = frame["Ui_{}".format(form_class)] - base_class = eval("QtWidgets.{}".format(widget_class)) - return type("{}Base".format(className), (form_class, base_class), {}) - - def loadUiWidget(path, parent=None, source_path=None): """ Load a Qt Designer .ui file and return an instance of the user interface @@ -602,14 +530,12 @@ def overrideCursor(cursor=QtCore.Qt.WaitCursor): Example: with overrideCursor(): # do something that may raise an error - """ - from Qt.QtWidgets import QApplication - - QApplication.setOverrideCursor(cursor) + """ + QtWidgets.QApplication.setOverrideCursor(cursor) try: yield finally: - QApplication.restoreOverrideCursor() + QtWidgets.QApplication.restoreOverrideCursor() def queryItemValue(url, key, default=None): diff --git a/usdmanager/version.py b/usdmanager/version.py index 531dfd9..2da5420 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.14.0' +__version__ = '0.15.0'