imgui_data_loader 0.1.0__py3-none-any.whl
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.
- imgui_data_loader/__init__.py +77 -0
- imgui_data_loader/_assets.py +53 -0
- imgui_data_loader/config.py +196 -0
- imgui_data_loader/dialog.py +348 -0
- imgui_data_loader/persistence.py +88 -0
- imgui_data_loader/py.typed +0 -0
- imgui_data_loader/runner.py +68 -0
- imgui_data_loader/theme.py +88 -0
- imgui_data_loader/widgets.py +127 -0
- imgui_data_loader-0.1.0.dist-info/METADATA +382 -0
- imgui_data_loader-0.1.0.dist-info/RECORD +14 -0
- imgui_data_loader-0.1.0.dist-info/WHEEL +5 -0
- imgui_data_loader-0.1.0.dist-info/licenses/LICENSE +21 -0
- imgui_data_loader-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Optional persistence for recent files + last-used directory.
|
|
2
|
+
|
|
3
|
+
Pass any object implementing :class:`PreferenceStore` as
|
|
4
|
+
``FileDialogConfig.persistence``. The dialog will seed the picker's start
|
|
5
|
+
directory from :meth:`default_dir` and call :meth:`record_selection` after a
|
|
6
|
+
successful pick. :class:`JsonPreferenceStore` is a batteries-included default.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
from ._assets import config_dir
|
|
16
|
+
|
|
17
|
+
try: # Protocol is 3.8+, runtime_checkable too
|
|
18
|
+
from typing import Protocol, runtime_checkable
|
|
19
|
+
except ImportError: # pragma: no cover
|
|
20
|
+
Protocol = object # type: ignore
|
|
21
|
+
|
|
22
|
+
def runtime_checkable(x): # type: ignore
|
|
23
|
+
return x
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class PreferenceStore(Protocol):
|
|
28
|
+
"""Minimal interface the dialog uses for persistence."""
|
|
29
|
+
|
|
30
|
+
def default_dir(self) -> str:
|
|
31
|
+
"""Directory to open the native picker in (``""`` to fall back)."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def recent(self) -> List[str]:
|
|
35
|
+
"""Most-recent-first list of previously selected paths."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
def record_selection(self, result) -> None:
|
|
39
|
+
"""Persist a completed :class:`DialogResult`."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class JsonPreferenceStore:
|
|
44
|
+
"""A JSON-file-backed :class:`PreferenceStore`.
|
|
45
|
+
|
|
46
|
+
Stores a capped most-recent-first list plus the last-used directory.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, path: Optional[str] = None, max_recent: int = 20):
|
|
50
|
+
self.path = Path(path) if path else (config_dir() / "recent.json")
|
|
51
|
+
self.max_recent = max_recent
|
|
52
|
+
self._data = {"recent": [], "last_dir": ""}
|
|
53
|
+
self._load()
|
|
54
|
+
|
|
55
|
+
def _load(self) -> None:
|
|
56
|
+
try:
|
|
57
|
+
if self.path.is_file():
|
|
58
|
+
loaded = json.loads(self.path.read_text())
|
|
59
|
+
if isinstance(loaded, dict):
|
|
60
|
+
self._data.update(loaded)
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def _save(self) -> None:
|
|
65
|
+
try:
|
|
66
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
self.path.write_text(json.dumps(self._data, indent=2))
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def default_dir(self) -> str:
|
|
72
|
+
d = self._data.get("last_dir", "")
|
|
73
|
+
return d if d and Path(d).exists() else ""
|
|
74
|
+
|
|
75
|
+
def recent(self) -> List[str]:
|
|
76
|
+
return list(self._data.get("recent", []))
|
|
77
|
+
|
|
78
|
+
def record_selection(self, result) -> None:
|
|
79
|
+
paths = list(getattr(result, "paths", None) or [])
|
|
80
|
+
if not paths:
|
|
81
|
+
return
|
|
82
|
+
recent = [p for p in self._data.get("recent", []) if p not in paths]
|
|
83
|
+
for p in reversed(paths):
|
|
84
|
+
recent.insert(0, p)
|
|
85
|
+
self._data["recent"] = recent[: self.max_recent]
|
|
86
|
+
first = Path(paths[0])
|
|
87
|
+
self._data["last_dir"] = str(first if first.is_dir() else first.parent)
|
|
88
|
+
self._save()
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""One-shot harness: open the dialog in its own window and return the result."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ._assets import default_ini_path, ensure_assets
|
|
9
|
+
from .config import DialogResult, FileDialogConfig
|
|
10
|
+
from .dialog import FileDialog
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_file_dialog(
|
|
14
|
+
config: Optional[FileDialogConfig] = None,
|
|
15
|
+
*,
|
|
16
|
+
runner_params=None,
|
|
17
|
+
) -> DialogResult:
|
|
18
|
+
"""Show the dialog, block until the user picks or quits, return the result.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
config : FileDialogConfig | None
|
|
23
|
+
Dialog configuration.
|
|
24
|
+
runner_params : hello_imgui.RunnerParams | None
|
|
25
|
+
Advanced: supply your own runner params (e.g. to select a null backend
|
|
26
|
+
for headless tests). Window title/size/ini are filled in from
|
|
27
|
+
``config`` only when not already set.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
DialogResult
|
|
32
|
+
``bool(result)`` is True for a real selection; the paths are in
|
|
33
|
+
``result.paths``. A quit / cancel yields an empty, ``cancelled`` result.
|
|
34
|
+
"""
|
|
35
|
+
from imgui_bundle import hello_imgui, immapp
|
|
36
|
+
|
|
37
|
+
config = config or FileDialogConfig()
|
|
38
|
+
ensure_assets(config.assets_folder)
|
|
39
|
+
|
|
40
|
+
dlg = FileDialog(config)
|
|
41
|
+
params = runner_params if runner_params is not None else hello_imgui.RunnerParams()
|
|
42
|
+
params.app_window_params.window_title = config.window_title or config.title
|
|
43
|
+
if config.window_size:
|
|
44
|
+
params.app_window_params.window_geometry.size = tuple(config.window_size)
|
|
45
|
+
params.app_window_params.window_geometry.size_auto = False
|
|
46
|
+
params.app_window_params.resizable = config.resizable
|
|
47
|
+
# Route the layout .ini. hello_imgui resolves ini_filename relative to
|
|
48
|
+
# ini_folder_type (default: the current working directory), so an empty or
|
|
49
|
+
# relative name would drop a file next to wherever the app was launched.
|
|
50
|
+
# Pin it to an absolute path instead (default: ~/.config/imgui_data_loader).
|
|
51
|
+
if not params.ini_filename:
|
|
52
|
+
ini = config.ini_path or default_ini_path()
|
|
53
|
+
params.ini_filename = ini
|
|
54
|
+
if os.path.isabs(ini):
|
|
55
|
+
params.ini_folder_type = hello_imgui.IniFolderType.absolute_path
|
|
56
|
+
parent = os.path.dirname(ini)
|
|
57
|
+
if parent:
|
|
58
|
+
os.makedirs(parent, exist_ok=True)
|
|
59
|
+
params.callbacks.show_gui = dlg.render
|
|
60
|
+
|
|
61
|
+
addons = immapp.AddOnsParams()
|
|
62
|
+
addons.with_markdown = True
|
|
63
|
+
addons.with_implot = False
|
|
64
|
+
addons.with_implot3d = False
|
|
65
|
+
|
|
66
|
+
immapp.run(runner_params=params, add_ons_params=addons)
|
|
67
|
+
|
|
68
|
+
return dlg.result or DialogResult(paths=[], kind=None, cancelled=True)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Colors and style for the file dialog.
|
|
2
|
+
|
|
3
|
+
A :class:`Theme` is a plain dataclass of RGBA float tuples, so it can be
|
|
4
|
+
constructed, copied, and inspected without an active imgui context (handy in
|
|
5
|
+
tests). Colors are converted to ``imgui.ImVec4`` lazily at draw time.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, replace
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
|
|
13
|
+
from imgui_bundle import imgui
|
|
14
|
+
|
|
15
|
+
Color = Tuple[float, float, float, float]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def to_vec4(color) -> "imgui.ImVec4":
|
|
19
|
+
"""Coerce an ``(r, g, b[, a])`` tuple (or an ImVec4) to ``imgui.ImVec4``."""
|
|
20
|
+
if isinstance(color, imgui.ImVec4):
|
|
21
|
+
return color
|
|
22
|
+
r, g, b = color[0], color[1], color[2]
|
|
23
|
+
a = color[3] if len(color) > 3 else 1.0
|
|
24
|
+
return imgui.ImVec4(r, g, b, a)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Theme:
|
|
29
|
+
"""Color palette + a couple of rounding knobs for the dialog.
|
|
30
|
+
|
|
31
|
+
Every field is an ``(r, g, b, a)`` float tuple in the 0..1 range. Build a
|
|
32
|
+
variant with :meth:`replace`, e.g. ``Theme.dark().replace(accent=(...))``.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
bg: Color = (0.11, 0.11, 0.12, 1.0)
|
|
36
|
+
bg_card: Color = (0.16, 0.16, 0.17, 1.0)
|
|
37
|
+
accent: Color = (0.20, 0.50, 0.85, 1.0)
|
|
38
|
+
accent_hover: Color = (0.25, 0.55, 0.90, 1.0)
|
|
39
|
+
accent_active: Color = (0.15, 0.45, 0.80, 1.0)
|
|
40
|
+
text: Color = (1.0, 1.0, 1.0, 1.0)
|
|
41
|
+
text_dim: Color = (0.75, 0.75, 0.77, 1.0)
|
|
42
|
+
border: Color = (0.35, 0.35, 0.37, 0.7)
|
|
43
|
+
secondary: Color = (0.35, 0.35, 0.37, 1.0)
|
|
44
|
+
secondary_hover: Color = (0.42, 0.42, 0.44, 1.0)
|
|
45
|
+
secondary_active: Color = (0.28, 0.28, 0.30, 1.0)
|
|
46
|
+
ok: Color = (0.40, 1.00, 0.40, 1.0)
|
|
47
|
+
warn: Color = (1.00, 0.80, 0.20, 1.0)
|
|
48
|
+
err: Color = (1.00, 0.40, 0.40, 1.0)
|
|
49
|
+
na: Color = (0.50, 0.50, 0.50, 1.0)
|
|
50
|
+
frame_bg: Color = (0.22, 0.22, 0.23, 1.0)
|
|
51
|
+
frame_bg_hovered: Color = (0.28, 0.28, 0.29, 1.0)
|
|
52
|
+
separator: Color = (0.35, 0.35, 0.37, 0.6)
|
|
53
|
+
# icon_button face (neutral bg behind the accent-colored icon + label)
|
|
54
|
+
button_face: Color = (0.18, 0.18, 0.20, 1.0)
|
|
55
|
+
button_face_hover: Color = (0.22, 0.22, 0.25, 1.0)
|
|
56
|
+
button_face_active: Color = (0.15, 0.15, 0.17, 1.0)
|
|
57
|
+
frame_rounding: float = 6.0
|
|
58
|
+
child_rounding: float = 6.0
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def dark() -> "Theme":
|
|
62
|
+
return Theme()
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def light() -> "Theme":
|
|
66
|
+
return Theme(
|
|
67
|
+
bg=(0.94, 0.94, 0.95, 1.0),
|
|
68
|
+
bg_card=(0.99, 0.99, 1.00, 1.0),
|
|
69
|
+
accent=(0.10, 0.45, 0.80, 1.0),
|
|
70
|
+
accent_hover=(0.15, 0.50, 0.85, 1.0),
|
|
71
|
+
accent_active=(0.05, 0.40, 0.75, 1.0),
|
|
72
|
+
text=(0.10, 0.10, 0.12, 1.0),
|
|
73
|
+
text_dim=(0.35, 0.35, 0.40, 1.0),
|
|
74
|
+
border=(0.70, 0.70, 0.74, 0.9),
|
|
75
|
+
secondary=(0.80, 0.80, 0.83, 1.0),
|
|
76
|
+
secondary_hover=(0.86, 0.86, 0.89, 1.0),
|
|
77
|
+
secondary_active=(0.72, 0.72, 0.75, 1.0),
|
|
78
|
+
frame_bg=(0.88, 0.88, 0.90, 1.0),
|
|
79
|
+
frame_bg_hovered=(0.82, 0.82, 0.85, 1.0),
|
|
80
|
+
separator=(0.70, 0.70, 0.74, 0.6),
|
|
81
|
+
button_face=(0.90, 0.90, 0.92, 1.0),
|
|
82
|
+
button_face_hover=(0.85, 0.85, 0.88, 1.0),
|
|
83
|
+
button_face_active=(0.80, 0.80, 0.83, 1.0),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def replace(self, **changes) -> "Theme":
|
|
87
|
+
"""Return a copy with the given fields overridden."""
|
|
88
|
+
return replace(self, **changes)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Reusable, themed imgui helpers.
|
|
2
|
+
|
|
3
|
+
These are standalone: call them from your own ``info``/``options`` callbacks to
|
|
4
|
+
match the dialog's look (centered text, wrapped tooltips, accent icon buttons).
|
|
5
|
+
Colors accept either an ``imgui.ImVec4`` or a plain ``(r, g, b[, a])`` tuple.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
|
|
12
|
+
from imgui_bundle import hello_imgui, imgui
|
|
13
|
+
|
|
14
|
+
from .theme import Theme, to_vec4
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def call_draw(cb, dialog) -> None:
|
|
18
|
+
"""Invoke a draw callback, passing ``dialog`` only if it takes an argument.
|
|
19
|
+
|
|
20
|
+
Supports ``fn(dialog)``, ``fn()``, bound methods, and ``fn(*args)``.
|
|
21
|
+
"""
|
|
22
|
+
if cb is None:
|
|
23
|
+
return
|
|
24
|
+
try:
|
|
25
|
+
params = inspect.signature(cb).parameters.values()
|
|
26
|
+
wants_arg = any(
|
|
27
|
+
p.kind
|
|
28
|
+
in (
|
|
29
|
+
p.POSITIONAL_ONLY,
|
|
30
|
+
p.POSITIONAL_OR_KEYWORD,
|
|
31
|
+
p.VAR_POSITIONAL,
|
|
32
|
+
)
|
|
33
|
+
for p in params
|
|
34
|
+
)
|
|
35
|
+
except (TypeError, ValueError):
|
|
36
|
+
wants_arg = True
|
|
37
|
+
if wants_arg:
|
|
38
|
+
cb(dialog)
|
|
39
|
+
else:
|
|
40
|
+
cb()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def wrapped_tooltip(text: str, wrap_em: float = 24.0) -> None:
|
|
44
|
+
"""A tooltip whose text wraps to ``wrap_em`` em units (fits narrow dialogs)."""
|
|
45
|
+
imgui.begin_tooltip()
|
|
46
|
+
try:
|
|
47
|
+
imgui.push_text_wrap_pos(hello_imgui.em_size(wrap_em))
|
|
48
|
+
imgui.text_unformatted(text)
|
|
49
|
+
imgui.pop_text_wrap_pos()
|
|
50
|
+
finally:
|
|
51
|
+
imgui.end_tooltip()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def text_wrapped_colored(color, text: str) -> None:
|
|
55
|
+
"""Colored equivalent of ``imgui.text_wrapped``."""
|
|
56
|
+
imgui.push_style_color(imgui.Col_.text, to_vec4(color))
|
|
57
|
+
try:
|
|
58
|
+
imgui.text_wrapped(text)
|
|
59
|
+
finally:
|
|
60
|
+
imgui.pop_style_color()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def center_text(text: str, color=None) -> None:
|
|
64
|
+
"""Draw horizontally centered text."""
|
|
65
|
+
avail_w = imgui.get_content_region_avail().x
|
|
66
|
+
text_sz = imgui.calc_text_size(text)
|
|
67
|
+
imgui.set_cursor_pos_x(imgui.get_cursor_pos_x() + (avail_w - text_sz.x) * 0.5)
|
|
68
|
+
if color is not None:
|
|
69
|
+
imgui.text_colored(to_vec4(color), text)
|
|
70
|
+
else:
|
|
71
|
+
imgui.text(text)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def center_next_item(width: float) -> None:
|
|
75
|
+
"""Move the cursor so the next ``width``-wide item is centered."""
|
|
76
|
+
avail_w = imgui.get_content_region_avail().x
|
|
77
|
+
imgui.set_cursor_pos_x(imgui.get_cursor_pos_x() + (avail_w - width) * 0.5)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def push_button_style(theme: Theme, primary: bool = True) -> None:
|
|
81
|
+
"""Push button colors (accent if ``primary`` else neutral). Pair with
|
|
82
|
+
:func:`pop_button_style`."""
|
|
83
|
+
if primary:
|
|
84
|
+
imgui.push_style_color(imgui.Col_.button, to_vec4(theme.accent))
|
|
85
|
+
imgui.push_style_color(imgui.Col_.button_hovered, to_vec4(theme.accent_hover))
|
|
86
|
+
imgui.push_style_color(imgui.Col_.button_active, to_vec4(theme.accent_active))
|
|
87
|
+
else:
|
|
88
|
+
imgui.push_style_color(imgui.Col_.button, to_vec4(theme.secondary))
|
|
89
|
+
imgui.push_style_color(imgui.Col_.button_hovered, to_vec4(theme.secondary_hover))
|
|
90
|
+
imgui.push_style_color(imgui.Col_.button_active, to_vec4(theme.secondary_active))
|
|
91
|
+
imgui.push_style_color(imgui.Col_.text, to_vec4(theme.text))
|
|
92
|
+
imgui.push_style_var(imgui.StyleVar_.frame_rounding, theme.frame_rounding)
|
|
93
|
+
imgui.push_style_var(imgui.StyleVar_.frame_border_size, 0.0)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def pop_button_style() -> None:
|
|
97
|
+
imgui.pop_style_var(2)
|
|
98
|
+
imgui.pop_style_color(4)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def icon_button(
|
|
102
|
+
icon: str,
|
|
103
|
+
label: str,
|
|
104
|
+
size,
|
|
105
|
+
theme: Theme = None,
|
|
106
|
+
tooltip: str = "",
|
|
107
|
+
) -> bool:
|
|
108
|
+
"""A neutral-faced button with an accent-colored icon + label and a blue
|
|
109
|
+
outline. ``icon`` may be empty. Returns True on click."""
|
|
110
|
+
if theme is None:
|
|
111
|
+
theme = Theme.dark()
|
|
112
|
+
imgui.push_style_color(imgui.Col_.button, to_vec4(theme.button_face))
|
|
113
|
+
imgui.push_style_color(imgui.Col_.button_hovered, to_vec4(theme.button_face_hover))
|
|
114
|
+
imgui.push_style_color(imgui.Col_.button_active, to_vec4(theme.button_face_active))
|
|
115
|
+
imgui.push_style_color(imgui.Col_.text, to_vec4(theme.accent))
|
|
116
|
+
imgui.push_style_color(imgui.Col_.border, to_vec4(theme.accent))
|
|
117
|
+
imgui.push_style_var(imgui.StyleVar_.frame_rounding, theme.frame_rounding)
|
|
118
|
+
imgui.push_style_var(imgui.StyleVar_.frame_border_size, 1.5)
|
|
119
|
+
|
|
120
|
+
button_text = f"{icon} {label}" if icon else label
|
|
121
|
+
clicked = imgui.button(button_text, size)
|
|
122
|
+
if tooltip and imgui.is_item_hovered():
|
|
123
|
+
wrapped_tooltip(tooltip)
|
|
124
|
+
|
|
125
|
+
imgui.pop_style_var(2)
|
|
126
|
+
imgui.pop_style_color(5)
|
|
127
|
+
return clicked
|