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,77 @@
|
|
|
1
|
+
"""imgui_data_loader — a themed, configurable imgui file/folder open dialog.
|
|
2
|
+
|
|
3
|
+
Quick start
|
|
4
|
+
-----------
|
|
5
|
+
>>> from imgui_data_loader import run_file_dialog, FileDialogConfig, FileType
|
|
6
|
+
>>> result = run_file_dialog(FileDialogConfig(
|
|
7
|
+
... title="My App",
|
|
8
|
+
... filetypes=[FileType("Images", "*.tif *.png"), FileType("All Files", "*")],
|
|
9
|
+
... ))
|
|
10
|
+
>>> if result:
|
|
11
|
+
... print(result.paths)
|
|
12
|
+
|
|
13
|
+
Embed it in your own imgui/hello_imgui app instead:
|
|
14
|
+
>>> from imgui_data_loader import FileDialog, FileDialogConfig
|
|
15
|
+
>>> dlg = FileDialog(FileDialogConfig(close_on_select=False))
|
|
16
|
+
>>> # params.callbacks.show_gui = dlg.render
|
|
17
|
+
>>> # each frame: r = dlg.take_result(); if r: ...
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from ._assets import ensure_assets
|
|
23
|
+
from .config import (
|
|
24
|
+
ButtonSpec,
|
|
25
|
+
DialogResult,
|
|
26
|
+
FileDialogConfig,
|
|
27
|
+
FileType,
|
|
28
|
+
PickKind,
|
|
29
|
+
flatten_filters,
|
|
30
|
+
)
|
|
31
|
+
from .dialog import FileDialog
|
|
32
|
+
from .persistence import JsonPreferenceStore, PreferenceStore
|
|
33
|
+
from .runner import run_file_dialog
|
|
34
|
+
from .theme import Theme, to_vec4
|
|
35
|
+
from .widgets import (
|
|
36
|
+
call_draw,
|
|
37
|
+
center_next_item,
|
|
38
|
+
center_text,
|
|
39
|
+
icon_button,
|
|
40
|
+
pop_button_style,
|
|
41
|
+
push_button_style,
|
|
42
|
+
text_wrapped_colored,
|
|
43
|
+
wrapped_tooltip,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
# config
|
|
50
|
+
"FileDialogConfig",
|
|
51
|
+
"FileType",
|
|
52
|
+
"ButtonSpec",
|
|
53
|
+
"PickKind",
|
|
54
|
+
"DialogResult",
|
|
55
|
+
"flatten_filters",
|
|
56
|
+
# dialog + harness
|
|
57
|
+
"FileDialog",
|
|
58
|
+
"run_file_dialog",
|
|
59
|
+
"ensure_assets",
|
|
60
|
+
# theme
|
|
61
|
+
"Theme",
|
|
62
|
+
"to_vec4",
|
|
63
|
+
# persistence
|
|
64
|
+
"PreferenceStore",
|
|
65
|
+
"JsonPreferenceStore",
|
|
66
|
+
# reusable widgets
|
|
67
|
+
"icon_button",
|
|
68
|
+
"wrapped_tooltip",
|
|
69
|
+
"text_wrapped_colored",
|
|
70
|
+
"center_text",
|
|
71
|
+
"center_next_item",
|
|
72
|
+
"push_button_style",
|
|
73
|
+
"pop_button_style",
|
|
74
|
+
"call_draw",
|
|
75
|
+
# meta
|
|
76
|
+
"__version__",
|
|
77
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Asset / config-path helpers.
|
|
2
|
+
|
|
3
|
+
The dialog relies on the FontAwesome 6 font so button icons render. That font
|
|
4
|
+
ships inside ``imgui-bundle`` and hello_imgui loads it into the default font
|
|
5
|
+
when the assets folder points at a directory containing
|
|
6
|
+
``fonts/Font_Awesome_6_Free-Solid-900.otf``. :func:`ensure_assets` points
|
|
7
|
+
hello_imgui at imgui-bundle's own bundled assets, so the library ships no fonts
|
|
8
|
+
of its own.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def imgui_bundle_assets_dir() -> Optional[str]:
|
|
19
|
+
"""Path to imgui-bundle's bundled ``assets`` folder, or None."""
|
|
20
|
+
try:
|
|
21
|
+
import imgui_bundle
|
|
22
|
+
|
|
23
|
+
p = Path(imgui_bundle.__file__).parent / "assets"
|
|
24
|
+
return str(p) if p.is_dir() else None
|
|
25
|
+
except Exception:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def ensure_assets(assets_folder: Optional[str] = None) -> None:
|
|
30
|
+
"""Point hello_imgui at an assets folder that provides the icon font.
|
|
31
|
+
|
|
32
|
+
Defaults to imgui-bundle's bundled assets. Pass ``assets_folder`` to use
|
|
33
|
+
your own (it must contain ``fonts/Font_Awesome_6_Free-Solid-900.otf`` for
|
|
34
|
+
the icons to show).
|
|
35
|
+
"""
|
|
36
|
+
from imgui_bundle import hello_imgui
|
|
37
|
+
|
|
38
|
+
folder = assets_folder or imgui_bundle_assets_dir()
|
|
39
|
+
if folder:
|
|
40
|
+
hello_imgui.set_assets_folder(str(folder))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def config_dir() -> Path:
|
|
44
|
+
"""Per-user config dir (``$XDG_CONFIG_HOME/imgui_data_loader``)."""
|
|
45
|
+
base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
|
|
46
|
+
d = Path(base) / "imgui_data_loader"
|
|
47
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
return d
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def default_ini_path(name: str = "file_dialog") -> str:
|
|
52
|
+
"""Path for hello_imgui's ``.ini`` (window layout) file."""
|
|
53
|
+
return str(config_dir() / f"{name}.ini")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Configuration objects for the file dialog.
|
|
2
|
+
|
|
3
|
+
None of these require an active imgui context, so a config can be built and
|
|
4
|
+
inspected anywhere (``_default_buttons`` only imports icon *string constants*).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Callable,
|
|
14
|
+
List,
|
|
15
|
+
Optional,
|
|
16
|
+
Sequence,
|
|
17
|
+
Union,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .theme import Theme
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
23
|
+
from .dialog import FileDialog
|
|
24
|
+
from .persistence import PreferenceStore
|
|
25
|
+
|
|
26
|
+
# A draw callback may take the FileDialog instance or nothing; both are
|
|
27
|
+
# accepted (see widgets.call_draw for the arity handling).
|
|
28
|
+
DrawCallback = Callable[..., None]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PickKind(str, Enum):
|
|
32
|
+
"""What a button asks the OS picker for."""
|
|
33
|
+
|
|
34
|
+
OPEN_FILE = "open_file"
|
|
35
|
+
SELECT_FOLDER = "select_folder"
|
|
36
|
+
SAVE_FILE = "save_file"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class FileType:
|
|
41
|
+
"""A named file filter, e.g. ``FileType("Images", "*.tif *.png")``.
|
|
42
|
+
|
|
43
|
+
``patterns`` is a space-separated glob string (portable_file_dialogs
|
|
44
|
+
format). A list/tuple of patterns is also accepted and joined.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
patterns: str = "*"
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
if isinstance(self.patterns, (list, tuple)):
|
|
52
|
+
self.patterns = " ".join(self.patterns)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def flatten_filters(filetypes: Sequence[FileType]) -> List[str]:
|
|
56
|
+
"""Flatten ``[FileType, ...]`` into pfd's ``[name, patterns, ...]`` list."""
|
|
57
|
+
out: List[str] = []
|
|
58
|
+
for ft in filetypes:
|
|
59
|
+
out.append(ft.name)
|
|
60
|
+
out.append(ft.patterns)
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ButtonSpec:
|
|
66
|
+
"""One picker button on the dialog.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
label : str
|
|
71
|
+
Button text.
|
|
72
|
+
kind : PickKind
|
|
73
|
+
Which native picker to open.
|
|
74
|
+
icon : str
|
|
75
|
+
A FontAwesome 6 icon character (e.g. ``fa.ICON_FA_FOLDER_OPEN``);
|
|
76
|
+
optional.
|
|
77
|
+
tooltip : str
|
|
78
|
+
Hover tooltip.
|
|
79
|
+
multiselect : bool
|
|
80
|
+
For ``OPEN_FILE`` only: allow selecting several files.
|
|
81
|
+
filetypes : list[FileType] | None
|
|
82
|
+
Overrides the dialog-wide ``filetypes`` for this button.
|
|
83
|
+
title : str | None
|
|
84
|
+
Native dialog window title (defaults to ``label``).
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
label: str
|
|
88
|
+
kind: PickKind = PickKind.OPEN_FILE
|
|
89
|
+
icon: str = ""
|
|
90
|
+
tooltip: str = ""
|
|
91
|
+
multiselect: bool = False
|
|
92
|
+
filetypes: Optional[List[FileType]] = None
|
|
93
|
+
title: Optional[str] = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class DialogResult:
|
|
98
|
+
"""Outcome of the dialog.
|
|
99
|
+
|
|
100
|
+
``paths`` holds the selected path(s); it is empty when the user quit /
|
|
101
|
+
cancelled. ``bool(result)`` is True only for a real, non-empty selection.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
paths: List[str] = field(default_factory=list)
|
|
105
|
+
kind: Optional[PickKind] = None
|
|
106
|
+
cancelled: bool = False
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def path(self) -> Optional[str]:
|
|
110
|
+
"""First selected path, or None."""
|
|
111
|
+
return self.paths[0] if self.paths else None
|
|
112
|
+
|
|
113
|
+
def __bool__(self) -> bool:
|
|
114
|
+
return bool(self.paths) and not self.cancelled
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _default_buttons() -> List[ButtonSpec]:
|
|
118
|
+
from imgui_bundle import icons_fontawesome_6 as fa
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
ButtonSpec(
|
|
122
|
+
"Open File(s)",
|
|
123
|
+
PickKind.OPEN_FILE,
|
|
124
|
+
icon=fa.ICON_FA_FILE_IMAGE,
|
|
125
|
+
tooltip="Select one or more files",
|
|
126
|
+
multiselect=True,
|
|
127
|
+
),
|
|
128
|
+
ButtonSpec(
|
|
129
|
+
"Select Folder",
|
|
130
|
+
PickKind.SELECT_FOLDER,
|
|
131
|
+
icon=fa.ICON_FA_FOLDER_OPEN,
|
|
132
|
+
tooltip="Select a folder",
|
|
133
|
+
),
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class FileDialogConfig:
|
|
139
|
+
"""Everything the dialog can be told to do.
|
|
140
|
+
|
|
141
|
+
All fields have sensible defaults; the empty ``FileDialogConfig()`` gives
|
|
142
|
+
an "Open File(s)" + "Select Folder" launcher.
|
|
143
|
+
|
|
144
|
+
Content slots (``header_draw``, ``top_draw``, ``info``, ``options_draw``,
|
|
145
|
+
``footer_draw``) are callbacks that draw arbitrary imgui inside the dialog.
|
|
146
|
+
Each callback may accept the :class:`FileDialog` instance or no argument.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
# --- header / branding ---
|
|
150
|
+
title: str = "Open Data"
|
|
151
|
+
subtitle: str = ""
|
|
152
|
+
|
|
153
|
+
# --- picker buttons ---
|
|
154
|
+
buttons: List[ButtonSpec] = field(default_factory=_default_buttons)
|
|
155
|
+
filetypes: List[FileType] = field(
|
|
156
|
+
default_factory=lambda: [FileType("All Files", "*")]
|
|
157
|
+
)
|
|
158
|
+
default_dir: str = ""
|
|
159
|
+
|
|
160
|
+
# --- look ---
|
|
161
|
+
theme: Theme = field(default_factory=Theme.dark)
|
|
162
|
+
|
|
163
|
+
# --- content slots (each: fn(dialog) or fn()) ---
|
|
164
|
+
header_draw: Optional[DrawCallback] = None
|
|
165
|
+
top_draw: Optional[DrawCallback] = None
|
|
166
|
+
info: Union[DrawCallback, Sequence[DrawCallback], None] = None
|
|
167
|
+
options_draw: Optional[DrawCallback] = None
|
|
168
|
+
footer_draw: Optional[DrawCallback] = None
|
|
169
|
+
|
|
170
|
+
# --- footer labels / behavior ---
|
|
171
|
+
options_label: str = "Options"
|
|
172
|
+
show_options_button: bool = True
|
|
173
|
+
show_quit_button: bool = True
|
|
174
|
+
quit_label: str = "Quit"
|
|
175
|
+
quit_on_escape: bool = True
|
|
176
|
+
close_on_select: bool = True
|
|
177
|
+
|
|
178
|
+
# --- window / harness (used by run_file_dialog) ---
|
|
179
|
+
window_title: Optional[str] = None
|
|
180
|
+
window_size: tuple = (360, 720)
|
|
181
|
+
resizable: bool = True
|
|
182
|
+
ini_path: Optional[str] = None
|
|
183
|
+
assets_folder: Optional[str] = None
|
|
184
|
+
|
|
185
|
+
# --- integrations ---
|
|
186
|
+
persistence: "Optional[PreferenceStore]" = None
|
|
187
|
+
on_select: Optional[Callable[[DialogResult], None]] = None
|
|
188
|
+
on_cancel: Optional[Callable[[], None]] = None
|
|
189
|
+
|
|
190
|
+
def info_callbacks(self) -> List[DrawCallback]:
|
|
191
|
+
"""Normalize ``info`` to a list of callbacks."""
|
|
192
|
+
if self.info is None:
|
|
193
|
+
return []
|
|
194
|
+
if callable(self.info):
|
|
195
|
+
return [self.info]
|
|
196
|
+
return list(self.info)
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""The :class:`FileDialog` widget.
|
|
2
|
+
|
|
3
|
+
Render it inside a hello_imgui / immapp frame (set ``params.callbacks.show_gui
|
|
4
|
+
= dialog.render``). Buttons trigger the OS-native picker via
|
|
5
|
+
``portable_file_dialogs``; when a selection completes it lands on
|
|
6
|
+
:attr:`FileDialog.result`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from imgui_bundle import (
|
|
15
|
+
hello_imgui,
|
|
16
|
+
icons_fontawesome_6 as fa,
|
|
17
|
+
imgui,
|
|
18
|
+
imgui_ctx,
|
|
19
|
+
portable_file_dialogs as pfd,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .config import (
|
|
23
|
+
ButtonSpec,
|
|
24
|
+
DialogResult,
|
|
25
|
+
FileDialogConfig,
|
|
26
|
+
FileType,
|
|
27
|
+
PickKind,
|
|
28
|
+
flatten_filters,
|
|
29
|
+
)
|
|
30
|
+
from .theme import Theme, to_vec4
|
|
31
|
+
from .widgets import (
|
|
32
|
+
call_draw,
|
|
33
|
+
center_next_item,
|
|
34
|
+
center_text,
|
|
35
|
+
icon_button,
|
|
36
|
+
pop_button_style,
|
|
37
|
+
push_button_style,
|
|
38
|
+
wrapped_tooltip,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FileDialog:
|
|
43
|
+
"""A themed, configurable file/folder open dialog.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
config : FileDialogConfig | None
|
|
48
|
+
Behavior and content. Defaults to a plain "Open File(s) / Select
|
|
49
|
+
Folder" launcher.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, config: Optional[FileDialogConfig] = None):
|
|
53
|
+
self.config = config or FileDialogConfig()
|
|
54
|
+
self._pending = None # active portable_file_dialogs handle
|
|
55
|
+
self._pending_kind: Optional[PickKind] = None
|
|
56
|
+
self._result: Optional[DialogResult] = None
|
|
57
|
+
self._open_options = False
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
# public surface
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
@property
|
|
63
|
+
def theme(self) -> Theme:
|
|
64
|
+
return self.config.theme
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def result(self) -> Optional[DialogResult]:
|
|
68
|
+
"""Last completed result (peek; does not clear)."""
|
|
69
|
+
return self._result
|
|
70
|
+
|
|
71
|
+
def take_result(self) -> Optional[DialogResult]:
|
|
72
|
+
"""Return the last result and clear it (for embedded per-frame polling)."""
|
|
73
|
+
r, self._result = self._result, None
|
|
74
|
+
return r
|
|
75
|
+
|
|
76
|
+
def open_options(self) -> None:
|
|
77
|
+
"""Programmatically open the Options popup on the next frame."""
|
|
78
|
+
self._open_options = True
|
|
79
|
+
|
|
80
|
+
def cancel(self) -> None:
|
|
81
|
+
"""Finish with an empty, cancelled result (as the Quit button does)."""
|
|
82
|
+
self._finish(DialogResult(paths=[], kind=None, cancelled=True))
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# native picker plumbing
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
def _start_dir(self) -> str:
|
|
88
|
+
if self.config.default_dir:
|
|
89
|
+
return str(self.config.default_dir)
|
|
90
|
+
store = self.config.persistence
|
|
91
|
+
if store is not None:
|
|
92
|
+
try:
|
|
93
|
+
d = store.default_dir()
|
|
94
|
+
if d:
|
|
95
|
+
return str(d)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
return str(Path.home())
|
|
99
|
+
|
|
100
|
+
def _filters_for(self, btn: ButtonSpec) -> List[str]:
|
|
101
|
+
fts = btn.filetypes if btn.filetypes is not None else self.config.filetypes
|
|
102
|
+
return flatten_filters(fts or [FileType("All Files", "*")])
|
|
103
|
+
|
|
104
|
+
def _launch(self, btn: ButtonSpec) -> None:
|
|
105
|
+
start = self._start_dir()
|
|
106
|
+
title = btn.title or btn.label
|
|
107
|
+
if btn.kind == PickKind.OPEN_FILE:
|
|
108
|
+
opt = pfd.opt.multiselect if btn.multiselect else pfd.opt.none
|
|
109
|
+
self._pending = pfd.open_file(title, start, self._filters_for(btn), opt)
|
|
110
|
+
elif btn.kind == PickKind.SELECT_FOLDER:
|
|
111
|
+
self._pending = pfd.select_folder(title, start)
|
|
112
|
+
elif btn.kind == PickKind.SAVE_FILE:
|
|
113
|
+
self._pending = pfd.save_file(title, start, self._filters_for(btn))
|
|
114
|
+
else:
|
|
115
|
+
return
|
|
116
|
+
self._pending_kind = btn.kind
|
|
117
|
+
|
|
118
|
+
def _poll(self) -> None:
|
|
119
|
+
if self._pending is None or not self._pending.ready():
|
|
120
|
+
return
|
|
121
|
+
raw = self._pending.result()
|
|
122
|
+
kind = self._pending_kind
|
|
123
|
+
self._pending = None
|
|
124
|
+
self._pending_kind = None
|
|
125
|
+
if isinstance(raw, list):
|
|
126
|
+
paths = [p for p in raw if p]
|
|
127
|
+
elif raw:
|
|
128
|
+
paths = [raw]
|
|
129
|
+
else:
|
|
130
|
+
paths = []
|
|
131
|
+
# An empty result means the user cancelled the native picker itself;
|
|
132
|
+
# stay open so they can try another button.
|
|
133
|
+
if paths:
|
|
134
|
+
self._finish(DialogResult(paths=paths, kind=kind, cancelled=False))
|
|
135
|
+
|
|
136
|
+
def _finish(self, result: DialogResult) -> None:
|
|
137
|
+
self._result = result
|
|
138
|
+
cfg = self.config
|
|
139
|
+
if not result.cancelled:
|
|
140
|
+
if cfg.persistence is not None:
|
|
141
|
+
try:
|
|
142
|
+
cfg.persistence.record_selection(result)
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
if cfg.on_select is not None:
|
|
146
|
+
cfg.on_select(result)
|
|
147
|
+
if cfg.close_on_select:
|
|
148
|
+
self._request_exit()
|
|
149
|
+
else:
|
|
150
|
+
if cfg.on_cancel is not None:
|
|
151
|
+
cfg.on_cancel()
|
|
152
|
+
self._request_exit()
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _request_exit() -> None:
|
|
156
|
+
try:
|
|
157
|
+
hello_imgui.get_runner_params().app_shall_exit = True
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
# rendering
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
def render(self) -> None:
|
|
165
|
+
"""Draw one frame of the dialog. Use as ``callbacks.show_gui``."""
|
|
166
|
+
theme = self.theme
|
|
167
|
+
imgui.push_style_color(imgui.Col_.window_bg, to_vec4(theme.bg))
|
|
168
|
+
imgui.push_style_color(imgui.Col_.child_bg, imgui.ImVec4(0, 0, 0, 0))
|
|
169
|
+
imgui.push_style_color(imgui.Col_.text, to_vec4(theme.text))
|
|
170
|
+
imgui.push_style_color(imgui.Col_.border, to_vec4(theme.border))
|
|
171
|
+
imgui.push_style_color(imgui.Col_.separator, to_vec4(theme.separator))
|
|
172
|
+
imgui.push_style_color(imgui.Col_.frame_bg, to_vec4(theme.frame_bg))
|
|
173
|
+
imgui.push_style_color(imgui.Col_.frame_bg_hovered, to_vec4(theme.frame_bg_hovered))
|
|
174
|
+
imgui.push_style_color(imgui.Col_.check_mark, to_vec4(theme.accent))
|
|
175
|
+
imgui.push_style_var(imgui.StyleVar_.window_padding, hello_imgui.em_to_vec2(1.0, 0.8))
|
|
176
|
+
imgui.push_style_var(imgui.StyleVar_.frame_padding, hello_imgui.em_to_vec2(0.6, 0.4))
|
|
177
|
+
imgui.push_style_var(imgui.StyleVar_.item_spacing, hello_imgui.em_to_vec2(0.6, 0.4))
|
|
178
|
+
imgui.push_style_var(imgui.StyleVar_.frame_rounding, theme.frame_rounding)
|
|
179
|
+
|
|
180
|
+
with imgui_ctx.begin_child(
|
|
181
|
+
"##idl_main",
|
|
182
|
+
size=imgui.ImVec2(0, 0),
|
|
183
|
+
window_flags=imgui.WindowFlags_.no_scrollbar,
|
|
184
|
+
):
|
|
185
|
+
imgui.push_id("imgui_data_loader")
|
|
186
|
+
|
|
187
|
+
self._draw_header()
|
|
188
|
+
|
|
189
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.3))
|
|
190
|
+
imgui.separator()
|
|
191
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.3))
|
|
192
|
+
|
|
193
|
+
if self.config.top_draw is not None:
|
|
194
|
+
call_draw(self.config.top_draw, self)
|
|
195
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.2))
|
|
196
|
+
|
|
197
|
+
self._draw_buttons()
|
|
198
|
+
self._draw_info_card()
|
|
199
|
+
self._draw_options_popup()
|
|
200
|
+
self._poll()
|
|
201
|
+
self._draw_footer()
|
|
202
|
+
|
|
203
|
+
imgui.pop_id()
|
|
204
|
+
|
|
205
|
+
imgui.pop_style_var(4)
|
|
206
|
+
imgui.pop_style_color(8)
|
|
207
|
+
|
|
208
|
+
def _button_width(self) -> float:
|
|
209
|
+
avail_w = imgui.get_content_region_avail().x
|
|
210
|
+
return min(avail_w - hello_imgui.em_size(2), hello_imgui.em_size(16))
|
|
211
|
+
|
|
212
|
+
def _draw_header(self) -> None:
|
|
213
|
+
if self.config.header_draw is not None:
|
|
214
|
+
call_draw(self.config.header_draw, self)
|
|
215
|
+
return
|
|
216
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.3))
|
|
217
|
+
center_text(self.config.title, self.theme.accent)
|
|
218
|
+
if self.config.subtitle:
|
|
219
|
+
center_text(self.config.subtitle, self.theme.text_dim)
|
|
220
|
+
|
|
221
|
+
def _draw_buttons(self) -> None:
|
|
222
|
+
btn_w = self._button_width()
|
|
223
|
+
btn_h = hello_imgui.em_size(1.8)
|
|
224
|
+
for i, btn in enumerate(self.config.buttons):
|
|
225
|
+
center_next_item(btn_w)
|
|
226
|
+
imgui.push_id(i)
|
|
227
|
+
if icon_button(
|
|
228
|
+
btn.icon,
|
|
229
|
+
btn.label,
|
|
230
|
+
imgui.ImVec2(btn_w, btn_h),
|
|
231
|
+
theme=self.theme,
|
|
232
|
+
tooltip=btn.tooltip,
|
|
233
|
+
):
|
|
234
|
+
self._launch(btn)
|
|
235
|
+
imgui.pop_id()
|
|
236
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.2))
|
|
237
|
+
|
|
238
|
+
def _draw_info_card(self) -> None:
|
|
239
|
+
callbacks = self.config.info_callbacks()
|
|
240
|
+
if not callbacks:
|
|
241
|
+
return
|
|
242
|
+
theme = self.theme
|
|
243
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.2))
|
|
244
|
+
avail_w = imgui.get_content_region_avail().x
|
|
245
|
+
card_w = avail_w - hello_imgui.em_size(1)
|
|
246
|
+
center_next_item(card_w)
|
|
247
|
+
|
|
248
|
+
imgui.push_style_color(imgui.Col_.child_bg, to_vec4(theme.bg_card))
|
|
249
|
+
imgui.push_style_var(imgui.StyleVar_.child_rounding, theme.child_rounding)
|
|
250
|
+
imgui.push_style_var(imgui.StyleVar_.cell_padding, hello_imgui.em_to_vec2(0.4, 0.2))
|
|
251
|
+
|
|
252
|
+
child_flags = imgui.ChildFlags_.borders | imgui.ChildFlags_.auto_resize_y
|
|
253
|
+
window_flags = imgui.WindowFlags_.no_scrollbar
|
|
254
|
+
with imgui_ctx.begin_child(
|
|
255
|
+
"##idl_info",
|
|
256
|
+
size=imgui.ImVec2(card_w, 0),
|
|
257
|
+
child_flags=child_flags,
|
|
258
|
+
window_flags=window_flags,
|
|
259
|
+
):
|
|
260
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.2))
|
|
261
|
+
imgui.indent(hello_imgui.em_size(0.6))
|
|
262
|
+
for j, cb in enumerate(callbacks):
|
|
263
|
+
if j:
|
|
264
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.2))
|
|
265
|
+
imgui.push_id(1000 + j)
|
|
266
|
+
call_draw(cb, self)
|
|
267
|
+
imgui.pop_id()
|
|
268
|
+
imgui.unindent(hello_imgui.em_size(0.6))
|
|
269
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.3))
|
|
270
|
+
|
|
271
|
+
imgui.pop_style_var(2)
|
|
272
|
+
imgui.pop_style_color()
|
|
273
|
+
|
|
274
|
+
def _draw_options_popup(self) -> None:
|
|
275
|
+
if self.config.options_draw is None:
|
|
276
|
+
return
|
|
277
|
+
if self._open_options:
|
|
278
|
+
imgui.open_popup("##idl_options")
|
|
279
|
+
self._open_options = False
|
|
280
|
+
if not imgui.begin_popup("##idl_options", imgui.WindowFlags_.always_auto_resize):
|
|
281
|
+
return
|
|
282
|
+
try:
|
|
283
|
+
imgui.text_colored(
|
|
284
|
+
to_vec4(self.theme.accent),
|
|
285
|
+
f"{fa.ICON_FA_GEARS} {self.config.options_label}",
|
|
286
|
+
)
|
|
287
|
+
imgui.separator()
|
|
288
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.2))
|
|
289
|
+
call_draw(self.config.options_draw, self)
|
|
290
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.3))
|
|
291
|
+
if imgui.button("Close", imgui.ImVec2(hello_imgui.em_size(6), 0)):
|
|
292
|
+
imgui.close_current_popup()
|
|
293
|
+
finally:
|
|
294
|
+
imgui.end_popup()
|
|
295
|
+
|
|
296
|
+
def _draw_footer(self) -> None:
|
|
297
|
+
cfg = self.config
|
|
298
|
+
if cfg.footer_draw is not None:
|
|
299
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.3))
|
|
300
|
+
call_draw(cfg.footer_draw, self)
|
|
301
|
+
self._handle_escape()
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
show_options = cfg.options_draw is not None and cfg.show_options_button
|
|
305
|
+
show_quit = cfg.show_quit_button
|
|
306
|
+
if not (show_options or show_quit):
|
|
307
|
+
self._handle_escape()
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
imgui.dummy(hello_imgui.em_to_vec2(0, 0.3))
|
|
311
|
+
theme = self.theme
|
|
312
|
+
style = imgui.get_style()
|
|
313
|
+
# size each button to its (icon + label) text so custom labels don't clip
|
|
314
|
+
opt_text = f"{fa.ICON_FA_GEARS} {cfg.options_label}"
|
|
315
|
+
quit_text = f"{fa.ICON_FA_XMARK} {cfg.quit_label}"
|
|
316
|
+
pad = style.frame_padding.x * 2 + hello_imgui.em_size(0.6)
|
|
317
|
+
min_w = hello_imgui.em_size(4.5)
|
|
318
|
+
opt_w = max(imgui.calc_text_size(opt_text).x + pad, min_w)
|
|
319
|
+
quit_w = max(imgui.calc_text_size(quit_text).x + pad, min_w)
|
|
320
|
+
spacing = style.item_spacing.x
|
|
321
|
+
row_w = 0.0
|
|
322
|
+
if show_options:
|
|
323
|
+
row_w += opt_w
|
|
324
|
+
if show_quit:
|
|
325
|
+
row_w += quit_w
|
|
326
|
+
if show_options and show_quit:
|
|
327
|
+
row_w += spacing
|
|
328
|
+
center_next_item(row_w)
|
|
329
|
+
|
|
330
|
+
btn_h = hello_imgui.em_size(1.5)
|
|
331
|
+
push_button_style(theme, primary=False)
|
|
332
|
+
if show_options:
|
|
333
|
+
if imgui.button(opt_text, imgui.ImVec2(opt_w, btn_h)):
|
|
334
|
+
self._open_options = True
|
|
335
|
+
if imgui.is_item_hovered():
|
|
336
|
+
wrapped_tooltip(cfg.options_label)
|
|
337
|
+
if show_quit:
|
|
338
|
+
imgui.same_line()
|
|
339
|
+
if show_quit:
|
|
340
|
+
if imgui.button(quit_text, imgui.ImVec2(quit_w, btn_h)):
|
|
341
|
+
self.cancel()
|
|
342
|
+
pop_button_style()
|
|
343
|
+
|
|
344
|
+
self._handle_escape()
|
|
345
|
+
|
|
346
|
+
def _handle_escape(self) -> None:
|
|
347
|
+
if self.config.quit_on_escape and imgui.is_key_pressed(imgui.Key.escape):
|
|
348
|
+
self.cancel()
|