Recapture keyboard focus in Kivy after clicking out of a `TextInput` object - python

The following code creates a Kivy app with a single screen, MainScreen, displaying a Label and TextInput object.
If you give some keyboard input when the app first loads, it prints in the console with information about which key was pressed. However, if you then click in the TextInput object the keyboard refocuses so you can type in the TextInput object. However, once you click back out of the TextInput object, e.g. on the label or background, and press more keys, the same console printing operation doesn't execute; it seems that the keyboard doesn't 'defocus' from the TextInput object.
How do I recapture keyboard focus after clicking out of a TextInput object?
from kivy.app import App
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.textinput import TextInput
kv = '''ScreenManagement:
MainScreen:
<MainScreen>:
name: "main"
BoxLayout:
Label:
text: "Label"
font_size: 20
size_hint: 0.2,0.1
TextInput
input_filter: 'float'
font_size: 20
hint_text: "Input Text"
size_hint: 0.2,0.1'''
class MainScreen(Screen):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
self._keyboard.bind(on_key_down=self._on_keyboard_down)
def _keyboard_closed(self):
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
self._keyboard = None
def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
print('INFO: The key', keycode, 'has been pressed')
return True # return True to accept the key. Otherwise, it will be used by the system.
class ScreenManagement(ScreenManager):
pass
class MainApp(App):
def build(self):
return Builder.load_string(kv)
if __name__ == "__main__":
MainApp().run()

One possible solution is to use the on_touch_down event and reconfigure the keyboard if you press a section that does not contain a TextInput:
class MainScreen(Screen):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.config_keyboard()
def on_touch_down(self, touch):
self.isTextInput = False
def filter(widget):
for child in widget.children:
filter(child)
if isinstance(widget, TextInput) and widget.collide_point(*touch.pos):
self.isTextInput = True
widget.on_touch_down(touch)
filter(self)
if not self.isTextInput and self._keyboard is None:
self.config_keyboard()
def config_keyboard(self):
self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
self._keyboard.bind(on_key_down=self._on_keyboard_down)
def _keyboard_closed(self):
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
self._keyboard = None
def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
print('INFO: The key', keycode, 'has been pressed')
return True # return True to accept the key. Otherwise, it will be used by the system.

Related

Kivy Window.request_keyboard only working on the second screen

