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 from kivy.uix.floatlayout import FloatLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.label import Label from kivy.uix.textinput import TextInput from kivy.uix.button import Button from kivy.uix.scrollview import ScrollView from kivy.uix.popup import Popup from kivy.clock import Clock from kivy.core.window import Window from database_manager import DatabaseManager 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 Window.fullscreen = 'auto' # Root float layout so we can overlay the exit button root = FloatLayout() # Main layout with better spacing for fullscreen main_layout = BoxLayout(orientation='vertical', padding=40, spacing=20, size_hint=(1, 1), pos_hint={'x': 0, 'y': 0}) # 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) content_layout.bind(minimum_height=content_layout.setter('height')) # Title title = Label(text='Database Search & Update', font_size=28, bold=True, size_hint_y=None, height=50) content_layout.add_widget(title) # Search section search_layout = GridLayout(cols=2, size_hint_y=None, height=100, spacing=15, row_force_default=True, row_default_height=45) search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=20, bold=True)) self.id_input = TextInput( multiline=False, size_hint_x=0.75, hint_text='Enter ID and press Enter to search (max 20 chars)', readonly=False, font_size=20, 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( multiline=False, size_hint_x=0.75, hint_text='Mass value (read-only)', readonly=True, font_size=20, padding=[10, 10] ) 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) add_update_btn.bind(on_press=self.show_update_frame) button_layout.add_widget(add_update_btn) reset_btn = Button(text='Reset Values', font_size=22, bold=True) reset_btn.bind(on_press=self.reset_values) button_layout.add_widget(reset_btn) settings_btn = Button(text='Settings', font_size=22, bold=True) settings_btn.bind(on_press=self.show_settings) button_layout.add_widget(settings_btn) content_layout.add_widget(button_layout) # Extra spacing between buttons and update frame content_layout.add_widget(Label(size_hint_y=None, height=40)) # Update frame (initially disabled) self.update_frame = BoxLayout(orientation='vertical', padding=15, spacing=15, size_hint_y=None, height=200) self.update_frame_label = Label(text='Update Values', size_hint_y=None, height=40, font_size=22, bold=True) self.update_frame.add_widget(self.update_frame_label) update_inputs = GridLayout(cols=2, spacing=15, size_hint_y=None, height=100, row_force_default=True, row_default_height=45) update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=20, bold=True)) self.update_id_input = TextInput(multiline=False, size_hint_x=0.75, readonly=True, font_size=20, padding=[10, 10]) 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 update_buttons = GridLayout(cols=2, size_hint_y=None, height=60, spacing=15) self.update_confirm_btn = Button(text='Confirm Add/Update', disabled=True, font_size=22, bold=True) self.update_confirm_btn.bind(on_press=self.add_update_record) update_buttons.add_widget(self.update_confirm_btn) self.delete_btn = Button(text='Delete', disabled=True, font_size=22, bold=True) self.delete_btn.bind(on_press=self.delete_record) update_buttons.add_widget(self.delete_btn) self.update_frame.add_widget(update_buttons) content_layout.add_widget(self.update_frame) # Initially disable update frame self.set_update_frame_enabled(False) # Status label - larger and more prominent self.status_label = Label( text='Ready', size_hint_y=None, height=50, color=(0, 0.8, 0, 1), font_size=22, bold=True ) content_layout.add_widget(self.status_label) main_layout.add_widget(content_layout) # 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) # Exit button — fixed to the bottom-right corner exit_btn = Button( text='Exit', font_size=20, bold=True, background_color=(0.85, 0.1, 0.1, 1), color=(1, 1, 1, 1), size_hint=(None, None), size=(120, 55), pos_hint={'right': 1, 'y': 0} ) exit_btn.bind(on_press=lambda x: self.stop()) root.add_widget(exit_btn) # Init DB and load data in background so the UI appears immediately def _init_db(dt): def _do(): self.db_manager.init_database() Clock.schedule_once(lambda dt2: self.refresh_data(None)) threading.Thread(target=_do, daemon=True).start() 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() mass_text = self.mass_input.text.strip() self.set_update_frame_enabled(True) # If mass field is empty, just clear update frame if not record_id: self.update_id_input.text = '' self.update_mass_input.text = '' return self.update_id_input.text = record_id self.update_mass_input.text = mass_text def search_record(self, instance): record_id = self.id_input.text.strip() if not record_id: self.show_status("Please enter an ID to search", error=True) return if len(record_id) > 20: self.show_status("ID must be 20 characters or less", error=True) return self.show_status("Searching...", error=False) resolved_id = self._resolve_id(record_id) def _do(): try: 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(resolved_id) else: self.show_status(f"ID '{record_id}' not found in database", error=True) self.mass_input.text = "" Clock.schedule_once(_update) except Exception as e: err = str(e) Clock.schedule_once(lambda dt: self.show_status(f"Search error: {err}", error=True)) threading.Thread(target=_do, daemon=True).start() def add_update_record(self, instance): """Add or update a record from the update frame.""" record_id = self.update_id_input.text.strip() mass_text = self.update_mass_input.text.strip() if not record_id or not mass_text: self.show_status("Please enter both ID and mass in update frame", error=True) return if len(record_id) > 20: self.show_status("ID must be 20 characters or less", error=True) return try: mass = float(mass_text) except ValueError: self.show_status("Mass must be a valid number", error=True) return self.show_status("Saving...", error=False) def _do(): try: success = self.db_manager.add_or_update_record(record_id, mass) def _update(dt): if success: self.show_status(f"Successfully added/updated: {record_id} = {mass}") self.update_id_input.text = "" self.update_mass_input.text = "" self.set_update_frame_enabled(False) self.refresh_data(None) else: self.show_status("Failed to add/update record", error=True) Clock.schedule_once(_update) except Exception as e: err = str(e) Clock.schedule_once(lambda dt: self.show_status(f"Save error: {err}", error=True)) threading.Thread(target=_do, daemon=True).start() def delete_record(self, instance): """Delete a record using the update frame fields.""" record_id = self.update_id_input.text.strip() if not record_id: self.show_status("Please enter an ID in the update fields to delete", error=True) return # Confirm deletion self.show_confirmation_popup( f"Are you sure you want to delete ID '{record_id}'?", lambda: self.confirm_delete(record_id) ) def confirm_delete(self, record_id): """Confirm and execute deletion.""" self.show_status("Deleting...", error=False) def _do(): try: success = self.db_manager.delete_record(record_id) def _update(dt): if success: self.show_status(f"Successfully deleted: {record_id}") self.clear_fields() self.update_id_input.text = "" self.update_mass_input.text = "" self.set_update_frame_enabled(False) self.refresh_data(None) else: self.show_status(f"Failed to delete ID '{record_id}' (not found)", error=True) Clock.schedule_once(_update) except Exception as e: err = str(e) Clock.schedule_once(lambda dt: self.show_status(f"Delete error: {err}", error=True)) threading.Thread(target=_do, daemon=True).start() def clear_fields(self): """Clear the ID and mass fields.""" self.id_input.text = "" self.mass_input.text = "" def reset_values(self, instance): """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) def refresh_data(self, instance): """Refresh the data display (runs in a background thread).""" def _do(): try: records = self.db_manager.read_all_data() count = len(records) Clock.schedule_once(lambda dt: self.show_status(f"Data refreshed - {count} records found")) except Exception as e: err = str(e) Clock.schedule_once(lambda dt: self.show_status(f"Error refreshing data: {err}", error=True)) threading.Thread(target=_do, daemon=True).start() def highlight_record(self, record_id): """Highlight a specific record in the display.""" # Since we removed the data display, just show a status message pass def show_status(self, message, error=False): """Show status message.""" self.status_label.text = message if error: self.status_label.color = (1, 0.2, 0.2, 1) # Red for errors else: self.status_label.color = (0, 0.8, 0, 1) # Green for success # Clear status after 5 seconds Clock.schedule_once(lambda dt: self.clear_status(), 5) def clear_status(self): """Clear the status message.""" self.status_label.text = "Ready" self.status_label.color = (0, 0.8, 0, 1) def show_settings(self, instance): """Show settings popup for database configuration.""" content = BoxLayout(orientation='vertical', spacing=15, padding=20) # Title title_label = Label(text='Database Server Settings', font_size=24, bold=True, size_hint_y=None, height=40) content.add_widget(title_label) # IP address input ip_layout = BoxLayout(orientation='horizontal', size_hint_y=None, height=60, spacing=10) ip_layout.add_widget(Label(text='Server IP Address:', font_size=20, bold=True, size_hint_x=0.4)) ip_input = TextInput( text=self.db_manager.host, multiline=False, font_size=20, size_hint_x=0.6, padding=[10, 10] ) ip_layout.add_widget(ip_input) content.add_widget(ip_layout) # Info label info_label = Label( text='Enter the IP address or hostname of the database server.\nOther connection settings remain unchanged.', font_size=16, size_hint_y=None, height=60 ) content.add_widget(info_label) # Test connection button + result label test_btn = Button(text='Test Connection', font_size=20, bold=True, size_hint_y=None, height=55) content.add_widget(test_btn) test_result_label = Label( text='', font_size=17, size_hint_y=None, height=40, bold=True ) content.add_widget(test_result_label) # Buttons buttons = BoxLayout(size_hint_y=None, height=60, spacing=15) save_btn = Button(text='Save', font_size=20, bold=True) cancel_btn = Button(text='Cancel', font_size=20, bold=True) buttons.add_widget(save_btn) buttons.add_widget(cancel_btn) content.add_widget(buttons) popup = Popup( title='Settings', content=content, size_hint=(0.6, 0.65) ) def save_settings(): new_host = ip_input.text.strip() if new_host: self.db_manager.host = new_host self.db_manager.save_host(new_host) # Reconnect with new settings if self.db_manager.connection: try: self.db_manager.connection.close() except: pass self.db_manager.connection = None self.show_status(f"Database server updated to: {new_host}") popup.dismiss() else: self.show_status("Please enter a valid IP address", error=True) def test_connection(instance): host = ip_input.text.strip() if not host: test_result_label.text = 'Please enter a host first.' test_result_label.color = (1, 0.6, 0, 1) return test_result_label.text = 'Testing...' test_result_label.color = (1, 1, 1, 1) def _do(): success, message = self.db_manager.test_connection(host) def _update(dt): if success: test_result_label.text = f'OK: {message}' test_result_label.color = (0, 0.85, 0, 1) else: test_result_label.text = f'Failed: {message}' test_result_label.color = (1, 0.2, 0.2, 1) Clock.schedule_once(_update) threading.Thread(target=_do, daemon=True).start() test_btn.bind(on_press=test_connection) save_btn.bind(on_press=lambda x: save_settings()) cancel_btn.bind(on_press=popup.dismiss) popup.open() def show_confirmation_popup(self, message, confirm_callback): """Show a confirmation popup.""" content = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text=message, text_size=(300, None), halign='center') content.add_widget(label) buttons = BoxLayout(size_hint_y=None, height=50, spacing=10) yes_btn = Button(text='Yes') no_btn = Button(text='No') buttons.add_widget(yes_btn) buttons.add_widget(no_btn) content.add_widget(buttons) popup = Popup( title='Confirm Action', content=content, size_hint=(0.8, 0.4) ) yes_btn.bind(on_press=lambda x: (confirm_callback(), popup.dismiss())) no_btn.bind(on_press=popup.dismiss) popup.open() if __name__ == '__main__': DatabaseApp().run()