How to prevent memory leaks when dismissing Kivy ModalView instance? - python

In my app, I create instances of ModalView that contain child widgets with callbacks that are bound to widget properties or scheduled with Clock. Here is an example code to demonstrate this. I find that dismiss() method of the ModalView instance leaves intact the callback bindings and the Clock scheduled callbacks of its child widgets. I have to take care of unbinding and unscheduling these myself. This can get messy when I bind to callbacks that take args (I then have to use fbind and funbind_uid methods whilst keeping track of uids). Similarly, Clock scheduled callbacks that take args are tricky to unschedule since they are anonymous then, having been scheduled either using a lambda or a partial.
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.uix.modalview import ModalView
from kivy.uix.label import Label
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivy.properties import ObjectProperty
from kivy.clock import Clock
import datetime
Builder.load_string('''
#: kivy 1.9.2
<MainWidget>:
Button:
text: 'Push the button (pu-push the button)'
on_press:
root.showtime()
''')
class MyWidget(FloatLayout):
text=StringProperty() # this can be bound to 'text' property of a child widget for observation
timenow=ObjectProperty()
def __init__(self, **kwargs):
super(FloatLayout, self).__init__(**kwargs)
self.bind(timenow=self.update_text)
Clock.schedule_interval(self.poll_datetime, .5)
def poll_datetime(self, dt):
self.timenow = datetime.datetime.now()
print "polling datetime"
def update_text(self, *args):
self.text=self.timenow.strftime("%Y-%m-%d %H:%M:%S")
print "updating text"
def cleanup(self, *args):
self.unbind(timenow=self.update_text)
Clock.unschedule(self.poll_datetime)
print "cleaning up"
class MainWidget(FloatLayout):
def __init__(self, **kwargs):
super(MainWidget, self).__init__(**kwargs)
def showtime(self):
overlay = ModalView()
container=MyWidget()
timelabel=Label()
container.bind(text=timelabel.setter('text'))
container.bind(pos=timelabel.setter('pos'))
container.add_widget(timelabel)
cancelbutton=Button(text='Cancel', size_hint=(None, None))
cancelbutton.bind(on_press=container.cleanup)
cancelbutton.bind(on_press=overlay.dismiss)
container.add_widget(cancelbutton)
overlay.add_widget(container)
overlay.open()
class MyApp(App):
def build(self):
mw=MainWidget()
return mw
if __name__ == '__main__':
MyApp().run()
Am I doing this right? Does ModalView's dismiss() method leave other objects behind that I am not even aware of? What is a good way to detect such objects being left behind? Is there a way to ensure complete destruction of child widgets of ModalView instance upon dismiss() is called?

I think you are doing it right. When the ModalView is dismissed, its reference in the parent window is removed, and if no other references to it are held anywhere, it will be garbage collected, and any references that it holds will also be garbage collected. However, the Clock.schedule_interval() is holding a reference to the ModalView, so it does not get garbage collected. This is the correct behavior, as your call to schedule events means that you want those scheduled events to continue until they are cancelled.
An easier way to cancel the scheduled events is to use:
self.sched = Clock.schedule_interval(self.poll_datetime, .5)
in the __init__() method of MyWidget. Then in the cleanup() method use:
self.sched.cancel()
You don't need to unbind the timenow binding as that will disappear with the garbage collection that will happen after the cancel.

Related

Cannot disable buttons in Kivy (Python)

