ovui-widgets-content 0.2.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.
Files changed (47) hide show
  1. ovui_widgets/content/__init__.py +51 -0
  2. ovui_widgets/content/backends/__init__.py +33 -0
  3. ovui_widgets/content/backends/backend_adapter.py +31 -0
  4. ovui_widgets/content/backends/local_fs_backend.py +43 -0
  5. ovui_widgets/content/bookmarks.py +24 -0
  6. ovui_widgets/content/file_exporter.py +383 -0
  7. ovui_widgets/content/file_importer.py +319 -0
  8. ovui_widgets/content/file_picker_dialog.py +778 -0
  9. ovui_widgets/content/style.py +677 -0
  10. ovui_widgets/content/widget/__init__.py +108 -0
  11. ovui_widgets/content/widget/bookmark_button.py +523 -0
  12. ovui_widgets/content/widget/browser_bar.py +507 -0
  13. ovui_widgets/content/widget/clipboard.py +102 -0
  14. ovui_widgets/content/widget/collections/__init__.py +44 -0
  15. ovui_widgets/content/widget/collections/base.py +142 -0
  16. ovui_widgets/content/widget/collections/bookmarks.py +189 -0
  17. ovui_widgets/content/widget/collections/disk_partitions.py +153 -0
  18. ovui_widgets/content/widget/collections/my_computer.py +214 -0
  19. ovui_widgets/content/widget/collections/recent.py +253 -0
  20. ovui_widgets/content/widget/column_delegate.py +350 -0
  21. ovui_widgets/content/widget/confirm_delete_dialog.py +393 -0
  22. ovui_widgets/content/widget/confirm_overwrite_dialog.py +502 -0
  23. ovui_widgets/content/widget/context_menu.py +1696 -0
  24. ovui_widgets/content/widget/drop_indicator.py +283 -0
  25. ovui_widgets/content/widget/file_bar.py +764 -0
  26. ovui_widgets/content/widget/file_browser_delegate.py +823 -0
  27. ovui_widgets/content/widget/file_browser_model.py +1410 -0
  28. ovui_widgets/content/widget/file_browser_widget.py +2802 -0
  29. ovui_widgets/content/widget/file_card.py +864 -0
  30. ovui_widgets/content/widget/file_grid_view.py +643 -0
  31. ovui_widgets/content/widget/file_item.py +467 -0
  32. ovui_widgets/content/widget/file_ops.py +261 -0
  33. ovui_widgets/content/widget/filter_button.py +359 -0
  34. ovui_widgets/content/widget/highlight_label.py +291 -0
  35. ovui_widgets/content/widget/navigation_model.py +549 -0
  36. ovui_widgets/content/widget/options_menu.py +425 -0
  37. ovui_widgets/content/widget/path_field.py +1126 -0
  38. ovui_widgets/content/widget/rename_controller.py +362 -0
  39. ovui_widgets/content/widget/search_field.py +598 -0
  40. ovui_widgets/content/widget/simple_input_dialog.py +397 -0
  41. ovui_widgets/content/widget/zoom_bar.py +470 -0
  42. ovui_widgets/content/window/__init__.py +23 -0
  43. ovui_widgets/content/window/content_browser_window.py +558 -0
  44. ovui_widgets_content-0.2.0.dist-info/METADATA +73 -0
  45. ovui_widgets_content-0.2.0.dist-info/RECORD +47 -0
  46. ovui_widgets_content-0.2.0.dist-info/WHEEL +5 -0
  47. ovui_widgets_content-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,51 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: LicenseRef-NvidiaProprietary
3
+ #
4
+ # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
5
+ # property and proprietary rights in and to this material, related
6
+ # documentation and any modifications thereto. Any use, reproduction,
7
+ # disclosure or distribution of this material and related documentation
8
+ # without an express license agreement from NVIDIA CORPORATION or
9
+ # its affiliates is strictly prohibited.
10
+
11
+ """ovui_widgets.content: URL-based file/asset browser peer widget package.
12
+
13
+ See the content browser behavior and the content browser implementation for the
14
+ step-by-step build-out. The package is organised after the Kit
15
+ ``omni.kit.widget.filebrowser`` split:
16
+
17
+ - ``widget/`` — embeddable tree/grid widget plus its item/model/delegate.
18
+ - ``window/`` — dockable shell that wraps the widget.
19
+ - ``bookmarks.py`` — :class:`BookmarksManager` (Step 44) for the
20
+ persisted ``name → url`` favourites surfaced by the nav pane's
21
+ ``BookmarksCollection``.
22
+ """
23
+
24
+ from importlib import import_module
25
+
26
+ __all__ = [
27
+ "BookmarksManager",
28
+ "ContentBrowserWindow",
29
+ "FileExporterHelper",
30
+ "FileImporterHelper",
31
+ "FilePickerDialog",
32
+ ]
33
+
34
+ _LAZY_EXPORTS = {
35
+ "BookmarksManager": ("ovui_widgets.content.bookmarks", "BookmarksManager"),
36
+ "ContentBrowserWindow": ("ovui_widgets.content.window", "ContentBrowserWindow"),
37
+ "FileExporterHelper": ("ovui_widgets.content.file_exporter", "FileExporterHelper"),
38
+ "FileImporterHelper": ("ovui_widgets.content.file_importer", "FileImporterHelper"),
39
+ "FilePickerDialog": ("ovui_widgets.content.file_picker_dialog", "FilePickerDialog"),
40
+ }
41
+
42
+
43
+ def __getattr__(name: str):
44
+ """Load UI-heavy content exports only when callers request them."""
45
+ try:
46
+ module_name, attr_name = _LAZY_EXPORTS[name]
47
+ except KeyError as exc:
48
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc
49
+ value = getattr(import_module(module_name), attr_name)
50
+ globals()[name] = value
51
+ return value
@@ -0,0 +1,33 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: LicenseRef-NvidiaProprietary
3
+ #
4
+ # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
5
+ # property and proprietary rights in and to this material, related
6
+ # documentation and any modifications thereto. Any use, reproduction,
7
+ # disclosure or distribution of this material and related documentation
8
+ # without an express license agreement from NVIDIA CORPORATION or
9
+ # its affiliates is strictly prohibited.
10
+
11
+ """Compatibility exports for content backend services.
12
+
13
+ Canonical backend contracts and local filesystem behavior live in
14
+ ``ovui_data_adapters.services.content.backends``.
15
+ """
16
+
17
+ from ovui_data_adapters.services.content.backends import (
18
+ BackendAdapter,
19
+ BackendChangeEvent,
20
+ BackendFileFlags,
21
+ BackendListEntry,
22
+ BackendResult,
23
+ LocalFSBackend,
24
+ )
25
+
26
+ __all__ = [
27
+ "BackendAdapter",
28
+ "BackendChangeEvent",
29
+ "BackendFileFlags",
30
+ "BackendListEntry",
31
+ "BackendResult",
32
+ "LocalFSBackend",
33
+ ]
@@ -0,0 +1,31 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: LicenseRef-NvidiaProprietary
3
+ #
4
+ # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
5
+ # property and proprietary rights in and to this material, related
6
+ # documentation and any modifications thereto. Any use, reproduction,
7
+ # disclosure or distribution of this material and related documentation
8
+ # without an express license agreement from NVIDIA CORPORATION or
9
+ # its affiliates is strictly prohibited.
10
+
11
+ """Compatibility exports for the content backend service.
12
+
13
+ The canonical implementation lives in
14
+ ``ovui_data_adapters.services.content.backends.backend_adapter``.
15
+ """
16
+
17
+ from ovui_data_adapters.services.content.backends.backend_adapter import (
18
+ BackendAdapter,
19
+ BackendChangeEvent,
20
+ BackendFileFlags,
21
+ BackendListEntry,
22
+ BackendResult,
23
+ )
24
+
25
+ __all__ = [
26
+ "BackendAdapter",
27
+ "BackendChangeEvent",
28
+ "BackendFileFlags",
29
+ "BackendListEntry",
30
+ "BackendResult",
31
+ ]
@@ -0,0 +1,43 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: LicenseRef-NvidiaProprietary
3
+ #
4
+ # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
5
+ # property and proprietary rights in and to this material, related
6
+ # documentation and any modifications thereto. Any use, reproduction,
7
+ # disclosure or distribution of this material and related documentation
8
+ # without an express license agreement from NVIDIA CORPORATION or
9
+ # its affiliates is strictly prohibited.
10
+
11
+ """Compatibility exports for the local filesystem content backend service.
12
+
13
+ The canonical implementation lives in
14
+ ``ovui_data_adapters.services.content.backends.local_fs_backend``.
15
+ """
16
+
17
+ from ovui_data_adapters.services.content.backends.local_fs_backend import (
18
+ BackendAdapter,
19
+ BackendFileFlags,
20
+ BackendListEntry,
21
+ BackendResult,
22
+ LocalFSBackend,
23
+ _canonicalise,
24
+ _fspath_to_url,
25
+ _SCHEME,
26
+ _stat_to_flags,
27
+ _url_to_fspath,
28
+ _WIN_HIDDEN_ATTR,
29
+ )
30
+
31
+ __all__ = [
32
+ "BackendAdapter",
33
+ "BackendFileFlags",
34
+ "BackendListEntry",
35
+ "BackendResult",
36
+ "LocalFSBackend",
37
+ "_SCHEME",
38
+ "_WIN_HIDDEN_ATTR",
39
+ "_canonicalise",
40
+ "_fspath_to_url",
41
+ "_stat_to_flags",
42
+ "_url_to_fspath",
43
+ ]
@@ -0,0 +1,24 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: LicenseRef-NvidiaProprietary
3
+ #
4
+ # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
5
+ # property and proprietary rights in and to this material, related
6
+ # documentation and any modifications thereto. Any use, reproduction,
7
+ # disclosure or distribution of this material and related documentation
8
+ # without an express license agreement from NVIDIA CORPORATION or
9
+ # its affiliates is strictly prohibited.
10
+
11
+ """Compatibility path for content bookmark persistence behavior.
12
+
13
+ The reusable manager now lives in
14
+ ``ovui_data_adapters.services.content.navigation``. UI collection models,
15
+ context menus, buttons, and visual rows stay under ``ovui_widgets.content``.
16
+ """
17
+
18
+ from ovui_data_adapters.services.content.navigation import (
19
+ BOOKMARKS_SETTINGS_KEY,
20
+ SETTINGS_KEY,
21
+ BookmarksManager,
22
+ )
23
+
24
+ __all__ = ["BOOKMARKS_SETTINGS_KEY", "SETTINGS_KEY", "BookmarksManager"]
@@ -0,0 +1,383 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: LicenseRef-NvidiaProprietary
3
+ #
4
+ # NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
5
+ # property and proprietary rights in and to this material, related
6
+ # documentation and any modifications thereto. Any use, reproduction,
7
+ # disclosure or distribution of this material and related documentation
8
+ # without an express license agreement from NVIDIA CORPORATION or
9
+ # its affiliates is strictly prohibited.
10
+
11
+ """FileExporterHelper — typed ``Save File As`` dialog wrapper.
12
+
13
+ See the content browser behavior and the content browser implementation step 55.
14
+ Sibling to :class:`FileImporterHelper` (Step 53): a thin wrapper over
15
+ :class:`FilePickerDialog` that collapses the dialog's two-arg
16
+ ``(filename, dirname)`` Apply callback into the Kit-standard four-arg
17
+ ``export_handler(filename, dirname, extension, selections)`` contract.
18
+ The wrapper's job is to:
19
+
20
+ 1. Pick a sensible starting directory for the dialog:
21
+ the ``filename_url`` argument's parent if given, else the Settings-
22
+ persisted ``ui.content.last_save_dir`` key, else the user's home.
23
+ 2. Construct (or reuse) a :class:`FilePickerDialog` configured for save
24
+ semantics: ``should_validate=True``, ``validation_mode="save"``,
25
+ ``apply_button_label=export_button_label``.
26
+ 3. On Apply — resolve the extension from the bar's selected combo
27
+ entry, fire the caller's ``export_handler`` with the typed four-arg
28
+ payload, persist the dirname into ``ui.content.last_save_dir``, and
29
+ hide the dialog.
30
+ 4. On Cancel / ESC — hide the dialog and no-op.
31
+
32
+ A singleton is exposed through :meth:`instance` so the one-line use
33
+ site in :mod:`ovui_widgets.app.menu_bar` (File > Save As) does not allocate a
34
+ fresh helper every time the menu is triggered. Reset via
35
+ :meth:`reset_singleton` in test fixtures.
36
+
37
+ **Extension contract** (architecture §22.4). The fourth arg to the
38
+ caller's ``export_handler`` is the ``.ext`` string derived from the
39
+ combo's currently-selected glob — e.g. ``"*.usd"`` → ``".usd"``. Kit's
40
+ exporter composes ``.postfix.ext``; ovgear v1 has no postfix surface,
41
+ so :meth:`_resolve_extension` strips the leading ``*`` from the first
42
+ pattern in the selected glob and returns that. The caller joins
43
+ ``filename + extension`` before writing to disk (matches the Step-55
44
+ menu handler's ``on_export`` closure).
45
+
46
+ **Overwrite confirmation** lives outside this helper — architecture
47
+ §22.5 point 4 and Kit's ``_show_file_existed_prompt`` (§23.9) both put
48
+ overwrite-check / :class:`ConfirmOverwriteDialog` dispatch in the
49
+ caller's ``on_export`` path, not the helper. The helper surfaces the
50
+ typed payload; the caller checks ``os.path.exists`` and branches.
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ import os
56
+ from typing import Callable, List, Optional, Tuple
57
+
58
+ from ovui_widgets.content.backends.backend_adapter import BackendAdapter
59
+ from ovui_widgets.content.backends.local_fs_backend import LocalFSBackend
60
+ from ovui_widgets.content.file_picker_dialog import FilePickerDialog
61
+
62
+ # Persistent Settings key for the last-used "Save" directory. Sibling to
63
+ # :data:`LAST_OPEN_DIR_SETTING` — Kit's architecture §36.3
64
+ # ``/persistent/app/file_exporter/directory`` collapses under ovgear's
65
+ # flat ``ui.*`` namespace to ``ui.content.last_save_dir``.
66
+ LAST_SAVE_DIR_SETTING = "ui.content.last_save_dir"
67
+
68
+ # Default file-extension combo entries for the exporter. Architecture
69
+ # §22.4 specifies this exact tuple (USD Binary/Ascii, USD Ascii, USD
70
+ # Crate) — tighter than the importer's list because the exporter only
71
+ # writes to formats the USD stage knows how to emit. ``.usdz`` is
72
+ # intentionally omitted: it is a package format Kit's exporter also
73
+ # drops.
74
+ DEFAULT_FILE_EXTENSION_TYPES: List[Tuple[str, str]] = [
75
+ ("*.usd", "USD Binary or Ascii"),
76
+ ("*.usda", "USD Ascii"),
77
+ ("*.usdc", "USD Crate"),
78
+ ]
79
+
80
+
81
+ class FileExporterHelper:
82
+ """Typed wrapper around :class:`FilePickerDialog` for Save As dialogs.
83
+
84
+ The helper owns at most one live dialog at a time; calling
85
+ :meth:`show` with an already-live dialog tears it down and rebuilds
86
+ so each :meth:`show` lands with the caller's exact configuration.
87
+ The singleton returned by :meth:`instance` is the entry point used
88
+ by :func:`ovui_widgets.app.menu_bar._on_save_as_clicked`; tests construct
89
+ bare instances with injected ``backend`` / ``settings`` dependencies.
90
+
91
+ ``export_handler`` signature::
92
+
93
+ export_handler(
94
+ filename: str,
95
+ dirname: str,
96
+ extension: str,
97
+ selections: List[str],
98
+ )
99
+
100
+ ``filename`` is the basename typed in the :class:`FileBar` (no
101
+ extension), ``dirname`` is the detail pane's current root URL,
102
+ ``extension`` is the combo's currently-selected glob stripped to
103
+ ``.ext`` (e.g. ``.usd``), and ``selections`` is the list of full
104
+ URLs from the detail pane's current selection (typically empty for
105
+ a Save As — the user is typing a new name, not picking an existing
106
+ file to overwrite).
107
+ """
108
+
109
+ _singleton: Optional["FileExporterHelper"] = None
110
+
111
+ # ── Construction / singleton ─────────────────────────────────────────
112
+
113
+ def __init__(
114
+ self,
115
+ backend: Optional[BackendAdapter] = None,
116
+ settings: Optional[object] = None,
117
+ ) -> None:
118
+ """Construct a helper with optional ``backend`` / ``settings`` overrides.
119
+
120
+ ``backend`` defaults to a fresh :class:`LocalFSBackend` — the
121
+ helper is OS-file-system-only at Step 55. ``settings`` defaults
122
+ to ``None``; the helper then resolves the live
123
+ :class:`ovui_widgets.common.settings.Settings` lazily through
124
+ :meth:`Application.instance`. Tests bypass the application
125
+ singleton by passing a pre-built :class:`Settings` instance.
126
+ """
127
+ self._backend: BackendAdapter = (
128
+ backend if backend is not None else LocalFSBackend()
129
+ )
130
+ self._settings_override: Optional[object] = settings
131
+ self._dialog: Optional[FilePickerDialog] = None
132
+
133
+ @classmethod
134
+ def instance(cls) -> "FileExporterHelper":
135
+ """Return the process-wide singleton, constructing on first call."""
136
+ if cls._singleton is None:
137
+ cls._singleton = cls()
138
+ return cls._singleton
139
+
140
+ @classmethod
141
+ def reset_singleton(cls) -> None:
142
+ """Tear down and clear the cached singleton.
143
+
144
+ Test fixtures call this between tests so a leftover helper from
145
+ one test cannot leak its live dialog into the next. Destroys any
146
+ live dialog via :meth:`destroy` before releasing the reference.
147
+ """
148
+ if cls._singleton is not None:
149
+ try:
150
+ cls._singleton.destroy()
151
+ except Exception: # noqa: BLE001
152
+ pass
153
+ cls._singleton = None
154
+
155
+ # ── Public surface ───────────────────────────────────────────────────
156
+
157
+ def show(
158
+ self,
159
+ title: str = "Save File As",
160
+ export_button_label: str = "Save",
161
+ file_extension_types: Optional[List[Tuple[str, str]]] = None,
162
+ export_handler: Optional[
163
+ Callable[[str, str, str, List[str]], None]
164
+ ] = None,
165
+ filename_url: Optional[str] = None,
166
+ should_validate: bool = True,
167
+ ) -> None:
168
+ """Materialise a configured :class:`FilePickerDialog` and reveal it.
169
+
170
+ If a dialog from a previous :meth:`show` is still live it is
171
+ destroyed first — each call lands with the caller's exact
172
+ configuration rather than reusing a stale one. The dialog's
173
+ ``on_apply`` wraps ``export_handler`` with the four-arg
174
+ contract, persists the Apply-time dirname to Settings, and
175
+ hides the dialog. The dialog's ``on_cancel`` is a bare ``None``;
176
+ the dialog's built-in hide-on-cancel path handles dismissal.
177
+
178
+ ``should_validate`` defaults to ``True`` (matches Step 53's
179
+ importer default; save-mode validation rejects an empty
180
+ filename — architecture §22.5 point 4).
181
+ """
182
+ if self._dialog is not None:
183
+ try:
184
+ self._dialog.destroy()
185
+ except Exception: # noqa: BLE001
186
+ pass
187
+ self._dialog = None
188
+
189
+ start_url = self._resolve_start_url(filename_url)
190
+ initial_filename = self._resolve_initial_filename(filename_url)
191
+ extensions = (
192
+ file_extension_types
193
+ if file_extension_types is not None
194
+ else list(DEFAULT_FILE_EXTENSION_TYPES)
195
+ )
196
+
197
+ def _on_apply_wrapper(filename: str, dirname: str) -> None:
198
+ """Run the typed Apply payload, persist the dir, hide the dialog."""
199
+ selections: List[str] = []
200
+ extension: str = ""
201
+ if self._dialog is not None:
202
+ try:
203
+ selections = self._dialog.get_selection()
204
+ except Exception: # noqa: BLE001
205
+ selections = []
206
+ extension = self._resolve_extension()
207
+ self._persist_last_save_dir(dirname)
208
+ if export_handler is not None:
209
+ try:
210
+ export_handler(filename, dirname, extension, selections)
211
+ except Exception: # noqa: BLE001
212
+ # Isolate the caller's handler so a raise does not
213
+ # leave the dialog stuck on screen. Mirrors Step 53's
214
+ # importer's BLE001 pattern; the reporter is the
215
+ # right sink for the diagnostic but a hard
216
+ # dependency on it from a wrapper that may run
217
+ # before :class:`Application` is up would complicate
218
+ # the init path.
219
+ pass
220
+ if self._dialog is not None:
221
+ self._dialog.hide()
222
+
223
+ self._dialog = FilePickerDialog(
224
+ title=title,
225
+ backend=self._backend,
226
+ start_url=start_url,
227
+ apply_button_label=export_button_label,
228
+ cancel_button_label="Cancel",
229
+ on_apply=_on_apply_wrapper,
230
+ on_cancel=None,
231
+ allow_multi_selection=False,
232
+ file_extension_types=extensions,
233
+ folder_only=False,
234
+ initial_filename=initial_filename,
235
+ should_validate=should_validate,
236
+ validation_mode="save",
237
+ )
238
+ self._dialog.show()
239
+
240
+ def destroy(self) -> None:
241
+ """Tear down the live dialog, if any.
242
+
243
+ Idempotent — safe to call when :meth:`show` was never invoked.
244
+ Leaves the helper reusable: a subsequent :meth:`show` builds a
245
+ fresh dialog from scratch.
246
+ """
247
+ if self._dialog is not None:
248
+ try:
249
+ self._dialog.destroy()
250
+ except Exception: # noqa: BLE001
251
+ pass
252
+ self._dialog = None
253
+
254
+ # ── Accessors ────────────────────────────────────────────────────────
255
+
256
+ @property
257
+ def dialog(self) -> Optional[FilePickerDialog]:
258
+ """The currently-live :class:`FilePickerDialog`, or ``None``."""
259
+ return self._dialog
260
+
261
+ @property
262
+ def backend(self) -> BackendAdapter:
263
+ """The backend used for start-URL normalisation + dialog ops."""
264
+ return self._backend
265
+
266
+ # ── Internals ────────────────────────────────────────────────────────
267
+
268
+ def _resolve_start_url(self, filename_url: Optional[str]) -> str:
269
+ """Pick the dialog's starting directory per the content browser implementation step 55.
270
+
271
+ Precedence: (1) ``filename_url``'s parent directory; (2) the
272
+ Settings-persisted ``ui.content.last_save_dir``; (3) the user's
273
+ home directory. Each step falls through on an empty / missing /
274
+ malformed value so the helper always returns a usable URL.
275
+ """
276
+ if filename_url:
277
+ parent = self._parent_url(filename_url)
278
+ if parent:
279
+ return parent
280
+ settings = self._get_settings()
281
+ if settings is not None:
282
+ saved = settings.get(LAST_SAVE_DIR_SETTING, None)
283
+ if saved:
284
+ return saved
285
+ return self._backend.normalize_url(
286
+ "file://" + os.path.expanduser("~"),
287
+ )
288
+
289
+ def _resolve_initial_filename(
290
+ self, filename_url: Optional[str],
291
+ ) -> str:
292
+ """Extract the basename from ``filename_url`` for pre-fill.
293
+
294
+ Returns an empty string when no URL is given. Matches Kit's
295
+ ``FileExporterExtension.show_window`` behaviour: a caller that
296
+ wants the dialog to reopen on a specific file (e.g. a "Save a
297
+ Copy" action) gets its basename pre-populated in the
298
+ :class:`FileBar` filename field.
299
+ """
300
+ if not filename_url:
301
+ return ""
302
+ try:
303
+ return self._backend.basename(filename_url) or ""
304
+ except Exception: # noqa: BLE001
305
+ return ""
306
+
307
+ def _parent_url(self, filename_url: str) -> Optional[str]:
308
+ """Return the parent URL of a file URL; ``None`` at backend root."""
309
+ try:
310
+ return self._backend.parent_url(filename_url)
311
+ except Exception: # noqa: BLE001
312
+ return None
313
+
314
+ def _resolve_extension(self) -> str:
315
+ """Return the current combo selection's ``.ext`` string.
316
+
317
+ Pulls :attr:`FileBar.selected_extension` from the dialog's bar
318
+ and maps the first pattern to a leading-dot extension — e.g.
319
+ ``("*.usd", "USD Binary or Ascii")`` → ``".usd"``. Kit's
320
+ exporter composes ``.postfix.ext`` (architecture §22.4); ovgear
321
+ v1 has no postfix surface so the extension alone is the whole
322
+ composition. Returns an empty string when the bar is missing /
323
+ the selected glob is malformed / the combo resolution fails —
324
+ the caller's ``on_export`` is expected to still receive a
325
+ reasonable filename in that degenerate case.
326
+ """
327
+ if self._dialog is None or self._dialog._file_bar is None:
328
+ return ""
329
+ try:
330
+ pattern, _desc = self._dialog._file_bar.selected_extension
331
+ except Exception: # noqa: BLE001
332
+ return ""
333
+ if not pattern:
334
+ return ""
335
+ # Split on comma so a multi-glob entry like ``"*.usd, *.usda"``
336
+ # resolves to ``.usd`` (the first pattern). ovgear's importer
337
+ # default uses multi-glob entries; the exporter default does not,
338
+ # but the splitter keeps both surfaces consistent.
339
+ first = pattern.split(",")[0].strip()
340
+ if first.startswith("*"):
341
+ first = first[1:]
342
+ return first
343
+
344
+ def _persist_last_save_dir(self, dirname: str) -> None:
345
+ """Write ``dirname`` to Settings — silent no-op if Settings unreachable."""
346
+ if not dirname:
347
+ return
348
+ settings = self._get_settings()
349
+ if settings is None:
350
+ return
351
+ try:
352
+ settings.set(LAST_SAVE_DIR_SETTING, dirname)
353
+ except Exception: # noqa: BLE001
354
+ pass
355
+
356
+ def _get_settings(self) -> Optional[object]:
357
+ """Resolve the live :class:`Settings`, honouring the test override.
358
+
359
+ The constructor ``settings`` kwarg wins; otherwise the helper
360
+ reaches through :meth:`Application.instance` — which raises
361
+ when no app is up, so wrap in a try/except and return ``None``
362
+ so the helper can still resolve a start URL from the home
363
+ fallback.
364
+ """
365
+ if self._settings_override is not None:
366
+ return self._settings_override
367
+ try:
368
+ from ovui_widgets.common.settings import Settings
369
+ return Settings.instance()
370
+ except Exception: # noqa: BLE001
371
+ return None
372
+
373
+
374
+ # ── Issue #35 Step 3: register the helper's reset_singleton classmethod
375
+ # so Application.shutdown() drops the live FilePickerDialog (and its
376
+ # ui.Window) before omni.ui.shutdown() runs. register_classmethod uses
377
+ # a stable string key `f"clsmethod:{module}.{qualname}.reset_singleton"`
378
+ # (Round 3 F3) — bound-method identity is unstable across re-imports,
379
+ # so we cannot pass FileExporterHelper.reset_singleton through register()
380
+ # directly.
381
+ from ovui_widgets.common.icon_caches import register_classmethod as _register_clsmethod_for_shutdown
382
+
383
+ _register_clsmethod_for_shutdown(FileExporterHelper, "reset_singleton")