Kivy screenmanager: switching screen after timeout with signal

Question:

Goal is to move to a settings screen when no button is pressed, text is entered or whatever for a certain time.

In fact, functionality is like a screensaver of some sorts.

code version 1

import signal
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen


class MenuScreen(Screen):
    pass


class SettingsScreen(Screen):
    pass


class wiscApp(App):

    def setscreensaver(self, *args):
        print("switching to settings")
        # --> here I need to switch to the settings screen
        # but this doens't work, bnoth sm and setscreen are not known here
        sm.switch_to(setscreen)

    def resetscreensavertimeout(self):
        print("resetting screensaver timer")
        signal.alarm(10)  # just 5 seconds for debugging

    def build(self):
        sm = ScreenManager()
        setscreen = SettingsScreen(name='settings')
        sm.add_widget(MenuScreen(name='menu'))
        sm.add_widget(setscreen)
        signal.signal(signal.SIGALRM, self.setscreensaver)
        self.resetscreensavertimeout()
        return sm


if __name__ == "__main__":
    wiscApp().run()

and the .kv

<MenuScreen>:
    BoxLayout:
        orientation: 'vertical'
        BoxLayout:
            Button:
                text: "resettimeout"
                on_press: app.resetscreensavertimeout()
            Button:
                text: "do other things"
        Button:
            text: 'settings'
            on_press: root.manager.current = 'settings'

<SettingsScreen>:
    BoxLayout:
        Button:
            text: "stop app"
            on_press: app.stop()
        Button:
            text: 'Back to menu'
            on_press: root.manager.current = 'menu'

This works perfectly up until the calling of sm.switch_to(setscreen) in the setscreensaver function.

I tried the following:
code version 2

import signal
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen


class MenuScreen(Screen):
    pass


class SettingsScreen(Screen):
    pass


class wiscApp(App):
    sm = ScreenManager()
    setscreen = SettingsScreen(name='settings')

    def setscreensaver(self, *args):
        print("switching to settings")
        # --> here I need to switch to the settings screen
        # but this doens't work, bnoth sm and setscreen are not known here
        self.sm.switch_to(self.setscreen)

    def resetscreensavertimeout(self):
        print("resetting screensaver timer")
        signal.alarm(10)  # just 5 seconds for debugging

    def build(self):
        self.sm.add_widget(MenuScreen(name='menu'))
        self.sm.add_widget(self.setscreen)
        signal.signal(signal.SIGALRM, self.setscreensaver)
        self.resetscreensavertimeout()
        return self.sm


if __name__ == "__main__":
    wiscApp().run()

But then the settings screen is BLANK!
In the first version of the code, I understand by it doens’t work: both sm and setscreen are undefined variables in that function.
In the second version, I don’t understand why the settings screen is blank.

edit
*** version 3 of the code***

import signal
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen


class MenuScreen(Screen):
    pass


class SettingsScreen(Screen):
    pass


class wiscApp(App):
    def setscreensaver(self, *args):
        print("switching to settings")
        # --> here I need to switch to the settings screen
        # but this doens't work, bnoth sm and setscreen are not known here
        self.sm.switch_to(self.setscreen)

    def resetscreensavertimeout(self):
        print("resetting screensaver timer")
        signal.alarm(10)  # just 5 seconds for debugging

    def build(self):
        self.sm = ScreenManager()
        self.setscreen = SettingsScreen(name='settings')
        self.sm.add_widget(MenuScreen(name='menu'))
        self.sm.add_widget(self.setscreen)
        signal.signal(signal.SIGALRM, self.setscreensaver)
        self.resetscreensavertimeout()
        return self.sm


if __name__ == "__main__":
    wiscApp().run()

In this version 3, transitioning to the settings screen with the signal works fine, but if I then click the menu button, I get this error (this error does not appear in the other versions of the code):

 kivy.uix.screenmanager.ScreenManagerException: No Screen with name "menu".

So, I have several questions

  1. how do I reset the timer every time a button is pressed, text is
    entered of whatever, other than defining callbacks for every event
    (e.g on_press: app.resetscreensavertimeout()) in the .kv code)?
  2. How do I switch to the settings screen in the first version of the
    code?
  3. Why is the settings screen blank in code version 2?
  4. Why does the error occur in version 3?
  5. Is there another (better) way to code this?

thanks a lot!

Asked By: Wannes

||

Answers:

Here is a modified version of your code that uses Clock.schedule_once() instead of signal:

class wiscApp(App):
    def setscreensaver(self, *args):
        print("switching to settings")
        self.resetscreensavertimeout()
        self.sm.current = 'settings'

    def resetscreensavertimeout(self, *args):
        print("resetting screensaver timer")
        self.resetEvent.cancel()
        self.resetEvent = Clock.schedule_once(self.setscreensaver, 5)

    def build(self):
        self.sm = ScreenManager()
        self.setscreen = SettingsScreen(name='settings')
        self.sm.add_widget(MenuScreen(name='menu'))
        self.sm.add_widget(self.setscreen)
        self.resetEvent = Clock.schedule_once(self.setscreensaver, 5)
        Window.bind(on_touch_down=self.resetscreensavertimeout)
        Window.bind(on_key_down=self.resetscreensavertimeout)
        return self.sm

This also uses Window.bind() to trigger the reset of the timeout whenever a button is pressed or a key is pressed.

Answered By: John Anderson

I took this one step further and defined a general-purpose function to add this functionality to any app.

from typing import Callable
from kivy.app import App
from kivy.clock import Clock
from kivy.core.window import Window

def add_inactivity_timeout(app: App, do_timeout: Callable, inactivity_minutes=60):
    """
    Sets up an inactivity timeout for the given Kivy app. "Activity" meaning
    a touch (a button click) or a key press.

    IMPORTANT: Call this from within the `build()` method, not `__init__()`.

    This adds a `reset_timeout()` method to the app which can be called
    directly by any other activity that needs to be considered (e.g.
    periodically within a long-running computation, or while accessing an
    external resource).

    Alternatively, call `cancel_timeout()` to disable the timeout on the
    way in to the long-running activity, and then call `reset_timeout()`
    afterwards to re-enable it.

    :param app: The kivy app to enhance.
    :param do_timeout: The method to call if a timeout occurs.
    :param inactivity_minutes: The timeout length in minutes. Defaults to 60.
    """
    app.inactivity_minutes = inactivity_minutes
    app.reset_event = None

    def cancel_timeout(*args):
        if app.reset_event:
            app.reset_event.cancel()

    def reset_timeout(*args):
        cancel_timeout()
        app.reset_event = Clock.schedule_once(do_timeout, app.inactivity_minutes * 60)

    app.cancel_timeout = cancel_timeout
    app.reset_timeout = reset_timeout
    reset_timeout()
    Window.bind(on_touch_down=reset_timeout)
    Window.bind(on_key_down=reset_timeout)

To use it, create a callback method for the timeout:

def inactive(self, *args):
    print("Timed out -- Inactive!")
    self.stop()

and then call

add_inactivity_timeout(self, self.inactive, inactivity_minutes=15)

from within build().

Answered By: Craig Jones
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.