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.
@@ -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