'''
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.
Related
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.
I want to pass to my project_selected function the selected item in my MDDialog after pressing the OK button. But I can't figure out how can I get the value and do this.
I could print the value inside my set_icon funtion in ItemConfirm class, but I don't know whats the better way to pass that value to HomeWindow(Screen) class or if it is possible to call it directly from inside of it once that the ItemConfirm is already called from inside a function that is part of HomeWindow class.
main.py
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen, ScreenManager
from kivymd.app import MDApp
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.list import OneLineAvatarIconListItem
class ItemConfirm(OneLineAvatarIconListItem):
divider = None
def set_icon(self, instance_check):
print(self.text)
instance_check.active = True
check_list = instance_check.get_widgets(instance_check.group)
for check in check_list:
if check != instance_check:
check.active = False
class HomeWindow(Screen):
dialog = None
def show_confirmation_dialog(self):
projects = [{id: 0, name: "example1"},{id: 1, name: "example2"}]
if not self.dialog:
self.dialog = MDDialog(
title="Projects",
type="confirmation",
auto_dismiss=False,
items=[ItemConfirm(text=f"{project['name']}") for project in projects],
buttons=[
MDFlatButton(text="CANCEL", theme_text_color="Custom"),
MDFlatButton(
text="OK",
theme_text_color="Custom",
on_release=self.project_selected,
),
],
)
self.dialog.open()
def project_selected(self, *args, **kwargs):
self.ids.project_selection.text = self.dialog.text
class WindowManager(ScreenManager):
pass
class RlogTimer(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "BlueGray"
self.root = Builder.load_file("templates/main.kv")
return self.root
if __name__ == "__main__":
RlogTimer().run()
templates/main.kv
#: import NoTransition kivy.uix.screenmanager.NoTransition
WindowManager:
HomeWindow:
<ScreenManager>:
transition: NoTransition()
<ItemConfirm>:
on_release: root.set_icon(check)
CheckboxLeftWidget:
id: check
group: "check"
<HomeWindow>:
name: "home"
MDBoxLayout:
orientation: "vertical"
MDToolbar:
id: title
title: "Redmine logTimer"
right_action_items: [["clock", lambda x: app.callback_2()]]
MDFlatButton:
id: project_selection
text: "Select Project"
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: root.show_confirmation_dialog()
You can achieve that as follows,
First create a prop. say, selected_project in HomeWindow as,
class HomeWindow(Screen):
dialog = None
selected_project = StringProperty()
Now set its value in method set_icon as,
def set_icon(self, instance_check):
...
app = MDApp.get_running_app() # Access the running app instance.
home_screen = app.root.get_screen("home") # Access required screen.
home_screen.selected_project = self.text # Now set value.
...
Now it's time to set this value in method project_selected,
def project_selected(self, *args, **kwargs):
self.ids.project_selection.text = self.selected_project
I am just learning OOP and PySide. I have created a code as below.
The application doesn't do anything much (it's a development project in learning stages).
import numpy as np
import sys
from qtpy.QtWidgets import (
QWidget,
QMainWindow,
QVBoxLayout,
QAction,
QMenu,
QLabel,
QApplication,
QMessageBox,
QDesktopWidget,
)
from qtpy.QtCore import Qt, Slot, QPoint, QObject
from qwt import (
QwtPlot,
QwtPlotMarker,
QwtPlotGrid,
QwtLegend,
QwtPlotCurve,
QwtLegendData,
)
class contexMenuHelper(QObject):
def __init__(self, plot, legend, legendItem):
super(contexMenuHelper, self).__init__()
self.plot = plot
self.legend = legend
self.legendItem = legendItem
#Slot(QPoint)
def contextMenuSlot(self, pos):
context = QMenu(self.legendItem)
context.addAction(QAction("Delete", self))
context.exec_(self.legendItem.mapToGlobal(pos))
class Plot(QwtPlot, QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setAxisTitle(QwtPlot.xBottom, "X-axis")
self.setAxisTitle(QwtPlot.yLeft, "Y-axis")
self.setCanvasBackground(Qt.white)
self.setAxisScale(QwtPlot.yLeft, -2, 2)
QwtPlotGrid.make(self, color=Qt.lightGray, width=0, style=Qt.DotLine)
legend = QwtLegend()
legend.setDefaultItemMode(QwtLegendData.Checkable)
self.insertLegend(legend, QwtPlot.RightLegend)
x = np.arange(-5.0, 5.0, 0.1)
curves = []
curves.append(
QwtPlotCurve.make(
x, np.cos(x), "Cosinus", self, linecolor="red", antialiased=True
)
)
curves.append(
QwtPlotCurve.make(
x, np.sin(x), "Sinus", self, linecolor="blue", antialiased=True
)
)
self.helpers = dict()
for a in curves:
legend.legendWidget(a).setContextMenuPolicy(Qt.CustomContextMenu)
h = contexMenuHelper(self, legend, legend.legendWidget(a))
self.helpers[a] = h
legend.legendWidget(a).customContextMenuRequested.connect(h.contextMenuSlot)
QwtPlotMarker.make(
align=Qt.AlignRight | Qt.AlignTop,
linestyle=QwtPlotMarker.HLine,
color="black",
plot=self,
)
for keys, value in self.helpers.items():
print(keys)
print(value)
# insert a vertical marker at x = 0
QwtPlotMarker.make(
align=Qt.AlignRight | Qt.AlignTop,
linestyle=QwtPlotMarker.VLine,
color="black",
plot=self,
)
legend.checked.connect(self.showCurve)
self.replot()
#Slot(object, bool, int)
def showCurve(self, obj, condition, num):
obj.setVisible(not condition)
self.replot()
#Slot(object, bool, int)
def __del__(self, obj, condition):
print('Destructor called, vehicle deleted.')
class SimplePlot(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout()
self.setLayout(layout)
plot = Plot()
plot.setTitle("Trigonometric")
self.setWindowTitle("Trigonometric")
layout.addWidget(plot)
label = QLabel("Press the legend to en/disable a curve")
layout.addWidget(label)
self.center()
def center(self):
qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def closeEvent(self, event):
reply = QMessageBox.question(
self,
"Message",
"Are you sure to quit?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
if reply == QMessageBox.Yes:
event.accept()
else:
event.ignore()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SimplePlot()
window.show()
window.resize(800, 600)
sys.exit(app.exec_())
I made the active legend and the context menu:
I want to make it so that when I select "Delete" from the context menu, the corresponding function waveform in the graph and the corresponding object in the legend will be deleted.
I have implemented it as follows. Perhaps someone will find my thinking useful. It works correctly and as I expected although there is a tiny error in the operation itself .
Do you see what error I mean?
class contexMenuHelper(QObject):
def __init__(self, plot, legend, legendItem):
super(contexMenuHelper, self).__init__()
self.plot = plot
self.legend = legend
self.legendItem = legendItem
self.emlSel = QAction("Delete")
#Slot(QPoint)
def contextMenuSlot(self, pos):
context = QMenu(self.legendItem)
context.addAction(self.emlSel)
context.exec_(self.legendItem.mapToGlobal(pos))
self.emlSel.triggered.connect(self.destroy())
#Slot()
def destroy(self):
QwtPlotCurve.detach(self.legend)
class Plot(QwtPlot, QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setAxisTitle(QwtPlot.xBottom, "X-axis")
self.setAxisTitle(QwtPlot.yLeft, "Y-axis")
self.setCanvasBackground(Qt.white)
self.setAxisScale(QwtPlot.yLeft, -2, 2)
QwtPlotGrid.make(self, color=Qt.lightGray, width=0, style=Qt.DotLine)
legend = QwtLegend()
legend.setDefaultItemMode(QwtLegendData.Checkable)
legend.resize(100,100)
self.insertLegend(legend, QwtPlot.RightLegend)
x = np.arange(-5.0, 5.0, 0.1)
curves = []
curves.append(
QwtPlotCurve.make(
x, np.cos(x), "Cosinus", self, linecolor="red", antialiased=True
)
)
curves.append(
QwtPlotCurve.make(
x, np.sin(x), "Sinus", self, linecolor="blue", antialiased=True
)
)
self.helpers = dict()
for a in curves:
legend.legendWidget(a).setContextMenuPolicy(Qt.CustomContextMenu)
h = contexMenuHelper(self, a, legend.legendWidget(a))
self.helpers[a] = h
legend.legendWidget(a).customContextMenuRequested.connect(h.contextMenuSlot)
QwtPlotMarker.make(
align=Qt.AlignRight | Qt.AlignTop,
linestyle=QwtPlotMarker.HLine,
color="black",
plot=self,
)
QwtPlotMarker.make(
align=Qt.AlignRight | Qt.AlignTop,
linestyle=QwtPlotMarker.VLine,
color="black",
plot=self,
)
legend.checked.connect(self.showCurve)
self.replot()
#Slot(object, bool, int)
def showCurve(self, obj, condition, num):
obj.setVisible(not condition)
self.replot()
class SimplePlot(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout()
self.setLayout(layout)
plot = Plot()
plot.setTitle("Trigonometric")
self.setWindowTitle("Trigonometric")
layout.addWidget(plot)
label = QLabel("Press the legend to en/disable a curve")
layout.addWidget(label)
self.center()
def center(self):
qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def closeEvent(self, event):
reply = QMessageBox.question(
self,
"Message",
"Are you sure to quit?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
if reply == QMessageBox.Yes:
event.accept()
else:
event.ignore()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SimplePlot()
window.show()
window.resize(850, 600)
sys.exit(app.exec_())
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!
I've put a loop into thread and I want my label to show the given thext. The issue is that it doesn't show most of texts I see in the terminal. It shows only:
>Text 0
>nothing
>Text 4
>Text 5
>nothing
>text 14
>text 15
>etc.
I've no idea why is that. I tried to use Clock.schedule_once to call prompt function but the resoult is the same.
CODE
from kivy.uix.boxlayout import BoxLayout
import threading
from kivy.app import App
from kivy.lang.builder import Builder
from time import sleep
kv = '''
<MainWindow>
Button:
text: 'Go!'
on_press: root.go()
Label:
id: label
MainWindow:
'''
class MainWindow(BoxLayout):
def __init__(self):
super(MainWindow, self).__init__()
def go(self):
thread = Prompter()
thread.start()
def prompt(self, text):
self.ids.label.text = text
print(self.ids.label.text)
class MyThread(threading.Thread):
def __init__(self):
super(MyThread, self).__init__()
def run(self):
for i in range(100):
text = 'TEXT {}'.format(i)
App.get_running_app().root.prompt(text)
sleep(0.5)
class Setup(App):
def build(self):
return Builder.load_string(kv)
if __name__ == '__main__':
Setup().run()
Try adding the #mainthread decorator to the prompt() method. Changes to the GUI must be done on the min thread.