I have problem with disabling of buttons in kivy library. When I disable button, it simply not disable. It waits in some strange way.
Let me show you my code:
import kivy
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
import time
class MainApp(App):
def build(self):
self.l = FloatLayout()
b = Button(text="BUTTON", pos_hint={"top":0.8, "right": 0.8}, size_hint=(0.6, 0.6))
b.bind(on_press=self.press)
self.l.add_widget(b)
return self.l
def press(self, btn):
btn.disabled = True
time.sleep(3.0)
btn.disabled = False
app = MainApp()
app.run()
When I press button, I want to disable it for 3 sec. But instead of it program "freeze" (without disabling of button), and then after 3 secs do animation of press (button blinks with blue color). Of cource program must "freeze" because of time.sleep(3.0), but after disabling of button (Which must be gray, but it dont change color...)
How to solve it? If I put there instead time.sleep() something like for cycle (with about 10 milions of cycle) to imitate of "doing something" by program, it behaves in the same way...
So how I can solve it? How to disable button in kivy, then do something and after it is done enable button again?
Thanks!
EDIT: My problem isn't, that program freezes for 3 seconds. I understand that calling time.sleep() is blocking. What I don't understand is why button is not disabled before (and during) sleep...
The time.sleep is blocking the code. Instead you need to use Clock to enable the button after 3 seconds. Below is the corrected code to achieve your target:
import kivy
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.clock import Clock
from functools import partial
class MainApp(App):
def build(self):
self.l = FloatLayout()
b = Button(text="BUTTON", pos_hint={"top":0.8, "right": 0.8}, size_hint=(0.6, 0.6))
b.bind(on_press=self.press)
self.l.add_widget(b)
return self.l
def press(self, btn):
btn.disabled = True
Clock.schedule_once(partial(self.btn_enable, btn), 3)
def btn_enable(self, btn, *args):
btn.disabled = False
app = MainApp()
app.run()
TL; DR
The animation happens after the press function is called. This means that you freeze the program when doing time.sleep.
What to do about it?
Instead, you need to do something non-blocking, meaning that it runs in three seconds, but it doesn't cause the program to freeze. Something that would probably work is to utilize threads (something similar to the example, but dealing with sending variables across threads).
Example
Here is an example for your code that does not work, so you can understand the gist of it. Most likely, you are going have to deal with passing variables across threads:
import kivy
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
# import time
import threading
class MainApp(App):
def build(self):
self.l = FloatLayout()
b = Button(text="BUTTON", pos_hint={"top":0.8, "right": 0.8}, size_hint=(0.6, 0.6))
b.bind(on_press=self.press)
self.l.add_widget(b)
return self.l
def press(self, btn):
btn.disabled = True
# time.sleep(3.0)
threading.Timer(3.0, lambda: btn.disabled = False).start()
app = MainApp()
app.run()
This was inspired by this answer.

".cancel()" function of Kivy.Clock doesn't work?

I have currently a problem regarding the "Clock" library of the framework
"Kivy". I am creating a Clock-Thread and want to terminate/cancel it but it does not seem to work.
I already read the documentation of Kivy.Clock function and did the same like them.
The link to the Kivy.Clock function
My Code:
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout
from kivy.core.window import Window
class MainWindow(BoxLayout):
def __init__(self):
super(MainWindow, self).__init__()
Window.size = (1280, 720)
ev1 = Clock.schedule_interval(self.print_func, 1)
ev1.cancel()
def print_func(self, dt = None):
print ("Test")
Imagine that a .kv file exist and that the MainWindow class is getting called. Everything works fine, except of the canceling of the Clock interval.
Expected result should be that the Clock-Thread should be terminated or rather set to inactive.
Actual result is that it has no effect and the Clock-Thread is still running.
EDIT: I got this function of Clock to work..
I found out that the .cancel() and .unschedule() functions for Clock.schedule_interval() don‘t work. I created then a trigger: Clock.create_trigger() and tried it with .cancel() and .unschedule(). These both functions worked perfectly.

How to run a Method on the exit of a kivy app

I would like to run a Method when the user tries to exit the app , kind of like a "are you sure you want to exit" or "Do you want to save the file" type of message whenever the user tries to exit by clicking the Exit button on top of the window
Some thing like
on_quit: app.root.saveSession()
If you want your application to simply run things after the GUI has closed, the easiest and smallest approach would be to place any exit code after TestApp().run(). run() creates a endless loop which also clears any event-data from within kivy so it doesn't hang. That endless loop breaks as soon as the window/gui instance dies. So there for, any code after will execute only after the GUI dies too.
If you want to create a graceful shutdown of the GUI with for instance socket-closing events or a popup asking the user if that's what they really want to do, then creating a hook for the on_request_close event is the way to go:
from kivy.config import Config
Config.set('kivy', 'exit_on_escape', '0')
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.popup import Popup
from kivy.core.window import Window
class ChildApp(App):
def build(self):
Window.bind(on_request_close=self.on_request_close)
return Label(text='Child')
def on_request_close(self, *args):
self.textpopup(title='Exit', text='Are you sure?')
return True
def textpopup(self, title='', text=''):
"""Open the pop-up with the name.
:param title: title of the pop-up to open
:type title: str
:param text: main text of the pop-up to open
:type text: str
:rtype: None
"""
box = BoxLayout(orientation='vertical')
box.add_widget(Label(text=text))
mybutton = Button(text='OK', size_hint=(1, 0.25))
box.add_widget(mybutton)
popup = Popup(title=title, content=box, size_hint=(None, None), size=(600, 300))
mybutton.bind(on_release=self.stop)
popup.open()
if __name__ == '__main__':
ChildApp().run()
Courtesy of pythonic64 who created a gist on the topic in a issue way back when.

How to Bind on_press to GridLayout in kivy python

i was trying to bind on_press method to gridLayout in kv language but i got this error AttributeError: pressed. I did some research even in the kivy doc but no help .So if any one has a solution to this problem please you may be a good resource
here is my testApp.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
class screendb(BoxLayout):
def mycall_back(self):
print('hello')
class testApp(App):
def build(self):
return screendb()
if __name__=='__main__':
testApp().run()
here is my testApp.kv
<Screendb#BoxLayout>:
GridLayout:
on_pressed:root.Mycall_back()
In your py file:
# Behaviors let you add behavior from one widget to another widget
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.gridlayout import GridLayout
# We create a new widget that is a GridLayout with access to Button Behaviors
# Like Button's 'on_press' method
class ButtonGrid(ButtonBehavior, GridLayout):
pass
class screendb(BoxLayout):
def mycall_back(self):
print('hello')
class testApp(App):
def build(self):
return screendb()
In your kv file:
# We create a Root Widget for the ButtonGrid
<ButtonGrid>:
<Screendb#BoxLayout>:
# We add an instance of the ButtonGrid class to our main layout
ButtonGrid:
# We can now use on_press because it is a part of the ButtonBehavior class, that makes up your ButtonGrid class.
on_press:root.mycall_back()
Note: There were a few minor mistakes in your post as well. For example, there is no method 'on_pressed', only 'on_press', you also wrote your callback as 'mycall_back' in your py file while writing it as 'Mycall_back' in your kv file, which refers to a different method that exists. Make sure your letter cases match.
video example

Kivy Sending text from spinner to another function

I've been trying to figure out how to get the value selected in a spinner into another function. I basically need a user to select some option and then press "save" prompting another function to write the data to a file (right now I just have it setup to print). When I run the form.finkle function it prints kivy.uix.button.Button object at 0x02C149D0
I'm sure its an easy fix, but I've been stuck on it for days.
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.popup import Popup
from kivy.uix.textinput import TextInput
from kivy.uix.spinner import Spinner
condspin = Spinner(text='Condition',values=('Good','Fair','Poor','Missing'))
typespin = Spinner(text='Type', values=('Metal','Wood','Pin','Missing'))
commlabel = Label(text='Comment')
commtext = TextInput(text="")
class Goose(App):
def build(self):
layout = GridLayout(cols=2,rows=6,padding=10,spacing=10)
layout.add_widget(Button(text='Hunter Parking'))
layout.add_widget(Button(text='Boat Launch'))
layout.add_widget(Button(text='ETRAP'))
layout.add_widget(Button(text='Monument',on_press=form.monform))
layout.add_widget(Button(text='Camp Site'))
layout.add_widget(Button(text='Sign'))
layout.add_widget(Button(text='Building'))
layout.add_widget(Button(text='Trail Head'))
layout.add_widget(Button(text='Dam'))
layout.add_widget(Button(text='Day Use'))
layout.add_widget(Button(text='Pavilion'))
layout.add_widget(Button(text='Misc Point'))
return layout
class form():
def finkle(condtest):
print condtest
def monform(self):
monbox = GridLayout(cols=2,rows=8,padding=20,spacing=20)
monpopup = Popup(title="Monument",content=monbox)
closebut = Button(text="Close")
closebut.bind(on_press=monpopup.dismiss)
savebut = Button(text="Save Point")
savebut.bind(on_press=form.finkle)
condtest = condspin.text
monbox.add_widget(condspin)
monbox.add_widget(typespin)
monbox.add_widget(commlabel)
monbox.add_widget(commtext)
monbox.add_widget(savebut)
monbox.add_widget(closebut)
monpopup.open()
Goose().run()
Since you have made the spinner global, you could just do print(condspin.text). More generally, you could pass the spinner as an argument, e.g.
from functools import partial
savebut.bind(on_press=partial(self.finkle, condspin))
and redefine the finkle method as
def finkle(self, spinner, button)
Note that I changed form.finkle to self.finkle and added both the self and spinner arguments. It's bad practice to call the method via the class like that.
There are some significant other bad style things in your code, and I would recommend some other changes. Mostly I would make use of kv language for basically everything, it will make the widget trees much clearer, more robust to changes later, and also make this binding very simple (you'd be able to refer to the spinner text via a kv id). Also, the form class is semi-unnecessary, you could replace it with a FormWidget that is the GridLayout you make in the monform function, adding all its children and behaviour in kv.

Categories