Skip to content

Commit

Permalink
Improved playhead scrubbing experience in usdview.
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
shriramiyer authored and pixar-oss committed Oct 29, 2018
1 parent 3dc4c5c commit 768348f
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 53 deletions.
1 change: 1 addition & 0 deletions pxr/usdImaging/lib/usdviewq/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pxr_library(usdviewq
appEventFilter.py
arrayAttributeView.py
customAttributes.py
frameSlider.py
appController.py
usdviewApi.py
plugin.py
Expand Down
41 changes: 11 additions & 30 deletions pxr/usdImaging/lib/usdviewq/appController.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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 ===================================

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =====================================

Expand Down Expand Up @@ -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.
Expand Down
129 changes: 129 additions & 0 deletions pxr/usdImaging/lib/usdviewq/frameSlider.py
Original file line number Diff line number Diff line change
@@ -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()
54 changes: 31 additions & 23 deletions pxr/usdImaging/lib/usdviewq/mainWindowUI.ui
Original file line number Diff line number Diff line change
Expand Up @@ -949,13 +949,16 @@
</widget>
</item>
<item>
<widget class="QSlider" name="frameSlider">
<widget class="FrameSlider" name="frameSlider">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="pageStep">
<number>0</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
Expand Down Expand Up @@ -1205,27 +1208,27 @@
<addaction name="actionQuit"/>
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
<string>&amp;Edit</string>
</property>
<widget class="QMenu" name="menuInterpolation">
<property name="title">
<string>Stage Interpolation</string>
</property>
</widget>
<addaction name="menuInterpolation"/>
<addaction name="separator"/>
<addaction name="actionLoad"/>
<addaction name="actionUnload"/>
<addaction name="separator"/>
<addaction name="actionActivate"/>
<addaction name="actionDeactivate"/>
<addaction name="separator"/>
<addaction name="actionMake_Visible"/>
<addaction name="actionVis_Only"/>
<addaction name="actionMake_Invisible"/>
<addaction name="actionRemove_Session_Visibility"/>
<addaction name="actionReset_All_Session_Visibility"/>
<property name="title">
<string>&amp;Edit</string>
</property>
<widget class="QMenu" name="menuInterpolation">
<property name="title">
<string>Stage Interpolation</string>
</property>
</widget>
<addaction name="menuInterpolation"/>
<addaction name="separator"/>
<addaction name="actionLoad"/>
<addaction name="actionUnload"/>
<addaction name="separator"/>
<addaction name="actionActivate"/>
<addaction name="actionDeactivate"/>
<addaction name="separator"/>
<addaction name="actionMake_Visible"/>
<addaction name="actionVis_Only"/>
<addaction name="actionMake_Invisible"/>
<addaction name="actionRemove_Session_Visibility"/>
<addaction name="actionReset_All_Session_Visibility"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
Expand Down Expand Up @@ -2657,7 +2660,12 @@
<customwidget>
<class>PrimTreeWidget</class>
<extends>QTreeWidget</extends>
<header>primTreeWidget.h</header>
<header>primTreeWidget</header>
</customwidget>
<customwidget>
<class>FrameSlider</class>
<extends>QSlider</extends>
<header>frameSlider</header>
</customwidget>
</customwidgets>
<resources/>
Expand Down

0 comments on commit 768348f

Please sign in to comment.