Search code examples
pythonkivywindowconfigconfigparser

Kivy Config.read not working properly when using Window.bind from within another class


i just discovered a bug and am trying to find a solution. This is my folder structure:

|- root
  |- GUI.py
  |- GUI.ini
  |- layouts
    |- root.kv
    |- style.kv
  |- lib
    |- kivy_utils.py
  |- static
    |- icon.ico
    |- Barlow-Regular.ttf
    |- Barlow-Bold.ttf

The GUI.py is my starting point for launching the GUI and looks like this:

# %% standard python library imports
import os

# %% kivy imports
# import kivy
from kivy.app import App
from kivy.core.window import Window
from kivy.config import Config
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout

# %% lib imports
from lib.kivy_utils import TTLabel

# read & overrule config
Config.read(os.path.join(os.path.dirname(os.path.abspath(__file__)), "GUI.ini"))
Window.size = Config.getint("graphics", "width"), Config.getint("graphics", "height")

# read kv language files
for kv in ["root.kv", "style.kv"]:
    Builder.load_file("layouts" + os.sep + kv)

class Root(BoxLayout):
    """ Main Root class for callbacks, UI building etc."""
    def __init__(self):
        super().__init__()

    def test_function(self):
        print("K TEST")


class Launcher(App):
    """Supplementary Launcher class"""

    def build(self):
        return Root()

    def on_pause(self):
        return True


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

I want to use the kivy_utils.py for custom classes, such as scalable widgets, tooltips etc.:

from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
from kivy.core.window import Window
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.slider import Slider
from kivy.uix.switch import Switch
from kivy.clock import Clock

class Tooltip(Label):
    pass


class HoverBehavior(object):
    """Hover behavior.
    :Events:
        `on_enter`
            Fired when mouse enter the bbox of the widget.
        `on_leave`
            Fired when the mouse exit the widget
    """

    hovered = BooleanProperty(False)
    border_point = ObjectProperty(None)

    def __init__(self, **kwargs):
        self.register_event_type('on_enter')
        self.register_event_type('on_leave')
        Window.bind(mouse_pos=self.on_mouse_pos)  # for recognizing tooltips
        super(HoverBehavior, self).__init__(**kwargs)

    def on_mouse_pos(self, *args):
        if not self.get_root_window():
            return
        pos = args[1]
        inside = self.collide_point(*self.to_widget(*pos))  # compensate for relative layout
        if self.hovered == inside:
            return
        self.border_point = pos
        self.hovered = inside
        if inside:
            self.dispatch('on_enter')
        else:
            self.dispatch('on_leave')

    def on_enter(self):
        pass

    def on_leave(self):
        pass


class TTLabel(HoverBehavior, Label):
    """Resizable Label with Tooltip on top of Label and HoverBehaviour class"""

    def __init__(self, **kwargs):
        super().__init__()
        self.tooltip = None  # Attribute set in kv file
        self.header = None  # Attribute set in kv file
        self.tooltip_wdg = Tooltip()
        Window.bind(on_resize=self.on_window_resize)  # binds font_size rescaling function to on_resize event
        Clock.schedule_once(self.on_window_resize, 1.5)  # called once at init cuz widget hasnt final size yet

    def on_enter(self):
        """Event fires when entering widget"""
        if self.tooltip:  # only binds event if tooltip variable is set
            Window.bind(mouse_pos=lambda w, p: setattr(self.tooltip_wdg, 'pos', p))  # binds position to cursor
            self.tooltip_wdg.text = self.tooltip  # sets text to tooltip variable
            Window.add_widget(self.tooltip_wdg)

    def on_leave(self):
        """Event fires when leaving widget"""
        if self.tooltip:
            Window.remove_widget(self.tooltip_wdg)

    def on_window_resize(self, *args):
        """Event fires when window is rescaled"""
        fire_refresh = True

        # # Fires when horizontal size is too small
        if self.size[0] < self._label.size[0]:
            fire_refresh = False
            self.texture_size[0] = self.size[0]  # reduce texture size to widget size
            if self.size[1] < self._label.size[1]:  # additionally, if vertical size is too small, reduce aswell
                self.texture_size[1] = self.size[1]
                return

        # Fires when vertical size is too small
        if self.size[1] < self._label.size[1]:
            fire_refresh = False
            self.texture_size[1] = self.size[1]
            if self.size[0] < self._label.size[0]:
                self.texture_size[0] = self.size[0]
                return

        # Fires when widget size > texture size  # TODO: is there another way not to fire all the time?
        if fire_refresh:
            self.texture_update()

And finally, my root.kv looks like this:

<Root>:
    BoxLayout:
        orientation: 'horizontal'
        Button:
            text: "Test button"
            on_release: root.test_function()
        TTLabel:
            text: "Test label with tooltip"
            tooltip: "Also WOW!"

So now to my actual problem: The code works, however(!) the Config.read command in my GUI.py file is not "refreshing" the current Config - the load works, i can access the attributes via Config.get, use Config.write etc - the .ini file gets updated, but the changes are not realized. Now i was thinking to manually refresh the parameters from the config (e.g. Window.size = Config.getint("graphics", "width"), Config.getint("graphics", "height")), but i want to use the Options menu (F1) and there must be another way i am sure. The problem occures as soon as i use Window.bind from within my kivy_utils.py. I tried loading the Config there, use self.get_root_window instead of Window, putting the Config.read command in various places in the kivy_utils.py etc. What am i missing?


Solution

  • I figured it out - i just had to import and read the config before the Window import:

    from kivy.config import Config
    Config.read("GUI.ini")
    from kivy.app import App
    from kivy.core.window import Window
    from kivy.lang import Builder
    from kivy.uix.boxlayout import BoxLayout
    

    Like this you can use the configurator (F1) and the affected changes are recognized properly (after a restart of the UI, however). Also i figured out a weird behaviour of environmental variables: the kivy documentation states:

    If you don't want to map any environment variables, you can disable the behavior::

    os.environ["KIVY_NO_ENV_CONFIG"] = "1"
    

    They probably flipped a bool there, because 0 actually disables environmental variables while 1 enables them. Will report to kivy repository.