feat: responsive layout scaling + centered content

- 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)
This commit is contained in:
2026-04-07 16:04:39 +03:00
parent e0f510cebc
commit 65c34314b0
2 changed files with 152 additions and 85 deletions

237
main.py
View File

@@ -22,164 +22,231 @@ class DatabaseApp(App):
self.active_numpad_input = None
def build(self):
# Set window to fullscreen
# Set window to fullscreen first so Window.height reflects the screen
Window.fullscreen = 'auto'
# Root float layout so we can overlay the exit button
# ------------------------------------------------------------------
# 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 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)
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 label + Exit button on the right
title_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=50, spacing=15)
title = Label(text='Database Search & Update', font_size=28, bold=True)
# 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=18,
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=110
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
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))
# 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 and press Enter to search (max 20 chars)',
readonly=False,
font_size=20,
padding=[10, 10]
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=20, bold=True))
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 value (read-only)',
readonly=True,
font_size=20,
padding=[10, 10]
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: 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)
# 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=18,
bold=True,
color=(0.4, 0.8, 1, 1)
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=16,
bold=True,
background_color=(0.3, 0.3, 0.3, 1)
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)
# 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)
# 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=22, bold=True)
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=22, bold=True)
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)
# Minimal spacing between buttons and update frame
content_layout.add_widget(Label(size_hint_y=None, height=10))
# 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)
# 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=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 = 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=20, bold=True))
self.update_mass_input = TextInput(multiline=False, size_hint_x=0.75, readonly=True, font_size=20, padding=[10, 10])
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)
# 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)
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=22, bold=True)
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)
# Initially disable update frame
self.set_update_frame_enabled(False)
# Status label - larger and more prominent
# Status label
self.status_label = Label(
text='Ready',
size_hint_y=None,
height=50,
color=(0, 0.8, 0, 1),
font_size=22,
bold=True
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)
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)
# 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=28, bold=True)
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=28, bold=True, background_color=(0.25, 0.25, 0.45, 1))
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=28, bold=True)
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=28, bold=True, background_color=(0.5, 0.3, 0.1, 1))
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=26, bold=True,
background_color=(0.1, 0.55, 0.1, 1), size_hint_y=None, height=64)
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)