Python Kivy: bind a command inside popup executes command - python

I want to have a popup for confirming that the user really wants to quit the app. Now when I try to bind commands to the two buttons, I can only add the dismiss directly inside the function, not via a callback. That may be ok.
But I can only call my closing routine through a callback, not inside the function. When I bind quit_app() inside this function it gets directly executed when opening the popup. Why? It just should bind, not execute.
(Old script deleted.)
I have updated my script a bit and included a minimum kv file. It works basically (like previously) but looks a bit odd.
UI-Test.py:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
# Kivy imports:
import kivy
from kivy.app import App
from kivy.uix import popup
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty, ReferenceListProperty, ObjectProperty
from kivy.uix.tabbedpanel import TabbedPanel
VersionString = "DRAFT"
AppName = 'UI-Test'
def CloseProgram(Message, Level):
print('Closing, level {} ({})'.format(Level, Message))
sys.exit()
def OnClosing(self):
print('Closing...')
# ToDo: call popup
def init():
print('Starting {} Version {}.'.format(AppName, VersionString))
print('You are using Python version: {}'.format(sys.version))
class TestApp(App):
title = AppName + ' ' + VersionString
def on_pause(self):
return True
def quit_app(self,btn):
CloseProgram('Normal Closing', 'Debug')
class Pop(BoxLayout):
def __init__(self, **kwargs):
super(Pop, self).__init__(**kwargs)
self.up()
def callback(instance):
if instance.id == 'quit':
TestApp.quit_app(TestApp, 1)
def up(self):
print('popup')
qbutton = Button(text='Quit', id='quit')
abutton = Button(text='Return to Program', id='return')
blayout = BoxLayout()
blayout.add_widget(qbutton)
blayout.add_widget(abutton)
self.popup = kivy.uix.popup.Popup(title='Quit Program?', content=blayout, size_hint=(None, None), size=(400, 400))
abutton.bind(on_release=self.popup.dismiss)
qbutton.bind(on_release=TestApp.Pop.callback)
self.popup.open()
if __name__ == '__main__':
init()
TestApp().run()
Test.kv:
#:kivy 1.9
<Button>:
font_size: 15
# Main Layout:
BoxLayout:
orientation: 'vertical'
Button:
text: "Quit"
id: "quit_button"
size_hint: (0.1, None)
size: (150, 50)
on_release: app.Pop.up(self)

Question
How would you call this popup from a kv file? When in my version (see
updated script) Pop is not part of TestApp I can't access it from kv
file
Solution - with kv file
kv file
Add import statement, #:import Factory kivy.factory.Factory
Define class rule, <Pop>: and add widgets.
Register, instantiate, and open class Pop() using Factory.Pop().open()
Factory object
The factory can be used to automatically register any class or module
and instantiate classes from it anywhere in your project.
Python Code
Use App.get_running_app() to get an instance of the class TestApp()
Example - with kv file
main.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
# Kivy imports:
import kivy
kivy.require('1.11.0')
from kivy.app import App
from kivy.uix.popup import Popup
VersionString = "DRAFT"
AppName = 'UI-Test'
def CloseProgram(Message, Level):
print('Closing, level {} ({})'.format(Level, Message))
sys.exit()
def OnClosing(self):
print('Closing...')
# ToDo: call popup
def init():
print('Starting {} Version {}.'.format(AppName, VersionString))
print('You are using Python version: {}'.format(sys.version))
class Pop(Popup):
def callback(self, instance):
App.get_running_app().quit_app(instance)
class TestApp(App):
title = AppName + ' ' + VersionString
def on_pause(self):
return True
def quit_app(self, btn):
CloseProgram('Normal Closing', 'Debug')
if __name__ == '__main__':
init()
TestApp().run()
test.kv
#:kivy 1.11.0
#:import Factory kivy.factory.Factory
<Pop>:
title: 'Quit Program?'
size_hint: None, None
size: 400, 400
BoxLayout:
Button:
text: 'Quit'
on_release:
root.dismiss()
root.callback(self)
Button:
text: 'Return to Program'
on_release: root.dismiss()
<Button>:
font_size: 15
# Main Layout:
BoxLayout:
orientation: 'vertical'
Button:
text: "Quit"
id: "quit_button"
size_hint: (0.1, None)
size: (150, 50)
on_release: Factory.Pop().open()
Output - with kv file
Solution - without kv file
Bind all the buttons before calling Popup.open()
Use App.get_running_app() to get an instance of the class aempAPP
Snippet
def callback(self, instance):
print("\ncallback:")
self.popup.dismiss()
App.get_running_app().quit_app(1)
def up(self):
...
self.popup = Popup(title='Quit Program?', content=blayout, size_hint=(None, None), size=(400, 400))
abutton.bind(on_release=self.popup.dismiss)
qbutton.bind(on_release=self.callback)
self.popup.open()
...
class aempApp(App):
...
def quit_app(self, value):
print(value)
Example - without kv file
main.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.popup import Popup
from kivy.uix.button import Button
class Pop(BoxLayout):
def __init__(self, **kwargs):
super(Pop, self).__init__(**kwargs)
self.up()
def callback(self, instance):
print("\ncallback:")
self.popup.dismiss()
App.get_running_app().quit_app(1)
def up(self):
print('popup')
qbutton = Button(text='Quit', id='quit')
abutton = Button(text='Return to Program', id='return')
blayout = BoxLayout()
blayout.add_widget(qbutton)
blayout.add_widget(abutton)
self.popup = Popup(title='Quit Program?', content=blayout, size_hint=(None, None), size=(400, 400))
abutton.bind(on_release=self.popup.dismiss)
qbutton.bind(on_release=self.callback)
self.popup.open()
class TestApp(App):
def build(self):
return Pop()
def quit_app(self, value):
print("value=", value)
if __name__ == "__main__":
TestApp().run()
Output - without kv file

Related

Object has no attribute 'root'

I am following a tutorial https://www.youtube.com/watch?v=ceMVwnKCtOU and almost copied his code, but I´ve remade it a bit but I get this error:
File "C:\Users\Daniel\PycharmProjects\stopwatch_timer\main.py", line 15, in stopwatch
self.root.ids.counter.text = str(int(self.root.ids.counter.text) + 1)
AttributeError: 'Stopwatch' object has no attribute 'root'
My main file:
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget
class Stopwatch(BoxLayout):
def start_stopwatch(self):
Clock.schedule_interval(self.stopwatch, 1)
def stopwatch(self, *args):
self.root.ids.counter.text = str(int(self.root.ids.counter.text) + 1)
class TimerApp(App):
pass
TimerApp().run()
My kivy file:
Stopwatch:
<Stopwatch>:
Button:
text: "Start Stopwatch"
on_press: root.start_stopwatch()
Label:
id: counter
text: "0"
What am I doing wrong?
You said "almost copied" but code in video is different.
You have code in Stopwatch but video has in MainApp - and this makes all problem.
MainApp needs self.root to access elements in GridLayout but GridLayout would need only self to access elements in GridLayout
The same with your code: TimerApp would need self.root to access elements in Stopwatch but Stopwatch needs only self to access elements in Stopwatch
If you use self.ids instead of self.root.ids then it will work
class Stopwatch(BoxLayout):
def start_stopwatch(self):
Clock.schedule_interval(self.update_watch, 1)
def update_watch(self, *args):
self.ids.counter.text = str(int(self.ids.counter.text) + 1)
class TimerApp(App):
pass
BTW:
If you would move Clock.schedule_interval to TimerApp then it would need self.root.stopwatch instead of self.stopwatch to run Stopwatch.stopwatch
class Stopwatch(BoxLayout):
def update_watch(self, *args):
self.ids.counter.text = str(int(self.ids.counter.text) + 1)
class TimerApp(App):
def start_stopwatch(self):
Clock.schedule_interval(self.root.update_watch, 1)
and timer.kv would need app instead of root
on_press: app.start_stopwatch()
EDIT:
Full version with self.root
main.py
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget
class Stopwatch(BoxLayout):
def update_watch(self, *args):
self.ids.counter.text = str(int(self.ids.counter.text) + 1)
class TimerApp(App):
job = None # to keep access to running Clock
def start_stop_watch(self, *args):
if not self.job:
self.root.ids.counter.text = "0"
self.job = Clock.schedule_interval(self.root.update_watch, 1)
self.root.ids.button.text = "Stop"
else:
self.job.cancel() # stop clock
self.job = None # to use again in `if not self.job:`
self.root.ids.button.text = "Start"
TimerApp().run()
timer.kv
Stopwatch:
<Stopwatch>:
Button:
id: button
text: "Start"
on_press: app.start_stop_watch()
Label:
id: counter
text: "0"
Full version without self.root
main.py
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget
class Stopwatch(BoxLayout):
job = None
def start_stop_watch(self, *args):
if not self.job:
self.ids.counter.text = "0"
self.job = Clock.schedule_interval(self.update_watch, 1)
self.ids.button.text = "Stop"
else:
self.job.cancel()
self.job = None
self.ids.button.text = "Start"
def update_watch(self, *args):
self.ids.counter.text = str(int(self.ids.counter.text) + 1)
class TimerApp(App):
pass
TimerApp().run()
timer.kv
Stopwatch:
<Stopwatch>:
Button:
id: button
text: "Start"
on_press: root.start_stop_watch()
Label:
id: counter
text: "0"
Full version with all code in TimerApp
main.py
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget
class Stopwatch(BoxLayout):
pass
class TimerApp(App):
job = None
def start_stop_watch(self, *args):
if not self.job:
self.root.ids.counter.text = "0"
self.job = Clock.schedule_interval(self.update_watch, 1) # without `root`
self.root.ids.button.text = "Stop"
else:
self.job.cancel()
self.job = None
self.root.ids.button.text = "Start"
def update_watch(self, *args):
self.root.ids.counter.text = str(int(self.root.ids.counter.text) + 1)
TimerApp().run()
timer.kv
Stopwatch:
<Stopwatch>:
Button:
id: button
text: "Start"
on_press: app.start_stop_watch()
Label:
id: counter
text: "0"

How to clear the text field after performing??? KivyMD

After performing an operation, for example, receiving text and closing the window, when opening the window in the text field text.
It is necessary that after closing the mdDialog window, the text is erased. I manage to save it, but in the string method. It must be erased by pressing the button and closing.
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.boxlayout import BoxLayout
KV = '''
<Content>
tes:tes
orientation: 'vertical'
spacing: '12dp'
size_hint_y: None
height: '120dp'
MDTextField:
id:tes
on_text:app.inputs(self)
MDFloatLayout:
MDFloatingActionButton:
icon:'plus'
pos_hint:{'center_y': .1, 'center_x':.5}
on_release:app.new()
'''
class Content(BoxLayout):
pass
class General(MDApp):
dialog=None
def build(self):
return Builder.load_string(KV)
def inputs(self, event):
self.txt = event.text
print(self.txt)
def no(self, event):
event.text=''
self.dialog.dismiss()
def yes(self, event):
event.text=''
self.dialog.dismiss()
def new(self):
if not self.dialog:
self.dialog = MDDialog(
type='custom',
content_cls=Content(),
buttons = [
MDFlatButton(text='Отмена', text_color=self.theme_cls.primary_color, on_release=self.no),
MDFlatButton(text='Добавить',text_color=self.theme_cls.primary_color, on_press=self.yes),
]
)
self.dialog.open()
General().run()
Good day.
I recommend you create text_field_text = StringProperty("My Default String") in your App class and let the widget.text = app.text_field_text.
From there, you could clear your app string property with the callback function.

