# Verze upravená JQ: # - reaguje na klávesnici # - obsahuje ochranu proti zdvojování písmen # - umožňuje vstup ze čtečky, případně diakritiku přemění na čísla from kivy.app import App from kivy.uix.screenmanager import ScreenManager, Screen from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.label import Label from kivy.uix.button import Button from kivy.metrics import dp from kivy.logger import Logger from kivy.uix.widget import Widget # ===================================================== # NUMBER PAD # ===================================================== from kivy.metrics import dp from kivy.uix.modalview import ModalView from kivy.uix.boxlayout import BoxLayout from time import time # JQ from kivy.core.window import Window from kivy.clock import Clock # JQ class NumberPad(ModalView): def _btn(self, txt): btn = Button(text=txt) if txt == "⌫": btn.font_size = 36 btn.bind(on_press=lambda *_: self._press(txt)) return btn def __init__( self, *, mode="number", max_len=10, mask=False, allow_fraction=False, show_dot=True, decimal_places=2, initial_value="", overwrite_on_first=False, on_accept=None, on_cancel=None, when=None, #obsahuje time promenou pro potlaceni eventu allow_text=False, # 🔥 NOVÉ auto_accept_scanner=True, # 🔥 NOVÉ scanner_timeout=0.05, # 🔥 NOVÉ **kwargs, ): super().__init__( size_hint=(None, None), size=(dp(360), dp(480)), auto_dismiss=False, **kwargs, ) self.when = when self.overwrite_on_first = overwrite_on_first self._editing_started = False self.on_accept = on_accept self.on_cancel = on_cancel self.show_dot = show_dot # JQ self._cz_to_num = str.maketrans("+ěščřžýáíé", "1234567890") self.allow_text = allow_text self.auto_accept_scanner = auto_accept_scanner self.scanner_timeout = scanner_timeout self._last_text = None self._last_text_time = 0 # JQ self._last_key_time = 0 self._scanner_buffer = "" # --- interní stav (ZDROJ PRAVDY) --- self.num = "" self.den = "1" self.is_fraction = False self.mode = mode self.allow_fraction = allow_fraction self.decimal_places = decimal_places self.max_len = max_len self.mask = mask # ---------- ROOT (JEDNOU!) ---------- root = BoxLayout( orientation="vertical", padding=dp(10), spacing=dp(10), ) self.add_widget(root) # --- display --- self.lbl = Label( font_size=32, size_hint_y=None, height=dp(60), halign="right", valign="middle", ) self.lbl.bind(size=lambda *_: setattr(self.lbl, "text_size", self.lbl.size)) root.add_widget(self.lbl) # --- keypad --- grid = GridLayout(cols=3, spacing=dp(5), size_hint_y=1) for txt in ["7", "8", "9", "4", "5", "6", "1", "2", "3"]: grid.add_widget(self._btn(txt)) if self.show_dot: grid.add_widget(self._btn(".")) else: grid.add_widget(Label()) grid.add_widget(self._btn("0")) grid.add_widget(self._btn("⌫")) root.add_widget(grid) # --- fractions --- if self.allow_fraction: frac = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(10)) frac.add_widget(self._btn("1/1")) frac.add_widget(self._btn("1/2")) frac.add_widget(self._btn("1/3")) root.add_widget(frac) # --- actions --- actions = BoxLayout(size_hint_y=None, height=dp(60), spacing=dp(10)) btn_cancel = Button(text="ZRUŠIT") btn_ok = Button(text="OK") btn_cancel.bind(on_release=self._cancel) btn_ok.bind(on_release=self._accept) actions.add_widget(btn_cancel) actions.add_widget(btn_ok) root.add_widget(actions) # ---------- TADY JE KLÍČ ---------- self._set_initial_value(initial_value) self._overwrite = bool(initial_value) self._refresh() def _set_initial_value(self, val: str): """ Nastaví počáteční hodnotu NumberPadu z řetězce: "3" nebo "3/2" """ if not val: return val = val.strip() if "/" in val and self.allow_fraction: try: a, b = val.split("/", 1) self.num = a self.den = b self.is_fraction = (b != "1") except Exception: self.num = "" self.den = "1" self.is_fraction = False else: # čisté číslo self.num = val self.den = "1" self.is_fraction = False def disable(self): for w in self.children[0].children: if hasattr(w, "disabled"): w.disabled = True # ------------------------------------------------- def clear(self): self.num = "" self.den = "1" self.value = "" self._refresh() def reset(self): """ Resetuje NumberPad do počátečního stavu """ self.num = "" self.den = "1" self.is_fraction = False self.value = "" self._refresh() def _press(self, txt): print(txt) def _start_new_input(self): self.num = "" self.den = "1" self._editing_started = True self.is_fraction = False self._overwrite = False # code: žádná fraction, žádná tečka if self.mode == "code": if txt in ("1/1", "1/2", "1/3"): return # fraction: žádná desetinná tečka if self.allow_fraction and txt == ".": return # BACKSPACE if txt == "⌫": self._scanner_buffer = "" # 🔥 FIX if self.num: # mažu poslední cifru čitatele self.num = self.num[:-1] else: # čitatel prázdný → ruším fraction (zpět na /1) if self.allow_fraction and self.den != "1": self.den = "1" self._refresh() return # FRACTION if txt in ("1/1", "1/2", "1/3"): if not self.allow_fraction: return new_den = txt.split("/")[1] # "1", "2", "3" # toggle chování if self.den == new_den: self.den = "1" else: self.den = new_den self._refresh() return # DESETINNÁ TEČKA (jen decimal režim) if txt == ".": if not self.show_dot or self.allow_fraction: return if "." in self.num: return if self._overwrite: self._start_new_input() self.num = self.num + "." if self.num else "0." self._refresh() return # ČÍSLICE # ---- CODE REŽIM ---- if self.mode == "code": if len(self.num) < self.max_len: self.num += txt self._refresh() return # ---- DECIMAL REŽIM (bez fraction) ---- if not self.allow_fraction: # simulace budoucí hodnoty if self._overwrite: self._start_new_input() new_num = self.num + txt if self.num else txt # kontrola desetinných míst if "." in new_num: _, dec_part = new_num.split(".", 1) if len(dec_part) > self.decimal_places: return # kontrola délky celé části if "." not in self.num and len(self.num) >= self.max_len: return self.num = new_num self._refresh() return # ---- FRACTION REŽIM ---- if self.allow_fraction: if self._overwrite: # první stisk → přepíšeme čitatele, jmenovatel zachováme self.num = txt self._overwrite = False else: if len(self.num) < self.max_len: self.num += txt self._refresh() return def _start_new_input(self): """ Připraví NumberPad na nový vstup přepíše initial value """ self.num = "" self.den = "1" self.is_fraction = False self._overwrite = False def _refresh(self): # JQ if self.allow_text: self.value = self.num self.lbl.text = self.num return # JQ n = self.num if self.num else "0" # --- výstupní hodnota (datový kontrakt) --- if self.allow_fraction: self.value = f"{n}/{self.den}" else: self.value = n # --- ZOBRAZENÍ --- if self.mode == "code": # 🔑 KLÍČ: při prázdném vstupu nezobrazuj NIC if not self.num: self.lbl.text = "" else: self.lbl.text = "*" * len(self.num) return # --- ostatní režimy --- if not self.allow_fraction: self.lbl.text = n return if self.den == "1": self.lbl.text = n else: self.lbl.text = f"{n}/{self.den}" def _compose(self) -> str: n = self.num if self.num else "0" if not self.allow_fraction: # čistý numerický výstup return n return f"{n}/{self.den}" def _accept(self, *_): if not self.num: Logger.info("IGNORE empty accept") return Logger.info(f"NumberPad ACCEPT: {self.value}") self._scanner_buffer = "" # validace fraction if self.when: self.when = time() if self.allow_fraction: try: num, den = self.value.split("/") if int(num) == 0: # 0 kusů → ignorovat self.dismiss() return except Exception: pass if self.on_accept: self.on_accept(self.value) self.dismiss() def _cancel(self, *_): Logger.info("NumberPad CANCEL") if self.on_cancel: self.on_cancel() self.dismiss() def enable(self): for w in self.children[0].children: if hasattr(w, "disabled"): w.disabled = False # JQ def on_open(self): Logger.info("NumberPad opened → bind keyboard") Window.bind(on_key_down=self._on_key_down) Window.bind(on_textinput=self._on_textinput) def on_dismiss(self): Window.unbind(on_key_down=self._on_key_down) Window.unbind(on_textinput=self._on_textinput) def _keyboard_closed(self): Logger.info("Keyboard closed") self._keyboard = None def _on_key_down(self, *args): # ------------------------ # NORMALIZACE ARGUMENTŮ # ------------------------ if len(args) == 4: # Keyboard API keyboard, keycode, text, modifiers = args key = keycode[1] if isinstance(keycode, tuple) else keycode elif len(args) == 5: # Window API window, key, scancode, text, modifiers = args else: return False now = time() # delta = now - self._last_key_time self._last_key_time = now # ------------------------ # CONTROL KEYS # ------------------------ # 🔥 ENTER = scanner done # Tohle fungovalo samostatně, ale ne z volani z loginscreen # if key in ("enter", "numpadenter", "tab", 13, 271, 9): # if self.auto_accept_scanner and self.allow_text: # self._accept() # else: # self._accept() # return True if key in ("enter", "numpadenter", "tab", 13, 271, 9): if not self.num: return True self._accept() return True if key in ("escape", 27): self._cancel() return True if key in ("backspace", 8): if self.num: self.num = self.num[:-1] self._refresh() return True return False def _on_textinput(self, window, text): Logger.info(f"TEXT INPUT: {text}") # 🔥 převod CZ → čísla if text: text = text.translate(self._cz_to_num) # anti-duplicate (ponecháme) now = time() if text == self._last_text and (now - self._last_text_time) < 0.02: return True self._last_text = text self._last_text_time = now if self.mode == "code": for ch in text: if ch in "0123456789*.": self._press(ch) return True if self.allow_text: allowed = set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ%&?<>#*[]-_/.") if text not in allowed: return True # 🔥 respektuj max_len if self.max_len and len(self.num) >= self.max_len: return True self.num += text self._refresh() return True for ch in text: if ch in "0123456789": self._press(ch) elif ch in ",." and self.show_dot and not self.allow_fraction: self._press(".") return True return False # JQ # ===================================================== # TEST APP # ===================================================== class TestApp(App): def build(self): return Widget() # 👈 prázdný root def on_start(self): def done(val): Logger.info(f"RESULT = {val}") # pad = NumberPad( # mode="code", # max_len=15, # mask=True, # allow_fraction=False, # on_accept=done, # on_cancel=lambda *_: Logger.info("CANCEL"), # allow_text=True, # auto_accept_scanner=True, # ) pad = NumberPad( mode="code", max_len=15, # on_accept=self._accept, # on_cancel=self._cancel, allow_text=True, auto_accept_scanner=True, ) pad.open() if __name__ == "__main__": TestApp().run()