ren-ui-python 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/PKG-INFO +1 -1
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/README.md +33 -33
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/pyproject.toml +1 -1
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/__init__.py +1 -1
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/app.py +4 -4
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/widgets.py +19 -19
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui_python.egg-info/PKG-INFO +1 -1
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/tests/test_samples.py +3 -3
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/tests/test_widgets.py +4 -4
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/LICENSE +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/PYPI.md +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/setup.cfg +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/__main__.py +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/_native.py +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/build.py +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/cli.py +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/native/bridge.cpp +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/native/icon.cpp +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/native/launcher.cpp +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/project.py +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/py.typed +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/resources.py +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui/system.py +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui_python.egg-info/SOURCES.txt +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui_python.egg-info/dependency_links.txt +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui_python.egg-info/entry_points.txt +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/src/ren_ui_python.egg-info/top_level.txt +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/tests/test_build.py +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/tests/test_images.py +0 -0
- {ren_ui_python-0.1.0 → ren_ui_python-0.2.0}/tests/test_project.py +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Python 코드로 **RenkuOS 네이티브 UI와 앱을 작성하고 `.hpkg`로 패키징**하는 프레임워크입니다.
|
|
4
4
|
웹뷰나 다른 OS의 위젯을 흉내 내지 않고, C++ 브리지를 통해 Haiku의 `libbe`를 호출합니다.
|
|
5
5
|
|
|
6
|
-
현재 버전은 **0.
|
|
6
|
+
현재 버전은 **0.2.0 초기 구현**입니다. 아래에 명시한 API를 지원하며, Haiku API 전체에 대한 바인딩은 아닙니다.
|
|
7
7
|
Python 3.11 이상이 필요합니다. macOS/Linux에서는 프로젝트 생성·검사·소스 스테이징·단위 테스트가 가능하고,
|
|
8
8
|
네이티브 UI 실행과 실제 HPKG 빌드는 Haiku에서 수행합니다.
|
|
9
9
|
|
|
@@ -12,10 +12,10 @@ Python 3.11 이상이 필요합니다. macOS/Linux에서는 프로젝트 생성
|
|
|
12
12
|
|
|
13
13
|
## 빠른 시작
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
[PyPI](https://pypi.org/project/ren-ui-python/)에서 설치합니다.
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
python3 -m pip install
|
|
18
|
+
python3 -m pip install ren-ui-python
|
|
19
19
|
ren init hello_haiku
|
|
20
20
|
ren check hello_haiku
|
|
21
21
|
```
|
|
@@ -52,8 +52,8 @@ python3 -m ren_ui build examples/hello
|
|
|
52
52
|
## 샘플 앱
|
|
53
53
|
|
|
54
54
|
각 디렉터리에 앱 코드, `ren.toml`, `LICENSE`가 들어 있습니다.
|
|
55
|
-
|
|
56
|
-
이전에
|
|
55
|
+
`python3 -m pip install ren-ui-python`으로 프레임워크를 설치한 뒤 Haiku에서 실행하세요.
|
|
56
|
+
이전에 설치했다면 `python3 -m pip install --upgrade ren-ui-python`으로 최신 버전으로 올립니다.
|
|
57
57
|
|
|
58
58
|
| 샘플 | 코드 | 배우는 내용 |
|
|
59
59
|
| --- | --- | --- |
|
|
@@ -142,7 +142,7 @@ class DialogDemo:
|
|
|
142
142
|
),
|
|
143
143
|
width=560, height=240,
|
|
144
144
|
# 메인 창을 닫으면 열려 있는 입력 창도 함께 종료합니다.
|
|
145
|
-
|
|
145
|
+
onclosed=self.quit,
|
|
146
146
|
)
|
|
147
147
|
|
|
148
148
|
def quit(self, event: Event):
|
|
@@ -195,7 +195,7 @@ class DialogDemo:
|
|
|
195
195
|
hint, name,
|
|
196
196
|
Row(apply_button, Button("취소", cancel), padding=0),
|
|
197
197
|
),
|
|
198
|
-
width=400, height=180, resizable=False,
|
|
198
|
+
width=400, height=180, resizable=False, onclosed=closed,
|
|
199
199
|
)
|
|
200
200
|
self.input_window = dialog.show()
|
|
201
201
|
|
|
@@ -375,7 +375,7 @@ class EditorDemo:
|
|
|
375
375
|
self.draft_text: str | None = None
|
|
376
376
|
self.draft_error: str | None = None
|
|
377
377
|
self.path = TextInput(label="파일 경로:")
|
|
378
|
-
self.editor = TextArea(
|
|
378
|
+
self.editor = TextArea(onchange=self.remember_change)
|
|
379
379
|
self.status = Label("파일의 전체 경로를 입력하고 열기 또는 저장을 누르세요.")
|
|
380
380
|
self.window = Window(
|
|
381
381
|
app, "Ren · 텍스트 에디터",
|
|
@@ -612,12 +612,12 @@ with Application("application/x-vnd.example-hello") as app:
|
|
|
612
612
|
| `system.settings_directory` | `find_directory` | 사용자 설정 디렉터리 |
|
|
613
613
|
|
|
614
614
|
버튼은 `@button.onclick` 데코레이터로 클릭 함수를 등록합니다. 생성자의 `onclick` 인자로 전달해도 됩니다.
|
|
615
|
-
입력·체크박스·슬라이더는 `
|
|
615
|
+
입력·체크박스·슬라이더는 `onchange`에 함수를 전달합니다.
|
|
616
616
|
콜백 인자는 `Event(source, kind, text, value)`입니다. `text`와 `value`는 이벤트 발생 시점의 스냅샷입니다.
|
|
617
617
|
`TextInput`은 편집 및 Enter 입력 시, 슬라이더는 드래그 및 확정 시 알림을 보내므로 같은 값의 이벤트가 반복될 수 있습니다.
|
|
618
618
|
`TextArea`는 편집 시와 표시된 위젯의 `.text`에 대입할 때 변경 이벤트를 보냅니다.
|
|
619
619
|
`@window.onclose`는 닫기 전 확인 함수이며 `True`를 반환해야 닫힙니다.
|
|
620
|
-
기존 `Window.
|
|
620
|
+
기존 `Window.onclosed`는 창이 이미 닫힌 후 호출되는 별도의 알림입니다.
|
|
621
621
|
마지막 창을 닫으면 앱도 종료됩니다.
|
|
622
622
|
|
|
623
623
|
## 문법과 API 사용법
|
|
@@ -729,7 +729,7 @@ app.call_soon(started)
|
|
|
729
729
|
### Window: 창 구성
|
|
730
730
|
|
|
731
731
|
```text
|
|
732
|
-
Window(app, title, content, *, width=480, height=280, resizable=True, onclose=None,
|
|
732
|
+
Window(app, title, content, *, width=480, height=280, resizable=True, onclose=None, onclosed=None)
|
|
733
733
|
```
|
|
734
734
|
|
|
735
735
|
`content`에는 위젯 또는 레이아웃 하나를 전달합니다. 여러 위젯을 넣으려면 `Column`이나 `Row`로 묶습니다.
|
|
@@ -750,7 +750,7 @@ window = Window(
|
|
|
750
750
|
width=520,
|
|
751
751
|
height=320,
|
|
752
752
|
resizable=False,
|
|
753
|
-
|
|
753
|
+
onclosed=closed,
|
|
754
754
|
)
|
|
755
755
|
window.show()
|
|
756
756
|
```
|
|
@@ -784,13 +784,13 @@ def before_close(event: Event) -> bool:
|
|
|
784
784
|
창 테두리의 닫기 버튼과 표시된 창의 `window.close()` 모두 이 확인을 거칩니다.
|
|
785
785
|
확인 중 중복 요청은 합치며, 핸들러는 네이티브 창 잠금을 해제한 Python 이벤트 처리 스레드에서 실행됩니다.
|
|
786
786
|
핸들러 안에서는 `window.close()`를 다시 호출할 필요 없이 결과만 반환하세요.
|
|
787
|
-
`window.close()`는 실제 닫힘을 기다리지 않습니다. 완료 처리는 `
|
|
787
|
+
`window.close()`는 실제 닫힘을 기다리지 않습니다. 완료 처리는 `onclosed`에서 수행합니다.
|
|
788
788
|
`app.quit()`도 각 창의 확인을 거칩니다. 허용한 창은 닫히고 취소한 창은 남으며, 모든 창이 닫힌 뒤 앱이 종료됩니다.
|
|
789
789
|
종료 버튼에서 특정 창만 닫으려면 `event: Event`를 받는 핸들러 안에서 `window.close()`를 호출하세요.
|
|
790
790
|
실행 전·종료 후 자원 정리인 `app.close()`는 확인 없이 자원을 해제합니다.
|
|
791
791
|
아직 `show()`하지 않은 창의 `close()`는 네이티브 창이 없으므로 핸들러 없이 객체를 닫습니다.
|
|
792
792
|
|
|
793
|
-
`
|
|
793
|
+
`onclosed`는 닫기 완료 알림이므로 콜백에서 그 창의 컨트롤을 수정하지 않습니다.
|
|
794
794
|
콜백의 반환값으로 닫기를 취소할 수 없습니다.
|
|
795
795
|
`width`, `height`, `resizable`, `content`는 표시 전 구성에 사용하며,
|
|
796
796
|
창 표시 후 해당 필드에 대입하는 방식으로 창 크기나 내용을 변경하는 API는 제공하지 않습니다.
|
|
@@ -847,10 +847,10 @@ content = Column(*labels)
|
|
|
847
847
|
```text
|
|
848
848
|
Label(text="")
|
|
849
849
|
Button(text, onclick=None, *, enabled=True, icon=None, icon_size=24)
|
|
850
|
-
TextInput(text="", *, label="",
|
|
851
|
-
TextArea(text="", *,
|
|
852
|
-
CheckBox(text, *, checked=False,
|
|
853
|
-
Slider(text="", *, minimum=0, maximum=100, value=0,
|
|
850
|
+
TextInput(text="", *, label="", onchange=None, enabled=True)
|
|
851
|
+
TextArea(text="", *, onchange=None, editable=True, wrap=True)
|
|
852
|
+
CheckBox(text, *, checked=False, onchange=None, enabled=True)
|
|
853
|
+
Slider(text="", *, minimum=0, maximum=100, value=0, onchange=None)
|
|
854
854
|
```
|
|
855
855
|
|
|
856
856
|
| 위젯 | 인자의 의미 | 생성 예 |
|
|
@@ -878,7 +878,7 @@ from ren_ui import Event, TextArea
|
|
|
878
878
|
def document_changed(event: Event):
|
|
879
879
|
print("변경된 내용:", event.text)
|
|
880
880
|
|
|
881
|
-
editor = TextArea("첫 줄\n둘째 줄",
|
|
881
|
+
editor = TextArea("첫 줄\n둘째 줄", onchange=document_changed)
|
|
882
882
|
preview = TextArea("선택·복사 가능한 읽기 전용 텍스트", editable=False)
|
|
883
883
|
```
|
|
884
884
|
|
|
@@ -944,9 +944,9 @@ def checked_changed(event: Event):
|
|
|
944
944
|
def volume_changed(event: Event):
|
|
945
945
|
print("음량:", event.value)
|
|
946
946
|
|
|
947
|
-
name = TextInput(label="이름:",
|
|
948
|
-
enabled = CheckBox("사용",
|
|
949
|
-
volume = Slider("음량",
|
|
947
|
+
name = TextInput(label="이름:", onchange=text_changed)
|
|
948
|
+
enabled = CheckBox("사용", onchange=checked_changed)
|
|
949
|
+
volume = Slider("음량", onchange=volume_changed)
|
|
950
950
|
```
|
|
951
951
|
|
|
952
952
|
`Event`는 읽기 전용 객체입니다. 처리 함수의 반환값은 사용하지 않습니다.
|
|
@@ -1051,7 +1051,7 @@ def main():
|
|
|
1051
1051
|
|
|
1052
1052
|
content = Column(
|
|
1053
1053
|
result,
|
|
1054
|
-
CheckBox("증가 버튼 사용", checked=True,
|
|
1054
|
+
CheckBox("증가 버튼 사용", checked=True, onchange=toggle),
|
|
1055
1055
|
Row(increment_button, Button("초기화", reset), padding=0),
|
|
1056
1056
|
Button("종료", quit_app),
|
|
1057
1057
|
)
|
|
@@ -1360,7 +1360,7 @@ PNG를 앱 아이콘과 버튼 아이콘으로 사용한 설치 앱입니다.
|
|
|
1360
1360
|
|
|
1361
1361
|
Python으로 Haiku 네이티브 앱을 만들고 HPKG로 배포하는 방법을 설명합니다. 설치와 첫 앱 작성부터 이벤트 처리, 샘플의 조작법, 패키징까지 순서대로 따라 할 수 있습니다.
|
|
1362
1362
|
|
|
1363
|
-
**대상 버전:** 0.
|
|
1363
|
+
**대상 버전:** 0.2.0 / 네이티브 브리지 ABI v5
|
|
1364
1364
|
**문서 기준:** 2026-10-05 / Python 3.11 이상
|
|
1365
1365
|
|
|
1366
1366
|

|
|
@@ -1394,10 +1394,10 @@ Python으로 Haiku 네이티브 앱을 만들고 HPKG로 배포하는 방법을
|
|
|
1394
1394
|
| package, rc, xres, resattr, mimeset | HPKG와 앱 리소스 생성 |
|
|
1395
1395
|
| 실행 중인 Haiku 데스크톱 | 실제 창과 위젯 표시 |
|
|
1396
1396
|
|
|
1397
|
-
|
|
1397
|
+
다음 명령을 실행합니다. 필요한 도구는 사용 중인 Haiku 환경에 맞춰 먼저 설치합니다.
|
|
1398
1398
|
|
|
1399
1399
|
```sh
|
|
1400
|
-
python3 -m pip install
|
|
1400
|
+
python3 -m pip install ren-ui-python
|
|
1401
1401
|
ren doctor
|
|
1402
1402
|
ren init manual_demo
|
|
1403
1403
|
ren check manual_demo
|
|
@@ -1550,7 +1550,7 @@ def save(event: Event):
|
|
|
1550
1550
|
|
|
1551
1551
|
#### 텍스트와 값 변경
|
|
1552
1552
|
|
|
1553
|
-
입력창·텍스트 영역·체크박스·슬라이더는 생성자의 `
|
|
1553
|
+
입력창·텍스트 영역·체크박스·슬라이더는 생성자의 `onchange`에 함수를 전달합니다. 이 위젯들에는 `onclick` 데코레이터를 사용하지 않습니다.
|
|
1554
1554
|
|
|
1555
1555
|
```python
|
|
1556
1556
|
from ren_ui import Event, TextInput, CheckBox
|
|
@@ -1558,11 +1558,11 @@ from ren_ui import Event, TextInput, CheckBox
|
|
|
1558
1558
|
def name_changed(event: Event):
|
|
1559
1559
|
print("입력:", event.text)
|
|
1560
1560
|
|
|
1561
|
-
def
|
|
1561
|
+
def optionchanged(event: Event):
|
|
1562
1562
|
print("체크:", bool(event.value))
|
|
1563
1563
|
|
|
1564
|
-
name = TextInput(label="이름:",
|
|
1565
|
-
option = CheckBox("알림",
|
|
1564
|
+
name = TextInput(label="이름:", onchange=name_changed)
|
|
1565
|
+
option = CheckBox("알림", onchange=optionchanged)
|
|
1566
1566
|
```
|
|
1567
1567
|
|
|
1568
1568
|
입력 확정이나 드래그 때문에 같은 값의 이벤트가 반복될 수 있습니다. `TextArea.text`에 대입해도 변경 이벤트가 발생하므로, 자신의 변경 핸들러에서 같은 값을 계속 대입하지 않도록 합니다.
|
|
@@ -1615,14 +1615,14 @@ def before_close(event: Event) -> bool:
|
|
|
1615
1615
|
- 확인 중 중복 닫기 요청은 합칩니다. 핸들러 안에서 다시 `window.close()`를 호출하지 말고 결과를 반환합니다.
|
|
1616
1616
|
- `window.close()`는 비동기 요청입니다. 호출 직후에 창이 이미 닫혔다고 가정하지 않습니다.
|
|
1617
1617
|
|
|
1618
|
-
#### onclose와
|
|
1618
|
+
#### onclose와 onclosed의 차이
|
|
1619
1619
|
|
|
1620
1620
|
| 이름 | 호출 시점 | 반환값 |
|
|
1621
1621
|
| --- | --- | --- |
|
|
1622
1622
|
| onclose | 창이 닫히기 전 | True만 닫기 허용 |
|
|
1623
|
-
|
|
|
1623
|
+
| onclosed | 실제로 닫힌 뒤 | 닫힘을 취소할 수 없음 |
|
|
1624
1624
|
|
|
1625
|
-
닫힌 후의 정리가 필요하면 생성자의 `
|
|
1625
|
+
닫힌 후의 정리가 필요하면 생성자의 `onclosed=handler`를 사용합니다. 이 알림에서는 이미 닫힌 창의 컨트롤을 수정하지 않습니다.
|
|
1626
1626
|
|
|
1627
1627
|
아직 `show()`하지 않은 창의 `close()`와 실행 전·종료 후 자원 정리인 `app.close()`는 확인 없이 정리합니다. 실행 중 종료를 요청할 때는 `window.close()` 또는 `app.quit()`를 사용합니다.
|
|
1628
1628
|
|
|
@@ -4,7 +4,7 @@ from .app import Application, Event
|
|
|
4
4
|
from .resources import asset_path
|
|
5
5
|
from .widgets import Button, CheckBox, Column, Image, Label, Row, Slider, TextArea, TextInput, Window
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.2.0"
|
|
8
8
|
__all__ = [
|
|
9
9
|
"Application", "Event", "Image", "asset_path", "Window", "Column", "Row", "Label", "Button",
|
|
10
10
|
"TextInput", "TextArea", "CheckBox", "Slider",
|
|
@@ -84,8 +84,8 @@ class Application:
|
|
|
84
84
|
if window._closed:
|
|
85
85
|
continue
|
|
86
86
|
window._closed = True
|
|
87
|
-
if window.
|
|
88
|
-
window.
|
|
87
|
+
if window.onclosed:
|
|
88
|
+
window.onclosed(Event(window, kind))
|
|
89
89
|
continue
|
|
90
90
|
if window._closed or window._closing:
|
|
91
91
|
continue
|
|
@@ -93,8 +93,8 @@ class Application:
|
|
|
93
93
|
window._decide_close(Event(window, kind))
|
|
94
94
|
continue
|
|
95
95
|
widget = window._widgets.get(view_id)
|
|
96
|
-
if widget is not None and widget.
|
|
97
|
-
widget.
|
|
96
|
+
if widget is not None and widget.onchange:
|
|
97
|
+
widget.onchange(Event(widget, kind, text, value))
|
|
98
98
|
except Exception:
|
|
99
99
|
# A failing handler must not kill the dispatcher or the app.
|
|
100
100
|
traceback.print_exc()
|
|
@@ -19,14 +19,14 @@ def _dimension(value, name, *, positive=False):
|
|
|
19
19
|
class Widget:
|
|
20
20
|
kind = ""
|
|
21
21
|
|
|
22
|
-
def __init__(self, text="", *,
|
|
22
|
+
def __init__(self, text="", *, onchange=None, enabled=True):
|
|
23
23
|
utf8(text)
|
|
24
|
-
if
|
|
25
|
-
raise TypeError("
|
|
24
|
+
if onchange is not None and not callable(onchange):
|
|
25
|
+
raise TypeError("onchange must be callable")
|
|
26
26
|
self._text = text
|
|
27
27
|
self._value = 0
|
|
28
28
|
self._enabled = bool(enabled)
|
|
29
|
-
self.
|
|
29
|
+
self.onchange = onchange
|
|
30
30
|
self._window = None
|
|
31
31
|
self._id = None
|
|
32
32
|
|
|
@@ -114,7 +114,7 @@ class Button(Widget):
|
|
|
114
114
|
|
|
115
115
|
def __init__(self, text, onclick=None, *, enabled=True,
|
|
116
116
|
icon: str | Path | None = None, icon_size: int = 24):
|
|
117
|
-
super().__init__(text,
|
|
117
|
+
super().__init__(text, onchange=onclick, enabled=enabled)
|
|
118
118
|
if type(icon_size) is not int or not 1 <= icon_size <= 256:
|
|
119
119
|
raise ValueError("icon_size must be an integer from 1 to 256")
|
|
120
120
|
self._icon_size = icon_size
|
|
@@ -143,7 +143,7 @@ class Button(Widget):
|
|
|
143
143
|
"""
|
|
144
144
|
if not callable(callback):
|
|
145
145
|
raise TypeError("onclick must be callable")
|
|
146
|
-
self.
|
|
146
|
+
self.onchange = callback
|
|
147
147
|
return callback
|
|
148
148
|
|
|
149
149
|
def click(self):
|
|
@@ -198,8 +198,8 @@ class Image(Widget):
|
|
|
198
198
|
class TextInput(Widget):
|
|
199
199
|
kind = "text"
|
|
200
200
|
|
|
201
|
-
def __init__(self, text="", *, label="",
|
|
202
|
-
super().__init__(text,
|
|
201
|
+
def __init__(self, text="", *, label="", onchange=None, enabled=True):
|
|
202
|
+
super().__init__(text, onchange=onchange, enabled=enabled)
|
|
203
203
|
utf8(label)
|
|
204
204
|
self.label = label
|
|
205
205
|
|
|
@@ -207,15 +207,15 @@ class TextInput(Widget):
|
|
|
207
207
|
class TextArea(Widget):
|
|
208
208
|
"""Scrollable UTF-8 text, up to 1 MiB, with native editing and undo.
|
|
209
209
|
|
|
210
|
-
Changes, including assignments to text after mounting, emit
|
|
210
|
+
Changes, including assignments to text after mounting, emit onchange.
|
|
211
211
|
wrap is a construction option; editable can be changed at runtime.
|
|
212
212
|
"""
|
|
213
213
|
kind = "textarea"
|
|
214
214
|
MAX_BYTES = 1024 * 1024
|
|
215
215
|
|
|
216
|
-
def __init__(self, text="", *,
|
|
216
|
+
def __init__(self, text="", *, onchange=None, editable=True, wrap=True):
|
|
217
217
|
self._validate_text(text)
|
|
218
|
-
super().__init__(text,
|
|
218
|
+
super().__init__(text, onchange=onchange, enabled=editable)
|
|
219
219
|
self.wrap = bool(wrap)
|
|
220
220
|
|
|
221
221
|
@staticmethod
|
|
@@ -240,8 +240,8 @@ class TextArea(Widget):
|
|
|
240
240
|
class CheckBox(Widget):
|
|
241
241
|
kind = "checkbox"
|
|
242
242
|
|
|
243
|
-
def __init__(self, text, *, checked=False,
|
|
244
|
-
super().__init__(text,
|
|
243
|
+
def __init__(self, text, *, checked=False, onchange=None, enabled=True):
|
|
244
|
+
super().__init__(text, onchange=onchange, enabled=enabled)
|
|
245
245
|
self._value = int(bool(checked))
|
|
246
246
|
|
|
247
247
|
@property
|
|
@@ -256,8 +256,8 @@ class CheckBox(Widget):
|
|
|
256
256
|
class Slider(Widget):
|
|
257
257
|
kind = "slider"
|
|
258
258
|
|
|
259
|
-
def __init__(self, text="", *, minimum=0, maximum=100, value=0,
|
|
260
|
-
super().__init__(text,
|
|
259
|
+
def __init__(self, text="", *, minimum=0, maximum=100, value=0, onchange=None):
|
|
260
|
+
super().__init__(text, onchange=onchange)
|
|
261
261
|
# The ABI carries layout/range values as floats; keep integers exact.
|
|
262
262
|
if not all(isinstance(v, int) and abs(v) <= 2**24 for v in (minimum, maximum, value)):
|
|
263
263
|
raise ValueError("slider bounds and value must be integers within +/- 2**24")
|
|
@@ -284,13 +284,13 @@ class Row(Column):
|
|
|
284
284
|
|
|
285
285
|
|
|
286
286
|
class Window:
|
|
287
|
-
def __init__(self, app, title, content, *, width=480, height=280, resizable=True, onclose=None,
|
|
287
|
+
def __init__(self, app, title, content, *, width=480, height=280, resizable=True, onclose=None, onclosed=None):
|
|
288
288
|
utf8(title)
|
|
289
289
|
if not isinstance(content, Widget):
|
|
290
290
|
raise TypeError("content must be a widget or layout")
|
|
291
|
-
if
|
|
292
|
-
raise TypeError("
|
|
293
|
-
self.app, self.content, self.
|
|
291
|
+
if onclosed is not None and not callable(onclosed):
|
|
292
|
+
raise TypeError("onclosed must be callable")
|
|
293
|
+
self.app, self.content, self.onclosed = app, content, onclosed
|
|
294
294
|
self._title = title
|
|
295
295
|
self.width = _dimension(width, "width", positive=True)
|
|
296
296
|
self.height = _dimension(height, "height", positive=True)
|
|
@@ -84,14 +84,14 @@ class DialogTests(unittest.TestCase):
|
|
|
84
84
|
dialog = demo.input_window
|
|
85
85
|
hint, name, buttons = dialog.content.children
|
|
86
86
|
name.text = " "
|
|
87
|
-
buttons.children[0].
|
|
87
|
+
buttons.children[0].onchange(None)
|
|
88
88
|
self.assertIn("한 글자", hint.text)
|
|
89
89
|
self.assertEqual(demo.name, "Haiku")
|
|
90
90
|
name.text = " 한글 이름 "
|
|
91
|
-
buttons.children[0].
|
|
91
|
+
buttons.children[0].onchange(None)
|
|
92
92
|
self.assertEqual(demo.name, "한글 이름")
|
|
93
93
|
self.assertTrue(dialog._closed)
|
|
94
|
-
dialog.
|
|
94
|
+
dialog.onclosed(Event(dialog, "closed"))
|
|
95
95
|
self.assertIsNone(demo.input_window)
|
|
96
96
|
|
|
97
97
|
|
|
@@ -31,7 +31,7 @@ class WidgetTests(unittest.TestCase):
|
|
|
31
31
|
app = Application("application/x-vnd.ren-test")
|
|
32
32
|
app._native = Mock()
|
|
33
33
|
events, closed = [], []
|
|
34
|
-
window = Window(app, "Close", Label("x"),
|
|
34
|
+
window = Window(app, "Close", Label("x"), onclosed=closed.append)
|
|
35
35
|
window._id = 1
|
|
36
36
|
app._windows[1] = window
|
|
37
37
|
|
|
@@ -119,7 +119,7 @@ class WidgetTests(unittest.TestCase):
|
|
|
119
119
|
for invalid in (None, 42):
|
|
120
120
|
with self.assertRaises(TypeError):
|
|
121
121
|
button.onclick(invalid)
|
|
122
|
-
self.assertIs(button.
|
|
122
|
+
self.assertIs(button.onchange, callback)
|
|
123
123
|
|
|
124
124
|
def test_composition_does_not_load_native_library(self):
|
|
125
125
|
with patch("ren_ui.app.Native", side_effect=AssertionError("must stay lazy")):
|
|
@@ -273,8 +273,8 @@ class WidgetTests(unittest.TestCase):
|
|
|
273
273
|
def test_event_snapshot_and_close_delivery(self):
|
|
274
274
|
app = Application("application/x-vnd.ren-test")
|
|
275
275
|
events = []
|
|
276
|
-
widget = TextInput(
|
|
277
|
-
window = Window(app, "Title", widget,
|
|
276
|
+
widget = TextInput(onchange=events.append)
|
|
277
|
+
window = Window(app, "Title", widget, onclosed=events.append)
|
|
278
278
|
app._windows[1] = window
|
|
279
279
|
window._widgets[2] = widget
|
|
280
280
|
app._enqueue(1, 2, b"change", "한글".encode(), 7)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|