Skip to content

Commit fb288dd

Browse files
committed
v2.2.3
1 parent cf7cbdb commit fb288dd

6 files changed

Lines changed: 229 additions & 96 deletions

File tree

DOCUMENTATION_INDEX.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@
119119
#### [docs/text_input.md](docs/text_input.md)
120120
**Поле ввода текста**
121121
- TextInput(Button), placeholder, value, max_length
122-
- on_change, on_submit, Enter/Escape/TEXTINPUT
122+
- **Типы поля:** input_type ("text" | "int" | "float"), min_val, max_val; для int/float — валидация и фильтрация при вставке
123+
- on_change, on_submit, Enter/Escape, **Ctrl+V** (вставка), **Ctrl+C** (копирование поля)
124+
- Парсинг чисел: модуль **spritePro.input_validation** (parse_input_value, can_add_char, filter_chars_for_paste)
123125
- activate/deactivate, handle_event
124126

125127
#### [docs/text.md](docs/text.md)

docs/API_REFERENCE.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,32 @@ validate_dict(
115115
)
116116
```
117117

118+
## Поля ввода (input_validation)
119+
120+
Модуль `spritePro.input_validation` — типизированный ввод и парсинг для текстовых полей (TextInput и редактор сцен). Типы: **text**, **int**, **float**.
121+
122+
### `InputType`
123+
Тип-алиас: `Literal["text", "int", "float"]`.
124+
125+
### `can_add_char(input_type, current, ch, allowed_text_chars=None)`
126+
Проверяет, можно ли добавить символ `ch` в строку `current` для данного типа поля. Для `"text"` при `allowed_text_chars=None` допускаются любые печатные символы; если передано множество символов — только они (например, для поля «имя» в редакторе).
127+
128+
### `filter_chars_for_paste(input_type, text, allowed_text_chars=None)`
129+
Фильтрует строку `text` (например из буфера обмена), оставляя только допустимые для типа поля символы. Для int — цифры и один минус в начале; для float — цифры, минус, одна точка.
130+
131+
### `parse_input_value(input_type, raw, min_val=None, max_val=None)`
132+
Парсит строку в значение. Возвращает `(ok: bool, value: Any)` — для int/float значение с учётом min_val/max_val, для text — `raw.strip()`. При ошибке парсинга — `(False, None)`.
133+
134+
**Пример:**
135+
```python
136+
from spritePro.input_validation import parse_input_value, InputType
137+
138+
ok, num = parse_input_value("int", "42", 0, 100) # (True, 42)
139+
ok, x = parse_input_value("float", "3.14", 0.0, 10.0) # (True, 3.14)
140+
```
141+
142+
---
143+
118144
## Система плагинов
119145

120146
### `PluginManager`

docs/text_input.md

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# TextInput
22

3-
Поле ввода текста на базе Button. Наследует от Button, при клике переходит в режим ввода (focus); поддерживает `pygame.TEXTINPUT`, Enter (подтверждение) и Escape (сброс фокуса).
3+
Поле ввода текста на базе Button. Наследует от Button, при клике переходит в режим ввода (focus); поддерживает типы **text** / **int** / **float**, `pygame.TEXTINPUT`, Enter (подтверждение), Escape (сброс фокуса), **Ctrl+V** (вставка) и **Ctrl+C** (копирование содержимого поля).
44

55
## Обзор
66

77
- **Наследование**: `TextInput(Button)` — кнопка без анимаций, при клике активирует ввод.
8-
- **События**: **on_change** — при каждом изменении текста (ввод/удаление символа); **on_submit** — при нажатии Enter (применение/подтверждение).
9-
- **Обработка в цикле**: обрабатывает клик (фокус/сброс), `KEYDOWN` (Enter, Escape, Backspace, цифровая клавиатура), `TEXTINPUT` (символы); события берутся из `spritePro.pygame_events` в `update()`.
8+
- **Тип поля**: `input_type`: **"text"** (любой печатный текст), **"int"** (целые числа), **"float"** (дробные). Для int/float некорректные символы не вводятся и отфильтровываются при вставке; при необходимости задают границы `min_val` / `max_val`.
9+
- **События**: **on_change** — при каждом изменении текста; **on_submit** — при нажатии Enter.
10+
- **Обработка в цикле**: обрабатывает клик, `KEYDOWN` (Enter, Escape, Backspace, Ctrl+V/Ctrl+C, цифровая клавиатура), `TEXTINPUT`; события из `spritePro.pygame_events` в `update()`.
1011

1112
## Параметры конструктора
1213

@@ -17,19 +18,31 @@
1718
| `placeholder` | str | "" | Текст-подсказка при пустом значении |
1819
| `value` | str | "" | Начальное значение |
1920
| `max_length` | int | 128 | Максимальная длина |
20-
| `on_change` | Callable[[str], None] | None | Вызывается при каждом изменении текста (ввод/удаление символа) |
21-
| `on_submit` | Callable[[str], None] | None | Вызывается при нажатии Enter (применение/подтверждение) |
21+
| `input_type` | "text" \| "int" \| "float" | "text" | Тип поля: текст, целое или дробное число |
22+
| `min_val` | float \| None | None | Нижняя граница для int/float (при парсинге) |
23+
| `max_val` | float \| None | None | Верхняя граница для int/float (при парсинге) |
24+
| `on_change` | Callable[[str], None] | None | Вызывается при каждом изменении текста |
25+
| `on_submit` | Callable[[str], None] | None | Вызывается при нажатии Enter |
2226
| `text_color` | (int,int,int) | (200,200,200) | Цвет текста |
2327
| `bg_color` | (int,int,int) | (45,45,52) | Цвет фона |
2428
| `active_bg_color` | (int,int,int) | (55,55,62) | Цвет фона при фокусе |
2529
| `font_size` | int | 18 | Размер шрифта |
2630
| `sorting_order` | int | 1000 | Слой отрисовки |
2731
| `scene` | Scene \| str \| None | None | Сцена |
2832

33+
## Типы поля (input_type)
34+
35+
- **text** — допускаются любые печатные символы, пробел и табуляция. Подходит для имён, сообщений.
36+
- **int** — только цифры и один минус в начале. Точка и запятая не вводятся; при вставке из буфера лишние символы отфильтровываются.
37+
- **float** — цифры, один минус в начале, одна десятичная точка (запятая при вставке заменяется на точку).
38+
39+
Для числовых полей при применении (например в `on_submit`) удобно парсить значение через модуль **spritePro.input_validation**: `parse_input_value(input_type, raw, min_val, max_val)` возвращает `(ok, value)`.
40+
2941
## Пример
3042

3143
```python
3244
import spritePro as s
45+
from spritePro.input_validation import parse_input_value
3346

