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,1665 @@
|
|
|
1
|
+
"""The set/get-anywhere API — read and write a view parameter at whichever
|
|
2
|
+
input source is actually DRIVING it (signature default, caller kwarg, mode
|
|
3
|
+
entry, @defaults decoration, class var, override comment, instance attr...).
|
|
4
|
+
|
|
5
|
+
`SourcePriority` is the hardcoded ranking of those sources; `_driving_source`
|
|
6
|
+
is the single pick every entry point shares, so `get_source_for`,
|
|
7
|
+
`from_anywhere`, `anywhere_value` and `set_anywhere` can never disagree.
|
|
8
|
+
The source registry itself is collected by `collect_input_sources`
|
|
9
|
+
(new_core_view) — imported lazily inside `_sources_for` to keep this module
|
|
10
|
+
free of an import cycle with the view code that calls it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
16
|
+
|
|
17
|
+
# We want to use the order of the tab elements as a way of determining the
|
|
18
|
+
# source of the input. From the function's perspective it just has parameters
|
|
19
|
+
# injected. Melty is routing data from many different sources and does its
|
|
20
|
+
# best to pick sane defaults when there are multiple sources. Which source
|
|
21
|
+
# takes priority depends on how the code is written, and the code may change.
|
|
22
|
+
# Rather than trying to infer the priority, we're just hard coding it here -
|
|
23
|
+
# edit the order of the elements to change the runtime behavior of meltygui.
|
|
24
|
+
# (Below is a rough memory of which sources take priority; reorder freely.)
|
|
25
|
+
class SourcePriority(Enum):
|
|
26
|
+
LIVE_COMMENT = 0 # the `# [tint=...]` override comment - the
|
|
27
|
+
# wrapper SPLATS it over kwargs after every
|
|
28
|
+
# other kwargs merges, so at runtime it
|
|
29
|
+
# beats @defaults, callers, and codecs alike
|
|
30
|
+
MODE = 1
|
|
31
|
+
WINDOW_DECORATION = 2 # @window(...) on the func or class, and
|
|
32
|
+
# @glfw_window(...) on the func - outranks
|
|
33
|
+
# @defaults (the window kwargs drive the
|
|
34
|
+
# window that renders the value)
|
|
35
|
+
CALLER = 3 # call-site kwargs - EXPLICITLY passed, so in
|
|
36
|
+
# the wrapper's gauntlet they beat every
|
|
37
|
+
# injected default layer, @defaults included
|
|
38
|
+
# (verified live: draw_code_tabs_and_cache's
|
|
39
|
+
# child_kwargs= wins over the GeneralParse
|
|
40
|
+
# class @defaults). DEPTH is the natural
|
|
41
|
+
# tiebreaker (see _source_priority) - no
|
|
42
|
+
# CALLER_0/CALLER_1 members needed
|
|
43
|
+
MODE_CHILD_KWARGS = 4 # child_kwargs={...} inside the PARENT's mode
|
|
44
|
+
# entry (Modes.NEW_CODE). Same merge into the
|
|
45
|
+
# child call as CHILD_KWARGS below. but on the
|
|
46
|
+
# parent it's the MODE setting child_kwargs -
|
|
47
|
+
# and mode outranks the parent's caller and
|
|
48
|
+
# @defaults - so it wins the dict whenever the
|
|
49
|
+
# mode entry carries one
|
|
50
|
+
CHILD_KWARGS = 5 # the PARENT view's child_kwargs={...} - an
|
|
51
|
+
# explicit call kwarg on the child, so
|
|
52
|
+
# caller-strength: above @defaults for the
|
|
53
|
+
# same reason as CALLER; the dict itself
|
|
54
|
+
# lives at any of the parent's OWN sources,
|
|
55
|
+
# resolved via from_anywhere("child_kwargs",
|
|
56
|
+
# parent)
|
|
57
|
+
AT_DEFAULT_CODE_TYPE = 6 # @defaults on a PARSED class in the value tree
|
|
58
|
+
AT_DEFAULT_OBJ_TYPE = 7 # @defaults on the value's runtime class
|
|
59
|
+
DECORATION = 8 # @render_func(...) kwargs on the view func
|
|
60
|
+
INSTANCE_ATTR = 9 # whitelisted instance attr on the value object
|
|
61
|
+
# (core_render.OBJ_ATTR_PARAMS, e.g.
|
|
62
|
+
# Lora.tint) - injected via setdefault, so
|
|
63
|
+
# every kwargs-borne source above wins
|
|
64
|
+
CLASS_VAR = 10 # class-body assignment on the value's class -
|
|
65
|
+
# reaches the view through the SAME getattr
|
|
66
|
+
# injection as INSTANCE_ATTR, where the
|
|
67
|
+
# instance var shadows it (Python lookup
|
|
68
|
+
# order), so it ranks below the instance
|
|
69
|
+
CODEC = 11 # the active codec's render_kwargs - the
|
|
70
|
+
# wrapper's lowest kwargs MERGE layer
|
|
71
|
+
# (core_render `render_kwargs | ...`);
|
|
72
|
+
# loses to every getattr-injected source
|
|
73
|
+
# above, drives only the layers below
|
|
74
|
+
RENDER_FUNC = 12 # signature defaults (def draw_x(speed=3)) —
|
|
75
|
+
# the WEAKEST code source: Python only applies
|
|
76
|
+
# a default when the name is absent from
|
|
77
|
+
# kwargs entirely, so every kwargs-borne
|
|
78
|
+
# source above wins. Ranked at 2 it used to
|
|
79
|
+
# mask caller/child_kwargs as _setting_source
|
|
80
|
+
# for any signature-defaulted param
|
|
81
|
+
# (syntax_highlight on draw_text), sending
|
|
82
|
+
# set_anywhere's ds_fallback when a draw_state
|
|
83
|
+
# write the explicit kwarg then shadowed
|
|
84
|
+
DRAW_STATE = 13 # the draw_state's own attrs (ds.tint - the
|
|
85
|
+
# style cascade's lowest fallback, persisted
|
|
86
|
+
# with window state) - the default: drives
|
|
87
|
+
# it when nothing else sets the param
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# The tab's kind captions → priority. Kinds are the single naming source
|
|
91
|
+
# (collect_input_sources stamps them); this is just the lookup.
|
|
92
|
+
_KIND_TO_PRIORITY = {
|
|
93
|
+
"mode": SourcePriority.MODE,
|
|
94
|
+
"signature": SourcePriority.RENDER_FUNC,
|
|
95
|
+
"code class default": SourcePriority.AT_DEFAULT_CODE_TYPE,
|
|
96
|
+
"class default": SourcePriority.AT_DEFAULT_OBJ_TYPE,
|
|
97
|
+
"code comment": SourcePriority.LIVE_COMMENT,
|
|
98
|
+
"class var": SourcePriority.CLASS_VAR,
|
|
99
|
+
"decoration": SourcePriority.DECORATION,
|
|
100
|
+
"window decoration": SourcePriority.WINDOW_DECORATION, # @window on the func
|
|
101
|
+
"class decoration": SourcePriority.WINDOW_DECORATION, # @window on the class
|
|
102
|
+
"glfw window decoration": SourcePriority.WINDOW_DECORATION, # @glfw_window on the func
|
|
103
|
+
"instance attr": SourcePriority.INSTANCE_ATTR,
|
|
104
|
+
"attr default": SourcePriority.AT_DEFAULT_OBJ_TYPE, # @defaults(attr="x", ...)
|
|
105
|
+
"child kwargs": SourcePriority.CHILD_KWARGS,
|
|
106
|
+
"mode child kwargs": SourcePriority.MODE_CHILD_KWARGS,
|
|
107
|
+
"codec": SourcePriority.CODEC,
|
|
108
|
+
"draw state": SourcePriority.DRAW_STATE,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _source_priority(kind):
|
|
113
|
+
"""Sort key for a source's kind caption: (SourcePriority value, depth).
|
|
114
|
+
Caller rows ("caller", "caller +1", ...) share one enum member with their
|
|
115
|
+
walk depth as the tiebreaker, so priority never hardcodes caller depth."""
|
|
116
|
+
if isinstance(kind, str) and kind.startswith("caller"):
|
|
117
|
+
depth = int(kind.split("+", 1)[1]) if "+" in kind else 0
|
|
118
|
+
return (SourcePriority.CALLER.value, depth)
|
|
119
|
+
p = _KIND_TO_PRIORITY.get(kind)
|
|
120
|
+
return (p.value, 0) if p is not None else (len(SourcePriority) + 1, 0)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _sources_for(draw_state, class_to_show=None):
|
|
124
|
+
"""collect_input_sources for a TARGET draw_state outside the context menu:
|
|
125
|
+
the host-caching ContextMenuState rides on the draw_state so repeat calls
|
|
126
|
+
(a drag writing per-release, say) reuse the parsed hosts. class_to_show
|
|
127
|
+
defaults to the value's runtime class, so the @defaults/class-var rows
|
|
128
|
+
resolve the same way here as under the context menu."""
|
|
129
|
+
from meltygui.state.inspection_state import ContextMenuState
|
|
130
|
+
from meltygui.core.rendering.render_dispatch import collect_input_sources
|
|
131
|
+
if not draw_state._call_site_captured and not draw_state._call_site_requested:
|
|
132
|
+
draw_state._call_site_requested = True
|
|
133
|
+
draw_state.invalidate_up(max_depth=6)
|
|
134
|
+
if class_to_show is None:
|
|
135
|
+
raw = draw_state._raw_input_value
|
|
136
|
+
# A view can SHOW a class itself (a @window class like Toggles):
|
|
137
|
+
# its class rows (@window(cls) / class vars / @defaults) anchor on
|
|
138
|
+
# THAT class - `raw.__class__` would hand them `type`.
|
|
139
|
+
class_to_show = raw if isinstance(raw, type) else getattr(raw, "__class__", None)
|
|
140
|
+
cm_state = getattr(draw_state, "_sa_cm_state", None)
|
|
141
|
+
if cm_state is None:
|
|
142
|
+
cm_state = ContextMenuState()
|
|
143
|
+
draw_state._sa_cm_state = cm_state
|
|
144
|
+
srcs = collect_input_sources(draw_state, cm_state, class_to_show)
|
|
145
|
+
# Keep-alive + revive: code hosts are EVICTABLE - the idle sweep pops them
|
|
146
|
+
# off the draw_state once their consumers close (RenderHost.sweep). A
|
|
147
|
+
# swept host still passes writes: the tree stays dirty but nothing runs
|
|
148
|
+
# its chain_out, so the write silently never fires (set_anywhere writing
|
|
149
|
+
# into a dirty orphan was the first field failure). notify_on_change is
|
|
150
|
+
# the sanctioned per-frame pulse: stamps liveness AND re-registers a
|
|
151
|
+
# swept host that the upstream straining. Same thing the input
|
|
152
|
+
# tab does at its end with its own draw_state.
|
|
153
|
+
for _h in (cm_state.render_func_dict, getattr(cm_state, "decoration_dict", None), cm_state.class_dict,
|
|
154
|
+
cm_state.mode_dict,
|
|
155
|
+
*[dh for (_sh, dh) in (cm_state.call_site_hosts or [])]):
|
|
156
|
+
if _h is not None:
|
|
157
|
+
_h.notify_on_change(draw_state)
|
|
158
|
+
return srcs
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
_UNSET = object()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Source layers that OUTRANK the draw_state/auto-state layer in the wrapper's
|
|
165
|
+
# kwargs gauntlet: their-set values (mode overrides, caller kwargs,
|
|
166
|
+
# child_kwargs, the comment splat, @window decoration kwargs). A write to a
|
|
167
|
+
# param driven by one of these must edit the source or an edit would be
|
|
168
|
+
# shadowed at runtime. Everything below (signature defaults, @render_func
|
|
169
|
+
# kwargs, @defaults, class vars, instance attrs, codec render_kwargs) merges
|
|
170
|
+
# BENEATH auto-state - a plain draw_state write both takes effect immediately
|
|
171
|
+
# and persists (auto_params), so it is the default write target.
|
|
172
|
+
_ABOVE_DRAW_STATE = {
|
|
173
|
+
SourcePriority.LIVE_COMMENT.value, SourcePriority.MODE.value,
|
|
174
|
+
SourcePriority.WINDOW_DECORATION.value, SourcePriority.CALLER.value,
|
|
175
|
+
SourcePriority.MODE_CHILD_KWARGS.value, SourcePriority.CHILD_KWARGS.value,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ── source speed: fast vs slow writers ──────────────────────────────────────
|
|
180
|
+
# A source's SPEED is how a write becomes LIVE. Fast sources apply in place -
|
|
181
|
+
# a draw_state field, a live instance attr, the codec's in-memory
|
|
182
|
+
# render_kwargs - one setattr/item-set and the next frame reads it. Slow
|
|
183
|
+
# sources are code-backed: the write lands in a parse dict and only becomes
|
|
184
|
+
# live through the debounced chain_out → save + recompile/hotswap trip. That
|
|
185
|
+
# trip is fine per click, but a DRAG writes every frame, and one trip per
|
|
186
|
+
# frame craters the frame rate (120 to ~30fps measured on the voxel camera
|
|
187
|
+
# driving a `# [cam_brightness=...]` comment). So while a left/right/middle
|
|
188
|
+
# mouse button is held - or a scroll is in flight (no release event, so
|
|
189
|
+
# "recent tick within the same interval") - slow writes DEFER: the value
|
|
190
|
+
# lands in the in-flight display cache (_sa_pending - anywhere_value/
|
|
191
|
+
# locate_* reads already prefer it) plus a per-ds deferred dict, and the
|
|
192
|
+
# real set_anywhere runs ONCE, when input goes quiet, via
|
|
193
|
+
# flush_deferred_writes (called per frame by draw_main).
|
|
194
|
+
_FAST_PRIORITIES = {
|
|
195
|
+
SourcePriority.DRAW_STATE.value, SourcePriority.INSTANCE_ATTR.value,
|
|
196
|
+
SourcePriority.CODEC.value,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def source_is_slow(kind):
|
|
201
|
+
"""True when a write to a source of this kind round-trips through
|
|
202
|
+
save/recompile rather than applying in place."""
|
|
203
|
+
return _source_priority(kind)[0] not in _FAST_PRIORITIES
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ── source precision: low-precision writers ─────────────────────────────────
|
|
207
|
+
# Some sources store floats as literal TEXT in code, where full float64
|
|
208
|
+
# precision is noise (`# [cam_x=0.30000000000000004]`). Kinds marked here get
|
|
209
|
+
# their float writes rounded to LOW_PRECISION_DECIMALS before landing in the
|
|
210
|
+
# source; the FULL-precision value parks on the draw_state's _sa_precise
|
|
211
|
+
# overlay, which anywhere_value serves for as long as the live value is still
|
|
212
|
+
# a rounding of it - the view sees the precise float (a voxel camera is
|
|
213
|
+
# accumulating sub-4dp drag deltas) while the code keeps a readable one. A
|
|
214
|
+
# write that ONLY moves digits beyond the cap skips the save/recompile trip
|
|
215
|
+
# entirely. The overlay is in-memory: an app reload sees the rounded source
|
|
216
|
+
# value (accepted trade-off). Mark a new source by adding its kind here.
|
|
217
|
+
LOW_PRECISION_DECIMALS = 4
|
|
218
|
+
_LOW_PRECISION_KINDS = {"code comment", "class var"}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def source_is_low_precision(kind):
|
|
222
|
+
"""True when float writes to a source of this kind are rounded to
|
|
223
|
+
LOW_PRECISION_DECIMALS decimal places (see the block comment above)."""
|
|
224
|
+
return kind in _LOW_PRECISION_KINDS
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _round_low_precision(value):
|
|
228
|
+
"""`value` rounded to LOW_PRECISION_DECIMALS — elementwise for float
|
|
229
|
+
sequences; anything without a float passes through untouched."""
|
|
230
|
+
if isinstance(value, float):
|
|
231
|
+
return round(value, LOW_PRECISION_DECIMALS)
|
|
232
|
+
if (isinstance(value, (tuple, list))
|
|
233
|
+
and any(isinstance(v, float) for v in value)):
|
|
234
|
+
seq = tuple if isinstance(value, tuple) else list
|
|
235
|
+
return seq(_round_low_precision(v) for v in value)
|
|
236
|
+
return value
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _is_rounding_of(live, full):
|
|
240
|
+
"""True when `live` is `full` rounded at LOW_PRECISION_DECIMALS or
|
|
241
|
+
coarser — i.e. the source still holds OUR write (possibly re-capped by
|
|
242
|
+
the source's float formatter), not an external edit."""
|
|
243
|
+
try:
|
|
244
|
+
if live is full or bool(live == full):
|
|
245
|
+
return True
|
|
246
|
+
except Exception:
|
|
247
|
+
return False
|
|
248
|
+
if (isinstance(full, float) and isinstance(live, (int, float))
|
|
249
|
+
and not isinstance(live, bool)):
|
|
250
|
+
return any(live == round(full, dp)
|
|
251
|
+
for dp in range(LOW_PRECISION_DECIMALS + 1))
|
|
252
|
+
if (isinstance(full, (tuple, list)) and isinstance(live, (tuple, list))
|
|
253
|
+
and len(full) == len(live)):
|
|
254
|
+
return all(_is_rounding_of(lv, fv) for lv, fv in zip(live, full))
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _stamp_precise(attr_name, value, draw_state):
|
|
259
|
+
"""Park the full-precision value a low-precision source write rounded
|
|
260
|
+
away; anywhere_value serves it over the rounded live value."""
|
|
261
|
+
precise = getattr(draw_state, "_sa_precise", None)
|
|
262
|
+
if precise is None:
|
|
263
|
+
precise = {}
|
|
264
|
+
draw_state._sa_precise = precise
|
|
265
|
+
precise[attr_name] = value
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _precise_or_live(attr_name, draw_state, live):
|
|
269
|
+
"""The full-precision overlay for `attr_name` while the live value is
|
|
270
|
+
still a rounding of it; once the source moves elsewhere (an external
|
|
271
|
+
edit, another driver) the overlay drops and live reads resume."""
|
|
272
|
+
precise = getattr(draw_state, "_sa_precise", None)
|
|
273
|
+
if precise is None or attr_name not in precise:
|
|
274
|
+
return live
|
|
275
|
+
full = precise[attr_name]
|
|
276
|
+
if _is_rounding_of(live, full):
|
|
277
|
+
return full
|
|
278
|
+
del precise[attr_name]
|
|
279
|
+
return live
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
_DRAG_BUTTONS = ("left_mouse", "right_mouse", "middle_mouse")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _drag_active():
|
|
286
|
+
"""A mouse button is currently held — the window during which slow-source
|
|
287
|
+
writes defer. Level state from the input handler (not per-frame events),
|
|
288
|
+
so it can't miss between drag events."""
|
|
289
|
+
h = getattr(Core.melty, "event_handler", None)
|
|
290
|
+
return h is not None and any(h.is_down(b) for b in _DRAG_BUTTONS)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# Scroll has no release: a gesture is "over" once no tick has arrived for the
|
|
294
|
+
# quiet window. Wheel notches land ~100ms+ apart, so the window must span the
|
|
295
|
+
# inter-tick gap or every notch would flush its own save/recompile trip.
|
|
296
|
+
_SCROLL_QUIET_FRAMES = 24
|
|
297
|
+
# Frame of the last seen scroll event - module registry, survives hotswap.
|
|
298
|
+
_last_scroll_frame = globals().get("_last_scroll_frame", [-1_000_000])
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _note_scroll():
|
|
302
|
+
"""Stamp the frame when a scroll event is in flight. events_by_type is
|
|
303
|
+
drained at frame start (begin_frame), so the flush call at the top of
|
|
304
|
+
draw_main sees this frame's ticks before any view's handler writes."""
|
|
305
|
+
if "scroll_y_changed" in (Core.melty.events_by_type or {}):
|
|
306
|
+
_last_scroll_frame[0] = Core.melty.frame_count
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _scroll_recent():
|
|
310
|
+
return Core.melty.frame_count - _last_scroll_frame[0] < _SCROLL_QUIET_FRAMES
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _input_busy():
|
|
314
|
+
"""True while a gesture that should hold off slow writes is in flight —
|
|
315
|
+
a held drag button, or a scroll within its quiet window."""
|
|
316
|
+
return _drag_active() or _scroll_recent()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# Draw_states holding deferred writes, flushed on release. Module registry -
|
|
320
|
+
# survives hotswap (re-exec reuses the existing global).
|
|
321
|
+
_DEFERRED_DS = globals().get("_DEFERRED_DS", set())
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _stamp_pending(attr_name, value, draw_state):
|
|
325
|
+
"""In-flight display cache (see anywhere_value): remember what was set and
|
|
326
|
+
what the live value was when the set was issued. Re-sets during a drag
|
|
327
|
+
refresh the UI value but KEEP the original live_at_set — live hasn't moved
|
|
328
|
+
yet, and that's the baseline whose change means "the trip landed"."""
|
|
329
|
+
live = (draw_state._kwargs or {}).get(attr_name, _UNSET)
|
|
330
|
+
if attr_name == "view_func":
|
|
331
|
+
live = getattr(draw_state, "_wrapper", None) or draw_state._view_func
|
|
332
|
+
pending = getattr(draw_state, "_sa_pending", None)
|
|
333
|
+
if pending is None:
|
|
334
|
+
pending = {}
|
|
335
|
+
draw_state._sa_pending = pending
|
|
336
|
+
prior = pending.get(attr_name)
|
|
337
|
+
# _UNSET normalizes to None: anywhere_value reads live with a None
|
|
338
|
+
# default, and the baselines must compare equal until the trip lands.
|
|
339
|
+
live_at_set = prior[1] if prior is not None else (
|
|
340
|
+
None if live is _UNSET else live)
|
|
341
|
+
pending[attr_name] = (value, live_at_set)
|
|
342
|
+
getattr(draw_state, "_sa_verify", {}).pop(attr_name, None)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ── Live write: the in-memory twin of the recompile ─────────────────────────
|
|
346
|
+
# A write to a DECORATOR source (`@window(tint=...)`, `@glfw_window(tint=...)`,
|
|
347
|
+
# `@render_func(tint=...)`) only became visible once the mouse-up recompile
|
|
348
|
+
# re-ran the decorator and the reconcile installed its fresh kwargs. Every
|
|
349
|
+
# render reads those kwargs from their place - the same place the recompile
|
|
350
|
+
# replaces - so a drag can write the value straight there and the window
|
|
351
|
+
# follows the picker live; the recompile on release lands the same value
|
|
352
|
+
# and nothing moves. Each case: kind caption → apply(attr, value,
|
|
353
|
+
# draw_state, class_to_show) → True when a registration was hit.
|
|
354
|
+
def _apply_window_decoration(attr_name, value, draw_state, class_to_show):
|
|
355
|
+
"""`Melty.annotated_window_classes[name] = (obj, kwargs)`: draw_main
|
|
356
|
+
copies `kwargs` into the window's call every frame. Matched on the
|
|
357
|
+
registered OBJECT (the view's wrapper or raw for a @window func, the
|
|
358
|
+
shown class for @window(cls)), never on the key — a `name=` registration
|
|
359
|
+
keys by that name. The dict is looked up per call: the recompile's
|
|
360
|
+
reconcile swaps in a fresh one."""
|
|
361
|
+
registry = getattr(Core.melty, "annotated_window_classes", None)
|
|
362
|
+
if not isinstance(registry, dict):
|
|
363
|
+
return False
|
|
364
|
+
view_func = (draw_state._kwargs or {}).get("_view_func_origin", getattr(draw_state, "_view_func", None))
|
|
365
|
+
owners = {id(o) for o in (view_func, _raw_of(view_func), class_to_show) if o is not None}
|
|
366
|
+
hit = False
|
|
367
|
+
for entry in registry.values():
|
|
368
|
+
if isinstance(entry, tuple) and len(entry) == 2 and id(entry[0]) in owners \
|
|
369
|
+
and isinstance(entry[1], dict):
|
|
370
|
+
entry[1][attr_name] = value
|
|
371
|
+
hit = True
|
|
372
|
+
return hit
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _apply_glfw_window_decoration(attr_name, value, draw_state, class_to_show):
|
|
376
|
+
"""`app._ROOTS[i] = (fn, config)`: the root body reads
|
|
377
|
+
`config['view_kwargs']` every frame (app._root_body), and a re-run
|
|
378
|
+
decorator updates that same dict in place."""
|
|
379
|
+
import meltygui.core.runtime.app as app
|
|
380
|
+
view_func = (draw_state._kwargs or {}).get("_view_func_origin", getattr(draw_state, "_view_func", None))
|
|
381
|
+
owners = {id(o) for o in (view_func, _raw_of(view_func)) if o is not None}
|
|
382
|
+
hit = False
|
|
383
|
+
for fn, config in app._ROOTS:
|
|
384
|
+
if id(fn) in owners or id(_raw_of(fn)) in owners:
|
|
385
|
+
kwargs = config.get('view_kwargs')
|
|
386
|
+
if not isinstance(kwargs, dict):
|
|
387
|
+
kwargs = config['view_kwargs'] = {}
|
|
388
|
+
kwargs[attr_name] = value
|
|
389
|
+
hit = True
|
|
390
|
+
return hit
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _apply_render_func_decoration(attr_name, value, draw_state, class_to_show):
|
|
394
|
+
"""`@render_func(tint=…)` lives in the wrapper's closure: the `o_kwargs`
|
|
395
|
+
dict (merged under every call's kwargs) and, for the tint, the
|
|
396
|
+
`_decoration_tint` cell the wrapper compares identities against. The
|
|
397
|
+
hotswap's _transfer_wrapper_state later overwrites both cells with the
|
|
398
|
+
freshly-decorated values — the same ones."""
|
|
399
|
+
wrapper = getattr(draw_state, "_view_func", None)
|
|
400
|
+
code = getattr(wrapper, "__code__", None)
|
|
401
|
+
cells = getattr(wrapper, "__closure__", None)
|
|
402
|
+
if code is None or cells is None:
|
|
403
|
+
return False
|
|
404
|
+
names = code.co_freevars
|
|
405
|
+
if "o_kwargs" not in names:
|
|
406
|
+
return False
|
|
407
|
+
o_kwargs = cells[names.index("o_kwargs")].cell_contents
|
|
408
|
+
if not isinstance(o_kwargs, dict):
|
|
409
|
+
return False
|
|
410
|
+
o_kwargs[attr_name] = value
|
|
411
|
+
if attr_name == "tint":
|
|
412
|
+
# The identity the wrapper's `_tint_as_decoration` compares against
|
|
413
|
+
# sits in that nested helper's own closure (a cell of the wrapper).
|
|
414
|
+
for cell in _decoration_tint_cells(wrapper):
|
|
415
|
+
cell.cell_contents = value
|
|
416
|
+
return True
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _decoration_tint_cells(wrapper):
|
|
420
|
+
"""The `_decoration_tint` cells reachable from a render_func wrapper:
|
|
421
|
+
on the wrapper itself or on a nested function it closes over."""
|
|
422
|
+
found = []
|
|
423
|
+
seen = set()
|
|
424
|
+
stack = [wrapper]
|
|
425
|
+
while stack:
|
|
426
|
+
fn = stack.pop()
|
|
427
|
+
if id(fn) in seen:
|
|
428
|
+
continue
|
|
429
|
+
seen.add(id(fn))
|
|
430
|
+
code = getattr(fn, "__code__", None)
|
|
431
|
+
cells = getattr(fn, "__closure__", None)
|
|
432
|
+
if code is None or not cells:
|
|
433
|
+
continue
|
|
434
|
+
for name, cell in zip(code.co_freevars, cells):
|
|
435
|
+
try:
|
|
436
|
+
content = cell.cell_contents
|
|
437
|
+
except ValueError:
|
|
438
|
+
continue
|
|
439
|
+
if name == "_decoration_tint":
|
|
440
|
+
found.append(cell)
|
|
441
|
+
elif callable(content) and getattr(content, "__closure__", None):
|
|
442
|
+
stack.append(content)
|
|
443
|
+
return found
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _raw_of(func):
|
|
447
|
+
try:
|
|
448
|
+
import inspect
|
|
449
|
+
return inspect.unwrap(func) if func is not None else None
|
|
450
|
+
except Exception:
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
_LIVE_APPLY = {
|
|
455
|
+
"window decoration": _apply_window_decoration,
|
|
456
|
+
"class decoration": _apply_window_decoration,
|
|
457
|
+
"glfw window decoration": _apply_glfw_window_decoration,
|
|
458
|
+
"decoration": _apply_render_func_decoration,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def live_apply(attr_name, value, draw_state, kind, class_to_show=None):
|
|
463
|
+
"""Write `value` into the in-memory registration a source of `kind`
|
|
464
|
+
feeds the render loop from (_LIVE_APPLY), so the view shows it on the
|
|
465
|
+
next frame without waiting for the save + recompile. False for kinds
|
|
466
|
+
that only become live through a recompile (signature, @defaults,
|
|
467
|
+
callers, mode) — those keep the display cache alone."""
|
|
468
|
+
apply = _LIVE_APPLY.get(kind)
|
|
469
|
+
if apply is None:
|
|
470
|
+
return False
|
|
471
|
+
try:
|
|
472
|
+
hit = apply(attr_name, value, draw_state, class_to_show)
|
|
473
|
+
except Exception as e:
|
|
474
|
+
print(f"[live_apply] {kind} {attr_name}: {e}")
|
|
475
|
+
return False
|
|
476
|
+
if hit:
|
|
477
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
478
|
+
request_render()
|
|
479
|
+
return hit
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _defer_write(attr_name, value, draw_state, class_to_show, source=None, kind=None):
|
|
483
|
+
"""Park a slow-source write for the duration of the drag: the display
|
|
484
|
+
cache serves reads immediately; flush_deferred_writes runs the real
|
|
485
|
+
set_anywhere on release."""
|
|
486
|
+
deferred = getattr(draw_state, "_sa_deferred", None)
|
|
487
|
+
if deferred is None:
|
|
488
|
+
deferred = {}
|
|
489
|
+
draw_state._sa_deferred = deferred
|
|
490
|
+
deferred[attr_name] = (value, class_to_show, source, kind)
|
|
491
|
+
_DEFERRED_DS.add(draw_state)
|
|
492
|
+
_stamp_pending(attr_name, value, draw_state)
|
|
493
|
+
live_apply(attr_name, value, draw_state, kind, class_to_show)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def flush_deferred_writes():
|
|
497
|
+
"""Per-frame (draw_main): once no drag button is held, run each parked
|
|
498
|
+
write through the normal set_anywhere — one slow trip per gesture, not
|
|
499
|
+
one per event. A no-op set-check when nothing is parked."""
|
|
500
|
+
_note_scroll()
|
|
501
|
+
if not _DEFERRED_DS:
|
|
502
|
+
return
|
|
503
|
+
if _input_busy():
|
|
504
|
+
# Rendering is event-driven: after the last scroll tick no further
|
|
505
|
+
# input arrives, so keep frames flowing until the quiet window expires
|
|
506
|
+
# and the deferred writes actually flush.
|
|
507
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
508
|
+
request_render()
|
|
509
|
+
return
|
|
510
|
+
for ds in list(_DEFERRED_DS):
|
|
511
|
+
_DEFERRED_DS.discard(ds)
|
|
512
|
+
deferred = getattr(ds, "_sa_deferred", None) or {}
|
|
513
|
+
items = list(deferred.items())
|
|
514
|
+
deferred.clear()
|
|
515
|
+
for attr_name, (value, class_to_show, source, _kind) in items:
|
|
516
|
+
set_anywhere(attr_name, value, ds, class_to_show=class_to_show,
|
|
517
|
+
allow_any=True, ds_fallback=True, source=source)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _pref_matches(pref, kind):
|
|
521
|
+
"""True when a source row's kind caption satisfies a preferred_source
|
|
522
|
+
flag — given either as a kind caption string ("code comment") or a
|
|
523
|
+
SourcePriority member (matched through the same priority table)."""
|
|
524
|
+
if isinstance(pref, SourcePriority):
|
|
525
|
+
return _source_priority(kind)[0] == pref.value
|
|
526
|
+
return kind == pref
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _preferred_source_for(draw_state):
|
|
530
|
+
"""The `preferred_source` flag riding this view's resolved kwargs, or an
|
|
531
|
+
enclosing window's — a spawning view (live view) stamps it on the value
|
|
532
|
+
windows it opens, and edits from anywhere in that subtree (params panel
|
|
533
|
+
rows, context menus) should land at the nominated source. Walks _parent
|
|
534
|
+
(self-loop root) then hops parent_window, same as other ancestor walks."""
|
|
535
|
+
node, hops = draw_state, 0
|
|
536
|
+
while node is not None and hops < 32:
|
|
537
|
+
pref = (getattr(node, "_kwargs", None) or {}).get("preferred_source")
|
|
538
|
+
if pref is not None:
|
|
539
|
+
return pref
|
|
540
|
+
parent = getattr(node, "_parent", None)
|
|
541
|
+
nxt = parent if parent is not None and parent is not node else None
|
|
542
|
+
if nxt is None:
|
|
543
|
+
pw = getattr(node, "parent_window", None)
|
|
544
|
+
nxt = pw if pw is not None and pw is not node else None
|
|
545
|
+
node = nxt
|
|
546
|
+
hops += 1
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def get_value_for_source(attr_name, input_source, draw_state, class_to_show=None):
|
|
551
|
+
"""(input_source, value) for `attr_name` as set by a specific
|
|
552
|
+
SourcePriority source on this view, or (input_source, None) when that
|
|
553
|
+
source doesn't set it. The same registry the input tab shows."""
|
|
554
|
+
srcs = _sources_for(draw_state, class_to_show)
|
|
555
|
+
for sname, sdict in srcs["sources"].items():
|
|
556
|
+
kind = srcs["kinds"].get(sname)
|
|
557
|
+
if _source_priority(kind)[0] == input_source.value and attr_name in sdict:
|
|
558
|
+
return input_source, sdict[attr_name]
|
|
559
|
+
return input_source, None
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _unset_value(v):
|
|
563
|
+
"""True when a stored value can't DRIVE a param: None (declared-unset) or
|
|
564
|
+
a fully transparent color (an alpha-0 4-tuple — the codec opt-out
|
|
565
|
+
convention; it renders nothing, so it must not claim the pick)."""
|
|
566
|
+
if v is None:
|
|
567
|
+
return True
|
|
568
|
+
return (isinstance(v, (tuple, list)) and len(v) >= 4
|
|
569
|
+
and isinstance(v[3], (int, float)) and not v[3])
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _setting_source(srcs, attr_name):
|
|
573
|
+
"""The highest-priority (SourcePriority order) WRITABLE source that
|
|
574
|
+
actually SETS `attr_name`, or None when no source does. Split out from
|
|
575
|
+
_driving_source so a caller can tell "some source holds this value" from
|
|
576
|
+
"nothing does, the pick is only the stamp-fallback"."""
|
|
577
|
+
sources, kinds = srcs["sources"], srcs["kinds"]
|
|
578
|
+
writable = set(srcs["writable"])
|
|
579
|
+
# `is not None`: a parse'd `param=None` (signature defaults, cleared
|
|
580
|
+
# kwargs) is DECLARED-UNSET - it must not claim driving over a source
|
|
581
|
+
# holding a real value (the signature's child_kwargs=None poisoned
|
|
582
|
+
# from_anywhere for every real setter below it).
|
|
583
|
+
candidates = [sname for sname in sources
|
|
584
|
+
if sname in writable
|
|
585
|
+
and not _unset_value(sources[sname].get(attr_name))]
|
|
586
|
+
if attr_name == "view_func" and srcs.get("view_func") is not None:
|
|
587
|
+
import inspect
|
|
588
|
+
from meltygui.core.input.view_selection import resolve_view_func
|
|
589
|
+
live = inspect.unwrap(resolve_view_func(srcs["view_func"]))
|
|
590
|
+
candidates = [s for s in candidates if not (getattr(sources[s], "direct", False)
|
|
591
|
+
or getattr(sources[s], "implicit_view_func", False))
|
|
592
|
+
or inspect.unwrap(resolve_view_func(sources[s][attr_name])) is live]
|
|
593
|
+
if not candidates:
|
|
594
|
+
return None
|
|
595
|
+
return min(candidates, key=lambda s: _source_priority(kinds.get(s)))
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _driving_source(srcs, attr_name):
|
|
599
|
+
"""The source name actually driving `attr_name`: the highest-priority
|
|
600
|
+
WRITABLE source that currently sets it, else the signature source as the
|
|
601
|
+
stamp-fallback, else None. Shared by get_source_for / from_anywhere /
|
|
602
|
+
set_anywhere so they can never disagree."""
|
|
603
|
+
target = _setting_source(srcs, attr_name)
|
|
604
|
+
if target is not None:
|
|
605
|
+
return target
|
|
606
|
+
kinds, writable = srcs["kinds"], set(srcs["writable"])
|
|
607
|
+
if attr_name == "view_func":
|
|
608
|
+
return next((s for s in srcs["sources"]
|
|
609
|
+
if kinds.get(s) == "draw state" and s in writable), None)
|
|
610
|
+
return next((s for s in srcs["sources"]
|
|
611
|
+
if kinds.get(s) == "signature" and s in writable), None)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def get_source_for(attr_name, draw_state, class_to_show=None):
|
|
615
|
+
"""Name of the source driving `attr_name` on this view (display label)."""
|
|
616
|
+
return _driving_source(_sources_for(draw_state, class_to_show), attr_name)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# Call sites in these framework locations are BAD default write targets - a
|
|
620
|
+
# kwarg stamped into view_collection's dispatch call, the render loop
|
|
621
|
+
# (end_frame / update_melty_windows / the studio's draw), or any core_view
|
|
622
|
+
# plumbing would restyle every view app-wide. Caller rows located here are
|
|
623
|
+
# skipped by default_write_source's pick - they stay visible and manually
|
|
624
|
+
# pickable in the info tab's dropdown.
|
|
625
|
+
_FRAMEWORK_CALLER_DIRS = ("/meltygui/rendering/", "/meltygui/views/",
|
|
626
|
+
"/meltygui/code/", "/meltygui/state/", "/meltygui/utils/",
|
|
627
|
+
"/meltygui/editor/")
|
|
628
|
+
_FRAMEWORK_CALLER_FILES = ("melty.py", "app.py", "surface.py", "background.py")
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _is_framework_caller(location):
|
|
632
|
+
"""True when a caller row's (file, line) sits inside the meltygui framework
|
|
633
|
+
— or is unknown, which must never be defaulted into either."""
|
|
634
|
+
if not location or not location[0]:
|
|
635
|
+
return True
|
|
636
|
+
p = str(location[0]).replace("\\", "/")
|
|
637
|
+
if "/meltygui/" in p:
|
|
638
|
+
return True
|
|
639
|
+
# Component packages built on the toolkit register their own folders.
|
|
640
|
+
from meltygui.core.runtime.extensions import framework_folders
|
|
641
|
+
if any(str(folder).replace("\\", "/") in p for folder in framework_folders()):
|
|
642
|
+
return True
|
|
643
|
+
if any(d in p for d in _FRAMEWORK_CALLER_DIRS):
|
|
644
|
+
return True
|
|
645
|
+
return ("/meltygui/" in p
|
|
646
|
+
and p.rsplit("/", 1)[-1] in _FRAMEWORK_CALLER_FILES)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def window_source_writable(srcs, target):
|
|
650
|
+
"""Automatic window gestures may always edit comments/local state.
|
|
651
|
+
|
|
652
|
+
Source-code changes require the GUI-editor toggle; framework dispatch
|
|
653
|
+
call sites are never an automatic target, even with that toggle enabled.
|
|
654
|
+
"""
|
|
655
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
656
|
+
kind = srcs['kinds'].get(target)
|
|
657
|
+
if kind in ('code comment', 'draw state'):
|
|
658
|
+
return True
|
|
659
|
+
if not Toggles.dangerous_edit_mode:
|
|
660
|
+
return False
|
|
661
|
+
if isinstance(kind, str) and kind.startswith('caller'):
|
|
662
|
+
return not _is_framework_caller(srcs['locations'].get(target))
|
|
663
|
+
return target is not None
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def window_position_movable(draw_state, kwargs):
|
|
667
|
+
if kwargs.get('window_pos') is None:
|
|
668
|
+
return True
|
|
669
|
+
# Live windows nominate their comment even before it has an entry.
|
|
670
|
+
if kwargs.get('preferred_source') == 'code comment':
|
|
671
|
+
return True
|
|
672
|
+
srcs = _sources_for(draw_state)
|
|
673
|
+
return window_source_writable(srcs, _setting_source(srcs, 'window_pos'))
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def default_write_source(attr_name, draw_state, class_to_show=None, srcs=None):
|
|
677
|
+
"""The source name a NEW write of `attr_name` should default to — the
|
|
678
|
+
info tab's picker preselection.
|
|
679
|
+
|
|
680
|
+
1. A source already setting the attr wins (the normal driving pick).
|
|
681
|
+
2. Otherwise, use the source setting view_func when available.
|
|
682
|
+
3. With no view_func source, use the view's OTHER params as a cue: the writable source
|
|
683
|
+
already defining the most of them is where this view is being
|
|
684
|
+
configured, so a new param belongs there too (SourcePriority as the
|
|
685
|
+
tie-breaker). The signature is excluded — it defines EVERY param by
|
|
686
|
+
construction and would always win — and so is the draw_state (it's
|
|
687
|
+
the fallback, not a configuration site).
|
|
688
|
+
4. No cue at all: the highest-priority writable code source, else the
|
|
689
|
+
draw_state."""
|
|
690
|
+
if srcs is None:
|
|
691
|
+
srcs = _sources_for(draw_state, class_to_show)
|
|
692
|
+
writable = set(srcs["writable"])
|
|
693
|
+
|
|
694
|
+
def _eligible(sname):
|
|
695
|
+
kind = srcs["kinds"].get(sname)
|
|
696
|
+
if sname not in writable or kind in ("signature", "draw state"):
|
|
697
|
+
return False
|
|
698
|
+
if isinstance(kind, str) and kind.startswith("caller"):
|
|
699
|
+
# Caller row names are the frame's function name (with an
|
|
700
|
+
# optional " ^N" dedup suffix). A dunder name (__call__ - a
|
|
701
|
+
# wrapper/dispatch protocol) is machinery regardless of where
|
|
702
|
+
# it lives, never a place to stamp a view kwarg.
|
|
703
|
+
fn = sname.split(" ^", 1)[0]
|
|
704
|
+
if fn.startswith("__") and fn.endswith("__"):
|
|
705
|
+
return False
|
|
706
|
+
return not _is_framework_caller(srcs["locations"].get(sname))
|
|
707
|
+
return True
|
|
708
|
+
|
|
709
|
+
target = _setting_source(srcs, attr_name)
|
|
710
|
+
if target is not None and _eligible(target):
|
|
711
|
+
return target
|
|
712
|
+
target = _setting_source(srcs, "view_func")
|
|
713
|
+
if target is not None:
|
|
714
|
+
return target
|
|
715
|
+
others = set(view_param_names(draw_state))
|
|
716
|
+
others.discard(attr_name)
|
|
717
|
+
|
|
718
|
+
candidates = [s for s in srcs["sources"] if _eligible(s)]
|
|
719
|
+
best, best_key = None, None
|
|
720
|
+
for sname in candidates:
|
|
721
|
+
sdict = srcs["sources"][sname]
|
|
722
|
+
try:
|
|
723
|
+
count = sum(1 for k in sdict if k in others)
|
|
724
|
+
except Exception:
|
|
725
|
+
count = 0
|
|
726
|
+
if not count:
|
|
727
|
+
continue
|
|
728
|
+
key = (-count, _source_priority(srcs["kinds"].get(sname)))
|
|
729
|
+
if best_key is None or key < best_key:
|
|
730
|
+
best, best_key = sname, key
|
|
731
|
+
if best is not None:
|
|
732
|
+
return best
|
|
733
|
+
if candidates:
|
|
734
|
+
return min(candidates,
|
|
735
|
+
key=lambda s: _source_priority(srcs["kinds"].get(s)))
|
|
736
|
+
return "draw_state"
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def from_anywhere(attr_name, draw_state, class_to_show=None, default=None):
|
|
740
|
+
"""Read `attr_name` from whichever source is driving it — the read mirror
|
|
741
|
+
of set_anywhere, resolved through the same registry and priority pick.
|
|
742
|
+
`default` when no source sets the attr (the driving pick may be the
|
|
743
|
+
signature FALLBACK, which doesn't set it yet — a + affordance case)."""
|
|
744
|
+
srcs = _sources_for(draw_state, class_to_show)
|
|
745
|
+
target = _driving_source(srcs, attr_name)
|
|
746
|
+
if target is None:
|
|
747
|
+
return default
|
|
748
|
+
return srcs["sources"][target].get(attr_name, default)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
# Parameters the set-anywhere round trip supports. The trip is multi-frame
|
|
752
|
+
# (write → debounced chain_out → save → hotswap → new o_kwargs), so supported
|
|
753
|
+
# params also get the in-flight display cache below; grow this list as params
|
|
754
|
+
# are verified end-to-end. This gates DIRECT set_anywhere usage: both generic
|
|
755
|
+
# accessors - `draw_state.locate_<param>` and the ParamProxy - pass
|
|
756
|
+
# allow_any=True, but the point of an arbitrary-name accessor is that any
|
|
757
|
+
# param on the view is settable.
|
|
758
|
+
SET_ANYWHERE_PARAMS = ("tint", "view_func")
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def anywhere_value(attr_name, draw_state, default=None, live_kwargs=None):
|
|
762
|
+
"""The value a set-anywhere editor should DISPLAY for `attr_name`:
|
|
763
|
+
draw_state._kwargs — the framework-resolved truth — except while a
|
|
764
|
+
set_anywhere round trip is in flight, when it's the pending UI value
|
|
765
|
+
(cached on the draw_state by set_anywhere), so the widget doesn't snap
|
|
766
|
+
back to the stale value for the frames the write→save→hotswap takes.
|
|
767
|
+
|
|
768
|
+
Completion is "the live value MOVED off what it was when the set was
|
|
769
|
+
issued" — never equality with the set value, which round-trips through
|
|
770
|
+
source formatting (float reformat) and might never compare equal. Moving
|
|
771
|
+
to anything (our set landing, or someone else's edit) clears the entry
|
|
772
|
+
and live reads resume."""
|
|
773
|
+
_anywhere_recompile_tick(draw_state)
|
|
774
|
+
live = (draw_state._kwargs if live_kwargs is None else live_kwargs) or {}
|
|
775
|
+
live = live.get(attr_name)
|
|
776
|
+
if attr_name == "view_func":
|
|
777
|
+
live = getattr(draw_state, "_wrapper", None) or draw_state._view_func
|
|
778
|
+
if _unset_value(live):
|
|
779
|
+
live = None # alpha-0 = the codec opt-out; fall through
|
|
780
|
+
if live is None:
|
|
781
|
+
# The style cascade's own last fallback: the draw_state field
|
|
782
|
+
# (ds.tint) - the DRAW_STATE source. Without this, ds-tinted windows
|
|
783
|
+
# read as "no value" here while visibly wearing one.
|
|
784
|
+
live = getattr(draw_state, attr_name, None)
|
|
785
|
+
if live is None:
|
|
786
|
+
live = default
|
|
787
|
+
_anywhere_verify_tick(attr_name, draw_state, live)
|
|
788
|
+
pending = getattr(draw_state, "_sa_pending", None)
|
|
789
|
+
entry = pending.get(attr_name) if pending else None
|
|
790
|
+
if entry is None:
|
|
791
|
+
return _precise_or_live(attr_name, draw_state, live)
|
|
792
|
+
ui_value, live_at_set = entry
|
|
793
|
+
try:
|
|
794
|
+
moved = not (live is live_at_set or bool(live == live_at_set))
|
|
795
|
+
except Exception:
|
|
796
|
+
moved = True
|
|
797
|
+
if moved:
|
|
798
|
+
del pending[attr_name]
|
|
799
|
+
# The trip LANDED (live moved off the at-set baseline). Queue the
|
|
800
|
+
# value cross-check for 2 frames out - comparing any sooner fires
|
|
801
|
+
# on every valid set (the old write = check's failure mode).
|
|
802
|
+
verify = getattr(draw_state, "_sa_verify", None)
|
|
803
|
+
if verify is None:
|
|
804
|
+
verify = {}
|
|
805
|
+
draw_state._sa_verify = verify
|
|
806
|
+
verify[attr_name] = (ui_value, Core.melty.frame_count)
|
|
807
|
+
return _precise_or_live(attr_name, draw_state, live)
|
|
808
|
+
return ui_value
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _anywhere_agrees(a, b):
|
|
812
|
+
"""Tolerant post-trip comparison: a set value round-trips through source
|
|
813
|
+
formatting (floats re-rendered at 2 decimals, a 3-tuple may come back
|
|
814
|
+
4-long), so exact equality would flag working trips. Numeric sequences
|
|
815
|
+
compare elementwise over the common prefix at 0.01; incomparables pass
|
|
816
|
+
(no basis to warn)."""
|
|
817
|
+
if a is b:
|
|
818
|
+
return True
|
|
819
|
+
try:
|
|
820
|
+
if bool(a == b):
|
|
821
|
+
return True
|
|
822
|
+
except Exception:
|
|
823
|
+
return True
|
|
824
|
+
if (isinstance(a, (tuple, list)) and isinstance(b, (tuple, list))
|
|
825
|
+
and a and b):
|
|
826
|
+
try:
|
|
827
|
+
return all(abs(float(x) - float(y)) <= 0.01
|
|
828
|
+
for x, y in zip(a, b))
|
|
829
|
+
except (TypeError, ValueError):
|
|
830
|
+
return True
|
|
831
|
+
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
|
832
|
+
return abs(float(a) - float(b)) <= 0.01
|
|
833
|
+
return True
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _anywhere_verify_tick(attr_name, draw_state, live):
|
|
837
|
+
"""Deferred set_anywhere cross-check: 2 frames after a trip lands,
|
|
838
|
+
compare the live value against what was set. A real divergence here
|
|
839
|
+
means the SourcePriority pick wrote to a source that ISN'T driving this
|
|
840
|
+
view — the one thing the alert exists to catch."""
|
|
841
|
+
verify = getattr(draw_state, "_sa_verify", None)
|
|
842
|
+
entry = verify.get(attr_name) if verify else None
|
|
843
|
+
if entry is None:
|
|
844
|
+
return
|
|
845
|
+
set_value, landed_frame = entry
|
|
846
|
+
if Core.melty.frame_count - landed_frame < 2:
|
|
847
|
+
return
|
|
848
|
+
del verify[attr_name]
|
|
849
|
+
if not _anywhere_agrees(live, set_value):
|
|
850
|
+
from meltygui.core.diagnostics.notifications import notify
|
|
851
|
+
notify(f"set_anywhere: '{attr_name}' settled at {live!r}, not the "
|
|
852
|
+
f"{set_value!r} that was set — SourcePriority may not match "
|
|
853
|
+
f"meltygui's routing for this view", tag="set_anywhere")
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _owning_code_host(cm_state, kind):
|
|
857
|
+
"""(str_host, source_obj) whose buffer a write of this kind lands in — the
|
|
858
|
+
pair the writer-side hotswap drives. None for kinds that apply LIVE with
|
|
859
|
+
no compile (comment splat, instance attr) or aren't wired yet (callers)."""
|
|
860
|
+
if kind in ("decoration", "window decoration", "glfw window decoration") and getattr(cm_state, "decoration_str", None) is not None:
|
|
861
|
+
return cm_state.decoration_str, cm_state.decoration_key
|
|
862
|
+
if kind in ("signature", "decoration", "window decoration", "glfw window decoration"):
|
|
863
|
+
return cm_state.render_func_str, (cm_state.host_key or (None, None))[0]
|
|
864
|
+
if kind in ("class var", "class default", "class decoration"):
|
|
865
|
+
return cm_state.class_str, (cm_state.host_key or (None, None))[1]
|
|
866
|
+
if isinstance(kind, str) and kind.startswith("caller"):
|
|
867
|
+
# One host pair per walked frame (_collect_input_sources), indexed by
|
|
868
|
+
# the kind's depth suffix. The str host's input is the CallSite, which
|
|
869
|
+
# recompile_source hotswaps via _recompile_caller (the ENCLOSING
|
|
870
|
+
# function, with the edited statement spliced-in).
|
|
871
|
+
depth = int(kind.split("+", 1)[1]) if "+" in kind else 0
|
|
872
|
+
hosts = cm_state.call_site_hosts or []
|
|
873
|
+
if depth < len(hosts) and hosts[depth][0] is not None:
|
|
874
|
+
_sh = hosts[depth][0]
|
|
875
|
+
return _sh, getattr(_sh, "input_value", None)
|
|
876
|
+
return None, None
|
|
877
|
+
if kind in ("mode", "mode child kwargs"):
|
|
878
|
+
# "mode child kwargs" rows are registered off the PARENT's mode host,
|
|
879
|
+
# but code_hosts_for caches per-mode reference, so the target's own
|
|
880
|
+
# mode host (same enum class) is the same host pair. A leaf whose
|
|
881
|
+
# current_mode enum differs from the parent's would stamp the wrong
|
|
882
|
+
# host and the recompile wait times out harmlessly.
|
|
883
|
+
return cm_state.mode_str, cm_state.mode_key
|
|
884
|
+
return None, None
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _anywhere_recompile_tick(draw_state):
|
|
888
|
+
"""Writer-side hotswap driver, run every frame the anywhere UI reads a
|
|
889
|
+
value (anywhere_value). set_anywhere stamps what it's waiting for; the
|
|
890
|
+
tick starts the recompile the moment the owning host's buffer moves off
|
|
891
|
+
the pre-write snapshot (the debounced chain_out landing), then keeps the
|
|
892
|
+
runner polled until the hotswap lands. This lives ENTIRELY on the writer
|
|
893
|
+
side — code_file_io has no edit-driven recompile (the cache hosts also
|
|
894
|
+
back visible editor panes, where any `edited` trigger fires per
|
|
895
|
+
keystroke)."""
|
|
896
|
+
pend = getattr(draw_state, "_sa_recompile", None)
|
|
897
|
+
if not pend:
|
|
898
|
+
return
|
|
899
|
+
# Once per frame: anywhere_value calls this per PARAM (the params panel /
|
|
900
|
+
# info tab read every param each refresh), and a second run_recompile
|
|
901
|
+
# of the same name in one frame triggers run_in_background's duplicate-
|
|
902
|
+
# unique detection (sa_recompile<u> : Forcing re-render" spam).
|
|
903
|
+
if pend.get("tick_frame") == Core.melty.frame_count:
|
|
904
|
+
return
|
|
905
|
+
pend["tick_frame"] = Core.melty.frame_count
|
|
906
|
+
from meltygui.code.new_converters import host_code_state
|
|
907
|
+
from meltygui.code.new_converters import run_recompile
|
|
908
|
+
# Keep the owning host alive through the wait - it may be an idle-swept
|
|
909
|
+
# cache host that still needs to load and run its chain.
|
|
910
|
+
pend["host"].notify_on_change(draw_state)
|
|
911
|
+
cs = host_code_state(pend["host"])
|
|
912
|
+
if cs is None or cs.address is None:
|
|
913
|
+
return
|
|
914
|
+
if pend["buf"] is None:
|
|
915
|
+
# Stamped before the host had a code_state: adopt the first buffer we
|
|
916
|
+
# see as the pre-write baseline and wait for it to move.
|
|
917
|
+
pend["buf"] = cs.text_cache
|
|
918
|
+
return
|
|
919
|
+
start = False
|
|
920
|
+
if not pend["started"]:
|
|
921
|
+
if cs.text_cache is pend["buf"]:
|
|
922
|
+
return # chain_out hasn't landed yet
|
|
923
|
+
pend["started"] = True
|
|
924
|
+
pend["start_frame"] = Core.melty.frame_count
|
|
925
|
+
start = True
|
|
926
|
+
run_recompile(pend["source"], cs, draw_state, start=start,
|
|
927
|
+
name=f"sa_recompile{draw_state.unique}")
|
|
928
|
+
landed = (cs._recompiled_on_frame is not None
|
|
929
|
+
and cs._recompiled_on_frame >= pend["start_frame"])
|
|
930
|
+
# Timeout only counts AFTER the compile started - comparing an unstarted
|
|
931
|
+
# pend's start_frame=0 against the session frame count cleared every
|
|
932
|
+
# stamp on its first tick (the runner silently never ran).
|
|
933
|
+
if landed or (pend["started"]
|
|
934
|
+
and Core.melty.frame_count - pend["start_frame"] > 600):
|
|
935
|
+
draw_state._sa_recompile = None
|
|
936
|
+
# Switching draw_any's renderer can retire this DrawState. If it is
|
|
937
|
+
# reused later, its old baseline may still equal the live renderer;
|
|
938
|
+
# don't resurrect a selection whose recompile already completed.
|
|
939
|
+
pending = getattr(draw_state, "_sa_pending", None)
|
|
940
|
+
if pending:
|
|
941
|
+
pending.pop("view_func", None)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def set_anywhere(attr_name, value, draw_state, class_to_show=None, allow_any=False,
|
|
945
|
+
ds_fallback=False, source=None):
|
|
946
|
+
"""Set `attr_name` at whichever input source is actually driving it —
|
|
947
|
+
code, comment, decoration, mode entry — using the same registry the input
|
|
948
|
+
tab edits. The write is a plain item-set on the source's bubbling parse
|
|
949
|
+
dict, so the owning host goes dirty and saves through its normal chain.
|
|
950
|
+
|
|
951
|
+
The DRIVING source is the highest-priority (SourcePriority order) writable
|
|
952
|
+
source that currently sets the attr; when none sets it, the value stamps
|
|
953
|
+
alongside view_func, or into the signature defaults when no source sets
|
|
954
|
+
view_func (the + button's fallback).
|
|
955
|
+
Returns the source name written to, or None when nothing writable exists.
|
|
956
|
+
|
|
957
|
+
Sanity cross-check, not bulletproof: if the driving source's pre-write
|
|
958
|
+
value disagrees with the live draw_state._kwargs value, our hardcoded
|
|
959
|
+
priority table probably mis-ranked this view's sources — notify, don't
|
|
960
|
+
throw, and write anyway (the user asked for the set).
|
|
961
|
+
|
|
962
|
+
`allow_any` skips the SET_ANYWHERE_PARAMS gate — the generic accessors
|
|
963
|
+
pass it (the whole point of `locate_<param>` is that any signature param
|
|
964
|
+
is writable), while direct calls keep the whitelist.
|
|
965
|
+
|
|
966
|
+
`ds_fallback` changes what happens when NO source sets the param: instead
|
|
967
|
+
of stamping the signature default (the + affordance's behavior), the value
|
|
968
|
+
follows the source setting view_func, falling back to draw_state when
|
|
969
|
+
there is none. This also applies to auto-state params driven only by
|
|
970
|
+
default layers; higher-priority sources keep their existing writes.
|
|
971
|
+
|
|
972
|
+
`source` names an EXPLICIT target (a registered source name, or the
|
|
973
|
+
literal "draw_state") — the info tab's per-param picker passes it. It
|
|
974
|
+
overrides preferred_source and the driving pick entirely; the write can
|
|
975
|
+
also CREATE the entry at that source (a new caller kwarg, a new class
|
|
976
|
+
var), which is how the picker's + affordance stamps a param nothing
|
|
977
|
+
sets yet."""
|
|
978
|
+
from meltygui.core.diagnostics.notifications import notify
|
|
979
|
+
if not allow_any and attr_name not in SET_ANYWHERE_PARAMS:
|
|
980
|
+
notify(f"set_anywhere: '{attr_name}' not in SET_ANYWHERE_PARAMS",
|
|
981
|
+
tag="set_anywhere")
|
|
982
|
+
return None
|
|
983
|
+
if attr_name == "view_func":
|
|
984
|
+
from meltygui.core.input.view_selection import resolve_view_func
|
|
985
|
+
try:
|
|
986
|
+
value = resolve_view_func(value)
|
|
987
|
+
except ValueError as error:
|
|
988
|
+
notify(str(error), tag="set_anywhere")
|
|
989
|
+
return None
|
|
990
|
+
# Mid-drag repeat write to an already-deferred attr: skip the registry
|
|
991
|
+
# walk entirely - the drag's FIRST write resolved the target (slow) and
|
|
992
|
+
# parked it; later frames just stamp the parked value. This is what
|
|
993
|
+
# makes drag frames ~ instantaneous.
|
|
994
|
+
_deferred = getattr(draw_state, "_sa_deferred", None)
|
|
995
|
+
if _deferred and attr_name in _deferred and _input_busy():
|
|
996
|
+
_prev = _deferred[attr_name]
|
|
997
|
+
deferred_source = source if source is not None else _prev[2]
|
|
998
|
+
_kind = _prev[3]
|
|
999
|
+
_deferred[attr_name] = (value, class_to_show, deferred_source, _kind)
|
|
1000
|
+
_stamp_pending(attr_name, value, draw_state)
|
|
1001
|
+
live_apply(attr_name, value, draw_state, _kind, class_to_show)
|
|
1002
|
+
_last = getattr(draw_state, "_sa_last_source", None)
|
|
1003
|
+
return _last.get(attr_name) if _last else None
|
|
1004
|
+
srcs = _sources_for(draw_state, class_to_show)
|
|
1005
|
+
sources = srcs["sources"]
|
|
1006
|
+
# A spawning view can NOMINATE where edits land: live view stamps
|
|
1007
|
+
# preferred_source="code comment" on the value windows it spawns, so a
|
|
1008
|
+
# panel/menu edit targets the site's `# [<key>=...]` comment even when the
|
|
1009
|
+
# comment doesn't set the param yet - the write CREATES the entry there
|
|
1010
|
+
# (the registered row is a _LazyOverrideEntry when no comment exists,
|
|
1011
|
+
# and its first write materializes one). Falls through to the normal
|
|
1012
|
+
# pick when no writable source of that kind is registered (tree unparsed).
|
|
1013
|
+
target = None
|
|
1014
|
+
if source is not None:
|
|
1015
|
+
# Explicit target from the picker: honor it or fail loudly - silently
|
|
1016
|
+
# falling back to the automatic pick would write somewhere the user
|
|
1017
|
+
# didn't choose.
|
|
1018
|
+
_writable = set(srcs["writable"])
|
|
1019
|
+
if source in sources and source in _writable:
|
|
1020
|
+
target = source
|
|
1021
|
+
elif source == "draw_state":
|
|
1022
|
+
# The ds row only registers once a whitelisted attr diverged, so
|
|
1023
|
+
# the picker offers the literal name even when unregistered.
|
|
1024
|
+
if attr_name == "view_func":
|
|
1025
|
+
draw_state.auto_params[attr_name] = value
|
|
1026
|
+
draw_state.invalidate_up(max_depth=6)
|
|
1027
|
+
else:
|
|
1028
|
+
setattr(draw_state, attr_name, value)
|
|
1029
|
+
_last_ds = getattr(draw_state, "_sa_last_source", None)
|
|
1030
|
+
if _last_ds is None:
|
|
1031
|
+
_last_ds = {}
|
|
1032
|
+
draw_state._sa_last_source = _last_ds
|
|
1033
|
+
_last_ds[attr_name] = "draw state"
|
|
1034
|
+
return "draw state"
|
|
1035
|
+
else:
|
|
1036
|
+
notify(f"set_anywhere: picked source {source!r} isn't writable "
|
|
1037
|
+
f"for '{attr_name}'", tag="set_anywhere")
|
|
1038
|
+
return None
|
|
1039
|
+
pref = _preferred_source_for(draw_state) if target is None else None
|
|
1040
|
+
if pref is not None:
|
|
1041
|
+
_writable = set(srcs["writable"])
|
|
1042
|
+
target = next((s for s in sources
|
|
1043
|
+
if s in _writable
|
|
1044
|
+
and _pref_matches(pref, srcs["kinds"].get(s))), None)
|
|
1045
|
+
if target is None and ds_fallback:
|
|
1046
|
+
# Preserve sources above auto-state and existing reserved param
|
|
1047
|
+
# sources (such as tint). Otherwise save alongside view_func, with
|
|
1048
|
+
# the old draw_state fallback for calls without a renderer source.
|
|
1049
|
+
_setting = _setting_source(srcs, attr_name)
|
|
1050
|
+
_kind = srcs["kinds"].get(_setting) if _setting is not None else None
|
|
1051
|
+
from meltygui.core.core_render import _draw_state_reserved_names
|
|
1052
|
+
# None (DrawState not constructible yet) makes reserved set unknown;
|
|
1053
|
+
# keep the legacy pick for the call rather than throwing.
|
|
1054
|
+
_mirrored = attr_name not in (_draw_state_reserved_names() or ())
|
|
1055
|
+
if (_setting is None
|
|
1056
|
+
or (_mirrored
|
|
1057
|
+
and _source_priority(_kind)[0] not in _ABOVE_DRAW_STATE)):
|
|
1058
|
+
target = _setting_source(srcs, "view_func")
|
|
1059
|
+
if target is None:
|
|
1060
|
+
if attr_name == "view_func":
|
|
1061
|
+
draw_state.auto_params[attr_name] = value
|
|
1062
|
+
draw_state.invalidate_up(max_depth=6)
|
|
1063
|
+
else:
|
|
1064
|
+
setattr(draw_state, attr_name, value)
|
|
1065
|
+
# Stamp provenance like every other target - without it a ds
|
|
1066
|
+
# write is invisible ("where did my line_height=2 go?"): the
|
|
1067
|
+
# value lives only in auto_params.
|
|
1068
|
+
_last_ds = getattr(draw_state, "_sa_last_source", None)
|
|
1069
|
+
if _last_ds is None:
|
|
1070
|
+
_last_ds = {}
|
|
1071
|
+
draw_state._sa_last_source = _last_ds
|
|
1072
|
+
_last_ds[attr_name] = "draw state"
|
|
1073
|
+
return "draw state"
|
|
1074
|
+
if target is None and _setting_source(srcs, attr_name) is None:
|
|
1075
|
+
target = _setting_source(srcs, "view_func")
|
|
1076
|
+
if target is None:
|
|
1077
|
+
target = _driving_source(srcs, attr_name)
|
|
1078
|
+
if target is None:
|
|
1079
|
+
notify(f"set_anywhere: no writable source for '{attr_name}'",
|
|
1080
|
+
tag="set_anywhere")
|
|
1081
|
+
return None
|
|
1082
|
+
if attr_name == "view_func" and srcs["kinds"].get(target) == "signature" and attr_name not in sources[target]:
|
|
1083
|
+
notify("Choose a caller, defaults, comment, or draw-state source for the renderer", tag="set_anywhere")
|
|
1084
|
+
return None
|
|
1085
|
+
|
|
1086
|
+
if source is None and ds_fallback and attr_name in ('closed', 'window_pos'):
|
|
1087
|
+
# Parameter panels inherit their inspected window's comment for
|
|
1088
|
+
# parameter edits, but their own geometry/visibility belongs to the
|
|
1089
|
+
# panel. Never close or move the ancestor through that inherited row.
|
|
1090
|
+
owner = srcs.get('comment_owners', {}).get(target, draw_state)
|
|
1091
|
+
if owner is not draw_state:
|
|
1092
|
+
setattr(draw_state, attr_name, value)
|
|
1093
|
+
last = getattr(draw_state, '_sa_last_source', None)
|
|
1094
|
+
if last is None:
|
|
1095
|
+
last = {}
|
|
1096
|
+
draw_state._sa_last_source = last
|
|
1097
|
+
last[attr_name] = 'draw state'
|
|
1098
|
+
return 'draw state'
|
|
1099
|
+
if not window_source_writable(srcs, target):
|
|
1100
|
+
return None
|
|
1101
|
+
|
|
1102
|
+
# Last-written source, by attr - lazily maintained (stamped here on every
|
|
1103
|
+
# set, and by the popover's lazy resolve on first open): cheap provenance
|
|
1104
|
+
# for display without a per-frame collection.
|
|
1105
|
+
_last = getattr(draw_state, "_sa_last_source", None)
|
|
1106
|
+
if _last is None:
|
|
1107
|
+
_last = {}
|
|
1108
|
+
draw_state._sa_last_source = _last
|
|
1109
|
+
_last[attr_name] = target
|
|
1110
|
+
|
|
1111
|
+
# SLOW target + gesture in flight (drag or scroll): park the write
|
|
1112
|
+
# instead of running the save/recompile cycle per event (see the
|
|
1113
|
+
# full-speed block above).
|
|
1114
|
+
if _input_busy() and source_is_slow(srcs["kinds"].get(target)):
|
|
1115
|
+
_defer_write(attr_name, value, draw_state, class_to_show, source=source,
|
|
1116
|
+
kind=srcs["kinds"].get(target))
|
|
1117
|
+
return target
|
|
1118
|
+
|
|
1119
|
+
# (No write-time sanity cross-check here: mid-trip the live value
|
|
1120
|
+
# LEGITIMATELY disagrees with the source, so comparing now cries wolf on
|
|
1121
|
+
# every working set. Verification is deferred - see anywhere_value: 2
|
|
1122
|
+
# frames after the round trip lands, live vs what we set.)
|
|
1123
|
+
_t_kind = srcs["kinds"].get(target)
|
|
1124
|
+
write_value = value
|
|
1125
|
+
if source_is_low_precision(_t_kind):
|
|
1126
|
+
write_value = _round_low_precision(value)
|
|
1127
|
+
try:
|
|
1128
|
+
_rounded_away = not bool(write_value == value)
|
|
1129
|
+
except Exception:
|
|
1130
|
+
_rounded_away = True
|
|
1131
|
+
if _rounded_away:
|
|
1132
|
+
_stamp_precise(attr_name, value, draw_state)
|
|
1133
|
+
# A write that only moves digits BELOW the limit is a no-op at the
|
|
1134
|
+
# source: the overlay already serves the precise value, so skip
|
|
1135
|
+
# the save/recompile trip (and clear any parked pending entry -
|
|
1136
|
+
# no trip means the live baseline will never move to clear it).
|
|
1137
|
+
try:
|
|
1138
|
+
if bool(sources[target].get(attr_name) == write_value):
|
|
1139
|
+
_pending = getattr(draw_state, "_sa_pending", None)
|
|
1140
|
+
if _pending:
|
|
1141
|
+
_pending.pop(attr_name, None)
|
|
1142
|
+
return target
|
|
1143
|
+
except Exception:
|
|
1144
|
+
pass
|
|
1145
|
+
if attr_name == "view_func":
|
|
1146
|
+
from meltygui.core.input.view_selection import resolve_view_func
|
|
1147
|
+
from meltygui.core.input.view_selection import view_reference_code
|
|
1148
|
+
value = resolve_view_func(value)
|
|
1149
|
+
if _t_kind == "code comment":
|
|
1150
|
+
write_value = f"RenderFuncs.{value.__name__}" if value is not None else None
|
|
1151
|
+
elif not getattr(sources[target], "direct", False) and source_is_slow(_t_kind):
|
|
1152
|
+
location = srcs["locations"].get(target)
|
|
1153
|
+
write_value = view_reference_code(value, location[0] if location else None)
|
|
1154
|
+
else:
|
|
1155
|
+
write_value = value
|
|
1156
|
+
sources[target][attr_name] = write_value
|
|
1157
|
+
# The in-memory twin of the recompile the write arms below: the window
|
|
1158
|
+
# shows the value now, the hotswap lands the same live on the source's
|
|
1159
|
+
# save (see _LIVE_APPLY).
|
|
1160
|
+
live_apply(attr_name, value, draw_state, _t_kind, class_to_show)
|
|
1161
|
+
# A write to a BELOW-draw_state layer (signature, @defaults, class var,
|
|
1162
|
+
# codec, var) would be otherwise shadowed by a diverged auto_param
|
|
1163
|
+
# riding kwargs. The ds layer is framework session state, not user code -
|
|
1164
|
+
# so the code write CLAIMS the param: drop the stale auto_param and let
|
|
1165
|
+
# the new source value drive (the field case: a stale
|
|
1166
|
+
# auto_params['hide_internal'] kept overriding a fresh signature edit).
|
|
1167
|
+
if _t_kind != "draw state" and _source_priority(_t_kind)[0] not in _ABOVE_DRAW_STATE:
|
|
1168
|
+
_ap = getattr(draw_state, "auto_params", None)
|
|
1169
|
+
if isinstance(_ap, dict):
|
|
1170
|
+
_ap.pop(attr_name, None)
|
|
1171
|
+
# In-flight display cache - see _stamp_pending. The FULL-precision value:
|
|
1172
|
+
# the UI keeps serving it through the trip, then the overlay takes over.
|
|
1173
|
+
_stamp_pending(attr_name, value, draw_state)
|
|
1174
|
+
# Deferred writer-side hotswap: code-backed sources only become LIVE via
|
|
1175
|
+
# recompile, and the source text only exists after the host's debounced
|
|
1176
|
+
# chain_out. Snapshot the current buffer identity; the per-frame tick
|
|
1177
|
+
# (anywhere_value → _anywhere_recompile_tick) starts the recompile when
|
|
1178
|
+
# the buffer moves and polls the runner until the hotswap lands.
|
|
1179
|
+
_arm_recompile(draw_state, sources, target, _t_kind)
|
|
1180
|
+
return target
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def _arm_recompile(draw_state, sources, target, kind):
|
|
1184
|
+
"""Stamp the deferred writer-side hotswap for a code-backed edit at
|
|
1185
|
+
`target` (kind caption `kind`) — shared by set_anywhere and
|
|
1186
|
+
clear_anywhere. The per-frame tick (_anywhere_recompile_tick) starts the
|
|
1187
|
+
recompile when the owning host's buffer moves off the snapshot."""
|
|
1188
|
+
if kind in ("child kwargs", "mode child kwargs", "attr default"):
|
|
1189
|
+
# These rows are parse rows of an ANCESTOR's code (stacked
|
|
1190
|
+
# @defaults) - which ancestor is not guessable from the ds graph (a
|
|
1191
|
+
# Lora item's parent is a plain dict view), but the written row KNOWS
|
|
1192
|
+
# its owner: its bubbling root IS the owning render host, whose
|
|
1193
|
+
# upstream render host's input is the real source object. Stamp the
|
|
1194
|
+
# deferred hotswap against exactly that; without it the trip never
|
|
1195
|
+
# lands and the pending cache shows the un-landed value forever.
|
|
1196
|
+
from meltygui.core.conversion.render_host import RenderHost
|
|
1197
|
+
_row = sources.get(target)
|
|
1198
|
+
_broot = (getattr(_row, "_bubble_root", None)
|
|
1199
|
+
or getattr(getattr(_row, "_dp", None), "_bubble_root", None))
|
|
1200
|
+
if isinstance(_broot, RenderHost):
|
|
1201
|
+
_sh = _broot.input_value if isinstance(_broot.input_value, RenderHost) else None
|
|
1202
|
+
_src_obj = getattr(_sh, "input_value", None) if _sh is not None else None
|
|
1203
|
+
if _sh is not None and _src_obj is not None:
|
|
1204
|
+
from meltygui.code.new_converters import host_code_state
|
|
1205
|
+
_cs = host_code_state(_sh)
|
|
1206
|
+
draw_state._sa_recompile = {
|
|
1207
|
+
"host": _sh, "source": _src_obj, "started": False,
|
|
1208
|
+
"start_frame": 0,
|
|
1209
|
+
"buf": _cs.text_cache if _cs is not None else None}
|
|
1210
|
+
return
|
|
1211
|
+
cm_state = getattr(draw_state, "_sa_cm_state", None)
|
|
1212
|
+
if cm_state is not None:
|
|
1213
|
+
_rc_host, _rc_source = _owning_code_host(cm_state, kind)
|
|
1214
|
+
if _rc_host is not None and _rc_source is not None:
|
|
1215
|
+
from meltygui.code.new_converters import host_code_state
|
|
1216
|
+
_cs = host_code_state(_rc_host)
|
|
1217
|
+
draw_state._sa_recompile = {
|
|
1218
|
+
"host": _rc_host, "source": _rc_source, "started": False,
|
|
1219
|
+
"start_frame": 0,
|
|
1220
|
+
"buf": _cs.text_cache if _cs is not None else None}
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def clear_anywhere(attr_name, draw_state, source, class_to_show=None):
|
|
1224
|
+
"""Delete `attr_name`'s entry AT `source` — the info tab's trash button.
|
|
1225
|
+
A code source loses its parse entry (bubbling __delitem__ dirties the
|
|
1226
|
+
host → normal chain_out/save) and the module hotswaps through the same
|
|
1227
|
+
deferred trip as a set_anywhere write. "draw_state" drops the diverged
|
|
1228
|
+
auto_param (and nulls a whitelisted ds attr), so lower-priority layers
|
|
1229
|
+
resume driving. Returns the source cleared, or None when it held
|
|
1230
|
+
nothing."""
|
|
1231
|
+
from meltygui.core.diagnostics.notifications import notify
|
|
1232
|
+
srcs = _sources_for(draw_state, class_to_show)
|
|
1233
|
+
# In-flight caches for this attr are stale either way a clear goes.
|
|
1234
|
+
for _slot in ("_sa_pending", "_sa_precise", "_sa_deferred"):
|
|
1235
|
+
_d = getattr(draw_state, _slot, None)
|
|
1236
|
+
if isinstance(_d, dict):
|
|
1237
|
+
_d.pop(attr_name, None)
|
|
1238
|
+
kind = srcs["kinds"].get(source)
|
|
1239
|
+
if source == "draw_state" or kind == "draw state":
|
|
1240
|
+
cleared = False
|
|
1241
|
+
_ap = getattr(draw_state, "auto_params", None)
|
|
1242
|
+
if isinstance(_ap, dict) and attr_name in _ap:
|
|
1243
|
+
del _ap[attr_name]
|
|
1244
|
+
cleared = True
|
|
1245
|
+
from meltygui.core.core_render import OBJ_ATTR_PARAMS
|
|
1246
|
+
if (attr_name in OBJ_ATTR_PARAMS
|
|
1247
|
+
and getattr(draw_state, attr_name, None) is not None):
|
|
1248
|
+
setattr(draw_state, attr_name, None)
|
|
1249
|
+
cleared = True
|
|
1250
|
+
return "draw_state" if cleared else None
|
|
1251
|
+
sdict = srcs["sources"].get(source)
|
|
1252
|
+
if not isinstance(sdict, dict) or attr_name not in sdict:
|
|
1253
|
+
notify(f"clear_anywhere: {source!r} doesn't set '{attr_name}'",
|
|
1254
|
+
tag="set_anywhere")
|
|
1255
|
+
return None
|
|
1256
|
+
if kind == "instance attr" and hasattr(sdict, "_obj"):
|
|
1257
|
+
# Snapshot adapter over the live object - clear the OBJECT, not just
|
|
1258
|
+
# the snapshot (a snapshot del would resurrect next collect).
|
|
1259
|
+
try:
|
|
1260
|
+
delattr(sdict._obj, attr_name)
|
|
1261
|
+
except AttributeError:
|
|
1262
|
+
setattr(sdict._obj, attr_name, None)
|
|
1263
|
+
dict.__delitem__(sdict, attr_name)
|
|
1264
|
+
draw_state.invalidate_up(max_depth=6)
|
|
1265
|
+
return source
|
|
1266
|
+
if kind == "codec":
|
|
1267
|
+
# _CodecSource writes fan out to per-file meta / class render_kwargs;
|
|
1268
|
+
# a snapshot del wouldn't reach them. Not wired yet.
|
|
1269
|
+
notify(f"clear_anywhere: clearing at the codec isn't supported yet",
|
|
1270
|
+
tag="set_anywhere")
|
|
1271
|
+
return None
|
|
1272
|
+
if attr_name == "view_func" and getattr(sdict, "direct", False):
|
|
1273
|
+
notify("A direct call needs a render function; choose another renderer", tag="set_anywhere")
|
|
1274
|
+
return None
|
|
1275
|
+
del sdict[attr_name]
|
|
1276
|
+
_arm_recompile(draw_state, srcs["sources"], source, kind)
|
|
1277
|
+
return source
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def _permute_slots(sdict, order):
|
|
1281
|
+
"""Refill the slots of `sdict`'s keys that appear in `order` with those
|
|
1282
|
+
same keys sorted by `order`; every other key (dunder bookkeeping, params
|
|
1283
|
+
the panel doesn't show, comment keys) keeps its slot. Slot permutation,
|
|
1284
|
+
the same rule every CST writer applies on save (_reorder_params /
|
|
1285
|
+
_reorder_call_kwargs / _reorder_class_body / _reformat_override_comment),
|
|
1286
|
+
so the dict's new order is exactly what the source will read. In place
|
|
1287
|
+
via clear/update, which a bubbling parse dict bubbles to its host as a
|
|
1288
|
+
change. False when fewer than two keys are involved or nothing moves."""
|
|
1289
|
+
present = [k for k in dict.keys(sdict) if k in order]
|
|
1290
|
+
if len(present) < 2:
|
|
1291
|
+
return False
|
|
1292
|
+
wanted = sorted(present, key=order.__getitem__)
|
|
1293
|
+
if wanted == present:
|
|
1294
|
+
return False
|
|
1295
|
+
refill = iter(wanted)
|
|
1296
|
+
items = []
|
|
1297
|
+
for k, v in dict.items(sdict):
|
|
1298
|
+
if k in order:
|
|
1299
|
+
nk = next(refill)
|
|
1300
|
+
items.append((nk, dict.__getitem__(sdict, nk)))
|
|
1301
|
+
else:
|
|
1302
|
+
items.append((k, v))
|
|
1303
|
+
sdict.clear()
|
|
1304
|
+
sdict.update(items)
|
|
1305
|
+
return True
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
def reorder_anywhere(keys, draw_state, class_to_show=None):
|
|
1309
|
+
"""Write a new ORDER of the view's params — `keys`, the complete key order
|
|
1310
|
+
the params panel was dragged into — to every code-backed source that
|
|
1311
|
+
stores two or more of them: the signature's parameter list, a caller's
|
|
1312
|
+
or decorator's kwargs, a class body, a mode entry's kwargs, an override
|
|
1313
|
+
comment. Each is an ordered store in its own right, so each one follows
|
|
1314
|
+
the panel's relative order (slot permutation: a source holding a SUBSET
|
|
1315
|
+
of the params neither gains nor loses keys) and they all agree after the
|
|
1316
|
+
drag — the signature among them, which is where the panel's own order
|
|
1317
|
+
derives from. Only parse-node dicts qualify: the adapter rows (instance
|
|
1318
|
+
attr, draw_state, codec) are snapshots over live objects with no
|
|
1319
|
+
persisted order. Returns the source names written, in priority order."""
|
|
1320
|
+
from meltygui.core.conversion.bubbling import _BubblingDictMixin
|
|
1321
|
+
srcs = _sources_for(draw_state, class_to_show)
|
|
1322
|
+
order = {k: i for i, k in enumerate(keys)}
|
|
1323
|
+
writable = set(srcs["writable"])
|
|
1324
|
+
written = []
|
|
1325
|
+
for sname, sdict in srcs["sources"].items():
|
|
1326
|
+
if sname not in writable or not isinstance(sdict, _BubblingDictMixin):
|
|
1327
|
+
continue
|
|
1328
|
+
if _permute_slots(sdict, order):
|
|
1329
|
+
written.append(sname)
|
|
1330
|
+
# Deferred writer-side hotswap, like set_anywhere. One stamp per
|
|
1331
|
+
# draw_state, so arm lowest-priority first and let the signature (the
|
|
1332
|
+
# first registered, the one the panel's order reads back from) win.
|
|
1333
|
+
for sname in reversed(written):
|
|
1334
|
+
_arm_recompile(draw_state, srcs["sources"], sname, srcs["kinds"].get(sname))
|
|
1335
|
+
return written
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def _pending_param_order(draw_state):
|
|
1339
|
+
"""The view function's parameter order as the PENDING source has it: the
|
|
1340
|
+
signature parse held by this view's code host (`_sa_cm_state`, there once
|
|
1341
|
+
anything has collected the view's sources). A reorder rewrites that parse
|
|
1342
|
+
at once while the live function only follows after the save + hotswap
|
|
1343
|
+
trip, so ordering the proxy by it shows the drag's result immediately
|
|
1344
|
+
instead of snapping back for the trip's duration. None when no host is
|
|
1345
|
+
in reach — the live signature order stands."""
|
|
1346
|
+
cm_state = getattr(draw_state, "_sa_cm_state", None)
|
|
1347
|
+
host = getattr(cm_state, "render_func_dict", None)
|
|
1348
|
+
if host is None:
|
|
1349
|
+
return None
|
|
1350
|
+
try:
|
|
1351
|
+
params = host.deep.parameters()
|
|
1352
|
+
except Exception:
|
|
1353
|
+
return None
|
|
1354
|
+
if not isinstance(params, dict):
|
|
1355
|
+
return None
|
|
1356
|
+
return [k for k in dict.keys(params)
|
|
1357
|
+
if isinstance(k, str) and not k.startswith("__")]
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
def view_param_names(draw_state):
|
|
1361
|
+
"""The render view's own input parameters, in signature order — what
|
|
1362
|
+
`locate_params` iterates.
|
|
1363
|
+
|
|
1364
|
+
Signature params minus the ones that aren't inputs at all: the wrapper's
|
|
1365
|
+
injected/plumbing names and event params (core_render's own
|
|
1366
|
+
_AUTO_PARAM_EXCLUDE / _is_event_param_name — the same predicates
|
|
1367
|
+
auto-state uses, so the two lists can't drift), plus injected-state params
|
|
1368
|
+
(`gl_state: GLState = None` — a class annotation with a None default, owned
|
|
1369
|
+
by set_default's misc path). DrawState-reserved names (width, tint, ...)
|
|
1370
|
+
are deliberately KEPT: auto-state skips them because they have legacy
|
|
1371
|
+
manual handling, but they're still real inputs of the view.
|
|
1372
|
+
|
|
1373
|
+
Also deliberately NOT extended with render_func_kwarg_names(): those
|
|
1374
|
+
framework kwargs are shared by every view and would bury its actual
|
|
1375
|
+
params."""
|
|
1376
|
+
return _func_param_names(getattr(draw_state, "_view_func", None))
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def _func_param_names(func):
|
|
1380
|
+
"""Input param names of a render func (unwrapped), with the wrapper's
|
|
1381
|
+
injected/plumbing/event/state params filtered — the engine behind
|
|
1382
|
+
view_param_names and header_param_names."""
|
|
1383
|
+
import inspect
|
|
1384
|
+
if func is None:
|
|
1385
|
+
return []
|
|
1386
|
+
try:
|
|
1387
|
+
params = inspect.signature(inspect.unwrap(func)).parameters
|
|
1388
|
+
except (TypeError, ValueError):
|
|
1389
|
+
return []
|
|
1390
|
+
from meltygui.core.core_render import _AUTO_PARAM_EXCLUDE
|
|
1391
|
+
from meltygui.core.core_render import _is_event_param_name
|
|
1392
|
+
out = []
|
|
1393
|
+
for name, p in params.items():
|
|
1394
|
+
if name in _AUTO_PARAM_EXCLUDE or _is_event_param_name(name):
|
|
1395
|
+
continue
|
|
1396
|
+
if name.startswith("_"):
|
|
1397
|
+
continue # placeholder/private (`_`), not a real input
|
|
1398
|
+
if p.kind in (inspect.Parameter.VAR_POSITIONAL,
|
|
1399
|
+
inspect.Parameter.VAR_KEYWORD):
|
|
1400
|
+
continue
|
|
1401
|
+
ann = p.annotation
|
|
1402
|
+
if (ann is not inspect.Parameter.empty and inspect.isclass(ann)
|
|
1403
|
+
and p.default is None):
|
|
1404
|
+
continue # injected state (GLState / CodeState / ...)
|
|
1405
|
+
out.append(name)
|
|
1406
|
+
return out
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def signature_default_for(attr_name, draw_state):
|
|
1410
|
+
"""The param's DECLARED default: the view function's signature, else the
|
|
1411
|
+
header function's (locate_all_params spans both). None when neither
|
|
1412
|
+
declares one — the + affordance falls back to this when the resolved
|
|
1413
|
+
value is None (a header param nothing sets resolves to None; stamping
|
|
1414
|
+
that None would create an entry that still reads as unset)."""
|
|
1415
|
+
import inspect
|
|
1416
|
+
for fn in (getattr(draw_state, "_view_func", None),
|
|
1417
|
+
(getattr(draw_state, "_kwargs", None) or {}).get("with_header")):
|
|
1418
|
+
if not callable(fn):
|
|
1419
|
+
continue
|
|
1420
|
+
try:
|
|
1421
|
+
p = inspect.signature(inspect.unwrap(fn)).parameters.get(attr_name)
|
|
1422
|
+
except (TypeError, ValueError):
|
|
1423
|
+
continue
|
|
1424
|
+
if p is not None and p.default is not inspect.Parameter.empty:
|
|
1425
|
+
return p.default
|
|
1426
|
+
return None
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
# Header plumbing _AUTO_PARAM_EXCLUDE didn't cover - the header receives
|
|
1430
|
+
# these from the wrapper/parent per call, they're never user inputs.
|
|
1431
|
+
_HEADER_PARAM_EXCLUDE = {"meltygui", "parent_show_add_delete", "name_func"}
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
def header_param_names(draw_state):
|
|
1435
|
+
"""Params of the view's HEADER function — the resolved `with_header`
|
|
1436
|
+
kwarg (a render_func or plain callable). Empty when the view has no
|
|
1437
|
+
header. Same filtering as the view's own params."""
|
|
1438
|
+
hf = (getattr(draw_state, "_kwargs", None) or {}).get("with_header")
|
|
1439
|
+
if not callable(hf):
|
|
1440
|
+
return []
|
|
1441
|
+
return [n for n in _func_param_names(hf) if n not in _HEADER_PARAM_EXCLUDE]
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
# Builtin bases a signature default may SPECIALIZE (TensorDim(int)). The
|
|
1445
|
+
# stored/parsed value round-trips as the plain base (comments literal_eval to
|
|
1446
|
+
# int, auto_params to the base), so the read edge re-wraps it in the
|
|
1447
|
+
# default's type; that type is what routes the value to its custom renderer.
|
|
1448
|
+
_SPECIALIZE_BASES = (int, float, str, tuple)
|
|
1449
|
+
_default_types_cache = {}
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def _signature_default_types(draw_state):
|
|
1453
|
+
"""{param: type} for signature defaults whose type is a strict SUBCLASS
|
|
1454
|
+
of a builtin base — the params whose values should be re-specialized on
|
|
1455
|
+
read. Cached per (wrapper, unwrapped) function identity pair so a hotswap
|
|
1456
|
+
that changes the signature refreshes it."""
|
|
1457
|
+
import inspect
|
|
1458
|
+
func = getattr(draw_state, "_view_func", None)
|
|
1459
|
+
if func is None:
|
|
1460
|
+
return {}
|
|
1461
|
+
try:
|
|
1462
|
+
inner = inspect.unwrap(func)
|
|
1463
|
+
except Exception:
|
|
1464
|
+
return {}
|
|
1465
|
+
key = (id(func), id(inner))
|
|
1466
|
+
cached = _default_types_cache.get(key)
|
|
1467
|
+
if cached is not None:
|
|
1468
|
+
return cached
|
|
1469
|
+
out = {}
|
|
1470
|
+
try:
|
|
1471
|
+
for n, p in inspect.signature(inner).parameters.items():
|
|
1472
|
+
d = p.default
|
|
1473
|
+
if d is inspect.Parameter.empty or d is None or isinstance(d, bool):
|
|
1474
|
+
continue
|
|
1475
|
+
t = type(d)
|
|
1476
|
+
for b in _SPECIALIZE_BASES:
|
|
1477
|
+
if isinstance(d, b) and t is not b:
|
|
1478
|
+
out[n] = t
|
|
1479
|
+
break
|
|
1480
|
+
except (TypeError, ValueError):
|
|
1481
|
+
pass
|
|
1482
|
+
_default_types_cache[key] = out
|
|
1483
|
+
return out
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
class ParamProxy(dict):
|
|
1487
|
+
"""Live dict view over one render view's input parameters:
|
|
1488
|
+
|
|
1489
|
+
ds.locate_params["x_dim"] # resolved value (from _kwargs)
|
|
1490
|
+
ds.locate_params["x_dim"] = 0 # set_anywhere on the driving source
|
|
1491
|
+
for param, value in ds.locate_params: ...
|
|
1492
|
+
draw_collection(ds.locate_params) # renders like any dict
|
|
1493
|
+
|
|
1494
|
+
A REAL dict subclass, so every isinstance(x, dict) path in the framework
|
|
1495
|
+
(draw_collection's key routing, converters, serialization probes, `{**p}`,
|
|
1496
|
+
C-level fast paths that bypass overridden methods entirely) treats it as
|
|
1497
|
+
the dict it looks like. The inherited storage holds a SNAPSHOT of the
|
|
1498
|
+
resolved values, refreshed by `refresh()` whenever the draw_state hands
|
|
1499
|
+
the proxy out; the overridden accessors read live on top of it.
|
|
1500
|
+
|
|
1501
|
+
Reads and writes keep the `locate_<param>` asymmetry: reading is the
|
|
1502
|
+
framework-resolved value, only writing walks the source registry. Writes
|
|
1503
|
+
skip the SET_ANYWHERE_PARAMS whitelist and fall back to the draw_state
|
|
1504
|
+
when no source in code sets the param (allow_any / ds_fallback).
|
|
1505
|
+
|
|
1506
|
+
Iterating yields (param, value) PAIRS — the loop this exists for. That's
|
|
1507
|
+
the one deviation from dict, and it's confined to Python-level `for x in
|
|
1508
|
+
proxy`: `keys()`, `dict(proxy)`, `{**proxy}` and every C-level consumer go
|
|
1509
|
+
through the storage and see plain keys."""
|
|
1510
|
+
|
|
1511
|
+
# No instance __dict__: draw_collection prefers getattr(input_value, key)
|
|
1512
|
+
# over collection[key] for anything that has one, which would hand a param
|
|
1513
|
+
# named like a dict method (`items`, `values`, ...) a bound method instead
|
|
1514
|
+
# of its value. With __slots__ the proxy is storage-only and every read
|
|
1515
|
+
# resolves through __getitem__.
|
|
1516
|
+
__slots__ = ("_ds", "_names_fn")
|
|
1517
|
+
|
|
1518
|
+
def __init__(self, draw_state, names_fn=None):
|
|
1519
|
+
super().__init__()
|
|
1520
|
+
self._ds = draw_state
|
|
1521
|
+
# names_fn: which params this proxy spans - view_param_names by
|
|
1522
|
+
# default; the grouped locate_all_params supplies the header-only
|
|
1523
|
+
# supplier for its 'header' sub-dict. Same read/write semantics
|
|
1524
|
+
# either way (the header draws against the same draw_state/kwargs).
|
|
1525
|
+
self._names_fn = names_fn
|
|
1526
|
+
# Warm the view's code hosts now (one registry walk; the hosts parse
|
|
1527
|
+
# in the background): a reorder only reaches sources that are parsed
|
|
1528
|
+
# when the drop lands, and without this the first drag on a fresh
|
|
1529
|
+
# panel would be the call that first creates the signature host and
|
|
1530
|
+
# the comment would move while the header (the panel's own order)
|
|
1531
|
+
# stayed put. Guarded: a proxy can be minted for a view whose sources
|
|
1532
|
+
# haven't been collected (no view func yet), it's a plain snapshot.
|
|
1533
|
+
try:
|
|
1534
|
+
_sources_for(draw_state)
|
|
1535
|
+
except Exception:
|
|
1536
|
+
pass
|
|
1537
|
+
self.refresh()
|
|
1538
|
+
|
|
1539
|
+
def _names(self):
|
|
1540
|
+
names = (self._names_fn or view_param_names)(self._ds)
|
|
1541
|
+
# Live-signature names go in the PENDING signature's order when a code
|
|
1542
|
+
# host holds one (see _pending_param_order); ones the host doesn't
|
|
1543
|
+
# know (header params) keep their relative order at the end.
|
|
1544
|
+
pending = _pending_param_order(self._ds)
|
|
1545
|
+
if pending:
|
|
1546
|
+
pos = {k: i for i, k in enumerate(pending)}
|
|
1547
|
+
names.sort(key=lambda n: pos.get(n, len(pos)))
|
|
1548
|
+
return names
|
|
1549
|
+
|
|
1550
|
+
def reorder_keys(self, keys):
|
|
1551
|
+
"""drag_drop.Reorder's collection hook: `keys` is this proxy's
|
|
1552
|
+
complete new key order. The order lives in the view's code sources,
|
|
1553
|
+
so it is written there (reorder_anywhere) and the snapshot follows
|
|
1554
|
+
on refresh — the pending signature parse already reads back in the
|
|
1555
|
+
new order. True when any source moved."""
|
|
1556
|
+
written = reorder_anywhere(list(keys), self._ds)
|
|
1557
|
+
if written:
|
|
1558
|
+
self.refresh()
|
|
1559
|
+
return bool(written)
|
|
1560
|
+
|
|
1561
|
+
def _specialize(self, name, value):
|
|
1562
|
+
"""Re-wrap a plain parsed value in the signature default's subtype
|
|
1563
|
+
(int 1 → TensorDim(1)) so type routing picks the custom renderer.
|
|
1564
|
+
Values the subtype can't take (a dim given by NAME) pass through."""
|
|
1565
|
+
dt = _signature_default_types(self._ds).get(name)
|
|
1566
|
+
if dt is None or value is None or type(value) is dt:
|
|
1567
|
+
return value
|
|
1568
|
+
try:
|
|
1569
|
+
return dt(value)
|
|
1570
|
+
except (TypeError, ValueError):
|
|
1571
|
+
return value
|
|
1572
|
+
|
|
1573
|
+
def refresh(self):
|
|
1574
|
+
"""Re-snapshot the inherited storage from the live values. Cheap (a
|
|
1575
|
+
_kwargs read per param) and the reason a handed-out proxy is never
|
|
1576
|
+
stale, including for consumers that read the storage directly."""
|
|
1577
|
+
ds = self._ds
|
|
1578
|
+
live = {k: self._specialize(k, anywhere_value(k, ds))
|
|
1579
|
+
for k in self._names()}
|
|
1580
|
+
dict.clear(self)
|
|
1581
|
+
dict.update(self, live) # bypasses __setitem__; a snapshot, not a set
|
|
1582
|
+
return self
|
|
1583
|
+
|
|
1584
|
+
def __getitem__(self, name):
|
|
1585
|
+
if not dict.__contains__(self, name):
|
|
1586
|
+
raise KeyError(name)
|
|
1587
|
+
return self._specialize(name, anywhere_value(name, self._ds))
|
|
1588
|
+
|
|
1589
|
+
def __setitem__(self, name, value):
|
|
1590
|
+
set_anywhere(name, value, self._ds, allow_any=True, ds_fallback=True)
|
|
1591
|
+
# Take the snapshot in step so in-frame reads (a collection row
|
|
1592
|
+
# re-reading what it just wrote) don't show the pre-write value.
|
|
1593
|
+
dict.__setitem__(self, name, value)
|
|
1594
|
+
|
|
1595
|
+
def get(self, name, default=None):
|
|
1596
|
+
if not dict.__contains__(self, name):
|
|
1597
|
+
return default
|
|
1598
|
+
return self._specialize(name, anywhere_value(name, self._ds,
|
|
1599
|
+
default=default))
|
|
1600
|
+
|
|
1601
|
+
def items(self):
|
|
1602
|
+
return [(k, self._specialize(k, anywhere_value(k, self._ds)))
|
|
1603
|
+
for k in dict.keys(self)]
|
|
1604
|
+
|
|
1605
|
+
def values(self):
|
|
1606
|
+
return [v for _k, v in self.items()]
|
|
1607
|
+
|
|
1608
|
+
def __iter__(self):
|
|
1609
|
+
return iter(self.items())
|
|
1610
|
+
|
|
1611
|
+
def __repr__(self):
|
|
1612
|
+
return f"ParamProxy({dict(self.items())!r})"
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
def _header_only_param_names(draw_state):
|
|
1616
|
+
"""Header params MINUS ones the view itself declares — those belong to
|
|
1617
|
+
the 'params' group (view-first dedup, same rule the flat proxy had)."""
|
|
1618
|
+
own = set(view_param_names(draw_state))
|
|
1619
|
+
return [n for n in header_param_names(draw_state) if n not in own]
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
class GroupedParamProxy(dict):
|
|
1623
|
+
"""locate_all_params' shape: two nested live dicts —
|
|
1624
|
+
|
|
1625
|
+
{'params': <ParamProxy over the view's own params>,
|
|
1626
|
+
'header': <ParamProxy over the header-only params>}
|
|
1627
|
+
|
|
1628
|
+
Each leaf keeps ParamProxy semantics (reads resolve via anywhere_value,
|
|
1629
|
+
item-writes route through set_anywhere); the grouping just separates the
|
|
1630
|
+
two origins so they render/route as distinct nested dicts. refresh()
|
|
1631
|
+
re-snapshots both — the DrawState property calls it per access, same
|
|
1632
|
+
stable-identity rules as locate_params."""
|
|
1633
|
+
__slots__ = ()
|
|
1634
|
+
|
|
1635
|
+
def __init__(self, draw_state):
|
|
1636
|
+
super().__init__()
|
|
1637
|
+
dict.__setitem__(self, "params", ParamProxy(draw_state))
|
|
1638
|
+
dict.__setitem__(self, "header",
|
|
1639
|
+
ParamProxy(draw_state,
|
|
1640
|
+
names_fn=_header_only_param_names))
|
|
1641
|
+
|
|
1642
|
+
def refresh(self):
|
|
1643
|
+
for v in dict.values(self):
|
|
1644
|
+
v.refresh()
|
|
1645
|
+
return self
|
|
1646
|
+
|
|
1647
|
+
def __setitem__(self, key, value):
|
|
1648
|
+
# draw_collection writes the changed CHILD dict into its parent
|
|
1649
|
+
# (`grouped['params'] = edited`) - accepting that would swap the live
|
|
1650
|
+
# sub-proxy for a plain dict snapshot and disconnect routing. The
|
|
1651
|
+
# sub-proxies are canonical and already received the leaf writes in
|
|
1652
|
+
# place, so a group-slot write is a no-op; unknown keys are refused.
|
|
1653
|
+
if key not in self:
|
|
1654
|
+
raise KeyError(f"GroupedParamProxy has fixed groups, not {key!r}")
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
# ── draw_state.locate_* ─────────────────────────────────────────────────────
|
|
1658
|
+
# The accessors themselves live on the DrawState CLASS, in
|
|
1659
|
+
# model/core_model/draw_state.py, so they work on every draw_state whether or
|
|
1660
|
+
# not this module has been imported yet; they call back into anywhere_value /
|
|
1661
|
+
# set_anywhere / ParamProxy here via a lazy descriptor. `locate_<param>` is
|
|
1662
|
+
# generic over the param: reads route through DrawState.__getattr__ (miss-only,
|
|
1663
|
+
# so ordinary reads pay nothing) and writes through DrawState._locate_set,
|
|
1664
|
+
# which @live's existing __setattr__ dispatches on the LOCATE_PARAMS. See the
|
|
1665
|
+
# comment block there for why the two paths are wired differently.
|