[prev in list] [next in list] [prev in thread] [next in thread] 

List:       pykde
Subject:    Re: [PyQt] Curious behaviour of slider/spin when controlled by keyboard
From:       Shriramana Sharma <samjnaa () gmail ! com>
Date:       2012-11-15 17:53:27
Message-ID: CAH-HCWVScpCX0S4FLyKGimnccbmUw-fAdSUuJt=_jik3ogxYPQ () mail ! gmail ! com
[Download RAW message or body]

Hello. Could anyone please look into the curious behaviour I reported
last month as seen below? (I'll also ask this on the Qt Interest list
as it concerns C++ as well.)

I've included the buggy PyQt program as well as a minimal test of a
slider in C++ and PyQt. I note that in both the C++/PyQt minimal
examples the bug is reproducible only when there is only a slider + a
doubleSpin and not when there is also an integer spin. When there is
an integer spin in addition, there is some lagging behind between the
widgets (as can be seen visually as well as in the debug output) but
it gets through and I'm able to run the whole range using the
keyboard, but if there is no integer spin and only the double spin
with the slider, the problem surfaces. As it is seen also in C++ I
presume it is not a PyQt-only problem, but this is something probably
other PyQt programmers also can face or would have faced. I'd like to
know what to do to fix this.

Thanks!

Shriramana.

On Mon, Oct 1, 2012 at 9:32 AM, Shriramana Sharma <samjnaa@gmail.com> wrote:
> Hello. With my work on a simple Cubic Bezier investigation application
> in PyQt (attached, obviously under GPL), I ran into a curious
> behaviour of the slider/spin (for the time along the curve) when
> controlled by the keyboard.
>
> Steps:
> 1. Let the focus be either on the slider (on the left) or the spin
> (below it). (The default slider/spin value is 0.50.)
> 3. Press up-arrow key to increase the slider/spin value.
>
> Observation:
> The value will not increase past 0.56.
>
> Steps:
> 4. Press down-arrow key to decrease the slider/spin value until 0.30.
> 5. Press down-arrow once more.
>
> Observation:
> The value jumps down from 0.30 to 0.28 even though the precision is set at 0.01.
>
> Step:
> 6. Press up-arrow.
>
> Observation:
> The value will now not rise above 0.28.
>
> Step:
> 7. Adjust the slider position using the mouse.
>
> Observation:
> The value can change to any value in its full range from 0.00 to 1.00.
>
> Step:
> 8. Adjust the slider using the mouse to go beyond 0.60.
> 9. Press down-arrow to decrease the value until 0.59.
> 10. Press down-arrow once more.
>
> Observation:
> 11. The value jumps down to 0.56.
> 12. It will no longer go above 0.56 using the keyboard (as before).
>
> Query:
> My sliderMoved, spinChanged slots are straightforward, and just
> convert the integer slider value to the spin and update the bezier
> widget accordingly. In which case, I do not understand what it is I am
> doing wrong in my programming. However, I wrote a minimal test where
> the behaviour is not seen. Any guidance is appreciated.
>
> Thanks!
>
> --
> Shriramana Sharma



-- 
Shriramana Sharma

["bezierview.py" (text/x-python)]

#! /usr/bin/env python3

from PyQt4 . QtCore import *
from PyQt4 . QtGui import *
from math import sqrt

# float->string formatting function

def str_three_decimals ( a ) :
	return str ( int ( a * 1000 + 0.5 ) / 1000 )

# mathematical function

def quadraticroots ( a, b, c ) :
	if a == 0 :
		if b != 0 : return [ - c / b ]
		else : return [] # no valid equation so no roots
	det = b * b - 4 * a * c
	if det < 0 : return [] # only real roots will be returned
	if det == 0 : return [ - b / ( 2 * a ) ] # one root
	return [ ( - b - sqrt ( det ) ) / ( 2 * a ),
	         ( - b + sqrt ( det ) ) / ( 2 * a ) ]

# bezier analysis functions

