diff --git a/main.py b/main.py index f2d6746..148db4e 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,6 @@ import threading +from kivy.config import Config +Config.set('kivy', 'keyboard_mode', 'system') from kivy.app import App from kivy.uix.boxlayout import BoxLayout from kivy.uix.anchorlayout import AnchorLayout @@ -17,6 +19,7 @@ class DatabaseApp(App): def __init__(self, **kwargs): super().__init__(**kwargs) self.db_manager = DatabaseManager() + self.active_numpad_input = None def build(self): # Set window to fullscreen @@ -29,8 +32,8 @@ class DatabaseApp(App): main_layout = BoxLayout(orientation='vertical', padding=40, spacing=20, size_hint=(1, 1), pos_hint={'x': 0, 'y': 0}) - # Top spacer for vertical centering - main_layout.add_widget(Label(size_hint_y=0.15)) + # Top spacer (reduced) + main_layout.add_widget(Label(size_hint_y=0.03)) # Content container - centered content_layout = BoxLayout(orientation='vertical', spacing=30, size_hint_y=None) @@ -52,6 +55,8 @@ class DatabaseApp(App): padding=[10, 10] ) self.id_input.bind(on_text_validate=self.search_record) + self.id_input.bind(on_text=self.update_mode_indicator) + self.id_input.bind(focus=self.on_id_input_focus) search_layout.add_widget(self.id_input) search_layout.add_widget(Label(text='Mass:', size_hint_x=0.25, font_size=20, bold=True)) self.mass_input = TextInput( @@ -65,6 +70,28 @@ class DatabaseApp(App): search_layout.add_widget(self.mass_input) content_layout.add_widget(search_layout) + # Mode indicator row: label + override button + self.manual_override = None # None = auto, 'BOX' or 'PRODUCT' = manual + mode_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=40, spacing=15) + self.mode_label = Label( + text='Article type detected: PRODUCT', + size_hint_x=0.75, + font_size=18, + bold=True, + color=(0.4, 0.8, 1, 1) + ) + mode_row.add_widget(self.mode_label) + self.override_btn = Button( + text='Override type', + size_hint_x=0.25, + font_size=16, + bold=True, + background_color=(0.3, 0.3, 0.3, 1) + ) + self.override_btn.bind(on_press=self.toggle_override) + mode_row.add_widget(self.override_btn) + content_layout.add_widget(mode_row) + # Button section - larger buttons (3 columns now) button_layout = GridLayout(cols=3, size_hint_y=None, height=70, spacing=15) add_update_btn = Button(text='Add/Update', font_size=22, bold=True) @@ -91,6 +118,7 @@ class DatabaseApp(App): update_inputs.add_widget(self.update_id_input) update_inputs.add_widget(Label(text='Mass:', size_hint_x=0.25, font_size=20, bold=True)) self.update_mass_input = TextInput(multiline=False, size_hint_x=0.75, readonly=True, font_size=20, padding=[10, 10]) + self.update_mass_input.bind(focus=self.on_mass_input_focus) update_inputs.add_widget(self.update_mass_input) self.update_frame.add_widget(update_inputs) # Add update and delete buttons in same row @@ -119,9 +147,29 @@ class DatabaseApp(App): content_layout.add_widget(self.status_label) main_layout.add_widget(content_layout) - - # Bottom spacer for vertical centering - main_layout.add_widget(Label(size_hint_y=0.15)) + + # Numeric keypad — digits + decimal, backspace, enter + numpad_wrapper = BoxLayout(orientation='vertical', size_hint_y=None, height=320, spacing=8, padding=[40, 8, 40, 8]) + numpad = GridLayout(cols=3, size_hint_y=None, height=240, spacing=8) + for digit in ['1', '2', '3', '4', '5', '6', '7', '8', '9']: + btn = Button(text=digit, font_size=28, bold=True) + btn.bind(on_press=self.numpad_press) + numpad.add_widget(btn) + dot_btn = Button(text='.', font_size=28, bold=True, background_color=(0.25, 0.25, 0.45, 1)) + dot_btn.bind(on_press=self.numpad_press) + numpad.add_widget(dot_btn) + zero_btn = Button(text='0', font_size=28, bold=True) + zero_btn.bind(on_press=self.numpad_press) + numpad.add_widget(zero_btn) + back_btn = Button(text='⌫', font_size=28, bold=True, background_color=(0.5, 0.3, 0.1, 1)) + back_btn.bind(on_press=self.numpad_backspace) + numpad.add_widget(back_btn) + numpad_wrapper.add_widget(numpad) + enter_btn = Button(text='Enter', font_size=26, bold=True, + background_color=(0.1, 0.55, 0.1, 1), size_hint_y=None, height=64) + enter_btn.bind(on_press=self.numpad_enter) + numpad_wrapper.add_widget(enter_btn) + main_layout.add_widget(numpad_wrapper) root.add_widget(main_layout) @@ -148,12 +196,84 @@ class DatabaseApp(App): Clock.schedule_once(_init_db, 0.1) return root + def on_id_input_focus(self, instance, focused): + """Track active numpad target when ID field gains focus.""" + if focused: + self.active_numpad_input = self.id_input + + def on_mass_input_focus(self, instance, focused): + """Track active numpad target when mass field gains focus.""" + if focused: + self.active_numpad_input = self.update_mass_input + + def _refocus_active(self): + """Return keyboard focus to the active field so scanner input keeps working.""" + target = self.active_numpad_input if self.active_numpad_input else self.id_input + if not target.readonly: + Clock.schedule_once(lambda dt: setattr(target, 'focus', True), 0.05) + + def numpad_enter(self, instance): + """Enter key: trigger search if ID field is active, then refocus.""" + if self.active_numpad_input is self.id_input or self.active_numpad_input is None: + self.search_record(instance) + self._refocus_active() + + def numpad_press(self, instance): + """Append a digit/dot to the active input field, then refocus for scanner.""" + target = self.active_numpad_input if self.active_numpad_input else self.id_input + if not target.readonly: + target.text += instance.text + self._refocus_active() + + def numpad_backspace(self, instance): + """Remove the last character from the active input field, then refocus for scanner.""" + target = self.active_numpad_input if self.active_numpad_input else self.id_input + if not target.readonly: + target.text = target.text[:-1] + self._refocus_active() + def set_update_frame_enabled(self, enabled): self.update_id_input.readonly = not enabled self.update_mass_input.readonly = not enabled self.update_confirm_btn.disabled = not enabled self.delete_btn.disabled = not enabled + def _resolve_id(self, raw_id: str) -> str: + """Resolve ID using manual override if set, otherwise auto-detect.""" + mode = self.manual_override if self.manual_override else ( + 'BOX' if (len(raw_id) == 8 and raw_id.isdigit()) else 'PRODUCT' + ) + if mode == 'BOX': + return raw_id.lstrip('0') or '0' + return raw_id + + def toggle_override(self, instance): + """Manually flip the detected type between BOX and PRODUCT.""" + current_auto = 'BOX' if (len(self.id_input.text.strip()) == 8 and self.id_input.text.strip().isdigit()) else 'PRODUCT' + # Determine current effective mode + effective = self.manual_override if self.manual_override else current_auto + # Flip it + self.manual_override = 'PRODUCT' if effective == 'BOX' else 'BOX' + self._apply_mode_label(self.manual_override, is_override=True) + + def update_mode_indicator(self, instance, value): + """Auto-detect mode on each keystroke; resets any manual override.""" + self.manual_override = None + text = value.strip() + mode = 'BOX' if (len(text) == 8 and text.isdigit()) else 'PRODUCT' + self._apply_mode_label(mode, is_override=False) + + def _apply_mode_label(self, mode, is_override): + """Update the mode label and override button appearance.""" + prefix = '[Manual] ' if is_override else '' + if mode == 'BOX': + self.mode_label.text = f'{prefix}Article type detected: BOX' + self.mode_label.color = (1, 0.75, 0, 1) + else: + self.mode_label.text = f'{prefix}Article type detected: PRODUCT' + self.mode_label.color = (0.4, 0.8, 1, 1) + self.override_btn.background_color = (0.6, 0.2, 0.6, 1) if is_override else (0.3, 0.3, 0.3, 1) + def show_update_frame(self, instance): # If no value in search, copy from search fields record_id = self.id_input.text.strip() @@ -179,14 +299,15 @@ class DatabaseApp(App): self.show_status("Searching...", error=False) + resolved_id = self._resolve_id(record_id) def _do(): try: - record = self.db_manager.search_by_id(record_id) + record = self.db_manager.search_by_id(resolved_id) def _update(dt): if record: self.mass_input.text = str(record[1]) self.show_status(f"Found: {record[0]} = {record[1]}") - self.highlight_record(record_id) + self.highlight_record(resolved_id) else: self.show_status(f"ID '{record_id}' not found in database", error=True) self.mass_input.text = "" @@ -276,6 +397,7 @@ class DatabaseApp(App): """Reset/clear the first ID and mass fields and set focus on ID field.""" self.id_input.text = "" self.mass_input.text = "" + self.active_numpad_input = self.id_input self.id_input.focus = True self.show_status("Fields cleared", error=False)