Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with VKeyboard - Inaccurate Key Presses and Input Handling with KivyMD TextField #1753

Open
dcl920108 opened this issue Nov 21, 2024 · 5 comments

Comments

@dcl920108
Copy link

dcl920108 commented Nov 21, 2024

Description:
I am encountering an issue using VKeyboard in my KivyMD application where the virtual key presses do not register correctly, and the input values are not being entered properly into the KivyMD TextField.

Here are the main issues:

  1. Inaccurate Key Press Registration:
    • When pressing a key on the virtual keyboard, the key that is actually displayed or registered is different from the key that was pressed.
    • For example, when pressing "4" on the virtual keyboard, the application sometimes displays "6" instead.

130926

  1. Failure to Input Correct Values into TextField:
    • The values from VKeyboard are not properly reflected in the KivyMD TextField.
    • Despite capturing the key press event (on_key_up), the value does not appear correctly in the input field, or it does not get entered at all.

Code Extract:

Below is the part of the code dealing with the virtual keyboard (VKeyboard) and the KivyMD TextField:

{
    "title" : "Numeric",
    "description" : "A numeric keypad",
    "cols" : 3,
    "rows": 4,
    "normal_1": [
    ["7", "7", "7", 1],
    ["8", "8", "8", 1],
    ["9", "9", "9", 1]],
    "normal_2": [
    ["4", "4", "4", 1],
    ["5", "5", "5", 1],
    ["6", "6", "6", 1]],
    "normal_3": [
    ["1", "1", "1", 1],
    ["2", "2", "2", 1],
    ["3", "3", "3", 1]],
    "normal_4": [
    ["0", "0", "0", 1],
    [".", ".", ".", 1],
    ["\u232b", null, "backspace", 1]],
    "shift_1": [
    ["7", "7", "7", 1],
    ["8", "8", "8", 1],
    ["9", "9", "9", 1]],
    "shift_2": [
    ["4", "4", "4", 1],
    ["5", "5", "5", 1],
    ["6", "6", "6", 1]],
    "shift_3": [
    ["1", "1", "1", 1],
    ["2", "2", "2", 1],
    ["3", "3", "3", 1]],
    "shift_4": [
    ["0", "0", "0", 1],
    [".", ".", ".", 1],
    ["\u232b", null, "backspace", 1]]
    }

Issues Summary:

  1. Key Press Mismatch:

    • When pressing a key on the VKeyboard, the displayed or registered key is incorrect. For example, pressing "4" may result in "6" being registered.
  2. Input Issue with TextField:

    • Even though I am capturing key presses through the on_key_up event, the value is not correctly entered into the KivyMD TextField.

Expected Behavior:

  • When pressing a key on the virtual keyboard, the correct corresponding value should be displayed in the debug output and accurately entered into the TextField.

Actual Behavior:

  • The key shown or entered is often incorrect and doesn't match the intended input.
  • The entered value is not correctly displayed in the TextField.

Environment:

  • Kivy Version: 2.3.0
  • KivyMD Version: 2.0.1.dev0
  • Python Version: 3.9
  • Platform: Raspberry Pi CM4

Steps to Reproduce:

  1. Set up VKeyboard with a numeric layout (numeric.json) as shown above.
  2. Bind the VKeyboard to a TextField using the focus event (on_focus) to control the visibility of the virtual keyboard.
  3. Run the application and attempt to input numbers through the virtual keyboard.
  4. Observe that the key that is pressed is often incorrectly registered or not properly reflected in the TextField.

Additional Information:

  • The layout file (numeric.json) used in this example contains a simple numeric keypad layout.
  • There is no issue when using a physical keyboard.
  • Debugging information (on_key_up) confirms the mismatch between the key press and the input value.

I would appreciate any advice or suggestions on how to resolve this issue. Thank you!

@dcl920108
Copy link
Author

Minimal Reproducible Example