3447
class FormScene(s.Scene):
3548
def __init__(self):
@@ -44,12 +57,26 @@ class FormScene(s.Scene):
4457
on_submit=self._on_submit,
4558
scene=self,
4659
)
60+
self.num_input = s.TextInput(
61+
pos=(s.WH_C.x, 260),
62+
value="0",
63+
input_type="int",
64+
min_val=0,
65+
max_val=100,
66+
on_submit=self._on_number_submit,
67+
scene=self,
68+
)
4769

4870
def _on_change(self, value: str) -> None:
4971
print("Текст:", value)
5072

5173
def _on_submit(self, value: str) -> None:
5274
print("Отправлено:", value)
75+
76+
def _on_number_submit(self, value: str) -> None:
77+
ok, num = parse_input_value("int", value, 0, 100)
78+
if ok and num is not None:
79+
print("Число:", num)
5380
```
5481

5582
## События: изменение и применение
@@ -64,7 +91,9 @@ class FormScene(s.Scene):
6491
- **Enter** — сброс фокуса и вызов `on_submit(value)`.
6592
- **Escape** — сброс фокуса без вызова `on_submit`.
6693
- **Backspace** — удаление последнего символа.
67-
- **TEXTINPUT** и цифровая клавиатура — добавление символов (с учётом `max_length`).
94+
- **Ctrl+V** (Cmd+V на Mac) — вставка из буфера обмена; для int/float вставляются только допустимые символы.
95+
- **Ctrl+C** (Cmd+C на Mac) — копирование содержимого поля в буфер обмена.
96+
- **TEXTINPUT** и цифровая клавиатура — добавление символов (для int/float только допустимые, с учётом `max_length`).
6897

6998
## Методы
7099

@@ -78,3 +107,4 @@ class FormScene(s.Scene):
78107
- [Button](button.md) — базовая кнопка.
79108
- [Slider](slider.md) — слайдер.
80109
- [Input](input.md) — состояние клавиш и мыши.
110+
- Модуль **spritePro.input_validation**`InputType`, `can_add_char`, `filter_chars_for_paste`, `parse_input_value` для типизированного ввода и парсинга (используется TextInput и редактором сцен).

spritePro/editor/ui/input_handling.py

Lines changed: 22 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
"""Обработка ввода: текстовые поля с типами text / int / float."""
1+
"""Обработка ввода: текстовые поля с типами text / int / float (редактор)."""
22

3-
from typing import Any, Literal, Optional, Tuple
3+
from typing import Optional
44

55
import pygame
66
import string
77

8-
InputType = Literal["text", "int", "float"]
8+
from ...input_validation import (
9+
InputType,
10+
can_add_char as _can_add_char,
11+
filter_chars_for_paste as _filter_chars_for_paste,
12+
parse_input_value,
13+
)
914

10-
ALLOWED_INPUT_CHARS = set("0123456789.,-")
11-
ALLOWED_INT_CHARS = set("0123456789-")
1215
ALLOWED_NAME_CHARS = set(string.ascii_letters + string.digits + string.whitespace + "._-()")
1316

1417
KEYPAD_MAP = {
@@ -30,77 +33,16 @@
3033
}
3134

3235

33-
def can_add_char(input_type: InputType, current: str, ch: str) -> bool:
34-
if not ch:
35-
return False
36-
if input_type == "text":
37-
return ch in ALLOWED_NAME_CHARS or (ord(ch) >= 0x20 and ch not in "\x00\r\n")
38-
if input_type == "int":
39-
if ch == "-":
40-
return not current or current == "-"
41-
return ch in "0123456789"
42-
if input_type == "float":
43-
if ch == "-":
44-
return not current or current == "-"
45-
if ch in ".,":
46-
return "." not in current.replace("-", "")
47-
return ch in "0123456789"
48-
return False
49-
50-
51-
def filter_chars_for_paste(input_type: InputType, text: str) -> str:
52-
if input_type == "text":
53-
return "".join(c for c in text if c in ALLOWED_NAME_CHARS or (ord(c) >= 0x20 and c not in "\x00\r\n"))
54-
if input_type == "int":
55-
out = []
56-
for c in text:
57-
if c in "0123456789":
58-
out.append(c)
59-
elif c == "-" and not out:
60-
out.append(c)
61-
return "".join(out)
62-
if input_type == "float":
63-
out = []
64-
seen_dot = False
65-
for c in text:
66-
if c in "0123456789":
67-
out.append(c)
68-
elif c == "-" and not out:
69-
out.append(c)
70-
elif c in ".," and not seen_dot:
71-
out.append(".")
72-
seen_dot = True
73-
return "".join(out)
74-
return ""
75-
76-
77-
def parse_input_value(
78-
input_type: InputType,
79-
raw: str,
80-
min_val: Optional[float] = None,
81-
max_val: Optional[float] = None,
82-
) -> Tuple[bool, Any]:
83-
raw = raw.strip().replace(",", ".")
84-
if not raw or raw == "-":
85-
return False, None
86-
try:
87-
if input_type == "int":
88-
value = int(float(raw))
89-
if min_val is not None:
90-
value = max(int(min_val), value)
91-
if max_val is not None:
92-
value = min(int(max_val), value)
93-
return True, value
94-
if input_type == "float":
95-
value = float(raw)
96-
if min_val is not None:
97-
value = max(min_val, value)
98-
if max_val is not None:
99-
value = min(max_val, value)
100-
return True, value
101-
return True, raw.strip()
102-
except ValueError:
103-
return False, None
36+
def _allowed_for_editor_text(name: str) -> Optional[set]:
37+
return ALLOWED_NAME_CHARS if name == "prop_input_name" else None
38+
39+
40+
def can_add_char(input_type: InputType, current: str, ch: str, name: str = "") -> bool:
41+
return _can_add_char(input_type, current, ch, _allowed_for_editor_text(name) if name else None)
42+
43+
44+
def filter_chars_for_paste(input_type: InputType, text: str, name: str = "") -> str:
45+
return _filter_chars_for_paste(input_type, text, _allowed_for_editor_text(name) if name else None)
10446

10547

10648
def get_active_input_type(editor) -> InputType:
@@ -119,7 +61,7 @@ def _paste_into_editor_buffer(editor, name: str) -> None:
11961
return
12062
text = data.decode("utf-8", errors="replace")
12163
input_type = get_active_input_type(editor)
122-
filtered = filter_chars_for_paste(input_type, text)
64+
filtered = filter_chars_for_paste(input_type, text, name)
12365
if filtered:
12466
editor._text_input_buffers[name] = editor._text_input_buffers.get(name, "") + filtered
12567
except Exception:
@@ -171,13 +113,13 @@ def handle_text_input_keydown(editor, event: pygame.event.Event) -> bool:
171113
if event.key in keypad_no_dot and not (event.unicode or ""):
172114
ch = keypad_no_dot[event.key]
173115
buf = editor._text_input_buffers.get(name, "")
174-
if can_add_char(input_type, buf, ch):
116+
if can_add_char(input_type, buf, ch, name):
175117
editor._text_input_buffers[name] = buf + ch
176118
return True
177119
elif event.key in KEYPAD_MAP and not (event.unicode or ""):
178120
ch = KEYPAD_MAP[event.key]
179121
buf = editor._text_input_buffers.get(name, "")
180-
if can_add_char(input_type, buf, ch):
122+
if can_add_char(input_type, buf, ch, name):
181123
editor._text_input_buffers[name] = buf + ch
182124
return True
183125
return True
@@ -192,7 +134,7 @@ def handle_text_input_text(editor, event: pygame.event.Event) -> bool:
192134
buf = editor._text_input_buffers.get(name, "")
193135
added = ""
194136
for c in (event.text or ""):
195-
if can_add_char(input_type, buf + added, c):
137+
if can_add_char(input_type, buf + added, c, name):
196138
added += c
197139
if added:
198140
editor._text_input_buffers[name] = buf + added

spritePro/input_validation.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Валидация и парсинг полей ввода: типы text / int / float.
2+
3+
Используется компонентом TextInput и редактором сцен.
4+
"""
5+
6+
from typing import Any, Literal, Optional, Set, Tuple
7+
8+
InputType = Literal["text", "int", "float"]
9+
10+
11+
def can_add_char(
12+
input_type: InputType,
13+
current: str,
14+
ch: str,
15+
allowed_text_chars: Optional[Set[str]] = None,
16+
) -> bool:
17+
"""Можно ли добавить символ ch в текущую строку current для данного типа поля."""
18+
if not ch:
19+
return False
20+
if input_type == "text":
21+
if allowed_text_chars is not None:
22+
return ch in allowed_text_chars or (ord(ch) >= 0x20 and ch not in "\x00\r\n")
23+
return ch.isprintable() or ch in " \t"
24+
if input_type == "int":
25+
if ch == "-":
26+
return not current or current == "-"
27+
return ch in "0123456789"
28+
if input_type == "float":
29+
if ch == "-":
30+
return not current or current == "-"
31+
if ch in ".,":
32+
return "." not in current.replace("-", "")
33+
return ch in "0123456789"
34+
return False
35+
36+
37+
def filter_chars_for_paste(
38+
input_type: InputType,
39+
text: str,
40+
allowed_text_chars: Optional[Set[str]] = None,
41+
) -> str:
42+
"""Оставляет в text только допустимые для типа поля символы (для вставки из буфера)."""
43+
if input_type == "text":
44+
if allowed_text_chars is not None:
45+
return "".join(
46+
c for c in text
47+
if c in allowed_text_chars or (ord(c) >= 0x20 and c not in "\x00\r\n")
48+
)
49+
return "".join(c for c in text if c.isprintable() or c in " \t")
50+
if input_type == "int":
51+
out = []
52+
for c in text:
53+
if c in "0123456789":
54+
out.append(c)
55+
elif c == "-" and not out:
56+
out.append(c)
57+
return "".join(out)
58+
if input_type == "float":
59+
out = []
60+
seen_dot = False
61+
for c in text:
62+
if c in "0123456789":
63+
out.append(c)
64+
elif c == "-" and not out:
65+
out.append(c)
66+
elif c in ".," and not seen_dot:
67+
out.append(".")
68+
seen_dot = True
69+
return "".join(out)
70+
return ""
71+
72+
73+
def parse_input_value(
74+
input_type: InputType,
75+
raw: str,
76+
min_val: Optional[float] = None,
77+
max_val: Optional[float] = None,
78+
) -> Tuple[bool, Any]:
79+
"""Парсит строку в значение по типу поля. Возвращает (ok, value)."""
80+
raw = raw.strip().replace(",", ".")
81+
if not raw or raw == "-":
82+
return False, None
83+
try:
84+
if input_type == "int":
85+
value = int(float(raw))
86+
if min_val is not None:
87+
value = max(int(min_val), value)
88+
if max_val is not None:
89+
value = min(int(max_val), value)
90+
return True, value
91+
if input_type == "float":
92+
value = float(raw)
93+
if min_val is not None:
94+
value = max(min_val, value)
95+
if max_val is not None:
96+
value = min(max_val, value)
97+
return True, value
98+
return True, raw.strip()
99+
except ValueError:
100+
return False, None

0 commit comments

Comments
 (0)