feat: add box/product auto-detection, numpad UI, touch screen support
- Auto-detect article type: 8-digit strings = BOX (strip leading zeros), anything else = PRODUCT - Live mode indicator label with manual override toggle button - On-screen numeric keypad (digits, decimal, backspace, Enter) - Active field routing: numpad writes to whichever field is focused - Scanner compatibility: Config keyboard_mode=system, refocus after each numpad press so HID scanner always has a target - Reset Values now also redirects numpad/scanner focus to ID field - Suppress OS virtual keyboard on touch for ID and mass fields
This commit is contained in:
136
main.py
136
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user