So I am making an app in kivy python and it has 2 screens that I want the app to have keyboard access to. So i have implemented the standard request keyboard as given on the Kivy documentation. But it is only working on Screen2 and not on screen1. Plus, when I am on screen1, it still implements the on_keyboard_down() of screen2.
Below is a reproducible code with the same problem.
import kivy
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
Builder.load_string("""
<screen1>:
Label:
text: "Screen 1"
Button:
text: "go s2"
on_press: root.c1()
<screen2>:
Label:
text: "Screen 2"
Button:
text: "go s1"
on_press: root.c2()
""")
class screen1(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.request_keyboard()
def request_keyboard(self):
print("s1")
self._keyboard = Window.request_keyboard(self._keyboard_closed, self, "text")
if self._keyboard.widget:
pass
self._keyboard.bind(on_key_down=self._on_keyboard_down)
self._keyboard.bind(on_key_up=self._on_keyboard_up)
def _keyboard_closed(self):
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
self._keyboard = None
def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
if keycode[1] == "escape":
app = App.get_running_app()
app.stop()
else:
#self.encry_times +=1
pressed_letter = keycode[1].upper()
c = pressed_letter
print("s1" + c)
return True
def _on_keyboard_up(self, keyboard, keycode):
pass
def c1(self):
eni_app.screen_manager.current = "s2"
class screen2(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.request_keyboard()
def request_keyboard(self):
print("s2")
self._keyboard = Window.request_keyboard(self._keyboard_closed, self, "text")
if self._keyboard.widget:
pass
self._keyboard.bind(on_key_down=self._on_keyboard_down)
self._keyboard.bind(on_key_up=self._on_keyboard_up)
def _keyboard_closed(self):
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
self._keyboard = None
def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
if keycode[1] == "escape":
app = App.get_running_app()
app.stop()
else:
#self.encry_times +=1
pressed_letter = keycode[1].upper()
c = pressed_letter
print("s2" + c)
return True
def _on_keyboard_up(self, keyboard, keycode):
pass
def c2(self):
eni_app.screen_manager.current = "s1"
class TestApp(App):
def build(self):
self.screen_manager = ScreenManager(transition = FadeTransition())
self.s1_page = screen1()
screen = Screen(name = "s1")
screen.add_widget(self.s1_page)
self.screen_manager.add_widget(screen)
self.s2_page = screen2()
screen = Screen(name = "s2")
screen.add_widget(self.s2_page)
self.screen_manager.add_widget(screen)
return self.screen_manager
if __name__ == '__main__':
eni_app = TestApp()
eni_app.run()
I have tried adding a on_enter function but it completely blocks any access to the keyboard. on_pre_enter has a similar effect on the code.
The expectation from the code above is: it should print s1+ (whatever key is pressed) and on screen2 it should press s2+(whatever key is pressed)

Instantiate data class from dropdown menu options using on_touch_up

I would like to populate MyData with information gathered from dropdown menus that show up in a popup window with "on_touch_up" in AddTouch. That data includes the position of "on_touch_up", in addition to the dropdown data. I am able to print the position within the AddTouch class, but I am having a hard time getting the data further down in my script using (for example: print('from MyMainApp: {}'.format(MyData.pos))).
I am also unable to get "mainbutton" or "dropdown" to show up in a popup window.
Hacking around with this I came up with the following which works, but doesn't do what i need
.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.uix.dropdown import DropDown
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.graphics import Color, Rectangle
class AddTouch(Widget):
def __init__(self, **kwargs):
super(AddTouch, self).__init__(**kwargs)
with self.canvas:
Color(1, 0, 0, 0.5, mode="rgba")
self.rect = Rectangle(pos=(0, 0), size=(10, 10))
def on_touch_down(self, touch):
self.rect.pos = touch.pos
def on_touch_move(self, touch):
self.rect.pos = touch.pos
def on_touch_up(self, touch):
# final position
self.pos = touch.pos
print(self.pos)
class MyPopup(Popup):
def __init__(self, **kwargs):
super(MyPopup, self).__init__(**kwargs)
# create a main button
self.mainbutton = Button(text='Hello', size_hint=(None, None))
# create a dropdown with 10 buttons
self.dropdown = DropDown()
for index in range(10):
btn = Button(text='Value %d' % index, size_hint_y=None, height=44)
btn.bind(on_release=lambda btn: self.dropdown.select(btn.text))
self.dropdown.add_widget(btn)
self.mainbutton.bind(on_release=self.dropdown.open)
self.dropdown.bind(on_select=lambda instance, x: setattr(self.mainbutton, 'text', x))
class MyData:
def __init__(self, **kwargs):
super(MyData, self).__init__(**kwargs)
self.pos=AddTouch.pos
# using kivy screen for consistency
class MainWindow(Screen):
pass
class WindowManager(ScreenManager):
pass
kv = Builder.load_file("dropd.kv")
class MyMainApp(App):
def build(self):
return kv
print('from MyMainApp: {}'.format(MyData.pos))
if __name__ == "__main__":
MyMainApp().run()
.kv
WindowManager:
MainWindow:
<MainWindow>:
name: "main"
AddTouch:
on_touch_up:
#MyPopup gives 'MyPopup' is not defined, even if I add <MyPopup>: below
#root.MyPopup gives 'MainWindow' object has no attribute 'MyPopup'
I tried adding a simple dynamic Popup class in the .kv file based on this, but again it says 'MyPopup' is not defined:
.kv
AddTouch
on_touch_up:
MyPopup
<MyPopup#Popup>:
auto_dismiss: False
Button:
text: 'Close me!'
on_release: root.dismiss()
What am I missing (other than experience, ability, and general intelligence)?
In addition to adding the line:
self.content = self.mainbutton
to the MyPopup __init__() method, you can trigger the MyPopup creation by modifying your .kv file as:
#:import Factory kivy.factory.Factory
WindowManager:
MainWindow:
<MainWindow>:
name: "main"
AddTouch:
on_touch_up:
Factory.MyPopup().open()

How to update Button text in Kivy

I'm making an MP3 Player for a project using Kivy. I am having issues on updating the text of a Button.
The only method that I've used and successfully worked was to update the button text directly, but I want to update a variable that is what the button's text is.
Here's the minimum reproducible example:
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
class FirstKivy(App):
def __init__(self, **kwargs):
super(FirstKivy, self).__init__(**kwargs)
self.pausePlay = "Play"
def build(self):
layout = BoxLayout(orientation = "vertical")
btn = Button(text = self.pausePlay)
btn.bind(on_press = self.changePausePlay)
layout.add_widget(btn)
return layout
def changePausePlay(self, button):
if self.pausePlay == "Play":
self.pausePlay = "Pause"
elif self.pausePlay == "Pause":
self.pausePlay = "Play"
FirstKivy().run()
I expect the button's text to change from "Play" to "Pause" on click and then from "Pause" to "Play on click again. No error messages are sent.
Any help is appreciated, I'm new to Kivy as well as OOP in Python.
The easiest way to do it is to use kv to build the gui with a StringProperty to hold the Button text:
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import StringProperty
kv = '''
BoxLayout:
orientation: 'vertical'
Button:
text: app.pausePlay
on_press: app.changePausePlay(self)
'''
class FirstKivy(App):
pausePlay = StringProperty('Play')
def __init__(self, **kwargs):
super(FirstKivy, self).__init__(**kwargs)
def build(self):
layout = Builder.load_string(kv)
return layout
def changePausePlay(self, button):
if self.pausePlay == "Play":
self.pausePlay = "Pause"
elif self.pausePlay == "Pause":
self.pausePlay = "Play"
FirstKivy().run()
Some key points. The kv language automatically sets up bindings when it can (creating the same gui in Python does not). The StringProperty allows kv to set up the binding so that any change in pausePlay will be reflected in the Button text.

RippleButton class is working in one Python script but not while working with screens

While I run this script rippleexample2.py with rippleexample2.kv, the buttons should have ripple effects upon pressing, but it doesn't work.
I know the RippleButton class is working fine in ctmbtn.py, when a button is pressed here, there is ripple effect. I don't know what is wrong here. Perhaps binding function?
rippleexample2.py
from kivy.app import App
from kivy.uix.touchripple import TouchRippleBehavior
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder
from kivy.properties import (StringProperty, NumericProperty, ObjectProperty, ListProperty, DictProperty, BooleanProperty)
class RippleButton(TouchRippleBehavior, Button):
isRippled = BooleanProperty(False)
def __init__(self, **kwargs):
super(RippleButton, self).__init__(**kwargs)
def on_touch_down(self, touch):
collide_point = self.collide_point(touch.x, touch.y)
if collide_point and not self.isRippled:
self.isRippled = True
self.ripple_show(touch)
return super(RippleButton, self).on_touch_down(touch)
def on_touch_up(self, touch):
collide_point = self.collide_point(touch.x, touch.y)
if collide_point and self.isRippled:
self.isRippled = False
self.ripple_fade()
return super(RippleButton, self).on_touch_up(touch)
class Login(Screen):
pass
class MainScreen(Screen):
pass
class ScreenManager(ScreenManager):
pass
Login = Builder.load_file("rippleexample2.kv")
class SimpleKivy4(App):
def build(self):
return Login
if __name__ == "__main__":
SimpleKivy4().run()
rippleexample2.kv
ScreenManager:
Login:
MainScreen:
<Login>:
name:"login"
RippleButton:
text:'Login'
font_size: 24
size_hint: (.4,.25)
on_release: app.root.current = "main"
<MainScreen>:
name: "main"
Button:
text: 'back'
on_release: app.root.current = "login"
ctmbtn.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import (StringProperty, NumericProperty, ObjectProperty,
ListProperty, DictProperty, BooleanProperty)
from kivy.uix.touchripple import TouchRippleBehavior
class RippleButton(TouchRippleBehavior, Button):
isRippled = BooleanProperty(False)
def __init__(self, **kwargs):
super(RippleButton, self).__init__(**kwargs)
def on_touch_down(self, touch):
collide_point = self.collide_point(touch.x, touch.y)
if collide_point and not self.isRippled:
self.isRippled = True
self.ripple_show(touch)
return super(RippleButton, self).on_touch_down(touch)
def on_touch_up(self, touch):
collide_point = self.collide_point(touch.x, touch.y)
if collide_point and self.isRippled:
self.isRippled = False
self.ripple_fade()
return super(RippleButton, self).on_touch_up(touch)
class RootWidget(BoxLayout):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.add_widget(RippleButton(text='btn 1'))
cb = CustomBtn()
cb.bind(pressed=self.btn_pressed)
self.add_widget(cb)
self.add_widget(RippleButton(text='btn 2'))
def btn_pressed(self, instance, pos):
print ('pos: printed from root widget: {pos}'.format(pos=pos))
class CustomBtn(Widget):
pressed = ListProperty([0, 0])
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.pressed = touch.pos
# we consumed the touch. return False here to propagate
# the touch further to the children.
return True
return super(CustomBtn, self).on_touch_down(touch)
def on_pressed(self, instance, pos):
print ('pressed at {pos}'.format(pos=pos))
class TestApp(App):
def build(self):
return RootWidget()
if __name__ == '__main__':
TestApp().run()
The Touch Ripple code is still experimental, and it is not visible when using it in kv language for Button widget.
Actually, the Button touch ripple animation on interaction works when using kv language. It is just not visible because the background_color (r, g, b, a) of Button widget defaults to [1, 1, 1, 1] or grey color with no transparency. Whereby 'a' (alpha compositing or transparency) is 1 i.e. not transparent.
The ripple_color defaults to [1., 1., 1., .5] or grey color with half transparency.
Solution
The temporary work around is to change the alpha compositing or transparency of Button widget to 0.5.
Please refer to my example for Kivy RippleButton

Kivy ToggleButtonBehavior

I try to build a toggle between two or more labels like radio buttons. It work so far with the ToggleButtonBehavior to change the state between the group but when I click the selected item it get deselected and should not do it.
class SelectButton(ToggleButtonBehavior, Label):
active = BooleanProperty(False)
def __init__(self, **kwargs):
super(SelectButton, self).__init__(**kwargs)
self.text='off'
self.color=[1,0,1,1]
self.bind(state=self.toggle)
def toggle(self, instance, value):
if self.state == 'down':
self.text = 'on'
else:
self.text = 'off'
Is there a way to get a behavior like a radio button?
There is a allow_no_selection property:
This specifies whether the widgets in a group allow no selection i.e.
everything to be deselected.
allow_no_selection is a BooleanProperty and defaults to True
After setting it to False and using a group everything starts to work as intended:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.properties import BooleanProperty
from kivy.lang import Builder
Builder.load_string('''
<MyWidget>:
SelectButton:
state: 'down'
text: 'on'
SelectButton
state: 'normal'
text: 'off'
SelectButton
state: 'normal'
text: 'off'
''')
class SelectButton(ToggleButtonBehavior, Label):
active = BooleanProperty(False)
def __init__(self, **kwargs):
super(SelectButton, self).__init__(**kwargs)
self.allow_no_selection = False
self.group = "mygroup"
self.color=[1,0,1,1]
self.bind(state=self.toggle)
def toggle(self, instance, value):
if self.state == 'down':
self.text = 'on'
else:
self.text = 'off'
class MyWidget(BoxLayout):
pass
class MyApp(App):
def build(self):
return MyWidget()
if __name__ == '__main__':
MyApp().run()

Categories