From 768348fc909fd97f0c0b2850280249e31fd97197 Mon Sep 17 00:00:00 2001 From: shriramiyer Date: Mon, 29 Oct 2018 13:26:41 -0700 Subject: [PATCH] Improved playhead scrubbing experience in usdview. When a user clicks at a point on the slider that corresponds to a certain frame, the play head now moves to the frame instead of jumping 10 steps in that direction. This behavior is implemented in a custom derived class of QSlider (named FrameSlider). Most of the frame slider functionality has been moved out of appController into this separate module. (Internal change: 1905609) --- pxr/usdImaging/lib/usdviewq/CMakeLists.txt | 1 + pxr/usdImaging/lib/usdviewq/appController.py | 41 ++---- pxr/usdImaging/lib/usdviewq/frameSlider.py | 129 +++++++++++++++++++ pxr/usdImaging/lib/usdviewq/mainWindowUI.ui | 54 ++++---- 4 files changed, 172 insertions(+), 53 deletions(-) create mode 100644 pxr/usdImaging/lib/usdviewq/frameSlider.py diff --git a/pxr/usdImaging/lib/usdviewq/CMakeLists.txt b/pxr/usdImaging/lib/usdviewq/CMakeLists.txt index 16f401186e..d738d42fef 100644 --- a/pxr/usdImaging/lib/usdviewq/CMakeLists.txt +++ b/pxr/usdImaging/lib/usdviewq/CMakeLists.txt @@ -33,6 +33,7 @@ pxr_library(usdviewq appEventFilter.py arrayAttributeView.py customAttributes.py + frameSlider.py appController.py usdviewApi.py plugin.py diff --git a/pxr/usdImaging/lib/usdviewq/appController.py b/pxr/usdImaging/lib/usdviewq/appController.py index a755a8a608..d941461068 100644 --- a/pxr/usdImaging/lib/usdviewq/appController.py +++ b/pxr/usdImaging/lib/usdviewq/appController.py @@ -539,7 +539,8 @@ def __init__(self, parserData, resolverContextFn): self._ui.propertyView.setHorizontalScrollMode( QtWidgets.QAbstractItemView.ScrollPerPixel) - self._ui.frameSlider.setTracking(self._dataModel.viewSettings.redrawOnScrub) + self._ui.frameSlider.setUpdateOnFrameScrub( + self._dataModel.viewSettings.redrawOnScrub) self._ui.colorGroup = QtWidgets.QActionGroup(self) self._ui.colorGroup.setExclusive(True) @@ -716,11 +717,8 @@ def __init__(self, parserData, resolverContextFn): self._ui.primView.expanded.connect(self._primViewExpanded) - self._ui.frameSlider.valueChanged.connect(self.setFrame) - - self._ui.frameSlider.sliderMoved.connect(self._sliderMoved) - - self._ui.frameSlider.sliderReleased.connect(self._updateOnFrameChange) + self._ui.frameSlider.signalFrameChanged.connect(self.setFrame) + self._ui.frameSlider.signalPositionChanged.connect(self._sliderMoved) self._ui.frameField.editingFinished.connect(self._frameStringChanged) @@ -1012,14 +1010,6 @@ def __init__(self, parserData, resolverContextFn): self._setupDebugMenu() - # timer for slider. when user stops scrubbing for 0.5s, update stuff. - self._sliderTimer = QtCore.QTimer(self) - self._sliderTimer.setInterval(500) - - # Connect the update timer to _frameStringChanged, which will ensure - # we update _currentTime prior to updating UI - self._sliderTimer.timeout.connect(self._frameStringChanged) - # We refresh as if all view settings changed. In the future, we # should do more granular refreshes. This first requires more # granular signals from ViewSettingsDataModel. @@ -1211,8 +1201,7 @@ def _UpdateTimeSamples(self, resetStageDataOnly=False): if self._playbackAvailable: if not resetStageDataOnly: - self._ui.frameSlider.setRange(0, len(self._timeSamples)-1) - self._ui.frameSlider.setValue(self._ui.frameSlider.minimum()) + self._ui.frameSlider.resetSlider(len(self._timeSamples)) self._setPlayShortcut() self._ui.playButton.setCheckable(True) self._ui.playButton.setChecked(False) @@ -1483,7 +1472,7 @@ def _reloadVaryingUI(self): else: self._resetPrimView(restoreSelection=False) - self._ui.frameSlider.setValue(self._ui.frameSlider.minimum()) + self._ui.frameSlider.resetToMinimum() if not self._stageView: @@ -1674,7 +1663,8 @@ def _adjustDefaultMaterial(self, checked): def _redrawOptionToggled(self, checked): self._dataModel.viewSettings.redrawOnScrub = checked - self._ui.frameSlider.setTracking(self._dataModel.viewSettings.redrawOnScrub) + self._ui.frameSlider.setUpdateOnFrameScrub( + self._dataModel.viewSettings.redrawOnScrub) # Frame-by-frame/Playback functionality =================================== @@ -1742,18 +1732,12 @@ def _advanceFrameForPlayback(self): def _advanceFrame(self): if not self._playbackAvailable: return - newValue = self._ui.frameSlider.value() + 1 - if newValue > self._ui.frameSlider.maximum(): - newValue = self._ui.frameSlider.minimum() - self._ui.frameSlider.setValue(newValue) + self._ui.frameSlider.advanceFrame() def _retreatFrame(self): if not self._playbackAvailable: return - newValue = self._ui.frameSlider.value() - 1 - if newValue < self._ui.frameSlider.minimum(): - newValue = self._ui.frameSlider.maximum() - self._ui.frameSlider.setValue(newValue) + self._ui.frameSlider.retreatFrame() def _findIndexOfFieldContents(self, field): # don't convert string to float directly because of rounding error @@ -1795,15 +1779,13 @@ def _frameStringChanged(self): if (indexOfFrame != Usd.TimeCode.Default()): self.setFrame(indexOfFrame, forceUpdate=True) - self._ui.frameSlider.setValue(indexOfFrame) + self._ui.frameSlider.setValueImmediate(indexOfFrame) self._ui.frameField.setText( str(self._dataModel.currentFrame.GetValue())) def _sliderMoved(self, value): self._ui.frameField.setText(str(self._timeSamples[value])) - self._sliderTimer.stop() - self._sliderTimer.start() # Prim/Attribute search functionality ===================================== @@ -3118,7 +3100,6 @@ def _updateOnFrameChange(self, refreshUI = True): self._updateHUDGeomCounts() self._updatePropertyView() self._refreshAttributeValue() - self._sliderTimer.stop() # value sources of an attribute can change upon frame change # due to value clips, so we must update the layer stack. diff --git a/pxr/usdImaging/lib/usdviewq/frameSlider.py b/pxr/usdImaging/lib/usdviewq/frameSlider.py new file mode 100644 index 0000000000..b4b54662bd --- /dev/null +++ b/pxr/usdImaging/lib/usdviewq/frameSlider.py @@ -0,0 +1,129 @@ +# +# Copyright 2018 Pixar +# +# Licensed under the Apache License, Version 2.0 (the "Apache License") +# with the following modification; you may not use this file except in +# compliance with the Apache License and the following modification to it: +# Section 6. Trademarks. is deleted and replaced with: +# +# 6. Trademarks. This License does not grant permission to use the trade +# names, trademarks, service marks, or product names of the Licensor +# and its affiliates, except as required to comply with Section 4(c) of +# the License and to reproduce the content of the NOTICE file. +# +# You may obtain a copy of the Apache License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the Apache License with the above modification is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the Apache License for the specific +# language governing permissions and limitations under the Apache License. +# +from qt import QtCore, QtGui, QtWidgets + +class FrameSlider(QtWidgets.QSlider): + # Emitted when the current frame of the slider changes and the stage's + # current frame needs to be updated. + signalFrameChanged = QtCore.Signal(int) + + # Emitted when the slider position has changed but the underlying frame + # value hasn't been changed. + signalPositionChanged = QtCore.Signal(int) + + def __init__(self, parent): + super(FrameSlider, self).__init__(parent) + self._sliderTimer = QtCore.QTimer(self) + self._sliderTimer.setInterval(500) + self._sliderTimer.timeout.connect(self.sliderTimeout) + self.valueChanged.connect(self.sliderValueChanged) + self._mousePressed = False + self._scrubbing = False + self._updateOnFrameScrub = False + + def setUpdateOnFrameScrub(self, updateOnFrameScrub): + self._updateOnFrameScrub = updateOnFrameScrub + + def sliderTimeout(self): + if not self._updateOnFrameScrub and self._mousePressed: + self._sliderTimer.stop() + self.signalPositionChanged.emit(self.value()) + return + self.frameChanged() + + def frameChanged(self): + self._sliderTimer.stop() + self.signalFrameChanged.emit(self.value()) + + def sliderValueChanged(self, value): + self._sliderTimer.stop() + self._sliderTimer.start() + + def setValueImmediate(self, value): + self.setValue(value) + self.frameChanged() + + def setValueFromEvent(self, event, immediate=True): + currentValue = self.value() + movePosition = self.minimum() + ((self.maximum()-self.minimum()) * + event.x()) / float(self.width()) + targetPosition = round(movePosition) + if targetPosition == currentValue: + if (movePosition - currentValue) >= 0: + targetPosition = currentValue + 1 + else: + targetPosition = currentValue - 1; + if immediate: + self.setValueImmediate(targetPosition) + else: + self.setValue(targetPosition) + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self._mousePressed = True + self.setValueFromEvent(event) + event.accept() + super(FrameSlider, self).mousePressEvent(event) + + def mouseMoveEvent(self, event): + # Since mouseTracking is disabled by default, this event callback is + # only invoked when a mouse is pressed down and moved (i.e. dragged or + # scrubbed). + self._scrubbing = True + #if not self._updateOnFrameScrub: + #return super(FrameSlider, self).mouseMoveEvent(event) + self.setValueFromEvent(event, immediate=False) + event.accept() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self._mousePressed = False + # If this is just a click (and not a drag with mouse pressed), + # we don't want setValue twice for the same frame value. + if self._scrubbing: + self.setValueFromEvent(event) + event.accept() + self._scrubbing = False + super(FrameSlider, self).mouseReleaseEvent(event) + + def advanceFrame(self): + newValue = self.value() + 1 + if newValue > self.maximum(): + newValue = self.minimum() + self.setValueImmediate(newValue) + + def retreatFrame(self): + newValue = self.value() - 1 + if newValue < self.minimum(): + newValue = self.maximum() + self.setValueImmediate(newValue) + + def resetSlider(self, numTimeSamples): + self.setRange(0, numTimeSamples-1) + self.resetToMinimum() + + def resetToMinimum(self): + self.setValue(self.minimum()) + # Call this here to push the update immediately. + self.frameChanged() diff --git a/pxr/usdImaging/lib/usdviewq/mainWindowUI.ui b/pxr/usdImaging/lib/usdviewq/mainWindowUI.ui index d937799448..0013630ebe 100644 --- a/pxr/usdImaging/lib/usdviewq/mainWindowUI.ui +++ b/pxr/usdImaging/lib/usdviewq/mainWindowUI.ui @@ -949,13 +949,16 @@ - + 0 0 + + 0 + Qt::Horizontal @@ -1205,27 +1208,27 @@ - - &Edit - - - - Stage Interpolation - - - - - - - - - - - - - - - + + &Edit + + + + Stage Interpolation + + + + + + + + + + + + + + + @@ -2657,7 +2660,12 @@ PrimTreeWidget QTreeWidget -
primTreeWidget.h
+
primTreeWidget
+
+ + FrameSlider + QSlider +
frameSlider