[BUG]Slider Bar Click Behavior

I have a slider bar that works just fine if I grab the handle and slide it left/right. However, if I click somewhere on the slider that's not the handle the behavior is pretty much unpredictable. Is there a way to only allow the handle to be used?

image
image
Result:
image

This slider is min 0, max 100

1 Like

It's not intuitive, but if you turn snap to ticks off, the slider will increment to any nonzero minor tick spacing value when the user clicks off handle, so I imagine that setting the minor ticks spacing to 1 with snap to ticks off would minimize the problem you are seeing. For a 0 to 100 scale, I've found a minor tick setting of 5 to be useful for off handle incrementing.

Ya, kind of except that now clicking above/below the handle moves it up/down one of the tick amounts (not sure if it's minor or major).

The only way I can see to give the slider the behavior you want would be to remove the track listener, and script the movements yourself, but that gets somewhat complex. It's possible that creating a custom slider would be simpler.

To remove the listener, add this to the slider's propertyChange event handler:

# Runs once and only once at run time
# Won't run in the designer unless preview mode is active prior to the window being opened.
if event.propertyName == 'componentRunning':

	# Iterate through the various mouse listeners, and surgically remove the track listener
	for listener in event.source.mouseListeners:
		if 'SynthTrackListener' in listener.__class__.__name__:
			event.source.removeMouseListener(listener)

Moving the knob around and setting values will require quite a bit of scaling in multiple event handlers, so I would set up a library script for this, or add a custom method to the component like this one here:


#Custom method on the slider component with parameters: value, originalMin, originalMax, scaledMin, scaledMax 
#def getScaledValue(self, value, originalMin, originalMax, scaledMin, scaledMax):
	"""
	Arguments:
		self: A reference to the[...]
	"""
	originalRange = originalMax - originalMin
	
	# Cast one of the variables to float, or the rules of integer division will evaluate this as zero
	percentage = (value - originalMin)/float(originalRange)
	scaledRange = scaledMax - scaledMin
	
	# Cast the scaled value back to an int, and return it
	return int((percentage * scaledRange) + scaledMin)

Use the mousePressed event handler to determine whether or not the slider knob should be dragged. This can be conveyed to the mouseDragged event handler by adding a custom property to the component called "dragEnabled"

# Written for the mousePressed event handler
# Do not allow dragging if the component is disabled
if event.source.enabled:
	from java.awt import Rectangle
	knobRadius = 30
	if event.source.inverted and event.source.horizontal:
		valuePosition = event.source.getScaledValue(event.source.value, event.source.minimum, event.source.maximum, 0 , event.source.width)
		knobBounds = Rectangle(event.source.width - valuePosition - knobRadius, event.source.height / 2 - knobRadius, 2 * knobRadius, 2 * knobRadius)
	elif event.source.inverted and not event.source.horizontal:
		valuePosition = event.source.getScaledValue(event.source.value, event.source.minimum, event.source.maximum, 0 , event.source.height)
		knobBounds = Rectangle(event.source.width / 2 - knobRadius, valuePosition - knobRadius, 2 * knobRadius, 2 * knobRadius)
	elif event.source.horizontal:
		valuePosition = event.source.getScaledValue(event.source.value, event.source.minimum, event.source.maximum, 0 , event.source.width)
		knobBounds = Rectangle(valuePosition - knobRadius, event.source.height / 2 - knobRadius , 2 * knobRadius, 2 * knobRadius)
	else:
		valuePosition = event.source.getScaledValue(event.source.value, event.source.minimum, event.source.maximum, 0 , event.source.height)
		knobBounds = Rectangle(event.source.width / 2 - knobRadius, event.source.height - valuePosition - knobRadius, 2 * knobRadius, 2 * knobRadius)
	
	if knobBounds.contains(event.x, event.y):
		event.source.dragEnabled = True
		
	event.source.parent.getComponent('Paintable Canvas').leftClick = True

When the user drags the slider, and the dragEnabled custom property has been set during the mouse pressed event, control the knob position with this script:

# Written for the mouse dragged event handler
# Enabled from the mousePressed event handler
if event.source.dragEnabled:
	# There are four ways the slider can be configured that will change where the knob is positioned within the component in relationship to the slider value,
	# ...so they all have to be accounted for to ensure the calculation comes out correct
	if event.source.inverted and event.source.horizontal:
		newValue = event.source.getScaledValue(event.source.width - event.x, 0, event.source.width, event.source.minimum, event.source.maximum)
	elif event.source.inverted and not event.source.horizontal:
		newValue = event.source.getScaledValue(event.y, 0, event.source.height, event.source.minimum, event.source.maximum)
	elif event.source.horizontal: # horizontal and not inverted 
		newValue = event.source.getScaledValue(event.x, 0, event.source.width, event.source.minimum, event.source.maximum)
	else: # not inverted and not horizontal
		newValue = event.source.getScaledValue(event.source.height - event.y, 0, event.source.height, event.source.minimum, event.source.maximum)
	
	# Prevent the calculated value from exceding the bounds of the component
	if newValue < event.source.minimum:
		newValue = event.source.minimum
	elif newValue > event.source.maximum:
		newValue = event.source.maximum
	
	# When a the slider's value property changes, the knob automatically moves to the appropriate position
	event.source.value = newValue

Finally, when the user releases the mouse button, disable slider movements with the mouse released event handler:

# Written for the mouseReleased event handler,
event.source.dragEnabled = False

Result:
Presentation1

Edit: Note that this approach relies on the componentRunning event handler, so it won't work in the designer unless preview mode is already running when the window is opened.

1 Like

Nice job! Way more work than it's worth but it works :+1:

I'll probably just leave it be.

Maybe it would be simpler to, instead of removing the listener, use the mouse position on click to just move the handle to the mouse position?

1 Like

I've done custom sliders before using transforms on custom shapes, and from scratch using the paintable canvas, so I already knew the code. I believe I have a tutorial or two on this topic somewhere in the forum.

If that's all you want, you could overlay a transparent component, such as a label, and consume the mouse click with its mouseClicked event handler.

The script would look like this:

# This script is written for a slider overlay component's mouseClicked event handler
# It assumes the slider is horizontal and is NOT inverted

# Get the slider from the parent container
slider = event.source.parent.getComponent('Slider')

# Calculate the percentage of pixels represented by the mouse event from left to right
# Cast one of the variables to float, or the rules of integer division will evaluate this as zero
percentage = event.x/float(event.source.width)

# Get the range of possible slider values
sliderRange = slider.maximum - slider.minimum

# Apply the percentage to the range of slidervalues and calculate a potential newValue
# Cast this back to an integer, or it will error out when applied to the slider's value property
newValue = int((percentage * sliderRange) + slider.minimum)

# Check the validity of the new value
# ..to contain the applied value to within the min and max range of the slider
if newValue < slider.minimum:
	slider.value = slider.minimum
elif newValue > slider.maximum:
	slider.value = slider.maximum
else:
	slider.value = newValue	
1 Like

Would that work to still drag the handle?

Yes. Come to think of it, you could throw that into a library script and and call it from either the overlay component's mouseClicked or mouseDragged event handlers.

The library script would look like this:

def setSliderValue(slider, event):
	# This script is written for a slider overlay component's mouseClicked event handler
	# It assumes the slider is horizontal and is NOT inverted
	
	# Calculate the percentage of pixels represented by the mouse event from left to right
	# Cast one of the variables to float, or the rules of integer division will evaluate this as zero
	percentage = event.x/float(event.source.width)
	
	# Get the range of possible slider values
	sliderRange = slider.maximum - slider.minimum
	
	# Apply the percentage to the range of slidervalues and calculate a potential newValue
	# Cast this back to an integer, or it will error out when applied to the slider's value property
	newValue = int((percentage * sliderRange) + slider.minimum)
	
	# Check the validity of the new value
	# ..to contain the applied value to within the min and max range of the slider
	if newValue < slider.minimum:
		slider.value = slider.minimum
	elif newValue > slider.maximum:
		slider.value = slider.maximum
	else:
		slider.value = newValue	

...and you would call it from the mouseClicked and mouseDragged event handlers like this:

# Get the slider from the parent container
slider = event.source.parent.getComponent('Slider')

# set the slider value from the event location using library script
componentScripts.setSliderValue(slider, event)

Edit, added:
Result:
Presentation2

2 Likes

Still seems rather silly that it takes all this for something that's so simple and is intuitive on every other slider I've seen.

Thanks!

1 Like

The most direct solution would probably be to poke into the look and feel, which is what actually creates the mouse listeners involved, but it's still going to be quite messy. The real root of the problem is Java Swing establishing a weird concept for sliders in the first place.

1 Like

Just for completeness

def setSliderValue(slider, event):
	'''
		Script is written for a slider overlay component's mouseClicked event handler
		
		Args:
			slider	(obj)	:	Slider object
			event	(obj)	:	Event handler
			
		Returns:
			n/a
			*slider.value
	'''
	# Calculate the percentage of pixels represented by the mouse event from left to right or bottom to top
	# Cast one of the variables to float, or the rules of integer division will evaluate this as zero
	if slider.horizontal:
		percentage = event.x/float(event.source.width)
	else:
		percentage = (event.source.height - event.y)/float(event.source.height)
		
	#inverted slider, invert the percentage
	if slider.inverted:
		percentage = 1.0 - percentage
	
	# Get the range of possible slider values
	sliderRange = slider.maximum - slider.minimum
	
	# Apply the percentage to the range of slidervalues and calculate a potential newValue
	# Cast this back to an integer, or it will error out when applied to the slider's value property
	newValue = int((percentage * sliderRange) + slider.minimum)
	
	# Check the validity of the new value
	# ..to contain the applied value to within the min and max range of the slider
	if newValue < slider.minimum:
		slider.value = slider.minimum
	elif newValue > slider.maximum:
		slider.value = slider.maximum
	else:
		slider.value = newValue
1 Like

...and dangerous. I've damaged the gui in unexpected ways during more than one of my attempts at hacking the look and feel of a component.

1 Like

Yep. This thread really covers the advantages and disadvantages of Vision vs Perspective.
In Vision, you're very close to "the metal" - admittedly, very far away from the actual metal, but quite close to the underlying abstraction - Swing itself. An advanced Vision developer is more than halfway to being a decent Swing developer.

In Perspective, for safety and practicality, we've inverted that assumption - you're almost totally unable to pierce the "abstraction" at any layer, since your code isn't actually running on the frontend session and it's governed by the browser sandbox in any event. But the upside is your IT department has nothing to complain about, your "client" is always up to date, and you aren't subject to someone trying to be clever and breaking your GUI all the time :smile:

1 Like

I certainly wouldn't recommend doing any R&D work in a production environment. There's a reason why "advanced Vision developers" use a dev server; breaking the gui there doesn't hurt anything.

1 Like

Oh, for sure. I'm more alluding to the general FUD around "blah blah Java is insecure" that you hear a lot. No Vision clients -> no Java processes on client machines -> nothing for IT to complain about.

3 Likes