This script is a minimal reproducible example of a KivyMD-based graphical user interface (GUI) application. It demonstrates the following features:

  1. Light-themed User Interface: The app uses a light theme with a green primary palette for a clean and simple look.
  2. Main Screen and Navigation: It consists of two main screens - a main screen and an isothermal control screen, managed by a screen manager for easy navigation.
  3. Input Fields with Virtual Keyboard: Users can input temperature, cycle count, and cycle time using text fields. The app uses a virtual numeric keyboard (VKeyboard) that appears when a text field is focused.
  4. Dynamic Layout: The GUI contains multiple buttons for navigation, configuration, and switching between different screens. It also features a back button for easy navigation.
  5. Date-Time Display: The app displays the current date and time, which updates every second.
  6. Button Actions: Buttons such as "Thermal Cycle", "Isothermal", and "Confirm Settings" are provided for user interactions, but the backend functionality is not implemented in this minimal example.

The main purpose of this example is to show how to create a user interface with KivyMD that includes interactive elements such as buttons, text fields, and a virtual keyboard, while ensuring the interface is responsive and user-friendly.

from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.screen import MDScreen
from kivymd.uix.button import MDButton, MDButtonIcon, MDButtonText
from kivymd.uix.label import MDLabel
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.fitimage import FitImage
from kivymd.uix.screenmanager import MDScreenManager
from kivymd.uix.textfield import (
    MDTextField,
    MDTextFieldLeadingIcon,
    MDTextFieldHintText,
    MDTextFieldHelperText,
)
from kivy.uix.vkeyboard import VKeyboard
from kivy.clock import Clock
from datetime import datetime