def pointForTime ( t, p1, c1, c2, p2 ) :
	if t <= 0 : return p1
	if t >= 1 : return p2
	return p1 + ( c1 - p1 ) * 3 * t + ( c2 - c1 * 2 + p1 ) * 3 * t * t + ( p2 - c2 * 3 + \
c1 * 3 - p1 ) * t * t * t

def dirForTime   ( t, p1, c1, c2, p2 ) :
	if t < 0 : t = 0
	if t > 1 : t = 1
	return      ( c1 - p1 ) * 3     + ( c2 - c1 * 2 + p1 ) * 6 * t     + ( p2 - c2 * 3 + \
c1 * 3 - p1 ) * 3 * t * t

def accelForTime ( t, p1, c1, c2, p2 ) :
	if t < 0 : t = 0
	if t > 1 : t = 1
	return                            ( c2 - c1 * 2 + p1 ) * 6         + ( p2 - c2 * 3 + \
c1 * 3 - p1 ) * 6 * t

def timesOfCusp ( p1, c1, c2, p2 ) :
	a = c1 - p1
	b = c2 - c1 - a
	c = p2 - c2 - a - b * 2
	# dir = ( c * t * t + b * 2 * t + a ) * 3
	# at cusp, dir vector becomes null i.e. both x and y components are zero
	# qA = c ; qB = 2 * b ; qC = a
	rootsX = quadraticroots ( c . x (), b . x () * 2, a . x () )
	rootsY = quadraticroots ( c . y (), b . y () * 2, a . y () )
	cusps = []
	for x in rootsX :
		if x in rootsY : cusps += [ x ]
	return cusps

def timesOfInflection ( p1, c1, c2, p2 ) :

	# algorithm from http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html


	a = c1 - p1
	b = c2 - c1 - a
	c = p2 - c2 - a - b * 2
	
	qA = ( b . x () * c . y () - b . y () * c . x () )
	qB = ( a . x () * c . y () - a . y () * c . x () )
	qC = ( a . x () * b . y () - a . y () * b . x () )

	roots = quadraticroots ( qA, qB, qC )
	cusps = timesOfCusp ( p1, c1, c2, p2 )

	validroots = []
	for x in roots :
		if x > 0 and x < 1 and x not in cusps : validroots . append ( x )
	return validroots

# widgets

class BezierWidget ( QWidget ) :
	
	def __init__ ( self, parent = None ) :
		
		super ( BezierWidget, self ) . __init__ ( parent )

		self . setFixedSize ( 400, 400 )
		self . setMouseTracking ( True )
		
		self . p1 = QPointF ( 100, 150 )
		self . c1 = QPointF ( 166, 250 )
		self . c2 = QPointF ( 234, 250 )
		self . p2 = QPointF ( 300, 150 )

		self . bezTime = 0.5
		self . timePoint = pointForTime ( self . bezTime, self . p1, self . c1, self . c2, \
self . p2 )  self . timeDir   = dirForTime   ( self . bezTime, self . p1, self . c1, \
self . c2, self . p2 )  self . timeAccel = accelForTime ( self . bezTime, self . p1, \
self . c1, self . c2, self . p2 )  self . calcInflectionAndCusp ()

		self . tweaking = False

		self . diamond = QPainterPath ()
		self . diamond . moveTo (  4,  0 )
		self . diamond . lineTo (  0,  4 )
		self . diamond . lineTo ( -4,  0 )
		self . diamond . lineTo (  0, -4 )
		self . diamond . closeSubpath ()
	
	def paintEvent ( self, event ) :
		
		painter = QPainter ( self )
		painter . setRenderHint ( QPainter . Antialiasing )
		painter . translate ( 0, 400 )
		painter . scale ( 1, -1 )
		
		palette = QApplication . palette ()
		bezierPen = QPen ( palette . text (), 1 )
		handlePen = QPen ( palette . highlight (), 2, Qt . DashLine )
		chandlePen = QPen ( palette . highlightedText (), 0.5, Qt . DashLine )

		handle1 = QPainterPath ()
		handle1 . moveTo ( self . p1 ) ; handle1 . lineTo ( self . c1 )
		painter . strokePath ( handle1, handlePen )

		handle2 = QPainterPath ()
		handle2 . moveTo ( self . p2 ) ; handle2 . lineTo ( self . c2 )
		painter . strokePath ( handle2, handlePen )

		chandle1 = QPainterPath ()
		chandle1 . moveTo ( self . p1 ) ; chandle1 . lineTo ( self . c2 )
		painter . strokePath ( chandle1, chandlePen )

		chandle2 = QPainterPath ()
		chandle2 . moveTo ( self . p2 ) ; chandle2 . lineTo ( self . c1 )
		painter . strokePath ( chandle2, chandlePen )

		bezier = QPainterPath ()
		bezier . moveTo ( self . p1 )
		bezier . cubicTo ( self . c1, self . c2, self . p2 )
		painter . strokePath ( bezier, bezierPen )

		for pt in ( self . p1, self . c1, self . c2, self . p2 ) :
			painter . save ()
			painter . translate ( pt )
			painter . fillPath ( self . diamond, palette . highlightedText () )
			painter . restore ()

		for i in range ( 9 ) :
			pt = pointForTime ( ( i + 1 ) / 10, self . p1, self . c1, self . c2, self . p2 )
			painter . save ()
			painter . translate ( pt )
			painter . scale ( 0.5, 0.5 )
			painter . fillPath ( self . diamond, palette . highlightedText () )
			painter . restore ()

		self . timePoint = pointForTime ( self . bezTime, self . p1, self . c1, self . c2, \
self . p2 )  self . timeDir   = dirForTime   ( self . bezTime, self . p1, self . c1, \
self . c2, self . p2 )  self . timeAccel = accelForTime ( self . bezTime, self . p1, \
self . c1, self . c2, self . p2 )  
		dirLine   = QPainterPath ()
		dirLine   . lineTo ( self . timeDir   / 10 )
		accelLine = QPainterPath ()
		accelLine . lineTo ( self . timeAccel / 10 )

		painter . save ()
		painter . translate ( self . timePoint )
		painter . strokePath ( dirLine, QPen ( palette . highlight (), 2 ) )
		painter . strokePath ( accelLine, QPen ( palette . linkVisited (), 2 ) )
		painter . scale ( 1.5, 1.5 )
		painter . fillPath ( self . diamond, palette . highlightedText () )
		painter . restore ()

		for i in range ( len ( self . inflectionTimes ) ) :

			painter . save ()
			
			pt = pointForTime ( self . inflectionTimes [ i ], self . p1, self . c1, self . c2, \
self . p2 )  painter . translate ( pt )
			painter . fillPath ( self . diamond, palette . linkVisited () )

			painter . restore ()

	def mousePressEvent ( self, event ) :
		pt = event . posF ()
		pt . setY ( 399 - pt . y () )
		offset = 5
		hotspot = QRectF ( - offset, - offset, offset * 2, offset * 2 )
		for x in ( self . p1, self . c1, self . c2, self . p2 ) :
			if hotspot . translated ( x ) . contains ( pt ) :
				self . tweaking = True
				if   x is self . p1 : self . tweakedPoint = "p1"
				elif x is self . c1 : self . tweakedPoint = "c1"
				elif x is self . c2 : self . tweakedPoint = "c2"
				elif x is self . p2 : self . tweakedPoint = "p2"
				return
	
	def mouseMoveEvent ( self, event ) :
		
		pt = event . posF ()
		if pt . x () < 0 : pt . setX ( 0 )
		if pt . x () > 399 : pt . setX ( 399 )
		if pt . y () < 0 : pt . setY ( 0 )
		if pt . y () > 399 : pt . setY ( 399 )
		
		ptInt = pt . toPoint ()
		QToolTip . showText ( self . mapToGlobal ( ptInt ), "%d,%d" % ( ptInt . x (), 399 - \
ptInt . y () ), self )

		if self . tweaking :
			
			pt . setY ( 399 - pt . y () )
			if   self . tweakedPoint == "p1" : self . p1 = pt
			elif self . tweakedPoint == "c1" : self . c1 = pt
			elif self . tweakedPoint == "c2" : self . c2 = pt
			elif self . tweakedPoint == "p2" : self . p2 = pt
			self . repaint ()
			self . emit ( SIGNAL ( "pointsModifiedInWidget ()" ) )
	
	def mouseReleaseEvent ( self, event ) :
		self . tweaking = False

	def calcInflectionAndCusp ( self ) :
		self . cuspTimes        = timesOfCusp       ( self . p1, self . c1, self . c2, self \
. p2 )  self . inflectionTimes  = timesOfInflection ( self . p1, self . c1, self . \
c2, self . p2 )  self . inflectionDirs   = [   dirForTime ( t, self . p1, self . c1, \
self . c2, self . p2 ) for t in self . inflectionTimes ]  self . inflectionAccels = [ \
accelForTime ( t, self . p1, self . c1, self . c2, self . p2 ) for t in self . \
inflectionTimes ]  
class MainWindow ( QWidget ) :

	def __init__ ( self, parent = None ) :

		super ( MainWindow, self ) . __init__ ( parent )

		self . setWindowTitle ( "Cubic Bezier Sandbox" )
		
		self . timeSlider = QSlider ()
		self . timeSlider . setMinimum ( 0 )
		self . timeSlider . setMaximum ( 100 )
		self . timeSlider . setValue ( 50 )

		self . timeSpin = QDoubleSpinBox ()
		self . timeSpin . setDecimals ( 2 )
		self . timeSpin . setMinimum ( 0 )
		self . timeSpin . setMaximum ( 1 )
		self . timeSpin . setSingleStep ( 0.01 )
		self . timeSpin . setValue ( 0.5 )

		self . lhsLayout = QVBoxLayout ()
		self . lhsLayout . addWidget ( self . timeSlider, 0, Qt . AlignHCenter )
		self . lhsLayout . addWidget ( self . timeSpin )

		self . bezierWidget = BezierWidget ()
		
		self . p1Label = QLabel ( "p1" )
		self . c1Label = QLabel ( "c1" )
		self . c2Label = QLabel ( "c2" )
		self . p2Label = QLabel ( "p2" )

		self . p1xSpin = QSpinBox ()
		self . p1ySpin = QSpinBox ()
		self . c1xSpin = QSpinBox ()
		self . c1ySpin = QSpinBox ()
		self . c2xSpin = QSpinBox ()
		self . c2ySpin = QSpinBox ()
		self . p2xSpin = QSpinBox ()
		self . p2ySpin = QSpinBox ()
		
		self . xyFieldsGrid = QGridLayout ()
		xyFieldsGridItems = ( ( self . p1Label, self . p1xSpin, self . p1ySpin ),
		                      ( self . c1Label, self . c1xSpin, self . c1ySpin ),
		                      ( self . c2Label, self . c2xSpin, self . c2ySpin ),
		                      ( self . p2Label, self . p2xSpin, self . p2ySpin ) )
		for i in range ( 4 ) :
			for j in range ( 3 ) :
				self . xyFieldsGrid . addWidget ( xyFieldsGridItems [ i ] [ j ], i, j )
				if j > 0 : # isinstance ( xyFieldsGridItems [ i ] [ j ], QSpinBox ) would be \
pedantically correct  xyFieldsGridItems [ i ] [ j ] . setMaximum ( 399 )
		
		self . inflectTimeLabel   = QLabel ( "Inflection times" )
		self . inflectTime1Label  = QLabel ()
		self . inflectTime2Label  = QLabel ()
		self . inflDirLabel       = QLabel ( "Inflection dirs" )
		self . inflDirValLabels   = [ [ QLabel (), QLabel () ], [ QLabel (), QLabel () ] ]
		self . inflAccelLabel     = QLabel ( "Inflection accels" )
		self . inflAccelValLabels = [ [ QLabel (), QLabel () ], [ QLabel (), QLabel () ] ]
		self . timePointLabel     = QLabel ( "Point for time" )
		self . timePointXLabel    = QLabel ()
		self . timePointYLabel    = QLabel ()
		self . timeDirLabel       = QLabel ( "Velocity for time" )
		self . timeDirXLabel      = QLabel ()
		self . timeDirYLabel      = QLabel ()
		self . timeAccelLabel     = QLabel ( "Acceleration for time" )
		self . timeAccelXLabel    = QLabel ()
		self . timeAccelYLabel    = QLabel ()
		
		self . otherFieldsGrid = QGridLayout ()
		a = self . otherFieldsGrid . addWidget
		a ( self . inflectTimeLabel, 0, 0, 1, 2 )
		a ( self . inflectTime1Label, 1, 0 )
		a ( self . inflectTime2Label, 1, 1 )
		a ( self . inflDirLabel, 2, 0, 1, 2 )
		for i in range ( 2 ) :
			for j in range ( 2 ) :
				a ( self . inflDirValLabels [ i ] [ j ], 3 + i, j )
		a ( self . inflAccelLabel, 5, 0, 1, 2 )
		for i in range ( 2 ) :
			for j in range ( 2 ) :
				a ( self . inflAccelValLabels [ i ] [ j ], 6 + i, j )
		a ( self . timePointLabel, 8, 0, 1, 2 )
		a ( self . timePointXLabel, 9, 0 )
		a ( self . timePointYLabel, 9, 1 )
		a ( self . timeDirLabel, 10, 0, 1, 2 )
		a ( self . timeDirXLabel, 11, 0 )
		a ( self . timeDirYLabel, 11, 1 )
		a ( self . timeAccelLabel, 12, 0, 1, 2 )
		a ( self . timeAccelXLabel, 13, 0 )
		a ( self . timeAccelYLabel, 13, 1 )
		
		self . rhsLayout = QVBoxLayout ()
		self . rhsLayout . addLayout ( self . xyFieldsGrid )
		self . rhsLayout . addLayout ( self . otherFieldsGrid )
		self . rhsLayout . addStretch ()

		self . mainLayout = QHBoxLayout ( self )
		self . mainLayout . addLayout ( self . lhsLayout )
		self . mainLayout . addWidget ( self . bezierWidget )
		self . mainLayout . addLayout ( self . rhsLayout )

		QObject . connect ( self . timeSlider, SIGNAL ( "valueChanged ( int )" ), self . \
sliderMoved )  QObject . connect ( self . timeSpin, SIGNAL ( "valueChanged ( double \
)" ), self . spinChanged )  
		QObject . connect ( self . bezierWidget, SIGNAL ( "pointsModifiedInWidget ()" ), \
self . updateXYFields )  QObject . connect ( self . bezierWidget, SIGNAL ( \
"pointsModifiedInWidget ()" ), self . updateAnalysisFields )  self . updateXYFields \
()  self . updateAnalysisFields ()

		for x in self . children () :
			if isinstance ( x, QSpinBox ) :
				QObject . connect ( x, SIGNAL ( "valueChanged ( int )" ), self . \
updateBezierWidgetData )  QObject . connect ( x, SIGNAL ( "valueChanged ( int )" ), \
self . bezierWidget . repaint )  QObject . connect ( x, SIGNAL ( "valueChanged ( int \
)" ), self . updateAnalysisFields )  # note that updateAnalysisFields takes the data \
from the bezier widget so it should also come after updateBezierWidgetData

	def sliderMoved ( self, timeScaled ) :
		self . timeSpin . setValue ( timeScaled / 100 )
		self . bezierWidget . bezTime = timeScaled / 100
		self . bezierWidget . repaint ()
		self . updateAnalysisFields ()

	def spinChanged ( self, time ) :
		self . timeSlider . setValue ( time * 100 )
		self . bezierWidget . bezTime = time
		self . bezierWidget . repaint ()
		self . updateAnalysisFields ()

	def updateXYFields ( self ) :
		bezierSpinMap = ( ( self . bezierWidget . p1, self . p1xSpin, self . p1ySpin ),
		                  ( self . bezierWidget . c1, self . c1xSpin, self . c1ySpin ),
		                  ( self . bezierWidget . c2, self . c2xSpin, self . c2ySpin ),
		                  ( self . bezierWidget . p2, self . p2xSpin, self . p2ySpin ) )
		for mapitem in bezierSpinMap :
			mapitem [ 1 ] . setValue ( mapitem [ 0 ] . x () )
			mapitem [ 2 ] . setValue ( mapitem [ 0 ] . y () )

	def updateBezierWidgetData ( self ) :
		bw = self . bezierWidget
		bezierSpinMap = ( ( bw . p1, self . p1xSpin, self . p1ySpin ),
		                  ( bw . c1, self . c1xSpin, self . c1ySpin ),
		                  ( bw . c2, self . c2xSpin, self . c2ySpin ),
		                  ( bw . p2, self . p2xSpin, self . p2ySpin ) )
		for mapitem in bezierSpinMap :
			mapitem [ 0 ] . setX ( mapitem [ 1 ] . value () )
			mapitem [ 0 ] . setY ( mapitem [ 2 ] . value () )
		bw . calcInflectionAndCusp ()

	def updateAnalysisFields ( self ) :
		bw = self . bezierWidget
		if len ( bw . inflectionTimes ) == 0 :
			self . inflectTime1Label . setText ( "-" )
			self . inflectTime2Label . setText ( "-" )
			for row in self . inflDirValLabels :
				for x in row : x . setText ( "-" )
			for row in self . inflAccelValLabels :
				for x in row : x . setText ( "-" )
		elif len ( bw . inflectionTimes ) == 1 :
			self . inflectTime1Label . setText ( str_three_decimals ( bw . inflectionTimes [ 0 \
] ) )  self . inflectTime2Label . setText ( "-" )
			d = self . inflDirValLabels
			d [ 0 ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionDirs [ 0 ] . x () ) \
)  d [ 0 ] [ 1 ] . setText ( str_three_decimals ( bw . inflectionDirs [ 0 ] . y () ) \
)  d [ 1 ] [ 0 ] . setText ( "-" )
			d [ 1 ] [ 1 ] . setText ( "-" )
			a = self . inflAccelValLabels
			a [ 0 ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionAccels [ 0 ] . x () \
) )  a [ 0 ] [ 1 ] . setText ( str_three_decimals ( bw . inflectionAccels [ 0 ] . y \
() ) )  a [ 1 ] [ 0 ] . setText ( "-" )
			a [ 1 ] [ 1 ] . setText ( "-" )
		elif len ( bw . inflectionTimes ) == 2 :
			self . inflectTime1Label . setText ( str_three_decimals ( bw . inflectionTimes [ 0 \
] ) )  self . inflectTime2Label . setText ( str_three_decimals ( bw . inflectionTimes \
[ 1 ] ) )  for i in ( 0, 1 ) :
				self . inflDirValLabels   [ i ] [ 0 ] . setText ( str_three_decimals ( bw . \
inflectionDirs   [ i ] . x () ) )  self . inflDirValLabels   [ i ] [ 1 ] . setText ( \
str_three_decimals ( bw . inflectionDirs   [ i ] . y () ) )  self . \
inflAccelValLabels [ i ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionAccels \
[ i ] . x () ) )  self . inflAccelValLabels [ i ] [ 1 ] . setText ( \
str_three_decimals ( bw . inflectionAccels [ i ] . y () ) )  for ( v, x, y ) in ( ( \
                bw . timePoint, self . timePointXLabel, self . timePointYLabel ),
		                     ( bw . timeDir  , self . timeDirXLabel  , self . timeDirYLabel \
                ),
		                     ( bw . timeAccel, self . timeAccelXLabel, self . \
timeAccelYLabel ) ) :  x . setText ( str_three_decimals ( v . x () ) )
			y . setText ( str_three_decimals ( v . y () ) )

app = QApplication ( [] )
mainWindow = MainWindow ()
mainWindow . show ()
app . exec_ ()


["slider-minimal-test.tar.gz" (application/x-gzip)]

_______________________________________________
PyQt mailing list    PyQt@riverbankcomputing.com
http://www.riverbankcomputing.com/mailman/listinfo/pyqt

[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic