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.
- ovui_widgets/content/__init__.py +51 -0
- ovui_widgets/content/backends/__init__.py +33 -0
- ovui_widgets/content/backends/backend_adapter.py +31 -0
- ovui_widgets/content/backends/local_fs_backend.py +43 -0
- ovui_widgets/content/bookmarks.py +24 -0
- ovui_widgets/content/file_exporter.py +383 -0
- ovui_widgets/content/file_importer.py +319 -0
- ovui_widgets/content/file_picker_dialog.py +778 -0
- ovui_widgets/content/style.py +677 -0
- ovui_widgets/content/widget/__init__.py +108 -0
- ovui_widgets/content/widget/bookmark_button.py +523 -0
- ovui_widgets/content/widget/browser_bar.py +507 -0
- ovui_widgets/content/widget/clipboard.py +102 -0
- ovui_widgets/content/widget/collections/__init__.py +44 -0
- ovui_widgets/content/widget/collections/base.py +142 -0
- ovui_widgets/content/widget/collections/bookmarks.py +189 -0
- ovui_widgets/content/widget/collections/disk_partitions.py +153 -0
- ovui_widgets/content/widget/collections/my_computer.py +214 -0
- ovui_widgets/content/widget/collections/recent.py +253 -0
- ovui_widgets/content/widget/column_delegate.py +350 -0
- ovui_widgets/content/widget/confirm_delete_dialog.py +393 -0
- ovui_widgets/content/widget/confirm_overwrite_dialog.py +502 -0
- ovui_widgets/content/widget/context_menu.py +1696 -0
- ovui_widgets/content/widget/drop_indicator.py +283 -0
- ovui_widgets/content/widget/file_bar.py +764 -0
- ovui_widgets/content/widget/file_browser_delegate.py +823 -0
- ovui_widgets/content/widget/file_browser_model.py +1410 -0
- ovui_widgets/content/widget/file_browser_widget.py +2802 -0
- ovui_widgets/content/widget/file_card.py +864 -0
- ovui_widgets/content/widget/file_grid_view.py +643 -0
- ovui_widgets/content/widget/file_item.py +467 -0
- ovui_widgets/content/widget/file_ops.py +261 -0
- ovui_widgets/content/widget/filter_button.py +359 -0
- ovui_widgets/content/widget/highlight_label.py +291 -0
- ovui_widgets/content/widget/navigation_model.py +549 -0
- ovui_widgets/content/widget/options_menu.py +425 -0
- ovui_widgets/content/widget/path_field.py +1126 -0
- ovui_widgets/content/widget/rename_controller.py +362 -0
- ovui_widgets/content/widget/search_field.py +598 -0
- ovui_widgets/content/widget/simple_input_dialog.py +397 -0
- ovui_widgets/content/widget/zoom_bar.py +470 -0
- ovui_widgets/content/window/__init__.py +23 -0
- ovui_widgets/content/window/content_browser_window.py +558 -0
- ovui_widgets_content-0.2.0.dist-info/METADATA +73 -0
- ovui_widgets_content-0.2.0.dist-info/RECORD +47 -0
- ovui_widgets_content-0.2.0.dist-info/WHEEL +5 -0
- 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")
|