class MainScreen(MDScreen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        
        # 设置浅色主题和主色调
        self.theme_cls = MDApp.get_running_app().theme_cls
        self.theme_cls.theme_style = "Light"
        self.theme_cls.primary_palette = "Green"

        # 设置屏幕背景颜色
        self.md_bg_color = (1, 1, 1, 1)

        # 创建整体布局
        layout = MDBoxLayout(orientation="horizontal")
        self.left_layout = MDFloatLayout(size_hint=(0.4, 1))
        self.right_layout = MDFloatLayout(size_hint=(0.6, 1))

        qpcr_button = MDButton(
            MDButtonIcon(icon="ruler-square-compass"),
            MDButtonText(text="Thermal Cycle", font_style="Title"),
            style="elevated",
            pos_hint={"center_x": 0.7, "center_y": 0.6},
            height="200dp",
            size_hint=(3.2, 0.8)  # size_hint=(0.4, 0.1)
        )

        iso_button = MDButton(
            MDButtonIcon(icon="liquor"),
            MDButtonText(text="      Isothermal      ", font_style="Title"),
            style="elevated",
            pos_hint={"center_x": 1.8, "center_y": 0.6},
            height="200dp",
            size_hint=(6.4, 1.6)  # size_hint=(0.4, 0.1)
        )
        iso_button.bind(on_press=self.switch_to_isothermal)  # 绑定切换屏幕的方法

        # 将按钮添加到左侧布局
        self.left_layout.add_widget(qpcr_button)
        self.left_layout.add_widget(iso_button)

        # 创建底部的时间标签
        self.date_time_label = MDLabel(
            text="YYYY-MM-DD HH:MM:SS",
            halign="left",
            size_hint=(None, None),
            size=(dp(200), dp(40)),
            pos_hint={"x": -0.65, "y": 0.01},
        )

        # 将时间和 Logo 添加到右侧布局
        self.right_layout.add_widget(self.date_time_label)


        # 将左右布局添加到整体布局中
        layout.add_widget(self.left_layout)
        layout.add_widget(self.right_layout)

        # 将整体布局添加到屏幕中
        self.add_widget(layout)

        # 定期更新时间
        Clock.schedule_interval(self.update_date_time, 1)

    def update_date_time(self, dt):
        now = datetime.now()
        self.date_time_label.text = now.strftime("%Y-%m-%d %H:%M:%S")

    def switch_to_isothermal(self, *args):
        # 切换到名为 'isothermal' 的屏幕
        if self.manager:
            print("Switching to AGD screen...")  # 添加调试信息
            self.manager.current = "isothermal"

class MotorControlScreen(MDScreen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.locked = False
        self.data_records = []
        self.temperature_records = []  # 用于存储温度记录
        self.current_cycle = 0
        self.total_cycles = 20
        self.num_samples = 1000
        self.countdown_time = 360
        self.image_widget = None
        self.show_temperature_plot = False

        # 创建 VKeyboard 对象
        self.keyboard = VKeyboard()
        self.keyboard.layout_path = "./"  # 设置键盘布局文件的路径(假设布局文件位于当前目录下)
        self.keyboard.layout = 'numeric.json'  # 只使用数字键盘布局
        self.keyboard.size_hint = (1, 0.3)
        self.keyboard.pos_hint = {"center_x": 0, "y": 0}
        self.keyboard.bind(on_key_up=self.on_key_up)
        print("Keyboard binding successful")  # 添加调试信息

        # 调用 build_ui() 并将生成的 screen 作为主界面
        self.add_widget(self.build_ui())

    def build_ui(self):
        screen = MDScreen(md_bg_color=(1, 1, 1, 1))
        layout = MDBoxLayout(orientation="horizontal")
        self.left_layout = MDFloatLayout(size_hint=(0.4, 1))
        self.right_layout = MDFloatLayout(size_hint=(0.6, 1))

        back_button = MDButton(
            MDButtonIcon(icon="arrow-left"),  # 设置图标为返回箭头
            MDButtonText(text="Back", font_style="Title"),
            style="elevated",
            pos_hint={"center_x": 2.2, "center_y": 0.95},  # 左上角位置
            size_hint=(0.25, 0.1),
            on_release=self.switch_to_main_screen
        )

        toggle_plot_button = MDButton(
            MDButtonIcon(icon="swap-horizontal"),
            MDButtonText(text="Switching chart"),
            style="elevated",
            pos_hint={"center_x": -0.35, "center_y": 0.2},
            height="56dp",
            size_hint_x=0.6
        )

        # 添加温度输入框
        self.temperature_input = MDTextField(
            MDTextFieldLeadingIcon(icon="thermometer"),
            MDTextFieldHintText(text="Target Temperature (°C)"),
            MDTextFieldHelperText(text="Please enter the target temperature (e.g., 98)", mode="persistent"),
            mode="outlined",
            size_hint_x=None,
            width="240dp",
            pos_hint={"center_x": 0.5, "center_y": 0.9},
        )
        self.temperature_input.bind(focus=self.show_keyboard)

        # 添加循环次数输入框
        self.cycle_count_input = MDTextField(
            MDTextFieldLeadingIcon(icon="repeat"),
            MDTextFieldHintText(text="Cycle Count"),
            MDTextFieldHelperText(text="Please enter the number of cycles (e.g., 20)", mode="persistent"),
            mode="outlined",
            size_hint_x=None,
            width="240dp",
            pos_hint={"center_x": 0.5, "center_y": 0.7},
        )
        self.cycle_count_input.bind(focus=self.show_keyboard)

        # 添加每个循环时间输入框
        self.cycle_time_input = MDTextField(
            MDTextFieldLeadingIcon(icon="timer"),
            MDTextFieldHintText(text="Cycle Time (seconds)"),
            MDTextFieldHelperText(text="Please enter the time for each cycle (e.g., 60)", mode="persistent"),
            mode="outlined",
            size_hint_x=None,
            width="240dp",
            pos_hint={"center_x": 0.5, "center_y": 0.5},
        )
        self.cycle_time_input.bind(focus=self.show_keyboard)

        # 确认按钮,确认设定并开始实验流程
        confirm_button = MDButton(
            MDButtonIcon(icon="check"),
            MDButtonText(text="Confirm Settings"),
            style="elevated",
            pos_hint={"center_x": 0.5, "center_y": 0.3},
            height="56dp",
            size_hint_x=0.6
        )

        # 绑定按钮到 toggle_plot() 函数,用于切换显示的图表
        toggle_plot_button.bind(on_press=self.toggle_plot)

        self.left_layout.add_widget(self.temperature_input)
        self.left_layout.add_widget(self.cycle_count_input)
        self.left_layout.add_widget(self.cycle_time_input)
        self.left_layout.add_widget(confirm_button)
        self.left_layout.add_widget(back_button)

        self.right_layout.add_widget(toggle_plot_button)
        self.right_layout.add_widget(self.keyboard)

        layout.add_widget(self.left_layout)
        layout.add_widget(self.right_layout)

        screen.add_widget(layout)

        return screen

    def show_keyboard(self, instance, value):
        if value:  # 当输入框被聚焦时显示键盘
            self.keyboard.opacity = 1
            instance.focus = True  # 确保输入框获得焦点
        else:  # 当失去焦点时隐藏键盘
            self.keyboard.opacity = 0

    def on_key_up(self, keyboard, keycode, *args):
        print(f"Key pressed: {keycode}")  # 调试信息

        # 确保 keycode 至少包含两个元素,避免索引错误
        if len(keycode) < 2:
            return

        # 检查哪个输入框当前处于聚焦状态
        if self.temperature_input.focus:
            print("Temperature input focused")
        elif self.cycle_count_input.focus:
            print("Cycle count input focused")
        elif self.cycle_time_input.focus:
            print("Cycle time input focused")

        # 将输入的值添加到相应的输入框中
        if keycode[1] == 'backspace':
            # 如果按下的是回删键,则删除输入框中的最后一个字符
            if self.temperature_input.focus:
                self.temperature_input.text = self.temperature_input.text[:-1]
            elif self.cycle_count_input.focus:
                self.cycle_count_input.text = self.cycle_count_input.text[:-1]
            elif self.cycle_time_input.focus:
                self.cycle_time_input.text = self.cycle_time_input.text[:-1]
        elif keycode[1].isdigit() or keycode[1].isalpha():
            # 如果按下的是数字键或字母键,则将字符添加到相应的输入框
            if self.temperature_input.focus:
                self.temperature_input.text += keycode[1]
            elif self.cycle_count_input.focus:
                self.cycle_count_input.text += keycode[1]
            elif self.cycle_time_input.focus:
                self.cycle_time_input.text += keycode[1]

    def switch_to_main_screen(self, instance):
        if self.manager:
            self.manager.current = "main"

    def toggle_plot(self, instance):
        self.show_temperature_plot = not self.show_temperature_plot

class MainApp(MDApp):
    def build(self):
        # 创建 ScreenManager 来管理所有界面
        sm = MDScreenManager()

        # 添加主界面
        main_screen = MainScreen(name="main")
        sm.add_widget(main_screen)

        # 添加等温控制界面
        isothermal_screen = MotorControlScreen(name="isothermal")
        sm.add_widget(isothermal_screen)

        # 设置初始显示的界面
        sm.current = "main"

        return sm

# 运行应用程序
if __name__ == "__main__":
    MainApp().run()

@Tickets14
Copy link

Hello @dcl920108, were you able to fix this? I'm currently experiencing the same issue.

@dcl920108
Copy link
Author

Hello @dcl920108, were you able to fix this? I'm currently experiencing the same issue.

Hi @Tickets14

I haven't fully resolved the issue yet, but I have discovered a few possible approaches that might help fix it. Right now, I'm trying to understand how vkeyboard integrates and works together with native Kivy elements. You can find the example provided by Kivy here: Kivy Keyboard Examples.

I hope to solve it this week. At the moment, I am able to press the keys accurately, but there are still some issues left. If I make any progress, I'll update this thread with the latest information.

@dcl920108
Copy link
Author

Quote reply

Hi @Tickets14

problem solved!

The problem was solved by adding the following lines to configure the keyboard behavior:

from kivy.config import Config
Config.set('kivy', 'keyboard_mode', 'systemanddock')
Config.set('kivy', 'keyboard_layout', './numeric.json')

How it solved the problem:

  • Keyboard Mode: Setting keyboard_mode to 'systemanddock' ensured that the keyboard can use the system default and dock, allowing the custom numeric layout to be properly displayed.
  • Keyboard Layout: The line specifying the layout ('./numeric.json') ensured that only the numeric keyboard is used, making the input process more stable and focused on numeric input.

Note: It is also important to upgrade Kivy and KivyMD to the latest versions. This helps avoid compatibility issues and makes sure that the latest bug fixes and features are included, improving the overall stability and behavior of virtual keyboard support.

@dcl920108
Copy link
Author

Removed Manual Configuration of VKeyboard Layout:

In the original code, you manually created a VKeyboard object and attempted to set its layout:

self.keyboard = VKeyboard()
self.keyboard.layout_path = "./"  # Set the keyboard layout file path
self.keyboard.layout = 'numeric.json'  # Use only the numeric keyboard layout

This manual approach led to issues in managing the keyboard configuration and caused inconsistencies in how the keyboard appeared and behaved. By removing this and instead using the global configuration (Config.set), the keyboard settings became more reliable and consistent throughout the application.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants