Welcome!

Sign in with your CogSci, Facebook, Google, or Twitter account.

Or register to create a new account.

We'll use your information only for signing in to this forum.

Supported by

Vertical Touchscreen Slider with Confirm Button

kezzo2002kezzo2002 Posts: 20

I realise there have been quite a lot of threads on the implementation of sliders & their use on touch screens. So sorry to add another! But I have read through these and had lots of trial and error - but as a beginner my lack of python understanding is hindering me from achieving my desired outcome. I am hoping someone can help...

What I am trying to achieve is a vertical slider that works on a touchscreen with an image at the top and bottom, that when participants press or slide their finger up and down the bar/line it fills (or a marker moves up and down - I'm not too fussy on the aesthetics). As with the other threads - I'd prefer it if this only occurs within the area of the slider not just anywhere on the screen. Then, when participants are happy with their response, I'd like there to be a confirm button for them to press that then logs their response. I'm pretty sure much of the code has been presented on the threads I've looked at - but for the life of me I can't piece it all together!

Thanks in advance for any guidance...

Comments

  • sebastiaansebastiaan Posts: 2,737

    Hi,

    The existing solutions are indeed not entirely satisfying. So let's see how you can do this more elegantly, by adding a slider widget to the forms. The following code will inject a slider into the form widgets, meaning that from then on you can use this slider just like any of the other widgets. Ideally, you would do this in the Prepare phase of an inline_script at the very start of the experiment.

    This particular variation of the slider simply registers a value between 0 and 1, and shows this as a vertically filled bar (empty = 0, filled = 1).

    from libopensesame.widgets._widget import widget
    from libopensesame import widgets
    
    
    class slider(widget):
    
        def __init__(self, form, var=None):
    
            widget.__init__(self, form)
            self._fill = .5
            self.var = var
            self.set_var(self._fill)
    
        def render(self):
    
            x, y, w, h = self.rect      
            self.draw_frame(rect=(x,y+h*(1-self._fill),w,h*self._fill))
    
        def on_mouse_click(self, pos):
    
            x, y, w, h = self.rect
            yclick = pos[1]
            dclick = yclick-y
            drect = h       
            self._fill = min(1, max(0, 1-dclick/drect))
            self.set_var(self._fill)
    
    
    widgets.slider = slider
    

    And then you can simply use the slider as a widget in a form_base, like below. This snippet will show a slider at the top and a button at the bottom to close the form. Not the most elegant of configurations, but it shows the general idea.

    widget 0 0 2 1 slider var=response
    widget 0 1 2 1 button
    

    Hope this helps!

    Cheers,
    Sebastiaan

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • kezzo2002kezzo2002 Posts: 20

    Hi Sebastiaan,

    Thank you for this. This looks like a much better solution moving forward - however when I paste the code above into the prepare section (or the run section) of an inline script at the beginning of my experiment, and then later on add a new form base with the code you suggest - I get a grey box above a button.... like it doesn't recognise that there is now a new slider widget...

  • sebastiaansebastiaan Posts: 2,737

    I get a grey box above a button.... like it doesn't recognise that there is now a new slider widget...

    Try clicking ont the grey box. :wink: The slider adjusts to the form geometry, so if you adjust the geometry such that the slider isn't a box but an elongated vertical rectangle, it will look more like a slider.

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • kezzo2002kezzo2002 Posts: 20

    Ah! I see.... Thank you! With a bit of tinkering to make it look pretty this will work a treat.... (I will post the code when I have done so in case of use to others!)

    One thing though... on a touchscreen this doesn't truly slide when you move your finger - only when you press in different places (presumably because its recognising clicks rather than drags/mouse-movement). The sliders on earlier discussion threads did this quite well (although they couldn't be added as a widget like this which I think is much better)...Is it possible to achieve this sliding movement - within the confines of the slider area on the form?

  • sebastiaansebastiaan Posts: 2,737

    .Is it possible to achieve this sliding movement - within the confines of the slider area on the form?

    Partly, yes. OpenSesame's mouse object doesn't understand mouse-up events, so to implement this you need to access the underlying libraries directly. Below is a slight extension of the above slider that does so but only for the back-ends that rely on pygame (right now all except psycho).

    from libopensesame.widgets._widget import widget
    from libopensesame import widgets
    from openexp._mouse.legacy import legacy        
    
    
    class slider(widget):
    
        def __init__(self, form, var=None):
    
            widget.__init__(self, form)
            self._fill = .5
            self.var = var
            self.set_var(self._fill)
            self._mouse = mouse()           
            self._use_pygame = isinstance(self._mouse, legacy)
    
        def render(self):
    
            x, y, w, h = self.rect      
            self.draw_frame(rect=(x,y+h*(1-self._fill),w,h*self._fill))
    
        def on_mouse_click(self, pos):
    
            return self._pygame(pos) if self._use_pygame else self._basic(pos)
    
        def _basic(self, pos):
    
            x, y, w, h = self.rect
            yclick = pos[1]
            dclick = yclick-y
            drect = h       
            self._fill = min(1, max(0, 1-dclick/drect))
            self.set_var(self._fill)
    
        def _pygame(self, pos):
    
            import pygame
    
            self._basic(pos)
            while True:
                for event in pygame.event.get():
                    if event.type == pygame.MOUSEBUTTONUP:
                        return
                    if event.type != pygame.MOUSEMOTION:
                        continue
                    pos = self._mouse.get_pos()[0]
                    self._basic(pos)
                    self.form.render()
    
    
    widgets.slider = slider
    
    Thanked by 1kezzo2002

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • kezzo2002kezzo2002 Posts: 20

    This works beautifully! Thanks ever so much. A final side note - if I wanted a frame/box round the slider area (so min and max were clearer) - is the best thing to add into the above code instructions to draw a box, or to add an image of a box to the same space on the custom form?

  • sebastiaansebastiaan Posts: 2,737
    edited July 11

    if I wanted a frame/box round the slider area (so min and max were clearer) - is the best thing to add into the above code instructions to draw a box, or to add an image of a box to the same space on the custom form?

    There are various ways to do this, but for maximum flexibility, you could customize slider.render() even further and directly draw to the form's canvas. For example like so:

        def render(self):
    
            x, y, w, h = self.rect                      
            # Slider
            self.form.canvas.rect(x,y+h*(1-self._fill),w,h*self._fill, fill=True,
                color='gray')
            # Outline
            self.form.canvas.rect(x, y, w, h, penwidth=4, color='white')
    
    Thanked by 1kezzo2002

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • kezzo2002kezzo2002 Posts: 20

    Hi @sebastiaan, this works great now. Just a couple of questions for further tweaks:

    1. How would I amend the code to have a horizontal slider if needed?

    2. Is it possible to amend the slider from being a block that fills to something like a line with a marker?
      For example - something like the Affective Slider (Betella et al., 2016)

  • sebastiaansebastiaan Posts: 2,737

    That's possible. But I think it's a useful (and doable) exercise to try this yourself! If you look at the render() function (where the drawing happens), you'll find all the ingredients you need.

    • self.rect is an x, y, w, h tuple that indicates the bounding box of the slider widget in pixels.
    • self._fill is a value between 0 and 1 that indicates how full the slider is.
    • self.form.canvas is a regular canvas object that you can draw on.

    You also need to modify the _basic() function so that it determines the fill based on the horizontal position of the click (if you want a horizontal slider).

        def _basic(self, pos):
    
            x, y, w, h = self.rect
            xclick = pos[0]
            dclick = xclick-x
            drect = w
            self._fill = min(1, max(0, dclick/drect))
            self.set_var(self._fill)
    

    Cheers!

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • kezzo2002kezzo2002 Posts: 20
    edited August 7

    Hi @sebastiaan, so after a lot of trial and error I'm really close - but having a couple of issues I can't work out how to sort. My Inline Script is as follows:

    from libopensesame.widgets._widget import widget
    from libopensesame import widgets
    from openexp._mouse.legacy import legacy        
    
    class slider(widget):
    
        def __init__(self, form, var=None):
    
            widget.__init__(self, form)
            self._fill = .5
            self.var = var
            self.set_var(self._fill)
            self._mouse = mouse()           
            self._use_pygame = isinstance(self._mouse, legacy)
    
        def render(self):
    
            x, y, w, h = self.rect      
    
            self.form.canvas.rect(x,y,w*self._fill,h,fill=True,
                color='gray')
            self.form.canvas.rect(x, y, w, h, penwidth=4, color='gray')
            self.form.canvas.circle(x*(1-self._fill),y+(h/2), h/2, penwidth=3, color='black')
        def on_mouse_click(self, pos):
    
            return self._pygame(pos) if self._use_pygame else self._basic(pos)
    
        def _basic(self, pos):
    
            x, y, w, h = self.rect
            xclick = pos[0]
            dclick = xclick-x
            drect = w       
            self._fill = min(1, max(0, dclick/drect))
            self.set_var(self._fill)
    
        def _pygame(self, pos):
    
            import pygame
    
            self._basic(pos)
            while True:
                for event in pygame.event.get():
                    if event.type == pygame.MOUSEBUTTONUP:
                        return
                    if event.type != pygame.MOUSEMOTION:
                        continue
                    pos = self._mouse.get_pos()[0]
                    self._basic(pos)
                    self.form.render()
    
    
    widgets.slider = slider
    

    and in my custom form I have:

    set timeout infinite
    set spacing 10
    set rows "3;1;1;3;1;1;1;1"
    set only_render no
    set margins "50;50;50;50"
    set cols "1;5;1"
    set _theme gray
    widget 0 0 3 1 label text="Mark on the slider below how <b>Happy</b> you are right now"
    widget 1 1 1 1 slider var=s1response
    widget 1 2 1 1 image path="AS_intensity_cue-2.png"
    widget 0 1 1 2 image path="AS_happy-2.png"
    widget 2 1 1 2 image path="AS_unhappy-2.png"
    widget 0 3 3 1 label text="Mark on the slider below how <b>Alert</b> you are right now"
    widget 1 4 1 1 slider var=s2response
    widget 1 5 1 1 image path="AS_intensity_cue-2.png"
    widget 0 4 1 2 image path="AS_sleepy-2.png"
    widget 2 4 1 2 image path="AS_wideawake-2.png"
    widget 0 7 3 1 button text="<b>Confirm</b>"`
    

    My current issues are:

    1. The circle marker doesn't move inline with the fill, it only seems to go halfway.
    2. The image "AS_intensity_cue-2.png" isn't the same size as the slider - can you resize images to fit according to the widget dimensions?
    3. I'd ideally like to replace the frame [self.form.canvas.rect(x, y, w, h, penwidth=4, color='gray')] with an image of a frame with rounded edges. I tried the code below - but I can't work out how to get it to be inline with the slider
    path = exp.pool[u'AS_track.png']
    self.form.canvas.image(path)
    

    Apologies for all the questions - really appreciate the help. Will make sure I post the completed code incase of use to others viewing the forum.

  • sebastiaansebastiaan Posts: 2,737

    The circle marker doesn't move inline with the fill, it only seems to go halfway.

    Yes, your trigonometry is off :wink: x*(1-self._fill) should be x+w*self._fill:

    self.form.canvas.circle(x+w*self._fill,y+(h/2), h/2, penwidth=3, color='white')
    

    The image "AS_intensity_cue-2.png" isn't the same size as the slider - can you resize images to fit according to the widget dimensions?

    That already happens. But the width/ height ratio is not changed, because that would distort the image. So you have to make sure that this ratio is correct in the original image, and then you will see that it will adjust to the size of the widget.

    I'd ideally like to replace the frame [self.form.canvas.rect(x, y, w, h, penwidth=4, color='gray')] with an image of a frame with rounded edges. I tried the code below - but I can't work out how to get it to be inline with the slider

    I wouldn't use an image for the outline. This will make it difficult to scale the size of the widget. Instead, I would use canvas.polygon() to draw a slightly fancier (but scalable) outline in the render() function:

    Cheers!

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • kezzo2002kezzo2002 Posts: 20

    Thanks @sebastiaan I will have a go with that. For a curve using canvas.polygon - does that mean I just need to put in lots and lots of points to get something resembling a circle?

  • sebastiaansebastiaan Posts: 2,737

    For a curve using canvas.polygon - does that mean I just need to put in lots and lots of points to get something resembling a circle?

    Exactly!

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • kezzo2002kezzo2002 Posts: 20

    @sebastiaan OK - so who thought drawing a curved line would be so hard - but its bamboozling me! Using some pythag to help me work out co-ordinates relative to the size of the widget I tried something like this:

    from cmath import sqrt
            r=(h/2)
            n0 = x,y 
            n1 = x-1,(y+sqrt((r**2)-(1**2)))
            n2 = x-2,(y+sqrt((r**2)-(2**2)))
            n3 = x-3,(y+sqrt((r**2)-(3**2)))
            ...
            n400 = x-400,(y+sqrt((r**2)-(400**2)))
            ....
            self.form.canvas.polygon([n0, n1, n2, n3, ...n400...])

    I only got so far in writing the remaining lines but nothing is showing up so far - and I'm probably barking up the wrong tree - is this even remotely close?

  • sebastiaansebastiaan Posts: 2,737

    Here's another way to draw a rounded rectangle. An easier way probably. Do you see the logic? The corners are drawn with four circles, and the body is a cross of two rectangles. The rounded rectangle is filled, but to draw only an outline, you can simply draw a slightly smaller rounded rectangle inside.

    def draw_rounded_rect(cnvs, x, y, w, h, color='gray'):
    
        r = min(w//4, h//4)
        cnvs.circle(x+r, y+r, r, color=color, fill=True)
        cnvs.circle(x+r, y+h-r, r, color=color, fill=True)
        cnvs.circle(x+w-r, y+r, r, color=color, fill=True)
        cnvs.circle(x+w-r, y+h-r, r, color=color, fill=True)
        cnvs.rect(x+r, y, w-2*r, h, color=color, fill=True)
        cnvs.rect(x, y+r, w, h-2*r, color=color, fill=True)
    
    
    c = canvas()
    draw_rounded_rect(c, -100, -100, 200, 300, color='white')
    draw_rounded_rect(c, -95, -95, 190, 290, color='black')
    c.show()
    

    There's much bigger issues in the world, I know. But I first have to take care of the world I know.
    cogsci.nl/smathot

  • kezzo2002kezzo2002 Posts: 20

    OK - that last bit went a bit beyond me - but incase this code is of use to others - this is what I used to implement the Affective Slider (Betella et al., 2016)...

    In Inline Script:

    `
    from libopensesame.widgets._widget import widget
    from libopensesame import widgets
    from openexp._mouse.legacy import legacy

    class slider(widget):

    def __init__(self, form, var=None):
    
        widget.__init__(self, form)
        self._fill = .5
        self.var = var
        self.set_var(self._fill)
        self._mouse = mouse()           
        self._use_pygame = isinstance(self._mouse, legacy)
    
    def render(self):
    
        x, y, w, h = self.rect      
    
        self.form.canvas.rect(x,y,w*self._fill,h,fill=True,
            color='white')
    
        self.form.canvas.rect(x-(h/2), y, w+(h+(h*.1)), h, penwidth=1, color='black')
    
        self.form.canvas.rect(x+w*(self._fill)-((h/2)+(h*.05)), y-(h*.05), h+(h*0.1), h+(h*0.1), penwidth=3, color='black')
    
    def on_mouse_click(self, pos):
    
        return self._pygame(pos) if self._use_pygame else self._basic(pos)
    
    def _basic(self, pos):
    
        x, y, w, h = self.rect
        xclick = pos[0]
        dclick = xclick-x
        drect = w       
        self._fill = min(1, max(0, dclick/drect))
        self.set_var(self._fill)
    
    def _pygame(self, pos):
    
        import pygame
    
        self._basic(pos)
        while True:
            for event in pygame.event.get():
                if event.type == pygame.MOUSEBUTTONUP:
                    return
                if event.type != pygame.MOUSEMOTION:
                    continue
                pos = self._mouse.get_pos()[0]
                self._basic(pos)
                self.form.render()
    

    widgets.slider = slider`

    In Custom Form

    set timeout infinite
    set spacing 10
    set rows "3;1;1;3;1;1;1;1"
    set only_render no
    set margins "50;50;50;50"
    set cols "1;5;1"
    set _theme gray
    widget 0 0 3 1 label text="Mark on the slider below how <b>Happy</b> you are right now"
    widget 1 1 1 1 slider var=s1response
    widget 1 2 1 1 image path="AS_intensity_cue-2.png"
    widget 0 1 1 2 image path="AS_happy-2.png"
    widget 2 1 1 2 image path="AS_unhappy-2.png"
    widget 0 3 3 1 label text="Mark on the slider below how <b>Alert</b> you are right now"
    widget 1 4 1 1 slider var=s2response
    widget 1 5 1 1 image path="AS_intensity_cue-2.png"
    widget 0 4 1 2 image path="AS_sleepy-2.png"
    widget 2 4 1 2 image path="AS_wideawake-2.png"
    widget 0 7 3 1 button text="<b>Confirm</b>"

    Seems to work well - Thanks @sebastiaan for all your help.

Sign In or Register to comment.