Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e14dc70
bugfix: fix 30+ Pi runtime crashes across UI panels and widgets
gmmcosta15 Apr 13, 2026
bc9f17c
test: update networkWindow QTimer patch path to QtCore.QTimer
gmmcosta15 Apr 13, 2026
965193b
fix: add set_filament_state to SensorWidget; remove dead change_fil_s…
gmmcosta15 Apr 13, 2026
f1ebc12
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 May 25, 2026
4686a2b
Merge remote-tracking branch 'origin/dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 16, 2026
ae97e93
fix:resolve conflicts in filamentTab/mainWindow/connectionPage/notifi…
gmmcosta15 Jun 16, 2026
61c40c5
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 16, 2026
e26bdae
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 16, 2026
fca4c48
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 17, 2026
ecde9eb
Merge remote-tracking branch 'origin/dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 19, 2026
23c0ffe
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 19, 2026
49c65e1
Merge remote-tracking branch 'origin/dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 30, 2026
077da1e
Merge branch 'bugfix/ui-panel-crashes' into dev
gmmcosta15 Aug 20, 2026
d53f7a1
Merge remote-tracking branch 'origin/dev' into dev
gmmcosta15 Aug 20, 2026
2e96451
style: ruff format after origin/dev merge
gmmcosta15 Aug 20, 2026
765a631
feat(keyboard): full-screen numeric keypad for IP and mask fields
gmmcosta15 Aug 27, 2026
5b46c46
fix(keyboard): merge dev, keep delete from wiping the prefilled value
gmmcosta15 Sep 25, 2026
158bd60
Merge remote-tracking branch 'origin/dev' into feat/keyboard-numeric-…
gmmcosta15 Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions BlocksScreen/lib/panels/networkWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3721,7 +3721,9 @@ def _setup_keyboard(self) -> None:
(self.wifi_sip_dns2_field, self.wifi_static_ip_page),
]:
field.clicked.connect(
lambda _=False, f=field, p=page: self._on_show_keyboard(p, f)
lambda _=False, f=field, p=page: self._on_show_keyboard(
p, f, numeric=True
)
)

def _setup_scrollbar_signals(self) -> None:
Expand Down Expand Up @@ -3764,11 +3766,16 @@ def _configure_list_view_palette(self) -> None:
self.listView.setPalette(palette)

