meltygui 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.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
"""Collection view functions and supporting definitions."""
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from collections import deque
|
|
4
|
+
from collections.abc import MutableMapping
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from meltygui.core.conversion.bubbling import _BubblingDict
|
|
7
|
+
from meltygui.core.conversion.bubbling import _DeepPath
|
|
8
|
+
from meltygui.code.libcst_conversion import CallParse
|
|
9
|
+
from meltygui.code.libcst_conversion import ClassParse
|
|
10
|
+
from meltygui.code.libcst_conversion import EnumParse
|
|
11
|
+
from meltygui.code.libcst_conversion import FunctionParse
|
|
12
|
+
from meltygui.code.libcst_conversion import GeneralParse
|
|
13
|
+
from meltygui.hdr_color import pack_color
|
|
14
|
+
from meltygui.core.melty import CollectionAction
|
|
15
|
+
from meltygui.core.melty import Melty
|
|
16
|
+
from meltygui.core.melty import SearchTerm
|
|
17
|
+
from meltygui.core.rendering.modes import Modes
|
|
18
|
+
from meltygui.core.core_render import render_func
|
|
19
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
20
|
+
from meltygui.core.rendering.shaped import Shaped
|
|
21
|
+
from meltygui.state.new_core_model import ColorPickerState
|
|
22
|
+
from meltygui.state.new_core_model import TabState
|
|
23
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
24
|
+
from meltygui.view.header_view import draw_header
|
|
25
|
+
from types import NoneType
|
|
26
|
+
import meltygui_imgui as imgui
|
|
27
|
+
import types
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@render_func(use_cache=False, show_bg=False, indent_size=2, disable_scroll=True,
|
|
31
|
+
shadow=False, selectable=False, bg_offset=1)
|
|
32
|
+
def draw_collection_as_tabs(input_value, tab_state: TabState = None, draw_state=None, unique=0,
|
|
33
|
+
excluded=None, included=None, show_excluded=False, show_system=False,
|
|
34
|
+
folder_type=None):
|
|
35
|
+
"""Draws a dict as a tab bar: each inner collection gets its own tab (key = tab
|
|
36
|
+
name, contents via draw_any); all non-collection items are grouped into one
|
|
37
|
+
final "General" tab.
|
|
38
|
+
|
|
39
|
+
folder_type: a type or tuple of types that get their own tab, overriding
|
|
40
|
+
the default "any collection" rule — e.g. folder_type=(dict, GeneralParse)
|
|
41
|
+
puts dicts and GeneralParses in tabs while tuples/lists land in General.
|
|
42
|
+
|
|
43
|
+
Item filtering matches draw_collection: `excluded` names are hidden,
|
|
44
|
+
`included` names always show (overriding every hide rule), the type's
|
|
45
|
+
__excluded_attrs__ hide unless Toggles.show_excluded, and _underscored_
|
|
46
|
+
keys hide unless show_system. show_excluded=True disables all hiding."""
|
|
47
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
48
|
+
from meltygui.view.decoration_view import draw_bg
|
|
49
|
+
from meltygui.view.tab_view import draw_tab_bar
|
|
50
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
51
|
+
import meltygui.core.input.drag_drop_core as _drag_drop
|
|
52
|
+
|
|
53
|
+
if excluded is None:
|
|
54
|
+
excluded = set()
|
|
55
|
+
if included is None:
|
|
56
|
+
included = set()
|
|
57
|
+
excl_attrs = getattr(type(input_value), "__excluded_attrs__", None)
|
|
58
|
+
|
|
59
|
+
def _key_visible(key):
|
|
60
|
+
key_str = str(key).split("##")[0]
|
|
61
|
+
if key_str in included:
|
|
62
|
+
return True
|
|
63
|
+
if show_excluded:
|
|
64
|
+
return True
|
|
65
|
+
if (excl_attrs is not None and not Toggles.show_excluded
|
|
66
|
+
and key_str in excl_attrs):
|
|
67
|
+
return False
|
|
68
|
+
if key_str in excluded:
|
|
69
|
+
return False
|
|
70
|
+
if not show_system and (key_str.startswith("_") or key_str.endswith("_")):
|
|
71
|
+
return False
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
if folder_type is not None:
|
|
75
|
+
tab_types = folder_type
|
|
76
|
+
else:
|
|
77
|
+
tab_types = (dict, defaultdict, MutableMapping, types.MappingProxyType, list, tuple, set, deque)
|
|
78
|
+
visible = [(k, v) for k, v in input_value.items() if _key_visible(k)]
|
|
79
|
+
tab_keys = [k for k, v in visible if isinstance(v, tab_types)]
|
|
80
|
+
general = {k: v for k, v in visible if not isinstance(v, tab_types)}
|
|
81
|
+
|
|
82
|
+
tabs = [str(k) for k in tab_keys]
|
|
83
|
+
key_by_name = {str(k): k for k in tab_keys}
|
|
84
|
+
if general:
|
|
85
|
+
tabs.append("General")
|
|
86
|
+
if not tabs:
|
|
87
|
+
return False, input_value
|
|
88
|
+
|
|
89
|
+
tab_state.selected_tabs = [t for t in tab_state.selected_tabs if t in tabs]
|
|
90
|
+
if not tab_state.selected_tabs:
|
|
91
|
+
tab_state.selected_tabs = [tabs[0]]
|
|
92
|
+
|
|
93
|
+
# Tab tints come from each child's render kwargs (via return_extras below).
|
|
94
|
+
# The bar draws before the children, so tints lag by one frame; they're held
|
|
95
|
+
# on tab_state (serialized) so tabs retain their color across sessions.
|
|
96
|
+
if getattr(tab_state, "tab_tints", None) is None:
|
|
97
|
+
tab_state.tab_tints = {}
|
|
98
|
+
tints = [tab_state.tab_tints.get(t) for t in tabs]
|
|
99
|
+
# View icons ride the same one-frame-lag path as tints: each child's
|
|
100
|
+
# resolved `icon` kwarg (the rendered icon, e.g. from # [icon=...] comments)
|
|
101
|
+
# is held on tab_state and prefixed onto its tab label.
|
|
102
|
+
if getattr(tab_state, "tab_icons", None) is None:
|
|
103
|
+
tab_state.tab_icons = {}
|
|
104
|
+
icons = [tab_state.tab_icons.get(t) for t in tabs]
|
|
105
|
+
|
|
106
|
+
indent_size = 6
|
|
107
|
+
imgui.dummy(0, 5)
|
|
108
|
+
spacing = 8
|
|
109
|
+
|
|
110
|
+
# Framework DragDrop: this draw_state IS the drop collection (its
|
|
111
|
+
# input_value is the dict a Reorder/Insert applies to via
|
|
112
|
+
# Melty.dnd_requests + the undo tail - undo included). The tab buttons
|
|
113
|
+
# register as its whole-rect drag items; _dnd_horizontal makes the slot
|
|
114
|
+
# lines vertical gaps between tabs. dnd_keys maps each tab to its
|
|
115
|
+
# (dict key, dict index) - insert indices in the slots are then already
|
|
116
|
+
# dict-space, with non-collection keys being skipped. The synthetic
|
|
117
|
+
# General tab gets None: not draggable and contributes no slot.
|
|
118
|
+
draw_state._dnd_drop_target = True
|
|
119
|
+
draw_state._dnd_horizontal = True
|
|
120
|
+
dict_keys = list(input_value.keys())
|
|
121
|
+
dnd_keys = []
|
|
122
|
+
for t in tabs:
|
|
123
|
+
k = key_by_name.get(t) if not (t == "General" and t not in key_by_name) else None
|
|
124
|
+
dnd_keys.append((k, dict_keys.index(k)) if k is not None else None)
|
|
125
|
+
|
|
126
|
+
tab_changed, new_tabs, bar_ds = draw_tab_bar(input_value=tab_state.selected_tabs,
|
|
127
|
+
tab_height=30, show_bg=False, bg_offset=1,
|
|
128
|
+
name=f"tab_bar{unique}", wrap=True,
|
|
129
|
+
collection=tabs, tints=tints, icons=icons, as_toggles=False,
|
|
130
|
+
dnd_collection_ds=draw_state, dnd_keys=dnd_keys,
|
|
131
|
+
return_extras=True)
|
|
132
|
+
if tab_changed:
|
|
133
|
+
tab_state.selected_tabs = new_tabs
|
|
134
|
+
|
|
135
|
+
# The bar's view is cached, but the dragged button must be baked through
|
|
136
|
+
# the bar's body at the gesture edges (pickup: pick up the floating window
|
|
137
|
+
# kwargs + bake the placeholder; drop: restore the inline button). Between
|
|
138
|
+
# the edges DragDrop._keep_alive re-registers the floating window on its
|
|
139
|
+
# layer, so no per-frame invalidation is needed.
|
|
140
|
+
dnd_active_here = _drag_drop.DragDrop.active and _drag_drop.DragDrop.source_ds is draw_state
|
|
141
|
+
if dnd_active_here != getattr(draw_state, "_tab_dnd_was_active", False):
|
|
142
|
+
draw_state._tab_dnd_was_active = dnd_active_here
|
|
143
|
+
if bar_ds is not None:
|
|
144
|
+
request_render()
|
|
145
|
+
|
|
146
|
+
imgui.dummy(0, 2)
|
|
147
|
+
changed = False
|
|
148
|
+
# Rendered content views in visual order, as (dict_index, draw_state) -
|
|
149
|
+
# for the between-content drop slots published below.
|
|
150
|
+
content_stack = []
|
|
151
|
+
# Selected tabs stack vertically in tab-bar order (no columns).
|
|
152
|
+
for tab in tabs:
|
|
153
|
+
if tab not in tab_state.selected_tabs:
|
|
154
|
+
continue
|
|
155
|
+
content_dragged = False
|
|
156
|
+
if tab == "General" and tab not in key_by_name:
|
|
157
|
+
general_changed, new_general, child_ds = draw_any(general, name=f"Tab: General {unique}",
|
|
158
|
+
disable_scroll=False, indent_size=indent_size,
|
|
159
|
+
use_cache=True, return_extras=True)
|
|
160
|
+
if general_changed:
|
|
161
|
+
for k, v in new_general.items():
|
|
162
|
+
input_value[k] = v
|
|
163
|
+
changed |= general_changed
|
|
164
|
+
else:
|
|
165
|
+
key = key_by_name[tab]
|
|
166
|
+
# With several tabs open, each content view is ALSO a drag item of
|
|
167
|
+
# this dict (key= and _collection_draw_state to make its header a
|
|
168
|
+
# pickup handle from DragDrop.register_item) - dragging a stacked
|
|
169
|
+
# collection reorders the tabs just like dragging a tab button.
|
|
170
|
+
# The item_ds identity check disambiguates the two views sharing
|
|
171
|
+
# (collection, key): only the one actually picked up floats.
|
|
172
|
+
multi = len(tab_state.selected_tabs) > 1
|
|
173
|
+
content_extra = {}
|
|
174
|
+
content_dragged = False
|
|
175
|
+
if multi:
|
|
176
|
+
content_extra["key"] = key
|
|
177
|
+
prev_ds = (getattr(draw_state, "_tab_child_ds", None) or {}).get(tab)
|
|
178
|
+
if (prev_ds is not None and _drag_drop.DragDrop.item_ds is prev_ds
|
|
179
|
+
and _drag_drop.DragDrop.is_dragged_child(draw_state, key)):
|
|
180
|
+
content_dragged = True
|
|
181
|
+
content_extra.update(_drag_drop.DragDrop.dragged_item_kwargs())
|
|
182
|
+
|
|
183
|
+
content_extra["use_cache"] = True
|
|
184
|
+
tab_content_changed, value, child_ds = draw_any(input_value[key], name=f"{tab}",
|
|
185
|
+
disable_scroll=False, indent_size=indent_size,
|
|
186
|
+
return_extras=True,
|
|
187
|
+
**content_extra)
|
|
188
|
+
if child_ds is not None:
|
|
189
|
+
# Membership follows multi-select: cleared when only one tab
|
|
190
|
+
# is open so the lone content view stops being a pickup handle.
|
|
191
|
+
child_ds._collection_draw_state = draw_state if multi else None
|
|
192
|
+
if not content_dragged:
|
|
193
|
+
content_stack.append((dict_keys.index(key), child_ds))
|
|
194
|
+
if content_dragged:
|
|
195
|
+
# The dragged content deferred to a floating window - hold its
|
|
196
|
+
# vertical slot open at its pickup rect (placeholder draws its
|
|
197
|
+
# own item spacing).
|
|
198
|
+
_drag_drop.DragDrop.draw_placeholder(False, spacing,
|
|
199
|
+
style_manager=Core.melty.style_manager,
|
|
200
|
+
draw_bg=draw_bg)
|
|
201
|
+
if tab_content_changed:
|
|
202
|
+
input_value[key] = value
|
|
203
|
+
changed |= tab_content_changed
|
|
204
|
+
|
|
205
|
+
if not content_dragged:
|
|
206
|
+
# (the placeholder path already added its own item spacing)
|
|
207
|
+
imgui.dummy(0, spacing)
|
|
208
|
+
|
|
209
|
+
if child_ds is not None:
|
|
210
|
+
# Track each tab's content draw_state so deselected tabs can be
|
|
211
|
+
# marked hidden below (they stop rendering but keep stale
|
|
212
|
+
# geometry, which leaked DragDrop drop lines).
|
|
213
|
+
if getattr(draw_state, "_tab_child_ds", None) is None:
|
|
214
|
+
draw_state._tab_child_ds = {}
|
|
215
|
+
draw_state._tab_child_ds[tab] = child_ds
|
|
216
|
+
child_ds._hidden_offscreen = False
|
|
217
|
+
|
|
218
|
+
child_tint = child_ds._kwargs.get("tint", None) if child_ds is not None else None
|
|
219
|
+
if child_tint is not None and tab_state.tab_tints.get(tab) != child_tint:
|
|
220
|
+
tab_state.tab_tints[tab] = child_tint
|
|
221
|
+
draw_state.invalidate()
|
|
222
|
+
|
|
223
|
+
child_icon = child_ds._kwargs.get("icon", None) if child_ds is not None else None
|
|
224
|
+
if child_icon is not None and tab_state.tab_icons.get(tab) != child_icon:
|
|
225
|
+
tab_state.tab_icons[tab] = child_icon
|
|
226
|
+
draw_state.invalidate()
|
|
227
|
+
|
|
228
|
+
# Deselected tabs' content: not collapsed, not closed, but no longer
|
|
229
|
+
# rendered - so nothing downstream knows it's invisible. Stamp
|
|
230
|
+
# _hidden_offscreen (the same flag end_frame uses for spawner-scrolled
|
|
231
|
+
# nested tiles) so DragDrop's slot sweep skips their whole subtrees.
|
|
232
|
+
hidden_reg = getattr(draw_state, "_tab_child_ds", None)
|
|
233
|
+
if hidden_reg:
|
|
234
|
+
for t, t_ds in hidden_reg.items():
|
|
235
|
+
if t not in tab_state.selected_tabs or t not in tabs:
|
|
236
|
+
t_ds._hidden_offscreen = True
|
|
237
|
+
|
|
238
|
+
# Publish drop slots BETWEEN the stacked content views (the framework only
|
|
239
|
+
# derives slots from _children, which here are the tab buttons): one
|
|
240
|
+
# horizontal line above each content (insert before its dict key) and one
|
|
241
|
+
# below the last (append after it). Rebuilt from live geometry every
|
|
242
|
+
# render, so they track scroll/reflow; a dragged content is excluded (its
|
|
243
|
+
# home slot is the cancel target). Insert indices are dict-space,
|
|
244
|
+
# same as the bar slots.
|
|
245
|
+
extra_slots = []
|
|
246
|
+
for idx, c_ds in content_stack:
|
|
247
|
+
top, left = c_ds.abs_top, c_ds.abs_left
|
|
248
|
+
if top is None or left is None or not c_ds.width:
|
|
249
|
+
continue
|
|
250
|
+
extra_slots.append((idx, left, left + c_ds.width, top - 2, False))
|
|
251
|
+
if extra_slots:
|
|
252
|
+
last_idx, last_ds = content_stack[-1]
|
|
253
|
+
if last_ds.abs_top is not None and last_ds.abs_left is not None and last_ds.width:
|
|
254
|
+
extra_slots.append((last_idx + 1, last_ds.abs_left,
|
|
255
|
+
last_ds.abs_left + last_ds.width,
|
|
256
|
+
last_ds.abs_top + (last_ds.height or 0) + 3, False))
|
|
257
|
+
draw_state._dnd_extra_slots = extra_slots
|
|
258
|
+
|
|
259
|
+
return changed, input_value
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@render_func(is_default_for=(dict, MutableMapping, defaultdict, tuple, list, GeneralParse,
|
|
263
|
+
CallParse, ClassParse, EnumParse, FunctionParse, _BubblingDict, _DeepPath),
|
|
264
|
+
use_cache=True, header_same_line=False, show_bg=True, show_instance_vars=False, align_header=False,
|
|
265
|
+
manual_content_height=True, shadow=True, selectable=False, bg_offset=-0.8,
|
|
266
|
+
wrap=False, with_header=draw_header, indent_size=3, searchable=True, child_kwargs=None)
|
|
267
|
+
def draw_collection(input_value, draw_state, depth, style_manager, meta, icon=None,
|
|
268
|
+
mode=None, keys=None, get_attr=None, set_attr=None, show_excluded=False,
|
|
269
|
+
child_kwargs=None, show_bg=False, show_search=False, align_header=False, wrap=False,
|
|
270
|
+
on_collapse=False, search_text="", return_item=False, close_triggers_delete=False,
|
|
271
|
+
on_expand=False, show_add_delete=False, show_add_types=None, item_spacing_y=3, show_system=False,
|
|
272
|
+
included=None, horizontal=False, show_indices=False, excluded=None, annotation=None,
|
|
273
|
+
drop_tail_height=None, **kwargs):
|
|
274
|
+
"""
|
|
275
|
+
Universal collection renderer
|
|
276
|
+
show_add_types={"Display Name": TypeA, ...} draws a second + button in the
|
|
277
|
+
header that instantiates the chosen type (rendered by draw_header; the
|
|
278
|
+
value just rides the kwargs through). Several entries get a chevron
|
|
279
|
+
dropdown to pick from; a single entry binds the + directly with no
|
|
280
|
+
chevron. A bare list of types is accepted and keyed by __name__.
|
|
281
|
+
"""
|
|
282
|
+
from meltygui.core.conversion.render_host import RenderHost
|
|
283
|
+
from meltygui.editor.text_editor import _scroll_into_view
|
|
284
|
+
from meltygui.core.windowing.glfw_utils import print_stack_trace
|
|
285
|
+
from meltygui.view.decoration_view import draw_bg
|
|
286
|
+
from meltygui.view.header_view import draw_header_end
|
|
287
|
+
from meltygui.core.cache.tile_cache import snap_int
|
|
288
|
+
from meltygui.model.collection_model import annotation_item_type
|
|
289
|
+
from meltygui.model.collection_model import _collection_match_keys
|
|
290
|
+
from meltygui.model.search_model import _fuzzy_key_match
|
|
291
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
292
|
+
from meltygui.core.rendering.render_dispatch import seperator
|
|
293
|
+
import meltygui.core.input.drag_drop_core as _drag_drop
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if excluded is None:
|
|
299
|
+
excluded = set()
|
|
300
|
+
|
|
301
|
+
if included is None:
|
|
302
|
+
included = set()
|
|
303
|
+
|
|
304
|
+
if child_kwargs is None:
|
|
305
|
+
child_kwargs = {}
|
|
306
|
+
|
|
307
|
+
# A RenderHost rendered DIRECTLY (draw_collection(host) - the settings
|
|
308
|
+
# column of draw_space_mouse, the modifies playground) is this view's
|
|
309
|
+
# delegate, so pulse it: notify_on_change is the per-frame liveness
|
|
310
|
+
# stamp that idle sweep (RenderHost.sweep) reads, so it re-registers a
|
|
311
|
+
# swept host. Without the pulse an evictable code host was deregistered
|
|
312
|
+
# after idle_frames, the wrapper never drew again, and an edit eve
|
|
313
|
+
# dirtied the dict but the outbound chain (dict -> cst -> source -> save)
|
|
314
|
+
# never ran - the code never updated (Lukas 09-04). Idle frames skip
|
|
315
|
+
# the body, so a swept host revives on the next frame an edit re-runs it.
|
|
316
|
+
if isinstance(input_value, RenderHost):
|
|
317
|
+
input_value.notify_on_change(draw_state)
|
|
318
|
+
|
|
319
|
+
changed = False
|
|
320
|
+
|
|
321
|
+
if hasattr(input_value, 'children') and isinstance(input_value.children, (list, dict, defaultdict,
|
|
322
|
+
types.MappingProxyType, deque)):
|
|
323
|
+
input_value = input_value.children
|
|
324
|
+
|
|
325
|
+
if show_bg and draw_state.total_z_offset < 0:
|
|
326
|
+
imgui.dummy(1, 3)
|
|
327
|
+
else:
|
|
328
|
+
imgui.dummy(1, 1)
|
|
329
|
+
|
|
330
|
+
# --- configure per collection type ---
|
|
331
|
+
collection = input_value
|
|
332
|
+
|
|
333
|
+
# When the value *is* a class (e.g. an @window-registered class drawn
|
|
334
|
+
# directly), its per-attribute `{field}_meta` overrides live on the class
|
|
335
|
+
# itself, not on its metaclass. Use the class as parent_type so get_child_meta
|
|
336
|
+
# can find them; otherwise fall back to the instance's class.
|
|
337
|
+
parent_type = input_value if isinstance(input_value, type) else input_value.__class__
|
|
338
|
+
if isinstance(input_value, (str, int, float, bool, Enum, NoneType)):
|
|
339
|
+
imgui.text("No view for type: " + str(type(input_value)))
|
|
340
|
+
return False, input_value
|
|
341
|
+
if keys is None:
|
|
342
|
+
if isinstance(input_value,
|
|
343
|
+
(dict, list, tuple, set, defaultdict, MutableMapping, types.MappingProxyType, _DeepPath, deque)):
|
|
344
|
+
apply_change = True
|
|
345
|
+
parent_type = input_value.__class__
|
|
346
|
+
if isinstance(input_value, types.MappingProxyType):
|
|
347
|
+
keys = input_value.keys()
|
|
348
|
+
elif isinstance(input_value, (dict, defaultdict, MutableMapping, types.MappingProxyType)):
|
|
349
|
+
keys = input_value.keys()
|
|
350
|
+
collection = input_value
|
|
351
|
+
else:
|
|
352
|
+
keys = range(len(input_value))
|
|
353
|
+
collection = list(input_value)
|
|
354
|
+
|
|
355
|
+
elif hasattr(input_value, "__dict__") and depth < Core.melty.max_depth:
|
|
356
|
+
if hasattr(type(input_value), "__field_defaults__") and hasattr(input_value, 'to_dict'):
|
|
357
|
+
type(input_value).__field_defaults__.update(input_value.__dict__)
|
|
358
|
+
# __field_defaults__ accumulates keys from every instance, so a
|
|
359
|
+
# field deleted from THIS instance lingers there. Skip keys the
|
|
360
|
+
# instance no longer resolves (neither set nor the class default)
|
|
361
|
+
# so deleting a field removes its row instead of leaving a ghost.
|
|
362
|
+
keys = [k for k in type(input_value).__field_defaults__
|
|
363
|
+
if k in input_value.__dict__ or hasattr(input_value, k)]
|
|
364
|
+
else:
|
|
365
|
+
if input_value is None or input_value.__dict__ is None:
|
|
366
|
+
return False, input_value
|
|
367
|
+
keys = input_value.__dict__.keys()
|
|
368
|
+
collection = input_value.__dict__
|
|
369
|
+
use_tint = False
|
|
370
|
+
apply_change = True
|
|
371
|
+
else:
|
|
372
|
+
imgui.text("No view for type: " + str(type(input_value)))
|
|
373
|
+
return False, input_value
|
|
374
|
+
|
|
375
|
+
keys = list(keys)[:]
|
|
376
|
+
|
|
377
|
+
# --- per-key type annotations ---
|
|
378
|
+
# Class-level annotations (walking the MRO) name a type per attribute when
|
|
379
|
+
# rendering an object's __dict__; a typed container annotation
|
|
380
|
+
# (Dict[str, Lora] / List[Lora]) covers every key otherwise. Each child
|
|
381
|
+
# inherits its annotation so its own add button can instantiate the right
|
|
382
|
+
# item type (replaces the old meta.field_type path).
|
|
383
|
+
parent_annotations = {}
|
|
384
|
+
for _klass in reversed(getattr(parent_type, "__mro__", ())):
|
|
385
|
+
_anns = _klass.__dict__.get("__annotations__")
|
|
386
|
+
if _anns:
|
|
387
|
+
parent_annotations.update(_anns)
|
|
388
|
+
item_annotation = annotation_item_type(annotation)
|
|
389
|
+
|
|
390
|
+
# --- search (key matching) ---
|
|
391
|
+
# Same dual resolution as the text editor: a forwarded SearchTerm carries
|
|
392
|
+
# the shared cross-view session, or the owner's own session when this
|
|
393
|
+
# collection hosts the find UI. We claim one slot per matching key, in
|
|
394
|
+
# visual order interleaved with the children (claimed below), so the
|
|
395
|
+
# combined next/prev sequence reads top-to-bottom. The current key is
|
|
396
|
+
# latched so incidental repaints don't shift the highlight.
|
|
397
|
+
_search_term = search_text or (draw_state.search_text if draw_state.search_active else "")
|
|
398
|
+
if isinstance(_search_term, SearchTerm):
|
|
399
|
+
search_session = _search_term
|
|
400
|
+
elif draw_state.search_active and draw_state._search_session is not None:
|
|
401
|
+
search_session = draw_state._search_session
|
|
402
|
+
else:
|
|
403
|
+
search_session = None
|
|
404
|
+
search_q = str(_search_term).lower() if (search_session is not None and _search_term) else ""
|
|
405
|
+
search_current_y = None # screen-Y of the current key's row (for scroll)
|
|
406
|
+
search_current_h = None
|
|
407
|
+
# On a full-search frame (term change / nav) render every row — even ones
|
|
408
|
+
# the off-screen optimization would skip — so the row holding the current
|
|
409
|
+
# match is reached and can scroll into view.
|
|
410
|
+
_search_full_render = search_session is not None and search_session.scroll_to
|
|
411
|
+
|
|
412
|
+
# Stash a matcher so the search owner's tree walk (DrawState.descendants /
|
|
413
|
+
# meltygui.search_walk) can count this collection's key matches without
|
|
414
|
+
# rendering. It counts only this collection's own keys (the whole key list,
|
|
415
|
+
# not just the rows the loop below draws); child collections / text editors
|
|
416
|
+
# are separate tree nodes with their own matchers, so the walk sums them
|
|
417
|
+
# without double-counting. Keys are re-derived lazily on call (only on
|
|
418
|
+
# counting frames), so an unsearched render pays nothing for it.
|
|
419
|
+
def _search_matcher(term, session, _iv=input_value, _keys=keys,
|
|
420
|
+
_excl=excluded, _se=show_excluded):
|
|
421
|
+
q = str(term).lower()
|
|
422
|
+
if not q:
|
|
423
|
+
return
|
|
424
|
+
mk = _collection_match_keys(_iv, _keys, _excl, _se)
|
|
425
|
+
session.claim(sum(1 for _, k in mk if _fuzzy_key_match(q, k)))
|
|
426
|
+
|
|
427
|
+
draw_state._search_matcher = _search_matcher
|
|
428
|
+
|
|
429
|
+
# The owner's pre-body walk picked the global-current match and stashed its
|
|
430
|
+
# local index on us (_search_active_local) when one of OUR keys holds it —
|
|
431
|
+
# the same walk that produced the count, so selection and count agree.
|
|
432
|
+
# Resolve that ordinal (over our matching keys, in key order) to the key
|
|
433
|
+
# index the loop should highlight + scroll to. None when the current match
|
|
434
|
+
# lives in a child instead (that child carries its own mark).
|
|
435
|
+
_current_key_idx = None
|
|
436
|
+
if search_q and draw_state._search_active_local is not None:
|
|
437
|
+
_matching = [i for (i, k) in
|
|
438
|
+
_collection_match_keys(input_value, keys, excluded, show_excluded)
|
|
439
|
+
if _fuzzy_key_match(search_q, k)]
|
|
440
|
+
if 0 <= draw_state._search_active_local < len(_matching):
|
|
441
|
+
_current_key_idx = _matching[draw_state._search_active_local]
|
|
442
|
+
|
|
443
|
+
# --- unified loop ---
|
|
444
|
+
drew_any = False
|
|
445
|
+
|
|
446
|
+
start_cursor = imgui.get_cursor_pos()[1]
|
|
447
|
+
rect = Core.melty.get_clip_rect()
|
|
448
|
+
|
|
449
|
+
premature_break = False
|
|
450
|
+
|
|
451
|
+
Core.melty.collection_index_stack.append(0)
|
|
452
|
+
this_collection = len(Core.melty.collection_index_stack) - 1
|
|
453
|
+
|
|
454
|
+
max_items = 5000
|
|
455
|
+
start_index = 0
|
|
456
|
+
end_index = min(len(keys) - 1, max_items)
|
|
457
|
+
|
|
458
|
+
scroll_offset = draw_state.scroll_offset
|
|
459
|
+
true_left = draw_state.left - scroll_offset[0]
|
|
460
|
+
true_top = draw_state.top - scroll_offset[1]
|
|
461
|
+
|
|
462
|
+
# Remove excluded from keys
|
|
463
|
+
item_to_return = None
|
|
464
|
+
# Keys of children whose close (X) was clicked this frame — collected during the
|
|
465
|
+
# loop and removed from the collection AFTER it (never mutate keys mid-iteration).
|
|
466
|
+
to_delete = set()
|
|
467
|
+
# Whether any row was row-skipped this pass - a skipped pass reconstructs
|
|
468
|
+
# the layout from cached relative_pos/height caches, so its accuracy is only
|
|
469
|
+
# as good as those caches; a skip-free pass measured everything for real.
|
|
470
|
+
rows_skipped = False
|
|
471
|
+
|
|
472
|
+
for idx in range(start_index, end_index + 1):
|
|
473
|
+
key = keys[idx]
|
|
474
|
+
relative_pos = imgui.get_cursor_screen_pos()
|
|
475
|
+
relative_pos = (relative_pos[0] - true_left,
|
|
476
|
+
relative_pos[1] - true_top + item_spacing_y)
|
|
477
|
+
|
|
478
|
+
child_draw_state = draw_state._children.get(idx, None)
|
|
479
|
+
|
|
480
|
+
# A child currently being drag-and-dropped renders as a floating
|
|
481
|
+
# window (kwargs injected below) - never row-skip it, its stale
|
|
482
|
+
# relative_pos no longer says where it is.
|
|
483
|
+
is_dragged = _drag_drop.DragDrop.is_dragged_child(draw_state, key)
|
|
484
|
+
|
|
485
|
+
# ----- off-screen detection -----
|
|
486
|
+
# Is this row scrolled outside the viewport? When not searching, skip it
|
|
487
|
+
# entirely up front (the perf early-out). On a search-counting frame keep
|
|
488
|
+
# `clipped` to decide below: reuse a cached match count (no render) or
|
|
489
|
+
# render to (re)count.
|
|
490
|
+
clipped = False
|
|
491
|
+
if (not horizontal and not is_dragged and child_draw_state is not None
|
|
492
|
+
and child_draw_state.relative_pos is not None
|
|
493
|
+
and not Core.melty.frame_count <= 2
|
|
494
|
+
and (not draw_state.invalid_content_height or imgui.is_mouse_down(0)
|
|
495
|
+
or imgui.is_mouse_down(1) or imgui.is_mouse_down(2))):
|
|
496
|
+
_spy = true_top + child_draw_state.relative_pos[1] - child_draw_state.header_height
|
|
497
|
+
_bottom = _spy + child_draw_state.height + child_draw_state.header_height
|
|
498
|
+
clipped = (_bottom + child_draw_state.height < rect[1] or _spy > rect[3])
|
|
499
|
+
|
|
500
|
+
if clipped and not _search_full_render:
|
|
501
|
+
rows_skipped = True
|
|
502
|
+
imgui.set_cursor_screen_pos((imgui.get_cursor_screen_pos()[0],
|
|
503
|
+
(true_top + child_draw_state.relative_pos[1] +
|
|
504
|
+
child_draw_state.height)))
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
Core.melty.collection_index_stack[this_collection] = idx
|
|
508
|
+
item = None
|
|
509
|
+
if get_attr is None:
|
|
510
|
+
if isinstance(collection, dict) and key not in collection:
|
|
511
|
+
# A declared attr deleted from the instance __dict__ still
|
|
512
|
+
# resolves through the class default - use that instead of
|
|
513
|
+
# degrading to a "Key not found" ghost row.
|
|
514
|
+
if hasattr(input_value, "__dict__") and hasattr(input_value, str(key)):
|
|
515
|
+
item = getattr(input_value, str(key), None)
|
|
516
|
+
else:
|
|
517
|
+
imgui.text("Key not found: " + str(key))
|
|
518
|
+
continue
|
|
519
|
+
elif hasattr(input_value, "__dict__") and hasattr(input_value, str(key)):
|
|
520
|
+
item = getattr(input_value, str(key), None)
|
|
521
|
+
else:
|
|
522
|
+
item = collection[key]
|
|
523
|
+
else:
|
|
524
|
+
try:
|
|
525
|
+
item = get_attr(input_value, key)
|
|
526
|
+
except Exception as e:
|
|
527
|
+
imgui.text(f"Error getting key {key}")
|
|
528
|
+
|
|
529
|
+
# visual separator (object extras)
|
|
530
|
+
if key is None and item is None:
|
|
531
|
+
seperator(Core.melty.spacing[1])
|
|
532
|
+
continue
|
|
533
|
+
# apply global skip to all types
|
|
534
|
+
if isinstance(key, (float, Enum, NoneType)):
|
|
535
|
+
key_str = f"{input_value.__class__.__name__}"
|
|
536
|
+
elif isinstance(key, int):
|
|
537
|
+
key_str = f"{key}"
|
|
538
|
+
else:
|
|
539
|
+
key_str = str(key)
|
|
540
|
+
|
|
541
|
+
if not show_excluded and hasattr(type(input_value), "__excluded_attrs__"):
|
|
542
|
+
if not Toggles.show_excluded:
|
|
543
|
+
if key_str in type(input_value).__excluded_attrs__ and key_str not in included:
|
|
544
|
+
continue
|
|
545
|
+
display_name = None
|
|
546
|
+
|
|
547
|
+
if key_str in excluded and key_str not in included:
|
|
548
|
+
continue
|
|
549
|
+
|
|
550
|
+
if not show_excluded and (not show_system and (key_str.startswith("_") or key_str.endswith("_"))):
|
|
551
|
+
if key_str not in included:
|
|
552
|
+
continue
|
|
553
|
+
|
|
554
|
+
# ----- SEARCH (key match) -----
|
|
555
|
+
# Whether this key is the global-current match was decided by the
|
|
556
|
+
# owner's pre-body walk (resolved to _current_key_idx above); we just
|
|
557
|
+
# flag it and record its row Y so the post-loop block scrolls to it.
|
|
558
|
+
key_is_match = bool(search_q) and _fuzzy_key_match(search_q, key_str.lower())
|
|
559
|
+
key_is_current = key_is_match and idx == _current_key_idx
|
|
560
|
+
if key_is_current:
|
|
561
|
+
search_current_y = imgui.get_cursor_screen_pos()[1]
|
|
562
|
+
|
|
563
|
+
prev_tint = None
|
|
564
|
+
try:
|
|
565
|
+
|
|
566
|
+
y_offset = Core.melty.collection_spacing
|
|
567
|
+
if show_indices:
|
|
568
|
+
display_name = f"{str(idx)}"
|
|
569
|
+
|
|
570
|
+
item_kwargs = {
|
|
571
|
+
'type_collection': kwargs.get("real_type", type(input_value)),
|
|
572
|
+
'real_type': type(item),
|
|
573
|
+
'return_extras': True,
|
|
574
|
+
'key': key,
|
|
575
|
+
'on_collapse': on_collapse,
|
|
576
|
+
'on_expand': on_expand,
|
|
577
|
+
'collection': input_value,
|
|
578
|
+
'name': key_str,
|
|
579
|
+
'display_name': display_name,
|
|
580
|
+
'parent_show_add_delete': show_add_delete,
|
|
581
|
+
'show_add_delete': show_add_delete,
|
|
582
|
+
'with_header_end': draw_header_end if show_add_delete else None,
|
|
583
|
+
'annotation': parent_annotations.get(key, item_annotation),
|
|
584
|
+
'y_offset': y_offset,
|
|
585
|
+
'mode': mode,
|
|
586
|
+
'wrap': wrap,
|
|
587
|
+
'search_match': key_is_match,
|
|
588
|
+
'search_current': key_is_current,
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if "content_width" in draw_state._kwargs:
|
|
592
|
+
item_kwargs['content_width'] = draw_state._kwargs['content_width'] - Core.melty.spacing[0] * 2
|
|
593
|
+
|
|
594
|
+
# Folders: a dict child of a typed-add collection (show_add_types)
|
|
595
|
+
# inherits the same type choices, so nested folders keep the
|
|
596
|
+
# [+ <type> v] affordance all the way down.
|
|
597
|
+
if show_add_types and isinstance(item, dict):
|
|
598
|
+
item_kwargs['show_add_types'] = show_add_types
|
|
599
|
+
|
|
600
|
+
# Per-field `# [tint=...]` comment overrides
|
|
601
|
+
# (__overrides__['__<field>__'] from the parent) are fed by the
|
|
602
|
+
# render_func wrapper from the collection= + key= passed above -
|
|
603
|
+
# see the __overrides__ block in core_render.
|
|
604
|
+
|
|
605
|
+
item_kwargs = item_kwargs | child_kwargs
|
|
606
|
+
if is_dragged:
|
|
607
|
+
# Detach the dragged child to a floating closable window,
|
|
608
|
+
# pinned to its pre-pickup size (the wrapper glues the
|
|
609
|
+
# window_pos to the cursor each dispatch).
|
|
610
|
+
item_kwargs.update(_drag_drop.DragDrop.dragged_item_kwargs())
|
|
611
|
+
if isinstance(input_value, (list, tuple, set)) or horizontal:
|
|
612
|
+
item_kwargs['align_header'] = False
|
|
613
|
+
|
|
614
|
+
if horizontal and child_draw_state is not None:
|
|
615
|
+
rect = Core.melty.get_clip_rect()
|
|
616
|
+
right_edge = rect[2]
|
|
617
|
+
space_left = right_edge - (imgui.get_cursor_screen_pos()[0] + child_draw_state.width)
|
|
618
|
+
|
|
619
|
+
if len(child_draw_state._children) > 0 and child_draw_state.height > 50:
|
|
620
|
+
imgui.dummy(0, 0)
|
|
621
|
+
|
|
622
|
+
elif space_left < 0:
|
|
623
|
+
imgui.new_line()
|
|
624
|
+
imgui.dummy(0, item_spacing_y)
|
|
625
|
+
|
|
626
|
+
item_return = draw_any(item, **item_kwargs)
|
|
627
|
+
|
|
628
|
+
if len(item_return) == 3:
|
|
629
|
+
item_changed, out_val, returned_ds = item_return
|
|
630
|
+
else:
|
|
631
|
+
item_changed, out_val, returned_ds = item_return[0], item_return[1], None
|
|
632
|
+
|
|
633
|
+
if returned_ds is not None:
|
|
634
|
+
draw_state._children[idx] = returned_ds
|
|
635
|
+
returned_ds._collection_draw_state = draw_state
|
|
636
|
+
returned_ds.relative_pos = relative_pos
|
|
637
|
+
# Child window closed via its X (closable + closed) -> queue its key
|
|
638
|
+
# for removal from the collection (applied after the loop).
|
|
639
|
+
if returned_ds.closable and returned_ds.closed:
|
|
640
|
+
if close_triggers_delete:
|
|
641
|
+
to_delete.add(key)
|
|
642
|
+
if key_is_current:
|
|
643
|
+
search_current_h = returned_ds.header_height
|
|
644
|
+
if is_dragged:
|
|
645
|
+
# The child deferred to a floating window and drew nothing
|
|
646
|
+
# inline — hold its slot open with a placeholder so the
|
|
647
|
+
# collection layout doesn't shift. Neither the horizontal
|
|
648
|
+
# branch nor any other math off returned_ds may run here:
|
|
649
|
+
# returned_ds.abs_* is the floating window glued to the
|
|
650
|
+
# mouse, so a mid-drag re-render would measure the flow
|
|
651
|
+
# back of the cursor. The placeholder anchors itself to the
|
|
652
|
+
# pickup slot (DragDrop.home_rect).
|
|
653
|
+
_drag_drop.DragDrop.draw_placeholder(horizontal, item_spacing_y,
|
|
654
|
+
style_manager=style_manager,
|
|
655
|
+
draw_bg=draw_bg)
|
|
656
|
+
elif horizontal:
|
|
657
|
+
imgui.same_line(spacing=0)
|
|
658
|
+
imgui.set_cursor_screen_pos((returned_ds.abs_left + returned_ds.width, returned_ds.abs_top))
|
|
659
|
+
else:
|
|
660
|
+
imgui.dummy(0, item_spacing_y)
|
|
661
|
+
|
|
662
|
+
if isinstance(out_val, CollectionAction):
|
|
663
|
+
# perform the move; this should mutate the plain dicts you attached
|
|
664
|
+
result = Core.melty.to_apply(out_val)
|
|
665
|
+
item_changed, out_val = False, None
|
|
666
|
+
|
|
667
|
+
if set_attr is not None and item_changed:
|
|
668
|
+
try:
|
|
669
|
+
set_attr(input_value, key, out_val)
|
|
670
|
+
except Exception as e:
|
|
671
|
+
print(f"Error setting key {key} to value {out_val}: {e}")
|
|
672
|
+
else:
|
|
673
|
+
if "return_item" not in item_kwargs:
|
|
674
|
+
if item_changed and apply_change and key is not None:
|
|
675
|
+
if isinstance(input_value, (dict, defaultdict, MutableMapping, types.MappingProxyType)):
|
|
676
|
+
input_value[key] = out_val
|
|
677
|
+
elif isinstance(input_value, list):
|
|
678
|
+
input_value[key] = out_val
|
|
679
|
+
elif isinstance(input_value, deque):
|
|
680
|
+
input_value[key] = out_val
|
|
681
|
+
elif isinstance(input_value, tuple):
|
|
682
|
+
temp = list(input_value)
|
|
683
|
+
temp[key] = out_val
|
|
684
|
+
input_value = parent_type(temp)
|
|
685
|
+
else:
|
|
686
|
+
setattr(input_value, key_str, out_val)
|
|
687
|
+
|
|
688
|
+
changed |= item_changed
|
|
689
|
+
if item_changed and return_item:
|
|
690
|
+
item_to_return = out_val
|
|
691
|
+
|
|
692
|
+
except Exception as e:
|
|
693
|
+
print(f"Error rendering field '{key_str}' of {type(input_value).__name__}: {e}")
|
|
694
|
+
print_stack_trace(exception=e)
|
|
695
|
+
|
|
696
|
+
finally:
|
|
697
|
+
if prev_tint is not None:
|
|
698
|
+
style_manager.set_imgui_tint(*prev_tint)
|
|
699
|
+
|
|
700
|
+
Core.melty.collection_index_stack.pop()
|
|
701
|
+
|
|
702
|
+
# Apply removals for children closed via their X this frame (collected above, so the
|
|
703
|
+
# collection is never mutated mid-iteration). Only dict-like / list collections are
|
|
704
|
+
# safely key-deletable here; tuples/sets/object-__dict__ are left untouched. Sets
|
|
705
|
+
# `changed` so the edit propagates to the owner (e.g. a RenderHost io_function).
|
|
706
|
+
if to_delete:
|
|
707
|
+
print(f"Deleting keys {to_delete} {input_value.__class__.__name__}")
|
|
708
|
+
if isinstance(input_value, (dict, defaultdict, MutableMapping, _BubblingDict)):
|
|
709
|
+
for _k in to_delete:
|
|
710
|
+
print(f"_k in to_delete Deleting key {_k}")
|
|
711
|
+
if _k in input_value:
|
|
712
|
+
print(f"del input_value[_k found in, deleting")
|
|
713
|
+
del input_value[_k]
|
|
714
|
+
changed = True
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
elif isinstance(input_value, list):
|
|
718
|
+
for _i in sorted((k for k in to_delete if isinstance(k, int)), reverse=True):
|
|
719
|
+
if 0 <= _i < len(input_value):
|
|
720
|
+
del input_value[_i]
|
|
721
|
+
changed = True
|
|
722
|
+
|
|
723
|
+
# When navigation just happened, scroll the current key into view.
|
|
724
|
+
# draw_collection disables its own scroll, so _scroll_into_view walks up to
|
|
725
|
+
# the real scroll container. (A current match inside a child is scrolled by
|
|
726
|
+
# that child itself.) _current_key_idx came from the owner's walk.
|
|
727
|
+
if search_session is not None and search_session.scroll_to:
|
|
728
|
+
draw_state._search_current_key = _current_key_idx
|
|
729
|
+
if search_current_y is not None:
|
|
730
|
+
h = search_current_h or imgui.get_text_line_height()
|
|
731
|
+
_scroll_into_view(draw_state, search_current_y, search_current_y + h)
|
|
732
|
+
|
|
733
|
+
# Prune stale child slots - indices outside the current key range, left
|
|
734
|
+
# over from deletions/cross-collection edits. Their draw_states keep old
|
|
735
|
+
# geometry that ghost-walks (skip-advance, drop slots) would trip over.
|
|
736
|
+
if draw_state._children:
|
|
737
|
+
for _stale_idx in [i for i in draw_state._children
|
|
738
|
+
if isinstance(i, int) and i > end_index]:
|
|
739
|
+
del draw_state._children[_stale_idx]
|
|
740
|
+
|
|
741
|
+
# Static drop tail for drag-and-drop collections: a fixed strip of empty
|
|
742
|
+
# space below the last row. Its height counts towards the collection's
|
|
743
|
+
# measured height, which pushes the PARENT's "append to folder" slot down
|
|
744
|
+
# - without it a folder ends right at its last child, so that slot and the
|
|
745
|
+
# nested collection's own "append at end" slot (last child bottom + 3) land
|
|
746
|
+
# nearly on top of each other. Also gives an empty collection a droppable
|
|
747
|
+
# body. Always present (NOT drag-conditional, per Lukas) so layout never
|
|
748
|
+
# shifts when a drag begins. Gated to dict/list here - exactly what
|
|
749
|
+
# the DnD system treats as a drop target (see drag_drop._is_target_collection).
|
|
750
|
+
# Height comes live from Toggles.Collection.drop_tail_height (so the Toggles
|
|
751
|
+
# UI edits it globally); a per-call drop_tail_height= kwarg overrides it.
|
|
752
|
+
_drop_tail = (drop_tail_height if drop_tail_height is not None
|
|
753
|
+
else Toggles.Collection.drop_tail_height)
|
|
754
|
+
if (_drop_tail and not horizontal
|
|
755
|
+
and isinstance(draw_state._raw_input_value, (dict, list))):
|
|
756
|
+
tail_h = int(_drop_tail)
|
|
757
|
+
# Single-line collections (the wrapper same-line's the body BESIDE the
|
|
758
|
+
# header when not multi_line - e.g. a small dict) need the tail to
|
|
759
|
+
# also span the header height, or a bare vertical dummy sits to the
|
|
760
|
+
# right of the header and never grows the box past header height. Add
|
|
761
|
+
# the header height so the tail reaches BELOW the header, giving an
|
|
762
|
+
# empty collection a real droppable body. Multi-line collections
|
|
763
|
+
# already stack the body under the header, so a bare tail is fine.
|
|
764
|
+
if not draw_state.multi_line:
|
|
765
|
+
tail_h += int(draw_state.header_height or 0)
|
|
766
|
+
imgui.dummy(1, tail_h)
|
|
767
|
+
|
|
768
|
+
end_pos = imgui.get_cursor_pos()[1]
|
|
769
|
+
content_height = (end_pos - start_cursor)
|
|
770
|
+
imgui.dummy(1, 0)
|
|
771
|
+
|
|
772
|
+
# Commit the measurement when it's trustworthy: a skip-free pass measured
|
|
773
|
+
# every row for real (commit even mid-drag - that's what lets a collection
|
|
774
|
+
# update DURING a drag instead of storming after it); a pass with
|
|
775
|
+
# skips reconstructed the layout from caches, so only commit it in the
|
|
776
|
+
# old steady-state conditions (no buttons held).
|
|
777
|
+
measured_fully = not rows_skipped and not premature_break
|
|
778
|
+
if measured_fully or (not imgui.is_mouse_down(0) and not imgui.is_mouse_down(1)
|
|
779
|
+
and not imgui.is_mouse_down(2) and not Melty.space_mouse_drag
|
|
780
|
+
and not premature_break):
|
|
781
|
+
draw_state.content_height = snap_int(content_height)
|
|
782
|
+
draw_state.invalid_content_height = False
|
|
783
|
+
|
|
784
|
+
draw_state.premature_break = premature_break
|
|
785
|
+
if return_item:
|
|
786
|
+
if changed:
|
|
787
|
+
return changed, item_to_return
|
|
788
|
+
else:
|
|
789
|
+
return False, input_value
|
|
790
|
+
|
|
791
|
+
return changed, input_value
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@render_func(is_default_for=(
|
|
795
|
+
'tint', 'help_yellow_tint', 'color', 'context_select_tint', "text_color", "gradient_color", "outline_color",
|
|
796
|
+
# Any 3- or 4-tuple of numbers with a float in it is a colour (promoted
|
|
797
|
+
# dtype: `(0, 0, 0, 0.1)` works, an all-int `(1, 2, 3)` doesn't). The
|
|
798
|
+
# name entries above also catch plain tints like `tint=(0, 0, 0)`.
|
|
799
|
+
Shaped(tuple, (3,), float), Shaped(tuple, (4,), float)),
|
|
800
|
+
has_popup=True,
|
|
801
|
+
indent_size=2, is_tree=False, align_header=True, header_same_line=True, wrap=True,
|
|
802
|
+
show_name=True, selectable=False, max_width=100, min_width=33, use_cache=False, with_header=draw_header)
|
|
803
|
+
def draw_tuple(input_value: tuple | types.NoneType, name, unique, draw_state, outline=False,
|
|
804
|
+
info=None):
|
|
805
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
806
|
+
from meltygui.view.color_view import draw_color_picker
|
|
807
|
+
from meltygui.view.control_view import button
|
|
808
|
+
from meltygui.view.color_view import color_picker_height
|
|
809
|
+
from meltygui.view.color_view import color_picker_top_offset
|
|
810
|
+
from meltygui.view.color_view import color_picker_width
|
|
811
|
+
import meltygui.core.windowing.window_api as glfw
|
|
812
|
+
|
|
813
|
+
is_open = False
|
|
814
|
+
changed = False
|
|
815
|
+
|
|
816
|
+
if input_value is None:
|
|
817
|
+
# Position BEFORE drawing, exactly like the color branch below - a
|
|
818
|
+
# first frame that skips the same_line breaks the header row (every
|
|
819
|
+
# remaining item wraps to a new line) and the early return under the
|
|
820
|
+
# button never restores it. Single-line rows only: line placement
|
|
821
|
+
# belongs in the wrapper (it same_lines after the header unless it
|
|
822
|
+
# chose multi_line), and forcing same_line on a multi_line frame drags
|
|
823
|
+
# the widget back onto the header's line.
|
|
824
|
+
if not draw_state.multi_line:
|
|
825
|
+
imgui.same_line(spacing=4)
|
|
826
|
+
if button("", height=21, shadow=False, z_offset=0, corner_radius=4, tint=(0, 0, 0, 0.1
|
|
827
|
+
), tint_value=0.14, use_cache=True,
|
|
828
|
+
show_bg=True, text_pad=7, name=f"add_tuple##{unique}",
|
|
829
|
+
show_button_bg=True)[0]:
|
|
830
|
+
input_value = (0.0, 0.0, 0.0, 1.0)
|
|
831
|
+
request_render()
|
|
832
|
+
return True, input_value
|
|
833
|
+
|
|
834
|
+
else:
|
|
835
|
+
# if not draw_state.multi_line:
|
|
836
|
+
# imgui.same_line(spacing=4)
|
|
837
|
+
|
|
838
|
+
is_color = input_value is not None and isinstance(input_value, tuple) and len(input_value) in (3, 4) and all(
|
|
839
|
+
isinstance(c, (float, int)) for c in input_value)
|
|
840
|
+
if is_color:
|
|
841
|
+
# A swatch trigger that opens our own colour-picker popover (replacing
|
|
842
|
+
# imgui's built-in popup). Same popover pattern as the dropdown: identity
|
|
843
|
+
# in Melty.popover_focused_ds is the open state; click toggles it; the
|
|
844
|
+
# picker window is anchored under the swatch and dismissed on outside
|
|
845
|
+
# click / Esc. The picker itself is stateless and returns the new colour.
|
|
846
|
+
from meltygui.core.rendering.mode import Mode
|
|
847
|
+
is_open = Melty.popover_focused_ds is draw_state
|
|
848
|
+
col = list(input_value)
|
|
849
|
+
alpha = col[3] if len(col) == 4 else 1.0
|
|
850
|
+
# ALPHA_PREVIEW_HALF makes the swatch split: one half shows the colour
|
|
851
|
+
# composited over a checkerboard at its real alpha, the other fully
|
|
852
|
+
# opaque - so a length-4 tuple's transparency is visible in the chip itself
|
|
853
|
+
# (plain color_button forces opaque regardless of the alpha we pass).
|
|
854
|
+
flags = imgui.COLOR_EDIT_NO_TOOLTIP | imgui.COLOR_EDIT_ALPHA_PREVIEW_HALF
|
|
855
|
+
if imgui.color_button(f"##swatch{unique}{name}", col[0], col[1], col[2], alpha,
|
|
856
|
+
flags=flags, width=17, height=17):
|
|
857
|
+
Melty.popover_focused_ds = None if is_open else draw_state
|
|
858
|
+
if not is_open:
|
|
859
|
+
Melty._popover_open_frame = Melty.frame_count # grace the opening click
|
|
860
|
+
request_render()
|
|
861
|
+
if outline:
|
|
862
|
+
# Tight ring around the CHIP (the item just drawn) - the
|
|
863
|
+
# widget's draw_state box is the measured min/max_width
|
|
864
|
+
# envelope, far wider than the swatch.
|
|
865
|
+
_omn, _omx = imgui.get_item_rect_min(), imgui.get_item_rect_max()
|
|
866
|
+
imgui.get_window_draw_list().add_rect(
|
|
867
|
+
_omn.x - 1.5, _omn.y - 1.5, _omx.x + 1.5, _omx.y + 1.5,
|
|
868
|
+
pack_color(1.0, 1.0, 1.0, 0.55), rounding=4.0)
|
|
869
|
+
is_open = Melty.popover_focused_ds is draw_state # reflect the toggle this frame
|
|
870
|
+
|
|
871
|
+
# The picker window is closable -> fixed size (auto-resize is off for
|
|
872
|
+
# closable windows), and its content is raw imgui (not child render_funcs)
|
|
873
|
+
# so the framework can't measure it. Size the window to fit the SV square
|
|
874
|
+
# (180) + the N channel drag-floats and the hex line, so nothing clips.
|
|
875
|
+
# info: a general-purpose caption for the popover (e.g. the anywhere
|
|
876
|
+
# swatch's "last known source"). A CALLABLE resolves only while the
|
|
877
|
+
# popover is open for a lazy path, so closed swatches never pay for it.
|
|
878
|
+
_info = None
|
|
879
|
+
if is_open and info is not None:
|
|
880
|
+
_info = info() if callable(info) else info
|
|
881
|
+
picker_h = color_picker_height(4, bool(_info))
|
|
882
|
+
# parent_window=draw_state anchors the popover under the swatch and makes
|
|
883
|
+
# the tuple the picker's ancestor, so clear_focus (which protects the
|
|
884
|
+
# clicked swatch window closure) leaves the popover open when you click
|
|
885
|
+
# the tuple, and dismisses it when you click anywhere else.
|
|
886
|
+
# Flip up when opening down would run past the display bottom: the
|
|
887
|
+
# popover renders at the invoking cursor + window_pos (core_render's
|
|
888
|
+
# nested-window anchor), and the cursor here sits just under the swatch -
|
|
889
|
+
# so the up offset is the picker's own height plus the swatch row.
|
|
890
|
+
_pop_y = color_picker_top_offset()
|
|
891
|
+
_anchor_y = imgui.get_cursor_screen_pos()[1]
|
|
892
|
+
_disp_h = imgui.get_io().display_size[1]
|
|
893
|
+
if _anchor_y + _pop_y + picker_h > _disp_h - 10:
|
|
894
|
+
_pop_y = -(picker_h + 38)
|
|
895
|
+
color_changed, new_color = draw_color_picker(input_value, name=f"color_picker{unique}",
|
|
896
|
+
closed=not is_open, window_pos=(0, _pop_y), info=_info,
|
|
897
|
+
parent_window=draw_state, width=color_picker_width(), height=picker_h,
|
|
898
|
+
mode=Modes.POPOVER)
|
|
899
|
+
if is_open:
|
|
900
|
+
if color_changed:
|
|
901
|
+
if new_color is not None:
|
|
902
|
+
input_value = tuple(new_color)
|
|
903
|
+
else:
|
|
904
|
+
input_value = None
|
|
905
|
+
request_render()
|
|
906
|
+
changed = True
|
|
907
|
+
# Dismiss on a click outside the swatch/popover, or on Esc.
|
|
908
|
+
# if imgui.is_mouse_clicked(0):
|
|
909
|
+
# mx, my = imgui.get_mouse_pos()
|
|
910
|
+
# if not any(_is_in_subtree(d, draw_state) for d in Core.melty.bvh_query(mx, my)):
|
|
911
|
+
# Melty.popover_focused_ds = None
|
|
912
|
+
# request_render()
|
|
913
|
+
if any(k == glfw.KEY_ESCAPE for k, _ in Core.melty.frame_key_events):
|
|
914
|
+
Melty.popover_focused_ds = None
|
|
915
|
+
request_render()
|
|
916
|
+
# Keep re-rendering while a slider/square is being dragged so the live
|
|
917
|
+
# imgui interaction (is_item_active) updates the frame.
|
|
918
|
+
if Melty.imgui_any_item_active or imgui.is_mouse_down(0):
|
|
919
|
+
Melty.cache.invalidate_up(draw_state._tile_id, max_depth=10, force=True)
|
|
920
|
+
request_render()
|
|
921
|
+
|
|
922
|
+
# elif input_value is not None and len(input_value) > 0 and isinstance(input_value[0], (float, int)):
|
|
923
|
+
# str_value = ", ".join([str(v) for v in input_value])
|
|
924
|
+
# ch, input_str = imgui.input_text("##tuple", str_value)
|
|
925
|
+
# if ch:
|
|
926
|
+
# try:
|
|
927
|
+
# new_tuple = eval(f"({input_str},)")
|
|
928
|
+
# if isinstance(new_tuple, tuple):
|
|
929
|
+
# input_value = new_tuple
|
|
930
|
+
# changed = True
|
|
931
|
+
# except Exception:
|
|
932
|
+
# pass
|
|
933
|
+
# else:
|
|
934
|
+
# changed, input_value = draw_collection(input_value=input_value)
|
|
935
|
+
|
|
936
|
+
return changed, input_value
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def draw_tuple_fast(input_value, draw_state, view_id, x=None, y=None, size=17,
|
|
940
|
+
outline=False, info=None, priority_delta=4, setter=None,
|
|
941
|
+
swatch=None, view_owner=None):
|
|
942
|
+
"""draw_tuple's colour chip for immediate-mode bodies (the code editor's
|
|
943
|
+
tab bar) — the fast_dock idea: no render_func / imgui widget per chip,
|
|
944
|
+
the swatch goes straight to the draw list and the click is a plain
|
|
945
|
+
`draw_state.on_action` sub on the chip's rect, so a bar of N tabs pays
|
|
946
|
+
N rects instead of N wrapper calls. The picker is the same
|
|
947
|
+
`draw_color_picker` POPOVER draw_tuple opens; several chips share one
|
|
948
|
+
draw_state, so the open one is `draw_state._tint_edit_key == view_id`
|
|
949
|
+
(beside Melty.popover_focused_ds, which names the draw_state). `x`/`y`
|
|
950
|
+
default to the current cursor screen position; the chip claims no
|
|
951
|
+
layout. Returns (changed, value) like draw_tuple. `setter(value)` is the
|
|
952
|
+
write the caller makes with a changed value — given, every change is
|
|
953
|
+
recorded on the undo stack (a SetterChange on the host's draw_state,
|
|
954
|
+
keyed by view_id) and undo/redo re-apply it through the setter, since
|
|
955
|
+
a chip has no wrapper of its own for Melty.undo_requests to land in.
|
|
956
|
+
`swatch` (an rgb(a) tuple) is what the chip PAINTS instead of the value
|
|
957
|
+
itself — a muted preview, e.g. the tint mixed toward its background —
|
|
958
|
+
while the picker still opens on, and edits, the real value.
|
|
959
|
+
`view_owner` is the VIEW draw_state whose paint the chip edits (a
|
|
960
|
+
window header's tint chip passes its host — not the `owner` flag below,
|
|
961
|
+
which is this chip owning the popover): given, the picker also carries
|
|
962
|
+
the view's `bg_offset` / `z_offset` rows (`draw_view_offsets_fast`,
|
|
963
|
+
written back through `view_owner.locate_<param>`) and, with
|
|
964
|
+
`Toggles.dynamic_styles`, the Residuals tab."""
|
|
965
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
966
|
+
from meltygui.view.color_view import draw_color_picker
|
|
967
|
+
from meltygui.view.color_view import _popover_anchor
|
|
968
|
+
from meltygui.view.color_view import color_picker_height
|
|
969
|
+
from meltygui.view.color_view import color_picker_top_offset
|
|
970
|
+
from meltygui.view.color_view import color_picker_width
|
|
971
|
+
import meltygui.core.windowing.window_api as glfw
|
|
972
|
+
|
|
973
|
+
# [tint=(0.85, 0.75, 0.05)]
|
|
974
|
+
corner_radius = 4.0
|
|
975
|
+
# [tint=(0.85, 0.75, 0.05)]
|
|
976
|
+
outline_color = (1.0, 1.0, 1.0, 0.55)
|
|
977
|
+
# The checker under a translucent chip (draw_tuple's COLOR_PREVIEW_HALF).
|
|
978
|
+
checker_dark, checker_light = (0.25, 0.25, 0.25), (0.6, 0.6, 0.6)
|
|
979
|
+
|
|
980
|
+
changed = False
|
|
981
|
+
if x is None or y is None:
|
|
982
|
+
cx, cy = imgui.get_cursor_screen_pos()
|
|
983
|
+
x = cx if x is None else x
|
|
984
|
+
y = cy if y is None else y
|
|
985
|
+
rect = (x, y, x + size, y + size)
|
|
986
|
+
draw_list = imgui.get_window_draw_list()
|
|
987
|
+
|
|
988
|
+
is_color = (isinstance(input_value, tuple) and len(input_value) in (3, 4)
|
|
989
|
+
and all(isinstance(c, (float, int)) for c in input_value))
|
|
990
|
+
if not is_color:
|
|
991
|
+
# Missing tint: a hollow chip; a click stamps in an opaque black.
|
|
992
|
+
draw_list.add_rect(x, y, x + size, y + size,
|
|
993
|
+
pack_color(1.0, 1.0, 1.0, 0.35),
|
|
994
|
+
rounding=corner_radius)
|
|
995
|
+
if draw_state.on_action("left_mouse_down", view_id=view_id, rect=rect,
|
|
996
|
+
priority_delta=priority_delta) is not None:
|
|
997
|
+
request_render()
|
|
998
|
+
return True, (0.0, 0.0, 0.0, 1.0)
|
|
999
|
+
return False, input_value
|
|
1000
|
+
|
|
1001
|
+
shown = swatch if swatch is not None else input_value
|
|
1002
|
+
r, g, b = float(shown[0]), float(shown[1]), float(shown[2])
|
|
1003
|
+
alpha = float(shown[3]) if len(shown) == 4 else 1.0
|
|
1004
|
+
if alpha < 1.0:
|
|
1005
|
+
# Left half: colour over a checkerboard at its real alpha; right
|
|
1006
|
+
# half: the colour opaque - so transparency shows in the chip.
|
|
1007
|
+
half = x + size * 0.5
|
|
1008
|
+
draw_list.add_rect_filled(x, y, half, y + size,
|
|
1009
|
+
pack_color(*checker_dark, 1.0),
|
|
1010
|
+
rounding=corner_radius,
|
|
1011
|
+
flags=imgui.DRAW_ROUND_CORNERS_LEFT)
|
|
1012
|
+
cell = size * 0.5
|
|
1013
|
+
draw_list.add_rect_filled(x + cell * 0.5, y, half, y + cell * 0.5,
|
|
1014
|
+
pack_color(*checker_light, 1.0))
|
|
1015
|
+
draw_list.add_rect_filled(x, y + cell * 0.5, x + cell * 0.5, y + size,
|
|
1016
|
+
pack_color(*checker_light, 1.0))
|
|
1017
|
+
draw_list.add_rect_filled(x, y, half, y + size,
|
|
1018
|
+
pack_color(r, g, b, alpha),
|
|
1019
|
+
rounding=corner_radius,
|
|
1020
|
+
flags=imgui.DRAW_ROUND_CORNERS_LEFT)
|
|
1021
|
+
draw_list.add_rect_filled(half, y, x + size, y + size,
|
|
1022
|
+
pack_color(r, g, b, 1.0),
|
|
1023
|
+
rounding=corner_radius,
|
|
1024
|
+
flags=imgui.DRAW_ROUND_CORNERS_RIGHT)
|
|
1025
|
+
else:
|
|
1026
|
+
draw_list.add_rect_filled(x, y, x + size, y + size,
|
|
1027
|
+
pack_color(r, g, b, 1.0),
|
|
1028
|
+
rounding=corner_radius)
|
|
1029
|
+
if outline:
|
|
1030
|
+
draw_list.add_rect(x - 1.5, y - 1.5, x + size + 1.5, y + size + 1.5,
|
|
1031
|
+
pack_color(*outline_color),
|
|
1032
|
+
rounding=corner_radius)
|
|
1033
|
+
|
|
1034
|
+
# `owner`: this chip last opened the popover. It stays the owner past an
|
|
1035
|
+
# outside click / escape / re-run (which clear the slot) until its next
|
|
1036
|
+
# run, where it draws the picker once more with closed=True and lets go.
|
|
1037
|
+
# The window itself does not wait for that run: a Mode.POPOVER window is
|
|
1038
|
+
# discarded by end_frame as soon as the slot no longer names it or an
|
|
1039
|
+
# ancestor (Melty.popover_orphaned) - so a chip whose host stopped
|
|
1040
|
+
# running (tab switched away, tile served from the blit cache) leaves its
|
|
1041
|
+
# picker hanging on screen.
|
|
1042
|
+
# draw_tuple's lifecycle: the popover slot names a draw_state whose
|
|
1043
|
+
# ancestor closure is protected from the click-away clear_focus. In
|
|
1044
|
+
# draw_tuple that is the chip's OWN draw_state; here every chip shares
|
|
1045
|
+
# the host's (the whole editor is its closure — nothing outside the
|
|
1046
|
+
# picker could ever close it), so the slot holds the PICKER WINDOW's
|
|
1047
|
+
# draw_state (its own closure = the picker) once it exists, and the
|
|
1048
|
+
# host only for the opening frame (covered by the popover grace).
|
|
1049
|
+
owner = getattr(draw_state, "_tint_edit_key", None) == view_id
|
|
1050
|
+
picker_name = f"color_picker{view_id}"
|
|
1051
|
+
# The picker hangs off the HEADER row, so its parent_window is the
|
|
1052
|
+
# enclosing WINDOW (what draw_tuple's shared ds has), never the host
|
|
1053
|
+
# itself: a collapsed host reads abs_closed, and end_frame discards
|
|
1054
|
+
# any nested window under an abs_closed parent - the picker opened on
|
|
1055
|
+
# a collapsed item's header closes on its first open.
|
|
1056
|
+
anchor = _popover_anchor(draw_state)
|
|
1057
|
+
picker_ds = None
|
|
1058
|
+
if owner:
|
|
1059
|
+
for nested in Melty.root_draw_states.get(anchor.id, ()):
|
|
1060
|
+
if getattr(nested, "name", None) == picker_name:
|
|
1061
|
+
picker_ds = nested
|
|
1062
|
+
break
|
|
1063
|
+
is_open = owner and Melty.popover_focused_ds is not None and (
|
|
1064
|
+
Melty.popover_focused_ds is draw_state
|
|
1065
|
+
or Melty.popover_focused_ds is picker_ds)
|
|
1066
|
+
if draw_state.on_action("left_mouse_down", view_id=view_id, rect=rect,
|
|
1067
|
+
priority_delta=priority_delta) is not None:
|
|
1068
|
+
if is_open:
|
|
1069
|
+
Melty.popover_focused_ds = None
|
|
1070
|
+
else:
|
|
1071
|
+
Melty.popover_focused_ds = draw_state
|
|
1072
|
+
draw_state._tint_edit_key = view_id
|
|
1073
|
+
Melty._popover_open_frame = Melty.frame_count # grace the opening click
|
|
1074
|
+
owner = True
|
|
1075
|
+
is_open = not is_open
|
|
1076
|
+
draw_state.invalidate()
|
|
1077
|
+
request_render()
|
|
1078
|
+
if not owner:
|
|
1079
|
+
return False, input_value
|
|
1080
|
+
|
|
1081
|
+
# ---- the picker popover: drawn while owned, closed once not open ----
|
|
1082
|
+
from meltygui.core.cache.invalidation_tracker import Note
|
|
1083
|
+
_info = (info() if callable(info) else info) if is_open else None
|
|
1084
|
+
picker_h = color_picker_height(4, bool(_info), has_owner=view_owner is not None)
|
|
1085
|
+
# Anchor under the chip; flip up past the display bottom (draw_tuple).
|
|
1086
|
+
_pop_y = color_picker_top_offset()
|
|
1087
|
+
_disp_w, _disp_h = imgui.get_io().display_size
|
|
1088
|
+
if y + size + _pop_y + picker_h > _disp_h - 10:
|
|
1089
|
+
_pop_y = -(picker_h + 38)
|
|
1090
|
+
# Flip LEFT the same way: a chip at a row's right edge (the file
|
|
1091
|
+
# listing's tint picker) cannot open off the display's right side, so
|
|
1092
|
+
# anchor the picker off the chip's right edge instead of its left.
|
|
1093
|
+
_pop_x = 0
|
|
1094
|
+
picker_w = color_picker_width()
|
|
1095
|
+
residual_tab = (view_owner is not None and Toggles.dynamic_styles and picker_ds is not None
|
|
1096
|
+
and any(isinstance(state, ColorPickerState) and state.tab == "residuals"
|
|
1097
|
+
for state in picker_ds.misc.values()))
|
|
1098
|
+
if residual_tab:
|
|
1099
|
+
picker_w = max(picker_w, 520)
|
|
1100
|
+
picker_h = max(picker_h, 560)
|
|
1101
|
+
_pop_y = color_picker_top_offset()
|
|
1102
|
+
if y + size + _pop_y + picker_h > _disp_h - 10:
|
|
1103
|
+
_pop_y = -(picker_h + 38)
|
|
1104
|
+
if x + picker_w > _disp_w - 10:
|
|
1105
|
+
_pop_x = -(picker_w - size)
|
|
1106
|
+
imgui.set_cursor_screen_pos((x, y + size))
|
|
1107
|
+
color_changed, new_color, picker_ds = draw_color_picker(
|
|
1108
|
+
input_value, name=picker_name, closed=not is_open,
|
|
1109
|
+
window_pos=(_pop_x, _pop_y), info=_info, parent_window=anchor,
|
|
1110
|
+
owner=view_owner,
|
|
1111
|
+
width=picker_w, height=picker_h, mode=Modes.POPOVER,
|
|
1112
|
+
return_extras=True)
|
|
1113
|
+
|
|
1114
|
+
imgui.same_line(spacing=0)
|
|
1115
|
+
if is_open and Melty.popover_focused_ds is draw_state:
|
|
1116
|
+
# Hand the slot from the host to the picker window now that it is
|
|
1117
|
+
# drawn (same frame, after the opening grace) - the draw_state the
|
|
1118
|
+
# wrapper hands back, never a lookup that can miss: while the slot
|
|
1119
|
+
# held the HOST, every click inside the host's window (the whole
|
|
1120
|
+
# editor for a side-bar chip) protected it, and the picker could not
|
|
1121
|
+
# be dismissed. From here the window lives on the slot alone -
|
|
1122
|
+
# Melty.popover_orphaned discards it at end_frame once the slot
|
|
1123
|
+
# moves away, whether or not this chip ever runs again.
|
|
1124
|
+
Melty.popover_focused_ds = picker_ds
|
|
1125
|
+
if not is_open:
|
|
1126
|
+
draw_state._tint_edit_key = None
|
|
1127
|
+
# The picker closed on the final frame: one DEEP cascade so every
|
|
1128
|
+
# nested tile under the host (depth-shifted bgs, headers, rows)
|
|
1129
|
+
# settles on it - the live edits below only reached the host's
|
|
1130
|
+
# direct children.
|
|
1131
|
+
Melty.cache.invalidate_up(draw_state._tile_id, force=True,
|
|
1132
|
+
max_depth=Toggles.Style.tint_edit_close_depth,
|
|
1133
|
+
note=Note(name="tint chip closed", reason=str(view_id),
|
|
1134
|
+
tint=(0.85, 0.75, 0.05)))
|
|
1135
|
+
request_render()
|
|
1136
|
+
return False, input_value
|
|
1137
|
+
if color_changed:
|
|
1138
|
+
previous = input_value
|
|
1139
|
+
from meltygui.core.styling.style import Style
|
|
1140
|
+
input_value = (Style(new_color, **previous.__getnewargs_ex__()[1])
|
|
1141
|
+
if isinstance(previous, Style) and new_color is not None
|
|
1142
|
+
else tuple(new_color) if new_color is not None else None)
|
|
1143
|
+
changed = True
|
|
1144
|
+
if setter is not None:
|
|
1145
|
+
from meltygui.state.core_undo import UndoManager
|
|
1146
|
+
UndoManager.record(draw_state, previous, input_value, setter=setter,
|
|
1147
|
+
key=view_id, label=str(view_id))
|
|
1148
|
+
# A live edit: the host (the bg its tint paints) and its direct
|
|
1149
|
+
# children re-render - a shallow cascade, because the picker fires
|
|
1150
|
+
# this every frame of a drag. The host's OWN tile is what
|
|
1151
|
+
# `draw_state.invalidate()` covered; its children are what it
|
|
1152
|
+
# missed (the tint shows through them). Deeper tiles wait for the
|
|
1153
|
+
# closing cascade above.
|
|
1154
|
+
Melty.cache.invalidate_up(draw_state._tile_id, force=True,
|
|
1155
|
+
max_depth=Toggles.Style.tint_edit_live_depth,
|
|
1156
|
+
note=Note(name="tint chip edit", reason=str(view_id),
|
|
1157
|
+
tint=(0.85, 0.75, 0.05)))
|
|
1158
|
+
request_render()
|
|
1159
|
+
if any(k == glfw.KEY_ESCAPE for k, _ in Core.melty.frame_key_events):
|
|
1160
|
+
Melty.popover_focused_ds = None
|
|
1161
|
+
request_render()
|
|
1162
|
+
# Keep the frames coming while a slider/square drag is live - the
|
|
1163
|
+
# picker is use_cache=False, so a frame is all it needs; the host's
|
|
1164
|
+
# tiles are triggered by the change branch above, never a frame.
|
|
1165
|
+
if Melty.imgui_any_item_active or imgui.is_mouse_down(0):
|
|
1166
|
+
request_render()
|
|
1167
|
+
return changed, input_value
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
@render_func(is_default_for=(types.MappingProxyType), shadow=False, show_bg=False, show_add_delete=False,
|
|
1171
|
+
with_header=draw_header)
|
|
1172
|
+
def draw_mapping_proxy(input_value):
|
|
1173
|
+
# To list first, then back to mapping proxy
|
|
1174
|
+
try:
|
|
1175
|
+
dict_values = dict(input_value)
|
|
1176
|
+
changed, new_dict = draw_collection(dict_values, show_bg=False, indent_size=0, show_header=False,
|
|
1177
|
+
show_add_delete=False)
|
|
1178
|
+
if changed:
|
|
1179
|
+
return True, types.MappingProxyType(new_dict)
|
|
1180
|
+
|
|
1181
|
+
except Exception as e:
|
|
1182
|
+
imgui.text(f"Error converting MappingProxyType to dict: {e}")
|
|
1183
|
+
return False, input_value
|
|
1184
|
+
|
|
1185
|
+
return changed, input_value
|