- All widget heights, fonts, padding and spacing derived from Window.height so layout fits any screen (800p, 1080p, etc.) - Content block wrapped in AnchorLayout (anchor_y=center) so it sits centered in the space above the numpad instead of bottom-aligned - Rebuilt DatabaseApp.exe (30.9 MB)
634 lines
27 KiB
Python
634 lines
27 KiB
Python
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 first so Window.height reflects the screen
|
|
Window.fullscreen = 'auto'
|
|
|
|
# ------------------------------------------------------------------
|
|
# Responsive sizing: derive all dimensions from the actual screen height
|
|
# so the layout fits on any display (800p, 900p, 1080p, etc.)
|
|
# ------------------------------------------------------------------
|
|
wh = Window.height # actual screen height after fullscreen
|
|
|
|
# Numpad occupies 29% of screen height (fixed proportion)
|
|
h_numpad_wr = int(wh * 0.29)
|
|
|
|
# Main layout outer padding and spacing (scaled)
|
|
s = max(0.65, min(1.0, wh / 1080.0))
|
|
pad_v = max(8, int(20 * s)) # top / bottom padding
|
|
pad_h = max(8, int(30 * s)) # left / right padding
|
|
m_spacing = max(4, int(10 * s)) # gap between content and numpad
|
|
|
|
# Space available for the 6 content rows (after numpad + padding + gap)
|
|
avail_total = wh - h_numpad_wr - 2 * pad_v - m_spacing
|
|
c_spacing = max(4, int(12 * s)) # gap between content rows
|
|
avail_items = avail_total - c_spacing * 5 # 6 rows → 5 gaps
|
|
|
|
# Distribute height proportionally among rows
|
|
# Reference weights: title=50, search=100, mode=38, buttons=65, update=187, status=40
|
|
_w = [50, 100, 38, 65, 187, 40]
|
|
_t = sum(_w)
|
|
def _h(weight):
|
|
return max(24, int(avail_items * weight / _t))
|
|
|
|
h_title = _h(_w[0])
|
|
h_search = _h(_w[1])
|
|
h_row = max(20, h_search // 2)
|
|
h_mode = _h(_w[2])
|
|
h_buttons = _h(_w[3])
|
|
h_update = _h(_w[4])
|
|
h_status = _h(_w[5])
|
|
|
|
# Update-frame internal heights
|
|
upd_pad = max(4, int(8 * s))
|
|
upd_spc = max(4, int(8 * s))
|
|
h_upd_title = max(20, int(h_update * 0.20))
|
|
h_upd_row = max(20, int(h_update * 0.24))
|
|
h_upd_inputs = h_upd_row * 2 + upd_spc
|
|
h_upd_btns = max(28, h_update - h_upd_title - h_upd_inputs - 2*upd_pad - 2*upd_spc)
|
|
|
|
# Numpad internal heights
|
|
np_pad_v = max(3, int(6 * s))
|
|
np_spc = max(3, int(6 * s))
|
|
h_enter_btn = max(34, int(h_numpad_wr * 0.24))
|
|
h_numpad_gr = h_numpad_wr - h_enter_btn - 2 * np_pad_v - np_spc
|
|
|
|
# Font sizes (scaled)
|
|
f_title = max(14, int(26 * s))
|
|
f_normal = max(11, int(18 * s))
|
|
f_btn = max(12, int(20 * s))
|
|
f_numpad = max(15, int(26 * s))
|
|
f_enter = max(14, int(24 * s))
|
|
f_mode = max(10, int(16 * s))
|
|
f_override = max(10, int(14 * s))
|
|
f_status = max(12, int(20 * s))
|
|
sp = max(6, int(10 * s)) # generic widget spacing
|
|
|
|
# ------------------------------------------------------------------
|
|
# Build UI
|
|
# ------------------------------------------------------------------
|
|
root = FloatLayout()
|
|
|
|
main_layout = BoxLayout(
|
|
orientation='vertical',
|
|
padding=[pad_h, pad_v, pad_h, pad_v],
|
|
spacing=m_spacing,
|
|
size_hint=(1, 1),
|
|
pos_hint={'x': 0, 'y': 0}
|
|
)
|
|
|
|
# --- Content container (fills remaining space above numpad) ---
|
|
content_layout = BoxLayout(orientation='vertical', spacing=c_spacing, size_hint_y=None)
|
|
content_layout.bind(minimum_height=content_layout.setter('height'))
|
|
|
|
# Title row: title + Exit button
|
|
title_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=h_title, spacing=sp)
|
|
title = Label(text='Database Search & Update', font_size=f_title, bold=True)
|
|
title_row.add_widget(title)
|
|
exit_btn = Button(
|
|
text='Exit',
|
|
font_size=max(11, int(16 * s)),
|
|
bold=True,
|
|
background_color=(0.85, 0.1, 0.1, 1),
|
|
color=(1, 1, 1, 1),
|
|
size_hint_x=None,
|
|
width=max(70, int(100 * s))
|
|
)
|
|
exit_btn.bind(on_press=lambda x: self.stop())
|
|
title_row.add_widget(exit_btn)
|
|
content_layout.add_widget(title_row)
|
|
|
|
# Search section (ID + Mass)
|
|
search_layout = GridLayout(
|
|
cols=2, size_hint_y=None, height=h_search,
|
|
spacing=sp, row_force_default=True, row_default_height=h_row
|
|
)
|
|
search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
|
self.id_input = TextInput(
|
|
multiline=False, size_hint_x=0.75,
|
|
hint_text='Enter ID (max 20 chars)',
|
|
readonly=False, font_size=f_normal, padding=[7, 7]
|
|
)
|
|
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=f_normal, bold=True))
|
|
self.mass_input = TextInput(
|
|
multiline=False, size_hint_x=0.75,
|
|
hint_text='Mass (read-only)',
|
|
readonly=True, font_size=f_normal, padding=[7, 7]
|
|
)
|
|
search_layout.add_widget(self.mass_input)
|
|
content_layout.add_widget(search_layout)
|
|
|
|
# Mode indicator row
|
|
self.manual_override = None
|
|
mode_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=h_mode, spacing=sp)
|
|
self.mode_label = Label(
|
|
text='Article type detected: PRODUCT',
|
|
size_hint_x=0.75, font_size=f_mode, 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=f_override, 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)
|
|
|
|
# Action buttons (Add/Update, Reset, Settings)
|
|
button_layout = GridLayout(cols=3, size_hint_y=None, height=h_buttons, spacing=sp)
|
|
add_update_btn = Button(text='Add/Update', font_size=f_btn, 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=f_btn, bold=True)
|
|
reset_btn.bind(on_press=self.reset_values)
|
|
button_layout.add_widget(reset_btn)
|
|
settings_btn = Button(text='Settings', font_size=f_btn, bold=True)
|
|
settings_btn.bind(on_press=self.show_settings)
|
|
button_layout.add_widget(settings_btn)
|
|
content_layout.add_widget(button_layout)
|
|
|
|
# Update frame
|
|
self.update_frame = BoxLayout(
|
|
orientation='vertical', padding=upd_pad, spacing=upd_spc,
|
|
size_hint_y=None, height=h_update
|
|
)
|
|
self.update_frame_label = Label(
|
|
text='Update Values', size_hint_y=None, height=h_upd_title,
|
|
font_size=f_btn, bold=True
|
|
)
|
|
self.update_frame.add_widget(self.update_frame_label)
|
|
update_inputs = GridLayout(
|
|
cols=2, spacing=sp, size_hint_y=None, height=h_upd_inputs,
|
|
row_force_default=True, row_default_height=h_upd_row
|
|
)
|
|
update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
|
self.update_id_input = TextInput(
|
|
multiline=False, size_hint_x=0.75, readonly=True, font_size=f_normal, padding=[7, 7]
|
|
)
|
|
update_inputs.add_widget(self.update_id_input)
|
|
update_inputs.add_widget(Label(text='Mass:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
|
self.update_mass_input = TextInput(
|
|
multiline=False, size_hint_x=0.75, readonly=True, font_size=f_normal, padding=[7, 7]
|
|
)
|
|
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)
|
|
update_buttons = GridLayout(cols=2, size_hint_y=None, height=h_upd_btns, spacing=sp)
|
|
self.update_confirm_btn = Button(text='Confirm Add/Update', disabled=True, font_size=f_btn, 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=f_btn, 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)
|
|
|
|
self.set_update_frame_enabled(False)
|
|
|
|
# Status label
|
|
self.status_label = Label(
|
|
text='Ready', size_hint_y=None, height=h_status,
|
|
color=(0, 0.8, 0, 1), font_size=f_status, bold=True
|
|
)
|
|
content_layout.add_widget(self.status_label)
|
|
|
|
# Wrap content in an AnchorLayout that fills all space above the numpad
|
|
# so the content block is vertically centred regardless of screen size
|
|
content_anchor = AnchorLayout(anchor_x='center', anchor_y='center', size_hint_y=1)
|
|
content_anchor.add_widget(content_layout)
|
|
main_layout.add_widget(content_anchor)
|
|
|
|
# --- Numeric keypad ---
|
|
numpad_wrapper = BoxLayout(
|
|
orientation='vertical', size_hint_y=None, height=h_numpad_wr,
|
|
spacing=np_spc, padding=[pad_h, np_pad_v, pad_h, np_pad_v]
|
|
)
|
|
numpad = GridLayout(cols=3, size_hint_y=None, height=h_numpad_gr, spacing=max(3, int(6 * s)))
|
|
for digit in ['1', '2', '3', '4', '5', '6', '7', '8', '9']:
|
|
btn = Button(text=digit, font_size=f_numpad, bold=True)
|
|
btn.bind(on_press=self.numpad_press)
|
|
numpad.add_widget(btn)
|
|
dot_btn = Button(text='.', font_size=f_numpad, 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=f_numpad, bold=True)
|
|
zero_btn.bind(on_press=self.numpad_press)
|
|
numpad.add_widget(zero_btn)
|
|
back_btn = Button(text='⌫', font_size=f_numpad, 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=f_enter, bold=True,
|
|
background_color=(0.1, 0.55, 0.1, 1), size_hint_y=None, height=h_enter_btn
|
|
)
|
|
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)
|
|
|
|
# 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() |