Kivy - How to access instance of class created by .kv file?

My class OrderManagementScreen is firstly instantiated by Builder.load_file and then by sm.add_widget(OrderManagementScreen(name='order_management')).
I would like to be able to access the instance created by Builder.load_file - how can I do this?
This post is the closest I could find, but when I do MDApp.get_running_app().root it returns None.
app.py: (mixture of attempts in my update_database function)
import atexit
from kivymd.app import MDApp
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen, ScreenManager
from order_management.constants import HELP_TEXT
from order_management.presentation.order_management.order_management_view import OrderManagementScreen
from order_management.presentation.components.dialog import Dialog
from order_management.data.query import Query
class App(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_file(
"order_management/presentation/main.kv")
self._query = Query()
something = MDApp.get_running_app()
self._dialog = Dialog()
def build(self):
self.theme_cls.primary_palette = "Green"
sm = ScreenManager()
# Instantiates OrderManagementScreen.py
sm.add_widget(OrderManagementScreen(name='order_management'))
return self.screen
def handle_menu_click(self):
self._dialog.render_dialog("Usage Guide:", HELP_TEXT, None, None)
def update_database(self):
# order_management_screen = MDApp.get_running_app().screen.children[0].manager.get_screen('order_management')
# order_management_screen = self.screen.get_screen('order_management')
self.order_management_screen.update_database()
if __name__ == "__main__":
App().run()
main_app = App()
atexit.register(main_app.update_database) # Calls update_database on exit.
main.kv:
#:kivy 1.11.0
#:include order_management/presentation/order_management/order_management_ui.kv
# Having to use v1.11.0 because of bug relating to MDDataTable
<Toolbar#AnchorLayout>:
AnchorLayout:
anchor_x: 'center'
anchor_y: 'top'
MDToolbar:
title: 'Order Management App'
specific_text_color: app.theme_cls.accent_color
right_action_items: [['help-circle', lambda x: app.handle_menu_click()]]
elevation: 9
ScreenManager:
OrderManagementScreen:
Toolbar:
order_management_ui.kv:
#:kivy 1.11.0
# Having to use v1.11.0 because of bug relating to MDDataTable
<OrderManagementScreen>:
name: 'order_management'
id: order_management
GridLayout:
cols: 1
pos_hint: {'top': 0.85}
ScrollView:
id: table_container
I guess it goes to show how sometimes you just need to step away from the computer and come back with fresh eyes...
My solution was to simply store the instance of app when run so that when I called my update_database function is was looking at the correct instance - and therefore could access the instance of a screen class the kivy had created with:
self.root.children[0].manager.get_screen('order_management')
I use children[0] here because this is the first screen I added to ScreenManager, and 'order_management' is the name of the screen obviously.
I hope that this by chance does help someone else.
New app.py:
import atexit
from kivymd.app import MDApp
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen, ScreenManager
from order_management.constants import HELP_TEXT
from order_management.presentation.order_management.order_management_view import OrderManagementScreen
from order_management.presentation.components.dialog import Dialog
from order_management.data.query import Query
class App(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_file(
"order_management/presentation/main.kv")
self._query = Query()
self._dialog = Dialog()
def build(self):
self.theme_cls.primary_palette = "Green"
sm = ScreenManager()
sm.add_widget(OrderManagementScreen(name='order_management'))
return self.screen
def handle_menu_click(self):
self._dialog.render_dialog("Usage Guide:", HELP_TEXT, None, None)
def update_database(self):
order_management_screen = self.root.children[0].manager.get_screen('order_management')
order_management_screen.update_database()
if __name__ == "__main__":
app = App()
app.run()
atexit.register(app.update_database) # Calls update_database on exit.

How can I change screen using bottom sheet in kivymd?

I have made a simple app in kivymd. But I can not change screen on click on button inside kivymd. Everything works great. But when I click on button then it popup toast also but screen is not changing. What will be changes or better implementation for this?
app.py
from kivymd.app import MDApp
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.lang import Builder
from main_screen_str import helper_string
from kivy.core.window import Window
from kivymd.toast import toast
from kivymd.uix.bottomsheet import MDGridBottomSheet
Window.size = (300, 500)
class MainScreen(Screen):
pass
class SettingsScreen(Screen):
pass
class AboutScreen(Screen):
pass
class MainApp(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sm = ScreenManager()
self.sm.add_widget(MainScreen(name="main_screen"))
self.sm.add_widget(SettingsScreen(name="settings_screen"))
self.sm.add_widget(AboutScreen(name="about_screen"))
self.main_str = Builder.load_string(helper_string)
def build(self):
screen = Screen()
screen.add_widget(self.main_str)
return screen
def callback_for_menu_items(self, *args):
if args[0] == 'Home':
toast(args[0])
self.sm.current = "main_screen"
if args[0] == 'Settings':
toast(args[0])
self.sm.current = "settings_screen"
if args[0] == 'About':
toast(args[0])
self.sm.current = "about_screen"
def show_example_grid_bottom_sheet(self):
self.bottom_sheet_menu = MDGridBottomSheet()
data = {
"Home": "home",
"Settings": "settings",
"About": "information-outline",
}
for item in data.items():
self.bottom_sheet_menu.add_item(
item[0],
lambda x, y=item[0]: self.callback_for_menu_items(y),
icon_src=item[1],
)
self.bottom_sheet_menu.open()
if __name__ == '__main__':
MainApp().run()
This builder string to create screen.
Are there any better solution for this?
builder string
helper_string = """
ScreenManager:
MainScreen:
SettingsScreen:
AboutScreen:
<MainScreen>:
name: 'main_screen'
MDIconButton:
icon: "menu"
theme_text_color: "Custom"
text_color: 1,0,0,1
on_press: app.show_example_grid_bottom_sheet()
<SettingsScreen>:
name: 'settings_screen'
<AboutScreen>:
name: 'about_screen'
"""
In your __init__() method of the App, you are building self.sm with the lines:
self.sm = ScreenManager()
self.sm.add_widget(MainScreen(name="main_screen"))
self.sm.add_widget(SettingsScreen(name="settings_screen"))
self.sm.add_widget(AboutScreen(name="about_screen"))
But self.sm is not used as part of your GUI. So your changes to self.sm has no effect on your GUI. The line following that:
self.main_str = Builder.load_string(helper_string)
basically does exactly the same thing as the previous lines.
Then in your build() method, you are creating a new Screen and adding the self.main_str as a child of that Screen.
While you can have a ScreenManager as a child of a Screen, in your posted example that does not seem to serve any purpose.
Here is a modified version of part of the MainApp that I think will do what you want:
class MainApp(MDApp):
# def __init__(self, **kwargs):
# super().__init__(**kwargs)
# self.sm = ScreenManager()
# self.sm.add_widget(MainScreen(name="main_screen"))
# self.sm.add_widget(SettingsScreen(name="settings_screen"))
# self.sm.add_widget(AboutScreen(name="about_screen"))
#
# self.main_str = Builder.load_string(helper_string)
def build(self):
self.sm = Builder.load_string(helper_string)
return self.sm
# screen = Screen()
# screen.add_widget(self.main_str)
# return screen
The above code greatly simplifies the build() method, eliminates the __init__() method, and now self.sm is actually part of the GUI.
Note that when you load a kv string that has a root node with Builder.load_string(), that root node is created and returned. The lines in your kv string:
ScreenManager:
MainScreen:
SettingsScreen:
AboutScreen:
result in a ScreenManager instance being created along with the three children listed for it, so the code in your __init__() method was duplicating that.

TypeError: super(type, obj) when using exec()

Below is the whole (test) app written with Kivy.
This is something like app preview application:
user enters a text of kv markup (see variable self.kv) and a text of classes (see variable self.text). Then he clicks the "preview" button and sees the result on the right side of the application.
Loading kv is implemented using kivy Builder.load_string(). Class loading is implemented using exec(, globals()).
The main problem is that for some reason I get the following error when I click on the preview button for the third time (the first 2 clicks work without errors):
TypeError: super(type, obj): obj must be an instance or subtype of type
The error can be because of exec(), (without exec I don’t get this error).
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
KV = '''
BoxLayout:
BoxLayout:
orientation: 'vertical'
CodeEd
id: code_editor
Button:
text: 'Preview'
on_release: app.preview()
Preview:
id: preview_area
<CodeEd#TextInput>
text: app.text
<Preview#RelativeLayout>
'''
class MyApp(App):
def build(self):
self._kv_filename = 'KvEditor_internal.' + str(self.uid)
self.text = '''
class MyButton(Button):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
print (333)
super(MyButton, self).on_touch_down(touch)
'''
self.kv = 'MyButton'
self.root = Builder.load_string(KV)
def preview(self):
preview_area = self.root.ids.preview_area
#if 'MyButton' in globals():
# del globals()['MyButton']
#print ('===================')
#print ([i for i in dict(globals())])
try:
exec(self.text, globals())
except:
print ('some error when exec class ')
Builder.unload_file(self._kv_filename)
try:
preview_area.add_widget(Builder.load_string(self.kv, filename=self._kv_filename))
except Exception as e:
print (e.message if getattr(e, r"message", None) else str(e))
MyApp().run()
How to solve this problem?
Question - Preview area is RelativeLayout
It seems to work, but could you edit or add an example, please? I do
not need the number of buttons in the preview area increased. I just
want every time after pressing "Preview", in the preview area I have
content that just reflects the current text of self.kv and self.text.
Example
The following example has most of the enhancements applied and the Preview area is a RelativeLayout.
main.py
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.properties import NumericProperty, StringProperty
from kivy.factory import Factory
KV = '''
BoxLayout:
BoxLayout:
orientation: 'vertical'
CodeEd
id: code_editor
Button:
text: 'Preview'
on_release: app.preview()
Preview:
id: preview_area
<CodeEd#TextInput>
text: app.text
<Preview#RelativeLayout>
'''
class MyApp(App):
text = StringProperty('')
previous_text = StringProperty('')
def build(self):
self._kv_filename = 'KvEditor_internal.' + str(self.uid)
self.text = '''
class MyButton(Button):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
print (333)
return super(Button, self).on_touch_down(touch)
'''
self.previous_text = self.text
self.kv = 'MyButton'
self.root = Builder.load_string(KV)
def preview(self):
preview_area = self.root.ids.preview_area
self.text = self.root.ids.code_editor.text
try:
# Class loading is implemented using exec(, globals())
exec(self.text, globals())
except:
print('some error when exec class ')
Builder.unload_file(self._kv_filename)
try:
# check for code changes
if self.text != self.previous_text:
Factory.unregister(self.kv)
Factory.register(self.kv, cls=globals()[self.kv])
total_children = len(preview_area.children)
preview_area.clear_widgets()
for child in range(total_children):
preview_area.add_widget(Builder.load_string(self.kv, filename=self._kv_filename))
self.previous_text = self.text
preview_area.add_widget(Builder.load_string(self.kv, filename=self._kv_filename))
except Exception as e:
print(e.message if getattr(e, r"message", None) else str(e))
MyApp().run()
Output
Question 1 - TypeError
TypeError: super(type, obj): obj must be an instance or subtype of
type
Solution - TypeError
There are two solutions to the TypeError.
Method 1
Replace super(MyButton, self).on_touch_down(touch) with return False
Method 2
Replace super(MyButton, self).on_touch_down(touch) with return super(Button, self).on_touch_down(touch)
Question 2 - Support code changes to MyButton class
What if MyButton class exists, but I want to make changes to this
class, for example change some of its methods, and so on?
Solution - Support code changes to MyButton class
In order to support code changes to MyButton class, the following enhancements are required:
Add import statement for Kivy Properties, from kivy.properties import NumericProperty, StringProperty
Add import statement for Kivy Factory object, from kivy.factory import Factory
Add a new class attribute, previous_text of type, StringProperty to keep track of code changes.
Initialize self.previous_text
Update self.text when method preview() is invoked.
Check for code changes
Unregister previous class MyButton using Kivy Factory
Register new class MyButton using Kivy Factory
Save the total number of MyButton added
Remove previously added MyButton using clear_widgets(). If previously added MyButtons are not removed, it will not have the functionality of the new features/code changes.
Use for loop to re-add previously added MyButton with the new functionality / features.
Assign new code changes, self.text to self.previous_text
Snippets
from kivy.properties import NumericProperty, StringProperty
from kivy.factory import Factory
from kivy.logger import Logger
...
class MyApp(App):
i = NumericProperty(0)
text = StringProperty('')
previous_text = StringProperty('')
def build(self):
...
self.text = '''
...
return True # consumed on_touch_down & don't propagate
# return False
return super(Button, self).on_touch_down(touch)
'''
self.previous_text = self.text
...
def preview(self):
preview_area = self.root.ids.preview_area
self.text = self.root.ids.code_editor.text
...
try:
# check for code changes
if self.text != self.previous_text:
Factory.unregister(self.kv)
Factory.register(self.kv, cls=globals()[self.kv])
total_children = len(preview_area.children)
preview_area.clear_widgets()
for child in range(total_children):
btn = Builder.load_string(self.kv, filename=self._kv_filename)
btn.text = str(child + 1)
preview_area.add_widget(btn)
self.previous_text = self.text
Example
The following example illustrates a code editor supporting code changes to MyButton class, and MyButton widgets are added into a GridLayout.
main.py
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import NumericProperty, StringProperty
from kivy.uix.button import Button
from kivy.logger import Logger
from kivy.factory import Factory
KV = '''
BoxLayout:
BoxLayout:
orientation: 'vertical'
CodeEd
id: code_editor
Button:
text: 'Preview'
on_release: app.preview()
Preview:
id: preview_area
<CodeEd#TextInput>:
text: app.text
<Preview#GridLayout>:
cols: 3
'''
class MyApp(App):
i = NumericProperty(0)
text = StringProperty('')
previous_text = StringProperty('')
def build(self):
self._kv_filename = 'KvEditor_internal.' + str(self.uid)
self.text = '''
class MyButton(Button):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
print(f"touch.pos={touch.pos}")
print(f"Button.text={self.text}")
return True # consumed on_touch_down & don't propagate
# return False
return super(Button, self).on_touch_down(touch)
'''
self.previous_text = self.text
self.kv = 'MyButton'
self.root = Builder.load_string(KV)
def preview(self):
preview_area = self.root.ids.preview_area
self.text = self.root.ids.code_editor.text
try:
# Class loading is implemented using exec(, globals())
exec(self.text, globals())
except Exception as msg:
print('\nException: some error when exec class ')
Logger.error("KivyApp: Exception: some error when exec class")
print(msg)
quit()
Builder.unload_file(self._kv_filename)
try:
# check for code changes
if self.text != self.previous_text:
Factory.unregister(self.kv)
Factory.register(self.kv, cls=globals()['MyButton'])
total_children = len(preview_area.children)
preview_area.clear_widgets()
for child in range(total_children):
btn = Builder.load_string(self.kv, filename=self._kv_filename)
btn.text = str(child + 1)
preview_area.add_widget(btn)
self.previous_text = self.text
self.i += 1
btn = Builder.load_string(self.kv, filename=self._kv_filename)
btn.text = str(self.i)
preview_area.add_widget(btn)
except Exception as e:
print(e.message if getattr(e, r"message", None) else str(e))
MyApp().run()
Output

Categories

Resources