Kivy Overlaying Sliders - kivy

I'm wondering if anyone has coded up a kivy slider variant with two drag-bars to indicate an interval ala:
Qualtrics: Slider bar with two sliders knobs to indicate intervals
I'm thinking a possible implementation would be two sliders in a floatlayout (set on top of eachother), then only allowing selection of the slider if the actual button/drag-knob on the slider is selected, and then preventing crossover of the slide button (plus a little deadspace to avoid ) by preventing the knobs from colliding.
Implementation something like:
FloatLayout:
Slider:
pos:self.x, self.y
id:slider_1
on_value:print("SLIDER1 CHANGED")
Slider:
pos:self.x, self.y
id:slider_2
on_value:print("SLIDER2 CHANGED")
However on initial testing I am unable to select the underlying slider as any click on the area where they are both layed out only moves the knob of the top slider to the location that was clicked.
So questions:
How do allow kivy to only change the value of a slider if it is directly clicked on the drag-knob (and not anywhere on the widget)?
How do you enable kivy to check for collisions on all widgets (as opposed to just colliding the topmost layer?
Thanks.
edit:
Thanks to #John_Anderson 's answers I worked it out. Attached is the resultant code:
<PairedSliders>
FloatLayout:
CustomSlider:
id:slider_lower
pos:self.x,self.y
sensitivity:'handle'
value:20
CustomSlider:
id:slider_upper
pos:self.x,self.y
sensitivity:'handle'
value:30
<KivyDragger>:
BoxLayout:
PairedSliders:
And the app that will run it:
from kivy.app import App
from kivy.clock import Clock
from kivy.logger import Logger, LOG_LEVELS
from kivy.uix.slider import Slider
from kivy.uix.floatlayout import FloatLayout
class PairedSliders(FloatLayout):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.limit_deadband = 5
#Schedule initialization for limits set in .kv file.
Clock.schedule_once(lambda x:self.initialize())
def initialize(self):
#Allow .kv language to set value, and bandwidth limits.
upper_slider = self.ids['slider_upper']
lower_slider = self.ids['slider_lower']
#Double check that the initializations make sense.
if upper_slider.value < lower_slider.value+self.limit_deadband:
raise Exception(f'Use KV Language to set upper/lower value pairs past limit deadband. '+
f'Lower:{lower_slider.value}, Upper:{upper_slider.value}, Deadband:{self.limit_deadband}')
upper_slider.name = 'Upper'
upper_slider.deadband = self.limit_deadband
upper_slider.set_limits(lower_slider.value, upper_slider.max)
lower_slider.name = 'Lower'
lower_slider.deadband = self.limit_deadband
lower_slider.set_limits(lower_slider.min, upper_slider.value)
def on_touch_up(self, touch):
#See where we moved. Set limits on the opposing slider.
upper_slider = self.ids['slider_upper']
lower_slider = self.ids['slider_lower']
#Set the upper limit for the lower slider to the current value.
if upper_slider.marked:
upper_slider.marked = False
Logger.debug('Upper grabbed')
lower_slider.set_limits(lower_slider.min, upper_slider.value)
#Set the lower limit for the upper slider to the current value.
if lower_slider.marked:
lower_slider.marked = False
Logger.debug('lower grabbed')
upper_slider.set_limits(lower_slider.value, upper_slider.max)
class CustomSlider(Slider):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.limits = (self.min, self.max)
self.deadband=5
self.name = ''
self.grabbed = False
self.marked = False
def set_limits(self, lower, upper):
Logger.debug(f'{self.name} limit set:{lower}, {upper}')
self.limits = (lower, upper)
def on_touch_move(self, touch):
#If we're oob on limits, pin by ungrabbing.
if self.disabled or not self.collide_point(*touch.pos):
return
#Due to both sliders occupying the same space, we avoid movement if we're not flagged.
if not self.grabbed:
return
#Check to make sure our deadband doesn't lock out the max/min limits on the edges.
#Also do deadband/2 so you don't "lock up" by always being at the limit.
if self.limits[0] == self.min:
min_limit = self.min
else:
min_limit = self.limits[0]+self.deadband/2
if self.limits[1] == self.max:
max_limit = self.max
else:
max_limit = self.limits[1] - self.deadband/2
#Check for limits. Stop the motion by ungrabbing if we trigger.
if self.value < min_limit:
#Place in the deadband limit.
Logger.debug(f'{self.name} VAL:{self.value} OOB. MIN:{self.value} < {min_limit}')
self.value = self.limits[0]+self.deadband
touch.ungrab(self)
elif self.value > max_limit:
Logger.debug(f'{self.name} VAL:{self.value} OOB. MAX:{self.value} > {min_limit}')
#Place in the deadband limit.
self.value = self.limits[1]-self.deadband
touch.ungrab(self)
else:
return super().on_touch_move(touch)
def on_touch_down(self, touch):
if self.disabled or not self.collide_point(*touch.pos):
return
if touch.is_mouse_scrolling:
if 'down' in touch.button or 'left' in touch.button:
if self.step:
self.value = min(self.max, self.value + self.step)
else:
self.value = min(self.max,
self.value + (self.max - self.min)/20)
if 'up' in touch.button or 'right' in touch.button:
if self.step:
self.value = max(self.min, self.value - self.step)
else:
self.value = max(self.min,
self.value - (self.max - self.min)/20)
elif self.sensitivity == 'handle':
if self.children[0].collide_point(*touch.pos):
touch.grab(self)
self.grabbed = True
self.marked = True
else:
#Avoid touching the object if we haven't grabbed the handle itself.
return False
else:
touch.grab(self)
self.value_pos = touch.pos
return True
def on_touch_up(self, touch):
self.grabbed = False
return super().on_touch_up(touch)
class KivyDragger(FloatLayout):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class KivyDraggerApp(App):
def build(self):
return KivyDragger()
if __name__ == '__main__':
KivyDraggerApp().run()
Note that I still haven't figured out a way to access sub-members of class instantiations (ala the sliders ids) without first initializing everything, then scheduling an additional "initialize" function with the Clock.schedule_once() function, but it works aok for my purposes.

You can extend Slider to do what you want by a small adjustment to its on_touch_down() method:
class MySlider(Slider):
def on_touch_down(self, touch):
if self.disabled or not self.collide_point(*touch.pos):
return
if touch.is_mouse_scrolling:
if 'down' in touch.button or 'left' in touch.button:
if self.step:
self.value = min(self.max, self.value + self.step)
else:
self.value = min(
self.max,
self.value + (self.max - self.min) / 20)
if 'up' in touch.button or 'right' in touch.button:
if self.step:
self.value = max(self.min, self.value - self.step)
else:
self.value = max(
self.min,
self.value - (self.max - self.min) / 20)
elif self.sensitivity == 'handle':
if self.children[0].collide_point(*touch.pos):
touch.grab(self)
else:
# this is the modification
return False
else:
touch.grab(self)
self.value_pos = touch.pos
return True
And when you use it, add the sensitivity: 'handle' attribute, like this:
FloatLayout:
MySlider:
id:slider_1
value: 25
sensitivity: 'handle'
on_value:print("SLIDER1 CHANGED")
MySlider:
id:slider_2
value: 75
sensitivity: 'handle'
on_value:print("SLIDER2 CHANGED")
The sensitivity: 'handle' means that you can only adjust the Slider by clicking and dragging the handle.

Related

buttons do not appear with a recycle view

'''
Does not appear the buttons inside recycleview
'''
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.bx = RecycleBoxLayout(default_size=(None, dp(56)), default_size_hint=(1, None),
size_hint=(1, None), orientation='vertical',)
self.but = Button(text= 'hola')
self.bx.add_widget(self.but)
self.bx.bind(minimum_height=self.bx.setter("height"))
self.data = [{'text': str(x)} for x in range(100)]
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
The class RecycleView uses the attribute viewclass as data container, so you have to use self.viewclass = Button here.
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.bx = RecycleBoxLayout(
default_size=(None, dp(56)),
default_size_hint=(1, None),
size_hint=(1, None),
orientation='vertical',
)
self.bx.bind(minimum_height=self.bx.setter("height"))
self.add_widget(self.bx)
Clock.schedule_once(self.update_view)
def update_view(self, *args):
#Items that will be used as data-container.
self.viewclass = Button # Or, "Button"
self.data = [{'text': str(x)} for x in range(100)]
Also note that you've to schedule the data updation in order to get the view. Alternatively, you can define (almost) everything in kivy-lang without the need of scheduling. You can find an example in Kivy documentation.

Is there a way to move a kivy slider with button's and text box?

I have a project I would like to try and use kivy as a user interface.
The interface would have 1 slider 2 buttons and a text box. Basically the slider can be moved by sliding or finely incremented by the buttons. With a text box to read out the values but also able to manual enter a desired value.
I have googled around a bit and have not seen any examples of bonding all 3 inputs to 1 slider. I have only seen taking values from a slider and displaying it, I have no idea how to move a slider with a value.
main.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.lang.builder import Builder
class CustomSlider(BoxLayout):
def on_kv_post(self, base_widget):
super().on_kv_post(base_widget)
self.ids["button_less"].bind(on_press=self.on_button_less_click)
self.ids["button_more"].bind(on_press=self.on_button_more_click)
self.ids["text_input"].bind(text=self.on_text_input_changement)
self.ids["slider"].bind(on_touch_move=self.on_slider_move)
def on_slider_move(self, w, touch):
self.ids["slider"].value = 5 * round(self.ids["slider"].value / 5)
self.ids["text_input"].text = str(self.ids["slider"].value)
def on_button_less_click(self, w):
self.ids["text_input"].text = str(int(self.ids["text_input"].text) - 5)
def on_button_more_click(self, w):
self.ids["text_input"].text = str(int(self.ids["text_input"].text) + 5)
def on_text_input_changement(self, w, value):
self.ids["text_input"].text = self.get_value(value)
self.ids["slider"].value = int(self.ids["text_input"].text)
def get_value(self, value):
if not self.isInt(value):
return "0"
value = int(value)
if value < 0: return "0"
elif value > 100: return "100"
else: return str(value)
def isInt(self, value):
try:
int(value)
return True
except:
return False
class MyFirstKivyApp(App):
def build(self):
return Builder.load_file("main.kv")
MyFirstKivyApp().run()
main.kv
<CustomSlider>:
orientation: "vertical"
BoxLayout:
Slider:
id: slider
min: 0
max: 100
BoxLayout:
Button:
id: button_less
text: "-"
Button:
id: button_more
text: "+"
BoxLayout:
TextInput:
id: text_input
input_filter: "int"
text: "0"
CustomSlider:
you can try the following code
Kivy File:
Button:
id: button_more
text: "+"
on_release: root.on_button_more_click()
Python File inside the CustomSlider class:
def on_button_more_click(self):
self.ids.slider.value += 1

Kivymd Custom Input Dialog. problem with getting text

I am creating an Input Dialog using kivymd. Whenever I try to fetch the text from the text field, it doesn't output the text, rather it seems like the text is not there. (the dialog just pops up ok and the buttons are working fine).
part of the kivy code
<Content>
MDTextField:
id: pin
pos_hint: {"center_x": 0.5, "center_y": 0.5}
color_mode: 'custom'
line_color_focus: [0,0,1,1]
part of the python code
class Content(FloatLayout):
pass
class MenuScreen(Screen):
def __init__(self, **kwargs):
super(MenuScreen, self).__init__(**kwargs)
def show_confirmation_dialog(self):
# if not self.dialog:
self.dialog = MDDialog(
title="Enter Pin",
type="custom",
content_cls=Content(),
buttons=[
MDFlatButton(
text="cancel",on_release=self.callback
),
MDRaisedButton(
text="[b]ok[/b]",
on_release=self.ok,
markup=True,
),
],
size_hint_x=0.7,
auto_dismiss=False,
)
self.dialog.open()
def callback(self, *args):
self.dialog.dismiss()
def ok(self, *args):
pin = Content().ids.pin.text
if pin == "":
toast("enter pin")
else:
toast(f"pin is {pin}")
You can call the Content class to a variable and use it in the MDDialog.content_cls.
def show_custom_dialog(self):
content_cls = Content() # call the class to a variable
self.cdialog = MDDialog(content_cls=content_cls,
type='custom', title='Enter Pin'
)
self.cdialog.buttons = [
MDFlatButton(text='cancel',on_release=self.close_dialog),
MDRaisedButton(text='Ok',
on_release=lambda x:self.get_data(x,content_cls))
]
self.cdialog.open()
Then create a function that will get the event_button and the content class as arguments by use of lambda expression in the button as shown above.
def get_data(self,instance_btn, content_cls):
textfield = content_cls.ids.pin
# get input
value = textfield._get_text()
# do stufs here
toast(value)
At this point you can extract any id in the Content class.I hope this helps.
Refer below for full code
from kivmd.app import MDApp
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.dialog import MDDialog
from kivymd.uix.button import MDFlatButton, MDRaisedButtom
from kivy.lang.builder impor Builder
from kivy.toast import toast
kv = '''
MDBoxLayout:
orientation : 'vertical'
MDFlatbutton:
text : 'Dialog'
pos_hint : {'center_x':.5'}
on_release : app.show_custom_dialog()
<Content>:
MDTextField:
id : pin
pos_hint : {'center_x':.5,'center_y':.5}
'''
class Content(MDFloatLayout):
pass
class InputDialogApp(MDApp):
cdialog = None
def build(self):
return Builder.load_string(kv)
def show_custom_dialog(self):
content_cls = Content()
self.cdialog = MDDialog(title='Enter Pin',
content_cls=content_cls,
type='custom')
self.cdialog.buttons = [
MDFlatButton(text="Cancel",on_release=self.close_dialog),
MDRaisedButton(text="Ok",on_release=lambda x:self.get_data(x,content_cls))
]
self.cdialog.open()
def close_dialog(self, instance):
if self.cdialog:
self.cdialog.dismiss()
def get_data(self, instance_btn, content_cls):
textfield = content_cls.ids.pin
value = textfield._get_text()
# do stuffs here
toast(value)
self.close_dialog(instance_btn)
if __name__ == '__main__':
InputDialogApp().run()
I was also having this problem, and the comment by edwin helped a lot. However, one suggestion. instead of formatting it with buttons = [...] inside of MDDialog as opposed to after, like so self.cdialog = MDDialog(..., buttons = [...]) since otherwise the buttons dont show up but other than that it should work!

How to link screen to list items in a Kivymd list

I'm using ScreenManager. I would like to link the items in a OneLineListItem to new screen.
This is my KV file
screen_helper = """
ScreenManager:
GroupsScreen:
MyGroupScreen:
<GroupsScreen>:
name: 'groups'
ScrollView:
MDList:
id: container_groups
<MyGroupScreen>:
name: 'my_group'
ScrollView:
MDList:
id: container_group
MDRectangleFlatButton:
text: 'Back'
pos_hint: {'center_x':0.5,'center_y':0.1}
on_press: root.manager.current = 'groups'
and this is the python file
class GroupsScreen(Screen):
def on_enter(self, *args):
for i in range(5):
item = OneLineListItem(text='Gruppo ' + str(i))
self.ids.container_groups.add_widget(item)
class MyGroupScreen(Screen):
pass
sm = ScreenManager()
sm.add_widget(GroupsScreen(name='groups'))
sm.add_widget(MyGroupScreen(name='my_group'))
class DemoApp(MDApp):
def build(self):
self.theme_cls.primary_palette = "Red"
self.theme_cls.primary_hue = "500"
screen = Builder.load_string(screen_helper)
return screen
I would like to click on an item in the groups screen and go to MyGroupScreen.
The OneLineListItem class actually extends ButtonBehavior, so you can treat it like a Button. Just assign a on_press or on_release method to the OneLineListItem:
class GroupsScreen(Screen):
def on_kv_post(self, *args):
for i in range(5):
item = OneLineListItem(text='Gruppo ' + str(i))
item.on_release = self.switch_to_my_groups
self.ids.container_groups.add_widget(item)
def switch_to_my_groups(self, *args):
self.manager.current = 'my_group'
I changed your on_enter() method to a on_kv_post() to be sure that the kv rules have been executed (so that the ids are available).
Another problem is the lines:
sm = ScreenManager()
sm.add_widget(GroupsScreen(name='groups'))
sm.add_widget(MyGroupScreen(name='my_group'))
These lines should be removed. They attempt build the GUI, and then the result is not used. The line:
screen = Builder.load_string(screen_helper)
then rebuilds the GUI again, and this time it is actually used.

How do i create a countdown timer in ModalView after pressing a button?

I am trying to create a modalview with a timer in it. Upon pressing the "begin" button, a modal view should appear and the countdown should start. However, I am getting an valueerror message. ValueError: TimerView.timer has an invalid format (got main.TimerView object at 0x0000017AD40D6180>>>).May I know which part of the code is wrong? Thanks in advance.
from kivymd.app import MDApp
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition, WipeTransition, NoTransition, SlideTransition
from kivymd.theming import ThemeManager
from kivy.properties import StringProperty, NumericProperty
from kivy.uix.modalview import ModalView
from kivymd.uix.label import MDLabel
from kivy.clock import Clock
class TimerView(ModalView):
number = NumericProperty(15)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.background = "transparent_image.png"
self.background_color = [1/3,1/3,1/3,0.8]
self.auto_dismiss = False
self.size_hint = (None,None)
self.size = (150,50)
timer_countdown = MDLabel(font_style = 'H1', theme_text_color = 'Primary',
text = str(self.number), halign = 'center',
text_color = (1,1,1,1), size_hint_y = None)
self.add_widget(timer_countdown)
def decrement_time(self, dt):
self.number -= 1
def start(self,*args):
self.timer = Clock.schedule_interval(self.decrement_time, 1)
def stop(self):
Clock.unschedule(self.timer)
class MainScreen(Screen):
pass
class BeginScreen(Screen):
pass
class MyScreenManager(ScreenManager):
def __init__(self,**kwargs):
super().__init__(**kwargs)
self.view = TimerView()
def open_view(self):
self.view.bind(on_open=self.view.start)
self.view.open()
main_widget_kv = ('''
#: import ScrollEffect kivy.effects.scroll.ScrollEffect
MyScreenManager:
BeginScreen:
<BeginScreen>:
begin_button:begin_button
name: "begin"
canvas.before:
Color:
rgb: .1, .1, .1
FloatLayout:
id: begin_layout
Button:
id: begin_button
text: 'Begin'
font_size: 24
on_press: app.root.open_view()
size_hint: (.4,.25)
pos_hint: {"center_x":.5, "center_y":.2}
color: [0,0,0,1]
''')
class TestApp(MDApp):
def __init__(self,**kwargs):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Red"
super().__init__(**kwargs)
def build(self):
main_widget = Builder.load_string(main_widget_kv)
return main_widget
if __name__ == '__main__':
TestApp().run()
When you set the text of a Label in python code, it uses the value at that time, and will not change automatically. If you do the same thing in kv, it will update automatically (provided that the text references a Property). So just changing self.number has no effect on your timer_countdown Label.
So, you need to update that text explicitly. Here is a modified version of your code that does that:
class TimerView(ModalView):
number = NumericProperty(15)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.background = "transparent_image.png"
self.background_color = [1/3,1/3,1/3,0.8]
self.auto_dismiss = False
self.size_hint = (None,None)
self.size = (150,50)
self.timer_countdown = MDLabel(font_style = 'H1', theme_text_color = 'Primary',
text = str(self.number), halign = 'center',
text_color = (1,1,1,1), size_hint_y = None)
self.add_widget(self.timer_countdown)
def decrement_time(self, dt):
self.number -= 1
# self.timer_countdown.text = str(self.number)
def on_number(self, instance, value):
self.timer_countdown.text = str(value)
def start(self,*args):
self.t = Clock.schedule_interval(self.decrement_time, 1)
def stop(self):
Clock.unschedule(self.t)
A reference to the MDLabel is kept in self.timer_countdown and the on_number() method gets executed whenever number changes, and just updates the MDLabel. Note that you can also do the update by just uncommenting the line:
# self.timer_countdown.text = str(self.number)
In that case, number does not need to be a Property, and the on_number() method is not needed.

Resources