def _on_show_keyboard(
self, panel: QtWidgets.QWidget, field: QtWidgets.QLineEdit
self,
panel: QtWidgets.QWidget,
field: QtWidgets.QLineEdit,
numeric: bool = False,
) -> None:
"""Show the QWERTY keyboard panel, saving the originating panel and input field."""
self._previous_panel = panel
self._current_field = field
self._qwerty.setPattern("ip" if numeric else "")
self._qwerty.setNumericOnly(numeric)
self._qwerty.set_value(field.text())
self._qwerty.show()
field.clearFocus()
Expand Down
172 changes: 144 additions & 28 deletions BlocksScreen/lib/panels/widgets/keyboardPage.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""On-screen keyboards: full QWERTY and a numeric variant for IP and mask entry."""

import typing

from lib.utils.icon_button import IconButton
from lib.utils.numpad_button import NumpadButton
from PyQt6 import QtCore, QtGui, QtWidgets

_LOWERCASE = list("qwertyuiopasdfghjklzxcvbnm")
Expand Down Expand Up @@ -72,6 +75,30 @@ def _make_key_font(size: int = 29) -> QtGui.QFont:
return font


def _valid_ip(value: str) -> bool:
# Partial entry: empty octets are still being typed.
parts = value.split(".")
return len(parts) <= 4 and all(p.isdigit() and int(p) <= 255 for p in parts if p)


def _valid_float(value: str) -> bool:
if not value:
return True
try:
float(value)
except ValueError:
return value.endswith(".")
return True


_PATTERN_VALIDATORS = {
"ip": _valid_ip,
"hex": lambda v: all(c in "0123456789abcdefABCDEF" for c in v),
"int": lambda v: v == "" or v.lstrip("-").isdigit(),
"float": _valid_float,
}


class CustomQwertyKeyboard(QtWidgets.QDialog):
"""Custom on-screen QWERTY keyboard for touch input."""

Expand All @@ -89,8 +116,11 @@ def __init__(self, parent: QtWidgets.QWidget) -> None:
self.suffix: str = ""
self.symbolsrun: bool = False
self._key_buttons: list[QtWidgets.QPushButton] = []
self._row_widgets: list[QtWidgets.QWidget] = []
self._numpad_digits: list[QtWidgets.QPushButton] = []
self._pattern: str = ""
self._max_length: int = 0
self._numeric_only: bool = False
self.firsttime: bool = True

self._setup_ui()
Expand All @@ -102,6 +132,13 @@ def __init__(self, parent: QtWidgets.QWidget) -> None:
for btn in self._key_buttons:
btn.clicked.connect(lambda _, b=btn: self.value_inserted(b.text()))

for btn in self._numpad_digits:
btn.clicked.connect(lambda _, b=btn: self.value_inserted(b.text()))

self.np_dot.clicked.connect(lambda: self.value_inserted("."))
self.np_delete.clicked.connect(lambda: self.value_inserted("clear"))
self.np_enter.clicked.connect(lambda: self.value_inserted("enter"))

self.K_dot.clicked.connect(lambda: self.value_inserted("."))
self.K_space.clicked.connect(lambda: self.value_inserted(" "))
self.k_Enter.clicked.connect(lambda: self.value_inserted("enter"))
Expand Down Expand Up @@ -132,6 +169,9 @@ def __init__(self, parent: QtWidgets.QWidget) -> None:
" background-color: #212120;"
" color: white;"
"}"
'QPushButton[numpad_key="true"] {'
" font-family: 'Momcake-Bold';"
"}"
)
self.handle_keyboard_layout()

Expand All @@ -147,10 +187,32 @@ def setPattern(self, pattern: str) -> None:
"""Set input validation pattern: 'ip', 'hex', 'int', 'float', or '' for no pattern."""
self._pattern = pattern

def setNumericOnly(self, enabled: bool) -> None:
"""Swap the QWERTY rows for a full-size numpad on IP, mask, gateway and DNS fields."""
if self._numeric_only == enabled:
return
self._numeric_only = enabled
for widget in self._row_widgets:
widget.setVisible(not enabled)
for btn in (
self.K_shift,
self.K_keychange,
self.K_space,
self.K_dot,
self.k_delete,
self.k_Enter,
):
btn.setVisible(not enabled)
self._numpad_widget.setVisible(enabled)
if enabled:
self.K_shift.setChecked(False)
self.K_keychange.setChecked(False)
self.symbolsrun = False
self.handle_keyboard_layout()

def setMaxLength(self, length: int) -> None:
"""Set maximum allowed length for user input (excluding prefix/suffix)."""
if length < 0:
length = 0
length = max(length, 0)
if length == 0:
length = 999
self._max_length = length
Expand All @@ -162,29 +224,9 @@ def _flash_limit_warning(self) -> None:
)

def _validate_pattern(self, value: str) -> bool:
if not self._pattern:
return True
if self._pattern == "ip":
parts = value.split(".")
if len(parts) > 4:
return False
for part in parts:
if part and (not part.isdigit() or int(part) > 255):
return False
return True
if self._pattern == "hex":
return all(c in "0123456789abcdefABCDEF" for c in value)
if self._pattern == "int":
return value == "" or value.lstrip("-").isdigit()
if self._pattern == "float":
if not value:
return True
try:
float(value)
return True
except ValueError:
return value.endswith(".")
return True
"""Return True if value is an acceptable partial entry for the active pattern."""
validator = _PATTERN_VALIDATORS.get(self._pattern or "")
return validator(value) if validator else True

def _get_mainWindow_widget(self) -> QtWidgets.QMainWindow | None:
"""Get the main application window"""
Expand Down Expand Up @@ -213,10 +255,12 @@ def _geometry_calc(self) -> None:
self.setGeometry(x, y, width, height)

def showEvent(self, a0: QtGui.QShowEvent | None) -> None:
"""Re-implemented method, recompute layout geometry before showing."""
self._geometry_calc()
self.firsttime = True
return super().showEvent(a0)

@QtCore.pyqtSlot()
def handle_keyboard_layout(self) -> None:
"""Update key labels based on current shift/keychange state."""
shift = self.K_shift.isChecked()
Expand All @@ -239,17 +283,19 @@ def handle_keyboard_layout(self) -> None:
else:
layout = _LOWERCASE

for btn, txt in zip(self._key_buttons, layout):
for btn, txt in zip(self._key_buttons, layout, strict=False):
btn.setText(txt)

self.K_shift.setText("#+=") if keychange else self.K_shift.setText("⇧")

def value_inserted(self, value: str) -> None:
"""Handle key press: append char, delete, or submit on enter."""

if self.firsttime and value not in ("enter", "clear"):
self.current_value = ""
if self.firsttime:
self.firsttime = False
# Only a typed key replaces the prefill; delete edits it in place.
if value not in ("enter", "clear"):
self.current_value = ""

if value == "&&":
value = "&"
Expand All @@ -262,6 +308,7 @@ def value_inserted(self, value: str) -> None:
self.setSuffix("")
self.setPattern("")
self.setMaxLength(0)
self.setNumericOnly(False)
return

if value == "clear":
Expand Down Expand Up @@ -322,6 +369,71 @@ def _create_key_button(
btn.setObjectName(name)
return btn

def _create_numpad_button(self, text: str, name: str) -> NumpadButton:
"""Create a pill key matching the CustomNumpad look."""
btn = NumpadButton(self._numpad_widget)
btn.setSizePolicy(
QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed
)
btn.setMinimumSize(QtCore.QSize(150, 60))
btn.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft)
btn.setFlat(True)
btn.setText(text)
btn.setProperty("numpad_key", True)
btn.setObjectName(name)
return btn

def _create_numpad_icon(self, name: str, pixmap: str) -> IconButton:
"""Create a 60x60 icon key for the numpad enter/clear actions."""
btn = IconButton(parent=self._numpad_widget)
btn.setSizePolicy(
QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed
)
btn.setMinimumSize(QtCore.QSize(60, 60))
btn.setMaximumSize(QtCore.QSize(60, 60))
btn.setFlat(True)
btn.setProperty("icon_pixmap", QtGui.QPixmap(pixmap))
btn.setProperty("button_type", "icon")
btn.setObjectName(name)
return btn

def _setup_numpad(self) -> None:
"""Build the digits-only pad shown in place of the QWERTY rows."""
self._numpad_widget = QtWidgets.QWidget(parent=self)
self._numpad_widget.setGeometry(QtCore.QRect(90, 150, 620, 280))
grid = QtWidgets.QGridLayout(self._numpad_widget)
grid.setContentsMargins(0, 0, 0, 0)
grid.setSpacing(6)
grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)

self._numpad_digits = []
for idx, digit in enumerate("789456123"):
btn = self._create_numpad_button(digit, f"np_{digit}")
btn.setProperty("position", ("left", "", "right")[idx % 3])
grid.addWidget(btn, idx // 3, idx % 3)
self._numpad_digits.append(btn)

# Bottom row keeps the digit grid: "." left, "0" centred under 8/5/2.
zero = self._create_numpad_button("0", "np_0")
zero.setProperty("position", "")
grid.addWidget(zero, 3, 1)
self._numpad_digits.append(zero)

self.np_dot = self._create_numpad_button(".", "np_dot")
self.np_dot.setProperty("position", "left")
grid.addWidget(self.np_dot, 3, 0)

self.np_delete = self._create_numpad_icon(
"np_delete", ":/dialog/media/btn_icons/no.svg"
)
self.np_enter = self._create_numpad_icon(
"np_enter", ":/dialog/media/btn_icons/yes.svg"
)
grid.addWidget(self.np_delete, 0, 3, 2, 1, QtCore.Qt.AlignmentFlag.AlignCenter)
grid.addWidget(self.np_enter, 2, 3, 2, 1, QtCore.Qt.AlignmentFlag.AlignCenter)

self._numpad_widget.setVisible(False)

def _setup_ui(self) -> None:
self.setObjectName("self")
self.resize(800, 480)
Expand Down Expand Up @@ -376,6 +488,8 @@ def _setup_ui(self) -> None:
row3_layout.addWidget(btn)
self._key_buttons.append(btn)

self._row_widgets = [row1_widget, row2_widget, row3_widget]

# Shift button (left of row 3)
self.K_shift = QtWidgets.QPushButton(parent=self)
self.K_shift.setGeometry(QtCore.QRect(10, 280, 81, 51))
Expand Down Expand Up @@ -420,6 +534,8 @@ def _setup_ui(self) -> None:
self.k_Enter.setAutoRepeat(False)
self.k_Enter.setObjectName("k_Enter")

self._setup_numpad()

# Back button (top-right)
self.numpad_back_btn = IconButton(parent=self)
self.numpad_back_btn.setGeometry(QtCore.QRect(720, 20, 60, 60))
Expand Down
Loading
Loading