mininterface 0.7.1__tar.gz → 0.7.3__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.
- {mininterface-0.7.1 → mininterface-0.7.3}/PKG-INFO +10 -5
- {mininterface-0.7.1 → mininterface-0.7.3}/README.md +5 -1
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/cli_parser.py +33 -22
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/tag.py +2 -2
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/tag_factory.py +5 -1
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/textual_interface/textual_adaptor.py +5 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/textual_interface/textual_app.py +9 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/textual_interface/textual_button_app.py +3 -0
- mininterface-0.7.3/mininterface/textual_interface/textual_facet.py +64 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/tk_interface/date_entry.py +134 -64
- mininterface-0.7.3/mininterface/tk_interface/external_fix.py +74 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/tk_interface/tk_facet.py +7 -4
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/tk_interface/tk_window.py +3 -4
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/tk_interface/utils.py +16 -12
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/types.py +4 -11
- {mininterface-0.7.1 → mininterface-0.7.3}/pyproject.toml +7 -6
- mininterface-0.7.1/mininterface/textual_interface/textual_facet.py +0 -31
- {mininterface-0.7.1 → mininterface-0.7.3}/LICENSE +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/ValidationFail.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/__init__.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/__main__.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/auxiliary.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/exceptions.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/experimental.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/facet.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/form_dict.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/interfaces.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/mininterface.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/redirectable.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/showcase.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/start.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/subcommands.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/text_interface.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/textual_interface/__init__.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/textual_interface/widgets.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/tk_interface/__init__.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/tk_interface/redirect_text_tkinter.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/type_stubs.py +0 -0
- {mininterface-0.7.1 → mininterface-0.7.3}/mininterface/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: mininterface
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: A minimal access to GUI, TUI, CLI and config
|
|
5
5
|
Home-page: https://github.com/CZ-NIC/mininterface
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -14,15 +14,16 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Provides-Extra: all
|
|
17
|
+
Provides-Extra: gui
|
|
17
18
|
Provides-Extra: img
|
|
19
|
+
Provides-Extra: tui
|
|
18
20
|
Provides-Extra: web
|
|
19
21
|
Requires-Dist: autocombobox (==1.4.2)
|
|
20
22
|
Requires-Dist: humanize
|
|
21
23
|
Requires-Dist: pyyaml
|
|
22
24
|
Requires-Dist: textual (>=0.84,<0.85)
|
|
23
|
-
Requires-Dist: tkcalendar
|
|
24
25
|
Requires-Dist: tkinter-tooltip
|
|
25
|
-
Requires-Dist: tkinter_form (==0.1
|
|
26
|
+
Requires-Dist: tkinter_form (==0.2.1)
|
|
26
27
|
Requires-Dist: tkscrollableframe
|
|
27
28
|
Requires-Dist: typing_extensions
|
|
28
29
|
Requires-Dist: tyro (==0.8.14)
|
|
@@ -132,7 +133,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
|
|
|
132
133
|
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
|
|
133
134
|
|
|
134
135
|
```bash
|
|
135
|
-
pip install mininterface
|
|
136
|
+
pip install mininterface[all] # GPLv3 and compatible
|
|
136
137
|
```
|
|
137
138
|
|
|
138
139
|
## Minimal installation
|
|
@@ -144,6 +145,10 @@ pip install --no-dependencies mininterface
|
|
|
144
145
|
pip install tyro typing_extensions pyyaml
|
|
145
146
|
```
|
|
146
147
|
|
|
148
|
+
## MacOS GUI
|
|
149
|
+
|
|
150
|
+
If the GUI does not work on MacOS, you might need to launch: `brew install python-tk`
|
|
151
|
+
|
|
147
152
|
# Docs
|
|
148
153
|
See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic.github.io/mininterface/Overview/).
|
|
149
154
|
|
|
@@ -102,7 +102,7 @@ The config variables needed by your program are kept in cozy dataclasses. Write
|
|
|
102
102
|
Install with a single command from [PyPi](https://pypi.org/project/mininterface/).
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
|
-
pip install mininterface
|
|
105
|
+
pip install mininterface[all] # GPLv3 and compatible
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
## Minimal installation
|
|
@@ -114,6 +114,10 @@ pip install --no-dependencies mininterface
|
|
|
114
114
|
pip install tyro typing_extensions pyyaml
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
## MacOS GUI
|
|
118
|
+
|
|
119
|
+
If the GUI does not work on MacOS, you might need to launch: `brew install python-tk`
|
|
120
|
+
|
|
117
121
|
# Docs
|
|
118
122
|
See the docs overview at [https://cz-nic.github.io/mininterface/](https://cz-nic.github.io/mininterface/Overview/).
|
|
119
123
|
|
|
@@ -150,7 +150,7 @@ def run_tyro_parser(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
150
150
|
if ask_for_missing and getattr(e, "code", None) == 2 and eavesdrop:
|
|
151
151
|
# Some required arguments are missing. Determine which.
|
|
152
152
|
wf = {}
|
|
153
|
-
for arg in
|
|
153
|
+
for arg in _fetch_eavesdrop_args():
|
|
154
154
|
treat_missing(type_form, kwargs, parser, wf, arg)
|
|
155
155
|
|
|
156
156
|
# Second attempt to parse CLI
|
|
@@ -195,7 +195,6 @@ def treat_missing(env_class, kwargs: dict, parser: ArgumentParser, wf: dict, arg
|
|
|
195
195
|
# However, the UI then is not able to use ex. the number filtering capabilities.
|
|
196
196
|
# Putting there None is not a good idea as dataclass_to_tagdict fails if None is not allowed by the annotation.
|
|
197
197
|
tag = wf[field_name] = tag_factory(MissingTagValue(),
|
|
198
|
-
# tag = wf[field_name] = tag_factory(MISSING,
|
|
199
198
|
argument.help.replace("(required)", ""),
|
|
200
199
|
validation=not_empty,
|
|
201
200
|
_src_class=env_class,
|
|
@@ -205,9 +204,17 @@ def treat_missing(env_class, kwargs: dict, parser: ArgumentParser, wf: dict, arg
|
|
|
205
204
|
# A None would be enough because Mininterface will ask for the missing values
|
|
206
205
|
# promply, however, Pydantic model would fail.
|
|
207
206
|
# As it serves only for tyro parsing and the field is marked wrong, the made up value is never used or seen.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
set_default(kwargs, field_name, tag._make_default_value())
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _fetch_eavesdrop_args():
|
|
211
|
+
return eavesdrop.partition(":")[2].strip().split(", ")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def set_default(kwargs, field_name, val):
|
|
215
|
+
if "default" not in kwargs:
|
|
216
|
+
kwargs["default"] = SimpleNamespace()
|
|
217
|
+
setattr(kwargs["default"], field_name, val)
|
|
211
218
|
|
|
212
219
|
|
|
213
220
|
def _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
@@ -228,15 +235,20 @@ def _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
228
235
|
Returns:
|
|
229
236
|
Configuration namespace.
|
|
230
237
|
"""
|
|
238
|
+
if isinstance(env_or_list, list):
|
|
239
|
+
subcommands, env = env_or_list, None
|
|
240
|
+
else:
|
|
241
|
+
subcommands, env = None, env_or_list
|
|
242
|
+
|
|
231
243
|
# Load config file
|
|
232
|
-
if config_file and
|
|
233
|
-
#
|
|
244
|
+
if config_file and subcommands:
|
|
245
|
+
# Reading config files when using subcommands is not implemented.
|
|
234
246
|
static = {}
|
|
235
247
|
kwargs["default"] = None
|
|
236
248
|
warnings.warn(f"Config file {config_file} is ignored because subcommands are used."
|
|
237
|
-
"It is not easy to set
|
|
238
|
-
"Describe the developer your usecase so that they might implement this.")
|
|
239
|
-
if "default" not in kwargs and not
|
|
249
|
+
" It is not easy to set how this should work."
|
|
250
|
+
" Describe the developer your usecase so that they might implement this.")
|
|
251
|
+
if "default" not in kwargs and not subcommands:
|
|
240
252
|
# Undocumented feature. User put a namespace into kwargs["default"]
|
|
241
253
|
# that already serves for defaults. We do not fetch defaults yet from a config file.
|
|
242
254
|
disk = {}
|
|
@@ -244,29 +256,28 @@ def _parse_cli(env_or_list: Type[EnvClass] | list[Type[EnvClass]],
|
|
|
244
256
|
disk = yaml.safe_load(config_file.read_text()) or {} # empty file is ok
|
|
245
257
|
# Nested dataclasses have to be properly initialized. YAML gave them as dicts only.
|
|
246
258
|
for key in (key for key, val in disk.items() if isinstance(val, dict)):
|
|
247
|
-
disk[key] =
|
|
259
|
+
disk[key] = env.__annotations__[key](**disk[key])
|
|
248
260
|
|
|
249
261
|
# Fill default fields
|
|
250
|
-
if pydantic and issubclass(
|
|
262
|
+
if pydantic and issubclass(env, BaseModel):
|
|
251
263
|
# Unfortunately, pydantic needs to fill the default with the actual values,
|
|
252
264
|
# the default value takes the precedence over the hard coded one, even if missing.
|
|
253
|
-
static = {key:
|
|
254
|
-
for ann in yield_annotations(
|
|
255
|
-
# static = {key:
|
|
256
|
-
# for key, _ in iterate_attributes(
|
|
257
|
-
elif attr and attr.has(
|
|
265
|
+
static = {key: env.model_fields.get(key).default
|
|
266
|
+
for ann in yield_annotations(env) for key in ann if not key.startswith("__") and not key in disk}
|
|
267
|
+
# static = {key: env_.model_fields.get(key).default
|
|
268
|
+
# for key, _ in iterate_attributes(env_) if not key in disk}
|
|
269
|
+
elif attr and attr.has(env):
|
|
258
270
|
# Unfortunately, attrs needs to fill the default with the actual values,
|
|
259
271
|
# the default value takes the precedence over the hard coded one, even if missing.
|
|
260
272
|
# NOTE Might not work for inherited models.
|
|
261
273
|
static = {key: field.default
|
|
262
|
-
for key, field in attr.fields_dict(
|
|
274
|
+
for key, field in attr.fields_dict(env).items() if not key.startswith("__") and not key in disk}
|
|
263
275
|
else:
|
|
264
276
|
# To ensure the configuration file does not need to contain all keys, we have to fill in the missing ones.
|
|
265
277
|
# Otherwise, tyro will spawn warnings about missing fields.
|
|
266
278
|
static = {key: val
|
|
267
|
-
for key, val in yield_defaults(
|
|
268
|
-
kwargs["default"] = SimpleNamespace(**(
|
|
279
|
+
for key, val in yield_defaults(env) if not key.startswith("__") and not key in disk}
|
|
280
|
+
kwargs["default"] = SimpleNamespace(**(static | disk))
|
|
269
281
|
|
|
270
282
|
# Load configuration from CLI
|
|
271
|
-
|
|
272
|
-
return env, wrong_fields
|
|
283
|
+
return run_tyro_parser(subcommands or env, kwargs, add_verbosity, ask_for_missing, args)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from ast import literal_eval
|
|
2
2
|
from dataclasses import dataclass, fields
|
|
3
|
-
from datetime import
|
|
3
|
+
from datetime import date, time
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from types import FunctionType, MethodType, NoneType, UnionType
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, Type, TypeVar, Union, get_args, get_origin
|
|
@@ -683,7 +683,7 @@ class Tag:
|
|
|
683
683
|
except (SyntaxError, ValueError):
|
|
684
684
|
self.set_error_text(f"Not a valid {self._repr_annotation()}")
|
|
685
685
|
return False
|
|
686
|
-
elif self.
|
|
686
|
+
elif self._is_subclass((time, date)):
|
|
687
687
|
try:
|
|
688
688
|
out_value = self.annotation.fromisoformat(ui_value)
|
|
689
689
|
except ValueError:
|
|
@@ -72,7 +72,11 @@ def tag_factory(val=None, description=None, annotation=None, *args, _src_obj=Non
|
|
|
72
72
|
new = copy(metadata)
|
|
73
73
|
new.val = val if val is not None else new.val
|
|
74
74
|
new.description = description or new.description
|
|
75
|
-
|
|
75
|
+
if new.annotation is None:
|
|
76
|
+
# pAnnot: Annotated[date, Tag(name="hello")] = datetime.fromisoformat(...)
|
|
77
|
+
# -> DatetimeTag(date=True)
|
|
78
|
+
new.annotation = annotation
|
|
79
|
+
return tag_assure_type(new._fetch_from(Tag(*args, **kwargs)))
|
|
76
80
|
# NOTE The mechanism is not perfect. When done, we may test configs.PathTagClass.
|
|
77
81
|
# * fetch_from will not transfer PathTag.multiple
|
|
78
82
|
# * copy will not transfer list[Path] from `Annotated[list[Path], Tag(...)]`
|
|
@@ -28,6 +28,8 @@ class TextualAdaptor(BackendAdaptor):
|
|
|
28
28
|
def __init__(self, interface: "TextualInterface"):
|
|
29
29
|
self.interface = interface
|
|
30
30
|
self.facet = interface.facet = TextualFacet(self, interface.env)
|
|
31
|
+
self.app: TextualApp | None = None
|
|
32
|
+
self.layout_elements = []
|
|
31
33
|
|
|
32
34
|
@staticmethod
|
|
33
35
|
def widgetize(tag: Tag) -> Widget | Changeable:
|
|
@@ -73,6 +75,9 @@ class TextualAdaptor(BackendAdaptor):
|
|
|
73
75
|
|
|
74
76
|
def run_dialog(self, form: TagDict, title: str = "", submit: bool | str = True) -> TagDict:
|
|
75
77
|
super().run_dialog(form, title, submit)
|
|
78
|
+
# Unfortunately, there seems to be no way to reuse the app.
|
|
79
|
+
# Which blocks using multiple form external .form() calls from the web interface.
|
|
80
|
+
# Textual cannot run in a thread, it seems it cannot run in another process, self.suspend() is of no use.
|
|
76
81
|
self.app = app = TextualApp(self, submit)
|
|
77
82
|
if title:
|
|
78
83
|
app.title = title
|
|
@@ -26,6 +26,14 @@ class TextualApp(App[bool | None]):
|
|
|
26
26
|
# ("down", "go_up", "Go down"),
|
|
27
27
|
# ]
|
|
28
28
|
|
|
29
|
+
DEFAULT_CSS = """
|
|
30
|
+
ImageViewer{
|
|
31
|
+
|
|
32
|
+
height: 20;
|
|
33
|
+
}
|
|
34
|
+
"""
|
|
35
|
+
""" Limit layout image size """
|
|
36
|
+
|
|
29
37
|
def __init__(self, adaptor: "TextualAdaptor", submit: str | bool = True):
|
|
30
38
|
super().__init__()
|
|
31
39
|
self.title = adaptor.facet._title
|
|
@@ -52,6 +60,7 @@ class TextualApp(App[bool | None]):
|
|
|
52
60
|
yield Label(text, id="buffered_text")
|
|
53
61
|
focus_set = False
|
|
54
62
|
with VerticalScroll():
|
|
63
|
+
yield from self.adaptor.layout_elements
|
|
55
64
|
for i, fieldt in enumerate(self.widgets):
|
|
56
65
|
if isinstance(fieldt, Input):
|
|
57
66
|
yield Label(fieldt.placeholder)
|
{mininterface-0.7.1 → mininterface-0.7.3}/mininterface/textual_interface/textual_button_app.py
RENAMED
|
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from typing import TYPE_CHECKING, Any
|
|
3
3
|
|
|
4
4
|
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.containers import VerticalScroll
|
|
5
6
|
from textual.widgets import Button, Footer, Label
|
|
6
7
|
|
|
7
8
|
from ..exceptions import Cancelled
|
|
@@ -60,6 +61,7 @@ class TextualButtonApp(App):
|
|
|
60
61
|
self.focused_i: int = 0
|
|
61
62
|
self.values = {}
|
|
62
63
|
self.interface = interface
|
|
64
|
+
self.adaptor = self.interface.adaptor
|
|
63
65
|
|
|
64
66
|
def yes_no(self, text: str, focus_no=True) -> DummyWrapper:
|
|
65
67
|
return self.buttons(text, [("Yes", True), ("No", False)], int(focus_no))
|
|
@@ -78,6 +80,7 @@ class TextualButtonApp(App):
|
|
|
78
80
|
yield Footer()
|
|
79
81
|
if text := self.interface._redirected.join():
|
|
80
82
|
yield Label(text, id="buffered_text")
|
|
83
|
+
yield from self.adaptor.layout_elements
|
|
81
84
|
yield Label(self.text, id="question")
|
|
82
85
|
|
|
83
86
|
self.values.clear()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from warnings import warn
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from textual.widgets import (Checkbox, Footer, Header, Input, Label,
|
|
7
|
+
RadioButton, Static)
|
|
8
|
+
|
|
9
|
+
from humanize import naturalsize
|
|
10
|
+
|
|
11
|
+
from ..exceptions import DependencyRequired
|
|
12
|
+
from ..facet import Facet, Image, LayoutElement
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .textual_adaptor import TextualAdaptor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TextualFacet(Facet):
|
|
18
|
+
adaptor: "TextualAdaptor"
|
|
19
|
+
|
|
20
|
+
def __init__(self, *args, **kwargs):
|
|
21
|
+
super().__init__(*args, **kwargs)
|
|
22
|
+
# Since TextualApp turns off, we need to have its values stored somewhere
|
|
23
|
+
self._title = ""
|
|
24
|
+
|
|
25
|
+
# NOTE: multiline title will not show up
|
|
26
|
+
def set_title(self, title: str):
|
|
27
|
+
self._title = title
|
|
28
|
+
try:
|
|
29
|
+
self.adaptor.app.title = title
|
|
30
|
+
except:
|
|
31
|
+
# NOTE: When you receive Facet in Command.init, the app does not exist yet
|
|
32
|
+
warn("Setting textual title not implemented well.")
|
|
33
|
+
|
|
34
|
+
def _layout(self, elements: list[LayoutElement]):
|
|
35
|
+
append = self.adaptor.layout_elements.append
|
|
36
|
+
try:
|
|
37
|
+
from PIL import Image as ImagePIL
|
|
38
|
+
from textual_imageview.viewer import ImageViewer
|
|
39
|
+
PIL = True
|
|
40
|
+
except:
|
|
41
|
+
PIL = False
|
|
42
|
+
|
|
43
|
+
for el in elements:
|
|
44
|
+
match el:
|
|
45
|
+
case Image():
|
|
46
|
+
if not PIL:
|
|
47
|
+
raise DependencyRequired("img")
|
|
48
|
+
append(ImageViewer(ImagePIL.open(el.src)))
|
|
49
|
+
case Path():
|
|
50
|
+
size = naturalsize(el.stat().st_size)
|
|
51
|
+
mtime = datetime.fromtimestamp(el.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
|
|
52
|
+
append(Label(f"{el} / {size} / {mtime}"))
|
|
53
|
+
case str():
|
|
54
|
+
append(Label(el))
|
|
55
|
+
case _:
|
|
56
|
+
append(Label("Error in the layout: Unknown {el}"))
|
|
57
|
+
|
|
58
|
+
def submit(self, *args, **kwargs):
|
|
59
|
+
super().submit(*args, **kwargs)
|
|
60
|
+
try:
|
|
61
|
+
self.adaptor.app.action_confirm()
|
|
62
|
+
except:
|
|
63
|
+
# NOTE: When you receive Facet in Command.init, the app does not exist yet
|
|
64
|
+
warn("Setting textual title not implemented well.")
|
|
@@ -14,11 +14,24 @@ if TYPE_CHECKING:
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class DateEntryFrame(tk.Frame):
|
|
17
|
+
|
|
17
18
|
def __init__(self, master, tk_app: "TkWindow", tag: DatetimeTag, variable: tk.Variable, **kwargs):
|
|
18
19
|
super().__init__(master, **kwargs)
|
|
19
20
|
|
|
20
21
|
self.tk_app = tk_app
|
|
21
22
|
self.tag = tag
|
|
23
|
+
if tag.date and tag.time:
|
|
24
|
+
if tag.full_precision:
|
|
25
|
+
self.datetimeformat = '%Y-%m-%d %H:%M:%S'
|
|
26
|
+
else:
|
|
27
|
+
self.datetimeformat = '%Y-%m-%d %H:%M'
|
|
28
|
+
elif tag.time and not tag.date:
|
|
29
|
+
if tag.full_precision:
|
|
30
|
+
self.datetimeformat = '%H:%M:%S'
|
|
31
|
+
else:
|
|
32
|
+
self.datetimeformat = '%H:%M'
|
|
33
|
+
else:
|
|
34
|
+
self.datetimeformat = '%Y-%m-%d'
|
|
22
35
|
|
|
23
36
|
# Date entry
|
|
24
37
|
self.spinbox = self.create_spinbox(variable)
|
|
@@ -27,7 +40,7 @@ class DateEntryFrame(tk.Frame):
|
|
|
27
40
|
self.frame = tk.Frame(self)
|
|
28
41
|
|
|
29
42
|
# The calendar widget
|
|
30
|
-
if Calendar:
|
|
43
|
+
if Calendar and tag.date:
|
|
31
44
|
# Toggle calendar button
|
|
32
45
|
tk.Button(self, text="…", command=self.toggle_calendar).grid(row=0, column=1)
|
|
33
46
|
|
|
@@ -37,19 +50,20 @@ class DateEntryFrame(tk.Frame):
|
|
|
37
50
|
self.calendar.bind("<<CalendarSelected>>", self.on_date_select)
|
|
38
51
|
self.calendar.grid()
|
|
39
52
|
# Initialize calendar with the current date
|
|
40
|
-
self.update_calendar(self.spinbox.get(),
|
|
53
|
+
self.update_calendar(self.spinbox.get(), self.datetimeformat)
|
|
41
54
|
else:
|
|
42
55
|
self.calendar = None
|
|
43
56
|
|
|
44
|
-
self.bind_all_events()
|
|
45
|
-
|
|
46
57
|
def create_spinbox(self, variable: tk.Variable):
|
|
47
|
-
spinbox = tk.Spinbox(self,
|
|
58
|
+
spinbox = tk.Spinbox(self, wrap=True, textvariable=variable)
|
|
48
59
|
spinbox.grid()
|
|
49
60
|
if not variable.get():
|
|
50
|
-
spinbox.insert(0, datetime.now().strftime(
|
|
61
|
+
spinbox.insert(0, datetime.now().strftime(self.datetimeformat))
|
|
51
62
|
spinbox.focus_set()
|
|
52
|
-
|
|
63
|
+
if (not self.tag.date and self.tag.time):
|
|
64
|
+
spinbox.icursor(0)
|
|
65
|
+
else:
|
|
66
|
+
spinbox.icursor(8)
|
|
53
67
|
|
|
54
68
|
# Bind up/down arrow keys
|
|
55
69
|
spinbox.bind("<Up>", self.increment_value)
|
|
@@ -60,20 +74,20 @@ class DateEntryFrame(tk.Frame):
|
|
|
60
74
|
|
|
61
75
|
# Bind key release event to update calendar when user changes the input field
|
|
62
76
|
spinbox.bind("<KeyRelease>", self.on_spinbox_change)
|
|
63
|
-
return spinbox
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
self.bind_all("<Control-c>", self.copy_to_clipboard)
|
|
78
|
+
# Toggle calendar widget with ctrl+shift+c
|
|
79
|
+
spinbox.bind("<Control-Shift-C>", self.toggle_calendar)
|
|
68
80
|
|
|
69
81
|
# Select all in the spinbox with ctrl+a
|
|
70
|
-
|
|
82
|
+
spinbox.bind("<Control-a>", self.select_all)
|
|
83
|
+
|
|
84
|
+
# Copy to clipboard with ctrl+c
|
|
85
|
+
spinbox.bind("<Control-c>", self.copy_to_clipboard)
|
|
71
86
|
|
|
72
87
|
# Paste from clipboard with ctrl+v
|
|
73
|
-
|
|
88
|
+
spinbox.bind("<Control-v>", self.paste_from_clipboard)
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
self.bind_all("<Control-Shift-C>", lambda event: self.toggle_calendar())
|
|
90
|
+
return spinbox
|
|
77
91
|
|
|
78
92
|
def toggle_calendar(self, event=None):
|
|
79
93
|
if not self.calendar:
|
|
@@ -102,7 +116,10 @@ class DateEntryFrame(tk.Frame):
|
|
|
102
116
|
def find_valid_time(self):
|
|
103
117
|
input = self.spinbox.get()
|
|
104
118
|
# use regex to find the time part
|
|
105
|
-
|
|
119
|
+
if self.tag.full_precision:
|
|
120
|
+
time_part = re.search(r'\d{2}:\d{2}:\d{2}', input)
|
|
121
|
+
else:
|
|
122
|
+
time_part = re.search(r'\d{2}:\d{2}', input)
|
|
106
123
|
if time_part:
|
|
107
124
|
return time_part.group()
|
|
108
125
|
return False
|
|
@@ -114,51 +131,82 @@ class DateEntryFrame(tk.Frame):
|
|
|
114
131
|
date = self.find_valid_date()
|
|
115
132
|
time = self.find_valid_time()
|
|
116
133
|
|
|
117
|
-
if date:
|
|
118
|
-
split_input = re.split(r'[-
|
|
119
|
-
|
|
134
|
+
if date and not time:
|
|
135
|
+
split_input = re.split(r'[-]', date)
|
|
136
|
+
new_value_str = self.increment_part(split_input, caret_pos, delta, '-')
|
|
137
|
+
elif date and time:
|
|
138
|
+
split_input = re.split(r'[- :]', date_str)
|
|
139
|
+
new_value_str = self.increment_part(split_input, caret_pos, delta, ' ')
|
|
140
|
+
elif not date and time:
|
|
141
|
+
split_input = re.split(r'[:]', time)
|
|
142
|
+
new_value_str = self.increment_part(split_input, caret_pos, delta, ':')
|
|
143
|
+
else:
|
|
144
|
+
return
|
|
120
145
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
146
|
+
# Validate the new date
|
|
147
|
+
try:
|
|
148
|
+
datetime.strptime(new_value_str, self.datetimeformat)
|
|
149
|
+
self.spinbox.delete(0, tk.END)
|
|
150
|
+
self.spinbox.insert(0, new_value_str)
|
|
151
|
+
self.spinbox.icursor(caret_pos)
|
|
152
|
+
if Calendar:
|
|
153
|
+
self.update_calendar(new_value_str, self.datetimeformat)
|
|
154
|
+
except ValueError as e:
|
|
155
|
+
pass
|
|
125
156
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
157
|
+
def increment_part(self, split_input, caret_pos, delta, separator):
|
|
158
|
+
part_index = self.get_part_index(caret_pos)
|
|
159
|
+
if part_index > len(split_input) - 1:
|
|
160
|
+
return separator.join(split_input)
|
|
161
|
+
|
|
162
|
+
# Increment or decrement the relevant part
|
|
163
|
+
number = int(split_input[part_index])
|
|
164
|
+
new_number = number + delta
|
|
165
|
+
split_input[part_index] = str(new_number).zfill(len(split_input[part_index]))
|
|
166
|
+
|
|
167
|
+
if self.tag.full_precision and separator == ' ':
|
|
168
|
+
return f"{split_input[0]}-{split_input[1]}-{split_input[2]} "\
|
|
169
|
+
f"{split_input[3]}:{split_input[4]}:{split_input[5]}"
|
|
170
|
+
elif separator == ' ':
|
|
171
|
+
return f"{split_input[0]}-{split_input[1]}-{split_input[2]} "\
|
|
172
|
+
f"{split_input[3]}:{split_input[4]}"
|
|
173
|
+
elif separator == ':':
|
|
174
|
+
if self.tag.full_precision:
|
|
175
|
+
return f"{split_input[0]}:{split_input[1]}:{split_input[2]}"
|
|
130
176
|
else:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def get_part_index(self, caret_pos, split_length):
|
|
146
|
-
if caret_pos < 5: # year
|
|
147
|
-
return 0
|
|
148
|
-
elif caret_pos < 8: # month
|
|
149
|
-
return 1
|
|
150
|
-
elif caret_pos < 11: # day
|
|
151
|
-
return 2
|
|
152
|
-
elif split_length > 3:
|
|
153
|
-
if caret_pos < 14: # hour
|
|
177
|
+
return f"{split_input[0]}:{split_input[1]}"
|
|
178
|
+
else:
|
|
179
|
+
return separator.join(split_input)
|
|
180
|
+
|
|
181
|
+
def get_part_index(self, caret_pos):
|
|
182
|
+
if self.tag.date and self.tag.time:
|
|
183
|
+
if caret_pos < 5: # year
|
|
184
|
+
return 0
|
|
185
|
+
elif caret_pos < 8: # month
|
|
186
|
+
return 1
|
|
187
|
+
elif caret_pos < 11: # day
|
|
188
|
+
return 2
|
|
189
|
+
elif caret_pos < 14: # hour
|
|
154
190
|
return 3
|
|
155
191
|
elif caret_pos < 17: # minute
|
|
156
192
|
return 4
|
|
157
|
-
|
|
193
|
+
else: # second
|
|
158
194
|
return 5
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
195
|
+
elif self.tag.date:
|
|
196
|
+
if caret_pos < 5: # year
|
|
197
|
+
return 0
|
|
198
|
+
elif caret_pos < 8: # month
|
|
199
|
+
return 1
|
|
200
|
+
elif caret_pos < 11: # day
|
|
201
|
+
return 2
|
|
202
|
+
elif self.tag.time:
|
|
203
|
+
if caret_pos < 3: # hour
|
|
204
|
+
return 0
|
|
205
|
+
elif caret_pos < 6: # minute
|
|
206
|
+
return 1
|
|
207
|
+
else: # second
|
|
208
|
+
return 2
|
|
209
|
+
return 0
|
|
162
210
|
|
|
163
211
|
def on_spinbox_click(self, event):
|
|
164
212
|
# Check if the click was on the spinbox arrows
|
|
@@ -168,21 +216,39 @@ class DateEntryFrame(tk.Frame):
|
|
|
168
216
|
self.decrement_value()
|
|
169
217
|
|
|
170
218
|
def on_date_select(self, event):
|
|
171
|
-
|
|
219
|
+
|
|
220
|
+
# find caret position to keep it in the same place
|
|
221
|
+
caret_pos = self.spinbox.index(tk.INSERT)
|
|
222
|
+
|
|
223
|
+
selected_date = self.calendar.selection_get().strftime('%Y-%m-%d')
|
|
224
|
+
if self.tag.time:
|
|
225
|
+
time = self.find_valid_time()
|
|
226
|
+
if time:
|
|
227
|
+
selected_date += f" {time}"
|
|
228
|
+
else:
|
|
229
|
+
if self.tag.full_precision:
|
|
230
|
+
selected_date += " 00:00:00"
|
|
231
|
+
else:
|
|
232
|
+
selected_date += " 00:00"
|
|
233
|
+
|
|
234
|
+
|
|
172
235
|
self.spinbox.delete(0, tk.END)
|
|
173
|
-
self.spinbox.insert(0, selected_date
|
|
174
|
-
|
|
236
|
+
self.spinbox.insert(0, selected_date)
|
|
237
|
+
|
|
238
|
+
# Keep the caret position
|
|
239
|
+
self.spinbox.icursor(caret_pos)
|
|
175
240
|
|
|
176
241
|
def on_spinbox_change(self, event):
|
|
177
242
|
if Calendar:
|
|
178
243
|
self.update_calendar(self.spinbox.get())
|
|
179
244
|
|
|
180
245
|
def update_calendar(self, date_str, string_format='%Y-%m-%d'):
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
246
|
+
if self.tag.date:
|
|
247
|
+
try:
|
|
248
|
+
date = datetime.strptime(date_str, string_format)
|
|
249
|
+
self.calendar.selection_set(date)
|
|
250
|
+
except ValueError:
|
|
251
|
+
pass
|
|
186
252
|
|
|
187
253
|
def copy_to_clipboard(self, event=None):
|
|
188
254
|
self.clipboard_clear()
|
|
@@ -194,7 +260,7 @@ class DateEntryFrame(tk.Frame):
|
|
|
194
260
|
popup = tk.Toplevel(self)
|
|
195
261
|
popup.wm_title("")
|
|
196
262
|
|
|
197
|
-
label = tk.Label(popup, text=message
|
|
263
|
+
label = tk.Label(popup, text=message)
|
|
198
264
|
label.pack(side="top", fill="x", pady=10, padx=10)
|
|
199
265
|
|
|
200
266
|
# Position the popup window in the top-left corner of the widget
|
|
@@ -218,4 +284,8 @@ class DateEntryFrame(tk.Frame):
|
|
|
218
284
|
|
|
219
285
|
def paste_from_clipboard(self, event=None):
|
|
220
286
|
self.spinbox.delete(0, tk.END)
|
|
221
|
-
|
|
287
|
+
|
|
288
|
+
def round_time(self, dt):
|
|
289
|
+
if self.tag.full_precision:
|
|
290
|
+
return dt
|
|
291
|
+
return dt[:-4]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# The purpose of the file is to put the descriptions to the bottom of the widgets
|
|
2
|
+
# as it was in the former version of the tkinter_form and to limit their width.
|
|
3
|
+
from tkinter import ttk
|
|
4
|
+
|
|
5
|
+
from tkinter_form import Form, Value, FieldForm
|
|
6
|
+
|
|
7
|
+
orig = Form._Form__create_widgets
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def __create_widgets_monkeypatched(
|
|
11
|
+
self, form_dict: dict, name_config: str, button_command: callable
|
|
12
|
+
) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Create form widgets
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
form_dict (dict): form dict base
|
|
18
|
+
name_config (str): name_config
|
|
19
|
+
button (bool): button_config
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
index = 0
|
|
23
|
+
for _, (name_key, value) in enumerate(form_dict.items()):
|
|
24
|
+
index += 1
|
|
25
|
+
description = None
|
|
26
|
+
if isinstance(value, Value):
|
|
27
|
+
value, description = value.val, value.description
|
|
28
|
+
|
|
29
|
+
self.rowconfigure(index, weight=1)
|
|
30
|
+
|
|
31
|
+
if isinstance(value, dict):
|
|
32
|
+
widget = Form(self, name_key, value)
|
|
33
|
+
widget.grid(row=index, column=0, columnspan=3, sticky="nesw")
|
|
34
|
+
|
|
35
|
+
self.fields[name_key] = widget
|
|
36
|
+
last_index = index
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
variable = self._Form__type_vars[type(value)]()
|
|
40
|
+
widget = self._Form__type_widgets[type(value)](self)
|
|
41
|
+
|
|
42
|
+
self.columnconfigure(1, weight=1)
|
|
43
|
+
widget.grid(row=index, column=1, sticky="nesw", padx=2, pady=2)
|
|
44
|
+
label = ttk.Label(self, text=name_key)
|
|
45
|
+
self.columnconfigure(0, weight=1)
|
|
46
|
+
label.grid(row=index, column=0, sticky="nes", padx=2, pady=2)
|
|
47
|
+
|
|
48
|
+
# Add a further description to the row below the widget
|
|
49
|
+
description_label = None
|
|
50
|
+
if not description is None:
|
|
51
|
+
index += 1
|
|
52
|
+
description_label = ttk.Label(self, text=description, wraplength=1000)
|
|
53
|
+
description_label.grid(row=index, column=1, columnspan=2, sticky="nesw", padx=2, pady=2)
|
|
54
|
+
|
|
55
|
+
self.fields[name_key] = FieldForm(
|
|
56
|
+
master=self,
|
|
57
|
+
label=label,
|
|
58
|
+
widget=widget,
|
|
59
|
+
variable=variable,
|
|
60
|
+
value=value,
|
|
61
|
+
description=description_label,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
last_index = index
|
|
65
|
+
|
|
66
|
+
if button_command:
|
|
67
|
+
self._Form__command = button_command
|
|
68
|
+
self.button = ttk.Button(
|
|
69
|
+
self, text=name_config, command=self._Form__command_button
|
|
70
|
+
)
|
|
71
|
+
self.button.grid(row=last_index + 1, column=0, columnspan=3, sticky="nesw")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
Form._Form__create_widgets = __create_widgets_monkeypatched
|
|
@@ -36,10 +36,13 @@ class TkFacet(Facet):
|
|
|
36
36
|
raise DependencyRequired("img")
|
|
37
37
|
filename = el.src
|
|
38
38
|
img = ImagePIL.open(filename)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
max_width, max_height = 250, 250
|
|
40
|
+
w_o, h_o = img.size
|
|
41
|
+
scale = min(max_width / w_o, max_height / h_o)
|
|
42
|
+
img = img.resize((int(w_o * scale), int(h_o * scale)), ImagePIL.LANCZOS)
|
|
43
|
+
img_p = ImageTk.PhotoImage(img)
|
|
44
|
+
panel = Label(self.adaptor.frame, image=img_p)
|
|
45
|
+
panel.image = img_p
|
|
43
46
|
panel.pack()
|
|
44
47
|
case Path():
|
|
45
48
|
size = naturalsize(el.stat().st_size)
|
|
@@ -72,17 +72,16 @@ class TkWindow(Tk, BackendAdaptor):
|
|
|
72
72
|
self.form = Form(self.frame,
|
|
73
73
|
name_form="",
|
|
74
74
|
form_dict=formdict_to_widgetdict(form, self.widgetize),
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
name_button=submit if isinstance(submit, str) else "Ok",
|
|
76
|
+
button_command=self._ok if submit else None
|
|
77
77
|
)
|
|
78
78
|
self.form.pack()
|
|
79
79
|
|
|
80
80
|
# Add radio etc.
|
|
81
|
-
replace_widgets(self, self.form.
|
|
81
|
+
replace_widgets(self, self.form.fields, form)
|
|
82
82
|
|
|
83
83
|
# Set the submit and exit options
|
|
84
84
|
if self.form.button:
|
|
85
|
-
self.form.button.config(command=self._ok)
|
|
86
85
|
tip, keysym = ("Enter", "<Return>")
|
|
87
86
|
ToolTip(self.form.button, msg=tip) # NOTE is not destroyed in _clear
|
|
88
87
|
self._bind_event(keysym, self._ok)
|
|
@@ -6,12 +6,15 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
|
|
7
7
|
from autocombobox import AutoCombobox
|
|
8
8
|
|
|
9
|
+
from tkinter_form.tkinter_form import Form, FieldForm
|
|
10
|
+
|
|
9
11
|
from ..auxiliary import flatten, flatten_keys
|
|
10
12
|
from ..experimental import MININTERFACE_CONFIG, FacetCallback, SubmitButton
|
|
11
13
|
from ..form_dict import TagDict
|
|
12
14
|
from ..tag import Tag
|
|
13
15
|
from ..types import DatetimeTag, PathTag
|
|
14
16
|
from .date_entry import DateEntryFrame
|
|
17
|
+
from .external_fix import __create_widgets_monkeypatched
|
|
15
18
|
|
|
16
19
|
if TYPE_CHECKING:
|
|
17
20
|
from tk_window import TkWindow
|
|
@@ -42,13 +45,12 @@ class AnyVariable(Variable):
|
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
def ready_to_replace(widget: Widget,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
variable: Variable) -> tuple[Widget, dict]:
|
|
48
|
+
variable: Variable,
|
|
49
|
+
field_form: FieldForm) -> tuple[Widget, dict]:
|
|
48
50
|
if widget.winfo_manager() == 'grid':
|
|
49
51
|
grid_info = widget.grid_info()
|
|
50
52
|
widget.grid_forget()
|
|
51
|
-
|
|
53
|
+
field_form.variable = variable
|
|
52
54
|
return grid_info
|
|
53
55
|
else:
|
|
54
56
|
raise ValueError(f"GuiInterface: Cannot tackle the form, unknown winfo_manager {widget.winfo_manager()}.")
|
|
@@ -87,17 +89,18 @@ def _set_true(variable: Variable, tag: Tag):
|
|
|
87
89
|
|
|
88
90
|
def replace_widgets(tk_app: "TkWindow", nested_widgets, form: TagDict):
|
|
89
91
|
def _fetch(variable):
|
|
90
|
-
return ready_to_replace(widget,
|
|
92
|
+
return ready_to_replace(widget, variable, field_form)
|
|
91
93
|
|
|
92
94
|
# NOTE tab order broken, injected to another position
|
|
93
95
|
# NOTE should the button receive tag or directly
|
|
94
96
|
# the whole facet (to change the current form)? Specifiable by experimental.FacetCallback.
|
|
95
97
|
nested_widgets = widgets_to_dict(nested_widgets)
|
|
96
|
-
for
|
|
98
|
+
for tag, field_form in zip(flatten(form), flatten(nested_widgets)):
|
|
97
99
|
tag: Tag
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
field_form: FieldForm
|
|
101
|
+
label1: Widget = field_form.label
|
|
102
|
+
widget: Widget = field_form.widget
|
|
103
|
+
variable = field_form.variable
|
|
101
104
|
subwidgets = []
|
|
102
105
|
master = widget.master
|
|
103
106
|
|
|
@@ -136,6 +139,7 @@ def replace_widgets(tk_app: "TkWindow", nested_widgets, form: TagDict):
|
|
|
136
139
|
# Calendar
|
|
137
140
|
elif isinstance(tag, DatetimeTag):
|
|
138
141
|
grid_info = widget.grid_info()
|
|
142
|
+
widget.grid_forget()
|
|
139
143
|
nested_frame = DateEntryFrame(master, tk_app, tag, variable)
|
|
140
144
|
nested_frame.grid(row=grid_info['row'], column=grid_info['column'])
|
|
141
145
|
widget = nested_frame.spinbox
|
|
@@ -184,15 +188,15 @@ def create_button(master, _fetch, tag, label1, command=None):
|
|
|
184
188
|
return variable, widget2
|
|
185
189
|
|
|
186
190
|
|
|
187
|
-
def widgets_to_dict(widgets_dict) -> dict:
|
|
191
|
+
def widgets_to_dict(widgets_dict) -> dict[str, dict | FieldForm]:
|
|
188
192
|
""" Convert tkinter_form.widgets to a dict """
|
|
189
193
|
result = {}
|
|
190
194
|
for key, value in widgets_dict.items():
|
|
191
195
|
if isinstance(value, dict):
|
|
192
196
|
result[key] = widgets_to_dict(value)
|
|
193
|
-
elif
|
|
197
|
+
elif isinstance(value, Form):
|
|
194
198
|
# this is another tkinter_form.Form, recursively parse
|
|
195
|
-
result[key] = widgets_to_dict(value.
|
|
199
|
+
result[key] = widgets_to_dict(value.fields)
|
|
196
200
|
else: # value is a tuple of (Label, Widget (like Entry))
|
|
197
201
|
result[key] = value
|
|
198
202
|
return result
|
|
@@ -159,10 +159,7 @@ class PathTag(Tag):
|
|
|
159
159
|
@dataclass(repr=False)
|
|
160
160
|
class DatetimeTag(Tag):
|
|
161
161
|
"""
|
|
162
|
-
|
|
163
|
-
Experimental. Still in development.
|
|
164
|
-
|
|
165
|
-
Datetime is supported.
|
|
162
|
+
Datetime, date and time types are supported.
|
|
166
163
|
|
|
167
164
|
```python3
|
|
168
165
|
from datetime import datetime
|
|
@@ -214,15 +211,14 @@ class DatetimeTag(Tag):
|
|
|
214
211
|
# 
|
|
215
212
|
|
|
216
213
|
# NOTE: It would be nice we might put any date format to be parsed.
|
|
217
|
-
# NOTE: The parameters are still ignored.
|
|
218
214
|
|
|
219
215
|
date: bool = False
|
|
220
|
-
""" The date part is active """
|
|
216
|
+
""" The date part is active. True for datetime and date. """
|
|
221
217
|
|
|
222
218
|
time: bool = False
|
|
223
|
-
""" The time part is active """
|
|
219
|
+
""" The time part is active. True for datetime and time. """
|
|
224
220
|
|
|
225
|
-
full_precision:
|
|
221
|
+
full_precision: bool = False
|
|
226
222
|
""" Include full time precison, seconds, microseconds. """
|
|
227
223
|
|
|
228
224
|
def __post_init__(self):
|
|
@@ -230,9 +226,6 @@ class DatetimeTag(Tag):
|
|
|
230
226
|
if self.annotation:
|
|
231
227
|
self.date = issubclass(self.annotation, date)
|
|
232
228
|
self.time = issubclass(self.annotation, time) or issubclass(self.annotation, datetime)
|
|
233
|
-
if not self.date and not self.time:
|
|
234
|
-
self.date = self.time = True
|
|
235
|
-
# NOTE: self.full_precision ...
|
|
236
229
|
|
|
237
230
|
def _make_default_value(self):
|
|
238
231
|
return datetime.now()
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "mininterface"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.3"
|
|
8
8
|
description = "A minimal access to GUI, TUI, CLI and config"
|
|
9
9
|
authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
@@ -19,17 +19,18 @@ typing_extensions = "*"
|
|
|
19
19
|
pyyaml = "*"
|
|
20
20
|
# Standard requirements
|
|
21
21
|
autocombobox = "1.4.2"
|
|
22
|
-
humanize = "*"
|
|
22
|
+
humanize = "*" # used only in the TkInterface, hence it is not a minimal requirement
|
|
23
23
|
textual = "~0.84"
|
|
24
24
|
tkinter-tooltip = "*"
|
|
25
|
-
tkinter_form = "0.1
|
|
25
|
+
tkinter_form = "0.2.1"
|
|
26
26
|
tkscrollableframe = "*"
|
|
27
|
-
tkcalendar = "*" # TODO put into extras?
|
|
28
27
|
|
|
29
28
|
[tool.poetry.extras]
|
|
30
29
|
web = ["textual-serve"]
|
|
31
|
-
img = ["pillow"]
|
|
32
|
-
|
|
30
|
+
img = ["pillow", "textual_imageview"]
|
|
31
|
+
tui = ["textual_imageview"]
|
|
32
|
+
gui = ["pillow", "tkcalendar"]
|
|
33
|
+
all = ["textual-serve", "pillow", "tkcalendar", "textual_imageview"]
|
|
33
34
|
|
|
34
35
|
[tool.poetry.scripts]
|
|
35
36
|
mininterface = "mininterface.__main__:main"
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING
|
|
2
|
-
from warnings import warn
|
|
3
|
-
from ..facet import Facet
|
|
4
|
-
if TYPE_CHECKING:
|
|
5
|
-
from .textual_adaptor import TextualAdaptor
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TextualFacet(Facet):
|
|
9
|
-
adaptor: "TextualAdaptor"
|
|
10
|
-
|
|
11
|
-
def __init__(self, *args, **kwargs):
|
|
12
|
-
super().__init__(*args, **kwargs)
|
|
13
|
-
# Since TextualApp turns off, we need to have its values stored somewhere
|
|
14
|
-
self._title = ""
|
|
15
|
-
|
|
16
|
-
# NOTE: multiline title will not show up
|
|
17
|
-
def set_title(self, title: str):
|
|
18
|
-
self._title = title
|
|
19
|
-
try:
|
|
20
|
-
self.adaptor.app.title = title
|
|
21
|
-
except:
|
|
22
|
-
# NOTE: When you receive Facet in Command.init, the app does not exist yet
|
|
23
|
-
warn("Setting textual title not implemented well.")
|
|
24
|
-
|
|
25
|
-
def submit(self, *args, **kwargs):
|
|
26
|
-
super().submit(*args, **kwargs)
|
|
27
|
-
try:
|
|
28
|
-
self.adaptor.app.action_confirm()
|
|
29
|
-
except:
|
|
30
|
-
# NOTE: When you receive Facet in Command.init, the app does not exist yet
|
|
31
|
-
warn("Setting textual title not implemented well.")
|
|
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
|
{mininterface-0.7.1 → mininterface-0.7.3}/mininterface/tk_interface/redirect_text_tkinter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|